BangleApps/apps/rep/app.ts

338 lines
6.0 KiB
TypeScript
Raw Normal View History

2023-07-01 10:54:25 +00:00
type RepSettings = {
2023-07-01 10:55:00 +00:00
record: boolean,
recordStopOnExit: boolean,
2023-07-01 10:54:25 +00:00
stepMs: number,
};
2023-06-18 21:26:11 +00:00
{
2023-05-26 07:00:53 +00:00
const L = require("Layout");
2023-05-17 11:46:44 +00:00
2023-06-18 21:18:42 +00:00
type StoreRep = {
/// duration in ms
2023-05-18 21:39:11 +00:00
dur: number,
2023-06-18 21:18:42 +00:00
/// label of this rep
2023-05-18 21:39:11 +00:00
label: string,
2023-06-18 21:18:42 +00:00
};
type Rep = StoreRep & {
/// cumulative duration (ms)
accDur: number,
2023-05-18 21:39:11 +00:00
};
2023-06-18 21:18:42 +00:00
const storeReps = require("Storage")
.readJSON("rep.json") as Rep[] | undefined;
2023-04-29 11:30:23 +00:00
2023-06-18 21:18:42 +00:00
if(storeReps == null){
E.showAlert("No reps in storage\nLoad them on with the app loader")
.then(() => load());
2023-05-18 21:39:11 +00:00
2023-06-18 21:18:42 +00:00
throw new Error("no storage");
}
2023-05-21 14:13:18 +00:00
2023-06-18 21:18:42 +00:00
const reps = storeReps.map((r: StoreRep, i: number, a: Rep[]): Rep => {
const r2 = r as Rep;
r2.accDur = i > 0
? a[i-1]!.accDur + r.dur
: r.dur;
return r2;
});
2023-04-29 11:30:23 +00:00
2023-07-01 11:00:19 +00:00
const settings = (require("Storage").readJSON("rep.setting.json", true) || {}) as RepSettings;
settings.record ??= false;
settings.recordStopOnExit ??= false;
settings.stepMs ??= 5 * 1000;
2023-05-26 07:01:28 +00:00
const fontSzMain = 54;
const fontScaleRep = 2;
2023-05-18 21:39:11 +00:00
const fontSzRep = 20;
2023-05-21 14:13:18 +00:00
const fontSzRepDesc = 12;
2023-05-26 07:02:06 +00:00
const blue = "#205af7";
2023-07-01 10:54:25 +00:00
const ffStep = settings.stepMs;
2023-05-18 21:39:11 +00:00
2023-06-18 21:26:11 +00:00
let state: State | undefined;
let drawInterval: IntervalId | undefined;
let lastRepIndex: number | null = null;
let firstTime = true;
2023-05-17 11:46:44 +00:00
2023-05-25 22:19:15 +00:00
const renderDuration = (l: Layout.RenderedHierarchy) => {
let lbl;
g.clearRect(l.x, l.y, l.x+l.w, l.y+l.h);
if(state){
const [i, repElapsed] = state.currentRepPair();
if(i !== null){
let thisDur = reps[i]!.dur;
const remaining = thisDur - repElapsed;
lbl = msToMinSec(remaining);
const fract = repElapsed / thisDur;
g.setColor(blue)
.fillRect(
l.x,
l.y,
l.x + fract * l.w,
l.y + l.h
);
}else{
lbl = msToMinSec(repElapsed);
}
}else{
lbl = "RDY";
}
2023-06-18 20:41:36 +00:00
if(l.font)
g.setFont(l.font); // don't chain - might be undefined
2023-05-25 22:19:30 +00:00
2023-05-25 22:19:15 +00:00
g.setColor(l.col || g.theme.fg)
.setFontAlign(0, 0)
.drawString(
lbl,
l.x+(l.w>>1),
l.y+(l.h>>1)
);
};
2023-05-26 07:00:53 +00:00
const layout = new L({
2023-05-17 11:46:44 +00:00
type: "v",
c: [
{
2023-05-25 22:21:04 +00:00
type: "h",
c: [
{
id: "duration",
lazyBuster: 1,
type: "custom",
font: `Vector:${fontSzMain}` as FontNameWithScaleFactor,
fillx: 1,
filly: 1,
render: renderDuration,
},
{
id: "repIdx",
type: "txt",
2023-05-26 07:01:28 +00:00
font: `6x8:${fontScaleRep}`,
2023-05-26 07:00:53 +00:00
label: "---",
r: Layout.Rotation.Deg90,
2023-05-25 22:21:04 +00:00
},
]
2023-05-17 11:46:44 +00:00
},
2023-05-18 21:39:11 +00:00
{
2023-05-21 14:13:18 +00:00
type: "txt",
font: `Vector:${fontSzRepDesc}`,
label: "Activity / Duration",
2023-05-18 21:39:11 +00:00
},
2023-05-17 11:46:44 +00:00
{
2023-06-18 21:26:11 +00:00
id: "cur_name",
2023-05-21 14:13:18 +00:00
type: "txt",
font: `Vector:${fontSzRep}`,
label: "",
2023-06-18 21:26:11 +00:00
col: blue,
2023-05-21 14:13:18 +00:00
//pad: 4,
fillx: 1,
},
{
type: "txt",
font: `Vector:${fontSzRepDesc}`,
label: "Next / Duration",
},
{
2023-06-18 21:26:11 +00:00
id: "next_name",
2023-05-21 14:13:18 +00:00
type: "txt",
font: `Vector:${fontSzRep}`,
label: "",
//pad: 4,
fillx: 1,
2023-05-17 11:46:44 +00:00
},
{
type: "h",
2023-05-21 14:13:18 +00:00
c: [
{
id: "prev",
type: "btn",
2023-06-18 21:26:11 +00:00
label: "<<",
2023-05-21 14:13:18 +00:00
fillx: 1,
2023-06-18 21:26:11 +00:00
cb: () => {
buzzInteraction();
state?.rewind();
2023-06-30 11:18:41 +00:00
drawRep();
2023-06-18 21:26:11 +00:00
},
2023-05-17 11:46:44 +00:00
},
2023-05-21 14:13:18 +00:00
{
id: "play",
type: "btn",
2023-06-18 21:26:11 +00:00
label: "Play",
2023-05-21 14:13:18 +00:00
fillx: 1,
2023-06-18 21:26:11 +00:00
cb: () => {
buzzInteraction();
if(!state)
state = new State();
state.toggle();
if(state.paused){
clearInterval(drawInterval!);
drawInterval = undefined;
}else{
drawInterval = setInterval(drawRep, 1000);
}
drawRep();
},
2023-05-21 14:13:18 +00:00
},
{
id: "next",
type: "btn",
2023-06-18 21:26:11 +00:00
label: ">>",
2023-05-21 14:13:18 +00:00
fillx: 1,
2023-06-18 21:26:11 +00:00
cb: () => {
buzzInteraction();
state?.forward();
2023-06-30 11:18:41 +00:00
drawRep();
2023-06-18 21:26:11 +00:00
},
2023-05-21 14:13:18 +00:00
}
]
2023-05-17 11:46:44 +00:00
}
]
}, {lazy: true});
class State {
paused: boolean = true;
begin: number = Date.now(); // only valid if !paused
accumulated: number = 0;
2023-05-21 14:13:18 +00:00
toggle() {
if(this.paused){
this.begin = Date.now();
}else{
const diff = Date.now() - this.begin;
this.accumulated += diff;
}
2023-05-21 14:13:18 +00:00
this.paused = !this.paused;
2023-05-21 14:13:18 +00:00
}
getElapsedTotal() {
return (this.paused ? 0 : Date.now() - this.begin) + this.accumulated;
}
2023-05-21 14:13:18 +00:00
getElapsedForRep() {
2023-06-18 21:26:11 +00:00
return this.currentRepPair()[1];
}
currentRepPair(): [number | null, number] {
const elapsed = this.getElapsedTotal();
const i = this.currentRepIndex();
2023-06-18 21:26:11 +00:00
const repElapsed = elapsed - (i! > 0 ? reps[i!-1]!.accDur : 0);
2023-05-21 14:13:18 +00:00
2023-06-18 21:26:11 +00:00
return [i, repElapsed];
}
2023-04-29 11:30:23 +00:00
currentRepIndex() {
const elapsed = this.getElapsedTotal();
let ent;
2023-07-13 17:15:08 +00:00
for(let i = 0; (ent = reps[i]); i++)
if(elapsed < ent.accDur)
return i;
return null;
2023-05-18 21:39:11 +00:00
}
2023-06-18 21:26:11 +00:00
forward() {
2023-05-26 07:01:58 +00:00
this.accumulated += ffStep;
2023-06-18 21:26:11 +00:00
}
rewind() {
2023-05-26 07:01:58 +00:00
this.accumulated -= ffStep;
2023-06-18 21:26:11 +00:00
}
}
2023-05-18 21:39:11 +00:00
const repToLabel = (i: number, id: string) => {
const rep = reps[i];
2023-06-18 21:26:11 +00:00
if(rep)
layout[`${id}_name`]!.label = `${rep.label} / ${msToMinSec(rep.dur)}`;
2023-06-18 21:26:11 +00:00
else
2023-05-18 21:39:11 +00:00
emptyLabel(id);
};
const emptyLabel = (id: string) => {
2023-05-21 14:13:18 +00:00
layout[`${id}_name`]!.label = "<none> / 0m";
2023-05-18 21:39:11 +00:00
};
2023-05-21 14:13:18 +00:00
const pad2 = (s: number) => ('0' + s.toFixed(0)).slice(-2);
2023-05-18 21:39:11 +00:00
const msToMinSec = (ms: number) => {
2023-06-29 20:19:49 +00:00
const sec = Math.floor(ms / 1000);
const min = Math.floor(sec / 60);
2023-05-21 14:13:18 +00:00
return min.toFixed(0) + ":" + pad2(sec % 60);
};
2023-05-17 11:46:44 +00:00
2023-05-21 14:13:18 +00:00
const drawRep = () => {
(layout["duration"] as any).lazyBuster ^= 1;
if(state){
2023-06-18 21:26:11 +00:00
const i = state.currentRepIndex();
if(i !== lastRepIndex){
buzzNewRep();
lastRepIndex = i;
2023-05-26 07:01:47 +00:00
const repIdx = layout["repIdx"]!;
repIdx.label = i !== null ? `Rep ${i+1}` : "Done";
// work around a bug in clearing a rotated txt(?)
layout.forgetLazyState();
layout.clear();
2023-06-18 21:26:11 +00:00
}
2023-05-21 14:13:18 +00:00
layout["play"]!.label = state.paused ? "Play" : "Pause";
2023-05-21 14:13:18 +00:00
if(i !== null){
repToLabel(i, "cur");
repToLabel(i+1, "next");
}else{
emptyLabel("cur");
emptyLabel("next");
}
2023-05-17 11:46:44 +00:00
}
layout.render();
2023-04-29 11:30:23 +00:00
};
const buzzInteraction = () => Bangle.buzz(250);
const buzzNewRep = () => {
let n = firstTime ? 1 : 3;
firstTime = false;
const buzz = () => {
Bangle.buzz(1000).then(() => {
if (--n <= 0)
return;
setTimeout(buzz, 250);
});
};
buzz();
};
2023-06-18 21:26:11 +00:00
2023-07-01 10:55:00 +00:00
const init = () => {
g.clear();
layout.setUI();
2023-07-01 10:55:00 +00:00
drawRep();
Bangle.drawWidgets();
};
2023-06-18 21:26:11 +00:00
2023-07-01 10:55:00 +00:00
Bangle.loadWidgets();
if (settings.record && WIDGETS["recorder"]) {
(WIDGETS["recorder"] as RecorderWidget)
.setRecording(true)
.then(init);
if (settings.recordStopOnExit)
E.on('kill', () => (WIDGETS["recorder"] as RecorderWidget).setRecording(false));
} else {
init();
}
2023-05-17 11:46:44 +00:00
2023-06-18 21:26:11 +00:00
}