// 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; } }