986 lines
26 KiB
JavaScript
986 lines
26 KiB
JavaScript
const Layout = require('Layout');
|
|
const locale = require('locale');
|
|
const pickers = require('more_pickers');
|
|
|
|
const tt = require('tevtimer');
|
|
|
|
|
|
// UI //
|
|
|
|
// Length of time displaying timer view before moving timer to top of
|
|
// timer list
|
|
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',
|
|
'current hh:mm:ss': '12x20',
|
|
'time hh:mm:ss': '12x20',
|
|
|
|
'start hh:mm': '12x20',
|
|
'current hh:mm': '12x20',
|
|
'time hh:mm': '12x20',
|
|
|
|
'start auto': '12x20',
|
|
'current auto': '12x20',
|
|
'time auto': '12x20',
|
|
|
|
'name': '12x20',
|
|
|
|
'format-menu': '12x20',
|
|
},
|
|
|
|
'row2': {
|
|
'start hh:mm:ss': 'Vector:34x42',
|
|
'current hh:mm:ss': 'Vector:34x42',
|
|
'time hh:mm:ss': 'Vector:24x42',
|
|
|
|
'start hh:mm': 'Vector:48x42',
|
|
'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',
|
|
|
|
'format-menu': 'Vector:26x42',
|
|
},
|
|
|
|
'row3': {
|
|
'start hh:mm:ss': 'Vector:34x56',
|
|
'current hh:mm:ss': 'Vector:34x56',
|
|
'time hh:mm:ss': 'Vector:24x56',
|
|
|
|
'start hh:mm': 'Vector:48x56',
|
|
'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',
|
|
|
|
'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 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',
|
|
];
|
|
|
|
// 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',
|
|
};
|
|
|
|
|
|
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 + '", "' + format + '")');
|
|
return '12x20';
|
|
}
|
|
return font;
|
|
}
|
|
|
|
|
|
function next_time_update(interval, curr_time, direction) {
|
|
// 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;
|
|
}
|
|
|
|
// Find the next time we should update the display
|
|
let next_update = tt.mod(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;
|
|
}
|
|
|
|
|
|
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
|
|
// parameter.
|
|
|
|
constructor(timer) {
|
|
this.timer = timer;
|
|
|
|
this.layout = null;
|
|
this.listeners = {};
|
|
this.listeners.timer_render_timeout = null;
|
|
}
|
|
|
|
start() {
|
|
// Initialize, display, and activate the UI
|
|
|
|
this._initLayout();
|
|
this.layout.update();
|
|
this.layout.clear();
|
|
this.render();
|
|
|
|
// Physical button handler
|
|
this.listeners.button = setWatch(
|
|
this.start_stop_timer.bind(this),
|
|
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);
|
|
switch_UI(new TimerView(tt.TIMERS[
|
|
tt.mod(new_index, tt.TIMERS.length)
|
|
]));
|
|
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);
|
|
|
|
// 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() {
|
|
// 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;
|
|
}
|
|
clearWatch(this.listeners.button);
|
|
Bangle.removeListener('drag', this.listeners.drag);
|
|
clearTimeout(this.listeners.to_top_timeout);
|
|
Bangle.removeListener('lock', this.listeners.lock);
|
|
Bangle.setUI();
|
|
}
|
|
|
|
_initLayout() {
|
|
const layout = new Layout(
|
|
{
|
|
type: 'v',
|
|
bgCol: g.theme.bg,
|
|
c: [
|
|
{
|
|
type: 'txt',
|
|
id: 'row1',
|
|
label: '',
|
|
font: row_font('row1', tt.SETTINGS.format.row1),
|
|
fillx: 1,
|
|
},
|
|
{
|
|
type: 'txt',
|
|
id: 'row2',
|
|
label: '',
|
|
font: row_font('row2', tt.SETTINGS.format.row2),
|
|
fillx: 1,
|
|
},
|
|
{
|
|
type: 'txt',
|
|
id: 'row3',
|
|
label: '',
|
|
font: row_font('row3', tt.SETTINGS.format.row3),
|
|
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.timer));
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
);
|
|
this.layout = layout;
|
|
}
|
|
|
|
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) {
|
|
this.layout.update();
|
|
}
|
|
|
|
if (!item || item == 'timer') {
|
|
|
|
let update_interval = Infinity;
|
|
|
|
for (var id of ROW_IDS) {
|
|
const elem = this.layout[id];
|
|
const running = this.timer.is_running();
|
|
|
|
let format = tt.SETTINGS.format[id];
|
|
// Special handling for “auto” formats
|
|
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 (format == 'start hh:mm:ss') {
|
|
elem.label = tt.format_duration(this.timer.to_msec(this.timer.origin), true);
|
|
|
|
} else if (format == 'current hh:mm:ss') {
|
|
elem.label = tt.format_duration(this.timer.to_msec(), true);
|
|
if (running) {
|
|
update_interval = Math.min(
|
|
update_interval,
|
|
next_time_update(1000, this.timer.to_msec(), this.timer.rate)
|
|
);
|
|
}
|
|
|
|
} 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 (format == 'start hh:mm') {
|
|
elem.label = tt.format_duration(this.timer.to_msec(this.timer.origin), false);
|
|
|
|
} 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
|
|
update_interval = Math.min(
|
|
update_interval,
|
|
next_time_update(60000, this.timer.to_msec(), this.timer.rate)
|
|
);
|
|
}
|
|
|
|
} 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 (format == 'name') {
|
|
elem.label = this.timer.display_name();
|
|
}
|
|
|
|
elem.font = row_font(id, format);
|
|
this.layout.clear(elem);
|
|
this.layout.render(elem);
|
|
}
|
|
|
|
|
|
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) {
|
|
console.debug('Next render update scheduled in ' + update_interval + ' ms');
|
|
this.listeners.timer_render_timeout = setTimeout(
|
|
() => {
|
|
this.listeners.timer_render_timeout = null;
|
|
this.render('timer');
|
|
},
|
|
update_interval
|
|
);
|
|
}
|
|
}
|
|
|
|
if (!item || item == 'status') {
|
|
this.layout.start_btn.label =
|
|
this.timer.is_running() ? 'Pause' : 'Start';
|
|
this.layout.render(this.layout.buttons);
|
|
update_status_widget(this.timer);
|
|
}
|
|
}
|
|
|
|
start_stop_timer() {
|
|
// Start or pause the timer
|
|
|
|
if (this.timer.is_running()) {
|
|
this.timer.pause();
|
|
} else {
|
|
this.timer.start();
|
|
}
|
|
tt.set_timers_dirty();
|
|
this.render('status');
|
|
this.render('timer');
|
|
}
|
|
}
|
|
|
|
|
|
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;
|
|
|
|
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() {
|
|
// Initialize, display, and activate the UI
|
|
|
|
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
|
|
|
|
// 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 - 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') {
|
|
this.decr_format_idx(row_id);
|
|
} else {
|
|
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() {
|
|
// 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);
|
|
}
|
|
|
|
_initLayout() {
|
|
const layout = new Layout(
|
|
{
|
|
type: 'v',
|
|
bgCol: g.theme.bg,
|
|
c: [
|
|
{
|
|
type: 'h',
|
|
c: [
|
|
{
|
|
type: 'custom',
|
|
id: 'row1.prev',
|
|
render: lay => draw_triangle(lay, false),
|
|
width: ARROW_BTN_SIZE,
|
|
height: ARROW_BTN_SIZE,
|
|
},
|
|
{
|
|
type: 'txt',
|
|
id: 'row1',
|
|
label: FORMAT_DISPLAY[FORMAT_MENU[this.format_idx.row1]],
|
|
font: row_font('row1', 'format-menu'),
|
|
fillx: 1,
|
|
},
|
|
{
|
|
type: 'custom',
|
|
id: 'row1.next',
|
|
render: lay => draw_triangle(lay, true),
|
|
width: ARROW_BTN_SIZE,
|
|
height: ARROW_BTN_SIZE,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
type: 'h',
|
|
c: [
|
|
{
|
|
type: 'custom',
|
|
id: 'row2.prev',
|
|
render: lay => draw_triangle(lay, false),
|
|
width: ARROW_BTN_SIZE,
|
|
height: ARROW_BTN_SIZE,
|
|
},
|
|
{
|
|
type: 'txt',
|
|
id: 'row2',
|
|
label: FORMAT_DISPLAY[FORMAT_MENU[this.format_idx.row2]],
|
|
font: row_font('row2', 'format-menu'),
|
|
fillx: 1,
|
|
},
|
|
{
|
|
type: 'custom',
|
|
id: 'row2.next',
|
|
render: lay => draw_triangle(lay, true),
|
|
width: ARROW_BTN_SIZE,
|
|
height: ARROW_BTN_SIZE,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
type: 'h',
|
|
c: [
|
|
{
|
|
type: 'custom',
|
|
id: 'row3.prev',
|
|
render: lay => draw_triangle(lay, false),
|
|
width: ARROW_BTN_SIZE,
|
|
height: ARROW_BTN_SIZE,
|
|
},
|
|
{
|
|
type: 'txt',
|
|
id: 'row3',
|
|
label: FORMAT_DISPLAY[FORMAT_MENU[this.format_idx.row3]],
|
|
font: row_font('row3', 'format-menu'),
|
|
fillx: 1,
|
|
},
|
|
{
|
|
type: 'custom',
|
|
id: 'row3.next',
|
|
render: lay => draw_triangle(lay, true),
|
|
width: ARROW_BTN_SIZE,
|
|
height: ARROW_BTN_SIZE,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
type: 'h',
|
|
id: 'buttons',
|
|
c: [
|
|
{type: 'btn', font: '6x8:2', fillx: 1, label: 'Cancel', id: 'cancel_btn',
|
|
cb: () => { this.cancel(); }
|
|
},
|
|
{type: 'btn', font: '6x8:2', fillx: 1, label: 'OK', id: 'ok_btn',
|
|
cb: () => { this.ok(); }
|
|
},
|
|
]
|
|
}
|
|
]
|
|
}
|
|
);
|
|
this.layout = layout;
|
|
}
|
|
|
|
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);
|
|
this.layout.render(elem);
|
|
}
|
|
|
|
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;
|
|
}
|
|
this.update_row(row_id);
|
|
}
|
|
|
|
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;
|
|
}
|
|
this.update_row(row_id);
|
|
}
|
|
|
|
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]];
|
|
}
|
|
tt.set_settings_dirty();
|
|
switch_UI(new TimerView(this.timer));
|
|
}
|
|
|
|
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(),
|
|
back: this.back.bind(this)
|
|
},
|
|
'Reset': () => { E.showMenu(reset_menu); },
|
|
'Timers': () => {
|
|
switch_UI(new TimerMenu(tt.TIMERS, this.timer));
|
|
},
|
|
'Edit': this.edit_menu.bind(this),
|
|
'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;
|
|
}
|
|
|
|
const reset_menu = {
|
|
'': {
|
|
title: 'Confirm reset',
|
|
back: () => { E.showMenu(top_menu); }
|
|
},
|
|
'Reset': () => {
|
|
this.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_timer(tt.TIMERS, this.timer)));
|
|
},
|
|
'Cancel': () => { E.showMenu(top_menu); },
|
|
};
|
|
|
|
E.showMenu(top_menu);
|
|
}
|
|
|
|
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) {}
|
|
|
|
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.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),
|
|
'Buzz count': {
|
|
value: this.timer.buzz_count,
|
|
min: 0,
|
|
max: 15,
|
|
step: 1,
|
|
wrap: true,
|
|
format: v => v === 0 ? "Forever" : v,
|
|
onchange: v => {
|
|
this.timer.buzz_count = v;
|
|
tt.set_timers_dirty();
|
|
},
|
|
},
|
|
};
|
|
|
|
if (!keyboard) {
|
|
delete edit_menu.Name;
|
|
}
|
|
|
|
E.showMenu(edit_menu);
|
|
}
|
|
|
|
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,
|
|
s: Math.floor(this.timer.origin % 60),
|
|
};
|
|
|
|
function picker_format(v) {
|
|
// Display leading 0 for single digit values in the picker
|
|
return v < 10 ? '0' + v : v;
|
|
}
|
|
|
|
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();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
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",
|
|
back: this.back.bind(this)
|
|
}
|
|
};
|
|
this.timers.forEach((timer) => {
|
|
menu[timer.display_status() + ' ' + timer.display_name()] =
|
|
() => { switch_UI(new TimerView(timer)); };
|
|
});
|
|
E.showMenu(menu);
|
|
}
|
|
}
|
|
|
|
|
|
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();
|
|
}
|
|
CURRENT_UI = new_UI;
|
|
CURRENT_UI.start();
|
|
}
|
|
|
|
|
|
// Load and start up app //
|
|
|
|
Bangle.loadWidgets();
|
|
Bangle.drawWidgets();
|
|
|
|
var CURRENT_UI = null;
|
|
|
|
tt.update_system_alarms();
|
|
|
|
update_status_widget(tt.TIMERS[0]);
|
|
switch_UI(new TimerView(tt.TIMERS[0]));
|