feat(ui): 接入 vendor 真实图标库 + 图标选择器

- assets/icons/ 共 80+ 张真实品牌图标嵌入 UI 包
- entry 新增 IconKey 字段,持久化到 config.Entry.IconName
- iconPickerDialog 网格布局图标选择器
- Add 完成后自动弹出图标选择器,Skip 则用 vendor 占位图
- ⋯ 菜单新增 "Change icon..." 项
- vendorIconFor 解析顺序:catalog → vendor 占位 → qr 兜底
This commit is contained in:
2026-06-12 09:41:30 +08:00
parent 352b1d24b8
commit 969600b6ee
95 changed files with 298 additions and 27 deletions
+36 -3
View File
@@ -56,6 +56,11 @@ type entry struct {
Auth authenticator.Authenticator
Code string
// IconKey selects a specific brand icon from the embedded catalog
// (e.g. "GitHubIcon", "DropboxIcon"). Empty means fall back to the
// vendor-name placeholder ("google", "steam", …).
IconKey string
// Hotkey is the user-configured global shortcut string ("Ctrl+Alt+G")
// or "" if none is set.
Hotkey string
@@ -106,6 +111,7 @@ type appState struct {
rowMenu *rowActionMenu
renameDlg *renameDialog
confirmDelDlg *confirmDeleteDialog
iconPickerDlg *iconPickerDialog
rowTargetIdx int
store *store
@@ -144,7 +150,7 @@ func (st *appState) snapshotEntries() []config.Entry {
defer st.mu.Unlock()
out := make([]config.Entry, 0, len(st.entries))
for _, en := range st.entries {
out = append(out, entryFromAuthenticator(en.Name, en.Auth, en.Hotkey))
out = append(out, entryFromAuthenticator(en.Name, en.Auth, en.Hotkey, en.IconKey))
}
return out
}
@@ -285,7 +291,7 @@ func (st *appState) absorbConfig(cfg *config.Config) {
global.Log.WithField("func", fn).WithError(err).Warn("skip entry")
continue
}
st.entries = append(st.entries, &entry{Name: e.Name, Auth: a, Hotkey: e.Hotkey})
st.entries = append(st.entries, &entry{Name: e.Name, Auth: a, Hotkey: e.Hotkey, IconKey: e.IconName})
}
}
@@ -305,7 +311,7 @@ func (st *appState) mergeImportedConfig(cfg *config.Config) {
global.Log.WithField("func", fn).WithError(err).Warn("skip imported entry")
continue
}
st.entries = append(st.entries, &entry{Name: e.Name, Auth: a, Hotkey: e.Hotkey})
st.entries = append(st.entries, &entry{Name: e.Name, Auth: a, Hotkey: e.Hotkey, IconKey: e.IconName})
}
}
@@ -608,8 +614,13 @@ func drawFrame(gtx layout.Context, th *material.Theme, st *appState, w *app.Wind
if added != nil {
st.mu.Lock()
st.entries = append(st.entries, &entry{Name: name, Auth: added})
idx := len(st.entries) - 1
st.mu.Unlock()
st.store.Push()
// Auto-open icon picker so the user can choose a brand
// icon for the entry right after adding it.
st.rowTargetIdx = idx
st.iconPickerDlg = newIconPickerDialog("")
}
st.dialog = nil
w.Invalidate()
@@ -656,6 +667,14 @@ func drawFrame(gtx layout.Context, th *material.Theme, st *appState, w *app.Wind
st.moveEntry(idx, -1)
case rowActionMoveDown:
st.moveEntry(idx, +1)
case rowActionChangeIcon:
st.mu.Lock()
current := ""
if idx >= 0 && idx < len(st.entries) {
current = st.entries[idx].IconKey
}
st.mu.Unlock()
st.iconPickerDlg = newIconPickerDialog(current)
}
w.Invalidate()
} else {
@@ -680,6 +699,20 @@ func drawFrame(gtx layout.Context, th *material.Theme, st *appState, w *app.Wind
w.Invalidate()
})
}
if st.iconPickerDlg != nil {
return st.iconPickerDlg.Layout(gtx, th, func(iconKey string, cancel bool) {
if !cancel {
st.mu.Lock()
if st.rowTargetIdx >= 0 && st.rowTargetIdx < len(st.entries) {
st.entries[st.rowTargetIdx].IconKey = iconKey
}
st.mu.Unlock()
st.store.Push()
}
st.iconPickerDlg = nil
w.Invalidate()
})
}
if st.tradesDialog != nil {
return st.tradesDialog.Layout(gtx, th)