BangleApps/apps/tevtimer/app.js

546 lines
14 KiB
JavaScript

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]));