diff --git a/internal/i18n/locales/de.toml b/internal/i18n/locales/de.toml index 00240dd..7bf4cee 100644 --- a/internal/i18n/locales/de.toml +++ b/internal/i18n/locales/de.toml @@ -503,3 +503,9 @@ other = "Löschen bestätigen" [msg_confirm_delete] other = "Eintrag \"%s\" wirklich löschen? Dies kann nicht rückgängig gemacht werden." + +[action_change_icon] +other = "Symbol ändern..." + +[dialog_icon_picker_title] +other = "Symbol auswählen" diff --git a/internal/i18n/locales/en.toml b/internal/i18n/locales/en.toml index 14d2bdb..d902077 100644 --- a/internal/i18n/locales/en.toml +++ b/internal/i18n/locales/en.toml @@ -505,3 +505,9 @@ other = "Confirm delete" [msg_confirm_delete] other = "Delete the entry \"%s\"? This cannot be undone." + +[action_change_icon] +other = "Change icon..." + +[dialog_icon_picker_title] +other = "Choose icon" diff --git a/internal/i18n/locales/zh-CN.toml b/internal/i18n/locales/zh-CN.toml index 506424f..0714c2a 100644 --- a/internal/i18n/locales/zh-CN.toml +++ b/internal/i18n/locales/zh-CN.toml @@ -503,3 +503,9 @@ other = "确认删除" [msg_confirm_delete] other = "确定删除条目 \"%s\"?此操作不可撤销。" + +[action_change_icon] +other = "更换图标..." + +[dialog_icon_picker_title] +other = "选择图标" diff --git a/internal/ui/app.go b/internal/ui/app.go index 35eee3d..92380ed 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -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) diff --git a/internal/ui/assets/icons/AmazonAWSIcon.png b/internal/ui/assets/icons/AmazonAWSIcon.png new file mode 100644 index 0000000..ea9980a Binary files /dev/null and b/internal/ui/assets/icons/AmazonAWSIcon.png differ diff --git a/internal/ui/assets/icons/AmazonIcon.png b/internal/ui/assets/icons/AmazonIcon.png new file mode 100644 index 0000000..1ff92dc Binary files /dev/null and b/internal/ui/assets/icons/AmazonIcon.png differ diff --git a/internal/ui/assets/icons/AndroidIcon.png b/internal/ui/assets/icons/AndroidIcon.png new file mode 100644 index 0000000..f87bbf8 Binary files /dev/null and b/internal/ui/assets/icons/AndroidIcon.png differ diff --git a/internal/ui/assets/icons/AppleColorIcon.png b/internal/ui/assets/icons/AppleColorIcon.png new file mode 100644 index 0000000..a80facc Binary files /dev/null and b/internal/ui/assets/icons/AppleColorIcon.png differ diff --git a/internal/ui/assets/icons/AppleIcon.png b/internal/ui/assets/icons/AppleIcon.png new file mode 100644 index 0000000..6d93298 Binary files /dev/null and b/internal/ui/assets/icons/AppleIcon.png differ diff --git a/internal/ui/assets/icons/AppleWhiteIcon.png b/internal/ui/assets/icons/AppleWhiteIcon.png new file mode 100644 index 0000000..e1a684c Binary files /dev/null and b/internal/ui/assets/icons/AppleWhiteIcon.png differ diff --git a/internal/ui/assets/icons/ArcheAgeIcon.png b/internal/ui/assets/icons/ArcheAgeIcon.png new file mode 100644 index 0000000..ce67dd2 Binary files /dev/null and b/internal/ui/assets/icons/ArcheAgeIcon.png differ diff --git a/internal/ui/assets/icons/ArenaNetIcon.png b/internal/ui/assets/icons/ArenaNetIcon.png new file mode 100644 index 0000000..a620a81 Binary files /dev/null and b/internal/ui/assets/icons/ArenaNetIcon.png differ diff --git a/internal/ui/assets/icons/BattleNetAuthenticatorIcon.png b/internal/ui/assets/icons/BattleNetAuthenticatorIcon.png new file mode 100644 index 0000000..88eb0e0 Binary files /dev/null and b/internal/ui/assets/icons/BattleNetAuthenticatorIcon.png differ diff --git a/internal/ui/assets/icons/BitBucketIcon.png b/internal/ui/assets/icons/BitBucketIcon.png new file mode 100644 index 0000000..c07d9fb Binary files /dev/null and b/internal/ui/assets/icons/BitBucketIcon.png differ diff --git a/internal/ui/assets/icons/BitcoinEuroIcon.png b/internal/ui/assets/icons/BitcoinEuroIcon.png new file mode 100644 index 0000000..bf8c59d Binary files /dev/null and b/internal/ui/assets/icons/BitcoinEuroIcon.png differ diff --git a/internal/ui/assets/icons/BitcoinGoldIcon.png b/internal/ui/assets/icons/BitcoinGoldIcon.png new file mode 100644 index 0000000..57d43b2 Binary files /dev/null and b/internal/ui/assets/icons/BitcoinGoldIcon.png differ diff --git a/internal/ui/assets/icons/BitcoinIcon.png b/internal/ui/assets/icons/BitcoinIcon.png new file mode 100644 index 0000000..b3b5bb6 Binary files /dev/null and b/internal/ui/assets/icons/BitcoinIcon.png differ diff --git a/internal/ui/assets/icons/BluePixel.png b/internal/ui/assets/icons/BluePixel.png new file mode 100644 index 0000000..211fc35 Binary files /dev/null and b/internal/ui/assets/icons/BluePixel.png differ diff --git a/internal/ui/assets/icons/ChromeIcon.png b/internal/ui/assets/icons/ChromeIcon.png new file mode 100644 index 0000000..9c9f6f7 Binary files /dev/null and b/internal/ui/assets/icons/ChromeIcon.png differ diff --git a/internal/ui/assets/icons/DefianceIcon.png b/internal/ui/assets/icons/DefianceIcon.png new file mode 100644 index 0000000..8c20bcb Binary files /dev/null and b/internal/ui/assets/icons/DefianceIcon.png differ diff --git a/internal/ui/assets/icons/DiabloIcon.png b/internal/ui/assets/icons/DiabloIcon.png new file mode 100644 index 0000000..394e4fb Binary files /dev/null and b/internal/ui/assets/icons/DiabloIcon.png differ diff --git a/internal/ui/assets/icons/DigitalOceanIcon.png b/internal/ui/assets/icons/DigitalOceanIcon.png new file mode 100644 index 0000000..7c29084 Binary files /dev/null and b/internal/ui/assets/icons/DigitalOceanIcon.png differ diff --git a/internal/ui/assets/icons/DogeIcon.png b/internal/ui/assets/icons/DogeIcon.png new file mode 100644 index 0000000..b6b8ee0 Binary files /dev/null and b/internal/ui/assets/icons/DogeIcon.png differ diff --git a/internal/ui/assets/icons/DreamhostIcon.png b/internal/ui/assets/icons/DreamhostIcon.png new file mode 100644 index 0000000..7b70499 Binary files /dev/null and b/internal/ui/assets/icons/DreamhostIcon.png differ diff --git a/internal/ui/assets/icons/DropboxIcon.png b/internal/ui/assets/icons/DropboxIcon.png new file mode 100644 index 0000000..fb5e431 Binary files /dev/null and b/internal/ui/assets/icons/DropboxIcon.png differ diff --git a/internal/ui/assets/icons/DropboxWhiteIcon.png b/internal/ui/assets/icons/DropboxWhiteIcon.png new file mode 100644 index 0000000..2e1850e Binary files /dev/null and b/internal/ui/assets/icons/DropboxWhiteIcon.png differ diff --git a/internal/ui/assets/icons/EA2Icon.png b/internal/ui/assets/icons/EA2Icon.png new file mode 100644 index 0000000..913461a Binary files /dev/null and b/internal/ui/assets/icons/EA2Icon.png differ diff --git a/internal/ui/assets/icons/EA3Icon.png b/internal/ui/assets/icons/EA3Icon.png new file mode 100644 index 0000000..f4b9ce6 Binary files /dev/null and b/internal/ui/assets/icons/EA3Icon.png differ diff --git a/internal/ui/assets/icons/EAIcon.png b/internal/ui/assets/icons/EAIcon.png new file mode 100644 index 0000000..0e8a6bb Binary files /dev/null and b/internal/ui/assets/icons/EAIcon.png differ diff --git a/internal/ui/assets/icons/EvernoteIcon.png b/internal/ui/assets/icons/EvernoteIcon.png new file mode 100644 index 0000000..990e9cb Binary files /dev/null and b/internal/ui/assets/icons/EvernoteIcon.png differ diff --git a/internal/ui/assets/icons/FacebookIcon.png b/internal/ui/assets/icons/FacebookIcon.png new file mode 100644 index 0000000..d2fe5b7 Binary files /dev/null and b/internal/ui/assets/icons/FacebookIcon.png differ diff --git a/internal/ui/assets/icons/FirefallIcon.png b/internal/ui/assets/icons/FirefallIcon.png new file mode 100644 index 0000000..893c703 Binary files /dev/null and b/internal/ui/assets/icons/FirefallIcon.png differ diff --git a/internal/ui/assets/icons/FlickrIcon.png b/internal/ui/assets/icons/FlickrIcon.png new file mode 100644 index 0000000..313e007 Binary files /dev/null and b/internal/ui/assets/icons/FlickrIcon.png differ diff --git a/internal/ui/assets/icons/GMailIcon.png b/internal/ui/assets/icons/GMailIcon.png new file mode 100644 index 0000000..87cfb48 Binary files /dev/null and b/internal/ui/assets/icons/GMailIcon.png differ diff --git a/internal/ui/assets/icons/GitHub2Icon.png b/internal/ui/assets/icons/GitHub2Icon.png new file mode 100644 index 0000000..7e5e138 Binary files /dev/null and b/internal/ui/assets/icons/GitHub2Icon.png differ diff --git a/internal/ui/assets/icons/GitHubIcon.png b/internal/ui/assets/icons/GitHubIcon.png new file mode 100644 index 0000000..ab038f9 Binary files /dev/null and b/internal/ui/assets/icons/GitHubIcon.png differ diff --git a/internal/ui/assets/icons/GitIcon.png b/internal/ui/assets/icons/GitIcon.png new file mode 100644 index 0000000..d17417b Binary files /dev/null and b/internal/ui/assets/icons/GitIcon.png differ diff --git a/internal/ui/assets/icons/GitLabFox2Icon.png b/internal/ui/assets/icons/GitLabFox2Icon.png new file mode 100644 index 0000000..58d723a Binary files /dev/null and b/internal/ui/assets/icons/GitLabFox2Icon.png differ diff --git a/internal/ui/assets/icons/GitLabIcon.png b/internal/ui/assets/icons/GitLabIcon.png new file mode 100644 index 0000000..bb274f2 Binary files /dev/null and b/internal/ui/assets/icons/GitLabIcon.png differ diff --git a/internal/ui/assets/icons/Gitlab2Icon.png b/internal/ui/assets/icons/Gitlab2Icon.png new file mode 100644 index 0000000..0368451 Binary files /dev/null and b/internal/ui/assets/icons/Gitlab2Icon.png differ diff --git a/internal/ui/assets/icons/GitlabFoxIon.png b/internal/ui/assets/icons/GitlabFoxIon.png new file mode 100644 index 0000000..57992b0 Binary files /dev/null and b/internal/ui/assets/icons/GitlabFoxIon.png differ diff --git a/internal/ui/assets/icons/Google2Icon.png b/internal/ui/assets/icons/Google2Icon.png new file mode 100644 index 0000000..14de27f Binary files /dev/null and b/internal/ui/assets/icons/Google2Icon.png differ diff --git a/internal/ui/assets/icons/GoogleAuthenticator2Icon.png b/internal/ui/assets/icons/GoogleAuthenticator2Icon.png new file mode 100644 index 0000000..311a79b Binary files /dev/null and b/internal/ui/assets/icons/GoogleAuthenticator2Icon.png differ diff --git a/internal/ui/assets/icons/GoogleAuthenticatorIcon.png b/internal/ui/assets/icons/GoogleAuthenticatorIcon.png new file mode 100644 index 0000000..ca5ed4f Binary files /dev/null and b/internal/ui/assets/icons/GoogleAuthenticatorIcon.png differ diff --git a/internal/ui/assets/icons/GoogleIcon.png b/internal/ui/assets/icons/GoogleIcon.png new file mode 100644 index 0000000..c6826ee Binary files /dev/null and b/internal/ui/assets/icons/GoogleIcon.png differ diff --git a/internal/ui/assets/icons/IFTTT2Icon.png b/internal/ui/assets/icons/IFTTT2Icon.png new file mode 100644 index 0000000..107bfbd Binary files /dev/null and b/internal/ui/assets/icons/IFTTT2Icon.png differ diff --git a/internal/ui/assets/icons/IFTTT3Icon.png b/internal/ui/assets/icons/IFTTT3Icon.png new file mode 100644 index 0000000..c3ef073 Binary files /dev/null and b/internal/ui/assets/icons/IFTTT3Icon.png differ diff --git a/internal/ui/assets/icons/IFTTTIcon.png b/internal/ui/assets/icons/IFTTTIcon.png new file mode 100644 index 0000000..0a4fd34 Binary files /dev/null and b/internal/ui/assets/icons/IFTTTIcon.png differ diff --git a/internal/ui/assets/icons/InstagramIcon.png b/internal/ui/assets/icons/InstagramIcon.png new file mode 100644 index 0000000..cfc21c9 Binary files /dev/null and b/internal/ui/assets/icons/InstagramIcon.png differ diff --git a/internal/ui/assets/icons/ItchIcon.png b/internal/ui/assets/icons/ItchIcon.png new file mode 100644 index 0000000..3ef71f0 Binary files /dev/null and b/internal/ui/assets/icons/ItchIcon.png differ diff --git a/internal/ui/assets/icons/KickStarterIcon.png b/internal/ui/assets/icons/KickStarterIcon.png new file mode 100644 index 0000000..29b9568 Binary files /dev/null and b/internal/ui/assets/icons/KickStarterIcon.png differ diff --git a/internal/ui/assets/icons/LastPassIcon.png b/internal/ui/assets/icons/LastPassIcon.png new file mode 100644 index 0000000..fcbed3f Binary files /dev/null and b/internal/ui/assets/icons/LastPassIcon.png differ diff --git a/internal/ui/assets/icons/LinkedinIcon.png b/internal/ui/assets/icons/LinkedinIcon.png new file mode 100644 index 0000000..4ada56f Binary files /dev/null and b/internal/ui/assets/icons/LinkedinIcon.png differ diff --git a/internal/ui/assets/icons/LitecoinIcon.png b/internal/ui/assets/icons/LitecoinIcon.png new file mode 100644 index 0000000..5765ce1 Binary files /dev/null and b/internal/ui/assets/icons/LitecoinIcon.png differ diff --git a/internal/ui/assets/icons/MacIcon.png b/internal/ui/assets/icons/MacIcon.png new file mode 100644 index 0000000..e1ab6a6 Binary files /dev/null and b/internal/ui/assets/icons/MacIcon.png differ diff --git a/internal/ui/assets/icons/MicrosoftAuthenticatorIcon.png b/internal/ui/assets/icons/MicrosoftAuthenticatorIcon.png new file mode 100644 index 0000000..c6b139c Binary files /dev/null and b/internal/ui/assets/icons/MicrosoftAuthenticatorIcon.png differ diff --git a/internal/ui/assets/icons/NameIcon.png b/internal/ui/assets/icons/NameIcon.png new file mode 100644 index 0000000..baafb15 Binary files /dev/null and b/internal/ui/assets/icons/NameIcon.png differ diff --git a/internal/ui/assets/icons/OktaVerifyAuthenticatorIcon.png b/internal/ui/assets/icons/OktaVerifyAuthenticatorIcon.png new file mode 100644 index 0000000..980cdeb Binary files /dev/null and b/internal/ui/assets/icons/OktaVerifyAuthenticatorIcon.png differ diff --git a/internal/ui/assets/icons/OptionsIcon.png b/internal/ui/assets/icons/OptionsIcon.png new file mode 100644 index 0000000..cb5866c Binary files /dev/null and b/internal/ui/assets/icons/OptionsIcon.png differ diff --git a/internal/ui/assets/icons/OriginIcon.png b/internal/ui/assets/icons/OriginIcon.png new file mode 100644 index 0000000..51efcfd Binary files /dev/null and b/internal/ui/assets/icons/OriginIcon.png differ diff --git a/internal/ui/assets/icons/PayPalIcon.png b/internal/ui/assets/icons/PayPalIcon.png new file mode 100644 index 0000000..ade324b Binary files /dev/null and b/internal/ui/assets/icons/PayPalIcon.png differ diff --git a/internal/ui/assets/icons/RefreshIcon.png b/internal/ui/assets/icons/RefreshIcon.png new file mode 100644 index 0000000..1ba04d4 Binary files /dev/null and b/internal/ui/assets/icons/RefreshIcon.png differ diff --git a/internal/ui/assets/icons/RefreshIconWithLock.png b/internal/ui/assets/icons/RefreshIconWithLock.png new file mode 100644 index 0000000..322bf64 Binary files /dev/null and b/internal/ui/assets/icons/RefreshIconWithLock.png differ diff --git a/internal/ui/assets/icons/RiftIcon.png b/internal/ui/assets/icons/RiftIcon.png new file mode 100644 index 0000000..3e51d67 Binary files /dev/null and b/internal/ui/assets/icons/RiftIcon.png differ diff --git a/internal/ui/assets/icons/RuneScapeIcon.png b/internal/ui/assets/icons/RuneScapeIcon.png new file mode 100644 index 0000000..ec48a44 Binary files /dev/null and b/internal/ui/assets/icons/RuneScapeIcon.png differ diff --git a/internal/ui/assets/icons/SteamAuthenticatorIcon.png b/internal/ui/assets/icons/SteamAuthenticatorIcon.png new file mode 100644 index 0000000..d8ecb4c Binary files /dev/null and b/internal/ui/assets/icons/SteamAuthenticatorIcon.png differ diff --git a/internal/ui/assets/icons/SteamIcon.png b/internal/ui/assets/icons/SteamIcon.png new file mode 100644 index 0000000..0d6d4b1 Binary files /dev/null and b/internal/ui/assets/icons/SteamIcon.png differ diff --git a/internal/ui/assets/icons/Swtor.png b/internal/ui/assets/icons/Swtor.png new file mode 100644 index 0000000..d31781e Binary files /dev/null and b/internal/ui/assets/icons/Swtor.png differ diff --git a/internal/ui/assets/icons/SwtorEmpire.png b/internal/ui/assets/icons/SwtorEmpire.png new file mode 100644 index 0000000..8c30df7 Binary files /dev/null and b/internal/ui/assets/icons/SwtorEmpire.png differ diff --git a/internal/ui/assets/icons/SwtorRepublic.png b/internal/ui/assets/icons/SwtorRepublic.png new file mode 100644 index 0000000..d2eef00 Binary files /dev/null and b/internal/ui/assets/icons/SwtorRepublic.png differ diff --git a/internal/ui/assets/icons/TeamviewerIcon.png b/internal/ui/assets/icons/TeamviewerIcon.png new file mode 100644 index 0000000..5b0aefe Binary files /dev/null and b/internal/ui/assets/icons/TeamviewerIcon.png differ diff --git a/internal/ui/assets/icons/TextIcon.png b/internal/ui/assets/icons/TextIcon.png new file mode 100644 index 0000000..211e24c Binary files /dev/null and b/internal/ui/assets/icons/TextIcon.png differ diff --git a/internal/ui/assets/icons/Tumblr2Icon.png b/internal/ui/assets/icons/Tumblr2Icon.png new file mode 100644 index 0000000..ab13991 Binary files /dev/null and b/internal/ui/assets/icons/Tumblr2Icon.png differ diff --git a/internal/ui/assets/icons/Tumblr3Icon.png b/internal/ui/assets/icons/Tumblr3Icon.png new file mode 100644 index 0000000..9b479f4 Binary files /dev/null and b/internal/ui/assets/icons/Tumblr3Icon.png differ diff --git a/internal/ui/assets/icons/TumblrIcon.png b/internal/ui/assets/icons/TumblrIcon.png new file mode 100644 index 0000000..bdff13d Binary files /dev/null and b/internal/ui/assets/icons/TumblrIcon.png differ diff --git a/internal/ui/assets/icons/TwitterIcon.png b/internal/ui/assets/icons/TwitterIcon.png new file mode 100644 index 0000000..9689fe2 Binary files /dev/null and b/internal/ui/assets/icons/TwitterIcon.png differ diff --git a/internal/ui/assets/icons/WarcraftIcon.png b/internal/ui/assets/icons/WarcraftIcon.png new file mode 100644 index 0000000..20495bc Binary files /dev/null and b/internal/ui/assets/icons/WarcraftIcon.png differ diff --git a/internal/ui/assets/icons/WildstarIcon.png b/internal/ui/assets/icons/WildstarIcon.png new file mode 100644 index 0000000..87210d3 Binary files /dev/null and b/internal/ui/assets/icons/WildstarIcon.png differ diff --git a/internal/ui/assets/icons/WinAuth2Icon.png b/internal/ui/assets/icons/WinAuth2Icon.png new file mode 100644 index 0000000..9e7ee6e Binary files /dev/null and b/internal/ui/assets/icons/WinAuth2Icon.png differ diff --git a/internal/ui/assets/icons/WinAuthIcon.png b/internal/ui/assets/icons/WinAuthIcon.png new file mode 100644 index 0000000..565c77d Binary files /dev/null and b/internal/ui/assets/icons/WinAuthIcon.png differ diff --git a/internal/ui/assets/icons/Windows7Icon.png b/internal/ui/assets/icons/Windows7Icon.png new file mode 100644 index 0000000..4caad7c Binary files /dev/null and b/internal/ui/assets/icons/Windows7Icon.png differ diff --git a/internal/ui/assets/icons/Windows8Icon.png b/internal/ui/assets/icons/Windows8Icon.png new file mode 100644 index 0000000..6aea209 Binary files /dev/null and b/internal/ui/assets/icons/Windows8Icon.png differ diff --git a/internal/ui/assets/icons/WindowsPhoneIcon.png b/internal/ui/assets/icons/WindowsPhoneIcon.png new file mode 100644 index 0000000..0cc2262 Binary files /dev/null and b/internal/ui/assets/icons/WindowsPhoneIcon.png differ diff --git a/internal/ui/assets/icons/WordpressIcon.png b/internal/ui/assets/icons/WordpressIcon.png new file mode 100644 index 0000000..3235448 Binary files /dev/null and b/internal/ui/assets/icons/WordpressIcon.png differ diff --git a/internal/ui/assets/icons/WordpressWhiteIcon.png b/internal/ui/assets/icons/WordpressWhiteIcon.png new file mode 100644 index 0000000..1a556fa Binary files /dev/null and b/internal/ui/assets/icons/WordpressWhiteIcon.png differ diff --git a/internal/ui/assets/icons/YahooIcon.png b/internal/ui/assets/icons/YahooIcon.png new file mode 100644 index 0000000..0149728 Binary files /dev/null and b/internal/ui/assets/icons/YahooIcon.png differ diff --git a/internal/ui/assets/icons/battlenet.png b/internal/ui/assets/icons/battlenet.png new file mode 100644 index 0000000..bb42e38 Binary files /dev/null and b/internal/ui/assets/icons/battlenet.png differ diff --git a/internal/ui/assets/icons/eBayIcon.png b/internal/ui/assets/icons/eBayIcon.png new file mode 100644 index 0000000..1186ef2 Binary files /dev/null and b/internal/ui/assets/icons/eBayIcon.png differ diff --git a/internal/ui/assets/icons/padlock-closed.png b/internal/ui/assets/icons/padlock-closed.png new file mode 100644 index 0000000..8106495 Binary files /dev/null and b/internal/ui/assets/icons/padlock-closed.png differ diff --git a/internal/ui/assets/icons/padlock-open.png b/internal/ui/assets/icons/padlock-open.png new file mode 100644 index 0000000..45798e4 Binary files /dev/null and b/internal/ui/assets/icons/padlock-open.png differ diff --git a/internal/ui/convert.go b/internal/ui/convert.go index 9711ed5..fa610f2 100644 --- a/internal/ui/convert.go +++ b/internal/ui/convert.go @@ -11,10 +11,11 @@ import ( // in-memory authenticator plus its display name. The vendor string is // derived from the authenticator's Name() (which already returns // "google" / "microsoft" / "okta" / "hotp" / "battlenet" / "steam"). -func entryFromAuthenticator(name string, a authenticator.Authenticator, hotkey string) config.Entry { +func entryFromAuthenticator(name string, a authenticator.Authenticator, hotkey, iconKey string) config.Entry { return config.Entry{ Name: name, Vendor: a.Name(), + IconName: iconKey, SecretRaw: a.SecretData(), Hotkey: hotkey, } diff --git a/internal/ui/dialog_icon_picker.go b/internal/ui/dialog_icon_picker.go new file mode 100644 index 0000000..457216b --- /dev/null +++ b/internal/ui/dialog_icon_picker.go @@ -0,0 +1,137 @@ +package ui + +import ( + "image" + + "gioui.org/layout" + "gioui.org/op/clip" + "gioui.org/op/paint" + "gioui.org/unit" + "gioui.org/widget" + "gioui.org/widget/material" + + "git.wxccs.org/iceking2nd/winauth-go/internal/i18n" +) + +// iconPickerDialog shows the full catalog of brand icons in a scrollable +// grid. The user clicks an icon to select it, then OK to confirm. A +// fixed column count keeps row layout cheap (no FlowWrap needed). +type iconPickerDialog struct { + icons []string + selected string + clickables []widget.Clickable + list widget.List + okBtn widget.Clickable + cancelBtn widget.Clickable + cols int +} + +func newIconPickerDialog(currentKey string) *iconPickerDialog { + keys := allIconKeys() + d := &iconPickerDialog{ + icons: keys, + selected: currentKey, + clickables: make([]widget.Clickable, len(keys)), + cols: 6, + } + d.list.Axis = layout.Vertical + return d +} + +// iconExistsInCatalog reports whether key is in the published icon +// catalog (as opposed to the fallback vendor icons). +func iconExistsInCatalog(key string) bool { + for _, k := range allIconKeys() { + if k == key { + return true + } + } + return false +} + +func (d *iconPickerDialog) Layout( + gtx layout.Context, th *material.Theme, + onDone func(iconKey string, cancel bool), +) layout.Dimensions { + for i := range d.icons { + if d.clickables[i].Clicked(gtx) { + d.selected = d.icons[i] + } + } + if d.cancelBtn.Clicked(gtx) { + onDone("", true) + return layout.Dimensions{Size: gtx.Constraints.Max} + } + if d.okBtn.Clicked(gtx) { + onDone(d.selected, false) + return layout.Dimensions{Size: gtx.Constraints.Max} + } + + body := func(gtx layout.Context) layout.Dimensions { + // Cap body height so the picker scrolls instead of overflowing + // the screen on small windows. + maxH := gtx.Dp(unit.Dp(320)) + if gtx.Constraints.Max.Y > maxH { + gtx.Constraints.Max.Y = maxH + } + + cellDp := unit.Dp(48) + cellPx := gtx.Dp(cellDp) + cols := d.cols + totalRows := (len(d.icons) + cols - 1) / cols + + return material.List(th, &d.list).Layout(gtx, totalRows, func(gtx layout.Context, rowIdx int) layout.Dimensions { + start := rowIdx * cols + end := start + cols + if end > len(d.icons) { + end = len(d.icons) + } + rowIcons := d.icons[start:end] + + children := make([]layout.FlexChild, len(rowIcons)) + for j, key := range rowIcons { + idx := start + j + key := key + sel := key == d.selected + children[j] = layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return layout.Inset{ + Right: unit.Dp(4), Bottom: unit.Dp(4), + }.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + gtx.Constraints.Min = image.Pt(cellPx, cellPx) + gtx.Constraints.Max = image.Pt(cellPx, cellPx) + return d.clickables[idx].Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return drawIconCell(gtx, key, sel) + }) + }) + }) + } + return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, children...) + }) + } + + return modalCard(gtx, th, i18n.T("dialog_icon_picker_title"), + i18n.T("btn_ok"), i18n.T("btn_cancel"), + &d.okBtn, &d.cancelBtn, body) +} + +// drawIconCell paints one grid cell: optional selection background + +// the icon scaled to fit. Returns the fixed cell-size dimensions. +func drawIconCell(gtx layout.Context, key string, selected bool) layout.Dimensions { + size := gtx.Constraints.Min + if selected { + rect := clip.Rect{Max: size}.Push(gtx.Ops) + paint.ColorOp{Color: activePalette.RingBg}.Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + rect.Pop() + } + img := vendorIconFor(key) + if img != nil { + w := widget.Image{ + Src: paint.NewImageOp(img), + Fit: widget.Contain, + Position: layout.Center, + } + w.Layout(gtx) + } + return layout.Dimensions{Size: size} +} diff --git a/internal/ui/entry_ring.go b/internal/ui/entry_ring.go index e403993..e18d491 100644 --- a/internal/ui/entry_ring.go +++ b/internal/ui/entry_ring.go @@ -26,11 +26,16 @@ func entryProgressRing(gtx layout.Context, en *entry) layout.Dimensions { } vendor := en.Auth.Name() + // Prefer the user-chosen brand icon over the vendor placeholder. + iconKey := vendor + if en.IconKey != "" { + iconKey = en.IconKey + } if vendor == "hotp" { // HOTP has no period to visualise — just show the icon scaled // to the full slot so rows still align. - return drawCenteredIcon(gtx, vendor, ringDp, ringDp) + return drawCenteredIcon(gtx, iconKey, ringDp, ringDp) } period := totpPeriod(en.Auth) @@ -64,7 +69,7 @@ func entryProgressRing(gtx layout.Context, en *entry) layout.Dimensions { iconGtx.Constraints.Min = image.Pt(iconPx, iconPx) iconGtx.Constraints.Max = image.Pt(iconPx, iconPx) macro := op.Record(gtx.Ops) - paintIcon(iconGtx, vendor) + paintIcon(iconGtx, iconKey) call := macro.Stop() offset := image.Pt( (ringDims.Size.X-iconPx)/2, diff --git a/internal/ui/row_menu.go b/internal/ui/row_menu.go index 91a634c..0ffeeba 100644 --- a/internal/ui/row_menu.go +++ b/internal/ui/row_menu.go @@ -18,6 +18,7 @@ const ( rowActionDelete rowActionMoveUp rowActionMoveDown + rowActionChangeIcon ) // rowActionMenu is the small popup opened by the ⋯ button on each entry @@ -29,11 +30,12 @@ type rowActionMenu struct { canMoveUp bool canMoveDown bool - renameBtn widget.Clickable - deleteBtn widget.Clickable - moveUpBtn widget.Clickable - moveDownBtn widget.Clickable - cancelBtn widget.Clickable + renameBtn widget.Clickable + deleteBtn widget.Clickable + moveUpBtn widget.Clickable + moveDownBtn widget.Clickable + changeIconBtn widget.Clickable + cancelBtn widget.Clickable } func newRowActionMenu(idx, total int) *rowActionMenu { @@ -61,6 +63,8 @@ func (m *rowActionMenu) Pick(gtx layout.Context) (rowAction, bool) { return rowActionNone, false } return rowActionMoveDown, true + case m.changeIconBtn.Clicked(gtx): + return rowActionChangeIcon, true case m.cancelBtn.Clicked(gtx): return rowActionNone, true } @@ -102,6 +106,7 @@ func (m *rowActionMenu) Layout(gtx layout.Context, th *material.Theme) layout.Di layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout), row(&m.renameBtn, i18n.T("action_rename"), true), row(&m.deleteBtn, i18n.T("action_delete"), true), + row(&m.changeIconBtn, i18n.T("action_change_icon"), true), row(&m.moveUpBtn, i18n.T("action_move_up"), m.canMoveUp), row(&m.moveDownBtn, i18n.T("action_move_down"), m.canMoveDown), layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), diff --git a/internal/ui/vendor_icons.go b/internal/ui/vendor_icons.go index ff974ec..33ac3bb 100644 --- a/internal/ui/vendor_icons.go +++ b/internal/ui/vendor_icons.go @@ -5,20 +5,88 @@ import ( "embed" "image" _ "image/png" + "sort" + "strings" + "sync" "git.wxccs.org/iceking2nd/winauth-go/internal/global" ) -//go:embed assets/vendor_icons/*.png assets/app_icon.png +//go:embed assets/vendor_icons/*.png assets/icons/*.png assets/app_icon.png var assetsFS embed.FS +var ( + // iconCatalog holds all decoded brand-icons keyed by stem (filename + // without ".png"). Populated once on first access. + iconCatalog map[string]image.Image + iconCatalogOnce sync.Once + iconSortedKeys []string +) + +// loadIconCatalog decodes every PNG from assets/icons/ into iconCatalog. +// Also populates iconSortedKeys for the icon picker. +func loadIconCatalog() { + const fn = "internal.ui.loadIconCatalog" + iconCatalog = map[string]image.Image{} + + dir, err := assetsFS.ReadDir("assets/icons") + if err != nil { + global.Log.WithField("func", fn).WithError(err).Warn("read icons dir failed") + return + } + + var keys []string + for _, entry := range dir { + if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".png") { + continue + } + key := strings.TrimSuffix(entry.Name(), ".png") + data, err := assetsFS.ReadFile("assets/icons/" + entry.Name()) + if err != nil { + continue + } + img, _, err := image.Decode(bytes.NewReader(data)) + if err != nil { + global.Log.WithField("func", fn).WithError(err). + WithField("file", entry.Name()).Warn("icon decode failed") + continue + } + iconCatalog[key] = img + keys = append(keys, key) + } + sort.Strings(keys) + iconSortedKeys = keys + global.Log.WithField("func", fn).WithField("count", len(keys)).Debug("icons loaded") +} + +// allIconKeys returns the sorted list of available icon keys for the +// icon picker. Safe to call before icons are loaded — triggers load. +func allIconKeys() []string { + iconCatalogOnce.Do(loadIconCatalog) + return iconSortedKeys +} + +// iconByKey returns the named icon or nil. Safe to call before icons are +// loaded — triggers load. +func iconByKey(key string) image.Image { + iconCatalogOnce.Do(loadIconCatalog) + return iconCatalog[key] +} + +// --------------------------------------------------------------------------- +// Original vendor-icon fallbacks (assets/vendor_icons/). These are simple +// placeholder squares with a single initial — kept so old entries without an +// IconKey still render something recognisable. + // vendorIcons holds the decoded vendor PNGs keyed by the authenticator -// Name() return value. Built once at first lookup so we pay the decode -// cost once per process instead of per frame. +// Name() return value. Built once at first lookup. var vendorIcons = map[string]image.Image{} -func loadVendorIcons() { - const fn = "internal.ui.loadVendorIcons" +func ensureVendorIcons() { + const fn = "internal.ui.ensureVendorIcons" + if len(vendorIcons) > 0 { + return + } files := []string{"google", "microsoft", "okta", "hotp", "battlenet", "steam", "qr"} for _, name := range files { data, err := assetsFS.ReadFile("assets/vendor_icons/" + name + ".png") @@ -37,24 +105,28 @@ func loadVendorIcons() { } } -// vendorIconFor returns the icon image bound to the given authenticator -// vendor key ("google", "microsoft", ...). Falls back to the generic -// "qr" tile when no specific icon is registered, and nil when even that -// is unavailable (only happens if the embed is broken). -func vendorIconFor(vendor string) image.Image { - if len(vendorIcons) == 0 { - loadVendorIcons() +// vendorIconFor returns the icon image for the given key. Resolution +// order: +// 1. iconCatalog (full brand icons from assets/icons/) +// 2. vendorIcons (fallback placeholders from assets/vendor_icons/) +// 3. vendorIcons["qr"] (generic fallback) +// +// This preserves backward compatibility: callers that pass a vendor name +// ("google", "steam", …) still get their placeholder. +func vendorIconFor(key string) image.Image { + iconCatalogOnce.Do(loadIconCatalog) + ensureVendorIcons() + if img, ok := iconCatalog[key]; ok { + return img } - if img, ok := vendorIcons[vendor]; ok { + if img, ok := vendorIcons[key]; ok { return img } return vendorIcons["qr"] } // AppIcon returns the decoded application icon, or nil if the asset is -// missing. Currently unused at runtime — Gio v0.7 has no API to set a -// window icon, so this exists so callers (e.g. a future tray bucket) -// can reuse the same bitmap. +// missing. func AppIcon() image.Image { data, err := assetsFS.ReadFile("assets/app_icon.png") if err != nil {