summaryrefslogtreecommitdiff
path: root/source/ui
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-10-21 03:55:10 +0200
committercirakg <cirakg@cirakg.xyz>2026-10-21 03:55:10 +0200
commit5e8ac1e2f5a660e55ea132996e1d5730cfa4fa64 (patch)
tree9bb99bdca1ef8f33c93c5100816448681329d810 /source/ui
parent88f1cac4fa530fc30fd6ef01970af11c7a959b8e (diff)
feat(glance): show the favourite code in the glance (opt-in)
With "Show the favourite code in the glance" on, the glance shows the code of the first favourite TOTP or Steam entry without the PIN, with a countdown bar. The glance can't open the vault, so the About scan seals that entry under "g" (device key, aad OTPW-GL-1) after every change, and removes it when the option is off or no entry qualifies; the glance also reads the option itself. HKDF moves to its own module so the glance loads only the crypto it needs (37 % of its 64 KB). Manage entry gains "Mark as favourite (glance)" and "Remove favourite", queued for locked entries like the other changes. Lines are inset on round screens so the Instinct 3's curved glance corner doesn't cut them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'source/ui')
-rw-r--r--source/ui/GlanceView.mc128
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);
}
}