Files
iceking2nd 969600b6ee feat(ui): 接入 vendor 真实图标库 + 图标选择器
- assets/icons/ 共 80+ 张真实品牌图标嵌入 UI 包
- entry 新增 IconKey 字段,持久化到 config.Entry.IconName
- iconPickerDialog 网格布局图标选择器
- Add 完成后自动弹出图标选择器,Skip 则用 vendor 占位图
- ⋯ 菜单新增 "Change icon..." 项
- vendorIconFor 解析顺序:catalog → vendor 占位 → qr 兜底
2026-06-12 09:41:30 +08:00

141 lines
3.9 KiB
Go

package ui
import (
"bytes"
"embed"
"image"
_ "image/png"
"sort"
"strings"
"sync"
"git.wxccs.org/iceking2nd/winauth-go/internal/global"
)
//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.
var vendorIcons = map[string]image.Image{}
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")
if err != nil {
global.Log.WithField("func", fn).WithError(err).
WithField("vendor", name).Warn("vendor icon missing from embed")
continue
}
img, _, err := image.Decode(bytes.NewReader(data))
if err != nil {
global.Log.WithField("func", fn).WithError(err).
WithField("vendor", name).Warn("vendor icon decode failed")
continue
}
vendorIcons[name] = img
}
}
// 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[key]; ok {
return img
}
return vendorIcons["qr"]
}
// AppIcon returns the decoded application icon, or nil if the asset is
// missing.
func AppIcon() image.Image {
data, err := assetsFS.ReadFile("assets/app_icon.png")
if err != nil {
return nil
}
img, _, err := image.Decode(bytes.NewReader(data))
if err != nil {
return nil
}
return img
}