aboutsummaryrefslogtreecommitdiff
path: root/source/ui/MainMenu.mc
diff options
context:
space:
mode:
Diffstat (limited to 'source/ui/MainMenu.mc')
-rw-r--r--source/ui/MainMenu.mc172
1 files changed, 172 insertions, 0 deletions
diff --git a/source/ui/MainMenu.mc b/source/ui/MainMenu.mc
new file mode 100644
index 0000000..b827784
--- /dev/null
+++ b/source/ui/MainMenu.mc
@@ -0,0 +1,172 @@
+// Author: cirakg & claude ai
+// Summary: The menu (README 13.1): lock now, PIN actions, settings results, entry IDs, clock status, about.
+//
+// A native Menu2, so scrolling, touch and buttons behave like every other
+// Garmin menu. Debug builds add the items from DebugMenu.
+
+import Toybox.Application;
+import Toybox.Lang;
+import Toybox.Time;
+import Toybox.Time.Gregorian;
+import Toybox.Timer;
+import Toybox.WatchUi;
+
+module MainMenu {
+
+ function build() as WatchUi.Menu2 {
+ var vault = AppState.theVault();
+ var menu = new WatchUi.Menu2({:title => "OTP Wallet"});
+ if (vault.session.isUnlocked()) {
+ menu.addItem(new WatchUi.MenuItem("Lock now", null, :lock, null));
+ }
+ menu.addItem(new WatchUi.MenuItem(vault.hasPin() ? "Change PIN" : "Set PIN", null, :pin, null));
+ if (vault.hasPin()) {
+ menu.addItem(new WatchUi.MenuItem("Reset PIN", null, :reset, null)); // the confirmation says what it erases
+ }
+ menu.addItem(new WatchUi.MenuItem("Settings results", null, :results, null));
+ menu.addItem(new WatchUi.MenuItem("Entry IDs", null, :ids, null));
+ menu.addItem(new WatchUi.MenuItem("Clock status", null, :clock, null));
+ menu.addItem(new WatchUi.MenuItem("About", null, :about, null));
+ DebugMenu.addItems(menu);
+ return menu;
+ }
+
+ function entryIdLines() as Array<String> {
+ var vault = AppState.theVault();
+ var ids = vault.ids();
+ var lines = [] as Array<String>;
+ for (var i = 0; i < ids.size(); i++) {
+ var header = vault.header(ids[i]);
+ var name = header == null ? "damaged" : (header.name.length() > 0 ? header.name : "Locked entry");
+ lines.add(ids[i] + " " + name);
+ }
+ if (lines.size() == 0) {
+ lines.add("No entries yet.");
+ }
+ var pending = vault.pendingIds().size();
+ if (pending > 0) {
+ lines.add(pending + " locked entries wait for your PIN.");
+ }
+ return lines;
+ }
+
+ function clockLines() as Array<String> {
+ var lines = [] as Array<String>;
+ var watch = Clock.nowUnix(0);
+ var info = Gregorian.utcInfo(new Time.Moment(watch.toNumber()), Time.FORMAT_SHORT);
+ lines.add("Watch: " + info.hour.format("%02d") + ":" + info.min.format("%02d") + ":" + info.sec.format("%02d") + " UTC");
+ var trusted = Clock.trustedUnix();
+ if (trusted == null) {
+ lines.add("GPS time: not synced yet. Codes use the watch clock.");
+ } else {
+ var diff = (watch - trusted).toNumber();
+ lines.add("GPS time: synced");
+ lines.add("Difference: " + diff + " s" + (diff.abs() > 15 ? " (codes may be wrong)" : ""));
+ }
+ lines.add("Time offset setting: " + AppState.options.timeOffset + " s");
+ return lines;
+ }
+
+ function aboutLines() as Array<String> {
+ var vault = AppState.theVault();
+ var lines = ["OTP Wallet " + AppInfo.VERSION] as Array<String>;
+ lines.add("Entries: " + vault.ids().size() + (vault.pendingIds().size() > 0 ? " (+" + vault.pendingIds().size() + " pending)" : ""));
+ lines.add("Storage: " + (storageBytes(vault) + 1023) / 1024 + " KB");
+ lines.add("Self-test: " + AppState.selfTestSummary);
+ lines.add("No network access: your data stays on this watch.");
+ return lines;
+ }
+
+ // Bytes the entry records take (the largest part of the store).
+ function storageBytes(vault as Vault) as Number {
+ var total = 0;
+ var ids = vault.ids();
+ for (var i = 0; i < ids.size(); i++) {
+ var record = Application.Storage.getValue("e." + ids[i]);
+ if (record instanceof Lang.ByteArray) {
+ total += record.size();
+ }
+ }
+ return total;
+ }
+}
+
+class MainMenuDelegate extends WatchUi.Menu2InputDelegate {
+
+ function initialize() {
+ Menu2InputDelegate.initialize();
+ }
+
+ function onSelect(item as WatchUi.MenuItem) as Void {
+ AppState.activity();
+ var id = item.getId();
+ var vault = AppState.theVault();
+ if (id == :lock) {
+ AppState.lock();
+ WatchUi.popView(WatchUi.SLIDE_DOWN);
+ } else if (id == :pin) {
+ if (!vault.hasPin()) {
+ WatchUi.switchToView(new PinView(PinView.MODE_CREATE, null), new PinDelegate(), WatchUi.SLIDE_LEFT);
+ } else if (vault.session.isUnlocked()) {
+ WatchUi.switchToView(new PinView(PinView.MODE_CHANGE, null), new PinDelegate(), WatchUi.SLIDE_LEFT);
+ } else {
+ var then = new ChangePinAfterUnlock();
+ WatchUi.switchToView(new PinView(PinView.MODE_UNLOCK, then.method(:run)), new PinDelegate(), WatchUi.SLIDE_LEFT);
+ }
+ } else if (id == :reset) {
+ WatchUi.pushView(new WatchUi.Confirmation("Erase locked entries and set a new PIN?"), new ResetPinConfirmation(),
+ WatchUi.SLIDE_LEFT);
+ } else if (id == :results) {
+ WatchUi.switchToView(new InfoView("Settings results",
+ ["Nothing yet. Results appear here after you add or change entries in the phone settings."], "results"),
+ new InfoDelegate(), WatchUi.SLIDE_LEFT);
+ } else if (id == :ids) {
+ WatchUi.switchToView(new InfoView("Entry IDs", MainMenu.entryIdLines(), "ids"), new InfoDelegate(), WatchUi.SLIDE_LEFT);
+ } else if (id == :clock) {
+ WatchUi.switchToView(new InfoView("Clock status", MainMenu.clockLines(), "clock"), new InfoDelegate(), WatchUi.SLIDE_LEFT);
+ } else if (id == :about) {
+ WatchUi.switchToView(new InfoView("About", MainMenu.aboutLines(), "about"), new InfoDelegate(), WatchUi.SLIDE_LEFT);
+ } else {
+ DebugMenu.handle(id);
+ }
+ }
+}
+
+// After unlocking from the menu's "Change PIN": ask for the new PIN.
+class ChangePinAfterUnlock {
+
+ function initialize() {
+ }
+
+ function run() as Void {
+ WatchUi.switchToView(new PinView(PinView.MODE_CHANGE, null), new PinDelegate(), WatchUi.SLIDE_LEFT);
+ }
+}
+
+// "Reset PIN" after the user confirmed. The confirmation closes itself after
+// onResponse, so the PIN screen opens from a short timer, replacing the menu.
+class ResetPinConfirmation extends WatchUi.ConfirmationDelegate {
+
+ // Kept so the one-shot timer isn't collected before it fires.
+ var timer as Timer.Timer? = null;
+
+ function initialize() {
+ ConfirmationDelegate.initialize();
+ }
+
+ function onResponse(response as WatchUi.Confirm) as Boolean {
+ if (response == WatchUi.CONFIRM_YES) {
+ AppState.theVault().resetPin();
+ AppState.theCodes().clear();
+ var t = new Timer.Timer();
+ t.start(method(:openCreatePin), 100, false);
+ timer = t;
+ }
+ return true;
+ }
+
+ function openCreatePin() as Void {
+ timer = null;
+ WatchUi.switchToView(new PinView(PinView.MODE_CREATE, null), new PinDelegate(), WatchUi.SLIDE_LEFT);
+ }
+}