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)
- Vibration alerts for mode change & completion
- Adjustable buzzertime
- Pause/resume with a tap
- Displays current time and time left
- Enable/disable clock in setting
- Close app with button/tap at the end
- While screen is locked only update screen on mode change
- And every time the clock changes
- Can be changed to updating every second in settings
master
parent
2f82895196
commit
1aa9ba0e2f
|
|
@ -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!
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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)
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("mEw4cA///A4IDBvvv11zw0xlljjnnJ3USoARP0uICJ+hnOACJ8mkARO9Mn0AGDhP2FQ8FhM9L4nyyc4CI0OpJZBgVN//lkmSsARGnlMPoMH2mSpMkzPQCAsBoViAgMC/WTt2T2giGhUTiBWDm3SU5FQ7yNOgeHum7Ypu+3sB5rFMgP3tEB5MxBg2X//+yAFBOIKhBngcFn8pkmTO4ShFAAUT+cSSQOSpgKDlihCPoN/mIOBCIVvUIsBk//zWStOz////u27QRCheTzEOtVJnV+6070BgGj2a4EL5V39MAgkm2ARGvGbNwMkOgUHknwCAsC43DvAIEg8mGo0Um+yCI0nkARF0O8nQjHCIsFh1gCJ08WwM6rARLgftNAMzCIsDI4te4gDBuYRM/pxCCJoADCI6PHdINDCI0kYo8BqYRHYowRByZ9GCJEDCLXACLVQAoUL+mXCJBrBiARD7clCJNzBIl8pIRIgEuwBGExMmUI4qH9MnYo4AH3MxCB0Ai/oCJ4AY"))
|
||||||
|
|
@ -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);
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 6.3 KiB |
|
|
@ -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 }
|
||||||
|
]
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
|
|
@ -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();
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue