Skip to content

Commit 5bfb257

Browse files
authored
Merge pull request #3900 from Koell/jwalk
feat: japanese walking
2 parents 4de0fca + 1aa9ba0 commit 5bfb257

File tree

7 files changed

+284
-0
lines changed

7 files changed

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

0 commit comments

Comments
 (0)