Merge pull request #1788 from sir-indy/master

[layout & smpltmr] New interface for Simple Timer, and changes to layout to make it work.
pull/1791/head
Gordon Williams 2022-05-04 11:16:38 +01:00 committed by GitHub
commit 537744bcf7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 200 additions and 139 deletions

View File

@ -1 +1,2 @@
0.01: Release
0.01: Release
0.02: Rewrite with new interface

View File

@ -1,21 +1,18 @@
# Simple Timer
A simple app to set a timer quickly. Simply tab on top/bottom/left/right
to select the minutes and tab in the middle of the screen to start/stop
the timer. Note that this timer depends on qalarm.
A simple app to set a timer quickly. Drag or tap on the up and down buttons over the hour, minute or second to set the time.
# Overview
If you open the app, you can simply control the timer
by clicking on top, bottom, left or right of the screen.
If you tab at the middle of the screen, the timer is
started / stopped.
This app uses the `sched` library, which allows the timer to continue to run in the background when this app is closed.
![](description.png)
![](screenshot_1.png)
![](screenshot_2.png)
![](screenshot_3.png)
![](screenshot_4.png)
# Creator
# Creators
[David Peer](https://github.com/peerdavid)
[Sir Indy](https://github.com/sir-indy)
# Thanks to...
Time icon created by <a href="https://www.flaticon.com/free-icons/time" title="time icons">CreativeCons - Flaticon</a>

View File

@ -3,122 +3,188 @@
*
* Creator: David Peer
* Date: 02/2022
*
* Modified: Sir Indy
* Date: 05/2022
*/
Bangle.loadWidgets();
const alarm = require("sched");
const Layout = require("Layout");
const alarm = require("sched")
const TIMER_IDX = "smpltmr";
const screenWidth = g.getWidth();
const screenHeight = g.getHeight();
const cx = parseInt(screenWidth/2);
const cy = parseInt(screenHeight/2)-12;
var minutes = 5;
var interval; //used for the 1 second interval timer
function isTimerEnabled(){
var alarmObj = alarm.getAlarm(TIMER_IDX);
if(alarmObj===undefined || !alarmObj.on){
return false;
const secondsToTime = (s) => new Object({h:Math.floor((s/3600) % 24), m:Math.floor((s/60) % 60), s:Math.floor(s % 60)});
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
function formatTime(s) {
var t = secondsToTime(s);
if (t.h) {
return t.h + ':' + ("0" + t.m).substr(-2) + ':' + ("0" + t.s).substr(-2);
} else {
return t.m + ':' + ("0" + t.s).substr(-2);
}
return true;
}
function getTimerMin(){
var alarmObj = alarm.getAlarm(TIMER_IDX);
return Math.round(alarm.getTimeToAlarm(alarmObj)/(60*1000));
var seconds = 5 * 60; // Default to 5 minutes
var drawTimeout;
//var timerRunning = false;
function timerRunning() {
return (alarm.getTimeToAlarm(alarm.getAlarm(TIMER_IDX)) != undefined)
}
const imgArrow = atob("CQmBAAgOBwfD47ndx+OA");
const imgPause = atob("GBiBAP+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B/w==");
const imgPlay = atob("GBiBAIAAAOAAAPgAAP4AAP+AAP/gAP/4AP/+AP//gP//4P//+P///v///v//+P//4P//gP/+AP/4AP/gAP+AAP4AAPgAAOAAAIAAAA==");
function onDrag(event) {
if (!timerRunning()) {
Bangle.buzz(40, 0.3);
var diff = -Math.round(event.dy/5);
if (event.x < timePickerLayout.hours.w) {
diff *= 3600;
} else if (event.x > timePickerLayout.mins.x && event.x < timePickerLayout.secs.x) {
diff *= 60;
}
updateTimePicker(diff);
}
}
function setTimer(minutes){
function onTouch(button, xy) {
if (xy.y > (timePickerLayout.btnStart.y||timerLayout.btnStart.y)) {
Bangle.buzz(40, 0.3);
onButton();
return;
}
if (!timerRunning()) {
var touchMidpoint = timePickerLayout.hours.y + timePickerLayout.hours.h/2;
var diff = 0;
Bangle.buzz(40, 0.3);
if (xy.y > 24 && xy.y < touchMidpoint - 10) {
diff = 1;
} else if (xy.y > touchMidpoint + 10 && xy.y < timePickerLayout.btnStart.y) {
diff = -1;
}
if (xy.x < timePickerLayout.hours.w) {
diff *= 3600;
} else if (xy.x > timePickerLayout.mins.x && xy.x < timePickerLayout.secs.x) {
diff *= 60;
}
updateTimePicker(diff);
}
}
function onButton() {
g.clearRect(Bangle.appRect);
if (timerRunning()) {
timerStop();
} else {
if (seconds > 0) {
timerRun();
}
}
}
function updateTimePicker(diff) {
seconds = clamp(seconds + (diff || 0), 0, 24 * 3600 - 1);
var set_time = secondsToTime(seconds);
updateLayoutField(timePickerLayout, 'hours', set_time.h);
updateLayoutField(timePickerLayout, 'mins', set_time.m);
updateLayoutField(timePickerLayout, 'secs', set_time.s);
}
function updateTimer() {
var timeToNext = alarm.getTimeToAlarm(alarm.getAlarm(TIMER_IDX));
updateLayoutField(timerLayout, 'timer', formatTime(timeToNext / 1000));
queueDraw(1000);
}
function queueDraw(millisecs) {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
updateTimer();
}, millisecs - (Date.now() % millisecs));
}
function timerRun() {
alarm.setAlarm(TIMER_IDX, {
// msg : "Simple Timer",
timer : minutes*60*1000,
vibrate : ".-.-",
hidden: true,
timer : seconds * 1000
});
alarm.reload();
g.clearRect(Bangle.appRect);
timerLayout.render();
updateTimer();
}
function deleteTimer(){
function timerStop() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
var timeToNext = alarm.getTimeToAlarm(alarm.getAlarm(TIMER_IDX));
if (timeToNext != undefined) {
seconds = timeToNext / 1000;
}
alarm.setAlarm(TIMER_IDX, undefined);
alarm.reload();
g.clearRect(Bangle.appRect);
timePickerLayout.render();
updateTimePicker();
}
setWatch(_=>load(), BTN1);
function draw(){
g.clear(1);
Bangle.drawWidgets();
if (interval) {
clearInterval(interval);
}
interval = undefined;
// Write time
g.setFontAlign(0, 0, 0);
g.setFont("Vector", 32).setFontAlign(0,-1);
var started = isTimerEnabled();
var text = minutes + " min.";
if(started){
var min = getTimerMin();
text = min + " min.";
}
var rectWidth = parseInt(g.stringWidth(text) / 2);
if(started){
interval = setInterval(draw, 1000);
g.setColor("#ff0000");
} else {
g.setColor(g.theme.fg);
}
g.fillRect(cx-rectWidth-5, cy-5, cx+rectWidth, cy+30);
g.setColor(g.theme.bg);
g.drawString(text, cx, cy);
}
Bangle.on('touch', function(btn, e){
var left = parseInt(g.getWidth() * 0.25);
var right = g.getWidth() - left;
var upper = parseInt(g.getHeight() * 0.25);
var lower = g.getHeight() - upper;
var isLeft = e.x < left;
var isRight = e.x > right;
var isUpper = e.y < upper;
var isLower = e.y > lower;
var isMiddle = !isLeft && !isRight && !isUpper && !isLower;
var started = isTimerEnabled();
if(isRight && !started){
minutes += 1;
Bangle.buzz(40, 0.3);
} else if(isLeft && !started){
minutes -= 1;
Bangle.buzz(40, 0.3);
} else if(isUpper && !started){
minutes += 5;
Bangle.buzz(40, 0.3);
} else if(isLower && !started){
minutes -= 5;
Bangle.buzz(40, 0.3);
} else if(isMiddle) {
if(!started){
setTimer(minutes);
} else {
deleteTimer();
}
Bangle.buzz(80, 0.6);
}
minutes = Math.max(0, minutes);
draw();
var timePickerLayout = new Layout({
type:"v", c: [
{type:undefined, height:2},
{type:"h", c: [
{type:"v", width:g.getWidth()/3, c: [
{type:"txt", font:"6x8", label:/*LANG*/"Hours"},
{type:"img", pad:8, src:imgArrow},
{type:"txt", font:"20%", label:"00", id:"hours", filly:1, fillx:1},
{type:"img", pad:8, src:imgArrow, r:2}
]},
{type:"v", width:g.getWidth()/3, c: [
{type:"txt", font:"6x8", label:/*LANG*/"Minutes"},
{type:"img", pad:8, src:imgArrow},
{type:"txt", font:"20%", label:"00", id:"mins", filly:1, fillx:1},
{type:"img", pad:8, src:imgArrow, r:2}
]},
{type:"v", width:g.getWidth()/3, c: [
{type:"txt", font:"6x8", label:/*LANG*/"Seconds"},
{type:"img", pad:8, src:imgArrow},
{type:"txt", font:"20%", label:"00", id:"secs", filly:1, fillx:1},
{type:"img", pad:8, src:imgArrow, r:2}
]},
]},
{type:"btn", src:imgPlay, id:"btnStart", fillx:1 }
], filly:1
});
g.reset();
draw();
var timerLayout = new Layout({
type:"v", c: [
{type:"txt", font:"22%", label:"0:00", id:"timer", fillx:1, filly:1 },
{type:"btn", src:imgPause, id:"btnStart", cb: l=>timerStop(), fillx:1 }
], filly:1
});
function updateLayoutField(layout, field, value) {
layout.clear(layout[field]);
layout[field].label = value;
layout.render(layout[field]);
}
Bangle.loadWidgets();
Bangle.drawWidgets();
Bangle.setUI({
mode : "custom",
touch : function(n,e) {onTouch(n,e);},
drag : function(e) {onDrag(e);},
btn : function(n) {onButton();},
});
g.clearRect(Bangle.appRect);
if (timerRunning()) {
timerLayout.render();
updateTimer();
} else {
timePickerLayout.render();
updateTimePicker();
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

View File

@ -2,13 +2,13 @@
"id": "smpltmr",
"name": "Simple Timer",
"shortName": "Simple Timer",
"version": "0.01",
"version": "0.02",
"description": "A very simple app to start a timer.",
"icon": "app.png",
"tags": "tool",
"tags": "tool,alarm,timer",
"dependencies": {"scheduler":"type"},
"supports": ["BANGLEJS2"],
"screenshots": [{"url":"screenshot.png"}, {"url": "screenshot_2.png"}],
"screenshots": [{"url":"screenshot_1.png"}, {"url": "screenshot_2.png"}, {"url": "screenshot_3.png"}, {"url": "screenshot_4.png"}],
"readme": "README.md",
"storage": [
{"name":"smpltmr.app.js","url":"app.js"},

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -1,18 +1,13 @@
/* Copyright (c) 2022 Bangle.js contributors. See the file LICENSE for copying permission. */
/*
Take a look at README.md for hints on developing with this library.
Usage:
```
var Layout = require("Layout");
var layout = new Layout( layoutObject, options )
layout.render(optionalObject);
```
For example:
```
var Layout = require("Layout");
var layout = new Layout( {
@ -24,23 +19,22 @@ var layout = new Layout( {
g.clear();
layout.render();
```
layoutObject has:
* A `type` field of:
* `undefined` - blank, can be used for padding
* `"txt"` - a text label, with value `label` and `r` for text rotation. 'font' is required
* `"txt"` - a text label, with value `label`. 'font' is required
* `"btn"` - a button, with value `label` and callback `cb`
optional `src` specifies an image (like img) in which case label is ignored
Default font is `6x8`, scale 2. This can be overridden with the `font` or `scale` fields.
* `"img"` - an image where `src` is an image, or a function which is called to return an image to draw.
optional `scale` specifies if image should be scaled up or not
* `"custom"` - a custom block where `render(layoutObj)` is called to render
* `"h"` - Horizontal layout, `c` is an array of more `layoutObject`
* `"v"` - Vertical layout, `c` is an array of more `layoutObject`
* A `id` field. If specified the object is added with this name to the
returned `layout` object, so can be referenced as `layout.foo`
* A `font` field, eg `6x8` or `30%` to use a percentage of screen height
* A `font` field, eg `6x8` or `30%` to use a percentage of screen height. Set scale with :, e.g. `6x8:2`.
* A `scale` field, eg `2` to set scale of an image
* A `r` field to set rotation of text or images (0: 0°, 1: 90°, 2: 180°, 3: 270°).
* A `wrap` field to enable line wrapping. Requires some combination of `width`/`height`
and `fillx`/`filly` to be set. Not compatible with text rotation.
* A `col` field, eg `#f00` for red
@ -51,34 +45,25 @@ layoutObject has:
* A `fillx` int to choose if the object should fill available space in x. 0=no, 1=yes, 2=2x more space
* A `filly` int to choose if the object should fill available space in y. 0=no, 1=yes, 2=2x more space
* `width` and `height` fields to optionally specify minimum size
options is an object containing:
* `lazy` - a boolean specifying whether to enable automatic lazy rendering
* `btns` - array of objects containing:
* `label` - the text on the button
* `cb` - a callback function
* `cbl` - a callback function for long presses
* `back` - a callback function, passed as `back` into Bangle.setUI
If automatic lazy rendering is enabled, calls to `layout.render()` will attempt to automatically
determine what objects have changed or moved, clear their previous locations, and re-render just those objects.
Once `layout.update()` is called, the following fields are added
to each object:
* `x` and `y` for the top left position
* `w` and `h` for the width and height
* `_w` and `_h` for the **minimum** width and height
Other functions:
* `layout.update()` - update positions of everything if contents have changed
* `layout.debug(obj)` - draw outlines for objects on screen
* `layout.clear(obj)` - clear the given object (you can also just specify `bgCol` to clear before each render)
* `layout.forgetLazyState()` - if lazy rendering is enabled, makes the next call to `render()` perform a full re-render
*/
@ -259,12 +244,22 @@ Layout.prototype.render = function (l) {
x,y+h-5,
x,y+4
], bg = l.selected?g.theme.bgH:g.theme.bg2;
g.setColor(bg).fillPoly(poly).setColor(l.selected ? g.theme.fgH : g.theme.fg2).drawPoly(poly);
g.setColor(bg).fillPoly(poly).setColor(l.selected ? g.theme.fgH : g.theme.fg2).drawPoly(poly);
if (l.col!==undefined) g.setColor(l.col);
if (l.src) g.setBgColor(bg).drawImage("function"==typeof l.src?l.src():l.src, l.x + 10 + (0|l.pad), l.y + 8 + (0|l.pad));
else g.setFont("6x8",2).setFontAlign(0,0,l.r).drawString(l.label,l.x+l.w/2,l.y+l.h/2);
if (l.src) g.setBgColor(bg).drawImage(
"function"==typeof l.src?l.src():l.src,
l.x + l.w/2,
l.y + l.h/2,
{scale: l.scale||undefined, rotate: Math.PI*0.5*(l.r||0)}
);
else g.setFont(l.font||"6x8:2").setFontAlign(0,0,l.r).drawString(l.label,l.x+l.w/2,l.y+l.h/2);
}, "img":function(l){
g.drawImage("function"==typeof l.src?l.src():l.src, l.x + (0|l.pad), l.y + (0|l.pad), l.scale?{scale:l.scale}:undefined);
g.drawImage(
"function"==typeof l.src?l.src():l.src,
l.x + l.w/2,
l.y + l.h/2,
{scale: l.scale||undefined, rotate: Math.PI*0.5*(l.r||0)}
);
}, "custom":function(l){
l.render(l);
},"h":function(l) { l.c.forEach(render); },
@ -365,7 +360,9 @@ Layout.prototype.update = function() {
l._w = m.width; l._h = m.height;
}
}, "btn": function(l) {
var m = l.src?g.imageMetrics("function"==typeof l.src?l.src():l.src):g.setFont("6x8",2).stringMetrics(l.label);
if (l.font && l.font.endsWith("%"))
l.font = "Vector"+Math.round(g.getHeight()*l.font.slice(0,-1)/100);
var m = l.src?g.imageMetrics("function"==typeof l.src?l.src():l.src):g.setFont(l.font||"6x8:2").stringMetrics(l.label);
l._h = 16 + m.height;
l._w = 20 + m.width;
}, "img": function(l) {