BangleApps_old/apps/tevtimer/app.js

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