diff options
Diffstat (limited to 'source/ui/CodeView.mc')
| -rw-r--r-- | source/ui/CodeView.mc | 179 |
1 files changed, 179 insertions, 0 deletions
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; + } +} |
