From 1aa9ba0e2f28f564ee118b0b61d68a7fc66d3ca1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20K=C3=B6ll?= Date: Sun, 22 Jun 2025 16:51:48 +0200 Subject: [PATCH] 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 --- apps/jwalk/README.md | 21 +++++ apps/jwalk/app-icon.js | 1 + apps/jwalk/app.js | 178 ++++++++++++++++++++++++++++++++++++++ apps/jwalk/app.png | Bin 0 -> 6426 bytes apps/jwalk/metadata.json | 19 ++++ apps/jwalk/screenshot.png | Bin 0 -> 5615 bytes apps/jwalk/settings.js | 65 ++++++++++++++ 7 files changed, 284 insertions(+) create mode 100644 apps/jwalk/README.md create mode 100644 apps/jwalk/app-icon.js create mode 100644 apps/jwalk/app.js create mode 100644 apps/jwalk/app.png create mode 100644 apps/jwalk/metadata.json create mode 100644 apps/jwalk/screenshot.png create mode 100644 apps/jwalk/settings.js 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 0000000000000000000000000000000000000000..caa09a9de7713f5475df82e9e13142d99e4ff33a GIT binary patch literal 6426 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a00decdf909fb01725bc444f976504f05b011190 GIT binary patch literal 5615 zcmeHKc~leU79Y?8;sT0Qv{YjdtV@!~3YmsYS(P9n4WRO5n3+I;EF=K}ir^Eh6@>!& zM1+bvm7?@nv4SWC~DefR$E@4F}S zjlZAQaP#rz5CjeP@pcb@AR{aA8D(YyELWegKL=mGCItp-1JDGTO0AU2VhEZxQAN-Q zolFWrx)!;=+o)Qa*`e2~o!yLvJJ(H)-|2GiNu~My$s2!PbHY4!P+HI1L$%SVIfENA z{<*q> zHST4WPU~)2`Po+v;s2hgULnPf1;h!n%AV`eTZXJCcdn{?W7Sgdvhl5_y;p2( z9)CSnecfbUbz0ULc)B)!+>Hl?VUy-AK`fVfN<(h%nF&RK^3r6pX8HTfn)R+CP)9*> zwv%_QtM$rjJEJ_NnU5J6Tkl`^`&N49j@2Gz{w?OENZz7XrsDBAqsMw3-GBXhV|eq? zV8)W^wnUuD8sZeTUk6Y~Iekil@$kUA2D{vnVV;?v z1a6tBesewLQRM+Cnsi)j{%h*Pv8_GUrpA@7`xLF$>dMOvJ>sgtXK^dLy& zqEn$*G@+%16A?0n6TP$Y2%RRwo#;V)F)UWiA|hqpNorz#l3yT}6paaSx{I^9NQVFb zIiW>qI(dvjgXo;-6fOdOlV%p3M!9ICo#?@0f7&dinxOHRJSNQW(8=OCbZ2v#NR3O8 z0C!IV1h{jeM{2bygvCloNMI&#nM!p8i!BriSulsi;V^&)LzAe`qB@2`V@pExVz?6; zOf6GsWl9B-iDaat!j9jw#dKLjV9AgnNgdDhPz^?4~w)FB5`}akV z6hz46Dk=(){hp^*ChaHdz1+wZs+@s|fbc%t_q^ZLPB{ZFu^4ezVsT`8KJHF*a(@I@ zVlo_|eo8S|Amw5(1Lg~01`l&&GaLmRHUq`NIb2@2Kq%l51E72q8ZD~82oef_Gi3mW zE#%-1LLQsJ;Yl$7;lO1G1spDe7mf=Y9Wj^_E`$d_%vH;PD$$sMS&>jUfZ{p|*eIJP zWjOGJ0sz1V**KyYNDk*YfD0ys`4kk6A)ZRL90l!^$DL=_PPm>&kgzO&2zL<|Bc$3e*FqYT1myws#J~qRe^l2;x;}`34|4vfuKycd=KT*-gaZ5nN&t^a3m-p;0FPNF;d8v)AwBsn zs@jzcEJIY@AsPr8QA>V|>XgMeFq&$8#2%*iO~zPSvY`z>jDny~@_gLg0(C7NZLOIt z@aRv@RNU03Tuz=k%GfwQb1zz2Co>rpeW{==?OXNfmlm$Bv!}k@Kg@M#&0tIWRM;qO zNOehT<;0!qei-)X=J|%jxkHNXKp7WpE+r3Ee$~80B>l;6Y3t_txZ-S#w-JgOQFN!e z41wF((bEI6I=m#@+#xhyh;N#I@oR6V`(1halLB+vj21p}~%mpJa>>zFgGp}u1D<;^*_*2ZKtkTm75 zK;WMbe_l9m+DoB5nMASh{94hbeG8IHLMJx=N+Lphuh9Hpc~twt4s5r`;Iv_{ph_9- zA25AE+hN+{MbjuWNI#|ajntmcG`<#fZ@zRg#nqn|gc|=oZkRe!T` zcbr6Hwx+!&ukq{jIKf7e_EQH7Moz5NODqZ&9K((CeDfT-mbSznn|chl^SvZWljyRM z6#&NG%9^{webwS%Fmz;R$yyVKm*!I+W?9f<}KKtArtK*jw z8k?`$ZL2%qp0}s`bWNI_K5U6o=WdqkQE<5WMA_fNcLMI~H`POfc0mBu>Ob_`pz|;n zhdI^z3N&Zqr`lzt!#9?$5-C}M{u&6ax$^z?=1%%w91NA;?I(QaO)Frd{I2iOf7lN@ zKy%H5s~&?C4GbC*0NK6x#Ut9yE+}I{=-}rsDw(5eaiGl96Bi5CPftxcxA#@)p*y9S zC3Uaz$XO7Uk-x7cFX?HemrLhc$CBpdEq58057#AFcVBJX*0InjvjIp9YARTX$vf&7 z1Z>-*l@vGkY-mbbuiq2Zt=VvkPfEyNQ(i0z+CMXNbpgMmSfUynv Nc=);RnHl!a{{Y7}G*SQn literal 0 HcmV?d00001 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