diff --git a/apps/jwalk/README.md b/apps/jwalk/README.md new file mode 100644 index 000000000..96383a211 --- /dev/null +++ b/apps/jwalk/README.md @@ -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) \ No newline at end of file diff --git a/apps/jwalk/app-icon.js b/apps/jwalk/app-icon.js new file mode 100644 index 000000000..405359b5a --- /dev/null +++ b/apps/jwalk/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4cA///A4IDBvvv11zw0xlljjnnJ3USoARP0uICJ+hnOACJ8mkARO9Mn0AGDhP2FQ8FhM9L4nyyc4CI0OpJZBgVN//lkmSsARGnlMPoMH2mSpMkzPQCAsBoViAgMC/WTt2T2giGhUTiBWDm3SU5FQ7yNOgeHum7Ypu+3sB5rFMgP3tEB5MxBg2X//+yAFBOIKhBngcFn8pkmTO4ShFAAUT+cSSQOSpgKDlihCPoN/mIOBCIVvUIsBk//zWStOz////u27QRCheTzEOtVJnV+6070BgGj2a4EL5V39MAgkm2ARGvGbNwMkOgUHknwCAsC43DvAIEg8mGo0Um+yCI0nkARF0O8nQjHCIsFh1gCJ08WwM6rARLgftNAMzCIsDI4te4gDBuYRM/pxCCJoADCI6PHdINDCI0kYo8BqYRHYowRByZ9GCJEDCLXACLVQAoUL+mXCJBrBiARD7clCJNzBIl8pIRIgEuwBGExMmUI4qH9MnYo4AH3MxCB0Ai/oCJ4AY")) \ No newline at end of file diff --git a/apps/jwalk/app.js b/apps/jwalk/app.js new file mode 100644 index 000000000..2a29bcd7f --- /dev/null +++ b/apps/jwalk/app.js @@ -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); \ No newline at end of file diff --git a/apps/jwalk/app.png b/apps/jwalk/app.png new file mode 100644 index 000000000..caa09a9de Binary files /dev/null and b/apps/jwalk/app.png differ diff --git a/apps/jwalk/metadata.json b/apps/jwalk/metadata.json new file mode 100644 index 000000000..71035df61 --- /dev/null +++ b/apps/jwalk/metadata.json @@ -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 } + ] +} \ No newline at end of file diff --git a/apps/jwalk/screenshot.png b/apps/jwalk/screenshot.png new file mode 100644 index 000000000..a00decdf9 Binary files /dev/null and b/apps/jwalk/screenshot.png differ diff --git a/apps/jwalk/settings.js b/apps/jwalk/settings.js new file mode 100644 index 000000000..553f65213 --- /dev/null +++ b/apps/jwalk/settings.js @@ -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(); +}) \ No newline at end of file