fckeuspy-go/ui.go
2025-09-22 19:42:23 +02:00

535 lines
16 KiB
Go

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(260, 260))
p.pubQR.FillMode = canvas.ImageFillContain
p.pubQR.SetMinSize(fyne.NewSize(220, 220))
p.crtQR.FillMode = canvas.ImageFillContain
p.crtQR.SetMinSize(fyne.NewSize(220, 220))
p.peerQR.FillMode = canvas.ImageFillContain
p.peerQR.SetMinSize(fyne.NewSize(220, 220))
p.payloadQR.FillMode = canvas.ImageFillContain
p.payloadQR.SetMinSize(fyne.NewSize(260, 260))
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(10)
p.peer.SetMinRowsVisible(6)
p.msg.SetMinRowsVisible(8)
p.cipherOut.SetMinRowsVisible(8)
p.payload.SetMinRowsVisible(8)
p.plainOut.SetMinRowsVisible(8)
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()
tileIdentity := buttonTile(btnCopyPub, btnCopyCrt, btnPaste, btnShowPub, btnShowCrt, btnClear, deleteBtn, toggleBtn)
group := container.NewVBox(
widget.NewLabelWithStyle("Moje identita", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
tileIdentity,
identityContainer,
)
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
}
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),
)
group := container.NewVBox(
widget.NewLabelWithStyle("Šifrování", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
container.NewHBox(widget.NewLabel("Veřejný klíč příjemce"), peerToggle), peerBtns, peerContainer,
widget.NewLabel("Zpráva"), msgBtns, parts.msg,
container.NewHBox(widget.NewLabel("Výstup"), toggleBtn),
outputContainer,
)
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) }))
group := container.NewVBox(
widget.NewLabelWithStyle("Dešifrování", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
container.NewHBox(widget.NewLabel("Payload"), payloadToggle), payloadBtns, payloadContainer,
widget.NewLabel("Výsledek"), plainBtns, parts.plainOut,
)
return container.NewVScroll(group)
}
func buildTabbedUI(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.CanvasObject {
idTab := container.NewTabItem("Identita", buildIdentityTab(parts, svc, vaultPath))
encTab := container.NewTabItem("Šifrování", buildEncryptTab(parts, svc))
decTab := container.NewTabItem("Dešifrování", buildDecryptTab(parts, svc))
tabs := container.NewAppTabs(idTab, 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)
}