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
|
@ -1 +1,2 @@
|
|||
0.01: Release
|
||||
0.01: Release
|
||||
0.02: Rewrite with new interface
|
|
@ -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.
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
|
||||
# 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>
|
|
@ -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();
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 7.6 KiB |
|
@ -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"},
|
||||
|
|
Before Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 2.9 KiB |
|
@ -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) {
|
||||
|
|