From 133091a32e245d5be8106afb90ada49555e70e99 Mon Sep 17 00:00:00 2001 From: Daniel Wu Date: Fri, 12 Jun 2026 10:07:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E9=A6=96=E6=AC=A1=E5=90=AF?= =?UTF-8?q?=E5=8A=A8=E5=8A=A0=E5=AF=86=E5=BC=95=E5=AF=BC=20+=20=E7=9C=9F?= =?UTF-8?q?=E7=AA=97=E5=8F=A3=E5=9B=BE=E6=A0=87=20(.syso)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 首次启动: - 配置文件不存在时弹欢迎对话框:选「启用密码保护」或「稍后再说」 - 选启用直接进 setPasswordDialog,OK 时 store.SetPassword 触发首次保存 - io/fs.ErrNotExist 检测新分支 - modalCard 抽出 modalShell 内核以便 welcomeDialog 自定义 footer 真窗口图标: - tools/gen_ico 从 PNG 生成多分辨率 .ico(7 尺寸:16~256) - 提交 rsrc_windows.syso(rsrc 通过 goproxy.cn 生成),go build 自动链入 - tray_windows.go 改用嵌入资源 ID=1,失败 fallback IDI_APPLICATION - README 更新完整的图标生成流程 --- internal/i18n/locales/de.toml | 14 +++ internal/ui/app.go | 28 +++++- internal/ui/assets/README.md | 20 +++- internal/ui/assets/app_icon.ico | Bin 0 -> 5447 bytes internal/ui/assets/rsrc_windows.syso | Bin 0 -> 6074 bytes internal/ui/dialog.go | 33 ++++-- internal/ui/dialog_welcome.go | 66 ++++++++++++ internal/win32/tray_windows.go | 47 +++++---- tools/gen_ico/main.go | 144 +++++++++++++++++++++++++++ 9 files changed, 317 insertions(+), 35 deletions(-) create mode 100644 internal/ui/assets/app_icon.ico create mode 100644 internal/ui/assets/rsrc_windows.syso create mode 100644 internal/ui/dialog_welcome.go create mode 100644 tools/gen_ico/main.go diff --git a/internal/i18n/locales/de.toml b/internal/i18n/locales/de.toml index 7bf4cee..f423c23 100644 --- a/internal/i18n/locales/de.toml +++ b/internal/i18n/locales/de.toml @@ -509,3 +509,17 @@ other = "Symbol ändern..." [dialog_icon_picker_title] other = "Symbol auswählen" + +# --- Erster Start: Verschlüsselungsauswahl --- + +[dialog_welcome_title] +other = "Willkommen bei WinAuth" + +[msg_welcome_intro] +other = "Wähle, ob die Liste der Authenticatoren verschlüsselt auf der Festplatte gespeichert werden soll. Die Verschlüsselung schützt die Datei mit einem Passwort deiner Wahl. Du kannst dies später jederzeit in den Einstellungen ändern." + +[btn_welcome_enable_password] +other = "Passwortschutz aktivieren" + +[btn_welcome_skip] +other = "Vorerst überspringen" diff --git a/internal/ui/app.go b/internal/ui/app.go index 92380ed..c52f933 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "image/color" + "io/fs" "os" "sync" "time" @@ -97,6 +98,7 @@ type appState struct { pwDialog *passwordDialog setPwDialog *setPasswordDialog changePwDlg *changePasswordDialog + welcomeDlg *welcomeDialog importDialog *importLegacyDialog hotkeyDialog *hotkeyDialog hotkeyTarget *entry @@ -169,10 +171,15 @@ func loop(w *app.Window, configPath string) error { w.Invalidate() }) - // First-load attempt: empty passphrase. If the file is encrypted we'll - // surface a password dialog on the first frame. + // First-load attempt: empty passphrase. The branches are: + // - file missing → first-run welcome dialog (encrypt / skip) + // - file encrypted → password dialog + // - other load error → log + show error banner if cfg, err := state.store.Load(nil); err != nil { switch { + case errors.Is(err, fs.ErrNotExist): + global.Log.WithField("func", fn).Info("no config file; showing first-run welcome") + state.welcomeDlg = newWelcomeDialog() case errors.Is(err, ErrPasswordRequired): state.pwDialog = newPasswordDialog(i18n.T("msg_password_required")) default: @@ -474,6 +481,23 @@ func drawFrame(gtx layout.Context, th *material.Theme, st *appState, w *app.Wind st.rowMenu = newRowActionMenu(moreTargetIdx, total) } + // First-run welcome dialog. Routed before all other dialogs so the + // encryption choice appears immediately on a fresh install. + if st.welcomeDlg != nil { + return st.welcomeDlg.Layout(gtx, th, func(r welcomeResult) { + st.welcomeDlg = nil + if r.encrypt { + // Hand off to the existing setPasswordDialog. OK on + // that dialog will call store.SetPassword, which + // creates the config file. + st.setPwDialog = newSetPasswordDialog() + } + // skip: leave entries empty and unencrypted; the user + // can encrypt later via Settings → Set password. + w.Invalidate() + }) + } + // Password retry / first-decrypt loop. if st.pwDialog != nil { return st.pwDialog.Layout(gtx, th, func(pw []byte, ok bool) { diff --git a/internal/ui/assets/README.md b/internal/ui/assets/README.md index 7535148..6632260 100644 --- a/internal/ui/assets/README.md +++ b/internal/ui/assets/README.md @@ -25,13 +25,27 @@ embedded at build time via `//go:embed` in `internal/ui/vendor_icons.go`. A 256×256 placeholder for the application window/tray icon. Gio v0.7 has no runtime API to set the window icon — for Windows you need to embed a -`.ico` resource via `rsrc` or `goversioninfo` at build time: +`.ico` resource via `rsrc` or `goversioninfo` at build time. The +workflow is: ``` -go install github.com/akavel/rsrc@latest -rsrc -ico app.ico -o cmd/winauth/rsrc_windows.syso +# 1. Regenerate app_icon.png (optional — only if you change the source) +go run ./tools/gen_icons + +# 2. Bake a multi-resolution app_icon.ico from the PNG +go run ./tools/gen_ico + +# 3. Compile the .ico into a COFF .syso that Go auto-links +GOPROXY=https://goproxy.cn,direct go run github.com/akavel/rsrc@latest \ + -ico internal/ui/assets/app_icon.ico \ + -o internal/ui/assets/rsrc_windows.syso ``` +`rsrc_windows.syso` lives next to other assets so `go build` picks it up +without any extra build-tag wiring. The tray runtime loads the icon by +resource id 1 (the first (and only) icon rsrc emits); if loading fails +it falls back to `IDI_APPLICATION`. + To regenerate the placeholders, run: ``` diff --git a/internal/ui/assets/app_icon.ico b/internal/ui/assets/app_icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..f2c5881b917863ae3430a1ae90419b65ef18e47a GIT binary patch literal 5447 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 MPC2CL-*1Bd0<`5w9RL6T literal 0 HcmV?d00001 diff --git a/internal/ui/assets/rsrc_windows.syso b/internal/ui/assets/rsrc_windows.syso new file mode 100644 index 0000000000000000000000000000000000000000..28804c38d47f7362bfab3f8bfd308448cf97f351 GIT binary patch literal 6074 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 1 { + srcPath = os.Args[1] + } + if len(os.Args) > 2 { + dstPath = os.Args[2] + } + if err := run(srcPath, dstPath); err != nil { + fmt.Fprintln(os.Stderr, "gen_ico:", err) + os.Exit(1) + } +} + +func run(srcPath, dstPath string) error { + srcF, err := os.Open(srcPath) + if err != nil { + return fmt.Errorf("open src: %w", err) + } + defer srcF.Close() + src, _, err := image.Decode(srcF) + if err != nil { + return fmt.Errorf("decode src: %w", err) + } + + var entries []icoEntry + for _, size := range icoSizes { + dst := image.NewNRGBA(image.Rect(0, 0, size, size)) + // CatmullRom gives sharper results at small sizes than the + // default nearest-neighbour. + draw.CatmullRom.Scale(dst, dst.Bounds(), src, src.Bounds(), draw.Over, nil) + + var buf bytes.Buffer + if err := png.Encode(&buf, dst); err != nil { + return fmt.Errorf("encode %dpx: %w", size, err) + } + entries = append(entries, icoEntry{w: size, h: size, png: buf.Bytes()}) + } + + out, err := os.Create(dstPath) + if err != nil { + return fmt.Errorf("create dst: %w", err) + } + defer out.Close() + return writeICO(out, entries) +} + +// writeICO serialises a Vista+ PNG-based .ico file: ICONDIR + one +// ICONDIRENTRY per image + the PNG bytes concatenated. +func writeICO(w io.Writer, entries []icoEntry) error { + if _, err := w.Write([]byte{ + 0, 0, // reserved + 1, 0, // type = 1 (icon) + byte(len(entries)), 0, // count + }); err != nil { + return err + } + + const dirHdrLen = 6 + const dirEntryLen = 16 + headerLen := dirHdrLen + dirEntryLen*len(entries) + + offset := headerLen + for _, e := range entries { + wByte := byte(e.w) + if e.w >= 256 { + // 0 means 256 in the .ico directory. + wByte = 0 + } + hByte := byte(e.h) + if e.h >= 256 { + hByte = 0 + } + size := uint32(len(e.png)) + + entry := make([]byte, dirEntryLen) + entry[0] = wByte + entry[1] = hByte + entry[2] = 0 // color palette + entry[3] = 0 // reserved + binary.LittleEndian.PutUint16(entry[4:], 1) // planes + binary.LittleEndian.PutUint16(entry[6:], 32) // bit count + binary.LittleEndian.PutUint32(entry[8:], size) + binary.LittleEndian.PutUint32(entry[12:], uint32(offset)) + if _, err := w.Write(entry); err != nil { + return err + } + offset += int(size) + } + + for _, e := range entries { + if _, err := w.Write(e.png); err != nil { + return err + } + } + return nil +}