2023-06-12 01:53:48 +00:00
|
|
|
const textInput = require("textinput");
|
2023-06-12 03:20:00 +00:00
|
|
|
|
2023-06-12 01:53:48 +00:00
|
|
|
g.reset();
|
2023-06-13 19:22:25 +00:00
|
|
|
g.clearRect(Bangle.appRect);
|
2023-06-12 15:30:47 +00:00
|
|
|
E.showMessage("Loading ... ");
|
2023-06-12 01:53:48 +00:00
|
|
|
Bangle.loadWidgets();
|
|
|
|
Bangle.drawWidgets();
|
|
|
|
|
2023-06-13 02:25:18 +00:00
|
|
|
const localTaskFile = "wrkmem.json";
|
2023-06-13 16:45:01 +00:00
|
|
|
const savedData = {
|
|
|
|
tasks: [], keyboardAlpha: undefined, settings: {textOutlines: true, noWordBreaks: true}
|
|
|
|
};
|
2023-06-13 02:25:18 +00:00
|
|
|
|
2023-06-13 16:45:01 +00:00
|
|
|
Object.assign(savedData, require("Storage")
|
|
|
|
.readJSON(localTaskFile, true) || {});
|
2023-06-13 02:25:18 +00:00
|
|
|
|
2024-03-04 20:34:50 +00:00
|
|
|
//let currentMenu;
|
2023-06-13 02:25:18 +00:00
|
|
|
|
2023-06-13 19:22:25 +00:00
|
|
|
const allTasks = savedData.tasks;
|
|
|
|
const SWIPE = {
|
|
|
|
LEFT: 2, RIGHT: 0, UP: 3, DOWN: 1,
|
2023-06-13 02:25:18 +00:00
|
|
|
}
|
|
|
|
|
2023-06-13 19:22:25 +00:00
|
|
|
/**
|
|
|
|
* A management object that helps us keep track of all our task timeouts.
|
|
|
|
* @type {{queueResponseTimeout: nudgeManager.queueResponseTimeout, taskTimeout: null, queueNudge:
|
|
|
|
* nudgeManager.queueNudge, interrupt: nudgeManager.interrupt, responseTimeout: null, activeTask: null}}
|
|
|
|
*/
|
2023-06-12 23:26:02 +00:00
|
|
|
const nudgeManager = {
|
|
|
|
activeTask : null, taskTimeout: null, responseTimeout: null, interrupt: () => {
|
|
|
|
if (this.taskTimeout) clearTimeout(this.taskTimeout);
|
|
|
|
if (this.responseTimeout) clearTimeout(this.responseTimeout);
|
|
|
|
this.activeTask = null;
|
|
|
|
}, queueNudge : (task, nudgeFn) => {
|
|
|
|
if (this.responseTimeout) clearTimeout(this.responseTimeout);
|
|
|
|
if (this.taskTimeout) clearTimeout(this.taskTimeout);
|
2023-06-13 16:45:01 +00:00
|
|
|
this.activeTask = task;
|
2023-06-13 02:25:18 +00:00
|
|
|
const backoffIndex = task.useBackoff ? task.backoffIndex : 1;
|
2023-06-13 16:45:01 +00:00
|
|
|
const time = task.incrementalBackoffSet[backoffIndex] * task.interval * 1000;
|
|
|
|
this.taskTimeout = setTimeout(nudgeFn, time);
|
2023-06-12 23:26:02 +00:00
|
|
|
}, queueResponseTimeout: (defaultFn) => {
|
|
|
|
// This timeout shouldn't be set if we've queued a response timeout, but we clear it anyway.
|
|
|
|
if (this.taskTimeout) clearTimeout(this.taskTimeout);
|
|
|
|
this.responseTimeout = setTimeout(defaultFn, 15000);
|
|
|
|
},
|
|
|
|
}
|
2023-06-12 03:20:00 +00:00
|
|
|
|
2023-06-13 19:22:25 +00:00
|
|
|
let keyboardAlpha, keyboardAlphaShift;
|
|
|
|
|
|
|
|
if (textInput.generateKeyboard) {
|
|
|
|
//const charSet = textInput.createCharSet("ABCDEFGHIJKLMNOPQRSTUVWXYZ", ["spc", "ok", "del"]);
|
|
|
|
keyboardAlpha = textInput.generateKeyboard([
|
|
|
|
["a", "b", "c", "j", "k", "l", "s", "t", "u"],
|
|
|
|
["d", "e", "f", "m", "n", "o", "v", "w", "x"],
|
|
|
|
["g", "h", "i", "p", "q", "r", "y", "z"],
|
|
|
|
"spc",
|
|
|
|
"ok",
|
|
|
|
"del"
|
|
|
|
]);
|
|
|
|
keyboardAlphaShift = textInput.generateKeyboard([
|
|
|
|
["A", "B", "C", "J", "K", "L", "S", "T", "U"],
|
|
|
|
["D", "E", "F", "M", "N", "O", "V", "W", "X"],
|
|
|
|
["G", "H", "I", "P", "Q", "R", "Y", "Z"],
|
|
|
|
"spc",
|
|
|
|
"cncl",
|
|
|
|
"del"
|
|
|
|
])
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Save the data in 'savedData' to flash memory.
|
|
|
|
*/
|
|
|
|
function save() {
|
|
|
|
require("Storage")
|
|
|
|
.writeJSON("wrkmem.json", savedData);
|
|
|
|
}
|
|
|
|
|
2023-06-14 13:41:20 +00:00
|
|
|
/**
|
|
|
|
* This function is a workaround wrapper for a menu navigation bug. After 'onchange' the menu re-renders itself
|
|
|
|
* so to avoid graphical glitches we postpone whatever funciton we actually want by 5ms.
|
|
|
|
* @param fn The function you actually want to call
|
|
|
|
* @returns {function(): any} The same function wrapped in a setTimeout with a 5ms delay.
|
|
|
|
*/
|
|
|
|
function st5(fn) {
|
|
|
|
return () => setTimeout(fn, 5);
|
|
|
|
}
|
|
|
|
|
2023-06-13 19:22:25 +00:00
|
|
|
/**
|
|
|
|
* Given a position and set of dimensions, create a button object that represents a rectangle in space containing text
|
|
|
|
* and some associated functionality.
|
|
|
|
* @param x
|
|
|
|
* @param y
|
|
|
|
* @param w
|
|
|
|
* @param h
|
|
|
|
* @param text
|
|
|
|
* @param callback
|
|
|
|
* @returns {{padding: number, r: number, getDrawable: (function(*, *, *, *, *): {r, x, y, y2, x2}), w, x, h, y, text:
|
|
|
|
* string, onTouch: ((function(*, *): (*|undefined))|*)}}
|
|
|
|
*/
|
2023-06-12 01:53:48 +00:00
|
|
|
function createButton(x, y, w, h, text, callback) {
|
|
|
|
text = text || "";
|
|
|
|
const x2 = x + w;
|
|
|
|
const y2 = y + h;
|
|
|
|
const r = 8;
|
|
|
|
const padding = 2;
|
|
|
|
const getDrawable = (xOff, yOff, wOff, hOff, rOff) => {
|
|
|
|
xOff = xOff || 0;
|
|
|
|
yOff = yOff || 0;
|
|
|
|
wOff = wOff || 0;
|
|
|
|
hOff = hOff || 0;
|
|
|
|
rOff = rOff || 0;
|
|
|
|
return {x: x + xOff, y: y + yOff, x2: x2 + wOff, y2: y2 + hOff, r: r + rOff};
|
|
|
|
};
|
|
|
|
let onTouch;
|
|
|
|
if (callback) {
|
|
|
|
onTouch = (button, xy) => {
|
|
|
|
const isTouched = x < xy.x && x2 > xy.x && y < xy.y && y2 > xy.y;
|
|
|
|
if (isTouched) {
|
|
|
|
Bangle.buzz(30, 1);
|
|
|
|
return callback();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
x, y, w, h, r, text, getDrawable, padding, onTouch
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-06-13 19:22:25 +00:00
|
|
|
/**
|
|
|
|
* Given a button object, draw that button onto the screen. This includes the background, borders, effects, and text.
|
|
|
|
* @param button
|
|
|
|
*/
|
2023-06-12 01:53:48 +00:00
|
|
|
function drawButton(button) {
|
|
|
|
const textMaxWidth = button.w - 2 * button.padding;
|
|
|
|
let textOutlineCol = g.theme.bgH;
|
|
|
|
let textCol = g.theme.fg;
|
|
|
|
if (button.onTouch) {
|
|
|
|
g.setColor(g.theme.fg)
|
|
|
|
.fillRect(button.getDrawable());
|
|
|
|
g.setColor(g.theme.bg)
|
|
|
|
.fillRect(button.getDrawable(2, 1, -1, -1));
|
|
|
|
g.setColor(g.theme.bg2)
|
|
|
|
.fillRect(button.getDrawable(3, 3, -3, -3));
|
|
|
|
textOutlineCol = g.theme.bg;
|
|
|
|
textCol = g.theme.fg;
|
|
|
|
}
|
|
|
|
const font = getBestFontForButton(button);
|
|
|
|
// Wrap sometimes adds a line break at the beginning if your string is very small.
|
|
|
|
// So we filter out any elements that are empty.
|
|
|
|
const wrapText = g.setFont(font)
|
|
|
|
.wrapString(button.text, textMaxWidth)
|
|
|
|
.filter(t => !!t)
|
|
|
|
.join("\n");
|
2023-06-13 16:45:01 +00:00
|
|
|
if (savedData.settings.textOutlines) {
|
|
|
|
g.setFontAlign(0, 0)
|
|
|
|
.setColor(textOutlineCol)
|
|
|
|
.drawString(wrapText, button.x + button.w / 2 + 1, button.y + button.h / 2 - 1, false);
|
|
|
|
g.setFontAlign(0, 0)
|
|
|
|
.setColor(textOutlineCol)
|
|
|
|
.drawString(wrapText, button.x + button.w / 2 - 1, button.y + button.h / 2 - 1, false);
|
|
|
|
g.setFontAlign(0, 0)
|
|
|
|
.setColor(textOutlineCol)
|
|
|
|
.drawString(wrapText, button.x + button.w / 2 - 1, button.y + button.h / 2 + 1, false);
|
|
|
|
g.setFontAlign(0, 0)
|
|
|
|
.setColor(textOutlineCol)
|
|
|
|
.drawString(wrapText, button.x + button.w / 2 + 1, button.y + button.h / 2 + 1, false);
|
|
|
|
}
|
2023-06-12 01:53:48 +00:00
|
|
|
g.setFontAlign(0, 0)
|
|
|
|
.setColor(textCol)
|
|
|
|
.drawString(wrapText, button.x + button.w / 2, button.y + button.h / 2, false);
|
|
|
|
g.reset();
|
|
|
|
}
|
|
|
|
|
2023-06-13 19:22:25 +00:00
|
|
|
/**
|
|
|
|
* Given a button object, determine what font would be best to display the button's text without breaching the
|
|
|
|
* dimensions of the button itself. Not perfectly at the moment, but serviceably.
|
|
|
|
* @param button
|
|
|
|
* @returns {string}
|
|
|
|
*/
|
2023-06-12 01:53:48 +00:00
|
|
|
function getBestFontForButton(button) {
|
|
|
|
const allowedFonts = ["12x20", "6x15", "6x8", "4x6"];
|
|
|
|
let stringMet = g.setFont("Vector:100")
|
|
|
|
.stringMetrics(button.text);
|
|
|
|
let stringArea = stringMet.width * stringMet.height;
|
|
|
|
const sampleMetric = g.setFont("Vector:100")
|
|
|
|
.stringMetrics("D");
|
|
|
|
const vectorRatio = sampleMetric.height / sampleMetric.width;
|
|
|
|
// Effective height helps us handle tall skinny buttons, since text is usually horizontal.
|
|
|
|
let effectiveHeight = Math.min(button.h, button.w);
|
2023-06-13 16:45:01 +00:00
|
|
|
if (!button.text.includes(" ") && savedData.settings.noWordBreaks) {
|
2023-06-12 01:53:48 +00:00
|
|
|
effectiveHeight = effectiveHeight / vectorRatio
|
|
|
|
}
|
|
|
|
const buttonArea = button.w * effectiveHeight;
|
|
|
|
const ratio = stringArea / buttonArea;
|
|
|
|
const vecSize = Math.floor(100 / ratio);
|
|
|
|
if (vecSize > 20) {
|
|
|
|
return "Vector:" + vecSize;
|
|
|
|
}
|
|
|
|
let i;
|
|
|
|
for (i = 0; i < allowedFonts.length - 1; i++) {
|
|
|
|
stringMet = g.setFont(allowedFonts[i])
|
|
|
|
.stringMetrics(button.text);
|
|
|
|
stringArea = Math.max(stringMet.width, button.w) * stringMet.height;
|
|
|
|
if (stringArea < buttonArea * 0.8) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return allowedFonts[i];
|
|
|
|
}
|
|
|
|
|
2023-06-13 19:22:25 +00:00
|
|
|
/**
|
|
|
|
* Given a rotation (0-3) and a label, create an object representing a swipe hint, complete with draw instructions,
|
|
|
|
* a handler, and the specified rotation.
|
|
|
|
* @param rot A number, preferably from the SWIPE enum. 0 = 0 degrees. 1 = 90 degrees. 2 = 180 degrees, etc.
|
|
|
|
* @param text The text to display on the swipe hint
|
|
|
|
* @param callback The function to be called when the corresponding direction is swiped.
|
|
|
|
* @returns {{rot, onSwipe: ((function(*, *): (*|undefined))|*), draw: draw}}
|
|
|
|
*/
|
2023-06-12 01:53:48 +00:00
|
|
|
function createSwipeControl(rot, text, callback) {
|
|
|
|
let draw = () => {};
|
|
|
|
let appRect = Bangle.appRect;
|
|
|
|
let isSwiped = () => {};
|
|
|
|
switch (rot) {
|
|
|
|
case 0:
|
|
|
|
draw = () => drawSwipeHint(appRect.x + appRect.w / 2, appRect.y + appRect.h - 6, 0, text);
|
|
|
|
isSwiped = (LR, UD) => LR === 1;
|
|
|
|
break;
|
|
|
|
case 1:
|
|
|
|
case -3:
|
|
|
|
draw = () => drawSwipeHint(appRect.x + 6, appRect.y + appRect.h / 2, 1, text);
|
|
|
|
isSwiped = (LR, UD) => UD === 1;
|
|
|
|
break;
|
|
|
|
case 2:
|
|
|
|
case -2:
|
|
|
|
draw = () => drawSwipeHint(appRect.x + appRect.w / 2, appRect.y + appRect.h - 16, 0, text, true);
|
|
|
|
isSwiped = (LR, UD) => LR === -1;
|
|
|
|
break;
|
|
|
|
case 3:
|
|
|
|
case -1:
|
|
|
|
draw = () => drawSwipeHint(appRect.x + appRect.w - 6, appRect.y + appRect.h / 2, 3, text);
|
|
|
|
isSwiped = (LR, UD) => UD === -1;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
const onSwipe = (LR, UD) => {
|
|
|
|
if (isSwiped(LR, UD)) {
|
|
|
|
return callback();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return {draw, onSwipe, rot};
|
|
|
|
}
|
|
|
|
|
2023-06-13 19:22:25 +00:00
|
|
|
/**
|
|
|
|
* Given a position, rotation, text, and mirror option, draw a swipe hint on the screen.
|
|
|
|
* @param x The x position of the center of the swipe hint.
|
|
|
|
* @param y The y position of the center of the swipe hint.
|
|
|
|
* @param rot The SWIPE rotation enumerated value (0-3) indicating the direction.
|
|
|
|
* @param text The text to display in the hint.
|
|
|
|
* @param flip Whether or not to flip the direction of the swipe hint (left to right, up to down, etc).
|
|
|
|
*/
|
2023-06-12 01:53:48 +00:00
|
|
|
function drawSwipeHint(x, y, rot, text, flip) {
|
|
|
|
const tw = g.setFont("6x8")
|
|
|
|
.stringWidth(text);
|
|
|
|
const w = tw + 41;
|
|
|
|
const gRot = Graphics.createArrayBuffer(w, 8, 1, {msb: true});
|
|
|
|
gRot.setFont("6x8")
|
|
|
|
.setFontAlign(0, -1)
|
|
|
|
.drawString(text, w / 2, 1);
|
|
|
|
gRot.drawLine(0, 4, (w - tw) / 2 - 4, 4);
|
|
|
|
gRot.drawLine((w + tw + 4) / 2, 4, w, 4);
|
|
|
|
if (flip) {
|
|
|
|
gRot.drawLine(0, 4, 4, 1);
|
|
|
|
gRot.drawLine(0, 4, 4, 7);
|
|
|
|
} else {
|
|
|
|
gRot.drawLine(w, 4, w - 4, 1);
|
|
|
|
gRot.drawLine(w, 4, w - 4, 7);
|
|
|
|
}
|
|
|
|
g.setColor(g.theme.fg)
|
|
|
|
.drawImage(gRot, x, y, {rotate: Math.PI / 2 * rot});
|
|
|
|
}
|
|
|
|
|
2023-06-13 19:22:25 +00:00
|
|
|
/**
|
|
|
|
* Given a set of options, create a drawable / UI-able menu object that attempts to lay out buttons and swipe hints in
|
|
|
|
* a given space. Returns an object with both setUI and render functions.
|
|
|
|
* @param options
|
|
|
|
* @returns {{setUI: (function(): void), buttons: *[], render: render}}
|
|
|
|
*/
|
2023-06-12 01:53:48 +00:00
|
|
|
function createMenu(options) {
|
|
|
|
let width = options.width || Bangle.appRect.w;
|
|
|
|
let height = options.height || Bangle.appRect.h;
|
|
|
|
let offsetY = Bangle.appRect.y;
|
|
|
|
const spaceBetween = options.spaceBetween || 5;
|
|
|
|
const spaceAround = options.spaceAround || 5;
|
|
|
|
const titleFont = options.titleFont || "12x20";
|
|
|
|
let marginTop = 0;
|
|
|
|
let marginBottom = 0;
|
|
|
|
let marginLeft = 0;
|
|
|
|
let marginRight = 0;
|
|
|
|
const swipeControls = options.swipeControls || [];
|
2023-06-12 03:20:00 +00:00
|
|
|
// Add some margin space to fit swipe control hints if they exist.
|
2023-06-12 01:53:48 +00:00
|
|
|
swipeControls.forEach(control => {
|
|
|
|
if (control.rot === 0) marginBottom += 8;
|
|
|
|
if (control.rot === 1) marginLeft += 8;
|
|
|
|
if (control.rot === 2) marginBottom += 8;
|
|
|
|
if (control.rot === 3) marginRight += 8;
|
|
|
|
});
|
2023-06-12 03:20:00 +00:00
|
|
|
// Add top margin to fit the title.
|
2023-06-12 01:53:48 +00:00
|
|
|
if (options.title) {
|
|
|
|
const mets = g.setFont(titleFont)
|
|
|
|
.stringMetrics(options.title);
|
|
|
|
marginTop += mets.height;
|
|
|
|
}
|
|
|
|
height = height - marginTop - marginBottom;
|
|
|
|
width = width - marginLeft - marginRight;
|
|
|
|
const isHorizontal = !!options.isHorizontal;
|
|
|
|
const numGridSpaces = options.items.reduce((acc, item) => (acc + (item.size || 1)), 0);
|
|
|
|
const shortDim = isHorizontal ? width : height;
|
|
|
|
const length = ((shortDim - spaceBetween) / numGridSpaces) - spaceAround;
|
|
|
|
const buttons = [];
|
2023-06-12 03:20:00 +00:00
|
|
|
// currentGrid tracks what grid square we are covering. Any item may cover multiple grid squares.
|
2023-06-12 01:53:48 +00:00
|
|
|
let currentGrid = 0;
|
2023-06-12 03:20:00 +00:00
|
|
|
options.items.forEach((item) => {
|
2023-06-12 01:53:48 +00:00
|
|
|
let x, y, w, h;
|
|
|
|
const mySize = item.size || 1;
|
2023-06-12 03:20:00 +00:00
|
|
|
// myLength represents the shorter of the two dimensions of the button (depending on menu orientation, w / h).
|
2023-06-12 01:53:48 +00:00
|
|
|
const myLength = length * mySize + spaceBetween * (mySize - 1);
|
|
|
|
if (isHorizontal) {
|
|
|
|
x = spaceAround + currentGrid * (length + spaceBetween) + marginLeft;
|
|
|
|
y = spaceAround + marginTop + offsetY;
|
|
|
|
w = myLength;
|
|
|
|
h = height - 2 * spaceAround;
|
|
|
|
} else {
|
|
|
|
x = spaceAround + marginLeft;
|
|
|
|
y = spaceAround + currentGrid * (length + spaceBetween) + marginTop + offsetY;
|
|
|
|
w = width - 2 * spaceAround;
|
|
|
|
h = myLength;
|
|
|
|
}
|
|
|
|
currentGrid += item.size || 1;
|
|
|
|
buttons.push(createButton(x, y, w, h, item.text, item.callback));
|
|
|
|
})
|
|
|
|
|
|
|
|
function render() {
|
|
|
|
buttons.forEach(drawButton);
|
|
|
|
if (options.title) {
|
|
|
|
g.setFont(titleFont)
|
|
|
|
.setFontAlign(0, -1)
|
|
|
|
.drawString(options.title, width / 2 + marginLeft, offsetY);
|
|
|
|
}
|
|
|
|
swipeControls.forEach(control => control.draw());
|
|
|
|
}
|
|
|
|
|
|
|
|
const touchFunc = (button, xy) => buttons.forEach(b => b.onTouch && b.onTouch(button, xy));
|
|
|
|
const swipeFunc = (LR, UD) => swipeControls.forEach(s => s.onSwipe(LR, UD));
|
2023-06-13 16:45:01 +00:00
|
|
|
const btnFunc = options.backFn;
|
2023-06-12 01:53:48 +00:00
|
|
|
return {
|
2023-06-13 16:45:01 +00:00
|
|
|
buttons, render, setUI: () => Bangle.setUI({mode: "custom", touch: touchFunc, swipe: swipeFunc, btn: btnFunc})
|
2023-06-12 01:53:48 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-06-13 19:22:25 +00:00
|
|
|
/**
|
|
|
|
* Given a menu object (a custom menu object created in this app, not an Espruino menu object) draw the menu to the
|
|
|
|
* screen and set the UI framework to the one appropriate to that menu.
|
|
|
|
* @param menu
|
|
|
|
*/
|
2023-06-12 01:53:48 +00:00
|
|
|
function setMenu(menu) {
|
2023-06-13 16:45:01 +00:00
|
|
|
save();
|
2023-06-12 01:53:48 +00:00
|
|
|
g.reset();
|
2023-06-13 19:22:25 +00:00
|
|
|
g.clearRect(Bangle.appRect);
|
2024-03-04 20:34:50 +00:00
|
|
|
//currentMenu = menu;
|
2023-06-12 01:53:48 +00:00
|
|
|
menu.render();
|
|
|
|
menu.setUI();
|
2023-06-13 19:22:25 +00:00
|
|
|
Bangle.drawWidgets();
|
2023-06-12 01:53:48 +00:00
|
|
|
}
|
|
|
|
|
2023-06-13 19:22:25 +00:00
|
|
|
/**
|
|
|
|
* Create a new task with a given set of initial text. The user will be prompted with a keyboard to title the task.
|
|
|
|
* Once the task is created, begin that task.
|
|
|
|
* @param initialText
|
|
|
|
*/
|
2023-06-12 03:20:00 +00:00
|
|
|
function newTask(initialText) {
|
2023-06-13 02:25:18 +00:00
|
|
|
nudgeManager.interrupt();
|
2023-06-12 03:20:00 +00:00
|
|
|
initialText = initialText || "";
|
2023-06-13 19:22:25 +00:00
|
|
|
textInput.input({text: initialText, keyboardMain: keyboardAlpha, keyboardShift: keyboardAlphaShift})
|
2023-06-12 01:53:48 +00:00
|
|
|
.then(text => {
|
2023-06-13 19:22:25 +00:00
|
|
|
if (!text) {
|
|
|
|
setMenu(mainMenu);
|
|
|
|
}
|
2023-06-12 03:20:00 +00:00
|
|
|
const task = createTask(text)
|
2023-06-12 15:30:47 +00:00
|
|
|
allTasks.unshift(task);
|
2023-06-13 02:25:18 +00:00
|
|
|
save();
|
2023-06-12 15:30:47 +00:00
|
|
|
startTask(task);
|
2023-06-12 01:53:48 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-06-13 19:22:25 +00:00
|
|
|
/**
|
|
|
|
* Begin the indicated task, taking the user to the corresponding menu / display screen and starting all relevant timers
|
|
|
|
* @param task
|
|
|
|
*/
|
2023-06-12 15:30:47 +00:00
|
|
|
function startTask(task) {
|
2023-06-12 23:26:02 +00:00
|
|
|
nudgeManager.queueNudge(task, () => nudge(task));
|
2023-06-12 15:30:47 +00:00
|
|
|
g.clear();
|
2023-06-13 16:45:01 +00:00
|
|
|
const onPressBack = () => {
|
|
|
|
nudgeManager.interrupt();
|
|
|
|
setMenu(mainMenu)
|
|
|
|
}
|
|
|
|
setMenu(getTaskMenu(task, onPressBack));
|
2023-06-12 15:30:47 +00:00
|
|
|
}
|
|
|
|
|
2023-06-13 19:22:25 +00:00
|
|
|
/**
|
|
|
|
* Remind the user of an ongoing task, prompting them to affirm that they are on task, distracted, or, after a set time
|
|
|
|
* period, unresponsive.
|
|
|
|
* @param task
|
|
|
|
*/
|
2023-06-12 23:26:02 +00:00
|
|
|
function nudge(task) {
|
|
|
|
Bangle.buzz(250, 1)
|
|
|
|
.then(() => {
|
|
|
|
Bangle.setLocked(false);
|
|
|
|
Bangle.setLCDPower(true);
|
|
|
|
});
|
|
|
|
const nudgeMenu = createMenu({
|
|
|
|
title : "Are you on task?", titleFont: "6x8", items: [
|
2023-06-13 02:25:18 +00:00
|
|
|
{text: task.text, size: 1}, {text: "On Task", size: 1, callback: () => affirmOnTask(task)}, {
|
|
|
|
text: "Distracted", size: 1, callback: () => affirmDistracted(task)
|
2023-06-12 23:26:02 +00:00
|
|
|
}
|
|
|
|
], isHorizontal: false
|
|
|
|
});
|
|
|
|
setMenu(nudgeMenu);
|
|
|
|
nudgeManager.queueResponseTimeout(() => concludeUnresponsive(task));
|
|
|
|
}
|
|
|
|
|
2023-06-13 19:22:25 +00:00
|
|
|
/**
|
|
|
|
* Invoked when the user affirms that they are on task, increasing the affirmation count on the given task and
|
|
|
|
* advancing the incremental backoff counter. Congratulates the user for the response.
|
|
|
|
* @param task
|
|
|
|
*/
|
2023-06-12 23:26:02 +00:00
|
|
|
function affirmOnTask(task) {
|
|
|
|
task.affirmCount++;
|
|
|
|
task.backoffIndex = Math.min(task.incrementalBackoffSet.length - 1, task.backoffIndex + 1);
|
|
|
|
showTempMessage("Great job!", "On Task!", () => startTask(task));
|
|
|
|
}
|
|
|
|
|
2023-06-13 19:22:25 +00:00
|
|
|
/**
|
|
|
|
* Invoked when the user affirms that they were distracted, increasing the distraction count and lowering the
|
|
|
|
* incremental backoff counter. Encourages the user to keep trying.
|
|
|
|
* @param task
|
|
|
|
*/
|
2023-06-12 23:26:02 +00:00
|
|
|
function affirmDistracted(task) {
|
|
|
|
task.distractCount++;
|
|
|
|
task.backoffIndex = Math.max(0, task.backoffIndex - 1);
|
|
|
|
showTempMessage("Don't worry! You've got this!", "Distracted!", () => startTask(task));
|
|
|
|
}
|
|
|
|
|
2023-06-13 19:22:25 +00:00
|
|
|
/**
|
|
|
|
* Invoked when the user has not responded to an "on task?" prompt. Increments the unresponsive count and decrements
|
|
|
|
* the incremental backoff counter.
|
|
|
|
* @param task
|
|
|
|
*/
|
2023-06-12 23:26:02 +00:00
|
|
|
function concludeUnresponsive(task) {
|
2023-06-13 02:25:18 +00:00
|
|
|
Bangle.buzz(250, 1)
|
|
|
|
.then(() => Bangle.setLCDPower(true));
|
2023-06-12 23:26:02 +00:00
|
|
|
task.unresponsiveCount++;
|
|
|
|
task.backoffIndex = Math.max(0, task.backoffIndex - 1);
|
|
|
|
nudgeManager.queueResponseTimeout(() => concludeUnresponsive(task))
|
|
|
|
}
|
|
|
|
|
2023-06-13 19:22:25 +00:00
|
|
|
/**
|
|
|
|
* Shows the user a message for a short period of time, then calls the "then function"
|
|
|
|
* @param text
|
|
|
|
* @param title
|
|
|
|
* @param thenFn
|
|
|
|
*/
|
2023-06-12 23:26:02 +00:00
|
|
|
function showTempMessage(text, title, thenFn) {
|
2023-06-13 02:25:18 +00:00
|
|
|
E.showMessage(text, {title});
|
2023-06-12 23:26:02 +00:00
|
|
|
setTimeout(() => {
|
|
|
|
thenFn();
|
|
|
|
}, 1500);
|
|
|
|
}
|
|
|
|
|
2023-06-12 15:30:47 +00:00
|
|
|
/**
|
|
|
|
* Mark the task as completed and then push it to the bottom of the list.
|
|
|
|
* @param task
|
|
|
|
*/
|
|
|
|
function completeTask(task) {
|
2023-06-12 23:26:02 +00:00
|
|
|
nudgeManager.interrupt();
|
2023-06-12 15:30:47 +00:00
|
|
|
task.complete = true;
|
|
|
|
removeTask(task, allTasks);
|
|
|
|
allTasks.push(task);
|
2023-06-13 02:25:18 +00:00
|
|
|
save();
|
2023-06-12 15:30:47 +00:00
|
|
|
setMenu(getTaskMenu(task));
|
|
|
|
}
|
|
|
|
|
2023-06-13 19:22:25 +00:00
|
|
|
/**
|
|
|
|
* Mark the task as not completed and then push it to the top of the list.
|
|
|
|
* @param task
|
|
|
|
*/
|
2023-06-12 15:30:47 +00:00
|
|
|
function restartTask(task) {
|
|
|
|
task.complete = false;
|
|
|
|
removeTask(task, allTasks);
|
|
|
|
allTasks.unshift(task);
|
2023-06-13 02:25:18 +00:00
|
|
|
save();
|
2023-06-12 23:26:02 +00:00
|
|
|
startTask(task);
|
2023-06-12 15:30:47 +00:00
|
|
|
}
|
|
|
|
|
2023-06-13 19:22:25 +00:00
|
|
|
/**
|
|
|
|
* Remove the task from the given list.
|
|
|
|
* @param task
|
|
|
|
* @param list
|
|
|
|
*/
|
2023-06-12 15:30:47 +00:00
|
|
|
function removeTask(task, list) {
|
|
|
|
const taskIndex = list.findIndex((item) => item === task);
|
|
|
|
if (taskIndex !== -1) {
|
|
|
|
list.splice(taskIndex, 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-13 19:22:25 +00:00
|
|
|
/**
|
|
|
|
* Creates a task object given a set of text.
|
|
|
|
* @param text
|
|
|
|
* @returns {{distractCount: number, backoffIndex: number, incrementalBackoffSet: number[], affirmCount: number,
|
|
|
|
* unresponsiveCount: number, interval: number, text, complete: boolean, useBackoff: boolean}}
|
|
|
|
*/
|
2023-06-12 03:20:00 +00:00
|
|
|
function createTask(text) {
|
|
|
|
const incrementalBackoffSet = [0.5, 1, 2, 4, 8, 16, 32];
|
|
|
|
return {
|
|
|
|
text,
|
|
|
|
affirmCount : 0,
|
|
|
|
distractCount : 0,
|
|
|
|
unresponsiveCount: 0,
|
|
|
|
interval : 30,
|
|
|
|
backoffIndex : 1,
|
|
|
|
incrementalBackoffSet,
|
2023-06-13 02:25:18 +00:00
|
|
|
complete : false,
|
2023-06-13 16:45:01 +00:00
|
|
|
useBackoff : true
|
2023-06-12 03:20:00 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-06-13 19:22:25 +00:00
|
|
|
/**
|
|
|
|
* Shows a menu for editing the various properties of a given task. Also exposes the functions to start, restart, or
|
|
|
|
* delete the given task.
|
|
|
|
* @param task
|
|
|
|
* @param backFn
|
|
|
|
*/
|
|
|
|
function editTask(task, backFn) {
|
|
|
|
nudgeManager.interrupt();
|
|
|
|
let editMenu = [];
|
|
|
|
if (task.complete) {
|
2023-06-14 13:41:20 +00:00
|
|
|
editMenu.push({title: "Start Task", onchange: st5(() => restartTask(task))});
|
|
|
|
editMenu.push({title: "View Task", onchange: st5(() => startTask(task))});
|
2023-06-13 19:22:25 +00:00
|
|
|
} else {
|
2023-06-14 13:41:20 +00:00
|
|
|
editMenu.push({title: "Resume Task", onchange: st5(() => startTask(task))});
|
2023-06-13 19:22:25 +00:00
|
|
|
}
|
2023-06-14 13:41:20 +00:00
|
|
|
editMenu.push({title: "Rename", onchange: st5(() => renameTask(task, () => editTask(task, backFn)))});
|
2023-06-13 19:22:25 +00:00
|
|
|
editMenu.push({title: "Interval", value: task.interval, min: 10, step: 10, onchange: v => task.interval = v});
|
|
|
|
editMenu.push({title: "Incremental Backoff", value: !!task.useBackoff, onchange: v => task.useBackoff = v});
|
2023-06-14 13:41:20 +00:00
|
|
|
editMenu.push({title: "DELETE", onchange: st5(() => deleteTask(task, () => editTask(task, backFn), backFn))});
|
2023-06-13 19:22:25 +00:00
|
|
|
editMenu.push({title: "Statistics:"});
|
|
|
|
editMenu.push({title: "On Task: " + task.affirmCount});
|
|
|
|
editMenu.push({title: "Distracted: " + task.distractCount});
|
|
|
|
editMenu.push({title: "Unresponsive: " + task.unresponsiveCount});
|
|
|
|
editMenu[""] = {title: task.text, back: backFn};
|
|
|
|
E.showMenu(editMenu);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove the given task from the task list permanently if the user hits "yes" on the confirmation dialogue.
|
|
|
|
* @param task The task to delete.
|
|
|
|
* @param backFn The function to be called when the user cancels.
|
|
|
|
* @param deleteBackFn The function to be called when the user confirms.
|
|
|
|
*/
|
|
|
|
function deleteTask(task, backFn, deleteBackFn) {
|
|
|
|
E.showPrompt("Delete " + task.text + "?")
|
|
|
|
.then(shouldDelete => {
|
|
|
|
if (shouldDelete) {
|
|
|
|
removeTask(task, allTasks);
|
|
|
|
deleteBackFn();
|
|
|
|
} else {
|
|
|
|
backFn();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Change the text of the given task, and then execute the given function.
|
|
|
|
* @param task
|
|
|
|
* @param backFn The function to execute after the renaming. Typically to show some previous menu.
|
|
|
|
* @returns {*}
|
|
|
|
*/
|
|
|
|
function renameTask(task, backFn) {
|
|
|
|
return textInput.input({text: task.text, keyboardMain: keyboardAlpha, keyboardShift: keyboardAlphaShift})
|
|
|
|
.then(text => {
|
|
|
|
task.text = text
|
|
|
|
save();
|
|
|
|
backFn();
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the "menu" that displays a given active task. This may not seem like a menu to users, but it includes swipe
|
|
|
|
* controls and can sometimes include pressable buttons as well.
|
|
|
|
* @param task
|
|
|
|
* @param backFn
|
|
|
|
* @returns {{setUI: (function(): void), buttons: *[], render: render}}
|
|
|
|
*/
|
2023-06-13 16:45:01 +00:00
|
|
|
function getTaskMenu(task, backFn) {
|
|
|
|
const d = new Date();
|
|
|
|
const h = d.getHours(), m = d.getMinutes();
|
|
|
|
const time = h + ":" + m.toString()
|
|
|
|
.padStart(2, 0);
|
2023-06-12 15:30:47 +00:00
|
|
|
const taskSwipeControls = [
|
2023-06-13 02:25:18 +00:00
|
|
|
createSwipeControl(SWIPE.LEFT, "Menu", () => {
|
|
|
|
setMenu(mainMenu);
|
|
|
|
nudgeManager.interrupt();
|
|
|
|
}), createSwipeControl(SWIPE.RIGHT, "New Task", newTask),
|
2023-06-12 15:30:47 +00:00
|
|
|
];
|
|
|
|
const items = [];
|
|
|
|
if (task.complete) {
|
|
|
|
taskSwipeControls.push(createSwipeControl(SWIPE.UP, "Restart", () => restartTask(task)));
|
2023-06-13 02:25:18 +00:00
|
|
|
taskSwipeControls.push(createSwipeControl(SWIPE.DOWN,
|
|
|
|
"Task List",
|
2023-06-13 17:19:12 +00:00
|
|
|
() => showTaskList(() => true, () => startTask(task))));
|
2023-06-12 15:30:47 +00:00
|
|
|
items.push({text: task.text + " completed!", size: 1});
|
|
|
|
const nextTask = getNextTask(task, allTasks);
|
|
|
|
if (nextTask) {
|
|
|
|
items.push({text: "next task: " + nextTask.text, size: 2, callback: () => startTask(nextTask)})
|
|
|
|
} else {
|
|
|
|
items.push({
|
|
|
|
text: "Affirmed: " + task.affirmCount + "\nDistracted: " + task.distractCount + "\nUnresponsive: " + task.unresponsiveCount,
|
|
|
|
size: 3
|
|
|
|
});
|
|
|
|
}
|
|
|
|
} else {
|
2023-06-13 02:25:18 +00:00
|
|
|
items.push({text: task.text, size: 2})
|
|
|
|
taskSwipeControls.push(createSwipeControl(SWIPE.UP, "Complete", () => completeTask(task)))
|
|
|
|
taskSwipeControls.push(createSwipeControl(SWIPE.DOWN, "Edit Task", () => editTask(task, () => startTask(task))))
|
2023-06-12 15:30:47 +00:00
|
|
|
}
|
|
|
|
return createMenu({
|
2023-06-13 16:45:01 +00:00
|
|
|
items, spaceAround: 0, spaceBetween: 0, swipeControls: taskSwipeControls, title: time, backFn
|
2023-06-12 15:30:47 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-06-13 19:22:25 +00:00
|
|
|
/**
|
|
|
|
* Given a task, determine the next incomplete task in the task list and return it. Return undefined if there are no
|
|
|
|
* other incomplete tasks.
|
|
|
|
* @param task
|
|
|
|
* @param list
|
|
|
|
* @returns {undefined|*}
|
|
|
|
*/
|
2023-06-12 15:30:47 +00:00
|
|
|
function getNextTask(task, list) {
|
|
|
|
const activeList = list.filter(x => (!x.complete || x === task));
|
|
|
|
const thisTaskPosition = activeList.findIndex(t => t === task);
|
|
|
|
let nextTask = activeList[0];
|
|
|
|
if (thisTaskPosition !== -1 && activeList[thisTaskPosition + 1]) {
|
|
|
|
nextTask = activeList[thisTaskPosition + 1];
|
|
|
|
}
|
|
|
|
return nextTask === task ? undefined : nextTask;
|
|
|
|
}
|
|
|
|
|
2023-06-12 19:32:49 +00:00
|
|
|
/**
|
2023-06-13 19:22:25 +00:00
|
|
|
* Show the list of tasks in a menu, filtered by the filterFn. Selecting a task in this menu will bring you to that
|
|
|
|
* task's edit menu.
|
|
|
|
* @param filterFn
|
|
|
|
* @param backFn
|
2023-06-12 19:32:49 +00:00
|
|
|
*/
|
2023-06-13 17:19:12 +00:00
|
|
|
function showTaskList(filterFn, backFn) {
|
2023-06-12 19:32:49 +00:00
|
|
|
let taskMenu = [];
|
2023-06-13 19:22:25 +00:00
|
|
|
const list = allTasks.filter(filterFn);
|
2023-06-12 23:26:02 +00:00
|
|
|
taskMenu = taskMenu.concat(list.map(task => {
|
2023-06-12 19:32:49 +00:00
|
|
|
return {
|
|
|
|
// Workaround - navigation has phantom buttons rendered with E.showMenu unless you delay slightly.
|
2023-06-14 13:41:20 +00:00
|
|
|
title: task.text, onchange: st5(() => editTask(task, () => showTaskList(filterFn, backFn)))
|
2023-06-12 19:32:49 +00:00
|
|
|
}
|
|
|
|
}))
|
2023-06-12 23:26:02 +00:00
|
|
|
taskMenu[""] = {title: "Tasks", back: backFn};
|
2023-06-12 19:32:49 +00:00
|
|
|
E.showMenu(taskMenu);
|
2023-06-12 15:30:47 +00:00
|
|
|
}
|
|
|
|
|
2023-06-13 19:22:25 +00:00
|
|
|
/**
|
|
|
|
* Show the menu for editing settings and tasks.
|
|
|
|
* @param backFn
|
|
|
|
*/
|
2023-06-13 16:45:01 +00:00
|
|
|
function showSettingsMenu(backFn) {
|
2023-06-13 19:22:25 +00:00
|
|
|
const settingsMenu = {
|
2023-06-13 16:45:01 +00:00
|
|
|
"" : {title: "Manage", back: backFn},
|
2023-06-13 17:19:12 +00:00
|
|
|
"Pending Tasks" : () => showTaskList(task => !task.complete, () => showSettingsMenu(backFn)),
|
|
|
|
"Completed Tasks": () => showTaskList(task => task.complete, () => showSettingsMenu(backFn)),
|
2023-06-13 16:45:01 +00:00
|
|
|
"Text Outlines" : {value: savedData.settings.textOutlines, onchange: v => savedData.settings.textOutlines = v},
|
|
|
|
"No Word Breaks" : {value: savedData.settings.noWordBreaks, onchange: v => savedData.settings.noWordBreaks = v}
|
|
|
|
}
|
|
|
|
E.showMenu(settingsMenu);
|
|
|
|
}
|
2023-06-12 15:30:47 +00:00
|
|
|
const mainMenu = createMenu({
|
2023-06-12 01:53:48 +00:00
|
|
|
title : "Working Memory", items: [
|
2023-06-13 16:45:01 +00:00
|
|
|
{text: "New Task", size: 2, callback: () => newTask("")}, {
|
|
|
|
text: "Manage", size: 1, callback: () => showSettingsMenu(() => setMenu(mainMenu))
|
2023-06-12 01:53:48 +00:00
|
|
|
}
|
|
|
|
], isHorizontal: false
|
|
|
|
});
|
|
|
|
|
2023-06-12 15:30:47 +00:00
|
|
|
setMenu(mainMenu);
|