diff options
Diffstat (limited to 'source/ui/CodeView.mc')
| -rw-r--r-- | source/ui/CodeView.mc | 177 |
1 files changed, 152 insertions, 25 deletions
diff --git a/source/ui/CodeView.mc b/source/ui/CodeView.mc index fc1c889..2687d6c 100644 --- a/source/ui/CodeView.mc +++ b/source/ui/CodeView.mc @@ -1,5 +1,5 @@ // 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. +// Summary: Shows a barcode entry: black modules on white at the largest whole module size that fits, linear or 2D. // // README 12.2: // * Always black on white (whatever the theme), quiet zone included. @@ -8,26 +8,38 @@ // 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). +// * 2D codes use the largest centred square (round screens) or the +// screen's shorter side, at least 2 px per module. +// * 2D codes are encoded once, as a job on a timer ("Encoding... 40%"), +// and cached sealed with the entry's key (Vault.saveCodeCache), so the +// next opening is instant. // * 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. +// The payload is decrypted when the screen opens and wiped once encoded. import Toybox.Attention; import Toybox.Graphics; import Toybox.Lang; +import Toybox.System; +import Toybox.Timer; import Toybox.WatchUi; class CodeView extends WatchUi.View { static const QUIET_MODULES = 10; static const MIN_LINEAR_PX = 1; + static const MIN_MATRIX_PX = 2; + static const QR_QUIET = 4; var header as Entry; - private var _code as Codes.LinearCode or Codes.CodeError or Null = null; + private var _code as Codes.LinearCode or Codes.Matrix or Codes.CodeError or Null = null; private var _showText as Boolean = true; private var _shownLogged as Boolean = false; + private var _job as Qr.QrJob? = null; + private var _runner as Jobs.JobRunner? = null; + private var _ticker as Timer.Timer? = null; + private var _startedAt as Number = 0; function initialize(entryHeader as Entry) { View.initialize(); @@ -41,21 +53,90 @@ class CodeView extends WatchUi.View { } function onHide() as Void { + stopJob(); _code = null; } private function encode() as Void { + var vault = AppState.theVault(); try { - var entry = AppState.theVault().open(header.id); + if (Codes.isMatrix(header.type)) { + var cached = vault.loadCodeCache(header.id); + var matrix = cached != null ? Codes.Matrix.fromBytes(cached) : null; + if (matrix != null) { + _code = matrix; + return; + } + } + var entry = vault.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"); + if (payload == null) { + _code = new Codes.CodeError("damaged text"); + } else if (entry.type == Codes.QR) { + startQr(payload, Codes.ecLevel(entry.options)); + } else { + _code = Codes.encode(entry.type, payload, entry.options); + } entry.wipeSecrets(); } catch (e instanceof VaultError) { _code = new Codes.CodeError(e.code == VaultError.LOCKED ? "locked" : "damaged entry"); } } + private function startQr(payload as String, ec as Number) as Void { + _startedAt = System.getTimer(); + var job = new Qr.QrJob(payload, ec, 0, -1); + _job = job; + var runner = new Jobs.JobRunner(job, 1, method(:onEncoded)); + _runner = runner; + runner.start(); + var ticker = new Timer.Timer(); + ticker.start(method(:onTick), 250, true); + _ticker = ticker; + } + + function onTick() as Void { + WatchUi.requestUpdate(); + } + + function onEncoded() as Void { + var job = _job; + stopJob(); + if (job == null) { + return; + } + var err = job.error; + var result = job.result; + if (err != null || result == null) { + _code = new Codes.CodeError(err != null ? err : "could not encode"); + } else { + Diag.data("qr_encoded", "version=" + result.version + " ms=" + (System.getTimer() - _startedAt)); + var matrix = new Codes.Matrix(result.size, result.size, QR_QUIET, result.modules); + _code = matrix; + try { + AppState.theVault().saveCodeCache(header.id, matrix.toBytes()); + } catch (e instanceof VaultError) { + // Locked meanwhile: no cache this time. + } + } + WatchUi.requestUpdate(); + } + + private function stopJob() as Void { + var runner = _runner; + if (runner != null) { + runner.cancel(); + } + var ticker = _ticker; + if (ticker != null) { + ticker.stop(); + } + _runner = null; + _ticker = null; + _job = null; + } + // 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 { @@ -77,27 +158,39 @@ class CodeView extends WatchUi.View { var smallFont = Graphics.FONT_XTINY; var smallH = dc.getFontHeight(smallFont); var code = _code; + var job = _job; if (code instanceof Codes.LinearCode) { drawLinear(dc, code, smallFont, smallH); + } else if (code instanceof Codes.Matrix) { + drawMatrix(dc, code, smallFont, smallH); + } else if (job != null) { + dc.drawText(w / 2, h / 2 - smallH / 2, smallFont, "Encoding… " + job.percent() + "%", Graphics.TEXT_JUSTIFY_CENTER); } 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; - } + drawMessage(dc, code instanceof Codes.CodeError ? code.message : "…", smallFont, smallH); } - // Name at the top and ID at the bottom, in small print. - var top = Ui.isRound() ? h / 8 : 4; + // Name at the top and type and ID at the bottom, in small print. + var top = Ui.isRound() ? h / 10 : 2; 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); + var bottom = Ui.isRound() ? h - h / 10 - smallH : h - smallH - 2; + dc.drawText(w / 2, bottom, smallFont, Ui.fit(dc, Codes.name(header.type) + " · " + header.id, smallFont, + 2 * Ui.halfWidthAt(dc, bottom)), Graphics.TEXT_JUSTIFY_CENTER); if (!_shownLogged && code != null) { _shownLogged = true; - Diag.shown(code instanceof Codes.LinearCode ? "code" : "code_error"); + Diag.shown(code instanceof Codes.CodeError ? "code_error" : "code"); + } + } + + private function drawMessage(dc as Graphics.Dc, message as String, font as Graphics.FontType, fontH as Number) as Void { + var w = dc.getWidth(); + var h = dc.getHeight(); + dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_TRANSPARENT); + var lines = Ui.wrap(dc, message, 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; } } @@ -113,13 +206,7 @@ class CodeView extends WatchUi.View { 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; - } + drawMessage(dc, "Too large for this screen: needs " + total + " px, " + width + " px available", font, fontH); return; } var x0 = (w - modules.size() * px) / 2; @@ -141,6 +228,46 @@ class CodeView extends WatchUi.View { dc.drawText(w / 2, bandTop + barH + 2, font, code.text, Graphics.TEXT_JUSTIFY_CENTER); } } + + // A 2D code in the largest centred square (or rectangle) that fits. + private function drawMatrix(dc as Graphics.Dc, code as Codes.Matrix, font as Graphics.FontType, fontH as Number) as Void { + var w = dc.getWidth(); + var h = dc.getHeight(); + // Round screens: the largest square inside the circle (side w/sqrt 2); + // others: the shorter side, minus room for the name and ID lines. + var areaW = Ui.isRound() ? w * 707 / 1000 : w - 4; + var areaH = Ui.isRound() ? h * 707 / 1000 : h - 2 * fontH - 8; + var totalW = code.width + 2 * code.quiet; + var totalH = code.height + 2 * code.quiet; + var px = areaW / totalW; + if (areaH / totalH < px) { + px = areaH / totalH; + } + if (px < MIN_MATRIX_PX) { + drawMessage(dc, "Too large for this screen: needs " + MIN_MATRIX_PX + " px per module, a " + + totalW * MIN_MATRIX_PX + " px square", font, fontH); + return; + } + var x0 = (w - code.width * px) / 2; + var y0 = (h - code.height * px) / 2; + dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_WHITE); + var m = code.modules; + for (var y = 0; y < code.height; y++) { + var row = y * code.width; + var x = 0; + while (x < code.width) { + if (m[row + x] == 1) { + var start = x; + while (x < code.width && m[row + x] == 1) { + x++; + } + dc.fillRectangle(x0 + start * px, y0 + y * px, (x - start) * px, px); + } else { + x++; + } + } + } + } } // Input on a code: a tap asks for full brightness again; BACK closes. |
