feature/ui - zatim ne uplne uhlazena ale celkem pouzitelna appka #1

Merged
luke-20 merged 17 commits from feature/ui into main 2025-09-28 21:05:52 +02:00
2 changed files with 168 additions and 75 deletions
Showing only changes of commit 9ff71823a9 - Show all commits

210
ui.go
View File

@ -6,6 +6,7 @@ import (
"image/color" "image/color"
"io" "io"
"os" "os"
"strings"
"time" "time"
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
@ -118,9 +119,6 @@ func buildIdentityTab(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.
identityToggle := widget.NewToolbarAction(theme.VisibilityOffIcon(), nil) identityToggle := widget.NewToolbarAction(theme.VisibilityOffIcon(), nil)
identityToolbar := widget.NewToolbar( identityToolbar := widget.NewToolbar(
widget.NewToolbarAction(theme.ContentCopyIcon(), func() { copyClip(svc.PublicPEM()+"\n"+svc.PublicCert(), parts) }), widget.NewToolbarAction(theme.ContentCopyIcon(), func() { copyClip(svc.PublicPEM()+"\n"+svc.PublicCert(), parts) }),
widget.NewToolbarAction(theme.ContentPasteIcon(), func() { parts.outKey.SetText(fyne.CurrentApp().Clipboard().Content()) }),
widget.NewToolbarAction(theme.ContentClearIcon(), func() { parts.outKey.SetText("") }),
widget.NewToolbarSeparator(),
identityToggle, identityToggle,
) )
@ -184,13 +182,25 @@ func buildIdentityTab(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.
if parts.showQR { if parts.showQR {
updateQRImages() updateQRImages()
// Wrap each QR with its copy button // Wrap each QR with its copy button
pubBox := container.NewVBox(parts.pubQR, widget.NewButton("Copy", func() { copyClip(svc.PublicPEM(), parts) })) pubBox := container.NewVBox(
crtBox := container.NewVBox(parts.crtQR, widget.NewButton("Copy", func() { copyClip(svc.PublicCert(), parts) })) widget.NewLabel("Veřejný klíč (PEM)"),
parts.pubQR,
widget.NewButton("Copy", func() { copyClip(svc.PublicPEM(), parts) }),
)
crtBox := container.NewVBox(
widget.NewLabel("Certifikát (X.509)"),
parts.crtQR,
widget.NewButton("Copy", func() { copyClip(svc.PublicCert(), parts) }),
)
identityContainer.Add(container.NewGridWithColumns(2, pubBox, crtBox)) identityContainer.Add(container.NewGridWithColumns(2, pubBox, crtBox))
} else { } else {
// show combined text for convenience // show combined text for convenience
parts.outKey.SetText(svc.PublicPEM() + "\n" + svc.PublicCert()) parts.outKey.SetText(svc.PublicPEM() + "\n" + svc.PublicCert())
identityContainer.Add(parts.outKey) parts.outKey.Disable()
identityContainer.Add(container.NewVBox(
widget.NewLabel("Veřejný klíč + Certifikát (plaintext)"),
parts.outKey,
))
} }
identityContainer.Refresh() identityContainer.Refresh()
if parts.showQR { if parts.showQR {
@ -579,64 +589,122 @@ func buttonTile(btns ...fyne.CanvasObject) fyne.CanvasObject {
// --- Contacts UI --- // --- Contacts UI ---
func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject { func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
// Data + filtering
var all []Contact
var filtered []Contact
selected := -1
load := func() {
items, _ := svc.ListContacts()
all = items
filtered = items
}
applyFilter := func(q string) {
if q == "" {
filtered = all
return
}
lower := strings.ToLower(q)
tmp := make([]Contact, 0, len(all))
for _, c := range all {
if strings.Contains(strings.ToLower(c.Name), lower) || strings.Contains(strings.ToLower(c.Cert), lower) {
tmp = append(tmp, c)
}
}
filtered = tmp
}
// Left list
list := widget.NewList( list := widget.NewList(
func() int { items, _ := svc.ListContacts(); return len(items) }, func() int { return len(filtered) },
func() fyne.CanvasObject { return widget.NewLabel("") }, func() fyne.CanvasObject { return widget.NewLabel("") },
func(i widget.ListItemID, o fyne.CanvasObject) { func(i widget.ListItemID, o fyne.CanvasObject) {
items, _ := svc.ListContacts() if i >= 0 && int(i) < len(filtered) {
if i >= 0 && i < len(items) { o.(*widget.Label).SetText(filtered[i].Name)
o.(*widget.Label).SetText(items[i].Name)
} }
}, },
) )
var itemsCache []Contact
selected := -1 // Right detail form
refresh := func() {
items, _ := svc.ListContacts()
itemsCache = items
list.Refresh()
}
list.OnSelected = func(id widget.ListItemID) {
selected = int(id)
if id < 0 || id >= len(itemsCache) {
return
}
c := itemsCache[id]
nameEntry := widget.NewEntry() nameEntry := widget.NewEntry()
nameEntry.SetText(c.Name)
certEntry := widget.NewMultiLineEntry() certEntry := widget.NewMultiLineEntry()
certEntry.SetMinRowsVisible(6) certEntry.SetMinRowsVisible(8)
certEntry.SetText(c.Cert) certEntry.Wrapping = fyne.TextWrapWord
form := widget.NewForm( // Detail toolbar
widget.NewFormItem("Název", nameEntry), certToolbar := widget.NewToolbar(
widget.NewFormItem("Certifikát / Public key", certEntry), widget.NewToolbarAction(theme.ContentCopyIcon(), func() { copyClip(certEntry.Text, parts) }),
widget.NewToolbarAction(theme.ContentPasteIcon(), func() {
certEntry.SetText(fyne.CurrentApp().Clipboard().Content())
}),
widget.NewToolbarAction(theme.ContentClearIcon(), func() { certEntry.SetText("") }),
) )
dialog.ShowCustomConfirm("Kontakt", "Uložit", "Zrušit", form, func(ok bool) { saveBtn := widget.NewButtonWithIcon("Uložit", theme.ConfirmIcon(), func() {
if !ok { if selected < 0 || selected >= len(filtered) {
return // new or none selected -> create new
} _ = svc.SaveContact(Contact{Name: nameEntry.Text, Cert: certEntry.Text})
} else {
c := filtered[selected]
c.Name = nameEntry.Text c.Name = nameEntry.Text
c.Cert = certEntry.Text c.Cert = certEntry.Text
_ = svc.SaveContact(c) _ = svc.SaveContact(c)
refresh()
}, fyne.CurrentApp().Driver().AllWindows()[0])
} }
list.OnUnselected = func(id widget.ListItemID) { selected = -1 } load()
list.Refresh()
parts.showToast("Uloženo")
})
useBtn := widget.NewButtonWithIcon("Použít ve Šifrování", theme.ConfirmIcon(), func() {
parts.peer.SetText(certEntry.Text)
parts.showToast("Kontakt vložen do Šifrování")
})
addFromClipboard := widget.NewButton("Přidat z clipboardu", func() { detail := container.NewBorder(
container.NewVBox(
widget.NewLabel("Název"),
nameEntry,
widget.NewLabel("Certifikát / Public key"),
certToolbar,
),
container.NewHBox(layout.NewSpacer(), saveBtn, useBtn),
nil, nil,
certEntry,
)
// List selection behavior
list.OnSelected = func(id widget.ListItemID) {
idx := int(id)
if idx < 0 || idx >= len(filtered) {
return
}
selected = idx
c := filtered[idx]
nameEntry.SetText(c.Name)
certEntry.SetText(c.Cert)
}
list.OnUnselected = func(widget.ListItemID) { selected = -1 }
// Header: title, search, toolbar
search := widget.NewEntry()
search.SetPlaceHolder("Hledat…")
search.OnChanged = func(s string) { applyFilter(s); list.Refresh() }
addNew := widget.NewToolbarAction(theme.ContentAddIcon(), func() {
nameEntry.SetText("Kontakt")
certEntry.SetText("")
selected = -1
})
addFromClipboard := widget.NewToolbarAction(theme.ContentPasteIcon(), func() {
txt := fyne.CurrentApp().Clipboard().Content() txt := fyne.CurrentApp().Clipboard().Content()
if txt == "" { if txt == "" {
parts.showToast("Schází data v clipboardu") parts.showToast("Schází data v clipboardu")
return return
} }
name := extractCN(txt) nm := extractCN(txt)
if name == "" { if nm == "" {
name = "Kontakt" nm = "Kontakt"
} }
_ = svc.SaveContact(Contact{Name: name, Cert: txt}) nameEntry.SetText(nm)
refresh() certEntry.SetText(txt)
selected = -1
}) })
addFromFile := widget.NewButton("Přidat ze souboru", func() { addFromFile := widget.NewToolbarAction(theme.FolderOpenIcon(), func() {
fd := dialog.NewFileOpen(func(r fyne.URIReadCloser, err error) { fd := dialog.NewFileOpen(func(r fyne.URIReadCloser, err error) {
if err != nil || r == nil { if err != nil || r == nil {
return return
@ -644,41 +712,45 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
defer r.Close() defer r.Close()
b, _ := io.ReadAll(r) b, _ := io.ReadAll(r)
txt := string(b) txt := string(b)
name := extractCN(txt) nm := extractCN(txt)
if name == "" { if nm == "" {
name = "Kontakt" nm = "Kontakt"
} }
_ = svc.SaveContact(Contact{Name: name, Cert: txt}) nameEntry.SetText(nm)
refresh() certEntry.SetText(txt)
selected = -1
}, fyne.CurrentApp().Driver().AllWindows()[0]) }, fyne.CurrentApp().Driver().AllWindows()[0])
fd.Show() fd.Show()
}) })
deleteBtn := widget.NewButton("Smazat", func() { deleteAction := widget.NewToolbarAction(theme.DeleteIcon(), func() {
sel := selected if selected < 0 || selected >= len(filtered) {
if sel < 0 || sel >= len(itemsCache) {
return return
} }
_ = svc.DeleteContact(itemsCache[sel].ID) _ = svc.DeleteContact(filtered[selected].ID)
refresh() load()
applyFilter(search.Text)
list.Refresh()
nameEntry.SetText("")
certEntry.SetText("")
selected = -1
}) })
copyBtn := widget.NewButton("Zkopírovat cert", func() { copyAction := widget.NewToolbarAction(theme.ContentCopyIcon(), func() {
sel := selected if selected < 0 || selected >= len(filtered) {
if sel < 0 || sel >= len(itemsCache) {
return return
} }
copyClip(itemsCache[sel].Cert, parts) copyClip(filtered[selected].Cert, parts)
})
useBtn := widget.NewButton("Použít ve Šifrování", func() {
sel := selected
if sel < 0 || sel >= len(itemsCache) {
return
}
parts.peer.SetText(itemsCache[sel].Cert)
parts.showToast("Kontakt vložen do Šifrování")
}) })
topToolbar := widget.NewToolbar(addNew, addFromClipboard, addFromFile, widget.NewToolbarSeparator(), copyAction, deleteAction)
header := container.NewHBox(widget.NewLabelWithStyle("Kontakty", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), layout.NewSpacer(), search, topToolbar)
header := container.NewHBox(addFromClipboard, addFromFile, deleteBtn, copyBtn, useBtn) // Split view
content := container.NewBorder(header, nil, nil, nil, list) left := container.NewBorder(nil, nil, nil, nil, list)
refresh() right := container.NewStack(detail)
return content split := container.NewHSplit(left, right)
split.SetOffset(0.35)
// Initialize
load()
list.Refresh()
return container.NewBorder(header, nil, nil, nil, split)
} }

View File

@ -13,6 +13,7 @@ import (
"errors" "errors"
encrypt "fckeuspy-go/lib" encrypt "fckeuspy-go/lib"
"fmt" "fmt"
mrand "math/rand"
) )
// VaultService implementuje ServiceFacade nad SecureJSONStore. // VaultService implementuje ServiceFacade nad SecureJSONStore.
@ -100,9 +101,9 @@ func (v *VaultService) ListContacts() ([]Contact, error) {
func (v *VaultService) SaveContact(c Contact) error { func (v *VaultService) SaveContact(c Contact) error {
list, _ := v.ListContacts() list, _ := v.ListContacts()
// upsert by ID, if empty ID derive from CN or hash // upsert by ID; if empty ID, assign a new unique random ID to avoid overwriting
if c.ID == "" { if c.ID == "" {
c.ID = deriveContactID(c) c.ID = generateUniqueContactID(list)
} }
replaced := false replaced := false
for i := range list { for i := range list {
@ -144,6 +145,26 @@ func deriveContactID(c Contact) string {
return fmt.Sprintf("%x", sum[:8]) return fmt.Sprintf("%x", sum[:8])
} }
// generateUniqueContactID creates a short random hex ID that does not collide with existing list.
func generateUniqueContactID(existing []Contact) string {
exists := make(map[string]struct{}, len(existing))
for _, c := range existing {
exists[c.ID] = struct{}{}
}
for {
b := make([]byte, 6)
if _, err := rand.Read(b); err != nil {
// fallback to time-based seed rand
n := mrand.Int63()
return fmt.Sprintf("%x", n)
}
id := fmt.Sprintf("%x", b)
if _, ok := exists[id]; !ok {
return id
}
}
}
func extractCN(pemText string) string { func extractCN(pemText string) string {
block, _ := pem.Decode([]byte(pemText)) block, _ := pem.Decode([]byte(pemText))
if block == nil { if block == nil {