2023-06-18 21:26:11 +00:00
|
|
|
{
|
2023-05-17 11:46:44 +00:00
|
|
|
const Layout = require("Layout");
|
|
|
|
|
2023-05-18 21:39:11 +00:00
|
|
|
type Rep = {
|
|
|
|
dur: number,
|
|
|
|
label: string,
|
2023-05-21 19:44:00 +00:00
|
|
|
accDur: number,
|
2023-05-18 21:39:11 +00:00
|
|
|
};
|
|
|
|
|
2023-05-25 22:18:18 +00:00
|
|
|
const reps: Rep[] = (require("Storage")
|
|
|
|
.readJSON("rep.json") as Rep[] | undefined /* TODO */)
|
|
|
|
.map(((r: Rep, i: number, a: Rep[]): Rep => {
|
|
|
|
const r2 = r as Rep;
|
2023-04-29 11:30:23 +00:00
|
|
|
|
2023-05-25 22:18:18 +00:00
|
|
|
r2.dur = r2.dur * 60 * 1000;
|
2023-05-18 21:39:11 +00:00
|
|
|
|
2023-05-25 22:18:18 +00:00
|
|
|
r2.accDur = i > 0
|
|
|
|
? a[i-1]!.accDur + r.dur
|
|
|
|
: r.dur;
|
2023-05-21 14:13:18 +00:00
|
|
|
|
2023-05-25 22:18:18 +00:00
|
|
|
return r as Rep;
|
|
|
|
}) as any);
|
2023-04-29 11:30:23 +00:00
|
|
|
|
2023-05-18 21:39:11 +00:00
|
|
|
const fontSzMain = 64;
|
|
|
|
const fontSzRep = 20;
|
2023-05-21 14:13:18 +00:00
|
|
|
const fontSzRepDesc = 12;
|
2023-06-18 21:26:11 +00:00
|
|
|
const blue = "#86caf7";
|
|
|
|
const step = 5 * 1000;
|
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;
|
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-05-25 22:19:30 +00:00
|
|
|
g.setFont(l.font!); // don't chain - might be undefined (TODO: typings)
|
|
|
|
|
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-17 11:46:44 +00:00
|
|
|
const layout = new Layout({
|
|
|
|
type: "v",
|
|
|
|
c: [
|
2023-05-21 19:44:00 +00:00
|
|
|
{
|
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",
|
|
|
|
font: "6x8",
|
|
|
|
r: Rotation.Deg90,
|
|
|
|
},
|
|
|
|
]
|
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-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-05-21 14:13:18 +00:00
|
|
|
}
|
|
|
|
]
|
2023-05-17 11:46:44 +00:00
|
|
|
}
|
|
|
|
]
|
|
|
|
}, {lazy: true});
|
|
|
|
|
2023-05-21 19:44:00 +00:00
|
|
|
class State {
|
|
|
|
paused: boolean = true;
|
|
|
|
begin: number = Date.now(); // only valid if !paused
|
|
|
|
accumulated: number = 0;
|
2023-05-21 14:13:18 +00:00
|
|
|
|
2023-05-21 19:44:00 +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
|
|
|
|
2023-05-21 19:44:00 +00:00
|
|
|
this.paused = !this.paused;
|
2023-05-21 14:13:18 +00:00
|
|
|
}
|
|
|
|
|
2023-05-21 19:44:00 +00:00
|
|
|
getElapsedTotal() {
|
|
|
|
return (this.paused ? 0 : Date.now() - this.begin) + this.accumulated;
|
|
|
|
}
|
2023-05-21 14:13:18 +00:00
|
|
|
|
2023-05-21 19:44:00 +00:00
|
|
|
getElapsedForRep() {
|
2023-06-18 21:26:11 +00:00
|
|
|
return this.currentRepPair()[1];
|
|
|
|
}
|
|
|
|
|
|
|
|
currentRepPair(): [number | null, number] {
|
2023-05-21 19:44:00 +00:00
|
|
|
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-05-21 19:44:00 +00:00
|
|
|
}
|
2023-04-29 11:30:23 +00:00
|
|
|
|
2023-05-21 19:44:00 +00:00
|
|
|
currentRepIndex() {
|
|
|
|
const elapsed = this.getElapsedTotal();
|
|
|
|
let ent;
|
|
|
|
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-05-21 19:44:00 +00:00
|
|
|
|
2023-06-18 21:26:11 +00:00
|
|
|
forward() {
|
|
|
|
this.accumulated += step;
|
|
|
|
}
|
|
|
|
|
|
|
|
rewind() {
|
|
|
|
this.accumulated -= step;
|
|
|
|
}
|
|
|
|
}
|
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)
|
2023-05-21 19:44:00 +00:00
|
|
|
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
|
|
|
|
2023-05-21 19:44:00 +00:00
|
|
|
const msToMinSec = (ms: number) => {
|
2023-05-21 14:13:18 +00:00
|
|
|
const sec = Math.round(ms / 1000);
|
2023-05-18 21:39:11 +00:00
|
|
|
const min = Math.round(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-21 14:13:18 +00:00
|
|
|
|
2023-05-21 19:44:00 +00:00
|
|
|
layout["play"]!.label = state.paused ? "Play" : "Pause";
|
|
|
|
|
2023-05-21 14:13:18 +00:00
|
|
|
if(i !== null){
|
2023-06-18 21:26:11 +00:00
|
|
|
layout["repIdx"]!.label = `Rep ${i+1}`;
|
2023-05-21 14:13:18 +00:00
|
|
|
repToLabel(i, "cur");
|
|
|
|
repToLabel(i+1, "next");
|
|
|
|
}else{
|
2023-06-18 21:26:11 +00:00
|
|
|
layout["repIdx"]!.label = "Done";
|
2023-05-21 14:13:18 +00:00
|
|
|
emptyLabel("cur");
|
|
|
|
emptyLabel("next");
|
|
|
|
}
|
2023-05-17 11:46:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
layout.render();
|
2023-04-29 11:30:23 +00:00
|
|
|
};
|
|
|
|
|
2023-06-18 21:26:11 +00:00
|
|
|
const buzzInteraction = () => Bangle.buzz(20);
|
|
|
|
const buzzNewRep = () => Bangle.buzz(500);
|
|
|
|
|
|
|
|
Bangle.loadWidgets();
|
|
|
|
|
2023-05-17 11:46:44 +00:00
|
|
|
g.clear();
|
|
|
|
drawRep();
|
|
|
|
|
2023-06-18 21:26:11 +00:00
|
|
|
Bangle.drawWidgets();
|
|
|
|
}
|