package main import ( "bytes" "encoding/base64" "errors" encrypt "fckeuspy-go/lib" "fmt" "image" "image/color" "image/png" "io" "log" "os" "os/exec" "path/filepath" "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" ) // --- Core UI model --- type uiParts struct { outKey, msg, peer, cipherOut, payload, plainOut *widget.Entry toastLabel *widget.Label cipherQR, pubQR, crtQR, peerQR, payloadQR *canvas.Image showQR, showPeerQR, 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), peerQR: canvas.NewImageFromImage(nil), payloadQR: canvas.NewImageFromImage(nil), showQR: true, showPeerQR: true, showPayloadQR: true, } p.cipherQR.SetMinSize(fyne.NewSize(220, 220)) p.pubQR.SetMinSize(fyne.NewSize(200, 200)) p.peerQR.SetMinSize(fyne.NewSize(200, 200)) p.payloadQR.SetMinSize(fyne.NewSize(220, 220)) p.toastLabel.Hide() return p } func (p *uiParts) showToast(s string) { fyne.Do(func() { p.toastLabel.SetText(s); p.toastLabel.Show() }) time.AfterFunc(1500*time.Millisecond, func() { fyne.Do(func() { if p.toastLabel.Text == s { p.toastLabel.Hide() } }) }) } // Theme type simpleTheme struct{} func (simpleTheme) Color(n fyne.ThemeColorName, v fyne.ThemeVariant) color.Color { if n == theme.ColorNameBackground { return color.NRGBA{24, 27, 31, 255} } return theme.DefaultTheme().Color(n, v) } 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) } // Facade interface type ServiceFacade interface { Encrypt(msg, peer string) (string, error) Decrypt(json string) (string, error) PublicPEM() string PublicCert() string ListContacts() ([]Contact, error) SaveContact(c Contact) error DeleteContact(id string) error } // Clipboard helpers func copyClip(s string, parts *uiParts) { fyne.CurrentApp().Clipboard().SetContent(s) parts.showToast("Zkopírováno") } func copyImageToClipboard(img image.Image, parts *uiParts) { if img == nil { return } buf := &bytes.Buffer{} if err := png.Encode(buf, img); err != nil { parts.showToast("Chyba PNG") return } choose := func() *exec.Cmd { wayland := os.Getenv("WAYLAND_DISPLAY") != "" has := func(b string) bool { _, e := exec.LookPath(b); return e == nil } if wayland { if has("wl-copy") { return exec.Command("wl-copy", "--type", "image/png") } if has("xclip") { return exec.Command("xclip", "-selection", "clipboard", "-t", "image/png") } } else { if has("xclip") { return exec.Command("xclip", "-selection", "clipboard", "-t", "image/png") } if has("wl-copy") { return exec.Command("wl-copy", "--type", "image/png") } } return nil }() if choose == nil { parts.showToast("Chybí wl-copy/xclip") return } stdin, _ := choose.StdinPipe() if err := choose.Start(); err != nil { parts.showToast("Nelze spustit") return } _, _ = stdin.Write(buf.Bytes()) _ = stdin.Close() if err := choose.Wait(); err != nil { parts.showToast("Selhalo") return } parts.showToast("QR obrázek ve schránce") } func readImageClipboard() (image.Image, error) { has := func(b string) bool { _, e := exec.LookPath(b); return e == nil } tryDecode := func(d []byte) (image.Image, bool) { if len(d) == 0 { return nil, false } if img, _, e := image.Decode(bytes.NewReader(d)); e == nil { return img, true } s := strings.TrimSpace(string(d)) if strings.HasPrefix(s, "data:image") { if p := strings.Index(s, ","); p > 0 { s = s[p+1:] } } if raw, err := base64.StdEncoding.DecodeString(s); err == nil { if img, _, e2 := image.Decode(bytes.NewReader(raw)); e2 == nil { return img, true } } return nil, false } if os.Getenv("WAYLAND_DISPLAY") != "" && has("wl-paste") { if types, err := exec.Command("wl-paste", "--list-types").Output(); err == nil { log.Printf("[clip] wl types: %s", strings.TrimSpace(string(types))) pref := []string{"image/png", "image/jpeg", "image/jpg", "image/webp"} seen := map[string]bool{} for _, p := range pref { seen[p] = true } order := append([]string{}, pref...) for _, t := range strings.Split(string(types), "\n") { t = strings.TrimSpace(t) if t == "" || !strings.HasPrefix(t, "image/") { continue } if !seen[t] { order = append(order, t) } } for _, t := range order { if data, err := exec.Command("wl-paste", "--type", t).Output(); err == nil { log.Printf("[clip] got type %s size=%d", t, len(data)) if img, ok := tryDecode(data); ok { // save debug _ = os.MkdirAll("qr_debug", 0o755) fn := filepath.Join("qr_debug", fmt.Sprintf("clip_raw_%d.png", time.Now().UnixNano())) if f, e := os.Create(fn); e == nil { _ = png.Encode(f, img) f.Close() log.Printf("[clip] saved %s", fn) } return img, nil } } } } if data, err := exec.Command("wl-paste").Output(); err == nil { log.Printf("[clip] generic wl-paste size=%d", len(data)) if img, ok := tryDecode(data); ok { return img, nil } } } if has("xclip") { for _, m := range []string{"image/png", "image/jpeg", "image/jpg", "image/bmp"} { if data, err := exec.Command("xclip", "-selection", "clipboard", "-t", m, "-o").Output(); err == nil { log.Printf("[clip] xclip type %s size=%d", m, len(data)) if img, ok := tryDecode(data); ok { return img, nil } } } if data, err := exec.Command("xclip", "-selection", "clipboard", "-o").Output(); err == nil { log.Printf("[clip] xclip raw size=%d", len(data)) if img, ok := tryDecode(data); ok { return img, nil } } } return nil, errors.New("nenalezen žádný obrázek ve schránce") } // Identity tab func buildIdentityTab(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.CanvasObject { // Toolbar: choose what to encode into the QR to keep density manageable mode := "combined" // combined, cert, pub chooser := widget.NewSelect([]string{"Veřejný+Cert", "Jen Cert", "Jen Veřejný"}, func(string) {}) chooser.Selected = "Veřejný+Cert" var update func() chooser.OnChanged = func(v string) { switch v { case "Jen Cert": mode = "cert" case "Jen Veřejný": mode = "pub" default: mode = "combined" } update() } deleteBtn := widget.NewButton("Smazat identitu", func() { pw := widget.NewPasswordEntry() form := widget.NewForm(widget.NewFormItem("Heslo", pw)) warn := widget.NewLabel("Smazat vše?") d := dialog.NewCustomConfirm("Potvrdit smazání", "Smazat", "Zrušit", container.NewVBox(warn, form), func(ok bool) { if !ok { return } if _, err := encrypt.OpenEncryptedStore(vaultPath, pw.Text); err != nil { dialog.NewError(errors.New("neplatné heslo"), fyne.CurrentApp().Driver().AllWindows()[0]).Show() return } _ = os.Remove(vaultPath) fyne.CurrentApp().Quit() }, fyne.CurrentApp().Driver().AllWindows()[0]) d.Resize(fyne.NewSize(420, 200)) d.Show() }) makeQR := func(data string, target *canvas.Image) { if data == "" { target.Image = nil target.Refresh() return } if b, err := GenerateQRPNG(data, 512); err == nil { if im, err2 := LoadPNG(b); err2 == nil { target.Image = im target.Refresh() } } } update = func() { if parts.showQR { var text string switch mode { case "cert": text = strings.TrimSpace(svc.PublicCert()) case "pub": text = strings.TrimSpace(svc.PublicPEM()) default: text = strings.TrimSpace(svc.PublicPEM() + "\n" + svc.PublicCert()) } makeQR(text, parts.pubQR) // debug save for comparison if parts.pubQR.Image != nil { _ = os.MkdirAll("qr_debug", 0o755) if f, e := os.Create(filepath.Join("qr_debug", "identity_current.png")); e == nil { png.Encode(f, parts.pubQR.Image) f.Close() } } } else { parts.pubQR.Image = nil parts.pubQR.Refresh() } } update() box := container.NewVBox(container.NewHBox(widget.NewLabel("QR obsah:"), chooser, layout.NewSpacer(), widget.NewButtonWithIcon("Kopírovat jako obrázek", theme.ContentPasteIcon(), func() { copyImageToClipboard(parts.pubQR.Image, parts) })), parts.pubQR) return container.NewVScroll(container.NewVBox(container.NewHBox(widget.NewLabelWithStyle("Moje identita", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), layout.NewSpacer()), box, deleteBtn)) } // Decrypt tab func buildDecryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject { parts.plainOut.Disable() parts.payload.Disable() parts.payload.SetPlaceHolder("Cyphertext se načte po vložení QR kódu…") parts.payload.OnChanged = nil parts.payloadQR.FillMode = canvas.ImageFillContain parts.payloadQR.SetMinSize(fyne.NewSize(260, 260)) decrypt := func(text string) { trimmed := strings.TrimSpace(text) if trimmed == "" { parts.plainOut.SetText("") return } go func(j string) { res, err := svc.Decrypt(j) if err != nil { fyne.Do(func() { parts.plainOut.SetText("") parts.showToast("Chyba dešifrování") }) return } fyne.Do(func() { parts.plainOut.SetText(res) }) }(trimmed) } setPayload := func(cipher string, img image.Image) { parts.payload.SetText(cipher) parts.payloadQR.Image = img parts.payloadQR.Refresh() decrypt(cipher) } decodeFromImage := func(img image.Image) { if img == nil { parts.showToast("Žádný QR obrázek") return } txt, err := DecodeQR(img) if err != nil { bounds := img.Bounds() inv := image.NewRGBA(bounds) for y := bounds.Min.Y; y < bounds.Max.Y; y++ { for x := bounds.Min.X; x < bounds.Max.X; x++ { r, g, b, a := img.At(x, y).RGBA() inv.Set(x, y, color.RGBA{uint8(255 - r/257), uint8(255 - g/257), uint8(255 - b/257), uint8(a / 257)}) } } if txt2, err2 := DecodeQR(inv); err2 == nil { setPayload(txt2, img) parts.showToast("Načteno z invert QR") return } parts.showToast("QR nenalezen: " + err.Error()) return } setPayload(txt, img) parts.showToast("Načteno z QR") } pasteQRBtn := widget.NewButtonWithIcon("Vložit ze schránky", theme.ContentPasteIcon(), func() { img, err := readImageClipboard() if err != nil { parts.showToast("Chyba schránky: " + err.Error()) return } decodeFromImage(img) }) openQRBtn := widget.NewButtonWithIcon("Otevřít obrázek", theme.FolderOpenIcon(), func() { win := fyne.CurrentApp().Driver().AllWindows()[0] fd := dialog.NewFileOpen(func(rc fyne.URIReadCloser, err error) { if err != nil || rc == nil { return } defer rc.Close() data, readErr := io.ReadAll(rc) if readErr != nil { parts.showToast("Chyba čtení souboru") return } img, _, decErr := image.Decode(bytes.NewReader(data)) if decErr != nil { parts.showToast("Neplatný obrázek: " + decErr.Error()) return } decodeFromImage(img) }, win) fd.SetFilter(storage.NewExtensionFileFilter([]string{".png", ".jpg", ".jpeg"})) fd.Show() }) copyPayloadBtn := widget.NewButtonWithIcon("Kopírovat payload", theme.ContentCopyIcon(), func() { if parts.payload.Text == "" { return } copyClip(parts.payload.Text, parts) }) clearBtn := widget.NewButtonWithIcon("Vymazat", theme.ContentClearIcon(), func() { parts.payload.SetText("") parts.payloadQR.Image = nil parts.payloadQR.Refresh() parts.plainOut.SetText("") parts.showToast("Vymazáno") }) toolbar := container.NewHBox(pasteQRBtn, openQRBtn, copyPayloadBtn, clearBtn, layout.NewSpacer()) return container.NewVBox( widget.NewLabelWithStyle("Dešifrování", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), toolbar, container.NewHBox(parts.payloadQR, layout.NewSpacer()), widget.NewLabel("Payload"), parts.payload, widget.NewLabel("Výsledek"), parts.plainOut, ) } // Per-contact encryption popup (QR-only output) func openEncryptPopup(parts *uiParts, svc ServiceFacade, ct Contact) { msgEntry := widget.NewMultiLineEntry() msgEntry.SetMinRowsVisible(6) msgEntry.Wrapping = fyne.TextWrapWord status := widget.NewLabel("Zadej zprávu…") qrImg := canvas.NewImageFromImage(nil) qrImg.FillMode = canvas.ImageFillContain qrImg.SetMinSize(fyne.NewSize(300, 300)) updateQR := func(text string) { if strings.TrimSpace(text) == "" { qrImg.Image = nil qrImg.Refresh() return } if b, err := GenerateQRPNG(text, 512); err == nil { if im, err2 := LoadPNG(b); err2 == nil { qrImg.Image = im qrImg.Refresh() } } } win := fyne.CurrentApp().Driver().AllWindows()[0] lastCipher := "" doEncrypt := func() { m := strings.TrimSpace(msgEntry.Text) fyne.Do(func() { if m == "" { status.SetText("Zpráva je prázdná") lastCipher = "" updateQR("") return } status.SetText("Šifruji…") }) if m == "" { return } go func(txt string) { res, err := svc.Encrypt(txt, ct.Cert) if err != nil { fyne.Do(func() { status.SetText("Chyba: " + err.Error()) }) return } fyne.Do(func() { lastCipher = res; updateQR(res); status.SetText("Hotovo") }) }(m) } var tmr *time.Timer msgEntry.OnChanged = func(string) { if tmr != nil { tmr.Stop() } tmr = time.AfterFunc(300*time.Millisecond, doEncrypt) } copyPayloadBtn := widget.NewButton("Kopírovat payload", func() { if lastCipher != "" { copyClip(lastCipher, parts) status.SetText("Zkopírováno") } }) copyQRBtn := widget.NewButton("Kopírovat QR", func() { copyImageToClipboard(qrImg.Image, parts) }) saveQRBtn := widget.NewButton("Uložit QR", func() { if qrImg.Image == nil { return } img := qrImg.Image fd := dialog.NewFileSave(func(wc fyne.URIWriteCloser, err error) { if err != nil || wc == nil { return } defer wc.Close() _ = png.Encode(wc, img) status.SetText("QR uložen") }, win) fd.SetFileName("message_qr.png") fd.Show() }) content := container.NewVBox(widget.NewLabel("Zpráva"), msgEntry, widget.NewSeparator(), container.NewHBox(widget.NewLabel("QR kód"), layout.NewSpacer(), copyPayloadBtn, copyQRBtn, saveQRBtn), qrImg, status) title := ct.Name if title == "" { title = "(bez názvu)" } if cn := extractCN(ct.Cert); cn != "" && !strings.Contains(title, cn) { title = fmt.Sprintf("%s (%s)", title, cn) } dlg := dialog.NewCustom(fmt.Sprintf("Poslat zprávu: %s", title), "Zavřít", content, win) dlg.Resize(fyne.NewSize(640, 520)) dlg.Show() } // Contacts tab with QR-only popup func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject { var all, filtered []Contact load := func() { items, _ := svc.ListContacts(); all = items; filtered = items } apply := func(q string) { if q == "" { filtered = all return } low := strings.ToLower(q) tmp := make([]Contact, 0, len(all)) for _, c := range all { if strings.Contains(strings.ToLower(c.Name), low) || strings.Contains(strings.ToLower(c.Cert), low) { tmp = append(tmp, c) } } filtered = tmp } makeDefault := func() string { base := "Nový kontakt" exists := false maxN := 1 for _, c := range all { if c.Name == base { exists = true } if strings.HasPrefix(c.Name, base+" ") { var n int if _, err := fmt.Sscanf(c.Name, "Nový kontakt %d", &n); err == nil && n >= maxN { maxN = n + 1 } } } if !exists { return base } return fmt.Sprintf("%s %d", base, maxN) } search := widget.NewEntry() var list *widget.List openPopup := func(existing *Contact) { nameEntry := widget.NewEntry() if existing != nil { nameEntry.SetText(existing.Name) } else { nameEntry.SetText(makeDefault()) } var certValue string if existing != nil { certValue = existing.Cert } manualEntry := widget.NewMultiLineEntry() manualEntry.SetMinRowsVisible(4) manualEntry.Wrapping = fyne.TextWrapWord manualEntry.SetPlaceHolder("Sem lze vložit text PEM pokud QR selže…") if certValue != "" { manualEntry.SetText(certValue) } qrImg := canvas.NewImageFromImage(nil) qrImg.FillMode = canvas.ImageFillContain qrImg.SetMinSize(fyne.NewSize(300, 300)) updateQR := func() { if strings.TrimSpace(certValue) == "" { qrImg.Image = nil qrImg.Refresh() return } if b, err := GenerateQRPNG(certValue, 512); err == nil { if im, err2 := LoadPNG(b); err2 == nil { qrImg.Image = im qrImg.Refresh() } } } updateQR() // now that updateQR exists, bind manualEntry changes manualEntry.OnChanged = func(s string) { certValue = s updateQR() } pasteText := widget.NewToolbarAction(theme.ContentPasteIcon(), func() { clip := fyne.CurrentApp().Clipboard().Content() if strings.TrimSpace(clip) == "" { parts.showToast("Schránka prázdná") return } certValue = clip manualEntry.SetText(certValue) updateQR() parts.showToast("Vloženo") }) pasteQR := widget.NewToolbarAction(theme.ComputerIcon(), func() { img, err := readImageClipboard() if err != nil { parts.showToast("Chyba čtení schránky: " + err.Error()) return } if img == nil { parts.showToast("Žádný QR obrázek") return } txt, decErr := DecodeQR(img) if decErr != nil { bounds := img.Bounds() inv := image.NewRGBA(bounds) for y := bounds.Min.Y; y < bounds.Max.Y; y++ { for x := bounds.Min.X; x < bounds.Max.X; x++ { r, g, b, a := img.At(x, y).RGBA() inv.Set(x, y, color.RGBA{uint8(255 - r/257), uint8(255 - g/257), uint8(255 - b/257), uint8(a / 257)}) } } if txt2, err2 := DecodeQR(inv); err2 == nil { certValue = txt2 manualEntry.SetText(certValue) updateQR() parts.showToast("Načteno z invert QR") return } debugDir := "qr_debug" _ = os.MkdirAll(debugDir, 0o755) fp := filepath.Join(debugDir, fmt.Sprintf("qr_clip_%d.png", time.Now().UnixNano())) if f, e := os.Create(fp); e == nil { _ = png.Encode(f, img) _ = f.Close() parts.showToast("QR nenalezen: " + decErr.Error() + " (" + fp + ")") } else { parts.showToast("QR nenalezen: " + decErr.Error()) } return } certValue = txt manualEntry.SetText(certValue) updateQR() parts.showToast("Načteno z QR") }) openImg := widget.NewToolbarAction(theme.FolderOpenIcon(), func() { win := fyne.CurrentApp().Driver().AllWindows()[0] fd := dialog.NewFileOpen(func(rc fyne.URIReadCloser, err error) { if err != nil || rc == nil { return } defer rc.Close() data, _ := io.ReadAll(rc) img, _, e2 := image.Decode(bytes.NewReader(data)) if e2 != nil { parts.showToast("Neplatný obrázek: " + e2.Error()) return } txt, e3 := DecodeQR(img) if e3 != nil { parts.showToast("QR nenalezeno: " + e3.Error()) return } certValue = txt manualEntry.SetText(certValue) updateQR() parts.showToast("Načteno z QR") }, win) fd.SetFilter(storage.NewExtensionFileFilter([]string{".png", ".jpg", ".jpeg"})) fd.Show() }) clearAct := widget.NewToolbarAction(theme.ContentClearIcon(), func() { certValue = ""; manualEntry.SetText(""); updateQR() }) copyAct := widget.NewToolbarAction(theme.ContentCopyIcon(), func() { if certValue != "" { copyClip(certValue, parts) } }) saveAct := widget.NewToolbarAction(theme.DocumentSaveIcon(), func() { if qrImg.Image == nil { return } win := fyne.CurrentApp().Driver().AllWindows()[0] img := qrImg.Image fd := dialog.NewFileSave(func(wc fyne.URIWriteCloser, err error) { if err != nil || wc == nil { return } defer wc.Close() _ = png.Encode(wc, img) parts.showToast("QR uložen") }, win) fd.SetFileName("contact_qr.png") fd.Show() }) toolbar := widget.NewToolbar(pasteText, pasteQR, openImg, widget.NewToolbarSeparator(), copyAct, saveAct, clearAct) win := fyne.CurrentApp().Driver().AllWindows()[0] var popup dialog.Dialog save := func(useEncrypt bool) { name := strings.TrimSpace(nameEntry.Text) cert := strings.TrimSpace(certValue) if cert == "" { parts.showToast("Chybí cert") return } cn := extractCN(cert) ask := cn != "" && (name == "" || name == "Kontakt" || name == "Nový kontakt" || name != cn) proceed := func(final string) { if final == "" || final == "Kontakt" || final == "Nový kontakt" { final = makeDefault() } if existing == nil { _ = svc.SaveContact(Contact{Name: final, Cert: cert}) } else { c := *existing c.Name = final c.Cert = cert _ = svc.SaveContact(c) } load() apply(strings.TrimSpace(search.Text)) list.Refresh() parts.showToast("Uloženo") if useEncrypt { parts.peer.SetText(cert) } if popup != nil { popup.Hide() } } if ask { dialog.NewCustomConfirm("Common Name", "Použít", "Ponechat", widget.NewLabel(fmt.Sprintf("Common Name: %s\nPoužít jako název?", cn)), func(ok bool) { if ok { proceed(cn) } else { proceed(name) } }, win).Show() return } proceed(name) } delBtn := widget.NewButtonWithIcon("Smazat", theme.DeleteIcon(), func() { if existing == nil { popup.Hide() return } dialog.NewConfirm("Smazat", "Opravdu smazat?", func(ok bool) { if !ok { return } _ = svc.DeleteContact(existing.ID) load() apply(strings.TrimSpace(search.Text)) list.Refresh() popup.Hide() parts.showToast("Smazáno") }, win).Show() }) useBtn := widget.NewButton("Použít ve Šifrování", func() { save(true) }) saveBtn := widget.NewButton("Uložit", func() { save(false) }) row := container.NewHBox(saveBtn, useBtn, layout.NewSpacer(), delBtn) title := "Nový kontakt" if existing != nil { title = "Upravit kontakt" } // manual entry area below QR for fallback or direct edit popup = dialog.NewCustom(title, "Zavřít", container.NewVBox( widget.NewLabel("Název"), nameEntry, widget.NewLabel("Certifikát / Public key (QR)"), toolbar, qrImg, widget.NewLabel("Text PEM"), manualEntry, widget.NewSeparator(), row), win) popup.Resize(fyne.NewSize(640, 520)) popup.Show() } list = widget.NewList(func() int { return len(filtered) }, func() fyne.CanvasObject { lbl := widget.NewLabel("") msg := widget.NewButton("Zpráva", nil) edit := widget.NewButton("Upravit", nil) msg.Importance = widget.LowImportance edit.Importance = widget.LowImportance return container.NewBorder(nil, nil, lbl, container.NewHBox(msg, edit)) }, func(i widget.ListItemID, o fyne.CanvasObject) { if int(i) < 0 || int(i) >= len(filtered) { return } c := filtered[i] row := o.(*fyne.Container) lbl := row.Objects[0].(*widget.Label) btnBox := row.Objects[1].(*fyne.Container) msgBtn := btnBox.Objects[0].(*widget.Button) editBtn := btnBox.Objects[1].(*widget.Button) name := c.Name if name == "" { name = "(bez názvu)" } if cn := extractCN(c.Cert); cn != "" { lbl.SetText(fmt.Sprintf("%s (%s)", name, cn)) } else { lbl.SetText(name) } msgBtn.OnTapped = func() { openEncryptPopup(parts, svc, c) } editBtn.OnTapped = func() { var ptr *Contact for i := range all { if all[i].ID == c.ID { ptr = &all[i] break } } openPopup(ptr) } }) search.SetPlaceHolder("Hledat…") search.OnChanged = func(s string) { apply(s); list.Refresh() } addBtn := widget.NewButtonWithIcon("Přidat", theme.ContentAddIcon(), func() { openPopup(nil) }) header := container.NewHBox(widget.NewLabelWithStyle("Kontakty", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), layout.NewSpacer(), addBtn) load() list.Refresh() return container.NewBorder(header, nil, nil, nil, container.NewBorder(search, nil, nil, nil, list)) } func buildTabbedUI(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.CanvasObject { tabs := container.NewAppTabs( container.NewTabItem("Identita", buildIdentityTab(parts, svc, vaultPath)), container.NewTabItem("Kontakty", buildContactsTab(parts, svc)), container.NewTabItem("Dešifrování", buildDecryptTab(parts, svc)), ) fyne.CurrentApp().Settings().SetTheme(simpleTheme{}) return container.NewBorder(nil, parts.toastLabel, nil, nil, tabs) }