From e11a905171b47abbe7f8e23a9976e4c1887c0af3 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Fri, 31 Jan 2025 19:15:03 -0600 Subject: [PATCH 01/67] Use Triangle Timer as a basis for new countdown timer app --- apps/tevtimer/alarm.js | 66 +++++ apps/tevtimer/app-icon.js | 1 + apps/tevtimer/app.js | 545 ++++++++++++++++++++++++++++++++++++ apps/tevtimer/app.png | Bin 0 -> 819 bytes apps/tevtimer/app.xcf | Bin 0 -> 2112 bytes apps/tevtimer/lib.js | 462 ++++++++++++++++++++++++++++++ apps/tevtimer/metadata.json | 21 ++ 7 files changed, 1095 insertions(+) create mode 100644 apps/tevtimer/alarm.js create mode 100644 apps/tevtimer/app-icon.js create mode 100644 apps/tevtimer/app.js create mode 100644 apps/tevtimer/app.png create mode 100644 apps/tevtimer/app.xcf create mode 100644 apps/tevtimer/lib.js create mode 100644 apps/tevtimer/metadata.json diff --git a/apps/tevtimer/alarm.js b/apps/tevtimer/alarm.js new file mode 100644 index 000000000..677cf775b --- /dev/null +++ b/apps/tevtimer/alarm.js @@ -0,0 +1,66 @@ +const tt = require('triangletimer'); + +function showAlarm(alarm) { + const settings = require("sched").getSettings(); + const tri_timer = tt.TIMERS[alarm.data.idx]; + const message = tt.format_triangle(tri_timer) + '\n' + alarm.msg; + + Bangle.loadWidgets(); + Bangle.drawWidgets(); + + // buzzCount should really be called buzzRepeat, so subtract 1 + let buzzCount = tri_timer.buzz_count - 1; + + tt.update_system_alarms(); + + E.showPrompt(message, { + title: 'Triangle timer', + buttons: { "Goto": true, "OK": false } + }).then(function (go) { + buzzCount = 0; + + Bangle.emit("alarmDismiss", alarm); + + if (go) { + console.log('alarm ' + alarm.data.idx); + tt.set_last_viewed_timer(tri_timer); + load('triangletimer.app.js'); + } else { + load(); + } + }); + + function buzz() { + if (settings.unlockAtBuzz) { + Bangle.setLocked(false); + } + + const pattern = tri_timer.vibrate_pattern || settings.defaultTimerPattern; + console.log('buzz: ' + pattern); + console.log('buzzCount: ' + buzzCount); + require("buzz").pattern(pattern).then(() => { + if (buzzCount == null || buzzCount--) { + setTimeout(buzz, settings.buzzIntervalMillis); + } else if (alarm.as) { // auto-snooze + // buzzCount should really be called buzzRepeat, so subtract 1 + buzzCount = tri_timer.buzz_count - 1; + setTimeout(buzz, settings.defaultSnoozeMillis); + } + }); + } + + if ((require("Storage").readJSON("setting.json", 1) || {}).quiet > 1) + return; + + buzz(); +} + +let alarms = require("sched").getAlarms(); +let active = require("sched").getActiveAlarms(alarms); +if (active.length) { + // if there's an alarm, show it + showAlarm(active[0]); +} else { + // otherwise just go back to default app + setTimeout(load, 100); +} diff --git a/apps/tevtimer/app-icon.js b/apps/tevtimer/app-icon.js new file mode 100644 index 000000000..2299fb6c4 --- /dev/null +++ b/apps/tevtimer/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("lEo4cC///A4IDC/Hgg/g/8f/H/g9JkmSAQORAgYCEAYcipPPnnzAQoOCpGSgN58+eAQoOComAgIdKHAMAgI7KxJlDBxNIgESBwO69euAQhWBoAOD69duoCEBwKSEDpAaCHZVIyCxFBw+AhIOEtu27YCDQYIONbwwOOHYwODg3AAgSnGBwfbAgUyBokgFIcbsEAgYOKJAOA7YOGiVJBwUN23YHYoOFgE2AQIIBBwZoGAAMf/4ACBxxoDBxSVHHYwOJgEJBxsOYgTgHBwz+HBw+QHZlJiQ8JggOCkQOJg4sBBwNAOIJ0CAQYODklIBxuJSQgEDJQckyK2DBwcJlMiBwWSRIIOFpRHB7YOCpGSYQtSBwoFBpL1BgMkygICBwgCBIggXDA==")) diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js new file mode 100644 index 000000000..095f2c3c3 --- /dev/null +++ b/apps/tevtimer/app.js @@ -0,0 +1,545 @@ +const Layout = require('Layout'); + +const tt = require('triangletimer'); + +// UI // + +class TimerView { + constructor(tri_timer) { + this.tri_timer = tri_timer; + + this.layout = null; + this.listeners = {}; + this.timer_timeout = null; + } + + start() { + this._initLayout(); + this.layout.clear(); + this.render(); + tt.set_last_viewed_timer(this.tri_timer); + let render_status = () => { this.render(); }; + this.tri_timer.on('status', render_status); + this.tri_timer.on('auto-pause', tt.set_timers_dirty); + this.listeners.status = render_status; + + // Touch handler + function touchHandler(button, xy) { + for (var id of ['row1', 'row2', 'row3']) { + const elem = this.layout[id]; + if (!xy.type && + elem.x <= xy.x && xy.x < elem.x + elem.w && + elem.y <= xy.y && xy.y < elem.y + elem.h) { + Bangle.buzz(50, 0.5); + tt.SETTINGS.view_mode = (tt.SETTINGS.view_mode + 1) % 4; + tt.schedule_save_settings(); + setTimeout(this.render.bind(this), 0); + break; + } + } + } + this.listeners.touch = touchHandler.bind(this); + Bangle.on('touch', this.listeners.touch); + + // Physical button handler + this.listeners.button = setWatch( + this.start_stop_timer.bind(this), + BTN, + {edge: 'falling', debounce: 50, repeat: true} + ); + } + + stop() { + if (this.timer_timeout !== null) { + clearTimeout(this.timer_timeout); + this.timer_timeout = null; + } + this.tri_timer.removeListener('status', this.listeners.status); + Bangle.removeListener('touch', this.listeners.touch); + clearWatch(this.listeners.button); + Bangle.setUI(); + } + + _initLayout() { + const layout = new Layout( + { + type: 'v', + bgCol: g.theme.bg, + c: [ + { + type: 'txt', + id: 'row1', + label: '8888', + font: 'Vector:56x42', + fillx: 1, + }, + { + type: 'txt', + id: 'row2', + label: '8888', + font: 'Vector:56x56', + fillx: 1, + }, + { + type: 'txt', + id: 'row3', + label: '', + font: '12x20', + fillx: 1, + }, + { + type: 'h', + id: 'buttons', + c: [ + {type: 'btn', font: '6x8:2', fillx: 1, label: 'St/Pa', id: 'start_btn', + cb: this.start_stop_timer.bind(this)}, + {type: 'btn', font: '6x8:2', fillx: 1, label: 'Menu', id: 'menu_btn', + cb: () => { + switch_UI(new TimerViewMenu(this.tri_timer)); + } + } + ] + } + ] + } + ); + this.layout = layout; + } + + render(item) { + console.debug('render called: ' + item); + this.tri_timer.check_auto_pause(); + + if (!item) { + this.layout.update(); + } + + if (!item || item == 'timer') { + + let timer_as_linear = this.tri_timer.get(); + if (timer_as_linear < 0) { + // Handle countdown timer expiration + timer_as_linear = 0; + setTimeout(() => { this.render('status'); }, 0); + } + const timer_as_tri = tt.as_triangle( + timer_as_linear, this.tri_timer.increment); + + var label1, label2, font1, font2; + if (tt.SETTINGS.view_mode == 0) { + label1 = timer_as_tri[0]; + label2 = Math.ceil(timer_as_tri[1]); + font1 = 'Vector:56x42'; + font2 = 'Vector:56x56'; + } else if (tt.SETTINGS.view_mode == 1) { + label1 = timer_as_tri[0]; + label2 = Math.ceil(timer_as_tri[0] - timer_as_tri[1]); + font1 = 'Vector:56x42'; + font2 = 'Vector:56x56'; + } else if (tt.SETTINGS.view_mode == 2) { + label1 = tt.format_triangle(this.tri_timer); + let ttna = this.tri_timer.time_to_next_event(); + if (ttna !== null) { + label2 = tt.format_duration(ttna, true); + } else { + label2 = '--:--:--'; + } + font1 = 'Vector:30x42'; + font2 = 'Vector:34x56'; + } else if (tt.SETTINGS.view_mode == 3) { + label1 = timer_as_tri[0]; + let ttna = this.tri_timer.time_to_next_event(); + if (ttna !== null) { + label2 = tt.format_duration(ttna, false); + } else { + label2 = '--:--'; + } + font1 = 'Vector:56x42'; + font2 = 'Vector:48x56'; + } + + if (label1 !== this.layout.row1.label) { + this.layout.row1.label = label1; + this.layout.row1.font = font1; + this.layout.clear(this.layout.row1); + this.layout.render(this.layout.row1); + } + + if (label2 !== this.layout.row2.label) { + this.layout.row2.label = label2; + this.layout.row2.font = font2; + this.layout.clear(this.layout.row2); + this.layout.render(this.layout.row2); + } + + } + + if (!item || item == 'status') { + this.layout.start_btn.label = + this.tri_timer.is_running() ? 'Pause' : 'Start'; + this.layout.render(this.layout.buttons); + + this.layout.row3.label = + this.tri_timer.display_status() + + ' ' + this.tri_timer.provisional_name(); + this.layout.clear(this.layout.row3); + this.layout.render(this.layout.row3); + } + + if (this.tri_timer.is_running() && this.tri_timer.get() > 0) { + if (this.timer_timeout) { + clearTimeout(this.timer_timeout); + this.timer_timeout = null; + } + + // Calculate approximate time next display update is needed. + // Usual case: update when numbers change once per second. + let next_tick = this.tri_timer.get() % 1; + if (this.tri_timer.rate > 0) { + next_tick = 1 - next_tick; + } + // Convert next_tick from seconds to milliseconds and add + // compensating factor of 50ms due to timeouts apparently + // sometimes triggering too early. + next_tick = next_tick / Math.abs(this.tri_timer.rate) + 50; + + // For slow-update view mode, only update about every 60 + // seconds instead of every second + if (tt.SETTINGS.view_mode == 3) { + console.debug(this.tri_timer.time_to_next_event()); + next_tick = this.tri_timer.time_to_next_event() % 60000; + } + + console.debug('Next render update scheduled in ' + next_tick); + this.timer_timeout = setTimeout( + () => { this.timer_timeout = null; this.render('timer'); }, + next_tick + ); + } + } + + start_stop_timer() { + if (this.tri_timer.is_running()) { + this.tri_timer.pause(); + } else { + this.tri_timer.start(); + } + tt.set_timers_dirty(); + } +} + + +class TimerViewMenu { + constructor(tri_timer) { + this.tri_timer = tri_timer; + } + + start() { + this.top_menu(); + } + + stop() { + E.showMenu(); + } + + back() { + switch_UI(new TimerView(this.tri_timer)); + } + + top_menu() { + const top_menu = { + '': { + title: this.tri_timer.display_name(), + back: this.back.bind(this) + }, + 'Reset': () => { E.showMenu(reset_menu); }, + 'Timers': () => { + switch_UI(new TimerMenu(tt.TIMERS, this.tri_timer)); + }, + 'Edit': this.edit_menu.bind(this), + 'Add': () => { + tt.set_timers_dirty(); + const new_timer = tt.add_tri_timer(tt.TIMERS, this.tri_timer); + const timer_view_menu = new TimerViewMenu(new_timer); + timer_view_menu.edit_menu(); + }, + 'Delete': () => { E.showMenu(delete_menu); }, + }; + if (tt.TIMERS.length <= 1) { + // Prevent user deleting last timer + delete top_menu.Delete; + } + + const reset_menu = { + '': { + title: 'Confirm reset', + back: () => { E.showMenu(top_menu); } + }, + 'Reset': () => { + this.tri_timer.reset(); + tt.set_timers_dirty(); + this.back(); + }, + 'Cancel': () => { E.showMenu(top_menu); }, + }; + + const delete_menu = { + '': { + title: 'Confirm delete', + back: () => { E.showMenu(top_menu); } + }, + 'Delete': () => { + tt.set_timers_dirty(); + switch_UI(new TimerView(tt.delete_tri_timer(tt.TIMERS, this.tri_timer))); + }, + 'Cancel': () => { E.showMenu(top_menu); }, + }; + + E.showMenu(top_menu); + } + + edit_menu() { + const edit_menu = { + '': { + title: 'Edit: ' + this.tri_timer.display_name(), + back: () => { this.top_menu(); }, + }, + 'Direction': { + value: this.tri_timer.rate >= 0, + format: v => (v ? 'Up' : 'Down'), + onchange: v => { + this.tri_timer.rate = -this.tri_timer.rate; + tt.set_timers_dirty(); + } + }, + 'Start (Tri)': this.edit_start_tri_menu.bind(this), + 'Start (HMS)': this.edit_start_hms_menu.bind(this), + 'Increment': { + value: this.tri_timer.increment, + min: 1, + max: 9999, + step: 1, + wrap: true, + onchange: v => { + this.tri_timer.increment = v; + tt.set_timers_dirty(); + }, + }, + 'Events': this.edit_events_menu.bind(this), + }; + + E.showMenu(edit_menu); + } + + edit_start_tri_menu() { + let origin_tri = tt.as_triangle( + this.tri_timer.origin, this.tri_timer.increment); + + const edit_start_tri_menu = { + '': { + title: 'Start (Tri)', + back: this.edit_menu.bind(this), + }, + 'Outer': { + value: origin_tri[0], + min: 0, + max: Math.floor(9999 / this.tri_timer.increment) + * this.tri_timer.increment, + step: this.tri_timer.increment, + wrap: true, + noList: true, + onchange: v => { + origin_tri[0] = v; + edit_start_tri_menu.Inner.max = origin_tri[0]; + origin_tri[1] = (this.tri_timer.rate >= 0) ? + 1 : origin_tri[0]; + edit_start_tri_menu.Inner.value = origin_tri[1]; + this.tri_timer.origin = tt.as_linear( + origin_tri, this.tri_timer.increment + ); + tt.set_timers_dirty(); + } + }, + 'Inner': { + value: origin_tri[1], + min: 0, + max: origin_tri[0], + step: 1, + wrap: true, + noList: true, + onchange: v => { + origin_tri[1] = v; + this.tri_timer.origin = tt.as_linear( + origin_tri, this.tri_timer.increment + ); + tt.set_timers_dirty(); + } + }, + }; + + E.showMenu(edit_start_tri_menu); + } + + edit_start_hms_menu() { + let origin_hms = { + h: Math.floor(this.tri_timer.origin / 3600), + m: Math.floor(this.tri_timer.origin / 60) % 60, + s: Math.floor(this.tri_timer.origin % 60), + }; + + const update_origin = () => { + this.tri_timer.origin = origin_hms.h * 3600 + + origin_hms.m * 60 + + origin_hms.s; + }; + + const edit_start_hms_menu = { + '': { + title: 'Start (HMS)', + back: this.edit_menu.bind(this), + }, + 'Hours': { + value: origin_hms.h, + min: 0, + max: 9999, + wrap: true, + onchange: v => { + origin_hms.h = v; + update_origin(); + tt.set_timers_dirty(); + } + }, + 'Minutes': { + value: origin_hms.m, + min: 0, + max: 59, + wrap: true, + onchange: v => { + origin_hms.m = v; + update_origin(); + tt.set_timers_dirty(); + } + }, + 'Seconds': { + value: origin_hms.s, + min: 0, + max: 59, + wrap: true, + onchange: v => { + origin_hms.s = v; + update_origin(); + tt.set_timers_dirty(); + } + }, + }; + + E.showMenu(edit_start_hms_menu); + } + + edit_events_menu() { + const events_menu = { + '': { + title: 'Events', + back: () => { this.edit_menu(); } + }, + 'Outer alarm': { + value: this.tri_timer.outer_alarm, + format: v => (v ? 'On' : 'Off'), + onchange: v => { + this.tri_timer.outer_alarm = v; + tt.set_timers_dirty(); + }, + }, + 'Outer action': { + value: tt.ACTIONS.indexOf(this.tri_timer.outer_action), + min: 0, + max: tt.ACTIONS.length - 1, + format: v => tt.ACTIONS[v], + onchange: v => { + this.tri_timer.outer_action = tt.ACTIONS[v]; + tt.set_timers_dirty(); + }, + }, + 'End alarm': { + value: this.tri_timer.end_alarm, + format: v => (v ? 'On' : 'Off'), + onchange: v => { + this.tri_timer.end_alarm = v; + tt.set_timers_dirty(); + }, + }, + 'Vibrate pattern': require("buzz_menu").pattern( + this.tri_timer.vibrate_pattern, + v => this.tri_timer.vibrate_pattern = v), + 'Buzz count': { + value: this.tri_timer.buzz_count, + min: 0, + max: 15, + step: 1, + wrap: true, + format: v => v === 0 ? "Forever" : v, + onchange: v => { + this.tri_timer.buzz_count = v; + tt.set_timers_dirty(); + }, + }, + }; + + E.showMenu(events_menu); + } +} + + +class TimerMenu { + constructor(tri_timers, focused_timer) { + this.tri_timers = tri_timers; + this.focused_timer = focused_timer; + } + + start() { + this.top_menu(); + } + + stop() { + E.showMenu(); + } + + back() { + switch_UI(new TimerViewMenu(this.focused_timer)); + } + + top_menu() { + let menu = { + '': { + title: "Timers", + back: this.back.bind(this) + } + }; + this.tri_timers.forEach((tri_timer) => { + menu[tri_timer.display_status() + ' ' + tri_timer.display_name()] = + () => { switch_UI(new TimerView(tri_timer)); }; + }); + E.showMenu(menu); + } +} + + +function switch_UI(new_UI) { + if (CURRENT_UI) { + CURRENT_UI.stop(); + } + CURRENT_UI = new_UI; + CURRENT_UI.start(); +} + + +// Load and start up app // + +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +var CURRENT_UI = null; + +tt.update_system_alarms(); + +switch_UI(new TimerView(tt.TIMERS[0])); diff --git a/apps/tevtimer/app.png b/apps/tevtimer/app.png new file mode 100644 index 0000000000000000000000000000000000000000..507eb696b47ce3ec1a9b8e5dbe354cb0ed0558b2 GIT binary patch literal 819 zcmV-31I+x1P)EX>4Tx04R}tkv&MmKp2MKwn|kh9W0{akfAzR5EXIMDionYs1;guFuC*(nlvOS zE{=k0!NH%!s)LKOt`4q(Aov5~?BJy6A|-y86k5c1aCZ;yeecWNcYwcMW~$jS4yc-C zq!Mu`u_ypovrW+RV2J!T! zrE}gVj;cejJx~0i`z~v4w@T5zI^3A z0;2`WUiWx+S9{<7t!eh}2Rv7Dkp&1Cp#T5?8&FJCMKJ&X|Ns90009300RR60{{a60 z{{R60{{R60Xv6>%00001bW%=J06^y0W&i*H0b)x>L;#2d9Y_EG010qNS#tmYE+YT{ zE+YYWr9XB6000McNliru=m`%N7&O?l@eSad^gZEa<4bO1wgWnpw>WFU8G zbZ8()Nlj2!fese{007%bL_t(2&ux?44Z<)Cgsp7gWP!#-h}SAG0c?P}0xPfrW0N1- zaiW~mmruF#wQ&KO92qeqdK)o|{$bR-5&eA95u%?nGs_xbF-OGxscHucyxdh8#_AqV zI)+1L8-un$aWnQWQE@LIs(lH2W`k)cz{69e*H4pFPX}U{VWuHAi%h*ZQ=nh5!1Fx&GVsHY*Jgw~%150ft0<5* x;PtKrD)bw`o@BkH4|JF@nl`j0RNWFHrH4q(6M)nM;()|~7f91&yCO}hBtUzDr{Ky9 z@CIBFFTe|ML*mK>355A(v!>ov=mTKu^L#U7uV=<~5+~;u(dA$iy*%pm2(6PrLB0hT zxC;bYYX|^x*@cOLHUO51n?P`kWdgJ{?TpL32JjtNkFkAsFg-sXP8QJ9-Rj4) zVSh0^jb0^-v*^*|Cr`z5E1jC0_v2wVo+YPd_WRC!aWx)B^XWJ_J*;#_@nAZh&Z15# zy0iGCA04H=cL?{%3U@C`ySOjs7yUspiKE^Tw5yz!Dcu*7WDzBk!P#&wE2g8-e7K0x z`4(T&{A7vQYP>8Gxt}bfq2?Ss7|PFPNq-WLhvJljk>&$ZTRDIO;Sb(mMEwD=OP!_e z!lm5<43`5nykhal;%gSKT72E&A-L>kS_wqHE8lNt9D?o(#|4sDTNXF>!-36e>Yb&S zLS%WX4=a2ysn5nmm@fE&}oDA`q!A0!MGIiM$v8csw0E2l=laiFx>v7n=8R zDbvwppUv^Utnk6){V?{BO57xuJ)l8~*MT~I#7h}M=mB^DS%s`vaKOC`!eyRo*@TyH zm*n>O;Cq>WAJUOK2DuHWj9=@Fe-Vva&B~`=_yOZxQihgcU4t5khq+9SDgos*K&3pm zB+wcp12u3)ashc*;({-AB%&&r-iED>^)1NX1d;CfSNxO+tZ1(zHqlG=l9gmES^9`x zvX&|qDN*5VV|D6DJoE&QCY-vKq9h>v)MF?dmEub%pkH zK>K*H``V`i?NUoSfTFf$3z}Z@L;N1+tty-CVx^ybCBPRe_;M9HgxIN$9UIsgaX|kA Dm3a*5 literal 0 HcmV?d00001 diff --git a/apps/tevtimer/lib.js b/apps/tevtimer/lib.js new file mode 100644 index 000000000..403fc86db --- /dev/null +++ b/apps/tevtimer/lib.js @@ -0,0 +1,462 @@ +const Storage = require('Storage'); +const Sched = require('sched'); +const Time_utils = require('time_utils'); + + +// Data models // + +class PrimitiveTimer { + constructor(origin, is_running, rate, name) { + this.origin = origin || 0; + // default rate +1 unit per 1000 ms, countup + this.rate = rate || 0.001; + this.name = name || ''; + + this._start_time = Date.now(); + this._pause_time = is_running ? null : this._start_time; + } + + display_name() { + return this.name ? this.name : this.provisional_name(); + } + + provisional_name() { + return (this.rate >= 0 ? 'U' : 'D') + + ' ' + + Time_utils.formatDuration(this.origin / this.rate); + } + + display_status() { + let status = ''; + + // Indicate timer expired if its current value is <= 0 and it's + // a countdown timer + if (this.get() <= 0 && this.rate < 0) { + status += '!'; + } + + if (this.is_running()) { + status += '>'; + } + + return status; + } + + is_running() { + return !this._pause_time; + } + + start() { + if (!this.is_running()) { + this._start_time += Date.now() - this._pause_time; + this._pause_time = null; + } + } + + pause() { + if (this.is_running()) { + this._pause_time = Date.now(); + } + } + + reset() { + this.set(this.origin); + } + + get() { + const now = Date.now(); + const elapsed = + (now - this._start_time) + - (this.is_running() ? 0 : (now - this._pause_time)); + return this.origin + (this.rate * elapsed); + } + + set(new_value) { + const now = Date.now(); + this._start_time = (now - new_value / this.rate) + + (this.origin / this.rate); + if (!this.is_running()) { + this._pause_time = now; + } + } + + dump() { + return { + cls: 'PrimitiveTimer', + version: 0, + origin: this.origin, + rate: this.rate, + name: this.name, + start_time: this._start_time, + pause_time: this._pause_time + }; + } + + static load(data) { + if (!(data.cls == 'PrimitiveTimer' && data.version == 0)) { + console.error('Incompatible data type for loading PrimitiveTimer state'); + } + let loaded = new this(data.origin, false, data.rate, data.name); + loaded._start_time = data.start_time; + loaded._pause_time = data.pause_time; + return loaded; + } +} + + +class TriangleTimer extends PrimitiveTimer { + constructor(origin, is_running, rate, name, increment) { + super(origin, is_running, rate, name); + this.increment = increment || 1; + + this.end_alarm = false; + this.outer_alarm = false; + this.outer_action = 'Cont'; + this.pause_checkpoint = null; + this.vibrate_pattern = null; + this.buzz_count = 4; + } + + provisional_name() { + const origin_as_tri = as_triangle( + this.origin, + this.increment + ); + return (this.rate >= 0 ? 'U' : 'D') + + ' ' + + origin_as_tri[0] + '/' + origin_as_tri[1] + + ' x' + this.increment; + } + + start() { + super.start(); + this.emit('status'); + } + + pause() { + super.pause(); + this.emit('status'); + } + + check_auto_pause() { + const current_time = super.get(); + + if (this.is_running() && + this.outer_action == 'Pause') { + if (this.pause_checkpoint === null) { + this.pause_checkpoint = current_time + + this.time_to_next_outer_event() * this.rate; + console.debug('timer auto-pause setup: ' + this.pause_checkpoint); + } else if ( + (this.rate >= 0 && current_time >= this.pause_checkpoint) + || (this.rate < 0 && current_time <= this.pause_checkpoint) + ) { + console.debug('timer auto-pause triggered'); + this.pause(); + this.set(this.pause_checkpoint); + this.pause_checkpoint = null; + this.emit('auto-pause'); + } + } else { + this.pause_checkpoint = null; + } + } + + reset() { + this.pause_checkpoint = null; + return super.reset(); + } + + get() { + return super.get(); + } + + set(new_value) { + return super.set(new_value); + } + + time_to_next_alarm() { + if (!this.is_running()) + return null; + + if (this.outer_alarm) { + return this.time_to_next_outer_event(); + } + + if (this.end_alarm) { + return this.time_to_end_event(); + } + + return null; + } + + time_to_next_event() { + let next = null; + + if (this.outer_alarm || this.outer_action !== 'Cont') { + next = this.time_to_next_outer_event(); + } + + if (next === null) { + next = this.time_to_end_event(); + } + + return next + } + + time_to_next_outer_event() { + const as_tri = as_triangle(super.get(), this.increment); + let inner_left = this.rate > 0 ? as_tri[0] - as_tri[1] : as_tri[1]; + // Avoid getting stuck if we're paused precisely on the event time + if (!inner_left) { + inner_left = as_tri[0] + Math.sign(this.rate) * this.increment; + } + return Math.max(0, inner_left / Math.abs(this.rate)); + } + + time_to_end_event() { + if (this.rate <= 0 && this.get() > 0) { + return this.get() / Math.abs(this.rate); + } + return null; + } + + dump() { + let data = super.dump(); + data.cls = 'TriangleTimer'; + data.increment = this.increment; + data.end_alarm = this.end_alarm; + data.outer_alarm = this.outer_alarm; + data.outer_action = this.outer_action; + data.pause_checkpoint = this.pause_checkpoint; + data.vibrate_pattern = this.vibrate_pattern; + data.buzz_count = this.buzz_count; + return data; + } + + static load(data) { + if (!(data.cls == 'TriangleTimer' && data.version == 0)) { + console.error('Incompatible data type for loading TriangleTimer state'); + } + let loaded = new this( + data.origin, false, data.rate, data.name, data.increment); + loaded._start_time = data.start_time; + loaded._pause_time = data.pause_time; + loaded.end_alarm = data.end_alarm; + loaded.outer_alarm = data.outer_alarm; + loaded.outer_action = data.outer_action; + loaded.pause_checkpoint = data.pause_checkpoint; + loaded.vibrate_pattern = data.vibrate_pattern; + if (data.buzz_count !== undefined) { + loaded.buzz_count = data.buzz_count; + } + return loaded; + } +} + + +function fixed_ceil(value) { + // JavaScript sucks balls + return Math.ceil(Math.round(value * 1e10) / 1e10); +} + + +function as_triangle(linear_time, increment) { + if (increment === undefined) increment = 1; + linear_time = linear_time / increment; + const outer = fixed_ceil((Math.sqrt(linear_time * 8 + 1) - 1) / 2); + const inner = outer - (outer * (outer + 1) / 2 - linear_time); + return [outer * increment, inner * increment]; +} + +function as_linear(triangle_time, increment) { + if (increment === undefined) increment = 1; + const outer = triangle_time[0], inner = triangle_time[1]; + return (outer + (outer - 1) % increment + 1) + * fixed_ceil(outer / increment) / 2 + - outer + inner; +} + + +function format_triangle(tri_timer) { + const tri = as_triangle(tri_timer.get(), tri_timer.increment); + return tri[0] + '/' + Math.ceil(tri[1]); +} + + +function format_duration(msec, have_seconds) { + const time = Time_utils.decodeTime(msec); + time.h += time.d * 24; + let str = time.h + ":" + ("0" + time.m).substr(-2); + if (have_seconds) { + str += ":" + ("0" + time.s).substr(-2); + } + return str; +} + + +// Persistent state // + +const TIMERS_FILENAME = 'triangletimer.timers.json'; +const SETTINGS_FILENAME = 'triangletimer.json'; + +const SCHEDULED_SAVE_TIMEOUT = 15000; + +var SAVE_TIMERS_TIMEOUT = null; +var SAVE_SETTINGS_TIMEOUT = null; + + +function load_timers() { + console.log('loading timers'); + let timers = Storage.readJSON(TIMERS_FILENAME, true) || []; + if (timers.length) { + // Deserealize timer objects + timers = timers.map(t => TriangleTimer.load(t)); + } else { + timers = [new TriangleTimer()]; + } + return timers; +} + +function save_timers() { + console.log('saving timers'); + const dumped_timers = TIMERS.map(t => t.dump()); + if (!Storage.writeJSON(TIMERS_FILENAME, dumped_timers)) { + E.showAlert('Trouble saving timers'); + } +} + +function schedule_save_timers() { + if (SAVE_TIMERS_TIMEOUT === null) { + console.log('scheduling timer save'); + SAVE_TIMERS_TIMEOUT = setTimeout(() => { + save_timers(); + SAVE_TIMERS_TIMEOUT = null; + }, SCHEDULED_SAVE_TIMEOUT); + } else { + console.log('timer save already scheduled'); + } +} + +function save_settings() { + console.log('saving settings'); + if (!Storage.writeJSON(SETTINGS_FILENAME, SETTINGS)) { + E.showAlert('Trouble saving settings'); + } +} + +function schedule_save_settings() { + if (SAVE_SETTINGS_TIMEOUT === null) { + console.log('scheduling settings save'); + SAVE_SETTINGS_TIMEOUT = setTimeout(() => { + save_settings(); + SAVE_SETTINGS_TIMEOUT = null; + }, SCHEDULED_SAVE_TIMEOUT); + } else { + console.log('settings save already scheduled'); + } +} + +const SETTINGS = Object.assign({ + 'view_mode': 0, +}, Storage.readJSON(SETTINGS_FILENAME, true) || {}); + +var TIMERS = load_timers(); + +const ACTIONS = [ + 'Cont', + 'Pause', +]; + + +// Persistent data convenience functions + +function delete_tri_timer(timers, tri_timer) { + const idx = timers.indexOf(tri_timer); + if (idx !== -1) { + timers.splice(idx, 1); + } else { + console.warn('delete_tri_timer: Bug? Tried to delete a timer not in list'); + } + // Return another timer to switch UI to after deleting the focused + // one + return timers[Math.min(idx, timers.length - 1)]; +} + +function add_tri_timer(timers, tri_timer) { + // Create a copy of current timer object + const new_timer = TriangleTimer.load(tri_timer.dump()); + timers.unshift(new_timer); + return new_timer; +} + +function set_last_viewed_timer(tri_timer) { + const idx = TIMERS.indexOf(tri_timer); + if (idx == -1) { + console.warn('set_last_viewed_timer: Bug? Called with a timer not found in list'); + } else if (idx == 0) { + console.debug('set_last_viewed_timer: Already set as last timer'); + } else { + // Move tri_timer to top of list + TIMERS.splice(idx, 1); + TIMERS.unshift(tri_timer); + set_timers_dirty(); + } +} + +function set_timers_dirty() { + setTimeout(update_system_alarms, 500); + schedule_save_timers(); +} + +function set_settings_dirty() { + schedule_save_settings(); +} + + +// Alarm handling // + +function delete_system_alarms() { + var alarms = Sched.getAlarms().filter(a => a.appid == 'triangletimer'); + for (let alarm of alarms) { + console.debug('delete sched alarm ' + alarm.id); + Sched.setAlarm(alarm.id, undefined); + } + Sched.reload(); +} + +function set_system_alarms() { + for (let idx = 0; idx < TIMERS.length; idx++) { + let timer = TIMERS[idx]; + timer.check_auto_pause(); + let time_to_next_alarm = timer.time_to_next_alarm(); + if (time_to_next_alarm !== null) { + console.debug('set sched alarm ' + idx + ' (' + time_to_next_alarm/1000 + ')'); + Sched.setAlarm(idx.toString(), { + appid: 'triangletimer', + timer: time_to_next_alarm, + msg: timer.display_name(), + js: "load('triangletimer.alarm.js');", + data: { idx: idx }, + }); + } + } + Sched.reload(); +} + +function update_system_alarms() { + delete_system_alarms(); + set_system_alarms(); +} + + +E.on('kill', () => { save_timers(); }); +E.on('kill', () => { save_settings(); }); + + +exports = {TIMERS, SETTINGS, ACTIONS, + load_timers, save_timers, schedule_save_timers, save_settings, schedule_save_settings, + PrimitiveTimer, TriangleTimer, + as_triangle, as_linear, format_triangle, format_duration, + delete_tri_timer, add_tri_timer, set_last_viewed_timer, set_timers_dirty, set_settings_dirty, + update_system_alarms}; diff --git a/apps/tevtimer/metadata.json b/apps/tevtimer/metadata.json new file mode 100644 index 000000000..81cfed958 --- /dev/null +++ b/apps/tevtimer/metadata.json @@ -0,0 +1,21 @@ +{ + "id": "triangletimer", + "name": "Triangle timer", + "shortName":"Triangle timer", + "icon": "app.png", + "version": "0.01", + "description": "Timers with incrementally increasing or decreasing periods", + "tags": "timer", + "supports": ["BANGLEJS2"], + "dependencies": {"scheduler": "type"}, + "storage": [ + {"name": "triangletimer.app.js", "url": "app.js"}, + {"name": "triangletimer.alarm.js", "url": "alarm.js"}, + {"name": "triangletimer", "url": "lib.js"}, + {"name": "triangletimer.img", "url": "app-icon.js", "evaluate": true} + ], + "data": [ + {"name": "triangletimer.json"}, + {"name": "triangletimer.timers.json"} + ] +} From d9e0e04d107efad03a3823838afb4012830acd1a Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Fri, 31 Jan 2025 19:15:58 -0600 Subject: [PATCH 02/67] Update metadata --- apps/tevtimer/metadata.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/tevtimer/metadata.json b/apps/tevtimer/metadata.json index 81cfed958..3cb69bc26 100644 --- a/apps/tevtimer/metadata.json +++ b/apps/tevtimer/metadata.json @@ -1,21 +1,21 @@ { - "id": "triangletimer", - "name": "Triangle timer", - "shortName":"Triangle timer", + "id": "tevtimer", + "name": "tev's timer", + "shortName":"tev's timer", "icon": "app.png", "version": "0.01", - "description": "Timers with incrementally increasing or decreasing periods", + "description": "An ergonomic countdown timer app", "tags": "timer", "supports": ["BANGLEJS2"], "dependencies": {"scheduler": "type"}, "storage": [ - {"name": "triangletimer.app.js", "url": "app.js"}, - {"name": "triangletimer.alarm.js", "url": "alarm.js"}, - {"name": "triangletimer", "url": "lib.js"}, - {"name": "triangletimer.img", "url": "app-icon.js", "evaluate": true} + {"name": "tevtimer.app.js", "url": "app.js"}, + {"name": "tevtimer.alarm.js", "url": "alarm.js"}, + {"name": "tevtimer", "url": "lib.js"}, + {"name": "tevtimer.img", "url": "app-icon.js", "evaluate": true} ], "data": [ - {"name": "triangletimer.json"}, - {"name": "triangletimer.timers.json"} + {"name": "tevtimer.json"}, + {"name": "tevtimer.timers.json"} ] } From f5cd3e6740c7107c4ec100a293b9c0fe7bfc42dd Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Wed, 12 Feb 2025 21:58:22 -0600 Subject: [PATCH 03/67] Initial conversion to new app and adjust display code --- apps/tevtimer/app.js | 154 +++++++++++++++++++++++++------------------ apps/tevtimer/lib.js | 17 +++-- 2 files changed, 102 insertions(+), 69 deletions(-) diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js index 095f2c3c3..d8b6de44c 100644 --- a/apps/tevtimer/app.js +++ b/apps/tevtimer/app.js @@ -1,9 +1,56 @@ const Layout = require('Layout'); +const locale = require('locale'); + +const tt = require('tevtimer'); -const tt = require('triangletimer'); // UI // +ROW_IDS = ['row1', 'row2', 'row3']; + +FONT = { + 'row1': { + 'start hh:mm:ss': '12x20', + 'current hh:mm:ss': '12x20', + 'time hh:mm:ss': '12x20', + + 'start hh:mm': '12x20', + 'current hh:mm': '12x20', + 'time hh:mm': '12x20', + }, + + 'row2': { + 'start hh:mm:ss': 'Vector:34x42', + 'current hh:mm:ss': 'Vector:34x42', + 'time hh:mm:ss': 'Vector:34x42', + + 'start hh:mm': 'Vector:56x42', + 'current hh:mm': 'Vector:56x42', + 'time hh:mm': 'Vector:56x42', + }, + + 'row3': { + 'start hh:mm:ss': 'Vector:34x56', + 'current hh:mm:ss': 'Vector:34x56', + 'time hh:mm:ss': 'Vector:34x56', + + 'start hh:mm': 'Vector:56x56', + 'current hh:mm': 'Vector:56x56', + 'time hh:mm': 'Vector:56x56', + } +}; + + +function row_font(row_name, mode_name) { + font = FONT[row_name][mode_name]; + if (font === undefined) { + console.error('Unknown font for row_font("' + row_name + '", "' + mode_name + '")'); + return '12x20'; + } + return font; +} + + class TimerView { constructor(tri_timer) { this.tri_timer = tri_timer; @@ -25,7 +72,7 @@ class TimerView { // Touch handler function touchHandler(button, xy) { - for (var id of ['row1', 'row2', 'row3']) { + for (var id of ROW_IDS) { const elem = this.layout[id]; if (!xy.type && elem.x <= xy.x && xy.x < elem.x + elem.w && @@ -69,22 +116,22 @@ class TimerView { { type: 'txt', id: 'row1', - label: '8888', - font: 'Vector:56x42', + label: '88:88:88', + font: row_font('row1', tt.SETTINGS.view_mode['row1']), fillx: 1, }, { type: 'txt', id: 'row2', - label: '8888', - font: 'Vector:56x56', + label: '88:88:88', + font: row_font('row2', tt.SETTINGS.view_mode['row2']), fillx: 1, }, { type: 'txt', id: 'row3', label: '', - font: '12x20', + font: row_font('row3', tt.SETTINGS.view_mode['row3']), fillx: 1, }, { @@ -116,62 +163,34 @@ class TimerView { if (!item || item == 'timer') { - let timer_as_linear = this.tri_timer.get(); - if (timer_as_linear < 0) { - // Handle countdown timer expiration - timer_as_linear = 0; - setTimeout(() => { this.render('status'); }, 0); - } - const timer_as_tri = tt.as_triangle( - timer_as_linear, this.tri_timer.increment); + this._update_fonts(); - var label1, label2, font1, font2; - if (tt.SETTINGS.view_mode == 0) { - label1 = timer_as_tri[0]; - label2 = Math.ceil(timer_as_tri[1]); - font1 = 'Vector:56x42'; - font2 = 'Vector:56x56'; - } else if (tt.SETTINGS.view_mode == 1) { - label1 = timer_as_tri[0]; - label2 = Math.ceil(timer_as_tri[0] - timer_as_tri[1]); - font1 = 'Vector:56x42'; - font2 = 'Vector:56x56'; - } else if (tt.SETTINGS.view_mode == 2) { - label1 = tt.format_triangle(this.tri_timer); - let ttna = this.tri_timer.time_to_next_event(); - if (ttna !== null) { - label2 = tt.format_duration(ttna, true); - } else { - label2 = '--:--:--'; + for (var id of ROW_IDS) { + const elem = this.layout[id]; + let mode = tt.SETTINGS.view_mode[id]; + if (mode == 'start hh:mm:ss') { + elem.label = tt.format_duration(this.tri_timer.origin / Math.abs(this.tri_timer.rate), true); + } else if (mode == 'current hh:mm:ss') { + elem.label = tt.format_duration(this.tri_timer.get() / Math.abs(this.tri_timer.rate), true); + } else if (mode == 'time hh:mm:ss') { + elem.label = locale.time(new Date()).trim(); + + } else if (mode == 'start hh:mm') { + elem.label = tt.format_duration(this.tri_timer.origin / Math.abs(this.tri_timer.rate), false); + } else if (mode == 'current hh:mm') { + elem.label = tt.format_duration(this.tri_timer.get() / Math.abs(this.tri_timer.rate), false); + } else if (mode == 'time hh:mm') { + elem.label = locale.time(new Date(), 1).trim(); + + } else if (mode == 'name') { + elem.label = this.tri_timer.display_name(); } - font1 = 'Vector:30x42'; - font2 = 'Vector:34x56'; - } else if (tt.SETTINGS.view_mode == 3) { - label1 = timer_as_tri[0]; - let ttna = this.tri_timer.time_to_next_event(); - if (ttna !== null) { - label2 = tt.format_duration(ttna, false); - } else { - label2 = '--:--'; - } - font1 = 'Vector:56x42'; - font2 = 'Vector:48x56'; + this.layout.clear(elem); + this.layout.render(elem); } - if (label1 !== this.layout.row1.label) { - this.layout.row1.label = label1; - this.layout.row1.font = font1; - this.layout.clear(this.layout.row1); - this.layout.render(this.layout.row1); - } - - if (label2 !== this.layout.row2.label) { - this.layout.row2.label = label2; - this.layout.row2.font = font2; - this.layout.clear(this.layout.row2); - this.layout.render(this.layout.row2); - } + } if (!item || item == 'status') { @@ -179,11 +198,11 @@ class TimerView { this.tri_timer.is_running() ? 'Pause' : 'Start'; this.layout.render(this.layout.buttons); - this.layout.row3.label = - this.tri_timer.display_status() - + ' ' + this.tri_timer.provisional_name(); - this.layout.clear(this.layout.row3); - this.layout.render(this.layout.row3); + // this.layout.row3.label = + // this.tri_timer.display_status() + // + ' ' + this.tri_timer.provisional_name(); + // this.layout.clear(this.layout.row3); + // this.layout.render(this.layout.row3); } if (this.tri_timer.is_running() && this.tri_timer.get() > 0) { @@ -218,6 +237,15 @@ class TimerView { } } + _update_fonts() { + for (var id of ROW_IDS) { + const elem = this.layout[id]; + elem.font = row_font(id, tt.SETTINGS.view_mode[id]); + this.layout.clear(elem); + this.layout.render(elem); + } + } + start_stop_timer() { if (this.tri_timer.is_running()) { this.tri_timer.pause(); diff --git a/apps/tevtimer/lib.js b/apps/tevtimer/lib.js index 403fc86db..0ff1fd096 100644 --- a/apps/tevtimer/lib.js +++ b/apps/tevtimer/lib.js @@ -297,8 +297,8 @@ function format_duration(msec, have_seconds) { // Persistent state // -const TIMERS_FILENAME = 'triangletimer.timers.json'; -const SETTINGS_FILENAME = 'triangletimer.json'; +const TIMERS_FILENAME = 'tevtimer.timers.json'; +const SETTINGS_FILENAME = 'tevtimer.json'; const SCHEDULED_SAVE_TIMEOUT = 15000; @@ -357,8 +357,13 @@ function schedule_save_settings() { } } +// Default settings const SETTINGS = Object.assign({ - 'view_mode': 0, + 'view_mode': { + 'row1': 'time hh:mm', + 'row2': 'start hh:mm:ss', + 'row3': 'current hh:mm:ss', + }, }, Storage.readJSON(SETTINGS_FILENAME, true) || {}); var TIMERS = load_timers(); @@ -417,7 +422,7 @@ function set_settings_dirty() { // Alarm handling // function delete_system_alarms() { - var alarms = Sched.getAlarms().filter(a => a.appid == 'triangletimer'); + var alarms = Sched.getAlarms().filter(a => a.appid == 'tevtimer'); for (let alarm of alarms) { console.debug('delete sched alarm ' + alarm.id); Sched.setAlarm(alarm.id, undefined); @@ -433,10 +438,10 @@ function set_system_alarms() { if (time_to_next_alarm !== null) { console.debug('set sched alarm ' + idx + ' (' + time_to_next_alarm/1000 + ')'); Sched.setAlarm(idx.toString(), { - appid: 'triangletimer', + appid: 'tevtimer', timer: time_to_next_alarm, msg: timer.display_name(), - js: "load('triangletimer.alarm.js');", + js: "load('tevtimer.alarm.js');", data: { idx: idx }, }); } From 37b9558fc7057d5cf699beea757ac18e1a7fd64b Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Fri, 14 Feb 2025 21:32:35 -0600 Subject: [PATCH 04/67] Clean up edit menus for timer-only operation --- apps/tevtimer/app.js | 127 ++++--------------------------------------- apps/tevtimer/lib.js | 3 +- 2 files changed, 12 insertions(+), 118 deletions(-) diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js index d8b6de44c..3f70fe902 100644 --- a/apps/tevtimer/app.js +++ b/apps/tevtimer/app.js @@ -332,82 +332,27 @@ class TimerViewMenu { title: 'Edit: ' + this.tri_timer.display_name(), back: () => { this.top_menu(); }, }, - 'Direction': { - value: this.tri_timer.rate >= 0, - format: v => (v ? 'Up' : 'Down'), - onchange: v => { - this.tri_timer.rate = -this.tri_timer.rate; - tt.set_timers_dirty(); - } - }, - 'Start (Tri)': this.edit_start_tri_menu.bind(this), - 'Start (HMS)': this.edit_start_hms_menu.bind(this), - 'Increment': { - value: this.tri_timer.increment, - min: 1, - max: 9999, + 'Start': this.edit_start_hms_menu.bind(this), + 'Vibrate pattern': require("buzz_menu").pattern( + this.tri_timer.vibrate_pattern, + v => this.tri_timer.vibrate_pattern = v), + 'Buzz count': { + value: this.tri_timer.buzz_count, + min: 0, + max: 15, step: 1, wrap: true, + format: v => v === 0 ? "Forever" : v, onchange: v => { - this.tri_timer.increment = v; + this.tri_timer.buzz_count = v; tt.set_timers_dirty(); }, }, - 'Events': this.edit_events_menu.bind(this), }; E.showMenu(edit_menu); } - edit_start_tri_menu() { - let origin_tri = tt.as_triangle( - this.tri_timer.origin, this.tri_timer.increment); - - const edit_start_tri_menu = { - '': { - title: 'Start (Tri)', - back: this.edit_menu.bind(this), - }, - 'Outer': { - value: origin_tri[0], - min: 0, - max: Math.floor(9999 / this.tri_timer.increment) - * this.tri_timer.increment, - step: this.tri_timer.increment, - wrap: true, - noList: true, - onchange: v => { - origin_tri[0] = v; - edit_start_tri_menu.Inner.max = origin_tri[0]; - origin_tri[1] = (this.tri_timer.rate >= 0) ? - 1 : origin_tri[0]; - edit_start_tri_menu.Inner.value = origin_tri[1]; - this.tri_timer.origin = tt.as_linear( - origin_tri, this.tri_timer.increment - ); - tt.set_timers_dirty(); - } - }, - 'Inner': { - value: origin_tri[1], - min: 0, - max: origin_tri[0], - step: 1, - wrap: true, - noList: true, - onchange: v => { - origin_tri[1] = v; - this.tri_timer.origin = tt.as_linear( - origin_tri, this.tri_timer.increment - ); - tt.set_timers_dirty(); - } - }, - }; - - E.showMenu(edit_start_tri_menu); - } - edit_start_hms_menu() { let origin_hms = { h: Math.floor(this.tri_timer.origin / 3600), @@ -463,58 +408,6 @@ class TimerViewMenu { E.showMenu(edit_start_hms_menu); } - - edit_events_menu() { - const events_menu = { - '': { - title: 'Events', - back: () => { this.edit_menu(); } - }, - 'Outer alarm': { - value: this.tri_timer.outer_alarm, - format: v => (v ? 'On' : 'Off'), - onchange: v => { - this.tri_timer.outer_alarm = v; - tt.set_timers_dirty(); - }, - }, - 'Outer action': { - value: tt.ACTIONS.indexOf(this.tri_timer.outer_action), - min: 0, - max: tt.ACTIONS.length - 1, - format: v => tt.ACTIONS[v], - onchange: v => { - this.tri_timer.outer_action = tt.ACTIONS[v]; - tt.set_timers_dirty(); - }, - }, - 'End alarm': { - value: this.tri_timer.end_alarm, - format: v => (v ? 'On' : 'Off'), - onchange: v => { - this.tri_timer.end_alarm = v; - tt.set_timers_dirty(); - }, - }, - 'Vibrate pattern': require("buzz_menu").pattern( - this.tri_timer.vibrate_pattern, - v => this.tri_timer.vibrate_pattern = v), - 'Buzz count': { - value: this.tri_timer.buzz_count, - min: 0, - max: 15, - step: 1, - wrap: true, - format: v => v === 0 ? "Forever" : v, - onchange: v => { - this.tri_timer.buzz_count = v; - tt.set_timers_dirty(); - }, - }, - }; - - E.showMenu(events_menu); - } } diff --git a/apps/tevtimer/lib.js b/apps/tevtimer/lib.js index 0ff1fd096..f3c4a3aac 100644 --- a/apps/tevtimer/lib.js +++ b/apps/tevtimer/lib.js @@ -313,7 +313,8 @@ function load_timers() { // Deserealize timer objects timers = timers.map(t => TriangleTimer.load(t)); } else { - timers = [new TriangleTimer()]; + timers = [new TriangleTimer(600, false, -0.001)]; + timers[0].end_alarm = true; } return timers; } From 0a0c6f4d21b998d34c6554da701d960e9839f0f0 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Sun, 16 Feb 2025 16:42:20 -0600 Subject: [PATCH 05/67] Some first steps toward moving from TriangleTimer to PrimitiveTimer --- apps/tevtimer/alarm.js | 28 ++++++------------ apps/tevtimer/app.js | 32 +-------------------- apps/tevtimer/lib.js | 65 ++++++++++++++++++++++++++++++++++-------- 3 files changed, 63 insertions(+), 62 deletions(-) diff --git a/apps/tevtimer/alarm.js b/apps/tevtimer/alarm.js index 677cf775b..3119f47a2 100644 --- a/apps/tevtimer/alarm.js +++ b/apps/tevtimer/alarm.js @@ -1,33 +1,23 @@ -const tt = require('triangletimer'); +const tt = require('tevtimer'); function showAlarm(alarm) { const settings = require("sched").getSettings(); - const tri_timer = tt.TIMERS[alarm.data.idx]; - const message = tt.format_triangle(tri_timer) + '\n' + alarm.msg; + const timer = tt.TIMERS[alarm.data.idx]; + const message = timer.display_name() + '\n' + alarm.msg; Bangle.loadWidgets(); Bangle.drawWidgets(); // buzzCount should really be called buzzRepeat, so subtract 1 - let buzzCount = tri_timer.buzz_count - 1; - - tt.update_system_alarms(); + let buzzCount = timer.buzz_count - 1; E.showPrompt(message, { - title: 'Triangle timer', - buttons: { "Goto": true, "OK": false } + title: 'Timer', + buttons: { "OK": true } }).then(function (go) { buzzCount = 0; - Bangle.emit("alarmDismiss", alarm); - - if (go) { - console.log('alarm ' + alarm.data.idx); - tt.set_last_viewed_timer(tri_timer); - load('triangletimer.app.js'); - } else { - load(); - } + load(); }); function buzz() { @@ -35,7 +25,7 @@ function showAlarm(alarm) { Bangle.setLocked(false); } - const pattern = tri_timer.vibrate_pattern || settings.defaultTimerPattern; + const pattern = timer.vibrate_pattern || settings.defaultTimerPattern; console.log('buzz: ' + pattern); console.log('buzzCount: ' + buzzCount); require("buzz").pattern(pattern).then(() => { @@ -43,7 +33,7 @@ function showAlarm(alarm) { setTimeout(buzz, settings.buzzIntervalMillis); } else if (alarm.as) { // auto-snooze // buzzCount should really be called buzzRepeat, so subtract 1 - buzzCount = tri_timer.buzz_count - 1; + buzzCount = timer.buzz_count - 1; setTimeout(buzz, settings.defaultSnoozeMillis); } }); diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js index 3f70fe902..1aa34ea9a 100644 --- a/apps/tevtimer/app.js +++ b/apps/tevtimer/app.js @@ -65,28 +65,6 @@ class TimerView { this.layout.clear(); this.render(); tt.set_last_viewed_timer(this.tri_timer); - let render_status = () => { this.render(); }; - this.tri_timer.on('status', render_status); - this.tri_timer.on('auto-pause', tt.set_timers_dirty); - this.listeners.status = render_status; - - // Touch handler - function touchHandler(button, xy) { - for (var id of ROW_IDS) { - const elem = this.layout[id]; - if (!xy.type && - elem.x <= xy.x && xy.x < elem.x + elem.w && - elem.y <= xy.y && xy.y < elem.y + elem.h) { - Bangle.buzz(50, 0.5); - tt.SETTINGS.view_mode = (tt.SETTINGS.view_mode + 1) % 4; - tt.schedule_save_settings(); - setTimeout(this.render.bind(this), 0); - break; - } - } - } - this.listeners.touch = touchHandler.bind(this); - Bangle.on('touch', this.listeners.touch); // Physical button handler this.listeners.button = setWatch( @@ -101,8 +79,6 @@ class TimerView { clearTimeout(this.timer_timeout); this.timer_timeout = null; } - this.tri_timer.removeListener('status', this.listeners.status); - Bangle.removeListener('touch', this.listeners.touch); clearWatch(this.listeners.button); Bangle.setUI(); } @@ -155,7 +131,6 @@ class TimerView { render(item) { console.debug('render called: ' + item); - this.tri_timer.check_auto_pause(); if (!item) { this.layout.update(); @@ -197,12 +172,6 @@ class TimerView { this.layout.start_btn.label = this.tri_timer.is_running() ? 'Pause' : 'Start'; this.layout.render(this.layout.buttons); - - // this.layout.row3.label = - // this.tri_timer.display_status() - // + ' ' + this.tri_timer.provisional_name(); - // this.layout.clear(this.layout.row3); - // this.layout.render(this.layout.row3); } if (this.tri_timer.is_running() && this.tri_timer.get() > 0) { @@ -253,6 +222,7 @@ class TimerView { this.tri_timer.start(); } tt.set_timers_dirty(); + this.render('status'); } } diff --git a/apps/tevtimer/lib.js b/apps/tevtimer/lib.js index f3c4a3aac..0cf65d0c8 100644 --- a/apps/tevtimer/lib.js +++ b/apps/tevtimer/lib.js @@ -12,6 +12,9 @@ class PrimitiveTimer { this.rate = rate || 0.001; this.name = name || ''; + this.vibrate_pattern = ';;;'; + this.buzz_count = 4; + this._start_time = Date.now(); this._pause_time = is_running ? null : this._start_time; } @@ -21,9 +24,11 @@ class PrimitiveTimer { } provisional_name() { - return (this.rate >= 0 ? 'U' : 'D') - + ' ' - + Time_utils.formatDuration(this.origin / this.rate); + return ( + Time_utils.formatDuration(this.origin / Math.abs(this.rate)) + + ' / ' + + Time_utils.formatDuration(Math.abs(this.get() / Math.abs(this.rate))) + ); } display_status() { @@ -88,7 +93,9 @@ class PrimitiveTimer { rate: this.rate, name: this.name, start_time: this._start_time, - pause_time: this._pause_time + pause_time: this._pause_time, + vibrate_pattern: this.vibrate_pattern, + buzz_count: this.buzz_count, }; } @@ -99,8 +106,43 @@ class PrimitiveTimer { let loaded = new this(data.origin, false, data.rate, data.name); loaded._start_time = data.start_time; loaded._pause_time = data.pause_time; + loaded.vibrate_pattern = data.vibrate_pattern; + loaded.buzz_count = data.buzz_count; return loaded; } + + + ////// Temporary compatibility code ////// + check_auto_pause() { + console.warn('check_auto_pause: not implemented'); + } + + time_to_next_alarm() { + console.warn('time_to_next_alarm: not implemented'); + + if (!this.is_running()) + return null; + + return this.get() / Math.abs(this.rate); + } + + time_to_next_event() { + console.warn('time_to_next_event: not implemented'); + return null; + } + + time_to_next_outer_event() { + console.warn('time_to_next_outer_event: not implemented'); + return null; + } + + time_to_end_event() { + console.warn('time_to_end_event: not implemented'); + if (this.rate <= 0 && this.get() > 0) { + return this.get() / Math.abs(this.rate); + } + return null; + } } @@ -311,9 +353,9 @@ function load_timers() { let timers = Storage.readJSON(TIMERS_FILENAME, true) || []; if (timers.length) { // Deserealize timer objects - timers = timers.map(t => TriangleTimer.load(t)); + timers = timers.map(t => PrimitiveTimer.load(t)); } else { - timers = [new TriangleTimer(600, false, -0.001)]; + timers = [new PrimitiveTimer(600, false, -0.001)]; timers[0].end_alarm = true; } return timers; @@ -391,7 +433,7 @@ function delete_tri_timer(timers, tri_timer) { function add_tri_timer(timers, tri_timer) { // Create a copy of current timer object - const new_timer = TriangleTimer.load(tri_timer.dump()); + const new_timer = PrimitiveTimer.load(tri_timer.dump()); timers.unshift(new_timer); return new_timer; } @@ -434,14 +476,13 @@ function delete_system_alarms() { function set_system_alarms() { for (let idx = 0; idx < TIMERS.length; idx++) { let timer = TIMERS[idx]; - timer.check_auto_pause(); - let time_to_next_alarm = timer.time_to_next_alarm(); - if (time_to_next_alarm !== null) { - console.debug('set sched alarm ' + idx + ' (' + time_to_next_alarm/1000 + ')'); + let time_to_next_alarm = timer.get() / Math.abs(timer.rate); + if (timer.is_running() && time_to_next_alarm > 0) { + console.debug('set sched alarm ' + idx + ' (' + time_to_next_alarm + ')'); Sched.setAlarm(idx.toString(), { appid: 'tevtimer', timer: time_to_next_alarm, - msg: timer.display_name(), + msg: '', js: "load('tevtimer.alarm.js');", data: { idx: idx }, }); From e8e069866fb3adf7d1178f17eb47fb93c63146ac Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Sun, 16 Feb 2025 17:41:09 -0600 Subject: [PATCH 06/67] Correct display of expired (negative) timers --- apps/tevtimer/app.js | 65 ++++++++++++++++++++++---------------------- apps/tevtimer/lib.js | 3 ++ 2 files changed, 36 insertions(+), 32 deletions(-) diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js index 1aa34ea9a..26723b425 100644 --- a/apps/tevtimer/app.js +++ b/apps/tevtimer/app.js @@ -140,22 +140,30 @@ class TimerView { this._update_fonts(); + let update_interval = Infinity; + for (var id of ROW_IDS) { const elem = this.layout[id]; let mode = tt.SETTINGS.view_mode[id]; if (mode == 'start hh:mm:ss') { elem.label = tt.format_duration(this.tri_timer.origin / Math.abs(this.tri_timer.rate), true); + update_interval = Math.min(update_interval, 1); } else if (mode == 'current hh:mm:ss') { elem.label = tt.format_duration(this.tri_timer.get() / Math.abs(this.tri_timer.rate), true); + update_interval = Math.min(update_interval, 1); } else if (mode == 'time hh:mm:ss') { elem.label = locale.time(new Date()).trim(); + update_interval = Math.min(update_interval, 1); } else if (mode == 'start hh:mm') { elem.label = tt.format_duration(this.tri_timer.origin / Math.abs(this.tri_timer.rate), false); + update_interval = Math.min(update_interval, 60); } else if (mode == 'current hh:mm') { elem.label = tt.format_duration(this.tri_timer.get() / Math.abs(this.tri_timer.rate), false); + update_interval = Math.min(update_interval, 60); } else if (mode == 'time hh:mm') { elem.label = locale.time(new Date(), 1).trim(); + update_interval = Math.min(update_interval, 60); } else if (mode == 'name') { elem.label = this.tri_timer.display_name(); @@ -164,8 +172,31 @@ class TimerView { this.layout.render(elem); } + if (this.tri_timer.is_running()) { + if (this.timer_timeout) { + clearTimeout(this.timer_timeout); + this.timer_timeout = null; + } - + // Set up timeout to render timer again when needed + if (update_interval !== Infinity) { + + // Calculate approximate time next render is needed. + let next_update = this.tri_timer.get() % update_interval; + if (next_update < 0) { + next_update = 1 + next_update; + } + // Convert next_update from seconds to milliseconds and add + // compensating factor of 50ms due to timeouts apparently + // sometimes triggering too early. + next_update = next_update / Math.abs(this.tri_timer.rate) + 50; + console.debug('Next render update scheduled in ' + next_update); + this.timer_timeout = setTimeout( + () => { this.timer_timeout = null; this.render('timer'); }, + next_update + ); + } + } } if (!item || item == 'status') { @@ -173,37 +204,6 @@ class TimerView { this.tri_timer.is_running() ? 'Pause' : 'Start'; this.layout.render(this.layout.buttons); } - - if (this.tri_timer.is_running() && this.tri_timer.get() > 0) { - if (this.timer_timeout) { - clearTimeout(this.timer_timeout); - this.timer_timeout = null; - } - - // Calculate approximate time next display update is needed. - // Usual case: update when numbers change once per second. - let next_tick = this.tri_timer.get() % 1; - if (this.tri_timer.rate > 0) { - next_tick = 1 - next_tick; - } - // Convert next_tick from seconds to milliseconds and add - // compensating factor of 50ms due to timeouts apparently - // sometimes triggering too early. - next_tick = next_tick / Math.abs(this.tri_timer.rate) + 50; - - // For slow-update view mode, only update about every 60 - // seconds instead of every second - if (tt.SETTINGS.view_mode == 3) { - console.debug(this.tri_timer.time_to_next_event()); - next_tick = this.tri_timer.time_to_next_event() % 60000; - } - - console.debug('Next render update scheduled in ' + next_tick); - this.timer_timeout = setTimeout( - () => { this.timer_timeout = null; this.render('timer'); }, - next_tick - ); - } } _update_fonts() { @@ -223,6 +223,7 @@ class TimerView { } tt.set_timers_dirty(); this.render('status'); + this.render('timer'); } } diff --git a/apps/tevtimer/lib.js b/apps/tevtimer/lib.js index 0cf65d0c8..9834c5760 100644 --- a/apps/tevtimer/lib.js +++ b/apps/tevtimer/lib.js @@ -327,6 +327,9 @@ function format_triangle(tri_timer) { function format_duration(msec, have_seconds) { + if (msec < 0) { + return '-' + format_duration(-msec, have_seconds); + } const time = Time_utils.decodeTime(msec); time.h += time.d * 24; let str = time.h + ":" + ("0" + time.m).substr(-2); From b0bdd78549aa4db850d01ddd28e11ba037c11665 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Tue, 18 Feb 2025 19:50:20 -0600 Subject: [PATCH 07/67] Try to restore timer snooze functionality --- apps/tevtimer/alarm.js | 53 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/apps/tevtimer/alarm.js b/apps/tevtimer/alarm.js index 3119f47a2..23cbf26db 100644 --- a/apps/tevtimer/alarm.js +++ b/apps/tevtimer/alarm.js @@ -1,9 +1,19 @@ const tt = require('tevtimer'); function showAlarm(alarm) { + const alarmIndex = alarms.indexOf(alarm); const settings = require("sched").getSettings(); const timer = tt.TIMERS[alarm.data.idx]; - const message = timer.display_name() + '\n' + alarm.msg; + let message = timer.display_name() + '\n' + alarm.msg; + + if (alarm.msg) { + message += "\n" + alarm.msg; + } else { + message = (alarm.timer + ? atob("ACQswgD//33vRcGHIQAAABVVVAAAAAAAABVVVAAAAAAAABVVVAAAAAAAABVVVAAAAAAAABVVVAAAAAAAABVVVAAAAAAAAAP/wAAAAAAAAAP/wAAAAAAAAAqqoAPAAAAAAqqqqoP8AAAAKqqqqqv/AAACqqqqqqq/wAAKqqqlWqqvwAAqqqqlVaqrAACqqqqlVVqqAAKqqqqlVVaqgAKqaqqlVVWqgAqpWqqlVVVqoAqlWqqlVVVaoCqlV6qlVVVaqCqVVfqlVVVWqCqVVf6lVVVWqKpVVX/lVVVVqqpVVV/+VVVVqqpVVV//lVVVqqpVVVfr1VVVqqpVVVfr1VVVqqpVVVb/lVVVqqpVVVW+VVVVqqpVVVVVVVVVqiqVVVVVVVVWqCqVVVVVVVVWqCqlVVVVVVVaqAqlVVVVVVVaoAqpVVVVVVVqoAKqVVVVVVWqgAKqlVVVVVaqgACqpVVVVVqqAAAqqlVVVaqoAAAKqqVVWqqgAAACqqqqqqqAAAAAKqqqqqgAAAAAAqqqqoAAAAAAAAqqoAAAAA==") + : atob("AC0swgF97///RcEpMlVVVVVVf9VVVVVVVVX/9VVf9VVf/1VVV///1Vf9VX///VVX///VWqqlV///1Vf//9aqqqqpf//9V///2qqqqqqn///V///6qqqqqqr///X//+qqoAAKqqv//3//6qoAAAAKqr//3//qqAAAAAAqq//3/+qoAADwAAKqv/3/+qgAADwAACqv/3/aqAAADwAAAqp/19qoAAADwAAAKqfV1qgAAADwAAACqXVWqgAAADwAAACqlVWqAAAADwAAAAqlVWqAAAADwAAAAqlVWqAAAADwAAAAqlVaoAAAADwAAAAKpVaoAAAADwAAAAKpVaoAAAADwAAAAKpVaoAAAAOsAAAAKpVaoAAAAOsAAAAKpVaoAAAAL/AAAAKpVaoAAAAgPwAAAKpVaoAAACAD8AAAKpVWqAAAIAA/AAAqlVWqAAAgAAPwAAqlVWqAACAAADwAAqlVWqgAIAAAAAACqlVVqgAgAAAAAACqVVVqoAAAAAAAAKqVVVaqAAAAAAAAqpVVVWqgAAAAAACqlVVVWqoAAAAAAKqlVVVVqqAAAAAAqqVVVVVaqoAAAAKqpVVVVVeqqoAAKqqtVVVVV/6qqqqqqr/VVVVX/2qqqqqqn/1VVVf/VaqqqqpV/9VVVf9VVWqqlVVf9VVVf1VVVVVVVVX9VQ==") + ) + " " + message + } Bangle.loadWidgets(); Bangle.drawWidgets(); @@ -12,11 +22,44 @@ function showAlarm(alarm) { let buzzCount = timer.buzz_count - 1; E.showPrompt(message, { - title: 'Timer', - buttons: { "OK": true } - }).then(function (go) { + title: 'tev timer', + buttons: { "Snooze": true, "Stop": false } // default is sleep so it'll come back in some mins + }).then(function (sleep) { buzzCount = 0; - Bangle.emit("alarmDismiss", alarm); + + if (sleep) { + if (alarm.ot === undefined) { + alarm.ot = alarm.t; + } + let time = new Date(); + let currentTime = (time.getHours()*3600000)+(time.getMinutes()*60000)+(time.getSeconds()*1000); + alarm.t = currentTime + settings.defaultSnoozeMillis; + alarm.t %= 86400000; + Bangle.emit("alarmSnooze", alarm); + } else { + let del = alarm.del === undefined ? settings.defaultDeleteExpiredTimers : alarm.del; + if (del) { + alarms.splice(alarmIndex, 1); + } else { + if (alarm.date && alarm.rp) { + setNextRepeatDate(alarm); + } else if (!alarm.timer) { + alarm.last = new Date().getDate(); + } + if (alarm.ot !== undefined) { + alarm.t = alarm.ot; + delete alarm.ot; + } + if (!alarm.rp) { + alarm.on = false; + } + } + Bangle.emit("alarmDismiss", alarm); + } + + // The updated alarm is still a member of 'alarms' + // so writing to array writes changes back directly + require("sched").setAlarms(alarms); load(); }); From acd92f5ab9aae1554091b7dfd78c0644f92feb25 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Wed, 19 Feb 2025 18:03:36 -0600 Subject: [PATCH 08/67] Rename old tri_timer references --- apps/tevtimer/app.js | 76 ++++++++++++++++++++++---------------------- apps/tevtimer/lib.js | 20 ++++++------ 2 files changed, 48 insertions(+), 48 deletions(-) diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js index 26723b425..d1230b2a8 100644 --- a/apps/tevtimer/app.js +++ b/apps/tevtimer/app.js @@ -52,8 +52,8 @@ function row_font(row_name, mode_name) { class TimerView { - constructor(tri_timer) { - this.tri_timer = tri_timer; + constructor(timer) { + this.timer = timer; this.layout = null; this.listeners = {}; @@ -64,7 +64,7 @@ class TimerView { this._initLayout(); this.layout.clear(); this.render(); - tt.set_last_viewed_timer(this.tri_timer); + tt.set_last_viewed_timer(this.timer); // Physical button handler this.listeners.button = setWatch( @@ -118,7 +118,7 @@ class TimerView { cb: this.start_stop_timer.bind(this)}, {type: 'btn', font: '6x8:2', fillx: 1, label: 'Menu', id: 'menu_btn', cb: () => { - switch_UI(new TimerViewMenu(this.tri_timer)); + switch_UI(new TimerViewMenu(this.timer)); } } ] @@ -146,33 +146,33 @@ class TimerView { const elem = this.layout[id]; let mode = tt.SETTINGS.view_mode[id]; if (mode == 'start hh:mm:ss') { - elem.label = tt.format_duration(this.tri_timer.origin / Math.abs(this.tri_timer.rate), true); + elem.label = tt.format_duration(this.timer.origin / Math.abs(this.timer.rate), true); update_interval = Math.min(update_interval, 1); } else if (mode == 'current hh:mm:ss') { - elem.label = tt.format_duration(this.tri_timer.get() / Math.abs(this.tri_timer.rate), true); + elem.label = tt.format_duration(this.timer.get() / Math.abs(this.timer.rate), true); update_interval = Math.min(update_interval, 1); } else if (mode == 'time hh:mm:ss') { elem.label = locale.time(new Date()).trim(); update_interval = Math.min(update_interval, 1); } else if (mode == 'start hh:mm') { - elem.label = tt.format_duration(this.tri_timer.origin / Math.abs(this.tri_timer.rate), false); + elem.label = tt.format_duration(this.timer.origin / Math.abs(this.timer.rate), false); update_interval = Math.min(update_interval, 60); } else if (mode == 'current hh:mm') { - elem.label = tt.format_duration(this.tri_timer.get() / Math.abs(this.tri_timer.rate), false); + elem.label = tt.format_duration(this.timer.get() / Math.abs(this.timer.rate), false); update_interval = Math.min(update_interval, 60); } else if (mode == 'time hh:mm') { elem.label = locale.time(new Date(), 1).trim(); update_interval = Math.min(update_interval, 60); } else if (mode == 'name') { - elem.label = this.tri_timer.display_name(); + elem.label = this.timer.display_name(); } this.layout.clear(elem); this.layout.render(elem); } - if (this.tri_timer.is_running()) { + if (this.timer.is_running()) { if (this.timer_timeout) { clearTimeout(this.timer_timeout); this.timer_timeout = null; @@ -182,14 +182,14 @@ class TimerView { if (update_interval !== Infinity) { // Calculate approximate time next render is needed. - let next_update = this.tri_timer.get() % update_interval; + let next_update = this.timer.get() % update_interval; if (next_update < 0) { next_update = 1 + next_update; } // Convert next_update from seconds to milliseconds and add // compensating factor of 50ms due to timeouts apparently // sometimes triggering too early. - next_update = next_update / Math.abs(this.tri_timer.rate) + 50; + next_update = next_update / Math.abs(this.timer.rate) + 50; console.debug('Next render update scheduled in ' + next_update); this.timer_timeout = setTimeout( () => { this.timer_timeout = null; this.render('timer'); }, @@ -201,7 +201,7 @@ class TimerView { if (!item || item == 'status') { this.layout.start_btn.label = - this.tri_timer.is_running() ? 'Pause' : 'Start'; + this.timer.is_running() ? 'Pause' : 'Start'; this.layout.render(this.layout.buttons); } } @@ -216,10 +216,10 @@ class TimerView { } start_stop_timer() { - if (this.tri_timer.is_running()) { - this.tri_timer.pause(); + if (this.timer.is_running()) { + this.timer.pause(); } else { - this.tri_timer.start(); + this.timer.start(); } tt.set_timers_dirty(); this.render('status'); @@ -229,8 +229,8 @@ class TimerView { class TimerViewMenu { - constructor(tri_timer) { - this.tri_timer = tri_timer; + constructor(timer) { + this.timer = timer; } start() { @@ -242,23 +242,23 @@ class TimerViewMenu { } back() { - switch_UI(new TimerView(this.tri_timer)); + switch_UI(new TimerView(this.timer)); } top_menu() { const top_menu = { '': { - title: this.tri_timer.display_name(), + title: this.timer.display_name(), back: this.back.bind(this) }, 'Reset': () => { E.showMenu(reset_menu); }, 'Timers': () => { - switch_UI(new TimerMenu(tt.TIMERS, this.tri_timer)); + switch_UI(new TimerMenu(tt.TIMERS, this.timer)); }, 'Edit': this.edit_menu.bind(this), 'Add': () => { tt.set_timers_dirty(); - const new_timer = tt.add_tri_timer(tt.TIMERS, this.tri_timer); + const new_timer = tt.add_timer(tt.TIMERS, this.timer); const timer_view_menu = new TimerViewMenu(new_timer); timer_view_menu.edit_menu(); }, @@ -275,7 +275,7 @@ class TimerViewMenu { back: () => { E.showMenu(top_menu); } }, 'Reset': () => { - this.tri_timer.reset(); + this.timer.reset(); tt.set_timers_dirty(); this.back(); }, @@ -289,7 +289,7 @@ class TimerViewMenu { }, 'Delete': () => { tt.set_timers_dirty(); - switch_UI(new TimerView(tt.delete_tri_timer(tt.TIMERS, this.tri_timer))); + switch_UI(new TimerView(tt.delete_timer(tt.TIMERS, this.timer))); }, 'Cancel': () => { E.showMenu(top_menu); }, }; @@ -300,22 +300,22 @@ class TimerViewMenu { edit_menu() { const edit_menu = { '': { - title: 'Edit: ' + this.tri_timer.display_name(), + title: 'Edit: ' + this.timer.display_name(), back: () => { this.top_menu(); }, }, 'Start': this.edit_start_hms_menu.bind(this), 'Vibrate pattern': require("buzz_menu").pattern( - this.tri_timer.vibrate_pattern, - v => this.tri_timer.vibrate_pattern = v), + this.timer.vibrate_pattern, + v => this.timer.vibrate_pattern = v), 'Buzz count': { - value: this.tri_timer.buzz_count, + value: this.timer.buzz_count, min: 0, max: 15, step: 1, wrap: true, format: v => v === 0 ? "Forever" : v, onchange: v => { - this.tri_timer.buzz_count = v; + this.timer.buzz_count = v; tt.set_timers_dirty(); }, }, @@ -326,13 +326,13 @@ class TimerViewMenu { edit_start_hms_menu() { let origin_hms = { - h: Math.floor(this.tri_timer.origin / 3600), - m: Math.floor(this.tri_timer.origin / 60) % 60, - s: Math.floor(this.tri_timer.origin % 60), + h: Math.floor(this.timer.origin / 3600), + m: Math.floor(this.timer.origin / 60) % 60, + s: Math.floor(this.timer.origin % 60), }; const update_origin = () => { - this.tri_timer.origin = origin_hms.h * 3600 + this.timer.origin = origin_hms.h * 3600 + origin_hms.m * 60 + origin_hms.s; }; @@ -383,8 +383,8 @@ class TimerViewMenu { class TimerMenu { - constructor(tri_timers, focused_timer) { - this.tri_timers = tri_timers; + constructor(timers, focused_timer) { + this.timers = timers; this.focused_timer = focused_timer; } @@ -407,9 +407,9 @@ class TimerMenu { back: this.back.bind(this) } }; - this.tri_timers.forEach((tri_timer) => { - menu[tri_timer.display_status() + ' ' + tri_timer.display_name()] = - () => { switch_UI(new TimerView(tri_timer)); }; + this.timers.forEach((timer) => { + menu[timer.display_status() + ' ' + timer.display_name()] = + () => { switch_UI(new TimerView(timer)); }; }); E.showMenu(menu); } diff --git a/apps/tevtimer/lib.js b/apps/tevtimer/lib.js index 9834c5760..bda9e7bf8 100644 --- a/apps/tevtimer/lib.js +++ b/apps/tevtimer/lib.js @@ -422,35 +422,35 @@ const ACTIONS = [ // Persistent data convenience functions -function delete_tri_timer(timers, tri_timer) { - const idx = timers.indexOf(tri_timer); +function delete_timer(timers, timer) { + const idx = timers.indexOf(timer); if (idx !== -1) { timers.splice(idx, 1); } else { - console.warn('delete_tri_timer: Bug? Tried to delete a timer not in list'); + console.warn('delete_timer: Bug? Tried to delete a timer not in list'); } // Return another timer to switch UI to after deleting the focused // one return timers[Math.min(idx, timers.length - 1)]; } -function add_tri_timer(timers, tri_timer) { +function add_timer(timers, timer) { // Create a copy of current timer object - const new_timer = PrimitiveTimer.load(tri_timer.dump()); + const new_timer = PrimitiveTimer.load(timer.dump()); timers.unshift(new_timer); return new_timer; } -function set_last_viewed_timer(tri_timer) { - const idx = TIMERS.indexOf(tri_timer); +function set_last_viewed_timer(timer) { + const idx = TIMERS.indexOf(timer); if (idx == -1) { console.warn('set_last_viewed_timer: Bug? Called with a timer not found in list'); } else if (idx == 0) { console.debug('set_last_viewed_timer: Already set as last timer'); } else { - // Move tri_timer to top of list + // Move timer to top of list TIMERS.splice(idx, 1); - TIMERS.unshift(tri_timer); + TIMERS.unshift(timer); set_timers_dirty(); } } @@ -508,5 +508,5 @@ exports = {TIMERS, SETTINGS, ACTIONS, load_timers, save_timers, schedule_save_timers, save_settings, schedule_save_settings, PrimitiveTimer, TriangleTimer, as_triangle, as_linear, format_triangle, format_duration, - delete_tri_timer, add_tri_timer, set_last_viewed_timer, set_timers_dirty, set_settings_dirty, + delete_timer, add_timer, set_last_viewed_timer, set_timers_dirty, set_settings_dirty, update_system_alarms}; From 1d9b90219810b81f8a6b15cc422a64ad6ec14412 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Wed, 19 Feb 2025 18:14:13 -0600 Subject: [PATCH 09/67] Remove unused Triangle Timer code --- apps/tevtimer/lib.js | 219 +------------------------------------------ 1 file changed, 4 insertions(+), 215 deletions(-) diff --git a/apps/tevtimer/lib.js b/apps/tevtimer/lib.js index bda9e7bf8..e1512b1c2 100644 --- a/apps/tevtimer/lib.js +++ b/apps/tevtimer/lib.js @@ -111,221 +111,15 @@ class PrimitiveTimer { return loaded; } - - ////// Temporary compatibility code ////// - check_auto_pause() { - console.warn('check_auto_pause: not implemented'); - } - - time_to_next_alarm() { - console.warn('time_to_next_alarm: not implemented'); - - if (!this.is_running()) - return null; - - return this.get() / Math.abs(this.rate); - } - - time_to_next_event() { - console.warn('time_to_next_event: not implemented'); - return null; - } - - time_to_next_outer_event() { - console.warn('time_to_next_outer_event: not implemented'); - return null; - } - - time_to_end_event() { - console.warn('time_to_end_event: not implemented'); - if (this.rate <= 0 && this.get() > 0) { - return this.get() / Math.abs(this.rate); - } - return null; - } -} - - -class TriangleTimer extends PrimitiveTimer { - constructor(origin, is_running, rate, name, increment) { - super(origin, is_running, rate, name); - this.increment = increment || 1; - - this.end_alarm = false; - this.outer_alarm = false; - this.outer_action = 'Cont'; - this.pause_checkpoint = null; - this.vibrate_pattern = null; - this.buzz_count = 4; - } - - provisional_name() { - const origin_as_tri = as_triangle( - this.origin, - this.increment - ); - return (this.rate >= 0 ? 'U' : 'D') - + ' ' - + origin_as_tri[0] + '/' + origin_as_tri[1] - + ' x' + this.increment; - } - - start() { - super.start(); - this.emit('status'); - } - - pause() { - super.pause(); - this.emit('status'); - } - - check_auto_pause() { - const current_time = super.get(); - - if (this.is_running() && - this.outer_action == 'Pause') { - if (this.pause_checkpoint === null) { - this.pause_checkpoint = current_time - + this.time_to_next_outer_event() * this.rate; - console.debug('timer auto-pause setup: ' + this.pause_checkpoint); - } else if ( - (this.rate >= 0 && current_time >= this.pause_checkpoint) - || (this.rate < 0 && current_time <= this.pause_checkpoint) - ) { - console.debug('timer auto-pause triggered'); - this.pause(); - this.set(this.pause_checkpoint); - this.pause_checkpoint = null; - this.emit('auto-pause'); - } - } else { - this.pause_checkpoint = null; - } - } - - reset() { - this.pause_checkpoint = null; - return super.reset(); - } - - get() { - return super.get(); - } - - set(new_value) { - return super.set(new_value); - } - - time_to_next_alarm() { - if (!this.is_running()) - return null; - - if (this.outer_alarm) { - return this.time_to_next_outer_event(); - } - - if (this.end_alarm) { - return this.time_to_end_event(); - } - - return null; - } - - time_to_next_event() { - let next = null; - - if (this.outer_alarm || this.outer_action !== 'Cont') { - next = this.time_to_next_outer_event(); - } - - if (next === null) { - next = this.time_to_end_event(); - } - - return next - } - - time_to_next_outer_event() { - const as_tri = as_triangle(super.get(), this.increment); - let inner_left = this.rate > 0 ? as_tri[0] - as_tri[1] : as_tri[1]; - // Avoid getting stuck if we're paused precisely on the event time - if (!inner_left) { - inner_left = as_tri[0] + Math.sign(this.rate) * this.increment; - } - return Math.max(0, inner_left / Math.abs(this.rate)); - } - - time_to_end_event() { - if (this.rate <= 0 && this.get() > 0) { - return this.get() / Math.abs(this.rate); - } - return null; - } - - dump() { - let data = super.dump(); - data.cls = 'TriangleTimer'; - data.increment = this.increment; - data.end_alarm = this.end_alarm; - data.outer_alarm = this.outer_alarm; - data.outer_action = this.outer_action; - data.pause_checkpoint = this.pause_checkpoint; - data.vibrate_pattern = this.vibrate_pattern; - data.buzz_count = this.buzz_count; - return data; - } - - static load(data) { - if (!(data.cls == 'TriangleTimer' && data.version == 0)) { - console.error('Incompatible data type for loading TriangleTimer state'); - } - let loaded = new this( - data.origin, false, data.rate, data.name, data.increment); - loaded._start_time = data.start_time; - loaded._pause_time = data.pause_time; - loaded.end_alarm = data.end_alarm; - loaded.outer_alarm = data.outer_alarm; - loaded.outer_action = data.outer_action; - loaded.pause_checkpoint = data.pause_checkpoint; - loaded.vibrate_pattern = data.vibrate_pattern; - if (data.buzz_count !== undefined) { - loaded.buzz_count = data.buzz_count; - } - return loaded; - } } function fixed_ceil(value) { - // JavaScript sucks balls + // JavaScript sucks return Math.ceil(Math.round(value * 1e10) / 1e10); } -function as_triangle(linear_time, increment) { - if (increment === undefined) increment = 1; - linear_time = linear_time / increment; - const outer = fixed_ceil((Math.sqrt(linear_time * 8 + 1) - 1) / 2); - const inner = outer - (outer * (outer + 1) / 2 - linear_time); - return [outer * increment, inner * increment]; -} - -function as_linear(triangle_time, increment) { - if (increment === undefined) increment = 1; - const outer = triangle_time[0], inner = triangle_time[1]; - return (outer + (outer - 1) % increment + 1) - * fixed_ceil(outer / increment) / 2 - - outer + inner; -} - - -function format_triangle(tri_timer) { - const tri = as_triangle(tri_timer.get(), tri_timer.increment); - return tri[0] + '/' + Math.ceil(tri[1]); -} - - function format_duration(msec, have_seconds) { if (msec < 0) { return '-' + format_duration(-msec, have_seconds); @@ -414,11 +208,6 @@ const SETTINGS = Object.assign({ var TIMERS = load_timers(); -const ACTIONS = [ - 'Cont', - 'Pause', -]; - // Persistent data convenience functions @@ -504,9 +293,9 @@ E.on('kill', () => { save_timers(); }); E.on('kill', () => { save_settings(); }); -exports = {TIMERS, SETTINGS, ACTIONS, +exports = {TIMERS, SETTINGS, load_timers, save_timers, schedule_save_timers, save_settings, schedule_save_settings, - PrimitiveTimer, TriangleTimer, - as_triangle, as_linear, format_triangle, format_duration, + PrimitiveTimer, + format_duration, delete_timer, add_timer, set_last_viewed_timer, set_timers_dirty, set_settings_dirty, update_system_alarms}; From 3a2b2bc60ef17482b2df4edd6c34c9d4d236220b Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Sun, 23 Feb 2025 18:10:58 -0600 Subject: [PATCH 10/67] Implement swipe left/right to switch timers --- apps/tevtimer/app.js | 50 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js index d1230b2a8..ad94e98d9 100644 --- a/apps/tevtimer/app.js +++ b/apps/tevtimer/app.js @@ -6,9 +6,16 @@ const tt = require('tevtimer'); // UI // -ROW_IDS = ['row1', 'row2', 'row3']; +// Length of time displaying timer view before moving timer to top of +// timer list +const MOVE_TO_TOP_TIMEOUT = 5000; -FONT = { +// Min number of pixels of movement to recognize a touchscreen drag/swipe +const DRAG_THRESHOLD = 50; + +const ROW_IDS = ['row1', 'row2', 'row3']; + +const FONT = { 'row1': { 'start hh:mm:ss': '12x20', 'current hh:mm:ss': '12x20', @@ -64,7 +71,6 @@ class TimerView { this._initLayout(); this.layout.clear(); this.render(); - tt.set_last_viewed_timer(this.timer); // Physical button handler this.listeners.button = setWatch( @@ -72,6 +78,42 @@ class TimerView { BTN, {edge: 'falling', debounce: 50, repeat: true} ); + + let distanceX = null; + function dragHandler(ev) { + if (ev.b) { + if (distanceX === null) { + // Drag started + distanceX = ev.dx; + } else { + // Drag in progress + distanceX += ev.dx; + } + } else { + // Drag released + distanceX = null; + } + if (Math.abs(distanceX) > DRAG_THRESHOLD) { + // Horizontal scroll threshold reached + Bangle.buzz(50, 0.5); + // Switch UI view to next or previous timer in list based on + // sign of distanceX + let new_index = tt.TIMERS.indexOf(this.timer) + Math.sign(distanceX); + if (new_index < 0) { + new_index = tt.TIMERS.length - 1; + } else if (new_index >= tt.TIMERS.length) { + new_index = 0; + } + switch_UI(new TimerView(tt.TIMERS[new_index])); + distanceX = null; + } + } + this.listeners.drag = dragHandler.bind(this); + Bangle.on('drag', this.listeners.drag); + + // Auto move-to-top on use handler + this.listeners.to_top_timeout = setTimeout( + tt.set_last_viewed_timer, MOVE_TO_TOP_TIMEOUT, this.timer); } stop() { @@ -80,6 +122,8 @@ class TimerView { this.timer_timeout = null; } clearWatch(this.listeners.button); + Bangle.removeListener('drag', this.listeners.drag); + clearTimeout(this.listeners.to_top_timeout); Bangle.setUI(); } From 11ab4144d47646a20b1b98b14b46a8a5725adee0 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Fri, 21 Mar 2025 13:58:42 -0500 Subject: [PATCH 11/67] Implement naming timers with textinput apps --- apps/tevtimer/app.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js index ad94e98d9..aa32257ff 100644 --- a/apps/tevtimer/app.js +++ b/apps/tevtimer/app.js @@ -342,11 +342,26 @@ class TimerViewMenu { } edit_menu() { + let keyboard = null; + try { keyboard = require("textinput"); } catch (e) {} + const edit_menu = { '': { title: 'Edit: ' + this.timer.display_name(), back: () => { this.top_menu(); }, }, + 'Name': { + value: this.timer.name, + onchange: () => { + setTimeout(() => { + keyboard.input({text:this.timer.name}).then(text => { + this.timer.name = text; + tt.set_timers_dirty(); + setTimeout(() => { this.edit_menu(); }, 0); + }); + }, 0); + } + }, 'Start': this.edit_start_hms_menu.bind(this), 'Vibrate pattern': require("buzz_menu").pattern( this.timer.vibrate_pattern, @@ -365,6 +380,10 @@ class TimerViewMenu { }, }; + if (!keyboard) { + delete edit_menu.Name; + } + E.showMenu(edit_menu); } From 2b3ec99bd42adcca3d71a758b032987e5f66c3d4 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Sun, 23 Mar 2025 19:22:15 -0500 Subject: [PATCH 12/67] Move `timeout` var to `listeners` object --- apps/tevtimer/app.js | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js index aa32257ff..1d515f477 100644 --- a/apps/tevtimer/app.js +++ b/apps/tevtimer/app.js @@ -64,7 +64,7 @@ class TimerView { this.layout = null; this.listeners = {}; - this.timer_timeout = null; + this.listeners.timer_render_timeout = null; } start() { @@ -117,9 +117,9 @@ class TimerView { } stop() { - if (this.timer_timeout !== null) { - clearTimeout(this.timer_timeout); - this.timer_timeout = null; + if (this.listeners.timer_render_timeout !== null) { + clearTimeout(this.listeners.timer_render_timeout); + this.listeners.timer_render_timeout = null; } clearWatch(this.listeners.button); Bangle.removeListener('drag', this.listeners.drag); @@ -217,9 +217,9 @@ class TimerView { } if (this.timer.is_running()) { - if (this.timer_timeout) { - clearTimeout(this.timer_timeout); - this.timer_timeout = null; + if (this.listeners.timer_render_timeout) { + clearTimeout(this.listeners.timer_render_timeout); + this.listeners.timer_render_timeout = null; } // Set up timeout to render timer again when needed @@ -235,8 +235,11 @@ class TimerView { // sometimes triggering too early. next_update = next_update / Math.abs(this.timer.rate) + 50; console.debug('Next render update scheduled in ' + next_update); - this.timer_timeout = setTimeout( - () => { this.timer_timeout = null; this.render('timer'); }, + this.listeners.timer_render_timeout = setTimeout( + () => { + this.listeners.timer_render_timeout = null; + this.render('timer'); + }, next_update ); } From 7d5158123d4c2e24099af587c8c83687f1e0f1e2 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Thu, 27 Mar 2025 17:15:48 -0500 Subject: [PATCH 13/67] Beginnings of display format UI --- apps/tevtimer/app.js | 108 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 103 insertions(+), 5 deletions(-) diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js index 1d515f477..1cde7da6a 100644 --- a/apps/tevtimer/app.js +++ b/apps/tevtimer/app.js @@ -24,6 +24,10 @@ const FONT = { 'start hh:mm': '12x20', 'current hh:mm': '12x20', 'time hh:mm': '12x20', + + 'name': '12x20', + + 'mode': '12x20', }, 'row2': { @@ -34,6 +38,10 @@ const FONT = { 'start hh:mm': 'Vector:56x42', 'current hh:mm': 'Vector:56x42', 'time hh:mm': 'Vector:56x42', + + 'name': 'Vector:34x42', + + 'mode': 'Vector:34x42', }, 'row3': { @@ -44,9 +52,23 @@ const FONT = { 'start hh:mm': 'Vector:56x56', 'current hh:mm': 'Vector:56x56', 'time hh:mm': 'Vector:56x56', + + 'name': 'Vector:34x56', + + 'mode': 'Vector:34x56', } }; +VIEW_MODE_MENU = [ + 'start hh:mm:ss', + 'start hh:mm', + 'current hh:mm:ss', + 'current hh:mm', + 'time hh:mm:ss', + 'time hh:mm', + 'name', +]; + function row_font(row_name, mode_name) { font = FONT[row_name][mode_name]; @@ -136,22 +158,22 @@ class TimerView { { type: 'txt', id: 'row1', - label: '88:88:88', - font: row_font('row1', tt.SETTINGS.view_mode['row1']), + label: '', + font: row_font('row1', tt.SETTINGS.view_mode.row1), fillx: 1, }, { type: 'txt', id: 'row2', - label: '88:88:88', - font: row_font('row2', tt.SETTINGS.view_mode['row2']), + label: '', + font: row_font('row2', tt.SETTINGS.view_mode.row2), fillx: 1, }, { type: 'txt', id: 'row3', label: '', - font: row_font('row3', tt.SETTINGS.view_mode['row3']), + font: row_font('row3', tt.SETTINGS.view_mode.row3), fillx: 1, }, { @@ -275,6 +297,79 @@ class TimerView { } +class TimerModeView { + constructor(timer) { + this.timer = timer; + + this.layout = null; + this.listeners = {}; + } + + start() { + this._initLayout(); + this.layout.clear(); + this.render(); + } + + stop() { + + } + + _initLayout() { + const layout = new Layout( + { + type: 'v', + bgCol: g.theme.bg, + c: [ + { + type: 'txt', + id: 'row1', + label: tt.SETTINGS.view_mode.row1, + font: row_font('row1', 'mode'), + fillx: 1, + }, + { + type: 'txt', + id: 'row2', + label: tt.SETTINGS.view_mode.row2, + font: row_font('row2', 'mode'), + fillx: 1, + }, + { + type: 'txt', + id: 'row3', + label: tt.SETTINGS.view_mode.row3, + font: row_font('row3', 'mode'), + fillx: 1, + }, + { + type: 'h', + id: 'buttons', + c: [ + {type: 'btn', font: '6x8:2', fillx: 1, label: 'Cancel', id: 'cancel_btn', + cb: () => { + switch_UI(new TimerViewMenu(this.timer)); + } + }, + {type: 'btn', font: '6x8:2', fillx: 1, label: 'OK', id: 'ok_btn', + cb: () => { + switch_UI(new TimerView(this.timer)); + } + }, + ] + } + ] + } + ); + this.layout = layout; + } + + render() { + this.layout.render(); + } +} + + class TimerViewMenu { constructor(timer) { this.timer = timer; @@ -303,6 +398,9 @@ class TimerViewMenu { switch_UI(new TimerMenu(tt.TIMERS, this.timer)); }, 'Edit': this.edit_menu.bind(this), + 'Format': () => { + switch_UI(new TimerModeView(this.timer)); + }, 'Add': () => { tt.set_timers_dirty(); const new_timer = tt.add_timer(tt.TIMERS, this.timer); From dcfedb5b45a122c58a12ddc62a7913205323eb37 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Sun, 30 Mar 2025 18:46:15 -0500 Subject: [PATCH 14/67] Display view mode change arrows --- apps/tevtimer/app.js | 109 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 92 insertions(+), 17 deletions(-) diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js index 1cde7da6a..f042de7e3 100644 --- a/apps/tevtimer/app.js +++ b/apps/tevtimer/app.js @@ -41,7 +41,7 @@ const FONT = { 'name': 'Vector:34x42', - 'mode': 'Vector:34x42', + 'mode': 'Vector:26x42', }, 'row3': { @@ -55,7 +55,7 @@ const FONT = { 'name': 'Vector:34x56', - 'mode': 'Vector:34x56', + 'mode': 'Vector:26x56', } }; @@ -69,6 +69,16 @@ VIEW_MODE_MENU = [ 'name', ]; +VIEW_MODE_DISPLAY = { + 'start hh:mm:ss': 'Start HMS', + 'start hh:mm': 'Start HM', + 'current hh:mm:ss': 'Curr HMS', + 'current hh:mm': 'Curr HM', + 'time hh:mm:ss': 'Time HMS', + 'time hh:mm': 'Time HM', + 'name': 'Name', +} + function row_font(row_name, mode_name) { font = FONT[row_name][mode_name]; @@ -316,31 +326,96 @@ class TimerModeView { } _initLayout() { + function draw_triangle(lay, flip) { + flip = flip ? lay.width : 0; + g.setColor(g.theme.fg2) + .fillPoly([flip + lay.x, lay.y + lay.height / 2, + lay.x + lay.width - flip, lay.y, + lay.x + lay.width - flip, lay.y + lay.height]); + } + const layout = new Layout( { type: 'v', bgCol: g.theme.bg, c: [ { - type: 'txt', - id: 'row1', - label: tt.SETTINGS.view_mode.row1, - font: row_font('row1', 'mode'), - fillx: 1, + type: 'h', + c: [ + { + type: 'custom', + id: 'row1_left', + render: lay => draw_triangle(lay, false), + width: 10, + height: 10, + }, + { + type: 'txt', + id: 'row1', + label: VIEW_MODE_DISPLAY[tt.SETTINGS.view_mode.row1], + font: row_font('row1', 'mode'), + fillx: 1, + }, + { + type: 'custom', + id: 'row1_right', + render: lay => draw_triangle(lay, true), + width: 10, + height: 10, + }, + ], }, { - type: 'txt', - id: 'row2', - label: tt.SETTINGS.view_mode.row2, - font: row_font('row2', 'mode'), - fillx: 1, + type: 'h', + c: [ + { + type: 'custom', + id: 'row2_left', + render: lay => draw_triangle(lay, false), + width: 10, + height: 10, + }, + { + type: 'txt', + id: 'row2', + label: VIEW_MODE_DISPLAY[tt.SETTINGS.view_mode.row2], + font: row_font('row2', 'mode'), + fillx: 1, + }, + { + type: 'custom', + id: 'row2_right', + render: lay => draw_triangle(lay, true), + width: 10, + height: 10, + }, + ], }, { - type: 'txt', - id: 'row3', - label: tt.SETTINGS.view_mode.row3, - font: row_font('row3', 'mode'), - fillx: 1, + type: 'h', + c: [ + { + type: 'custom', + id: 'row3_left', + render: lay => draw_triangle(lay, false), + width: 10, + height: 10, + }, + { + type: 'txt', + id: 'row3', + label: VIEW_MODE_DISPLAY[tt.SETTINGS.view_mode.row3], + font: row_font('row3', 'mode'), + fillx: 1, + }, + { + type: 'custom', + id: 'row3_right', + render: lay => draw_triangle(lay, true), + width: 10, + height: 10, + }, + ], }, { type: 'h', From 4854c45762b3ceb76db07500bd51e469bf2984f2 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Sun, 30 Mar 2025 19:00:33 -0500 Subject: [PATCH 15/67] =?UTF-8?q?Change=20=E2=80=9Cview=20mode=E2=80=9D=20?= =?UTF-8?q?terminology=20to=20=E2=80=9Cformat=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/tevtimer/app.js | 24 ++++++++++++------------ apps/tevtimer/lib.js | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js index f042de7e3..771f52fed 100644 --- a/apps/tevtimer/app.js +++ b/apps/tevtimer/app.js @@ -59,7 +59,7 @@ const FONT = { } }; -VIEW_MODE_MENU = [ +FORMAT_MENU = [ 'start hh:mm:ss', 'start hh:mm', 'current hh:mm:ss', @@ -69,7 +69,7 @@ VIEW_MODE_MENU = [ 'name', ]; -VIEW_MODE_DISPLAY = { +FORMAT_DISPLAY = { 'start hh:mm:ss': 'Start HMS', 'start hh:mm': 'Start HM', 'current hh:mm:ss': 'Curr HMS', @@ -169,21 +169,21 @@ class TimerView { type: 'txt', id: 'row1', label: '', - font: row_font('row1', tt.SETTINGS.view_mode.row1), + font: row_font('row1', tt.SETTINGS.format.row1), fillx: 1, }, { type: 'txt', id: 'row2', label: '', - font: row_font('row2', tt.SETTINGS.view_mode.row2), + font: row_font('row2', tt.SETTINGS.format.row2), fillx: 1, }, { type: 'txt', id: 'row3', label: '', - font: row_font('row3', tt.SETTINGS.view_mode.row3), + font: row_font('row3', tt.SETTINGS.format.row3), fillx: 1, }, { @@ -220,7 +220,7 @@ class TimerView { for (var id of ROW_IDS) { const elem = this.layout[id]; - let mode = tt.SETTINGS.view_mode[id]; + let mode = tt.SETTINGS.format[id]; if (mode == 'start hh:mm:ss') { elem.label = tt.format_duration(this.timer.origin / Math.abs(this.timer.rate), true); update_interval = Math.min(update_interval, 1); @@ -288,7 +288,7 @@ class TimerView { _update_fonts() { for (var id of ROW_IDS) { const elem = this.layout[id]; - elem.font = row_font(id, tt.SETTINGS.view_mode[id]); + elem.font = row_font(id, tt.SETTINGS.format[id]); this.layout.clear(elem); this.layout.render(elem); } @@ -307,7 +307,7 @@ class TimerView { } -class TimerModeView { +class TimerFormatView { constructor(timer) { this.timer = timer; @@ -352,7 +352,7 @@ class TimerModeView { { type: 'txt', id: 'row1', - label: VIEW_MODE_DISPLAY[tt.SETTINGS.view_mode.row1], + label: FORMAT_DISPLAY[tt.SETTINGS.format.row1], font: row_font('row1', 'mode'), fillx: 1, }, @@ -378,7 +378,7 @@ class TimerModeView { { type: 'txt', id: 'row2', - label: VIEW_MODE_DISPLAY[tt.SETTINGS.view_mode.row2], + label: FORMAT_DISPLAY[tt.SETTINGS.format.row2], font: row_font('row2', 'mode'), fillx: 1, }, @@ -404,7 +404,7 @@ class TimerModeView { { type: 'txt', id: 'row3', - label: VIEW_MODE_DISPLAY[tt.SETTINGS.view_mode.row3], + label: FORMAT_DISPLAY[tt.SETTINGS.format.row3], font: row_font('row3', 'mode'), fillx: 1, }, @@ -474,7 +474,7 @@ class TimerViewMenu { }, 'Edit': this.edit_menu.bind(this), 'Format': () => { - switch_UI(new TimerModeView(this.timer)); + switch_UI(new TimerFormatView(this.timer)); }, 'Add': () => { tt.set_timers_dirty(); diff --git a/apps/tevtimer/lib.js b/apps/tevtimer/lib.js index e1512b1c2..b1b7e6082 100644 --- a/apps/tevtimer/lib.js +++ b/apps/tevtimer/lib.js @@ -199,7 +199,7 @@ function schedule_save_settings() { // Default settings const SETTINGS = Object.assign({ - 'view_mode': { + 'format': { 'row1': 'time hh:mm', 'row2': 'start hh:mm:ss', 'row3': 'current hh:mm:ss', From 127b1b5f5eb29efa36337ef5268f540dd38acca0 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Wed, 2 Apr 2025 17:04:23 -0500 Subject: [PATCH 16/67] Finish implementation of TimerFormatView --- apps/tevtimer/app.js | 183 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 151 insertions(+), 32 deletions(-) diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js index 771f52fed..ac85f0aaf 100644 --- a/apps/tevtimer/app.js +++ b/apps/tevtimer/app.js @@ -13,6 +13,8 @@ const MOVE_TO_TOP_TIMEOUT = 5000; // Min number of pixels of movement to recognize a touchscreen drag/swipe const DRAG_THRESHOLD = 50; +const ARROW_BTN_SIZE = 15; + const ROW_IDS = ['row1', 'row2', 'row3']; const FONT = { @@ -33,13 +35,13 @@ const FONT = { 'row2': { 'start hh:mm:ss': 'Vector:34x42', 'current hh:mm:ss': 'Vector:34x42', - 'time hh:mm:ss': 'Vector:34x42', + 'time hh:mm:ss': 'Vector:24x42', 'start hh:mm': 'Vector:56x42', 'current hh:mm': 'Vector:56x42', 'time hh:mm': 'Vector:56x42', - 'name': 'Vector:34x42', + 'name': 'Vector:24x42', 'mode': 'Vector:26x42', }, @@ -47,13 +49,13 @@ const FONT = { 'row3': { 'start hh:mm:ss': 'Vector:34x56', 'current hh:mm:ss': 'Vector:34x56', - 'time hh:mm:ss': 'Vector:34x56', + 'time hh:mm:ss': 'Vector:24x56', 'start hh:mm': 'Vector:56x56', 'current hh:mm': 'Vector:56x56', 'time hh:mm': 'Vector:56x56', - 'name': 'Vector:34x56', + 'name': 'Vector:24x56', 'mode': 'Vector:26x56', } @@ -101,6 +103,7 @@ class TimerView { start() { this._initLayout(); + this.layout.update(); this.layout.clear(); this.render(); @@ -313,19 +316,102 @@ class TimerFormatView { this.layout = null; this.listeners = {}; + + // Get format name indeces for UI + this.format_idx = {}; + for (var row_id of ROW_IDS) { + let idx = FORMAT_MENU.indexOf(tt.SETTINGS.format[row_id]); + if (idx === -1) { + console.warn('Unknown format "' + tt.SETTINGS.format[row_id] + '"'); + idx = 0; + } + this.format_idx[row_id] = idx; + } } start() { this._initLayout(); + this.layout.update(); this.layout.clear(); this.render(); + + // Drag handler + let distanceX = null; + function dragHandler(ev) { + if (ev.b) { + if (distanceX === null) { + // Drag started + distanceX = ev.dx; + } else { + // Drag in progress + distanceX += ev.dx; + } + } else { + // Drag released + distanceX = null; + } + if (Math.abs(distanceX) > DRAG_THRESHOLD) { + // Horizontal drag threshold reached + // Increment or decrement row's format index based on sign of + // distanceX + for (var row_id of ROW_IDS) { + if (ev.y < this.layout[row_id].y + this.layout[row_id].h) { + Bangle.buzz(50, 0.5); + if (Math.sign(distanceX) > 0) { + this.incr_format_idx(row_id); + } else { + this.decr_format_idx(row_id); + } + distanceX = null; + break; + } + } + } + } + this.listeners.drag = dragHandler.bind(this); + Bangle.on('drag', this.listeners.drag); + + // Touch handler + function touchHandler(button, xy) { + // Increment or decrement row's format index based on the arrow tapped + for (var row_id of ROW_IDS) { + const prev_id = row_id + '.prev'; + const next_id = row_id + '.next'; + if (xy.x >= this.layout[prev_id].x + && xy.x <= this.layout[prev_id].x + this.layout[prev_id].w + && xy.y >= this.layout[prev_id].y + && xy.y <= this.layout[prev_id].y + this.layout[prev_id].h) { + this.decr_format_idx(row_id); + break; + } else if (xy.x >= this.layout[next_id].x + && xy.x <= this.layout[next_id].x + this.layout[next_id].w + && xy.y >= this.layout[next_id].y + && xy.y <= this.layout[next_id].y + this.layout[next_id].h) { + this.incr_format_idx(row_id); + break; + } + } + } + this.listeners.touch = touchHandler.bind(this); + Bangle.on('touch', this.listeners.touch); + + // Physical button handler + this.listeners.button = setWatch( + this.ok.bind(this), + BTN, + {edge: 'falling', debounce: 50, repeat: true} + ); } stop() { - + Bangle.removeListener('drag', this.listeners.drag); + Bangle.removeListener('touch', this.listeners.touch); + clearWatch(this.listeners.button); } _initLayout() { + // Render right-pointing triangle if `flip`, else left-pointing + // triangle function draw_triangle(lay, flip) { flip = flip ? lay.width : 0; g.setColor(g.theme.fg2) @@ -344,24 +430,24 @@ class TimerFormatView { c: [ { type: 'custom', - id: 'row1_left', + id: 'row1.prev', render: lay => draw_triangle(lay, false), - width: 10, - height: 10, + width: ARROW_BTN_SIZE, + height: ARROW_BTN_SIZE, }, { type: 'txt', id: 'row1', - label: FORMAT_DISPLAY[tt.SETTINGS.format.row1], + label: FORMAT_DISPLAY[FORMAT_MENU[this.format_idx.row1]], font: row_font('row1', 'mode'), fillx: 1, }, { type: 'custom', - id: 'row1_right', + id: 'row1.next', render: lay => draw_triangle(lay, true), - width: 10, - height: 10, + width: ARROW_BTN_SIZE, + height: ARROW_BTN_SIZE, }, ], }, @@ -370,24 +456,24 @@ class TimerFormatView { c: [ { type: 'custom', - id: 'row2_left', + id: 'row2.prev', render: lay => draw_triangle(lay, false), - width: 10, - height: 10, + width: ARROW_BTN_SIZE, + height: ARROW_BTN_SIZE, }, { type: 'txt', id: 'row2', - label: FORMAT_DISPLAY[tt.SETTINGS.format.row2], + label: FORMAT_DISPLAY[FORMAT_MENU[this.format_idx.row2]], font: row_font('row2', 'mode'), fillx: 1, }, { type: 'custom', - id: 'row2_right', + id: 'row2.next', render: lay => draw_triangle(lay, true), - width: 10, - height: 10, + width: ARROW_BTN_SIZE, + height: ARROW_BTN_SIZE, }, ], }, @@ -396,24 +482,24 @@ class TimerFormatView { c: [ { type: 'custom', - id: 'row3_left', + id: 'row3.prev', render: lay => draw_triangle(lay, false), - width: 10, - height: 10, + width: ARROW_BTN_SIZE, + height: ARROW_BTN_SIZE, }, { type: 'txt', id: 'row3', - label: FORMAT_DISPLAY[tt.SETTINGS.format.row3], + label: FORMAT_DISPLAY[FORMAT_MENU[this.format_idx.row3]], font: row_font('row3', 'mode'), fillx: 1, }, { type: 'custom', - id: 'row3_right', + id: 'row3.next', render: lay => draw_triangle(lay, true), - width: 10, - height: 10, + width: ARROW_BTN_SIZE, + height: ARROW_BTN_SIZE, }, ], }, @@ -422,14 +508,10 @@ class TimerFormatView { id: 'buttons', c: [ {type: 'btn', font: '6x8:2', fillx: 1, label: 'Cancel', id: 'cancel_btn', - cb: () => { - switch_UI(new TimerViewMenu(this.timer)); - } + cb: () => { this.cancel(); } }, {type: 'btn', font: '6x8:2', fillx: 1, label: 'OK', id: 'ok_btn', - cb: () => { - switch_UI(new TimerView(this.timer)); - } + cb: () => { this.ok(); } }, ] } @@ -442,6 +524,43 @@ class TimerFormatView { render() { this.layout.render(); } + + update_row(row_id) { + const elem = this.layout[row_id]; + elem.label = FORMAT_DISPLAY[FORMAT_MENU[this.format_idx[row_id]]]; + this.layout.clear(elem); + this.layout.render(elem); + } + + incr_format_idx(row_id) { + this.format_idx[row_id] += 1; + if (this.format_idx[row_id] >= FORMAT_MENU.length) { + this.format_idx[row_id] = 0; + } + this.update_row(row_id); + } + + decr_format_idx(row_id) { + this.format_idx[row_id] -= 1; + if (this.format_idx[row_id] < 0) { + this.format_idx[row_id] = FORMAT_MENU.length - 1; + } + this.update_row(row_id); + } + + // Save new format settings and return to TimerView + ok() { + for (var row_id of ROW_IDS) { + tt.SETTINGS.format[row_id] = FORMAT_MENU[this.format_idx[row_id]]; + } + tt.set_settings_dirty(); + switch_UI(new TimerView(this.timer)); + } + + // Return to TimerViewMenu without saving changes + cancel() { + switch_UI(new TimerViewMenu(this.timer)); + } } From b6d169f1890b60302569d7df3a9befc4a7709d7f Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Wed, 2 Apr 2025 19:21:01 -0500 Subject: [PATCH 17/67] Fix bugs in UI timer update timing --- apps/tevtimer/app.js | 93 +++++++++++++++++++++++++++++--------------- apps/tevtimer/lib.js | 4 ++ 2 files changed, 65 insertions(+), 32 deletions(-) diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js index ac85f0aaf..58f3e6b37 100644 --- a/apps/tevtimer/app.js +++ b/apps/tevtimer/app.js @@ -92,6 +92,31 @@ function row_font(row_name, mode_name) { } +// Determine time in milliseconds until next display update for a timer +// that should be updated every `interval` milliseconds. +function next_time_update(interval, curr_time, direction) { + if (interval <= 0) { + // Don't update if interval is zero or negative + return Infinity; + } + + // Find the next time we should update the display + let next_update = (curr_time % interval); + if (direction < 0) { + next_update = 1 - next_update; + } + if (next_update < 0) { + // Handle negative modulus + next_update += interval; + } + next_update = interval - next_update; + + // Add compensating factor of 50ms due to timeouts apparently + // sometimes triggering too early. + return next_update + 50; +} + + class TimerView { constructor(timer) { this.timer = timer; @@ -223,27 +248,42 @@ class TimerView { for (var id of ROW_IDS) { const elem = this.layout[id]; + const running = this.timer.is_running(); let mode = tt.SETTINGS.format[id]; if (mode == 'start hh:mm:ss') { elem.label = tt.format_duration(this.timer.origin / Math.abs(this.timer.rate), true); - update_interval = Math.min(update_interval, 1); } else if (mode == 'current hh:mm:ss') { elem.label = tt.format_duration(this.timer.get() / Math.abs(this.timer.rate), true); - update_interval = Math.min(update_interval, 1); + if (running) { + update_interval = Math.min( + update_interval, + next_time_update(1000, this.timer.get_msec(), this.timer.rate) + ); + } } else if (mode == 'time hh:mm:ss') { elem.label = locale.time(new Date()).trim(); - update_interval = Math.min(update_interval, 1); + update_interval = Math.min( + update_interval, + next_time_update(1000, Date.now(), 1) + ); } else if (mode == 'start hh:mm') { elem.label = tt.format_duration(this.timer.origin / Math.abs(this.timer.rate), false); - update_interval = Math.min(update_interval, 60); } else if (mode == 'current hh:mm') { elem.label = tt.format_duration(this.timer.get() / Math.abs(this.timer.rate), false); - update_interval = Math.min(update_interval, 60); + if (running) { + // Update every minute for current HM when running + update_interval = Math.min( + update_interval, + next_time_update(60000, this.timer.get_msec(), this.timer.rate) + ); + } } else if (mode == 'time hh:mm') { elem.label = locale.time(new Date(), 1).trim(); - update_interval = Math.min(update_interval, 60); - + update_interval = Math.min( + update_interval, + next_time_update(60000, Date.now(), 1) + ); } else if (mode == 'name') { elem.label = this.timer.display_name(); } @@ -251,33 +291,22 @@ class TimerView { this.layout.render(elem); } - if (this.timer.is_running()) { - if (this.listeners.timer_render_timeout) { - clearTimeout(this.listeners.timer_render_timeout); - this.listeners.timer_render_timeout = null; - } - // Set up timeout to render timer again when needed - if (update_interval !== Infinity) { + if (this.listeners.timer_render_timeout) { + clearTimeout(this.listeners.timer_render_timeout); + this.listeners.timer_render_timeout = null; + } - // Calculate approximate time next render is needed. - let next_update = this.timer.get() % update_interval; - if (next_update < 0) { - next_update = 1 + next_update; - } - // Convert next_update from seconds to milliseconds and add - // compensating factor of 50ms due to timeouts apparently - // sometimes triggering too early. - next_update = next_update / Math.abs(this.timer.rate) + 50; - console.debug('Next render update scheduled in ' + next_update); - this.listeners.timer_render_timeout = setTimeout( - () => { - this.listeners.timer_render_timeout = null; - this.render('timer'); - }, - next_update - ); - } + // Set up timeout to render timer again when needed + if (update_interval !== Infinity) { + console.debug('Next render update scheduled in ' + update_interval); + this.listeners.timer_render_timeout = setTimeout( + () => { + this.listeners.timer_render_timeout = null; + this.render('timer'); + }, + update_interval + ); } } diff --git a/apps/tevtimer/lib.js b/apps/tevtimer/lib.js index b1b7e6082..98ac5ebef 100644 --- a/apps/tevtimer/lib.js +++ b/apps/tevtimer/lib.js @@ -76,6 +76,10 @@ class PrimitiveTimer { return this.origin + (this.rate * elapsed); } + get_msec() { + return this.get() / Math.abs(this.rate); + } + set(new_value) { const now = Date.now(); this._start_time = (now - new_value / this.rate) From 91edde04287d3bf7cf078569b7d4e27b64daa811 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Fri, 4 Apr 2025 14:19:30 -0500 Subject: [PATCH 18/67] Bit of code simplification/cleanup --- apps/tevtimer/app.js | 60 +++++++++++++++++++++----------------------- apps/tevtimer/lib.js | 37 +++++++++++++++++++-------- 2 files changed, 55 insertions(+), 42 deletions(-) diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js index 58f3e6b37..2d84b2060 100644 --- a/apps/tevtimer/app.js +++ b/apps/tevtimer/app.js @@ -61,7 +61,7 @@ const FONT = { } }; -FORMAT_MENU = [ +const FORMAT_MENU = [ 'start hh:mm:ss', 'start hh:mm', 'current hh:mm:ss', @@ -71,7 +71,7 @@ FORMAT_MENU = [ 'name', ]; -FORMAT_DISPLAY = { +const FORMAT_DISPLAY = { 'start hh:mm:ss': 'Start HMS', 'start hh:mm': 'Start HM', 'current hh:mm:ss': 'Curr HMS', @@ -79,11 +79,11 @@ FORMAT_DISPLAY = { 'time hh:mm:ss': 'Time HMS', 'time hh:mm': 'Time HM', 'name': 'Name', -} +}; function row_font(row_name, mode_name) { - font = FONT[row_name][mode_name]; + let font = FONT[row_name][mode_name]; if (font === undefined) { console.error('Unknown font for row_font("' + row_name + '", "' + mode_name + '")'); return '12x20'; @@ -101,7 +101,7 @@ function next_time_update(interval, curr_time, direction) { } // Find the next time we should update the display - let next_update = (curr_time % interval); + let next_update = tt.mod(curr_time, interval); if (direction < 0) { next_update = 1 - next_update; } @@ -159,12 +159,9 @@ class TimerView { // Switch UI view to next or previous timer in list based on // sign of distanceX let new_index = tt.TIMERS.indexOf(this.timer) + Math.sign(distanceX); - if (new_index < 0) { - new_index = tt.TIMERS.length - 1; - } else if (new_index >= tt.TIMERS.length) { - new_index = 0; - } - switch_UI(new TimerView(tt.TIMERS[new_index])); + switch_UI(new TimerView(tt.TIMERS[ + tt.mod(new_index, tt.TIMERS.length) + ])); distanceX = null; } } @@ -251,13 +248,13 @@ class TimerView { const running = this.timer.is_running(); let mode = tt.SETTINGS.format[id]; if (mode == 'start hh:mm:ss') { - elem.label = tt.format_duration(this.timer.origin / Math.abs(this.timer.rate), true); + elem.label = tt.format_duration(this.timer.to_msec(this.timer.origin), true); } else if (mode == 'current hh:mm:ss') { - elem.label = tt.format_duration(this.timer.get() / Math.abs(this.timer.rate), true); + elem.label = tt.format_duration(this.timer.to_msec(), true); if (running) { update_interval = Math.min( update_interval, - next_time_update(1000, this.timer.get_msec(), this.timer.rate) + next_time_update(1000, this.timer.to_msec(), this.timer.rate) ); } } else if (mode == 'time hh:mm:ss') { @@ -268,14 +265,14 @@ class TimerView { ); } else if (mode == 'start hh:mm') { - elem.label = tt.format_duration(this.timer.origin / Math.abs(this.timer.rate), false); + elem.label = tt.format_duration(this.timer.to_msec(this.timer.origin), false); } else if (mode == 'current hh:mm') { - elem.label = tt.format_duration(this.timer.get() / Math.abs(this.timer.rate), false); + elem.label = tt.format_duration(this.timer.to_msec(), false); if (running) { // Update every minute for current HM when running update_interval = Math.min( update_interval, - next_time_update(60000, this.timer.get_msec(), this.timer.rate) + next_time_update(60000, this.timer.to_msec(), this.timer.rate) ); } } else if (mode == 'time hh:mm') { @@ -403,21 +400,20 @@ class TimerFormatView { // Touch handler function touchHandler(button, xy) { // Increment or decrement row's format index based on the arrow tapped - for (var row_id of ROW_IDS) { - const prev_id = row_id + '.prev'; - const next_id = row_id + '.next'; - if (xy.x >= this.layout[prev_id].x - && xy.x <= this.layout[prev_id].x + this.layout[prev_id].w - && xy.y >= this.layout[prev_id].y - && xy.y <= this.layout[prev_id].y + this.layout[prev_id].h) { - this.decr_format_idx(row_id); - break; - } else if (xy.x >= this.layout[next_id].x - && xy.x <= this.layout[next_id].x + this.layout[next_id].w - && xy.y >= this.layout[next_id].y - && xy.y <= this.layout[next_id].y + this.layout[next_id].h) { - this.incr_format_idx(row_id); - break; + for (let row_id of ROW_IDS) { + for (let btn_id of ['prev', 'next']) { + let elem = row_id + '.' + btn_id; + if (xy.x >= this.layout[elem].x + && xy.x <= this.layout[elem].x + this.layout[elem].w + && xy.y >= this.layout[elem].y + && xy.y <= this.layout[elem].y + this.layout[elem].h) { + if (btn_id === 'prev') { + this.decr_format_idx(row_id); + } else { + this.incr_format_idx(row_id); + } + break; + } } } } diff --git a/apps/tevtimer/lib.js b/apps/tevtimer/lib.js index 98ac5ebef..aafe445f8 100644 --- a/apps/tevtimer/lib.js +++ b/apps/tevtimer/lib.js @@ -3,6 +3,20 @@ const Sched = require('sched'); const Time_utils = require('time_utils'); +// Convenience // + +function mod(n, m) { + // Modulus function that works like Python's % operator + return ((n % m) + m) % m; +} + +function ceil(value) { + // JavaScript's Math.ceil function is weird, too + // Attempt to work around it + return Math.ceil(Math.round(value * 1e10) / 1e10); +} + + // Data models // class PrimitiveTimer { @@ -76,10 +90,6 @@ class PrimitiveTimer { return this.origin + (this.rate * elapsed); } - get_msec() { - return this.get() / Math.abs(this.rate); - } - set(new_value) { const now = Date.now(); this._start_time = (now - new_value / this.rate) @@ -89,6 +99,18 @@ class PrimitiveTimer { } } + // Convert given timer value to milliseconds using this.rate + // Uses the current value of the timer if no value is provided + to_msec(value) { + if (typeof value === 'undefined') { + value = this.get(); + } + if (typeof value !== 'number') { + throw new Error('Invalid value type for to_msec'); + } + return ceil(value / Math.abs(this.rate)); + } + dump() { return { cls: 'PrimitiveTimer', @@ -118,12 +140,6 @@ class PrimitiveTimer { } -function fixed_ceil(value) { - // JavaScript sucks - return Math.ceil(Math.round(value * 1e10) / 1e10); -} - - function format_duration(msec, have_seconds) { if (msec < 0) { return '-' + format_duration(-msec, have_seconds); @@ -298,6 +314,7 @@ E.on('kill', () => { save_settings(); }); exports = {TIMERS, SETTINGS, + mod, ceil, load_timers, save_timers, schedule_save_timers, save_settings, schedule_save_settings, PrimitiveTimer, format_duration, From 7294b0477c7737ec493a7b025770eaae494dd03b Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Fri, 4 Apr 2025 18:27:23 -0500 Subject: [PATCH 19/67] Add accidentally omitted function (though I don't plan on using it) --- apps/tevtimer/alarm.js | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/apps/tevtimer/alarm.js b/apps/tevtimer/alarm.js index 23cbf26db..50e4ddb5d 100644 --- a/apps/tevtimer/alarm.js +++ b/apps/tevtimer/alarm.js @@ -1,5 +1,34 @@ const tt = require('tevtimer'); +function setNextRepeatDate(alarm) { + let date = new Date(alarm.date); + let rp = alarm.rp; + if (rp===true) { // fallback in case rp is set wrong + date.setDate(date.getDate() + 1); + } else switch(rp.interval) { // rp is an object + case "day": + date.setDate(date.getDate() + rp.num); + break; + case "week": + date.setDate(date.getDate() + (rp.num * 7)); + break; + case "month": + if (!alarm.od) alarm.od = date.getDate(); + date = new Date(date.getFullYear(), date.getMonth() + rp.num, alarm.od); + if (date.getDate() != alarm.od) date.setDate(0); + break; + case "year": + if (!alarm.od) alarm.od = date.getDate(); + date = new Date(date.getFullYear() + rp.num, date.getMonth(), alarm.od); + if (date.getDate() != alarm.od) date.setDate(0); + break; + default: + console.log(`sched: unknown repeat '${JSON.stringify(rp)}'`); + break; + } + alarm.date = date.toLocalISOString().slice(0,10); +} + function showAlarm(alarm) { const alarmIndex = alarms.indexOf(alarm); const settings = require("sched").getSettings(); From fef2f224a6c161f507519319d13978c030b9a5d5 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Fri, 4 Apr 2025 18:27:30 -0500 Subject: [PATCH 20/67] Adjust font sizes --- apps/tevtimer/app.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js index 2d84b2060..00768da7f 100644 --- a/apps/tevtimer/app.js +++ b/apps/tevtimer/app.js @@ -37,8 +37,8 @@ const FONT = { 'current hh:mm:ss': 'Vector:34x42', 'time hh:mm:ss': 'Vector:24x42', - 'start hh:mm': 'Vector:56x42', - 'current hh:mm': 'Vector:56x42', + 'start hh:mm': 'Vector:48x42', + 'current hh:mm': 'Vector:48x42', 'time hh:mm': 'Vector:56x42', 'name': 'Vector:24x42', @@ -51,8 +51,8 @@ const FONT = { 'current hh:mm:ss': 'Vector:34x56', 'time hh:mm:ss': 'Vector:24x56', - 'start hh:mm': 'Vector:56x56', - 'current hh:mm': 'Vector:56x56', + 'start hh:mm': 'Vector:48x56', + 'current hh:mm': 'Vector:48x56', 'time hh:mm': 'Vector:56x56', 'name': 'Vector:24x56', From 0fdd128f3749214a387e0d41ec35e0dfbbe8b15d Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Sun, 6 Apr 2025 12:59:13 -0500 Subject: [PATCH 21/67] Simplify PrimitiveTimer.to_msec --- apps/tevtimer/lib.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/apps/tevtimer/lib.js b/apps/tevtimer/lib.js index aafe445f8..390b603b6 100644 --- a/apps/tevtimer/lib.js +++ b/apps/tevtimer/lib.js @@ -102,13 +102,8 @@ class PrimitiveTimer { // Convert given timer value to milliseconds using this.rate // Uses the current value of the timer if no value is provided to_msec(value) { - if (typeof value === 'undefined') { - value = this.get(); - } - if (typeof value !== 'number') { - throw new Error('Invalid value type for to_msec'); - } - return ceil(value / Math.abs(this.rate)); + value = value || this.get(); + return Math.ceil(value / Math.abs(this.rate)); } dump() { From ac2a1c5e5379bc26ab64147b0a22f7a5fd03ace2 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Mon, 7 Apr 2025 14:43:28 -0500 Subject: [PATCH 22/67] Switch to triple-picker for setting timers --- apps/tevtimer/app.js | 79 +++++++++++++++++--------------------------- 1 file changed, 31 insertions(+), 48 deletions(-) diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js index 00768da7f..d4ce0ac36 100644 --- a/apps/tevtimer/app.js +++ b/apps/tevtimer/app.js @@ -1,5 +1,6 @@ const Layout = require('Layout'); const locale = require('locale'); +const pickers = require('more_pickers'); const tt = require('tevtimer'); @@ -682,7 +683,7 @@ class TimerViewMenu { }, 0); } }, - 'Start': this.edit_start_hms_menu.bind(this), + 'Start': this.edit_start.bind(this), 'Vibrate pattern': require("buzz_menu").pattern( this.timer.vibrate_pattern, v => this.timer.vibrate_pattern = v), @@ -707,60 +708,42 @@ class TimerViewMenu { E.showMenu(edit_menu); } - edit_start_hms_menu() { + edit_start() { let origin_hms = { h: Math.floor(this.timer.origin / 3600), m: Math.floor(this.timer.origin / 60) % 60, s: Math.floor(this.timer.origin % 60), }; - const update_origin = () => { - this.timer.origin = origin_hms.h * 3600 - + origin_hms.m * 60 - + origin_hms.s; - }; + function picker_format(v) { + // Display leading 0 for single digit values in the picker + return v < 10 ? '0' + v : v; + } - const edit_start_hms_menu = { - '': { - title: 'Start (HMS)', - back: this.edit_menu.bind(this), - }, - 'Hours': { - value: origin_hms.h, - min: 0, - max: 9999, - wrap: true, - onchange: v => { - origin_hms.h = v; - update_origin(); - tt.set_timers_dirty(); - } - }, - 'Minutes': { - value: origin_hms.m, - min: 0, - max: 59, - wrap: true, - onchange: v => { - origin_hms.m = v; - update_origin(); - tt.set_timers_dirty(); - } - }, - 'Seconds': { - value: origin_hms.s, - min: 0, - max: 59, - wrap: true, - onchange: v => { - origin_hms.s = v; - update_origin(); - tt.set_timers_dirty(); - } - }, - }; - - E.showMenu(edit_start_hms_menu); + pickers.triplePicker({ + title: "Set Start", + value_1: origin_hms.h, + value_2: origin_hms.m, + value_3: origin_hms.s, + format_2: picker_format, + format_3: picker_format, + min_1: 0, + max_1: 99, + min_2: 0, + max_2: 59, + min_3: 0, + max_3: 59, + wrap_1: false, + wrap_2: true, + wrap_3: true, + separator_1: ':', + separator_2: ':', + back: this.edit_menu.bind(this), + onchange: (h, m, s) => { + this.timer.origin = h * 3600 + m * 60 + s; + tt.set_timers_dirty(); + } + }); } } From 1a9e5dbd6475d3c454b8ccf8d8d7b61be9eee0ea Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Tue, 8 Apr 2025 15:37:00 -0500 Subject: [PATCH 23/67] Fix PrimitiveTimer.to_msec if value is 0 --- apps/tevtimer/lib.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/tevtimer/lib.js b/apps/tevtimer/lib.js index 390b603b6..04c1acff1 100644 --- a/apps/tevtimer/lib.js +++ b/apps/tevtimer/lib.js @@ -102,7 +102,9 @@ class PrimitiveTimer { // Convert given timer value to milliseconds using this.rate // Uses the current value of the timer if no value is provided to_msec(value) { - value = value || this.get(); + if (value === undefined) { + value = this.get(); + } return Math.ceil(value / Math.abs(this.rate)); } From c7a0451649d20b3f1159c1ae3083f597426b6917 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Wed, 9 Apr 2025 17:01:41 -0500 Subject: [PATCH 24/67] Automatically assign IDs to timers so that referencing is possible --- apps/tevtimer/lib.js | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/apps/tevtimer/lib.js b/apps/tevtimer/lib.js index 04c1acff1..cd53e909e 100644 --- a/apps/tevtimer/lib.js +++ b/apps/tevtimer/lib.js @@ -20,11 +20,12 @@ function ceil(value) { // Data models // class PrimitiveTimer { - constructor(origin, is_running, rate, name) { + constructor(origin, is_running, rate, name, id) { this.origin = origin || 0; // default rate +1 unit per 1000 ms, countup this.rate = rate || 0.001; this.name = name || ''; + this.id = id || 0; this.vibrate_pattern = ';;;'; this.buzz_count = 4; @@ -115,6 +116,7 @@ class PrimitiveTimer { origin: this.origin, rate: this.rate, name: this.name, + id: this.id, start_time: this._start_time, pause_time: this._pause_time, vibrate_pattern: this.vibrate_pattern, @@ -126,7 +128,7 @@ class PrimitiveTimer { if (!(data.cls == 'PrimitiveTimer' && data.version == 0)) { console.error('Incompatible data type for loading PrimitiveTimer state'); } - let loaded = new this(data.origin, false, data.rate, data.name); + let loaded = new this(data.origin, false, data.rate, data.name, data.id); loaded._start_time = data.start_time; loaded._pause_time = data.pause_time; loaded.vibrate_pattern = data.vibrate_pattern; @@ -162,6 +164,28 @@ var SAVE_TIMERS_TIMEOUT = null; var SAVE_SETTINGS_TIMEOUT = null; +function next_id() { + // Find the next unused ID number for timers + let max_id = 0; + for (let timer of TIMERS) { + if (timer.id > max_id) { + max_id = timer.id; + } + } + return max_id + 1; +} + +function find_timer_by_id(id) { + // Find a timer in TIMERS by its ID; return the timer object or null + // if not found + for (let timer of TIMERS) { + if (timer.id == id) { + return timer; + } + } + return null; +} + function load_timers() { console.log('loading timers'); let timers = Storage.readJSON(TIMERS_FILENAME, true) || []; @@ -169,7 +193,7 @@ function load_timers() { // Deserealize timer objects timers = timers.map(t => PrimitiveTimer.load(t)); } else { - timers = [new PrimitiveTimer(600, false, -0.001)]; + timers = [new PrimitiveTimer(600, false, -0.001, '', 1)]; timers[0].end_alarm = true; } return timers; @@ -243,6 +267,9 @@ function delete_timer(timers, timer) { function add_timer(timers, timer) { // Create a copy of current timer object const new_timer = PrimitiveTimer.load(timer.dump()); + // Assign a new ID to the timer + new_timer.id = next_id(); + // Place it at the top of the list timers.unshift(new_timer); return new_timer; } From 4cbdaa231a9324ae1264ad4e5acc717ff60ce65a Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Fri, 11 Apr 2025 21:20:24 -0500 Subject: [PATCH 25/67] Provision for setting chained timers in the UI --- apps/tevtimer/app.js | 15 +++++++++++++++ apps/tevtimer/lib.js | 15 +++++++++------ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js index d4ce0ac36..4479a1e22 100644 --- a/apps/tevtimer/app.js +++ b/apps/tevtimer/app.js @@ -684,6 +684,21 @@ class TimerViewMenu { } }, 'Start': this.edit_start.bind(this), + 'At end': { + // Option to auto-start another timer when this one ends + format: v => v === -1 + ? "Stop" + : tt.TIMERS[v].display_status() + + ' ' + + tt.TIMERS[v].display_name(), + value: tt.find_timer_by_id(this.timer.chain_id), + min: -1, + max: tt.TIMERS.length - 1, + onchange: v => { + this.timer.chain_id = v === -1 ? null : tt.TIMERS[v].id; + tt.set_timers_dirty(); + } + }, 'Vibrate pattern': require("buzz_menu").pattern( this.timer.vibrate_pattern, v => this.timer.vibrate_pattern = v), diff --git a/apps/tevtimer/lib.js b/apps/tevtimer/lib.js index cd53e909e..9b27c1c3b 100644 --- a/apps/tevtimer/lib.js +++ b/apps/tevtimer/lib.js @@ -29,6 +29,7 @@ class PrimitiveTimer { this.vibrate_pattern = ';;;'; this.buzz_count = 4; + this.chain_id = null; this._start_time = Date.now(); this._pause_time = is_running ? null : this._start_time; @@ -117,6 +118,7 @@ class PrimitiveTimer { rate: this.rate, name: this.name, id: this.id, + chain_id: this.chain_id, start_time: this._start_time, pause_time: this._pause_time, vibrate_pattern: this.vibrate_pattern, @@ -129,6 +131,7 @@ class PrimitiveTimer { console.error('Incompatible data type for loading PrimitiveTimer state'); } let loaded = new this(data.origin, false, data.rate, data.name, data.id); + loaded.chain_id = data.chain_id; loaded._start_time = data.start_time; loaded._pause_time = data.pause_time; loaded.vibrate_pattern = data.vibrate_pattern; @@ -176,14 +179,13 @@ function next_id() { } function find_timer_by_id(id) { - // Find a timer in TIMERS by its ID; return the timer object or null - // if not found - for (let timer of TIMERS) { - if (timer.id == id) { - return timer; + // Return index of timer with ID id, or -1 if not found + for (let idx = 0; idx < TIMERS.length; idx++) { + if (TIMERS[idx].id == id) { + return idx; } } - return null; + return -1; } function load_timers() { @@ -339,6 +341,7 @@ E.on('kill', () => { save_settings(); }); exports = {TIMERS, SETTINGS, mod, ceil, + next_id, find_timer_by_id, load_timers, save_timers, schedule_save_timers, save_settings, schedule_save_settings, PrimitiveTimer, format_duration, From b0ee4673896b430a62660b1b0f8d85fd97f54ef1 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Sun, 20 Apr 2025 17:54:49 -0500 Subject: [PATCH 26/67] Create app icon --- apps/tevtimer/app-icon.js | 2 +- apps/tevtimer/app.png | Bin 819 -> 827 bytes apps/tevtimer/app.xcf | Bin 2112 -> 3474 bytes 3 files changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/tevtimer/app-icon.js b/apps/tevtimer/app-icon.js index 2299fb6c4..68b44efc6 100644 --- a/apps/tevtimer/app-icon.js +++ b/apps/tevtimer/app-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("lEo4cC///A4IDC/Hgg/g/8f/H/g9JkmSAQORAgYCEAYcipPPnnzAQoOCpGSgN58+eAQoOComAgIdKHAMAgI7KxJlDBxNIgESBwO69euAQhWBoAOD69duoCEBwKSEDpAaCHZVIyCxFBw+AhIOEtu27YCDQYIONbwwOOHYwODg3AAgSnGBwfbAgUyBokgFIcbsEAgYOKJAOA7YOGiVJBwUN23YHYoOFgE2AQIIBBwZoGAAMf/4ACBxxoDBxSVHHYwOJgEJBxsOYgTgHBwz+HBw+QHZlJiQ8JggOCkQOJg4sBBwNAOIJ0CAQYODklIBxuJSQgEDJQckyK2DBwcJlMiBwWSRIIOFpRHB7YOCpGSYQtSBwoFBpL1BgMkygICBwgCBIggXDA==")) +require("heatshrink").decompress(atob("mEwwcCpMgBo8EyVJkgCECKMki9duvXAQVcBwwCCwXr126AQXhCJOQEYsJCJQjFCJWSjojD6AOIAQVDEYeCCJdBuvAQI8SCItIl2ACJ1JgOAjlxNwdduA4HhkwgeONwfrwgRHkOGgC2PmHDSQnoCJMGjAjEoIRJwEBhy2OCIMF66AHgAREnHIkG6CJsWjkkhMg+fHSQd14QRDkG45eEIYMevSSD1y2EgvWjtwwMkwa2KhwjB0GQki2F0DpEuojBukQkmREYkECIdDa4PDhkCgEEnS2IoFhw0YsOCCIMXbREDEYcw8+gwQjC8IRDyAgBEYWAjnwyAMCZAmQEAQCBgHn10QCI4gCAQNgg8c+vCTgMgYQhEDAQM4EYOCoVJgTCEbwkB44jB6ARMBQkIgDUEAQoRQpAKEkfOwQRIoAHEkF54QROg/O3ARIeIlInn566hEWwZMF8/O3QRHwCBEWwN569BCJgtJAQ4RQkJjJAQxUFQ4oRWpDPLAQlCeZYCEQAgADCKI")) diff --git a/apps/tevtimer/app.png b/apps/tevtimer/app.png index 507eb696b47ce3ec1a9b8e5dbe354cb0ed0558b2..39a12e4488846fec09af8a27bd4e0fa0f525fe13 100644 GIT binary patch delta 765 zcmVR@u| zA2ex5Qd}Gb*MfsTi&X~~XI&j!1wrr!#9eSwbdeIjOA0N2V!Y$Hhxfks zYC7c$IgeF;&Rd+dYMr(3$zK>Q=qt-yr!|Tc7O?~gA{5k7K@~P)wCki;$k2Jx$3Now zC2}d`s(_JW0X1llT|f9A{O+w)oQiu%;Uv)W;y52;K<_TlYB4lw*=$foQ{L0UpRu?W1M(KqFRp;M1(2}wjjR5;6xlrav&APhyp>H!d0GTJ0V)`Sa0+>A~fyQV2E zI3U#{PtW)hD@rL>m8xq9Kz|DxpwN8Ew&ywciW&mJyMAVrL zLU6VlW@V)thLwI8Sl2-q=LTUIbi;r>u^WE|9)ysdG0NG{o)%KtW)TMTgEVfe2my>e zP3|SbP&R;ji7=cy7v92f?p*i|Q(xgOBg_V|nduVvfg^(F!uE>bW@8)h6)0+4yg!5x zEa%R>P%JfN13DnR3&qNF#}U{r@SNmVPai;gA8eD@tkNmQz1_vqX`b3mslx2dZWS%y vef?Nf#}QaAa14aIG6YKj$3T$EU;m*Oa9?6Jo7S-700000NkvXXu0mjf9X3y| delta 757 zcmVA_T~&qJ%Om#Aw$@F_EJ4xQBnp z@r&e=$yEX)#{w!)Avu2VKlt5SGe0@uCWYca=ZkHBi~zx1pi#5!?_=9;oB)Am;7V)x zD|KMzlk{3kiyi^tZQ$a%rOA81^3A0;2`WUiWx+S9{<7t!eh}2Rv7Dkp&1Cp#T5?8&FJCMKP1Q0U88o!~hhN z*a0|y=m`%N7&O?l@eSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{ z007%bL_t(2&ux?44Z<)Cgsp7gWP!#-h}SAG0c?P}0xPfrW0N1-aiW~mmruF#wQ&KO z92qeqdK)o|{$bR-5&eA95u%?nGs_xbF-OFI{i$jP3%uM_8OG`!PdbJ}W*dXHKyfqn zFHvzXAgX-{duD@aD8R#0rPoiBR8I$Dm|>Lym@TnZ1Pcq6UxnxoLK9_*1q`@400000NkvXXu0mjf&ZTo5)hke85Cv0W3hHVH1r;BN?Y{S~xbc9UdFZ z2e4)_UoZ<+v!6eh#eByI2q7SZLQc8 zd0+_m0r)ZDTz3K32hvqsWTD*H-j3>R#O9i&uQj7mJK9o@;`Tjt{pPJ-I5TNVCe&v&Z1b8eSKbvrLw9Fhcj<0pe<8G?(m|hG1^8$YcLk?b@kb@Mu zd!UMVjhpX3=EUxRwjlQUv_T~!_IqlU#0M|V_C+7NxfLOURUjvZgDWBo)_@K@7l#y3 z!JOB`0l1*N_=GdFRgc+`Sj~!a!4j}2(t#2a!2&QZrh-{96Li9dBBYD@B5;ZRcUJD{ z^<{e1BUr!dS$Z=d>p2qoZSsTA&CWx7*DESW?|msl@4EG@>qUC+33{1QIt#3j)Hxc~ zdEg3#dJW+Ih#(zlupD=RKExO+^qI%7K^9-h{4)=~i~tobT%@$l0E#B`JWb&?rf`2V zxb+KoF829!LY{74^VC(&oh47PFWFnC^%Q&7Ya{GI*ZMp)vf&cwT@G|))9Gi%Qxcw% z@RWq7Bs?Xpr=;^72&^x!WwknL#ACfBGe*8*%Uc}06+U-*?LiiVb0nFH}`v3p{ literal 2112 zcmds2&2AGh5T5b=WRs>@nl`j0RNWFHrH4q(6M)nM;()|~7f91&yCO}hBtUzDr{Ky9 z@CIBFFTe|ML*mK>355A(v!>ov=mTKu^L#U7uV=<~5+~;u(dA$iy*%pm2(6PrLB0hT zxC;bYYX|^x*@cOLHUO51n?P`kWdgJ{?TpL32JjtNkFkAsFg-sXP8QJ9-Rj4) zVSh0^jb0^-v*^*|Cr`z5E1jC0_v2wVo+YPd_WRC!aWx)B^XWJ_J*;#_@nAZh&Z15# zy0iGCA04H=cL?{%3U@C`ySOjs7yUspiKE^Tw5yz!Dcu*7WDzBk!P#&wE2g8-e7K0x z`4(T&{A7vQYP>8Gxt}bfq2?Ss7|PFPNq-WLhvJljk>&$ZTRDIO;Sb(mMEwD=OP!_e z!lm5<43`5nykhal;%gSKT72E&A-L>kS_wqHE8lNt9D?o(#|4sDTNXF>!-36e>Yb&S zLS%WX4=a2ysn5nmm@fE&}oDA`q!A0!MGIiM$v8csw0E2l=laiFx>v7n=8R zDbvwppUv^Utnk6){V?{BO57xuJ)l8~*MT~I#7h}M=mB^DS%s`vaKOC`!eyRo*@TyH zm*n>O;Cq>WAJUOK2DuHWj9=@Fe-Vva&B~`=_yOZxQihgcU4t5khq+9SDgos*K&3pm zB+wcp12u3)ashc*;({-AB%&&r-iED>^)1NX1d;CfSNxO+tZ1(zHqlG=l9gmES^9`x zvX&|qDN*5VV|D6DJoE&QCY-vKq9h>v)MF?dmEub%pkH zK>K*H``V`i?NUoSfTFf$3z}Z@L;N1+tty-CVx^ybCBPRe_;M9HgxIN$9UIsgaX|kA Dm3a*5 From f14046aff65cfa7fd0d67b53949580d5887433c7 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Tue, 22 Apr 2025 14:55:22 -0500 Subject: [PATCH 27/67] Correct unintentional modifications from original sched.js Fix breakage such as alarm reloading nonstop when a timer triggers --- apps/tevtimer/alarm.js | 79 ++++++++++++++++++++++++++---------------- 1 file changed, 50 insertions(+), 29 deletions(-) diff --git a/apps/tevtimer/alarm.js b/apps/tevtimer/alarm.js index 50e4ddb5d..32f1a9e53 100644 --- a/apps/tevtimer/alarm.js +++ b/apps/tevtimer/alarm.js @@ -1,40 +1,32 @@ -const tt = require('tevtimer'); - -function setNextRepeatDate(alarm) { - let date = new Date(alarm.date); - let rp = alarm.rp; - if (rp===true) { // fallback in case rp is set wrong - date.setDate(date.getDate() + 1); - } else switch(rp.interval) { // rp is an object - case "day": - date.setDate(date.getDate() + rp.num); - break; - case "week": - date.setDate(date.getDate() + (rp.num * 7)); - break; - case "month": - if (!alarm.od) alarm.od = date.getDate(); - date = new Date(date.getFullYear(), date.getMonth() + rp.num, alarm.od); - if (date.getDate() != alarm.od) date.setDate(0); - break; - case "year": - if (!alarm.od) alarm.od = date.getDate(); - date = new Date(date.getFullYear() + rp.num, date.getMonth(), alarm.od); - if (date.getDate() != alarm.od) date.setDate(0); - break; - default: - console.log(`sched: unknown repeat '${JSON.stringify(rp)}'`); - break; - } - alarm.date = date.toLocalISOString().slice(0,10); +// Chances are boot0.js got run already and scheduled *another* +// 'load(sched.js)' - so let's remove it first! +if (Bangle.SCHED) { + clearInterval(Bangle.SCHED); + delete Bangle.SCHED; } +const tt = require('tevtimer'); + function showAlarm(alarm) { const alarmIndex = alarms.indexOf(alarm); const settings = require("sched").getSettings(); const timer = tt.TIMERS[alarm.data.idx]; let message = timer.display_name() + '\n' + alarm.msg; + // If there's a timer chained from this one, start it + if (timer.chain_id !== null) { + const chainTimer = tt.TIMERS[tt.find_timer_by_id(timer.chain_id)]; + if (chainTimer !== undefined) { + chainTimer.reset(); + chainTimer.start(); + tt.set_last_viewed_timer(chainTimer); + tt.save_timers(); + tt.update_system_alarms(); // FIXME: This might break snoozing + } else { + console.warn("tevtimer: unable to find chained timer with ID " + timer.chain_id); + } + } + if (alarm.msg) { message += "\n" + alarm.msg; } else { @@ -111,6 +103,35 @@ function showAlarm(alarm) { }); } + function setNextRepeatDate(alarm) { + let date = new Date(alarm.date); + let rp = alarm.rp; + if (rp===true) { // fallback in case rp is set wrong + date.setDate(date.getDate() + 1); + } else switch(rp.interval) { // rp is an object + case "day": + date.setDate(date.getDate() + rp.num); + break; + case "week": + date.setDate(date.getDate() + (rp.num * 7)); + break; + case "month": + if (!alarm.od) alarm.od = date.getDate(); + date = new Date(date.getFullYear(), date.getMonth() + rp.num, alarm.od); + if (date.getDate() != alarm.od) date.setDate(0); + break; + case "year": + if (!alarm.od) alarm.od = date.getDate(); + date = new Date(date.getFullYear() + rp.num, date.getMonth(), alarm.od); + if (date.getDate() != alarm.od) date.setDate(0); + break; + default: + console.log(`sched: unknown repeat '${JSON.stringify(rp)}'`); + break; + } + alarm.date = date.toLocalISOString().slice(0,10); + } + if ((require("Storage").readJSON("setting.json", 1) || {}).quiet > 1) return; From fe04af7cc2b66184aa8438ec6c35abf211ca0de1 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Fri, 25 Apr 2025 15:56:10 -0500 Subject: [PATCH 28/67] Rework timer alarm handling Avoid conflicts with alarm snoozing, etc. --- apps/tevtimer/alarm.js | 46 +++++++++++++++++++++++------------------- apps/tevtimer/app.js | 2 +- apps/tevtimer/lib.js | 17 ++++++++++------ 3 files changed, 37 insertions(+), 28 deletions(-) diff --git a/apps/tevtimer/alarm.js b/apps/tevtimer/alarm.js index 32f1a9e53..1f5d68c80 100644 --- a/apps/tevtimer/alarm.js +++ b/apps/tevtimer/alarm.js @@ -8,9 +8,12 @@ if (Bangle.SCHED) { const tt = require('tevtimer'); function showAlarm(alarm) { - const alarmIndex = alarms.indexOf(alarm); const settings = require("sched").getSettings(); - const timer = tt.TIMERS[alarm.data.idx]; + const timer = tt.TIMERS[tt.find_timer_by_id(alarm.id)]; + if (timer === undefined) { + console.error("tevtimer: unable to find timer with ID " + alarm.id); + return; + } let message = timer.display_name() + '\n' + alarm.msg; // If there's a timer chained from this one, start it @@ -20,8 +23,6 @@ function showAlarm(alarm) { chainTimer.reset(); chainTimer.start(); tt.set_last_viewed_timer(chainTimer); - tt.save_timers(); - tt.update_system_alarms(); // FIXME: This might break snoozing } else { console.warn("tevtimer: unable to find chained timer with ID " + timer.chain_id); } @@ -58,29 +59,32 @@ function showAlarm(alarm) { alarm.t %= 86400000; Bangle.emit("alarmSnooze", alarm); } else { - let del = alarm.del === undefined ? settings.defaultDeleteExpiredTimers : alarm.del; - if (del) { - alarms.splice(alarmIndex, 1); - } else { - if (alarm.date && alarm.rp) { - setNextRepeatDate(alarm); - } else if (!alarm.timer) { - alarm.last = new Date().getDate(); - } - if (alarm.ot !== undefined) { - alarm.t = alarm.ot; - delete alarm.ot; - } - if (!alarm.rp) { - alarm.on = false; - } + // Don't do timer deletions here; this is handled by the + // tevtimer library code (and it may rearrange the alarm indeces + // in the process) + + if (alarm.date && alarm.rp) { + setNextRepeatDate(alarm); + } else if (!alarm.timer) { + alarm.last = new Date().getDate(); + } + if (alarm.ot !== undefined) { + alarm.t = alarm.ot; + delete alarm.ot; + } + if (!alarm.rp) { + alarm.on = false; } - Bangle.emit("alarmDismiss", alarm); } + Bangle.emit("alarmDismiss", alarm); // The updated alarm is still a member of 'alarms' // so writing to array writes changes back directly require("sched").setAlarms(alarms); + + // Update system alarms for any changed timers just before we finish + tt.update_system_alarms(); + load(); }); diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js index 4479a1e22..a63027815 100644 --- a/apps/tevtimer/app.js +++ b/apps/tevtimer/app.js @@ -297,7 +297,7 @@ class TimerView { // Set up timeout to render timer again when needed if (update_interval !== Infinity) { - console.debug('Next render update scheduled in ' + update_interval); + console.debug('Next render update scheduled in ' + update_interval + ' ms'); this.listeners.timer_render_timeout = setTimeout( () => { this.listeners.timer_render_timeout = null; diff --git a/apps/tevtimer/lib.js b/apps/tevtimer/lib.js index 9b27c1c3b..f4fbe8cf9 100644 --- a/apps/tevtimer/lib.js +++ b/apps/tevtimer/lib.js @@ -305,8 +305,13 @@ function set_settings_dirty() { function delete_system_alarms() { var alarms = Sched.getAlarms().filter(a => a.appid == 'tevtimer'); for (let alarm of alarms) { - console.debug('delete sched alarm ' + alarm.id); - Sched.setAlarm(alarm.id, undefined); + if (alarm.ot === undefined) { + console.debug('delete_system_alarms: delete sched alarm ' + alarm.id); + Sched.setAlarm(alarm.id, undefined); + } else { + // Avoid deleting timers awaiting snoozing + console.debug('delete_system_alarms: skipping snoozed alarm ' + alarm.id); + } } Sched.reload(); } @@ -314,15 +319,15 @@ function delete_system_alarms() { function set_system_alarms() { for (let idx = 0; idx < TIMERS.length; idx++) { let timer = TIMERS[idx]; - let time_to_next_alarm = timer.get() / Math.abs(timer.rate); + let time_to_next_alarm = timer.to_msec(); if (timer.is_running() && time_to_next_alarm > 0) { - console.debug('set sched alarm ' + idx + ' (' + time_to_next_alarm + ')'); - Sched.setAlarm(idx.toString(), { + console.debug('set_system_alarms: set sched alarm ' + timer.id + + ' (' + time_to_next_alarm + ' ms)'); + Sched.setAlarm(timer.id, { appid: 'tevtimer', timer: time_to_next_alarm, msg: '', js: "load('tevtimer.alarm.js');", - data: { idx: idx }, }); } } From 931b1f6cefb074c8b2c64fbddf84c9151b5f6701 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Sat, 26 Apr 2025 18:14:09 -0500 Subject: [PATCH 29/67] Resolve some conceptual conflicts between chained timers and snoozing 1. Make snoozing only available for non-chained timers (or the last timer in a chain) 2. Don't let dismissing a snoozed timer restart the chained timer if it was paused (though 1. should preclude this from happening anyway) --- apps/tevtimer/alarm.js | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/apps/tevtimer/alarm.js b/apps/tevtimer/alarm.js index 1f5d68c80..8db5381ce 100644 --- a/apps/tevtimer/alarm.js +++ b/apps/tevtimer/alarm.js @@ -16,13 +16,16 @@ function showAlarm(alarm) { } let message = timer.display_name() + '\n' + alarm.msg; - // If there's a timer chained from this one, start it - if (timer.chain_id !== null) { - const chainTimer = tt.TIMERS[tt.find_timer_by_id(timer.chain_id)]; + // If there's a timer chained from this one, start it (only for + // alarms not in snoozed status) + var isChainedTimer = false; + if (timer.chain_id !== null && alarm.ot === undefined) { + var chainTimer = tt.TIMERS[tt.find_timer_by_id(timer.chain_id)]; if (chainTimer !== undefined) { chainTimer.reset(); chainTimer.start(); tt.set_last_viewed_timer(chainTimer); + isChainedTimer = true; } else { console.warn("tevtimer: unable to find chained timer with ID " + timer.chain_id); } @@ -43,13 +46,20 @@ function showAlarm(alarm) { // buzzCount should really be called buzzRepeat, so subtract 1 let buzzCount = timer.buzz_count - 1; + // Alarm options for non-chained timer are OK (dismiss the alarm) and + // Snooze (retrigger the alarm after a delay). + // Alarm options for chained timer are OK (dismiss) and Halt (dismiss + // and pause the triggering timer). + let promptButtons = isChainedTimer + ? { "Halt": 'halt', "OK": 'ok' } + : { "Snooze": 'snooze', "OK": 'ok' }; E.showPrompt(message, { title: 'tev timer', - buttons: { "Snooze": true, "Stop": false } // default is sleep so it'll come back in some mins - }).then(function (sleep) { + buttons: promptButtons, + }).then(function (action) { buzzCount = 0; - if (sleep) { + if (action === 'snooze') { if (alarm.ot === undefined) { alarm.ot = alarm.t; } @@ -58,7 +68,8 @@ function showAlarm(alarm) { alarm.t = currentTime + settings.defaultSnoozeMillis; alarm.t %= 86400000; Bangle.emit("alarmSnooze", alarm); - } else { + } + if (action === 'ok' || action === 'halt') { // Don't do timer deletions here; this is handled by the // tevtimer library code (and it may rearrange the alarm indeces // in the process) @@ -76,6 +87,10 @@ function showAlarm(alarm) { alarm.on = false; } } + if (action === 'halt') { + timer.pause(); + chainTimer.pause(); + } Bangle.emit("alarmDismiss", alarm); // The updated alarm is still a member of 'alarms' @@ -85,7 +100,12 @@ function showAlarm(alarm) { // Update system alarms for any changed timers just before we finish tt.update_system_alarms(); - load(); + // Load `tevtimer` app upon halt, else the default (clock) app + if (action === 'halt') { + load('tevtimer.app.js'); + } else { + load(); + } }); function buzz() { From 7a8c56023f64c767d9032cd2cc250762c59e6101 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Sun, 27 Apr 2025 15:06:49 -0500 Subject: [PATCH 30/67] Fix further alarms not triggering while alarm alert is displayed --- apps/tevtimer/alarm.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/tevtimer/alarm.js b/apps/tevtimer/alarm.js index 8db5381ce..a4229aeb1 100644 --- a/apps/tevtimer/alarm.js +++ b/apps/tevtimer/alarm.js @@ -26,6 +26,7 @@ function showAlarm(alarm) { chainTimer.start(); tt.set_last_viewed_timer(chainTimer); isChainedTimer = true; + tt.update_system_alarms(); } else { console.warn("tevtimer: unable to find chained timer with ID " + timer.chain_id); } @@ -90,6 +91,7 @@ function showAlarm(alarm) { if (action === 'halt') { timer.pause(); chainTimer.pause(); + tt.update_system_alarms(); } Bangle.emit("alarmDismiss", alarm); @@ -97,9 +99,6 @@ function showAlarm(alarm) { // so writing to array writes changes back directly require("sched").setAlarms(alarms); - // Update system alarms for any changed timers just before we finish - tt.update_system_alarms(); - // Load `tevtimer` app upon halt, else the default (clock) app if (action === 'halt') { load('tevtimer.app.js'); From bca633dcec5354db17d0a84af5fb8ca0dd5dd2fb Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Sun, 27 Apr 2025 19:01:54 -0500 Subject: [PATCH 31/67] Write documentation for classes and functions; some cleanup --- apps/tevtimer/alarm.js | 14 ++++- apps/tevtimer/app.js | 116 +++++++++++++++++++++++++++++++++++------ apps/tevtimer/lib.js | 96 +++++++++++++++++++++++++++++++--- 3 files changed, 202 insertions(+), 24 deletions(-) diff --git a/apps/tevtimer/alarm.js b/apps/tevtimer/alarm.js index a4229aeb1..c635f2640 100644 --- a/apps/tevtimer/alarm.js +++ b/apps/tevtimer/alarm.js @@ -1,3 +1,6 @@ +// Derived from `sched.js` from the `sched` app, with modifications +// for features unique to the `tevtimer` app. + // Chances are boot0.js got run already and scheduled *another* // 'load(sched.js)' - so let's remove it first! if (Bangle.SCHED) { @@ -8,6 +11,8 @@ if (Bangle.SCHED) { const tt = require('tevtimer'); function showAlarm(alarm) { + // Alert the user of the alarm and handle the response + const settings = require("sched").getSettings(); const timer = tt.TIMERS[tt.find_timer_by_id(alarm.id)]; if (timer === undefined) { @@ -52,8 +57,8 @@ function showAlarm(alarm) { // Alarm options for chained timer are OK (dismiss) and Halt (dismiss // and pause the triggering timer). let promptButtons = isChainedTimer - ? { "Halt": 'halt', "OK": 'ok' } - : { "Snooze": 'snooze', "OK": 'ok' }; + ? { 'Halt': 'halt', 'OK': 'ok' } + : { 'Snooze': 'snooze', 'OK': 'ok' }; E.showPrompt(message, { title: 'tev timer', buttons: promptButtons, @@ -108,6 +113,8 @@ function showAlarm(alarm) { }); function buzz() { + // Handle buzzing and screen unlocking + if (settings.unlockAtBuzz) { Bangle.setLocked(false); } @@ -127,6 +134,9 @@ function showAlarm(alarm) { } function setNextRepeatDate(alarm) { + // Handle repeating alarms + // This is not used in tevtimer + let date = new Date(alarm.date); let rp = alarm.rp; if (rp===true) { // fallback in case rp is set wrong diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js index a63027815..d1d2ffa82 100644 --- a/apps/tevtimer/app.js +++ b/apps/tevtimer/app.js @@ -14,10 +14,13 @@ const MOVE_TO_TOP_TIMEOUT = 5000; // Min number of pixels of movement to recognize a touchscreen drag/swipe const DRAG_THRESHOLD = 50; +// Physical left/right button size in UI const ARROW_BTN_SIZE = 15; +// IDs of main screen labels const ROW_IDS = ['row1', 'row2', 'row3']; +// Fonts to use for each screen label and display format const FONT = { 'row1': { 'start hh:mm:ss': '12x20', @@ -30,7 +33,7 @@ const FONT = { 'name': '12x20', - 'mode': '12x20', + 'format-menu': '12x20', }, 'row2': { @@ -44,7 +47,7 @@ const FONT = { 'name': 'Vector:24x42', - 'mode': 'Vector:26x42', + 'format-menu': 'Vector:26x42', }, 'row3': { @@ -58,10 +61,12 @@ const FONT = { 'name': 'Vector:24x56', - 'mode': 'Vector:26x56', + 'format-menu': 'Vector:26x56', } }; +// List of format IDs available in the format menu +// (in the order they are displayed in the menu) const FORMAT_MENU = [ 'start hh:mm:ss', 'start hh:mm', @@ -72,6 +77,8 @@ const FORMAT_MENU = [ 'name', ]; +// Mapping of format IDs to their human-friendly names displayed in the +// format menu const FORMAT_DISPLAY = { 'start hh:mm:ss': 'Start HMS', 'start hh:mm': 'Start HM', @@ -83,20 +90,29 @@ const FORMAT_DISPLAY = { }; -function row_font(row_name, mode_name) { - let font = FONT[row_name][mode_name]; +function row_font(row_name, format) { + // Convenience function to retrieve the font ID for the given display + // field and format mode + + let font = FONT[row_name][format]; if (font === undefined) { - console.error('Unknown font for row_font("' + row_name + '", "' + mode_name + '")'); + console.error('Unknown font for row_font("' + row_name + '", "' + format + '")'); return '12x20'; } return font; } -// Determine time in milliseconds until next display update for a timer -// that should be updated every `interval` milliseconds. function next_time_update(interval, curr_time, direction) { - if (interval <= 0) { + // Determine time in milliseconds until next display update for a timer + // that should be updated every `interval` milliseconds. + // + // `curr_time` is the current time in milliseconds, and `direction` + // is either 1 (forward) or -1 (backward). The function returns the + // time in milliseconds until the next update, or Infinity if there + // is no update needed (e.g. if interval is zero or negative). + +if (interval <= 0) { // Don't update if interval is zero or negative return Infinity; } @@ -119,6 +135,10 @@ function next_time_update(interval, curr_time, direction) { class TimerView { + // Primary UI for displaying and operating a timer. The + // PrimitiveTimer object is passed to the constructor as a + // parameter. + constructor(timer) { this.timer = timer; @@ -128,6 +148,8 @@ class TimerView { } start() { + // Initialize, display, and activate the UI + this._initLayout(); this.layout.update(); this.layout.clear(); @@ -175,6 +197,8 @@ class TimerView { } stop() { + // Shut down the UI and clean up listeners and handlers + if (this.listeners.timer_render_timeout !== null) { clearTimeout(this.listeners.timer_render_timeout); this.listeners.timer_render_timeout = null; @@ -232,6 +256,10 @@ class TimerView { } render(item) { + // Draw the timer display and update the status and buttons. The + // `item` parameter specifies which part of the display to update. + // If `item` is not specified, the entire display is updated. + console.debug('render called: ' + item); if (!item) { @@ -254,14 +282,14 @@ class TimerView { elem.label = tt.format_duration(this.timer.to_msec(), true); if (running) { update_interval = Math.min( - update_interval, + update_interval, next_time_update(1000, this.timer.to_msec(), this.timer.rate) ); } } else if (mode == 'time hh:mm:ss') { elem.label = locale.time(new Date()).trim(); update_interval = Math.min( - update_interval, + update_interval, next_time_update(1000, Date.now(), 1) ); @@ -325,6 +353,8 @@ class TimerView { } start_stop_timer() { + // Start or pause the timer + if (this.timer.is_running()) { this.timer.pause(); } else { @@ -338,6 +368,10 @@ class TimerView { class TimerFormatView { + // UI for selecting the display format of a timer. The + // PrimitiveTimer object is passed to the constructor as a + // parameter. + constructor(timer) { this.timer = timer; @@ -357,6 +391,8 @@ class TimerFormatView { } start() { + // Initialize, display, and activate the UI + this._initLayout(); this.layout.update(); this.layout.clear(); @@ -430,6 +466,8 @@ class TimerFormatView { } stop() { + // Shut down the UI and clean up listeners and handlers + Bangle.removeListener('drag', this.listeners.drag); Bangle.removeListener('touch', this.listeners.touch); clearWatch(this.listeners.button); @@ -465,7 +503,7 @@ class TimerFormatView { type: 'txt', id: 'row1', label: FORMAT_DISPLAY[FORMAT_MENU[this.format_idx.row1]], - font: row_font('row1', 'mode'), + font: row_font('row1', 'format-menu'), fillx: 1, }, { @@ -491,7 +529,7 @@ class TimerFormatView { type: 'txt', id: 'row2', label: FORMAT_DISPLAY[FORMAT_MENU[this.format_idx.row2]], - font: row_font('row2', 'mode'), + font: row_font('row2', 'format-menu'), fillx: 1, }, { @@ -517,7 +555,7 @@ class TimerFormatView { type: 'txt', id: 'row3', label: FORMAT_DISPLAY[FORMAT_MENU[this.format_idx.row3]], - font: row_font('row3', 'mode'), + font: row_font('row3', 'format-menu'), fillx: 1, }, { @@ -548,10 +586,15 @@ class TimerFormatView { } render() { + // Draw the format selection UI. + this.layout.render(); } update_row(row_id) { + // Render the display format for the given row ID. The row ID + // should be one of 'row1', 'row2', or 'row3'. + const elem = this.layout[row_id]; elem.label = FORMAT_DISPLAY[FORMAT_MENU[this.format_idx[row_id]]]; this.layout.clear(elem); @@ -559,6 +602,9 @@ class TimerFormatView { } incr_format_idx(row_id) { + // Increment the selected format for the given row ID. The row ID + // should be one of 'row1', 'row2', or 'row3'. + this.format_idx[row_id] += 1; if (this.format_idx[row_id] >= FORMAT_MENU.length) { this.format_idx[row_id] = 0; @@ -567,6 +613,9 @@ class TimerFormatView { } decr_format_idx(row_id) { + // Decrement the selected format for the given row ID. The row ID + // should be one of 'row1', 'row2', or 'row3'. + this.format_idx[row_id] -= 1; if (this.format_idx[row_id] < 0) { this.format_idx[row_id] = FORMAT_MENU.length - 1; @@ -574,8 +623,8 @@ class TimerFormatView { this.update_row(row_id); } - // Save new format settings and return to TimerView ok() { + // Save new format settings and return to TimerView for (var row_id of ROW_IDS) { tt.SETTINGS.format[row_id] = FORMAT_MENU[this.format_idx[row_id]]; } @@ -583,31 +632,43 @@ class TimerFormatView { switch_UI(new TimerView(this.timer)); } - // Return to TimerViewMenu without saving changes cancel() { + // Return to TimerViewMenu without saving changes switch_UI(new TimerViewMenu(this.timer)); } } class TimerViewMenu { + // UI for displaying the timer menu. The PrimitiveTimer object is + // passed to the constructor as a parameter. + constructor(timer) { this.timer = timer; } start() { + // Display and activate the top menu of the timer view menu. + this.top_menu(); } stop() { + // Shut down the UI and clean up listeners and handlers + E.showMenu(); } back() { + // Return to the timer view + // (i.e. the timer that was previously displayed) + switch_UI(new TimerView(this.timer)); } top_menu() { + // Display the top-level menu for the timer + const top_menu = { '': { title: this.timer.display_name(), @@ -663,6 +724,9 @@ class TimerViewMenu { } edit_menu() { + // Display the edit menu for the timer. This can be called in + // place of `start` to jump directly to the edit menu. + let keyboard = null; try { keyboard = require("textinput"); } catch (e) {} @@ -724,6 +788,9 @@ class TimerViewMenu { } edit_start() { + // Display the edit > start menu for the timer + // (i.e. the timer's starting value) + let origin_hms = { h: Math.floor(this.timer.origin / 3600), m: Math.floor(this.timer.origin / 60) % 60, @@ -764,24 +831,37 @@ class TimerViewMenu { class TimerMenu { + // UI for displaying the list of timers. The list of + // PrimitiveTimer objects is passed to the constructor as a + // parameter. The currently focused timer is passed as the + // second parameter. + constructor(timers, focused_timer) { this.timers = timers; this.focused_timer = focused_timer; } start() { + // Display the top timer menu + this.top_menu(); } stop() { + // Shut down the UI and clean up listeners and handlers + E.showMenu(); } back() { + // Return to the timer's menu + switch_UI(new TimerViewMenu(this.focused_timer)); } top_menu() { + // Display the top-level menu for the timer list + let menu = { '': { title: "Timers", @@ -798,6 +878,10 @@ class TimerMenu { function switch_UI(new_UI) { + // Switch from one UI to another. The new UI instance is passed as a + // parameter. The old UI is stopped and cleaned up, and the new + // UI is started. + if (CURRENT_UI) { CURRENT_UI.stop(); } diff --git a/apps/tevtimer/lib.js b/apps/tevtimer/lib.js index f4fbe8cf9..62a97f71c 100644 --- a/apps/tevtimer/lib.js +++ b/apps/tevtimer/lib.js @@ -3,7 +3,7 @@ const Sched = require('sched'); const Time_utils = require('time_utils'); -// Convenience // +// Convenience functions // function mod(n, m) { // Modulus function that works like Python's % operator @@ -20,7 +20,20 @@ function ceil(value) { // Data models // class PrimitiveTimer { + // A simple timer object that can be used as a countdown or countup + // timer. It can be paused and resumed, and it can be reset to its + // original value. It can also be saved to and loaded from a + // persistent storage. + constructor(origin, is_running, rate, name, id) { + // origin: initial value of the timer + // is_running: true if the timer should begin running immediately, + // false if it should be paused + // rate: rate of the timer, in units per second. Positive for + // countup, negative for countdown + // name: name of the timer (can be empty) + // id: ID of the timer + this.origin = origin || 0; // default rate +1 unit per 1000 ms, countup this.rate = rate || 0.001; @@ -36,18 +49,26 @@ class PrimitiveTimer { } display_name() { + // Return a string to display as the timer name + // If the name is empty, return a generated name return this.name ? this.name : this.provisional_name(); } provisional_name() { + // Return a generated name for the timer based on the timer's + // origin and current value + return ( - Time_utils.formatDuration(this.origin / Math.abs(this.rate)) + Time_utils.formatDuration(this.to_msec(this.origin)) + ' / ' - + Time_utils.formatDuration(Math.abs(this.get() / Math.abs(this.rate))) + + Time_utils.formatDuration(this.to_msec()) ); } display_status() { + // Return a string representing the timer's status + // (e.g. running, paused, expired) + let status = ''; // Indicate timer expired if its current value is <= 0 and it's @@ -64,10 +85,14 @@ class PrimitiveTimer { } is_running() { + // Return true if the timer is running, false if it is paused + return !this._pause_time; } start() { + // Start the timer if it is paused + if (!this.is_running()) { this._start_time += Date.now() - this._pause_time; this._pause_time = null; @@ -75,6 +100,8 @@ class PrimitiveTimer { } pause() { + // Pause the timer if it is running + if (this.is_running()) { this._pause_time = Date.now(); } @@ -85,6 +112,8 @@ class PrimitiveTimer { } get() { + // Return the current value of the timer, in rate units + const now = Date.now(); const elapsed = (now - this._start_time) @@ -93,6 +122,8 @@ class PrimitiveTimer { } set(new_value) { + // Set the timer to a new value, in rate units + const now = Date.now(); this._start_time = (now - new_value / this.rate) + (this.origin / this.rate); @@ -101,9 +132,9 @@ class PrimitiveTimer { } } - // Convert given timer value to milliseconds using this.rate - // Uses the current value of the timer if no value is provided to_msec(value) { + // Convert given timer value to milliseconds using this.rate + // Uses the current value of the timer if no value is provided if (value === undefined) { value = this.get(); } @@ -111,6 +142,8 @@ class PrimitiveTimer { } dump() { + // Serialize the timer object to a JSON-compatible object + return { cls: 'PrimitiveTimer', version: 0, @@ -127,6 +160,9 @@ class PrimitiveTimer { } static load(data) { + // Deserialize a JSON-compatible object to a PrimitiveTimer + // object + if (!(data.cls == 'PrimitiveTimer' && data.version == 0)) { console.error('Incompatible data type for loading PrimitiveTimer state'); } @@ -143,6 +179,9 @@ class PrimitiveTimer { function format_duration(msec, have_seconds) { + // Format a duration in milliseconds as a string in HH:MM format + // (have_seconds is false) or HH:MM:SS format (have_seconds is true) + if (msec < 0) { return '-' + format_duration(-msec, have_seconds); } @@ -189,10 +228,13 @@ function find_timer_by_id(id) { } function load_timers() { + // Load timers from persistent storage + // If no timers are found, create and return a default timer + console.log('loading timers'); let timers = Storage.readJSON(TIMERS_FILENAME, true) || []; if (timers.length) { - // Deserealize timer objects + // Deserialize timer objects timers = timers.map(t => PrimitiveTimer.load(t)); } else { timers = [new PrimitiveTimer(600, false, -0.001, '', 1)]; @@ -202,6 +244,8 @@ function load_timers() { } function save_timers() { + // Save TIMERS to persistent storage + console.log('saving timers'); const dumped_timers = TIMERS.map(t => t.dump()); if (!Storage.writeJSON(TIMERS_FILENAME, dumped_timers)) { @@ -210,6 +254,11 @@ function save_timers() { } function schedule_save_timers() { + // Schedule a save of the timers to persistent storage + // after a timeout. This is used to reduce the number of + // writes to the flash storage when several changes are + // made in a short time. + if (SAVE_TIMERS_TIMEOUT === null) { console.log('scheduling timer save'); SAVE_TIMERS_TIMEOUT = setTimeout(() => { @@ -222,6 +271,8 @@ function schedule_save_timers() { } function save_settings() { + // Save SETTINGS to persistent storage + console.log('saving settings'); if (!Storage.writeJSON(SETTINGS_FILENAME, SETTINGS)) { E.showAlert('Trouble saving settings'); @@ -229,6 +280,11 @@ function save_settings() { } function schedule_save_settings() { + // Schedule a save of the settings to persistent storage + // after a timeout. This is used to reduce the number of + // writes to the flash storage when several changes are + // made in a short time. + if (SAVE_SETTINGS_TIMEOUT === null) { console.log('scheduling settings save'); SAVE_SETTINGS_TIMEOUT = setTimeout(() => { @@ -255,6 +311,10 @@ var TIMERS = load_timers(); // Persistent data convenience functions function delete_timer(timers, timer) { + // Find `timer` in array `timers` and remove it. + // Return the next timer in the list, or the last one if `timer` + // was the last one in the list. + const idx = timers.indexOf(timer); if (idx !== -1) { timers.splice(idx, 1); @@ -267,6 +327,11 @@ function delete_timer(timers, timer) { } function add_timer(timers, timer) { + // Create a independent timer object duplicating `timer`, assign it a + // new unique ID, and add it to the top of the array `timers`. + // Return the new timer object. + // This is used to create a new timer from an existing one. + // Create a copy of current timer object const new_timer = PrimitiveTimer.load(timer.dump()); // Assign a new ID to the timer @@ -277,6 +342,9 @@ function add_timer(timers, timer) { } function set_last_viewed_timer(timer) { + // Move `timer` to the top of the list of timers, so it will be + // displayed first when the timer list is shown. + const idx = TIMERS.indexOf(timer); if (idx == -1) { console.warn('set_last_viewed_timer: Bug? Called with a timer not found in list'); @@ -291,11 +359,17 @@ function set_last_viewed_timer(timer) { } function set_timers_dirty() { + // Mark the timers as modified and schedule a write to + // persistent storage. + setTimeout(update_system_alarms, 500); schedule_save_timers(); } function set_settings_dirty() { + // Mark the settings as modified and schedule a write to + // persistent storage. + schedule_save_settings(); } @@ -303,6 +377,9 @@ function set_settings_dirty() { // Alarm handling // function delete_system_alarms() { + // Delete system alarms associated with the tevtimer app (except those + // that are snoozed, so that they will trigger later) + var alarms = Sched.getAlarms().filter(a => a.appid == 'tevtimer'); for (let alarm of alarms) { if (alarm.ot === undefined) { @@ -317,6 +394,9 @@ function delete_system_alarms() { } function set_system_alarms() { + // Set system alarms (via `sched` app) for running countdown timers + // that will expire in the future. + for (let idx = 0; idx < TIMERS.length; idx++) { let timer = TIMERS[idx]; let time_to_next_alarm = timer.to_msec(); @@ -335,11 +415,15 @@ function set_system_alarms() { } function update_system_alarms() { + // Refresh system alarms (`sched` app) to reflect changes to timers + delete_system_alarms(); set_system_alarms(); } +// Make sure we save timers and settings when switching to another app +// or rebooting E.on('kill', () => { save_timers(); }); E.on('kill', () => { save_settings(); }); From c79d9fda80fc3ec860f11fb4da4c469cffe60073 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Sun, 27 Apr 2025 19:02:54 -0500 Subject: [PATCH 32/67] Display negative times in provisional names properly --- apps/tevtimer/lib.js | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/apps/tevtimer/lib.js b/apps/tevtimer/lib.js index 62a97f71c..ace0ba629 100644 --- a/apps/tevtimer/lib.js +++ b/apps/tevtimer/lib.js @@ -59,9 +59,9 @@ class PrimitiveTimer { // origin and current value return ( - Time_utils.formatDuration(this.to_msec(this.origin)) + format_duration_2(this.to_msec(this.origin)) + ' / ' - + Time_utils.formatDuration(this.to_msec()) + + format_duration_2(this.to_msec()) ); } @@ -195,6 +195,21 @@ function format_duration(msec, have_seconds) { } +function format_duration_2(msec) { + // Like `time_utils.formatDuration`, but handles negative durations + // and returns '0s' instead of an empty string for a duration of zero + + let s = Time_utils.formatDuration(Math.abs(msec)) + if (s === '') { + return '0s'; + } + if (msec < 0) { + return '- ' + s; + } + return s; +} + + // Persistent state // const TIMERS_FILENAME = 'tevtimer.timers.json'; @@ -433,6 +448,6 @@ exports = {TIMERS, SETTINGS, next_id, find_timer_by_id, load_timers, save_timers, schedule_save_timers, save_settings, schedule_save_settings, PrimitiveTimer, - format_duration, + format_duration, format_duration_2, delete_timer, add_timer, set_last_viewed_timer, set_timers_dirty, set_settings_dirty, update_system_alarms}; From e519f18db9980ac53c14e57980fee8f24674c04d Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Tue, 29 Apr 2025 18:19:00 -0500 Subject: [PATCH 33/67] Add widget for indicating running state of viewed timer --- apps/tevtimer/app.js | 61 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 11 deletions(-) diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js index d1d2ffa82..4f4271d9f 100644 --- a/apps/tevtimer/app.js +++ b/apps/tevtimer/app.js @@ -112,7 +112,7 @@ function next_time_update(interval, curr_time, direction) { // time in milliseconds until the next update, or Infinity if there // is no update needed (e.g. if interval is zero or negative). -if (interval <= 0) { + if (interval <= 0) { // Don't update if interval is zero or negative return Infinity; } @@ -134,6 +134,53 @@ if (interval <= 0) { } +function draw_triangle(lay, flip) { + // Render right-pointing triangle if `flip`, else left-pointing + // triangle + + flip = flip ? lay.width : 0; + g.setColor(g.theme.fg2) + .fillPoly([flip + lay.x, lay.y + lay.height / 2, + lay.x + lay.width - flip, lay.y, + lay.x + lay.width - flip, lay.y + lay.height]); +} + + +function update_status_widget(timer) { + // Update the status widget with the current timer status. The + // timer is passed as a parameter. + + function widget_draw() { + // Draw a right-pointing arrow if the timer is running + + g.reset(); + if (WIDGETS.tevtimer.width > 1) { + draw_triangle({ + x: WIDGETS.tevtimer.x, + // Center the arrow vertically in the 24-pixel-height widget area + y: WIDGETS.tevtimer.y + Math.floor((24 - ARROW_BTN_SIZE) / 2), + width: ARROW_BTN_SIZE, + height: ARROW_BTN_SIZE + }, true); + } + } + + // For some reason, a width of 0 when there's nothing to display + // doesn't work as expected, so we use 1 instead. + let width = timer.is_running() ? ARROW_BTN_SIZE : 1; + + if (WIDGETS.tevtimer === undefined) { + WIDGETS.tevtimer = { + area: 'tr', + width: width, + draw: widget_draw, + }; + } + WIDGETS.tevtimer.width = width; + Bangle.drawWidgets(); +} + + class TimerView { // Primary UI for displaying and operating a timer. The // PrimitiveTimer object is passed to the constructor as a @@ -340,6 +387,7 @@ class TimerView { this.layout.start_btn.label = this.timer.is_running() ? 'Pause' : 'Start'; this.layout.render(this.layout.buttons); + update_status_widget(this.timer); } } @@ -474,16 +522,6 @@ class TimerFormatView { } _initLayout() { - // Render right-pointing triangle if `flip`, else left-pointing - // triangle - function draw_triangle(lay, flip) { - flip = flip ? lay.width : 0; - g.setColor(g.theme.fg2) - .fillPoly([flip + lay.x, lay.y + lay.height / 2, - lay.x + lay.width - flip, lay.y, - lay.x + lay.width - flip, lay.y + lay.height]); - } - const layout = new Layout( { type: 'v', @@ -899,4 +937,5 @@ var CURRENT_UI = null; tt.update_system_alarms(); +update_status_widget(tt.TIMERS[0]); switch_UI(new TimerView(tt.TIMERS[0])); From 36f4b79835d197d82e0efe23012e3d21c160e1ad Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Wed, 30 Apr 2025 18:58:59 -0500 Subject: [PATCH 34/67] Implement 'Current auto' display format --- apps/tevtimer/app.js | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js index 4f4271d9f..1f38d805b 100644 --- a/apps/tevtimer/app.js +++ b/apps/tevtimer/app.js @@ -31,6 +31,8 @@ const FONT = { 'current hh:mm': '12x20', 'time hh:mm': '12x20', + 'current auto': '12x20', + 'name': '12x20', 'format-menu': '12x20', @@ -45,6 +47,8 @@ const FONT = { 'current hh:mm': 'Vector:48x42', 'time hh:mm': 'Vector:56x42', + 'current auto': 'Vector:34x42', + 'name': 'Vector:24x42', 'format-menu': 'Vector:26x42', @@ -59,6 +63,8 @@ const FONT = { 'current hh:mm': 'Vector:48x56', 'time hh:mm': 'Vector:56x56', + 'current auto': 'Vector:34x56', + 'name': 'Vector:24x56', 'format-menu': 'Vector:26x56', @@ -70,6 +76,7 @@ const FONT = { const FORMAT_MENU = [ 'start hh:mm:ss', 'start hh:mm', + 'current auto', 'current hh:mm:ss', 'current hh:mm', 'time hh:mm:ss', @@ -82,6 +89,7 @@ const FORMAT_MENU = [ const FORMAT_DISPLAY = { 'start hh:mm:ss': 'Start HMS', 'start hh:mm': 'Start HM', + 'current auto': 'Curr Auto', 'current hh:mm:ss': 'Curr HMS', 'current hh:mm': 'Curr HM', 'time hh:mm:ss': 'Time HMS', @@ -241,6 +249,20 @@ class TimerView { // Auto move-to-top on use handler this.listeners.to_top_timeout = setTimeout( tt.set_last_viewed_timer, MOVE_TO_TOP_TIMEOUT, this.timer); + + // Screen lock/unlock handler + function lockHandler() { + // If 'current auto' is an active format, update the timer + // display + for (var id of ROW_IDS) { + if (tt.SETTINGS.format[id] == 'current auto') { + this.render('timer'); + break; + } + } + } + this.listeners.lock = lockHandler.bind(this); + Bangle.on('lock', this.listeners.lock); } stop() { @@ -253,6 +275,7 @@ class TimerView { clearWatch(this.listeners.button); Bangle.removeListener('drag', this.listeners.drag); clearTimeout(this.listeners.to_top_timeout); + Bangle.removeListener('lock', this.listeners.lock); Bangle.setUI(); } @@ -325,7 +348,11 @@ class TimerView { let mode = tt.SETTINGS.format[id]; if (mode == 'start hh:mm:ss') { elem.label = tt.format_duration(this.timer.to_msec(this.timer.origin), true); - } else if (mode == 'current hh:mm:ss') { + + } else if ( + mode == 'current hh:mm:ss' + || (mode == 'current auto' && !Bangle.isLocked()) + ) { elem.label = tt.format_duration(this.timer.to_msec(), true); if (running) { update_interval = Math.min( @@ -333,6 +360,7 @@ class TimerView { next_time_update(1000, this.timer.to_msec(), this.timer.rate) ); } + } else if (mode == 'time hh:mm:ss') { elem.label = locale.time(new Date()).trim(); update_interval = Math.min( @@ -342,7 +370,11 @@ class TimerView { } else if (mode == 'start hh:mm') { elem.label = tt.format_duration(this.timer.to_msec(this.timer.origin), false); - } else if (mode == 'current hh:mm') { + + } else if ( + mode == 'current hh:mm' + || (mode == 'current auto' && Bangle.isLocked()) + ) { elem.label = tt.format_duration(this.timer.to_msec(), false); if (running) { // Update every minute for current HM when running @@ -351,15 +383,18 @@ class TimerView { next_time_update(60000, this.timer.to_msec(), this.timer.rate) ); } + } else if (mode == 'time hh:mm') { elem.label = locale.time(new Date(), 1).trim(); update_interval = Math.min( update_interval, next_time_update(60000, Date.now(), 1) ); + } else if (mode == 'name') { elem.label = this.timer.display_name(); } + this.layout.clear(elem); this.layout.render(elem); } From 391df1c82eac4ede475b0118ee44fcc9ca9c308b Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Wed, 30 Apr 2025 19:10:07 -0500 Subject: [PATCH 35/67] =?UTF-8?q?Allow=20horizontal=20font=20size=20change?= =?UTF-8?q?s=20for=20=E2=80=9CCurrent=20auto=E2=80=9D=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/tevtimer/app.js | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js index 1f38d805b..ec20ef1b8 100644 --- a/apps/tevtimer/app.js +++ b/apps/tevtimer/app.js @@ -338,21 +338,22 @@ class TimerView { if (!item || item == 'timer') { - this._update_fonts(); - let update_interval = Infinity; for (var id of ROW_IDS) { const elem = this.layout[id]; const running = this.timer.is_running(); + let mode = tt.SETTINGS.format[id]; + // Special handling for “auto” modes + if (mode == 'current auto') { + mode = Bangle.isLocked() ? 'current hh:mm' : 'current hh:mm:ss'; + } + if (mode == 'start hh:mm:ss') { elem.label = tt.format_duration(this.timer.to_msec(this.timer.origin), true); - } else if ( - mode == 'current hh:mm:ss' - || (mode == 'current auto' && !Bangle.isLocked()) - ) { + } else if (mode == 'current hh:mm:ss') { elem.label = tt.format_duration(this.timer.to_msec(), true); if (running) { update_interval = Math.min( @@ -371,10 +372,7 @@ class TimerView { } else if (mode == 'start hh:mm') { elem.label = tt.format_duration(this.timer.to_msec(this.timer.origin), false); - } else if ( - mode == 'current hh:mm' - || (mode == 'current auto' && Bangle.isLocked()) - ) { + } else if (mode == 'current hh:mm') { elem.label = tt.format_duration(this.timer.to_msec(), false); if (running) { // Update every minute for current HM when running @@ -395,6 +393,7 @@ class TimerView { elem.label = this.timer.display_name(); } + elem.font = row_font(id, mode); this.layout.clear(elem); this.layout.render(elem); } @@ -426,15 +425,6 @@ class TimerView { } } - _update_fonts() { - for (var id of ROW_IDS) { - const elem = this.layout[id]; - elem.font = row_font(id, tt.SETTINGS.format[id]); - this.layout.clear(elem); - this.layout.render(elem); - } - } - start_stop_timer() { // Start or pause the timer From b7a4d154ce14e0c1710a15394f82bbc3c7dd0f71 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Wed, 30 Apr 2025 19:18:10 -0500 Subject: [PATCH 36/67] =?UTF-8?q?Implement=20=E2=80=9CStart=20auto?= =?UTF-8?q?=E2=80=9D=20and=20=E2=80=9CTime=20auto=E2=80=9D=20formats?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/tevtimer/app.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js index ec20ef1b8..5eebc274e 100644 --- a/apps/tevtimer/app.js +++ b/apps/tevtimer/app.js @@ -31,7 +31,9 @@ const FONT = { 'current hh:mm': '12x20', 'time hh:mm': '12x20', + 'start auto': '12x20', 'current auto': '12x20', + 'time auto': '12x20', 'name': '12x20', @@ -47,7 +49,9 @@ const FONT = { 'current hh:mm': 'Vector:48x42', 'time hh:mm': 'Vector:56x42', + 'start auto': 'Vector:34x42', 'current auto': 'Vector:34x42', + 'time auto': 'Vector:24x42', 'name': 'Vector:24x42', @@ -63,7 +67,9 @@ const FONT = { 'current hh:mm': 'Vector:48x56', 'time hh:mm': 'Vector:56x56', + 'start auto': 'Vector:34x56', 'current auto': 'Vector:34x56', + 'time auto': 'Vector:24x56', 'name': 'Vector:24x56', @@ -74,11 +80,13 @@ const FONT = { // List of format IDs available in the format menu // (in the order they are displayed in the menu) const FORMAT_MENU = [ + 'start auto', 'start hh:mm:ss', 'start hh:mm', 'current auto', 'current hh:mm:ss', 'current hh:mm', + 'time auto', 'time hh:mm:ss', 'time hh:mm', 'name', @@ -87,11 +95,13 @@ const FORMAT_MENU = [ // Mapping of format IDs to their human-friendly names displayed in the // format menu const FORMAT_DISPLAY = { + 'start auto': 'Start Auto', 'start hh:mm:ss': 'Start HMS', 'start hh:mm': 'Start HM', 'current auto': 'Curr Auto', 'current hh:mm:ss': 'Curr HMS', 'current hh:mm': 'Curr HM', + 'time auto': 'Time Auto', 'time hh:mm:ss': 'Time HMS', 'time hh:mm': 'Time HM', 'name': 'Name', @@ -345,9 +355,13 @@ class TimerView { const running = this.timer.is_running(); let mode = tt.SETTINGS.format[id]; - // Special handling for “auto” modes - if (mode == 'current auto') { + // Special handling for “auto” formats + if (mode == 'start auto') { + mode = Bangle.isLocked() ? 'start hh:mm' : 'start hh:mm:ss'; + } else if (mode == 'current auto') { mode = Bangle.isLocked() ? 'current hh:mm' : 'current hh:mm:ss'; + } else if (mode == 'time auto') { + mode = Bangle.isLocked() ? 'time hh:mm' : 'time hh:mm:ss'; } if (mode == 'start hh:mm:ss') { From e2b765ea969bb85cabca71b66443e725ccfd136f Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Wed, 30 Apr 2025 19:19:26 -0500 Subject: [PATCH 37/67] Rename variable for clarity --- apps/tevtimer/app.js | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js index 5eebc274e..1e31ffef0 100644 --- a/apps/tevtimer/app.js +++ b/apps/tevtimer/app.js @@ -354,20 +354,20 @@ class TimerView { const elem = this.layout[id]; const running = this.timer.is_running(); - let mode = tt.SETTINGS.format[id]; + let format = tt.SETTINGS.format[id]; // Special handling for “auto” formats - if (mode == 'start auto') { - mode = Bangle.isLocked() ? 'start hh:mm' : 'start hh:mm:ss'; - } else if (mode == 'current auto') { - mode = Bangle.isLocked() ? 'current hh:mm' : 'current hh:mm:ss'; - } else if (mode == 'time auto') { - mode = Bangle.isLocked() ? 'time hh:mm' : 'time hh:mm:ss'; + if (format == 'start auto') { + format = Bangle.isLocked() ? 'start hh:mm' : 'start hh:mm:ss'; + } else if (format == 'current auto') { + format = Bangle.isLocked() ? 'current hh:mm' : 'current hh:mm:ss'; + } else if (format == 'time auto') { + format = Bangle.isLocked() ? 'time hh:mm' : 'time hh:mm:ss'; } - if (mode == 'start hh:mm:ss') { + if (format == 'start hh:mm:ss') { elem.label = tt.format_duration(this.timer.to_msec(this.timer.origin), true); - } else if (mode == 'current hh:mm:ss') { + } else if (format == 'current hh:mm:ss') { elem.label = tt.format_duration(this.timer.to_msec(), true); if (running) { update_interval = Math.min( @@ -376,17 +376,17 @@ class TimerView { ); } - } else if (mode == 'time hh:mm:ss') { + } else if (format == 'time hh:mm:ss') { elem.label = locale.time(new Date()).trim(); update_interval = Math.min( update_interval, next_time_update(1000, Date.now(), 1) ); - } else if (mode == 'start hh:mm') { + } else if (format == 'start hh:mm') { elem.label = tt.format_duration(this.timer.to_msec(this.timer.origin), false); - } else if (mode == 'current hh:mm') { + } else if (format == 'current hh:mm') { elem.label = tt.format_duration(this.timer.to_msec(), false); if (running) { // Update every minute for current HM when running @@ -396,18 +396,18 @@ class TimerView { ); } - } else if (mode == 'time hh:mm') { + } else if (format == 'time hh:mm') { elem.label = locale.time(new Date(), 1).trim(); update_interval = Math.min( update_interval, next_time_update(60000, Date.now(), 1) ); - } else if (mode == 'name') { + } else if (format == 'name') { elem.label = this.timer.display_name(); } - elem.font = row_font(id, mode); + elem.font = row_font(id, format); this.layout.clear(elem); this.layout.render(elem); } From cf48e8c36ed5a89c541597e32b1c21e2d74342f2 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Wed, 30 Apr 2025 19:29:24 -0500 Subject: [PATCH 38/67] Make format menu arrows easier to tap --- apps/tevtimer/app.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js index 1e31ffef0..6ec98f72c 100644 --- a/apps/tevtimer/app.js +++ b/apps/tevtimer/app.js @@ -524,11 +524,16 @@ class TimerFormatView { // Touch handler function touchHandler(button, xy) { // Increment or decrement row's format index based on the arrow tapped + + // Enlarge tap area by this amount in the X direction to make it + // easier to hit + const x_tolerance = 20; + for (let row_id of ROW_IDS) { for (let btn_id of ['prev', 'next']) { let elem = row_id + '.' + btn_id; - if (xy.x >= this.layout[elem].x - && xy.x <= this.layout[elem].x + this.layout[elem].w + if (xy.x >= this.layout[elem].x - x_tolerance + && xy.x <= this.layout[elem].x + this.layout[elem].w + x_tolerance && xy.y >= this.layout[elem].y && xy.y <= this.layout[elem].y + this.layout[elem].h) { if (btn_id === 'prev') { From 7530aa9ed1833b07a536f98f913c86ce3a27c2ee Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Thu, 1 May 2025 19:36:49 -0500 Subject: [PATCH 39/67] Framework for custom actions on phys. button and taps --- apps/tevtimer/app.js | 62 +++++++++++++++++++++++++++++++++++--------- apps/tevtimer/lib.js | 3 +++ 2 files changed, 53 insertions(+), 12 deletions(-) diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js index 6ec98f72c..1beeed1ec 100644 --- a/apps/tevtimer/app.js +++ b/apps/tevtimer/app.js @@ -190,7 +190,6 @@ function update_status_widget(timer) { if (WIDGETS.tevtimer === undefined) { WIDGETS.tevtimer = { area: 'tr', - width: width, draw: widget_draw, }; } @@ -222,11 +221,27 @@ class TimerView { // Physical button handler this.listeners.button = setWatch( - this.start_stop_timer.bind(this), + () => { this.dispatch_action(tt.SETTINGS.button_act); }, BTN, - {edge: 'falling', debounce: 50, repeat: true} + {edge: 'rising', debounce: 50, repeat: true} ); + // Tap handler + function tapHandler(button, xy) { + // Check if the tap was in the area of the timer display + if (xy.y < this.layout.buttons.y) { + // Dispatch action specified based on left or right half of display tapped + if (xy.x < this.layout.row1.x + this.layout.row1.w / 2) { + this.dispatch_action(tt.SETTINGS.left_tap_act); + } else { + this.dispatch_action(tt.SETTINGS.right_tap_act); + } + } + } + this.listeners.tap = tapHandler.bind(this); + Bangle.on('touch', this.listeners.tap); + + // Drag handler let distanceX = null; function dragHandler(ev) { if (ev.b) { @@ -283,6 +298,7 @@ class TimerView { this.listeners.timer_render_timeout = null; } clearWatch(this.listeners.button); + Bangle.removeListener('touch', this.listeners.tap); Bangle.removeListener('drag', this.listeners.drag); clearTimeout(this.listeners.to_top_timeout); Bangle.removeListener('lock', this.listeners.lock); @@ -451,6 +467,22 @@ class TimerView { this.render('status'); this.render('timer'); } + + dispatch_action(action) { + // Execute a UI action represented by the string `action`. + + if (action === 'start/stop') { + return this.start_stop_timer() + + } else if (action === 'edit_start') { + // Display the timer start edit UI + this.stop(); + CURRENT_UI = new TimerViewMenu(this.timer); + CURRENT_UI.edit_start( + () => { switch_UI(new TimerView(this.timer)); }, + ); + } + } } @@ -553,7 +585,7 @@ class TimerFormatView { this.listeners.button = setWatch( this.ok.bind(this), BTN, - {edge: 'falling', debounce: 50, repeat: true} + {edge: 'rising', debounce: 50, repeat: true} ); } @@ -748,19 +780,21 @@ class TimerViewMenu { switch_UI(new TimerView(this.timer)); } - top_menu() { + top_menu(back) { // Display the top-level menu for the timer + // `back` is optional and specifies the previous routine to return to + console.log('top_menu: ' + back); const top_menu = { '': { title: this.timer.display_name(), - back: this.back.bind(this) + back: back || (() => { this.back(); }), }, 'Reset': () => { E.showMenu(reset_menu); }, 'Timers': () => { switch_UI(new TimerMenu(tt.TIMERS, this.timer)); }, - 'Edit': this.edit_menu.bind(this), + 'Edit': () => { this.edit_menu() }, 'Format': () => { switch_UI(new TimerFormatView(this.timer)); }, @@ -805,9 +839,11 @@ class TimerViewMenu { E.showMenu(top_menu); } - edit_menu() { + edit_menu(back) { // Display the edit menu for the timer. This can be called in // place of `start` to jump directly to the edit menu. + // `back` is optional and specifies the previous routine to return to + console.log('edit_menu: ' + back); let keyboard = null; try { keyboard = require("textinput"); } catch (e) {} @@ -815,7 +851,7 @@ class TimerViewMenu { const edit_menu = { '': { title: 'Edit: ' + this.timer.display_name(), - back: () => { this.top_menu(); }, + back: back || (() => { this.top_menu() }), }, 'Name': { value: this.timer.name, @@ -829,7 +865,7 @@ class TimerViewMenu { }, 0); } }, - 'Start': this.edit_start.bind(this), + 'Start': () => { this.edit_start(); }, 'At end': { // Option to auto-start another timer when this one ends format: v => v === -1 @@ -869,9 +905,11 @@ class TimerViewMenu { E.showMenu(edit_menu); } - edit_start() { + edit_start(back) { // Display the edit > start menu for the timer // (i.e. the timer's starting value) + // `back` is optional and specifies the previous routine to return to + console.log('edit_start: ' + back); let origin_hms = { h: Math.floor(this.timer.origin / 3600), @@ -902,7 +940,7 @@ class TimerViewMenu { wrap_3: true, separator_1: ':', separator_2: ':', - back: this.edit_menu.bind(this), + back: back || (() => { this.edit_menu(); }), onchange: (h, m, s) => { this.timer.origin = h * 3600 + m * 60 + s; tt.set_timers_dirty(); diff --git a/apps/tevtimer/lib.js b/apps/tevtimer/lib.js index ace0ba629..3bb902e66 100644 --- a/apps/tevtimer/lib.js +++ b/apps/tevtimer/lib.js @@ -318,6 +318,9 @@ const SETTINGS = Object.assign({ 'row2': 'start hh:mm:ss', 'row3': 'current hh:mm:ss', }, + 'button_act': 'start/stop', + 'left_tap_act': 'edit_start', + 'right_tap_act': 'edit_start', }, Storage.readJSON(SETTINGS_FILENAME, true) || {}); var TIMERS = load_timers(); From 6fe2a36a2a1107d66d7d6406dd26126709feabec Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Thu, 1 May 2025 19:39:34 -0500 Subject: [PATCH 40/67] =?UTF-8?q?Apparently=20several=20UIs=20use=20button?= =?UTF-8?q?=20for=20cancel=20rather=20than=20OK,=20so=20best=20match=20the?= =?UTF-8?q?m=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/tevtimer/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js index 1beeed1ec..7f7556819 100644 --- a/apps/tevtimer/app.js +++ b/apps/tevtimer/app.js @@ -583,7 +583,7 @@ class TimerFormatView { // Physical button handler this.listeners.button = setWatch( - this.ok.bind(this), + this.cancel.bind(this), BTN, {edge: 'rising', debounce: 50, repeat: true} ); From 71494ebb387a8dd4285ee8e37b032f419c509b5e Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Sat, 3 May 2025 21:32:56 -0500 Subject: [PATCH 41/67] Rework UI code so selected routines can be called from different places Mainly, there needs to be a way to always go back to the UI that called another, wherever that is. --- apps/tevtimer/app.js | 363 ++++++++++++++++++++++++++++++------------- 1 file changed, 255 insertions(+), 108 deletions(-) diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js index 7f7556819..5df416920 100644 --- a/apps/tevtimer/app.js +++ b/apps/tevtimer/app.js @@ -198,6 +198,8 @@ function update_status_widget(timer) { } +// UI modes // + class TimerView { // Primary UI for displaying and operating a timer. The // PrimitiveTimer object is passed to the constructor as a @@ -472,14 +474,14 @@ class TimerView { // Execute a UI action represented by the string `action`. if (action === 'start/stop') { - return this.start_stop_timer() + this.start_stop_timer() } else if (action === 'edit_start') { - // Display the timer start edit UI - this.stop(); - CURRENT_UI = new TimerViewMenu(this.timer); - CURRENT_UI.edit_start( - () => { switch_UI(new TimerView(this.timer)); }, + switch_UI( + new TimerEditStart( + this.timer, + () => { switch_UI(new TimerView(this.timer)); } + ) ); } } @@ -487,12 +489,19 @@ class TimerView { class TimerFormatView { - // UI for selecting the display format of a timer. The - // PrimitiveTimer object is passed to the constructor as a - // parameter. + // UI for selecting the display format of a timer. + + constructor(timer, back) { + // `timer` is the current PrimitiveTimer object being edited. + // `back` is the function that activates the previous UI to return + // to when the format selection is exited. It is passed `true` if + // the format change was confirmed and `false` if it was canceled. + // If `back` is not specified, a default back handler is used that + // returns to the TimerView if accepted or TimerViewMenu if + // canceled. - constructor(timer) { this.timer = timer; + this.back = back || this._back; this.layout = null; this.listeners = {}; @@ -509,6 +518,15 @@ class TimerFormatView { } } + _back(ok) { + // Default back handler + if (ok) { + switch_UI(new TimerView(this.timer)); + } else { + switch_UI(new TimerViewMenu(this.timer)); + } + } + start() { // Initialize, display, and activate the UI @@ -743,28 +761,59 @@ class TimerFormatView { tt.SETTINGS.format[row_id] = FORMAT_MENU[this.format_idx[row_id]]; } tt.set_settings_dirty(); - switch_UI(new TimerView(this.timer)); + this.back(true); } cancel() { // Return to TimerViewMenu without saving changes - switch_UI(new TimerViewMenu(this.timer)); + this.back(false); } } class TimerViewMenu { - // UI for displaying the timer menu. The PrimitiveTimer object is - // passed to the constructor as a parameter. + // UI for displaying the timer menu. + + constructor(timer, back) { + // `timer` is the PrimitiveTimer object whose menu is being + // displayed. `back` is a function that activates the previous UI + // to return to when the menu is exited. - constructor(timer) { this.timer = timer; + this.back = back || this._back; + } + + _back() { + // Default back handler + // Return to TimerView for the current timer + switch_UI(new TimerView(this.timer)); } start() { - // Display and activate the top menu of the timer view menu. + // Display and activate the timer view menu. - this.top_menu(); + const menu = { + '': { + title: this.timer.display_name(), + back: (() => { this.back(); }), + }, + 'Reset': () => { switch_UI(new ResetTimer(this.timer)); }, + 'Timers': () => { switch_UI(new TimerMenu(tt.TIMERS, this.timer)); }, + 'Edit': () => { switch_UI(new TimerEditMenu(this.timer)); }, + 'Format': () => { switch_UI(new TimerFormatView(this.timer)); }, + 'Add': () => { + tt.set_timers_dirty(); + const new_timer = tt.add_timer(tt.TIMERS, this.timer); + switch_UI(new TimerEditMenu(new_timer)); + }, + 'Delete': () => { switch_UI(new DeleteTimer(this.timer)); }, + }; + if (tt.TIMERS.length <= 1) { + // Prevent user deleting last timer + delete menu.Delete; + } + + E.showMenu(menu); } stop() { @@ -773,85 +822,143 @@ class TimerViewMenu { E.showMenu(); } - back() { - // Return to the timer view - // (i.e. the timer that was previously displayed) +} - switch_UI(new TimerView(this.timer)); +class ResetTimer { + // UI for resetting a timer. + + constructor(timer, back) { + // `timer` is the PrimitiveTimer object to reset. + // `back` is a function that activates the previous UI to return + // to when the menu is exited. It is passed `true` if the timer is + // reset and `false` if it is canceled. If `back` is not + // specified, a default back handler is used that returns to + // TimerView if accepted or TimerViewMenu if canceled. + + this.timer = timer; + this.back = back || this._back; } - top_menu(back) { - // Display the top-level menu for the timer - // `back` is optional and specifies the previous routine to return to - console.log('top_menu: ' + back); + _back(ok) { + // Default back handler - const top_menu = { - '': { - title: this.timer.display_name(), - back: back || (() => { this.back(); }), - }, - 'Reset': () => { E.showMenu(reset_menu); }, - 'Timers': () => { - switch_UI(new TimerMenu(tt.TIMERS, this.timer)); - }, - 'Edit': () => { this.edit_menu() }, - 'Format': () => { - switch_UI(new TimerFormatView(this.timer)); - }, - 'Add': () => { - tt.set_timers_dirty(); - const new_timer = tt.add_timer(tt.TIMERS, this.timer); - const timer_view_menu = new TimerViewMenu(new_timer); - timer_view_menu.edit_menu(); - }, - 'Delete': () => { E.showMenu(delete_menu); }, - }; - if (tt.TIMERS.length <= 1) { - // Prevent user deleting last timer - delete top_menu.Delete; + if (ok) { + switch_UI(new TimerView(this.timer)); + } else { + switch_UI(new TimerViewMenu(this.timer)); } + } - const reset_menu = { + start() { + // Display and activate the reset timer confirmation menu. + + const menu = { '': { title: 'Confirm reset', - back: () => { E.showMenu(top_menu); } + back: () => { this.back(false); } }, 'Reset': () => { this.timer.reset(); tt.set_timers_dirty(); - this.back(); + this.back(true); }, - 'Cancel': () => { E.showMenu(top_menu); }, + 'Cancel': () => { this.back(false); }, }; - const delete_menu = { - '': { - title: 'Confirm delete', - back: () => { E.showMenu(top_menu); } - }, - 'Delete': () => { - tt.set_timers_dirty(); - switch_UI(new TimerView(tt.delete_timer(tt.TIMERS, this.timer))); - }, - 'Cancel': () => { E.showMenu(top_menu); }, - }; - - E.showMenu(top_menu); + E.showMenu(menu); } - edit_menu(back) { - // Display the edit menu for the timer. This can be called in - // place of `start` to jump directly to the edit menu. - // `back` is optional and specifies the previous routine to return to - console.log('edit_menu: ' + back); + stop() { + // Shut down the UI and clean up listeners and handlers + + E.showMenu(); + } +} + +class DeleteTimer { + // UI for deleting a timer. + + constructor(timer, back) { + // `timer` is the PrimitiveTimer object to delete. `back` is a + // function that activates the previous UI to return to when the + // menu is exited. It is passed `true` for the first parameter if + // the timer is deleted and `false` if it is canceled. For the + // second parameter, it is passed the same timer if canceled or + // another existing timer in the list if the given timer was + // deleted. If `back` is not specified, a default back handler is + // used that returns to TimerView if accepted or TimerViewMenu if + // canceled. + + this.timer = timer; + this.back = back || this._back; + } + + _back(ok, timer) { + // Default back handler + + if (ok) { + switch_UI(new TimerView(timer)); + } else { + switch_UI(new TimerViewMenu(timer)); + } + } + + start() { + // Display and activate the delete timer confirmation menu. + + const menu = { + '': { + title: 'Confirm delete', + back: () => { this.back(false, this.timer); } + }, + 'Delete': () => { + ok = true; + tt.set_timers_dirty(); + this.back(true, tt.delete_timer(tt.TIMERS, this.timer)); + }, + 'Cancel': () => { this.back(false, this.timer) }, + }; + + E.showMenu(menu); + + } + + stop() { + // Shut down the UI and clean up listeners and handlers + + E.showMenu(); + } +} + +class TimerEditMenu { + // UI for editing a timer. + + constructor(timer, back) { + // `timer` is the PrimitiveTimer object to edit. `back` is a + // function that activates the previous UI to return to when the + // menu is exited. If `back` is not specified, a default back + // handler is used that returns to TimerViewMenu. + + this.timer = timer; + this.back = back || this._back; + } + + _back() { + // Default back handler + + switch_UI(new TimerViewMenu(this.timer)); + } + + start() { + // Display the edit menu for the timer. let keyboard = null; try { keyboard = require("textinput"); } catch (e) {} - const edit_menu = { + const menu = { '': { title: 'Edit: ' + this.timer.display_name(), - back: back || (() => { this.top_menu() }), + back: () => { this.back(); } }, 'Name': { value: this.timer.name, @@ -860,12 +967,12 @@ class TimerViewMenu { keyboard.input({text:this.timer.name}).then(text => { this.timer.name = text; tt.set_timers_dirty(); - setTimeout(() => { this.edit_menu(); }, 0); + switch_UI(new TimerViewMenu(this.timer)); }); }, 0); } }, - 'Start': () => { this.edit_start(); }, + 'Start': () => { switch_UI(new TimerEditStart(this.timer)); }, 'At end': { // Option to auto-start another timer when this one ends format: v => v === -1 @@ -899,17 +1006,45 @@ class TimerViewMenu { }; if (!keyboard) { - delete edit_menu.Name; + // Hide the Name menu item if text input module is not available + delete menu.Name; } - E.showMenu(edit_menu); + E.showMenu(menu); } - edit_start(back) { + stop() { + // Shut down the UI and clean up listeners and handlers + + E.showMenu(); + } +} + + +class TimerEditStart { + // UI for editing the timer's starting value. + + constructor(timer, back) { + // `timer` is the PrimitiveTimer object to edit. `back` is a + // function that activates the previous UI to return to when the + // menu is exited. It is passed `true` if the timer is edited and + // `false` if it is canceled. If `back` is not specified, a + // default back handler is used that returns to TimerView. + + this.timer = timer; + this.back = back || this._back; + } + + _back(ok) { + // Default back handler + + switch_UI(new TimerEditMenu(this.timer)); + } + + start() { // Display the edit > start menu for the timer - // (i.e. the timer's starting value) - // `back` is optional and specifies the previous routine to return to - console.log('edit_start: ' + back); + + var ok = false; let origin_hms = { h: Math.floor(this.timer.origin / 3600), @@ -940,60 +1075,72 @@ class TimerViewMenu { wrap_3: true, separator_1: ':', separator_2: ':', - back: back || (() => { this.edit_menu(); }), + back: () => { this.back(ok); }, onchange: (h, m, s) => { + ok = true; this.timer.origin = h * 3600 + m * 60 + s; tt.set_timers_dirty(); } }); } -} - - -class TimerMenu { - // UI for displaying the list of timers. The list of - // PrimitiveTimer objects is passed to the constructor as a - // parameter. The currently focused timer is passed as the - // second parameter. - - constructor(timers, focused_timer) { - this.timers = timers; - this.focused_timer = focused_timer; - } - - start() { - // Display the top timer menu - - this.top_menu(); - } stop() { // Shut down the UI and clean up listeners and handlers E.showMenu(); } +} - back() { - // Return to the timer's menu - switch_UI(new TimerViewMenu(this.focused_timer)); +class TimerMenu { + // UI for choosing among the list of defined timers. + + constructor(timers, focused_timer, back) { + // `timers` is the list of PrimitiveTimer objects to display. + // `focused_timer` is the PrimitiveTimer object that is currently + // being displayed. `back` is a function that activates the + // previous UI to return to when the menu is exited. It is passed + // the selected timer object if a timer is selected or `null` if + // the menu is canceled, and the last-focused timer object. If not + // specified, a default back handler is used that returns to + // TimerView for the selected timer or TimerViewMenu if canceled. + + this.timers = timers; + this.focused_timer = focused_timer; + this.back = back || this._back; } - top_menu() { - // Display the top-level menu for the timer list + _back(timer, focused_timer) { + // Default back handler + + if (timer) { + switch_UI(new TimerView(timer)); + } else { + switch_UI(new TimerViewMenu(focused_timer)); + } + } + + start() { + // Display the timer menu let menu = { '': { title: "Timers", - back: this.back.bind(this) + back: () => { this.back(null, this.focused_timer); } } }; this.timers.forEach((timer) => { menu[timer.display_status() + ' ' + timer.display_name()] = - () => { switch_UI(new TimerView(timer)); }; + () => { this.back(timer, this.focused_timer); }; }); E.showMenu(menu); } + + stop() { + // Shut down the UI and clean up listeners and handlers + + E.showMenu(); + } } From cd8aa6974d3f104f283d5276ed50835ccb28bcf7 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Sat, 3 May 2025 21:39:31 -0500 Subject: [PATCH 42/67] May not be necessary but seems like a good idea? --- apps/tevtimer/app.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js index 5df416920..18875e0dc 100644 --- a/apps/tevtimer/app.js +++ b/apps/tevtimer/app.js @@ -1145,15 +1145,17 @@ class TimerMenu { function switch_UI(new_UI) { - // Switch from one UI to another. The new UI instance is passed as a - // parameter. The old UI is stopped and cleaned up, and the new - // UI is started. + // Switch from one UI mode to another (after current call stack + // completes). The new UI instance is passed as a parameter. The old + // UI is stopped and cleaned up, and the new UI is started. - if (CURRENT_UI) { - CURRENT_UI.stop(); - } - CURRENT_UI = new_UI; - CURRENT_UI.start(); + setTimeout(() => { + if (CURRENT_UI) { + CURRENT_UI.stop(); + } + CURRENT_UI = new_UI; + CURRENT_UI.start(); + }, 0); } From fcc9e62fe66146b162f63c2cb878639f760027f1 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Sun, 4 May 2025 12:11:12 -0500 Subject: [PATCH 43/67] Settings for quick actions (button, left/right tap) --- apps/tevtimer/app.js | 71 ++++++++++++++++++++++++++++++++++++++++++++ apps/tevtimer/lib.js | 15 +++++++++- 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js index 18875e0dc..0adf707fa 100644 --- a/apps/tevtimer/app.js +++ b/apps/tevtimer/app.js @@ -807,6 +807,7 @@ class TimerViewMenu { switch_UI(new TimerEditMenu(new_timer)); }, 'Delete': () => { switch_UI(new DeleteTimer(this.timer)); }, + 'Settings': () => { switch_UI(new AppSettingsMenu(this.timer)); }, }; if (tt.TIMERS.length <= 1) { // Prevent user deleting last timer @@ -1144,6 +1145,76 @@ class TimerMenu { } +class AppSettingsMenu { + // UI for displaying the app settings menu. + + constructor(timer, back) { + // `timer` is the last focused timer object (only used for default + // back handler described below). + // `back` is a function that activates the previous UI to + // return to when the menu is exited. If not specified, a default + // back handler is used that returns to the TimerViewMenu for the + // last-focused timer. + this.timer = timer; + this.back = back || this._back; + } + + _back() { + // Default back handler + switch_UI(new TimerViewMenu(this.timer)); + } + + start() { + // Display the app settings menu + + const menu = { + '': { + title: 'Settings', + back: () => { this.back(); } + }, + 'Button': { + value: tt.ACTIONS.indexOf(tt.SETTINGS.button_act), + min: 0, + max: tt.ACTIONS.length - 1, + format: v => tt.ACTION_NAMES[tt.ACTIONS[v]], + onchange: v => { + tt.SETTINGS.button_act = tt.ACTIONS[v]; + tt.set_settings_dirty(); + } + }, + 'Tap left': { + value: tt.ACTIONS.indexOf(tt.SETTINGS.left_tap_act), + min: 0, + max: tt.ACTIONS.length - 1, + format: v => tt.ACTION_NAMES[tt.ACTIONS[v]], + onchange: v => { + tt.SETTINGS.left_tap_act = tt.ACTIONS[v]; + tt.set_settings_dirty(); + } + }, + 'Tap right': { + value: tt.ACTIONS.indexOf(tt.SETTINGS.right_tap_act), + min: 0, + max: tt.ACTIONS.length - 1, + format: v => tt.ACTION_NAMES[tt.ACTIONS[v]], + onchange: v => { + tt.SETTINGS.right_tap_act = tt.ACTIONS[v]; + tt.set_settings_dirty(); + } + }, + }; + + E.showMenu(menu); + } + + stop() { + // Shut down the UI and clean up listeners and handlers + + E.showMenu(); + } +} + + function switch_UI(new_UI) { // Switch from one UI mode to another (after current call stack // completes). The new UI instance is passed as a parameter. The old diff --git a/apps/tevtimer/lib.js b/apps/tevtimer/lib.js index 3bb902e66..622a5c06a 100644 --- a/apps/tevtimer/lib.js +++ b/apps/tevtimer/lib.js @@ -312,6 +312,19 @@ function schedule_save_settings() { } // Default settings + +// List of actions in menu, in order presented +const ACTIONS = [ + 'start/stop', + 'edit_start', +]; + +// Map of action IDs to their UI displayed names +const ACTION_NAMES = { + 'start/stop': 'Start/stop', + 'edit_start': 'Edit start', +}; + const SETTINGS = Object.assign({ 'format': { 'row1': 'time hh:mm', @@ -446,7 +459,7 @@ E.on('kill', () => { save_timers(); }); E.on('kill', () => { save_settings(); }); -exports = {TIMERS, SETTINGS, +exports = {TIMERS, SETTINGS, ACTIONS, ACTION_NAMES, mod, ceil, next_id, find_timer_by_id, load_timers, save_timers, schedule_save_timers, save_settings, schedule_save_settings, From 37d567715d2e925dd3f7bcf3fcb344ad4d4f63d4 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Sun, 4 May 2025 15:00:38 -0500 Subject: [PATCH 44/67] Add some more quick actions --- apps/tevtimer/app.js | 34 ++++++++++++++++++++++++++++++++-- apps/tevtimer/lib.js | 8 ++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js index 0adf707fa..e1722e034 100644 --- a/apps/tevtimer/app.js +++ b/apps/tevtimer/app.js @@ -476,13 +476,43 @@ class TimerView { if (action === 'start/stop') { this.start_stop_timer() - } else if (action === 'edit_start') { + } else if (action === 'reset') { switch_UI( - new TimerEditStart( + new ResetTimer( this.timer, () => { switch_UI(new TimerView(this.timer)); } ) ); + + } else if (action === 'timers') { + switch_UI( + new TimerMenu( + tt.TIMERS, + this.timer, + (timer, focused_timer) => { + switch_UI(new TimerView(timer || focused_timer)); + } + ) + ); + + } else if (action == 'edit') { + switch_UI(new TimerEditMenu( + this.timer, + () => { switch_UI(new TimerView(this.timer)); } + )); + + } else if (action === 'edit_start') { + switch_UI(new TimerEditStart( + this.timer, + () => { switch_UI(new TimerView(this.timer)); } + )); + + } else if (action == 'format') { + switch_UI(new TimerFormatView( + this.timer, + () => { switch_UI(new TimerView(this.timer)); } + )); + } } } diff --git a/apps/tevtimer/lib.js b/apps/tevtimer/lib.js index 622a5c06a..20ee21ca7 100644 --- a/apps/tevtimer/lib.js +++ b/apps/tevtimer/lib.js @@ -316,13 +316,21 @@ function schedule_save_settings() { // List of actions in menu, in order presented const ACTIONS = [ 'start/stop', + 'reset', + 'timers', + 'edit', 'edit_start', + 'format', ]; // Map of action IDs to their UI displayed names const ACTION_NAMES = { 'start/stop': 'Start/stop', + 'reset': 'Reset', + 'timers': 'Timers', + 'edit': 'Edit timer', 'edit_start': 'Edit start', + 'format': 'Format', }; const SETTINGS = Object.assign({ From 73cd8122df25c019ea2013f69eff8d42f1e6b332 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Sun, 4 May 2025 15:15:34 -0500 Subject: [PATCH 45/67] Fix back when accessing TimerEditMenu via quick action Going to TimerEditMenu directly from a quick action and then entering Name or Start causes TimerEditMenu's back button not to go straight back to TimerView because TimerEditMenu's overridden back action gets lost. Solve this by making Name and Start's back actions return to the existing instance of TimerEditMenu rather than creating a new one. --- apps/tevtimer/app.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js index e1722e034..b17d453a5 100644 --- a/apps/tevtimer/app.js +++ b/apps/tevtimer/app.js @@ -998,12 +998,18 @@ class TimerEditMenu { keyboard.input({text:this.timer.name}).then(text => { this.timer.name = text; tt.set_timers_dirty(); - switch_UI(new TimerViewMenu(this.timer)); + switch_UI(this); }); }, 0); } }, - 'Start': () => { switch_UI(new TimerEditStart(this.timer)); }, + 'Start': () => { + switch_UI(new TimerEditStart( + this.timer, + () => { switch_UI(this); } + ) + ); + }, 'At end': { // Option to auto-start another timer when this one ends format: v => v === -1 From fcddbe159fb9a13d6cc539f367f6d8f07217b68c Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Sun, 4 May 2025 19:13:34 -0500 Subject: [PATCH 46/67] Add options for whether to confirm reset & delete --- apps/tevtimer/app.js | 39 +++++++++++++++++++++++++++++++++++---- apps/tevtimer/lib.js | 2 ++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js index b17d453a5..5163e4727 100644 --- a/apps/tevtimer/app.js +++ b/apps/tevtimer/app.js @@ -881,7 +881,8 @@ class ResetTimer { } start() { - // Display and activate the reset timer confirmation menu. + // Display and activate the reset timer confirmation menu if + // configured in settings, or immediately reset the timer if not. const menu = { '': { @@ -896,7 +897,13 @@ class ResetTimer { 'Cancel': () => { this.back(false); }, }; - E.showMenu(menu); + if (tt.SETTINGS.confirm_reset === true + || (tt.SETTINGS.confirm_reset === 'auto' + && this.timer.to_msec() > 0)) { + E.showMenu(menu); + } else { + menu.Reset(); + } } stop() { @@ -935,7 +942,9 @@ class DeleteTimer { } start() { - // Display and activate the delete timer confirmation menu. + // Display and activate the delete timer confirmation menu if + // configured in settings, or immediately delete the timer if + // not. const menu = { '': { @@ -950,7 +959,11 @@ class DeleteTimer { 'Cancel': () => { this.back(false, this.timer) }, }; - E.showMenu(menu); + if (tt.SETTINGS.confirm_delete) { + E.showMenu(menu); + } else { + menu.Delete(); + } } @@ -1238,6 +1251,24 @@ class AppSettingsMenu { tt.set_settings_dirty(); } }, + 'Confirm reset': { + value: [true, 'auto', false].indexOf(tt.SETTINGS.confirm_reset), + format: v => ['Always', 'Auto', 'Never'][v], + min: 0, + max: 2, + onchange: v => { + tt.SETTINGS.confirm_reset = [true, 'auto', false][v]; + tt.set_settings_dirty(); + } + }, + 'Confirm delete': { + value: tt.SETTINGS.confirm_delete, // boolean + format: v => v ? 'Always' : 'Never', + onchange: v => { + tt.SETTINGS.confirm_delete = v; + tt.set_settings_dirty(); + } + }, }; E.showMenu(menu); diff --git a/apps/tevtimer/lib.js b/apps/tevtimer/lib.js index 20ee21ca7..ed6456271 100644 --- a/apps/tevtimer/lib.js +++ b/apps/tevtimer/lib.js @@ -342,6 +342,8 @@ const SETTINGS = Object.assign({ 'button_act': 'start/stop', 'left_tap_act': 'edit_start', 'right_tap_act': 'edit_start', + 'confirm_reset': 'auto', + 'confirm_delete': true, }, Storage.readJSON(SETTINGS_FILENAME, true) || {}); var TIMERS = load_timers(); From bb8403a49c308e94ef5ee9b8cc9b00f1580f6c7b Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Sun, 4 May 2025 20:50:57 -0500 Subject: [PATCH 47/67] Oops, that does nothing --- apps/tevtimer/app.js | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js index 5163e4727..ef4b38029 100644 --- a/apps/tevtimer/app.js +++ b/apps/tevtimer/app.js @@ -952,7 +952,6 @@ class DeleteTimer { back: () => { this.back(false, this.timer); } }, 'Delete': () => { - ok = true; tt.set_timers_dirty(); this.back(true, tt.delete_timer(tt.TIMERS, this.timer)); }, From 2392a1b5acfd121df376e7c39b985d38910d571c Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Sun, 4 May 2025 22:35:30 -0500 Subject: [PATCH 48/67] Avoid button action trigger on long-press to exit app --- apps/tevtimer/app.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js index ef4b38029..959165fbd 100644 --- a/apps/tevtimer/app.js +++ b/apps/tevtimer/app.js @@ -225,7 +225,7 @@ class TimerView { this.listeners.button = setWatch( () => { this.dispatch_action(tt.SETTINGS.button_act); }, BTN, - {edge: 'rising', debounce: 50, repeat: true} + {edge: 'falling', debounce: 50, repeat: true} ); // Tap handler @@ -633,7 +633,7 @@ class TimerFormatView { this.listeners.button = setWatch( this.cancel.bind(this), BTN, - {edge: 'rising', debounce: 50, repeat: true} + {edge: 'falling', debounce: 50, repeat: true} ); } From 36b593e79754e5acbdf8698b7f3569bc11bd0c4a Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Mon, 5 May 2025 14:16:42 -0500 Subject: [PATCH 49/67] Add option to return to either default app or timer on alarm --- apps/tevtimer/alarm.js | 3 +-- apps/tevtimer/app.js | 8 ++++++++ apps/tevtimer/lib.js | 1 + 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/tevtimer/alarm.js b/apps/tevtimer/alarm.js index c635f2640..c9c81e037 100644 --- a/apps/tevtimer/alarm.js +++ b/apps/tevtimer/alarm.js @@ -104,8 +104,7 @@ function showAlarm(alarm) { // so writing to array writes changes back directly require("sched").setAlarms(alarms); - // Load `tevtimer` app upon halt, else the default (clock) app - if (action === 'halt') { + if (action === 'halt' || tt.SETTINGS.alarm_return) { load('tevtimer.app.js'); } else { load(); diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js index 959165fbd..220209374 100644 --- a/apps/tevtimer/app.js +++ b/apps/tevtimer/app.js @@ -1268,6 +1268,14 @@ class AppSettingsMenu { tt.set_settings_dirty(); } }, + 'On alarm go to': { + value: tt.SETTINGS.alarm_return, // boolean + format: v => v ? 'Timer' : 'Clock', + onchange: v => { + tt.SETTINGS.alarm_return = v; + tt.set_settings_dirty(); + } + }, }; E.showMenu(menu); diff --git a/apps/tevtimer/lib.js b/apps/tevtimer/lib.js index ed6456271..6bf5b370e 100644 --- a/apps/tevtimer/lib.js +++ b/apps/tevtimer/lib.js @@ -344,6 +344,7 @@ const SETTINGS = Object.assign({ 'right_tap_act': 'edit_start', 'confirm_reset': 'auto', 'confirm_delete': true, + 'alarm_return': false, }, Storage.readJSON(SETTINGS_FILENAME, true) || {}); var TIMERS = load_timers(); From 48495a7926cc4aece56d6d9ee3a2d506f71ab712 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Tue, 6 May 2025 21:59:49 -0500 Subject: [PATCH 50/67] More tweaks to try to fix alarm handling --- apps/tevtimer/alarm.js | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/apps/tevtimer/alarm.js b/apps/tevtimer/alarm.js index c9c81e037..5a65d20a1 100644 --- a/apps/tevtimer/alarm.js +++ b/apps/tevtimer/alarm.js @@ -31,7 +31,10 @@ function showAlarm(alarm) { chainTimer.start(); tt.set_last_viewed_timer(chainTimer); isChainedTimer = true; + + // Update system alarm list tt.update_system_alarms(); + alarms = require("sched").getAlarms(); } else { console.warn("tevtimer: unable to find chained timer with ID " + timer.chain_id); } @@ -73,30 +76,20 @@ function showAlarm(alarm) { let currentTime = (time.getHours()*3600000)+(time.getMinutes()*60000)+(time.getSeconds()*1000); alarm.t = currentTime + settings.defaultSnoozeMillis; alarm.t %= 86400000; + Bangle.emit("alarmSnooze", alarm); } if (action === 'ok' || action === 'halt') { - // Don't do timer deletions here; this is handled by the - // tevtimer library code (and it may rearrange the alarm indeces - // in the process) - - if (alarm.date && alarm.rp) { - setNextRepeatDate(alarm); - } else if (!alarm.timer) { - alarm.last = new Date().getDate(); - } - if (alarm.ot !== undefined) { - alarm.t = alarm.ot; - delete alarm.ot; - } - if (!alarm.rp) { - alarm.on = false; + let index = alarms.indexOf(alarm); + if (index !== -1) { + alarms.splice(index, 1); } } if (action === 'halt') { timer.pause(); chainTimer.pause(); tt.update_system_alarms(); + alarms = require("sched").getAlarms(); } Bangle.emit("alarmDismiss", alarm); From bfec737806f6225b59d53630c3d45505b224e3e0 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Tue, 6 May 2025 22:00:29 -0500 Subject: [PATCH 51/67] Enable auto-snooze for timer alarms --- apps/tevtimer/lib.js | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/tevtimer/lib.js b/apps/tevtimer/lib.js index 6bf5b370e..0f187ca72 100644 --- a/apps/tevtimer/lib.js +++ b/apps/tevtimer/lib.js @@ -450,6 +450,7 @@ function set_system_alarms() { timer: time_to_next_alarm, msg: '', js: "load('tevtimer.alarm.js');", + as: true, // Allow auto-snooze if not immediately dismissed }); } } From b7ab9da74a83ccd71bc3ab7bd6a83d3fbb0b2a75 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Wed, 7 May 2025 19:39:53 -0500 Subject: [PATCH 52/67] Replace deprecated substr() with slice() --- apps/tevtimer/lib.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/tevtimer/lib.js b/apps/tevtimer/lib.js index 0f187ca72..c3686cbaa 100644 --- a/apps/tevtimer/lib.js +++ b/apps/tevtimer/lib.js @@ -187,9 +187,9 @@ function format_duration(msec, have_seconds) { } const time = Time_utils.decodeTime(msec); time.h += time.d * 24; - let str = time.h + ":" + ("0" + time.m).substr(-2); + let str = time.h + ":" + ("0" + time.m).slice(-2); if (have_seconds) { - str += ":" + ("0" + time.s).substr(-2); + str += ":" + ("0" + time.s).slice(-2); } return str; } From 1d1e37acd6cb4eedc6287387188dddb8a785dcb0 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Wed, 7 May 2025 19:40:18 -0500 Subject: [PATCH 53/67] Depending on setting, stop or reset timer when alarm dismissed --- apps/tevtimer/alarm.js | 15 +++++++++++---- apps/tevtimer/app.js | 7 +++++++ apps/tevtimer/lib.js | 1 + 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/apps/tevtimer/alarm.js b/apps/tevtimer/alarm.js index 5a65d20a1..5aa9654ff 100644 --- a/apps/tevtimer/alarm.js +++ b/apps/tevtimer/alarm.js @@ -24,8 +24,9 @@ function showAlarm(alarm) { // If there's a timer chained from this one, start it (only for // alarms not in snoozed status) var isChainedTimer = false; + var chainTimer = null; if (timer.chain_id !== null && alarm.ot === undefined) { - var chainTimer = tt.TIMERS[tt.find_timer_by_id(timer.chain_id)]; + chainTimer = tt.TIMERS[tt.find_timer_by_id(timer.chain_id)]; if (chainTimer !== undefined) { chainTimer.reset(); chainTimer.start(); @@ -84,13 +85,19 @@ function showAlarm(alarm) { if (index !== -1) { alarms.splice(index, 1); } + if (timer !== chainTimer) { + timer.pause(); + if (tt.SETTINGS.auto_reset) { + timer.reset(); + } + } } if (action === 'halt') { - timer.pause(); chainTimer.pause(); - tt.update_system_alarms(); - alarms = require("sched").getAlarms(); } + tt.update_system_alarms(); + alarms = require("sched").getAlarms(); + Bangle.emit("alarmDismiss", alarm); // The updated alarm is still a member of 'alarms' diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js index 220209374..83b2e51fe 100644 --- a/apps/tevtimer/app.js +++ b/apps/tevtimer/app.js @@ -1276,6 +1276,13 @@ class AppSettingsMenu { tt.set_settings_dirty(); } }, + 'Auto reset': { + value: tt.SETTINGS.auto_reset, // boolean + onchange: v => { + tt.SETTINGS.auto_reset = v; + tt.set_settings_dirty(); + } + } }; E.showMenu(menu); diff --git a/apps/tevtimer/lib.js b/apps/tevtimer/lib.js index c3686cbaa..5fcaba3d5 100644 --- a/apps/tevtimer/lib.js +++ b/apps/tevtimer/lib.js @@ -345,6 +345,7 @@ const SETTINGS = Object.assign({ 'confirm_reset': 'auto', 'confirm_delete': true, 'alarm_return': false, + 'auto_reset': false, }, Storage.readJSON(SETTINGS_FILENAME, true) || {}); var TIMERS = load_timers(); From c6afc5c1cd48b201c12fe08ab6a8c5681b854414 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Mon, 12 May 2025 17:01:21 -0500 Subject: [PATCH 54/67] Fix issues with alarm operation, especially snoozing --- apps/tevtimer/alarm.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/apps/tevtimer/alarm.js b/apps/tevtimer/alarm.js index 5aa9654ff..1b08b49b2 100644 --- a/apps/tevtimer/alarm.js +++ b/apps/tevtimer/alarm.js @@ -21,6 +21,16 @@ function showAlarm(alarm) { } let message = timer.display_name() + '\n' + alarm.msg; + // Altering alarms from here is tricky. Making changes to timers + // requires calling tt.update_system_alarms() to update the system + // alarm list to reflect the new timer state. But that means we need + // to retrieve the alarms again from sched.getAlarms() before + // changing them ourselves or else we risk overwriting the changes. + // Likewise, after directly modifying alarms, we need to write them + // back with sched.setAlarms() before doing anything that will call + // tt.update_system_alarms(), or else the latter will work with an + // outdated list of alarms. + // If there's a timer chained from this one, start it (only for // alarms not in snoozed status) var isChainedTimer = false; @@ -77,6 +87,7 @@ function showAlarm(alarm) { let currentTime = (time.getHours()*3600000)+(time.getMinutes()*60000)+(time.getSeconds()*1000); alarm.t = currentTime + settings.defaultSnoozeMillis; alarm.t %= 86400000; + require("sched").setAlarms(alarms); Bangle.emit("alarmSnooze", alarm); } @@ -84,6 +95,7 @@ function showAlarm(alarm) { let index = alarms.indexOf(alarm); if (index !== -1) { alarms.splice(index, 1); + require("sched").setAlarms(alarms); } if (timer !== chainTimer) { timer.pause(); @@ -100,8 +112,6 @@ function showAlarm(alarm) { Bangle.emit("alarmDismiss", alarm); - // The updated alarm is still a member of 'alarms' - // so writing to array writes changes back directly require("sched").setAlarms(alarms); if (action === 'halt' || tt.SETTINGS.alarm_return) { From beeb0669edd25a8070e8b797906f35ec43441242 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Mon, 12 May 2025 17:02:09 -0500 Subject: [PATCH 55/67] Add README and app screenshot --- apps/tevtimer/README.md | 115 +++++++++++++++++++++++++++++++++++ apps/tevtimer/metadata.json | 2 + apps/tevtimer/screenshot.png | Bin 0 -> 3277 bytes 3 files changed, 117 insertions(+) create mode 100644 apps/tevtimer/README.md create mode 100644 apps/tevtimer/screenshot.png diff --git a/apps/tevtimer/README.md b/apps/tevtimer/README.md new file mode 100644 index 000000000..a88073660 --- /dev/null +++ b/apps/tevtimer/README.md @@ -0,0 +1,115 @@ +# tev's timer + +This Bangle.js 2 app aims to be an ergonomic timer that features: + +* Large, easy-to-read fonts + +* Multiple simultaneous timer operation + +* Interval and repeat timers + +* A customizable three-line display + +![App screenshot](screenshot.png) + +## Basic usage and controls + +The main timer screen appears when you start the app. The button on the lower-left of the screen starts or stops the timer. The lower-right button provides a menu for all other functions. If you have created more than one timer, you can swipe left or right above the buttons to quickly switch between them. + +## Timer menu + +The on-screen menu button displays the following menu items: + +* **Reset:** Reset the currently displayed timer back to its starting value. + +* **Timers:** Select a different timer to display. The most recently used timer is automatically moved to the top of the list. + +* **Edit:** Edit the currently displayed timer + +* **Format:** Customize the timer display + +* **Add:** Create a new timer with the same parameters as the currently displayed one. The Edit menu will appear allowing you to adjust the newly created timer. + +* **Delete:** Delete the currently displayed timer + +* **Settings:** Adjust global app settings + +## Editing a timer + +The following parameters can be adjusted individually for each timer by displaying that timer on the main screen and then selecting **Edit** from the menu: + +* **Name:** (available when a keyboard app is installed) Allows you to assign a custom name to the timer to display in menus + +* **Start:** Set the starting time of the timer + +* **At end:** Allows for creating interval or repeat timers. Selecting “Stop” causes the timer to simply stop when it expires and the resulting alert is dismissed. Selecting a timer here will cause the selected timer to reset and automatically begin counting down once this timer expires. See “Chained timers” below. + +* **Vibrate pattern:** Choose the vibration pattern for the alert when the timer expires + +* **Buzz count:** Choose the number of times the vibration pattern signals when the timer expires before silencing itself + +## Chained timers + +When a timer reaches its end, it can be configured to automatically start another timer, forming a chain of timers. For instance, if you create a timer A and a timer B, you can set timer A's **at end** setting to point to timer B. Then when timer A expires, timer B will automatically start. You can then edit timer B's **at end** setting to auto-start yet another timer, and so on. This procedure can be used to create an interval timer. You can also chain a timer to itself to create a repeating timer. If you set timer A's **at end** setting to timer A itself, timer A will repeat indefinitely, sounding an alert each time it expires, until you manually pause the timer. You can furthermore chain a series of timers back to itself to create a repeating set of intervals: timer A to timer B to timer C back to timer A, for instance. + +## Display format + +Selecting Format from the menu allows you to customize the display. The display has three lines, and each one can be set to one of the following general modes: + +* **Current:** Shows the current timer position as it counts down + +* **Start:** Shows the starting point of the timer + +* **Time:** Shows the current time of day + +* **Name:** Shows the name of the timer (if set by a keyboard app; otherwise displays an auto-generated name based on the timer's starting point and current position) + +The Current, Start, and Time modes each have three subtypes allowing you to set the precision of the displayed time: + +* **HMS:** Hours, minutes, and seconds + +* **HM:** Hours and minutes only + +* **Auto:** Displays only hours and minutes while the screen is locked; when unlocked, automatically displays the seconds too + +The primary benefit to choosing a mode that hides the seconds is to reduce battery consumption when the timer is being displayed for an extended period of time. + +## App settings + +The Settings option in the menu contains the following options which apply to all timers or to the app as a whole. + +### Button, Tap left, and Tap right + +Choose a shortcut action to perform when the physical button is pressed (after the screen is unlocked), when the left side of the touch screen (above the buttons when the main time screen is displayed) is tapped, and when the right side of the touch screen is tapped, respectively. By default, pressing the button toggles the timer between running and paused, and tapping either side of the screen brings up the screen for setting the starting time of the timer. These actions can be customized: + +* **Start/stop:** Toggle the timer between paused and running + +* **Reset:** Reset the timer + +* **Timers:** Display the timer selection menu to display a different timer + +* **Edit timer:** Display the timer edit menu + +* **Edit start:** Display the timer start time edit screen + +* **Format:** Display the display format selection screen + +### Confirm reset + +Normally when you choose to reset a timer, a menu prompts you to confirm the reset if the timer has not expired yet (the Auto option). This helps protect against accidentally resetting the timer. If you prefer to always see this confirmation menu, choose Always; if you would rather reset always happen instantly, choose Never. + +### Confirm delete + +Likewise, to protect against accidentally deleting a timer a confirmation menu appears when you select Delete from the menu. Setting this option to Never eliminates this extra step. + +### On alarm go to + +If set to Clock (default), when a timer expires and its alert is displayed, dismissing the alert will return to the default app (normally the preferred clock app). Setting this option to Timer will automatically restart the Tev Timer app instead. + +### Auto reset + +When a timer expires, it will by default begin counting up a time that represents the amount of time passed before its alarm was acknowledged. If you check this option, the timer will be reset back to its starting point instead, saving you the trouble of doing so manually before using the timer again. + +## Timer alerts + +When a timer expires, it will display an alert like the ones produced by the standard Scheduler app. For a timer that is not chained, or the last timer in a chain, two buttons “OK” and “Snooze” appear when an alert fires. “OK” completely dismisses the alert, while “Snooze” temporarily dismisses it (it will recur after the snooze interval configured in the Scheduler settings). For chained timers, the options are instead “OK” and “Halt”. “OK” dismisses the individual alert while allowing the next chained timer to continue running in the background, eventually sounding its alert. “Halt” stops the timer and cancels the chaining action, and the display will return to the timer that was running at the point when the “Halt” button was tapped. If you accidentally create a repeating timer that alerts too frequently and makes it impossible to use the Bangle.js watch normally, quickly tap the “Halt” button to stop the chaining action and regain control. diff --git a/apps/tevtimer/metadata.json b/apps/tevtimer/metadata.json index 3cb69bc26..fa62729fd 100644 --- a/apps/tevtimer/metadata.json +++ b/apps/tevtimer/metadata.json @@ -5,6 +5,8 @@ "icon": "app.png", "version": "0.01", "description": "An ergonomic countdown timer app", + "screenshots": [ {"url": "screenshot.png" } ], + "readme": "README.md", "tags": "timer", "supports": ["BANGLEJS2"], "dependencies": {"scheduler": "type"}, diff --git a/apps/tevtimer/screenshot.png b/apps/tevtimer/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..59bdbb26f99b00180405098ded556080a13d42df GIT binary patch literal 3277 zcma)9dpOhW8~<)BZ=(n!}tjZI=% za?WYV7-5kd!kpT)iQl?@fBpXZJec8N@!QJQM(ceaN~>7nXzT_ecCDvlmLP8yL_AJJe5CXZ4!sq)ztJSp0Kyib?JwY=M( zI{n0%Z`1>_A|(aB@q30je3SXHQ*XoeNfee+C9^HYWM}~%2hSof#kG7m2&qv_ji3*d z`_XfuytRK_B8TpWe{$k~H_5{>*z5XNcBK2C@LMzN8w*3~$T`#WBuEal~9j3H2$b;ngbWQfSQ=>%o% z$2=XQ6s7U@$2op0Y30uJbJ~%ABwx zsD3da+opl{ef%}cUYe$Zg9>HQ*}C;=wcWWHTTmj*?7)p@%he!wbcDJ3 z8q8WJG!UE;st2mk+?%aXRPjOUIktYn#P#ctzm#1LQy*_}3>&U0fh?U6@*(sgZ8P*h zy7*l;ycbPr{y@@Xl;8xZx$*<&VIZG4hS+EtH4t@l;74kPqRsG$ODGkfFUvuB#}u zsN8CrXj4_xsV}cdp6&|ugm?%hwYv)2 z^jwPa`fWm=)=TV6iBPKkdn7n8T=hbd$u#`bS7z(w81$7o=(G=T5%yxKsu~ZVZ8}{L zTKJqZY~H)QtRIohH|~LP3!7ESFaBR@Vh`qn=%`n4@>T$P(r#n(ZTaB%qc)@G{)E#n zAC&x(0X&-@C$uH;nnh`JxURuQfFAs*f(!o!UQU8U_^{eNYMY$Y(cb%R#y?=hUo==E zj0iP+g14gSu90HAmr^sOle?#Gn&9Rd6m|u9jrb#I#oDsChn3nwtMJ3cRHm zxpZLPc)Ay}LNB7FJo9`0)GN15lV9b20qc$2#?z3%_8%!1@^S2D-R?1-M`vwfKd-jm zrfGK;@FCQ4umi^i@cuEJyH!B$Gubyxd*4v@vWG7dL*l#=N|hw4zJ!@|lniU+Jswgo zUNPZ`EjwK|ln-c=CSQ-nr|aiwU8ia(jSe3Cq;wj|z?lwEsKY2g+!!x>g#v(cI~5kF z4aJr1^BRwd&Zk{bn7I@7m`^W1gv0tGL#p4#QwE+Z*9y~+ncmL1&=s;=hPYE6I9U?ATSfJ!twZ!!A`OQEeXGmlzJdZ z7&D1628tC_2Q0mC8SKe4!7H(U#k?hXww*%L!80X=c(=>MeUeg9P$QC0iNO>@!H-Tvtkl<1L%0z3r* zd;vpm(}FPP`+W?jubIANmZed_Bf3m$BVqM$t2a3#y=<`@S^ux2#!Vb!~dm_|C$QRBmeRiWimUrBqaS{LvVs2G}{P;_r45R85h%uh+&n z#!I#+GiF?E-Nf?hH%J`fCAqq95C4)1BgUo%LPjH*WR1jspTf+Z1$h55HcRJSob9w| zx%*jA5c;mq)JT-kSd;13SNWJ^Jt*eJb{G&SbQzs}nOC#ZUnPO<^y;!UL3}%Z>uX6w zOY5tfXEJucxo6XVB_tUBD0v7_N{_8rKv?u6P(oWm8^?ZUaA$iFt_F)sN5OKt&FP&_ z#AJ{ZWuv|!DM`?FOG1G{h*R{dfqH1@>9@0`eIL2V!)8X=?Lp7RVZ$Gdxfvc6XT%;e ztj~i2Rh_^_o#;wKvTi%tpT9-<-PP4oQQ7MuJe7oKWRN_jt{G zQYl!3^R{mxSNqx-RYM;)h?z?i+!l5L24}yBZjDT=ho%;&Erj!@z?rD{48>0c=SAkc zoXctaf^g}cJvA$o-N`WQ@^5BmQ?qgbcJS9rDSokhaV(0%CH(m&Gr-r5q&+i#>u__6 z!MktR5gi@ZN%;bFJR&Q~eXmniQ$wP*^QRqj^5{IpNMdPvG^Wv(YW< zyq%zCvdua+jO(73s(D>2{8QR=FGG>JF`w%lKM+2hHa0pW8gis&C75pM6m1>xJ6Tjl zz8k@T!=+Y^y#0ZO^v#i=p;iP8_d?LD6PFgGq(_r0IjW^ZkyN!PhNj{dxF46Ok8WCr zz9^2($v;s@mATsB0e-6TeR3bOj#7-f5Z!ggv_F*JrKE5xnm`Q@Pvv-*T18*+;ohg8 zil5#XYgkO+`OF0m(-)N&j4Q##R@&xRhv=~S631xMfGO{zgRqVeaa6sVMY${aZp7yy z9YgcQC*Dqp-avY3hCP8 z-F{8fPN-HZbPJocx?`+4k&&?5z%^f-Lmi)JrAideuZ82l4-am=PFQ$Am+4+|dA}vz YD+GtfTn{c5^e2F`;~9sl6PSem0OsvfYybcN literal 0 HcmV?d00001 From 343839e742b6368330af4d37f2fa4e1fd51707d6 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Mon, 12 May 2025 19:05:00 -0500 Subject: [PATCH 56/67] Adjust Markdown formatting --- apps/tevtimer/README.md | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/apps/tevtimer/README.md b/apps/tevtimer/README.md index a88073660..ed57699c9 100644 --- a/apps/tevtimer/README.md +++ b/apps/tevtimer/README.md @@ -3,11 +3,8 @@ This Bangle.js 2 app aims to be an ergonomic timer that features: * Large, easy-to-read fonts - * Multiple simultaneous timer operation - * Interval and repeat timers - * A customizable three-line display ![App screenshot](screenshot.png) @@ -21,17 +18,11 @@ The main timer screen appears when you start the app. The button on the lower-le The on-screen menu button displays the following menu items: * **Reset:** Reset the currently displayed timer back to its starting value. - * **Timers:** Select a different timer to display. The most recently used timer is automatically moved to the top of the list. - * **Edit:** Edit the currently displayed timer - * **Format:** Customize the timer display - * **Add:** Create a new timer with the same parameters as the currently displayed one. The Edit menu will appear allowing you to adjust the newly created timer. - * **Delete:** Delete the currently displayed timer - * **Settings:** Adjust global app settings ## Editing a timer @@ -39,13 +30,9 @@ The on-screen menu button displays the following menu items: The following parameters can be adjusted individually for each timer by displaying that timer on the main screen and then selecting **Edit** from the menu: * **Name:** (available when a keyboard app is installed) Allows you to assign a custom name to the timer to display in menus - * **Start:** Set the starting time of the timer - * **At end:** Allows for creating interval or repeat timers. Selecting “Stop” causes the timer to simply stop when it expires and the resulting alert is dismissed. Selecting a timer here will cause the selected timer to reset and automatically begin counting down once this timer expires. See “Chained timers” below. - * **Vibrate pattern:** Choose the vibration pattern for the alert when the timer expires - * **Buzz count:** Choose the number of times the vibration pattern signals when the timer expires before silencing itself ## Chained timers @@ -57,19 +44,14 @@ When a timer reaches its end, it can be configured to automatically start anothe Selecting Format from the menu allows you to customize the display. The display has three lines, and each one can be set to one of the following general modes: * **Current:** Shows the current timer position as it counts down - * **Start:** Shows the starting point of the timer - * **Time:** Shows the current time of day - * **Name:** Shows the name of the timer (if set by a keyboard app; otherwise displays an auto-generated name based on the timer's starting point and current position) The Current, Start, and Time modes each have three subtypes allowing you to set the precision of the displayed time: * **HMS:** Hours, minutes, and seconds - * **HM:** Hours and minutes only - * **Auto:** Displays only hours and minutes while the screen is locked; when unlocked, automatically displays the seconds too The primary benefit to choosing a mode that hides the seconds is to reduce battery consumption when the timer is being displayed for an extended period of time. @@ -83,15 +65,10 @@ The Settings option in the menu contains the following options which apply to al Choose a shortcut action to perform when the physical button is pressed (after the screen is unlocked), when the left side of the touch screen (above the buttons when the main time screen is displayed) is tapped, and when the right side of the touch screen is tapped, respectively. By default, pressing the button toggles the timer between running and paused, and tapping either side of the screen brings up the screen for setting the starting time of the timer. These actions can be customized: * **Start/stop:** Toggle the timer between paused and running - * **Reset:** Reset the timer - * **Timers:** Display the timer selection menu to display a different timer - * **Edit timer:** Display the timer edit menu - * **Edit start:** Display the timer start time edit screen - * **Format:** Display the display format selection screen ### Confirm reset From 3d4ba6b55c03206b6e3ff2145b71da1d65bc68e9 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Mon, 12 May 2025 19:05:14 -0500 Subject: [PATCH 57/67] Adjust app description --- apps/tevtimer/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/tevtimer/metadata.json b/apps/tevtimer/metadata.json index fa62729fd..a0860c748 100644 --- a/apps/tevtimer/metadata.json +++ b/apps/tevtimer/metadata.json @@ -4,7 +4,7 @@ "shortName":"tev's timer", "icon": "app.png", "version": "0.01", - "description": "An ergonomic countdown timer app", + "description": "A countdown timer app with interval and repeat features", "screenshots": [ {"url": "screenshot.png" } ], "readme": "README.md", "tags": "timer", From 1626615bd5c851862cbda11fad4b9a05c67a4c06 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Wed, 14 May 2025 14:25:42 -0500 Subject: [PATCH 58/67] Remove unused code --- apps/tevtimer/alarm.js | 37 +------------------------------------ 1 file changed, 1 insertion(+), 36 deletions(-) diff --git a/apps/tevtimer/alarm.js b/apps/tevtimer/alarm.js index 1b08b49b2..3b48e24d2 100644 --- a/apps/tevtimer/alarm.js +++ b/apps/tevtimer/alarm.js @@ -54,10 +54,7 @@ function showAlarm(alarm) { if (alarm.msg) { message += "\n" + alarm.msg; } else { - message = (alarm.timer - ? atob("ACQswgD//33vRcGHIQAAABVVVAAAAAAAABVVVAAAAAAAABVVVAAAAAAAABVVVAAAAAAAABVVVAAAAAAAABVVVAAAAAAAAAP/wAAAAAAAAAP/wAAAAAAAAAqqoAPAAAAAAqqqqoP8AAAAKqqqqqv/AAACqqqqqqq/wAAKqqqlWqqvwAAqqqqlVaqrAACqqqqlVVqqAAKqqqqlVVaqgAKqaqqlVVWqgAqpWqqlVVVqoAqlWqqlVVVaoCqlV6qlVVVaqCqVVfqlVVVWqCqVVf6lVVVWqKpVVX/lVVVVqqpVVV/+VVVVqqpVVV//lVVVqqpVVVfr1VVVqqpVVVfr1VVVqqpVVVb/lVVVqqpVVVW+VVVVqqpVVVVVVVVVqiqVVVVVVVVWqCqVVVVVVVVWqCqlVVVVVVVaqAqlVVVVVVVaoAqpVVVVVVVqoAKqVVVVVVWqgAKqlVVVVVaqgACqpVVVVVqqAAAqqlVVVaqoAAAKqqVVWqqgAAACqqqqqqqAAAAAKqqqqqgAAAAAAqqqqoAAAAAAAAqqoAAAAA==") - : atob("AC0swgF97///RcEpMlVVVVVVf9VVVVVVVVX/9VVf9VVf/1VVV///1Vf9VX///VVX///VWqqlV///1Vf//9aqqqqpf//9V///2qqqqqqn///V///6qqqqqqr///X//+qqoAAKqqv//3//6qoAAAAKqr//3//qqAAAAAAqq//3/+qoAADwAAKqv/3/+qgAADwAACqv/3/aqAAADwAAAqp/19qoAAADwAAAKqfV1qgAAADwAAACqXVWqgAAADwAAACqlVWqAAAADwAAAAqlVWqAAAADwAAAAqlVWqAAAADwAAAAqlVaoAAAADwAAAAKpVaoAAAADwAAAAKpVaoAAAADwAAAAKpVaoAAAAOsAAAAKpVaoAAAAOsAAAAKpVaoAAAAL/AAAAKpVaoAAAAgPwAAAKpVaoAAACAD8AAAKpVWqAAAIAA/AAAqlVWqAAAgAAPwAAqlVWqAACAAADwAAqlVWqgAIAAAAAACqlVVqgAgAAAAAACqVVVqoAAAAAAAAKqVVVaqAAAAAAAAqpVVVWqgAAAAAACqlVVVWqoAAAAAAKqlVVVVqqAAAAAAqqVVVVVaqoAAAAKqpVVVVVeqqoAAKqqtVVVVV/6qqqqqqr/VVVVX/2qqqqqqn/1VVVf/VaqqqqpV/9VVVf9VVWqqlVVf9VVVf1VVVVVVVVX9VQ==") - ) + " " + message + message = atob("ACQswgD//33vRcGHIQAAABVVVAAAAAAAABVVVAAAAAAAABVVVAAAAAAAABVVVAAAAAAAABVVVAAAAAAAABVVVAAAAAAAAAP/wAAAAAAAAAP/wAAAAAAAAAqqoAPAAAAAAqqqqoP8AAAAKqqqqqv/AAACqqqqqqq/wAAKqqqlWqqvwAAqqqqlVaqrAACqqqqlVVqqAAKqqqqlVVaqgAKqaqqlVVWqgAqpWqqlVVVqoAqlWqqlVVVaoCqlV6qlVVVaqCqVVfqlVVVWqCqVVf6lVVVWqKpVVX/lVVVVqqpVVV/+VVVVqqpVVV//lVVVqqpVVVfr1VVVqqpVVVfr1VVVqqpVVVb/lVVVqqpVVVW+VVVVqqpVVVVVVVVVqiqVVVVVVVVWqCqVVVVVVVVWqCqlVVVVVVVaqAqlVVVVVVVaoAqpVVVVVVVqoAKqVVVVVVWqgAKqlVVVVVaqgACqpVVVVVqqAAAqqlVVVaqoAAAKqqVVWqqgAAACqqqqqqqAAAAAKqqqqqgAAAAAAqqqqoAAAAAAAAqqoAAAAA==") + " " + message } Bangle.loadWidgets(); @@ -142,38 +139,6 @@ function showAlarm(alarm) { }); } - function setNextRepeatDate(alarm) { - // Handle repeating alarms - // This is not used in tevtimer - - let date = new Date(alarm.date); - let rp = alarm.rp; - if (rp===true) { // fallback in case rp is set wrong - date.setDate(date.getDate() + 1); - } else switch(rp.interval) { // rp is an object - case "day": - date.setDate(date.getDate() + rp.num); - break; - case "week": - date.setDate(date.getDate() + (rp.num * 7)); - break; - case "month": - if (!alarm.od) alarm.od = date.getDate(); - date = new Date(date.getFullYear(), date.getMonth() + rp.num, alarm.od); - if (date.getDate() != alarm.od) date.setDate(0); - break; - case "year": - if (!alarm.od) alarm.od = date.getDate(); - date = new Date(date.getFullYear() + rp.num, date.getMonth(), alarm.od); - if (date.getDate() != alarm.od) date.setDate(0); - break; - default: - console.log(`sched: unknown repeat '${JSON.stringify(rp)}'`); - break; - } - alarm.date = date.toLocalISOString().slice(0,10); - } - if ((require("Storage").readJSON("setting.json", 1) || {}).quiet > 1) return; From 7132dc8ce2194f28c56c64b44451da2205d3bc3d Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Sat, 17 May 2025 15:30:09 -0500 Subject: [PATCH 59/67] Prototype for web interface timer editor --- apps/tevtimer/interface.html | 448 +++++++++++++++++++++++++++++++++++ apps/tevtimer/metadata.json | 1 + 2 files changed, 449 insertions(+) create mode 100644 apps/tevtimer/interface.html diff --git a/apps/tevtimer/interface.html b/apps/tevtimer/interface.html new file mode 100644 index 000000000..8a10fac2f --- /dev/null +++ b/apps/tevtimer/interface.html @@ -0,0 +1,448 @@ + + + + + + + + + + +
Loading...
+ + + + \ No newline at end of file diff --git a/apps/tevtimer/metadata.json b/apps/tevtimer/metadata.json index a0860c748..8ea7226f2 100644 --- a/apps/tevtimer/metadata.json +++ b/apps/tevtimer/metadata.json @@ -8,6 +8,7 @@ "screenshots": [ {"url": "screenshot.png" } ], "readme": "README.md", "tags": "timer", + "interface": "interface.html", "supports": ["BANGLEJS2"], "dependencies": {"scheduler": "type"}, "storage": [ From 5724b0d8e33863b4fdd9cdada5dc9403f7712202 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Sat, 17 May 2025 18:58:55 -0500 Subject: [PATCH 60/67] Variable name & cleanup --- apps/tevtimer/interface.html | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/tevtimer/interface.html b/apps/tevtimer/interface.html index 8a10fac2f..2c75b593d 100644 --- a/apps/tevtimer/interface.html +++ b/apps/tevtimer/interface.html @@ -125,6 +125,9 @@ } ]; + const TIMERS_FILE = 'tevtimer.timers.json'; + const MAX_BUZZ_COUNT = 15; + var userTimers = []; function onInit() { @@ -135,7 +138,7 @@ -
+
`; document.getElementById('btn-reload-timers').addEventListener('click', reloadTimers); document.getElementById('btn-add-timer').addEventListener('click', addTimer); @@ -189,7 +192,7 @@ const activeElementId = activeElement ? activeElement.id : null; // Re-render the table - document.getElementById('timertable').innerHTML = timerBlocks(userTimers); + document.getElementById('timerblocks').innerHTML = timerBlocks(userTimers); updateAtEndDropdowns(); // Reattach button handlers @@ -267,7 +270,7 @@
- +
`; From aae5857754123bafb1a77b0f79094a13ef9f4d2a Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Sat, 17 May 2025 19:00:46 -0500 Subject: [PATCH 61/67] Prepare interface.html for live usage with real data --- apps/tevtimer/interface.html | 97 ++---------------------------------- 1 file changed, 5 insertions(+), 92 deletions(-) diff --git a/apps/tevtimer/interface.html b/apps/tevtimer/interface.html index 2c75b593d..bac205b78 100644 --- a/apps/tevtimer/interface.html +++ b/apps/tevtimer/interface.html @@ -55,76 +55,6 @@
Loading...
From cb7204d7008144926f14a54e5c193eeb7a2b8c09 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Sat, 17 May 2025 19:19:55 -0500 Subject: [PATCH 62/67] Defense --- apps/tevtimer/interface.html | 45 +++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/apps/tevtimer/interface.html b/apps/tevtimer/interface.html index 2c75b593d..f2930da5a 100644 --- a/apps/tevtimer/interface.html +++ b/apps/tevtimer/interface.html @@ -186,6 +186,13 @@ return maxId + 1; } + function splitHMS(hms) { + let h = Math.floor(hms / 3600); + let m = Math.floor((hms % 3600) / 60); + let s = Math.floor(hms % 60); + return [h, m, s]; + } + function updateTimerBlocks() { // Track the currently focused element const activeElement = document.activeElement; @@ -235,9 +242,7 @@ console.error('Unsupported timer rate'); continue; } - let h = Math.floor(timer.origin / 3600); - let m = Math.floor((timer.origin % 3600) / 60); - let s = Math.floor(timer.origin % 60); + let [h, m, s] = splitHMS(timer.origin); let atEndTimer = timer.chain_id ? getTimerById(timers, timer.chain_id) : null; let atEndSelected = atEndTimer ? atEndTimer.id : 'null'; @@ -308,16 +313,28 @@ // Update only the At End dropdowns updateAtEndDropdowns(); } else if (type === 'hours' || type === 'minutes' || type === 'seconds') { - let h = parseInt(document.getElementById(`hours-${index}`).value) || 0; - let m = parseInt(document.getElementById(`minutes-${index}`).value) || 0; - let s = parseInt(document.getElementById(`seconds-${index}`).value) || 0; - userTimers[index].origin = h * 3600 + m * 60 + s; + let hInput = document.getElementById(`hours-${index}`); + let mInput = document.getElementById(`minutes-${index}`); + let sInput = document.getElementById(`seconds-${index}`); + let h = parseInt(hInput.value) || 0; + let m = parseInt(mInput.value) || 0; + let s = parseInt(sInput.value) || 0; + userTimers[index].origin = Math.max( + Math.min(h * 3600 + m * 60 + s, 99 * 3600 + 59 * 60 + 59), + 0); + // Normalize the values in case minutes/seconds >59 + [h, m, s] = splitHMS(userTimers[index].origin); + hInput.value = h; + mInput.value = m; + sInput.value = s; } else if (type === 'atend') { userTimers[index].chain_id = value == 'null' ? null : parseInt(value); } else if (type === 'vibrate') { userTimers[index].vibrate_pattern = value; } else if (type === 'buzz') { - userTimers[index].buzz_count = parseInt(value); + userTimers[index].buzz_count = + Math.max(Math.min(MAX_BUZZ_COUNT, parseInt(value)), 0); + event.target.value = userTimers[index].buzz_count; } }); }); @@ -406,11 +423,13 @@ } function saveTimers() { - // Save the timers to storage - console.log(userTimers); - // Util.writeStorageJSON('tevtimer.timers.json', userTimers, () => { - // console.log('Timers saved successfully'); - // }); + if (userTimers.length) { + // (Guard in case the user manages to click Save before + // the timers are loaded, or something like that) + Util.writeStorage(TIMERS_FILE, JSON.stringify(userTimers), () => { + alert('Timers saved successfully.'); + }); + }; } function reloadTimers() { From d10d625cefb0e60aa4941ce85b30ac9981683386 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Sat, 17 May 2025 19:22:54 -0500 Subject: [PATCH 63/67] Clean up comments --- apps/tevtimer/interface.html | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/apps/tevtimer/interface.html b/apps/tevtimer/interface.html index f2930da5a..64a17d56c 100644 --- a/apps/tevtimer/interface.html +++ b/apps/tevtimer/interface.html @@ -285,17 +285,12 @@ } function attachButtonHandlers() { - // Handle "Move up" buttons document.querySelectorAll('.btn-move-up').forEach((button, index) => { button.addEventListener('click', () => moveTimerUp(index + 1)); }); - - // Handle "Move down" buttons document.querySelectorAll('.btn-move-down').forEach((button, index) => { button.addEventListener('click', () => moveTimerDown(index)); }); - - // Handle "Delete" buttons document.querySelectorAll('.btn-delete').forEach((button, index) => { button.addEventListener('click', () => deleteTimer(index)); }); @@ -309,8 +304,6 @@ if (type === 'name') { userTimers[index].name = value; - - // Update only the At End dropdowns updateAtEndDropdowns(); } else if (type === 'hours' || type === 'minutes' || type === 'seconds') { let hInput = document.getElementById(`hours-${index}`); @@ -342,10 +335,8 @@ function moveTimerUp(index) { if (index > 0) { - // Swap the timers [userTimers[index - 1], userTimers[index]] = [userTimers[index], userTimers[index - 1]]; - // Re-render the table updateTimerBlocks(); // Move focus to the new position of the "Move up" button @@ -358,10 +349,8 @@ function moveTimerDown(index) { if (index < userTimers.length - 1) { - // Swap the timers [userTimers[index], userTimers[index + 1]] = [userTimers[index + 1], userTimers[index]]; - // Re-render the table updateTimerBlocks(); // Move focus to the new position of the "Move down" button @@ -424,8 +413,8 @@ function saveTimers() { if (userTimers.length) { - // (Guard in case the user manages to click Save before - // the timers are loaded, or something like that) + // Guard in case the user manages to click Save before + // the timers are loaded, or something like that Util.writeStorage(TIMERS_FILE, JSON.stringify(userTimers), () => { alert('Timers saved successfully.'); }); From a8b27df71309f3554653c78c56302252664cec35 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Sun, 18 May 2025 16:39:55 -0500 Subject: [PATCH 64/67] Fix whitespace --- apps/tevtimer/interface.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/tevtimer/interface.html b/apps/tevtimer/interface.html index 739b11d05..8626c0880 100644 --- a/apps/tevtimer/interface.html +++ b/apps/tevtimer/interface.html @@ -327,7 +327,7 @@ // Move focus to the new timer's Name field document.getElementById(`name-${userTimers.length - 1}`).focus(); } - + function saveTimers() { if (userTimers.length) { // Guard in case the user manages to click Save before @@ -369,4 +369,4 @@ - \ No newline at end of file + From 611816f08c714a7261c7641165fe5b0afd86ebe7 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Sat, 24 May 2025 17:05:15 -0500 Subject: [PATCH 65/67] Back out of timer app if it is running while timers are changed (so they are reloaded properly) --- apps/tevtimer/interface.html | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/tevtimer/interface.html b/apps/tevtimer/interface.html index 8626c0880..625ca4e87 100644 --- a/apps/tevtimer/interface.html +++ b/apps/tevtimer/interface.html @@ -56,6 +56,7 @@