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
|
||||
|
||||
import (
|
||||
"errors"
|
||||
encrypt "fckeuspy-go/lib"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/app"
|
||||
@ -14,6 +15,48 @@ import (
|
||||
"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) {
|
||||
// App + storage dir
|
||||
a := app.NewWithID("fckeuspy")
|
||||
@ -41,18 +84,22 @@ func NewUI() (stprageDir string, window fyne.Window) {
|
||||
return filepath.Join(base, ""), w
|
||||
}
|
||||
|
||||
// ShowPasswordVaultDialog zobrazí dialog pro vytvoření nebo otevření trezoru.
|
||||
// Zatím pouze skeleton: vrací heslo přes callback, reálné volání CreateEncryptedStore/OpenEncryptedStore mimo.
|
||||
// ShowPasswordVaultDialog zobrazí moderní odemykací / registrační dialog s countdown animací při špatném hesle.
|
||||
// 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)) {
|
||||
// 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.SetPlaceHolder("Heslo…")
|
||||
unlockForm := widget.NewForm(
|
||||
widget.NewFormItem("Heslo", unlockPw),
|
||||
)
|
||||
unlockContent := container.NewVBox(unlockForm)
|
||||
|
||||
// Create tab
|
||||
unlockBtn := widget.NewButton("Odemknout", nil)
|
||||
unlockForm := container.NewVBox(widget.NewForm(widget.NewFormItem("Heslo", unlockPw)), unlockBtn)
|
||||
// Create
|
||||
createPw1 := widget.NewPasswordEntry()
|
||||
createPw1.SetPlaceHolder("Heslo…")
|
||||
createPw2 := widget.NewPasswordEntry()
|
||||
@ -60,111 +107,268 @@ func ShowPasswordVaultDialog(w fyne.Window, vaultPath string, onResult func(crea
|
||||
strengthBar := widget.NewProgressBar()
|
||||
strengthBar.Min = 0
|
||||
strengthBar.Max = 100
|
||||
// Skryj defaultní procenta uvnitř progress baru, protože máme vlastní overlay label
|
||||
strengthBar.TextFormatter = func() string { return "" }
|
||||
strengthLabel := widget.NewLabel("")
|
||||
updateStrength := func(pw string) {
|
||||
score, desc := simpleScore(pw)
|
||||
strengthBar.SetValue(float64(score))
|
||||
strengthLabel.SetText(desc)
|
||||
s, d := passwordStrengthScore(pw)
|
||||
strengthBar.SetValue(float64(s))
|
||||
strengthLabel.SetText(fmt.Sprintf("%s: %d%%", d, s))
|
||||
}
|
||||
createPw1.OnChanged = updateStrength
|
||||
genBtn := widget.NewButton("Generovat", func() {
|
||||
if v, err := encrypt.GenerateRandomPassword(20); err == nil {
|
||||
if v, err := encrypt.GenerateRandomPassword(24); err == nil {
|
||||
createPw1.SetText(v)
|
||||
createPw2.SetText(v)
|
||||
updateStrength(v)
|
||||
}
|
||||
})
|
||||
meter := container.NewVBox(strengthBar, strengthLabel)
|
||||
topCreate := container.NewBorder(nil, nil, nil, genBtn, meter)
|
||||
createForm := widget.NewForm(
|
||||
createBtn := widget.NewButton("Vytvořit trezor", nil)
|
||||
|
||||
// Obě pole stejné šířky – žádné tlačítko uvnitř prvního řádku
|
||||
form := widget.NewForm(
|
||||
widget.NewFormItem("Heslo", createPw1),
|
||||
widget.NewFormItem("Potvrzení", createPw2),
|
||||
)
|
||||
createContent := container.NewVBox(topCreate, createForm)
|
||||
|
||||
tabs := container.NewAppTabs(
|
||||
container.NewTabItem("Odemknout", unlockContent),
|
||||
container.NewTabItem("Vytvořit", createContent),
|
||||
// Řádek: progress bar s textem uvnitř (overlay) : tlačítko (70:30)
|
||||
strengthLabel.Alignment = fyne.TextAlignCenter
|
||||
leftStack := container.NewStack(
|
||||
strengthBar,
|
||||
container.NewCenter(strengthLabel),
|
||||
)
|
||||
tabs.SetTabLocation(container.TabLocationTop)
|
||||
// Výběr aktivní záložky dle existence souboru
|
||||
if _, err := os.Stat(vaultPath); err != nil { // neexistuje => vytvořit
|
||||
tabs.SelectIndex(1)
|
||||
strengthRow := container.New(&strengthRowLayout{}, leftStack, genBtn)
|
||||
createForm := container.NewVBox(
|
||||
form,
|
||||
strengthRow,
|
||||
createBtn,
|
||||
)
|
||||
stack := container.NewStack(unlockForm, createForm)
|
||||
showMode := func(c bool) {
|
||||
modeCreate = c
|
||||
if c {
|
||||
unlockForm.Hide()
|
||||
createForm.Show()
|
||||
} else {
|
||||
tabs.SelectIndex(0)
|
||||
createForm.Hide()
|
||||
unlockForm.Show()
|
||||
}
|
||||
|
||||
d := dialog.NewCustomConfirm("Trezor", "OK", "Zrušit", tabs, func(ok bool) {
|
||||
if !ok {
|
||||
statusLabel.Hide()
|
||||
}
|
||||
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, "")
|
||||
return
|
||||
}
|
||||
sel := tabs.Selected()
|
||||
if sel != nil && sel.Text == "Vytvořit" {
|
||||
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.Resize(fyne.NewSize(480, 320))
|
||||
d.Show()
|
||||
if modeCreate {
|
||||
w.Canvas().Focus(createPw1)
|
||||
} else {
|
||||
w.Canvas().Focus(unlockPw)
|
||||
}
|
||||
}
|
||||
|
||||
// simpleScore: hrubá heuristika (0-100)
|
||||
func simpleScore(pw string) (int, string) {
|
||||
l := len(pw)
|
||||
var u, lw, dg, sp int
|
||||
specials := "!@#-_+="
|
||||
// passwordStrengthScore: heuristický odhad síly (0-100) s ohledem na entropii, třídy znaků a penalizace.
|
||||
func passwordStrengthScore(pw string) (int, string) {
|
||||
if pw == "" {
|
||||
return 0, "Prázdné"
|
||||
}
|
||||
length := len(pw)
|
||||
// Charakteristiky
|
||||
hasUpper, hasLower, hasDigit, hasSymbol := false, false, false, false
|
||||
symbols := "!@#$%^&*()_-+=[]{}/?:.,<>|~`'\""
|
||||
freq := make(map[rune]int)
|
||||
for _, r := range pw {
|
||||
freq[r]++
|
||||
switch {
|
||||
case r >= 'A' && r <= 'Z':
|
||||
u++
|
||||
hasUpper = true
|
||||
case r >= 'a' && r <= 'z':
|
||||
lw++
|
||||
hasLower = true
|
||||
case r >= '0' && r <= '9':
|
||||
dg++
|
||||
hasDigit = true
|
||||
default:
|
||||
if strings.ContainsRune(specials, r) {
|
||||
sp++
|
||||
if strings.ContainsRune(symbols, r) {
|
||||
hasSymbol = true
|
||||
}
|
||||
}
|
||||
}
|
||||
cats := 0
|
||||
if u > 0 {
|
||||
cats++
|
||||
classes := 0
|
||||
if hasUpper {
|
||||
classes++
|
||||
}
|
||||
if lw > 0 {
|
||||
cats++
|
||||
if hasLower {
|
||||
classes++
|
||||
}
|
||||
if dg > 0 {
|
||||
cats++
|
||||
if hasDigit {
|
||||
classes++
|
||||
}
|
||||
if sp > 0 {
|
||||
cats++
|
||||
if hasSymbol {
|
||||
classes++
|
||||
}
|
||||
score := l*4 + cats*10
|
||||
if score > 100 {
|
||||
score = 100
|
||||
|
||||
// Shannon entropie per char
|
||||
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é"
|
||||
switch {
|
||||
case score >= 85:
|
||||
case raw >= 85:
|
||||
desc = "Silné"
|
||||
case score >= 60:
|
||||
case raw >= 65:
|
||||
desc = "Dobré"
|
||||
case score >= 30:
|
||||
case raw >= 45:
|
||||
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() {
|
||||
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) {
|
||||
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")
|
||||
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 {
|
||||
return
|
||||
}
|
||||
@ -154,6 +161,8 @@ func buildIdentityTab(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.
|
||||
}
|
||||
fyne.CurrentApp().Quit()
|
||||
}, fyne.CurrentApp().Driver().AllWindows()[0])
|
||||
d.Resize(fyne.NewSize(600, 250))
|
||||
d.Show()
|
||||
})
|
||||
|
||||
makeQR := func(data string, target *canvas.Image) {
|
||||
@ -294,7 +303,7 @@ func buildEncryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
||||
}
|
||||
updatePeer()
|
||||
|
||||
// Output section with QR/Text toggle
|
||||
// Output section toggle (QR vs text)
|
||||
outputContainer := container.NewVBox()
|
||||
outputToggleAction := widget.NewToolbarAction(theme.VisibilityOffIcon(), nil)
|
||||
updateQR := func(text string) {
|
||||
@ -316,11 +325,11 @@ func buildEncryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
||||
parts.cipherQR.Image = img
|
||||
parts.cipherQR.Refresh()
|
||||
}
|
||||
updateMode := func() {
|
||||
updateOutput := func() {
|
||||
outputContainer.Objects = nil
|
||||
if parts.showQR {
|
||||
if parts.showQR { // show QR mode
|
||||
updateQR(parts.cipherOut.Text)
|
||||
outputContainer.Add(parts.cipherQR) // copy tlačítko jen v toolbaru
|
||||
outputContainer.Add(parts.cipherQR)
|
||||
outputToggleAction.SetIcon(theme.VisibilityOffIcon())
|
||||
} else {
|
||||
outputContainer.Add(parts.cipherOut)
|
||||
@ -328,8 +337,8 @@ func buildEncryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
||||
}
|
||||
outputContainer.Refresh()
|
||||
}
|
||||
outputToggleAction.OnActivated = func() { parts.showQR = !parts.showQR; updateMode() }
|
||||
updateMode()
|
||||
outputToggleAction.OnActivated = func() { parts.showQR = !parts.showQR; updateOutput() }
|
||||
updateOutput()
|
||||
|
||||
encAction := func() {
|
||||
m := parts.msg.Text
|
||||
@ -346,9 +355,8 @@ func buildEncryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
||||
}
|
||||
fyne.Do(func() {
|
||||
parts.cipherOut.SetText(res)
|
||||
if parts.showQR {
|
||||
updateQR(res)
|
||||
}
|
||||
// refresh whichever mode is active
|
||||
updateOutput()
|
||||
parts.showToast("OK")
|
||||
})
|
||||
}(m, p)
|
||||
@ -421,13 +429,15 @@ func buildEncryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
||||
container.NewHBox(widget.NewLabel("Výstup"), layout.NewSpacer(), outputToolbar),
|
||||
outputContainer,
|
||||
)
|
||||
// Split: message (top) vs output (bottom) for better prostor "do spodu"
|
||||
split := container.NewVSplit(msgSection, outputSection)
|
||||
split.SetOffset(0.40)
|
||||
group := container.NewVBox(
|
||||
widget.NewLabelWithStyle("Šifrování", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||
peerSection,
|
||||
msgSection,
|
||||
outputSection,
|
||||
split,
|
||||
)
|
||||
return container.NewVScroll(group)
|
||||
return group
|
||||
}
|
||||
|
||||
// Tab: Decrypt
|
||||
@ -821,6 +831,9 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
||||
draft = &Contact{ID: "__draft__", Name: makeDefaultName(), Cert: ""}
|
||||
nameEntry.SetText(draft.Name)
|
||||
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
|
||||
searchQuery = ""
|
||||
search.SetText("")
|
||||
@ -860,11 +873,21 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
|
||||
deleteBtn := widget.NewButtonWithIcon("", theme.DeleteIcon(), func() {
|
||||
// If draft active and no real contact chosen -> cancel draft
|
||||
if draft != nil && selected == -1 {
|
||||
// remove draft and if exist some contacts immediately select first one
|
||||
draft = nil
|
||||
nameEntry.SetText("")
|
||||
certEntry.SetText("")
|
||||
list.Refresh()
|
||||
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")
|
||||
return
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user