fedc198452
- 新增 tools/gen_icons 生成 7 张 vendor 占位 PNG + 256x256 应用图标 - internal/ui/vendor_icons.go 用 //go:embed 加载并按 Authenticator.Name() 查表 - entry 行圆环中心叠 16dp vendor 图标,HOTP 直接用图标占位 - 图标为通用色块 + 首字母,无第三方商标;用户可按文件名替换为真实 logo - assets/README 说明替换方式与 Windows .syso 嵌入步骤
120 lines
3.5 KiB
Go
120 lines
3.5 KiB
Go
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()
|
||
|
||
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)
|
||
}
|
||
|
||
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, vendor)
|
||
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
|
||
}
|