From 211e54760378767e702e38a01c5f70ab8365962a Mon Sep 17 00:00:00 2001 From: Gabriele Monaco Date: Sat, 2 Jul 2022 14:43:03 +0200 Subject: [PATCH] presentation_timer: initial release --- apps/presentation_timer/ChangeLog | 1 + apps/presentation_timer/README.md | 49 ++++ apps/presentation_timer/metadata.json | 15 + .../presentation_timer.app.js | 272 ++++++++++++++++++ .../presentation_timer.icon.js | 1 + .../presentation_timer/presentation_timer.png | Bin 0 -> 1965 bytes apps/presentation_timer/screenshot1.png | Bin 0 -> 1943 bytes apps/presentation_timer/screenshot2.png | Bin 0 -> 1978 bytes apps/presentation_timer/screenshot3.png | Bin 0 -> 2060 bytes apps/presentation_timer/screenshot4.png | Bin 0 -> 2141 bytes 10 files changed, 338 insertions(+) create mode 100644 apps/presentation_timer/ChangeLog create mode 100644 apps/presentation_timer/README.md create mode 100644 apps/presentation_timer/metadata.json create mode 100644 apps/presentation_timer/presentation_timer.app.js create mode 100644 apps/presentation_timer/presentation_timer.icon.js create mode 100644 apps/presentation_timer/presentation_timer.png create mode 100644 apps/presentation_timer/screenshot1.png create mode 100644 apps/presentation_timer/screenshot2.png create mode 100644 apps/presentation_timer/screenshot3.png create mode 100644 apps/presentation_timer/screenshot4.png 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 0000000000000000000000000000000000000000..7db9866d7bff8ea4c630f5012cac50bc9af836ba GIT binary patch literal 1965 zcmV;e2U7TnP)TC7}~ zRBRI-3>90jCPe$NF+p1jMOQAOAfgD?xCq`rV7K#6JL}m!b9Q%j;h|r$+1;7>&o}?f zKmTQpU^E&LMRj%c&wKXlNrm6S)YMdXcXxLW{K?GBL>(O+WTZexZ`iN_ojG%ccE0VP z5)`Hm7zI-BO*%Y0JbH110{-OY=FZgA)VPrj@vOuU=q!AFebL0k1e%+hd&{wI-8!^r z(IOL=;T)N+MFEqqDOUMMg%VtgI~b@ZrOMCFqO*RaI4erKP1|q@zba z9TCwm)Xkh4YqlXugYnO}=cWcw9-!4~Q9?q3*+k7&N<0|Q)zxJZP^nbt{{8z>0lK_^ zyu3V9I~^<$u%u_|Al@Uuu3fu~b#-+L`YjP)?HyJ)7tgpMOO`A_etv$EMKBYfgT)YAu|&Wg$O^#fDHM`b zH5YjE=8egw>EIhTZpaJhbUJb};Di8JyDpj1^z^h<^DsLo05FNNvN9DuoWs|yUCW4z zi~GSU!-*3o{IJ3<@cS%=L?9<8hwTXF2|~rGQ<^Sax7|mYn*cA6(cMLU!1CqGky@>0 z_JNy%lW&zkuEWK}1;xa~NZRY`>(S1gJIQZXS65=nGcz;j=FOXY1gwVxW@l$5ZEkLE zy?dt)95@iGR4OM+N=kZgZ1j+e0ulmS16o^K zi|{#U4-O9QdiLyDAhsg1UPq4}eRucn-E+0IwXO~mU>~DsYHIqL&J>&DH0{G?IS>&M zak;s)f%DgeQ98xFQ}CUBv|vL z6yVe#$a{Kv5?=&9N3Yl0lmky~(@dkIqX~f+OP4N1At51XXlMvMefrd@yj22m#s@fe z?wpw)f;fQ>DjFFX;U|cSijuVT^z=yDAl(5+XD}G}2*Af%0)h|=_`u2fEG7X24;JJI zta)1|!Q#b>xd^O*NdP$;3k3xQQ2U^aj0_YT8|yd$*m+=Jprjo%!-W;Pe*HR0>FCl} z^Jh!~R$v#ya`yfG{d@!fNMqNpUvHHN0L%H}#S4A{&;sJEMx&7xhGnu9Qmvk32<8y@ zSFKuwR<2xWHtXBBZ_65k-2@a+w9>+r7u<;#H-W8;0vBvP*#7n6dJ;RmQ}uuq!Zx^-(l*6x>s2M@mIFhONy<;Bd*%pDd4u0XJHcLwLh zlS2L!5D-9a1Z-s;keorP35gmM@6N(01uTP{JpctEr=xk@t7Las>$s@NAq!kQ-rp!KDDaf8YV!(())kITlO--ns_N2i}&PKYt!2CnsAiupH&! zQUJDBt}0}qq@;v=KX~u};fRk02M1AjcsRKWUAAnQEC&y`6aX&)udw9E8m*$4IZ_9p7}dKES&om#E_V`^&3tD~c13pTO2xVX5p zaIN0n-u@}hK$X~)UWWI6&T_)s1)K_xsZ^@Zoq5pJt5hQ|jLB+qaX;Pn@YHBLSt}rqxDyk(uKE77mM|S{#b0X}J z1kZrTI zW)g{1ze=gpn#;S52~jkTT2~(2?w;%KyFc&#_`W>P=llJ9zt89SKF>ERz~4hh%UBD6 zKeRbJFE5{f`*gnCh3b|!|tFKuTI6ScwN#lL6&Xuh8Tg0>BYc-qIz|iDVoB1&Dzn?n!$>-)j zr9cbu{r;C`hsFGpZx#1T_aDg3$Am^t0wyklb|hS_y%W$OdlcY7B9u(aKE0&N3UOCI(*5P$~5Q)V7vSjDMx%`c_TZQW~e6Qs&4X1r4Jj=WDk zUcPN`d+zD$_V<PhSNruo+_K97>q;l}FezB9v% zpnhqrR8^eRfzW2QK`V=+A&s8+}$69&nd${ct*jT@H@N7CGFmhPRu>vcLox7`*y zz3iK-SfgGeqwSlLm93Y1$*uTlkaMG@?b%z@=(`oGTDO?@;ep~Miq9S8Go7Lb#p6Y~ zKrwjt3!(l~cA(2eEi!hR%bvBm_YsTWTwa|Tzh<|53%=q3V^mnTIHGmxCs%#%y5lL? z1H<~U`*zE^F%g2x^>4JuHEG0NN?rO@X~;ag!<>=hqhjXpI#<>BJ%hYGnnr*=T`~!Rn|k&re2aXC&58TOP8_2 zFOfsS{ay@VtcBC&$36<1j~ftiOsb&kOs(S&LhBIlxZm8^#161)Pq{I`03dXvTeu4V zg~V9Y>91qMj9ohoP1*3042sV`gPnpLho{l$0a%nXU9+3P82RnQWBhFDE+;F1qv39h zN-*I+QHOPxyUG18l4l&ZrL~kwP(%%BX7gSRsKh4Y#94mI;r&%FqpRWzX6_JwzNxH? zj?aK!ha{(tywXI~2IRqjAfIN6DNFc{0I+T?c)QVcqd;Yho7^h|m8ch04PzwGy%VK>@4EXRbjEK{ngB=5{Ex4Kkg~D}l4q0v7JUEH4u|d0^mszaM zI@EOR)y$#Ad(`TtsUJzex)_fEFtZc|Ni`KotL&n=2n}5ixj#E3&i=lcNjhBW+&B|| z#>Y{cW6z$6DObPn76~d4Ja~=7Hv?M(878^W-!MT#wT9yZg+$Czi@LmMR8UwZjI)j< z0X#M;2#m1uprfn-9{hY1A^`4m)cHm<=LI|Klb$nuGC&(X&cj5uCA@Hw1G4Itutqq5;Su!zOZ* z>W)?f9UMbC%Sk09UKRok6B$$%7uaTq z`y0K948B#%L=tmN_hu3by_W-)bv=}@cJDS9IiYZpbHme{1QCV~>XxKgT551!dDd!( zDHS8w8VZjbV<@acgJQ~4%-2y$_$wJp)%_RQwGAE!PFrqjx;_92B;_~JYM_;!o-s|O z3x(Im1d+F-o>87_*|64zBW);WITK&DHVvWL;$JpSYfLoijZx$EMpO?M;pOH}t)|e= F`~mFOW4r(W literal 0 HcmV?d00001 diff --git a/apps/presentation_timer/screenshot2.png b/apps/presentation_timer/screenshot2.png new file mode 100644 index 0000000000000000000000000000000000000000..cbd6f0bd106f77f0886e5d2ba6fe97f143d26e78 GIT binary patch literal 1978 zcmds2X;V{Y6izNTlCYT=kgX6_7qAfp1W7QF1Q4WX5CdAoplEDy0g**agED80q1Khv??Mp>O~{A7Jup7>_o8N|DM`n(f0^>rD&PCB9UNQv zF@dCRQA@t*i#xUVqI99o+3MahcY9<2%@PFRr z6v6}oW!Zc3!-wj@h;3c-9Yp&-Z#~~1M(Hp&FR9YzfQvO##{BMi$81Hyp4kVF%qtUi z&pI~DTq2b$%coC;(@Pbp<1b56LN-nARk@B141(q-=6tizI&n*)+Uj98Dm0AOY;NUv zo0X8uIG>Vwa^X15+GygV)+?cTT>HSWm5JRn?fN{%d^LvS^viQI##yhMd*wwf-YZHB zWa<6KD*5iU<*;#BZl$PuUMSBB<>@W?_8F++o$*IPtwRke^NZn*7oH^ zkH@3gK#9-38h_-13wXZts@coi7>+G7kC7r$oe~~#G5mqJKfg|cJ?*XFhALG1l>)oG z`vm8M;$g7Z6|!#Nz9H*rUGx26+q(+0nirVy#TJJta=)Gk zps~QEe!rOu+}0>yc}?HkTRW15vNW}I`47@$M;cR!%E}?6Tz_*@BWO%9@iSWFH*$?> zO!Xqhoj=`K!CSxBL(64v)?uq|%xtOhpXqpwpBr^b-Qwi7)o!A+h+=ouc&@(ZaO1-4 zS(0QEk9b9!Y#$Zg;F#2Ma5!xyTG>>9Yj1LRty;Rbu%*pMg?fJf~b7aBH zQtB$?W^>{^E^NrPmLejq>!-2b7f~``a|tB?*REj#xrg9nY4(5%Gz6-WyjUyS>I3*~ zn#PcJhg~mW=$zcS4=1}g5ab)kdM8fKf@3z|WZ6fYv>;(Q5F>z*I~r}#A!sMN$rC;j z+^5{{e22vB=$#u4c(CpO{SYA>RNWaoC)YiWlOd-&s@r9niU#k#vlp*{J(=2JIT_es zdLE8XlA`ym^JXDi>jL>kXYolX-q_%z7+a}H!(xzaYzPnb$$DD2TCvmO)T#yfVYDDT z4fcs_!u;BIH(jNf$G literal 0 HcmV?d00001 diff --git a/apps/presentation_timer/screenshot3.png b/apps/presentation_timer/screenshot3.png new file mode 100644 index 0000000000000000000000000000000000000000..40b375b3756eed96782c008ccf61a6e95f58e9de GIT binary patch literal 2060 zcmdUw`8%757RTe2nm4wJo!+7}v=S0kgs7U>*Q!!$HQctg-r7wN%R97V3EEPMpq^UC<;=4a=%)fV``m7gEM~@OjEvFBa3uUewbz9@Q_UPvK`bY4$ zELs|_p6(vyE=M-d4rfIGEffP?cYEsGUJ(NLg&Yik;|=lB*)Z|& z#bkAuI+D+75AVdgUAwwebrJP7qFD)DkalZ3lX5~}I3{;;h^61;3}Q}-AFRF(DBe&lbGk+Wpw z*(4Yn7{QDLKt@(>3KpfZX*=qTEk*#psvQ=yR<5w#jrrs$LHEnCG$oI^X31A*_rV7h z{r;)l`%h~?To4AxfK6UlPy`kSI*qkTk0f)qADu@N>zQgXRrzKd<0X*M`SGiN2%3og z79~$RwA+^wtbeiHbElvMWQ@5X9cSvd6Yv{?8UmBn$uNq>J8Qu~!ld&OL}HPyu{_DuKG>T@Z^N5$ zk^rUim)=0m;;l%iSeSbMYX_;vkVXVZGOVMTzQ=j}=I-a#J5^XYX8;nE)`tF!l81`n zchle+k`6mkWQJw{sm2Ln0r7auWlVT=Aw@b1G<^|@K3cC?8lM}Z_3jQ)%W zR7FA>9FM#N2HdW+BJtU8+Pt?D^tK<5vy-5b9eY0R0CFFQfPKX@I1z5!iVz{_d6zmH1QJ*s<=E%88q?SadRS%#W9hVo${ zzYu1JplHt~M#%m|<$xam5|!G88mG>fth(F-5(_uxkw*j=#N9gw1pk|7Yk^fC4|}=w z`3nWZ;juAq6z>(n2TJ@swM-hPkqQ%8!2nI-IMk literal 0 HcmV?d00001 diff --git a/apps/presentation_timer/screenshot4.png b/apps/presentation_timer/screenshot4.png new file mode 100644 index 0000000000000000000000000000000000000000..7c43cf91f5235da6e9ad29da81c197c62d4fd377 GIT binary patch literal 2141 zcmdT`X;72r8cjaZ5LsfPS1nsuGztk-u^eEswf`u* z@L(FRqMNV?%T}w{VH6=>=(hzX9K!&Ze^Rn#9uN1wl{ye5b##qJDr*(yGh6Q4(7#$_5T1pqUCY)S0!jw632h2dB;_A zK!jcwB?a_aNIjpp8VZJWnAFGV+kxau>Zq^8rp%Id(v>yn6WK_)y6<-oczZfqdn*(n z=XgDc(KiNw1CqHPT~bITNzyL3{|s?dNq|96>p9Q-7Goi`PFk(sOD-)CUIwU>X!DNIF3JE?6wtr^s3=1`= z@8O@Uj>3P_gl{RX#<%E2B7L+qP<_20V9d4w^ku4Lh-QJGezUrGhTEAEU{@JslN z)?1D7pu_6YJ!#_x34^+MYxY#e5tpSk%1%FSWU9jndX@2cV+q2Mp0&4r1nDT5b#r!s zgCTLD$TkBoWc=y@OX=S>g};6~iM{2~_bXboym6{g^X`w?uP>b~12eb#epeuL>I`)-O8c*{l@-YI48EwvQ zOalQ&(n$QjGa`ERUdqbWi+>iR?!#;c4H!j)euU`*VUb+f7B$Rn$|BN+$`zQ^5O6 z1%>~qQc-`Zpz!}dg={rcO@i=)F&9*mKy7dNx+?HyurmP)Rz37zX?{{qY*#bp+