package main import ( "errors" encrypt "fckeuspy-go/lib" "image/color" "io" "os" "time" "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/storage" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" ) type uiParts struct { outKey *widget.Entry msg *widget.Entry peer *widget.Entry cipherOut *widget.Entry payload *widget.Entry plainOut *widget.Entry toastLabel *widget.Label cipherQR *canvas.Image pubQR *canvas.Image crtQR *canvas.Image showQR bool peerQR *canvas.Image showPeerQR bool payloadQR *canvas.Image 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), showQR: true, peerQR: canvas.NewImageFromImage(nil), showPeerQR: true, payloadQR: canvas.NewImageFromImage(nil), showPayloadQR: true, } p.cipherQR.FillMode = canvas.ImageFillContain p.cipherQR.SetMinSize(fyne.NewSize(220, 220)) p.pubQR.FillMode = canvas.ImageFillContain p.pubQR.SetMinSize(fyne.NewSize(200, 200)) p.crtQR.FillMode = canvas.ImageFillContain p.crtQR.SetMinSize(fyne.NewSize(200, 200)) p.peerQR.FillMode = canvas.ImageFillContain p.peerQR.SetMinSize(fyne.NewSize(200, 200)) p.payloadQR.FillMode = canvas.ImageFillContain p.payloadQR.SetMinSize(fyne.NewSize(220, 220)) p.outKey.SetPlaceHolder("Veřejný klíč / certifikát…") p.msg.SetPlaceHolder("Sem napiš zprávu…") p.peer.SetPlaceHolder("-----BEGIN PUBLIC KEY----- … nebo CERTIFICATE …") p.cipherOut.SetPlaceHolder(`{"ek":"…","n":"…","ct":"…"}`) p.payload.SetPlaceHolder(`{"ek":"…","n":"…","ct":"…"}`) p.plainOut.SetPlaceHolder("Dešifrovaná zpráva…") // Zvýšení výšky (více řádků viditelně) p.outKey.SetMinRowsVisible(6) p.peer.SetMinRowsVisible(4) p.msg.SetMinRowsVisible(5) p.cipherOut.SetMinRowsVisible(5) p.payload.SetMinRowsVisible(5) p.plainOut.SetMinRowsVisible(5) p.toastLabel.Hide() return p } func (p *uiParts) showToast(msg string) { fyne.Do(func() { p.toastLabel.SetText(msg) p.toastLabel.Show() }) time.AfterFunc(1500*time.Millisecond, func() { fyne.Do(func() { if p.toastLabel.Text == msg { // avoid race if overwritten p.toastLabel.SetText("") p.toastLabel.Hide() } }) }) } // custom fixed dark theme type simpleTheme struct{} func (simpleTheme) Color(n fyne.ThemeColorName, v fyne.ThemeVariant) color.Color { base := theme.DefaultTheme().Color(n, v) if n == theme.ColorNameBackground || n == theme.ColorNameButton { return color.NRGBA{R: 30, G: 34, B: 39, A: 255} } return base } 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) } var forceDark = true // Build key section func buildIdentityTab(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.CanvasObject { btnCopyPub := widget.NewButton("Copy public.pem", func() { copyClip(svc.PublicPEM(), parts) }) btnCopyCrt := widget.NewButton("Copy identity.crt", func() { copyClip(svc.PublicCert(), parts) }) btnShowPub := widget.NewButton("Show pub", func() { parts.outKey.SetText(svc.PublicPEM()) }) btnShowCrt := widget.NewButton("Show crt", func() { parts.outKey.SetText(svc.PublicCert()) }) btnClear := widget.NewButton("Clear", func() { parts.outKey.SetText("") }) btnPaste := widget.NewButton("Paste", func() { parts.outKey.SetText(fyne.CurrentApp().Clipboard().Content()) }) deleteBtn := widget.NewButton("Smazat identitu", func() { pwEntry := widget.NewPasswordEntry() pwEntry.SetPlaceHolder("Heslo pro potvrzení…") content := widget.NewForm(widget.NewFormItem("Heslo", pwEntry)) dialog.ShowCustomConfirm("Potvrdit smazání", "Smazat", "Zrušit", content, func(ok bool) { if !ok { return } pw := pwEntry.Text if pw == "" { dialog.NewError(errors.New("heslo je prázdné"), fyne.CurrentApp().Driver().AllWindows()[0]).Show() return } if _, err := encrypt.OpenEncryptedStore(vaultPath, pw); err != nil { dialog.NewError(errors.New("neplatné heslo"), fyne.CurrentApp().Driver().AllWindows()[0]).Show() return } if err := os.Remove(vaultPath); err != nil { dialog.NewError(err, fyne.CurrentApp().Driver().AllWindows()[0]).Show() return } fyne.CurrentApp().Quit() }, fyne.CurrentApp().Driver().AllWindows()[0]) }) makeQR := func(data string, target *canvas.Image) { if data == "" { target.Image = nil target.Refresh() return } pngBytes, err := GenerateQRPNG(data, 512) if err != nil { return } img, err := LoadPNG(pngBytes) if err != nil { return } target.Image = img target.Refresh() } updateQRImages := func() { if parts.showQR { makeQR(svc.PublicPEM(), parts.pubQR) makeQR(svc.PublicCert(), parts.crtQR) } else { parts.pubQR.Image = nil parts.pubQR.Refresh() parts.crtQR.Image = nil parts.crtQR.Refresh() } } identityContainer := container.NewVBox() toggleBtn := widget.NewButton("", nil) var rebuild func() rebuild = func() { identityContainer.Objects = nil if parts.showQR { updateQRImages() // Wrap each QR with its copy button pubBox := container.NewVBox(parts.pubQR, widget.NewButton("Copy", func() { copyClip(svc.PublicPEM(), parts) })) crtBox := container.NewVBox(parts.crtQR, widget.NewButton("Copy", func() { copyClip(svc.PublicCert(), parts) })) identityContainer.Add(container.NewGridWithColumns(2, pubBox, crtBox)) } else { // show combined text for convenience parts.outKey.SetText(svc.PublicPEM() + "\n" + svc.PublicCert()) identityContainer.Add(parts.outKey) } identityContainer.Refresh() if parts.showQR { toggleBtn.SetText("Zobrazit plaintext") } else { toggleBtn.SetText("Zobrazit QR") } } toggleBtn.OnTapped = func() { parts.showQR = !parts.showQR; rebuild() } rebuild() // Group buttons by function: data viewing vs clipboard vs destructive clipboardRow := buttonTile(btnCopyPub, btnCopyCrt, btnPaste, btnClear) viewRow := buttonTile(btnShowPub, btnShowCrt, toggleBtn) destroyRow := buttonTile(deleteBtn) group := container.NewVBox( widget.NewLabelWithStyle("Moje identita", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), viewRow, identityContainer, clipboardRow, destroyRow, ) return container.NewVScroll(group) } // Helper functions separated to avoid circular dependency with encrypt.Service // We'll adapt by passing a small facade interface if needed. type ServiceFacade interface { Encrypt(msg, peer string) (string, error) Decrypt(json string) (string, error) PublicPEM() string PublicCert() string // Contacts API (only implemented by VaultService) ListContacts() ([]Contact, error) SaveContact(c Contact) error DeleteContact(id string) error } func copyClip(s string, parts *uiParts) { fyne.CurrentApp().Clipboard().SetContent(s) parts.showToast("Zkopírováno") } // (Removed legacy buildEncryptSection and buildDecryptSection) // assembleResponsive builds split view that collapses for narrow widths // Tab: Encrypt func buildEncryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject { parts.cipherOut.Disable() peerBtns := buttonTile( widget.NewButton("Paste", func() { parts.peer.SetText(fyne.CurrentApp().Clipboard().Content()) }), widget.NewButton("Clear", func() { parts.peer.SetText("") }), widget.NewButton("Copy", func() { copyClip(parts.peer.Text, parts) }), ) peerContainer := container.NewVBox() peerToggle := widget.NewButton("", nil) var updatePeer func() updatePeerQR := func(text string) { if text == "" { parts.peerQR.Image = nil parts.peerQR.Refresh() return } pngBytes, err := GenerateQRPNG(text, 512) if err != nil { return } img, err := LoadPNG(pngBytes) if err != nil { return } parts.peerQR.Image = img parts.peerQR.Refresh() } updatePeer = func() { peerContainer.Objects = nil if parts.showPeerQR { updatePeerQR(parts.peer.Text) peerContainer.Add(container.NewVBox(parts.peerQR, widget.NewButton("Copy", func() { copyClip(parts.peer.Text, parts) }))) peerToggle.SetText("Zobrazit plaintext") } else { peerContainer.Add(parts.peer) peerToggle.SetText("Zobrazit QR") } peerContainer.Refresh() } peerToggle.OnTapped = func() { parts.showPeerQR = !parts.showPeerQR; updatePeer() } parts.peer.OnChanged = func(string) { if parts.showPeerQR { updatePeerQR(parts.peer.Text) } } updatePeer() updateQR := func(text string) { if text == "" { parts.cipherQR.Image = nil parts.cipherQR.Refresh() return } pngBytes, err := GenerateQRPNG(text, 512) if err != nil { parts.showToast("QR error") return } img, err := LoadPNG(pngBytes) if err != nil { parts.showToast("PNG err") return } parts.cipherQR.Image = img parts.cipherQR.Refresh() } encAction := func() { m := parts.msg.Text p := parts.peer.Text if m == "" || p == "" { parts.showToast("Chybí data") return } go func(msg, peer string) { res, err := svc.Encrypt(msg, peer) if err != nil { fyne.Do(func() { parts.cipherOut.SetText(""); updateQR(""); parts.showToast("Chyba") }) return } fyne.Do(func() { parts.cipherOut.SetText(res) if parts.showQR { updateQR(res) } parts.showToast("OK") }) }(m, p) } importPeerQR := func() { fd := dialog.NewFileOpen(func(r fyne.URIReadCloser, err error) { if err != nil || r == nil { return } defer r.Close() data, _ := io.ReadAll(r) img, err := LoadPNG(data) if err != nil { dialog.NewError(err, fyne.CurrentApp().Driver().AllWindows()[0]).Show() return } text, err := DecodeQR(img) if err != nil { dialog.NewError(err, fyne.CurrentApp().Driver().AllWindows()[0]).Show() return } parts.peer.SetText(text) }, fyne.CurrentApp().Driver().AllWindows()[0]) fd.SetFilter(storage.NewExtensionFileFilter([]string{".png"})) fd.Show() } outputContainer := container.NewVBox() toggleBtn := widget.NewButton("", nil) var updateMode func() updateMode = func() { outputContainer.Objects = nil if parts.showQR { updateQR(parts.cipherOut.Text) outputContainer.Add(container.NewVBox(parts.cipherQR, widget.NewButton("Copy", func() { copyClip(parts.cipherOut.Text, parts) }))) } else { outputContainer.Add(parts.cipherOut) } if parts.showQR { toggleBtn.SetText("Zobrazit plaintext") } else { toggleBtn.SetText("Zobrazit QR") } outputContainer.Refresh() } toggleBtn.OnTapped = func() { parts.showQR = !parts.showQR; updateMode() } updateMode() msgBtns := buttonTile( widget.NewButton("Clear+Paste", func() { parts.msg.SetText(""); parts.msg.SetText(fyne.CurrentApp().Clipboard().Content()) }), widget.NewButton("Encrypt", func() { encAction(); updateMode() }), widget.NewButton("Copy", func() { copyClip(parts.cipherOut.Text, parts) }), widget.NewButton("Import Key QR", importPeerQR), ) peerSection := container.NewVBox( container.NewHBox(widget.NewLabel("Veřejný klíč příjemce"), peerToggle), peerContainer, peerBtns, ) msgSection := container.NewVBox( widget.NewLabel("Zpráva"), parts.msg, msgBtns, ) outputSection := container.NewVBox( container.NewHBox(widget.NewLabel("Výstup"), toggleBtn), outputContainer, ) group := container.NewVBox( widget.NewLabelWithStyle("Šifrování", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), peerSection, msgSection, outputSection, ) return container.NewVScroll(group) } // Tab: Decrypt func buildDecryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject { parts.plainOut.Disable() decryptAction := func() { pl := parts.payload.Text if pl == "" { parts.showToast("Chybí payload") return } go func(js string) { res, err := svc.Decrypt(js) if err != nil { fyne.Do(func() { parts.plainOut.SetText(""); parts.showToast("Chyba") }) return } fyne.Do(func() { parts.plainOut.SetText(res); parts.showToast("OK") }) }(pl) } updatePayloadQR := func(text string) { if text == "" { parts.payloadQR.Image = nil parts.payloadQR.Refresh() return } pngBytes, err := GenerateQRPNG(text, 512) if err != nil { return } img, err := LoadPNG(pngBytes) if err != nil { return } parts.payloadQR.Image = img parts.payloadQR.Refresh() } importPayloadQR := func() { fd := dialog.NewFileOpen(func(r fyne.URIReadCloser, err error) { if err != nil || r == nil { return } defer r.Close() data, _ := io.ReadAll(r) img, err := LoadPNG(data) if err != nil { dialog.NewError(err, fyne.CurrentApp().Driver().AllWindows()[0]).Show() return } text, err := DecodeQR(img) if err != nil { dialog.NewError(err, fyne.CurrentApp().Driver().AllWindows()[0]).Show() return } parts.payload.SetText(text) if parts.showPayloadQR { updatePayloadQR(text) } decryptAction() }, fyne.CurrentApp().Driver().AllWindows()[0]) fd.SetFilter(storage.NewExtensionFileFilter([]string{".png"})) fd.Show() } payloadBtns := buttonTile( widget.NewButton("Paste+Decrypt", func() { clip := fyne.CurrentApp().Clipboard().Content() parts.payload.SetText(clip) if parts.showPayloadQR { updatePayloadQR(clip) } decryptAction() }), widget.NewButton("Clear", func() { parts.payload.SetText("") parts.plainOut.SetText("") if parts.showPayloadQR { updatePayloadQR("") } }), widget.NewButton("QR Import", importPayloadQR), ) payloadContainer := container.NewVBox() payloadToggle := widget.NewButton("", nil) var updateMode func() updateMode = func() { payloadContainer.Objects = nil if parts.showPayloadQR { updatePayloadQR(parts.payload.Text) payloadContainer.Add(container.NewVBox(parts.payloadQR, widget.NewButton("Copy", func() { copyClip(parts.payload.Text, parts) }))) payloadToggle.SetText("Zobrazit plaintext") } else { payloadContainer.Add(parts.payload) payloadToggle.SetText("Zobrazit QR") } payloadContainer.Refresh() } payloadToggle.OnTapped = func() { parts.showPayloadQR = !parts.showPayloadQR; updateMode() } parts.payload.OnChanged = func(string) { if parts.showPayloadQR { updatePayloadQR(parts.payload.Text) } } updateMode() plainBtns := buttonTile(widget.NewButton("Copy", func() { copyClip(parts.plainOut.Text, parts) })) payloadSection := container.NewVBox( container.NewHBox(widget.NewLabel("Payload"), payloadToggle), payloadContainer, payloadBtns, ) resultSection := container.NewVBox( widget.NewLabel("Výsledek"), parts.plainOut, plainBtns, ) group := container.NewVBox( widget.NewLabelWithStyle("Dešifrování", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), payloadSection, resultSection, ) return container.NewVScroll(group) } func buildTabbedUI(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.CanvasObject { idTab := container.NewTabItem("Identita", buildIdentityTab(parts, svc, vaultPath)) contactsTab := container.NewTabItem("Kontakty", buildContactsTab(parts, svc)) encTab := container.NewTabItem("Šifrování", buildEncryptTab(parts, svc)) decTab := container.NewTabItem("Dešifrování", buildDecryptTab(parts, svc)) tabs := container.NewAppTabs(idTab, contactsTab, encTab, decTab) tabs.SetTabLocation(container.TabLocationTop) // apply fixed dark theme once fyne.CurrentApp().Settings().SetTheme(simpleTheme{}) prefs := fyne.CurrentApp().Preferences() if prefs != nil { idx := prefs.IntWithFallback("lastTab", 0) if idx >= 0 && idx < len(tabs.Items) { tabs.SelectIndex(idx) } // only persist lastTab now } tabs.OnSelected = func(ti *container.TabItem) { if prefs != nil { prefs.SetInt("lastTab", tabs.SelectedIndex()) } } return container.NewBorder(nil, parts.toastLabel, nil, nil, tabs) } // buttonTile renders buttons above a related entry with a colored background spanning full width func buttonTile(btns ...fyne.CanvasObject) fyne.CanvasObject { if len(btns) == 0 { return widget.NewLabel("") } cols := len(btns) if cols > 3 { cols = 3 } // wrap into multiple rows if many grid := container.NewGridWithColumns(cols, btns...) bgColor := color.NRGBA{R: 240, G: 240, B: 245, A: 255} if forceDark { bgColor = color.NRGBA{R: 50, G: 54, B: 60, A: 255} } rect := canvas.NewRectangle(bgColor) rect.SetMinSize(fyne.NewSize(100, 38)) padded := container.NewPadded(grid) return container.NewStack(rect, padded) } // --- Contacts UI --- func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject { list := widget.NewList( func() int { items, _ := svc.ListContacts(); return len(items) }, func() fyne.CanvasObject { return widget.NewLabel("") }, func(i widget.ListItemID, o fyne.CanvasObject) { items, _ := svc.ListContacts() if i >= 0 && i < len(items) { o.(*widget.Label).SetText(items[i].Name) } }, ) var itemsCache []Contact selected := -1 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.SetText(c.Name) certEntry := widget.NewMultiLineEntry() certEntry.SetMinRowsVisible(6) certEntry.SetText(c.Cert) form := widget.NewForm( widget.NewFormItem("Název", nameEntry), widget.NewFormItem("Certifikát / Public key", certEntry), ) dialog.ShowCustomConfirm("Kontakt", "Uložit", "Zrušit", form, func(ok bool) { if !ok { return } c.Name = nameEntry.Text c.Cert = certEntry.Text _ = svc.SaveContact(c) refresh() }, fyne.CurrentApp().Driver().AllWindows()[0]) } list.OnUnselected = func(id widget.ListItemID) { selected = -1 } addFromClipboard := widget.NewButton("Přidat z clipboardu", func() { txt := fyne.CurrentApp().Clipboard().Content() if txt == "" { parts.showToast("Schází data v clipboardu") return } name := extractCN(txt) if name == "" { name = "Kontakt" } _ = svc.SaveContact(Contact{Name: name, Cert: txt}) refresh() }) addFromFile := widget.NewButton("Přidat ze souboru", func() { fd := dialog.NewFileOpen(func(r fyne.URIReadCloser, err error) { if err != nil || r == nil { return } defer r.Close() b, _ := io.ReadAll(r) txt := string(b) name := extractCN(txt) if name == "" { name = "Kontakt" } _ = svc.SaveContact(Contact{Name: name, Cert: txt}) refresh() }, fyne.CurrentApp().Driver().AllWindows()[0]) fd.Show() }) deleteBtn := widget.NewButton("Smazat", func() { sel := selected if sel < 0 || sel >= len(itemsCache) { return } _ = svc.DeleteContact(itemsCache[sel].ID) refresh() }) copyBtn := widget.NewButton("Zkopírovat cert", func() { sel := selected if sel < 0 || sel >= len(itemsCache) { return } copyClip(itemsCache[sel].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í") }) header := container.NewHBox(addFromClipboard, addFromFile, deleteBtn, copyBtn, useBtn) content := container.NewBorder(header, nil, nil, nil, list) refresh() return content }