aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-10-20 11:24:58 +0200
committercirakg <cirakg@cirakg.xyz>2026-10-20 11:24:58 +0200
commitd94eb32c80d2dbf00a45d7e69804f8247240a769 (patch)
treed839d7ce999c2f3b73bcfcd3dd5a44f9364ccda7
parentcc2fa8917b65eeda8d222dcbd69e9342884bb545 (diff)
feat(ui): show Data Matrix and PDF417 codes; fit 2D codes inside round screens
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-rw-r--r--source/app/Fixtures.mc13
-rw-r--r--source/ui/CodeView.mc84
2 files changed, 66 insertions, 31 deletions
diff --git a/source/app/Fixtures.mc b/source/app/Fixtures.mc
index 0aa6ea9..0d6b683 100644
--- a/source/app/Fixtures.mc
+++ b/source/app/Fixtures.mc
@@ -61,6 +61,8 @@ module Fixtures {
qr.payload = Bytes.fromUtf8("https://example.com/member/fake-0042?name=Alice%20Example&card=0000111122223333&valid=2027-12-31&level=gold");
qr.options = Codes.DEFAULT_OPTIONS;
vault.add(qr);
+ vault.add(code("Parcel", Codes.DATAMATRIX, "PARCEL-FAKE-0042-7731"));
+ vault.add(code("Ticket", Codes.PDF417, "Boarding pass FAKE 0042 seat 12A"));
vault.lock();
if (TestMode.ON) {
startImport();
@@ -99,6 +101,17 @@ module Fixtures {
+ "\", \"info\": {\"secret\": \"" + secret + "\", \"algo\": \"SHA1\", \"digits\": " + digits + ", \"period\": 30}}";
}
+ function code(name as String, symbology as Number, payload as String) as Entry {
+ var e = new Entry();
+ e.kind = Entry.KIND_CODE;
+ e.type = symbology;
+ e.name = name;
+ e.group = "Personal";
+ e.payload = Bytes.fromUtf8(payload);
+ e.options = Codes.DEFAULT_OPTIONS;
+ return e;
+ }
+
function otp(name as String, account as String, group as String, type as Number, algorithm as Number,
digits as Number, period as Number, secret as String, locked as Boolean) as Entry {
var e = new Entry();
diff --git a/source/ui/CodeView.mc b/source/ui/CodeView.mc
index 2687d6c..88de402 100644
--- a/source/ui/CodeView.mc
+++ b/source/ui/CodeView.mc
@@ -8,8 +8,9 @@
// 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 use the largest module size at which the code fits inside the
+// circle (round screens) or the screen (Codes.fits): at least 2 px for
+// square codes, 1 px for PDF417 (a stacked linear code).
// * 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.
@@ -30,16 +31,17 @@ 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.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 _job as Codes.MatrixJob? = null;
private var _runner as Jobs.JobRunner? = null;
private var _ticker as Timer.Timer? = null;
private var _startedAt as Number = 0;
+ // The cache-saving one-shot timer (kept so it is not collected).
+ var saveTimer as Timer.Timer? = null;
function initialize(entryHeader as Entry) {
View.initialize();
@@ -73,8 +75,14 @@ class CodeView extends WatchUi.View {
_showText = (entry.options & Codes.OPT_SHOW_TEXT) != 0;
if (payload == null) {
_code = new Codes.CodeError("damaged text");
- } else if (entry.type == Codes.QR) {
- startQr(payload, Codes.ecLevel(entry.options));
+ } else if (Codes.isMatrix(entry.type)) {
+ var screen = System.getDeviceSettings();
+ var job = Codes.matrixJob(entry.type, payload, entry.options, screen.screenWidth, screen.screenHeight, Ui.isRound());
+ if (job != null) {
+ startJob(job);
+ } else {
+ _code = new Codes.CodeError(Codes.name(entry.type) + " is not supported yet");
+ }
} else {
_code = Codes.encode(entry.type, payload, entry.options);
}
@@ -84,9 +92,8 @@ class CodeView extends WatchUi.View {
}
}
- private function startQr(payload as String, ec as Number) as Void {
+ private function startJob(job as Codes.MatrixJob) 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;
@@ -107,22 +114,36 @@ class CodeView extends WatchUi.View {
return;
}
var err = job.error;
- var result = job.result;
- if (err != null || result == null) {
+ var matrix = job.matrix();
+ if (err != null || matrix == 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);
+ Diag.data("matrix_encoded", "type=" + header.type + " size=" + matrix.width + "x" + matrix.height
+ + " ms=" + (System.getTimer() - _startedAt));
_code = matrix;
- try {
- AppState.theVault().saveCodeCache(header.id, matrix.toBytes());
- } catch (e instanceof VaultError) {
- // Locked meanwhile: no cache this time.
- }
+ // Sealing and storing the cache gets its own timer event: the
+ // job's last step already used much of this event's budget.
+ var t = new Timer.Timer();
+ t.start(method(:saveCache), 50, false);
+ saveTimer = t;
}
WatchUi.requestUpdate();
}
+ // Store the encoded matrix sealed in the vault (one-shot timer).
+ function saveCache() as Void {
+ saveTimer = null;
+ var code = _code;
+ if (!(code instanceof Codes.Matrix)) {
+ return;
+ }
+ try {
+ AppState.theVault().saveCodeCache(header.id, code.toBytes());
+ } catch (e instanceof VaultError) {
+ // Locked meanwhile: no cache this time.
+ }
+ }
+
private function stopJob() as Void {
var runner = _runner;
if (runner != null) {
@@ -229,27 +250,28 @@ class CodeView extends WatchUi.View {
}
}
- // A 2D code in the largest centred square (or rectangle) that fits.
+ // A 2D code at the largest module size that fits (Codes.fits: inside the
+ // circle on round screens), centred.
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;
+ var totalH = code.drawnHeight() + 2 * code.quiet;
+ var minPx = Codes.minModulePx(header.type);
+ var px = 0;
+ for (var p = 16; p >= minPx && px == 0; p--) {
+ if (Codes.fits(totalW, totalH, p, w, h, Ui.isRound())) {
+ px = p;
+ }
}
- 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);
+ if (px == 0) {
+ drawMessage(dc, "Too large for this screen: needs " + minPx + " px per module, " + totalW * minPx + " x "
+ + totalH * minPx + " px", font, fontH);
return;
}
var x0 = (w - code.width * px) / 2;
- var y0 = (h - code.height * px) / 2;
+ var y0 = (h - code.drawnHeight() * px) / 2;
+ var rowPx = code.rowHeight * px;
dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_WHITE);
var m = code.modules;
for (var y = 0; y < code.height; y++) {
@@ -261,7 +283,7 @@ class CodeView extends WatchUi.View {
while (x < code.width && m[row + x] == 1) {
x++;
}
- dc.fillRectangle(x0 + start * px, y0 + y * px, (x - start) * px, px);
+ dc.fillRectangle(x0 + start * px, y0 + y * rowPx, (x - start) * px, rowPx);
} else {
x++;
}