1
0
Fork 0

Merge pull request #2016 from glemco/master

presentation_timer: initial release
master
Gordon Williams 2022-07-04 11:05:41 +01:00 committed by GitHub
commit cfb8b3c90f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 338 additions and 0 deletions

View File

@ -0,0 +1 @@
0.01: first release

View File

@ -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
```

View File

@ -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}
]
}

View File

@ -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<slides.length; i++) {
if(slides[i][0] > 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();

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+AC1WwIZXACmBF7FWAH4Ae/0WAFiQBF9+sAFgv/AAvXAAgvmFgoyWF6IuLGCIvPFpoxRF5wlIwIKJF8lWwIvjQpIvKGBgv8cpWBF5QwLF/4vrEJQvNGBQv/F5FWSCq/XGAVWB5DviEgRiJF8gxDF9q+SF5owWEJYv9GCggMF5wwSD5ovPGCAeOF6AwODp4vRGJYbRF6YAbF/4v/F8eBAYYECAYYvRACFWqwEGwNWwIeSF7IEFAD5VBGhpekAo6QiEYo1LR0QpGBgyOhAxCQfKIIhFGxpegA44+HF85gRA=="))

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB