1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
|
// 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);
}
}
|