feature/ui - zatim ne uplne uhlazena ale celkem pouzitelna appka #1
210
ui.go
210
ui.go
@ -6,6 +6,7 @@ import (
|
|||||||
"image/color"
|
"image/color"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
@ -118,9 +119,6 @@ func buildIdentityTab(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.
|
|||||||
identityToggle := widget.NewToolbarAction(theme.VisibilityOffIcon(), nil)
|
identityToggle := widget.NewToolbarAction(theme.VisibilityOffIcon(), nil)
|
||||||
identityToolbar := widget.NewToolbar(
|
identityToolbar := widget.NewToolbar(
|
||||||
widget.NewToolbarAction(theme.ContentCopyIcon(), func() { copyClip(svc.PublicPEM()+"\n"+svc.PublicCert(), parts) }),
|
widget.NewToolbarAction(theme.ContentCopyIcon(), func() { copyClip(svc.PublicPEM()+"\n"+svc.PublicCert(), parts) }),
|
||||||
widget.NewToolbarAction(theme.ContentPasteIcon(), func() { parts.outKey.SetText(fyne.CurrentApp().Clipboard().Content()) }),
|
|
||||||
widget.NewToolbarAction(theme.ContentClearIcon(), func() { parts.outKey.SetText("") }),
|
|
||||||
widget.NewToolbarSeparator(),
|
|
||||||
identityToggle,
|
identityToggle,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -184,13 +182,25 @@ func buildIdentityTab(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.
|
|||||||
if parts.showQR {
|
if parts.showQR {
|
||||||
updateQRImages()
|
updateQRImages()
|
||||||
// Wrap each QR with its copy button
|
// Wrap each QR with its copy button
|
||||||
pubBox := container.NewVBox(parts.pubQR, widget.NewButton("Copy", func() { copyClip(svc.PublicPEM(), parts) }))
|
pubBox := container.NewVBox(
|
||||||
crtBox := container.NewVBox(parts.crtQR, widget.NewButton("Copy", func() { copyClip(svc.PublicCert(), parts) }))
|
widget.NewLabel("Veřejný klíč (PEM)"),
|
||||||
|
parts.pubQR,
|
||||||
|
widget.NewButton("Copy", func() { copyClip(svc.PublicPEM(), parts) }),
|
||||||
|
)
|
||||||
|
crtBox := container.NewVBox(
|
||||||
|
widget.NewLabel("Certifikát (X.509)"),
|
||||||
|
parts.crtQR,
|
||||||
|
widget.NewButton("Copy", func() { copyClip(svc.PublicCert(), parts) }),
|
||||||
|
)
|
||||||
identityContainer.Add(container.NewGridWithColumns(2, pubBox, crtBox))
|
identityContainer.Add(container.NewGridWithColumns(2, pubBox, crtBox))
|
||||||
} else {
|
} else {
|
||||||
// show combined text for convenience
|
// show combined text for convenience
|
||||||
parts.outKey.SetText(svc.PublicPEM() + "\n" + svc.PublicCert())
|
parts.outKey.SetText(svc.PublicPEM() + "\n" + svc.PublicCert())
|
||||||
identityContainer.Add(parts.outKey)
|
parts.outKey.Disable()
|
||||||
|
identityContainer.Add(container.NewVBox(
|
||||||
|
widget.NewLabel("Veřejný klíč + Certifikát (plaintext)"),
|
||||||
|
parts.outKey,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
identityContainer.Refresh()
|
identityContainer.Refresh()
|
||||||
if parts.showQR {
|
if parts.showQR {
|
||||||
@ -579,64 +589,122 @@ func buttonTile(btns ...fyne.CanvasObject) fyne.CanvasObject {
|
|||||||
// --- Contacts UI ---
|
// --- Contacts UI ---
|
||||||
|
|
||||||
func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
||||||
|
// Data + filtering
|
||||||
|
var all []Contact
|
||||||
|
var filtered []Contact
|
||||||
|
selected := -1
|
||||||
|
load := func() {
|
||||||
|
items, _ := svc.ListContacts()
|
||||||
|
all = items
|
||||||
|
filtered = items
|
||||||
|
}
|
||||||
|
applyFilter := func(q string) {
|
||||||
|
if q == "" {
|
||||||
|
filtered = all
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lower := strings.ToLower(q)
|
||||||
|
tmp := make([]Contact, 0, len(all))
|
||||||
|
for _, c := range all {
|
||||||
|
if strings.Contains(strings.ToLower(c.Name), lower) || strings.Contains(strings.ToLower(c.Cert), lower) {
|
||||||
|
tmp = append(tmp, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filtered = tmp
|
||||||
|
}
|
||||||
|
|
||||||
|
// Left list
|
||||||
list := widget.NewList(
|
list := widget.NewList(
|
||||||
func() int { items, _ := svc.ListContacts(); return len(items) },
|
func() int { 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) {
|
||||||
items, _ := svc.ListContacts()
|
if i >= 0 && int(i) < len(filtered) {
|
||||||
if i >= 0 && i < len(items) {
|
o.(*widget.Label).SetText(filtered[i].Name)
|
||||||
o.(*widget.Label).SetText(items[i].Name)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
var itemsCache []Contact
|
|
||||||
selected := -1
|
// Right detail form
|
||||||
refresh := func() {
|
|
||||||
items, _ := svc.ListContacts()
|
|
||||||
itemsCache = items
|
|
||||||
list.Refresh()
|
|
||||||
}
|
|
||||||
list.OnSelected = func(id widget.ListItemID) {
|
|
||||||
selected = int(id)
|
|
||||||
if id < 0 || id >= len(itemsCache) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c := itemsCache[id]
|
|
||||||
nameEntry := widget.NewEntry()
|
nameEntry := widget.NewEntry()
|
||||||
nameEntry.SetText(c.Name)
|
|
||||||
certEntry := widget.NewMultiLineEntry()
|
certEntry := widget.NewMultiLineEntry()
|
||||||
certEntry.SetMinRowsVisible(6)
|
certEntry.SetMinRowsVisible(8)
|
||||||
certEntry.SetText(c.Cert)
|
certEntry.Wrapping = fyne.TextWrapWord
|
||||||
form := widget.NewForm(
|
// Detail toolbar
|
||||||
widget.NewFormItem("Název", nameEntry),
|
certToolbar := widget.NewToolbar(
|
||||||
widget.NewFormItem("Certifikát / Public key", certEntry),
|
widget.NewToolbarAction(theme.ContentCopyIcon(), func() { copyClip(certEntry.Text, parts) }),
|
||||||
|
widget.NewToolbarAction(theme.ContentPasteIcon(), func() {
|
||||||
|
certEntry.SetText(fyne.CurrentApp().Clipboard().Content())
|
||||||
|
}),
|
||||||
|
widget.NewToolbarAction(theme.ContentClearIcon(), func() { certEntry.SetText("") }),
|
||||||
)
|
)
|
||||||
dialog.ShowCustomConfirm("Kontakt", "Uložit", "Zrušit", form, func(ok bool) {
|
saveBtn := widget.NewButtonWithIcon("Uložit", theme.ConfirmIcon(), func() {
|
||||||
if !ok {
|
if selected < 0 || selected >= len(filtered) {
|
||||||
return
|
// new or none selected -> create new
|
||||||
}
|
_ = svc.SaveContact(Contact{Name: nameEntry.Text, Cert: certEntry.Text})
|
||||||
|
} else {
|
||||||
|
c := filtered[selected]
|
||||||
c.Name = nameEntry.Text
|
c.Name = nameEntry.Text
|
||||||
c.Cert = certEntry.Text
|
c.Cert = certEntry.Text
|
||||||
_ = svc.SaveContact(c)
|
_ = svc.SaveContact(c)
|
||||||
refresh()
|
|
||||||
}, fyne.CurrentApp().Driver().AllWindows()[0])
|
|
||||||
}
|
}
|
||||||
list.OnUnselected = func(id widget.ListItemID) { selected = -1 }
|
load()
|
||||||
|
list.Refresh()
|
||||||
|
parts.showToast("Uloženo")
|
||||||
|
})
|
||||||
|
useBtn := widget.NewButtonWithIcon("Použít ve Šifrování", theme.ConfirmIcon(), func() {
|
||||||
|
parts.peer.SetText(certEntry.Text)
|
||||||
|
parts.showToast("Kontakt vložen do Šifrování")
|
||||||
|
})
|
||||||
|
|
||||||
addFromClipboard := widget.NewButton("Přidat z clipboardu", func() {
|
detail := container.NewBorder(
|
||||||
|
container.NewVBox(
|
||||||
|
widget.NewLabel("Název"),
|
||||||
|
nameEntry,
|
||||||
|
widget.NewLabel("Certifikát / Public key"),
|
||||||
|
certToolbar,
|
||||||
|
),
|
||||||
|
container.NewHBox(layout.NewSpacer(), saveBtn, useBtn),
|
||||||
|
nil, nil,
|
||||||
|
certEntry,
|
||||||
|
)
|
||||||
|
|
||||||
|
// List selection behavior
|
||||||
|
list.OnSelected = func(id widget.ListItemID) {
|
||||||
|
idx := int(id)
|
||||||
|
if idx < 0 || idx >= len(filtered) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
selected = idx
|
||||||
|
c := filtered[idx]
|
||||||
|
nameEntry.SetText(c.Name)
|
||||||
|
certEntry.SetText(c.Cert)
|
||||||
|
}
|
||||||
|
list.OnUnselected = func(widget.ListItemID) { selected = -1 }
|
||||||
|
|
||||||
|
// Header: title, search, toolbar
|
||||||
|
search := widget.NewEntry()
|
||||||
|
search.SetPlaceHolder("Hledat…")
|
||||||
|
search.OnChanged = func(s string) { applyFilter(s); list.Refresh() }
|
||||||
|
addNew := widget.NewToolbarAction(theme.ContentAddIcon(), func() {
|
||||||
|
nameEntry.SetText("Kontakt")
|
||||||
|
certEntry.SetText("")
|
||||||
|
selected = -1
|
||||||
|
})
|
||||||
|
addFromClipboard := widget.NewToolbarAction(theme.ContentPasteIcon(), func() {
|
||||||
txt := fyne.CurrentApp().Clipboard().Content()
|
txt := fyne.CurrentApp().Clipboard().Content()
|
||||||
if txt == "" {
|
if txt == "" {
|
||||||
parts.showToast("Schází data v clipboardu")
|
parts.showToast("Schází data v clipboardu")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
name := extractCN(txt)
|
nm := extractCN(txt)
|
||||||
if name == "" {
|
if nm == "" {
|
||||||
name = "Kontakt"
|
nm = "Kontakt"
|
||||||
}
|
}
|
||||||
_ = svc.SaveContact(Contact{Name: name, Cert: txt})
|
nameEntry.SetText(nm)
|
||||||
refresh()
|
certEntry.SetText(txt)
|
||||||
|
selected = -1
|
||||||
})
|
})
|
||||||
addFromFile := widget.NewButton("Přidat ze souboru", 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 {
|
||||||
return
|
return
|
||||||
@ -644,41 +712,45 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
|||||||
defer r.Close()
|
defer r.Close()
|
||||||
b, _ := io.ReadAll(r)
|
b, _ := io.ReadAll(r)
|
||||||
txt := string(b)
|
txt := string(b)
|
||||||
name := extractCN(txt)
|
nm := extractCN(txt)
|
||||||
if name == "" {
|
if nm == "" {
|
||||||
name = "Kontakt"
|
nm = "Kontakt"
|
||||||
}
|
}
|
||||||
_ = svc.SaveContact(Contact{Name: name, Cert: txt})
|
nameEntry.SetText(nm)
|
||||||
refresh()
|
certEntry.SetText(txt)
|
||||||
|
selected = -1
|
||||||
}, fyne.CurrentApp().Driver().AllWindows()[0])
|
}, fyne.CurrentApp().Driver().AllWindows()[0])
|
||||||
fd.Show()
|
fd.Show()
|
||||||
})
|
})
|
||||||
deleteBtn := widget.NewButton("Smazat", func() {
|
deleteAction := widget.NewToolbarAction(theme.DeleteIcon(), func() {
|
||||||
sel := selected
|
if selected < 0 || selected >= len(filtered) {
|
||||||
if sel < 0 || sel >= len(itemsCache) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_ = svc.DeleteContact(itemsCache[sel].ID)
|
_ = svc.DeleteContact(filtered[selected].ID)
|
||||||
refresh()
|
load()
|
||||||
|
applyFilter(search.Text)
|
||||||
|
list.Refresh()
|
||||||
|
nameEntry.SetText("")
|
||||||
|
certEntry.SetText("")
|
||||||
|
selected = -1
|
||||||
})
|
})
|
||||||
copyBtn := widget.NewButton("Zkopírovat cert", func() {
|
copyAction := widget.NewToolbarAction(theme.ContentCopyIcon(), func() {
|
||||||
sel := selected
|
if selected < 0 || selected >= len(filtered) {
|
||||||
if sel < 0 || sel >= len(itemsCache) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
copyClip(itemsCache[sel].Cert, parts)
|
copyClip(filtered[selected].Cert, parts)
|
||||||
})
|
|
||||||
useBtn := widget.NewButton("Použít ve Šifrování", func() {
|
|
||||||
sel := selected
|
|
||||||
if sel < 0 || sel >= len(itemsCache) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
parts.peer.SetText(itemsCache[sel].Cert)
|
|
||||||
parts.showToast("Kontakt vložen do Šifrování")
|
|
||||||
})
|
})
|
||||||
|
topToolbar := widget.NewToolbar(addNew, addFromClipboard, addFromFile, widget.NewToolbarSeparator(), copyAction, deleteAction)
|
||||||
|
header := container.NewHBox(widget.NewLabelWithStyle("Kontakty", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), layout.NewSpacer(), search, topToolbar)
|
||||||
|
|
||||||
header := container.NewHBox(addFromClipboard, addFromFile, deleteBtn, copyBtn, useBtn)
|
// Split view
|
||||||
content := container.NewBorder(header, nil, nil, nil, list)
|
left := container.NewBorder(nil, nil, nil, nil, list)
|
||||||
refresh()
|
right := container.NewStack(detail)
|
||||||
return content
|
split := container.NewHSplit(left, right)
|
||||||
|
split.SetOffset(0.35)
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
load()
|
||||||
|
list.Refresh()
|
||||||
|
return container.NewBorder(header, nil, nil, nil, split)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
encrypt "fckeuspy-go/lib"
|
encrypt "fckeuspy-go/lib"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
mrand "math/rand"
|
||||||
)
|
)
|
||||||
|
|
||||||
// VaultService implementuje ServiceFacade nad SecureJSONStore.
|
// VaultService implementuje ServiceFacade nad SecureJSONStore.
|
||||||
@ -100,9 +101,9 @@ func (v *VaultService) ListContacts() ([]Contact, error) {
|
|||||||
|
|
||||||
func (v *VaultService) SaveContact(c Contact) error {
|
func (v *VaultService) SaveContact(c Contact) error {
|
||||||
list, _ := v.ListContacts()
|
list, _ := v.ListContacts()
|
||||||
// upsert by ID, if empty ID derive from CN or hash
|
// upsert by ID; if empty ID, assign a new unique random ID to avoid overwriting
|
||||||
if c.ID == "" {
|
if c.ID == "" {
|
||||||
c.ID = deriveContactID(c)
|
c.ID = generateUniqueContactID(list)
|
||||||
}
|
}
|
||||||
replaced := false
|
replaced := false
|
||||||
for i := range list {
|
for i := range list {
|
||||||
@ -144,6 +145,26 @@ func deriveContactID(c Contact) string {
|
|||||||
return fmt.Sprintf("%x", sum[:8])
|
return fmt.Sprintf("%x", sum[:8])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// generateUniqueContactID creates a short random hex ID that does not collide with existing list.
|
||||||
|
func generateUniqueContactID(existing []Contact) string {
|
||||||
|
exists := make(map[string]struct{}, len(existing))
|
||||||
|
for _, c := range existing {
|
||||||
|
exists[c.ID] = struct{}{}
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
b := make([]byte, 6)
|
||||||
|
if _, err := rand.Read(b); err != nil {
|
||||||
|
// fallback to time-based seed rand
|
||||||
|
n := mrand.Int63()
|
||||||
|
return fmt.Sprintf("%x", n)
|
||||||
|
}
|
||||||
|
id := fmt.Sprintf("%x", b)
|
||||||
|
if _, ok := exists[id]; !ok {
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func extractCN(pemText string) string {
|
func extractCN(pemText string) string {
|
||||||
block, _ := pem.Decode([]byte(pemText))
|
block, _ := pem.Decode([]byte(pemText))
|
||||||
if block == nil {
|
if block == nil {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user