package main import ( "errors" encrypt "fckeuspy-go/lib" "fmt" "image/color" "io" "os" "strings" "time" "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/storage" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" ) type uiParts struct { outKey *widget.Entry msg *widget.Entry peer *widget.Entry cipherOut *widget.Entry payload *widget.Entry plainOut *widget.Entry toastLabel *widget.Label cipherQR *canvas.Image pubQR *canvas.Image crtQR *canvas.Image showQR bool peerQR *canvas.Image showPeerQR bool payloadQR *canvas.Image showPayloadQR bool } func buildEntries() *uiParts { p := &uiParts{ outKey: widget.NewMultiLineEntry(), msg: widget.NewMultiLineEntry(), peer: widget.NewMultiLineEntry(), cipherOut: widget.NewMultiLineEntry(), payload: widget.NewMultiLineEntry(), plainOut: widget.NewMultiLineEntry(), toastLabel: widget.NewLabel(""), cipherQR: canvas.NewImageFromImage(nil), pubQR: canvas.NewImageFromImage(nil), crtQR: canvas.NewImageFromImage(nil), showQR: true, peerQR: canvas.NewImageFromImage(nil), showPeerQR: true, payloadQR: canvas.NewImageFromImage(nil), showPayloadQR: true, } p.cipherQR.FillMode = canvas.ImageFillContain p.cipherQR.SetMinSize(fyne.NewSize(220, 220)) p.pubQR.FillMode = canvas.ImageFillContain p.pubQR.SetMinSize(fyne.NewSize(200, 200)) p.crtQR.FillMode = canvas.ImageFillContain p.crtQR.SetMinSize(fyne.NewSize(200, 200)) p.peerQR.FillMode = canvas.ImageFillContain p.peerQR.SetMinSize(fyne.NewSize(200, 200)) p.payloadQR.FillMode = canvas.ImageFillContain p.payloadQR.SetMinSize(fyne.NewSize(220, 220)) p.outKey.SetPlaceHolder("Veřejný klíč / certifikát…") p.msg.SetPlaceHolder("Sem napiš zprávu…") p.peer.SetPlaceHolder("-----BEGIN PUBLIC KEY----- … nebo CERTIFICATE …") p.cipherOut.SetPlaceHolder(`{"ek":"…","n":"…","ct":"…"}`) p.payload.SetPlaceHolder(`{"ek":"…","n":"…","ct":"…"}`) p.plainOut.SetPlaceHolder("Dešifrovaná zpráva…") // Zvýšení výšky (více řádků viditelně) p.outKey.SetMinRowsVisible(15) p.peer.SetMinRowsVisible(15) p.msg.SetMinRowsVisible(6) p.cipherOut.SetMinRowsVisible(15) p.payload.SetMinRowsVisible(15) p.plainOut.SetMinRowsVisible(15) p.toastLabel.Hide() return p } func (p *uiParts) showToast(msg string) { fyne.Do(func() { p.toastLabel.SetText(msg) p.toastLabel.Show() }) time.AfterFunc(1500*time.Millisecond, func() { fyne.Do(func() { if p.toastLabel.Text == msg { // avoid race if overwritten p.toastLabel.SetText("") p.toastLabel.Hide() } }) }) } // custom fixed dark theme type simpleTheme struct{} func (simpleTheme) Color(n fyne.ThemeColorName, v fyne.ThemeVariant) color.Color { base := theme.DefaultTheme().Color(n, v) switch n { case theme.ColorNameBackground: return color.NRGBA{R: 24, G: 27, B: 31, A: 255} case theme.ColorNameButton: return color.NRGBA{R: 42, G: 46, B: 52, A: 255} case theme.ColorNameInputBackground: return color.NRGBA{R: 35, G: 39, B: 45, A: 255} case theme.ColorNameDisabled: // Zesvětlený disabled text pro lepší čitelnost read-only polí return color.NRGBA{R: 230, G: 233, B: 238, A: 255} } return base } func (simpleTheme) Font(st fyne.TextStyle) fyne.Resource { return theme.DefaultTheme().Font(st) } func (simpleTheme) Icon(n fyne.ThemeIconName) fyne.Resource { return theme.DefaultTheme().Icon(n) } func (simpleTheme) Size(n fyne.ThemeSizeName) float32 { return theme.DefaultTheme().Size(n) } var forceDark = true // can be toggled later // Build key section func buildIdentityTab(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.CanvasObject { // Toolbar actions identityToggle := widget.NewToolbarAction(theme.VisibilityOffIcon(), nil) identityToolbar := widget.NewToolbar( widget.NewToolbarAction(theme.ContentCopyIcon(), func() { copyClip(svc.PublicPEM()+"\n"+svc.PublicCert(), parts) }), identityToggle, ) deleteBtn := widget.NewButton("Smazat identitu", func() { pwEntry := widget.NewPasswordEntry() pwEntry.SetPlaceHolder("Heslo pro potvrzení…") warn := widget.NewRichTextFromMarkdown("**⚠ Nevratná akce**\n\nTato operace trvale smaže identitu, kontakty i uložené klíče. Pokračujte pouze pokud máte zálohu nebo opravdu chcete vše odstranit.\n") warn.Wrapping = fyne.TextWrapWord form := widget.NewForm(widget.NewFormItem("Heslo", pwEntry)) content := container.NewVBox( warn, widget.NewSeparator(), form, ) d := dialog.NewCustomConfirm("Potvrdit smazání", "Smazat", "Zrušit", content, func(ok bool) { if !ok { return } pw := pwEntry.Text if pw == "" { dialog.NewError(errors.New("heslo je prázdné"), fyne.CurrentApp().Driver().AllWindows()[0]).Show() return } if _, err := encrypt.OpenEncryptedStore(vaultPath, pw); err != nil { dialog.NewError(errors.New("neplatné heslo"), fyne.CurrentApp().Driver().AllWindows()[0]).Show() return } if err := os.Remove(vaultPath); err != nil { dialog.NewError(err, fyne.CurrentApp().Driver().AllWindows()[0]).Show() return } fyne.CurrentApp().Quit() }, fyne.CurrentApp().Driver().AllWindows()[0]) d.Resize(fyne.NewSize(600, 250)) d.Show() }) makeQR := func(data string, target *canvas.Image) { if data == "" { target.Image = nil target.Refresh() return } pngBytes, err := GenerateQRPNG(data, 512) if err != nil { return } img, err := LoadPNG(pngBytes) if err != nil { return } target.Image = img target.Refresh() } updateQRImages := func() { if parts.showQR { makeQR(svc.PublicPEM(), parts.pubQR) makeQR(svc.PublicCert(), parts.crtQR) } else { parts.pubQR.Image = nil parts.pubQR.Refresh() parts.crtQR.Image = nil parts.crtQR.Refresh() } } identityContainer := container.NewVBox() rebuild := func() { identityContainer.Objects = nil if parts.showQR { updateQRImages() // Wrap each QR with a small icon copy button pubBox := container.NewVBox( widget.NewLabel("Veřejný klíč (PEM)"), parts.pubQR, widget.NewButtonWithIcon("", theme.ContentCopyIcon(), func() { copyClip(svc.PublicPEM(), parts) }), ) crtBox := container.NewVBox( widget.NewLabel("Certifikát (X.509)"), parts.crtQR, widget.NewButtonWithIcon("", theme.ContentCopyIcon(), func() { copyClip(svc.PublicCert(), parts) }), ) identityContainer.Add(container.NewGridWithColumns(2, pubBox, crtBox)) } else { // show combined text for convenience parts.outKey.SetText(svc.PublicPEM() + "\n" + svc.PublicCert()) parts.outKey.Disable() identityContainer.Add(container.NewVBox( widget.NewLabel("Veřejný klíč + Certifikát (plaintext)"), parts.outKey, )) } identityContainer.Refresh() if parts.showQR { identityToggle.SetIcon(theme.VisibilityOffIcon()) } else { identityToggle.SetIcon(theme.VisibilityIcon()) } } identityToggle.OnActivated = func() { parts.showQR = !parts.showQR; rebuild() } rebuild() // Header with toolbar and content + destructive action header := container.NewHBox(widget.NewLabelWithStyle("Moje identita", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), layout.NewSpacer(), identityToolbar) content := container.NewVBox(header, identityContainer, buttonTile(deleteBtn)) return container.NewVScroll(content) } // Helper functions separated to avoid circular dependency with encrypt.Service // We'll adapt by passing a small facade interface if needed. type ServiceFacade interface { Encrypt(msg, peer string) (string, error) Decrypt(json string) (string, error) PublicPEM() string PublicCert() string // Contacts API (only implemented by VaultService) ListContacts() ([]Contact, error) SaveContact(c Contact) error DeleteContact(id string) error } func copyClip(s string, parts *uiParts) { fyne.CurrentApp().Clipboard().SetContent(s) parts.showToast("Zkopírováno") } // (Removed legacy buildEncryptSection and buildDecryptSection) // assembleResponsive builds split view that collapses for narrow widths // Tab: Encrypt func buildEncryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject { parts.cipherOut.Disable() // Peer section with QR/Text toggle peerContainer := container.NewVBox() peerToggleAction := widget.NewToolbarAction(theme.VisibilityOffIcon(), nil) var updatePeer func() updatePeerQR := func(text string) { if text == "" { parts.peerQR.Image = nil parts.peerQR.Refresh() return } pngBytes, err := GenerateQRPNG(text, 512) if err != nil { return } img, err := LoadPNG(pngBytes) if err != nil { return } parts.peerQR.Image = img parts.peerQR.Refresh() } updatePeer = func() { peerContainer.Objects = nil if parts.showPeerQR { updatePeerQR(parts.peer.Text) peerContainer.Add(parts.peerQR) // copy tlačítko jen v toolbaru peerToggleAction.SetIcon(theme.VisibilityOffIcon()) } else { peerContainer.Add(parts.peer) peerToggleAction.SetIcon(theme.VisibilityIcon()) } peerContainer.Refresh() } peerToggleAction.OnActivated = func() { parts.showPeerQR = !parts.showPeerQR; updatePeer() } parts.peer.OnChanged = func(string) { if parts.showPeerQR { updatePeerQR(parts.peer.Text) } } updatePeer() // Output section toggle (QR vs text) outputContainer := container.NewVBox() outputToggleAction := widget.NewToolbarAction(theme.VisibilityOffIcon(), nil) updateQR := func(text string) { if text == "" { parts.cipherQR.Image = nil parts.cipherQR.Refresh() return } pngBytes, err := GenerateQRPNG(text, 512) if err != nil { parts.showToast("QR error") return } img, err := LoadPNG(pngBytes) if err != nil { parts.showToast("PNG err") return } parts.cipherQR.Image = img parts.cipherQR.Refresh() } updateOutput := func() { outputContainer.Objects = nil if parts.showQR { // show QR mode updateQR(parts.cipherOut.Text) outputContainer.Add(parts.cipherQR) outputToggleAction.SetIcon(theme.VisibilityOffIcon()) } else { outputContainer.Add(parts.cipherOut) outputToggleAction.SetIcon(theme.VisibilityIcon()) } outputContainer.Refresh() } outputToggleAction.OnActivated = func() { parts.showQR = !parts.showQR; updateOutput() } updateOutput() encAction := func() { m := parts.msg.Text p := parts.peer.Text if m == "" || p == "" { parts.showToast("Chybí data") return } go func(msg, peer string) { res, err := svc.Encrypt(msg, peer) if err != nil { fyne.Do(func() { parts.cipherOut.SetText(""); updateQR(""); parts.showToast("Chyba") }) return } fyne.Do(func() { parts.cipherOut.SetText(res) // refresh whichever mode is active updateOutput() parts.showToast("OK") }) }(m, p) } importPeerQR := func() { fd := dialog.NewFileOpen(func(r fyne.URIReadCloser, err error) { if err != nil || r == nil { return } defer r.Close() data, _ := io.ReadAll(r) img, err := LoadPNG(data) if err != nil { dialog.NewError(err, fyne.CurrentApp().Driver().AllWindows()[0]).Show() return } text, err := DecodeQR(img) if err != nil { dialog.NewError(err, fyne.CurrentApp().Driver().AllWindows()[0]).Show() return } parts.peer.SetText(text) }, fyne.CurrentApp().Driver().AllWindows()[0]) fd.SetFilter(storage.NewExtensionFileFilter([]string{".png"})) fd.Show() } // Toolbars peerToolbar := widget.NewToolbar( widget.NewToolbarAction(theme.ContentPasteIcon(), func() { parts.peer.SetText(fyne.CurrentApp().Clipboard().Content()) }), widget.NewToolbarAction(theme.ContentCopyIcon(), func() { copyClip(parts.peer.Text, parts) }), widget.NewToolbarAction(theme.FolderOpenIcon(), importPeerQR), widget.NewToolbarAction(theme.ContentClearIcon(), func() { parts.peer.SetText("") }), widget.NewToolbarSeparator(), peerToggleAction, ) outputToolbar := widget.NewToolbar( widget.NewToolbarAction(theme.ContentCopyIcon(), func() { copyClip(parts.cipherOut.Text, parts) }), widget.NewToolbarAction(theme.ContentClearIcon(), func() { parts.cipherOut.SetText("") parts.msg.SetText("") if parts.showQR { updateQR("") } }), widget.NewToolbarSeparator(), outputToggleAction, ) // Šifrování automaticky při změně zprávy nebo peer parts.msg.OnChanged = func(string) { encAction() } parts.peer.OnChanged = func(string) { if parts.showPeerQR { updatePeerQR(parts.peer.Text) } encAction() } // Sections peerSection := container.NewVBox( container.NewHBox(widget.NewLabel("Veřejný klíč příjemce"), layout.NewSpacer(), peerToolbar), peerContainer, ) msgSection := container.NewVBox( widget.NewLabel("Zpráva"), parts.msg, ) outputSection := container.NewVBox( container.NewHBox(widget.NewLabel("Výstup"), layout.NewSpacer(), outputToolbar), outputContainer, ) // Split: message (top) vs output (bottom) for better prostor "do spodu" split := container.NewVSplit(msgSection, outputSection) split.SetOffset(0.40) group := container.NewVBox( widget.NewLabelWithStyle("Šifrování", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), peerSection, split, ) return group } // Tab: Decrypt func buildDecryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject { parts.plainOut.Disable() decryptAction := func() { pl := parts.payload.Text if pl == "" { parts.plainOut.SetText("") return } go func(js string) { res, err := svc.Decrypt(js) if err != nil { fyne.Do(func() { parts.plainOut.SetText("") }) return } fyne.Do(func() { parts.plainOut.SetText(res) }) }(pl) } updatePayloadQR := func(text string) { if text == "" { parts.payloadQR.Image = nil parts.payloadQR.Refresh() return } pngBytes, err := GenerateQRPNG(text, 512) if err != nil { return } img, err := LoadPNG(pngBytes) if err != nil { return } parts.payloadQR.Image = img parts.payloadQR.Refresh() } importPayloadQR := func() { fd := dialog.NewFileOpen(func(r fyne.URIReadCloser, err error) { if err != nil || r == nil { return } defer r.Close() data, _ := io.ReadAll(r) img, err := LoadPNG(data) if err != nil { dialog.NewError(err, fyne.CurrentApp().Driver().AllWindows()[0]).Show() return } text, err := DecodeQR(img) if err != nil { dialog.NewError(err, fyne.CurrentApp().Driver().AllWindows()[0]).Show() return } parts.payload.SetText(text) if parts.showPayloadQR { updatePayloadQR(text) } decryptAction() }, fyne.CurrentApp().Driver().AllWindows()[0]) fd.SetFilter(storage.NewExtensionFileFilter([]string{".png"})) fd.Show() } // Toolbar toggle action for payload section payloadToggleAction := widget.NewToolbarAction(theme.VisibilityOffIcon(), nil) payloadContainer := container.NewVBox() updateMode := func() { payloadContainer.Objects = nil if parts.showPayloadQR { updatePayloadQR(parts.payload.Text) // Jen samotný QR bez spodního copy tlačítka payloadContainer.Add(parts.payloadQR) payloadToggleAction.SetIcon(theme.VisibilityOffIcon()) } else { payloadContainer.Add(parts.payload) payloadToggleAction.SetIcon(theme.VisibilityIcon()) } payloadContainer.Refresh() } payloadToggleAction.OnActivated = func() { parts.showPayloadQR = !parts.showPayloadQR; updateMode() } parts.payload.OnChanged = func(string) { if parts.showPayloadQR { updatePayloadQR(parts.payload.Text) } decryptAction() } updateMode() // Build payload toolbar payloadToolbar := widget.NewToolbar( widget.NewToolbarAction(theme.ContentPasteIcon(), func() { clip := fyne.CurrentApp().Clipboard().Content() parts.payload.SetText(clip) }), widget.NewToolbarAction(theme.ContentCopyIcon(), func() { copyClip(parts.payload.Text, parts) }), widget.NewToolbarAction(theme.FolderOpenIcon(), importPayloadQR), widget.NewToolbarAction(theme.ContentClearIcon(), func() { parts.payload.SetText("") }), widget.NewToolbarSeparator(), payloadToggleAction, ) payloadSection := container.NewVBox( container.NewHBox(widget.NewLabel("Payload"), layout.NewSpacer(), payloadToolbar), payloadContainer, ) resultHeader := container.NewHBox(widget.NewLabel("Výsledek"), layout.NewSpacer()) plainWrap := container.NewBorder(resultHeader, nil, nil, nil, parts.plainOut) split := container.NewVSplit(payloadSection, plainWrap) split.SetOffset(0.30) group := container.NewVBox( widget.NewLabelWithStyle("Dešifrování", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), split, ) return group } func buildTabbedUI(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.CanvasObject { idTab := container.NewTabItem("Identita", buildIdentityTab(parts, svc, vaultPath)) contactsTab := container.NewTabItem("Kontakty", buildContactsTab(parts, svc)) encTab := container.NewTabItem("Šifrování", buildEncryptTab(parts, svc)) decTab := container.NewTabItem("Dešifrování", buildDecryptTab(parts, svc)) tabs := container.NewAppTabs(idTab, contactsTab, encTab, decTab) tabs.SetTabLocation(container.TabLocationTop) // apply theme; allow toggle via hidden shortcut Ctrl+T (demo) fyne.CurrentApp().Settings().SetTheme(simpleTheme{}) // register a shortcut to toggle QR/text globally (Ctrl+Q) and encrypt/decrypt (Ctrl+Enter inside tab) // (Shortcut toggle removed - API placeholder, lze doplnit přes desktop accelerator) prefs := fyne.CurrentApp().Preferences() // Vždy start na identitě (index 0), ignoruje předchozí uloženou pozici tabs.SelectIndex(0) tabs.OnSelected = func(ti *container.TabItem) { if prefs != nil { prefs.SetInt("lastTab", tabs.SelectedIndex()) } } return container.NewBorder(nil, parts.toastLabel, nil, nil, tabs) } // buttonTile renders buttons above a related entry with a colored background spanning full width func buttonTile(btns ...fyne.CanvasObject) fyne.CanvasObject { if len(btns) == 0 { return widget.NewLabel("") } cols := len(btns) if cols > 3 { cols = 3 } // wrap into multiple rows if many grid := container.NewGridWithColumns(cols, btns...) bgColor := color.NRGBA{R: 240, G: 240, B: 245, A: 255} if forceDark { bgColor = color.NRGBA{R: 50, G: 54, B: 60, A: 255} } rect := canvas.NewRectangle(bgColor) rect.SetMinSize(fyne.NewSize(100, 38)) padded := container.NewPadded(grid) return container.NewStack(rect, padded) } // --- Contacts UI --- func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject { // Data + filtering var all []Contact var filtered []Contact selected := -1 // index into filtered (real contacts only, not counting draft) var draft *Contact // transient unsaved contact placeholder; not persisted until save 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 (includes optional draft as first row) list := widget.NewList( func() int { if draft != nil { return len(filtered) + 1 } return len(filtered) }, func() fyne.CanvasObject { return widget.NewLabel("") }, func(i widget.ListItemID, o fyne.CanvasObject) { if draft != nil { if int(i) == 0 { // draft row name := draft.Name if strings.TrimSpace(name) == "" { name = "(nový)" } o.(*widget.Label).SetText("✳ " + name + " (nový)") return } // shift index for real contacts real := int(i) - 1 if real >= 0 && real < len(filtered) { name := filtered[real].Name if name == "" { name = "(bez názvu)" } o.(*widget.Label).SetText(name) } return } // no draft if i >= 0 && int(i) < len(filtered) { name := filtered[i].Name if name == "" { name = "(bez názvu)" } o.(*widget.Label).SetText(name) } }, ) noResults := widget.NewLabel("Žádné kontakty") noResults.Alignment = fyne.TextAlignCenter noResults.Hide() // searchQuery drží poslední text vyhledávání – empty state jen pokud není prázdný a nic nenalezeno searchQuery := "" updateEmptyState := func() { // pokud je draft aktivní, neukazovat prázdný stav if draft != nil { noResults.Hide() return } if len(filtered) == 0 && searchQuery != "" { noResults.Show() } else { noResults.Hide() } } // Right detail form nameEntry := widget.NewEntry() certEntry := widget.NewMultiLineEntry() certEntry.SetMinRowsVisible(8) certEntry.Wrapping = fyne.TextWrapWord // Detail toolbar certToolbar := widget.NewToolbar( 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("") }), ) // helper pro generování default názvu makeDefaultName := func() string { base := "Nový kontakt" // pokud neexistuje žádný s přesným názvem, vrať jen base existsExact := false maxN := 1 for _, c := range all { if c.Name == base { existsExact = true } if strings.HasPrefix(c.Name, base+" ") { // varianty s číslem var n int if _, err := fmt.Sscanf(c.Name, "Nový kontakt %d", &n); err == nil { if n >= maxN { maxN = n + 1 } } } } if !existsExact { return base } return fmt.Sprintf("%s %d", base, maxN) } // search entry (musí být před saveBtn kvůli použití) search := widget.NewEntry() search.SetPlaceHolder("Hledat…") saveBtn := widget.NewButtonWithIcon("Uložit", theme.ConfirmIcon(), func() { name := strings.TrimSpace(nameEntry.Text) cert := strings.TrimSpace(certEntry.Text) if cert == "" { parts.showToast("Chybí cert") return } if name == "" || name == "Kontakt" || name == "Nový kontakt" { name = makeDefaultName() } if draft != nil { // saving draft as new contact _ = svc.SaveContact(Contact{Name: name, Cert: cert}) draft = nil searchQuery = "" // clear filter to show new search.SetText("") } else if selected < 0 || selected >= len(filtered) { // new without draft (fallback) _ = svc.SaveContact(Contact{Name: name, Cert: cert}) searchQuery = "" search.SetText("") } else { // update existing c := filtered[selected] c.Name = name c.Cert = cert _ = svc.SaveContact(c) } load() applyFilter(searchQuery) list.Refresh() updateEmptyState() 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í") }) certRow := container.NewHBox(certToolbar, layout.NewSpacer(), saveBtn, useBtn) var detail fyne.CanvasObject detail = container.NewBorder( container.NewVBox( widget.NewLabel("Název"), nameEntry, widget.NewLabel("Certifikát / Public key"), certRow, ), nil, nil, nil, certEntry, ) // List selection behavior list.OnSelected = func(id widget.ListItemID) { idx := int(id) if draft != nil { if idx == 0 { // selecting draft row selected = -1 nameEntry.SetText(draft.Name) certEntry.SetText(draft.Cert) return } idx = idx - 1 // shift for real contacts } if idx < 0 || idx >= len(filtered) { return } selected = idx // cancel draft if switching away if draft != nil { draft = nil list.Refresh() } c := filtered[idx] nameEntry.SetText(c.Name) certEntry.SetText(c.Cert) } list.OnUnselected = func(widget.ListItemID) { selected = -1 } // Header: title, search, toolbar search.OnChanged = func(s string) { searchQuery = s applyFilter(s) selected = -1 // keep draft row if present; clear form only if no draft if draft == nil { nameEntry.SetText("") certEntry.SetText("") } list.Refresh() updateEmptyState() } addNewLabelBtn := widget.NewButtonWithIcon("Vytvořit nový", theme.ContentAddIcon(), func() { // If draft already exists just reselect it if draft != nil { list.Select(0) return } // remove selection of any existing contact selected = -1 list.UnselectAll() // create draft placeholder draft = &Contact{ID: "__draft__", Name: makeDefaultName(), Cert: ""} nameEntry.SetText(draft.Name) certEntry.SetText("") // auto select all text in name so user can immediately přepsat fyne.CurrentApp().Driver().CanvasForObject(nameEntry).Focus(nameEntry) nameEntry.TypedShortcut(&fyne.ShortcutSelectAll{}) // ensure filter cleared so user sees draft on top searchQuery = "" search.SetText("") list.Refresh() list.Select(0) updateEmptyState() parts.showToast("Nový kontakt (draft)") }) // (Odstraněno tlačítko 'vložit z clipboardu' z horní lišty podle požadavku) addFromFile := widget.NewToolbarAction(theme.FolderOpenIcon(), func() { fd := dialog.NewFileOpen(func(r fyne.URIReadCloser, err error) { if err != nil || r == nil { return } defer r.Close() b, _ := io.ReadAll(r) txt := string(b) nm := extractCN(txt) if nm == "" { nm = "Nový kontakt" } nameEntry.SetText(nm) certEntry.SetText(txt) selected = -1 }, fyne.CurrentApp().Driver().AllWindows()[0]) fd.Show() }) // Live update draft placeholder name as user types nameEntry.OnChanged = func(s string) { if draft != nil { draft.Name = s list.Refresh() } } // Delete button přesunuto k Uložit / Použít deleteBtn := widget.NewButtonWithIcon("", theme.DeleteIcon(), func() { // If draft active and no real contact chosen -> cancel draft if draft != nil && selected == -1 { // remove draft and if exist some contacts immediately select first one draft = nil list.Refresh() updateEmptyState() if len(filtered) > 0 { selected = 0 c := filtered[0] nameEntry.SetText(c.Name) certEntry.SetText(c.Cert) list.Select(0) } else { selected = -1 nameEntry.SetText("") certEntry.SetText("") } parts.showToast("Zrušeno") return } if selected < 0 || selected >= len(filtered) { return } c := filtered[selected] confirmMsg := fmt.Sprintf("Opravdu smazat kontakt %q?", c.Name) dialog.NewConfirm("Smazat kontakt", confirmMsg, func(ok bool) { if !ok { return } _ = svc.DeleteContact(c.ID) load() applyFilter(search.Text) selected = -1 nameEntry.SetText("") certEntry.SetText("") list.Refresh() updateEmptyState() parts.showToast("Smazáno") }, fyne.CurrentApp().Driver().AllWindows()[0]).Show() }) // Přestavění certRow aby obsahoval i deleteBtn (nahrazení původního detailu níže) certRow = container.NewHBox(certToolbar, layout.NewSpacer(), saveBtn, useBtn, deleteBtn) // aktualizace detail panelu: nahradíme předchozí variantu detail = container.NewBorder( container.NewVBox( widget.NewLabel("Název"), nameEntry, widget.NewLabel("Certifikát / Public key"), certRow, ), nil, nil, nil, certEntry, ) // Horní toolbar nyní jen se souborem (import z file) topToolbar := widget.NewToolbar(addFromFile) header := container.NewHBox(widget.NewLabelWithStyle("Kontakty", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), layout.NewSpacer(), addNewLabelBtn, topToolbar) // Left side: search fixed top, list fills remaining (widget.List scrolluje sama); overlay noResults left := container.NewBorder(search, nil, nil, nil, container.NewStack(list, noResults)) right := container.NewStack(detail) split := container.NewHSplit(left, right) split.SetOffset(0.35) // Initialize load() updateEmptyState() list.Refresh() return container.NewBorder(header, nil, nil, nil, split) }