diff --git a/apps/teatimer/ChangeLog b/apps/teatimer/ChangeLog index fcdcda875..cf68c1d69 100644 --- a/apps/teatimer/ChangeLog +++ b/apps/teatimer/ChangeLog @@ -4,3 +4,4 @@ 0.04: Get time zone from settings for showing the clock 0.05: Minor code improvements 0.06: Adjust format of title, save counter before leaving help screen +0.07: Refactor code, fix stuttering timer, add settings menu diff --git a/apps/teatimer/README.md b/apps/teatimer/README.md index b7ece6022..b7c628420 100644 --- a/apps/teatimer/README.md +++ b/apps/teatimer/README.md @@ -3,7 +3,7 @@ A simple timer. You can easily set up the time. The initial time is 2:30 On the first screen, you can -- tap to get help +- double tap to get help - swipe up/down to change the timer by +/- one minute - swipe left/right to change the time by +/- 15 seconds - press Btn1 to start @@ -12,24 +12,31 @@ Press Btn1 again to stop the timer - when time is up, your Bangle will buzz for 15 seconds - and it will count up to 60 seconds and stop after that -## Images -_1. Startscreen_ +The time changes can be adjusted in the settings menu. -![](TeatimerStart.jpg) +## Images +_1. Start screen_ + +![](TeatimerStart.png) Current time is displayed below the Title. Initial time is 2:30. _2. Help Screen_ -![](TeatimerHelp.jpg) +![](TeatimerHelp.png) _3. Tea Timer running_ -![](TeatimerRun.jpg) -Remainig time is shown in big font size. Above the initial time is shown. +![](TeatimerRun.png) +Remainig time is shown in big font size. -_4. When time is up_ +_4. Pause Timer -![](TeatimerUp.jpg) +![](TeatimerPause.png) +While the timer is running, you can pause and unpause it by pressing BTN1. + +_5. When time is up_ + +![](TeatimerUp.png) When time is up, the watch will buzz for 15 seconds. It will count up to 60 seconds. ## Requests diff --git a/apps/teatimer/TeatimerHelp.jpg b/apps/teatimer/TeatimerHelp.jpg deleted file mode 100644 index e22960c66..000000000 Binary files a/apps/teatimer/TeatimerHelp.jpg and /dev/null differ diff --git a/apps/teatimer/TeatimerHelp.png b/apps/teatimer/TeatimerHelp.png new file mode 100644 index 000000000..f26e290fb Binary files /dev/null and b/apps/teatimer/TeatimerHelp.png differ diff --git a/apps/teatimer/TeatimerPause.png b/apps/teatimer/TeatimerPause.png new file mode 100644 index 000000000..bb6738e54 Binary files /dev/null and b/apps/teatimer/TeatimerPause.png differ diff --git a/apps/teatimer/TeatimerRun.jpg b/apps/teatimer/TeatimerRun.jpg deleted file mode 100644 index a442d12a5..000000000 Binary files a/apps/teatimer/TeatimerRun.jpg and /dev/null differ diff --git a/apps/teatimer/TeatimerRun.png b/apps/teatimer/TeatimerRun.png new file mode 100644 index 000000000..0ef0832ce Binary files /dev/null and b/apps/teatimer/TeatimerRun.png differ diff --git a/apps/teatimer/TeatimerStart.jpg b/apps/teatimer/TeatimerStart.jpg deleted file mode 100644 index 4fa8f2fc4..000000000 Binary files a/apps/teatimer/TeatimerStart.jpg and /dev/null differ diff --git a/apps/teatimer/TeatimerStart.png b/apps/teatimer/TeatimerStart.png new file mode 100644 index 000000000..2793c9bae Binary files /dev/null and b/apps/teatimer/TeatimerStart.png differ diff --git a/apps/teatimer/TeatimerUp.jpg b/apps/teatimer/TeatimerUp.jpg deleted file mode 100644 index 80b8c3c8a..000000000 Binary files a/apps/teatimer/TeatimerUp.jpg and /dev/null differ diff --git a/apps/teatimer/TeatimerUp.png b/apps/teatimer/TeatimerUp.png new file mode 100644 index 000000000..f16ce059e Binary files /dev/null and b/apps/teatimer/TeatimerUp.png differ diff --git a/apps/teatimer/app.js b/apps/teatimer/app.js index a22000342..7e2e3dbb7 100644 --- a/apps/teatimer/app.js +++ b/apps/teatimer/app.js @@ -1,237 +1,217 @@ -// Tea Timer -// Button press stops timer, next press restarts timer -let drag; -var counter = 0; -var counterStart = 150; // 150 seconds -var counterInterval; -const states = { - init: 1, // unused - help: 2, // show help text - start: 4, // show/change initial counter - count: 8, // count down - countUp: 16, // count up after timer finished - stop: 32 // timer stopped +const FILE = "teatimer.json"; +const DEFAULTS = { + timerDuration: 150, + bigJump: 60, + smallJump: 15, + finishBuzzDuration: 1500, + overtimeBuzzDuration: 100, + overtimeBuzzLimit: 60, + overtimeBuzzSeconds: 15 }; -var state = states.start; -let setting = require("Storage").readJSON("setting.json",1); -E.setTimeZone(setting.timezone); -// Title showing current time -function appTitle() { - return "Tea Timer\n" + currentTime(); +// Enum for states +const STATES = { + INIT: "init", + RUNNING: "running", + PAUSED: "paused", + FINISHED: "finished", + OVERTIME: "overtime" +}; + +let savedSettings = require("Storage").readJSON(FILE, 1) || {}; +let settings = Object.assign({}, DEFAULTS, savedSettings); + +let state = STATES.INIT; +let showHelp = false; + +let startTime = 0; +let remaining = settings.timerDuration; +let target = 0; + +let drag = null; +let dragAdjusted = false; +let lastTapTime = 0; + +// === Helpers === +function formatTime(s) { + let m = Math.floor(s / 60); + let sec = (s % 60).toString().padStart(2, '0'); + return `${m}:${sec}`; } -function currentTime() { - let min = Date().getMinutes(); - if (min < 10) min = "0" + min; - return Date().getHours() + ":" + min; +function getTimeStr() { + let d = new Date(); + return `${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`; } -function timeFormated(sec) { - let min = Math.floor(sec / 60); - sec = sec % 60; - if (sec < 10) sec = "0" + sec; - return min + ":" + sec; +function isState(s) { + return state === s; } -// initialize timer and show timer value => state: start -function initTimer() { - counter = counterStart; - setState(states.start); - showCounter(true); +function setState(s) { + state = s; } -// timer value (counter) can be changed in state start -function changeCounter(diff) { - if (state == states.start) { - if (counter + diff > 0) { - counter = counter + diff; - showCounter(true); - } +// === UI Drawing === +function drawUI() { + g.reset(); + g.setBgColor(g.theme.bg).clear(); + g.setColor(g.theme.fg); + let cx = g.getWidth() / 2; + + // Time (top right) + g.setFont("6x8", 2); + g.setFontAlign(1, 0); + g.drawString(getTimeStr(), g.getWidth() - 4, 10); + + // Help text + if (showHelp) { + g.setFontAlign(0, 0); + g.setFont("Vector", 15); + g.drawString( + `Swipe up/down: ±${settings.bigJump}s\nSwipe left/right: ±${settings.smallJump}s\n\nBTN1: Start/Pause\nDouble Tap: Hide Help`, + cx, 80 + ); + return; + } + + // Title + g.setFont("Vector", 20); + g.setFontAlign(0, 0); + let label = (isState(STATES.OVERTIME)) ? "Time's Up!" : "Tea Timer"; + g.drawString(label, cx, 40); + + // Time remaining / overtime + g.setFont("Vector", 60); + g.setColor(isState(STATES.OVERTIME) ? "#f00" : g.theme.fg); + g.drawString(formatTime(remaining), cx, 100); + + // Bottom state text + g.setFontAlign(0, 0); + if (isState(STATES.PAUSED)) { + g.setFont("6x8", 2); + g.drawString("paused", cx, g.getHeight() - 20); + } else if (!isState(STATES.RUNNING) && !isState(STATES.OVERTIME)) { + g.setFont("Vector", 13); + g.drawString("double tap for help", cx, g.getHeight() - 20); } } -// start or restart timer => state: count +// === Timer Logic === function startTimer() { - counterStart = counter; - setState(states.count); - countDown(); - if (!counterInterval) - counterInterval = setInterval(countDown, 1000); + setState(STATES.RUNNING); + startTime = Date.now(); + target = startTime + remaining * 1000; } -/* show current counter value at start and while count down - Show - - Title with current time - - initial timer value - - remaining time - - hint for help in state start -*/ -function showCounter(withHint) { - g.reset(); // workaround for E.showMessage bg color in 2v14 and earlier - E.showMessage("", appTitle()); - g.reset().setFontAlign(0,0); // center font - // draw the current counter value - g.setBgColor(-1).setColor(0,0,1); // blue - g.setFont("Vector",20); // vector font, 20px - g.drawString("Timer: " + timeFormated(counterStart),80,55); - g.setFont("Vector",60); // vector font, 60px - g.drawString(timeFormated(counter),83,100); - if (withHint) { - g.setFont("Vector",20); // vector font, 80px - g.drawString("Tap for help",80,150); +function pauseTimer() { + if (isState(STATES.RUNNING)) { + remaining = Math.max(0, Math.ceil((target - Date.now()) / 1000)); + setState(STATES.PAUSED); } } -// count down and update every second -// when time is up, start counting up -function countDown() { - counter--; - // Out of time - if (counter<=0) { - outOfTime(); - countUp(); - counterInterval = setInterval(countUp, 1000); - return; +function resumeTimer() { + if (isState(STATES.PAUSED)) { + startTime = Date.now(); + target = startTime + remaining * 1000; + setState(STATES.RUNNING); } - showCounter(false); } -// -function outOfTime() { - E.showMessage("Time is up!",appTitle()); - setState(states.countUp); - resetTimer(); - Bangle.buzz(); - Bangle.buzz(); -} - -/* this counts up (one minute), after time is up - Show - - Title with current time - - initial timer value - - "Time is up!" - - time since timer finished -*/ -function countUp() { - // buzz for 15 seconds - counter++; - if (counter <=15) { - Bangle.buzz(); - } - // stop counting up after 60 seconds - if (counter > 60) { - outOfTime(); - return; - } - g.reset(); // workaround for E.showMessage bg color in 2v14 and earlier - E.showMessage("", appTitle()); - g.reset().setFontAlign(0,0); // center font - g.setBgColor(-1).setColor(0,0,1); // blue - g.setFont("Vector",20); // vector font, 20px - g.drawString("Timer: " + timeFormated(counterStart),80,55); - g.setFont("Vector",30); // vector font, 80px - g.setBgColor(-1).setColor(1,0,0); // red - g.drawString("Time is up!",85,85); - g.setFont("Vector",40); // vector font, 80px - // draw the current counter value - g.drawString(timeFormated(counter),80,130); -} - -// reset when interupted by user oder 60 seconds after timer finished function resetTimer() { - clearInterval(); - counterInterval = undefined; + setState(STATES.INIT); + remaining = settings.timerDuration; } -// timer is stopped by user => state: stop -function stopTimer() { - resetTimer(); - E.showMessage("Timer stopped!", appTitle()); - setState(states.stop); -} - -// timer is stopped by user while counting up => state: start -function stopTimer2() { - resetTimer(); - initTimer(); -} - - -function setState(st) { - state = st; -} - -function buttonPressed() { - switch(state) { - case states.init: - initTimer(); - break; - case states.help: - initTimer(); - break; - case states.start: - startTimer(); - break; - case states.count: - stopTimer(); - break; - case states.countUp: - stopTimer2(); - break; - case states.stop: - initTimer(); - break; - default: - initTimer(); - break; - } -} - -/* Change initial counter value by swiping - swipe up: +1 minute - swipe down: -1 minute - swipe right: +15 seconds - swipe left: -15 seconds */ -function initDragEvents() { - Bangle.on("drag", e => { - if (state == states.start) { - if (!drag) { // start dragging - drag = {x: e.x, y: e.y}; - } else if (!e.b) { // released - const dx = e.x-drag.x, dy = e.y-drag.y; - drag = null; - if (Math.abs(dx)>Math.abs(dy)+10) { - // horizontal - changeCounter(dx>0 ? 15 : -15); - } else if (Math.abs(dy)>Math.abs(dx)+10) { - // vertical - changeCounter(dy>0 ? -60 : 60); - } +function tick() { + if (isState(STATES.RUNNING)) { + remaining -= 1; + if (remaining <= 0) { + remaining = 0; + setState(STATES.OVERTIME); + startTime = Date.now(); + remaining = 0; // Start overtime count-up from 0 + Bangle.buzz(settings.finishBuzzDuration); + } + } else if (isState(STATES.OVERTIME)) { + remaining += 1; + if (remaining <= settings.overtimeBuzzSeconds) { + Bangle.buzz(settings.overtimeBuzzDuration, 0.3); + } + if (remaining >= settings.overtimeBuzzLimit) { + resetTimer(); // Stop overtime after max duration } } -}); + drawUI(); } -// show help text while in start state (see initDragEvents()) -function showHelp() { - if (state == states.start) { - state = states.help; - g.setBgColor(g.theme.bg); - g.setColor(g.theme.fg); - E.showMessage("Swipe up/down\n+/- one minute\n\nSwipe left/right\n+/- 15 seconds\n\nPress Btn1 to start","Tea timer help"); +// === UI Controls === +function toggleTimer() { + if (showHelp) { + showHelp = false; + } else if (isState(STATES.OVERTIME)) { + resetTimer(); + } else if (isState(STATES.INIT)) { + startTimer(); + } else if (isState(STATES.PAUSED)) { + resumeTimer(); + } else if (isState(STATES.RUNNING)) { + pauseTimer(); } - // return to start - else if (state == states.help) { - counterStart = counter; - initTimer(); + + drawUI(); +} + +function handleDoubleTap() { + if (isState(STATES.INIT)) { + let now = Date.now(); + if (now - lastTapTime < 400) { + showHelp = !showHelp; + drawUI(); + } + lastTapTime = now; } } -// drag events in start state (to change counter value) -initDragEvents(); -// Show help test in start state -Bangle.on('touch', function(button, xy) { showHelp(); }); -// event handling for button1 -setWatch(buttonPressed, BTN1, {repeat: true}); -initTimer(); \ No newline at end of file +function adjustTimer(diff) { + if (isState(STATES.INIT)) { + remaining = Math.max(5, remaining + diff); + settings.timerDuration = remaining; + drawUI(); + } +} + +function handleDrag(e) { + if (isState(STATES.INIT) && !showHelp) { + if (e.b) { + if (!drag) { + drag = { x: e.x, y: e.y }; + dragAdjusted = false; + } else if (!dragAdjusted) { + let dx = e.x - drag.x; + let dy = e.y - drag.y; + + if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > settings.smallJump) { + adjustTimer(dx > 0 ? settings.smallJump : -settings.smallJump); + dragAdjusted = true; + } else if (Math.abs(dy) > Math.abs(dx) && Math.abs(dy) > settings.bigJump) { + adjustTimer(dy > 0 ? -settings.bigJump : settings.bigJump); + dragAdjusted = true; + } + } + } else { + drag = null; + dragAdjusted = false; + } + } +} + +// === Init App === +setWatch(toggleTimer, BTN1, { repeat: true }); +Bangle.on("drag", handleDrag); +Bangle.on("touch", handleDoubleTap); + +resetTimer(); +drawUI(); +setInterval(tick, 1000); diff --git a/apps/teatimer/metadata.json b/apps/teatimer/metadata.json index 4d4ddd9e4..571053077 100644 --- a/apps/teatimer/metadata.json +++ b/apps/teatimer/metadata.json @@ -1,21 +1,26 @@ { "id": "teatimer", "name": "Tea Timer", - "version": "0.06", + "version": "0.07", "description": "A simple timer. You can easily set up the time.", "icon": "teatimer.png", "type": "app", "tags": "tool", "supports": ["BANGLEJS2"], "readme": "README.md", + "data": [ + { "name": "teatimer.json" } + ], "storage": [ {"name":"teatimer.app.js","url":"app.js"}, + {"name": "teatimer.settings.js", "url": "settings.js" }, {"name":"teatimer.img","url":"app-icon.js","evaluate":true} ], "screenshots": [ - {"url":"TeatimerStart.jpg"}, - {"url":"TeatimerHelp.jpg"}, - {"url":"TeatimerRun.jpg"}, - {"url":"TeatimerUp.jpg"} + {"url":"TeatimerStart.png"}, + {"url":"TeatimerHelp.png"}, + {"url":"TeatimerRun.png"}, + {"url":"TeatimerPause.png"}, + {"url":"TeatimerUp.png"} ] } diff --git a/apps/teatimer/settings.js b/apps/teatimer/settings.js new file mode 100644 index 000000000..8ed780344 --- /dev/null +++ b/apps/teatimer/settings.js @@ -0,0 +1,47 @@ +(function(back) { + const FILE = "teatimer.json"; + const DEFAULTS = { + timerDuration: 150, // Initial timer duration in seconds + bigJump: 60, // Jump for vertical swipes + smallJump: 15 // Jump for horizontal swipes + }; + + let settings = require("Storage").readJSON(FILE, 1) || DEFAULTS; + + function saveSettings() { + require("Storage").writeJSON(FILE, settings); + } + + function showSettingsMenu() { + E.showMenu({ + '': { title: 'Tea Timer Settings' }, + '< Back': back, + 'Default Duration (sec)': { + value: settings.timerDuration, + min: 5, max: 900, step: 5, + onchange: v => { + settings.timerDuration = v; + saveSettings(); + } + }, + 'Swipe Up/Down (sec)': { + value: settings.bigJump, + min: 5, max: 300, step: 5, + onchange: v => { + settings.bigJump = v; + saveSettings(); + } + }, + 'Swipe Left/Right (sec)': { + value: settings.smallJump, + min: 5, max: 60, step: 5, + onchange: v => { + settings.smallJump = v; + saveSettings(); + } + } + }); + } + + showSettingsMenu(); +})