feature/upgrade-ui - vylepsene ui #2

Merged
luke-20 merged 1 commits from feature/upgrade-ui into main 2025-09-28 22:23:22 +02:00
2 changed files with 73 additions and 164 deletions

View File

@ -7,7 +7,6 @@ import (
"image/color"
"image/draw"
"image/png"
"log"
"math"
"os/exec"
@ -32,17 +31,12 @@ func GenerateQRPNG(text string, size int) ([]byte, error) {
// DecodeQR decodes first QR code text from an image.
func DecodeQR(img image.Image) (string, error) {
log.Printf("[qr] start decode: %dx%d %T", img.Bounds().Dx(), img.Bounds().Dy(), img)
// Try basic decode first
codes, err := goqr.Recognize(img)
if err == nil && len(codes) > 0 {
log.Printf("[qr] basic success length=%d", len(codes))
return string(codes[0].Payload), nil
}
if err != nil {
log.Printf("[qr] basic error: %v", err)
} else {
log.Printf("[qr] basic no codes")
}
// Convert palette/indexed images to RGBA first to avoid issues with goqr
@ -55,14 +49,8 @@ func DecodeQR(img image.Image) (string, error) {
// Try again after conversion
codes, err := goqr.Recognize(img)
if err == nil && len(codes) > 0 {
log.Printf("[qr] palette-convert success")
return string(codes[0].Payload), nil
}
if err != nil {
log.Printf("[qr] palette-convert err: %v", err)
} else {
log.Printf("[qr] palette-convert no codes")
}
}
// Try multiple preprocessing strategies to improve recognition from clipboard screenshots
@ -213,20 +201,17 @@ func DecodeQR(img image.Image) (string, error) {
}
var firstErr error
for idx, attempt := range attempts {
for _, attempt := range attempts {
codes, err := goqr.Recognize(attempt)
if err != nil {
if firstErr == nil {
firstErr = err
}
log.Printf("[qr] attempt %d error: %v", idx, err)
continue
}
if len(codes) > 0 {
log.Printf("[qr] attempt %d success (%d codes)", idx, len(codes))
return string(codes[0].Payload), nil
}
log.Printf("[qr] attempt %d no codes", idx)
}
// ZXing helper used across multiple branches
@ -247,25 +232,19 @@ func DecodeQR(img image.Image) (string, error) {
}
}
// Also try ZXing on all preprocessed attempts
for idx, attempt := range attempts {
for _, attempt := range attempts {
if txt, err := decodeZX(attempt, map[gozxing.DecodeHintType]interface{}{
gozxing.DecodeHintType_TRY_HARDER: true,
gozxing.DecodeHintType_PURE_BARCODE: true,
}); err == nil && txt != "" {
log.Printf("[qr] zxing attempt %d success (pure)", idx)
return txt, nil
}
if txt, err := decodeZX(attempt, map[gozxing.DecodeHintType]interface{}{
gozxing.DecodeHintType_TRY_HARDER: true,
}); err == nil && txt != "" {
log.Printf("[qr] zxing attempt %d success (try-harder)", idx)
return txt, nil
}
}
if firstErr != nil {
log.Printf("[qr] preprocess firstErr: %v", firstErr)
}
// --- Heuristic fallback: auto-invert + quiet-zone pad + gozxing ---
addQuietZone := func(src image.Image) image.Image {
b := src.Bounds()
@ -315,7 +294,6 @@ func DecodeQR(img image.Image) (string, error) {
modified = append(modified, addQuietZone(invert(base)))
for _, m := range modified {
if codes, err := goqr.Recognize(m); err == nil && len(codes) > 0 {
log.Printf("[qr] quiet-zone/invert success")
return string(codes[0].Payload), nil
}
// Also try ZXing on these variants before moving on
@ -323,13 +301,11 @@ func DecodeQR(img image.Image) (string, error) {
gozxing.DecodeHintType_TRY_HARDER: true,
gozxing.DecodeHintType_PURE_BARCODE: true,
}); err == nil && txt != "" {
log.Printf("[qr] gozxing success (quiet-zone+pure)")
return txt, nil
}
if txt, err := decodeZX(m, map[gozxing.DecodeHintType]interface{}{
gozxing.DecodeHintType_TRY_HARDER: true,
}); err == nil && txt != "" {
log.Printf("[qr] gozxing success (quiet-zone+try-harder)")
return txt, nil
}
}
@ -340,27 +316,19 @@ func DecodeQR(img image.Image) (string, error) {
gozxing.DecodeHintType_PURE_BARCODE: true,
}
if txt, err := decodeZX(base, hints); err == nil && txt != "" {
log.Printf("[qr] gozxing success (hints)")
return txt, nil
} else if err != nil {
log.Printf("[qr] gozxing(hints) err: %v", err)
}
// Try again without PURE_BARCODE in case quiet zone is cropped
hints2 := map[gozxing.DecodeHintType]interface{}{gozxing.DecodeHintType_TRY_HARDER: true}
if txt, err := decodeZX(base, hints2); err == nil && txt != "" {
log.Printf("[qr] gozxing success (try-harder)")
return txt, nil
} else if err != nil {
log.Printf("[qr] gozxing(try-harder) err: %v", err)
}
// Try ZXing on rotations as well
for _, r := range rotate(base) {
if txt, err := decodeZX(r, hints2); err == nil && txt != "" {
log.Printf("[qr] gozxing success (rotated)")
return txt, nil
}
if txt, err := decodeZX(r, hints); err == nil && txt != "" {
log.Printf("[qr] gozxing success (rotated pure)")
return txt, nil
}
}
@ -392,7 +360,6 @@ func DecodeQR(img image.Image) (string, error) {
tryPure := func(src image.Image) (string, bool) {
for _, px := range []int{1, 2, 3} {
if txt, err := decodeZX(inset(src, px), hints); err == nil && txt != "" {
log.Printf("[qr] gozxing pure+inset %d success", px)
return txt, true
}
}
@ -425,11 +392,9 @@ func DecodeQR(img image.Image) (string, error) {
continue
}
if txt, err := decodeZX(cand, hints); err == nil && txt != "" {
log.Printf("[qr] gozxing pure+asym inset %d,%d,%d,%d success", l, t, r, btm)
return txt, nil
}
if txt, err := decodeZX(cand, hints2); err == nil && txt != "" {
log.Printf("[qr] gozxing asym inset %d,%d,%d,%d success", l, t, r, btm)
return txt, nil
}
}
@ -465,7 +430,6 @@ func DecodeQR(img image.Image) (string, error) {
if minX < maxX && minY < maxY {
crop := image.NewRGBA(image.Rect(0, 0, maxX-minX+1, maxY-minY+1))
draw.Draw(crop, crop.Bounds(), base, image.Point{X: minX, Y: minY}, draw.Src)
log.Printf("[qr] crop bbox size=%dx%d", crop.Bounds().Dx(), crop.Bounds().Dy())
for _, up := range []int{2, 3, 4} {
cw := crop.Bounds().Dx() * up
ch := crop.Bounds().Dy() * up
@ -481,10 +445,7 @@ func DecodeQR(img image.Image) (string, error) {
}
}
if txt, err := decodeZX(scaled, hints2); err == nil && txt != "" {
log.Printf("[qr] gozxing crop+scale success up=%d", up)
return txt, nil
} else if err != nil {
log.Printf("[qr] crop up=%d fail: %v", up, err)
}
}
}
@ -499,12 +460,8 @@ func DecodeQR(img image.Image) (string, error) {
c.Stdin = bytes.NewReader(buf.Bytes())
out, err := c.Output()
if err == nil && len(out) > 0 {
log.Printf("[qr] zbarimg success")
return string(out), nil
}
if err != nil {
log.Printf("[qr] zbarimg error: %v", err)
}
}
}
return "", errors.New("no qr code found")

190
ui.go
View File

@ -10,7 +10,6 @@ import (
"image/color"
"image/png"
"io"
"log"
"os"
"os/exec"
"path/filepath"
@ -55,6 +54,7 @@ func buildEntries() *uiParts {
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
}
@ -74,10 +74,15 @@ func (p *uiParts) showToast(s string) {
type simpleTheme struct{}
func (simpleTheme) Color(n fyne.ThemeColorName, v fyne.ThemeVariant) color.Color {
if n == theme.ColorNameBackground {
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) }
@ -172,7 +177,6 @@ func readImageClipboard() (image.Image, error) {
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 {
@ -190,23 +194,13 @@ func readImageClipboard() (image.Image, error) {
}
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
}
@ -215,14 +209,12 @@ func readImageClipboard() (image.Image, error) {
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
}
@ -234,18 +226,19 @@ func readImageClipboard() (image.Image, error) {
// 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"
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 "Jen Cert":
case "Certifikát":
mode = "cert"
case "Jen Veřejný":
case "Veřejný klíč":
mode = "pub"
default:
mode = "combined"
mode = "cert"
}
update()
}
@ -267,6 +260,8 @@ func buildIdentityTab(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.
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
@ -284,38 +279,51 @@ func buildIdentityTab(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.
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())
default: // cert
text = strings.TrimSpace(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))
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.payload.Disable()
parts.payload.SetPlaceHolder("Cyphertext se načte po vložení QR kódu…")
parts.payload.OnChanged = nil
parts.plainOut.Wrapping = fyne.TextWrapWord
parts.plainOut.SetMinRowsVisible(12)
parts.payloadQR.FillMode = canvas.ImageFillContain
parts.payloadQR.SetMinSize(fyne.NewSize(260, 260))
@ -339,7 +347,6 @@ func buildDecryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
}
setPayload := func(cipher string, img image.Image) {
parts.payload.SetText(cipher)
parts.payloadQR.Image = img
parts.payloadQR.Refresh()
decrypt(cipher)
@ -404,30 +411,26 @@ func buildDecryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
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())
// 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()),
widget.NewLabel("Payload"),
parts.payload,
widget.NewLabel("Výsledek"),
container.NewHBox(widget.NewLabel("Výsledek"), layout.NewSpacer(), copyDecBtn),
parts.plainOut,
)
}
@ -455,13 +458,11 @@ func openEncryptPopup(parts *uiParts, svc ServiceFacade, ct Contact) {
}
}
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
}
@ -476,7 +477,7 @@ func openEncryptPopup(parts *uiParts, svc ServiceFacade, ct Contact) {
fyne.Do(func() { status.SetText("Chyba: " + err.Error()) })
return
}
fyne.Do(func() { lastCipher = res; updateQR(res); status.SetText("Hotovo") })
fyne.Do(func() { updateQR(res); status.SetText("Hotovo") })
}(m)
}
var tmr *time.Timer
@ -486,12 +487,6 @@ func openEncryptPopup(parts *uiParts, svc ServiceFacade, ct Contact) {
}
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 {
@ -509,7 +504,7 @@ func openEncryptPopup(parts *uiParts, svc ServiceFacade, ct Contact) {
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)
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)"
@ -573,13 +568,6 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
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))
@ -597,23 +585,7 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
}
}
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() {
pasteQR := widget.NewToolbarAction(theme.ContentPasteIcon(), func() {
img, err := readImageClipboard()
if err != nil {
parts.showToast("Chyba čtení schránky: " + err.Error())
@ -636,7 +608,6 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
}
if txt2, err2 := DecodeQR(inv); err2 == nil {
certValue = txt2
manualEntry.SetText(certValue)
updateQR()
parts.showToast("Načteno z invert QR")
return
@ -654,7 +625,6 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
return
}
certValue = txt
manualEntry.SetText(certValue)
updateQR()
parts.showToast("Načteno z QR")
})
@ -678,37 +648,14 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
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)
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) {
@ -744,12 +691,19 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
}
}
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) {
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)
} else {
proceed(name)
return
}
proceed(strings.TrimSpace(entry.Text))
}, win).Show()
return
}
@ -772,9 +726,8 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
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)
row := container.NewHBox(layout.NewSpacer(), saveBtn, delBtn, layout.NewSpacer())
title := "Nový kontakt"
if existing != nil {
title = "Upravit kontakt"
@ -783,7 +736,6 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
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()