969600b6ee
- assets/icons/ 共 80+ 张真实品牌图标嵌入 UI 包 - entry 新增 IconKey 字段,持久化到 config.Entry.IconName - iconPickerDialog 网格布局图标选择器 - Add 完成后自动弹出图标选择器,Skip 则用 vendor 占位图 - ⋯ 菜单新增 "Change icon..." 项 - vendorIconFor 解析顺序:catalog → vendor 占位 → qr 兜底
141 lines
3.9 KiB
Go
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
|
|
}
|