675 lines
20 KiB
Go
675 lines
20 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(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
|
|
}
|