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 }