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 }