diff options
Diffstat (limited to 'source')
| -rw-r--r-- | source/app/Fixtures.mc | 5 | ||||
| -rw-r--r-- | source/ui/CodeView.mc | 179 | ||||
| -rw-r--r-- | source/ui/Nav.mc | 4 |
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. |
