diff --git a/apps/smpltmr/ChangeLog b/apps/smpltmr/ChangeLog index 07afedd21..bf128e2fb 100644 --- a/apps/smpltmr/ChangeLog +++ b/apps/smpltmr/ChangeLog @@ -1 +1,2 @@ -0.01: Release \ No newline at end of file +0.01: Release +0.02: Rewrite with new interface \ No newline at end of file diff --git a/apps/smpltmr/README.md b/apps/smpltmr/README.md index 1296166e2..eeb48d338 100644 --- a/apps/smpltmr/README.md +++ b/apps/smpltmr/README.md @@ -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 CreativeCons - Flaticon \ No newline at end of file diff --git a/apps/smpltmr/app.js b/apps/smpltmr/app.js index eb01e27d0..4e95d3a30 100644 --- a/apps/smpltmr/app.js +++ b/apps/smpltmr/app.js @@ -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(); \ No newline at end of file +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(); +} diff --git a/apps/smpltmr/description.png b/apps/smpltmr/description.png deleted file mode 100644 index 1286d1ab9..000000000 Binary files a/apps/smpltmr/description.png and /dev/null differ diff --git a/apps/smpltmr/metadata.json b/apps/smpltmr/metadata.json index 06bad962d..cb1ef6eab 100644 --- a/apps/smpltmr/metadata.json +++ b/apps/smpltmr/metadata.json @@ -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"}, diff --git a/apps/smpltmr/screenshot.png b/apps/smpltmr/screenshot.png deleted file mode 100644 index eff94475c..000000000 Binary files a/apps/smpltmr/screenshot.png and /dev/null differ diff --git a/apps/smpltmr/screenshot_1.png b/apps/smpltmr/screenshot_1.png new file mode 100644 index 000000000..54eb9d20c Binary files /dev/null and b/apps/smpltmr/screenshot_1.png differ diff --git a/apps/smpltmr/screenshot_2.png b/apps/smpltmr/screenshot_2.png index 7b5dc9a3d..fb0145f17 100644 Binary files a/apps/smpltmr/screenshot_2.png and b/apps/smpltmr/screenshot_2.png differ diff --git a/apps/smpltmr/screenshot_3.png b/apps/smpltmr/screenshot_3.png new file mode 100644 index 000000000..efa10d9c1 Binary files /dev/null and b/apps/smpltmr/screenshot_3.png differ diff --git a/apps/smpltmr/screenshot_4.png b/apps/smpltmr/screenshot_4.png new file mode 100644 index 000000000..c0f984378 Binary files /dev/null and b/apps/smpltmr/screenshot_4.png differ diff --git a/modules/Layout.js b/modules/Layout.js index 620817673..c978c611b 100644 --- a/modules/Layout.js +++ b/modules/Layout.js @@ -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) {