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
Showing only changes of commit 08d28224f1 - Show all commits

368
ui.go
View File

@ -3,6 +3,7 @@ package main
import ( import (
"errors" "errors"
encrypt "fckeuspy-go/lib" encrypt "fckeuspy-go/lib"
"fmt"
"image/color" "image/color"
"io" "io"
"os" "os"
@ -72,12 +73,12 @@ func buildEntries() *uiParts {
p.payload.SetPlaceHolder(`{"ek":"…","n":"…","ct":"…"}`) p.payload.SetPlaceHolder(`{"ek":"…","n":"…","ct":"…"}`)
p.plainOut.SetPlaceHolder("Dešifrovaná zpráva…") p.plainOut.SetPlaceHolder("Dešifrovaná zpráva…")
// Zvýšení výšky (více řádků viditelně) // Zvýšení výšky (více řádků viditelně)
p.outKey.SetMinRowsVisible(6) p.outKey.SetMinRowsVisible(15)
p.peer.SetMinRowsVisible(4) p.peer.SetMinRowsVisible(15)
p.msg.SetMinRowsVisible(5) p.msg.SetMinRowsVisible(6)
p.cipherOut.SetMinRowsVisible(5) p.cipherOut.SetMinRowsVisible(15)
p.payload.SetMinRowsVisible(5) p.payload.SetMinRowsVisible(15)
p.plainOut.SetMinRowsVisible(5) p.plainOut.SetMinRowsVisible(15)
p.toastLabel.Hide() p.toastLabel.Hide()
return p return p
} }
@ -102,8 +103,16 @@ type simpleTheme struct{}
func (simpleTheme) Color(n fyne.ThemeColorName, v fyne.ThemeVariant) color.Color { func (simpleTheme) Color(n fyne.ThemeColorName, v fyne.ThemeVariant) color.Color {
base := theme.DefaultTheme().Color(n, v) base := theme.DefaultTheme().Color(n, v)
if n == theme.ColorNameBackground || n == theme.ColorNameButton { switch n {
return color.NRGBA{R: 30, G: 34, B: 39, A: 255} 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 return base
} }
@ -111,7 +120,7 @@ func (simpleTheme) Font(st fyne.TextStyle) fyne.Resource { return theme.Defau
func (simpleTheme) Icon(n fyne.ThemeIconName) fyne.Resource { return theme.DefaultTheme().Icon(n) } 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) } func (simpleTheme) Size(n fyne.ThemeSizeName) float32 { return theme.DefaultTheme().Size(n) }
var forceDark = true var forceDark = true // can be toggled later
// Build key section // Build key section
func buildIdentityTab(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.CanvasObject { func buildIdentityTab(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.CanvasObject {
@ -181,16 +190,16 @@ func buildIdentityTab(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.
identityContainer.Objects = nil identityContainer.Objects = nil
if parts.showQR { if parts.showQR {
updateQRImages() updateQRImages()
// Wrap each QR with its copy button // Wrap each QR with a small icon copy button
pubBox := container.NewVBox( pubBox := container.NewVBox(
widget.NewLabel("Veřejný klíč (PEM)"), widget.NewLabel("Veřejný klíč (PEM)"),
parts.pubQR, parts.pubQR,
widget.NewButton("Copy", func() { copyClip(svc.PublicPEM(), parts) }), widget.NewButtonWithIcon("", theme.ContentCopyIcon(), func() { copyClip(svc.PublicPEM(), parts) }),
) )
crtBox := container.NewVBox( crtBox := container.NewVBox(
widget.NewLabel("Certifikát (X.509)"), widget.NewLabel("Certifikát (X.509)"),
parts.crtQR, parts.crtQR,
widget.NewButton("Copy", func() { copyClip(svc.PublicCert(), parts) }), widget.NewButtonWithIcon("", theme.ContentCopyIcon(), func() { copyClip(svc.PublicCert(), parts) }),
) )
identityContainer.Add(container.NewGridWithColumns(2, pubBox, crtBox)) identityContainer.Add(container.NewGridWithColumns(2, pubBox, crtBox))
} else { } else {
@ -269,7 +278,7 @@ func buildEncryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
peerContainer.Objects = nil peerContainer.Objects = nil
if parts.showPeerQR { if parts.showPeerQR {
updatePeerQR(parts.peer.Text) updatePeerQR(parts.peer.Text)
peerContainer.Add(container.NewVBox(parts.peerQR, widget.NewButton("Copy", func() { copyClip(parts.peer.Text, parts) }))) peerContainer.Add(parts.peerQR) // copy tlačítko jen v toolbaru
peerToggleAction.SetIcon(theme.VisibilityOffIcon()) peerToggleAction.SetIcon(theme.VisibilityOffIcon())
} else { } else {
peerContainer.Add(parts.peer) peerContainer.Add(parts.peer)
@ -311,7 +320,7 @@ func buildEncryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
outputContainer.Objects = nil outputContainer.Objects = nil
if parts.showQR { if parts.showQR {
updateQR(parts.cipherOut.Text) updateQR(parts.cipherOut.Text)
outputContainer.Add(container.NewVBox(parts.cipherQR, widget.NewButton("Copy", func() { copyClip(parts.cipherOut.Text, parts) }))) outputContainer.Add(parts.cipherQR) // copy tlačítko jen v toolbaru
outputToggleAction.SetIcon(theme.VisibilityOffIcon()) outputToggleAction.SetIcon(theme.VisibilityOffIcon())
} else { } else {
outputContainer.Add(parts.cipherOut) outputContainer.Add(parts.cipherOut)
@ -371,20 +380,33 @@ func buildEncryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
// Toolbars // Toolbars
peerToolbar := widget.NewToolbar( peerToolbar := widget.NewToolbar(
widget.NewToolbarAction(theme.ContentPasteIcon(), func() { parts.peer.SetText(fyne.CurrentApp().Clipboard().Content()) }), widget.NewToolbarAction(theme.ContentPasteIcon(), func() { parts.peer.SetText(fyne.CurrentApp().Clipboard().Content()) }),
widget.NewToolbarAction(theme.ContentClearIcon(), func() { parts.peer.SetText("") }),
widget.NewToolbarAction(theme.ContentCopyIcon(), func() { copyClip(parts.peer.Text, parts) }), widget.NewToolbarAction(theme.ContentCopyIcon(), func() { copyClip(parts.peer.Text, parts) }),
widget.NewToolbarAction(theme.FolderOpenIcon(), importPeerQR), widget.NewToolbarAction(theme.FolderOpenIcon(), importPeerQR),
widget.NewToolbarAction(theme.ContentClearIcon(), func() { parts.peer.SetText("") }),
widget.NewToolbarSeparator(), widget.NewToolbarSeparator(),
peerToggleAction, peerToggleAction,
) )
outputToolbar := widget.NewToolbar( outputToolbar := widget.NewToolbar(
widget.NewToolbarAction(theme.ContentCopyIcon(), func() { copyClip(parts.cipherOut.Text, parts) }), 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(), widget.NewToolbarSeparator(),
outputToggleAction, outputToggleAction,
) )
// Primary CTA // Šifrování automaticky při změně zprávy nebo peer
encryptBtn := widget.NewButtonWithIcon("Zašifrovat", theme.ConfirmIcon(), func() { encAction(); updateMode() }) parts.msg.OnChanged = func(string) { encAction() }
parts.peer.OnChanged = func(string) {
if parts.showPeerQR {
updatePeerQR(parts.peer.Text)
}
encAction()
}
// Sections // Sections
peerSection := container.NewVBox( peerSection := container.NewVBox(
@ -394,7 +416,6 @@ func buildEncryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
msgSection := container.NewVBox( msgSection := container.NewVBox(
widget.NewLabel("Zpráva"), widget.NewLabel("Zpráva"),
parts.msg, parts.msg,
container.NewHBox(layout.NewSpacer(), encryptBtn),
) )
outputSection := container.NewVBox( outputSection := container.NewVBox(
container.NewHBox(widget.NewLabel("Výstup"), layout.NewSpacer(), outputToolbar), container.NewHBox(widget.NewLabel("Výstup"), layout.NewSpacer(), outputToolbar),
@ -415,16 +436,16 @@ func buildDecryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
decryptAction := func() { decryptAction := func() {
pl := parts.payload.Text pl := parts.payload.Text
if pl == "" { if pl == "" {
parts.showToast("Chybí payload") parts.plainOut.SetText("")
return return
} }
go func(js string) { go func(js string) {
res, err := svc.Decrypt(js) res, err := svc.Decrypt(js)
if err != nil { if err != nil {
fyne.Do(func() { parts.plainOut.SetText(""); parts.showToast("Chyba") }) fyne.Do(func() { parts.plainOut.SetText("") })
return return
} }
fyne.Do(func() { parts.plainOut.SetText(res); parts.showToast("OK") }) fyne.Do(func() { parts.plainOut.SetText(res) })
}(pl) }(pl)
} }
updatePayloadQR := func(text string) { updatePayloadQR := func(text string) {
@ -478,7 +499,8 @@ func buildDecryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
payloadContainer.Objects = nil payloadContainer.Objects = nil
if parts.showPayloadQR { if parts.showPayloadQR {
updatePayloadQR(parts.payload.Text) updatePayloadQR(parts.payload.Text)
payloadContainer.Add(container.NewVBox(parts.payloadQR, widget.NewButton("Copy", func() { copyClip(parts.payload.Text, parts) }))) // Jen samotný QR bez spodního copy tlačítka
payloadContainer.Add(parts.payloadQR)
payloadToggleAction.SetIcon(theme.VisibilityOffIcon()) payloadToggleAction.SetIcon(theme.VisibilityOffIcon())
} else { } else {
payloadContainer.Add(parts.payload) payloadContainer.Add(parts.payload)
@ -491,6 +513,7 @@ func buildDecryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
if parts.showPayloadQR { if parts.showPayloadQR {
updatePayloadQR(parts.payload.Text) updatePayloadQR(parts.payload.Text)
} }
decryptAction()
} }
updateMode() updateMode()
@ -499,45 +522,27 @@ func buildDecryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
widget.NewToolbarAction(theme.ContentPasteIcon(), func() { widget.NewToolbarAction(theme.ContentPasteIcon(), func() {
clip := fyne.CurrentApp().Clipboard().Content() clip := fyne.CurrentApp().Clipboard().Content()
parts.payload.SetText(clip) parts.payload.SetText(clip)
if parts.showPayloadQR {
updatePayloadQR(clip)
}
decryptAction()
}),
widget.NewToolbarAction(theme.ContentClearIcon(), func() {
parts.payload.SetText("")
parts.plainOut.SetText("")
if parts.showPayloadQR {
updatePayloadQR("")
}
}), }),
widget.NewToolbarAction(theme.ContentCopyIcon(), func() { copyClip(parts.payload.Text, parts) }),
widget.NewToolbarAction(theme.FolderOpenIcon(), importPayloadQR), widget.NewToolbarAction(theme.FolderOpenIcon(), importPayloadQR),
widget.NewToolbarAction(theme.ContentClearIcon(), func() { parts.payload.SetText("") }),
widget.NewToolbarSeparator(), widget.NewToolbarSeparator(),
payloadToggleAction, payloadToggleAction,
) )
// Build result toolbar
resultToolbar := widget.NewToolbar(
widget.NewToolbarAction(theme.ContentCopyIcon(), func() { copyClip(parts.plainOut.Text, parts) }),
)
// Primary CTA for decryption
decryptBtn := widget.NewButtonWithIcon("Dešifrovat", theme.ConfirmIcon(), func() { decryptAction() })
payloadSection := container.NewVBox( payloadSection := container.NewVBox(
container.NewHBox(widget.NewLabel("Payload"), layout.NewSpacer(), payloadToolbar), container.NewHBox(widget.NewLabel("Payload"), layout.NewSpacer(), payloadToolbar),
payloadContainer, payloadContainer,
container.NewHBox(layout.NewSpacer(), decryptBtn),
)
resultSection := container.NewVBox(
container.NewHBox(widget.NewLabel("Výsledek"), layout.NewSpacer(), resultToolbar),
parts.plainOut,
) )
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( group := container.NewVBox(
widget.NewLabelWithStyle("Dešifrování", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), widget.NewLabelWithStyle("Dešifrování", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
payloadSection, split,
resultSection,
) )
return container.NewVScroll(group) return group
} }
func buildTabbedUI(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.CanvasObject { func buildTabbedUI(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.CanvasObject {
@ -548,16 +553,13 @@ func buildTabbedUI(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.Can
tabs := container.NewAppTabs(idTab, contactsTab, encTab, decTab) tabs := container.NewAppTabs(idTab, contactsTab, encTab, decTab)
tabs.SetTabLocation(container.TabLocationTop) tabs.SetTabLocation(container.TabLocationTop)
// apply fixed dark theme once // apply theme; allow toggle via hidden shortcut Ctrl+T (demo)
fyne.CurrentApp().Settings().SetTheme(simpleTheme{}) 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() prefs := fyne.CurrentApp().Preferences()
if prefs != nil { // Vždy start na identitě (index 0), ignoruje předchozí uloženou pozici
idx := prefs.IntWithFallback("lastTab", 0) tabs.SelectIndex(0)
if idx >= 0 && idx < len(tabs.Items) {
tabs.SelectIndex(idx)
}
// only persist lastTab now
}
tabs.OnSelected = func(ti *container.TabItem) { tabs.OnSelected = func(ti *container.TabItem) {
if prefs != nil { if prefs != nil {
prefs.SetInt("lastTab", tabs.SelectedIndex()) prefs.SetInt("lastTab", tabs.SelectedIndex())
@ -592,7 +594,8 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
// Data + filtering // Data + filtering
var all []Contact var all []Contact
var filtered []Contact var filtered []Contact
selected := -1 selected := -1 // index into filtered (real contacts only, not counting draft)
var draft *Contact // transient unsaved contact placeholder; not persisted until save
load := func() { load := func() {
items, _ := svc.ListContacts() items, _ := svc.ListContacts()
all = items all = items
@ -613,17 +616,66 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
filtered = tmp filtered = tmp
} }
// Left list // Left list (includes optional draft as first row)
list := widget.NewList( list := widget.NewList(
func() int { return len(filtered) }, func() int {
if draft != nil {
return len(filtered) + 1
}
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) {
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) { if i >= 0 && int(i) < len(filtered) {
o.(*widget.Label).SetText(filtered[i].Name) 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 // Right detail form
nameEntry := widget.NewEntry() nameEntry := widget.NewEntry()
certEntry := widget.NewMultiLineEntry() certEntry := widget.NewMultiLineEntry()
@ -637,18 +689,64 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
}), }),
widget.NewToolbarAction(theme.ContentClearIcon(), func() { certEntry.SetText("") }), 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() { saveBtn := widget.NewButtonWithIcon("Uložit", theme.ConfirmIcon(), func() {
if selected < 0 || selected >= len(filtered) { name := strings.TrimSpace(nameEntry.Text)
// new or none selected -> create new cert := strings.TrimSpace(certEntry.Text)
_ = svc.SaveContact(Contact{Name: nameEntry.Text, Cert: certEntry.Text}) if cert == "" {
} else { 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 := filtered[selected]
c.Name = nameEntry.Text c.Name = name
c.Cert = certEntry.Text c.Cert = cert
_ = svc.SaveContact(c) _ = svc.SaveContact(c)
} }
load() load()
applyFilter(searchQuery)
list.Refresh() list.Refresh()
updateEmptyState()
parts.showToast("Uloženo") parts.showToast("Uloženo")
}) })
useBtn := widget.NewButtonWithIcon("Použít ve Šifrování", theme.ConfirmIcon(), func() { useBtn := widget.NewButtonWithIcon("Použít ve Šifrování", theme.ConfirmIcon(), func() {
@ -656,14 +754,16 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
parts.showToast("Kontakt vložen do Šifrování") parts.showToast("Kontakt vložen do Šifrování")
}) })
detail := container.NewBorder( certRow := container.NewHBox(certToolbar, layout.NewSpacer(), saveBtn, useBtn)
var detail fyne.CanvasObject
detail = container.NewBorder(
container.NewVBox( container.NewVBox(
widget.NewLabel("Název"), widget.NewLabel("Název"),
nameEntry, nameEntry,
widget.NewLabel("Certifikát / Public key"), widget.NewLabel("Certifikát / Public key"),
certToolbar, certRow,
), ),
container.NewHBox(layout.NewSpacer(), saveBtn, useBtn), nil,
nil, nil, nil, nil,
certEntry, certEntry,
) )
@ -671,10 +771,24 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
// List selection behavior // List selection behavior
list.OnSelected = func(id widget.ListItemID) { list.OnSelected = func(id widget.ListItemID) {
idx := int(id) 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) { if idx < 0 || idx >= len(filtered) {
return return
} }
selected = idx selected = idx
// cancel draft if switching away
if draft != nil {
draft = nil
list.Refresh()
}
c := filtered[idx] c := filtered[idx]
nameEntry.SetText(c.Name) nameEntry.SetText(c.Name)
certEntry.SetText(c.Cert) certEntry.SetText(c.Cert)
@ -682,28 +796,40 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
list.OnUnselected = func(widget.ListItemID) { selected = -1 } list.OnUnselected = func(widget.ListItemID) { selected = -1 }
// Header: title, search, toolbar // Header: title, search, toolbar
search := widget.NewEntry() search.OnChanged = func(s string) {
search.SetPlaceHolder("Hledat…") searchQuery = s
search.OnChanged = func(s string) { applyFilter(s); list.Refresh() } applyFilter(s)
addNew := widget.NewToolbarAction(theme.ContentAddIcon(), func() {
nameEntry.SetText("Kontakt")
certEntry.SetText("")
selected = -1 selected = -1
}) // keep draft row if present; clear form only if no draft
addFromClipboard := widget.NewToolbarAction(theme.ContentPasteIcon(), func() { if draft == nil {
txt := fyne.CurrentApp().Clipboard().Content() nameEntry.SetText("")
if txt == "" { certEntry.SetText("")
parts.showToast("Schází data v clipboardu") }
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 return
} }
nm := extractCN(txt) // remove selection of any existing contact
if nm == "" {
nm = "Kontakt"
}
nameEntry.SetText(nm)
certEntry.SetText(txt)
selected = -1 selected = -1
list.UnselectAll()
// create draft placeholder
draft = &Contact{ID: "__draft__", Name: makeDefaultName(), Cert: ""}
nameEntry.SetText(draft.Name)
certEntry.SetText("")
// 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() { 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 {
@ -714,7 +840,7 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
txt := string(b) txt := string(b)
nm := extractCN(txt) nm := extractCN(txt)
if nm == "" { if nm == "" {
nm = "Kontakt" nm = "Nový kontakt"
} }
nameEntry.SetText(nm) nameEntry.SetText(nm)
certEntry.SetText(txt) certEntry.SetText(txt)
@ -722,35 +848,73 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
}, fyne.CurrentApp().Driver().AllWindows()[0]) }, fyne.CurrentApp().Driver().AllWindows()[0])
fd.Show() fd.Show()
}) })
deleteAction := widget.NewToolbarAction(theme.DeleteIcon(), func() {
if selected < 0 || selected >= len(filtered) { // Live update draft placeholder name as user types
return nameEntry.OnChanged = func(s string) {
} if draft != nil {
_ = svc.DeleteContact(filtered[selected].ID) draft.Name = s
load()
applyFilter(search.Text)
list.Refresh() 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 {
draft = nil
nameEntry.SetText("") nameEntry.SetText("")
certEntry.SetText("") certEntry.SetText("")
selected = -1 list.Refresh()
}) updateEmptyState()
copyAction := widget.NewToolbarAction(theme.ContentCopyIcon(), func() { parts.showToast("Zrušeno")
return
}
if selected < 0 || selected >= len(filtered) { if selected < 0 || selected >= len(filtered) {
return return
} }
copyClip(filtered[selected].Cert, parts) 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()
}) })
topToolbar := widget.NewToolbar(addNew, addFromClipboard, addFromFile, widget.NewToolbarSeparator(), copyAction, deleteAction) // Přestavění certRow aby obsahoval i deleteBtn (nahrazení původního detailu níže)
header := container.NewHBox(widget.NewLabelWithStyle("Kontakty", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), layout.NewSpacer(), search, topToolbar) 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)
// Split view // Left side: search fixed top, list fills remaining (widget.List scrolluje sama); overlay noResults
left := container.NewBorder(nil, nil, nil, nil, list) left := container.NewBorder(search, nil, nil, nil, container.NewStack(list, noResults))
right := container.NewStack(detail) right := container.NewStack(detail)
split := container.NewHSplit(left, right) split := container.NewHSplit(left, right)
split.SetOffset(0.35) split.SetOffset(0.35)
// Initialize // Initialize
load() load()
updateEmptyState()
list.Refresh() list.Refresh()
return container.NewBorder(header, nil, nil, nil, split) return container.NewBorder(header, nil, nil, nil, split)
} }