From 35042ff04ffc50e55ff59a385849442be4429d6b Mon Sep 17 00:00:00 2001 From: cirakg Date: Mon, 19 Oct 2026 22:28:09 +0200 Subject: feat(ui): add the list, OTP detail, PIN entry, menu, info screens and glance List with live codes and countdowns, adaptive rows and lazy headers; OTP detail with ring or bar, next code and HOTP next; PIN keypad or button selector with create, unlock, change, lockout display and pending promotion; native menu; glance with the entry count; self-test after the first frame; option properties with defaults. Codes are computed one per timer event, apart from drawing, for the watchdog. A UI-test build (uitest.jungle) starts with fake entries, PIN 123456 and a frozen clock. Co-Authored-By: Claude Opus 5.5 --- source/ui/InfoView.mc | 122 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 source/ui/InfoView.mc (limited to 'source/ui/InfoView.mc') diff --git a/source/ui/InfoView.mc b/source/ui/InfoView.mc new file mode 100644 index 0000000..3b75764 --- /dev/null +++ b/source/ui/InfoView.mc @@ -0,0 +1,122 @@ +// Author: cirakg & claude ai +// Summary: A scrollable text screen with a title: settings results, entry IDs, clock status, about, errors. + +import Toybox.Graphics; +import Toybox.Lang; +import Toybox.WatchUi; + +class InfoView extends WatchUi.View { + + private var _title as String; + private var _text as Array; + private var _screen as String; + private var _lines as Array? = null; // wrapped, on first draw + private var _first as Number = 0; + private var _visible as Number = 1; + private var _shownLogged as Boolean = false; + + // screen names the view in "DIAG shown" lines for UI scripts. + function initialize(title as String, text as Array, screen as String) { + View.initialize(); + _title = title; + _text = text; + _screen = screen; + } + + function onShow() as Void { + _shownLogged = false; + } + + // Scroll by delta lines; returns false when already at that end. + function scroll(delta as Number) as Boolean { + var lines = _lines; + if (lines == null) { + return false; + } + var max = lines.size() - _visible > 0 ? lines.size() - _visible : 0; + var next = _first + delta; + next = next < 0 ? 0 : (next > max ? max : next); + if (next == _first) { + return false; + } + _first = next; + WatchUi.requestUpdate(); + return true; + } + + function onUpdate(dc as Graphics.Dc) as Void { + Ui.clear(dc); + var w = dc.getWidth(); + var h = dc.getHeight(); + var font = Graphics.FONT_XTINY; + var lineH = dc.getFontHeight(font); + var y = Ui.isRound() ? h / 8 : 6; + dc.setColor(Ui.accent(), Graphics.COLOR_TRANSPARENT); + dc.drawText(w / 2, y, Graphics.FONT_SMALL, Ui.fit(dc, _title, Graphics.FONT_SMALL, 2 * Ui.halfWidthAt(dc, y)), + Graphics.TEXT_JUSTIFY_CENTER); + y += dc.getFontHeight(Graphics.FONT_SMALL) + 4; + var bottom = Ui.isRound() ? h - h / 10 : h - 4; + var lines = _lines; + if (lines == null) { + // Wrap to the narrowest width the text area has. + var width = 2 * Ui.halfWidthBetween(dc, y, bottom) - 8; + lines = [] as Array; + for (var i = 0; i < _text.size(); i++) { + lines.addAll(Ui.wrap(dc, _text[i], font, width)); + } + _lines = lines; + } + _visible = (bottom - y) / lineH; + dc.setColor(Ui.fg(), Graphics.COLOR_TRANSPARENT); + for (var i = _first; i < lines.size() && i < _first + _visible; i++) { + dc.drawText(w / 2, y, font, lines[i], Graphics.TEXT_JUSTIFY_CENTER); + y += lineH; + } + if (_first + _visible < lines.size()) { + var cx = w / 2; + dc.setColor(Ui.dim(), Graphics.COLOR_TRANSPARENT); + dc.fillPolygon([[cx - 6, bottom], [cx + 6, bottom], [cx, bottom + 6]] as Array); + } + if (!_shownLogged) { + _shownLogged = true; + Diag.shown(_screen); + } + } +} + +// Input on a text screen: scroll, BACK closes (and leaves the app when this +// is the only screen, as for start-up errors). +class InfoDelegate extends WatchUi.BehaviorDelegate { + + function initialize() { + BehaviorDelegate.initialize(); + } + + private function view() as InfoView? { + var current = WatchUi.getCurrentView()[0]; + return current instanceof InfoView ? current : null; + } + + function onNextPage() as Boolean { + AppState.activity(); + var v = view(); + if (v != null) { + v.scroll(3); + } + return true; + } + + function onPreviousPage() as Boolean { + AppState.activity(); + var v = view(); + if (v != null) { + v.scroll(-3); + } + return true; + } + + function onBack() as Boolean { + AppState.activity(); + return false; + } +} -- cgit v1.2.3