feat(ui): 开机自启动 + 窗口大小记忆

- win32/autostart: SetAutoStart/IsAutoStartEnabled 操作 HKCU Run 键
- win32/window_size: GetWindowSize 通过 GetWindowRect 获取窗口尺寸
- config: 新增 AutoStart、WindowWidth、WindowHeight 字段
- store: Preferences/SetPreferences 扩展支持 autoStart + windowSize
- dialog_preferences: 新增"开机自启动"复选框
- app.go: 启动时恢复窗口大小,关闭时通过 SetWindowSize 保存
- i18n: label_auto_start / hint_auto_start 三语翻译
This commit is contained in:
2026-06-12 11:10:25 +08:00
parent cb4d88ebdd
commit 1605b5b7ae
11 changed files with 278 additions and 17 deletions
+27 -8
View File
@@ -38,9 +38,20 @@ func Run(configPath string) error {
go func() {
w := new(app.Window)
// Read stored window size or use defaults.
cfg, _ := config.LoadYAML(configPath, nil)
winW, winH := 560, 420
if cfg != nil {
if cfg.WindowWidth > 0 {
winW = cfg.WindowWidth
}
if cfg.WindowHeight > 0 {
winH = cfg.WindowHeight
}
}
w.Option(
app.Title(i18n.T("app_title")),
app.Size(unit.Dp(560), unit.Dp(420)),
app.Size(unit.Dp(winW), unit.Dp(winH)),
)
if err := loop(w, configPath); err != nil {
global.Log.WithField("func", fn).WithError(err).Error("ui loop failed")
@@ -195,7 +206,7 @@ func loop(w *app.Window, configPath string) error {
// Seed the theme cache from the stored preference. Empty / unknown
// values resolve to "system" which buildTheme then maps to light or
// dark via the OS-specific systemPrefersDark probe.
_, themePref, _, _ := state.store.Preferences()
_, themePref, _, _, _, _, _ := state.store.Preferences()
state.themeMode = normalizeThemeMode(themePref)
// Spin up the global hotkey manager and register whatever the user
@@ -208,7 +219,7 @@ func loop(w *app.Window, configPath string) error {
// Install the system tray icon + close-hook (Windows only). HWND
// discovery races the very first FrameEvent, so kick it from a
// goroutine that polls FindWindow for a few hundred ms.
_, _, _, minToTray := state.store.Preferences()
_, _, _, minToTray, _, _, _ := state.store.Preferences()
go func() {
tr := installTrayRuntime(i18n.T("app_title"), minToTray, w.Invalidate)
state.mu.Lock()
@@ -229,6 +240,12 @@ func loop(w *app.Window, configPath string) error {
for {
switch e := w.Event().(type) {
case app.DestroyEvent:
// Persist window size before exiting.
if hwnd := win32.FindWindowByTitle(i18n.T("app_title")); hwnd != 0 {
if w, h, ok := win32.GetWindowSize(hwnd); ok && w > 0 && h > 0 {
state.store.SetWindowSize(w, h)
}
}
global.Log.WithField("func", fn).Info("window closed")
state.tray.Stop()
return e.Err
@@ -267,7 +284,7 @@ func (st *appState) maybeAutoLock() bool {
if !st.store.Encrypted() {
return false
}
_, _, autoLockMinutes, _ := st.store.Preferences()
_, _, autoLockMinutes, _, _, _, _ := st.store.Preferences()
if autoLockMinutes <= 0 {
return false
}
@@ -563,8 +580,8 @@ func drawFrame(gtx layout.Context, th *material.Theme, st *appState, w *app.Wind
case settingsActionRestoreBackup:
st.restoreDlg = newRestoreBackupDialog()
case settingsActionPreferences:
lang, theme, autoLock, minToTray := st.store.Preferences()
st.prefsDialog = newPreferencesDialog(lang, normalizeThemeMode(theme), autoLock, minToTray)
lang, theme, autoLock, minToTray, autoStart, _, _ := st.store.Preferences()
st.prefsDialog = newPreferencesDialog(lang, normalizeThemeMode(theme), autoLock, minToTray, autoStart)
case settingsActionAbout:
st.aboutDialog = newAboutDialog()
}
@@ -577,8 +594,10 @@ func drawFrame(gtx layout.Context, th *material.Theme, st *appState, w *app.Wind
if st.prefsDialog != nil {
return st.prefsDialog.Layout(gtx, th, func(r preferencesResult) {
if !r.cancel {
_, _, _, prevMinToTray := st.store.Preferences()
st.store.SetPreferences(r.language, string(r.theme), r.autoLockMinutes, r.minimizeToTray)
_, _, _, prevMinToTray, _, _, _ := st.store.Preferences()
_, _, _, _, _, winW, winH := st.store.Preferences()
st.store.SetPreferences(r.language, string(r.theme), r.autoLockMinutes, r.minimizeToTray, r.autoStart, winW, winH)
win32.SetAutoStart(r.autoStart)
i18n.SetLanguage(r.language)
st.themeMode = r.theme
st.theme = nil // force rebuild on next frame
+13 -2
View File
@@ -13,7 +13,7 @@ import (
)
// preferencesDialog edits the persistent UI prefs: UI language, theme,
// auto-lock timeout, and minimize-to-tray behaviour.
// auto-lock timeout, minimize-to-tray, and auto-start behaviour.
type preferencesDialog struct {
lang string
theme themeMode
@@ -38,6 +38,10 @@ type preferencesDialog struct {
// system tray instead of exiting. Tray icon stays visible either way.
minToTray widget.Bool
// Auto-start: when on, the app is registered in the Windows Run key
// to launch at logon.
autoStart widget.Bool
okBt widget.Clickable
cancelBt widget.Clickable
}
@@ -50,6 +54,7 @@ type preferencesResult struct {
theme themeMode
autoLockMinutes int
minimizeToTray bool
autoStart bool
}
// supportedLanguages is the closed set the prefs dialog exposes. Keep
@@ -63,7 +68,7 @@ var supportedLanguages = []struct {
{"de", "Deutsch"},
}
func newPreferencesDialog(currentLang string, currentTheme themeMode, currentAutoLock int, currentMinToTray bool) *preferencesDialog {
func newPreferencesDialog(currentLang string, currentTheme themeMode, currentAutoLock int, currentMinToTray, currentAutoStart bool) *preferencesDialog {
if currentLang == "" {
currentLang = "en"
}
@@ -74,6 +79,7 @@ func newPreferencesDialog(currentLang string, currentTheme themeMode, currentAut
d.autoLockEd.SingleLine = true
d.autoLockEd.SetText(strconv.Itoa(currentAutoLock))
d.minToTray.Value = currentMinToTray
d.autoStart.Value = currentAutoStart
return d
}
@@ -98,6 +104,7 @@ func (d *preferencesDialog) Layout(
theme: d.theme,
autoLockMinutes: mins,
minimizeToTray: d.minToTray.Value,
autoStart: d.autoStart.Value,
})
return layout.Dimensions{Size: gtx.Constraints.Max}
}
@@ -164,6 +171,10 @@ func (d *preferencesDialog) Layout(
layout.Rigid(material.CheckBox(th, &d.minToTray, i18n.T("label_minimize_to_tray")).Layout),
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
layout.Rigid(material.Caption(th, i18n.T("hint_minimize_to_tray")).Layout),
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
layout.Rigid(material.CheckBox(th, &d.autoStart, i18n.T("label_auto_start")).Layout),
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
layout.Rigid(material.Caption(th, i18n.T("hint_auto_start")).Layout),
)
}
+27 -3
View File
@@ -33,6 +33,9 @@ type store struct {
theme string
autoLockMinutes int
minimizeToTray bool
autoStart bool
windowWidth int
windowHeight int
// dirty signals a save is pending. The worker reads & resets it.
dirty bool
@@ -47,21 +50,24 @@ type store struct {
// Preferences returns the persisted top-level prefs. UI code reads these
// after Load to seed dialog defaults.
func (s *store) Preferences() (language, theme string, autoLockMinutes int, minimizeToTray bool) {
func (s *store) Preferences() (language, theme string, autoLockMinutes int, minimizeToTray, autoStart bool, windowWidth, windowHeight int) {
s.mu.Lock()
defer s.mu.Unlock()
return s.language, s.theme, s.autoLockMinutes, s.minimizeToTray
return s.language, s.theme, s.autoLockMinutes, s.minimizeToTray, s.autoStart, s.windowWidth, s.windowHeight
}
// SetPreferences updates the persisted top-level prefs and schedules a
// save. Callers pass the current value for each field — there is no
// per-field "leave unchanged" sentinel.
func (s *store) SetPreferences(language, theme string, autoLockMinutes int, minimizeToTray bool) {
func (s *store) SetPreferences(language, theme string, autoLockMinutes int, minimizeToTray, autoStart bool, windowWidth, windowHeight int) {
s.mu.Lock()
s.language = language
s.theme = theme
s.autoLockMinutes = autoLockMinutes
s.minimizeToTray = minimizeToTray
s.autoStart = autoStart
s.windowWidth = windowWidth
s.windowHeight = windowHeight
s.mu.Unlock()
s.Push()
}
@@ -119,6 +125,9 @@ func (s *store) Load(passphrase []byte) (*config.Config, error) {
s.theme = cfg.Theme
s.autoLockMinutes = cfg.AutoLockMinutes
s.minimizeToTray = cfg.MinimizeToTray
s.autoStart = cfg.AutoStart
s.windowWidth = cfg.WindowWidth
s.windowHeight = cfg.WindowHeight
s.mu.Unlock()
logger.WithField("entries", len(cfg.Entries)).Debug("config loaded into store")
return cfg, nil
@@ -160,6 +169,15 @@ func (s *store) Encrypted() bool {
return s.encrypted
}
// SetWindowSize updates the stored window dimensions and schedules a save.
func (s *store) SetWindowSize(w, h int) {
s.mu.Lock()
s.windowWidth = w
s.windowHeight = h
s.mu.Unlock()
s.Push()
}
// Push schedules a save. Calls within ~300ms of each other coalesce into
// a single write.
func (s *store) Push() {
@@ -196,6 +214,9 @@ func (s *store) run() {
theme := s.theme
autoLock := s.autoLockMinutes
minToTray := s.minimizeToTray
autoStart := s.autoStart
winW := s.windowWidth
winH := s.windowHeight
s.mu.Unlock()
entries := s.snapshotFn()
@@ -205,6 +226,9 @@ func (s *store) run() {
Theme: theme,
AutoLockMinutes: autoLock,
MinimizeToTray: minToTray,
AutoStart: autoStart,
WindowWidth: winW,
WindowHeight: winH,
Encrypted: enc,
Entries: entries,
}