diff options
| author | cirakg <cirakg@cirakg.xyz> | 2026-10-20 01:51:05 +0200 |
|---|---|---|
| committer | cirakg <cirakg@cirakg.xyz> | 2026-10-20 01:51:05 +0200 |
| commit | d537f4e154a40075405b0ab2fed14fbe2eeaac81 (patch) | |
| tree | f1fb9b148871901dda96fca11401d6f4d0ee1d59 /source/settings | |
| parent | 40d9a1d5c3e85486aea964c1446402a3be6ec05a (diff) | |
feat(settings): apply the Import form from the phone and show results
Import group in the app settings; the processor runs after the first
frame and on settings changes, clears the fields, writes the status,
remembers form hashes and opens the results screen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'source/settings')
| -rw-r--r-- | source/settings/Results.mc | 43 | ||||
| -rw-r--r-- | source/settings/SettingsProcessor.mc | 150 |
2 files changed, 193 insertions, 0 deletions
diff --git a/source/settings/Results.mc b/source/settings/Results.mc new file mode 100644 index 0000000..37c932f --- /dev/null +++ b/source/settings/Results.mc @@ -0,0 +1,43 @@ +// Author: cirakg & claude ai +// Summary: The latest settings results (README 10.3): kept in storage for the results screen, with an unseen flag for the list. + +import Toybox.Application; +import Toybox.Lang; + +module Results { + + const KEY = "res"; + const MAX_LINES = 60; + + // Replace the stored results with these lines (marked unseen). + function save(lines as Array<String>) as Void { + var kept = lines.size() > MAX_LINES ? lines.slice(0, MAX_LINES) as Array<String> : lines; + Application.Storage.setValue(KEY, {"l" => kept, "u" => true} as Dictionary<Application.Storage.KeyType, Application.Storage.ValueType>); + } + + // The stored lines, or a short explanation when there are none. + function lines() as Array<String> { + var stored = Application.Storage.getValue(KEY); + if (stored instanceof Dictionary && stored["l"] instanceof Array) { + return stored["l"] as Array<String>; + } + return ["Nothing yet. Results appear here after you import or change entries in the phone settings."]; + } + + function unseen() as Boolean { + var stored = Application.Storage.getValue(KEY); + return stored instanceof Dictionary && isTrue(stored["u"]); + } + + function markSeen() as Void { + var stored = Application.Storage.getValue(KEY); + if (stored instanceof Dictionary && isTrue(stored["u"])) { + stored["u"] = false; + Application.Storage.setValue(KEY, stored as Dictionary<Application.Storage.KeyType, Application.Storage.ValueType>); + } + } + + function isTrue(value as Application.Storage.ValueType) as Boolean { + return value instanceof Boolean && value; + } +} diff --git a/source/settings/SettingsProcessor.mc b/source/settings/SettingsProcessor.mc new file mode 100644 index 0000000..4c0a429 --- /dev/null +++ b/source/settings/SettingsProcessor.mc @@ -0,0 +1,150 @@ +// Author: cirakg & claude ai +// Summary: Applies the phone's settings forms on the watch (README 10.3); in M5 the Import form. +// +// Rules (README 10.3): +// * Runs from onSettingsChanged and at every start, after the first frame. +// * A form is applied only when its switch ("Import now") is on. Afterwards +// the watch turns the switch off and clears the text fields (they hold +// secrets) and writes a short result to the status field. +// * Idempotency: a hash of every applied form is remembered (the last 32), +// so the same form sent again does nothing. +// * The import runs as a job on a timer; when it ends, the results are +// stored and the results screen opens. +// Only the full app runs this, never the glance. + +import Toybox.Application; +import Toybox.Lang; +import Toybox.System; +import Toybox.WatchUi; + +class SettingsProcessor { + + static const TEXT_FIELDS = 8; + static const FORM_LOG_KEY = "fh"; + static const FORM_LOG_SIZE = 32; + static const STATUS_MAX_CHARS = 200; + + private var _job as Importer? = null; + // The job's timer driver (kept so it isn't collected while it runs). + var runner as Jobs.JobRunner? = null; + private var _formHash as String = ""; + private var _startedAt as Number = 0; + + function initialize() { + } + + function isBusy() as Boolean { + return _job != null; + } + + // "Importing… 40%" while an import runs, else null. + function busyText() as String? { + var job = _job; + return job != null ? "Importing… " + job.percent() + "%" : null; + } + + // Look at the forms and start whatever they ask for. + function start() as Void { + if (_job != null || AppState.vault == null) { + return; + } + if (!isTrue(prop("impApply"))) { + return; + } + var mode = prop("impMode"); + var locked = isTrue(prop("impLocked")); + var importMode = mode instanceof Number && mode >= Importer.MODE_APPEND && mode <= Importer.MODE_CANCEL ? mode : Importer.MODE_APPEND; + var vault = AppState.theVault(); + if (importMode == Importer.MODE_CANCEL) { + ImportBuffer.clear(vault.storage()); + finish(["Waiting import cancelled."], "Waiting import cancelled", false); + return; + } + var text = ""; + for (var i = 1; i <= TEXT_FIELDS; i++) { + var part = prop("impText" + i); + if (part instanceof String) { + text += part; + } + } + _formHash = formHash(importMode + "|" + locked + "|" + text); + if (alreadyApplied(_formHash)) { + finish(["This import was already applied. Change the text to import again."], "Already imported", false); + return; + } + _startedAt = System.getTimer(); + var job = new Importer(vault, text, importMode, locked, Clock.nowUnix(0)); + _job = job; + var r = new Jobs.JobRunner(job, 1, method(:onImportDone)); + runner = r; + r.start(); + WatchUi.requestUpdate(); + } + + function onImportDone() as Void { + var job = _job as Importer; + _job = null; + runner = null; + var report = job.report; + Diag.importDone(report.imported, System.getTimer() - _startedAt); + if (report.failure == null && report.waitingChars < 0) { + rememberApplied(_formHash); + } + AppState.theCodes().clear(); + finish(report.lines(), report.summary(), true); + } + + // Clear the form, write the status, store and show the results. + private function finish(lines as Array<String>, status as String, clearText as Boolean) as Void { + if (clearText) { + for (var i = 1; i <= TEXT_FIELDS; i++) { + setProp("impText" + i, ""); + } + } + setProp("impApply", false); + setProp("impStatus", Text.clip(status, STATUS_MAX_CHARS)); + Results.save(lines); + WatchUi.pushView(new InfoView("Results", lines, "results"), new InfoDelegate(), WatchUi.SLIDE_UP); + Results.markSeen(); + } + + private function isTrue(value as Application.Properties.ValueType or Null) as Boolean { + return value instanceof Boolean && value; + } + + private function prop(key as String) as Application.Properties.ValueType or Null { + try { + return Application.Properties.getValue(key); + } catch (e instanceof Lang.Exception) { + return null; + } + } + + private function setProp(key as String, value as Application.Properties.ValueType) as Void { + try { + Application.Properties.setValue(key, value); + } catch (e instanceof Lang.Exception) { + // A property the phone hasn't got yet: nothing to clear. + } + } + + // Short hash of a form's content (SHA-256, first 8 bytes, hex). + static function formHash(content as String) as String { + return Bytes.toHex(Hashes.digest(Hashes.SHA256, Bytes.fromUtf8(content)).slice(0, 8)); + } + + private function alreadyApplied(hash as String) as Boolean { + var log = Application.Storage.getValue(FORM_LOG_KEY); + return log instanceof Array && log.indexOf(hash) >= 0; + } + + private function rememberApplied(hash as String) as Void { + var stored = Application.Storage.getValue(FORM_LOG_KEY); + var log = stored instanceof Array ? stored as Array<Application.Storage.ValueType> : [] as Array<Application.Storage.ValueType>; + log.add(hash); + if (log.size() > FORM_LOG_SIZE) { + log = log.slice(log.size() - FORM_LOG_SIZE, null) as Array<Application.Storage.ValueType>; + } + Application.Storage.setValue(FORM_LOG_KEY, log); + } +} |
