feature/ui - zatim ne uplne uhlazena ale celkem pouzitelna appka #1
368
ui.go
368
ui.go
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user