summaryrefslogtreecommitdiff
path: root/source/ui/GlanceView.mc
diff options
context:
space:
mode:
Diffstat (limited to 'source/ui/GlanceView.mc')
-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);
}
}