diff options
Diffstat (limited to 'source/ui/GlanceView.mc')
| -rw-r--r-- | source/ui/GlanceView.mc | 128 |
1 files changed, 120 insertions, 8 deletions
diff --git a/source/ui/GlanceView.mc b/source/ui/GlanceView.mc index a7207ce..4540f2a 100644 --- a/source/ui/GlanceView.mc +++ b/source/ui/GlanceView.mc @@ -1,23 +1,83 @@ // Author: cirakg & claude ai -// Summary: The glance (widget-list preview): app name and number of entries. +// Summary: The glance (widget-list preview): the app name and number of entries, or the opt-in favourite code with its countdown. // -// Glances run with very little memory, so this reads only the entry index -// from storage and opens no vault and no keys. The opt-in favourite code -// (README 13.1) comes with M8. +// Glances run with very little memory (64 KB), so this opens no vault: it +// reads the entry index, and with "Show the favourite code in the glance" +// on, the one entry the app sealed for it (GlanceCode). The code is +// computed once per period (one HMAC; SHA-512 once per period too) and +// redrawn every second for the countdown bar while the glance is visible. +// The secret is wiped when the glance is hidden. import Toybox.Application; import Toybox.Graphics; import Toybox.Lang; +import Toybox.Math; +import Toybox.System; +import Toybox.Timer; import Toybox.WatchUi; (:glance) class OtpGlanceView extends WatchUi.GlanceView { + private var _item as GlanceCode.Item? = null; + private var _timer as Timer.Timer? = null; + private var _counter as Long = -1l; + private var _code as String = ""; + private var _measured as Boolean = false; + function initialize() { GlanceView.initialize(); } + function onShow() as Void { + var item = GlanceCode.load(); + _item = item; + _counter = -1l; + if (item != null) { + try { + var timer = new Timer.Timer(); + timer.start(method(:onTick), 1000, true); + _timer = timer; + } catch (e instanceof Lang.Exception) { + // No timers here: the code still updates whenever the system redraws. + } + } + } + + function onHide() as Void { + var timer = _timer; + if (timer != null) { + timer.stop(); + } + _timer = null; + var item = _item; + if (item != null) { + item.wipe(); + } + _item = null; + _code = ""; + } + + function onTick() as Void { + WatchUi.requestUpdate(); + } + function onUpdate(dc as Graphics.Dc) as Void { + var item = _item; + if (item != null) { + drawCode(dc, item); + } else { + drawCount(dc); + } + // The simulator starts in the glance; launch scripts open the app from it. + if (!_measured) { + _measured = true; + Diag.glanceMemory(); + } + Diag.shown(item != null ? "glance_code" : "glance"); + } + + private function drawCount(dc as Graphics.Dc) as Void { var count = 0; var meta = Application.Storage.getValue("m"); if (meta instanceof Dictionary) { @@ -27,11 +87,63 @@ class OtpGlanceView extends WatchUi.GlanceView { } } var h = dc.getHeight(); + var top = h / 2 - dc.getFontHeight(Graphics.FONT_GLANCE); + var x = inset(dc, top); dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT); - dc.drawText(0, h / 2 - dc.getFontHeight(Graphics.FONT_GLANCE), Graphics.FONT_GLANCE, "OTP WALLET", Graphics.TEXT_JUSTIFY_LEFT); + dc.drawText(x, top, Graphics.FONT_GLANCE, "OTP WALLET", Graphics.TEXT_JUSTIFY_LEFT); dc.setColor(Graphics.COLOR_LT_GRAY, Graphics.COLOR_TRANSPARENT); - dc.drawText(0, h / 2, Graphics.FONT_GLANCE, count == 1 ? "1 entry" : count + " entries", Graphics.TEXT_JUSTIFY_LEFT); - // The simulator starts in the glance; launch scripts open the app from it. - Diag.shown("glance"); + dc.drawText(x, h / 2, Graphics.FONT_GLANCE, count == 1 ? "1 entry" : count + " entries", + Graphics.TEXT_JUSTIFY_LEFT); + } + + // How far right to start text whose top is at y, so a round screen's + // edge doesn't cut it. A glance doesn't know where it sits, so this + // assumes it is centred across the screen and starts at the top, which + // overestimates the cut (the Instinct 3's glance reaches into the + // screen's top-left curve; on most round watches this is 0 or a few + // pixels). + private function inset(dc as Graphics.Dc, y as Number) as Number { + var screen = System.getDeviceSettings(); + if (screen.screenShape != System.SCREEN_SHAPE_ROUND) { + return 0; + } + var r = screen.screenWidth / 2; + var dy = r - y; + var edge = r - Math.sqrt(r * r - dy * dy).toNumber(); + var left = (screen.screenWidth - dc.getWidth()) / 2; + return edge > left ? edge - left : 0; + } + + // The label, the code and a bar for the seconds left in the period. + private function drawCode(dc as Graphics.Dc, item as GlanceCode.Item) as Void { + var now = Clock.nowUnix(0); + var counter = item.counter(now); + if (counter != _counter) { + _code = GlanceCode.grouped(item.code(now)); + _counter = counter; + } + var w = dc.getWidth(); + var h = dc.getHeight(); + // The label ends at mid-height, like the entry count's first line: + // on some watches (Instinct 3) the glance's top corners are cut by + // the round screen, and a glance can't know where it sits. + var labelH = dc.getFontHeight(Graphics.FONT_GLANCE); + var codeFont = Graphics.FONT_GLANCE_NUMBER; + if (h / 2 + dc.getFontHeight(codeFont) + 5 > h) { + codeFont = Graphics.FONT_GLANCE; + } + var codeH = dc.getFontHeight(codeFont); + var top = h / 2 - labelH; + dc.setColor(Graphics.COLOR_LT_GRAY, Graphics.COLOR_TRANSPARENT); + // One inset for the block (the top line's), so the lines align. + var barX = inset(dc, top); + dc.drawText(barX, top, Graphics.FONT_GLANCE, item.label.toUpper(), Graphics.TEXT_JUSTIFY_LEFT); + dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT); + dc.drawText(barX, h / 2, codeFont, _code, Graphics.TEXT_JUSTIFY_LEFT); + var barY = h / 2 + codeH + 2; + dc.setColor(Graphics.COLOR_DK_GRAY, Graphics.COLOR_TRANSPARENT); + dc.fillRectangle(barX, barY, w - barX, 3); + dc.setColor(Graphics.COLOR_BLUE, Graphics.COLOR_TRANSPARENT); + dc.fillRectangle(barX, barY, (w - barX) * item.secondsLeft(now) / item.periodSeconds(), 3); } } |
