Skip to content

Commit 7cc1d11

Browse files
committed
feat: japanese walking
A simple interval timer promoting alternating brisk and easy walking to boost fitness. - add - Configurable interval and total time - Choice of start mode (Relax/Intense) - Adjustable buzzertime - Vibration alerts for mode change & completion - Pause/resume with a tap - Displays current time and time left - Close app with button/tap at the end - While screen is locked only update screen on mode change
1 parent f555365 commit 7cc1d11

File tree

7 files changed

+259
-0
lines changed

7 files changed

+259
-0
lines changed

apps/jwalk/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Japanese Walking Timer
2+
3+
A simple timer designed to help you manage your walking intervals, whether you're in a relaxed mode or an intense workout!
4+
5+
![](screenshot.png)
6+
7+
## Usage
8+
9+
- The timer starts with a default total duration and interval duration, which can be adjusted in the settings.
10+
- Tap the screen to pause or resume the timer.
11+
- The timer will switch modes between "Relax" and "Intense" at the end of each interval.
12+
- The display shows the current time, the remaining interval time, and the total time left.
13+
14+
## Creator
15+
16+
[Fabian Köll] ([Koell](https://github.com/Koell))
17+
18+
19+
## Icon
20+
21+
[Icon](https://www.koreanwikiproject.com/wiki/images/2/2f/%E8%A1%8C.png)

apps/jwalk/app-icon.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/jwalk/app.js

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
const FILE = "jwalk.json";
2+
const DEFAULTS = {
3+
totalDuration: 30,
4+
intervalDuration: 3,
5+
startMode: 0,
6+
modeBuzzerDuration: 1000,
7+
finishBuzzerDuration: 1500,
8+
};
9+
10+
let settings = require("Storage").readJSON(FILE, 1) || DEFAULTS;
11+
12+
let state = {
13+
remainingTotal: settings.totalDuration * 60,
14+
intervalDuration: settings.intervalDuration * 60,
15+
remainingInterval: 0,
16+
intervalEnd: 0,
17+
paused: false,
18+
currentMode: settings.startMode === 1 ? "Intense" : "Relax",
19+
finished: false,
20+
forceDraw: false,
21+
};
22+
23+
let drawTimerInterval;
24+
let cachedLeftTime = "";
25+
26+
function formatTime(seconds) {
27+
let mins = Math.floor(seconds / 60);
28+
let secs = (seconds % 60).toString().padStart(2, '0');
29+
return `${mins}:${secs}`;
30+
}
31+
32+
function updateCachedLeftTime() {
33+
cachedLeftTime = "Left: " + formatTime(state.remainingTotal);
34+
}
35+
36+
function getTimeStr() {
37+
let d = new Date();
38+
return `${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`;
39+
}
40+
41+
function drawUI() {
42+
let y = Bangle.appRect.y + 8;
43+
g.reset().setBgColor(g.theme.bg).clearRect(Bangle.appRect);
44+
45+
g.setColor(g.theme.fg);
46+
47+
let displayInterval = state.paused
48+
? state.remainingInterval
49+
: Math.max(0, Math.floor((state.intervalEnd - Date.now()) / 1000));
50+
51+
g.setFont("Vector", 40);
52+
g.setFontAlign(0, 0);
53+
g.drawString(formatTime(displayInterval), g.getWidth() / 2, y + 70);
54+
55+
let cy = y + 100;
56+
if (state.paused) {
57+
g.setFont("Vector", 15);
58+
g.drawString("PAUSED", g.getWidth() / 2, cy);
59+
} else {
60+
let cx = g.getWidth() / 2;
61+
g.setColor(g.theme.accent || g.theme.fg2 || g.theme.fg);
62+
if (state.currentMode === "Relax") {
63+
g.fillCircle(cx, cy, 5);
64+
} else {
65+
g.fillPoly([
66+
cx, cy - 6,
67+
cx - 6, cy + 6,
68+
cx + 6, cy + 6
69+
]);
70+
}
71+
g.setColor(g.theme.fg);
72+
}
73+
74+
g.setFont("6x8", 2);
75+
g.setFontAlign(0, -1);
76+
g.drawString(state.currentMode, g.getWidth() / 2, y + 15);
77+
g.drawString(cachedLeftTime, g.getWidth() / 2, cy + 15);
78+
79+
g.setFontAlign(1, 0);
80+
g.drawString(getTimeStr(), g.getWidth() - 4, y);
81+
g.flip();
82+
}
83+
84+
function toggleMode() {
85+
state.currentMode = state.currentMode === "Relax" ? "Intense" : "Relax";
86+
Bangle.buzz(settings.modeBuzzerDuration);
87+
state.forceDraw = true;
88+
}
89+
90+
function finishWorkout() {
91+
clearInterval(drawTimerInterval);
92+
Bangle.buzz(settings.finishBuzzerDuration);
93+
state.finished = true;
94+
95+
setTimeout(() => {
96+
g.clear();
97+
g.setFont("Vector", 30);
98+
g.setFontAlign(0, 0);
99+
g.drawString("Well done!", g.getWidth() / 2, g.getHeight() / 2);
100+
g.flip();
101+
102+
const exitHandler = () => {
103+
Bangle.removeListener("touch", exitHandler);
104+
Bangle.removeListener("btn1", exitHandler);
105+
load(); // Exit app
106+
};
107+
108+
Bangle.on("touch", exitHandler);
109+
setWatch(exitHandler, BTN1, { repeat: false });
110+
}, 500);
111+
}
112+
113+
114+
function startNextInterval() {
115+
if (state.remainingTotal <= 0) {
116+
finishWorkout();
117+
return;
118+
}
119+
120+
state.remainingInterval = Math.min(state.intervalDuration, state.remainingTotal);
121+
state.remainingTotal -= state.remainingInterval;
122+
updateCachedLeftTime();
123+
state.intervalEnd = Date.now() + state.remainingInterval * 1000;
124+
125+
state.forceDraw = true;
126+
}
127+
128+
function tick() {
129+
if (state.finished) return;
130+
131+
if (!state.paused) {
132+
if ((state.intervalEnd - Date.now()) / 1000 <= 0) {
133+
toggleMode();
134+
startNextInterval();
135+
return;
136+
}
137+
}
138+
139+
if (!Bangle.isLocked() || state.forceDraw) {
140+
drawUI();
141+
state.forceDraw = false;
142+
}
143+
}
144+
145+
146+
function togglePause() {
147+
if (state.finished) return;
148+
149+
if (!state.paused) {
150+
state.remainingInterval = Math.max(0, Math.floor((state.intervalEnd - Date.now()) / 1000));
151+
state.paused = true;
152+
} else {
153+
state.intervalEnd = Date.now() + state.remainingInterval * 1000;
154+
state.paused = false;
155+
}
156+
drawUI();
157+
}
158+
159+
// Setup
160+
Bangle.on("touch", togglePause);
161+
Bangle.loadWidgets();
162+
Bangle.drawWidgets();
163+
164+
updateCachedLeftTime();
165+
startNextInterval();
166+
drawUI();
167+
drawTimerInterval = setInterval(tick, 1000);

apps/jwalk/app.png

6.28 KB
Loading

apps/jwalk/metadata.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"id": "jwalk",
3+
"name": "Japanese Walking",
4+
"shortName": "J-Walk",
5+
"icon": "app.png",
6+
"version": "0.01",
7+
"description": "Alternating walk timer: 3 min Relax / 3 min Intense for a set time. Tap to pause/resume. Start mode, interval and total time configurable via Settings.",
8+
"tags": "walk,timer,fitness",
9+
"supports": ["BANGLEJS","BANGLEJS2"],
10+
"readme": "README.md",
11+
"data": [
12+
{ "name": "jwalk.json" }
13+
],
14+
"storage": [
15+
{ "name": "jwalk.app.js", "url": "app.js" },
16+
{ "name": "jwalk.settings.js", "url": "settings.js" },
17+
{ "name": "jwalk.img", "url": "app-icon.js", "evaluate": true }
18+
]
19+
}

apps/jwalk/screenshot.png

5.48 KB
Loading

apps/jwalk/settings.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
(function (back) {
2+
const FILE = "jwalk.json";
3+
const DEFAULTS = {
4+
totalDuration: 30,
5+
intervalDuration: 3,
6+
startMode: 0,
7+
modeBuzzerDuration: 1000,
8+
finishBuzzerDuration: 1500,
9+
};
10+
11+
let settings = require("Storage").readJSON(FILE, 1) || DEFAULTS;
12+
13+
function saveSettings() {
14+
require("Storage").writeJSON(FILE, settings);
15+
}
16+
17+
function showSettingsMenu() {
18+
E.showMenu({
19+
'': { title: 'Japanese Walking' },
20+
'< Back': back,
21+
'Total Time (min)': {
22+
value: settings.totalDuration,
23+
min: 10, max: 60, step: 1,
24+
onchange: v => { settings.totalDuration = v; saveSettings(); }
25+
},
26+
'Interval (min)': {
27+
value: settings.intervalDuration,
28+
min: 1, max: 10, step: 1,
29+
onchange: v => { settings.intervalDuration = v; saveSettings(); }
30+
},
31+
'Start Mode': {
32+
value: settings.startMode,
33+
min: 0, max: 1,
34+
format: v => v ? "Intense" : "Relax",
35+
onchange: v => { settings.startMode = v; saveSettings(); }
36+
},
37+
'Mode Buzz (ms)': {
38+
value: settings.modeBuzzerDuration,
39+
min: 0, max: 2000, step: 50,
40+
onchange: v => { settings.modeBuzzerDuration = v; saveSettings(); }
41+
},
42+
'Finish Buzz (ms)': {
43+
value: settings.finishBuzzerDuration,
44+
min: 0, max: 5000, step: 100,
45+
onchange: v => { settings.finishBuzzerDuration = v; saveSettings(); }
46+
},
47+
});
48+
}
49+
50+
showSettingsMenu();
51+
})

0 commit comments

Comments
 (0)