diff --git a/apps/timersimple/ChangeLog b/apps/timersimple/ChangeLog new file mode 100644 index 000000000..7b83706bf --- /dev/null +++ b/apps/timersimple/ChangeLog @@ -0,0 +1 @@ +0.01: First release diff --git a/apps/timersimple/Readme.md b/apps/timersimple/Readme.md new file mode 100644 index 000000000..e940ba0ab --- /dev/null +++ b/apps/timersimple/Readme.md @@ -0,0 +1,10 @@ +# Simple Timer + +Does one thing well. Set a time in hours, minutes and seconds, and alerts you when time is up. Opening the app while the timer is running (or just leaving the app open) shows how much time is left on the timer. This is the part I felt was missing from the Alarms and Timer app. + +Drag or tap on the up and down buttons over the hour, minute or second to set the time. + +![](timersimple-scr1.png) +![](timersimple-scr2.png) + +Written by: [Sir Indy](https://github.com/sir-indy) For support and discussion please post in the [Bangle JS Forum](http://forum.espruino.com/microcosms/1424/) \ No newline at end of file diff --git a/apps/timersimple/icons8-time-span-48.png b/apps/timersimple/icons8-time-span-48.png new file mode 100644 index 000000000..9a9dfb6ab Binary files /dev/null and b/apps/timersimple/icons8-time-span-48.png differ diff --git a/apps/timersimple/metadata.json b/apps/timersimple/metadata.json new file mode 100644 index 000000000..5d0e5d5e4 --- /dev/null +++ b/apps/timersimple/metadata.json @@ -0,0 +1,18 @@ +{ + "id":"timersimple", + "name":"Timer Simple", + "shortName": "Timer Simple", + "version": "0.01", + "description": "Sets a single timer, and tells you how long left.", + "readme": "README.md", + "icon":"icons8-time-span-48.png", + "screenshots": [{"url":"timersimple-scr1.png"},{"url":"timersimple-scr2.png"}], + "tags": "tool,alarm,timer", + "supports": ["BANGLEJS2"], + "dependencies": {"scheduler":"type"}, + "allow_emulator": true, + "storage": [ + {"name":"timersimple.app.js","url":"timersimple.app.js"}, + {"name":"timersimple.img","url":"timersimple.icon.js","evaluate":true} + ] + } \ No newline at end of file diff --git a/apps/timersimple/timersimple.app.js b/apps/timersimple/timersimple.app.js new file mode 100644 index 000000000..ddc6bb4c1 --- /dev/null +++ b/apps/timersimple/timersimple.app.js @@ -0,0 +1,191 @@ +Modules.addCached("Layout",function(){function Layout(d,b){function e(a){a.id&&(h[a.id]=a),a.type||(a.type=''),a.c&&a.c.forEach(e)}this._l=this.l=d,this.physBtns=process.env.HWVERSION==2?1:3,b=b||{},this.lazy=b.lazy||!1;var c,f;if(Bangle.setUI(),process.env.HWVERSION!=2){c=[];function a(b){b.type=='btn'&&c.push(b),b.c&&b.c.forEach(a)}a(d),c.length&&(this.physBtns=0,this.buttons=c,this.selectedButton=-1,Bangle.setUI({mode:'updown',back:b.back},c=>{var a=this.selectedButton,b=this.buttons.length;if(c===undefined&&this.buttons[a])return this.buttons[a].cb();this.buttons[a]&&(delete this.buttons[a].selected,this.render(this.buttons[a])),a=(a+b+c)%b,this.buttons[a]&&(this.buttons[a].selected=1,this.render(this.buttons[a])),this.selectedButton=a}),f=!0)}if(b.back&&!f&&Bangle.setUI({mode:'custom',back:b.back}),b.btns){var a=b.btns;if(this.b=a,this.physBtns>=a.length){function b(a,b){b.time-b.lastTime>.75&&this.b[a].cbl?this.b[a].cbl(b):this.b[a].cb&&this.b[a].cb(b)}let c=Math.floor(Bangle.appRect.h/this.physBtns);Bangle.btnWatches&&Bangle.btnWatches.forEach(clearWatch),Bangle.btnWatches=[],this.physBtns>2&&a.length==1&&a.unshift({label:''});while(this.physBtns>a.length)a.push({label:''});a[0]&&Bangle.btnWatches.push(setWatch(b.bind(this,0),BTN1,{repeat:!0,edge:-1})),a[1]&&Bangle.btnWatches.push(setWatch(b.bind(this,1),BTN2,{repeat:!0,edge:-1})),a[2]&&Bangle.btnWatches.push(setWatch(b.bind(this,2),BTN3,{repeat:!0,edge:-1})),this._l.width=g.getWidth()-8,this._l={type:'h',filly:1,c:[this._l,{type:'v',pad:1,filly:1,c:a.map(a=>(a.type='txt',a.font='6x8',a.height=c,a.r=1,a))}]}}else this._l.width=g.getWidth()-32,this._l={type:'h',c:[this._l,{type:'v',c:a.map(a=>(a.type='btn',a.filly=1,a.width=32,a.r=1,a))}]},c&&c.push.apply(c,this._l.c[1].c)}if(process.env.HWVERSION==2){function a(b,c){b.cb&&c.x>=b.x&&c.y>=b.y&&c.x<=b.x+b.w&&c.y<=b.y+b.h&&(c.type==2&&b.cbl?b.cbl(c):b.cb&&b.cb(c)),b.c&&b.c.forEach(b=>a(b,c))}Bangle.touchHandler=(c,b)=>a(this._l,b),Bangle.on('touch',Bangle.touchHandler)}var h=this;e(this._l),this.updateNeeded=!0}function prepareLazyRender(a,h,b,i,c){var d=a.bgCol==null?c:g.toColor(a.bgCol);if(d!=c||a.type=='txt'||a.type=='btn'||a.type=='img'||a.type=='custom'){var e=a.c;delete a.c;var f='H'+E.CRC32(E.toJS(a));if(e&&(a.c=e),!delete h[f]){var j=i[f]=[a.x,a.y,a.x+a.w-1,a.y+a.h-1];j.bg=c==null?g.theme.bg:c,b&&(b.push(a),b=null)}}if(a.c)for(var k of a.c)prepareLazyRender(k,h,b,i,d)}Layout.prototype.remove=function(a){Bangle.btnWatches&&(Bangle.btnWatches.forEach(clearWatch),delete Bangle.btnWatches),Bangle.touchHandler&&(Bangle.removeListener('touch',Bangle.touchHandler),delete Bangle.touchHandler)},Layout.prototype.render=function(b){function c(a){"ram";g.reset(),a.col!==undefined&&g.setColor(a.col),a.bgCol!==undefined&&g.setBgColor(a.bgCol).clearRect(a.x,a.y,a.x+a.w-1,a.y+a.h-1),f[a.type](a)}b||(b=this._l),this.updateNeeded&&this.update();var f={'':function(){},txt:function(a){if(a.wrap){g.setFont(a.font).setFontAlign(0,-1);var b=g.wrapString(a.label,a.w),c=a.y+(a.h-g.getFontHeight()*b.length>>1);b.forEach((b,d)=>g.drawString(b,a.x+(a.w>>1),c+g.getFontHeight()*d))}else g.setFont(a.font).setFontAlign(0,0,a.r).drawString(a.label,a.x+(a.w>>1),a.y+(a.h>>1))},btn:function(a){var b=a.x+(0|a.pad),c=a.y+(0|a.pad),d=a.w-(a.pad<<1),e=a.h-(a.pad<<1),f=[b,c+4,b+4,c,b+d-5,c,b+d-1,c+4,b+d-1,c+e-5,b+d-5,c+e-1,b+4,c+e-1,b,c+e-5,b,c+4],h=a.selected?g.theme.bgH:g.theme.bg2;g.setColor(h).fillPoly(f).setColor(a.selected?g.theme.fgH:g.theme.fg2).drawPoly(f),a.col!==undefined&&g.setColor(a.col),a.src?g.setBgColor(h).drawImage('f'==(typeof a.src)[0]?a.src():a.src,a.x+10+(0|a.pad),a.y+8+(0|a.pad)):g.setFont('6x8',2).setFontAlign(0,0,a.r).drawString(a.label,a.x+a.w/2,a.y+a.h/2)},img:function(a){g.drawImage('f'==(typeof a.src)[0]?a.src():a.src,a.x+(0|a.pad),a.y+(0|a.pad),a.scale?{scale:a.scale}:undefined)},custom:function(a){a.render(a)},h:function(a){a.c.forEach(c)},v:function(a){a.c.forEach(c)}};if(this.lazy){this.rects||(this.rects={});var a=this.rects.clone(),d=[];prepareLazyRender(b,a,d,this.rects,null);for(var h in a)delete this.rects[h];var i=Object.keys(a).map(b=>a[b]).reverse();for(var e of i)g.setBgColor(e.bg).clearRect.apply(g,e);d.forEach(c)}else c(b)},Layout.prototype.forgetLazyState=function(){this.rects={}},Layout.prototype.layout=function(a){switch(a.type){case'h':{var b=a.x+(0|a.pad),h=0,d=a.c&&a.c.reduce((a,b)=>a+(0|b.fillx),0);d||(b+=a.w-a._w>>1,d=1);var e=b;a.c.forEach(c=>{c.x=0|e,b+=c._w,h+=0|c.fillx,e=b+Math.floor(h*(a.w-a._w)/d),c.w=0|e-c.x,c.h=0|(c.filly?a.h-(a.pad<<1):c._h),c.y=0|a.y+(0|a.pad)+((1+(0|c.valign))*(a.h-(a.pad<<1)-c.h)>>1),c.c&&this.layout(c)});break}case'v':{var c=a.y+(0|a.pad),i=0,f=a.c&&a.c.reduce((a,b)=>a+(0|b.filly),0);f||(c+=a.h-a._h>>1,f=1);var g=c;a.c.forEach(b=>{b.y=0|g,c+=b._h,i+=0|b.filly,g=c+Math.floor(i*(a.h-a._h)/f),b.h=0|g-b.y,b.w=0|(b.fillx?a.w-(a.pad<<1):b._w),b.x=0|a.x+(0|a.pad)+((1+(0|b.halign))*(a.w-(a.pad<<1)-b.w)>>1),b.c&&this.layout(b)});break}}},Layout.prototype.debug=function(a,b){a||(a=this._l),b=b||1,g.setColor(b&1,b&2,b&4).drawRect(a.x+b-1,a.y+b-1,a.x+a.w-b,a.y+a.h-b),a.pad&&g.drawRect(a.x+a.pad-1,a.y+a.pad-1,a.x+a.w-a.pad,a.y+a.h-a.pad),b++,a.c&&a.c.forEach(a=>this.debug(a,b))},Layout.prototype.update=function(){function b(a){"ram";if(c[a.type](a),a.r&1){var b=a._w;a._w=a._h,a._h=b}a._w=0|Math.max(a._w+(a.pad<<1),0|a.width),a._h=0|Math.max(a._h+(a.pad<<1),0|a.height)}delete this.updateNeeded;var c={txt:function(a){if(a.font.endsWith('%')&&(a.font='Vector'+Math.round(g.getHeight()*a.font.slice(0,-1)/100)),a.wrap)a._h=a._w=0;else{var b=g.setFont(a.font).stringMetrics(a.label);a._w=b.width,a._h=b.height}},btn:function(a){var b=a.src?g.imageMetrics('f'==(typeof a.src)[0]?a.src():a.src):g.setFont('6x8',2).stringMetrics(a.label);a._h=16+b.height,a._w=20+b.width},img:function(a){var b=g.imageMetrics('f'==(typeof a.src)[0]?a.src():a.src),c=a.scale||1;a._w=b.width*c,a._h=b.height*c},'':function(a){a._w=0,a._h=0},custom:function(a){a._w=0,a._h=0},h:function(a){a.c.forEach(b),a._h=a.c.reduce((a,b)=>Math.max(a,b._h),0),a._w=a.c.reduce((a,b)=>a+b._w,0),a.fillx==null&&a.c.some(a=>a.fillx)&&(a.fillx=1),a.filly==null&&a.c.some(a=>a.filly)&&(a.filly=1)},v:function(a){a.c.forEach(b),a._h=a.c.reduce((a,b)=>a+b._h,0),a._w=a.c.reduce((a,b)=>Math.max(a,b._w),0),a.fillx==null&&a.c.some(a=>a.fillx)&&(a.fillx=1),a.filly==null&&a.c.some(a=>a.filly)&&(a.filly=1)}},a=this._l;b(a),a.fillx||a.filly?(a.w=Bangle.appRect.w,a.h=Bangle.appRect.h,a.x=Bangle.appRect.x,a.y=Bangle.appRect.y):(a.w=a._w,a.h=a._h,a.x=Bangle.appRect.w-a.w>>1,a.y=Bangle.appRect.y+(Bangle.appRect.h-a.h>>1)),this.layout(a)},Layout.prototype.clear=function(a){a||(a=this._l),g.reset(),a.bgCol!==undefined&&g.setBgColor(a.bgCol),g.clearRect(a.x,a.y,a.x+a.w-1,a.y+a.h-1)},exports=Layout}); + +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); + } +} + +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +var Layout = require("Layout"); +var seconds = 5 * 60; +var drawTimeout; +var timerLayout; +var timePickerLayout; +var imgArrowDown = Graphics.createImage(` +xxx xxx +xxx xxx + xxx xxx + xxx xxx + xxxxx + xxxxx + xxx + xxx + x +`); +var imgArrowUp = Graphics.createImage(` + x + xxx + xxx + xxxxx + xxxxx + xxx xxx + xxx xxx +xxx xxx +xxx xxx +`); + +function onDrag(event) { + var scale = 1; + if (event.x < timePickerLayout.hours.w) { + scale = 3600; + } else if (event.x > timePickerLayout.mins.x && event.x < timePickerLayout.secs.x) { + scale = 60; + } + seconds -= Math.round(event.dy/5) * scale; + updateTimePicker(); +} + +function onTouch(button, xy) { + var touchMidpoint = timePickerLayout.hours.y + timePickerLayout.hours.h/2; + var diff = 0; + if (xy.y > 24 && xy.y < touchMidpoint - 10) { + diff = 1; + } else if (xy.y > touchMidpoint + 10 && xy.y < timePickerLayout.btnStart.y) { + diff = -1; + } else if (xy.y > timePickerLayout.btnStart.y) { + runTimer(); + return; + } + if (xy.x < timePickerLayout.hours.w) { + diff *= 3600; + } else if (xy.x > timePickerLayout.mins.x && xy.x < timePickerLayout.secs.x) { + diff *= 60; + } + seconds += diff; + updateTimePicker(); +} + +function updateTimePicker() { + seconds = clamp(seconds, 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 updateLayoutField(layout, field, value) { + layout.clear(layout[field]); + layout[field].label = value; + layout.render(layout[field]); +} + +function updateTimer() { + var timeToNext = require("sched").getTimeToAlarm(require("sched").getAlarm("simpletimer")); + var d = new Date(); + updateLayoutField(timerLayout, 'timer', formatTime(timeToNext / 1000)); + updateLayoutField(timerLayout, 'time', require("locale").time(d,1)); + queueDraw(1000); +} + +function queueDraw(millisecs) { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + updateTimer(); + }, millisecs - (Date.now() % millisecs)); +} + +function timerStop() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + seconds = require("sched").getTimeToAlarm(require("sched").getAlarm("simpletimer")) / 1000; + require("sched").setAlarm("simpletimer", undefined); + require("sched").reload(); + runTimePicker(); +} + +var timePickerLayoutCode = { + type:"v", c: [ + {type:undefined, height:2}, + //{type:"txt", font:"15%", label:"TIMER", id:"title"}, + {type:"h", c: [ + {type:"v", c: [ + {type:"txt", font:"6x8", label:/*LANG*/"Hours", col:g.theme.fg2}, + {type:"img", pad:8, src:imgArrowUp, col:g.theme.fg2}, + {type:"txt", font:"20%", label:"0", id:"hours", filly:1, fillx:1}, + {type:"img", pad:8, src:imgArrowDown, col:g.theme.fg2} + ]}, + {type:"v", c: [ + {type:"txt", font:"6x8", label:/*LANG*/"Minutes", col:g.theme.fg2}, + {type:"img", pad:8, src:imgArrowUp, col:g.theme.fg2}, + {type:"txt", font:"20%", label:"0", id:"mins", filly:1, fillx:1}, + {type:"img", pad:8, src:imgArrowDown, col:g.theme.fg2} + ]}, + {type:"v", c: [ + {type:"txt", font:"6x8", label:/*LANG*/"Seconds", col:g.theme.fg2}, + {type:"img", pad:8, src:imgArrowUp, col:g.theme.fg2}, + {type:"txt", font:"20%", label:"0", id:"secs", filly:1, fillx:1}, + {type:"img", pad:8, src:imgArrowDown, col:g.theme.fg2} + ]}, + ]}, + {type:"btn", font:"12x20", label:"Start", id:"btnStart", fillx:1 } + ], filly:1 +}; + +var timerLayoutCode = { + type:"v", c: [ + {type:undefined, height:8}, + {type:"txt", font:"6x8", label:/*LANG*/"Timer", id:"title", col:g.theme.fg2}, + {type:"txt", font:"22%", label:"0:00", id:"timer", fillx:1, filly:1 }, + {type:"h", c: [ + {type:"txt", font:"6x8", pad:8, label:/*LANG*/"Time Now:", halign:-1, col:g.theme.fg2}, + {type:"txt", font:"6x8", label:"00:00", id:"time", halign:1, col:g.theme.fg2}, + ]}, + {type:"btn", font:"12x20", label:"Stop", cb: l=>timerStop(), fillx:1 } + ], filly:1 +}; + +function runTimePicker() { + g.clearRect(Bangle.appRect); + timePickerLayout = new Layout(timePickerLayoutCode); + Bangle.setUI({ + mode : "custom", + touch : function(n,e) {onTouch(n,e);}, + drag : function(e) {onDrag(e);}, + btn : function(n) {runTimer();}, + }); + timePickerLayout.render(); + updateTimePicker(); +} + +function runTimer() { + require("sched").setAlarm("simpletimer", { + //msg : "Mr Flibble is very angry!", + vibrate : ".-.-", + hidden: true, + timer : seconds * 1000 + }); + require("sched").reload(); + g.clearRect(Bangle.appRect); + timerLayout = new Layout(timerLayoutCode); + timerLayout.render(); + updateTimer(); +} + +var timeToNext = require("sched").getTimeToAlarm(require("sched").getAlarm("simpletimer")); +if (timeToNext != undefined) { + g.clearRect(Bangle.appRect); + timerLayout = new Layout(timerLayoutCode); + timerLayout.render(); + updateTimer(); +} else { + runTimePicker(); +} diff --git a/apps/timersimple/timersimple.icon.js b/apps/timersimple/timersimple.icon.js new file mode 100644 index 000000000..cd40303a0 --- /dev/null +++ b/apps/timersimple/timersimple.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwcBkmSpIC/ARf//9JkQRMCAIRBAwIRKv4RFpARIz4QCCIdJiREJAAgJCCI0nCI3+BgOJCIs/CI3/9MkyJoIAAxuGp4RJ8gRQ/mSogRDu4RJNwKSEqXfCJPSCImSrYRJ+SkEyVfCJP6CIo1B4wRHUgIREA4MAj4SHCIeUCIP//EAt4RHkQRF//ggIDB+EHCJf/wEAAAQRM/0CoAmCCJf/4VDI5pcCNwoRKNZ4RMUIQRLYowAIYozpRrYRJ+QREqVLCJPSpGSCIdJv5GIyQREpVJfA///mSogRDpNJloRH8mSBwQRDku/CIwMBCIspkmXCAvpkmRCIslAYKkETwMkxIRFkmkyVLNwYJCBwgCDAwyeEAQqSBAwiMEAQwGFBxACDygDBkQOKAX4CD")) \ No newline at end of file