feature/ui - zatim ne uplne uhlazena ale celkem pouzitelna appka #1
354
fyne_ui.go
354
fyne_ui.go
@ -1,11 +1,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
encrypt "fckeuspy-go/lib"
|
encrypt "fckeuspy-go/lib"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
"fyne.io/fyne/v2/app"
|
"fyne.io/fyne/v2/app"
|
||||||
@ -14,6 +15,48 @@ import (
|
|||||||
"fyne.io/fyne/v2/widget"
|
"fyne.io/fyne/v2/widget"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// strengthRowLayout rozdělí dostupnou šířku v poměru ~70:30 (bar+label : tlačítko)
|
||||||
|
type strengthRowLayout struct{}
|
||||||
|
|
||||||
|
func (l *strengthRowLayout) Layout(objs []fyne.CanvasObject, size fyne.Size) {
|
||||||
|
if len(objs) != 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
left := objs[0]
|
||||||
|
right := objs[1]
|
||||||
|
leftW := float32(float64(size.Width) * 0.70)
|
||||||
|
if leftW < 10 {
|
||||||
|
leftW = size.Width / 2
|
||||||
|
}
|
||||||
|
left.Resize(fyne.NewSize(leftW, size.Height))
|
||||||
|
left.Move(fyne.NewPos(0, 0))
|
||||||
|
right.Resize(fyne.NewSize(size.Width-leftW, size.Height))
|
||||||
|
right.Move(fyne.NewPos(leftW, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *strengthRowLayout) MinSize(objs []fyne.CanvasObject) fyne.Size {
|
||||||
|
var w, h float32
|
||||||
|
if len(objs) == 2 {
|
||||||
|
m1 := objs[0].MinSize()
|
||||||
|
m2 := objs[1].MinSize()
|
||||||
|
w = m1.Width + m2.Width
|
||||||
|
if m1.Height > m2.Height {
|
||||||
|
h = m1.Height
|
||||||
|
} else {
|
||||||
|
h = m2.Height
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, o := range objs {
|
||||||
|
ms := o.MinSize()
|
||||||
|
w += ms.Width
|
||||||
|
if ms.Height > h {
|
||||||
|
h = ms.Height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fyne.NewSize(w, h)
|
||||||
|
}
|
||||||
|
|
||||||
func NewUI() (stprageDir string, window fyne.Window) {
|
func NewUI() (stprageDir string, window fyne.Window) {
|
||||||
// App + storage dir
|
// App + storage dir
|
||||||
a := app.NewWithID("fckeuspy")
|
a := app.NewWithID("fckeuspy")
|
||||||
@ -41,18 +84,22 @@ func NewUI() (stprageDir string, window fyne.Window) {
|
|||||||
return filepath.Join(base, ""), w
|
return filepath.Join(base, ""), w
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShowPasswordVaultDialog zobrazí dialog pro vytvoření nebo otevření trezoru.
|
// ShowPasswordVaultDialog zobrazí moderní odemykací / registrační dialog s countdown animací při špatném hesle.
|
||||||
// Zatím pouze skeleton: vrací heslo přes callback, reálné volání CreateEncryptedStore/OpenEncryptedStore mimo.
|
// Callback je zavolán pouze při úspěchu (ne při neplatném hesle). Zrušení vrací password="".
|
||||||
func ShowPasswordVaultDialog(w fyne.Window, vaultPath string, onResult func(create bool, password string)) {
|
func ShowPasswordVaultDialog(w fyne.Window, vaultPath string, onResult func(create bool, password string)) {
|
||||||
// Unlock tab
|
modeCreate := false
|
||||||
|
if _, err := os.Stat(vaultPath); err != nil {
|
||||||
|
modeCreate = true
|
||||||
|
}
|
||||||
|
statusLabel := widget.NewLabel("")
|
||||||
|
statusLabel.Wrapping = fyne.TextWrapWord
|
||||||
|
statusLabel.Hide()
|
||||||
|
// Unlock
|
||||||
unlockPw := widget.NewPasswordEntry()
|
unlockPw := widget.NewPasswordEntry()
|
||||||
unlockPw.SetPlaceHolder("Heslo…")
|
unlockPw.SetPlaceHolder("Heslo…")
|
||||||
unlockForm := widget.NewForm(
|
unlockBtn := widget.NewButton("Odemknout", nil)
|
||||||
widget.NewFormItem("Heslo", unlockPw),
|
unlockForm := container.NewVBox(widget.NewForm(widget.NewFormItem("Heslo", unlockPw)), unlockBtn)
|
||||||
)
|
// Create
|
||||||
unlockContent := container.NewVBox(unlockForm)
|
|
||||||
|
|
||||||
// Create tab
|
|
||||||
createPw1 := widget.NewPasswordEntry()
|
createPw1 := widget.NewPasswordEntry()
|
||||||
createPw1.SetPlaceHolder("Heslo…")
|
createPw1.SetPlaceHolder("Heslo…")
|
||||||
createPw2 := widget.NewPasswordEntry()
|
createPw2 := widget.NewPasswordEntry()
|
||||||
@ -60,111 +107,268 @@ func ShowPasswordVaultDialog(w fyne.Window, vaultPath string, onResult func(crea
|
|||||||
strengthBar := widget.NewProgressBar()
|
strengthBar := widget.NewProgressBar()
|
||||||
strengthBar.Min = 0
|
strengthBar.Min = 0
|
||||||
strengthBar.Max = 100
|
strengthBar.Max = 100
|
||||||
|
// Skryj defaultní procenta uvnitř progress baru, protože máme vlastní overlay label
|
||||||
|
strengthBar.TextFormatter = func() string { return "" }
|
||||||
strengthLabel := widget.NewLabel("")
|
strengthLabel := widget.NewLabel("")
|
||||||
updateStrength := func(pw string) {
|
updateStrength := func(pw string) {
|
||||||
score, desc := simpleScore(pw)
|
s, d := passwordStrengthScore(pw)
|
||||||
strengthBar.SetValue(float64(score))
|
strengthBar.SetValue(float64(s))
|
||||||
strengthLabel.SetText(desc)
|
strengthLabel.SetText(fmt.Sprintf("%s: %d%%", d, s))
|
||||||
}
|
}
|
||||||
createPw1.OnChanged = updateStrength
|
createPw1.OnChanged = updateStrength
|
||||||
genBtn := widget.NewButton("Generovat", func() {
|
genBtn := widget.NewButton("Generovat", func() {
|
||||||
if v, err := encrypt.GenerateRandomPassword(20); err == nil {
|
if v, err := encrypt.GenerateRandomPassword(24); err == nil {
|
||||||
createPw1.SetText(v)
|
createPw1.SetText(v)
|
||||||
createPw2.SetText(v)
|
createPw2.SetText(v)
|
||||||
updateStrength(v)
|
updateStrength(v)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
meter := container.NewVBox(strengthBar, strengthLabel)
|
createBtn := widget.NewButton("Vytvořit trezor", nil)
|
||||||
topCreate := container.NewBorder(nil, nil, nil, genBtn, meter)
|
|
||||||
createForm := widget.NewForm(
|
// Obě pole stejné šířky – žádné tlačítko uvnitř prvního řádku
|
||||||
|
form := widget.NewForm(
|
||||||
widget.NewFormItem("Heslo", createPw1),
|
widget.NewFormItem("Heslo", createPw1),
|
||||||
widget.NewFormItem("Potvrzení", createPw2),
|
widget.NewFormItem("Potvrzení", createPw2),
|
||||||
)
|
)
|
||||||
createContent := container.NewVBox(topCreate, createForm)
|
|
||||||
|
|
||||||
tabs := container.NewAppTabs(
|
// Řádek: progress bar s textem uvnitř (overlay) : tlačítko (70:30)
|
||||||
container.NewTabItem("Odemknout", unlockContent),
|
strengthLabel.Alignment = fyne.TextAlignCenter
|
||||||
container.NewTabItem("Vytvořit", createContent),
|
leftStack := container.NewStack(
|
||||||
|
strengthBar,
|
||||||
|
container.NewCenter(strengthLabel),
|
||||||
)
|
)
|
||||||
tabs.SetTabLocation(container.TabLocationTop)
|
strengthRow := container.New(&strengthRowLayout{}, leftStack, genBtn)
|
||||||
// Výběr aktivní záložky dle existence souboru
|
createForm := container.NewVBox(
|
||||||
if _, err := os.Stat(vaultPath); err != nil { // neexistuje => vytvořit
|
form,
|
||||||
tabs.SelectIndex(1)
|
strengthRow,
|
||||||
|
createBtn,
|
||||||
|
)
|
||||||
|
stack := container.NewStack(unlockForm, createForm)
|
||||||
|
showMode := func(c bool) {
|
||||||
|
modeCreate = c
|
||||||
|
if c {
|
||||||
|
unlockForm.Hide()
|
||||||
|
createForm.Show()
|
||||||
} else {
|
} else {
|
||||||
tabs.SelectIndex(0)
|
createForm.Hide()
|
||||||
|
unlockForm.Show()
|
||||||
}
|
}
|
||||||
|
statusLabel.Hide()
|
||||||
d := dialog.NewCustomConfirm("Trezor", "OK", "Zrušit", tabs, func(ok bool) {
|
}
|
||||||
if !ok {
|
showMode(modeCreate)
|
||||||
|
segment := widget.NewRadioGroup([]string{"Odemknout", "Vytvořit"}, func(val string) { showMode(val == "Vytvořit") })
|
||||||
|
segment.Horizontal = true
|
||||||
|
segment.Refresh()
|
||||||
|
if modeCreate {
|
||||||
|
segment.SetSelected("Vytvořit")
|
||||||
|
} else {
|
||||||
|
segment.SetSelected("Odemknout")
|
||||||
|
}
|
||||||
|
header := container.NewVBox(
|
||||||
|
widget.NewLabelWithStyle("🔐 Trezor", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
|
||||||
|
segment,
|
||||||
|
widget.NewSeparator(),
|
||||||
|
)
|
||||||
|
var d dialog.Dialog
|
||||||
|
completed := false
|
||||||
|
var countdownCancel chan struct{}
|
||||||
|
startCountdown := func(sec int) {
|
||||||
|
if countdownCancel != nil {
|
||||||
|
close(countdownCancel)
|
||||||
|
}
|
||||||
|
countdownCancel = make(chan struct{})
|
||||||
|
statusLabel.Show()
|
||||||
|
unlockBtn.Disable()
|
||||||
|
unlockPw.Disable()
|
||||||
|
go func() {
|
||||||
|
for i := sec; i > 0; i-- {
|
||||||
|
select {
|
||||||
|
case <-countdownCancel:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
iLocal := i
|
||||||
|
fyne.Do(func() { statusLabel.SetText(fmt.Sprintf("Neplatné heslo. Nový pokus za %d s", iLocal)) })
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
|
fyne.Do(func() { unlockBtn.Enable(); unlockPw.Enable(); unlockPw.SetText(""); statusLabel.Hide() })
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
unlockBtn.OnTapped = func() {
|
||||||
|
pw := unlockPw.Text
|
||||||
|
if pw == "" {
|
||||||
|
statusLabel.SetText("Zadejte heslo")
|
||||||
|
statusLabel.Show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := encrypt.OpenEncryptedStore(vaultPath, pw); err != nil {
|
||||||
|
statusLabel.SetText("Neplatné heslo")
|
||||||
|
statusLabel.Show()
|
||||||
|
startCountdown(3)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
completed = true
|
||||||
|
onResult(false, pw)
|
||||||
|
d.Hide()
|
||||||
|
}
|
||||||
|
createBtn.OnTapped = func() {
|
||||||
|
pw1, pw2 := createPw1.Text, createPw2.Text
|
||||||
|
if pw1 != pw2 {
|
||||||
|
statusLabel.SetText("Hesla se neshodují")
|
||||||
|
statusLabel.Show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := encrypt.ValidatePasswordForUI(pw1); err != nil {
|
||||||
|
statusLabel.SetText(err.Error())
|
||||||
|
statusLabel.Show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
completed = true
|
||||||
|
onResult(true, pw1)
|
||||||
|
d.Hide()
|
||||||
|
}
|
||||||
|
body := container.NewVBox(header, container.NewPadded(stack), statusLabel)
|
||||||
|
d = dialog.NewCustom("", "Zrušit", body, w)
|
||||||
|
d.SetOnClosed(func() {
|
||||||
|
if countdownCancel != nil {
|
||||||
|
close(countdownCancel)
|
||||||
|
}
|
||||||
|
if !completed {
|
||||||
onResult(false, "")
|
onResult(false, "")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
sel := tabs.Selected()
|
})
|
||||||
if sel != nil && sel.Text == "Vytvořit" {
|
d.Resize(fyne.NewSize(480, 320))
|
||||||
pw := createPw1.Text
|
|
||||||
if pw != createPw2.Text {
|
|
||||||
dialog.NewError(errors.New("Hesla se neshodují"), w).Show()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := encrypt.ValidatePasswordForUI(pw); err != nil {
|
|
||||||
dialog.NewError(err, w).Show()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
onResult(true, pw)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Unlock
|
|
||||||
onResult(false, unlockPw.Text)
|
|
||||||
}, w)
|
|
||||||
// Větší rozměr pro lepší přehlednost
|
|
||||||
d.Resize(fyne.NewSize(620, 440))
|
|
||||||
d.Show()
|
d.Show()
|
||||||
|
if modeCreate {
|
||||||
|
w.Canvas().Focus(createPw1)
|
||||||
|
} else {
|
||||||
|
w.Canvas().Focus(unlockPw)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// simpleScore: hrubá heuristika (0-100)
|
// passwordStrengthScore: heuristický odhad síly (0-100) s ohledem na entropii, třídy znaků a penalizace.
|
||||||
func simpleScore(pw string) (int, string) {
|
func passwordStrengthScore(pw string) (int, string) {
|
||||||
l := len(pw)
|
if pw == "" {
|
||||||
var u, lw, dg, sp int
|
return 0, "Prázdné"
|
||||||
specials := "!@#-_+="
|
}
|
||||||
|
length := len(pw)
|
||||||
|
// Charakteristiky
|
||||||
|
hasUpper, hasLower, hasDigit, hasSymbol := false, false, false, false
|
||||||
|
symbols := "!@#$%^&*()_-+=[]{}/?:.,<>|~`'\""
|
||||||
|
freq := make(map[rune]int)
|
||||||
for _, r := range pw {
|
for _, r := range pw {
|
||||||
|
freq[r]++
|
||||||
switch {
|
switch {
|
||||||
case r >= 'A' && r <= 'Z':
|
case r >= 'A' && r <= 'Z':
|
||||||
u++
|
hasUpper = true
|
||||||
case r >= 'a' && r <= 'z':
|
case r >= 'a' && r <= 'z':
|
||||||
lw++
|
hasLower = true
|
||||||
case r >= '0' && r <= '9':
|
case r >= '0' && r <= '9':
|
||||||
dg++
|
hasDigit = true
|
||||||
default:
|
default:
|
||||||
if strings.ContainsRune(specials, r) {
|
if strings.ContainsRune(symbols, r) {
|
||||||
sp++
|
hasSymbol = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cats := 0
|
classes := 0
|
||||||
if u > 0 {
|
if hasUpper {
|
||||||
cats++
|
classes++
|
||||||
}
|
}
|
||||||
if lw > 0 {
|
if hasLower {
|
||||||
cats++
|
classes++
|
||||||
}
|
}
|
||||||
if dg > 0 {
|
if hasDigit {
|
||||||
cats++
|
classes++
|
||||||
}
|
}
|
||||||
if sp > 0 {
|
if hasSymbol {
|
||||||
cats++
|
classes++
|
||||||
}
|
}
|
||||||
score := l*4 + cats*10
|
|
||||||
if score > 100 {
|
// Shannon entropie per char
|
||||||
score = 100
|
var shannon float64
|
||||||
|
for _, c := range freq {
|
||||||
|
p := float64(c) / float64(length)
|
||||||
|
shannon += -p * (log2(p))
|
||||||
}
|
}
|
||||||
|
// log2 hrubě implementujeme konverzí přes Ln, zde jednoduchá aproximace: log2(p) ~ (ln p)/0.693
|
||||||
|
// aby nebyla složitost, definujeme pomocnou funkci nahoře - zjednodušeno níže (inline hack):
|
||||||
|
// Použijeme předpočítané p^ využitím math (neimportujeme math -> krátký lookup) => nahradíme vlastní small table -> zvolíme fallback.
|
||||||
|
// Kvůli jednoduchosti: pokud shannon > 4.5 považuj za 4.5 (limit pro běžná hesla)
|
||||||
|
if shannon > 4.5 {
|
||||||
|
shannon = 4.5
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skóre komponenty
|
||||||
|
lengthScore := 0
|
||||||
|
switch {
|
||||||
|
case length >= 20:
|
||||||
|
lengthScore = 32
|
||||||
|
case length >= 16:
|
||||||
|
lengthScore = 26
|
||||||
|
case length >= 12:
|
||||||
|
lengthScore = 22
|
||||||
|
case length >= 10:
|
||||||
|
lengthScore = 18
|
||||||
|
case length >= 8:
|
||||||
|
lengthScore = 14
|
||||||
|
case length >= 6:
|
||||||
|
lengthScore = 8
|
||||||
|
default:
|
||||||
|
lengthScore = 4
|
||||||
|
}
|
||||||
|
classScore := classes * 10 // max 40
|
||||||
|
entropyScore := int((shannon / 4.5) * 28) // max ~28
|
||||||
|
|
||||||
|
// Penalizace monotónnosti & nízké diverzity
|
||||||
|
uniqueChars := len(freq)
|
||||||
|
diversityPenalty := 0
|
||||||
|
if uniqueChars <= 2 {
|
||||||
|
diversityPenalty = 25
|
||||||
|
} else if uniqueChars <= 4 {
|
||||||
|
diversityPenalty = 15
|
||||||
|
} else if uniqueChars <= 6 {
|
||||||
|
diversityPenalty = 5
|
||||||
|
}
|
||||||
|
// Sekvenční vzory
|
||||||
|
lower := strings.ToLower(pw)
|
||||||
|
for _, pat := range []string{"abc", "abcd", "qwert", "1234", "password", "heslo"} {
|
||||||
|
if strings.Contains(lower, pat) {
|
||||||
|
diversityPenalty += 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
raw := lengthScore + classScore + entropyScore - diversityPenalty
|
||||||
|
if raw < 0 {
|
||||||
|
raw = 0
|
||||||
|
}
|
||||||
|
if raw > 100 {
|
||||||
|
raw = 100
|
||||||
|
}
|
||||||
|
|
||||||
desc := "Slabé"
|
desc := "Slabé"
|
||||||
switch {
|
switch {
|
||||||
case score >= 85:
|
case raw >= 85:
|
||||||
desc = "Silné"
|
desc = "Silné"
|
||||||
case score >= 60:
|
case raw >= 65:
|
||||||
desc = "Dobré"
|
desc = "Dobré"
|
||||||
case score >= 30:
|
case raw >= 45:
|
||||||
desc = "Střední"
|
desc = "Střední"
|
||||||
|
case raw >= 25:
|
||||||
|
desc = "Nízké"
|
||||||
}
|
}
|
||||||
return score, desc
|
return raw, desc
|
||||||
|
}
|
||||||
|
|
||||||
|
// log2 pomocná aproximace (rychlá, bez importu math)
|
||||||
|
func log2(x float64) float64 {
|
||||||
|
// racionální aproximace: log2(x) ≈ ln(x)/ln(2); použijeme jednoduchý Newton pro ln
|
||||||
|
if x <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
// Polynomická aproximace pro ln v okolí 1: ln(x) ~ 2*( (y) + y^3/3 ), y=(x-1)/(x+1) (rychlý log)
|
||||||
|
y := (x - 1) / (x + 1)
|
||||||
|
y2 := y * y
|
||||||
|
ln := 2 * (y + y2*y/3)
|
||||||
|
return ln / 0.69314718056
|
||||||
}
|
}
|
||||||
|
|||||||
55
ui.go
55
ui.go
@ -134,8 +134,15 @@ func buildIdentityTab(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.
|
|||||||
deleteBtn := widget.NewButton("Smazat identitu", func() {
|
deleteBtn := widget.NewButton("Smazat identitu", func() {
|
||||||
pwEntry := widget.NewPasswordEntry()
|
pwEntry := widget.NewPasswordEntry()
|
||||||
pwEntry.SetPlaceHolder("Heslo pro potvrzení…")
|
pwEntry.SetPlaceHolder("Heslo pro potvrzení…")
|
||||||
content := widget.NewForm(widget.NewFormItem("Heslo", pwEntry))
|
warn := widget.NewRichTextFromMarkdown("**⚠ Nevratná akce**\n\nTato operace trvale smaže identitu, kontakty i uložené klíče. Pokračujte pouze pokud máte zálohu nebo opravdu chcete vše odstranit.\n")
|
||||||
dialog.ShowCustomConfirm("Potvrdit smazání", "Smazat", "Zrušit", content, func(ok bool) {
|
warn.Wrapping = fyne.TextWrapWord
|
||||||
|
form := widget.NewForm(widget.NewFormItem("Heslo", pwEntry))
|
||||||
|
content := container.NewVBox(
|
||||||
|
warn,
|
||||||
|
widget.NewSeparator(),
|
||||||
|
form,
|
||||||
|
)
|
||||||
|
d := dialog.NewCustomConfirm("Potvrdit smazání", "Smazat", "Zrušit", content, func(ok bool) {
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -154,6 +161,8 @@ func buildIdentityTab(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.
|
|||||||
}
|
}
|
||||||
fyne.CurrentApp().Quit()
|
fyne.CurrentApp().Quit()
|
||||||
}, fyne.CurrentApp().Driver().AllWindows()[0])
|
}, fyne.CurrentApp().Driver().AllWindows()[0])
|
||||||
|
d.Resize(fyne.NewSize(600, 250))
|
||||||
|
d.Show()
|
||||||
})
|
})
|
||||||
|
|
||||||
makeQR := func(data string, target *canvas.Image) {
|
makeQR := func(data string, target *canvas.Image) {
|
||||||
@ -294,7 +303,7 @@ func buildEncryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
|||||||
}
|
}
|
||||||
updatePeer()
|
updatePeer()
|
||||||
|
|
||||||
// Output section with QR/Text toggle
|
// Output section toggle (QR vs text)
|
||||||
outputContainer := container.NewVBox()
|
outputContainer := container.NewVBox()
|
||||||
outputToggleAction := widget.NewToolbarAction(theme.VisibilityOffIcon(), nil)
|
outputToggleAction := widget.NewToolbarAction(theme.VisibilityOffIcon(), nil)
|
||||||
updateQR := func(text string) {
|
updateQR := func(text string) {
|
||||||
@ -316,11 +325,11 @@ func buildEncryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
|||||||
parts.cipherQR.Image = img
|
parts.cipherQR.Image = img
|
||||||
parts.cipherQR.Refresh()
|
parts.cipherQR.Refresh()
|
||||||
}
|
}
|
||||||
updateMode := func() {
|
updateOutput := func() {
|
||||||
outputContainer.Objects = nil
|
outputContainer.Objects = nil
|
||||||
if parts.showQR {
|
if parts.showQR { // show QR mode
|
||||||
updateQR(parts.cipherOut.Text)
|
updateQR(parts.cipherOut.Text)
|
||||||
outputContainer.Add(parts.cipherQR) // copy tlačítko jen v toolbaru
|
outputContainer.Add(parts.cipherQR)
|
||||||
outputToggleAction.SetIcon(theme.VisibilityOffIcon())
|
outputToggleAction.SetIcon(theme.VisibilityOffIcon())
|
||||||
} else {
|
} else {
|
||||||
outputContainer.Add(parts.cipherOut)
|
outputContainer.Add(parts.cipherOut)
|
||||||
@ -328,8 +337,8 @@ func buildEncryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
|||||||
}
|
}
|
||||||
outputContainer.Refresh()
|
outputContainer.Refresh()
|
||||||
}
|
}
|
||||||
outputToggleAction.OnActivated = func() { parts.showQR = !parts.showQR; updateMode() }
|
outputToggleAction.OnActivated = func() { parts.showQR = !parts.showQR; updateOutput() }
|
||||||
updateMode()
|
updateOutput()
|
||||||
|
|
||||||
encAction := func() {
|
encAction := func() {
|
||||||
m := parts.msg.Text
|
m := parts.msg.Text
|
||||||
@ -346,9 +355,8 @@ func buildEncryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
|||||||
}
|
}
|
||||||
fyne.Do(func() {
|
fyne.Do(func() {
|
||||||
parts.cipherOut.SetText(res)
|
parts.cipherOut.SetText(res)
|
||||||
if parts.showQR {
|
// refresh whichever mode is active
|
||||||
updateQR(res)
|
updateOutput()
|
||||||
}
|
|
||||||
parts.showToast("OK")
|
parts.showToast("OK")
|
||||||
})
|
})
|
||||||
}(m, p)
|
}(m, p)
|
||||||
@ -421,13 +429,15 @@ func buildEncryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
|||||||
container.NewHBox(widget.NewLabel("Výstup"), layout.NewSpacer(), outputToolbar),
|
container.NewHBox(widget.NewLabel("Výstup"), layout.NewSpacer(), outputToolbar),
|
||||||
outputContainer,
|
outputContainer,
|
||||||
)
|
)
|
||||||
|
// Split: message (top) vs output (bottom) for better prostor "do spodu"
|
||||||
|
split := container.NewVSplit(msgSection, outputSection)
|
||||||
|
split.SetOffset(0.40)
|
||||||
group := container.NewVBox(
|
group := container.NewVBox(
|
||||||
widget.NewLabelWithStyle("Šifrování", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
widget.NewLabelWithStyle("Šifrování", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||||
peerSection,
|
peerSection,
|
||||||
msgSection,
|
split,
|
||||||
outputSection,
|
|
||||||
)
|
)
|
||||||
return container.NewVScroll(group)
|
return group
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tab: Decrypt
|
// Tab: Decrypt
|
||||||
@ -821,6 +831,9 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
|||||||
draft = &Contact{ID: "__draft__", Name: makeDefaultName(), Cert: ""}
|
draft = &Contact{ID: "__draft__", Name: makeDefaultName(), Cert: ""}
|
||||||
nameEntry.SetText(draft.Name)
|
nameEntry.SetText(draft.Name)
|
||||||
certEntry.SetText("")
|
certEntry.SetText("")
|
||||||
|
// auto select all text in name so user can immediately přepsat
|
||||||
|
fyne.CurrentApp().Driver().CanvasForObject(nameEntry).Focus(nameEntry)
|
||||||
|
nameEntry.TypedShortcut(&fyne.ShortcutSelectAll{})
|
||||||
// ensure filter cleared so user sees draft on top
|
// ensure filter cleared so user sees draft on top
|
||||||
searchQuery = ""
|
searchQuery = ""
|
||||||
search.SetText("")
|
search.SetText("")
|
||||||
@ -860,11 +873,21 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
|||||||
deleteBtn := widget.NewButtonWithIcon("", theme.DeleteIcon(), func() {
|
deleteBtn := widget.NewButtonWithIcon("", theme.DeleteIcon(), func() {
|
||||||
// If draft active and no real contact chosen -> cancel draft
|
// If draft active and no real contact chosen -> cancel draft
|
||||||
if draft != nil && selected == -1 {
|
if draft != nil && selected == -1 {
|
||||||
|
// remove draft and if exist some contacts immediately select first one
|
||||||
draft = nil
|
draft = nil
|
||||||
nameEntry.SetText("")
|
|
||||||
certEntry.SetText("")
|
|
||||||
list.Refresh()
|
list.Refresh()
|
||||||
updateEmptyState()
|
updateEmptyState()
|
||||||
|
if len(filtered) > 0 {
|
||||||
|
selected = 0
|
||||||
|
c := filtered[0]
|
||||||
|
nameEntry.SetText(c.Name)
|
||||||
|
certEntry.SetText(c.Cert)
|
||||||
|
list.Select(0)
|
||||||
|
} else {
|
||||||
|
selected = -1
|
||||||
|
nameEntry.SetText("")
|
||||||
|
certEntry.SetText("")
|
||||||
|
}
|
||||||
parts.showToast("Zrušeno")
|
parts.showToast("Zrušeno")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user