fckeuspy-go/ui.go
2025-09-28 22:22:25 +02:00

799 lines
23 KiB
Go
Executable File

package main
import (
"bytes"
"encoding/base64"
"errors"
encrypt "fckeuspy-go/lib"
"fmt"
"image"
"image/color"
"image/png"
"io"
"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.pubQR.FillMode = canvas.ImageFillContain
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 {
switch n {
case theme.ColorNameBackground:
return color.NRGBA{24, 27, 31, 255}
case theme.ColorNameDisabled:
// Make disabled text brighter for readability on dark background
return color.NRGBA{230, 233, 238, 255}
default:
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 {
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 {
if img, ok := tryDecode(data); ok {
return img, nil
}
}
}
}
if data, err := exec.Command("wl-paste").Output(); err == nil {
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 {
if img, ok := tryDecode(data); ok {
return img, nil
}
}
}
if data, err := exec.Command("xclip", "-selection", "clipboard", "-o").Output(); err == nil {
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 := "cert" // cert, pub
options := []string{"Certifikát", "Veřejný klíč"}
chooser := widget.NewSelect(options, func(string) {})
chooser.Selected = options[0]
var update func()
chooser.OnChanged = func(v string) {
switch v {
case "Certifikát":
mode = "cert"
case "Veřejný klíč":
mode = "pub"
default:
mode = "cert"
}
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()
})
// Keep button minimal; align right
deleteRow := container.NewHBox(layout.NewSpacer(), deleteBtn)
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 "pub":
text = strings.TrimSpace(svc.PublicPEM())
default: // cert
text = strings.TrimSpace(svc.PublicCert())
}
makeQR(text, parts.pubQR)
} else {
parts.pubQR.Image = nil
parts.pubQR.Refresh()
}
}
update()
saveQR := widget.NewButtonWithIcon("Uložit QR", theme.DocumentSaveIcon(), func() {
if parts.pubQR.Image == nil {
return
}
win := fyne.CurrentApp().Driver().AllWindows()[0]
img := parts.pubQR.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("identity_qr.png")
fd.Show()
})
qrRow := container.NewHBox(
widget.NewLabel("Obsah QR"),
chooser,
layout.NewSpacer(),
widget.NewButtonWithIcon("Kopírovat jako obrázek", theme.ContentPasteIcon(), func() { copyImageToClipboard(parts.pubQR.Image, parts) }),
saveQR,
)
box := container.NewVBox(qrRow, container.NewCenter(parts.pubQR))
return container.NewVScroll(container.NewVBox(container.NewHBox(widget.NewLabelWithStyle("Moje identita", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), layout.NewSpacer()), box, deleteRow))
}
// Decrypt tab
func buildDecryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
parts.plainOut.Disable()
parts.plainOut.Wrapping = fyne.TextWrapWord
parts.plainOut.SetMinRowsVisible(12)
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.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()
})
clearBtn := widget.NewButtonWithIcon("Vymazat", theme.ContentClearIcon(), func() {
parts.payloadQR.Image = nil
parts.payloadQR.Refresh()
parts.plainOut.SetText("")
parts.showToast("Vymazáno")
})
// Align buttons to the right by placing spacer first
toolbar := container.NewHBox(layout.NewSpacer(), pasteQRBtn, openQRBtn, clearBtn)
copyDecBtn := widget.NewButtonWithIcon("Kopírovat zprávu", theme.ContentCopyIcon(), func() {
if strings.TrimSpace(parts.plainOut.Text) != "" {
copyClip(parts.plainOut.Text, parts)
}
})
return container.NewVBox(
widget.NewLabelWithStyle("Dešifrování", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
toolbar,
container.NewHBox(parts.payloadQR, layout.NewSpacer()),
container.NewHBox(widget.NewLabel("Výsledek"), layout.NewSpacer(), copyDecBtn),
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]
doEncrypt := func() {
m := strings.TrimSpace(msgEntry.Text)
fyne.Do(func() {
if m == "" {
status.SetText("Zpráva je prázdná")
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() { 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)
}
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(), 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
}
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()
pasteQR := widget.NewToolbarAction(theme.ContentPasteIcon(), 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
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
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
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 = ""; updateQR() })
toolbar := widget.NewToolbar(pasteQR, openImg, 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 {
entry := widget.NewEntry()
entry.SetText(name)
content := container.NewVBox(
widget.NewLabel(fmt.Sprintf("Common Name nalezen v certifikátu: %s", cn)),
widget.NewLabel("Chcete použít CN jako název, nebo jej upravit?"),
entry,
)
dialog.NewCustomConfirm("Název kontaktu", "Použít CN", "Uložit", content, func(ok bool) {
if ok {
proceed(cn)
return
}
proceed(strings.TrimSpace(entry.Text))
}, 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()
})
saveBtn := widget.NewButton("Uložit", func() { save(false) })
row := container.NewHBox(layout.NewSpacer(), saveBtn, delBtn, layout.NewSpacer())
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.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)
}