summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--source/app/Fixtures.mc5
-rw-r--r--source/ui/CodeView.mc179
-rw-r--r--source/ui/Nav.mc4
3 files changed, 184 insertions, 4 deletions
diff --git a/source/app/Fixtures.mc b/source/app/Fixtures.mc
index 8ed35c9..114e879 100644
--- a/source/app/Fixtures.mc
+++ b/source/app/Fixtures.mc
@@ -46,10 +46,11 @@ module Fixtures {
vault.add(otp("Library", "frank", "Personal", Entry.OTP_TOTP, Hashes.SHA1, 7, 60, rfcKey, false));
var card = new Entry();
card.kind = Entry.KIND_CODE;
- card.type = 0;
+ card.type = Codes.EAN13;
card.name = "Gym card";
card.group = "Personal";
- card.payload = Bytes.fromUtf8("0000-1111-2222");
+ card.payload = Bytes.fromUtf8("590123412345"); // fake; the check digit 7 is added
+ card.options = Codes.DEFAULT_OPTIONS;
vault.add(card);
vault.lock();
if (TestMode.ON) {
diff --git a/source/ui/CodeView.mc b/source/ui/CodeView.mc
new file mode 100644
index 0000000..fc1c889
--- /dev/null
+++ b/source/ui/CodeView.mc
@@ -0,0 +1,179 @@
+// Author: cirakg & claude ai
+// Summary: Shows a barcode entry: black modules on a white screen at the largest whole module size that fits, with text.
+//
+// README 12.2:
+// * Always black on white (whatever the theme), quiet zone included.
+// * Linear codes use the widest horizontal band: on round screens the
+// band's narrower edge limits the width. The module size is the largest
+// whole number of pixels that fits the code plus 10 quiet modules on
+// each side; 1 px is allowed for linear codes (EAN-13 on 218 px screens
+// needs it, and the screenshot decode test covers it).
+// * Human-readable text under linear codes when enabled and there's room.
+// * Brightness: the backlight is turned on when shown and on every tap.
+// * Too big: a message with the size needed.
+// The payload is decrypted when the screen opens and wiped once encoded;
+// the module row stays in memory only while the screen is shown.
+
+import Toybox.Attention;
+import Toybox.Graphics;
+import Toybox.Lang;
+import Toybox.WatchUi;
+
+class CodeView extends WatchUi.View {
+
+ static const QUIET_MODULES = 10;
+ static const MIN_LINEAR_PX = 1;
+
+ var header as Entry;
+ private var _code as Codes.LinearCode or Codes.CodeError or Null = null;
+ private var _showText as Boolean = true;
+ private var _shownLogged as Boolean = false;
+
+ function initialize(entryHeader as Entry) {
+ View.initialize();
+ header = entryHeader;
+ }
+
+ function onShow() as Void {
+ _shownLogged = false;
+ encode();
+ boostBrightness();
+ }
+
+ function onHide() as Void {
+ _code = null;
+ }
+
+ private function encode() as Void {
+ try {
+ var entry = AppState.theVault().open(header.id);
+ var payload = Bytes.toUtf8(entry.payload as ByteArray);
+ _showText = (entry.options & Codes.OPT_SHOW_TEXT) != 0;
+ _code = payload != null ? Codes.encode(entry.type, payload, entry.options) : new Codes.CodeError("damaged text");
+ entry.wipeSecrets();
+ } catch (e instanceof VaultError) {
+ _code = new Codes.CodeError(e.code == VaultError.LOCKED ? "locked" : "damaged entry");
+ }
+ }
+
+ // Full backlight while a code is shown (AMOLED watches limit how long;
+ // the system then dims it, and a tap asks again).
+ function boostBrightness() as Void {
+ if (!AppState.options.fullBrightness || !(Attention has :backlight)) {
+ return;
+ }
+ try {
+ Attention.backlight(true);
+ } catch (e instanceof Lang.Exception) {
+ // BacklightOnTooLongException: the system's limit was reached.
+ }
+ }
+
+ function onUpdate(dc as Graphics.Dc) as Void {
+ var w = dc.getWidth();
+ var h = dc.getHeight();
+ dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_WHITE);
+ dc.clear();
+ var smallFont = Graphics.FONT_XTINY;
+ var smallH = dc.getFontHeight(smallFont);
+ var code = _code;
+ if (code instanceof Codes.LinearCode) {
+ drawLinear(dc, code, smallFont, smallH);
+ } else {
+ var message = code instanceof Codes.CodeError ? code.message : "…";
+ var lines = Ui.wrap(dc, message, smallFont, 2 * Ui.halfWidthAt(dc, h / 2) - 16);
+ var y = h / 2 - lines.size() * smallH / 2;
+ for (var i = 0; i < lines.size(); i++) {
+ dc.drawText(w / 2, y, smallFont, lines[i], Graphics.TEXT_JUSTIFY_CENTER);
+ y += smallH;
+ }
+ }
+ // Name at the top and ID at the bottom, in small print.
+ var top = Ui.isRound() ? h / 8 : 4;
+ dc.setColor(Graphics.COLOR_DK_GRAY, Graphics.COLOR_TRANSPARENT);
+ dc.drawText(w / 2, top, smallFont, Ui.fit(dc, header.name, smallFont, 2 * Ui.halfWidthAt(dc, top + smallH)),
+ Graphics.TEXT_JUSTIFY_CENTER);
+ var bottom = Ui.isRound() ? h - h / 8 - smallH : h - smallH - 4;
+ dc.drawText(w / 2, bottom, smallFont, Codes.name(header.type) + " · " + header.id, Graphics.TEXT_JUSTIFY_CENTER);
+ if (!_shownLogged && code != null) {
+ _shownLogged = true;
+ Diag.shown(code instanceof Codes.LinearCode ? "code" : "code_error");
+ }
+ }
+
+ // The bars in the widest band that fits, with text below when enabled.
+ private function drawLinear(dc as Graphics.Dc, code as Codes.LinearCode, font as Graphics.FontType, fontH as Number) as Void {
+ var w = dc.getWidth();
+ var h = dc.getHeight();
+ var modules = code.modules;
+ var total = modules.size() + 2 * QUIET_MODULES;
+ var barH = h * 30 / 100;
+ var textH = _showText ? fontH : 0;
+ var bandTop = (h - barH - textH) / 2;
+ var width = 2 * Ui.halfWidthBetween(dc, bandTop, bandTop + barH + textH) + (Ui.isRound() ? 12 : 8);
+ var px = width / total;
+ if (px < MIN_LINEAR_PX) {
+ var lines = Ui.wrap(dc, "Too large for this screen: needs " + total + " px, " + width + " px available",
+ font, 2 * Ui.halfWidthAt(dc, h / 2) - 16);
+ var y = h / 2 - lines.size() * fontH / 2;
+ for (var i = 0; i < lines.size(); i++) {
+ dc.drawText(w / 2, y, font, lines[i], Graphics.TEXT_JUSTIFY_CENTER);
+ y += fontH;
+ }
+ return;
+ }
+ var x0 = (w - modules.size() * px) / 2;
+ dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_WHITE);
+ var i = 0;
+ var n = modules.size();
+ while (i < n) {
+ if (modules[i] == 1) {
+ var start = i;
+ while (i < n && modules[i] == 1) {
+ i++;
+ }
+ dc.fillRectangle(x0 + start * px, bandTop, (i - start) * px, barH);
+ } else {
+ i++;
+ }
+ }
+ if (_showText) {
+ dc.drawText(w / 2, bandTop + barH + 2, font, code.text, Graphics.TEXT_JUSTIFY_CENTER);
+ }
+ }
+}
+
+// Input on a code: a tap asks for full brightness again; BACK closes.
+class CodeDelegate extends WatchUi.BehaviorDelegate {
+
+ function initialize() {
+ BehaviorDelegate.initialize();
+ }
+
+ function onTap(event as WatchUi.ClickEvent) as Boolean {
+ boost();
+ return true;
+ }
+
+ function onSelect() as Boolean {
+ boost();
+ return true;
+ }
+
+ private function boost() as Void {
+ AppState.activity();
+ var current = WatchUi.getCurrentView()[0];
+ if (current instanceof CodeView) {
+ current.boostBrightness();
+ }
+ }
+
+ function onBack() as Boolean {
+ var current = WatchUi.getCurrentView()[0];
+ if (current instanceof CodeView && current.header.locked && AppState.theVault().session.autoLockMs == 0) {
+ AppState.lock();
+ }
+ WatchUi.popView(WatchUi.SLIDE_RIGHT);
+ return true;
+ }
+}
diff --git a/source/ui/Nav.mc b/source/ui/Nav.mc
index 3e823e4..222386a 100644
--- a/source/ui/Nav.mc
+++ b/source/ui/Nav.mc
@@ -27,14 +27,14 @@ module Nav {
if (header.kind == Entry.KIND_OTP) {
return new OtpView(header);
}
- return new InfoView(header.name, ["Barcode display comes in a later version.", "ID " + header.id], "code");
+ return new CodeView(header);
}
function detailDelegate(header as Entry) as WatchUi.InputDelegate {
if (header.kind == Entry.KIND_OTP) {
return new OtpDelegate();
}
- return new InfoDelegate();
+ return new CodeDelegate();
}
// A simple text screen.