commit
5bfb257974
|
|
@ -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