Use Triangle Timer as a basis for new countdown timer app
parent
c74c99a6c3
commit
e11a905171
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("lEo4cC///A4IDC/Hgg/g/8f/H/g9JkmSAQORAgYCEAYcipPPnnzAQoOCpGSgN58+eAQoOComAgIdKHAMAgI7KxJlDBxNIgESBwO69euAQhWBoAOD69duoCEBwKSEDpAaCHZVIyCxFBw+AhIOEtu27YCDQYIONbwwOOHYwODg3AAgSnGBwfbAgUyBokgFIcbsEAgYOKJAOA7YOGiVJBwUN23YHYoOFgE2AQIIBBwZoGAAMf/4ACBxxoDBxSVHHYwOJgEJBxsOYgTgHBwz+HBw+QHZlJiQ8JggOCkQOJg4sBBwNAOIJ0CAQYODklIBxuJSQgEDJQckyK2DBwcJlMiBwWSRIIOFpRHB7YOCpGSYQtSBwoFBpL1BgMkygICBwgCBIggXDA=="))
|
||||||
|
|
@ -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]));
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 819 B |
Binary file not shown.
|
|
@ -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};
|
||||||
|
|
@ -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"}
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue