Merge pull request #3900 from Koell/jwalk

feat: japanese walking
master
thyttan 2025-07-01 01:27:58 +02:00 committed by GitHub
commit 5bfb257974
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 284 additions and 0 deletions

21
apps/jwalk/README.md Normal file
View File

@ -0,0 +1,21 @@
# Japanese Walking Timer
A simple timer designed to help you manage your walking intervals, whether you're in a relaxed mode or an intense workout!
![](screenshot.png)
## Usage
- The timer starts with a default total duration and interval duration, which can be adjusted in the settings.
- Tap the screen to pause or resume the timer.
- The timer will switch modes between "Relax" and "Intense" at the end of each interval.
- The display shows the current time, the remaining interval time, and the total time left.
## Creator
[Fabian Köll] ([Koell](https://github.com/Koell))
## Icon
[Icon](https://www.koreanwikiproject.com/wiki/images/2/2f/%E8%A1%8C.png)

1
apps/jwalk/app-icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4cA///A4IDBvvv11zw0xlljjnnJ3USoARP0uICJ+hnOACJ8mkARO9Mn0AGDhP2FQ8FhM9L4nyyc4CI0OpJZBgVN//lkmSsARGnlMPoMH2mSpMkzPQCAsBoViAgMC/WTt2T2giGhUTiBWDm3SU5FQ7yNOgeHum7Ypu+3sB5rFMgP3tEB5MxBg2X//+yAFBOIKhBngcFn8pkmTO4ShFAAUT+cSSQOSpgKDlihCPoN/mIOBCIVvUIsBk//zWStOz////u27QRCheTzEOtVJnV+6070BgGj2a4EL5V39MAgkm2ARGvGbNwMkOgUHknwCAsC43DvAIEg8mGo0Um+yCI0nkARF0O8nQjHCIsFh1gCJ08WwM6rARLgftNAMzCIsDI4te4gDBuYRM/pxCCJoADCI6PHdINDCI0kYo8BqYRHYowRByZ9GCJEDCLXACLVQAoUL+mXCJBrBiARD7clCJNzBIl8pIRIgEuwBGExMmUI4qH9MnYo4AH3MxCB0Ai/oCJ4AY"))

178
apps/jwalk/app.js Normal file
View File

@ -0,0 +1,178 @@
// === Utility Functions ===
function formatTime(seconds) {
let mins = Math.floor(seconds / 60);
let secs = (seconds % 60).toString().padStart(2, '0');
return `${mins}:${secs}`;
}
function getTimeStr() {
let d = new Date();
return `${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`;
}
function updateCachedLeftTime() {
cachedLeftTime = "Left: " + formatTime(state.remainingTotal);
}
// === Constants ===
const FILE = "jwalk.json";
const DEFAULTS = {
totalDuration: 30,
intervalDuration: 3,
startMode: 0,
modeBuzzerDuration: 1000,
finishBuzzerDuration: 1500,
showClock: 1,
updateWhileLocked: 0
};
// === Settings and State ===
let settings = require("Storage").readJSON(FILE, 1) || DEFAULTS;
let state = {
remainingTotal: settings.totalDuration * 60,
intervalDuration: settings.intervalDuration * 60,
remainingInterval: 0,
intervalEnd: 0,
paused: false,
currentMode: settings.startMode === 1 ? "Intense" : "Relax",
finished: false,
forceDraw: false,
};
let cachedLeftTime = "";
let lastMinuteStr = getTimeStr();
let drawTimerInterval;
// === UI Rendering ===
function drawUI() {
let y = Bangle.appRect.y + 8;
g.reset().setBgColor(g.theme.bg).clearRect(Bangle.appRect);
g.setColor(g.theme.fg);
let displayInterval = state.paused
? state.remainingInterval
: Math.max(0, Math.floor((state.intervalEnd - Date.now()) / 1000));
g.setFont("Vector", 40);
g.setFontAlign(0, 0);
g.drawString(formatTime(displayInterval), g.getWidth() / 2, y + 70);
let cy = y + 100;
if (state.paused) {
g.setFont("Vector", 15);
g.drawString("PAUSED", g.getWidth() / 2, cy);
} else {
let cx = g.getWidth() / 2;
g.setColor(g.theme.accent || g.theme.fg2 || g.theme.fg);
if (state.currentMode === "Relax") {
g.fillCircle(cx, cy, 5);
} else {
g.fillPoly([
cx, cy - 6,
cx - 6, cy + 6,
cx + 6, cy + 6
]);
}
g.setColor(g.theme.fg);
}
g.setFont("6x8", 2);
g.setFontAlign(0, -1);
g.drawString(state.currentMode, g.getWidth() / 2, y + 15);
g.drawString(cachedLeftTime, g.getWidth() / 2, cy + 15);
if (settings.showClock) {
g.setFontAlign(1, 0);
g.drawString(lastMinuteStr, g.getWidth() - 4, y);
}
g.flip();
}
// === Workout Logic ===
function toggleMode() {
state.currentMode = state.currentMode === "Relax" ? "Intense" : "Relax";
Bangle.buzz(settings.modeBuzzerDuration);
state.forceDraw = true;
}
function startNextInterval() {
if (state.remainingTotal <= 0) {
finishWorkout();
return;
}
state.remainingInterval = Math.min(state.intervalDuration, state.remainingTotal);
state.remainingTotal -= state.remainingInterval;
updateCachedLeftTime();
state.intervalEnd = Date.now() + state.remainingInterval * 1000;
state.forceDraw = true;
}
function togglePause() {
if (state.finished) return;
if (!state.paused) {
state.remainingInterval = Math.max(0, Math.floor((state.intervalEnd - Date.now()) / 1000));
state.paused = true;
} else {
state.intervalEnd = Date.now() + state.remainingInterval * 1000;
state.paused = false;
}
drawUI();
}
function finishWorkout() {
clearInterval(drawTimerInterval);
Bangle.buzz(settings.finishBuzzerDuration);
state.finished = true;
setTimeout(() => {
g.clear();
g.setFont("Vector", 30);
g.setFontAlign(0, 0);
g.drawString("Well done!", g.getWidth() / 2, g.getHeight() / 2);
g.flip();
const exitHandler = () => {
Bangle.removeListener("touch", exitHandler);
Bangle.removeListener("btn1", exitHandler);
load(); // Exit app
};
Bangle.on("touch", exitHandler);
setWatch(exitHandler, BTN1, { repeat: false });
}, 500);
}
// === Timer Tick ===
function tick() {
if (state.finished) return;
const currentMinuteStr = getTimeStr();
if (currentMinuteStr !== lastMinuteStr) {
lastMinuteStr = currentMinuteStr;
state.forceDraw = true;
}
if (!state.paused && (state.intervalEnd - Date.now()) / 1000 <= 0) {
toggleMode();
startNextInterval();
return;
}
if (state.forceDraw || settings.updateWhileLocked || !Bangle.isLocked()) {
drawUI();
state.forceDraw = false;
}
}
// === Initialization ===
Bangle.on("touch", togglePause);
Bangle.loadWidgets();
Bangle.drawWidgets();
updateCachedLeftTime();
startNextInterval();
drawUI();
drawTimerInterval = setInterval(tick, 1000);

BIN
apps/jwalk/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

19
apps/jwalk/metadata.json Normal file
View File

@ -0,0 +1,19 @@
{
"id": "jwalk",
"name": "Japanese Walking",
"shortName": "J-Walk",
"icon": "app.png",
"version": "0.01",
"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.",
"tags": "walk,timer,fitness",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"data": [
{ "name": "jwalk.json" }
],
"storage": [
{ "name": "jwalk.app.js", "url": "app.js" },
{ "name": "jwalk.settings.js", "url": "settings.js" },
{ "name": "jwalk.img", "url": "app-icon.js", "evaluate": true }
]
}

BIN
apps/jwalk/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

65
apps/jwalk/settings.js Normal file
View File

@ -0,0 +1,65 @@
(function (back) {
const FILE = "jwalk.json";
const DEFAULTS = {
totalDuration: 30,
intervalDuration: 3,
startMode: 0,
modeBuzzerDuration: 1000,
finishBuzzerDuration: 1500,
showClock: 1,
updateWhileLocked: 0
};
let settings = require("Storage").readJSON(FILE, 1) || DEFAULTS;
function saveSettings() {
require("Storage").writeJSON(FILE, settings);
}
function showSettingsMenu() {
E.showMenu({
'': { title: 'Japanese Walking' },
'< Back': back,
'Total Time (min)': {
value: settings.totalDuration,
min: 10, max: 60, step: 1,
onchange: v => { settings.totalDuration = v; saveSettings(); }
},
'Interval (min)': {
value: settings.intervalDuration,
min: 1, max: 10, step: 1,
onchange: v => { settings.intervalDuration = v; saveSettings(); }
},
'Start Mode': {
value: settings.startMode,
min: 0, max: 1,
format: v => v ? "Intense" : "Relax",
onchange: v => { settings.startMode = v; saveSettings(); }
},
'Display Clock': {
value: settings.showClock,
min: 0, max: 1,
format: v => v ? "Show" : "Hide" ,
onchange: v => { settings.showClock = v; saveSettings(); }
},
'Update UI While Locked': {
value: settings.updateWhileLocked,
min: 0, max: 1,
format: v => v ? "Always" : "On Change",
onchange: v => { settings.updateWhileLocked = v; saveSettings(); }
},
'Mode Buzz (ms)': {
value: settings.modeBuzzerDuration,
min: 0, max: 2000, step: 50,
onchange: v => { settings.modeBuzzerDuration = v; saveSettings(); }
},
'Finish Buzz (ms)': {
value: settings.finishBuzzerDuration,
min: 0, max: 5000, step: 100,
onchange: v => { settings.finishBuzzerDuration = v; saveSettings(); }
},
});
}
showSettingsMenu();
})