// Author: cirakg & claude ai // 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 (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) { var ids = meta["ids"]; if (ids instanceof Array) { count = ids.size(); } } var h = dc.getHeight(); var top = h / 2 - dc.getFontHeight(Graphics.FONT_GLANCE); var x = inset(dc, top); // In capitals the name reaches the round screen's edge on the // Forerunner 70 and 965, which cuts the last letter; the narrower // mixed case fits there. var title = "OTP WALLET"; if (x + dc.getTextWidthInPixels(title, Graphics.FONT_GLANCE) > dc.getWidth() - edgeCut(top)) { title = "OTP Wallet"; } dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT); dc.drawText(x, top, Graphics.FONT_GLANCE, title, Graphics.TEXT_JUSTIFY_LEFT); dc.setColor(Graphics.COLOR_LT_GRAY, Graphics.COLOR_TRANSPARENT); 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 left = (System.getDeviceSettings().screenWidth - dc.getWidth()) / 2; var edge = edgeCut(y); return edge > left ? edge - left : 0; } // How far a round screen's edge reaches in from each side at height y // (0 on other shapes), with the same assumption: the glance starts at // the top of the screen. The right end of a glance reaches the screen's // edge, so this is also what the curve cuts off there. private function edgeCut(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; return r - Math.sqrt(r * r - dy * dy).toNumber(); } // 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); } }