diff --git a/apps/presentation_timer/ChangeLog b/apps/presentation_timer/ChangeLog new file mode 100644 index 000000000..9db0e26c5 --- /dev/null +++ b/apps/presentation_timer/ChangeLog @@ -0,0 +1 @@ +0.01: first release diff --git a/apps/presentation_timer/README.md b/apps/presentation_timer/README.md new file mode 100644 index 000000000..b24426672 --- /dev/null +++ b/apps/presentation_timer/README.md @@ -0,0 +1,49 @@ +# Presentation Timer + +*Forked from Stopwatch Touch* + +Simple application to keep track of slides and +time during a presentation. Useful for conferences, +lectures or any presentation with a somewhat strict timing. + +The interface is pretty simple, it shows a stopwatch +and the number of the current slide (based on the time), +when the time for the last slide is approaching, +the button becomes red, when it passed, +the time will go on for another half a minute and stop automatically. + +The only way to upload personalized timings is +by uploading a CSV to the bangle (i.e. from the IDE), +in the future I'll possibly figure out a better way. + +Each line in the file (which must be called `presentation_timer.csv`) +contains the time in minutes at which the slide +is supposed to finish and the slide number, +separated by a semicolon. +For instance the line `1.5;1` means that slide 1 +is lasting until 1 minutes 30 seconds (yes it's decimal), +after another slide will start. +The only requirement is that timings are increasing, +so slides number don't have to be consecutive, +some can be skipped and they can even be short texts +(be careful with that, I didn't test it). + +At the moment the app is just quick and dirty +but it should do its job. + +## Screenshots + +![](screenshot1.png) +![](screenshot2.png) +![](screenshot3.png) +![](screenshot4.png) + +## Example configuration file + +_presentation_timer.csv_ +```csv +1.5;1 +2;2 +2.5;3 +3;4 +``` diff --git a/apps/presentation_timer/metadata.json b/apps/presentation_timer/metadata.json new file mode 100644 index 000000000..7a61ffbf0 --- /dev/null +++ b/apps/presentation_timer/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "presentation_timer", + "name": "Presentation Timer", + "version": "0.01", + "description": "A touch based presentation timer for Bangle JS 2", + "icon": "presentation_timer.png", + "screenshots": [{"url":"screenshot1.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"},{"url":"screenshot4.png"}], + "tags": "tools,app", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"presentation_timer.app.js","url":"presentation_timer.app.js"}, + {"name":"presentation_timer.img","url":"presentation_timer.icon.js","evaluate":true} + ] +} diff --git a/apps/presentation_timer/presentation_timer.app.js b/apps/presentation_timer/presentation_timer.app.js new file mode 100644 index 000000000..af2b35dd5 --- /dev/null +++ b/apps/presentation_timer/presentation_timer.app.js @@ -0,0 +1,272 @@ +let w = g.getWidth(); +let h = g.getHeight(); +let tTotal = Date.now(); +let tStart = tTotal; +let tCurrent = tTotal; +let running = false; +let timeY = 2*h/5; +let displayInterval; +let redrawButtons = true; +const iconScale = g.getWidth() / 178; // scale up/down based on Bangle 2 size + +// 24 pixel images, scale to watch +// 1 bit optimal, image string, no E.toArrayBuffer() +const pause_img = atob("GBiBAf////////////////wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP////////////////w=="); +const play_img = atob("GBjBAP//AAAAAAAAAAAIAAAOAAAPgAAP4AAP+AAP/AAP/wAP/8AP//AP//gP//gP//AP/8AP/wAP/AAP+AAP4AAPgAAOAAAIAAAAAAAAAAA="); +const reset_img = atob("GBiBAf////////////AAD+AAB+f/5+f/5+f/5+cA5+cA5+cA5+cA5+cA5+cA5+cA5+cA5+f/5+f/5+f/5+AAB/AAD////////////w=="); + +const margin = 0.5; //half a minute tolerance + +//dummy default values +var slides = [ + [0.3, 1], + [0.5, 2], + [0.7, 3], + [1,4] +]; + +function log_debug(o) { + //console.log(o); +} + +//first must be a number +function readSlides() { + let csv = require("Storage").read("presentation_timer.csv"); + if(!csv) return; + let lines = csv.split("\n").filter(e=>e); + log_debug("Loading "+lines.length+" slides"); + slides = lines.map(line=>{let s=line.split(";");return [+s[0],s[1]];}); +} + + +function timeToText(t) { + let hrs = Math.floor(t/3600000); + let mins = Math.floor(t/60000)%60; + let secs = Math.floor(t/1000)%60; + let tnth = Math.floor(t/100)%10; + let text; + + if (hrs === 0) + text = ("0"+mins).substr(-2) + ":" + ("0"+secs).substr(-2) + "." + tnth; + else + text = ("0"+hrs) + ":" + ("0"+mins).substr(-2) + ":" + ("0"+secs).substr(-2); + + //log_debug(text); + return text; +} + +function drawButtons() { + log_debug("drawButtons()"); + if (!running && tCurrent == tTotal) { + bigPlayPauseBtn.draw(); + } else if (!running && tCurrent != tTotal) { + resetBtn.draw(); + smallPlayPauseBtn.draw(); + } else { + bigPlayPauseBtn.draw(); + } + + redrawButtons = false; +} + +//not efficient but damn easy +function findSlide(time) { + time /= 60000; + //change colour for the last 30 seconds + if(time > slides[slides.length-1][0] - margin && bigPlayPauseBtn.color!="#f00") { + bigPlayPauseBtn.color="#f00"; + drawButtons(); + } + for(let i=0; i time) + return slides[i][1]; + } + //stop automatically + if(time > slides[slides.length-1][0] + margin) { + bigPlayPauseBtn.color="#0ff"; //restore + stopTimer(); + } + return /*LANG*/"end!"; +} + +function drawTime() { + log_debug("drawTime()"); + let Tt = tCurrent-tTotal; + let Ttxt = timeToText(Tt); + + Ttxt += "\n"+findSlide(Tt); + // total time + g.setFont("Vector",38); // check + g.setFontAlign(0,0); + g.clearRect(0, timeY - 42, w, timeY + 42); + g.setColor(g.theme.fg); + g.drawString(Ttxt, w/2, timeY); +} + +function draw() { + let last = tCurrent; + if (running) tCurrent = Date.now(); + g.setColor(g.theme.fg); + if (redrawButtons) drawButtons(); + drawTime(); +} + +function startTimer() { + log_debug("startTimer()"); + draw(); + displayInterval = setInterval(draw, 100); +} + +function stopTimer() { + log_debug("stopTimer()"); + if (displayInterval) { + clearInterval(displayInterval); + displayInterval = undefined; + } +} + +// BTN stop start +function stopStart() { + log_debug("stopStart()"); + + if (running) + stopTimer(); + + running = !running; + Bangle.buzz(); + + if (running) + tStart = Date.now() + tStart- tCurrent; + tTotal = Date.now() + tTotal - tCurrent; + tCurrent = Date.now(); + + setButtonImages(); + redrawButtons = true; + if (running) { + startTimer(); + } else { + draw(); + } +} + +function setButtonImages() { + if (running) { + bigPlayPauseBtn.setImage(pause_img); + smallPlayPauseBtn.setImage(pause_img); + resetBtn.setImage(reset_img); + } else { + bigPlayPauseBtn.setImage(play_img); + smallPlayPauseBtn.setImage(play_img); + resetBtn.setImage(reset_img); + } +} + +// lap or reset +function lapReset() { + log_debug("lapReset()"); + if (!running && tStart != tCurrent) { + redrawButtons = true; + Bangle.buzz(); + tStart = tCurrent = tTotal = Date.now(); + g.clearRect(0,24,w,h); + draw(); + } +} + +// simple on screen button class +function BUTTON(name,x,y,w,h,c,f,i) { + this.name = name; + this.x = x; + this.y = y; + this.w = w; + this.h = h; + this.color = c; + this.callback = f; + this.img = i; +} + +BUTTON.prototype.setImage = function(i) { + this.img = i; +} + +// if pressed the callback +BUTTON.prototype.check = function(x,y) { + //console.log(this.name + ":check() x=" + x + " y=" + y +"\n"); + + if (x>= this.x && x<= (this.x + this.w) && y>= this.y && y<= (this.y + this.h)) { + log_debug(this.name + ":callback\n"); + this.callback(); + return true; + } + return false; +}; + +BUTTON.prototype.draw = function() { + g.setColor(this.color); + g.fillRect(this.x, this.y, this.x + this.w, this.y + this.h); + g.setColor("#000"); // the icons and boxes are drawn black + if (this.img != undefined) { + let iw = iconScale * 24; // the images were loaded as 24 pixels, we will scale + let ix = this.x + ((this.w - iw) /2); + let iy = this.y + ((this.h - iw) /2); + log_debug("g.drawImage(" + ix + "," + iy + "{scale: " + iconScale + "})"); + g.drawImage(this.img, ix, iy, {scale: iconScale}); + } + g.drawRect(this.x, this.y, this.x + this.w, this.y + this.h); +}; + + +var bigPlayPauseBtn = new BUTTON("big",0, 3*h/4 ,w, h/4, "#0ff", stopStart, play_img); +var smallPlayPauseBtn = new BUTTON("small",w/2, 3*h/4 ,w/2, h/4, "#0ff", stopStart, play_img); +var resetBtn = new BUTTON("rst",0, 3*h/4, w/2, h/4, "#ff0", lapReset, pause_img); + +bigPlayPauseBtn.setImage(play_img); +smallPlayPauseBtn.setImage(play_img); +resetBtn.setImage(pause_img); + + +Bangle.on('touch', function(button, xy) { + var x = xy.x; + var y = xy.y; + + // adjust for outside the dimension of the screen + // http://forum.espruino.com/conversations/371867/#comment16406025 + if (y > h) y = h; + if (y < 0) y = 0; + if (x > w) x = w; + if (x < 0) x = 0; + + // not running, and reset + if (!running && tCurrent == tTotal && bigPlayPauseBtn.check(x, y)) return; + + // paused and hit play + if (!running && tCurrent != tTotal && smallPlayPauseBtn.check(x, y)) return; + + // paused and press reset + if (!running && tCurrent != tTotal && resetBtn.check(x, y)) return; + + // must be running + if (running && bigPlayPauseBtn.check(x, y)) return; +}); + +// Stop updates when LCD is off, restart when on +Bangle.on('lcdPower',on=>{ + if (on) { + draw(); // draw immediately, queue redraw + } else { // stop draw timer + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } +}); + +// Clear the screen once, at startup +g.setTheme({bg:"#000",fg:"#fff",dark:true}).clear(); +// above not working, hence using next 2 lines +g.setColor("#000"); +g.fillRect(0,0,w,h); + +Bangle.loadWidgets(); +Bangle.drawWidgets(); +draw(); +setWatch(() => load(), BTN, { repeat: false, edge: "falling" }); +readSlides(); diff --git a/apps/presentation_timer/presentation_timer.icon.js b/apps/presentation_timer/presentation_timer.icon.js new file mode 100644 index 000000000..f18768b2b --- /dev/null +++ b/apps/presentation_timer/presentation_timer.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AC1WwIZXACmBF7FWAH4Ae/0WAFiQBF9+sAFgv/AAvXAAgvmFgoyWF6IuLGCIvPFpoxRF5wlIwIKJF8lWwIvjQpIvKGBgv8cpWBF5QwLF/4vrEJQvNGBQv/F5FWSCq/XGAVWB5DviEgRiJF8gxDF9q+SF5owWEJYv9GCggMF5wwSD5ovPGCAeOF6AwODp4vRGJYbRF6YAbF/4v/F8eBAYYECAYYvRACFWqwEGwNWwIeSF7IEFAD5VBGhpekAo6QiEYo1LR0QpGBgyOhAxCQfKIIhFGxpegA44+HF85gRA==")) diff --git a/apps/presentation_timer/presentation_timer.png b/apps/presentation_timer/presentation_timer.png new file mode 100644 index 000000000..7db9866d7 Binary files /dev/null and b/apps/presentation_timer/presentation_timer.png differ diff --git a/apps/presentation_timer/screenshot1.png b/apps/presentation_timer/screenshot1.png new file mode 100644 index 000000000..d26720d7e Binary files /dev/null and b/apps/presentation_timer/screenshot1.png differ diff --git a/apps/presentation_timer/screenshot2.png b/apps/presentation_timer/screenshot2.png new file mode 100644 index 000000000..cbd6f0bd1 Binary files /dev/null and b/apps/presentation_timer/screenshot2.png differ diff --git a/apps/presentation_timer/screenshot3.png b/apps/presentation_timer/screenshot3.png new file mode 100644 index 000000000..40b375b37 Binary files /dev/null and b/apps/presentation_timer/screenshot3.png differ diff --git a/apps/presentation_timer/screenshot4.png b/apps/presentation_timer/screenshot4.png new file mode 100644 index 000000000..7c43cf91f Binary files /dev/null and b/apps/presentation_timer/screenshot4.png differ