// 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 { var vault = AppState.theVault(); var ids = vault.ids(); var lines = [] as Array; 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 { var lines = [] as Array; 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 { var vault = AppState.theVault(); var lines = ["OTP Wallet " + AppInfo.VERSION] as Array; 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("Results", Results.lines(), "results"), new InfoDelegate(), WatchUi.SLIDE_LEFT); Results.markSeen(); } 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); } }