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

125 lines
3.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package ui
import (
"image"
"time"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/paint"
"gioui.org/unit"
"gioui.org/widget"
"git.wxccs.org/iceking2nd/winauth-go/internal/authenticator"
)
// entryProgressRing draws the per-row TOTP countdown ring with the
// vendor icon nested in the centre. For HOTP entries the ring is
// omitted (no countdown to show) and the icon fills the same slot so
// rows still line up.
func entryProgressRing(gtx layout.Context, en *entry) layout.Dimensions {
const ringDp = 24
const iconDp = 16
if en.Auth == nil {
return layout.Dimensions{Size: image.Pt(gtx.Dp(ringDp), gtx.Dp(ringDp))}
}
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, iconKey, ringDp, ringDp)
}
period := totpPeriod(en.Auth)
if period <= 0 {
period = authenticator.DefaultPeriod
}
now := time.Now().Unix()
elapsed := now % int64(period)
remaining := int64(period) - elapsed
progress := float32(remaining) / float32(period)
fg := activePalette.RingFg
if remaining <= 5 {
fg = activePalette.RingFgWarn
}
bg := activePalette.RingBg
ringDims := progressRing{
Size: unit.Dp(ringDp),
Stroke: unit.Dp(2.5),
Progress: progress,
Color: fg,
BgColor: bg,
}.Layout(gtx)
// Overlay the vendor icon in the centre of the same slot. We do
// this by recording the icon op into a fixed-size context, then
// offsetting it so its centre matches the ring's centre.
iconPx := gtx.Dp(iconDp)
iconGtx := gtx
iconGtx.Constraints.Min = image.Pt(iconPx, iconPx)
iconGtx.Constraints.Max = image.Pt(iconPx, iconPx)
macro := op.Record(gtx.Ops)
paintIcon(iconGtx, iconKey)
call := macro.Stop()
offset := image.Pt(
(ringDims.Size.X-iconPx)/2,
(ringDims.Size.Y-iconPx)/2,
)
st := op.Offset(offset).Push(gtx.Ops)
call.Add(gtx.Ops)
st.Pop()
return ringDims
}
// drawCenteredIcon paints a vendor icon scaled to fit a w×h slot. Used
// for HOTP entries that have no ring around them.
func drawCenteredIcon(gtx layout.Context, vendor string, wDp, hDp unit.Dp) layout.Dimensions {
wPx, hPx := gtx.Dp(wDp), gtx.Dp(hDp)
gtx.Constraints.Min = image.Pt(wPx, hPx)
gtx.Constraints.Max = image.Pt(wPx, hPx)
return paintIcon(gtx, vendor)
}
// paintIcon draws the vendor icon scaled to fit the current
// constraints. Returns its layout dimensions so callers can compose it
// inside a Flex. Returns a zero-sized layout if the icon is missing.
func paintIcon(gtx layout.Context, vendor string) layout.Dimensions {
img := vendorIconFor(vendor)
if img == nil {
return layout.Dimensions{Size: gtx.Constraints.Min}
}
w := widget.Image{
Src: paint.NewImageOp(img),
Fit: widget.Contain,
Position: layout.Center,
}
// Match one source pixel to one device pixel scaled by Fit. We
// don't want device-DPI scaling on top of widget.Fit's own
// scaling, so leave Scale at its 1.0 default.
return w.Layout(gtx)
}
// totpPeriod extracts the configured period from any authenticator whose
// underlying Base we can reach. Returns 0 if the authenticator does not
// expose one (in which case callers fall back to the default).
func totpPeriod(a authenticator.Authenticator) int {
type periodGetter interface{ GetPeriod() int }
if pg, ok := a.(periodGetter); ok {
return pg.GetPeriod()
}
// All current TOTP-like authenticators (Google/Microsoft/Okta/Steam/
// BattleNet) embed authenticator.Base whose default period is 30s,
// matching the C# original. Hard-code that here.
return authenticator.DefaultPeriod
}