diff --git a/apps/chrono_orezeno/app-icon.js b/apps/chrono_orezeno/app-icon.js new file mode 100644 index 000000000..076e9b24e --- /dev/null +++ b/apps/chrono_orezeno/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mUy4cA///38WnOsn8FrNx/8NzN5js1IFkk23ZkgSOyXbsmQCJsCpdtyVACRsJlu2pJLPiVIAocQCRdJkA+DtARKJQQFCptsLhNJJQSDBgOt2wRJSoZxBgIFBCJKVCyQFBidbkmALIwRBAomWpOkVo0JCIkAggpB0kSCIsBCIoABrdtaI4RHgGt3TRGF4LaHq6tEZAkBBI0pNwyABkEEIQwvHNwMAyTwEDoZuGyCoCRg44EFgKEJBYRcFAYLJBNA4+IHAJpFKwQFHbYPaWYp5EAokS6Q5FQYoFEhZyFQYoFFhoxFJRT3CGIhKKPgxKLAAzOGeQISJBYrKBUYoLKagI4JdAw+GLhSDCHBESDow4BLhwaJF5QIB+AIFnKzFHAl4A4kHNwxxEF4iSBRhILCbwQFFAA0DyQmBAQdPTxMB5IREpPgCRKzCCIYQLI5wA/ABIA==")) diff --git a/apps/chrono_orezeno/chrono.app.js b/apps/chrono_orezeno/chrono.app.js new file mode 100644 index 000000000..15e874f61 --- /dev/null +++ b/apps/chrono_orezeno/chrono.app.js @@ -0,0 +1,220 @@ +const dataFile = "chrono_data.json"; +let chronoTime = require("Storage").readJSON(dataFile) || {start: 0, elapsed: 0, laps: [], mode: undefined}; + +const midX = Bangle.appRect.x + Bangle.appRect.x2 / 2; +const midY = Bangle.appRect.y + Bangle.appRect.y2 / 2; +const buttonH = 50; +const buttonsRect = {x: Bangle.appRect.x, y: Bangle.appRect.y2 - buttonH, x2: Bangle.appRect.x2, y2: Bangle.appRect.y2}; +const timeRect = {x: Bangle.appRect.x + 3, y: buttonsRect.y - 55, x2: Bangle.appRect.x2 - 3, y2: buttonsRect.y - 10}; +const lapsRect = {x: Bangle.appRect.x, y: Bangle.appRect.y, x2: Bangle.appRect.x2, y2: timeRect.y - 3}; + +const lapsFont = "Vector:25"; +const timeFont = "Vector:40"; +const lapsStrW = g.setFont(lapsFont).stringWidth("00:00.0"); +const timeStrW = g.setFont(timeFont).stringWidth("00:00.0"); +let updateIntID; + +const drawButton = function(button) { + g.setFont("Vector:27").setFontAlign(0,0).setColor(0, 1, 1); + g.fillRect({x: button.x, y: button.y, x2: button.x + button.w - 1, y2: button.y + button.h - 1, r: 12}); + g.setColor(0, 0, 0).fillRect({x: button.x + 3, y: button.y + 3, x2: button.x + button.w - 4, y2: button.y + button.h - 4, r: 9}); + g.setColor(-1); + g.setFontAlign(0,0).drawString(button.label, button.x + (button.w / 2), button.y + (button.h / 2)); +}; + +const clearButtons = function() { + g.clearRect(buttonsRect); +}; + +const startButton = {x: Bangle.appRect.x, y: Bangle.appRect.y2 - buttonH, w: Bangle.appRect.w, h: buttonH, label: "Start"}; +const lapButton = {x: Bangle.appRect.x, y: Bangle.appRect.y2 - buttonH, w: (Bangle.appRect.w / 2) - 2, h: buttonH, label: "Lap"}; +const pauseButton = {x: this.midX + 1, y: Bangle.appRect.y2 - buttonH, w: (Bangle.appRect.w / 2) - 2, h: buttonH, label: "Stop"}; +const resetButton = {x: Bangle.appRect.x, y: Bangle.appRect.y2 - buttonH, w: (Bangle.appRect.w / 2) - 2, h: buttonH, label: "Reset"}; +const smallStartButton = {x: this.midX + 2, y: Bangle.appRect.y2 - buttonH, w: (Bangle.appRect.w / 2) - 2, h: buttonH, label: "Start"}; + +const drawTime = function(timeStr) { + g.setBgColor(0, 0.25, 0.25); + g.clearRect(timeRect); + g.setFont(timeFont).setFontAlign(0,1); + g.drawString(timeStr, midX, timeRect.y2); + g.setBgColor(0, 0, 0); +}; + +const ms2str = function(ms) { + const h = Math.floor(ms / 3600000); + const m = Math.floor((ms / 60000) % 60); + const s = Math.floor((ms / 1000) % 60); + const tnth = Math.floor((ms % 1000) / 100); + const format = (timePart) => (timePart < 10) ? "0" + timePart : timePart; + + return (h > 0 ? format(h) + ":" : "") + format(m) + ":" + format(s) + (h > 0 ? "" : "." + tnth); +}; + +const keepUpdated = function(shouldUpdate) { + if (updateIntID) clearInterval(updateIntID); + if (shouldUpdate) { + updateIntID = setInterval(()=> { + chronoTime.elapsed = getElapsed(); + drawTime(ms2str(chronoTime.elapsed)); + }, 100); + } +}; + +const start = function() { + chronoTime.mode = "running"; + if (chronoTime.start === 0) chronoTime.start = Date.now(); + saveState(); + keepUpdated(true); + clearButtons(); + drawButton(lapButton); + drawButton(pauseButton); +}; + +const pause = function() { + chronoTime.mode = "stopped"; + const now = Date.now(); + chronoTime.elapsed = now - chronoTime.start; + saveState(); + keepUpdated(false); + drawTime(ms2str(chronoTime.elapsed)); + clearButtons(); + drawButton(resetButton); + drawButton(smallStartButton); +}; + +const getElapsed = function() { + return Date.now() - chronoTime.start; +} + +const restart = function() { + chronoTime.mode = "running"; + chronoTime.start = Date.now() - chronoTime.elapsed; + saveState(); + keepUpdated(true); + clearButtons(); + drawButton(lapButton); + drawButton(pauseButton); +} + +const reset = function() { + chronoTime.start = 0; + chronoTime.elapsed = 0; + chronoTime.laps = []; + chronoTime.mode = undefined; + saveState(); + g.clearRect(lapsRect); + drawTime(ms2str(chronoTime.elapsed)); + clearButtons(); + drawButton(startButton); +}; + +const saveState = function() { + require("Storage").writeJSON(dataFile, chronoTime); +}; + +const lapScreen = { + fh: g.setFont(lapsFont).getFontHeight(), + numX: midX - (timeStrW / 2) + 5, + strX: midX + (timeStrW / 2) - 5, +} +lapScreen.y0 = lapsRect.y2 - (chronoTime.laps.length * lapScreen.fh); + +const addLap = function() { + chronoTime.laps.push(chronoTime.elapsed); + lapScreen.y0 = lapsRect.y2 - (chronoTime.laps.length * lapScreen.fh); + saveState(); +}; + +const drawLaps = function() { + g.setFont(lapsFont); + g.clearRect(lapsRect); + g.setClipRect(lapsRect.x, lapsRect.y, lapsRect.x2, lapsRect.y2); + let y = lapScreen.y0; + let lapNum = 1; + for (let lap of chronoTime.laps) { + if (y + lapScreen.fh > lapsRect.y) { + g.setColor(0.5, 0.5, 0.5).setFontAlign(-1, -1).drawString(lapNum, lapScreen.numX, y); + g.setColor(-1).setFontAlign(1, -1).drawString(ms2str(lap), lapScreen.strX, y); + } + y += lapScreen.fh; + lapNum++; + } + g.setClipRect(Bangle.appRect.x, Bangle.appRect.y, Bangle.appRect.x2, Bangle.appRect.y2); +}; + +const dragHandler = function(d) { + const minY = lapsRect.y2 - (chronoTime.laps.length * lapScreen.fh); + const maxY = minY > lapsRect.y ? lapsRect.y2 - (chronoTime.laps.length * lapScreen.fh) : lapsRect.y; + if (d.dy && d.b) { + lapScreen.y0 += d.dy; + if (lapScreen.y0 < minY - 20) { // overscroll + lapScreen.y0 = minY - 20; + } else if (lapScreen.y0 > maxY + 20) { + lapScreen.y0 = maxY + 20; + } + } else if (!d.b) { + if (lapScreen.y0 < minY) { + lapScreen.y0 = minY; + } else if (lapScreen.y0 > maxY) { + lapScreen.y0 = maxY; + } + } + drawLaps(d.dy); +}; + +const buttonHandler = function(_n, e) { + if (e.y < Bangle.appRect.y2 - buttonH) { + return; + } else if (e.x < midX) { // button 1 + switch (chronoTime.mode) { + case undefined: + start(); + break; + case "running": + addLap(); + drawLaps(); + break; + case "stopped": + reset(); + chronoTime.mode = undefined; + break; + } + + } else if (e.x > midX) { // button 2 + switch (chronoTime.mode) { + case undefined: + start(); + break; + case "running": + pause(); + break; + case "stopped": + restart(); + break; + } + } +}; + +const main = function() { + + g.clear(); + if (chronoTime.mode == "running") { + chronoTime.elapsed = getElapsed(); + start(); + drawLaps(); + } else if (chronoTime.mode == "stopped") { + pause(); + drawLaps(); + } else if (chronoTime.mode == undefined){ + drawTime(ms2str(chronoTime.elapsed)); + drawButton(startButton); + } + + Bangle.setUI({mode: "custom", + touch: (_n, e) => buttonHandler(_n, e), + drag: (e) => dragHandler(e), + btn: (_n) => Bangle.showClock() + }); +}; + +main(); diff --git a/apps/chrono_orezeno/icons8-stopwatch-50.png b/apps/chrono_orezeno/icons8-stopwatch-50.png new file mode 100644 index 000000000..87be1f3f5 Binary files /dev/null and b/apps/chrono_orezeno/icons8-stopwatch-50.png differ diff --git a/apps/chrono_orezeno/metadata.json b/apps/chrono_orezeno/metadata.json new file mode 100644 index 000000000..2a1f20fb3 --- /dev/null +++ b/apps/chrono_orezeno/metadata.json @@ -0,0 +1,14 @@ +{ "id": "chrono_orezeno", + "name": "Stopwatch", + "shortName":"Stopwatch", + "version":"1.0", + "description": "Chronograph with lap timer. Saves state so it is able to run in the background", + "icon": "icons8-stopwatch-50.png", + "tags": "", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"chrono.app.js", "url":"chrono.app.js"}, + {"name":"chrono.img", "url":"app-icon.js", "evaluate":true} + ] +}