From 9402124a3fd09aeefe2c536f0678a85d25f81d42 Mon Sep 17 00:00:00 2001 From: James Taylor Date: Mon, 8 Jul 2024 19:00:00 +0100 Subject: [PATCH] splitsw: add new stopwatch app with split timer Signed-off-by: James Taylor --- apps/splitsw/ChangeLog | 1 + apps/splitsw/README.md | 30 +++++++ apps/splitsw/app-icon.js | 1 + apps/splitsw/app.js | 167 ++++++++++++++++++++++++++++++++++++ apps/splitsw/app.png | Bin 0 -> 1566 bytes apps/splitsw/metadata.json | 16 ++++ apps/splitsw/screenshot.png | Bin 0 -> 2927 bytes 7 files changed, 215 insertions(+) create mode 100644 apps/splitsw/ChangeLog create mode 100644 apps/splitsw/README.md create mode 100644 apps/splitsw/app-icon.js create mode 100644 apps/splitsw/app.js create mode 100644 apps/splitsw/app.png create mode 100644 apps/splitsw/metadata.json create mode 100644 apps/splitsw/screenshot.png diff --git a/apps/splitsw/ChangeLog b/apps/splitsw/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/splitsw/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/splitsw/README.md b/apps/splitsw/README.md new file mode 100644 index 000000000..5f8fbd54b --- /dev/null +++ b/apps/splitsw/README.md @@ -0,0 +1,30 @@ +# Stopwatch with split times + +A basic stopwatch with support for split times. + +![](screenshot.png) + +## Features + +Implemented: + +- Start stopwatch +- Stop stopwatch +- Show split times +- Reset stopwatch +- Keep display unlocked + +Future: + +- Save state and restore running stopwatch when it reopens +- View all split times +- Duplicate Start/Stop and/or Reset/Split button on the physical button +- Settings, e.g. what the physical button does, and whether to keep the backlight on + +## Creator + +James Taylor ([jt-nti](https://github.com/jt-nti)) + +## Icons + +The same icons as apps/stopwatch! diff --git a/apps/splitsw/app-icon.js b/apps/splitsw/app-icon.js new file mode 100644 index 000000000..32281b7ab --- /dev/null +++ b/apps/splitsw/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4UA///vvvvEF/muH+cDHgPABf4AElWoKhILClALH1WqAQIWHBYIABwAKEgQKD1WgBYkK1X1r4XHlWtqtVvQLG1XVBYNXHYsC1YJBBoPqC4kKEQILCvQ7EhW1BYdeBYkqytVqwCCQwkqCgILCq4LFIoILCqoLEIwIsBGQJIBBZ+pA4Na0oDBtQLGvSFCBaYjIHYR3CI5AADBYhrCAAaDHAASDGQASGCBYizCAASzFZYQACZYrjCIwb7QHgIkCvQ6EGAWq+tf1QuEGAWqAAQuFEgQKBEQw9DHIwAuA=")) diff --git a/apps/splitsw/app.js b/apps/splitsw/app.js new file mode 100644 index 000000000..5d70b471d --- /dev/null +++ b/apps/splitsw/app.js @@ -0,0 +1,167 @@ +Bangle.loadWidgets(); +g.clear(true); +Bangle.drawWidgets(); + +Bangle.setLCDTimeout(undefined); + +let renderIntervalId; +let startTime; +let stopTime; +let subtotal; +let currentSplitNumber; +let splitStartTime; +let splitSubtotal; + +var Layout = require("Layout"); +var layout = new Layout( { + type:"v", c: [ + {type:"txt", pad:4, font:"20%", label:"", id:"time", fillx:1}, + {type:"h", c: [ + {type:"btn", pad:4, font:"6x8:2", label:"Start", id:"startStop", cb: l=>startStop() }, + {type:"btn", pad:4, font:"6x8:2", label:"Reset", id:"resetSplit", cb: l=>resetSplit() } + ]}, + { + type:"v", pad:4, c: [ + {type:"txt", font:"6x8:2", label:"", id:"split", fillx:1}, + {type:"txt", font:"6x8:2", label:"", id:"prevSplit", fillx:1}, + ]}, + ] +}, { + lazy: true, + back: load, +}); + +// TODO The code in this function appears in various apps so it might be +// nice to add something to the time_utils module. (There is already a +// formatDuration function but that doesn't quite work the same way.) +const getTime = function(milliseconds) { + let hrs = Math.floor(milliseconds/3600000); + let mins = Math.floor(milliseconds/60000)%60; + let secs = Math.floor(milliseconds/1000)%60; + let tnth = Math.floor(milliseconds/100)%10; + let text; + + if (hrs === 0) { + text = ("0"+mins).slice(-2) + ":" + ("0"+secs).slice(-2) + "." + tnth; + } else { + text = ("0"+hrs) + ":" + ("0"+mins).slice(-2) + ":" + ("0"+secs).slice(-2); + } + + return text; +}; + +const renderIntervalCallback = function() { + if (startTime === undefined) { + return; + } + + updateStopwatch(); +}; + +const buzz = function() { + Bangle.buzz(50, 0.5); +}; + +const startStop = function() { + buzz(); + + if (layout.startStop.label === "Start") { + start(); + } else { + stop(); + } +}; + +const start = function() { + if (stopTime === undefined) { + startTime = Date.now(); + splitStartTime = startTime; + subtotal = 0; + splitSubtotal = 0; + currentSplitNumber = 1; + } else { + subtotal += stopTime - startTime; + splitSubtotal += stopTime - splitStartTime; + startTime = Date.now(); + splitStartTime = startTime; + stopTime = undefined; + } + + layout.startStop.label = "Stop"; + layout.resetSplit.label = "Split"; + updateStopwatch(); + + renderIntervalId = setInterval(renderIntervalCallback, 100); +}; + +const stop = function() { + stopTime = Date.now(); + + layout.startStop.label = "Start"; + layout.resetSplit.label = "Reset"; + updateStopwatch(); + + if (renderIntervalId !== undefined) { + clearInterval(renderIntervalId); + renderIntervalId = undefined; + } +}; + +const resetSplit = function() { + buzz(); + + if (layout.resetSplit.label === "Reset") { + reset(); + } else { + split(); + } +}; + +const reset = function() { + layout.startStop.label = "Start"; + layout.resetSplit.label = "Reset"; + layout.split.label = ""; + layout.prevSplit.label = ""; + + startTime = undefined; + stopTime = undefined; + subtotal = 0; + currentSplitNumber = 1; + splitStartTime = undefined; + splitSubtotal = 0; + + updateStopwatch(); +}; + +const split = function() { + const splitTime = Date.now() - splitStartTime + splitSubtotal; + layout.prevSplit.label = "#" + currentSplitNumber + " " + getTime(splitTime); + + splitStartTime = Date.now(); + splitSubtotal = 0; + currentSplitNumber++; + + updateStopwatch(); +}; + +const updateStopwatch = function() { + let elapsedTime; + + if (startTime === undefined) { + elapsedTime = 0; + } else { + elapsedTime = Date.now() - startTime + subtotal; + } + + layout.time.label = getTime(elapsedTime); + + if (splitStartTime !== undefined) { + const splitTime = Date.now() - splitStartTime + splitSubtotal; + layout.split.label = "#" + currentSplitNumber + " " + getTime(splitTime); + } + + layout.render(); + // layout.debug(); +}; + +updateStopwatch(); diff --git a/apps/splitsw/app.png b/apps/splitsw/app.png new file mode 100644 index 0000000000000000000000000000000000000000..92ffe73b7f172f7019486231d5bc0ba327c13a67 GIT binary patch literal 1566 zcmV+(2I2XMP)rZGih zXw&#$)jrm=iAGIK8xtz&C7L#UsP>_)mll=wMNO+ffoiD_T1#yUim(D(ys%vMfQPei zrVqOZR|L*1ZtO$(Kh4bfZvQzm-^@2KjcNS9p~TbJ{=+BFgR;y*?EJRZOY-C8(-tp7 zVORza@KQWI#+kt5$25We8u2D@-cmyFb37f!H4B+o`PybN-uT(0hvM!pG2RAaMai~Z zb8H5+fL|F7l^*}>gRWTg<2NIR8$39)H7FE$0)fri3@0JG9e^ZV$1ylSfBMXW2&%E$ zPO&yWxOMB+q<~{)6;Ked;unf2IeFmyUmqWV%KAJEV+T+Q%#hR?1dbK`*5J(O?<*bi zO;Z%!IDVk)sEnbh6R5b5f)d(`!dpynDcXcVZikO&eb-4ajOeKPP8=vpld&G6#BUZr!wEAw+((0R3 zrXbzLH_tucuqlj0hL$qu+9ey79D&OEyhyeIpghm3SOpJZ0ylh<6M!&@vIX9#5%D$^ zHQ6$u$q@+W`9uhB*iAW^QhKevc3RtRO1aoFFEin3XYxR#>%x|>M@G*&ki!ig!iN+9 z<#}G0?8a$r^U>1QCMBx?`|`Zb`y*l_6*ZQ>*_wbuTSMf8&2iX+vZ=DKzn?&FFTtQn zcOW38Q<&~zjy+M0be$yF)>!FS6Hq8Jo3G6BI3^Q~Myv1s(rIb4-UlX1veWFN9dY0H zM6f0xl*MMVIK!PJ8_3nan@8J?SO1b#qd3!TkLoU-P%r|SKQP~H&P}mTCOL|}{(ipr z>d0vS%cB+Cx!WzJJ2&|OAM+=YGa>N@@R-ePyWG|3Y)vhL_wLbMu#!7PtICQ=hz{kT!_!z`=nUlJl03sr&j0}&?P-3E-D?`>v1Tssg-)!E}hoy|JDk@}lRu&l- zYRIfV%j~QyRj3EpMT1a)jt8Ss%SYLw_Cy$ zv}^iU-;=_cfKaZ0SOQSht?Gs8E~%aDal2W+_GKB-2XL;tKLmucox&9>d6`XIZ8j@! z_e$Lm9|hmt>bvj55SBaNntrAr?+kNN?GIAf(_O<-+) z5IAObcQpIe7!!pQ3<2AlI-_PsIm(6=ma`>W50nETfTn>V*P-_A5exa4aj@g=$hAS; zoP8Zd*(akXM__&7H4yvF?#}Ce>f^YS>UvamU;ih1=wsFuSY{xIr6LQ8JwOA1AxJ1~ z^l9-RQP-pLYNJmx>tf&nGX~z3)sL;HKUp^iAua-QAzWIPE%35s-uOlRWH1CA?7VC2 zZtlWBm=oEKVJu<83hC#?DLkvxEdp_h2nm2Zr(G@2&2bfG$kht9Ju2IqI@PE(573Um z8a5O*#uCodUgj!lva#8|1C~Y)XWJ9ib=hh5;!L+aCpigk)WcS%7NR;3)K7Nx>QSlE zjN%tYjFMPp{?P~koIaf#emxNH08UH!@u4HcM>q%HlYK@Ri${Olt*FTk7vN$e@QR9l#uqzNePq-K{A831lR^T#FgJGPOIi#jBjcHipUslrouSl-W Q%m4rY07*qoM6N<$g48qYF8}}l literal 0 HcmV?d00001 diff --git a/apps/splitsw/metadata.json b/apps/splitsw/metadata.json new file mode 100644 index 000000000..d5ae19347 --- /dev/null +++ b/apps/splitsw/metadata.json @@ -0,0 +1,16 @@ +{ "id": "splitsw", + "name": "Stopwatch with split times", + "shortName":"Stopwatch", + "version":"0.01", + "description": "A basic stopwatch with support for split times", + "icon": "app.png", + "tags": "tool,outdoors,health", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "screenshots": [{ "url": "screenshot.png" }], + "allow_emulator": true, + "storage": [ + {"name":"splitsw.app.js","url":"app.js"}, + {"name":"splitsw.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/splitsw/screenshot.png b/apps/splitsw/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..184bc4fbcd7e9672ae60f2fb7a769ee7c173a5a3 GIT binary patch literal 2927 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 literal 0 HcmV?d00001