0918fd446a
- 修改密码对话框引入旧密码校验:通过 store.VerifyPassword 走 constant-time 比较,对话框自身不持有 live passphrase 副本。 - 配置加密且 auto_lock_minutes>0 时启用自动锁定;明文配置不受影响。 锁定时刻意保留 store.passphrase,避免解锁瞬间出现保存竞态——进程内存 无论如何都是威胁边界。 - HOTP 点击后 toast 仅显示推进后的计数器值,绝不携带验证码字符串。 - SaveYAML 每次写入前将旧文件轮转为 .bak(0o600),防止加密/序列化失败 导致用户无可恢复副本。 - Preferences 对话框新增"自动锁定(分钟)"输入项。
160 lines
5.1 KiB
Go
160 lines
5.1 KiB
Go
package ui
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
|
|
"gioui.org/layout"
|
|
"gioui.org/unit"
|
|
"gioui.org/widget"
|
|
"gioui.org/widget/material"
|
|
|
|
"git.wxccs.org/iceking2nd/winauth-go/internal/i18n"
|
|
)
|
|
|
|
// preferencesDialog edits the persistent UI prefs: UI language, theme,
|
|
// and auto-lock timeout. Minimize-to-tray lives in its own bucket and
|
|
// will appear here later.
|
|
type preferencesDialog struct {
|
|
lang string
|
|
theme themeMode
|
|
|
|
// Theme selection — three "radio" buttons.
|
|
themeSystemBt widget.Clickable
|
|
themeLightBt widget.Clickable
|
|
themeDarkBt widget.Clickable
|
|
|
|
// Language selection — one button per supported locale. Cheap
|
|
// substitute for a dropdown until Gio gains a native one.
|
|
langEnBt widget.Clickable
|
|
langZhCNBt widget.Clickable
|
|
langDeBt widget.Clickable
|
|
|
|
// Auto-lock timeout (whole minutes). 0 disables. Only takes effect
|
|
// when the config is encrypted — the prefs dialog does not gate the
|
|
// input on that, the store does.
|
|
autoLockEd widget.Editor
|
|
|
|
okBt widget.Clickable
|
|
cancelBt widget.Clickable
|
|
}
|
|
|
|
// preferencesResult is what Layout's callback receives. cancel skips
|
|
// any persistence.
|
|
type preferencesResult struct {
|
|
cancel bool
|
|
language string
|
|
theme themeMode
|
|
autoLockMinutes int
|
|
}
|
|
|
|
// supportedLanguages is the closed set the prefs dialog exposes. Keep
|
|
// in sync with internal/i18n/locales/*.toml.
|
|
var supportedLanguages = []struct {
|
|
tag string
|
|
label string
|
|
}{
|
|
{"en", "English"},
|
|
{"zh-CN", "中文 (简体)"},
|
|
{"de", "Deutsch"},
|
|
}
|
|
|
|
func newPreferencesDialog(currentLang string, currentTheme themeMode, currentAutoLock int) *preferencesDialog {
|
|
if currentLang == "" {
|
|
currentLang = "en"
|
|
}
|
|
if currentTheme == "" {
|
|
currentTheme = themeSystem
|
|
}
|
|
d := &preferencesDialog{lang: currentLang, theme: currentTheme}
|
|
d.autoLockEd.SingleLine = true
|
|
d.autoLockEd.SetText(strconv.Itoa(currentAutoLock))
|
|
return d
|
|
}
|
|
|
|
func (d *preferencesDialog) Layout(
|
|
gtx layout.Context, th *material.Theme,
|
|
onDone func(preferencesResult),
|
|
) layout.Dimensions {
|
|
if d.cancelBt.Clicked(gtx) {
|
|
onDone(preferencesResult{cancel: true})
|
|
return layout.Dimensions{Size: gtx.Constraints.Max}
|
|
}
|
|
if d.okBt.Clicked(gtx) {
|
|
// Best-effort parse — non-numeric or negative input maps to 0
|
|
// (disabled) rather than surfacing an error, since the field
|
|
// is paired with a "0 disables" hint.
|
|
mins, err := strconv.Atoi(strings.TrimSpace(d.autoLockEd.Text()))
|
|
if err != nil || mins < 0 {
|
|
mins = 0
|
|
}
|
|
onDone(preferencesResult{language: d.lang, theme: d.theme, autoLockMinutes: mins})
|
|
return layout.Dimensions{Size: gtx.Constraints.Max}
|
|
}
|
|
|
|
// Map button clicks to model updates before the next layout.
|
|
switch {
|
|
case d.themeSystemBt.Clicked(gtx):
|
|
d.theme = themeSystem
|
|
case d.themeLightBt.Clicked(gtx):
|
|
d.theme = themeLight
|
|
case d.themeDarkBt.Clicked(gtx):
|
|
d.theme = themeDark
|
|
case d.langEnBt.Clicked(gtx):
|
|
d.lang = "en"
|
|
case d.langZhCNBt.Clicked(gtx):
|
|
d.lang = "zh-CN"
|
|
case d.langDeBt.Clicked(gtx):
|
|
d.lang = "de"
|
|
}
|
|
|
|
// Render one labeled "chip" — emphasised when selected, regular
|
|
// otherwise. Implemented as a material.Button so the click target
|
|
// stays generous without us hand-rolling a Pointer area.
|
|
chip := func(btn *widget.Clickable, label string, selected bool) layout.FlexChild {
|
|
return layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
return layout.Inset{Right: unit.Dp(8), Bottom: unit.Dp(4)}.Layout(gtx,
|
|
func(gtx layout.Context) layout.Dimensions {
|
|
b := material.Button(th, btn, label)
|
|
if !selected {
|
|
b.Background = activePalette.RingBg
|
|
b.Color = activePalette.OnBackground
|
|
}
|
|
return b.Layout(gtx)
|
|
})
|
|
})
|
|
}
|
|
|
|
body := func(gtx layout.Context) layout.Dimensions {
|
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
|
layout.Rigid(material.Body1(th, i18n.T("label_theme")).Layout),
|
|
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
|
chip(&d.themeSystemBt, i18n.T("theme_system"), d.theme == themeSystem),
|
|
chip(&d.themeLightBt, i18n.T("theme_light"), d.theme == themeLight),
|
|
chip(&d.themeDarkBt, i18n.T("theme_dark"), d.theme == themeDark),
|
|
)
|
|
}),
|
|
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
|
|
layout.Rigid(material.Body1(th, i18n.T("label_language")).Layout),
|
|
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
|
chip(&d.langEnBt, supportedLanguages[0].label, d.lang == "en"),
|
|
chip(&d.langZhCNBt, supportedLanguages[1].label, d.lang == "zh-CN"),
|
|
chip(&d.langDeBt, supportedLanguages[2].label, d.lang == "de"),
|
|
)
|
|
}),
|
|
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
|
|
layout.Rigid(labeledEditor(th, i18n.T("label_auto_lock_minutes"), &d.autoLockEd, "")),
|
|
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
|
|
layout.Rigid(material.Caption(th, i18n.T("hint_auto_lock_disabled")).Layout),
|
|
)
|
|
}
|
|
|
|
return modalCard(gtx, th, i18n.T("dialog_preferences_title"),
|
|
i18n.T("btn_ok"), i18n.T("btn_cancel"),
|
|
&d.okBt, &d.cancelBt, body)
|
|
}
|