799 lines
23 KiB
Go
Executable File
799 lines
23 KiB
Go
Executable File
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"errors"
|
|
encrypt "fckeuspy-go/lib"
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"image/png"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"fyne.io/fyne/v2"
|
|
"fyne.io/fyne/v2/canvas"
|
|
"fyne.io/fyne/v2/container"
|
|
"fyne.io/fyne/v2/dialog"
|
|
"fyne.io/fyne/v2/layout"
|
|
"fyne.io/fyne/v2/storage"
|
|
"fyne.io/fyne/v2/theme"
|
|
"fyne.io/fyne/v2/widget"
|
|
)
|
|
|
|
// --- Core UI model ---
|
|
type uiParts struct {
|
|
outKey, msg, peer, cipherOut, payload, plainOut *widget.Entry
|
|
toastLabel *widget.Label
|
|
cipherQR, pubQR, crtQR, peerQR, payloadQR *canvas.Image
|
|
showQR, showPeerQR, 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),
|
|
peerQR: canvas.NewImageFromImage(nil),
|
|
payloadQR: canvas.NewImageFromImage(nil),
|
|
showQR: true, showPeerQR: true, showPayloadQR: true,
|
|
}
|
|
p.cipherQR.SetMinSize(fyne.NewSize(220, 220))
|
|
p.pubQR.SetMinSize(fyne.NewSize(200, 200))
|
|
p.peerQR.SetMinSize(fyne.NewSize(200, 200))
|
|
p.payloadQR.SetMinSize(fyne.NewSize(220, 220))
|
|
p.pubQR.FillMode = canvas.ImageFillContain
|
|
p.toastLabel.Hide()
|
|
return p
|
|
}
|
|
|
|
func (p *uiParts) showToast(s string) {
|
|
fyne.Do(func() { p.toastLabel.SetText(s); p.toastLabel.Show() })
|
|
time.AfterFunc(1500*time.Millisecond, func() {
|
|
fyne.Do(func() {
|
|
if p.toastLabel.Text == s {
|
|
p.toastLabel.Hide()
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
// Theme
|
|
type simpleTheme struct{}
|
|
|
|
func (simpleTheme) Color(n fyne.ThemeColorName, v fyne.ThemeVariant) color.Color {
|
|
switch n {
|
|
case theme.ColorNameBackground:
|
|
return color.NRGBA{24, 27, 31, 255}
|
|
case theme.ColorNameDisabled:
|
|
// Make disabled text brighter for readability on dark background
|
|
return color.NRGBA{230, 233, 238, 255}
|
|
default:
|
|
return theme.DefaultTheme().Color(n, v)
|
|
}
|
|
}
|
|
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) }
|
|
|
|
// Facade interface
|
|
type ServiceFacade interface {
|
|
Encrypt(msg, peer string) (string, error)
|
|
Decrypt(json string) (string, error)
|
|
PublicPEM() string
|
|
PublicCert() string
|
|
ListContacts() ([]Contact, error)
|
|
SaveContact(c Contact) error
|
|
DeleteContact(id string) error
|
|
}
|
|
|
|
// Clipboard helpers
|
|
func copyClip(s string, parts *uiParts) {
|
|
fyne.CurrentApp().Clipboard().SetContent(s)
|
|
parts.showToast("Zkopírováno")
|
|
}
|
|
|
|
func copyImageToClipboard(img image.Image, parts *uiParts) {
|
|
if img == nil {
|
|
return
|
|
}
|
|
buf := &bytes.Buffer{}
|
|
if err := png.Encode(buf, img); err != nil {
|
|
parts.showToast("Chyba PNG")
|
|
return
|
|
}
|
|
choose := func() *exec.Cmd {
|
|
wayland := os.Getenv("WAYLAND_DISPLAY") != ""
|
|
has := func(b string) bool { _, e := exec.LookPath(b); return e == nil }
|
|
if wayland {
|
|
if has("wl-copy") {
|
|
return exec.Command("wl-copy", "--type", "image/png")
|
|
}
|
|
if has("xclip") {
|
|
return exec.Command("xclip", "-selection", "clipboard", "-t", "image/png")
|
|
}
|
|
} else {
|
|
if has("xclip") {
|
|
return exec.Command("xclip", "-selection", "clipboard", "-t", "image/png")
|
|
}
|
|
if has("wl-copy") {
|
|
return exec.Command("wl-copy", "--type", "image/png")
|
|
}
|
|
}
|
|
return nil
|
|
}()
|
|
if choose == nil {
|
|
parts.showToast("Chybí wl-copy/xclip")
|
|
return
|
|
}
|
|
stdin, _ := choose.StdinPipe()
|
|
if err := choose.Start(); err != nil {
|
|
parts.showToast("Nelze spustit")
|
|
return
|
|
}
|
|
_, _ = stdin.Write(buf.Bytes())
|
|
_ = stdin.Close()
|
|
if err := choose.Wait(); err != nil {
|
|
parts.showToast("Selhalo")
|
|
return
|
|
}
|
|
parts.showToast("QR obrázek ve schránce")
|
|
}
|
|
|
|
func readImageClipboard() (image.Image, error) {
|
|
has := func(b string) bool { _, e := exec.LookPath(b); return e == nil }
|
|
tryDecode := func(d []byte) (image.Image, bool) {
|
|
if len(d) == 0 {
|
|
return nil, false
|
|
}
|
|
if img, _, e := image.Decode(bytes.NewReader(d)); e == nil {
|
|
return img, true
|
|
}
|
|
s := strings.TrimSpace(string(d))
|
|
if strings.HasPrefix(s, "data:image") {
|
|
if p := strings.Index(s, ","); p > 0 {
|
|
s = s[p+1:]
|
|
}
|
|
}
|
|
if raw, err := base64.StdEncoding.DecodeString(s); err == nil {
|
|
if img, _, e2 := image.Decode(bytes.NewReader(raw)); e2 == nil {
|
|
return img, true
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
if os.Getenv("WAYLAND_DISPLAY") != "" && has("wl-paste") {
|
|
if types, err := exec.Command("wl-paste", "--list-types").Output(); err == nil {
|
|
pref := []string{"image/png", "image/jpeg", "image/jpg", "image/webp"}
|
|
seen := map[string]bool{}
|
|
for _, p := range pref {
|
|
seen[p] = true
|
|
}
|
|
order := append([]string{}, pref...)
|
|
for _, t := range strings.Split(string(types), "\n") {
|
|
t = strings.TrimSpace(t)
|
|
if t == "" || !strings.HasPrefix(t, "image/") {
|
|
continue
|
|
}
|
|
if !seen[t] {
|
|
order = append(order, t)
|
|
}
|
|
}
|
|
for _, t := range order {
|
|
if data, err := exec.Command("wl-paste", "--type", t).Output(); err == nil {
|
|
if img, ok := tryDecode(data); ok {
|
|
return img, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if data, err := exec.Command("wl-paste").Output(); err == nil {
|
|
if img, ok := tryDecode(data); ok {
|
|
return img, nil
|
|
}
|
|
}
|
|
}
|
|
if has("xclip") {
|
|
for _, m := range []string{"image/png", "image/jpeg", "image/jpg", "image/bmp"} {
|
|
if data, err := exec.Command("xclip", "-selection", "clipboard", "-t", m, "-o").Output(); err == nil {
|
|
if img, ok := tryDecode(data); ok {
|
|
return img, nil
|
|
}
|
|
}
|
|
}
|
|
if data, err := exec.Command("xclip", "-selection", "clipboard", "-o").Output(); err == nil {
|
|
if img, ok := tryDecode(data); ok {
|
|
return img, nil
|
|
}
|
|
}
|
|
}
|
|
return nil, errors.New("nenalezen žádný obrázek ve schránce")
|
|
}
|
|
|
|
// Identity tab
|
|
func buildIdentityTab(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.CanvasObject {
|
|
// Toolbar: choose what to encode into the QR to keep density manageable
|
|
mode := "cert" // cert, pub
|
|
options := []string{"Certifikát", "Veřejný klíč"}
|
|
chooser := widget.NewSelect(options, func(string) {})
|
|
chooser.Selected = options[0]
|
|
var update func()
|
|
chooser.OnChanged = func(v string) {
|
|
switch v {
|
|
case "Certifikát":
|
|
mode = "cert"
|
|
case "Veřejný klíč":
|
|
mode = "pub"
|
|
default:
|
|
mode = "cert"
|
|
}
|
|
update()
|
|
}
|
|
deleteBtn := widget.NewButton("Smazat identitu", func() {
|
|
pw := widget.NewPasswordEntry()
|
|
form := widget.NewForm(widget.NewFormItem("Heslo", pw))
|
|
warn := widget.NewLabel("Smazat vše?")
|
|
d := dialog.NewCustomConfirm("Potvrdit smazání", "Smazat", "Zrušit", container.NewVBox(warn, form), func(ok bool) {
|
|
if !ok {
|
|
return
|
|
}
|
|
if _, err := encrypt.OpenEncryptedStore(vaultPath, pw.Text); err != nil {
|
|
dialog.NewError(errors.New("neplatné heslo"), fyne.CurrentApp().Driver().AllWindows()[0]).Show()
|
|
return
|
|
}
|
|
_ = os.Remove(vaultPath)
|
|
fyne.CurrentApp().Quit()
|
|
}, fyne.CurrentApp().Driver().AllWindows()[0])
|
|
d.Resize(fyne.NewSize(420, 200))
|
|
d.Show()
|
|
})
|
|
// Keep button minimal; align right
|
|
deleteRow := container.NewHBox(layout.NewSpacer(), deleteBtn)
|
|
makeQR := func(data string, target *canvas.Image) {
|
|
if data == "" {
|
|
target.Image = nil
|
|
target.Refresh()
|
|
return
|
|
}
|
|
if b, err := GenerateQRPNG(data, 512); err == nil {
|
|
if im, err2 := LoadPNG(b); err2 == nil {
|
|
target.Image = im
|
|
target.Refresh()
|
|
}
|
|
}
|
|
}
|
|
update = func() {
|
|
if parts.showQR {
|
|
var text string
|
|
switch mode {
|
|
case "pub":
|
|
text = strings.TrimSpace(svc.PublicPEM())
|
|
default: // cert
|
|
text = strings.TrimSpace(svc.PublicCert())
|
|
}
|
|
makeQR(text, parts.pubQR)
|
|
} else {
|
|
parts.pubQR.Image = nil
|
|
parts.pubQR.Refresh()
|
|
}
|
|
}
|
|
update()
|
|
saveQR := widget.NewButtonWithIcon("Uložit QR", theme.DocumentSaveIcon(), func() {
|
|
if parts.pubQR.Image == nil {
|
|
return
|
|
}
|
|
win := fyne.CurrentApp().Driver().AllWindows()[0]
|
|
img := parts.pubQR.Image
|
|
fd := dialog.NewFileSave(func(wc fyne.URIWriteCloser, err error) {
|
|
if err != nil || wc == nil {
|
|
return
|
|
}
|
|
defer wc.Close()
|
|
_ = png.Encode(wc, img)
|
|
parts.showToast("QR uložen")
|
|
}, win)
|
|
fd.SetFileName("identity_qr.png")
|
|
fd.Show()
|
|
})
|
|
qrRow := container.NewHBox(
|
|
widget.NewLabel("Obsah QR"),
|
|
chooser,
|
|
layout.NewSpacer(),
|
|
widget.NewButtonWithIcon("Kopírovat jako obrázek", theme.ContentPasteIcon(), func() { copyImageToClipboard(parts.pubQR.Image, parts) }),
|
|
saveQR,
|
|
)
|
|
box := container.NewVBox(qrRow, container.NewCenter(parts.pubQR))
|
|
return container.NewVScroll(container.NewVBox(container.NewHBox(widget.NewLabelWithStyle("Moje identita", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), layout.NewSpacer()), box, deleteRow))
|
|
}
|
|
|
|
// Decrypt tab
|
|
func buildDecryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
|
parts.plainOut.Disable()
|
|
parts.plainOut.Wrapping = fyne.TextWrapWord
|
|
parts.plainOut.SetMinRowsVisible(12)
|
|
parts.payloadQR.FillMode = canvas.ImageFillContain
|
|
parts.payloadQR.SetMinSize(fyne.NewSize(260, 260))
|
|
|
|
decrypt := func(text string) {
|
|
trimmed := strings.TrimSpace(text)
|
|
if trimmed == "" {
|
|
parts.plainOut.SetText("")
|
|
return
|
|
}
|
|
go func(j string) {
|
|
res, err := svc.Decrypt(j)
|
|
if err != nil {
|
|
fyne.Do(func() {
|
|
parts.plainOut.SetText("")
|
|
parts.showToast("Chyba dešifrování")
|
|
})
|
|
return
|
|
}
|
|
fyne.Do(func() { parts.plainOut.SetText(res) })
|
|
}(trimmed)
|
|
}
|
|
|
|
setPayload := func(cipher string, img image.Image) {
|
|
parts.payloadQR.Image = img
|
|
parts.payloadQR.Refresh()
|
|
decrypt(cipher)
|
|
}
|
|
|
|
decodeFromImage := func(img image.Image) {
|
|
if img == nil {
|
|
parts.showToast("Žádný QR obrázek")
|
|
return
|
|
}
|
|
txt, err := DecodeQR(img)
|
|
if err != nil {
|
|
bounds := img.Bounds()
|
|
inv := image.NewRGBA(bounds)
|
|
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
|
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
|
r, g, b, a := img.At(x, y).RGBA()
|
|
inv.Set(x, y, color.RGBA{uint8(255 - r/257), uint8(255 - g/257), uint8(255 - b/257), uint8(a / 257)})
|
|
}
|
|
}
|
|
if txt2, err2 := DecodeQR(inv); err2 == nil {
|
|
setPayload(txt2, img)
|
|
parts.showToast("Načteno z invert QR")
|
|
return
|
|
}
|
|
parts.showToast("QR nenalezen: " + err.Error())
|
|
return
|
|
}
|
|
setPayload(txt, img)
|
|
parts.showToast("Načteno z QR")
|
|
}
|
|
|
|
pasteQRBtn := widget.NewButtonWithIcon("Vložit ze schránky", theme.ContentPasteIcon(), func() {
|
|
img, err := readImageClipboard()
|
|
if err != nil {
|
|
parts.showToast("Chyba schránky: " + err.Error())
|
|
return
|
|
}
|
|
decodeFromImage(img)
|
|
})
|
|
|
|
openQRBtn := widget.NewButtonWithIcon("Otevřít obrázek", theme.FolderOpenIcon(), func() {
|
|
win := fyne.CurrentApp().Driver().AllWindows()[0]
|
|
fd := dialog.NewFileOpen(func(rc fyne.URIReadCloser, err error) {
|
|
if err != nil || rc == nil {
|
|
return
|
|
}
|
|
defer rc.Close()
|
|
data, readErr := io.ReadAll(rc)
|
|
if readErr != nil {
|
|
parts.showToast("Chyba čtení souboru")
|
|
return
|
|
}
|
|
img, _, decErr := image.Decode(bytes.NewReader(data))
|
|
if decErr != nil {
|
|
parts.showToast("Neplatný obrázek: " + decErr.Error())
|
|
return
|
|
}
|
|
decodeFromImage(img)
|
|
}, win)
|
|
fd.SetFilter(storage.NewExtensionFileFilter([]string{".png", ".jpg", ".jpeg"}))
|
|
fd.Show()
|
|
})
|
|
|
|
clearBtn := widget.NewButtonWithIcon("Vymazat", theme.ContentClearIcon(), func() {
|
|
parts.payloadQR.Image = nil
|
|
parts.payloadQR.Refresh()
|
|
parts.plainOut.SetText("")
|
|
parts.showToast("Vymazáno")
|
|
})
|
|
|
|
// Align buttons to the right by placing spacer first
|
|
toolbar := container.NewHBox(layout.NewSpacer(), pasteQRBtn, openQRBtn, clearBtn)
|
|
|
|
copyDecBtn := widget.NewButtonWithIcon("Kopírovat zprávu", theme.ContentCopyIcon(), func() {
|
|
if strings.TrimSpace(parts.plainOut.Text) != "" {
|
|
copyClip(parts.plainOut.Text, parts)
|
|
}
|
|
})
|
|
return container.NewVBox(
|
|
widget.NewLabelWithStyle("Dešifrování", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
|
toolbar,
|
|
container.NewHBox(parts.payloadQR, layout.NewSpacer()),
|
|
container.NewHBox(widget.NewLabel("Výsledek"), layout.NewSpacer(), copyDecBtn),
|
|
parts.plainOut,
|
|
)
|
|
}
|
|
|
|
// Per-contact encryption popup (QR-only output)
|
|
func openEncryptPopup(parts *uiParts, svc ServiceFacade, ct Contact) {
|
|
msgEntry := widget.NewMultiLineEntry()
|
|
msgEntry.SetMinRowsVisible(6)
|
|
msgEntry.Wrapping = fyne.TextWrapWord
|
|
status := widget.NewLabel("Zadej zprávu…")
|
|
qrImg := canvas.NewImageFromImage(nil)
|
|
qrImg.FillMode = canvas.ImageFillContain
|
|
qrImg.SetMinSize(fyne.NewSize(300, 300))
|
|
updateQR := func(text string) {
|
|
if strings.TrimSpace(text) == "" {
|
|
qrImg.Image = nil
|
|
qrImg.Refresh()
|
|
return
|
|
}
|
|
if b, err := GenerateQRPNG(text, 512); err == nil {
|
|
if im, err2 := LoadPNG(b); err2 == nil {
|
|
qrImg.Image = im
|
|
qrImg.Refresh()
|
|
}
|
|
}
|
|
}
|
|
win := fyne.CurrentApp().Driver().AllWindows()[0]
|
|
doEncrypt := func() {
|
|
m := strings.TrimSpace(msgEntry.Text)
|
|
fyne.Do(func() {
|
|
if m == "" {
|
|
status.SetText("Zpráva je prázdná")
|
|
updateQR("")
|
|
return
|
|
}
|
|
status.SetText("Šifruji…")
|
|
})
|
|
if m == "" {
|
|
return
|
|
}
|
|
go func(txt string) {
|
|
res, err := svc.Encrypt(txt, ct.Cert)
|
|
if err != nil {
|
|
fyne.Do(func() { status.SetText("Chyba: " + err.Error()) })
|
|
return
|
|
}
|
|
fyne.Do(func() { updateQR(res); status.SetText("Hotovo") })
|
|
}(m)
|
|
}
|
|
var tmr *time.Timer
|
|
msgEntry.OnChanged = func(string) {
|
|
if tmr != nil {
|
|
tmr.Stop()
|
|
}
|
|
tmr = time.AfterFunc(300*time.Millisecond, doEncrypt)
|
|
}
|
|
copyQRBtn := widget.NewButton("Kopírovat QR", func() { copyImageToClipboard(qrImg.Image, parts) })
|
|
saveQRBtn := widget.NewButton("Uložit QR", func() {
|
|
if qrImg.Image == nil {
|
|
return
|
|
}
|
|
img := qrImg.Image
|
|
fd := dialog.NewFileSave(func(wc fyne.URIWriteCloser, err error) {
|
|
if err != nil || wc == nil {
|
|
return
|
|
}
|
|
defer wc.Close()
|
|
_ = png.Encode(wc, img)
|
|
status.SetText("QR uložen")
|
|
}, win)
|
|
fd.SetFileName("message_qr.png")
|
|
fd.Show()
|
|
})
|
|
content := container.NewVBox(widget.NewLabel("Zpráva"), msgEntry, widget.NewSeparator(), container.NewHBox(widget.NewLabel("QR kód"), layout.NewSpacer(), copyQRBtn, saveQRBtn), qrImg, status)
|
|
title := ct.Name
|
|
if title == "" {
|
|
title = "(bez názvu)"
|
|
}
|
|
if cn := extractCN(ct.Cert); cn != "" && !strings.Contains(title, cn) {
|
|
title = fmt.Sprintf("%s (%s)", title, cn)
|
|
}
|
|
dlg := dialog.NewCustom(fmt.Sprintf("Poslat zprávu: %s", title), "Zavřít", content, win)
|
|
dlg.Resize(fyne.NewSize(640, 520))
|
|
dlg.Show()
|
|
}
|
|
|
|
// Contacts tab with QR-only popup
|
|
func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
|
var all, filtered []Contact
|
|
load := func() { items, _ := svc.ListContacts(); all = items; filtered = items }
|
|
apply := func(q string) {
|
|
if q == "" {
|
|
filtered = all
|
|
return
|
|
}
|
|
low := strings.ToLower(q)
|
|
tmp := make([]Contact, 0, len(all))
|
|
for _, c := range all {
|
|
if strings.Contains(strings.ToLower(c.Name), low) || strings.Contains(strings.ToLower(c.Cert), low) {
|
|
tmp = append(tmp, c)
|
|
}
|
|
}
|
|
filtered = tmp
|
|
}
|
|
makeDefault := func() string {
|
|
base := "Nový kontakt"
|
|
exists := false
|
|
maxN := 1
|
|
for _, c := range all {
|
|
if c.Name == base {
|
|
exists = true
|
|
}
|
|
if strings.HasPrefix(c.Name, base+" ") {
|
|
var n int
|
|
if _, err := fmt.Sscanf(c.Name, "Nový kontakt %d", &n); err == nil && n >= maxN {
|
|
maxN = n + 1
|
|
}
|
|
}
|
|
}
|
|
if !exists {
|
|
return base
|
|
}
|
|
return fmt.Sprintf("%s %d", base, maxN)
|
|
}
|
|
search := widget.NewEntry()
|
|
var list *widget.List
|
|
openPopup := func(existing *Contact) {
|
|
nameEntry := widget.NewEntry()
|
|
if existing != nil {
|
|
nameEntry.SetText(existing.Name)
|
|
} else {
|
|
nameEntry.SetText(makeDefault())
|
|
}
|
|
var certValue string
|
|
if existing != nil {
|
|
certValue = existing.Cert
|
|
}
|
|
qrImg := canvas.NewImageFromImage(nil)
|
|
qrImg.FillMode = canvas.ImageFillContain
|
|
qrImg.SetMinSize(fyne.NewSize(300, 300))
|
|
updateQR := func() {
|
|
if strings.TrimSpace(certValue) == "" {
|
|
qrImg.Image = nil
|
|
qrImg.Refresh()
|
|
return
|
|
}
|
|
if b, err := GenerateQRPNG(certValue, 512); err == nil {
|
|
if im, err2 := LoadPNG(b); err2 == nil {
|
|
qrImg.Image = im
|
|
qrImg.Refresh()
|
|
}
|
|
}
|
|
}
|
|
updateQR()
|
|
pasteQR := widget.NewToolbarAction(theme.ContentPasteIcon(), func() {
|
|
img, err := readImageClipboard()
|
|
if err != nil {
|
|
parts.showToast("Chyba čtení schránky: " + err.Error())
|
|
return
|
|
}
|
|
if img == nil {
|
|
parts.showToast("Žádný QR obrázek")
|
|
return
|
|
}
|
|
|
|
txt, decErr := DecodeQR(img)
|
|
if decErr != nil {
|
|
bounds := img.Bounds()
|
|
inv := image.NewRGBA(bounds)
|
|
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
|
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
|
r, g, b, a := img.At(x, y).RGBA()
|
|
inv.Set(x, y, color.RGBA{uint8(255 - r/257), uint8(255 - g/257), uint8(255 - b/257), uint8(a / 257)})
|
|
}
|
|
}
|
|
if txt2, err2 := DecodeQR(inv); err2 == nil {
|
|
certValue = txt2
|
|
updateQR()
|
|
parts.showToast("Načteno z invert QR")
|
|
return
|
|
}
|
|
debugDir := "qr_debug"
|
|
_ = os.MkdirAll(debugDir, 0o755)
|
|
fp := filepath.Join(debugDir, fmt.Sprintf("qr_clip_%d.png", time.Now().UnixNano()))
|
|
if f, e := os.Create(fp); e == nil {
|
|
_ = png.Encode(f, img)
|
|
_ = f.Close()
|
|
parts.showToast("QR nenalezen: " + decErr.Error() + " (" + fp + ")")
|
|
} else {
|
|
parts.showToast("QR nenalezen: " + decErr.Error())
|
|
}
|
|
return
|
|
}
|
|
certValue = txt
|
|
updateQR()
|
|
parts.showToast("Načteno z QR")
|
|
})
|
|
openImg := widget.NewToolbarAction(theme.FolderOpenIcon(), func() {
|
|
win := fyne.CurrentApp().Driver().AllWindows()[0]
|
|
fd := dialog.NewFileOpen(func(rc fyne.URIReadCloser, err error) {
|
|
if err != nil || rc == nil {
|
|
return
|
|
}
|
|
defer rc.Close()
|
|
data, _ := io.ReadAll(rc)
|
|
img, _, e2 := image.Decode(bytes.NewReader(data))
|
|
if e2 != nil {
|
|
parts.showToast("Neplatný obrázek: " + e2.Error())
|
|
return
|
|
}
|
|
|
|
txt, e3 := DecodeQR(img)
|
|
if e3 != nil {
|
|
parts.showToast("QR nenalezeno: " + e3.Error())
|
|
return
|
|
}
|
|
certValue = txt
|
|
updateQR()
|
|
parts.showToast("Načteno z QR")
|
|
}, win)
|
|
fd.SetFilter(storage.NewExtensionFileFilter([]string{".png", ".jpg", ".jpeg"}))
|
|
fd.Show()
|
|
})
|
|
clearAct := widget.NewToolbarAction(theme.ContentClearIcon(), func() { certValue = ""; updateQR() })
|
|
toolbar := widget.NewToolbar(pasteQR, openImg, clearAct)
|
|
win := fyne.CurrentApp().Driver().AllWindows()[0]
|
|
var popup dialog.Dialog
|
|
save := func(useEncrypt bool) {
|
|
name := strings.TrimSpace(nameEntry.Text)
|
|
cert := strings.TrimSpace(certValue)
|
|
if cert == "" {
|
|
parts.showToast("Chybí cert")
|
|
return
|
|
}
|
|
cn := extractCN(cert)
|
|
ask := cn != "" && (name == "" || name == "Kontakt" || name == "Nový kontakt" || name != cn)
|
|
proceed := func(final string) {
|
|
if final == "" || final == "Kontakt" || final == "Nový kontakt" {
|
|
final = makeDefault()
|
|
}
|
|
if existing == nil {
|
|
_ = svc.SaveContact(Contact{Name: final, Cert: cert})
|
|
} else {
|
|
c := *existing
|
|
c.Name = final
|
|
c.Cert = cert
|
|
_ = svc.SaveContact(c)
|
|
}
|
|
load()
|
|
apply(strings.TrimSpace(search.Text))
|
|
list.Refresh()
|
|
parts.showToast("Uloženo")
|
|
if useEncrypt {
|
|
parts.peer.SetText(cert)
|
|
}
|
|
if popup != nil {
|
|
popup.Hide()
|
|
}
|
|
}
|
|
if ask {
|
|
entry := widget.NewEntry()
|
|
entry.SetText(name)
|
|
content := container.NewVBox(
|
|
widget.NewLabel(fmt.Sprintf("Common Name nalezen v certifikátu: %s", cn)),
|
|
widget.NewLabel("Chcete použít CN jako název, nebo jej upravit?"),
|
|
entry,
|
|
)
|
|
dialog.NewCustomConfirm("Název kontaktu", "Použít CN", "Uložit", content, func(ok bool) {
|
|
if ok {
|
|
proceed(cn)
|
|
return
|
|
}
|
|
proceed(strings.TrimSpace(entry.Text))
|
|
}, win).Show()
|
|
return
|
|
}
|
|
proceed(name)
|
|
}
|
|
delBtn := widget.NewButtonWithIcon("Smazat", theme.DeleteIcon(), func() {
|
|
if existing == nil {
|
|
popup.Hide()
|
|
return
|
|
}
|
|
dialog.NewConfirm("Smazat", "Opravdu smazat?", func(ok bool) {
|
|
if !ok {
|
|
return
|
|
}
|
|
_ = svc.DeleteContact(existing.ID)
|
|
load()
|
|
apply(strings.TrimSpace(search.Text))
|
|
list.Refresh()
|
|
popup.Hide()
|
|
parts.showToast("Smazáno")
|
|
}, win).Show()
|
|
})
|
|
saveBtn := widget.NewButton("Uložit", func() { save(false) })
|
|
row := container.NewHBox(layout.NewSpacer(), saveBtn, delBtn, layout.NewSpacer())
|
|
title := "Nový kontakt"
|
|
if existing != nil {
|
|
title = "Upravit kontakt"
|
|
}
|
|
// manual entry area below QR for fallback or direct edit
|
|
popup = dialog.NewCustom(title, "Zavřít", container.NewVBox(
|
|
widget.NewLabel("Název"), nameEntry,
|
|
widget.NewLabel("Certifikát / Public key (QR)"), toolbar, qrImg,
|
|
widget.NewSeparator(), row), win)
|
|
popup.Resize(fyne.NewSize(640, 520))
|
|
popup.Show()
|
|
}
|
|
list = widget.NewList(func() int { return len(filtered) }, func() fyne.CanvasObject {
|
|
lbl := widget.NewLabel("")
|
|
msg := widget.NewButton("Zpráva", nil)
|
|
edit := widget.NewButton("Upravit", nil)
|
|
msg.Importance = widget.LowImportance
|
|
edit.Importance = widget.LowImportance
|
|
return container.NewBorder(nil, nil, lbl, container.NewHBox(msg, edit))
|
|
}, func(i widget.ListItemID, o fyne.CanvasObject) {
|
|
if int(i) < 0 || int(i) >= len(filtered) {
|
|
return
|
|
}
|
|
c := filtered[i]
|
|
row := o.(*fyne.Container)
|
|
lbl := row.Objects[0].(*widget.Label)
|
|
btnBox := row.Objects[1].(*fyne.Container)
|
|
msgBtn := btnBox.Objects[0].(*widget.Button)
|
|
editBtn := btnBox.Objects[1].(*widget.Button)
|
|
name := c.Name
|
|
if name == "" {
|
|
name = "(bez názvu)"
|
|
}
|
|
if cn := extractCN(c.Cert); cn != "" {
|
|
lbl.SetText(fmt.Sprintf("%s (%s)", name, cn))
|
|
} else {
|
|
lbl.SetText(name)
|
|
}
|
|
msgBtn.OnTapped = func() { openEncryptPopup(parts, svc, c) }
|
|
editBtn.OnTapped = func() {
|
|
var ptr *Contact
|
|
for i := range all {
|
|
if all[i].ID == c.ID {
|
|
ptr = &all[i]
|
|
break
|
|
}
|
|
}
|
|
openPopup(ptr)
|
|
}
|
|
})
|
|
search.SetPlaceHolder("Hledat…")
|
|
search.OnChanged = func(s string) { apply(s); list.Refresh() }
|
|
addBtn := widget.NewButtonWithIcon("Přidat", theme.ContentAddIcon(), func() { openPopup(nil) })
|
|
header := container.NewHBox(widget.NewLabelWithStyle("Kontakty", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), layout.NewSpacer(), addBtn)
|
|
load()
|
|
list.Refresh()
|
|
return container.NewBorder(header, nil, nil, nil, container.NewBorder(search, nil, nil, nil, list))
|
|
}
|
|
|
|
func buildTabbedUI(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.CanvasObject {
|
|
tabs := container.NewAppTabs(
|
|
container.NewTabItem("Identita", buildIdentityTab(parts, svc, vaultPath)),
|
|
container.NewTabItem("Kontakty", buildContactsTab(parts, svc)),
|
|
container.NewTabItem("Dešifrování", buildDecryptTab(parts, svc)),
|
|
)
|
|
fyne.CurrentApp().Settings().SetTheme(simpleTheme{})
|
|
return container.NewBorder(nil, parts.toastLabel, nil, nil, tabs)
|
|
}
|