1325 lines
35 KiB
JavaScript
1325 lines
35 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',
|
|
draw: widget_draw,
|
|
};
|
|
}
|
|
WIDGETS.tevtimer.width = width;
|
|
Bangle.drawWidgets();
|
|
}
|
|
|
|
|
|
// UI modes //
|
|
|
|
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.dispatch_action(tt.SETTINGS.button_act); },
|
|
BTN,
|
|
{edge: 'falling', 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) {
|
|
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('touch', this.listeners.tap);
|
|
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');
|
|
}
|
|
|
|
dispatch_action(action) {
|
|
// Execute a UI action represented by the string `action`.
|
|
|
|
if (action === 'start/stop') {
|
|
this.start_stop_timer()
|
|
|
|
} else if (action === 'reset') {
|
|
switch_UI(
|
|
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)); }
|
|
));
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
class TimerFormatView {
|
|
// 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.
|
|
|
|
this.timer = timer;
|
|
this.back = back || this._back;
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
_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
|
|
|
|
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.cancel.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();
|
|
this.back(true);
|
|
}
|
|
|
|
cancel() {
|
|
// Return to TimerViewMenu without saving changes
|
|
this.back(false);
|
|
}
|
|
}
|
|
|
|
|
|
class TimerViewMenu {
|
|
// 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.
|
|
|
|
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 timer view 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)); },
|
|
'Settings': () => { switch_UI(new AppSettingsMenu(this.timer)); },
|
|
};
|
|
if (tt.TIMERS.length <= 1) {
|
|
// Prevent user deleting last timer
|
|
delete menu.Delete;
|
|
}
|
|
|
|
E.showMenu(menu);
|
|
}
|
|
|
|
stop() {
|
|
// Shut down the UI and clean up listeners and handlers
|
|
|
|
E.showMenu();
|
|
}
|
|
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
_back(ok) {
|
|
// Default back handler
|
|
|
|
if (ok) {
|
|
switch_UI(new TimerView(this.timer));
|
|
} else {
|
|
switch_UI(new TimerViewMenu(this.timer));
|
|
}
|
|
}
|
|
|
|
start() {
|
|
// Display and activate the reset timer confirmation menu if
|
|
// configured in settings, or immediately reset the timer if not.
|
|
|
|
const menu = {
|
|
'': {
|
|
title: 'Confirm reset',
|
|
back: () => { this.back(false); }
|
|
},
|
|
'Reset': () => {
|
|
this.timer.reset();
|
|
tt.set_timers_dirty();
|
|
this.back(true);
|
|
},
|
|
'Cancel': () => { this.back(false); },
|
|
};
|
|
|
|
if (tt.SETTINGS.confirm_reset === true
|
|
|| (tt.SETTINGS.confirm_reset === 'auto'
|
|
&& this.timer.to_msec() > 0)) {
|
|
E.showMenu(menu);
|
|
} else {
|
|
menu.Reset();
|
|
}
|
|
}
|
|
|
|
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 if
|
|
// configured in settings, or immediately delete the timer if
|
|
// not.
|
|
|
|
const menu = {
|
|
'': {
|
|
title: 'Confirm delete',
|
|
back: () => { this.back(false, this.timer); }
|
|
},
|
|
'Delete': () => {
|
|
tt.set_timers_dirty();
|
|
this.back(true, tt.delete_timer(tt.TIMERS, this.timer));
|
|
},
|
|
'Cancel': () => { this.back(false, this.timer) },
|
|
};
|
|
|
|
if (tt.SETTINGS.confirm_delete) {
|
|
E.showMenu(menu);
|
|
} else {
|
|
menu.Delete();
|
|
}
|
|
|
|
}
|
|
|
|
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 menu = {
|
|
'': {
|
|
title: 'Edit: ' + this.timer.display_name(),
|
|
back: () => { this.back(); }
|
|
},
|
|
'Name': {
|
|
value: this.timer.name,
|
|
onchange: () => {
|
|
setTimeout(() => {
|
|
keyboard.input({text:this.timer.name}).then(text => {
|
|
this.timer.name = text;
|
|
tt.set_timers_dirty();
|
|
switch_UI(this);
|
|
});
|
|
}, 0);
|
|
}
|
|
},
|
|
'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
|
|
? "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) {
|
|
// Hide the Name menu item if text input module is not available
|
|
delete menu.Name;
|
|
}
|
|
|
|
E.showMenu(menu);
|
|
}
|
|
|
|
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
|
|
|
|
var ok = false;
|
|
|
|
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.back(ok); },
|
|
onchange: (h, m, s) => {
|
|
ok = true;
|
|
this.timer.origin = h * 3600 + m * 60 + s;
|
|
tt.set_timers_dirty();
|
|
}
|
|
});
|
|
}
|
|
|
|
stop() {
|
|
// Shut down the UI and clean up listeners and handlers
|
|
|
|
E.showMenu();
|
|
}
|
|
}
|
|
|
|
|
|
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;
|
|
}
|
|
|
|
_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(null, this.focused_timer); }
|
|
}
|
|
};
|
|
this.timers.forEach((timer) => {
|
|
menu[timer.display_status() + ' ' + timer.display_name()] =
|
|
() => { this.back(timer, this.focused_timer); };
|
|
});
|
|
E.showMenu(menu);
|
|
}
|
|
|
|
stop() {
|
|
// Shut down the UI and clean up listeners and handlers
|
|
|
|
E.showMenu();
|
|
}
|
|
}
|
|
|
|
|
|
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();
|
|
}
|
|
},
|
|
'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();
|
|
}
|
|
},
|
|
'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();
|
|
}
|
|
},
|
|
'Auto reset': {
|
|
value: tt.SETTINGS.auto_reset, // boolean
|
|
onchange: v => {
|
|
tt.SETTINGS.auto_reset = 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
|
|
// UI is stopped and cleaned up, and the new UI is started.
|
|
|
|
setTimeout(() => {
|
|
if (CURRENT_UI) {
|
|
CURRENT_UI.stop();
|
|
}
|
|
CURRENT_UI = new_UI;
|
|
CURRENT_UI.start();
|
|
}, 0);
|
|
}
|
|
|
|
|
|
// 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]));
|