[teatimer] - 0.07

- simplify ui
- enable settings
- adjustable time steps
- remove stuttering timer
- refactor code
master
Fabian Köll 2025-07-12 22:10:12 +02:00
parent e95668da52
commit 59b8cd3121
14 changed files with 258 additions and 218 deletions

View File

@ -4,3 +4,4 @@
0.04: Get time zone from settings for showing the clock 0.04: Get time zone from settings for showing the clock
0.05: Minor code improvements 0.05: Minor code improvements
0.06: Adjust format of title, save counter before leaving help screen 0.06: Adjust format of title, save counter before leaving help screen
0.07: Refactor code, fix stuttering timer, add settings menu

View File

@ -3,7 +3,7 @@
A simple timer. You can easily set up the time. The initial time is 2:30 A simple timer. You can easily set up the time. The initial time is 2:30
On the first screen, you can 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 up/down to change the timer by +/- one minute
- swipe left/right to change the time by +/- 15 seconds - swipe left/right to change the time by +/- 15 seconds
- press Btn1 to start - 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 - when time is up, your Bangle will buzz for 15 seconds
- and it will count up to 60 seconds and stop after that - and it will count up to 60 seconds and stop after that
## Images The time changes can be adjusted in the settings menu.
_1. Startscreen_
![](TeatimerStart.jpg) ## Images
_1. Start screen_
![](TeatimerStart.png)
Current time is displayed below the Title. Initial time is 2:30. Current time is displayed below the Title. Initial time is 2:30.
_2. Help Screen_ _2. Help Screen_
![](TeatimerHelp.jpg) ![](TeatimerHelp.png)
_3. Tea Timer running_ _3. Tea Timer running_
![](TeatimerRun.jpg) ![](TeatimerRun.png)
Remainig time is shown in big font size. Above the initial time is shown. 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. When time is up, the watch will buzz for 15 seconds. It will count up to 60 seconds.
## Requests ## Requests

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -1,237 +1,217 @@
// Tea Timer const FILE = "teatimer.json";
// Button press stops timer, next press restarts timer const DEFAULTS = {
let drag; timerDuration: 150,
var counter = 0; bigJump: 60,
var counterStart = 150; // 150 seconds smallJump: 15,
var counterInterval; finishBuzzDuration: 1500,
const states = { overtimeBuzzDuration: 100,
init: 1, // unused overtimeBuzzLimit: 60,
help: 2, // show help text overtimeBuzzSeconds: 15
start: 4, // show/change initial counter
count: 8, // count down
countUp: 16, // count up after timer finished
stop: 32 // timer stopped
}; };
var state = states.start;
let setting = require("Storage").readJSON("setting.json",1);
E.setTimeZone(setting.timezone);
// Title showing current time // Enum for states
function appTitle() { const STATES = {
return "Tea Timer\n" + currentTime(); 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() { function getTimeStr() {
let min = Date().getMinutes(); let d = new Date();
if (min < 10) min = "0" + min; return `${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`;
return Date().getHours() + ":" + min;
} }
function timeFormated(sec) { function isState(s) {
let min = Math.floor(sec / 60); return state === s;
sec = sec % 60;
if (sec < 10) sec = "0" + sec;
return min + ":" + sec;
} }
// initialize timer and show timer value => state: start function setState(s) {
function initTimer() { state = s;
counter = counterStart;
setState(states.start);
showCounter(true);
} }
// timer value (counter) can be changed in state start // === UI Drawing ===
function changeCounter(diff) { function drawUI() {
if (state == states.start) { g.reset();
if (counter + diff > 0) { g.setBgColor(g.theme.bg).clear();
counter = counter + diff; g.setColor(g.theme.fg);
showCounter(true); 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() { function startTimer() {
counterStart = counter; setState(STATES.RUNNING);
setState(states.count); startTime = Date.now();
countDown(); target = startTime + remaining * 1000;
if (!counterInterval)
counterInterval = setInterval(countDown, 1000);
} }
/* show current counter value at start and while count down function pauseTimer() {
Show if (isState(STATES.RUNNING)) {
- Title with current time remaining = Math.max(0, Math.ceil((target - Date.now()) / 1000));
- initial timer value setState(STATES.PAUSED);
- 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);
} }
} }
// count down and update every second function resumeTimer() {
// when time is up, start counting up if (isState(STATES.PAUSED)) {
function countDown() { startTime = Date.now();
counter--; target = startTime + remaining * 1000;
// Out of time setState(STATES.RUNNING);
if (counter<=0) {
outOfTime();
countUp();
counterInterval = setInterval(countUp, 1000);
return;
} }
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() { function resetTimer() {
clearInterval(); setState(STATES.INIT);
counterInterval = undefined; remaining = settings.timerDuration;
} }
// timer is stopped by user => state: stop function tick() {
function stopTimer() { if (isState(STATES.RUNNING)) {
resetTimer(); remaining -= 1;
E.showMessage("Timer stopped!", appTitle()); if (remaining <= 0) {
setState(states.stop); remaining = 0;
} setState(STATES.OVERTIME);
startTime = Date.now();
// timer is stopped by user while counting up => state: start remaining = 0; // Start overtime count-up from 0
function stopTimer2() { Bangle.buzz(settings.finishBuzzDuration);
resetTimer(); }
initTimer(); } else if (isState(STATES.OVERTIME)) {
} remaining += 1;
if (remaining <= settings.overtimeBuzzSeconds) {
Bangle.buzz(settings.overtimeBuzzDuration, 0.3);
function setState(st) { }
state = st; if (remaining >= settings.overtimeBuzzLimit) {
} resetTimer(); // Stop overtime after max duration
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);
}
} }
} }
}); drawUI();
} }
// show help text while in start state (see initDragEvents()) // === UI Controls ===
function showHelp() { function toggleTimer() {
if (state == states.start) { if (showHelp) {
state = states.help; showHelp = false;
g.setBgColor(g.theme.bg); } else if (isState(STATES.OVERTIME)) {
g.setColor(g.theme.fg); resetTimer();
E.showMessage("Swipe up/down\n+/- one minute\n\nSwipe left/right\n+/- 15 seconds\n\nPress Btn1 to start","Tea timer help"); } 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) { drawUI();
counterStart = counter; }
initTimer();
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) function adjustTimer(diff) {
initDragEvents(); if (isState(STATES.INIT)) {
// Show help test in start state remaining = Math.max(5, remaining + diff);
Bangle.on('touch', function(button, xy) { showHelp(); }); settings.timerDuration = remaining;
// event handling for button1 drawUI();
setWatch(buttonPressed, BTN1, {repeat: true}); }
initTimer(); }
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);

View File

@ -1,21 +1,26 @@
{ {
"id": "teatimer", "id": "teatimer",
"name": "Tea Timer", "name": "Tea Timer",
"version": "0.06", "version": "0.07",
"description": "A simple timer. You can easily set up the time.", "description": "A simple timer. You can easily set up the time.",
"icon": "teatimer.png", "icon": "teatimer.png",
"type": "app", "type": "app",
"tags": "tool", "tags": "tool",
"supports": ["BANGLEJS2"], "supports": ["BANGLEJS2"],
"readme": "README.md", "readme": "README.md",
"data": [
{ "name": "teatimer.json" }
],
"storage": [ "storage": [
{"name":"teatimer.app.js","url":"app.js"}, {"name":"teatimer.app.js","url":"app.js"},
{"name": "teatimer.settings.js", "url": "settings.js" },
{"name":"teatimer.img","url":"app-icon.js","evaluate":true} {"name":"teatimer.img","url":"app-icon.js","evaluate":true}
], ],
"screenshots": [ "screenshots": [
{"url":"TeatimerStart.jpg"}, {"url":"TeatimerStart.png"},
{"url":"TeatimerHelp.jpg"}, {"url":"TeatimerHelp.png"},
{"url":"TeatimerRun.jpg"}, {"url":"TeatimerRun.png"},
{"url":"TeatimerUp.jpg"} {"url":"TeatimerPause.png"},
{"url":"TeatimerUp.png"}
] ]
} }

47
apps/teatimer/settings.js Normal file
View File

@ -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();
})