diff --git a/apps/about/app-bangle2.js b/apps/about/app-bangle2.js index 0c40314a8..471b0670f 100644 --- a/apps/about/app-bangle2.js +++ b/apps/about/app-bangle2.js @@ -10,7 +10,7 @@ var img = atob("sIwDkm2S66DYwA2AAAAAHAHGSRxJEkAAgmGGBxDIADIdAFJIbAHF9HP00kBUC6Dt var imgHeight = g.imageMetrics(img).height; var imgScroll = Math.floor(Math.random()*imgHeight); -g.reset().setFont("6x15").setFontAlign(0,0); +g.clear(1).setFont("6x15").setFontAlign(0,0); g.drawString(ENV.VERSION + " " + NRF.getAddress(), g.getWidth()/2, 171); g.drawImage(img,0,24); diff --git a/apps/accellog/ChangeLog b/apps/accellog/ChangeLog index 80981fe27..94241c7a7 100644 --- a/apps/accellog/ChangeLog +++ b/apps/accellog/ChangeLog @@ -2,3 +2,4 @@ 0.02: Use the new multiplatform 'Layout' library 0.03: Exit as first menu option, dont show decimal places for seconds 0.04: Localisation, change Exit->Back to allow back-arrow to appear on 2v13 firmware +0.05: Add max G values during recording, record actual G values and magnitude to CSV diff --git a/apps/accellog/app.js b/apps/accellog/app.js index f4c1b3c5a..147f7503f 100644 --- a/apps/accellog/app.js +++ b/apps/accellog/app.js @@ -1,5 +1,6 @@ var fileNumber = 0; var MAXLOGS = 9; +var logRawData = false; function getFileName(n) { return "accellog."+n+".csv"; @@ -24,6 +25,11 @@ function showMenu() { /*LANG*/"View Logs" : function() { viewLogs(); }, + /*LANG*/"Log raw data" : { + value : logRawData, + format : v => v?/*LANG*/"Yes":/*LANG*/"No", + onchange : v => { logRawData=v; } + }, }; E.showMenu(menu); } @@ -78,6 +84,7 @@ function viewLogs() { } function startRecord(force) { + var stopped = false; if (!force) { // check for existing file var f = require("Storage").open(getFileName(fileNumber), "r"); @@ -92,39 +99,101 @@ function startRecord(force) { var Layout = require("Layout"); var layout = new Layout({ type: "v", c: [ - {type:"txt", font:"6x8", label:/*LANG*/"Samples", pad:2}, - {type:"txt", id:"samples", font:"6x8:2", label:" - ", pad:5, bgCol:g.theme.bg}, - {type:"txt", font:"6x8", label:/*LANG*/"Time", pad:2}, - {type:"txt", id:"time", font:"6x8:2", label:" - ", pad:5, bgCol:g.theme.bg}, - {type:"txt", font:"6x8:2", label:/*LANG*/"RECORDING", bgCol:"#f00", pad:5, fillx:1}, - ] - },{btns:[ // Buttons... - {label:/*LANG*/"STOP", cb:()=>{ - Bangle.removeListener('accel', accelHandler); - showMenu(); + { type: "h", c: [ + { type: "v", c: [ + {type:"txt", font:"6x8", label:/*LANG*/"Samples", pad:2}, + {type:"txt", id:"samples", font:"6x8:2", label:" - ", pad:5, bgCol:g.theme.bg}, + ]}, + { type: "v", c: [ + {type:"txt", font:"6x8", label:/*LANG*/"Time", pad:2}, + {type:"txt", id:"time", font:"6x8:2", label:" - ", pad:5, bgCol:g.theme.bg}, + ]}, + ]}, + { type: "h", c: [ + { type: "v", c: [ + {type:"txt", font:"6x8", label:/*LANG*/"Max X", pad:2}, + {type:"txt", id:"maxX", font:"6x8", label:" - ", pad:5, bgCol:g.theme.bg}, + ]}, + { type: "v", c: [ + {type:"txt", font:"6x8", label:/*LANG*/"Max Y", pad:2}, + {type:"txt", id:"maxY", font:"6x8", label:" - ", pad:5, bgCol:g.theme.bg}, + ]}, + { type: "v", c: [ + {type:"txt", font:"6x8", label:/*LANG*/"Max Z", pad:2}, + {type:"txt", id:"maxZ", font:"6x8", label:" - ", pad:5, bgCol:g.theme.bg}, + ]}, + ]}, + {type:"txt", font:"6x8", label:/*LANG*/"Max G", pad:2}, + {type:"txt", id:"maxMag", font:"6x8:4", label:" - ", pad:5, bgCol:g.theme.bg}, + {type:"txt", id:"state", font:"6x8:2", label:/*LANG*/"RECORDING", bgCol:"#f00", pad:5, fillx:1}, + ]}, + { + btns:[ // Buttons... + {id: "btnStop", label:/*LANG*/"STOP", cb:()=>{ + if (stopped) { + showMenu(); + } + else { + Bangle.removeListener('accel', accelHandler); + layout.state.label = /*LANG*/"STOPPED"; + layout.state.bgCol = /*LANG*/"#0f0"; + stopped = true; + layout.render(); + } }} ]}); layout.render(); // now start writing var f = require("Storage").open(getFileName(fileNumber), "w"); - f.write("Time (ms),X,Y,Z\n"); + f.write("Time (ms),X,Y,Z,Total\n"); var start = getTime(); var sampleCount = 0; + var maxMag = 0; + var maxX = 0; + var maxY = 0; + var maxZ = 0; function accelHandler(accel) { var t = getTime()-start; - f.write([ - t*1000, - accel.x*8192, - accel.y*8192, - accel.z*8192].map(n=>Math.round(n)).join(",")+"\n"); + if (logRawData) { + f.write([ + t*1000, + accel.x*8192, + accel.y*8192, + accel.z*8192, + accel.mag*8192, + ].map(n=>Math.round(n)).join(",")+"\n"); + } else { + f.write([ + Math.round(t*1000), + accel.x, + accel.y, + accel.z, + accel.mag, + ].join(",")+"\n"); + } + if (accel.mag > maxMag) { + maxMag = accel.mag.toFixed(2); + } + if (accel.x > maxX) { + maxX = accel.x.toFixed(2); + } + if (accel.y > maxY) { + maxY = accel.y.toFixed(2); + } + if (accel.z > maxZ) { + maxZ = accel.z.toFixed(2); + } sampleCount++; layout.samples.label = sampleCount; layout.time.label = Math.round(t)+"s"; - layout.render(layout.samples); - layout.render(layout.time); + layout.maxX.label = maxX; + layout.maxY.label = maxY; + layout.maxZ.label = maxZ; + layout.maxMag.label = maxMag; + layout.render(); } Bangle.setPollInterval(80); // 12.5 Hz - the default diff --git a/apps/accellog/metadata.json b/apps/accellog/metadata.json index fdf6cf320..903c57903 100644 --- a/apps/accellog/metadata.json +++ b/apps/accellog/metadata.json @@ -2,7 +2,7 @@ "id": "accellog", "name": "Acceleration Logger", "shortName": "Accel Log", - "version": "0.04", + "version": "0.05", "description": "Logs XYZ acceleration data to a CSV file that can be downloaded to your PC", "icon": "app.png", "tags": "outdoor", diff --git a/apps/agenda/ChangeLog b/apps/agenda/ChangeLog index 16a90242b..0a7916810 100644 --- a/apps/agenda/ChangeLog +++ b/apps/agenda/ChangeLog @@ -3,4 +3,5 @@ 0.03: Disable past events display from settings 0.04: Added awareness of allDay field 0.05: Displaying calendar colour and name -0.06: Added clkinfo for clocks. \ No newline at end of file +0.06: Added clkinfo for clocks. +0.07: Clkinfo improvements. \ No newline at end of file diff --git a/apps/agenda/README.md b/apps/agenda/README.md index 7063a70a2..1a0ec9264 100644 --- a/apps/agenda/README.md +++ b/apps/agenda/README.md @@ -10,11 +10,21 @@ Basic agenda reading the events synchronised from GadgetBridge. * Show the colour of the calendar in the list * Display description, location and calendar name after tapping on events +### Troubleshooting + +For the events sync to work, GadgetBridge needs to have the calendar permission and calendar sync should be enabled in the devices settings (gear sign in GB, also check the blacklisted calendars there, if events are missing). +Keep in mind that GadgetBridge won't synchronize all events on your calendar, just the ones in a time window of 7 days (you don't want your watch to explode), ideally every day old events get deleted since they appear out of such window. + +#### Force Sync + +If for any reason events still cannot sync or some are missing, you can try any of the following (just one, you normally don't need to do this): +1. from GB open the burger menu (side), tap debug and set time. +2. from the bangle, open settings > apps > agenda > Force calendar sync, then select not to delete the local events (this is equivalent to option 1). +3. do like option 2 but delete events, GB will synchronize a fresh database instead of patching the old one (good in case you somehow cannot get rid of older events) + +After any of the options, you may need to disconnect/force close Gadgetbridge before reconnecting and let it sync (give it some time for that too), restart the agenda app on the bangle after a while to see the changes. + ### Report a bug You can easily open an issue in the espruino repo, but I won't be notified and it might take time. If you want a (hopefully) quicker response, just report [on my fork](https://github.com/glemco/BangleApps). - -### Known Problems - -Any all-day event lasts just one day: that is a GB limitation that we will likely fix in the future. diff --git a/apps/agenda/agenda.clkinfo.js b/apps/agenda/agenda.clkinfo.js index a80c09002..6c2ddb3da 100644 --- a/apps/agenda/agenda.clkinfo.js +++ b/apps/agenda/agenda.clkinfo.js @@ -18,7 +18,7 @@ dateStr += entry.durationInSeconds < 86400 ? "/ " + locale.time(date,1) : ""; agendaItems.items.push({ - name: "agendaEntry-" + i, + name: null, get: () => ({ text: title + "\n" + dateStr, img: null}), show: function() { agendaItems.items[i].emit("redraw"); }, hide: function () {} diff --git a/apps/agenda/metadata.json b/apps/agenda/metadata.json index 2bce8ca56..6d91455f0 100644 --- a/apps/agenda/metadata.json +++ b/apps/agenda/metadata.json @@ -1,7 +1,7 @@ { "id": "agenda", "name": "Agenda", - "version": "0.06", + "version": "0.07", "description": "Simple agenda", "icon": "agenda.png", "screenshots": [{"url":"screenshot_agenda_overview.png"}, {"url":"screenshot_agenda_event1.png"}, {"url":"screenshot_agenda_event2.png"}], diff --git a/apps/agpsdata/ChangeLog b/apps/agpsdata/ChangeLog index ae26512de..8ada244d7 100644 --- a/apps/agpsdata/ChangeLog +++ b/apps/agpsdata/ChangeLog @@ -1,2 +1,5 @@ 0.01: First, proof of concept 0.02: Load AGPS data on app start and automatically in background +0.03: Do not load AGPS data on boot + Increase minimum interval to 6 hours +0.04: Write AGPS data chunks with delay to improve reliability diff --git a/apps/agpsdata/app.js b/apps/agpsdata/app.js index 647723bb4..4a6d2ba5c 100644 --- a/apps/agpsdata/app.js +++ b/apps/agpsdata/app.js @@ -36,7 +36,7 @@ function updateAgps() { g.clear(); if (!waiting) { waiting = true; - display("Updating A-GPS..."); + display("Updating A-GPS...", "takes ~ 10 seconds"); require("agpsdata").pull(function() { waiting = false; display("A-GPS updated.", "touch to close"); diff --git a/apps/agpsdata/boot.js b/apps/agpsdata/boot.js index 6415f0b52..2b1e6819c 100644 --- a/apps/agpsdata/boot.js +++ b/apps/agpsdata/boot.js @@ -16,13 +16,6 @@ } if (settings.enabled) { - let lastUpdate = settings.lastUpdate; - if (!lastUpdate || lastUpdate + settings.refresh * 1000 * 60 < Date.now()){ - if (!waiting){ - waiting = true; - require("agpsdata").pull(successCallback, errorCallback); - } - } setInterval(() => { if (!waiting && NRF.getSecurityStatus().connected){ waiting = true; diff --git a/apps/agpsdata/lib.js b/apps/agpsdata/lib.js index 7d9758c0a..34608a5c6 100644 --- a/apps/agpsdata/lib.js +++ b/apps/agpsdata/lib.js @@ -8,41 +8,52 @@ var FILE = "agpsdata.settings.json"; var settings; readSettings(); -function setAGPS(data) { - var js = jsFromBase64(data); - try { - eval(js); - return true; - } - catch(e) { - console.log("error:", e); - } - return false; +function setAGPS(b64) { + return new Promise(function(resolve, reject) { + var initCommands = "Bangle.setGPSPower(1);\n"; // turn GPS on + const gnsstype = settings.gnsstype || 1; // default GPS + initCommands += `Serial1.println("${CASIC_CHECKSUM("$PCAS04," + gnsstype)}")\n`; // set GNSS mode + // What about: + // NAV-TIMEUTC (0x01 0x10) + // NAV-PV (0x01 0x03) + // or AGPS.zip uses AID-INI (0x0B 0x01) + + eval(initCommands); + + try { + writeChunks(atob(b64), resolve); + } catch (e) { + console.log("error:", e); + reject(); + } + }); } -function jsFromBase64(b64) { - var bin = atob(b64); - var chunkSize = 128; - var js = "Bangle.setGPSPower(1);\n"; // turn GPS on - var gnsstype = settings.gnsstype || 1; // default GPS - js += `Serial1.println("${CASIC_CHECKSUM("$PCAS04,"+gnsstype)}")\n`; // set GNSS mode - // What about: - // NAV-TIMEUTC (0x01 0x10) - // NAV-PV (0x01 0x03) - // or AGPS.zip uses AID-INI (0x0B 0x01) +var chunkI = 0; +function writeChunks(bin, resolve) { + return new Promise(function(resolve2) { + const chunkSize = 128; + setTimeout(function() { + if (chunkI < bin.length) { + var chunk = bin.substr(chunkI, chunkSize); + js = `Serial1.write(atob("${btoa(chunk)}"))\n`; + eval(js); - for (var i=0;i { - let result = setAGPS(event.resp); - if (result) { - updateLastUpdate(); - if (successCallback) successCallback(); - } else { - console.log("error applying AGPS data"); - if (failureCallback) failureCallback("Error applying AGPS data"); - } - }).catch((e)=>{ - console.log("error", e); - if (failureCallback) failureCallback(e); - }); + const uri = "https://www.espruino.com/agps/casic.base64"; + if (Bangle.http) { + Bangle.http(uri, {timeout : 10000}) + .then(event => { + setAGPS(event.resp) + .then(r => { + updateLastUpdate(); + if (successCallback) + successCallback(); + }) + .catch((e) => { + console.log("error", e); + if (failureCallback) + failureCallback(e); + }); + }) + .catch((e) => { + console.log("error", e); + if (failureCallback) + failureCallback(e); + }); } else { console.log("error: No http method found"); - if (failureCallback) failureCallback(/*LANG*/"No http method"); + if (failureCallback) + failureCallback(/*LANG*/ "No http method"); } }; diff --git a/apps/agpsdata/metadata.json b/apps/agpsdata/metadata.json index e2f818d97..203a00f72 100644 --- a/apps/agpsdata/metadata.json +++ b/apps/agpsdata/metadata.json @@ -2,7 +2,7 @@ "name": "A-GPS Data Downloader App", "shortName":"A-GPS Data", "icon": "agpsdata.png", - "version":"0.02", + "version":"0.04", "description": "Once installed, this app allows you to download assisted GPS (A-GPS) data directly to your Bangle.js **via Gadgetbridge on an Android phone** when you run the app. If you just want to upload the latest AGPS data from this app loader, please use the `Assisted GPS Update (AGPS)` app.", "tags": "boot,tool,assisted,gps,agps,http", "allow_emulator":true, diff --git a/apps/agpsdata/settings.js b/apps/agpsdata/settings.js index 80a2f3956..64fa25330 100644 --- a/apps/agpsdata/settings.js +++ b/apps/agpsdata/settings.js @@ -35,7 +35,7 @@ function buildMainMenu() { }, "Refresh every" : { value : settings.refresh / 60, - min : 1, + min : 6, max : 168, step : 1, format : v => v + "h", diff --git a/apps/aiclock/ChangeLog b/apps/aiclock/ChangeLog new file mode 100644 index 000000000..31c55aef1 --- /dev/null +++ b/apps/aiclock/ChangeLog @@ -0,0 +1,3 @@ +0.01: New app! +0.02: Design improvements and fixes. +0.03: Indicate battery level through line occurrence. \ No newline at end of file diff --git a/apps/aiclock/README.md b/apps/aiclock/README.md new file mode 100644 index 000000000..9e23de3a6 --- /dev/null +++ b/apps/aiclock/README.md @@ -0,0 +1,23 @@ +# AI Clock +This clock was designed by stable diffusion ([paper](https://arxiv.org/abs/2112.10752)) using the following prompt: + +`A rectangle banglejs watchface` + + +The original output of stable diffusion is shown here: + +![](orig.png) + +My implementation is shown below. Note that horizontal lines occur randomly, but the +probability is correlated with the battery level. So if your screen contains only +a few lines its time to charge your bangle again ;) + +![](impl.png) + + +# Thanks to +The great open-source community: I used an open-source diffusion model (https://github.com/CompVis/stable-diffusion) +to generate a watch face for the open-source smartwatch BangleJs. + +## Creator +- [David Peer](https://github.com/peerdavid). \ No newline at end of file diff --git a/apps/aiclock/aiclock.app.js b/apps/aiclock/aiclock.app.js new file mode 100644 index 000000000..dbd053f2c --- /dev/null +++ b/apps/aiclock/aiclock.app.js @@ -0,0 +1,225 @@ +/** + * AI Clock + */ +require("Font7x11Numeric7Seg").add(Graphics); +Graphics.prototype.setFontGochiHand = function(scale) { + // Actual height 27 (29 - 3) + this.setFontCustom( + atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAA8AAAAADwAAAAAPAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAA/+AAAB//4AAH///gAH///gAAf//AAAB/+AAAAH8AAAAAAAAAAAAAAAAAAAAH8AAAAB/8AAAAP/4AAAB//wAAAPx/AAAB8B+AAAHgD4AAA+AHgAADwAeAAAPAB4AAA8AHgAAD4AeAAAPgB4AAAeAPgAAB8A8AAAH4HwAAAP/+AAAAf/wAAAA/+AAAAB/wAAAAB8AAAAAAAAAAADgAAAAAfAAAAAB4AAAAAPAAAAAB8AAAAAHgAAAAA8AAAAADwAAAAAf4AAAAB//8AAAD//4AAAH//gAAAD/+AAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4AHgAAHgA+AAA/AD4AAD4AfgAAfAD+AAB4Af4AAHgD/gAAeAfeAAB4D54AAHw/HgAAf/4fAAA//B8AAD/4DwAAH+APAAAHgA8AAAAADwAAAAAOAAAAAAAAABgAAAAAPAAAAAB8AAAAAHwAYAAAeAD4AAD4APwAAPA4fgAA8Hw+AADwfB4AAPh4HwAA+HgPAAB/+A8AAH/4DwAAP/weAAAf/j4AAAc//gAAAB/8AAAAD/gAAAAD8AAAAAAAAAAAAAAAAAADAAAAAA+AAAAAP4AAAAB/wAAAAP/AAAAD+8AAAAfzwAAAf8HAAAB/gcAAAH/hwAAAf//gAAA//+AAAAf//gAAAP//gAAAD/+AAAAB/4AAAAH/AAAAAeAAAAAAgAAAAAAAAAAAAcAAAB8H8AAAP4f4AAA/x/wAAD/H/gAAf+A+AAB74B4AAHnwHgAAefAfAAB58A8AAHj4DwAAePgPAAB4fA8AAHh+HgAAeD8+AAB4P/4AAHgf/AAAeA/4AAAAA+AAAAAAAAAAAAAAAAAAHgAAAAD/wAAAA//gAAAH//AAAA//+AAAD4H8AAAfA/wAAB4D/AAAHgP+AAAeB54AAB4HngAAHweeAAAfB54AAA4HngAAAAeeAAAAB/4AAAAH/AAAAAP4AAAAAfAAADwAAAAAPAAAAAA8HgAAADweAAAAPB4AAAA8HgAAADweAAAAPh4AAAA+HgAAAB4eAAAAHx4AAAAf//8AAA///wAAD//+AAAH//4AAAAeAAAAAB4AAAAAHgAAAAAeAAAAAB4AAAAAHgAAAAAAAAAAAAAAAAAAD+AAAA+f+AAAH//8AAA///wAAH/4fgAAePgeAAB4+B4AAHj4HwAAePgPAAB4+A8AAHz4DwAAfngeAAA//B4AAD/+HgAAH//8AAAP//wAAAAf+AAAAA/wAAAAAYAAAAAAAAAAA/gAAAAH/AAAAA/8AAAAD34AAAAeHgAAAB4eAAAAHh4AAAA8HgAAADweAAAAPDwAAAA8PAAAADx4AAAAPvgAAAAf///AAB///8AAH///wAAP///AAA/wA4AABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOA4AAAA8DwAAADwPAAAAPA8AAAAYBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=='), + 46, + atob("DQoXEBQVExUUFRYUDQ=="), + 40+(scale<<8)+(1<<16) + ); + return this; +} + +/* + * Set some important constants such as width, height and center + */ +var W = g.getWidth(),R=W/2; +var H = g.getHeight(); +var cx = W/2; +var cy = H/2; +var drawTimeout; + +/* + * Based on the great multi clock from https://github.com/jeffmer/BangleApps/ + */ +Graphics.prototype.drawRotRect = function(w, r1, r2, angle) { + angle = angle % 360; + var w2=w/2, h=r2-r1, theta=angle*Math.PI/180; + return this.fillPoly(this.transformVertices([-w2,0,-w2,-h,w2,-h,w2,0], + {x:cx+r1*Math.sin(theta),y:cy-r1*Math.cos(theta),rotate:theta})); +}; + + +function drawBackground() { + g.setFontAlign(0,0); + g.setColor(g.theme.fg); + + var bat = E.getBattery() / 100.0; + var y = 0; + while(y < H){ + // Show less lines in case of small battery level. + if(Math.random() > bat){ + y += 5; + continue; + } + + y += 3 + Math.floor(Math.random() * 10); + g.drawLine(0, y, W, y); + g.drawLine(0, y+1, W, y+1); + g.drawLine(0, y+2, W, y+2); + y += 2; + } +} + + +function drawCircle(isLocked){ + g.setColor(g.theme.fg); + g.fillCircle(cx, cy, 12); + + var c = isLocked ? "#f00" : g.theme.bg; + g.setColor(c); + g.fillCircle(cx, cy, 6); +} + +function toAngle(a){ + if (a < 0){ + return 360 + a; + } + + if(a > 360) { + return 360 - a; + } + + return a +} + +function drawTime(){ + var drawHourHand = g.drawRotRect.bind(g,8,12,R-38); + var drawMinuteHand = g.drawRotRect.bind(g,6,12,R-12 ); + + g.setFontAlign(0,0); + + // Compute angles + var date = new Date(); + var m = parseInt(date.getMinutes() * 360 / 60); + var h = date.getHours(); + h = h > 12 ? h-12 : h; + h += date.getMinutes()/60.0; + h = parseInt(h*360/12); + + // Draw minute and hour bg + g.setColor(g.theme.bg); + drawHourHand(toAngle(h-3)); + drawHourHand(toAngle(h+3)); + drawMinuteHand(toAngle(m-2)); + drawMinuteHand(toAngle(m+3)); + + // Draw minute and hour fg + g.setColor(g.theme.fg); + drawHourHand(h); + drawMinuteHand(m); +} + + + +function drawDate(){ + var date = new Date(); + g.setFontAlign(0,0); + g.setFontGochiHand(); + + var text = ("0"+date.getDate()).substr(-2) + "/" + ("0"+(date.getMonth()+1)).substr(-2); + var w = g.stringWidth(text); + g.setColor(g.theme.bg); + g.fillRect(cx-w/2-4, 20, cx+w/2+4, 40+12); + + g.setColor(g.theme.fg); + // Draw right line as designed by stable diffusion + g.drawLine(cx+w/2+5, 20, cx+w/2+5, 40+12); + g.drawLine(cx+w/2+6, 20, cx+w/2+6, 40+12); + g.drawLine(cx+w/2+7, 20, cx+w/2+7, 40+12); + + // And finally the text + g.drawString(text, cx, 40); +} + + +function drawDigits(){ + var date = new Date(); + + g.setFontAlign(0,0); + g.setFont("7x11Numeric7Seg",3); + + var text = ("0"+date.getHours()).substr(-2) + ":" + ("0"+date.getMinutes()).substr(-2); //Bangle.getHealthStatus("day").steps; + var w = g.stringWidth(text); + g.setColor(g.theme.bg); + g.fillRect(cx-w/2-4, 120, cx+w/2+4, 140+20); + + // Draw right line as designed by stable diffusion + g.setColor(g.theme.fg); + g.drawLine(cx+w/2+5, 120, cx+w/2+5, 140+20); + g.drawLine(cx+w/2+6, 120, cx+w/2+6, 140+20); + g.drawLine(cx+w/2+7, 120, cx+w/2+7, 140+20); + + // And the 7set text + g.setColor("#BBB"); + g.drawString("88:88", cx, 140); + g.drawString("88:88", cx+1, 140); + g.drawString("88:88", cx, 141); + + g.setColor(g.theme.fg); + g.drawString(text, cx, 140); + g.drawString(text, cx+1, 140); + g.drawString(text, cx, 141); +} + + + +function draw(){ + // Queue draw in one minute + queueDraw(); + + + g.reset(); + g.clearRect(0, 0, g.getWidth(), g.getHeight()); + + g.setColor(1,1,1); + drawBackground(); + drawDate(); + drawDigits(); + drawTime(); + drawCircle(Bangle.isLocked()); +} + + +/* + * Listeners + */ +Bangle.on('lcdPower',on=>{ + if (on) { + draw(true); + } else { // stop draw timer + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } +}); + +Bangle.on('lock', function(isLocked) { + drawCircle(isLocked); +}); + + +/* + * Some helpers + */ +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +} + + + +/* + * Lets start widgets, listen for btn etc. + */ +// Show launcher when middle button pressed +Bangle.setUI("clock"); +Bangle.loadWidgets(); +/* + * we are not drawing the widgets as we are taking over the whole screen + * so we will blank out the draw() functions of each widget and change the + * area to the top bar doesn't get cleared. + */ +for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";} + +// Clear the screen once, at startup and draw clock +g.setTheme({bg:"#fff",fg:"#000",dark:false}).clear(); +draw(); + +// After drawing the watch face, we can draw the widgets +// Bangle.drawWidgets(); diff --git a/apps/aiclock/aiclock.icon.js b/apps/aiclock/aiclock.icon.js new file mode 100644 index 000000000..0033b3848 --- /dev/null +++ b/apps/aiclock/aiclock.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgP/ACfAEZU/ECZELIKhSR/+PAoWAv4FDhk/x/ggP+j0fx/AgP8n8PCIX8CwIFC/F/w4FBgP4gEHC4QFE//w//DC4QFB8YFC+P/8IdCAoYdBAoPxDoQAd+CiKh4dQwDhfAA4A=")) \ No newline at end of file diff --git a/apps/aiclock/aiclock.png b/apps/aiclock/aiclock.png new file mode 100644 index 000000000..104261254 Binary files /dev/null and b/apps/aiclock/aiclock.png differ diff --git a/apps/aiclock/impl.png b/apps/aiclock/impl.png new file mode 100644 index 000000000..92374b680 Binary files /dev/null and b/apps/aiclock/impl.png differ diff --git a/apps/aiclock/metadata.json b/apps/aiclock/metadata.json new file mode 100644 index 000000000..2124b1b7e --- /dev/null +++ b/apps/aiclock/metadata.json @@ -0,0 +1,20 @@ +{ + "id": "aiclock", + "name": "AI Clock", + "shortName":"AI Clock", + "icon": "aiclock.png", + "version":"0.03", + "readme": "README.md", + "supports": ["BANGLEJS2"], + "description": "A watch face that was designed by an AI (stable diffusion) and implemented by a human.", + "type": "clock", + "tags": "clock", + "screenshots": [ + {"url":"orig.png"}, + {"url":"impl.png"} + ], + "storage": [ + {"name":"aiclock.app.js","url":"aiclock.app.js"}, + {"name":"aiclock.img","url":"aiclock.icon.js","evaluate":true} + ] +} diff --git a/apps/aiclock/orig.png b/apps/aiclock/orig.png new file mode 100644 index 000000000..009826454 Binary files /dev/null and b/apps/aiclock/orig.png differ diff --git a/apps/alpinenav/ChangeLog b/apps/alpinenav/ChangeLog new file mode 100644 index 000000000..b3d1e0874 --- /dev/null +++ b/apps/alpinenav/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! +0.02: Added adjustment for Bangle.js magnetometer heading fix diff --git a/apps/alpinenav/app.js b/apps/alpinenav/app.js index 29eeab0c9..7cffc39c3 100644 --- a/apps/alpinenav/app.js +++ b/apps/alpinenav/app.js @@ -224,7 +224,7 @@ Bangle.on('mag', function (m) { if (isNaN(m.heading)) compass_heading = "---"; else - compass_heading = 360 - Math.round(m.heading); + compass_heading = Math.round(m.heading); current_colour = g.getColor(); g.reset(); g.setColor(background_colour); diff --git a/apps/alpinenav/metadata.json b/apps/alpinenav/metadata.json index dcb56e912..c5a0e0611 100644 --- a/apps/alpinenav/metadata.json +++ b/apps/alpinenav/metadata.json @@ -1,7 +1,7 @@ { "id": "alpinenav", "name": "Alpine Nav", - "version": "0.01", + "version": "0.02", "description": "App that performs GPS monitoring to track and display position relative to a given origin in realtime", "icon": "app-icon.png", "tags": "outdoors,gps", diff --git a/apps/android/ChangeLog b/apps/android/ChangeLog index 0cc7aedd4..a65326941 100644 --- a/apps/android/ChangeLog +++ b/apps/android/ChangeLog @@ -13,3 +13,4 @@ 0.13: Added Bangle.http function (see Readme file for more info) 0.14: Fix timeout of http function not being cleaned up 0.15: Allow method/body/headers to be specified for `http` (needs Gadgetbridge 0.68.0b or later) +0.16: Bangle.http now fails immediately if there is no Bluetooth connection (fix #2152) diff --git a/apps/android/boot.js b/apps/android/boot.js index bc8e3032d..0d1edae99 100644 --- a/apps/android/boot.js +++ b/apps/android/boot.js @@ -139,6 +139,8 @@ // options = {id,timeout,xpath} Bangle.http = (url,options)=>{ options = options||{}; + if (!NRF.getSecurityStatus().connected) + return Promise.reject("Not connected to Bluetooth"); if (Bangle.httpRequest === undefined) Bangle.httpRequest={}; if (options.id === undefined) { diff --git a/apps/android/metadata.json b/apps/android/metadata.json index 5d1b2f561..ab340340c 100644 --- a/apps/android/metadata.json +++ b/apps/android/metadata.json @@ -2,7 +2,7 @@ "id": "android", "name": "Android Integration", "shortName": "Android", - "version": "0.15", + "version": "0.16", "description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.", "icon": "app.png", "tags": "tool,system,messages,notifications,gadgetbridge", diff --git a/apps/antonclk/ChangeLog b/apps/antonclk/ChangeLog index 9e75d889a..4ef0cee75 100644 --- a/apps/antonclk/ChangeLog +++ b/apps/antonclk/ChangeLog @@ -12,3 +12,5 @@ 0.08: fixed calendar weeknumber not shortened to two digits 0.09: Use default Bangle formatter for booleans 0.10: Use Bangle.setUI({remove:...}) to allow loading the launcher without a full reset on 2v16 +0.11: Moved enhanced Anton clock to 'Anton Clock Plus' and stripped this clock back down to make it faster for new users (270ms -> 170ms) + Modified to avoid leaving functions defined when using setUI({remove:...}) diff --git a/apps/antonclk/app.js b/apps/antonclk/app.js index 07f67f696..528866588 100644 --- a/apps/antonclk/app.js +++ b/apps/antonclk/app.js @@ -4,234 +4,42 @@ Graphics.prototype.setFontAnton = function(scale) { g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAA/gAAAAAAAAAAP/gAAAAAAAAAH//gAAAAAAAAB///gAAAAAAAAf///gAAAAAAAP////gAAAAAAD/////gAAAAAA//////gAAAAAP//////gAAAAH///////gAAAB////////gAAAf////////gAAP/////////gAD//////////AA//////////gAA/////////4AAA////////+AAAA////////gAAAA///////wAAAAA//////8AAAAAA//////AAAAAAA/////gAAAAAAA////4AAAAAAAA///+AAAAAAAAA///gAAAAAAAAA//wAAAAAAAAAA/8AAAAAAAAAAA/AAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//////AAAAAB///////8AAAAH////////AAAAf////////wAAA/////////4AAB/////////8AAD/////////+AAH//////////AAP//////////gAP//////////gAP//////////gAf//////////wAf//////////wAf//////////wAf//////////wA//8AAAAAB//4A//wAAAAAAf/4A//gAAAAAAP/4A//gAAAAAAP/4A//gAAAAAAP/4A//wAAAAAAf/4A///////////4Af//////////wAf//////////wAf//////////wAf//////////wAP//////////gAP//////////gAH//////////AAH//////////AAD/////////+AAB/////////8AAA/////////4AAAP////////gAAAD///////+AAAAAf//////4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/gAAAAAAAAAAP/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/AAAAAAAAAAA//AAAAAAAAAAA/+AAAAAAAAAAB/8AAAAAAAAAAD//////////gAH//////////gAP//////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/4AAAAB/gAAD//4AAAAf/gAAP//4AAAB//gAA///4AAAH//gAB///4AAAf//gAD///4AAA///gAH///4AAD///gAP///4AAH///gAP///4AAP///gAf///4AAf///gAf///4AB////gAf///4AD////gA////4AH////gA////4Af////gA////4A/////gA//wAAB/////gA//gAAH/////gA//gAAP/////gA//gAA///8//gA//gAD///w//gA//wA////g//gA////////A//gA///////8A//gA///////4A//gAf//////wA//gAf//////gA//gAf/////+AA//gAP/////8AA//gAP/////4AA//gAH/////gAA//gAD/////AAA//gAB////8AAA//gAA////wAAA//gAAP///AAAA//gAAD//8AAAA//gAAAP+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/+AAAAAD/wAAB//8AAAAP/wAAB///AAAA//wAAB///wAAB//wAAB///4AAD//wAAB///8AAH//wAAB///+AAP//wAAB///+AAP//wAAB////AAf//wAAB////AAf//wAAB////gAf//wAAB////gA///wAAB////gA///wAAB////gA///w//AAf//wA//4A//AAA//wA//gA//AAAf/wA//gB//gAAf/wA//gB//gAAf/wA//gD//wAA//wA//wH//8AB//wA///////////gA///////////gA///////////gA///////////gAf//////////AAf//////////AAP//////////AAP/////////+AAH/////////8AAH///+/////4AAD///+f////wAAA///8P////gAAAf//4H///+AAAAH//gB///wAAAAAP4AAH/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/wAAAAAAAAAA//wAAAAAAAAAP//wAAAAAAAAB///wAAAAAAAAf///wAAAAAAAH////wAAAAAAA/////wAAAAAAP/////wAAAAAB//////wAAAAAf//////wAAAAH///////wAAAA////////wAAAP////////wAAA///////H/wAAA//////wH/wAAA/////8AH/wAAA/////AAH/wAAA////gAAH/wAAA///4AAAH/wAAA//+AAAAH/wAAA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gAAAAAAAAH/4AAAAAAAAAAH/wAAAAAAAAAAH/wAAAAAAAAAAH/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB//8AAA/////+B///AAA/////+B///wAA/////+B///4AA/////+B///8AA/////+B///8AA/////+B///+AA/////+B////AA/////+B////AA/////+B////AA/////+B////gA/////+B////gA/////+B////gA/////+A////gA//gP/gAAB//wA//gf/AAAA//wA//gf/AAAAf/wA//g//AAAAf/wA//g//AAAA//wA//g//gAAA//wA//g//+AAP//wA//g////////gA//g////////gA//g////////gA//g////////gA//g////////AA//gf///////AA//gf//////+AA//gP//////+AA//gH//////8AA//gD//////4AA//gB//////wAA//gA//////AAAAAAAH////8AAAAAAAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//////gAAAAB///////+AAAAH////////gAAAf////////4AAB/////////8AAD/////////+AAH//////////AAH//////////gAP//////////gAP//////////gAf//////////wAf//////////wAf//////////wAf//////////wAf//////////4A//wAD/4AAf/4A//gAH/wAAP/4A//gAH/wAAP/4A//gAP/wAAP/4A//gAP/4AAf/4A//wAP/+AD//4A///wP//////4Af//4P//////wAf//4P//////wAf//4P//////wAf//4P//////wAP//4P//////gAP//4H//////gAH//4H//////AAH//4D/////+AAD//4D/////8AAB//4B/////4AAA//4A/////wAAAP/4AP////AAAAB/4AD///4AAAAAAAAAH/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//AAAAAAAAAAA//gAAAAAAAAAA//gAAAAAAAAAA//gAAAAAAADgA//gAAAAAAP/gA//gAAAAAH//gA//gAAAAB///gA//gAAAAP///gA//gAAAD////gA//gAAAf////gA//gAAB/////gA//gAAP/////gA//gAB//////gA//gAH//////gA//gA///////gA//gD///////gA//gf///////gA//h////////gA//n////////gA//////////gAA/////////AAAA////////wAAAA///////4AAAAA///////AAAAAA//////4AAAAAA//////AAAAAAA/////4AAAAAAA/////AAAAAAAA////8AAAAAAAA////gAAAAAAAA///+AAAAAAAAA///4AAAAAAAAA///AAAAAAAAAA//4AAAAAAAAAA/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//gB///wAAAAP//4H///+AAAA///8P////gAAB///+f////4AAD///+/////8AAH/////////+AAH//////////AAP//////////gAP//////////gAf//////////gAf//////////wAf//////////wAf//////////wA///////////wA//4D//wAB//4A//wB//gAA//4A//gA//gAAf/4A//gA//AAAf/4A//gA//gAAf/4A//wB//gAA//4A///P//8AH//4Af//////////wAf//////////wAf//////////wAf//////////wAf//////////gAP//////////gAP//////////AAH//////////AAD/////////+AAD///+/////8AAB///8f////wAAAf//4P////AAAAH//wD///8AAAAA/+AAf//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH//gAAAAAAAAB///+AA/+AAAAP////gA//wAAAf////wA//4AAB/////4A//8AAD/////8A//+AAD/////+A///AAH/////+A///AAP//////A///gAP//////A///gAf//////A///wAf//////A///wAf//////A///wAf//////A///wA///////AB//4A//4AD//AAP/4A//gAB//AAP/4A//gAA//AAP/4A//gAA/+AAP/4A//gAB/8AAP/4A//wAB/8AAf/4Af//////////wAf//////////wAf//////////wAf//////////wAf//////////wAP//////////gAP//////////gAH//////////AAH/////////+AAD/////////8AAB/////////4AAAf////////wAAAP////////AAAAB///////4AAAAAD/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/AAB/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="), 46, atob("EiAnGicnJycnJycnEw=="), 78 + (scale << 8) + (1 << 16)); }; -Graphics.prototype.setFontAntonSmall = function(scale) { - // Actual height 53 (52 - 0) - g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAAAAAAAAAAAAAMAAAAAAAAD8AAAAAAAA/8AAAAAAAf/8AAAAAAH//8AAAAAB///8AAAAA////8AAAAP////8AAAD/////8AAB//////8AAf//////8AH///////4A///////+AA///////AAA//////wAAA/////8AAAA////+AAAAA////gAAAAA///4AAAAAA//8AAAAAAA//AAAAAAAA/wAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/////wAAA//////8AAB//////+AAH///////gAH///////gAP///////wAf///////4Af///////4A////////8A////////8A////////8A//AAAAD/8A/8AAAAA/8A/8AAAAA/8A/8AAAAA/8A/+AAAAB/8A////////8A////////8A////////8Af///////4Af///////4AP///////wAP///////wAH///////gAD///////AAA//////8AAAP/////wAAAAAAAAAAAAAAAAAAAAAAAfwAAAAAAAA/4AAAAAAAA/4AAAAAAAB/wAAAAAAAB/wAAAAAAAD/wAAAAAAAD/gAAAAAAAH///////8AP///////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/4AAAP8AA//4AAA/8AB//4AAH/8AH//4AAP/8AP//4AA//8AP//4AB//8Af//4AD//8Af//4AP//8A///4Af//8A///4A///8A///4D///8A//AAH///8A/8AAP///8A/8AA//+/8A/8AD//8/8A/+Af//w/8A//////g/8A/////+A/8A/////8A/8Af////4A/8Af////wA/8AP////AA/8AP///+AA/8AH///8AA/8AD///wAA/8AA///AAA/8AAP/4AAA/8AAAAAAAAAAAAAAAAAAAAAAH4AAf/gAAA/4AAf/8AAD/4AAf//AAH/4AAf//gAP/4AAf//wAP/4AAf//wAf/4AAf//4Af/4AAf//4A//4AAf//8A//4AAf//8A//4AAP//8A//A/8AB/8A/8A/8AA/8A/8B/8AA/8A/8B/8AA/8A/+D//AB/8A////////8A////////8A////////8Af///////4Af///////4Af///////wAP///////gAH//9////gAD//4///+AAB//wf//4AAAP/AH//gAAAAAAAAAAAAAAAAAAAAAAAAAAAH/wAAAAAAB//wAAAAAAP//wAAAAAD///wAAAAA////wAAAAH////wAAAB/////wAAAf/////wAAD//////wAA///////wAA/////h/wAA////wB/wAA///8AB/wAA///AAB/wAA//gAAB/wAA////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8AAAAAAB/wAAAAAAAB/wAAAAAAAB/wAAAAAAAAAAAAAAAAAAAAAAAAAAAP/4AA////4P/+AA////4P//AA////4P//gA////4P//wA////4P//wA////4P//4A////4P//4A////4P//8A////4P//8A////4P//8A/8H/AAB/8A/8H+AAA/8A/8P+AAA/8A/8P+AAA/8A/8P/gAD/8A/8P/////8A/8P/////8A/8P/////8A/8P/////4A/8H/////4A/8H/////wA/8D/////wA/8B/////gA/8A////+AA/8AP///4AAAAAB///AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////wAAAf/////8AAB///////AAH///////gAP///////wAP///////wAf///////4Af///////4A////////8A////////8A////////8A/+AH/AB/8A/8AP+AA/8A/4Af+AA/8A/8Af+AA/8A/8Af/gH/8A//4f////8A//4f////8A//4f////8Af/4f////4Af/4f////4AP/4P////wAP/4P////gAH/4H////AAD/4D///+AAB/4B///4AAAP4AP//gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8AAAAAAAA/8AAAAAAAA/8AAAAAB8A/8AAAAB/8A/8AAAAf/8A/8AAAH//8A/8AAA///8A/8AAH///8A/8AA////8A/8AD////8A/8Af////8A/8B/////8A/8P/////8A/8//////8A////////AA///////AAA//////gAAA/////4AAAA/////AAAAA////4AAAAA////AAAAAA///8AAAAAA///gAAAAAA//+AAAAAAA//wAAAAAAA/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/gD//gAAA//4P//8AAD//8f///AAH//+////gAH///////wAP///////4AP///////8Af///////8Af///////+Af///////+A////////+A//B//AB/+A/+A/+AA/+A/8Af+AA/+A/+Af+AA/+A//A//AB/+A////////+Af///////+Af///////+Af///////8Af///////8AP///////4AH///////4AH//+////wAD//+////AAA//4P//+AAAP/gH//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH//gAfgAAA///8A/8AAB///+A//AAH////A//gAH////g//wAP////g//wAf////w//4Af////w//4A/////w//8A/////w//8A/////w//8A//gP/wA/8A/8AD/wA/8A/8AD/wAf8A/8AD/gA/8A/+AH/AB/8A////////8A////////8A////////8Af///////4Af///////4Af///////wAP///////wAH///////gAD//////+AAA//////4AAAP/////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="), 46, atob("DhgeFB4eHh4eHh4eDw=="), 60 + (scale << 8) + (1 << 16)); -}; - { // must be inside our own scope here so that when we are unloaded everything disappears - -const SETTINGSFILE = "antonclk.json"; -const isBangle1 = (process.env.HWVERSION == 1); - -// variables defined from settings -let secondsMode; -let secondsColoured; -let secondsWithColon; -let dateOnMain; -let dateOnSecs; -let weekDay; -let calWeek; -let upperCase; -let vectorFont; - -// dynamic variables + // we also define functions using 'let fn = function() {..}' for the same reason. function decls are global let drawTimeout; -let queueMillis = 1000; -let secondsScreen = true; +// Actually draw the watch face +let draw = function() { + var x = g.getWidth() / 2; + var y = g.getHeight() / 2; + g.reset().clearRect(Bangle.appRect); // clear whole background (w/o widgets) + var date = new Date(); + var timeStr = require("locale").time(date, 1); // Hour and minute + g.setFontAlign(0, 0).setFont("Anton").drawString(timeStr, x, y); + // Show date and day of week + var dateStr = require("locale").date(date, 0).toUpperCase()+"\n"+ + require("locale").dow(date, 0).toUpperCase(); + g.setFontAlign(0, 0).setFont("6x8", 2).drawString(dateStr, x, y+48); - -//For development purposes -/* -require('Storage').writeJSON(SETTINGSFILE, { - secondsMode: "Unlocked", // "Never", "Unlocked", "Always" - secondsColoured: true, - secondsWithColon: true, - dateOnMain: "Long", // "Short", "Long", "ISO8601" - dateOnSecs: "Year", // "No", "Year", "Weekday", LEGACY: true/false - weekDay: true, - calWeek: true, - upperCase: true, - vectorFont: true, -}); -*/ - -// OR (also for development purposes) -/* -require('Storage').erase(SETTINGSFILE); -*/ - -// Load settings -function loadSettings() { - // Helper function default setting - function def (value, def) {return value !== undefined ? value : def;} - - var settings = require('Storage').readJSON(SETTINGSFILE, true) || {}; - secondsMode = def(settings.secondsMode, "Never"); - secondsColoured = def(settings.secondsColoured, true); - secondsWithColon = def(settings.secondsWithColon, true); - dateOnMain = def(settings.dateOnMain, "Long"); - dateOnSecs = def(settings.dateOnSecs, "Year"); - weekDay = def(settings.weekDay, true); - calWeek = def(settings.calWeek, false); - upperCase = def(settings.upperCase, true); - vectorFont = def(settings.vectorFont, false); - - // Legacy - if (dateOnSecs === true) - dateOnSecs = "Year"; - if (dateOnSecs === false) - dateOnSecs = "No"; -} - -// schedule a draw for the next second or minute -function queueDraw() { + // queue next draw if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = setTimeout(function() { drawTimeout = undefined; draw(); - }, queueMillis - (Date.now() % queueMillis)); -} + }, 60000 - (Date.now() % 60000)); +}; -function updateState() { - if (Bangle.isLCDOn()) { - if ((secondsMode === "Unlocked" && !Bangle.isLocked()) || secondsMode === "Always") { - secondsScreen = true; - queueMillis = 1000; - } else { - secondsScreen = false; - queueMillis = 60000; - } - draw(); // draw immediately, queue redraw - } else { // stop draw timer - if (drawTimeout) clearTimeout(drawTimeout); - drawTimeout = undefined; - } -} - -function isoStr(date) { - return date.getFullYear() + "-" + ("0" + (date.getMonth() + 1)).slice(-2) + "-" + ("0" + date.getDate()).slice(-2); -} - -let calWeekBuffer = [false,false,false]; //buffer tz, date, week no (once calculated until other tz or date is requested) -function ISO8601calWeek(date) { //copied from: https://gist.github.com/IamSilviu/5899269#gistcomment-3035480 - dateNoTime = date; dateNoTime.setHours(0,0,0,0); - if (calWeekBuffer[0] === date.getTimezoneOffset() && calWeekBuffer[1] === dateNoTime) return calWeekBuffer[2]; - calWeekBuffer[0] = date.getTimezoneOffset(); - calWeekBuffer[1] = dateNoTime; - var tdt = new Date(date.valueOf()); - var dayn = (date.getDay() + 6) % 7; - tdt.setDate(tdt.getDate() - dayn + 3); - var firstThursday = tdt.valueOf(); - tdt.setMonth(0, 1); - if (tdt.getDay() !== 4) { - tdt.setMonth(0, 1 + ((4 - tdt.getDay()) + 7) % 7); - } - calWeekBuffer[2] = 1 + Math.ceil((firstThursday - tdt) / 604800000); - return calWeekBuffer[2]; -} - -function doColor() { - return !isBangle1 && !Bangle.isLocked() && secondsColoured; -} - -// Actually draw the watch face -function draw() { - var x = g.getWidth() / 2; - var y = g.getHeight() / 2 - (secondsMode !== "Never" ? 24 : (vectorFont ? 12 : 0)); - g.reset(); - /* This is to mark the widget areas during development. - g.setColor("#888") - .fillRect(0, 0, g.getWidth(), 23) - .fillRect(0, g.getHeight() - 23, g.getWidth(), g.getHeight()).reset(); - /* */ - g.clearRect(0, 24, g.getWidth(), g.getHeight() - 24); // clear whole background (w/o widgets) - var date = new Date(); // Actually the current date, this one is shown - var timeStr = require("locale").time(date, 1); // Hour and minute - g.setFontAlign(0, 0).setFont("Anton").drawString(timeStr, x, y); // draw time - if (secondsScreen) { - y += 65; - var secStr = (secondsWithColon ? ":" : "") + ("0" + date.getSeconds()).slice(-2); - if (doColor()) - g.setColor(0, 0, 1); - g.setFont("AntonSmall"); - if (dateOnSecs !== "No") { // A bit of a complex drawing with seconds on the right and date on the left - g.setFontAlign(1, 0).drawString(secStr, g.getWidth() - (isBangle1 ? 32 : 2), y); // seconds - y -= (vectorFont ? 15 : 13); - x = g.getWidth() / 4 + (isBangle1 ? 12 : 4) + (secondsWithColon ? 0 : g.stringWidth(":") / 2); - var dateStr2 = (dateOnMain === "ISO8601" ? isoStr(date) : require("locale").date(date, 1)); - var year; - var md; - var yearfirst; - if (dateStr2.match(/\d\d\d\d$/)) { // formatted date ends with year - year = (dateOnSecs === "Year" ? dateStr2.slice(-4) : require("locale").dow(date, 1)); - md = dateStr2.slice(0, -4); - if (!md.endsWith(".")) // keep separator before the year only if it is a dot (31.12. but 31/12) - md = md.slice(0, -1); - yearfirst = false; - } else { // formatted date begins with year - if (!dateStr2.match(/^\d\d\d\d/)) // if year position cannot be detected... - dateStr2 = isoStr(date); // ...use ISO date format instead - year = (dateOnSecs === "Year" ? dateStr2.slice(0, 4) : require("locale").dow(date, 1)); - md = dateStr2.slice(5); // never keep separator directly after year - yearfirst = true; - } - if (dateOnSecs === "Weekday" && upperCase) - year = year.toUpperCase(); - g.setFontAlign(0, 0); - if (vectorFont) - g.setFont("Vector", 24); - else - g.setFont("6x8", 2); - if (doColor()) - g.setColor(1, 0, 0); - g.drawString(md, x, (yearfirst ? y + (vectorFont ? 26 : 16) : y)); - g.drawString(year, x, (yearfirst ? y : y + (vectorFont ? 26 : 16))); - } else { - g.setFontAlign(0, 0).drawString(secStr, x, y); // Just the seconds centered - } - } else { // No seconds screen: Show date and optionally day of week - y += (vectorFont ? 50 : (secondsMode !== "Never") ? 52 : 40); - var dateStr = (dateOnMain === "ISO8601" ? isoStr(date) : require("locale").date(date, (dateOnMain === "Long" ? 0 : 1))); - if (upperCase) - dateStr = dateStr.toUpperCase(); - g.setFontAlign(0, 0); - if (vectorFont) - g.setFont("Vector", 24); - else - g.setFont("6x8", 2); - g.drawString(dateStr, x, y); - if (calWeek || weekDay) { - var dowcwStr = ""; - if (calWeek) - dowcwStr = " #" + ("0" + ISO8601calWeek(date)).slice(-2); - if (weekDay) - dowcwStr = require("locale").dow(date, calWeek ? 1 : 0) + dowcwStr; //weekDay e.g. Monday or weekDayShort # e.g. Mon #01 - else //week #01 - dowcwStr = /*LANG*/"week" + dowcwStr; - if (upperCase) - dowcwStr = dowcwStr.toUpperCase(); - g.drawString(dowcwStr, x, y + (vectorFont ? 26 : 16)); - } - } - - // queue next draw - queueDraw(); -} - -// Init the settings of the app -loadSettings(); -// Clear the screen once, at startup -g.clear(); -// Set dynamic state and perform initial drawing -updateState(); -// Register hooks for LCD on/off event and screen lock on/off event -Bangle.on('lcdPower', updateState); -Bangle.on('lock', updateState); // Show launcher when middle button pressed -Bangle.setUI({ +Bangle.setUI({ mode : "clock", remove : function() { // Called to unload all of the clock app - Bangle.removeListener('lcdPower', updateState); - Bangle.removeListener('lock', updateState); if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = undefined; delete Graphics.prototype.setFontAnton; - delete Graphics.prototype.setFontAntonSmall; }}); // Load widgets Bangle.loadWidgets(); -Bangle.drawWidgets(); +draw(); +setTimeout(Bangle.drawWidgets,0); } diff --git a/apps/antonclk/app.png b/apps/antonclk/app.png index a38093c5f..bb764d2a1 100644 Binary files a/apps/antonclk/app.png and b/apps/antonclk/app.png differ diff --git a/apps/antonclk/metadata.json b/apps/antonclk/metadata.json index b6134d1a1..b8242f11a 100644 --- a/apps/antonclk/metadata.json +++ b/apps/antonclk/metadata.json @@ -1,9 +1,8 @@ { "id": "antonclk", "name": "Anton Clock", - "version": "0.10", - "description": "A clock using the bold Anton font, optionally showing seconds and date in ISO-8601 format.", - "readme":"README.md", + "version": "0.11", + "description": "A simple clock using the bold Anton font. See `Anton Clock Plus` for an enhanced version", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], "type": "clock", @@ -12,8 +11,6 @@ "allow_emulator": true, "storage": [ {"name":"antonclk.app.js","url":"app.js"}, - {"name":"antonclk.settings.js","url":"settings.js"}, {"name":"antonclk.img","url":"app-icon.js","evaluate":true} - ], - "data": [{"name":"antonclk.json"}] + ] } diff --git a/apps/antonclk/screenshot.png b/apps/antonclk/screenshot.png index e949b8a24..9b38e90d5 100644 Binary files a/apps/antonclk/screenshot.png and b/apps/antonclk/screenshot.png differ diff --git a/apps/antonclkplus/ChangeLog b/apps/antonclkplus/ChangeLog new file mode 100644 index 000000000..3b0a3d8b8 --- /dev/null +++ b/apps/antonclkplus/ChangeLog @@ -0,0 +1,15 @@ +0.01: New App! +0.02: Load widgets after setUI so widclk knows when to hide +0.03: Clock now shows day of week under date. +0.04: Clock can optionally show seconds, date optionally in ISO-8601 format, weekdays and uppercase configurable, too. +0.05: Clock can optionally show ISO-8601 calendar weeknumber (default: Off) + when weekday name "Off": week #: + when weekday name "On": weekday name is cut at 6th position and .# is added +0.06: fixes #1271 - wrong settings name + when weekday name and calendar weeknumber are on then display is # + week is buffered until date or timezone changes +0.07: align default settings with app.js (otherwise the initial displayed settings will be confusing to users) +0.08: fixed calendar weeknumber not shortened to two digits +0.09: Use default Bangle formatter for booleans +0.10: Use Bangle.setUI({remove:...}) to allow loading the launcher without a full reset on 2v16 + Modified to avoid leaving functions defined when using setUI({remove:...}) diff --git a/apps/antonclk/README.md b/apps/antonclkplus/README.md similarity index 87% rename from apps/antonclk/README.md rename to apps/antonclkplus/README.md index 28a38f5fd..25b478dd9 100644 --- a/apps/antonclk/README.md +++ b/apps/antonclkplus/README.md @@ -1,6 +1,6 @@ -# Anton Clock - Large font digital watch with seconds and date +# Anton Clock Plus - Large font digital watch with seconds and date -Anton clock uses the "Anton" bold font to show the time in a clear, easily readable manner. On the Bangle.js 2, the time can be read easily even if the screen is locked and unlit. +Anton Clock Plus uses the "Anton" bold font to show the time in a clear, easily readable manner. On the Bangle.js 2, the time can be read easily even if the screen is locked and unlit. ## Features @@ -16,16 +16,16 @@ The basic time representation only shows hours and minutes of the current time. ## Usage -Install Anton clock through the Bangle.js app loader. -Configure it through the default Bangle.js configuration mechanism +* Install Anton Clock Plus through the Bangle.js app loader. +* Configure it through the default Bangle.js configuration mechanism (Settings app, "Apps" menu, "Anton clock" submenu). -If you like it, make it your default watch face +* If you like it, make it your default watch face (Settings app, "System" menu, "Clock" submenu, select "Anton clock"). ## Configuration -Anton clock is configured by the standard settings mechanism of Bangle.js's operating system: -Open the "Settings" app, then the "Apps" submenu and below it the "Anton clock" menu. +Anton Clock is configured by the standard settings mechanism of Bangle.js's operating system: +Open the `Settings` app, then the `Apps` submenu and below it the `Anton Clock+` menu. You configure Anton clock through several "on/off" switches in two menus. ### The main menu diff --git a/apps/antonclkplus/app-icon.js b/apps/antonclkplus/app-icon.js new file mode 100644 index 000000000..0c3aeb210 --- /dev/null +++ b/apps/antonclkplus/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgf/AH4At/l/Aofgh4DB+EAj4REQoM/AgP4AoeACIoLCg4FB4AFDCIwLCgAROgYIB8EBAoUH/gVBCIxQBCKYHBCJp9DI4ICBLJYRCn4RQEYMOR5ARDIgIRMYQZZBgARGZwZBDCKQrCgEDR5AdBUIQRJDoLXFCJD7J/xrICIQFCn4RH/4LDAoTaCCI4Ar/LLDCBfypMkCgMkyV/CJOSCIOf5IRGFwOfCJNP//JnmT588z/+pM/BYIRCk4RC/88+f/n4RCngRCz1JCIf5/nzGoQRIHwXPCIPJI4f8CJHJGQJKCCI59LCI5ZCCJ/+v/kBoM/+V/HIJrHBYJWB/JKB5x9JEYP8AQKdBpwRL841Dp41KZoTxBHYTXBWY77PCKKhJ/4/CcgMkXoQAiA=")) diff --git a/apps/antonclkplus/app.js b/apps/antonclkplus/app.js new file mode 100644 index 000000000..409d7d487 --- /dev/null +++ b/apps/antonclkplus/app.js @@ -0,0 +1,238 @@ +// Clock with large digits using the "Anton" bold font +Graphics.prototype.setFontAnton = function(scale) { + // Actual height 69 (68 - 0) + g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAA/gAAAAAAAAAAP/gAAAAAAAAAH//gAAAAAAAAB///gAAAAAAAAf///gAAAAAAAP////gAAAAAAD/////gAAAAAA//////gAAAAAP//////gAAAAH///////gAAAB////////gAAAf////////gAAP/////////gAD//////////AA//////////gAA/////////4AAA////////+AAAA////////gAAAA///////wAAAAA//////8AAAAAA//////AAAAAAA/////gAAAAAAA////4AAAAAAAA///+AAAAAAAAA///gAAAAAAAAA//wAAAAAAAAAA/8AAAAAAAAAAA/AAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//////AAAAAB///////8AAAAH////////AAAAf////////wAAA/////////4AAB/////////8AAD/////////+AAH//////////AAP//////////gAP//////////gAP//////////gAf//////////wAf//////////wAf//////////wAf//////////wA//8AAAAAB//4A//wAAAAAAf/4A//gAAAAAAP/4A//gAAAAAAP/4A//gAAAAAAP/4A//wAAAAAAf/4A///////////4Af//////////wAf//////////wAf//////////wAf//////////wAP//////////gAP//////////gAH//////////AAH//////////AAD/////////+AAB/////////8AAA/////////4AAAP////////gAAAD///////+AAAAAf//////4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/gAAAAAAAAAAP/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/AAAAAAAAAAA//AAAAAAAAAAA/+AAAAAAAAAAB/8AAAAAAAAAAD//////////gAH//////////gAP//////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/4AAAAB/gAAD//4AAAAf/gAAP//4AAAB//gAA///4AAAH//gAB///4AAAf//gAD///4AAA///gAH///4AAD///gAP///4AAH///gAP///4AAP///gAf///4AAf///gAf///4AB////gAf///4AD////gA////4AH////gA////4Af////gA////4A/////gA//wAAB/////gA//gAAH/////gA//gAAP/////gA//gAA///8//gA//gAD///w//gA//wA////g//gA////////A//gA///////8A//gA///////4A//gAf//////wA//gAf//////gA//gAf/////+AA//gAP/////8AA//gAP/////4AA//gAH/////gAA//gAD/////AAA//gAB////8AAA//gAA////wAAA//gAAP///AAAA//gAAD//8AAAA//gAAAP+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/+AAAAAD/wAAB//8AAAAP/wAAB///AAAA//wAAB///wAAB//wAAB///4AAD//wAAB///8AAH//wAAB///+AAP//wAAB///+AAP//wAAB////AAf//wAAB////AAf//wAAB////gAf//wAAB////gA///wAAB////gA///wAAB////gA///w//AAf//wA//4A//AAA//wA//gA//AAAf/wA//gB//gAAf/wA//gB//gAAf/wA//gD//wAA//wA//wH//8AB//wA///////////gA///////////gA///////////gA///////////gAf//////////AAf//////////AAP//////////AAP/////////+AAH/////////8AAH///+/////4AAD///+f////wAAA///8P////gAAAf//4H///+AAAAH//gB///wAAAAAP4AAH/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/wAAAAAAAAAA//wAAAAAAAAAP//wAAAAAAAAB///wAAAAAAAAf///wAAAAAAAH////wAAAAAAA/////wAAAAAAP/////wAAAAAB//////wAAAAAf//////wAAAAH///////wAAAA////////wAAAP////////wAAA///////H/wAAA//////wH/wAAA/////8AH/wAAA/////AAH/wAAA////gAAH/wAAA///4AAAH/wAAA//+AAAAH/wAAA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gAAAAAAAAH/4AAAAAAAAAAH/wAAAAAAAAAAH/wAAAAAAAAAAH/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB//8AAA/////+B///AAA/////+B///wAA/////+B///4AA/////+B///8AA/////+B///8AA/////+B///+AA/////+B////AA/////+B////AA/////+B////AA/////+B////gA/////+B////gA/////+B////gA/////+A////gA//gP/gAAB//wA//gf/AAAA//wA//gf/AAAAf/wA//g//AAAAf/wA//g//AAAA//wA//g//gAAA//wA//g//+AAP//wA//g////////gA//g////////gA//g////////gA//g////////gA//g////////AA//gf///////AA//gf//////+AA//gP//////+AA//gH//////8AA//gD//////4AA//gB//////wAA//gA//////AAAAAAAH////8AAAAAAAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//////gAAAAB///////+AAAAH////////gAAAf////////4AAB/////////8AAD/////////+AAH//////////AAH//////////gAP//////////gAP//////////gAf//////////wAf//////////wAf//////////wAf//////////wAf//////////4A//wAD/4AAf/4A//gAH/wAAP/4A//gAH/wAAP/4A//gAP/wAAP/4A//gAP/4AAf/4A//wAP/+AD//4A///wP//////4Af//4P//////wAf//4P//////wAf//4P//////wAf//4P//////wAP//4P//////gAP//4H//////gAH//4H//////AAH//4D/////+AAD//4D/////8AAB//4B/////4AAA//4A/////wAAAP/4AP////AAAAB/4AD///4AAAAAAAAAH/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//AAAAAAAAAAA//gAAAAAAAAAA//gAAAAAAAAAA//gAAAAAAADgA//gAAAAAAP/gA//gAAAAAH//gA//gAAAAB///gA//gAAAAP///gA//gAAAD////gA//gAAAf////gA//gAAB/////gA//gAAP/////gA//gAB//////gA//gAH//////gA//gA///////gA//gD///////gA//gf///////gA//h////////gA//n////////gA//////////gAA/////////AAAA////////wAAAA///////4AAAAA///////AAAAAA//////4AAAAAA//////AAAAAAA/////4AAAAAAA/////AAAAAAAA////8AAAAAAAA////gAAAAAAAA///+AAAAAAAAA///4AAAAAAAAA///AAAAAAAAAA//4AAAAAAAAAA/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//gB///wAAAAP//4H///+AAAA///8P////gAAB///+f////4AAD///+/////8AAH/////////+AAH//////////AAP//////////gAP//////////gAf//////////gAf//////////wAf//////////wAf//////////wA///////////wA//4D//wAB//4A//wB//gAA//4A//gA//gAAf/4A//gA//AAAf/4A//gA//gAAf/4A//wB//gAA//4A///P//8AH//4Af//////////wAf//////////wAf//////////wAf//////////wAf//////////gAP//////////gAP//////////AAH//////////AAD/////////+AAD///+/////8AAB///8f////wAAAf//4P////AAAAH//wD///8AAAAA/+AAf//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH//gAAAAAAAAB///+AA/+AAAAP////gA//wAAAf////wA//4AAB/////4A//8AAD/////8A//+AAD/////+A///AAH/////+A///AAP//////A///gAP//////A///gAf//////A///wAf//////A///wAf//////A///wAf//////A///wA///////AB//4A//4AD//AAP/4A//gAB//AAP/4A//gAA//AAP/4A//gAA/+AAP/4A//gAB/8AAP/4A//wAB/8AAf/4Af//////////wAf//////////wAf//////////wAf//////////wAf//////////wAP//////////gAP//////////gAH//////////AAH/////////+AAD/////////8AAB/////////4AAAf////////wAAAP////////AAAAB///////4AAAAAD/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/AAB/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="), 46, atob("EiAnGicnJycnJycnEw=="), 78 + (scale << 8) + (1 << 16)); +}; + +Graphics.prototype.setFontAntonSmall = function(scale) { + // Actual height 53 (52 - 0) + g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAAAAAAAAAAAAAMAAAAAAAAD8AAAAAAAA/8AAAAAAAf/8AAAAAAH//8AAAAAB///8AAAAA////8AAAAP////8AAAD/////8AAB//////8AAf//////8AH///////4A///////+AA///////AAA//////wAAA/////8AAAA////+AAAAA////gAAAAA///4AAAAAA//8AAAAAAA//AAAAAAAA/wAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/////wAAA//////8AAB//////+AAH///////gAH///////gAP///////wAf///////4Af///////4A////////8A////////8A////////8A//AAAAD/8A/8AAAAA/8A/8AAAAA/8A/8AAAAA/8A/+AAAAB/8A////////8A////////8A////////8Af///////4Af///////4AP///////wAP///////wAH///////gAD///////AAA//////8AAAP/////wAAAAAAAAAAAAAAAAAAAAAAAfwAAAAAAAA/4AAAAAAAA/4AAAAAAAB/wAAAAAAAB/wAAAAAAAD/wAAAAAAAD/gAAAAAAAH///////8AP///////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/4AAAP8AA//4AAA/8AB//4AAH/8AH//4AAP/8AP//4AA//8AP//4AB//8Af//4AD//8Af//4AP//8A///4Af//8A///4A///8A///4D///8A//AAH///8A/8AAP///8A/8AA//+/8A/8AD//8/8A/+Af//w/8A//////g/8A/////+A/8A/////8A/8Af////4A/8Af////wA/8AP////AA/8AP///+AA/8AH///8AA/8AD///wAA/8AA///AAA/8AAP/4AAA/8AAAAAAAAAAAAAAAAAAAAAAH4AAf/gAAA/4AAf/8AAD/4AAf//AAH/4AAf//gAP/4AAf//wAP/4AAf//wAf/4AAf//4Af/4AAf//4A//4AAf//8A//4AAf//8A//4AAP//8A//A/8AB/8A/8A/8AA/8A/8B/8AA/8A/8B/8AA/8A/+D//AB/8A////////8A////////8A////////8Af///////4Af///////4Af///////wAP///////gAH//9////gAD//4///+AAB//wf//4AAAP/AH//gAAAAAAAAAAAAAAAAAAAAAAAAAAAH/wAAAAAAB//wAAAAAAP//wAAAAAD///wAAAAA////wAAAAH////wAAAB/////wAAAf/////wAAD//////wAA///////wAA/////h/wAA////wB/wAA///8AB/wAA///AAB/wAA//gAAB/wAA////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8AAAAAAB/wAAAAAAAB/wAAAAAAAB/wAAAAAAAAAAAAAAAAAAAAAAAAAAAP/4AA////4P/+AA////4P//AA////4P//gA////4P//wA////4P//wA////4P//4A////4P//4A////4P//8A////4P//8A////4P//8A/8H/AAB/8A/8H+AAA/8A/8P+AAA/8A/8P+AAA/8A/8P/gAD/8A/8P/////8A/8P/////8A/8P/////8A/8P/////4A/8H/////4A/8H/////wA/8D/////wA/8B/////gA/8A////+AA/8AP///4AAAAAB///AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////wAAAf/////8AAB///////AAH///////gAP///////wAP///////wAf///////4Af///////4A////////8A////////8A////////8A/+AH/AB/8A/8AP+AA/8A/4Af+AA/8A/8Af+AA/8A/8Af/gH/8A//4f////8A//4f////8A//4f////8Af/4f////4Af/4f////4AP/4P////wAP/4P////gAH/4H////AAD/4D///+AAB/4B///4AAAP4AP//gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8AAAAAAAA/8AAAAAAAA/8AAAAAB8A/8AAAAB/8A/8AAAAf/8A/8AAAH//8A/8AAA///8A/8AAH///8A/8AA////8A/8AD////8A/8Af////8A/8B/////8A/8P/////8A/8//////8A////////AA///////AAA//////gAAA/////4AAAA/////AAAAA////4AAAAA////AAAAAA///8AAAAAA///gAAAAAA//+AAAAAAA//wAAAAAAA/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/gD//gAAA//4P//8AAD//8f///AAH//+////gAH///////wAP///////4AP///////8Af///////8Af///////+Af///////+A////////+A//B//AB/+A/+A/+AA/+A/8Af+AA/+A/+Af+AA/+A//A//AB/+A////////+Af///////+Af///////+Af///////8Af///////8AP///////4AH///////4AH//+////wAD//+////AAA//4P//+AAAP/gH//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH//gAfgAAA///8A/8AAB///+A//AAH////A//gAH////g//wAP////g//wAf////w//4Af////w//4A/////w//8A/////w//8A/////w//8A//gP/wA/8A/8AD/wA/8A/8AD/wAf8A/8AD/gA/8A/+AH/AB/8A////////8A////////8A////////8Af///////4Af///////4Af///////wAP///////wAH///////gAD//////+AAA//////4AAAP/////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="), 46, atob("DhgeFB4eHh4eHh4eDw=="), 60 + (scale << 8) + (1 << 16)); +}; + +{ // must be inside our own scope here so that when we are unloaded everything disappears + // we also define functions using 'let fn = function() {..}' for the same reason. function decls are global + +const SETTINGSFILE = "antonclk.json"; +const isBangle1 = (process.env.HWVERSION == 1); + +// variables defined from settings +let secondsMode; +let secondsColoured; +let secondsWithColon; +let dateOnMain; +let dateOnSecs; +let weekDay; +let calWeek; +let upperCase; +let vectorFont; + +// dynamic variables +let drawTimeout; +let queueMillis = 1000; +let secondsScreen = true; + + + +//For development purposes +/* +require('Storage').writeJSON(SETTINGSFILE, { + secondsMode: "Unlocked", // "Never", "Unlocked", "Always" + secondsColoured: true, + secondsWithColon: true, + dateOnMain: "Long", // "Short", "Long", "ISO8601" + dateOnSecs: "Year", // "No", "Year", "Weekday", LEGACY: true/false + weekDay: true, + calWeek: true, + upperCase: true, + vectorFont: true, +}); +*/ + +// OR (also for development purposes) +/* +require('Storage').erase(SETTINGSFILE); +*/ + +// Load settings +let loadSettings = function() { + // Helper function default setting + function def (value, def) {return value !== undefined ? value : def;} + + var settings = require('Storage').readJSON(SETTINGSFILE, true) || {}; + secondsMode = def(settings.secondsMode, "Never"); + secondsColoured = def(settings.secondsColoured, true); + secondsWithColon = def(settings.secondsWithColon, true); + dateOnMain = def(settings.dateOnMain, "Long"); + dateOnSecs = def(settings.dateOnSecs, "Year"); + weekDay = def(settings.weekDay, true); + calWeek = def(settings.calWeek, false); + upperCase = def(settings.upperCase, true); + vectorFont = def(settings.vectorFont, false); + + // Legacy + if (dateOnSecs === true) + dateOnSecs = "Year"; + if (dateOnSecs === false) + dateOnSecs = "No"; +} + +// schedule a draw for the next second or minute +let queueDraw = function() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, queueMillis - (Date.now() % queueMillis)); +} + +let updateState = function() { + if (Bangle.isLCDOn()) { + if ((secondsMode === "Unlocked" && !Bangle.isLocked()) || secondsMode === "Always") { + secondsScreen = true; + queueMillis = 1000; + } else { + secondsScreen = false; + queueMillis = 60000; + } + draw(); // draw immediately, queue redraw + } else { // stop draw timer + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } +} + +let isoStr = function(date) { + return date.getFullYear() + "-" + ("0" + (date.getMonth() + 1)).slice(-2) + "-" + ("0" + date.getDate()).slice(-2); +} + +let calWeekBuffer = [false,false,false]; //buffer tz, date, week no (once calculated until other tz or date is requested) +let ISO8601calWeek = function(date) { //copied from: https://gist.github.com/IamSilviu/5899269#gistcomment-3035480 + dateNoTime = date; dateNoTime.setHours(0,0,0,0); + if (calWeekBuffer[0] === date.getTimezoneOffset() && calWeekBuffer[1] === dateNoTime) return calWeekBuffer[2]; + calWeekBuffer[0] = date.getTimezoneOffset(); + calWeekBuffer[1] = dateNoTime; + var tdt = new Date(date.valueOf()); + var dayn = (date.getDay() + 6) % 7; + tdt.setDate(tdt.getDate() - dayn + 3); + var firstThursday = tdt.valueOf(); + tdt.setMonth(0, 1); + if (tdt.getDay() !== 4) { + tdt.setMonth(0, 1 + ((4 - tdt.getDay()) + 7) % 7); + } + calWeekBuffer[2] = 1 + Math.ceil((firstThursday - tdt) / 604800000); + return calWeekBuffer[2]; +} + +let doColor = function() { + return !isBangle1 && !Bangle.isLocked() && secondsColoured; +} + +// Actually draw the watch face +let draw = function() { + var x = g.getWidth() / 2; + var y = g.getHeight() / 2 - (secondsMode !== "Never" ? 24 : (vectorFont ? 12 : 0)); + g.reset(); + /* This is to mark the widget areas during development. + g.setColor("#888") + .fillRect(0, 0, g.getWidth(), 23) + .fillRect(0, g.getHeight() - 23, g.getWidth(), g.getHeight()).reset(); + /* */ + g.clearRect(0, 24, g.getWidth(), g.getHeight() - 24); // clear whole background (w/o widgets) + var date = new Date(); // Actually the current date, this one is shown + var timeStr = require("locale").time(date, 1); // Hour and minute + g.setFontAlign(0, 0).setFont("Anton").drawString(timeStr, x, y); // draw time + if (secondsScreen) { + y += 65; + var secStr = (secondsWithColon ? ":" : "") + ("0" + date.getSeconds()).slice(-2); + if (doColor()) + g.setColor(0, 0, 1); + g.setFont("AntonSmall"); + if (dateOnSecs !== "No") { // A bit of a complex drawing with seconds on the right and date on the left + g.setFontAlign(1, 0).drawString(secStr, g.getWidth() - (isBangle1 ? 32 : 2), y); // seconds + y -= (vectorFont ? 15 : 13); + x = g.getWidth() / 4 + (isBangle1 ? 12 : 4) + (secondsWithColon ? 0 : g.stringWidth(":") / 2); + var dateStr2 = (dateOnMain === "ISO8601" ? isoStr(date) : require("locale").date(date, 1)); + var year; + var md; + var yearfirst; + if (dateStr2.match(/\d\d\d\d$/)) { // formatted date ends with year + year = (dateOnSecs === "Year" ? dateStr2.slice(-4) : require("locale").dow(date, 1)); + md = dateStr2.slice(0, -4); + if (!md.endsWith(".")) // keep separator before the year only if it is a dot (31.12. but 31/12) + md = md.slice(0, -1); + yearfirst = false; + } else { // formatted date begins with year + if (!dateStr2.match(/^\d\d\d\d/)) // if year position cannot be detected... + dateStr2 = isoStr(date); // ...use ISO date format instead + year = (dateOnSecs === "Year" ? dateStr2.slice(0, 4) : require("locale").dow(date, 1)); + md = dateStr2.slice(5); // never keep separator directly after year + yearfirst = true; + } + if (dateOnSecs === "Weekday" && upperCase) + year = year.toUpperCase(); + g.setFontAlign(0, 0); + if (vectorFont) + g.setFont("Vector", 24); + else + g.setFont("6x8", 2); + if (doColor()) + g.setColor(1, 0, 0); + g.drawString(md, x, (yearfirst ? y + (vectorFont ? 26 : 16) : y)); + g.drawString(year, x, (yearfirst ? y : y + (vectorFont ? 26 : 16))); + } else { + g.setFontAlign(0, 0).drawString(secStr, x, y); // Just the seconds centered + } + } else { // No seconds screen: Show date and optionally day of week + y += (vectorFont ? 50 : (secondsMode !== "Never") ? 52 : 40); + var dateStr = (dateOnMain === "ISO8601" ? isoStr(date) : require("locale").date(date, (dateOnMain === "Long" ? 0 : 1))); + if (upperCase) + dateStr = dateStr.toUpperCase(); + g.setFontAlign(0, 0); + if (vectorFont) + g.setFont("Vector", 24); + else + g.setFont("6x8", 2); + g.drawString(dateStr, x, y); + if (calWeek || weekDay) { + var dowcwStr = ""; + if (calWeek) + dowcwStr = " #" + ("0" + ISO8601calWeek(date)).slice(-2); + if (weekDay) + dowcwStr = require("locale").dow(date, calWeek ? 1 : 0) + dowcwStr; //weekDay e.g. Monday or weekDayShort # e.g. Mon #01 + else //week #01 + dowcwStr = /*LANG*/"week" + dowcwStr; + if (upperCase) + dowcwStr = dowcwStr.toUpperCase(); + g.drawString(dowcwStr, x, y + (vectorFont ? 26 : 16)); + } + } + + // queue next draw + queueDraw(); +} + +// Init the settings of the app +loadSettings(); +// Clear the screen once, at startup +g.clear(); +// Set dynamic state and perform initial drawing +updateState(); +// Register hooks for LCD on/off event and screen lock on/off event +Bangle.on('lcdPower', updateState); +Bangle.on('lock', updateState); +// Show launcher when middle button pressed +Bangle.setUI({ + mode : "clock", + remove : function() { + // Called to unload all of the clock app + Bangle.removeListener('lcdPower', updateState); + Bangle.removeListener('lock', updateState); + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + delete Graphics.prototype.setFontAnton; + delete Graphics.prototype.setFontAntonSmall; + }}); +// Load widgets +Bangle.loadWidgets(); +Bangle.drawWidgets(); +} diff --git a/apps/antonclkplus/app.png b/apps/antonclkplus/app.png new file mode 100644 index 000000000..a38093c5f Binary files /dev/null and b/apps/antonclkplus/app.png differ diff --git a/apps/antonclkplus/metadata.json b/apps/antonclkplus/metadata.json new file mode 100644 index 000000000..05c59a4fb --- /dev/null +++ b/apps/antonclkplus/metadata.json @@ -0,0 +1,20 @@ +{ + "id": "antonclkplus", + "name": "Anton Clock Plus", + "shortName": "Anton Clock+", + "version": "0.10", + "description": "A clock using the bold Anton font, optionally showing seconds and date in ISO-8601 format.", + "readme":"README.md", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS","BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"antonclkplus.app.js","url":"app.js"}, + {"name":"antonclkplus.settings.js","url":"settings.js"}, + {"name":"antonclkplus.img","url":"app-icon.js","evaluate":true} + ], + "data": [{"name":"antonclkplus.json"}] +} diff --git a/apps/antonclkplus/screenshot.png b/apps/antonclkplus/screenshot.png new file mode 100644 index 000000000..e949b8a24 Binary files /dev/null and b/apps/antonclkplus/screenshot.png differ diff --git a/apps/antonclk/settings.js b/apps/antonclkplus/settings.js similarity index 100% rename from apps/antonclk/settings.js rename to apps/antonclkplus/settings.js diff --git a/apps/astral/ChangeLog b/apps/astral/ChangeLog index c93c2b6c2..747e5ac2e 100644 --- a/apps/astral/ChangeLog +++ b/apps/astral/ChangeLog @@ -2,3 +2,4 @@ 0.02: Fixed Whirlpool galaxy RA/DA, larger compass display, fixed moonphase overlapping battery widget 0.03: Update to use Bangle.setUI instead of setWatch 0.04: Tell clock widgets to hide. +0.05: Added adjustment for Bangle.js magnetometer heading fix diff --git a/apps/astral/app.js b/apps/astral/app.js index c4339bc09..a435ca9e3 100644 --- a/apps/astral/app.js +++ b/apps/astral/app.js @@ -834,7 +834,7 @@ Bangle.on('mag', function (m) { if (isNaN(m.heading)) compass_heading = "---"; else - compass_heading = 360 - Math.round(m.heading); + compass_heading = Math.round(m.heading); // g.setColor("#000000"); // g.fillRect(160, 10, 160, 20); g.setColor(display_colour); diff --git a/apps/astral/metadata.json b/apps/astral/metadata.json index d7120878b..647066a13 100644 --- a/apps/astral/metadata.json +++ b/apps/astral/metadata.json @@ -1,7 +1,7 @@ { "id": "astral", "name": "Astral Clock", - "version": "0.04", + "version": "0.05", "description": "Clock that calculates and displays Alt Az positions of all planets, Sun as well as several other astronomy targets (customizable) and current Moon phase. Coordinates are calculated by GPS & time and onscreen compass assists orienting. See Readme before using.", "icon": "app-icon.png", "type": "clock", diff --git a/apps/barometer/ChangeLog b/apps/barometer/ChangeLog index de3a5cb96..b429dda17 100644 --- a/apps/barometer/ChangeLog +++ b/apps/barometer/ChangeLog @@ -1,3 +1,4 @@ 0.01: Display pressure as number and hand 0.02: Use theme color 0.03: workaround for some firmwares that return 'undefined' for first call to barometer +0.04: Update every second, go back with short button press diff --git a/apps/barometer/app.js b/apps/barometer/app.js index 77d4c974f..7e793af4f 100644 --- a/apps/barometer/app.js +++ b/apps/barometer/app.js @@ -59,6 +59,7 @@ function drawTicks(){ function drawScaleLabels(){ g.setColor(g.theme.fg); g.setFont("Vector",12); + g.setFontAlign(-1,-1); let label = MIN; for (let i=0;i <= NUMBER_OF_LABELS; i++){ @@ -103,22 +104,29 @@ function drawIcons() { } g.setBgColor(g.theme.bg); -g.clear(); - -drawTicks(); -drawScaleLabels(); -drawIcons(); try { function baroHandler(data) { - if (data===undefined) // workaround for https://github.com/espruino/BangleApps/issues/1429 - setTimeout(() => Bangle.getPressure().then(baroHandler), 500); - else + g.clear(); + + drawTicks(); + drawScaleLabels(); + drawIcons(); + if (data!==undefined) { drawHand(Math.round(data.pressure)); + } } Bangle.getPressure().then(baroHandler); + setInterval(() => Bangle.getPressure().then(baroHandler), 1000); } catch(e) { - print(e.message); - print("barometer not supporter, show a demo value"); + if (e !== undefined) { + print(e.message); + } + print("barometer not supported, show a demo value"); drawHand(MIN); } + +Bangle.setUI({ + mode : "custom", + back : function() {load();} +}); diff --git a/apps/barometer/metadata.json b/apps/barometer/metadata.json index a385f2be2..767fa630b 100644 --- a/apps/barometer/metadata.json +++ b/apps/barometer/metadata.json @@ -1,7 +1,7 @@ { "id": "barometer", "name": "Barometer", "shortName":"Barometer", - "version":"0.03", + "version":"0.04", "description": "A simple barometer that displays the current air pressure", "icon": "barometer.png", "tags": "tool,outdoors", diff --git a/apps/beer/ChangeLog b/apps/beer/ChangeLog new file mode 100644 index 000000000..21ec45242 --- /dev/null +++ b/apps/beer/ChangeLog @@ -0,0 +1,3 @@ +0.01: New App! +0.02: Added adjustment for Bangle.js magnetometer heading fix + Bangle.js 2 compatibility diff --git a/apps/beer/app-icon.js b/apps/beer/app-icon.js index c700b3bd2..734985cb5 100644 --- a/apps/beer/app-icon.js +++ b/apps/beer/app-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mEwghC/AB0O/4AG8AXNgYXHmAXl94XH+AXNn4XH/wXW+YX/C6oWHAAIXN7sz9vdAAoXN9sznvuAAXf/vuC53jC4Xd7wXQ93jn3u9vv9vt7wXT/4tBAgIXQ7wvCC4PgC5sO6czIQJfBC6PumaPDC6wwCC50NYAJcBVgIDBCxrAFbgYXP7yoDF6TADL4YXPVAIXCRyAXC7wXW9zwBC6cNC9zABC4gWQC653CR4fQC6x3TF6gXXI4M9d6wAEC9EN73dAAZfQgczAAkwC/4XXAH4")) +require("heatshrink").decompress(atob("mEw4cA///wH9/++1P+u3//3/qv/gv+KHkJkmABxcBBwNJkmQCJYOByQCCCBUCCItJkARQkgQHggLBku25IRDJQ4LCtu27Mt2RKJCInbAQIRLpYROglt24OB6wSC7dwLQ4LB9u2EgfbsARJ8u2mwRO+u3CNJtHCJFpCINALJoRCpCiGBoMSdQcpegIRGyaPB+QRDkARIyQRBc4YRKyet23iCJxHB6QRBzOJCJ+dCJY1CpfMGphrCp2YNZlL54CBEZgLBAQoRBiTFFCNMvmQRPndiEcJHEyQQECJMpAYIRQyARQwAROI4IAGB4wCBNAoRmhIRHCA4A/AAo")) diff --git a/apps/beer/custom.html b/apps/beer/custom.html index a357ab378..f0895f93f 100644 --- a/apps/beer/custom.html +++ b/apps/beer/custom.html @@ -127,6 +127,8 @@ var img_nofix = require("heatshrink").decompress(atob("mUyxH+ACYhJDygtYGsqLVF8u02gziGBoyhQ5gwDGRozRGCQydGCgybGCwyZC5gAaGPQwnGRAwpGQ4xwGFYyFDKsrlYxYDCsBmUyg4yXLyUsFwMyq1WAgUsNCRjUmVXroAEq8yMbcllkskwCEkplDmQwDq0sC54xEHQ9RqQAGqIwCFgOBAASYBSgMBltRAA0sgJsOGJeBxAAGwMrgIXIloxOJYNSvl8CwIDCqMBlYxNC4wxQDIOCwVYDIIDBGJ9YwV8rADBwRJCSqAVCAYaVMC4oxCPYYxQSo4xMSpIxPY4T5HY54XIMbIxKgwXKfKjhEllWGJNWlgXJGLNXruCGI+CrtXGKP+GJB9HMZ6VO/wxJcI8lfJclfKAxKfJEAGJIXLGKSvBWYQZCMZbfEqTHBGJYyFfIo1DGJ4tDGJQwCGJB9IMZyVNGIYyEfJQxPfJgwEMgoZJgAxMltRAA0tGJQyEksslkmAQklGINXxDTBFwIDCq8rC4YACC4gwJMowAJldWAAwwBABowIGJ4AYGJIymGBQylGBgyjGBwyhGCAzeF6YycGCwzYF7IzVF7o1PDqYA==")); var img_fix = require("heatshrink").decompress(atob("mUyxH+ACYhJDygtYGsqLVF94zaDYkq6wAOlQyYJo2A63VAAIoC2m0GI16My5/H5/V64ABGQIwBGQ+rTKwWHkhiBGIYwDGQ3VZioVIqoiBGAJhEGRFPGSYTIYwQxCGA4yFqodJGKeqSgQwJGQmkGKQSJfAYwLGQfPDxQwRgHVfAi/EAA4xLGQwRLYwb5BABoxQCBcA43G5wABAgIAMEBgxQ0QxB54xB5gAG4xgBBYOiGJ4PMGInPGIhcCGIt4EJoxPvHM5oxBGAnO6xrCGoXMqgxdpwxD5qQFL4QADlQxdgAhBGILIDMYoADEBwwPgCHBfQzHDAAb4NACTIIAA74OACLIIMo7GOACQoBZAoHBHQPNA4QwggGiZBA5B54HBY0DIKMYtUGMMqFYLIGY4jGhZAr6FAAYwiZAgxIY0TIFfQgADvAfR/zISGJTGR/wxRkj6CGJBiSGKL6DGP4xOGSKVDGAwxRGAQxU5oxcGR75DGJEkGCYxPlXM5vPGA/MlQxUGR1OGIL4I5lOGCgyOqgxBShHMqgwVGJt4GJd4GKwyMvHG5vGABAxMGBQyM1mtABWsGC4yLGBYABGDAyKGKwwQGZKVUF6b/OABowWGbAvZGaovdGp4dTA")); +var W = g.getWidth(), H = g.getHeight(); + // https://github.com/Leaflet/Leaflet/blob/master/src/geo/projection/Projection.SphericalMercator.js function project(latlong) { var d = Math.PI / 180, @@ -170,32 +172,30 @@ Bangle.on('GPS', function(f) { Bangle.on('mag', function(m) { if (!Bangle.isLCDOn()) return; - var headingrad = m.heading*Math.PI/180; // in radians + var headingrad = (360-m.heading)*Math.PI/180; // in radians if (!isFinite(headingrad)) headingrad=0; if (nearest) - g.drawImage(img_fix,120,120,{ + g.drawImage(img_fix,W/2,H/2,{ rotate: (Math.PI/2)+headingrad-nearestangle, scale:3, }); else - g.drawImage(img_nofix,120,120,{ + g.drawImage(img_nofix,W/2,H/2,{ rotate: headingrad, scale:2, }); - g.clearRect(60,0,180,24); - g.setFontAlign(0,0); - g.setFont("6x8"); + g.clearRect(0,0,W,24).setFontAlign(0,0).setFont("6x8"); if (fix.fix) { - g.drawString(nearest ? nearest.name : "---",120,4); + g.drawString(nearest ? nearest.name : "---",W/2,4); g.setFont("6x8",2); - g.drawString(nearest ? Math.round(nearestdist)+"m" : "---",120,16); + g.drawString(nearest ? Math.round(nearestdist)+"m" : "---",W/2,16); } else { - g.drawString(fix.satellites+" satellites",120,4); + g.drawString(fix.satellites+" satellites",W/2,4); } }); Bangle.setCompassPower(1); Bangle.setGPSPower(1); -g.clear();`; +g.setColor("#fff").setBgColor("#000").clear();`; sendCustomizedApp({ storage:[ diff --git a/apps/beer/metadata.json b/apps/beer/metadata.json index cf69aee90..3a2421bd1 100644 --- a/apps/beer/metadata.json +++ b/apps/beer/metadata.json @@ -1,11 +1,11 @@ { "id": "beer", "name": "Beer Compass", - "version": "0.01", + "version": "0.02", "description": "Uploads all the pubs in an area onto your watch, so it can always point you at the nearest one", "icon": "app.png", "tags": "", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS","BANGLEJS2"], "custom": "custom.html", "storage": [ {"name":"beer.app.js"}, diff --git a/apps/boot/ChangeLog b/apps/boot/ChangeLog index a43ecf86e..7b95d8686 100644 --- a/apps/boot/ChangeLog +++ b/apps/boot/ChangeLog @@ -52,3 +52,10 @@ 0.46: Fix no clock found error on Bangle.js 2 0.47: Add polyfill for setUI with an object as an argument (fix regression for 2v12 devices after Layout module changed) 0.48: Workaround for BTHRM issues on Bangle.js 1 (write .boot files in chunks) +0.49: Store first found clock as a setting to speed up further boots +0.50: Allow setting of screen rotation + Remove support for 2v11 and earlier firmware +0.51: Remove patches for 2v10 firmware (BEEPSET and setUI) + Add patch to ensure that compass heading is corrected on pre-2v15.68 firmware + Ensure clock is only fast-loaded if it doesn't contain widgets +0.52: Ensure heading patch for pre-2v15.68 firmware applies to getCompass diff --git a/apps/boot/bootloader.js b/apps/boot/bootloader.js index 45e271f30..6e6466f48 100644 --- a/apps/boot/bootloader.js +++ b/apps/boot/bootloader.js @@ -1,8 +1,13 @@ // This runs after a 'fresh' boot -var clockApp=(require("Storage").readJSON("setting.json",1)||{}).clock; -if (clockApp) clockApp = require("Storage").read(clockApp); -if (!clockApp) { - clockApp = require("Storage").list(/\.info$/) +var s = require("Storage").readJSON("setting.json",1)||{}; +/* If were being called from JS code in order to load the clock quickly (eg from a launcher) +and the clock in question doesn't have widgets, force a normal 'load' as this will then +reset everything and remove the widgets. */ +if (global.__FILE__ && !s.clockHasWidgets) {load();throw "Clock has no widgets, can't fast load";} +// Otherwise continue to try and load the clock +var _clkApp = require("Storage").read(s.clock); +if (!_clkApp) { + _clkApp = require("Storage").list(/\.info$/) .map(file => { const app = require("Storage").readJSON(file,1); if (app && app.type == "clock") { @@ -11,9 +16,14 @@ if (!clockApp) { }) .filter(x=>x) .sort((a, b) => a.sortorder - b.sortorder)[0]; - if (clockApp) - clockApp = require("Storage").read(clockApp.src); + if (_clkApp){ + s.clock = _clkApp.src; + _clkApp = require("Storage").read(_clkApp.src); + s.clockHasWidgets = _clkApp.includes("Bangle.loadWidgets"); + require("Storage").writeJSON("setting.json", s); + } } -if (!clockApp) clockApp=`E.showMessage("No Clock Found");setWatch(()=>{Bangle.showLauncher();}, global.BTN2||BTN, {repeat:false,edge:"falling"});`; -eval(clockApp); -delete clockApp; +delete s; +if (!_clkApp) _clkApp=`E.showMessage("No Clock Found");setWatch(()=>{Bangle.showLauncher();}, global.BTN2||BTN, {repeat:false,edge:"falling"});`; +eval(_clkApp); +delete _clkApp; diff --git a/apps/boot/bootupdate.js b/apps/boot/bootupdate.js index 4cb3c52e4..e9a24f5f5 100644 --- a/apps/boot/bootupdate.js +++ b/apps/boot/bootupdate.js @@ -62,23 +62,6 @@ if (s.ble===false) boot += `if (!NRF.getSecurityStatus().connected) NRF.sleep(); if (s.timeout!==undefined) boot += `Bangle.setLCDTimeout(${s.timeout});\n`; if (!s.timeout) boot += `Bangle.setLCDPower(1);\n`; boot += `E.setTimeZone(${s.timezone});`; -// Set vibrate, beep, etc IF on older firmwares -if (!Bangle.F_BEEPSET) { - if (!s.vibrate) boot += `Bangle.buzz=Promise.resolve;\n` - if (s.beep===false) boot += `Bangle.beep=Promise.resolve;\n` - else if (s.beep=="vib" && !BANGLEJS2) boot += `Bangle.beep = function (time, freq) { - return new Promise(function(resolve) { - if ((0|freq)<=0) freq=4000; - if ((0|time)<=0) time=200; - if (time>5000) time=5000; - analogWrite(D13,0.1,{freq:freq}); - setTimeout(function() { - digitalWrite(D13,0); - resolve(); - }, time); - }); - };\n`; -} // Draw out of memory errors onto the screen boot += `E.on('errorFlag', function(errorFlags) { g.reset(1).setColor("#ff0000").setFont("6x8").setFontAlign(0,1).drawString(errorFlags,g.getWidth()/2,g.getHeight()-1).flip(); @@ -92,76 +75,14 @@ if (s.options) boot+=`Bangle.setOptions(${E.toJS(s.options)});\n`; if (s.brightness && s.brightness!=1) boot+=`Bangle.setLCDBrightness(${s.brightness});\n`; if (s.passkey!==undefined && s.passkey.length==6) boot+=`NRF.setSecurity({passkey:${E.toJS(s.passkey.toString())}, mitm:1, display:1});\n`; if (s.whitelist) boot+=`NRF.on('connect', function(addr) { if (!(require('Storage').readJSON('setting.json',1)||{}).whitelist.includes(addr)) NRF.disconnect(); });\n`; -// Pre-2v10 firmwares without a theme/setUI -delete g.theme; // deleting stops us getting confused by our own decl. builtins can't be deleted -if (!g.theme) { - boot += `g.theme={fg:-1,bg:0,fg2:-1,bg2:7,fgH:-1,bgH:0x02F7,dark:true};\n`; -} -try { - Bangle.setUI({}); // In 2v12.xx we added the option for mode to be an object - for 2v12 and earlier, add a fix if it fails with an object supplied -} catch(e) { - boot += `Bangle._setUI = Bangle.setUI; -Bangle.setUI=function(mode, cb) { - if (Bangle.uiRemove) { - Bangle.uiRemove(); - delete Bangle.uiRemove; - } - if ("object"==typeof mode) { - // TODO: handle mode.back? - mode = mode.mode; - } - Bangle._setUI(mode, cb); -};\n`; -} -delete E.showScroller; // deleting stops us getting confused by our own decl. builtins can't be deleted -if (!E.showScroller) { // added in 2v11 - this is a limited functionality polyfill - boot += `E.showScroller = (function(a){function n(){g.reset();b>=l+c&&(c=1+b-l);bm||m>=a.c)break;var f=24+d*a.h;a.draw(m,{x:0,y:f,w:h,h:a.h});d+c==b&&g.setColor(g.theme.fg).drawRect(0,f,h-1,f+a.h-1).drawRect(1,f+1,h-2,f+a.h-2)}g.setColor(c?g.theme.fg:g.theme.bg);g.fillPoly([e,6,e-14,20,e+14,20]);g.setColor(a.c>l+c?g.theme.fg:g.theme.bg);g.fillPoly([e,k-7,e-14,k-21,e+14,k-21])}if(!a)return Bangle.setUI();var b=0,c=0,h=g.getWidth(), -k=g.getHeight(),e=h/2,l=Math.floor((k-48)/a.h);g.reset().clearRect(0,24,h-1,k-1);n();Bangle.setUI("updown",d=>{d?(b+=d,0>b&&(b=a.c-1),b>=a.c&&(b=0),n()):a.select(b)})});\n`; -} -delete g.imageMetrics; // deleting stops us getting confused by our own decl. builtins can't be deleted -if (!g.imageMetrics) { // added in 2v11 - this is a limited functionality polyfill - boot += `Graphics.prototype.imageMetrics=function(src) { - if (src[0]) return {width:src[0],height:src[1]}; - else if ('object'==typeof src) return { - width:("width" in src) ? src.width : src.getWidth(), - height:("height" in src) ? src.height : src.getHeight()}; - var im = E.toString(src); - return {width:im.charCodeAt(0), height:im.charCodeAt(1)}; -};\n`; -} -delete g.stringMetrics; // deleting stops us getting confused by our own decl. builtins can't be deleted -if (!g.stringMetrics) { // added in 2v11 - this is a limited functionality polyfill - boot += `Graphics.prototype.stringMetrics=function(txt) { - txt = txt.toString().split("\\n"); - return {width:Math.max.apply(null,txt.map(x=>g.stringWidth(x))), height:this.getFontHeight()*txt.length}; -};\n`; -} -delete g.wrapString; // deleting stops us getting confused by our own decl. builtins can't be deleted -if (!g.wrapString) { // added in 2v11 - this is a limited functionality polyfill - boot += `Graphics.prototype.wrapString=function(str, maxWidth) { - var lines = []; - for (var unwrappedLine of str.split("\\n")) { - var words = unwrappedLine.split(" "); - var line = words.shift(); - for (var word of words) { - if (g.stringWidth(line + " " + word) > maxWidth) { - lines.push(line); - line = word; - } else { - line += " " + word; - } - } - lines.push(line); - } - return lines; -};\n`; -} -delete Bangle.appRect; // deleting stops us getting confused by our own decl. builtins can't be deleted -if (!Bangle.appRect) { // added in 2v11 - polyfill for older firmwares - boot += `Bangle.appRect = ((y,w,h)=>({x:0,y:0,w:w,h:h,x2:w-1,y2:h-1}))(g.getWidth(),g.getHeight()); - (lw=>{ Bangle.loadWidgets = () => { lw(); Bangle.appRect = ((y,w,h)=>({x:0,y:y,w:w,h:h-y,x2:w-1,y2:h-(1+h)}))(global.WIDGETS?24:0,g.getWidth(),g.getHeight()); }; })(Bangle.loadWidgets);\n`; -} +if (s.rotate) boot+=`g.setRotation(${s.rotate&3},${s.rotate>>2});\n` // screen rotation +// ================================================== FIXING OLDER FIRMWARES +// 2v15.68 and before had compass heading inverted. +if (process.version.replace("v","")<215.68) + boot += `Bangle.on('mag',e=>{if(!isNaN(e.heading))e.heading=360-e.heading;}); +Bangle.getCompass=(c=>(()=>{e=c();if(!isNaN(e.heading))e.heading=360-e.heading;return e;}))(Bangle.getCompass);`; +// ================================================== BOOT.JS // Append *.boot.js files // These could change bleServices/bleServiceOptions if needed var bootFiles = require('Storage').list(/\.boot\.js$/).sort((a,b)=>{ diff --git a/apps/boot/metadata.json b/apps/boot/metadata.json index 62adc4db1..339f8503e 100644 --- a/apps/boot/metadata.json +++ b/apps/boot/metadata.json @@ -1,7 +1,7 @@ { "id": "boot", "name": "Bootloader", - "version": "0.48", + "version": "0.52", "description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings", "icon": "bootloader.png", "type": "bootloader", diff --git a/apps/bthrm/ChangeLog b/apps/bthrm/ChangeLog index 57f0ecf3d..c7b5a865f 100644 --- a/apps/bthrm/ChangeLog +++ b/apps/bthrm/ChangeLog @@ -29,3 +29,8 @@ Use default boolean formatter in custom menu and directly apply config if useful Allow recording unmodified internal HR Better connection retry handling +0.13: Less time used during boot if disabled +0.14: Allow bonding (Debug menu) + Prevent mixing of BT and internal HRM events if both are enabled + Always use a grace period (default 0 ms) to decouple some connection steps + Device not found errors now utilize increasing timeouts diff --git a/apps/bthrm/boot.js b/apps/bthrm/boot.js index aa97d83b7..3e3d35737 100644 --- a/apps/bthrm/boot.js +++ b/apps/bthrm/boot.js @@ -1,633 +1 @@ -(function() { - var settings = Object.assign( - require('Storage').readJSON("bthrm.default.json", true) || {}, - require('Storage').readJSON("bthrm.json", true) || {} - ); - - var log = function(text, param){ - if (global.showStatusInfo) - showStatusInfo(text); - if (settings.debuglog){ - var logline = new Date().toISOString() + " - " + text; - if (param) logline += ": " + JSON.stringify(param); - print(logline); - } - }; - - log("Settings: ", settings); - - if (settings.enabled){ - - var clearCache = function() { - return require('Storage').erase("bthrm.cache.json"); - }; - - var getCache = function() { - var cache = require('Storage').readJSON("bthrm.cache.json", true) || {}; - if (settings.btid && settings.btid === cache.id) return cache; - clearCache(); - return {}; - }; - - var addNotificationHandler = function(characteristic) { - log("Setting notification handler"/*supportedCharacteristics[characteristic.uuid].handler*/); - characteristic.on('characteristicvaluechanged', (ev) => supportedCharacteristics[characteristic.uuid].handler(ev.target.value)); - }; - - var writeCache = function(cache) { - var oldCache = getCache(); - if (oldCache !== cache) { - log("Writing cache"); - require('Storage').writeJSON("bthrm.cache.json", cache); - } else { - log("No changes, don't write cache"); - } - }; - - var characteristicsToCache = function(characteristics) { - log("Cache characteristics"); - var cache = getCache(); - if (!cache.characteristics) cache.characteristics = {}; - for (var c of characteristics){ - //"handle_value":16,"handle_decl":15 - log("Saving handle " + c.handle_value + " for characteristic: ", c); - cache.characteristics[c.uuid] = { - "handle": c.handle_value, - "uuid": c.uuid, - "notify": c.properties.notify, - "read": c.properties.read - }; - } - writeCache(cache); - }; - - var characteristicsFromCache = function(device) { - var service = { device : device }; // fake a BluetoothRemoteGATTService - log("Read cached characteristics"); - var cache = getCache(); - if (!cache.characteristics) return []; - var restored = []; - for (var c in cache.characteristics){ - var cached = cache.characteristics[c]; - var r = new BluetoothRemoteGATTCharacteristic(); - log("Restoring characteristic ", cached); - r.handle_value = cached.handle; - r.uuid = cached.uuid; - r.properties = {}; - r.properties.notify = cached.notify; - r.properties.read = cached.read; - r.service = service; - addNotificationHandler(r); - log("Restored characteristic: ", r); - restored.push(r); - } - return restored; - }; - - log("Start"); - - var lastReceivedData={ - }; - - var supportedServices = [ - "0x180d", // Heart Rate - "0x180f", // Battery - ]; - - var bpmTimeout; - - var supportedCharacteristics = { - "0x2a37": { - //Heart rate measurement - active: false, - handler: function (dv){ - var flags = dv.getUint8(0); - - var bpm = (flags & 1) ? (dv.getUint16(1) / 100 /* ? */ ) : dv.getUint8(1); // 8 or 16 bit - supportedCharacteristics["0x2a37"].active = bpm > 0; - log("BTHRM BPM " + supportedCharacteristics["0x2a37"].active); - if (supportedCharacteristics["0x2a37"].active) stopFallback(); - if (bpmTimeout) clearTimeout(bpmTimeout); - bpmTimeout = setTimeout(()=>{ - supportedCharacteristics["0x2a37"].active = false; - startFallback(); - }, 3000); - - var sensorContact; - - if (flags & 2){ - sensorContact = !!(flags & 4); - } - - var idx = 2 + (flags&1); - - var energyExpended; - if (flags & 8){ - energyExpended = dv.getUint16(idx,1); - idx += 2; - } - var interval; - if (flags & 16) { - interval = []; - var maxIntervalBytes = (dv.byteLength - idx); - log("Found " + (maxIntervalBytes / 2) + " rr data fields"); - for(var i = 0 ; i < maxIntervalBytes / 2; i++){ - interval[i] = dv.getUint16(idx,1); // in milliseconds - idx += 2; - } - } - - var location; - if (lastReceivedData && lastReceivedData["0x180d"] && lastReceivedData["0x180d"]["0x2a38"]){ - location = lastReceivedData["0x180d"]["0x2a38"]; - } - - var battery; - if (lastReceivedData && lastReceivedData["0x180f"] && lastReceivedData["0x180f"]["0x2a19"]){ - battery = lastReceivedData["0x180f"]["0x2a19"]; - } - - if (settings.replace){ - var repEvent = { - bpm: bpm, - confidence: (sensorContact || sensorContact === undefined)? 100 : 0, - src: "bthrm" - }; - - log("Emitting HRM", repEvent); - Bangle.emit("HRM_int", repEvent); - } - - var newEvent = { - bpm: bpm - }; - - if (location) newEvent.location = location; - if (interval) newEvent.rr = interval; - if (energyExpended) newEvent.energy = energyExpended; - if (battery) newEvent.battery = battery; - if (sensorContact) newEvent.contact = sensorContact; - - log("Emitting BTHRM", newEvent); - Bangle.emit("BTHRM", newEvent); - } - }, - "0x2a38": { - //Body sensor location - handler: function(dv){ - if (!lastReceivedData["0x180d"]) lastReceivedData["0x180d"] = {}; - lastReceivedData["0x180d"]["0x2a38"] = parseInt(dv.buffer, 10); - } - }, - "0x2a19": { - //Battery - handler: function (dv){ - if (!lastReceivedData["0x180f"]) lastReceivedData["0x180f"] = {}; - lastReceivedData["0x180f"]["0x2a19"] = dv.getUint8(0); - } - } - }; - - var device; - var gatt; - var characteristics = []; - var blockInit = false; - var currentRetryTimeout; - var initialRetryTime = 40; - var maxRetryTime = 60000; - var retryTime = initialRetryTime; - - var connectSettings = { - minInterval: 7.5, - maxInterval: 1500 - }; - - var waitingPromise = function(timeout) { - return new Promise(function(resolve){ - log("Start waiting for " + timeout); - setTimeout(()=>{ - log("Done waiting for " + timeout); - resolve(); - }, timeout); - }); - }; - - if (settings.enabled){ - Bangle.isBTHRMActive = function (){ - return supportedCharacteristics["0x2a37"].active; - }; - - Bangle.isBTHRMOn = function(){ - return (Bangle._PWR && Bangle._PWR.BTHRM && Bangle._PWR.BTHRM.length > 0); - }; - - Bangle.isBTHRMConnected = function(){ - return gatt && gatt.connected; - }; - } - - if (settings.replace){ - Bangle.origIsHRMOn = Bangle.isHRMOn; - - Bangle.isHRMOn = function() { - if (settings.enabled && !settings.replace){ - return Bangle.origIsHRMOn(); - } else if (settings.enabled && settings.replace){ - return Bangle.isBTHRMOn(); - } - return Bangle.origIsHRMOn() || Bangle.isBTHRMOn(); - }; - } - - var clearRetryTimeout = function(resetTime) { - if (currentRetryTimeout){ - log("Clearing timeout " + currentRetryTimeout); - clearTimeout(currentRetryTimeout); - currentRetryTimeout = undefined; - } - if (resetTime) { - log("Resetting retry time"); - retryTime = initialRetryTime; - } - }; - - var retry = function() { - log("Retry"); - - if (!currentRetryTimeout){ - - var clampedTime = retryTime < 100 ? 100 : retryTime; - - log("Set timeout for retry as " + clampedTime); - clearRetryTimeout(); - currentRetryTimeout = setTimeout(() => { - log("Retrying"); - currentRetryTimeout = undefined; - initBt(); - }, clampedTime); - - retryTime = Math.pow(clampedTime, 1.1); - if (retryTime > maxRetryTime){ - retryTime = maxRetryTime; - } - } else { - log("Already in retry..."); - } - }; - - var buzzing = false; - var onDisconnect = function(reason) { - log("Disconnect: " + reason); - log("GATT", gatt); - log("Characteristics", characteristics); - clearRetryTimeout(reason != "Connection Timeout"); - supportedCharacteristics["0x2a37"].active = false; - startFallback(); - blockInit = false; - if (settings.warnDisconnect && !buzzing){ - buzzing = true; - Bangle.buzz(500,0.3).then(()=>waitingPromise(4500)).then(()=>{buzzing = false;}); - } - if (Bangle.isBTHRMOn()){ - retry(); - } - }; - - var createCharacteristicPromise = function(newCharacteristic) { - log("Create characteristic promise", newCharacteristic); - var result = Promise.resolve(); - // For values that can be read, go ahead and read them, even if we might be notified in the future - // Allows for getting initial state of infrequently updating characteristics, like battery - if (newCharacteristic.readValue){ - result = result.then(()=>{ - log("Reading data", newCharacteristic); - return newCharacteristic.readValue().then((data)=>{ - if (supportedCharacteristics[newCharacteristic.uuid] && supportedCharacteristics[newCharacteristic.uuid].handler) { - supportedCharacteristics[newCharacteristic.uuid].handler(data); - } - }); - }); - } - if (newCharacteristic.properties.notify){ - result = result.then(()=>{ - log("Starting notifications", newCharacteristic); - var startPromise = newCharacteristic.startNotifications().then(()=>log("Notifications started", newCharacteristic)); - if (settings.gracePeriodNotification > 0){ - log("Add " + settings.gracePeriodNotification + "ms grace period after starting notifications"); - startPromise = startPromise.then(()=>{ - log("Wait after connect"); - return waitingPromise(settings.gracePeriodNotification); - }); - } - return startPromise; - }); - } - return result.then(()=>log("Handled characteristic", newCharacteristic)); - }; - - var attachCharacteristicPromise = function(promise, characteristic) { - return promise.then(()=>{ - log("Handling characteristic:", characteristic); - return createCharacteristicPromise(characteristic); - }); - }; - - var createCharacteristicsPromise = function(newCharacteristics) { - log("Create characteristics promis ", newCharacteristics); - var result = Promise.resolve(); - for (var c of newCharacteristics){ - if (!supportedCharacteristics[c.uuid]) continue; - log("Supporting characteristic", c); - characteristics.push(c); - if (c.properties.notify){ - addNotificationHandler(c); - } - - result = attachCharacteristicPromise(result, c); - } - return result.then(()=>log("Handled characteristics")); - }; - - var createServicePromise = function(service) { - log("Create service promise", service); - var result = Promise.resolve(); - result = result.then(()=>{ - log("Handling service" + service.uuid); - return service.getCharacteristics().then((c)=>createCharacteristicsPromise(c)); - }); - return result.then(()=>log("Handled service" + service.uuid)); - }; - - var attachServicePromise = function(promise, service) { - return promise.then(()=>createServicePromise(service)); - }; - - var initBt = function () { - log("initBt with blockInit: " + blockInit); - if (blockInit){ - retry(); - return; - } - - blockInit = true; - - var promise; - var filters; - - if (!device){ - if (settings.btid){ - log("Configured device id", settings.btid); - filters = [{ id: settings.btid }]; - } else { - return; - } - log("Requesting device with filters", filters); - promise = NRF.requestDevice({ filters: filters, active: true }); - - if (settings.gracePeriodRequest){ - log("Add " + settings.gracePeriodRequest + "ms grace period after request"); - } - - promise = promise.then((d)=>{ - log("Got device", d); - d.on('gattserverdisconnected', onDisconnect); - device = d; - }); - - promise = promise.then(()=>{ - log("Wait after request"); - return waitingPromise(settings.gracePeriodRequest); - }); - } else { - promise = Promise.resolve(); - log("Reuse device", device); - } - - promise = promise.then(()=>{ - if (gatt){ - log("Reuse GATT", gatt); - } else { - log("GATT is new", gatt); - characteristics = []; - var cachedId = getCache().id; - if (device.id !== cachedId){ - log("Device ID changed from " + cachedId + " to " + device.id + ", clearing cache"); - clearCache(); - } - var newCache = getCache(); - newCache.id = device.id; - writeCache(newCache); - gatt = device.gatt; - } - - return Promise.resolve(gatt); - }); - - promise = promise.then((gatt)=>{ - if (!gatt.connected){ - log("Connecting..."); - var connectPromise = gatt.connect(connectSettings).then(function() { - log("Connected."); - }); - if (settings.gracePeriodConnect > 0){ - log("Add " + settings.gracePeriodConnect + "ms grace period after connecting"); - connectPromise = connectPromise.then(()=>{ - log("Wait after connect"); - return waitingPromise(settings.gracePeriodConnect); - }); - } - return connectPromise; - } else { - return Promise.resolve(); - } - }); - -/* promise = promise.then(() => { - log(JSON.stringify(gatt.getSecurityStatus())); - if (gatt.getSecurityStatus()['bonded']) { - log("Already bonded"); - return Promise.resolve(); - } else { - log("Start bonding"); - return gatt.startBonding() - .then(() => console.log(gatt.getSecurityStatus())); - } - });*/ - - promise = promise.then(()=>{ - if (!characteristics || characteristics.length === 0){ - characteristics = characteristicsFromCache(device); - } - }); - - promise = promise.then(()=>{ - var characteristicsPromise = Promise.resolve(); - if (characteristics.length === 0){ - characteristicsPromise = characteristicsPromise.then(()=>{ - log("Getting services"); - return gatt.getPrimaryServices(); - }); - - characteristicsPromise = characteristicsPromise.then((services)=>{ - log("Got services", services); - var result = Promise.resolve(); - for (var service of services){ - if (!(supportedServices.includes(service.uuid))) continue; - log("Supporting service", service.uuid); - result = attachServicePromise(result, service); - } - if (settings.gracePeriodService > 0) { - log("Add " + settings.gracePeriodService + "ms grace period after services"); - result = result.then(()=>{ - log("Wait after services"); - return waitingPromise(settings.gracePeriodService); - }); - } - return result; - }); - } else { - for (var characteristic of characteristics){ - characteristicsPromise = attachCharacteristicPromise(characteristicsPromise, characteristic, true); - } - } - - return characteristicsPromise; - }); - - return promise.then(()=>{ - log("Connection established, waiting for notifications"); - characteristicsToCache(characteristics); - clearRetryTimeout(true); - }).catch((e) => { - characteristics = []; - log("Error:", e); - onDisconnect(e); - }); - }; - - Bangle.setBTHRMPower = function(isOn, app) { - // Do app power handling - if (!app) app="?"; - if (Bangle._PWR===undefined) Bangle._PWR={}; - if (Bangle._PWR.BTHRM===undefined) Bangle._PWR.BTHRM=[]; - if (isOn && !Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM.push(app); - if (!isOn && Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM = Bangle._PWR.BTHRM.filter(a=>a!==app); - isOn = Bangle._PWR.BTHRM.length; - // so now we know if we're really on - if (isOn) { - switchFallback(); - if (!Bangle.isBTHRMConnected()) initBt(); - } else { // not on - log("Power off for " + app); - clearRetryTimeout(true); - if (gatt) { - if (gatt.connected){ - log("Disconnect with gatt", gatt); - try{ - gatt.disconnect().then(()=>{ - log("Successful disconnect"); - }).catch((e)=>{ - log("Error during disconnect promise", e); - }); - } catch (e){ - log("Error during disconnect attempt", e); - } - } - } - } - }; - - if (settings.replace){ - Bangle.on("HRM", (e) => { - e.modified = true; - Bangle.emit("HRM_int", e); - }); - - Bangle.origOn = Bangle.on; - Bangle.on = function(name, callback) { - if (name == "HRM") { - Bangle.origOn("HRM_int", callback); - } else { - Bangle.origOn(name, callback); - } - }; - - Bangle.origRemoveListener = Bangle.removeListener; - Bangle.removeListener = function(name, callback) { - if (name == "HRM") { - Bangle.origRemoveListener("HRM_int", callback); - } else { - Bangle.origRemoveListener(name, callback); - } - }; - - } - - Bangle.origSetHRMPower = Bangle.setHRMPower; - - if (settings.startWithHrm){ - - Bangle.setHRMPower = function(isOn, app) { - log("setHRMPower for " + app + ": " + (isOn?"on":"off")); - if (settings.enabled){ - Bangle.setBTHRMPower(isOn, app); - } - if ((settings.enabled && !settings.replace) || !settings.enabled){ - Bangle.origSetHRMPower(isOn, app); - } - }; - } - - var fallbackActive = false; - var inSwitch = false; - - var stopFallback = function(){ - if (fallbackActive){ - Bangle.origSetHRMPower(0, "bthrm_fallback"); - fallbackActive = false; - log("Fallback to HRM disabled"); - } - }; - - var startFallback = function(){ - if (!fallbackActive && settings.allowFallback) { - fallbackActive = true; - Bangle.origSetHRMPower(1, "bthrm_fallback"); - log("Fallback to HRM enabled"); - } - }; - - var switchFallback = function() { - log("Check falling back to HRM"); - if (!inSwitch){ - inSwitch = true; - if (Bangle.isBTHRMActive()){ - stopFallback(); - } else { - startFallback(); - } - } - inSwitch = false; - }; - - if (settings.replace){ - log("Replace HRM event"); - if (Bangle._PWR && Bangle._PWR.HRM){ - for (var i = 0; i < Bangle._PWR.HRM.length; i++){ - var app = Bangle._PWR.HRM[i]; - log("Moving app " + app); - Bangle.origSetHRMPower(0, app); - Bangle.setBTHRMPower(1, app); - if (Bangle._PWR.HRM===undefined) break; - } - } - } - - E.on("kill", ()=>{ - if (gatt && gatt.connected){ - log("Got killed, trying to disconnect"); - gatt.disconnect().then(()=>log("Disconnected on kill")).catch((e)=>log("Error during disconnnect on kill", e)); - } - }); - } -})(); +if ((require('Storage').readJSON("bthrm.json", true) || {}).enabled != false) require("bthrm").enable(); diff --git a/apps/bthrm/default.json b/apps/bthrm/default.json index fb284bcd2..2c729ec68 100644 --- a/apps/bthrm/default.json +++ b/apps/bthrm/default.json @@ -16,5 +16,6 @@ "gracePeriodNotification": 0, "gracePeriodConnect": 0, "gracePeriodService": 0, - "gracePeriodRequest": 0 + "gracePeriodRequest": 0, + "bonding": false } diff --git a/apps/bthrm/lib.js b/apps/bthrm/lib.js new file mode 100644 index 000000000..13e8b0383 --- /dev/null +++ b/apps/bthrm/lib.js @@ -0,0 +1,634 @@ +exports.enable = () => { + var settings = Object.assign( + require('Storage').readJSON("bthrm.default.json", true) || {}, + require('Storage').readJSON("bthrm.json", true) || {} + ); + + var log = function(text, param){ + if (global.showStatusInfo) + showStatusInfo(text); + if (settings.debuglog){ + var logline = new Date().toISOString() + " - " + text; + if (param) logline += ": " + JSON.stringify(param); + print(logline); + } + }; + + log("Settings: ", settings); + + if (settings.enabled){ + + var clearCache = function() { + return require('Storage').erase("bthrm.cache.json"); + }; + + var getCache = function() { + var cache = require('Storage').readJSON("bthrm.cache.json", true) || {}; + if (settings.btid && settings.btid === cache.id) return cache; + clearCache(); + return {}; + }; + + var addNotificationHandler = function(characteristic) { + log("Setting notification handler"/*supportedCharacteristics[characteristic.uuid].handler*/); + characteristic.on('characteristicvaluechanged', (ev) => supportedCharacteristics[characteristic.uuid].handler(ev.target.value)); + }; + + var writeCache = function(cache) { + var oldCache = getCache(); + if (oldCache !== cache) { + log("Writing cache"); + require('Storage').writeJSON("bthrm.cache.json", cache); + } else { + log("No changes, don't write cache"); + } + }; + + var characteristicsToCache = function(characteristics) { + log("Cache characteristics"); + var cache = getCache(); + if (!cache.characteristics) cache.characteristics = {}; + for (var c of characteristics){ + //"handle_value":16,"handle_decl":15 + log("Saving handle " + c.handle_value + " for characteristic: ", c); + cache.characteristics[c.uuid] = { + "handle": c.handle_value, + "uuid": c.uuid, + "notify": c.properties.notify, + "read": c.properties.read + }; + } + writeCache(cache); + }; + + var characteristicsFromCache = function(device) { + var service = { device : device }; // fake a BluetoothRemoteGATTService + log("Read cached characteristics"); + var cache = getCache(); + if (!cache.characteristics) return []; + var restored = []; + for (var c in cache.characteristics){ + var cached = cache.characteristics[c]; + var r = new BluetoothRemoteGATTCharacteristic(); + log("Restoring characteristic ", cached); + r.handle_value = cached.handle; + r.uuid = cached.uuid; + r.properties = {}; + r.properties.notify = cached.notify; + r.properties.read = cached.read; + r.service = service; + addNotificationHandler(r); + log("Restored characteristic: ", r); + restored.push(r); + } + return restored; + }; + + log("Start"); + + var lastReceivedData={ + }; + + var supportedServices = [ + "0x180d", // Heart Rate + "0x180f", // Battery + ]; + + var bpmTimeout; + + var supportedCharacteristics = { + "0x2a37": { + //Heart rate measurement + active: false, + handler: function (dv){ + var flags = dv.getUint8(0); + + var bpm = (flags & 1) ? (dv.getUint16(1) / 100 /* ? */ ) : dv.getUint8(1); // 8 or 16 bit + supportedCharacteristics["0x2a37"].active = bpm > 0; + log("BTHRM BPM " + supportedCharacteristics["0x2a37"].active); + if (supportedCharacteristics["0x2a37"].active) stopFallback(); + if (bpmTimeout) clearTimeout(bpmTimeout); + bpmTimeout = setTimeout(()=>{ + bpmTimeout = undefined; + supportedCharacteristics["0x2a37"].active = false; + startFallback(); + }, 3000); + + var sensorContact; + + if (flags & 2){ + sensorContact = !!(flags & 4); + } + + var idx = 2 + (flags&1); + + var energyExpended; + if (flags & 8){ + energyExpended = dv.getUint16(idx,1); + idx += 2; + } + var interval; + if (flags & 16) { + interval = []; + var maxIntervalBytes = (dv.byteLength - idx); + log("Found " + (maxIntervalBytes / 2) + " rr data fields"); + for(var i = 0 ; i < maxIntervalBytes / 2; i++){ + interval[i] = dv.getUint16(idx,1); // in milliseconds + idx += 2; + } + } + + var location; + if (lastReceivedData && lastReceivedData["0x180d"] && lastReceivedData["0x180d"]["0x2a38"]){ + location = lastReceivedData["0x180d"]["0x2a38"]; + } + + var battery; + if (lastReceivedData && lastReceivedData["0x180f"] && lastReceivedData["0x180f"]["0x2a19"]){ + battery = lastReceivedData["0x180f"]["0x2a19"]; + } + + if (settings.replace){ + var repEvent = { + bpm: bpm, + confidence: (sensorContact || sensorContact === undefined)? 100 : 0, + src: "bthrm" + }; + + log("Emitting aggregated HRM", repEvent); + Bangle.emit("HRM_R", repEvent); + } + + var newEvent = { + bpm: bpm + }; + + if (location) newEvent.location = location; + if (interval) newEvent.rr = interval; + if (energyExpended) newEvent.energy = energyExpended; + if (battery) newEvent.battery = battery; + if (sensorContact) newEvent.contact = sensorContact; + + log("Emitting BTHRM", newEvent); + Bangle.emit("BTHRM", newEvent); + } + }, + "0x2a38": { + //Body sensor location + handler: function(dv){ + if (!lastReceivedData["0x180d"]) lastReceivedData["0x180d"] = {}; + lastReceivedData["0x180d"]["0x2a38"] = parseInt(dv.buffer, 10); + } + }, + "0x2a19": { + //Battery + handler: function (dv){ + if (!lastReceivedData["0x180f"]) lastReceivedData["0x180f"] = {}; + lastReceivedData["0x180f"]["0x2a19"] = dv.getUint8(0); + } + } + }; + + var device; + var gatt; + var characteristics = []; + var blockInit = false; + var currentRetryTimeout; + var initialRetryTime = 40; + var maxRetryTime = 60000; + var retryTime = initialRetryTime; + + var connectSettings = { + minInterval: 7.5, + maxInterval: 1500 + }; + + var waitingPromise = function(timeout) { + return new Promise(function(resolve){ + log("Start waiting for " + timeout); + setTimeout(()=>{ + log("Done waiting for " + timeout); + resolve(); + }, timeout); + }); + }; + + if (settings.enabled){ + Bangle.isBTHRMActive = function (){ + return supportedCharacteristics["0x2a37"].active; + }; + + Bangle.isBTHRMOn = function(){ + return (Bangle._PWR && Bangle._PWR.BTHRM && Bangle._PWR.BTHRM.length > 0); + }; + + Bangle.isBTHRMConnected = function(){ + return gatt && gatt.connected; + }; + } + + if (settings.replace){ + Bangle.origIsHRMOn = Bangle.isHRMOn; + + Bangle.isHRMOn = function() { + if (settings.enabled && !settings.replace){ + return Bangle.origIsHRMOn(); + } else if (settings.enabled && settings.replace){ + return Bangle.isBTHRMOn(); + } + return Bangle.origIsHRMOn() || Bangle.isBTHRMOn(); + }; + } + + var clearRetryTimeout = function(resetTime) { + if (currentRetryTimeout){ + log("Clearing timeout " + currentRetryTimeout); + clearTimeout(currentRetryTimeout); + currentRetryTimeout = undefined; + } + if (resetTime) { + log("Resetting retry time"); + retryTime = initialRetryTime; + } + }; + + var retry = function() { + log("Retry"); + + if (!currentRetryTimeout){ + + var clampedTime = retryTime < 100 ? 100 : retryTime; + + log("Set timeout for retry as " + clampedTime); + clearRetryTimeout(); + currentRetryTimeout = setTimeout(() => { + log("Retrying"); + currentRetryTimeout = undefined; + initBt(); + }, clampedTime); + + retryTime = Math.pow(clampedTime, 1.1); + if (retryTime > maxRetryTime){ + retryTime = maxRetryTime; + } + } else { + log("Already in retry..."); + } + }; + + var buzzing = false; + var onDisconnect = function(reason) { + log("Disconnect: " + reason); + log("GATT", gatt); + log("Characteristics", characteristics); + + var retryTimeResetNeeded = true; + retryTimeResetNeeded &= reason != "Connection Timeout"; + retryTimeResetNeeded &= reason != "No device found matching filters"; + clearRetryTimeout(retryTimeResetNeeded); + supportedCharacteristics["0x2a37"].active = false; + startFallback(); + blockInit = false; + if (settings.warnDisconnect && !buzzing){ + buzzing = true; + Bangle.buzz(500,0.3).then(()=>waitingPromise(4500)).then(()=>{buzzing = false;}); + } + if (Bangle.isBTHRMOn()){ + retry(); + } + }; + + var createCharacteristicPromise = function(newCharacteristic) { + log("Create characteristic promise", newCharacteristic); + var result = Promise.resolve(); + // For values that can be read, go ahead and read them, even if we might be notified in the future + // Allows for getting initial state of infrequently updating characteristics, like battery + if (newCharacteristic.readValue){ + result = result.then(()=>{ + log("Reading data", newCharacteristic); + return newCharacteristic.readValue().then((data)=>{ + if (supportedCharacteristics[newCharacteristic.uuid] && supportedCharacteristics[newCharacteristic.uuid].handler) { + supportedCharacteristics[newCharacteristic.uuid].handler(data); + } + }); + }); + } + if (newCharacteristic.properties.notify){ + result = result.then(()=>{ + log("Starting notifications", newCharacteristic); + var startPromise = newCharacteristic.startNotifications().then(()=>log("Notifications started", newCharacteristic)); + + log("Add " + settings.gracePeriodNotification + "ms grace period after starting notifications"); + startPromise = startPromise.then(()=>{ + log("Wait after connect"); + return waitingPromise(settings.gracePeriodNotification); + }); + + return startPromise; + }); + } + return result.then(()=>log("Handled characteristic", newCharacteristic)); + }; + + var attachCharacteristicPromise = function(promise, characteristic) { + return promise.then(()=>{ + log("Handling characteristic:", characteristic); + return createCharacteristicPromise(characteristic); + }); + }; + + var createCharacteristicsPromise = function(newCharacteristics) { + log("Create characteristics promis ", newCharacteristics); + var result = Promise.resolve(); + for (var c of newCharacteristics){ + if (!supportedCharacteristics[c.uuid]) continue; + log("Supporting characteristic", c); + characteristics.push(c); + if (c.properties.notify){ + addNotificationHandler(c); + } + + result = attachCharacteristicPromise(result, c); + } + return result.then(()=>log("Handled characteristics")); + }; + + var createServicePromise = function(service) { + log("Create service promise", service); + var result = Promise.resolve(); + result = result.then(()=>{ + log("Handling service" + service.uuid); + return service.getCharacteristics().then((c)=>createCharacteristicsPromise(c)); + }); + return result.then(()=>log("Handled service" + service.uuid)); + }; + + var attachServicePromise = function(promise, service) { + return promise.then(()=>createServicePromise(service)); + }; + + var initBt = function () { + log("initBt with blockInit: " + blockInit); + if (blockInit){ + retry(); + return; + } + + blockInit = true; + + var promise; + var filters; + + if (!device){ + if (settings.btid){ + log("Configured device id", settings.btid); + filters = [{ id: settings.btid }]; + } else { + return; + } + log("Requesting device with filters", filters); + promise = NRF.requestDevice({ filters: filters, active: true }); + + if (settings.gracePeriodRequest){ + log("Add " + settings.gracePeriodRequest + "ms grace period after request"); + } + + promise = promise.then((d)=>{ + log("Got device", d); + d.on('gattserverdisconnected', onDisconnect); + device = d; + }); + + promise = promise.then(()=>{ + log("Wait after request"); + return waitingPromise(settings.gracePeriodRequest); + }); + } else { + promise = Promise.resolve(); + log("Reuse device", device); + } + + promise = promise.then(()=>{ + if (gatt){ + log("Reuse GATT", gatt); + } else { + log("GATT is new", gatt); + characteristics = []; + var cachedId = getCache().id; + if (device.id !== cachedId){ + log("Device ID changed from " + cachedId + " to " + device.id + ", clearing cache"); + clearCache(); + } + var newCache = getCache(); + newCache.id = device.id; + writeCache(newCache); + gatt = device.gatt; + } + + return Promise.resolve(gatt); + }); + + promise = promise.then((gatt)=>{ + if (!gatt.connected){ + log("Connecting..."); + var connectPromise = gatt.connect(connectSettings).then(function() { + log("Connected."); + }); + log("Add " + settings.gracePeriodConnect + "ms grace period after connecting"); + connectPromise = connectPromise.then(()=>{ + log("Wait after connect"); + return waitingPromise(settings.gracePeriodConnect); + }); + return connectPromise; + } else { + return Promise.resolve(); + } + }); + + if (settings.bonding){ + promise = promise.then(() => { + log(JSON.stringify(gatt.getSecurityStatus())); + if (gatt.getSecurityStatus()['bonded']) { + log("Already bonded"); + return Promise.resolve(); + } else { + log("Start bonding"); + return gatt.startBonding() + .then(() => console.log(gatt.getSecurityStatus())); + } + }); + } + + promise = promise.then(()=>{ + if (!characteristics || characteristics.length === 0){ + characteristics = characteristicsFromCache(device); + } + }); + + promise = promise.then(()=>{ + var characteristicsPromise = Promise.resolve(); + if (characteristics.length === 0){ + characteristicsPromise = characteristicsPromise.then(()=>{ + log("Getting services"); + return gatt.getPrimaryServices(); + }); + + characteristicsPromise = characteristicsPromise.then((services)=>{ + log("Got services", services); + var result = Promise.resolve(); + for (var service of services){ + if (!(supportedServices.includes(service.uuid))) continue; + log("Supporting service", service.uuid); + result = attachServicePromise(result, service); + } + log("Add " + settings.gracePeriodService + "ms grace period after services"); + result = result.then(()=>{ + log("Wait after services"); + return waitingPromise(settings.gracePeriodService); + }); + return result; + }); + } else { + for (var characteristic of characteristics){ + characteristicsPromise = attachCharacteristicPromise(characteristicsPromise, characteristic, true); + } + } + + return characteristicsPromise; + }); + + return promise.then(()=>{ + log("Connection established, waiting for notifications"); + characteristicsToCache(characteristics); + clearRetryTimeout(true); + }).catch((e) => { + characteristics = []; + log("Error:", e); + onDisconnect(e); + }); + }; + + Bangle.setBTHRMPower = function(isOn, app) { + // Do app power handling + if (!app) app="?"; + if (Bangle._PWR===undefined) Bangle._PWR={}; + if (Bangle._PWR.BTHRM===undefined) Bangle._PWR.BTHRM=[]; + if (isOn && !Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM.push(app); + if (!isOn && Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM = Bangle._PWR.BTHRM.filter(a=>a!==app); + isOn = Bangle._PWR.BTHRM.length; + // so now we know if we're really on + if (isOn) { + switchFallback(); + if (!Bangle.isBTHRMConnected()) initBt(); + } else { // not on + log("Power off for " + app); + clearRetryTimeout(true); + if (gatt) { + if (gatt.connected){ + log("Disconnect with gatt", gatt); + try{ + gatt.disconnect().then(()=>{ + log("Successful disconnect"); + }).catch((e)=>{ + log("Error during disconnect promise", e); + }); + } catch (e){ + log("Error during disconnect attempt", e); + } + } + } + } + }; + + if (settings.replace){ + // register a listener for original HRM events and emit as HRM_int + Bangle.on("HRM", (e) => { + e.modified = true; + Bangle.emit("HRM_int", e); + if (fallbackActive){ + // if fallback to internal HRM is active, emit as HRM_R to which everyone listens + Bangle.emit("HRM_R", e); + } + }); + + // force all apps wanting to listen to HRM to actually get events for HRM_R + Bangle.on = ( o => (name, cb) => { + o = o.bind(Bangle); + if (name == "HRM") o("HRM_R", cb); + else o(name, cb); + })(Bangle.on); + + Bangle.removeListener = ( o => (name, cb) => { + o = o.bind(Bangle); + if (name == "HRM") o("HRM_R", cb); + else o(name, cb); + })(Bangle.removeListener); + } + + Bangle.origSetHRMPower = Bangle.setHRMPower; + + if (settings.startWithHrm){ + Bangle.setHRMPower = function(isOn, app) { + log("setHRMPower for " + app + ": " + (isOn?"on":"off")); + if (settings.enabled){ + Bangle.setBTHRMPower(isOn, app); + } + if ((settings.enabled && !settings.replace) || !settings.enabled){ + Bangle.origSetHRMPower(isOn, app); + } + }; + } + + var fallbackActive = false; + var inSwitch = false; + + var stopFallback = function(){ + if (fallbackActive){ + Bangle.origSetHRMPower(0, "bthrm_fallback"); + fallbackActive = false; + log("Fallback to HRM disabled"); + } + }; + + var startFallback = function(){ + if (!fallbackActive && settings.allowFallback) { + fallbackActive = true; + Bangle.origSetHRMPower(1, "bthrm_fallback"); + log("Fallback to HRM enabled"); + } + }; + + var switchFallback = function() { + log("Check falling back to HRM"); + if (!inSwitch){ + inSwitch = true; + if (Bangle.isBTHRMActive()){ + stopFallback(); + } else { + startFallback(); + } + } + inSwitch = false; + }; + + if (settings.replace){ + log("Replace HRM event"); + if (Bangle._PWR && Bangle._PWR.HRM){ + for (var i = 0; i < Bangle._PWR.HRM.length; i++){ + var app = Bangle._PWR.HRM[i]; + log("Moving app " + app); + Bangle.origSetHRMPower(0, app); + Bangle.setBTHRMPower(1, app); + if (Bangle._PWR.HRM===undefined) break; + } + } + } + + E.on("kill", ()=>{ + if (gatt && gatt.connected){ + log("Got killed, trying to disconnect"); + gatt.disconnect().then(()=>log("Disconnected on kill")).catch((e)=>log("Error during disconnnect on kill", e)); + } + }); + } +}; diff --git a/apps/bthrm/metadata.json b/apps/bthrm/metadata.json index 4d2cb811b..df0ac1fc1 100644 --- a/apps/bthrm/metadata.json +++ b/apps/bthrm/metadata.json @@ -2,7 +2,7 @@ "id": "bthrm", "name": "Bluetooth Heart Rate Monitor", "shortName": "BT HRM", - "version": "0.12", + "version": "0.14", "description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.", "icon": "app.png", "type": "app", @@ -15,6 +15,7 @@ {"name":"bthrm.0.boot.js","url":"boot.js"}, {"name":"bthrm.img","url":"app-icon.js","evaluate":true}, {"name":"bthrm.settings.js","url":"settings.js"}, + {"name":"bthrm","url":"lib.js"}, {"name":"bthrm.default.json","url":"default.json"} ] } diff --git a/apps/bthrm/settings.js b/apps/bthrm/settings.js index 2b19ea46a..fb5aa04da 100644 --- a/apps/bthrm/settings.js +++ b/apps/bthrm/settings.js @@ -96,6 +96,12 @@ writeSettings("debuglog",v); } }, + 'Use bonding': { + value: !!settings.bonding, + onchange: v => { + writeSettings("bonding",v); + } + }, 'Grace periods': function() { E.showMenu(submenu_grace); } }; diff --git a/apps/calclock/ChangeLog b/apps/calclock/ChangeLog index b67f29e94..f4a0c96f5 100644 --- a/apps/calclock/ChangeLog +++ b/apps/calclock/ChangeLog @@ -1,3 +1,4 @@ 0.01: Initial version 0.02: More compact rendering & app icon 0.03: Tell clock widgets to hide. +0.04: Improve current time readability in light theme. diff --git a/apps/calclock/calclock.js b/apps/calclock/calclock.js index 343d95c04..a55dc05f9 100644 --- a/apps/calclock/calclock.js +++ b/apps/calclock/calclock.js @@ -68,7 +68,7 @@ function drawEvent(event, y) { var curEventHeight = 0; function drawCurrentEvents(y) { - g.setColor("#0ff"); + g.setColor(g.theme.dark ? "#0ff" : "#0000ff"); g.clearRect(5, y, g.getWidth() - 5, y + curEventHeight); curEventHeight = y; diff --git a/apps/calclock/metadata.json b/apps/calclock/metadata.json index 7bac5c721..3aab55186 100644 --- a/apps/calclock/metadata.json +++ b/apps/calclock/metadata.json @@ -2,7 +2,7 @@ "id": "calclock", "name": "Calendar Clock", "shortName": "CalClock", - "version": "0.03", + "version": "0.04", "description": "Show the current and upcoming events synchronized from Gadgetbridge", "icon": "calclock.png", "type": "clock", diff --git a/apps/calculator/ChangeLog b/apps/calculator/ChangeLog index a08a0f5a7..2e1ace7bf 100644 --- a/apps/calculator/ChangeLog +++ b/apps/calculator/ChangeLog @@ -3,3 +3,5 @@ 0.03: Support for different screen sizes and touchscreen 0.04: Display current operation on LHS 0.05: Grid positioning and swipe controls to switch between numbers, operators and special (for Bangle.js 2) +0.06: Bangle.js 2: Exit with a short press of the physical button +0.07: Bangle.js 2: Exit by pressing upper left corner of the screen diff --git a/apps/calculator/README.md b/apps/calculator/README.md index b25d355bf..62f6cef24 100644 --- a/apps/calculator/README.md +++ b/apps/calculator/README.md @@ -12,12 +12,20 @@ Basic calculator reminiscent of MacOs's one. Handy for small calculus. ## Controls +Bangle.js 1 - UP: BTN1 - DOWN: BTN3 - LEFT: BTN4 - RIGHT: BTN5 - SELECT: BTN2 +Bangle.js 2 +- Swipes to change visible buttons +- Click physical button to exit +- Press upper left corner of screen to exit (where the red back button would be) ## Creator + +## Contributors +[thyttan](https://github.com/thyttan) diff --git a/apps/calculator/app.js b/apps/calculator/app.js index 40953254e..d9a89a989 100644 --- a/apps/calculator/app.js +++ b/apps/calculator/app.js @@ -3,6 +3,8 @@ * * Original Author: Frederic Rousseau https://github.com/fredericrous * Created: April 2020 + * + * Contributors: thyttan https://github.com/thyttan */ g.clear(); @@ -402,43 +404,42 @@ if (process.env.HWVERSION==1) { swipeEnabled = false; drawGlobal(); } else { // touchscreen? - selected = "NONE"; + selected = "NONE"; swipeEnabled = true; prepareScreen(numbers, numbersGrid, COLORS.DEFAULT); prepareScreen(operators, operatorsGrid, COLORS.OPERATOR); prepareScreen(specials, specialsGrid, COLORS.SPECIAL); drawNumbers(); - Bangle.on('touch',(n,e)=>{ - for (var key in screen) { - if (typeof screen[key] == "undefined") break; - var r = screen[key].xy; - if (e.x>=r[0] && e.y>=r[1] && - e.x{ + for (var key in screen) { + if (typeof screen[key] == "undefined") break; + var r = screen[key].xy; + if (e.x>=r[0] && e.y>=r[1] && e.x { - if (!e.b) { - if (lastX > 50) { // right + }, + swipe : (LR, UD) => { + if (LR == 1) { // right drawSpecials(); - } else if (lastX < -50) { // left + } + if (LR == -1) { // left drawOperators(); - } else if (lastY > 50) { // down - drawNumbers(); - } else if (lastY < -50) { // up + } + if (UD == 1) { // down + drawNumbers(); + } + if (UD == -1) { // up drawNumbers(); } - lastX = 0; - lastY = 0; - } else { - lastX = lastX + e.dx; - lastY = lastY + e.dy; } }); + } - displayOutput(0); diff --git a/apps/calculator/metadata.json b/apps/calculator/metadata.json index e78e4d54f..1674b7843 100644 --- a/apps/calculator/metadata.json +++ b/apps/calculator/metadata.json @@ -2,7 +2,7 @@ "id": "calculator", "name": "Calculator", "shortName": "Calculator", - "version": "0.05", + "version": "0.07", "description": "Basic calculator reminiscent of MacOs's one. Handy for small calculus.", "icon": "calculator.png", "screenshots": [{"url":"screenshot_calculator.png"}], diff --git a/apps/calendar/ChangeLog b/apps/calendar/ChangeLog index 0583ea45f..db455679c 100644 --- a/apps/calendar/ChangeLog +++ b/apps/calendar/ChangeLog @@ -9,3 +9,4 @@ read start of week from system settings 0.09: Fix scope of let variables 0.10: Use default Bangle formatter for booleans +0.11: Fix off-by-one-error on next year diff --git a/apps/calendar/calendar.js b/apps/calendar/calendar.js index f4676fc22..f8785e52c 100644 --- a/apps/calendar/calendar.js +++ b/apps/calendar/calendar.js @@ -226,15 +226,14 @@ drawCalendar(date); clearWatch(); Bangle.on("touch", area => { const month = date.getMonth(); - let prevMonth; if (area == 1) { let prevMonth = month > 0 ? month - 1 : 11; if (prevMonth === 11) date.setFullYear(date.getFullYear() - 1); date.setMonth(prevMonth); } else { - let prevMonth = month < 11 ? month + 1 : 0; - if (prevMonth === 0) date.setFullYear(date.getFullYear() + 1); - date.setMonth(month + 1); + let nextMonth = month < 11 ? month + 1 : 0; + if (nextMonth === 0) date.setFullYear(date.getFullYear() + 1); + date.setMonth(nextMonth); } drawCalendar(date); }); diff --git a/apps/calendar/metadata.json b/apps/calendar/metadata.json index 48fd52d3e..88f20026d 100644 --- a/apps/calendar/metadata.json +++ b/apps/calendar/metadata.json @@ -1,7 +1,7 @@ { "id": "calendar", "name": "Calendar", - "version": "0.10", + "version": "0.11", "description": "Simple calendar", "icon": "calendar.png", "screenshots": [{"url":"screenshot_calendar.png"}], diff --git a/apps/circlesclock/ChangeLog b/apps/circlesclock/ChangeLog index c398a89b6..26e531be7 100644 --- a/apps/circlesclock/ChangeLog +++ b/apps/circlesclock/ChangeLog @@ -26,3 +26,7 @@ 0.12: Allow configuration of update interval 0.13: Load step goal from Bangle health app as fallback Memory optimizations +0.14: Support to show big weather info +0.15: Use Bangle.setUI({remove:...}) to allow loading the launcher without a full reset on 2v16 +0.16: Fix const error + Use widget_utils if available diff --git a/apps/circlesclock/README.md b/apps/circlesclock/README.md index aa429d5ec..8c8fbe4ae 100644 --- a/apps/circlesclock/README.md +++ b/apps/circlesclock/README.md @@ -9,10 +9,11 @@ It can show the following information (this can be configured): * Steps distance * Heart rate (automatically updates when screen is on and unlocked) * Battery (including charging status and battery low warning) - * Weather (requires [weather app](https://banglejs.com/apps/#weather)) + * Weather (requires [OWM weather provider](https://banglejs.com/apps/?id=owmweather)) * Humidity or wind speed as circle progress * Temperature inside circle * Condition as icon below circle + * Big weather icon next to clock * Time and progress until next sunrise or sunset (requires [my location app](https://banglejs.com/apps/#mylocation)) * Temperature, air pressure or altitude from internal pressure sensor @@ -27,6 +28,8 @@ The color of each circle can be configured. The following colors are available: ![Screenshot light theme](screenshot-light.png) ![Screenshot dark theme with four circles](screenshot-dark-4.png) ![Screenshot light theme with four circles](screenshot-light-4.png) +![Screenshot light theme with big weather enabled](screenshot-light-with-big-weather.png) + ## Ideas * Show compass heading @@ -35,4 +38,5 @@ The color of each circle can be configured. The following colors are available: Marco ([myxor](https://github.com/myxor)) ## Icons -Icons taken from [materialdesignicons](https://materialdesignicons.com) under Apache License 2.0 +Most of the icons are taken from [materialdesignicons](https://materialdesignicons.com) under Apache License 2.0 except the big weather icons which are from +[icons8](https://icons8.com/icon/set/weather/small--static--black) diff --git a/apps/circlesclock/app.js b/apps/circlesclock/app.js index fc501a5d0..25e34cce0 100644 --- a/apps/circlesclock/app.js +++ b/apps/circlesclock/app.js @@ -1,5 +1,5 @@ -const locale = require("locale"); -const storage = require("Storage"); +let locale = require("locale"); +let storage = require("Storage"); Graphics.prototype.setFontRobotoRegular50NumericOnly = function(scale) { // Actual height 39 (40 - 2) this.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAAB8AAAAAAAfAAAAAAAPwAAAAAAB8AAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAA4AAAAAAB+AAAAAAD/gAAAAAD/4AAAAAH/4AAAAAP/wAAAAAP/gAAAAAf/gAAAAAf/AAAAAA/+AAAAAB/+AAAAAB/8AAAAAD/4AAAAAH/4AAAAAD/wAAAAAA/wAAAAAAPgAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///wAAAB////gAAA////8AAA/////gAAP////8AAH8AAA/gAB8AAAD4AA+AAAAfAAPAAAADwADwAAAA8AA8AAAAPAAPAAAADwADwAAAA8AA8AAAAPAAPgAAAHwAB8AAAD4AAfwAAD+AAD/////AAA/////wAAH////4AAAf///4AAAB///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAPgAAAAAADwAAAAAAB8AAAAAAAfAAAAAAAHgAAAAAAD4AAAAAAA+AAAAAAAPAAAAAAAH/////wAB/////8AA//////AAP/////wAD/////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAfgAADwAAP4AAB8AAH+AAA/AAD/gAAfwAB/AAAf8AAfAAAP/AAPgAAH7wAD4AAD88AA8AAB+PAAPAAA/DwADwAAfg8AA8AAPwPAAPAAH4DwADwAH8A8AA+AD+APAAPwB/ADwAB/D/gA8AAf//gAPAAD//wADwAAf/wAA8AAD/4AAPAAAHwAADwAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAADgAAAHwAA+AAAD8AAP4AAB/AAD/AAA/wAA/wAAf4AAD+AAHwAAAPgAD4APAB8AA+ADwAPAAPAA8ADwADwAPAA8AA8ADwAPAAPAA8ADwADwAfAA8AA8AH4APAAPgD+AHwAB8B/wD4AAf7/+B+AAD//v//AAA//x//wAAD/4P/4AAAf8B/4AAAAYAH4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAHwAAAAAAH8AAAAAAD/AAAAAAD/wAAAAAD/8AAAAAB/vAAAAAB/jwAAAAA/g8AAAAA/wPAAAAAfwDwAAAAf4A8AAAAf4APAAAAP8ADwAAAP8AA8AAAH8AAPAAAD/////8AA//////AAP/////wAD/////8AA//////AAAAAAPAAAAAAADwAAAAAAA8AAAAAAAPAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAB/APwAAH//wD+AAD//8A/wAA///AH+AAP//wAPgAD/B4AB8AA8A+AAfAAPAPAADwADwDwAA8AA8A8AAPAAPAPAADwADwD4AA8AA8A+AAPAAPAPwAHwADwD8AD4AA8AfwD+AAPAH///AADwA///wAA8AH//4AAPAAf/4AAAAAB/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//AAAAAD//+AAAAD///4AAAD////AAAB////4AAA/78D/AAAfw8AH4AAPweAA+AAD4PgAHwAB8DwAA8AAfA8AAPAAHgPAADwAD4DwAA8AA+A8AAPAAPAPgAHwADwD4AB8AA8AfgA+AAPAH+B/gAAAA///wAAAAH//4AAAAA//8AAAAAH/8AAAAAAP4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAA8AAAAAAAPAAAAAAADwAAAAAAA8AAAABAAPAAAABwADwAAAB8AA8AAAB/AAPAAAB/wADwAAD/8AA8AAD/8AAPAAD/4AADwAD/4AAA8AD/4AAAPAH/wAAADwH/wAAAA8H/wAAAAPH/wAAAAD3/gAAAAA//gAAAAAP/gAAAAAD/gAAAAAA/AAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwA/4AAAH/Af/AAAH/8P/4AAD//n//AAA//7//4AAfx/+A+AAHwD+AHwAD4AfgB8AA8AHwAPAAPAA8ADwADwAPAA8AA8ADwAPAAPAA8ADwADwAfAA8AA+AH4AfAAHwD+AHwAB/D/4D4AAP/+/n+AAD//n//AAAf/w//gAAB/wH/wAAAHwA/4AAAAAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAD/8AAAAAD//wAAAAB//+AAAAA///wAAAAf4H+APAAH4AfgDwAD8AB8A8AA+AAfAPAAPAADwDwADwAA8B8AA8AAPAfAAPAADwHgADwAA8D4AA+AAeB+AAHwAHg/AAB+ADwfgAAP8D4/4AAD////8AAAf///8AAAB///+AAAAP//+AAAAAP/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAOAAAB8AAHwAAAfgAD8AAAH4AA/AAAB8AAHwAAAOAAA4AAAAAAAAAAAAAAAAAAAAAAAAAA"), 46, atob("DRUcHBwcHBwcHBwcDA=="), 50+(scale<<8)+(1<<16)); @@ -12,7 +12,7 @@ Graphics.prototype.setFontRobotoRegular21 = function(scale) { return this; }; -const SETTINGS_FILE = "circlesclock.json"; +let SETTINGS_FILE = "circlesclock.json"; let settings = Object.assign( storage.readJSON("circlesclock.default.json", true) || {}, storage.readJSON(SETTINGS_FILE, true) || {} @@ -22,13 +22,16 @@ let settings = Object.assign( if (settings.stepGoal == undefined) { let d = storage.readJSON("health.json", true) || {}; settings.stepGoal = d != undefined && d.settings != undefined ? d.settings.stepGoal : undefined; - - if (settings.stepGoal == undefined) { + + if (settings.stepGoal == undefined) { d = storage.readJSON("wpedom.json", true) || {}; settings.stepGoal = d != undefined && d.settings != undefined ? d.settings.goal : 10000; } } +let timerHrm; +let drawTimeout; + /* * Read location from myLocation app */ @@ -37,29 +40,30 @@ function getLocation() { } let location = getLocation(); -const showWidgets = settings.showWidgets || false; -const circleCount = settings.circleCount || 3; +let showWidgets = settings.showWidgets || false; +let circleCount = settings.circleCount || 3; +let showBigWeather = settings.showBigWeather || false; let hrtValue; let now = Math.round(new Date().getTime() / 1000); // layout values: -const colorFg = g.theme.dark ? '#fff' : '#000'; -const colorBg = g.theme.dark ? '#000' : '#fff'; -const colorGrey = '#808080'; -const colorRed = '#ff0000'; -const colorGreen = '#008000'; -const colorBlue = '#0000ff'; -const colorYellow = '#ffff00'; -const widgetOffset = showWidgets ? 24 : 0; -const dowOffset = circleCount == 3 ? 20 : 22; // dow offset relative to date -const h = g.getHeight() - widgetOffset; -const w = g.getWidth(); -const hOffset = (circleCount == 3 ? 34 : 30) - widgetOffset; -const h1 = Math.round(1 * h / 5 - hOffset); -const h2 = Math.round(3 * h / 5 - hOffset); -const h3 = Math.round(8 * h / 8 - hOffset - 3); // circle y position +let colorFg = g.theme.dark ? '#fff' : '#000'; +let colorBg = g.theme.dark ? '#000' : '#fff'; +let colorGrey = '#808080'; +let colorRed = '#ff0000'; +let colorGreen = '#008000'; +let colorBlue = '#0000ff'; +let colorYellow = '#ffff00'; +let widgetOffset = showWidgets ? 24 : 0; +let dowOffset = circleCount == 3 ? 20 : 22; // dow offset relative to date +let h = g.getHeight() - widgetOffset; +let w = g.getWidth(); +let hOffset = (circleCount == 3 ? 34 : 30) - widgetOffset; +let h1 = Math.round(1 * h / 5 - hOffset); +let h2 = Math.round(3 * h / 5 - hOffset); +let h3 = Math.round(8 * h / 8 - hOffset - 3); // circle y position /* * circle x positions @@ -73,21 +77,22 @@ const h3 = Math.round(8 * h / 8 - hOffset - 3); // circle y position * | (1) (2) (3) (4) | * => circles start at 1,3,5,7 / 8 */ -const parts = circleCount * 2; -const circlePosX = [ +let parts = circleCount * 2; +let circlePosX = [ Math.round(1 * w / parts), // circle1 Math.round(3 * w / parts), // circle2 Math.round(5 * w / parts), // circle3 Math.round(7 * w / parts), // circle4 ]; -const radiusOuter = circleCount == 3 ? 25 : 20; -const radiusInner = circleCount == 3 ? 20 : 15; -const circleFontSmall = circleCount == 3 ? "Vector:14" : "Vector:10"; -const circleFont = circleCount == 3 ? "Vector:15" : "Vector:11"; -const circleFontBig = circleCount == 3 ? "Vector:16" : "Vector:12"; -const iconOffset = circleCount == 3 ? 6 : 8; -const defaultCircleTypes = ["steps", "hr", "battery", "weather"]; +let radiusOuter = circleCount == 3 ? 25 : 20; +let radiusInner = circleCount == 3 ? 20 : 15; +let circleFontSmall = circleCount == 3 ? "Vector:14" : "Vector:10"; +let circleFont = circleCount == 3 ? "Vector:15" : "Vector:11"; +let circleFontBig = circleCount == 3 ? "Vector:16" : "Vector:12"; +let iconOffset = circleCount == 3 ? 6 : 8; +let defaultCircleTypes = ["steps", "hr", "battery", "weather"]; + function hideWidgets() { /* @@ -105,9 +110,16 @@ function hideWidgets() { function draw() { g.clear(true); + let widgetUtils; + + try { + widgetUtils = require("widget_utils"); + } catch (e) { + } if (!showWidgets) { - hideWidgets(); + if (widgetUtils) widgetUtils.hide(); else hideWidgets(); } else { + if (widgetUtils) widgetUtils.show(); Bangle.drawWidgets(); } @@ -116,27 +128,53 @@ function draw() { // time g.setFontRobotoRegular50NumericOnly(); - g.setFontAlign(0, -1); g.setColor(colorFg); - g.drawString(locale.time(new Date(), 1), w / 2, h1 + 6); + if (!showBigWeather) { + g.setFontAlign(0, -1); + g.drawString(locale.time(new Date(), 1), w / 2, h1 + 6); + } + else { + g.setFontAlign(-1, -1); + g.drawString(locale.time(new Date(), 1), 2, h1 + 6); + } now = Math.round(new Date().getTime() / 1000); // date & dow g.setFontRobotoRegular21(); - g.setFontAlign(0, 0); - g.drawString(locale.date(new Date()), w / 2, h2); - g.drawString(locale.dow(new Date()), w / 2, h2 + dowOffset); - + if (!showBigWeather) { + g.setFontAlign(0, 0); + g.drawString(locale.date(new Date()), w / 2, h2); + g.drawString(locale.dow(new Date()), w / 2, h2 + dowOffset); + } else { + g.setFontAlign(-1, 0); + g.drawString(locale.date(new Date()), 2, h2); + g.drawString(locale.dow(new Date()), 2, h2 + dowOffset, 1); + } + + // weather + if (showBigWeather) { + let weather = getWeather(); + let tempString = weather ? locale.temp(weather.temp - 273.15) : undefined; + g.setFontAlign(1, 0); + if (tempString) g.drawString(tempString, w, h2); + + let code = weather ? weather.code : -1; + let icon = getWeatherIconByCode(code, true); + if (icon) g.drawImage(icon, w - 48, h1, {scale:0.75}); + } + drawCircle(1); drawCircle(2); drawCircle(3); if (circleCount >= 4) drawCircle(4); + + queueDraw(); } function drawCircle(index) { let type = settings['circle' + index]; if (!type) type = defaultCircleTypes[index - 1]; - const w = getCircleXPosition(type); + let w = getCircleXPosition(type); switch (type) { case "steps": @@ -188,7 +226,7 @@ function getCirclePosition(type) { return circlePositionsCache[type]; } for (let i = 1; i <= circleCount; i++) { - const setting = settings['circle' + i]; + let setting = settings['circle' + i]; if (setting == type) { circlePositionsCache[type] = i - 1; return i - 1; @@ -204,7 +242,7 @@ function getCirclePosition(type) { } function getCircleXPosition(type) { - const circlePos = getCirclePosition(type); + let circlePos = getCirclePosition(type); if (circlePos != undefined) { return circlePosX[circlePos]; } @@ -216,14 +254,14 @@ function isCircleEnabled(type) { } function getCircleColor(type) { - const pos = getCirclePosition(type); - const color = settings["circle" + (pos + 1) + "color"]; + let pos = getCirclePosition(type); + let color = settings["circle" + (pos + 1) + "color"]; if (color && color != "") return color; } function getCircleIconColor(type, color, percent) { - const pos = getCirclePosition(type); - const colorizeIcon = settings["circle" + (pos + 1) + "colorizeIcon"] == true; + let pos = getCirclePosition(type); + let colorizeIcon = settings["circle" + (pos + 1) + "colorizeIcon"] == true; if (colorizeIcon) { return getGradientColor(color, percent); } else { @@ -234,18 +272,18 @@ function getCircleIconColor(type, color, percent) { function getGradientColor(color, percent) { if (isNaN(percent)) percent = 0; if (percent > 1) percent = 1; - const colorList = [ + let colorList = [ '#00FF00', '#80FF00', '#FFFF00', '#FF8000', '#FF0000' ]; if (color == "fg") { color = colorFg; } if (color == "green-red") { - const colorIndex = Math.round(colorList.length * percent); + let colorIndex = Math.round(colorList.length * percent); return colorList[Math.min(colorIndex, colorList.length) - 1] || "#00ff00"; } if (color == "red-green") { - const colorIndex = colorList.length - Math.round(colorList.length * percent); + let colorIndex = colorList.length - Math.round(colorList.length * percent); return colorList[Math.min(colorIndex, colorList.length)] || "#ff0000"; } return color; @@ -268,14 +306,14 @@ function getImage(graphic, color) { function drawSteps(w) { if (!w) w = getCircleXPosition("steps"); - const steps = getSteps(); + let steps = getSteps(); drawCircleBackground(w); - const color = getCircleColor("steps"); + let color = getCircleColor("steps"); let percent; - const stepGoal = settings.stepGoal; + let stepGoal = settings.stepGoal; if (stepGoal > 0) { percent = steps / stepGoal; if (stepGoal < steps) percent = 1; @@ -291,16 +329,16 @@ function drawSteps(w) { function drawStepsDistance(w) { if (!w) w = getCircleXPosition("stepsDistance"); - const steps = getSteps(); - const stepDistance = settings.stepLength; - const stepsDistance = Math.round(steps * stepDistance); + let steps = getSteps(); + let stepDistance = settings.stepLength; + let stepsDistance = Math.round(steps * stepDistance); drawCircleBackground(w); - const color = getCircleColor("stepsDistance"); + let color = getCircleColor("stepsDistance"); let percent; - const stepDistanceGoal = settings.stepDistanceGoal; + let stepDistanceGoal = settings.stepDistanceGoal; if (stepDistanceGoal > 0) { percent = stepsDistance / stepDistanceGoal; if (stepDistanceGoal < stepsDistance) percent = 1; @@ -317,16 +355,16 @@ function drawStepsDistance(w) { function drawHeartRate(w) { if (!w) w = getCircleXPosition("hr"); - const heartIcon = atob("EBCBAAAAAAAeeD/8P/x//n/+P/w//B/4D/AH4APAAYAAAAAA"); + let heartIcon = atob("EBCBAAAAAAAeeD/8P/x//n/+P/w//B/4D/AH4APAAYAAAAAA"); drawCircleBackground(w); - const color = getCircleColor("hr"); + let color = getCircleColor("hr"); let percent; if (hrtValue != undefined) { - const minHR = settings.minHR; - const maxHR = settings.maxHR; + let minHR = settings.minHR; + let maxHR = settings.maxHR; percent = (hrtValue - minHR) / (maxHR - minHR); if (isNaN(percent)) percent = 0; drawGauge(w, h3, percent, color); @@ -341,9 +379,9 @@ function drawHeartRate(w) { function drawBattery(w) { if (!w) w = getCircleXPosition("battery"); - const battery = E.getBattery(); + let battery = E.getBattery(); - const powerIcon = atob("EBCBAAAAA8ADwA/wD/AP8A/wD/AP8A/wD/AP8A/wD/AH4AAA"); + let powerIcon = atob("EBCBAAAAA8ADwA/wD/AP8A/wD/AP8A/wD/AP8A/wD/AH4AAA"); drawCircleBackground(w); @@ -371,18 +409,18 @@ function drawBattery(w) { function drawWeather(w) { if (!w) w = getCircleXPosition("weather"); - const weather = getWeather(); - const tempString = weather ? locale.temp(weather.temp - 273.15) : undefined; - const code = weather ? weather.code : -1; + let weather = getWeather(); + let tempString = weather ? locale.temp(weather.temp - 273.15) : undefined; + let code = weather ? weather.code : -1; drawCircleBackground(w); - const color = getCircleColor("weather"); + let color = getCircleColor("weather"); let percent; - const data = settings.weatherCircleData; + let data = settings.weatherCircleData; switch (data) { case "humidity": - const humidity = weather ? weather.hum : undefined; + let humidity = weather ? weather.hum : undefined; if (humidity >= 0) { percent = humidity / 100; drawGauge(w, h3, percent, color); @@ -390,7 +428,7 @@ function drawWeather(w) { break; case "wind": if (weather) { - const wind = locale.speed(weather.wind).match(/^(\D*\d*)(.*)$/); + let wind = locale.speed(weather.wind).match(/^(\D*\d*)(.*)$/); if (wind[1] >= 0) { if (wind[2] == "kmh") { wind[1] = windAsBeaufort(wind[1]); @@ -410,25 +448,24 @@ function drawWeather(w) { writeCircleText(w, tempString ? tempString : "?"); if (code > 0) { - const icon = getWeatherIconByCode(code); + let icon = getWeatherIconByCode(code); if (icon) g.drawImage(getImage(icon, getCircleIconColor("weather", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); } else { g.drawString("?", w, h3 + radiusOuter); } } - function drawSunProgress(w) { if (!w) w = getCircleXPosition("sunprogress"); - const percent = getSunProgress(); + let percent = getSunProgress(); // sunset icons: - const sunSetDown = atob("EBCBAAAAAAABgAAAAAATyAZoBCB//gAAAAAGYAPAAYAAAAAA"); - const sunSetUp = atob("EBCBAAAAAAABgAAAAAATyAZoBCB//gAAAAABgAPABmAAAAAA"); + let sunSetDown = atob("EBCBAAAAAAABgAAAAAATyAZoBCB//gAAAAAGYAPAAYAAAAAA"); + let sunSetUp = atob("EBCBAAAAAAABgAAAAAATyAZoBCB//gAAAAABgAPABmAAAAAA"); drawCircleBackground(w); - const color = getCircleColor("sunprogress"); + let color = getCircleColor("sunprogress"); drawGauge(w, h3, percent, color); @@ -436,15 +473,15 @@ function drawSunProgress(w) { let icon = sunSetDown; let text = "?"; - const times = getSunData(); + let times = getSunData(); if (times != undefined) { - const sunRise = Math.round(times.sunrise.getTime() / 1000); - const sunSet = Math.round(times.sunset.getTime() / 1000); + let sunRise = Math.round(times.sunrise.getTime() / 1000); + let sunSet = Math.round(times.sunset.getTime() / 1000); if (!isDay()) { // night if (now > sunRise) { // after sunRise - const upcomingSunRise = sunRise + 60 * 60 * 24; + let upcomingSunRise = sunRise + 60 * 60 * 24; text = formatSeconds(upcomingSunRise - now); } else { text = formatSeconds(sunRise - now); @@ -468,12 +505,12 @@ function drawTemperature(w) { getPressureValue("temperature").then((temperature) => { drawCircleBackground(w); - const color = getCircleColor("temperature"); + let color = getCircleColor("temperature"); let percent; if (temperature) { - const min = -40; - const max = 85; + let min = -40; + let max = 85; percent = (temperature - min) / (max - min); drawGauge(w, h3, percent, color); } @@ -482,7 +519,7 @@ function drawTemperature(w) { if (temperature) writeCircleText(w, locale.temp(temperature)); - + g.drawImage(getImage(atob("EBCBAAAAAYADwAJAAkADwAPAA8ADwAfgB+AH4AfgA8ABgAAA"), getCircleIconColor("temperature", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); }); @@ -494,12 +531,12 @@ function drawPressure(w) { getPressureValue("pressure").then((pressure) => { drawCircleBackground(w); - const color = getCircleColor("pressure"); + let color = getCircleColor("pressure"); let percent; if (pressure && pressure > 0) { - const minPressure = 950; - const maxPressure = 1050; + let minPressure = 950; + let maxPressure = 1050; percent = (pressure - minPressure) / (maxPressure - minPressure); drawGauge(w, h3, percent, color); } @@ -520,12 +557,12 @@ function drawAltitude(w) { getPressureValue("altitude").then((altitude) => { drawCircleBackground(w); - const color = getCircleColor("altitude"); + let color = getCircleColor("altitude"); let percent; if (altitude) { - const min = 0; - const max = 10000; + let min = 0; + let max = 10000; percent = (altitude - min) / (max - min); drawGauge(w, h3, percent, color); } @@ -544,7 +581,7 @@ function drawAltitude(w) { * wind goes from 0 to 12 (see https://en.wikipedia.org/wiki/Beaufort_scale) */ function windAsBeaufort(windInKmh) { - const beaufort = [2, 6, 12, 20, 29, 39, 50, 62, 75, 89, 103, 118]; + let beaufort = [2, 6, 12, 20, 29, 39, 50, 62, 75, 89, 103, 118]; let l = 0; while (l < beaufort.length && beaufort[l] < windInKmh) { l++; @@ -557,20 +594,22 @@ function windAsBeaufort(windInKmh) { * Choose weather icon to display based on weather conditition code * https://openweathermap.org/weather-conditions#Weather-Condition-Codes-2 */ -function getWeatherIconByCode(code) { - const codeGroup = Math.round(code / 100); +function getWeatherIconByCode(code, big) { + let codeGroup = Math.round(code / 100); + if (big == undefined) big = false; // weather icons: - const weatherCloudy = atob("EBCBAAAAAAAAAAfgD/Af8H/4//7///////9//z/+AAAAAAAA"); - const weatherSunny = atob("EBCBAAAAAYAQCBAIA8AH4A/wb/YP8A/gB+ARiBAIAYABgAAA"); - const weatherMoon = atob("EBCBAAAAAYAP8B/4P/w//D/8f/5//j/8P/w//B/4D/ABgAAA"); - const weatherPartlyCloudy = atob("EBCBAAAAAAAYQAMAD8AIQBhoW+AOYBwwOBBgHGAGP/wf+AAA"); - const weatherRainy = atob("EBCBAAAAAYAH4AwwOBBgGEAOQAJBgjPOEkgGYAZgA8ABgAAA"); - const weatherPartlyRainy = atob("EBCBAAAAEEAQAAeADMAYaFvoTmAMMDgQIBxhhiGGG9wDwAGA"); - const weatherSnowy = atob("EBCBAAAAAAADwAGAEYg73C50BCAEIC50O9wRiAGAA8AAAAAA"); - const weatherFoggy = atob("EBCBAAAAAAADwAZgDDA4EGAcQAZAAgAAf74AAAAAd/4AAAAA"); - const weatherStormy = atob("EBCBAAAAAYAH4AwwOBBgGEAOQMJAgjmOGcgAgACAAAAAAAAA"); - + let weatherCloudy = big ? atob("QEDBAP//wxgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAAB//gAAAAAAAP//gAAAAAAD///AAAAAAAf4H+AAAAAAD8AD8AAAAAAfgAH4AAAAAB8AAPwAAAAAPgAAf/AAAAB8AAA//AAAAHgAAB/+AAAAeAAAH/8AAAH4AAAIH4AAB/AAAAAHwAAf8AAAAAPgAD/wAAAAAeAAPwAAAAAB4AB8AAAAAADwAHgAAAAAAPAA+AAAAAAA8ADwAAAAAADwA/AAAAAAAPAH8AAAAAAA8A/wAAAAAAHwH4AAAAAAAfg+AAAAAAAAfHwAAAAAAAA+eAAAAAAAAB54AAAAAAAAHvAAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD/AAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD3gAAAAAAAAeeAAAAAAAAB58AAAAAAAAPj4AAAAAAAB8H4AAAAAAAfgP////////8Af////////gA////////8AAf//////+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : atob("EBCBAAAAAAAAAAfgD/Af8H/4//7///////9//z/+AAAAAAAA"); + let weatherSunny = big ? atob("QEDBAP//wxgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAwADwADAAAAHgAPAAeAAAAfAA8AD4AAAA+ADwAfAAAAB8APAD4AAAAD4B+AfAAAAAHw//D4AAAAAPv//fAAAAAAf///4AAAAAA/4H/AAAAAAB+AH4AAAAAAPgAHwAAAAAA8AAPAAAAAAHwAA+AAAAAAeAAB4AAAAAB4AAHgAAAAAPAAAPAAAA//8AAA//8AD//wAAD//wAP//AAAP//AA//8AAA//8AAADwAADwAAAAAHgAAeAAAAAAeAAB4AAAAAB8AAPgAAAAADwAA8AAAAAAPgAHwAAAAAAfgB+AAAAAAD/gf8AAAAAAf///4AAAAAD7//3wAAAAAfD/8PgAAAAD4B+AfAAAAAfADwA+AAAAD4APAB8AAAAfAA8AD4AAAB4ADwAHgAAADAAPAAMAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : atob("EBCBAAAAAYAQCBAIA8AH4A/wb/YP8A/gB+ARiBAIAYABgAAA"); + let weatherMoon = big ? atob("QEDBAP//wxgAAAYAAAAPAAAAD4AAAA8AAAAPwAAADwAAAA/gAAAPAAAAB/APAP/wAAAH+A8A//AAAAf4DwD/8AAAB/wPAP/wAAAH/gAADwAAAAe+AAAPAAAAB54AAA8AAAAHngAADwAAAAePAAAAAAAAD48OAAAAAAAPDw+AAAAAAB8PD8AAAAAAHg8P4AAAAAA+DwPwAAAAAHwfAfgAAAAB+D4A/AAA8AfwfgB/8AD//+D+AD/8AP//wfgAH/4Af/8B8AAf/wB//APgAAgfgD+AA8AAAAfAH8AHwAAAA+AP8B+AAAAB4Af//4AAAAHgA///gAAAAPAA//8AAAAA8AAf/wAAAADwAAAAAAAAAPAAAAAAAAAA8AcAAAAAAADwD+AAAAAAAfAfgAAAAAAB+D4AAAAAAAB8fAAAAAAAAD54AAAAAAAAHngAAAAAAAAe8AAAAAAAAA/wAAAAAAAAD/AAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD/AAAAAAAAAPeAAAAAAAAB54AAAAAAAAHnwAAAAAAAA+PgAAAAAAAHwfgAAAAAAB+A/////////wB////////+AD////////wAB///////4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : atob("EBCBAAAAAYAP8B/4P/w//D/8f/5//j/8P/w//B/4D/ABgAAA"); + let weatherPartlyCloudy = big ? atob("QEDBAP//wxgAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAABwAPAA4AAAAHgA8AHgAAAAfADwA+AAAAA+AfgHwAAAAB8P/w+AAAAAD7//3wAAAAAH///+BAAAAAP+B/wOAAAAAfgB+B8AAAAD4AD8H4AAAAPAA/wPwAAAB8AH+Af/AAAHgA/AA//AAAeAH4AB/+AADwAfAAH/8A//AD4AAIH4D/8AfAAAAHwP/wB4AAAAPg//AHgAAAAeAA8B+AAAAB4AB4fwAAAADwAHn/AAAAAPAAff8AAAAA8AA/8AAAAADwAD/AAAAAAPAEH4AAAAAA8A4PgAAAAAHwHgcAAAAAAfg+AwAAAAAAfHwAAAAAAAA+eAAAAAAAAB54AAAAAAAAHvAAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD/AAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD3gAAAAAAAAeeAAAAAAAAB58AAAAAAAAPj4AAAAAAAB8H4AAAAAAAfgP////////8Af////////gA////////8AAf//////+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : atob("EBCBAAAAAAAYQAMAD8AIQBhoW+AOYBwwOBBgHGAGP/wf+AAA"); + let weatherRainy = big ? atob("QEDBAP//wxgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAAB//gAAAAAAAP//gAAAAAAD///AAAAAAAf4H+AAAAAAD8AD8AAAAAAfgAH4AAAAAB8AAPwAAAAAPgAAf/AAAAB8AAA//AAAAHgAAB/+AAAAeAAAH/8AAAH4AAAIH4AAB/AAAAAHwAAf8AAAAAPgAD/wAAAAAeAAPwAAAAAB4AB8AAAAAADwAHgAAAAAAPAA+AAAAAAA8ADwAAAAAADwA/AAAAAAAPAH8AAAAAAA8A/wAAAAAAHwH4APAA8AAfg+AA8ADwAAfHwADwAPAAA+eAAPAA8AAB54AAAAAAAAHvAAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD/AADw8PDwAP8AAPDw8PAA/wAA8PDw8AD3gADw8PDwAeeAAAAAAAAB58AAAAAAAAPj4AAAAAAAB8H4AAAAAAAfgP/w8PDw8P8Af/Dw8PDw/gA/8PDw8PD8AAfw8PDw8OAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAPAAAAAAAPAA8AAAAAAA8ADwAAAAAADwAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : atob("EBCBAAAAAYAH4AwwOBBgGEAOQAJBgjPOEkgGYAZgA8ABgAAA"); + let weatherPartlyRainy = big ? atob("QEDBAP//wxgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAAB//gAAAAAAAP//gAAAAAAD///AAAAAAAf4H+AAAAAAD8AD8AAAAAAfgAH4AAAAAB8AAPwAAAAAPgAAf/AAAAB8AAA//AAAAHgAAB/+AAAAeAAAH/8AAAH4AAAIH4AAB/AAAAAHwAAf8AAAAAPgAD/wAAAAAeAAPwAAAAAB4AB8AAAAAADwAHgAAAAAAPAA+AAAAAAA8ADwAAAAAADwA/AAAAAAAPAH8AAAAAAA8A/wAAAAAAHwH4AAAA8AAfg+AAAADwAAfHwAAAAPAAA+eAAAAA8AAB54AAAADwAAHvAAAAAPAAAP8AAAAA8AAA/wAAAADwAAD/AAAA8PAAAP8AAADw8AAA/wAAAPDwAAD3gAAA8PAAAeeAAADw8AAB58AAAPDwAAPj4AAA8PAAB8H4AADw8AAfgP//8PDw//8Af//w8PD//gA///Dw8P/8AAf/8PDw/+AAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : atob("EBCBAAAAEEAQAAeADMAYaFvoTmAMMDgQIBxhhiGGG9wDwAGA"); + let weatherSnowy = big ? atob("QEDBAP//wxgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAAB//gAAAAAAAP//gAAAAAAD///AAAAAAAf4H+AAAAAAD8AD8AAAAAAfgAH4AAAAAB8AAPwAAAAAPgAAf/AAAAB8AAA//AAAAHgAAB/+AAAAeAAAH/8AAAH4AAAIH4AAB/AAAAAHwAAf8AAAAAPgAD/wAAAAAeAAPwAAAAAB4AB8AAAAAADwAHgAAAAAAPAA+AAAAAAA8ADwAAAAAADwA/AAAAAAAPAH8AAAAAAA8A/wAAAAAAHwH4AAAADwAfg+AAAAAPAAfHwAAAAA8AA+eAAAAADwAB54AA8AD/8AHvAADwAP/wAP8AAPAA//AA/wAA8AD/8AD/AA//AA8AAP8AD/8ADwAA/wAP/wAPAAD3gA//AA8AAeeAAPAAAAAB58AA8AAAAAPj4ADwAAAAB8H4APAAAAAfgP/wAA8A//8Af/AADwD//gA/8AAPAP/8AAfwAA8A/+AAAAAA//AAAAAAAAD/8AAAAAAAAP/wAAAAAAAA//AAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : atob("EBCBAAAAAAADwAGAEYg73C50BCAEIC50O9wRiAGAA8AAAAAA"); + let weatherFoggy = big ? atob("QEDBAP//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAwADwADAAAAHgAPAAeAAAAfAA8AD4AAAA+ADwAfAAAAB8APAD4AAAAD4B+AfAAAAAHw//D4AAAAAPv//fAAAAAAf///4AAAAAA/4H/AAAAAAB+AH4AAAAAAPgAHwAAAAAA8AAPAAAAAAHwAA+AAAAAAeAAB4AAAAAB4AAHgAAAAAPAAAPAAAAAAAAAA//8AAAAAAAD//wAAAAAAAP//AAAAAAAA//8AD///AADwAAAP//8AAeAAAA///wAB4AAAD///AAPgAAAAAAAAA8AAAAAAAAAHwAAAAAAAAB+AAAAAAAAAf8AAAAD///D/4AAAAP//8P3wAAAA///w8PgAAAD///CAfAAAAAAAAAA+AAAAAAAAAB8AAAAAAAAAD4AAAAAAAAAHgAAP//8PAAMAAA///w8AAAAAD///DwAAAAAP//8PAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : atob("EBCBAAAAAAADwAZgDDA4EGAcQAZAAgAAf74AAAAAd/4AAAAA"); + let weatherStormy = big ? atob("QEDBAP//wxgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAAB//gAAAAAAAP//gAAAAAAD///AAAAAAAf4H+AAAAAAD8AD8AAAAAAfgAH4AAAAAB8AAPwAAAAAPgAAf/AAAAB8AAA//AAAAHgAAB/+AAAAeAAAH/8AAAH4AAAIH4AAB/AAAAAHwAAf8AAAAAPgAD/wAAAAAeAAPwAAAAAB4AB8AAAAAADwAHgAAAAAAPAA+AAAAAAA8ADwAAAAAADwA/AAAAAAAPAH8AAAAAAA8A/wAAAAAAHwH4AAAAAAAfg+AAAAAAAAfHwAAAAAAAA+eAAAAAAAAB54AAAAD/AAHvAAAAAf4AAP8AAAAB/gAA/wAAAAP8AAD/AAAAA/gAAP8AAAAH+AAA/wAAAAfwAAD3gAAAD/AAAeeAAAAP4AAB58AAAB/AAAPj4AAAH8AAB8H4AAA/gAAfgP//+D//D/8Af//4f/4P/gA///B//B/8AAf/8P/8P+AAAAAAAPgAAAAAAAAB8AAAAAAAAAHwAAAAAAAAA+AAAAAAAAADwAAAAAAAAAfAAAAAAAAAB4AAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : atob("EBCBAAAAAYAH4AwwOBBgGEAOQMJAgjmOGcgAgACAAAAAAAAA"); + let unknown = big ? atob("QEDBAP//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAAAAAAf/4AAAAAAAH//4AAAAAAA///wAAAAAAH+B/gAAAAAA/AA/AAAAAAH4AB+AAAAAA/AAD4AAAAAD4H4HwAAAAAfB/4PgAAAAB8P/weAAAAAHg//h4AAAAA+Hw+HwAAAAD4eB8PAAAAAP/wDw8AAAAA//APDwAAAAD/8A8PAAAAAH/gDw8AAAAAAAAfDwAAAAAAAH4fAAAAAAAB/B4AAAAAAAf4HgAAAAAAD/A+AAAAAAAfwHwAAAAAAD8A+AAAAAAAPgH4AAAAAAB8B/AAAAAAAHgf4AAAAAAA+H+AAAAAAADwfwAAAAAAAPD8AAAAAAAA8PAAAAAAAAD/8AAAAAAAAP/wAAAAAAAA//AAAAAAAAB/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf+AAAAAAAAD/8AAAAAAAAP/wAAAAAAAA//AAAAAAAADw8AAAAAAAAPDwAAAAAAAA8PAAAAAAAADw8AAAAAAAAP/wAAAAAAAA//AAAAAAAAD/8AAAAAAAAH/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : undefined; + switch (codeGroup) { case 2: return weatherStormy; @@ -607,16 +646,16 @@ function getWeatherIconByCode(code) { return weatherCloudy; } default: - return undefined; + return unknown; } } function isDay() { - const times = getSunData(); + let times = getSunData(); if (times == undefined) return true; - const sunRise = Math.round(times.sunrise.getTime() / 1000); - const sunSet = Math.round(times.sunset.getTime() / 1000); + let sunRise = Math.round(times.sunrise.getTime() / 1000); + let sunSet = Math.round(times.sunset.getTime() / 1000); return (now > sunRise && now < sunSet); } @@ -633,7 +672,7 @@ function formatSeconds(s) { function getSunData() { if (location != undefined && location.lat != undefined) { - const SunCalc = require("https://raw.githubusercontent.com/mourner/suncalc/master/suncalc.js"); + let SunCalc = require("https://raw.githubusercontent.com/mourner/suncalc/master/suncalc.js"); // get today's sunlight times for lat/lon return SunCalc ? SunCalc.getTimes(new Date(), location.lat, location.lon) : undefined; } @@ -646,14 +685,14 @@ function getSunData() { * Taken from rebble app and modified */ function getSunProgress() { - const times = getSunData(); + let times = getSunData(); if (times == undefined) return 0; - const sunRise = Math.round(times.sunrise.getTime() / 1000); - const sunSet = Math.round(times.sunset.getTime() / 1000); + let sunRise = Math.round(times.sunrise.getTime() / 1000); + let sunSet = Math.round(times.sunset.getTime() / 1000); if (isDay()) { // during day - const dayLength = sunSet - sunRise; + let dayLength = sunSet - sunRise; if (now > sunRise) { return (now - sunRise) / dayLength; } else { @@ -662,10 +701,10 @@ function getSunProgress() { } else { // during night if (now < sunRise) { - const prevSunSet = sunSet - 60 * 60 * 24; + let prevSunSet = sunSet - 60 * 60 * 24; return 1 - (sunRise - now) / (sunRise - prevSunSet); } else { - const upcomingSunRise = sunRise + 60 * 60 * 24; + let upcomingSunRise = sunRise + 60 * 60 * 24; return (upcomingSunRise - now) / (upcomingSunRise - sunSet); } } @@ -700,16 +739,16 @@ function radians(a) { * This draws the actual gauge consisting out of lots of little filled circles */ function drawGauge(cx, cy, percent, color) { - const offset = 15; - const end = 360 - offset; - const radius = radiusInner + (circleCount == 3 ? 3 : 2); - const size = radiusOuter - radiusInner - 2; + let offset = 15; + let end = 360 - offset; + let radius = radiusInner + (circleCount == 3 ? 3 : 2); + let size = radiusOuter - radiusInner - 2; if (percent <= 0) return; // no gauge needed if (percent > 1) percent = 1; - const startRotation = -offset; - const endRotation = startRotation - ((end - offset) * percent); + let startRotation = -offset; + let endRotation = startRotation - ((end - offset) * percent); color = getGradientColor(color, percent); g.setColor(color); @@ -723,7 +762,7 @@ function drawGauge(cx, cy, percent, color) { function writeCircleText(w, content) { if (content == undefined) return; - const font = String(content).length > 4 ? circleFontSmall : String(content).length > 3 ? circleFont : circleFontBig; + let font = String(content).length > 4 ? circleFontSmall : String(content).length > 3 ? circleFont : circleFontBig; g.setFont(font); g.setFontAlign(0, 0); @@ -755,7 +794,7 @@ function getSteps() { } function getWeather() { - const jsonWeather = storage.readJSON('weather.json'); + let jsonWeather = storage.readJSON('weather.json'); return jsonWeather && jsonWeather.weather ? jsonWeather.weather : undefined; } @@ -796,7 +835,7 @@ function getPressureValue(type) { }); } -Bangle.on('lock', function(isLocked) { +function onLock(isLocked) { if (!isLocked) { draw(); if (isCircleEnabled("hr")) { @@ -805,11 +844,10 @@ Bangle.on('lock', function(isLocked) { } else { Bangle.setHRMPower(0, "circleclock"); } -}); +} +Bangle.on('lock', onLock); - -let timerHrm; -Bangle.on('HRM', function(hrm) { +function onHRM(hrm) { if (isCircleEnabled("hr")) { if (hrm.confidence >= (settings.confidence)) { hrtValue = hrm.bpm; @@ -826,23 +864,48 @@ Bangle.on('HRM', function(hrm) { }, settings.hrmValidity * 1000); } } -}); +} +Bangle.on('HRM', onHRM); -Bangle.on('charging', function(charging) { +function onCharging(charging) { if (isCircleEnabled("battery")) drawBattery(); -}); +} +Bangle.on('charging', onCharging); + if (isCircleEnabled("hr")) { enableHRMSensor(); } -Bangle.setUI("clock"); +Bangle.setUI({ + mode : "clock", + remove : function() { + // Called to unload all of the clock app + Bangle.removeListener('charging', onCharging); + Bangle.removeListener('lock', onLock); + Bangle.removeListener('HRM', onHRM); + + Bangle.setHRMPower(0, "circleclock"); + + if (timerHrm) clearTimeout(timerHrm); + timerHrm = undefined; + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + + delete Graphics.prototype.setFontRobotoRegular50NumericOnly; + delete Graphics.prototype.setFontRobotoRegular21; + }}); + Bangle.loadWidgets(); -// schedule a draw for the next minute -setTimeout(function() { - // draw in interval - setInterval(draw, settings.updateInterval * 1000); -}, 60000 - (Date.now() % 60000)); +// schedule a draw for the next second or minute +function queueDraw() { + let queueMillis = settings.updateInterval * 1000; + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, queueMillis - (Date.now() % queueMillis)); +} draw(); diff --git a/apps/circlesclock/default.json b/apps/circlesclock/default.json index ea00dc347..6247c058c 100644 --- a/apps/circlesclock/default.json +++ b/apps/circlesclock/default.json @@ -22,5 +22,6 @@ "circle3colorizeIcon": true, "circle4colorizeIcon": false, "hrmValidity": 60, - "updateInterval": 60 + "updateInterval": 60, + "showBigWeather": false } diff --git a/apps/circlesclock/metadata.json b/apps/circlesclock/metadata.json index 837fcaa88..c2d3b3364 100644 --- a/apps/circlesclock/metadata.json +++ b/apps/circlesclock/metadata.json @@ -1,7 +1,7 @@ { "id": "circlesclock", "name": "Circles clock", "shortName":"Circles clock", - "version":"0.13", + "version":"0.16", "description": "A clock with three or four circles for different data at the bottom in a probably familiar style", "icon": "app.png", "screenshots": [{"url":"screenshot-dark.png"}, {"url":"screenshot-light.png"}, {"url":"screenshot-dark-4.png"}, {"url":"screenshot-light-4.png"}], diff --git a/apps/circlesclock/screenshot-light-with-big-weather.png b/apps/circlesclock/screenshot-light-with-big-weather.png new file mode 100644 index 000000000..d1d569247 Binary files /dev/null and b/apps/circlesclock/screenshot-light-with-big-weather.png differ diff --git a/apps/circlesclock/settings.js b/apps/circlesclock/settings.js index fb23f8d5e..0aa8dc826 100644 --- a/apps/circlesclock/settings.js +++ b/apps/circlesclock/settings.js @@ -68,6 +68,11 @@ return x + 's'; }, onchange: x => save('updateInterval', x), + }, + /*LANG*/'show big weather': { + value: !!settings.showBigWeather, + format: () => (settings.showBigWeather ? 'Yes' : 'No'), + onchange: x => save('showBigWeather', x), } }; E.showMenu(menu); diff --git a/apps/compass/ChangeLog b/apps/compass/ChangeLog index deb1072f5..cb1c6d463 100644 --- a/apps/compass/ChangeLog +++ b/apps/compass/ChangeLog @@ -5,3 +5,4 @@ 0.05: Fix bearing not clearing correctly (visible in single or double digit bearings) 0.06: Add button for force compass calibration 0.07: Use 360-heading to output the correct heading value (fix #1866) +0.08: Added adjustment for Bangle.js magnetometer heading fix diff --git a/apps/compass/compass.js b/apps/compass/compass.js index dd398ffa6..9a7aec2fc 100644 --- a/apps/compass/compass.js +++ b/apps/compass/compass.js @@ -20,7 +20,7 @@ ag.setColor(1).fillCircle(AGM,AGM,AGM-1,AGM-1); ag.setColor(0).fillCircle(AGM,AGM,AGM-11,AGM-11); function arrow(r,c) { - r=r*Math.PI/180; + r=(360-r)*Math.PI/180; var p = Math.PI/2; ag.setColor(c).fillPoly([ AGM+AGH*Math.sin(r), AGM-AGH*Math.cos(r), @@ -34,7 +34,7 @@ var oldHeading = 0; Bangle.on('mag', function(m) { if (!Bangle.isLCDOn()) return; g.reset(); - if (isNaN(m.heading)) { + if (isNaN(m.heading)) { if (!wasUncalibrated) { g.clearRect(0,24,W,48); g.setFontAlign(0,-1).setFont("6x8"); @@ -49,7 +49,7 @@ Bangle.on('mag', function(m) { g.setFontAlign(0,0).setFont("6x8",3); var y = 36; g.clearRect(M-40,24,M+40,48); - g.drawString(Math.round(360-m.heading),M,y,true); + g.drawString(Math.round(m.heading),M,y,true); } diff --git a/apps/compass/metadata.json b/apps/compass/metadata.json index a3995a123..1a614e1f8 100644 --- a/apps/compass/metadata.json +++ b/apps/compass/metadata.json @@ -1,7 +1,7 @@ { "id": "compass", "name": "Compass", - "version": "0.07", + "version": "0.08", "description": "Simple compass that points North", "icon": "compass.png", "screenshots": [{"url":"screenshot_compass.png"}], diff --git a/apps/dotmatrixclock/ChangeLog b/apps/dotmatrixclock/ChangeLog index 7ab9e14a9..12edf33a3 100644 --- a/apps/dotmatrixclock/ChangeLog +++ b/apps/dotmatrixclock/ChangeLog @@ -1 +1,2 @@ 0.01: Create dotmatrix clock app +0.02: Added adjustment for Bangle.js magnetometer heading fix diff --git a/apps/dotmatrixclock/app.js b/apps/dotmatrixclock/app.js index ba34d4885..493a3c43f 100644 --- a/apps/dotmatrixclock/app.js +++ b/apps/dotmatrixclock/app.js @@ -186,7 +186,7 @@ function drawCompass(lastHeading) { 'NW' ]; const cps = Bangle.getCompass(); - let angle = cps.heading; + let angle = 360-cps.heading; let heading = angle? directions[Math.round(((angle %= 360) < 0 ? angle + 360 : angle) / 45) % 8]: "-- "; @@ -351,4 +351,4 @@ Bangle.on('faceUp', (up) => { setSensors(1); resetDisplayTimeout(); } -}); \ No newline at end of file +}); diff --git a/apps/dotmatrixclock/metadata.json b/apps/dotmatrixclock/metadata.json index 3425dc1b2..fdfb5271f 100644 --- a/apps/dotmatrixclock/metadata.json +++ b/apps/dotmatrixclock/metadata.json @@ -1,7 +1,7 @@ { "id": "dotmatrixclock", "name": "Dotmatrix Clock", - "version": "0.01", + "version": "0.02", "description": "A clear white-on-blue dotmatrix simulated clock", "icon": "dotmatrixclock.png", "type": "clock", diff --git a/apps/dtlaunch/ChangeLog b/apps/dtlaunch/ChangeLog index 16c550334..1054aa387 100644 --- a/apps/dtlaunch/ChangeLog +++ b/apps/dtlaunch/ChangeLog @@ -14,3 +14,5 @@ 0.14: Don't move pages when doing exit swipe - Bangle 2. 0.15: 'Swipe to exit'-code is slightly altered to be more reliable - Bangle 2. 0.16: Use default Bangle formatter for booleans +0.17: Bangle 2: Fast loading on exit to clock face. Added option for exit to +clock face by timeout. diff --git a/apps/dtlaunch/app-b2.js b/apps/dtlaunch/app-b2.js index 8cd5790bb..48d9b2f38 100644 --- a/apps/dtlaunch/app-b2.js +++ b/apps/dtlaunch/app-b2.js @@ -1,28 +1,32 @@ +{ // must be inside our own scope here so that when we are unloaded everything disappears + /* Desktop launcher * */ -var settings = Object.assign({ +let settings = Object.assign({ showClocks: true, showLaunchers: true, direct: false, - oneClickExit:false, - swipeExit: false + oneClickExit: false, + swipeExit: false, + timeOut: "Off" }, require('Storage').readJSON("dtlaunch.json", true) || {}); -if( settings.oneClickExit) - setWatch(_=> load(), BTN1); - -var s = require("Storage"); -var apps = s.list(/\.info$/).map(app=>{ - var a=s.readJSON(app,1); +if (settings.oneClickExit) { + var buttonWatch = setWatch(_=> returnToClock(), BTN1, {edge: 'falling'}); +} + +let s = require("Storage"); + var apps = s.list(/\.info$/).map(app=>{ + let a=s.readJSON(app,1); return a && { name:a.name, type:a.type, icon:a.icon, sortorder:a.sortorder, src:a.src };}).filter( app=>app && (app.type=="app" || (app.type=="clock" && settings.showClocks) || (app.type=="launch" && settings.showLaunchers) || !app.type)); apps.sort((a,b)=>{ - var n=(0|a.sortorder)-(0|b.sortorder); + let n=(0|a.sortorder)-(0|b.sortorder); if (n) return n; // do sortorder first if (a.nameb.name) return 1; @@ -33,29 +37,28 @@ apps.forEach(app=>{ app.icon = s.read(app.icon); // should just be a link to a memory area }); -var Napps = apps.length; -var Npages = Math.ceil(Napps/4); -var maxPage = Npages-1; -var selected = -1; -var oldselected = -1; -var page = 0; +let Napps = apps.length; +let Npages = Math.ceil(Napps/4); +let maxPage = Npages-1; +let selected = -1; +let oldselected = -1; +let page = 0; const XOFF = 24; const YOFF = 30; -function draw_icon(p,n,selected) { - var x = (n%2)*72+XOFF; - var y = n>1?72+YOFF:YOFF; +let drawIcon= function(p,n,selected) { + let x = (n%2)*72+XOFF; + let y = n>1?72+YOFF:YOFF; (selected?g.setColor(g.theme.fgH):g.setColor(g.theme.bg)).fillRect(x+11,y+3,x+60,y+52); g.clearRect(x+12,y+4,x+59,y+51); g.setColor(g.theme.fg); try{g.drawImage(apps[p*4+n].icon,x+12,y+4);} catch(e){} g.setFontAlign(0,-1,0).setFont("6x8",1); - var txt = apps[p*4+n].name.replace(/([a-z])([A-Z])/g, "$1 $2").split(" "); - var lineY = 0; - var line = ""; - while (txt.length > 0){ - var c = txt.shift(); - + let txt = apps[p*4+n].name.replace(/([a-z])([A-Z])/g, "$1 $2").split(" "); + let lineY = 0; + let line = ""; + while (txt.length > 0){ + let c = txt.shift(); if (c.length + 1 + line.length > 13){ if (line.length > 0){ g.drawString(line.trim(),x+36,y+54+lineY*8); @@ -67,29 +70,34 @@ function draw_icon(p,n,selected) { } } g.drawString(line.trim(),x+36,y+54+lineY*8); -} +}; -function drawPage(p){ +let drawPage = function(p){ g.reset(); g.clearRect(0,24,175,175); - var O = 88+YOFF/2-12*(Npages/2); - for (var j=0;j{ +Bangle.loadWidgets(); +//g.clear(); +Bangle.drawWidgets(); +drawPage(0); + +let swipeListenerDt = function(dirLeftRight, dirUpDown){ selected = 0; oldselected=-1; - if(settings.swipeExit && dirLeftRight==1) load(); + if(settings.swipeExit && dirLeftRight==1) returnToClock(); if (dirUpDown==-1||dirLeftRight==-1){ ++page; if (page>maxPage) page=0; drawPage(page); @@ -97,24 +105,25 @@ Bangle.on("swipe",(dirLeftRight, dirUpDown)=>{ --page; if (page<0) page=maxPage; drawPage(page); } -}); +}; +Bangle.on("swipe",swipeListenerDt); -function isTouched(p,n){ +let isTouched = function(p,n){ if (n<0 || n>3) return false; - var x1 = (n%2)*72+XOFF; var y1 = n>1?72+YOFF:YOFF; - var x2 = x1+71; var y2 = y1+81; + let x1 = (n%2)*72+XOFF; let y1 = n>1?72+YOFF:YOFF; + let x2 = x1+71; let y2 = y1+81; return (p.x>x1 && p.y>y1 && p.x{ - var i; +let touchListenerDt = function(_,p){ + let i; for (i=0;i<4;i++){ if((page*4+i)=0 || settings.direct) { if (selected!=i && !settings.direct){ - draw_icon(page,selected,false); + drawIcon(page,selected,false); } else { load(apps[page*4+i].src); } @@ -125,12 +134,39 @@ Bangle.on("touch",(_,p)=>{ } } if ((i==4 || (page*4+i)>Napps) && selected>=0) { - draw_icon(page,selected,false); + drawIcon(page,selected,false); selected=-1; } -}); +}; +Bangle.on("touch",touchListenerDt); -Bangle.loadWidgets(); -g.clear(); -Bangle.drawWidgets(); -drawPage(0); +const returnToClock = function() { + Bangle.setUI(); + if (buttonWatch) { + clearWatch(buttonWatch); + delete buttonWatch; + } + if (timeoutToClock) { + clearTimeout(timeoutToClock); + delete timeoutToClock; + } + Bangle.removeListener("swipe", swipeListenerDt); + Bangle.removeListener("touch", touchListenerDt); + var apps = []; + delete apps; + delete returnToClock; + setTimeout(eval, 0, s.read(".bootcde")); +}; + +// taken from Icon Launcher with minor alterations +var timeoutToClock; +const updateTimeoutToClock = function(){ + if (settings.timeOut!="Off"){ + let time=parseInt(settings.timeOut); //the "s" will be trimmed by the parseInt + if (timeoutToClock) clearTimeout(timeoutToClock); + timeoutToClock = setTimeout(returnToClock,time*1000); + } +}; +updateTimeoutToClock(); + +} // end of app scope diff --git a/apps/dtlaunch/metadata.json b/apps/dtlaunch/metadata.json index 36728f342..b71f4ca9b 100644 --- a/apps/dtlaunch/metadata.json +++ b/apps/dtlaunch/metadata.json @@ -1,7 +1,7 @@ { "id": "dtlaunch", "name": "Desktop Launcher", - "version": "0.16", + "version": "0.17", "description": "Desktop style App Launcher with six (four for Bangle 2) apps per page - fast access if you have lots of apps installed.", "screenshots": [{"url":"shot1.png"},{"url":"shot2.png"},{"url":"shot3.png"}], "icon": "icon.png", diff --git a/apps/dtlaunch/settings-b2.js b/apps/dtlaunch/settings-b2.js index fac9c0fff..80ad0414a 100644 --- a/apps/dtlaunch/settings-b2.js +++ b/apps/dtlaunch/settings-b2.js @@ -6,50 +6,63 @@ showLaunchers: true, direct: false, oneClickExit:false, - swipeExit: false + swipeExit: false, + timeOut: "Off" }, require('Storage').readJSON(FILE, true) || {}); function writeSettings() { require('Storage').writeJSON(FILE, settings); } + const timeOutChoices = [/*LANG*/"Off", "10s", "15s", "20s", "30s"]; + E.showMenu({ "" : { "title" : "Desktop launcher" }, - "< Back" : () => back(), - 'Show clocks': { + /*LANG*/"< Back" : () => back(), + /*LANG*/'Show clocks': { value: settings.showClocks, onchange: v => { settings.showClocks = v; writeSettings(); } }, - 'Show launchers': { + /*LANG*/'Show launchers': { value: settings.showLaunchers, onchange: v => { settings.showLaunchers = v; writeSettings(); } }, - 'Direct launch': { + /*LANG*/'Direct launch': { value: settings.direct, onchange: v => { settings.direct = v; writeSettings(); } }, - 'Swipe Exit': { + /*LANG*/'Swipe Exit': { value: settings.swipeExit, onchange: v => { settings.swipeExit = v; writeSettings(); } }, - 'One click exit': { + /*LANG*/'One click exit': { value: settings.oneClickExit, onchange: v => { settings.oneClickExit = v; writeSettings(); } - } + }, + /*LANG*/'Time Out': { // Adapted from Icon Launcher + value: timeOutChoices.indexOf(settings.timeOut), + min: 0, + max: timeOutChoices.length-1, + format: v => timeOutChoices[v], + onchange: v => { + settings.timeOut = timeOutChoices[v]; + writeSettings(); + } + }, }); -}) +}); diff --git a/apps/entonclk/ChangeLog b/apps/entonclk/ChangeLog new file mode 100644 index 000000000..62e2d0c20 --- /dev/null +++ b/apps/entonclk/ChangeLog @@ -0,0 +1 @@ +0.1: New App! \ No newline at end of file diff --git a/apps/entonclk/README.md b/apps/entonclk/README.md new file mode 100644 index 000000000..8c788c7a5 --- /dev/null +++ b/apps/entonclk/README.md @@ -0,0 +1,9 @@ +Enton - Enhanced Anton Clock + +This clock face is based on the 'Anton Clock'. + +Things I changed: + +- The main font for the time is now Audiowide +- Removed the written out day name and replaced it with steps and bpm +- Changed the date string to a (for me) more readable string \ No newline at end of file diff --git a/apps/entonclk/app-icon.js b/apps/entonclk/app-icon.js new file mode 100644 index 000000000..9993b0871 --- /dev/null +++ b/apps/entonclk/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkE/4A/AH4A/AH4A/AH4Aw+cikf/mQDCAAIFBAwQDBBYgXCgEDAQIABn4JBkAFBgIKDgQwFmMD+UCmcgl/zEIMzmcQmYKBmYiCAAfxC4QrBl8wBwcgkYsGC4sAiMAF4UxiIGBn8QAgMSC48wgMRiEDBAISCiYcFC48v//yC4PzgJAGiAXIiczPgPzC4JyBmf/AYQXI+KcCj8wmYFCgEjAYQ3G+cjbQIABJIMzAoUin7XIADpSEK4rWGI4MhmRJBn8j+U/d4MimUTkUzIw5dBl4UBMgIXBAgMyLYKOBmQXHiSbCDgMyl8z+UjmJ1BHgJbHCgM/IYQABAgQJBYYYA/AH4AtaQU/mTvBBozWBd44KBkUSkLnBEo8jkcvBI0/CgMiDAIXHHYIXImUzJQJHH+Y+Bn6Z/ABQA==")) \ No newline at end of file diff --git a/apps/entonclk/app.js b/apps/entonclk/app.js new file mode 100644 index 000000000..69fdea479 --- /dev/null +++ b/apps/entonclk/app.js @@ -0,0 +1,67 @@ +Graphics.prototype.setFontAudiowide = function() { + // Actual height 33 (36 - 4) + var widths = atob("CiAsESQjJSQkHyQkDA=="); + var font = atob("AAAAAAAAAAAAAAAAAAAAAPAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAADgAAAAAAHgAAAAAAfgAAAAAA/gAAAAAD/gAAAAAH/gAAAAAf/AAAAAB/8AAAAAD/4AAAAAP/gAAAAAf/AAAAAB/8AAAAAD/4AAAAAP/gAAAAAf+AAAAAB/8AAAAAH/wAAAAAP/gAAAAA/+AAAAAB/8AAAAAD/wAAAAAD/gAAAAAD+AAAAAAD4AAAAAADwAAAAAADAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAA//+AAAAB///AAAAH///wAAAP///4AAAf///8AAA////+AAA/4AP+AAB/gAD/AAB/AA9/AAD+AB+/gAD+AD+/gAD+AD+/gAD8AH+fgAD8AP8fgAD8AP4fgAD8Af4fgAD8A/wfgAD8A/gfgAD8B/gfgAD8D/AfgAD8D+AfgAD8H+AfgAD8P8AfgAD8P4AfgAD8f4AfgAD8/wAfgAD8/gAfgAD+/gA/gAD+/AA/gAB/eAB/AAB/sAD/AAB/wAH/AAA////+AAAf///8AAAP///4AAAH///wAAAD///gAAAA//+AAAAAP/4AAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD/////gAD/////gAD/////gAD/////gAD/////gAD/////gAD/////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//gAAAAH//gAAAAP//gAD8Af//gAD8A///gAD8B///gAD8B///gAD8B/AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD+D+AfgAD//+AfgAD//+AfgAB//8AfgAA//4AfgAAf/wAfgAAP/gAfgAAB8AAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD+B+A/gAD/////gAB/////AAB/////AAA////+AAAf///8AAAP///4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//4AAAAD//8AAAAD//+AAAAD//+AAAAD//+AAAAD//+AAAAD//+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAD/////gAD/////gAD/////gAD/////gAD/////gAD/////gAD/////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//AAfgAD//wAfgAD//4AfgAD//8AfgAD//8AfgAD//+AfgAD8D+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B/A/gAD8B///gAD8B///gAD8A///AAD8A///AAAAAf/+AAAAAP/4AAAAAD/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB///AAAAH///wAAAf///8AAAf///8AAA////+AAB/////AAB/h+H/AAD/B+B/gAD+B+A/gAD+B+A/gAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B/A/gAD8B///gAD8B///gAD8A///AAAAAf//AAAAAf/+AAAAAH/4AAAAAB/gAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD8AAAAgAD8AAABgAD8AAAHgAD8AAAfgAD8AAA/gAD8AAD/gAD8AAP/gAD8AA//gAD8AB//AAD8AH/8AAD8Af/wAAD8A//AAAD8D/+AAAD8P/4AAAD8f/gAAAD9//AAAAD//8AAAAD//wAAAAD//gAAAAD/+AAAAAD/4AAAAAD/wAAAAAD/AAAAAAD8AAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAH/4AAAAAP/8AAAH+f/+AAAf////AAA/////gAB/////gAB///A/gAD//+AfgAD//+AfgAD+D+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD+D+AfgAD//+AfgAD//+AfgAB///A/gAB/////gAA/////AAAP////AAAD+f/+AAAAAP/8AAAAAH/4AAAAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/AAAAAAf/wAAAAA//4AAAAB//8AAAAB//8AfgAD//+AfgAD/D+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD+B+A/gAD+B+A/gAD/B+B/gAB/////AAB/////AAA////+AAAf///8AAAP///4AAAH///wAAAB///AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAPAAAA/AAfgAAA/AAfgAAA/AAfgAAA/AAfgAAAeAAPAAAAAAAAAAAAAAAAAAAAAAAAAA"); + var scale = 1; // size multiplier for this font + g.setFontCustom(font, 46, widths, 48+(scale<<8)+(1<<16)); +}; + +function getSteps() { + var steps = 0; + try{ + if (WIDGETS.wpedom !== undefined) { + steps = WIDGETS.wpedom.getSteps(); + } else if (WIDGETS.activepedom !== undefined) { + steps = WIDGETS.activepedom.getSteps(); + } else { + steps = Bangle.getHealthStatus("day").steps; + } + } catch(ex) { + // In case we failed, we can only show 0 steps. + return "?"; + } + + return Math.round(steps); +} + +{ // must be inside our own scope here so that when we are unloaded everything disappears + // we also define functions using 'let fn = function() {..}' for the same reason. function decls are global +let drawTimeout; + + +// Actually draw the watch face +let draw = function() { + var x = g.getWidth() / 2; + var y = g.getHeight() / 2; + g.reset().clearRect(Bangle.appRect); // clear whole background (w/o widgets) + var date = new Date(); + var timeStr = require("locale").time(date, 1); // Hour and minute + g.setFontAlign(0, 0).setFont("Audiowide").drawString(timeStr, x, y); + var dateStr = require("locale").date(date, 1).toUpperCase(); + g.setFontAlign(0, 0).setFont("6x8", 2).drawString(dateStr, x, y+28); + g.setFontAlign(0, 0).setFont("6x8", 2); + g.drawString(getSteps(), 50, y+70); + g.drawString(Math.round(Bangle.getHealthStatus("last").bpm), g.getWidth() -37, y + 70); + + // queue next draw + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +}; + +// Show launcher when middle button pressed +Bangle.setUI({ + mode : "clock", + remove : function() { + // Called to unload all of the clock app + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + delete Graphics.prototype.setFontAnton; + }}); +// Load widgets +Bangle.loadWidgets(); +draw(); +setTimeout(Bangle.drawWidgets,0); +} diff --git a/apps/entonclk/app.png b/apps/entonclk/app.png new file mode 100644 index 000000000..5b634de5a Binary files /dev/null and b/apps/entonclk/app.png differ diff --git a/apps/entonclk/metadata.json b/apps/entonclk/metadata.json new file mode 100644 index 000000000..7e4947406 --- /dev/null +++ b/apps/entonclk/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "entonclk", + "name": "Enton Clock", + "version": "0.1", + "description": "A simple clock using the Audiowide font. ", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "readme":"README.md", + "storage": [ + {"name":"entonclk.app.js","url":"app.js"}, + {"name":"entonclk.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/entonclk/screenshot.png b/apps/entonclk/screenshot.png new file mode 100644 index 000000000..0905c6fc8 Binary files /dev/null and b/apps/entonclk/screenshot.png differ diff --git a/apps/gallery/ChangeLog b/apps/gallery/ChangeLog new file mode 100644 index 000000000..76db22053 --- /dev/null +++ b/apps/gallery/ChangeLog @@ -0,0 +1,2 @@ +0.01: New app! +0.02: Submitted to app loader \ No newline at end of file diff --git a/apps/gallery/README.md b/apps/gallery/README.md new file mode 100644 index 000000000..b70fa07c2 --- /dev/null +++ b/apps/gallery/README.md @@ -0,0 +1,18 @@ +# Gallery + +A simple gallery app + +## Usage + +Upon opening the gallery app, you will be presented with a list of images that you can display. Tap the image to show it. Brightness will be set to full, and the screen timeout will be disabled. When you are done viewing the image, you can tap the screen to go back to the list of images. Press BTN1 to flip the image upside down. + +## Adding images + +1. The gallery app does not perform any scaling, and does not support panning. Therefore, you should use your favorite image editor to produce an image of the appropriate size for your watch. (240x240 for Bangle 1 or 176x176 for Bangle 2.) How you achieve this is up to you. If on a Bangle 2, I recommend adjusting the colors here to comply with the color restrictions. + +2. Upload your image to the [Espruino image converter](https://www.espruino.com/Image+Converter). I recommend enabling compression and choosing one of the following color settings: + * 16 bit RGB565 for Bangle 1 + * 3 bit RGB for Bangle 2 + * 1 bit black/white for monochrome images that you want to respond to your system theme. (White will be rendered as your foreground color and black will be rendered as your background color.) + +3. Set the output format to an image string, copy it into the [IDE](https://www.espruino.com/ide/), and set the destination to a file in storage. The file name should begin with "gal-" (without the quotes) and end with ".img" (without the quotes) to appear in the gallery. Note that the gal- prefix and .img extension will be removed in the UI. Upload the file. \ No newline at end of file diff --git a/apps/gallery/app.js b/apps/gallery/app.js new file mode 100644 index 000000000..ca9392f13 --- /dev/null +++ b/apps/gallery/app.js @@ -0,0 +1,52 @@ +const storage = require('Storage'); + +let imageFiles = storage.list(/^gal-.*\.img/).sort(); + +let imageMenu = { '': { 'title': 'Gallery' } }; + +for (let fileName of imageFiles) { + let displayName = fileName.substr(4, fileName.length - 8); // Trim off the 'gal-' and '.img' for a friendly display name + imageMenu[displayName] = eval(`() => { drawImage("${fileName}"); }`); // Unfortunately, eval is the only reasonable way to do this +} + +let cachedOptions = Bangle.getOptions(); // We will change the backlight and timeouts later, and need to restore them when displaying the menu +let backlightSetting = storage.readJSON('setting.json').brightness; // LCD brightness is not included in there for some reason + +let angle = 0; // Store the angle of rotation +let image; // Cache the image here because we access it in multiple places + +function drawMenu() { + Bangle.removeListener('touch', drawMenu); // We no longer want touching to reload the menu + Bangle.setOptions(cachedOptions); // The drawImage function set no timeout, undo that + Bangle.setLCDBrightness(backlightSetting); // Restore backlight + image = undefined; // Delete the image from memory + + E.showMenu(imageMenu); +} + +function drawImage(fileName) { + E.showMenu(); // Remove the menu to prevent it from breaking things + setTimeout(() => { Bangle.on('touch', drawMenu); }, 300); // Touch the screen to go back to the image menu (300ms timeout to allow user to lift finger) + Bangle.setOptions({ // Disable display power saving while showing the image + lockTimeout: 0, + lcdPowerTimeout: 0, + backlightTimeout: 0 + }); + Bangle.setLCDBrightness(1); // Full brightness + + image = eval(storage.read(fileName)); // Sadly, the only reasonable way to do this + g.clear().reset().drawImage(image, 88, 88, { rotate: angle }); +} + +setWatch(info => { + if (image) { + if (angle == 0) angle = Math.PI; + else angle = 0; + Bangle.buzz(); + + g.clear().reset().drawImage(image, 88, 88, { rotate: angle }) + } +}, BTN1, { repeat: true }); + +// We don't load the widgets because there is no reasonable way to unload them +drawMenu(); \ No newline at end of file diff --git a/apps/gallery/icon.js b/apps/gallery/icon.js new file mode 100644 index 000000000..11fee53eb --- /dev/null +++ b/apps/gallery/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgIOLgf/AAX8Av4FBJgkMAos/CIfMAv4Fe4AF/Apq5EAAw")) \ No newline at end of file diff --git a/apps/gallery/icon.png b/apps/gallery/icon.png new file mode 100644 index 000000000..71835e93d Binary files /dev/null and b/apps/gallery/icon.png differ diff --git a/apps/gallery/metadata.json b/apps/gallery/metadata.json new file mode 100644 index 000000000..89f7606aa --- /dev/null +++ b/apps/gallery/metadata.json @@ -0,0 +1,26 @@ +{ + "id": "gallery", + "name": "Gallery", + "version": "0.02", + "description": "A gallery that lets you view images uploaded with the IDE (see README)", + "readme": "README.md", + "icon": "icon.png", + "type": "app", + "tags": "tools", + "supports": [ + "BANGLEJS2", + "BANGLEJS" + ], + "allow_emulator": true, + "storage": [ + { + "name": "gallery.app.js", + "url": "app.js" + }, + { + "name": "gallery.img", + "url": "icon.js", + "evaluate": true + } + ] +} \ No newline at end of file diff --git a/apps/gpsautotime/settings.js b/apps/gpsautotime/settings.js index be6e3bbec..34a6364fe 100644 --- a/apps/gpsautotime/settings.js +++ b/apps/gpsautotime/settings.js @@ -13,7 +13,7 @@ E.showMenu({ "" : { "title" : "GPS auto time" }, "< Back" : () => back(), - 'Show Widgets': { + 'Show Widget': { value: !!settings.show, onchange: v => { settings.show = v; diff --git a/apps/gpsautotime/widget.js b/apps/gpsautotime/widget.js index a21c14619..14d6fe140 100644 --- a/apps/gpsautotime/widget.js +++ b/apps/gpsautotime/widget.js @@ -9,7 +9,7 @@ delete settings; Bangle.on('GPS',function(fix) { - if (fix.fix) { + if (fix.fix && fix.time) { var curTime = fix.time.getTime()/1000; setTime(curTime); lastTimeSet = curTime; diff --git a/apps/gpsnav/ChangeLog b/apps/gpsnav/ChangeLog index 6f327f364..840f9ecbc 100644 --- a/apps/gpsnav/ChangeLog +++ b/apps/gpsnav/ChangeLog @@ -4,4 +4,5 @@ 0.04: Fix great circle formula 0.05: Use locale for speed and distance + fix Vector font sizes 0.06: Move waypoints.json (and editor) to 'waypoints' app -0.07: Add support for b2 \ No newline at end of file +0.07: Add support for b2 +0.08: Fix not displaying of wpindex = 0, correct compass drawing and nm calculation on b2 diff --git a/apps/gpsnav/app.js b/apps/gpsnav/app.js index e2b6ee6f1..68bd2cbda 100644 --- a/apps/gpsnav/app.js +++ b/apps/gpsnav/app.js @@ -36,7 +36,7 @@ function drawCompass(course) { } xpos+=15; } - if (wpindex!=0) { + if (wpindex>=0) { var bpos = brg - course; if (bpos>180) bpos -=360; if (bpos<-180) bpos +=360; @@ -220,7 +220,7 @@ function nextwp(inc){ } function doselect(){ - if (selected && wpindex!=0 && waypoints[wpindex].lat===undefined && savedfix.fix) { + if (selected && wpindex>=0 && waypoints[wpindex].lat===undefined && savedfix.fix) { waypoints[wpindex] ={name:"@"+wp.name, lat:savedfix.lat, lon:savedfix.lon}; wp = waypoints[wpindex]; require("waypoints").save(waypoints); diff --git a/apps/gpsnav/app_b2.js b/apps/gpsnav/app_b2.js index e46be649f..ee6519c92 100644 --- a/apps/gpsnav/app_b2.js +++ b/apps/gpsnav/app_b2.js @@ -13,7 +13,7 @@ var loc = { distance: [ require("locale").distance, (m) => { - return (m / 1.852).toFixed(3) + "nm "; + return (m / 1852).toFixed(3) + "nm "; } ] }; @@ -21,7 +21,7 @@ var loc = { function drawCompass(course) { if (!candraw) return; - g.setColor(g.theme.fg); + g.reset().clearRect(0, 24, 175, 71); g.setFont("Vector", 18); var start = course - 90; if (start < 0) start += 360; @@ -43,7 +43,7 @@ function drawCompass(course) { } xpos += 12; } - if (wpindex != 0) { + if (wpindex >= 0) { var bpos = brg - course; if (bpos > 180) bpos -= 360; if (bpos < -180) bpos += 360; @@ -106,9 +106,8 @@ function distance(a, b) { var selected = false; function drawN() { - g.clearRect(0, 89, 175, 175); + g.reset().clearRect(0, 89, 175, 175); var txt = loc.speed[locindex](speed); - g.setColor(g.theme.fg); g.setFont("6x8", 2); g.drawString("o", 68, 87); g.setFont("6x8", 1); @@ -117,10 +116,8 @@ function drawN() { var cs = course.toString().padStart(3, "0"); g.drawString(cs, 2, 89); g.drawString(txt.substring(0, txt.length - 3), 92, 89); - g.setColor(g.theme.fg); g.setFont("Vector", 18); var bs = brg.toString().padStart(3, "0"); - g.setColor(g.theme.fg); g.drawString("Brg:", 1, 128); g.drawString("Dist:", 1, 148); g.setColor(selected ? g.theme.bgH : g.theme.bg); @@ -241,7 +238,7 @@ function nextwp(inc) { } function doselect() { - if (selected && wpindex != 0 && waypoints[wpindex].lat === undefined && savedfix.fix) { + if (selected && wpindex >= 0 && waypoints[wpindex].lat === undefined && savedfix.fix) { waypoints[wpindex] = { name: "@" + wp.name, lat: savedfix.lat, @@ -265,4 +262,4 @@ drawAll(); startTimers(); Bangle.on('GPS', onGPS); // Toggle selected -setButtons(); \ No newline at end of file +setButtons(); diff --git a/apps/gpsnav/metadata.json b/apps/gpsnav/metadata.json index dce80112f..bc46a733c 100644 --- a/apps/gpsnav/metadata.json +++ b/apps/gpsnav/metadata.json @@ -1,7 +1,7 @@ { "id": "gpsnav", "name": "GPS Navigation", - "version": "0.07", + "version": "0.08", "description": "Displays GPS Course and Speed, + Directions to waypoint and waypoint recording, now with waypoint editor", "screenshots": [{"url":"screenshot-b2.png"}], "icon": "icon.png", diff --git a/apps/gpstrek/ChangeLog b/apps/gpstrek/ChangeLog new file mode 100644 index 000000000..d46ada767 --- /dev/null +++ b/apps/gpstrek/ChangeLog @@ -0,0 +1,10 @@ +0.01: New App! +0.02: Make selection of background activity more explicit +0.03: Fix listener for accel always active + Use custom UI with swipes instead of leftright +0.04: Fix compass heading +0.05: Added adjustment for Bangle.js magnetometer heading fix +0.06: Fix waypoint menu always selecting last waypoint + Fix widget adding listeners more than once +0.07: Show checkered flag for target markers + Single waypoints are now shown in the compass view diff --git a/apps/gpstrek/README.md b/apps/gpstrek/README.md new file mode 100644 index 000000000..c55f5a8bf --- /dev/null +++ b/apps/gpstrek/README.md @@ -0,0 +1,50 @@ +# GPS Trekking + +Helper for tracking the status/progress during hiking. Do NOT depend on this for navigation! + +This app is inspired by and uses code from "GPS Navigation" and "Navigation compass". + +## Usage + +Tapping or button to switch to the next information display, swipe right for the menu. + +Choose either a route or a waypoint as basis for the display. + +After this selection and availability of a GPS fix the compass will show a checkered flag for your destination and a green dot for possibly available waypoints on the way. +Waypoints are shown with name if available and distance to waypoint. + +As long as no GPS signal is available the compass shows the heading from the build in magnetometer. When a GPS fix becomes available, the compass display shows the GPS course. This can be differentiated by the display of bubble levels on top and sides of the compass. +If they are on display, the source is the magnetometer and you should keep the bangle level. There is currently no tilt compensation for the compass display. + +### Route + +Routes can be created from .gpx files containing "trkpt" elements with this script: [createRoute.sh](createRoute.sh) + +The resulting file needs to be uploaded to the watch and will be shown in the file selection menu. + +The route can be mirrored to switch start and destination. + +If the GPS position is closer than 30m to the next waypoint, the route is automatically advanced to the next waypoint. + +### Waypoints + +You can select a waypoint from the "Waypoints" app as destination. + +## Calibration + +### Altitude + +You can correct the barometric altitude display either by manually setting a known correct value or using the GPS fix elevation as reference. This will only affect the display of altitude values. + +### Compass + +If the compass fallback starts to show unreliable values, you can reset the calibration in the menu. It starts to show values again after turning 360°. + +## Widget + +The widget keeps the sensors alive and records some very basic statistics when the app is not started. It shows as the app icon in the widget bar when the background task is active. +This uses a lot of power so ensure to stop the app if you are not actively using it. + +# Creator + +[halemmerich](https://github.com/halemmerich) diff --git a/apps/gpstrek/app-icon.js b/apps/gpstrek/app-icon.js new file mode 100644 index 000000000..6b2924353 --- /dev/null +++ b/apps/gpstrek/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwIjggOAApMD4AFJg4FF8AFJh/wApMf/AFJn/8ApN//wFDvfeAof774FD+fPLwYFBMAUB8fHAoUDAoJaCgfD4YFIg+D4JgCAosPAoJgCh6DBAoUfAoJgCjwFBvAFBnwFBvgFBngFBngFBvh3BnwFBvH//8eMgQFBMwX//k//5eB//wh//wAFBAQcDRoU/4EDJQfAbYbfFACYA=")) diff --git a/apps/gpstrek/app.js b/apps/gpstrek/app.js new file mode 100644 index 000000000..f26811ed3 --- /dev/null +++ b/apps/gpstrek/app.js @@ -0,0 +1,857 @@ +const STORAGE = require("Storage"); +const showWidgets = true; +let numberOfSlices=4; + +if (showWidgets){ + Bangle.loadWidgets(); +} + +let state = WIDGETS.gpstrek.getState(); +WIDGETS.gpstrek.start(false); + +function parseNumber(toParse){ + if (toParse.includes(".")) return parseFloat(toParse); + return parseFloat("" + toParse + ".0"); +} + +function parseWaypoint(filename, offset, result){ + result.lat = parseNumber(STORAGE.read(filename, offset, 11)); + result.lon = parseNumber(STORAGE.read(filename, offset += 11, 12)); + return offset + 12; +} + +function parseWaypointWithElevation(filename, offset, result){ + offset = parseWaypoint(filename, offset, result); + result.alt = parseNumber(STORAGE.read(filename, offset, 6)); + return offset + 6; +} + +function parseWaypointWithName(filename, offset, result){ + offset = parseWaypoint(filename, offset, result); + return parseName(filename, offset, result); +} + +function parseName(filename, offset, result){ + let nameLength = STORAGE.read(filename, offset, 2) - 0; + result.name = STORAGE.read(filename, offset += 2, nameLength); + return offset + nameLength; +} + +function parseWaypointWithElevationAndName(filename, offset, result){ + offset = parseWaypointWithElevation(filename, offset, result); + return parseName(filename, offset, result); +} + +function getEntry(filename, offset, result){ + result.fileOffset = offset; + let type = STORAGE.read(filename, offset++, 1); + if (type == "") return -1; + switch (type){ + case "A": + offset = parseWaypoint(filename, offset, result); + break; + case "B": + offset = parseWaypointWithName(filename, offset, result); + break; + case "C": + offset = parseWaypointWithElevation(filename, offset, result); + break; + case "D": + offset = parseWaypointWithElevationAndName(filename, offset, result); + break; + default: + print("Unknown entry type", type); + return -1; + } + offset++; + + result.fileLength = offset - result.fileOffset; + //print(result); + return offset; +} + +const labels = ["N","NE","E","SE","S","SW","W","NW"]; +const loc = require("locale"); + +function matchFontSize(graphics, text, height, width){ + graphics.setFontVector(height); + let metrics; + let size = 1; + while (graphics.stringMetrics(text).width > 0.90 * width){ + size -= 0.05; + graphics.setFont("Vector",Math.floor(height*size)); + } +} + +function getDoubleLineSlice(title1,title2,provider1,provider2,refreshTime){ + let lastDrawn = Date.now() - Math.random()*refreshTime; + return { + refresh: function (){ + return Date.now() - lastDrawn > (Bangle.isLocked()?(refreshTime?refreshTime:5000):(refreshTime?refreshTime*2:10000)); + }, + draw: function (graphics, x, y, height, width){ + lastDrawn = Date.now(); + if (typeof title1 == "function") title1 = title1(); + if (typeof title2 == "function") title2 = title2(); + graphics.clearRect(x,y,x+width,y+height); + + let value = provider1(); + matchFontSize(graphics, title1 + value, Math.floor(height*0.5), width); + graphics.setFontAlign(-1,-1); + graphics.drawString(title1, x+2, y); + graphics.setFontAlign(1,-1); + graphics.drawString(value, x+width, y); + + value = provider2(); + matchFontSize(graphics, title2 + value, Math.floor(height*0.5), width); + graphics.setFontAlign(-1,-1); + graphics.drawString(title2, x+2, y+(height*0.5)); + graphics.setFontAlign(1,-1); + graphics.drawString(value, x+width, y+(height*0.5)); + } + }; +} + +function getTargetSlice(targetDataSource){ + let nameIndex = 0; + let lastDrawn = Date.now() - Math.random()*3000; + return { + refresh: function (){ + return Date.now() - lastDrawn > (Bangle.isLocked()?10000:3000); + }, + draw: function (graphics, x, y, height, width){ + lastDrawn = Date.now(); + graphics.clearRect(x,y,x+width,y+height); + if (targetDataSource.icon){ + graphics.drawImage(targetDataSource.icon,x,y + (height - 16)/2); + x += 16; + width -= 16; + } + + if (!targetDataSource.getTarget() || !targetDataSource.getStart()) return; + + let dist = distance(targetDataSource.getStart(),targetDataSource.getTarget()); + if (isNaN(dist)) dist = Infinity; + let bearingString = bearing(targetDataSource.getStart(),targetDataSource.getTarget()) + "°"; + if (targetDataSource.getTarget().name) { + graphics.setFont("Vector",Math.floor(height*0.5)); + let scrolledName = (targetDataSource.getTarget().name || "").substring(nameIndex); + if (graphics.stringMetrics(scrolledName).width > width){ + nameIndex++; + } else { + nameIndex = 0; + } + graphics.drawString(scrolledName, x+2, y); + + let distanceString = loc.distance(dist,2); + matchFontSize(graphics, distanceString + bearingString, height*0.5, width); + graphics.drawString(bearingString, x+2, y+(height*0.5)); + graphics.setFontAlign(1,-1); + graphics.drawString(distanceString, x + width, y+(height*0.5)); + } else { + graphics.setFont("Vector",Math.floor(height*1)); + let bearingString = bearing(targetDataSource.getStart(),targetDataSource.getTarget()) + "°"; + let formattedDist = loc.distance(dist,2); + let distNum = (formattedDist.match(/[0-9\.]+/) || [Infinity])[0]; + let size = 0.8; + let distNumMetrics; + while (graphics.stringMetrics(bearingString).width + (distNumMetrics = graphics.stringMetrics(distNum)).width > 0.90 * width){ + size -= 0.05; + graphics.setFont("Vector",Math.floor(height*size)); + } + graphics.drawString(bearingString, x+2, y + (height - distNumMetrics.height)/2); + graphics.setFontAlign(1,-1); + graphics.drawString(distNum, x + width, y + (height - distNumMetrics.height)/2); + graphics.setFont("Vector",Math.floor(height*0.25)); + + graphics.setFontAlign(-1,1); + if (targetDataSource.getProgress){ + graphics.drawString(targetDataSource.getProgress(), x + 2, y + height); + } + graphics.setFontAlign(1,1); + if (!isNaN(distNum) && distNum != Infinity) + graphics.drawString(formattedDist.match(/[a-zA-Z]+/), x + width, y + height); + } + } + }; +} + +function drawCompass(graphics, x, y, height, width, increment, start){ + graphics.setFont12x20(); + graphics.setFontAlign(0,-1); + graphics.setColor(graphics.theme.fg); + let frag = 0 - start%15; + if (frag>0) frag = 0; + let xpos = 0 + frag*increment; + for (let i=start;i<=720;i+=15){ + var res = i + frag; + if (res%90==0) { + graphics.drawString(labels[Math.floor(res/45)%8],xpos,y+2); + graphics.fillRect(xpos-2,Math.floor(y+height*0.6),xpos+2,Math.floor(y+height)); + } else if (res%45==0) { + graphics.drawString(labels[Math.floor(res/45)%8],xpos,y+2); + graphics.fillRect(xpos-2,Math.floor(y+height*0.75),xpos+2,Math.floor(y+height)); + } else if (res%15==0) { + graphics.fillRect(xpos,Math.floor(y+height*0.9),xpos+1,Math.floor(y+height)); + } + xpos+=increment*15; + if (xpos > width + 20) break; + } +} + +function getCompassSlice(compassDataSource){ + let lastDrawn = Date.now() - Math.random()*2000; + const buffers = 4; + let buf = []; + return { + refresh : function (){return Bangle.isLocked()?(Date.now() - lastDrawn > 2000):true;}, + draw: function (graphics, x,y,height,width){ + lastDrawn = Date.now(); + const max = 180; + const increment=width/max; + + graphics.clearRect(x,y,x+width,y+height); + + var start = compassDataSource.getCourse() - 90; + if (isNaN(compassDataSource.getCourse())) start = -90; + if (start<0) start+=360; + start = start % 360; + + if (state.acc && compassDataSource.getCourseType() == "MAG"){ + drawCompass(graphics,0,y+width*0.05,height-width*0.05,width,increment,start); + } else { + drawCompass(graphics,0,y,height,width,increment,start); + } + + + if (compassDataSource.getPoints){ + for (let p of compassDataSource.getPoints()){ + var bpos = p.bearing - compassDataSource.getCourse(); + if (bpos>180) bpos -=360; + if (bpos<-180) bpos +=360; + bpos+=120; + let min = 0; + let max = 180; + if (bpos<=min){ + bpos = Math.floor(width*0.05); + } else if (bpos>=max) { + bpos = Math.ceil(width*0.95); + } else { + bpos=Math.round(bpos*increment); + } + if (p.color){ + graphics.setColor(p.color); + } + if (p.icon){ + graphics.drawImage(p.icon, bpos,y+height-12, {rotate:0,scale:2}); + } else { + graphics.fillCircle(bpos,y+height-12,Math.floor(width*0.03)); + } + } + } + if (compassDataSource.getMarkers){ + for (let m of compassDataSource.getMarkers()){ + g.setColor(m.fillcolor); + let mpos = m.xpos * width; + if (m.xpos < 0.05) mpos = Math.floor(width*0.05); + if (m.xpos > 0.95) mpos = Math.ceil(width*0.95); + g.fillPoly(triangle(mpos,y+height-m.height, m.height, m.width)); + g.setColor(m.linecolor); + g.drawPoly(triangle(mpos,y+height-m.height, m.height, m.width),true); + } + } + graphics.setColor(g.theme.fg); + graphics.fillRect(x,y,Math.floor(width*0.05),y+height); + graphics.fillRect(Math.ceil(width*0.95),y,width,y+height); + if (state.acc && compassDataSource.getCourseType() == "MAG") { + let xh = E.clip(width*0.5-height/2+(((state.acc.x+1)/2)*height),width*0.5 - height/2, width*0.5 + height/2); + let yh = E.clip(y+(((state.acc.y+1)/2)*height),y,y+height); + + graphics.fillRect(width*0.5 - height/2, y, width*0.5 + height/2, y + Math.floor(width*0.05)); + + graphics.setColor(g.theme.bg); + graphics.drawLine(width*0.5 - 5, y, width*0.5 - 5, y + Math.floor(width*0.05)); + graphics.drawLine(width*0.5 + 5, y, width*0.5 + 5, y + Math.floor(width*0.05)); + graphics.fillRect(xh-1,y,xh+1,y+Math.floor(width*0.05)); + + let left = Math.floor(width*0.05); + let right = Math.ceil(width*0.95); + graphics.drawLine(0,y+height/2-5,left,y+height/2-5); + graphics.drawLine(right,y+height/2-5,x+width,y+height/2-5); + graphics.drawLine(0,y+height/2+5,left,y+height/2+5); + graphics.drawLine(right,y+height/2+5,x+width,y+height/2+5); + graphics.fillRect(0,yh-1,left,yh+1); + graphics.fillRect(right,yh-1,x+width,yh+1); + } + graphics.setColor(g.theme.fg); + graphics.drawRect(Math.floor(width*0.05),y,Math.ceil(width*0.95),y+height); + } + }; +} + +function radians(a) { + return a*Math.PI/180; +} + +function degrees(a) { + var d = a*180/Math.PI; + return (d+360)%360; +} + +function bearing(a,b){ + if (!a || !b || !a.lon || !a.lat || !b.lon || !b.lat) return Infinity; + var delta = radians(b.lon-a.lon); + var alat = radians(a.lat); + var blat = radians(b.lat); + var y = Math.sin(delta) * Math.cos(blat); + var x = Math.cos(alat)*Math.sin(blat) - + Math.sin(alat)*Math.cos(blat)*Math.cos(delta); + return Math.round(degrees(Math.atan2(y, x))); +} + +function distance(a,b){ + if (!a || !b || !a.lon || !a.lat || !b.lon || !b.lat) return Infinity; + var x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2)); + var y = radians(b.lat-a.lat); + return Math.round(Math.sqrt(x*x + y*y) * 6371000); +} + +function triangle (x, y, width, height){ + return [ + Math.round(x),Math.round(y), + Math.round(x+width * 0.5), Math.round(y+height), + Math.round(x-width * 0.5), Math.round(y+height) + ]; +} + +function onSwipe(dir){ + if (dir < 0) { + nextScreen(); + } else if (dir > 0) { + switchMenu(); + } else { + nextScreen(); + } +} + +function setButtons(){ + let options = { + mode: "custom", + swipe: onSwipe, + btn: nextScreen, + touch: nextScreen + }; + Bangle.setUI(options); +} + +function getApproxFileSize(name){ + let currentStart = STORAGE.getStats().totalBytes; + let currentSize = 0; + for (let i = currentStart; i > 500; i/=2){ + let currentDiff = i; + //print("Searching", currentDiff); + while (STORAGE.read(name, currentSize+currentDiff, 1) == ""){ + //print("Loop", currentDiff); + currentDiff = Math.ceil(currentDiff/2); + } + i = currentDiff*2; + currentSize += currentDiff; + } + return currentSize; +} + +function parseRouteData(filename, progressMonitor){ + let routeInfo = {}; + + routeInfo.filename = filename; + routeInfo.refs = []; + + let c = {}; + let scanOffset = 0; + routeInfo.length = 0; + routeInfo.count = 0; + routeInfo.mirror = false; + let lastSeenWaypoint; + let lastSeenAlt; + let waypoint = {}; + + routeInfo.up = 0; + routeInfo.down = 0; + + let size = getApproxFileSize(filename); + + while ((scanOffset = getEntry(filename, scanOffset, waypoint)) > 0) { + if (routeInfo.count % 5 == 0) progressMonitor(scanOffset, "Loading", size); + if (lastSeenWaypoint){ + routeInfo.length += distance(lastSeenWaypoint, waypoint); + + let diff = waypoint.alt - lastSeenAlt; + //print("Distance", routeInfo.length, "alt", lastSeenAlt, waypoint.alt, diff); + if (waypoint.alt && lastSeenAlt && diff > 3){ + if (lastSeenAlt < waypoint.alt){ + //print("Up", diff); + routeInfo.up += diff; + } else { + //print("Down", diff); + routeInfo.down += diff; + } + } + } + routeInfo.count++; + routeInfo.refs.push(waypoint.fileOffset); + lastSeenWaypoint = waypoint; + if (!isNaN(waypoint.alt)) lastSeenAlt = waypoint.alt; + waypoint = {}; + } + + set(routeInfo, 0); + return routeInfo; +} + +function hasPrev(route){ + if (route.mirror) return route.index < (route.count - 1); + return route.index > 0; +} + +function hasNext(route){ + if (route.mirror) return route.index > 0; + return route.index < (route.count - 1); +} + +function next(route){ + if (!hasNext(route)) return; + if (route.mirror) set(route, --route.index); + if (!route.mirror) set(route, ++route.index); +} + +function set(route, index){ + route.currentWaypoint = {}; + route.index = index; + getEntry(route.filename, route.refs[index], route.currentWaypoint); +} + +function prev(route){ + if (!hasPrev(route)) return; + if (route.mirror) set(route, ++route.index); + if (!route.mirror) set(route, --route.index); +} + +let lastMirror; +let cachedLast; + +function getLast(route){ + let wp = {}; + if (lastMirror != route.mirror){ + if (route.mirror) getEntry(route.filename, route.refs[0], wp); + if (!route.mirror) getEntry(route.filename, route.refs[route.count - 1], wp); + lastMirror = route.mirror; + cachedLast = wp; + } + return cachedLast; +} + +function removeMenu(){ + E.showMenu(); + switchNav(); +} + +function showProgress(progress, title, max){ + //print("Progress",progress,max) + let message = title? title: "Loading"; + if (max){ + message += " " + E.clip((progress/max*100),0,100).toFixed(0) +"%"; + } else { + let dots = progress % 4; + for (let i = 0; i < dots; i++) message += "."; + for (let i = dots; i < 4; i++) message += " "; + } + E.showMessage(message); +} + +function handleLoading(c){ + E.showMenu(); + state.route = parseRouteData(c, showProgress); + state.waypoint = null; + removeMenu(); + state.route.mirror = false; +} + +function showRouteSelector (){ + var menu = { + "" : { + back : showRouteMenu, + } + }; + + STORAGE.list(/\.trf$/).forEach((file)=>{ + menu[file] = ()=>{handleLoading(file);}; + }); + + E.showMenu(menu); +} + +function showRouteMenu(){ + var menu = { + "" : { + "title" : "Route", + back : showMenu, + }, + "Select file" : showRouteSelector + }; + + if (state.route){ + menu.Mirror = { + value: state && state.route && !!state.route.mirror || false, + onchange: v=>{ + state.route.mirror = v; + } + }; + menu['Select closest waypoint'] = function () { + if (state.currentPos && state.currentPos.lat){ + setClosestWaypoint(state.route, null, showProgress); removeMenu(); + } else { + E.showAlert("No position").then(()=>{E.showMenu(menu);}); + } + }; + menu['Select closest waypoint (not visited)'] = function () { + if (state.currentPos && state.currentPos.lat){ + setClosestWaypoint(state.route, state.route.index, showProgress); removeMenu(); + } else { + E.showAlert("No position").then(()=>{E.showMenu(menu);}); + } + }; + menu['Select waypoint'] = { + value : state.route.index, + min:1,max:state.route.count,step:1, + onchange : v => { set(state.route, v-1); } + }; + menu['Select waypoint as current position'] = function (){ + state.currentPos.lat = state.route.currentWaypoint.lat; + state.currentPos.lon = state.route.currentWaypoint.lon; + state.currentPos.alt = state.route.currentWaypoint.alt; + removeMenu(); + }; + } + + if (state.route && hasPrev(state.route)) + menu['Previous waypoint'] = function() { prev(state.route); removeMenu(); }; + if (state.route && hasNext(state.route)) + menu['Next waypoint'] = function() { next(state.route); removeMenu(); }; + E.showMenu(menu); +} + +function showWaypointSelector(){ + let waypoints = require("waypoints").load(); + var menu = { + "" : { + back : showWaypointMenu, + } + }; + + waypoints.forEach((wp,c)=>{ + menu[waypoints[c].name] = function (){ + state.waypoint = waypoints[c]; + state.waypointIndex = c; + state.route = null; + removeMenu(); + }; + }); + + E.showMenu(menu); +} + +function showCalibrationMenu(){ + let menu = { + "" : { + "title" : "Calibration", + back : showMenu, + }, + "Barometer (GPS)" : ()=>{ + if (!state.currentPos || isNaN(state.currentPos.alt)){ + E.showAlert("No GPS altitude").then(()=>{E.showMenu(menu);}); + } else { + state.calibAltDiff = state.altitude - state.currentPos.alt; + E.showAlert("Calibrated Altitude Difference: " + state.calibAltDiff.toFixed(0)).then(()=>{removeMenu();}); + } + }, + "Barometer (Manual)" : { + value : Math.round(state.currentPos && (state.currentPos.alt != undefined && !isNaN(state.currentPos.alt)) ? state.currentPos.alt: state.altitude), + min:-2000,max: 10000,step:1, + onchange : v => { state.calibAltDiff = state.altitude - v; } + }, + "Reset Compass" : ()=>{ Bangle.resetCompass(); removeMenu();}, + }; + E.showMenu(menu); +} + +function showWaypointMenu(){ + let menu = { + "" : { + "title" : "Waypoint", + back : showMenu, + }, + "Select waypoint" : showWaypointSelector, + }; + E.showMenu(menu); +} + +function showBackgroundMenu(){ + let menu = { + "" : { + "title" : "Background", + back : showMenu, + }, + "Start" : ()=>{ E.showPrompt("Start?").then((v)=>{ if (v) {WIDGETS.gpstrek.start(true); removeMenu();} else {showMenu();}}).catch(()=>{E.showMenu(mainmenu);});}, + "Stop" : ()=>{ E.showPrompt("Stop?").then((v)=>{ if (v) {WIDGETS.gpstrek.stop(true); removeMenu();} else {showMenu();}}).catch(()=>{E.showMenu(mainmenu);});}, + }; + E.showMenu(menu); +} + +function showMenu(){ + var mainmenu = { + "" : { + "title" : "Main", + back : removeMenu, + }, + "Route" : showRouteMenu, + "Waypoint" : showWaypointMenu, + "Background" : showBackgroundMenu, + "Calibration": showCalibrationMenu, + "Reset" : ()=>{ E.showPrompt("Do Reset?").then((v)=>{ if (v) {WIDGETS.gpstrek.resetState(); removeMenu();} else {E.showMenu(mainmenu);}});}, + "Info rows" : { + value : numberOfSlices, + min:1,max:6,step:1, + onchange : v => { setNumberOfSlices(v); } + }, + }; + + E.showMenu(mainmenu); +} + +let scheduleDraw = true; + +function switchMenu(){ + screen = 0; + scheduleDraw = false; + showMenu(); +} + +function drawInTimeout(){ + setTimeout(()=>{ + draw(); + if (scheduleDraw) + setTimeout(drawInTimeout, 0); + },0); +} + +function switchNav(){ + if (!screen) screen = 1; + setButtons(); + scheduleDraw = true; + drawInTimeout(); +} + +function nextScreen(){ + screen++; + if (screen > maxScreens){ + screen = 1; + } +} + +function setClosestWaypoint(route, startindex, progress){ + if (startindex >= state.route.count) startindex = state.route.count - 1; + if (!state.currentPos.lat){ + set(route, startindex); + return; + } + let minDist = 100000000000000; + let minIndex = 0; + for (let i = startindex?startindex:0; i < route.count - 1; i++){ + if (progress && (i % 5 == 0)) progress(i-(startindex?startindex:0), "Searching", route.count); + let wp = {}; + getEntry(route.filename, route.refs[i], wp); + let curDist = distance(state.currentPos, wp); + if (curDist < minDist){ + minDist = curDist; + minIndex = i; + } else { + if (startindex) break; + } + } + set(route, minIndex); +} + +let screen = 1; + +const finishIcon = atob("CggB//meZmeZ+Z5n/w=="); + +const compassSliceData = { + getCourseType: function(){ + return (state.currentPos && state.currentPos.course) ? "GPS" : "MAG"; + }, + getCourse: function (){ + if(compassSliceData.getCourseType() == "GPS") return state.currentPos.course; + return state.compassHeading?state.compassHeading:undefined; + }, + getPoints: function (){ + let points = []; + if (state.currentPos && state.currentPos.lon && state.route && state.route.currentWaypoint){ + points.push({bearing:bearing(state.currentPos, state.route.currentWaypoint), color:"#0f0"}); + } + if (state.currentPos && state.currentPos.lon && state.route){ + points.push({bearing:bearing(state.currentPos, getLast(state.route)), icon: finishIcon}); + } + if (state.currentPos && state.currentPos.lon && state.waypoint){ + points.push({bearing:bearing(state.currentPos, state.waypoint), icon: finishIcon}); + } + return points; + }, + getMarkers: function (){ + return [{xpos:0.5, width:10, height:10, linecolor:g.theme.fg, fillcolor:"#f00"}]; + } +}; + +const waypointData = { + icon: atob("EBCBAAAAAAAAAAAAcIB+zg/uAe4AwACAAAAAAAAAAAAAAAAA"), + getProgress: function() { + return (state.route.index + 1) + "/" + state.route.count; + }, + getTarget: function (){ + if (distance(state.currentPos,state.route.currentWaypoint) < 30 && hasNext(state.route)){ + next(state.route); + Bangle.buzz(1000); + } + return state.route.currentWaypoint; + }, + getStart: function (){ + return state.currentPos; + } +}; + +const finishData = { + icon: atob("EBABAAA/4DmgJmAmYDmgOaAmYD/gMAAwADAAMAAwAAAAAAA="), + getTarget: function (){ + if (state.route) return getLast(state.route); + if (state.waypoint) return state.waypoint; + }, + getStart: function (){ + return state.currentPos; + } +}; + +let sliceHeight; +function setNumberOfSlices(number){ + numberOfSlices = number; + sliceHeight = Math.floor((g.getHeight()-(showWidgets?24:0))/numberOfSlices); +} + +let slices = []; +let maxScreens = 1; +setNumberOfSlices(3); + +let compassSlice = getCompassSlice(compassSliceData); +let waypointSlice = getTargetSlice(waypointData); +let finishSlice = getTargetSlice(finishData); +let eleSlice = getDoubleLineSlice("Up","Down",()=>{ + return loc.distance(state.up,3) + "/" + (state.route ? loc.distance(state.route.up,3):"---"); +},()=>{ + return loc.distance(state.down,3) + "/" + (state.route ? loc.distance(state.route.down,3): "---"); +}); + +let statusSlice = getDoubleLineSlice("Speed","Alt",()=>{ + let speed = 0; + if (state.currentPos && state.currentPos.speed) speed = state.currentPos.speed; + return loc.speed(speed,2); +},()=>{ + let alt = Infinity; + if (!isNaN(state.altitude)){ + alt = isNaN(state.calibAltDiff) ? state.altitude : (state.altitude - state.calibAltDiff); + } + if (state.currentPos && state.currentPos.alt) alt = state.currentPos.alt; + return loc.distance(alt,3); +}); + +let status2Slice = getDoubleLineSlice("Compass","GPS",()=>{ + return (state.compassHeading?Math.round(state.compassHeading):"---") + "°"; +},()=>{ + let course = "---°"; + if (state.currentPos && state.currentPos.course) course = state.currentPos.course + "°"; + return course; +},200); + +let healthSlice = getDoubleLineSlice("Heart","Steps",()=>{ + return state.bpm; +},()=>{ + return state.steps; +}); + +let system2Slice = getDoubleLineSlice("Bat","",()=>{ + return (Bangle.isCharging()?"+":"") + E.getBattery().toFixed(0)+"% " + NRF.getBattery().toFixed(2) + "V"; +},()=>{ + return ""; +}); + +let systemSlice = getDoubleLineSlice("RAM","Storage",()=>{ + let ram = process.memory(false); + return ((ram.blocksize * ram.free)/1024).toFixed(0)+"kB"; +},()=>{ + return (STORAGE.getFree()/1024).toFixed(0)+"kB"; +}); + +function updateSlices(){ + slices = []; + slices.push(compassSlice); + + if (state.currentPos && state.currentPos.lat && state.route && state.route.currentWaypoint && state.route.index < state.route.count - 1) { + slices.push(waypointSlice); + } + if (state.currentPos && state.currentPos.lat && (state.route || state.waypoint)) { + slices.push(finishSlice); + } + if ((state.route && state.route.down !== undefined) || state.down != undefined) { + slices.push(eleSlice); + } + slices.push(statusSlice); + slices.push(status2Slice); + slices.push(healthSlice); + slices.push(systemSlice); + slices.push(system2Slice); + maxScreens = Math.ceil(slices.length/numberOfSlices); +} + +function clear() { + g.clearRect(0,(showWidgets ? 24 : 0), g.getWidth(),g.getHeight()); +} +let lastDrawnScreen; +let firstDraw = true; + +function draw(){ + if (!screen) return; + let ypos = showWidgets ? 24 : 0; + + let firstSlice = (screen-1)*numberOfSlices; + + updateSlices(); + + let force = lastDrawnScreen != screen || firstDraw; + if (force){ + clear(); + if (showWidgets){ + Bangle.drawWidgets(); + } + } + lastDrawnScreen = screen; + + for (let slice of slices.slice(firstSlice,firstSlice + numberOfSlices)) { + g.reset(); + if (!slice.refresh || slice.refresh() || force) slice.draw(g,0,ypos,sliceHeight,g.getWidth()); + ypos += sliceHeight+1; + g.drawLine(0,ypos-1,g.getWidth(),ypos-1); + } + firstDraw = false; +} + + +switchNav(); + +g.clear(); diff --git a/apps/gpstrek/createRoute.sh b/apps/gpstrek/createRoute.sh new file mode 100755 index 000000000..729e6af00 --- /dev/null +++ b/apps/gpstrek/createRoute.sh @@ -0,0 +1,14 @@ +#!/bin/bash +[ -z "$1" ] && echo Give gpx file name + + +xmlstarlet select -t -m '//_:trkpt' \ + --if '_:name and _:ele' -o D \ + --elif '_:ele and not(_:name)' -o C \ + --elif 'not(_:ele) and _:name' -o B \ + --else -o A -b \ + -v 'format-number(@lat,"+00.0000000;-00.0000000")' \ + -v 'format-number(@lon,"+000.0000000;-000.0000000")' \ + --if '_:ele' -v 'format-number(_:ele,"+00000;-00000")' -b \ + --if _:name -v 'format-number(string-length(_:name),"00")' -v '_:name' -b \ + -n "$1" | iconv -f utf8 -t iso8859-1 > "$(basename "$1" | sed -e "s|.gpx||").trf" diff --git a/apps/gpstrek/icon.png b/apps/gpstrek/icon.png new file mode 100644 index 000000000..e1ff2b99d Binary files /dev/null and b/apps/gpstrek/icon.png differ diff --git a/apps/gpstrek/metadata.json b/apps/gpstrek/metadata.json new file mode 100644 index 000000000..cf5d06baa --- /dev/null +++ b/apps/gpstrek/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "gpstrek", + "name": "GPS Trekking", + "version": "0.07", + "description": "Helper for tracking the status/progress during hiking. Do NOT depend on this for navigation!", + "icon": "icon.png", + "screenshots": [{"url":"screen1.png"},{"url":"screen2.png"},{"url":"screen3.png"},{"url":"screen4.png"}], + "tags": "tool,outdoors,gps", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "dependencies" : { "waypoints":"type" }, + "storage": [ + {"name":"gpstrek.app.js","url":"app.js"}, + {"name":"gpstrek.wid.js","url":"widget.js"}, + {"name":"gpstrek.img","url":"app-icon.js","evaluate":true} + ], + "data": [{"name":"gpstrek.state.json"}] +} diff --git a/apps/gpstrek/screen1.png b/apps/gpstrek/screen1.png new file mode 100644 index 000000000..3cfd7d31b Binary files /dev/null and b/apps/gpstrek/screen1.png differ diff --git a/apps/gpstrek/screen2.png b/apps/gpstrek/screen2.png new file mode 100644 index 000000000..9a6e14e06 Binary files /dev/null and b/apps/gpstrek/screen2.png differ diff --git a/apps/gpstrek/screen3.png b/apps/gpstrek/screen3.png new file mode 100644 index 000000000..a0c7fd8c3 Binary files /dev/null and b/apps/gpstrek/screen3.png differ diff --git a/apps/gpstrek/screen4.png b/apps/gpstrek/screen4.png new file mode 100644 index 000000000..7b6812077 Binary files /dev/null and b/apps/gpstrek/screen4.png differ diff --git a/apps/gpstrek/widget.js b/apps/gpstrek/widget.js new file mode 100644 index 000000000..347df2df5 --- /dev/null +++ b/apps/gpstrek/widget.js @@ -0,0 +1,155 @@ +(() => { +const STORAGE=require('Storage'); +let state = STORAGE.readJSON("gpstrek.state.json")||{}; +let bgChanged = false; + +function saveState(){ + state.saved = Date.now(); + STORAGE.writeJSON("gpstrek.state.json", state); +} + +E.on("kill",()=>{ + if (bgChanged){ + saveState(); + } +}); + + +function onPulse(e){ + state.bpm = e.bpm; +} + +function onGPS(fix) { + if(fix.fix) state.currentPos = fix; +} + +function onMag(e) { + if (!state.compassHeading) state.compassHeading = e.heading; + + //if (a+180)mod 360 == b then + //return (a+b)/2 mod 360 and ((a+b)/2 mod 360) + 180 (they are both the solution, so you may choose one depending if you prefer counterclockwise or clockwise direction) +//else + //return arctan( (sin(a)+sin(b)) / (cos(a)+cos(b) ) + + /* + let average; + let a = radians(compassHeading); + let b = radians(e.heading); + if ((a+180) % 360 == b){ + average = ((a+b)/2 % 360); //can add 180 depending on rotation + } else { + average = Math.atan( (Math.sin(a)+Math.sin(b))/(Math.cos(a)+Math.cos(b)) ); + } + print("Angle",compassHeading,e.heading, average); + compassHeading = (compassHeading + degrees(average)) % 360; + */ + state.compassHeading = Math.round(e.heading); +} + +function onStep(e) { + state.steps++; +} + +function onPressure(e) { + state.pressure = e.pressure; + + if (!state.altitude){ + state.altitude = e.altitude; + state.up = 0; + state.down = 0; + } + let diff = state.altitude - e.altitude; + if (Math.abs(diff) > 3){ + if (diff > 0){ + state.up += diff; + } else { + state.down -= diff; + } + state.altitude = e.altitude; + } +} + +function onAcc (e){ + state.acc = e; +} + +function start(bg){ + Bangle.removeListener('GPS', onGPS); + Bangle.removeListener("HRM", onPulse); + Bangle.removeListener("mag", onMag); + Bangle.removeListener("step", onStep); + Bangle.removeListener("pressure", onPressure); + Bangle.removeListener('accel', onAcc); + Bangle.on('GPS', onGPS); + Bangle.on("HRM", onPulse); + Bangle.on("mag", onMag); + Bangle.on("step", onStep); + Bangle.on("pressure", onPressure); + Bangle.on('accel', onAcc); + + Bangle.setGPSPower(1, "gpstrek"); + Bangle.setHRMPower(1, "gpstrek"); + Bangle.setCompassPower(1, "gpstrek"); + Bangle.setBarometerPower(1, "gpstrek"); + if (bg){ + if (!state.active) bgChanged = true; + state.active = true; + saveState(); + } + Bangle.drawWidgets(); +} + +function stop(bg){ + if (bg){ + if (state.active) bgChanged = true; + state.active = false; + } else if (!state.active) { + Bangle.setGPSPower(0, "gpstrek"); + Bangle.setHRMPower(0, "gpstrek"); + Bangle.setCompassPower(0, "gpstrek"); + Bangle.setBarometerPower(0, "gpstrek"); + Bangle.removeListener('GPS', onGPS); + Bangle.removeListener("HRM", onPulse); + Bangle.removeListener("mag", onMag); + Bangle.removeListener("step", onStep); + Bangle.removeListener("pressure", onPressure); + Bangle.removeListener('accel', onAcc); + } + saveState(); + Bangle.drawWidgets(); +} + +function initState(){ + //cleanup volatile state here + state.currentPos={}; + state.steps = Bangle.getStepCount(); + state.calibAltDiff = 0; + state.up = 0; + state.down = 0; +} + +if (state.saved && state.saved < Date.now() - 60000){ + initState(); +} + +if (state.active){ + start(false); +} + +WIDGETS["gpstrek"]={ + area:"tl", + width:state.active?24:0, + resetState: initState, + getState: function() { + return state; + }, + start:start, + stop:stop, + draw:function() { + if (state.active){ + g.reset(); + g.drawImage(atob("GBiBAAAAAAAAAAAYAAAYAAAYAAA8AAA8AAB+AAB+AADbAADbAAGZgAGZgAMYwAMYwAcY4AYYYA5+cA3/sB/D+B4AeBAACAAAAAAAAA=="), this.x, this.y); + } + } +}; +})(); diff --git a/apps/groceryaug/ChangeLog b/apps/groceryaug/ChangeLog new file mode 100644 index 000000000..906046782 --- /dev/null +++ b/apps/groceryaug/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! +0.02: Refactor code to store grocery list in separate file diff --git a/apps/groceryaug/README.md b/apps/groceryaug/README.md new file mode 100644 index 000000000..aa1e62beb --- /dev/null +++ b/apps/groceryaug/README.md @@ -0,0 +1,6 @@ +Modified version of the Grocery App - lets you upload an image with the products you need to shop - Display a list of product and track if you already put them in your cart. + +Uses this API to do the OCR: https://rapidapi.com/serendi/api/pen-to-print-handwriting-ocr +With a free account you get 100 API calls a month. + +![Demonstration of groceryaug app](groceryaug_preview.gif) diff --git a/apps/groceryaug/app.js b/apps/groceryaug/app.js new file mode 100644 index 000000000..00408abba --- /dev/null +++ b/apps/groceryaug/app.js @@ -0,0 +1,25 @@ +var filename = 'grocery_list_aug.json'; +var settings = require("Storage").readJSON(filename,1)|| { products: [] }; + +function updateSettings() { + require("Storage").writeJSON(filename, settings); + Bangle.buzz(); +} + + +const mainMenu = settings.products.reduce(function(m, p, i){ +const name = p.name; + m[name] = { + value: p.ok, + format: v => v?'[x]':'[ ]', + onchange: v => { + settings.products[i].ok = v; + updateSettings(); + } + }; + return m; +}, { + '': { 'title': 'Grocery list' } +}); +mainMenu['< Back'] = ()=>{load();}; +E.showMenu(mainMenu); diff --git a/apps/groceryaug/groceryaug-icon.js b/apps/groceryaug/groceryaug-icon.js new file mode 100644 index 000000000..33b649647 --- /dev/null +++ b/apps/groceryaug/groceryaug-icon.js @@ -0,0 +1 @@ +E.toArrayBuffer(atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAiAAMAADAAAwAAMAAiAAMAAAAAAAA/8zP/Mz/zM/8z/zM/8zP/Mz/zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA///MzMzMzMzMzM/////8zP//zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA///MzMzMzMzMz//////8zP//zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA///MzMzMzMzMzM/////8zP//zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA///MzMzMzMzMz//////8zP//zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA/////////////MzMzMzMzP//zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAARE////////////////////////zERAAARE////////////////////////zERAAERE////////////////////////zEREAERE////////////////////////zEREAAREzMzMzMzMzMzMzMzMzMzMzMzMzERAAABEREREREREREREREREREREREREREQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")) diff --git a/apps/groceryaug/groceryaug.html b/apps/groceryaug/groceryaug.html new file mode 100644 index 000000000..6ed07df62 --- /dev/null +++ b/apps/groceryaug/groceryaug.html @@ -0,0 +1,145 @@ + + + + + + Enter/change API key +
+ + + + +

Products

+
+ + + +
+ + + +
+
+

+ + + + + + + + + diff --git a/apps/groceryaug/groceryaug.png b/apps/groceryaug/groceryaug.png new file mode 100644 index 000000000..895a6bbca Binary files /dev/null and b/apps/groceryaug/groceryaug.png differ diff --git a/apps/groceryaug/groceryaug_preview.gif b/apps/groceryaug/groceryaug_preview.gif new file mode 100644 index 000000000..9b099f86e Binary files /dev/null and b/apps/groceryaug/groceryaug_preview.gif differ diff --git a/apps/groceryaug/metadata.json b/apps/groceryaug/metadata.json new file mode 100644 index 000000000..13f377584 --- /dev/null +++ b/apps/groceryaug/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "groceryaug", + "name": "Grocery Augmented", + "version": "0.02", + "description": "Modified version of the Grocery App - lets you upload an image with the products you need to shop - Display a list of product and track if you already put them in your cart.", + "icon": "groceryaug.png", + "readme":"README.md", + "type": "app", + "tags": "tool,outdoors,shopping,list", + "supports": ["BANGLEJS", "BANGLEJS2"], + "custom": "groceryaug.html", + "allow_emulator": true, + "storage": [ + {"name":"groceryaug.app.js","url":"app.js"}, + {"name":"groceryaug.img","url":"groceryaug-icon.js","evaluate":true} + ] +} diff --git a/apps/ha/ChangeLog b/apps/ha/ChangeLog index c28526abd..a4865be3f 100644 --- a/apps/ha/ChangeLog +++ b/apps/ha/ChangeLog @@ -1,4 +1,5 @@ 0.01: Release 0.02: Includeas the ha.lib.js library that can be used by other apps or clocks. 0.03: Added clkinfo for clocks. -0.04: Feedback if clkinfo run is called. \ No newline at end of file +0.04: Feedback if clkinfo run is called. +0.05: Clkinfo improvements. \ No newline at end of file diff --git a/apps/ha/ha.clkinfo.js b/apps/ha/ha.clkinfo.js index ad7f51c57..1b1e468d7 100644 --- a/apps/ha/ha.clkinfo.js +++ b/apps/ha/ha.clkinfo.js @@ -10,7 +10,7 @@ triggers.forEach((trigger, i) => { haItems.items.push({ - name: "haTrigger-" + i, + name: null, get: () => ({ text: trigger.display, img: trigger.getIcon()}), show: function() { haItems.items[i].emit("redraw"); }, hide: function () {}, diff --git a/apps/ha/metadata.json b/apps/ha/metadata.json index 8ccaea598..052e82fe0 100644 --- a/apps/ha/metadata.json +++ b/apps/ha/metadata.json @@ -1,7 +1,7 @@ { "id": "ha", "name": "HomeAssistant", - "version": "0.04", + "version": "0.05", "description": "Integrates your BangleJS into HomeAssistant.", "icon": "ha.png", "type": "app", diff --git a/apps/ncfrun/ChangeLog b/apps/henkinen/ChangeLog similarity index 100% rename from apps/ncfrun/ChangeLog rename to apps/henkinen/ChangeLog diff --git a/apps/henkinen/README.md b/apps/henkinen/README.md new file mode 100644 index 000000000..e17e86121 --- /dev/null +++ b/apps/henkinen/README.md @@ -0,0 +1,7 @@ +# Henkinen + +By Jukio Kallio + +A tiny app helping you to breath and relax. + +![](screenshot1.png) diff --git a/apps/henkinen/app-icon.js b/apps/henkinen/app-icon.js new file mode 100644 index 000000000..7c82a375d --- /dev/null +++ b/apps/henkinen/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkEogA0/4AKCpNPCxYAB+gtTGJQuOGBAWPGAwuQGAwXH+cykc/C6UhgMSkMQiQXKBQsgiYFDmMCMBIIEmAWEDAUDC5nzBwogDMYgXHBoohJC4wuJEQwXG+ALDmUQgMjEYcPC5MhAYXxgAACj4ICVYYXGIwXzCwYABHAUwC5HyEwXwC4pEC+MvC4/xEoUQC4sBHIQlCC4vwIxBIEGYQXFmJKCC45ECfQQXIRoiRGC5EiOxB4EBwQXdI653XU67XX+QJCPAwrC+JKCC4v/gZIIHIUwCAQXGkIDCSIg4C/8SC5PwEwX/mUQgMjAwXzJQQXH+ZICAA8wEYYXGBgoAEEQoXHGBIhFC44OBcgQADmIgFC5H/kAYEmMCBooXDp4KFkMBiUhiCjDAAX0C5RjBmUjPo4XMABQXEMAwALCwgwRFwowRCwwwPFw4xOCpIArA")) diff --git a/apps/henkinen/app.js b/apps/henkinen/app.js new file mode 100644 index 000000000..d7c7bd5ed --- /dev/null +++ b/apps/henkinen/app.js @@ -0,0 +1,127 @@ +// Henkinen +// +// Bangle.js 2 breathing helper +// by Jukio Kallio +// www.jukiokallio.com + +require("FontHaxorNarrow7x17").add(Graphics); + +// settings +const breath = { + theme: "default", + x:0, y:0, w:0, h:0, + size: 60, + + bgcolor: g.theme.bg, + incolor: g.theme.fg, + keepcolor: g.theme.fg, + outcolor: g.theme.fg, + + font: "HaxorNarrow7x17", fontsize: 1, + textcolor: g.theme.fg, + texty: 18, + + in: 4000, + keep: 7000, + out: 8000 +}; + +// set some additional settings +breath.w = g.getWidth(); // size of the background +breath.h = g.getHeight(); +breath.x = breath.w * 0.5; // position of the circles +breath.y = breath.h * 0.45; +breath.texty = breath.y + breath.size + breath.texty; // text position + +var wait = 100; // wait time, normally a minute +var time = 0; // for time keeping + + +// timeout used to update every minute +var drawTimeout; + +// schedule a draw for the next minute +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, wait - (Date.now() % wait)); +} + + +// main function +function draw() { + // make date object + var date = new Date(); + + // update current time + time += wait - (Date.now() % wait); + if (time > breath.in + breath.keep + breath.out) time = 0; // reset time + + // Reset the state of the graphics library + g.reset(); + + // Clear the area where we want to draw the time + g.setColor(breath.bgcolor); + g.fillRect(0, 0, breath.w, breath.h); + + // calculate circle size + var circle = 0; + if (time < breath.in) { + // breath in + circle = time / breath.in; + g.setColor(breath.incolor); + + } else if (time < breath.in + breath.keep) { + // keep breath + circle = 1; + g.setColor(breath.keepcolor); + + } else if (time < breath.in + breath.keep + breath.out) { + // breath out + circle = ((breath.in + breath.keep + breath.out) - time) / breath.out; + g.setColor(breath.outcolor); + + } + + // draw breath circle + g.fillCircle(breath.x, breath.y, breath.size * circle); + + // breath area + g.setColor(breath.textcolor); + g.drawCircle(breath.x, breath.y, breath.size); + + // draw text + g.setFontAlign(0,0).setFont(breath.font, breath.fontsize).setColor(breath.textcolor); + + if (time < breath.in) { + // breath in + g.drawString("Breath in", breath.x, breath.texty); + + } else if (time < breath.in + breath.keep) { + // keep breath + g.drawString("Keep it in", breath.x, breath.texty); + + } else if (time < breath.in + breath.keep + breath.out) { + // breath out + g.drawString("Breath out", breath.x, breath.texty); + + } + + // queue draw + queueDraw(); +} + + +// Clear the screen once, at startup +g.clear(); +// draw immediately at first +draw(); + + +// keep LCD on +Bangle.setLCDPower(1); + +// Show launcher when middle button pressed +Bangle.setUI("clock"); diff --git a/apps/henkinen/app.png b/apps/henkinen/app.png new file mode 100644 index 000000000..575ecbcd4 Binary files /dev/null and b/apps/henkinen/app.png differ diff --git a/apps/henkinen/metadata.json b/apps/henkinen/metadata.json new file mode 100644 index 000000000..1f1bb77fc --- /dev/null +++ b/apps/henkinen/metadata.json @@ -0,0 +1,15 @@ +{ "id": "henkinen", + "name": "Henkinen - Tiny Breathing Helper", + "shortName":"Henkinen", + "version":"0.01", + "description": "A tiny app helping you to breath and relax.", + "icon": "app.png", + "screenshots": [{"url":"screenshot1.png"}], + "tags": "outdoors", + "supports" : ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"henkinen.app.js","url":"app.js"}, + {"name":"henkinen.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/henkinen/screenshot1.png b/apps/henkinen/screenshot1.png new file mode 100644 index 000000000..938494673 Binary files /dev/null and b/apps/henkinen/screenshot1.png differ diff --git a/apps/iconlaunch/ChangeLog b/apps/iconlaunch/ChangeLog index 991f15abb..c71da1467 100644 --- a/apps/iconlaunch/ChangeLog +++ b/apps/iconlaunch/ChangeLog @@ -1,3 +1,14 @@ 0.01: Initial release 0.02: implemented "direct launch" and "one click exit" settings 0.03: Use default Bangle formatter for booleans +0.04: Support new fast app switching +0.05: Allow to directly eval apps instead of loading +0.06: Cache apps for faster start +0.07: Read app icons on demand + Add swipe-to-exit +0.08: Only use fast loading for switching to clock to prevent problems in full screen apps +0.09: Remove fast load option since clocks containing Bangle.loadWidgets are now always normally loaded +0.10: changed the launch.json file name in iconlaunch.json ( launch.cache.json -> iconlaunch.cache.json) + used Object.assing for the settings + fix cache not deleted when "showClocks" options is changed + added timeOut to return to the clock diff --git a/apps/iconlaunch/app.js b/apps/iconlaunch/app.js index 59e9eb0e3..479956019 100644 --- a/apps/iconlaunch/app.js +++ b/apps/iconlaunch/app.js @@ -1,163 +1,138 @@ -const s = require("Storage"); -const settings = s.readJSON("launch.json", true) || { showClocks: true, fullscreen: false,direct:false,oneClickExit:false }; +{ + const s = require("Storage"); + const settings = Object.assign({ + showClocks: true, + fullscreen: false, + direct: false, + oneClickExit: false, + swipeExit: false, + timeOut:"Off" + }, s.readJSON("iconlaunch.json", true) || {}); -if( settings.oneClickExit) - setWatch(_=> load(), BTN1); - -if (!settings.fullscreen) { - Bangle.loadWidgets(); - Bangle.drawWidgets(); -} - -var apps = s - .list(/\.info$/) - .map((app) => { - var a = s.readJSON(app, 1); - return ( - a && { - name: a.name, - type: a.type, - icon: a.icon, - sortorder: a.sortorder, - src: a.src, + console.log(settings); + if (!settings.fullscreen) { + Bangle.loadWidgets(); + Bangle.drawWidgets(); + } + let launchCache = s.readJSON("iconlaunch.cache.json", true)||{}; + let launchHash = s.hash(/\.info/); + if (launchCache.hash!=launchHash) { + launchCache = { + hash : launchHash, + apps : s.list(/\.info$/) + .map(app=>{let a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src};}) + .filter(app=>app && (app.type=="app" || (app.type=="clock" && settings.showClocks) || !app.type)) + .sort((a,b)=>{ + let n=(0|a.sortorder)-(0|b.sortorder); + if (n) return n; // do sortorder first + if (a.nameb.name) return 1; + return 0; + }) }; + s.writeJSON("iconlaunch.cache.json", launchCache); + } + let scroll = 0; + let selectedItem = -1; + const R = Bangle.appRect; + const iconSize = 48; + const appsN = Math.floor(R.w / iconSize); + const whitespace = (R.w - appsN * iconSize) / (appsN + 1); + const itemSize = iconSize + whitespace; + let drawItem = function(itemI, r) { + g.clearRect(r.x, r.y, r.x + r.w - 1, r.y + r.h - 1); + let x = 0; + for (let i = itemI * appsN; i < appsN * (itemI + 1); i++) { + if (!launchCache.apps[i]) break; + x += whitespace; + if (!launchCache.apps[i].icon) { + g.setFontAlign(0, 0, 0).setFont("12x20:2").drawString("?", x + r.x + iconSize / 2, r.y + iconSize / 2); + } else { + if (!launchCache.apps[i].icondata) launchCache.apps[i].icondata = s.read(launchCache.apps[i].icon); + g.drawImage(launchCache.apps[i].icondata, x + r.x, r.y); } - ); - }) - .filter( - (app) => - app && - (app.type == "app" || - (app.type == "clock" && settings.showClocks) || - !app.type) - ); -apps.sort((a, b) => { - var n = (0 | a.sortorder) - (0 | b.sortorder); - if (n) return n; // do sortorder first - if (a.name < b.name) return -1; - if (a.name > b.name) return 1; - return 0; -}); -apps.forEach((app) => { - if (app.icon) app.icon = s.read(app.icon); // should just be a link to a memory area -}); - -let scroll = 0; -let selectedItem = -1; -const R = Bangle.appRect; - -const iconSize = 48; - -const appsN = Math.floor(R.w / iconSize); -const whitespace = (R.w - appsN * iconSize) / (appsN + 1); - -const itemSize = iconSize + whitespace; - -function drawItem(itemI, r) { - g.clearRect(r.x, r.y, r.x + r.w - 1, r.y + r.h - 1); - let x = 0; - for (let i = itemI * appsN; i < appsN * (itemI + 1); i++) { - if (!apps[i]) break; - x += whitespace; - if (!apps[i].icon) { - g.setFontAlign(0,0,0).setFont("12x20:2").drawString("?", x + r.x+iconSize/2, r.y + iconSize/2); - } else { - g.drawImage(apps[i].icon, x + r.x, r.y); + if (selectedItem == i) { + g.drawRect( + x + r.x - 1, + r.y - 1, + x + r.x + iconSize + 1, + r.y + iconSize + 1 + ); + } + x += iconSize; } - if (selectedItem == i) { - g.drawRect( - x + r.x - 1, - r.y - 1, - x + r.x + iconSize + 1, - r.y + iconSize + 1 - ); - } - x += iconSize; - } - drawText(itemI); -} - -function drawItemAuto(i) { - var y = idxToY(i); - g.reset().setClipRect(R.x, y, R.x2, y + itemSize); - drawItem(i, { - x: R.x, - y: y, - w: R.w, - h: itemSize - }); - g.setClipRect(0, 0, g.getWidth() - 1, g.getHeight() - 1); -} - -let lastIsDown = false; - -function drawText(i) { - const selectedApp = apps[selectedItem]; - const idy = (selectedItem - (selectedItem % 3)) / 3; - if (!selectedApp || i != idy) return; - const appY = idxToY(idy) + iconSize / 2; - g.setFontAlign(0, 0, 0); - g.setFont("12x20"); - const rect = g.stringMetrics(selectedApp.name); - g.clearRect( - R.w / 2 - rect.width / 2, - appY - rect.height / 2, - R.w / 2 + rect.width / 2, - appY + rect.height / 2 - ); - g.drawString(selectedApp.name, R.w / 2, appY); -} - -function selectItem(id, e) { - const iconN = E.clip(Math.floor((e.x - R.x) / itemSize), 0, appsN - 1); - const appId = id * appsN + iconN; - if( settings.direct && apps[appId]) - { - load(apps[appId].src); - return; - } - if (appId == selectedItem && apps[appId]) { - const app = apps[appId]; - if (!app.src || s.read(app.src) === undefined) { - E.showMessage( /*LANG*/ "App Source\nNot found"); - } else { - load(app.src); - } - } - selectedItem = appId; - drawItems(); -} - -function idxToY(i) { - return i * itemSize + R.y - (scroll & ~1); -} - -function YtoIdx(y) { - return Math.floor((y + (scroll & ~1) - R.y) / itemSize); -} - -function drawItems() { - g.reset().clearRect(R.x, R.y, R.x2, R.y2); - g.setClipRect(R.x, R.y, R.x2, R.y2); - var a = YtoIdx(R.y); - var b = Math.min(YtoIdx(R.y2), 99); - for (var i = a; i <= b; i++) + drawText(itemI); + }; + let drawItemAuto = function(i) { + let y = idxToY(i); + g.reset().setClipRect(R.x, y, R.x2, y + itemSize); drawItem(i, { + x: R.x, + y: y, + w: R.w, + h: itemSize + }); + g.setClipRect(0, 0, g.getWidth() - 1, g.getHeight() - 1); + }; + let lastIsDown = false; + let drawText = function(i) { + const selectedApp = launchCache.apps[selectedItem]; + const idy = (selectedItem - (selectedItem % 3)) / 3; + if (!selectedApp || i != idy) return; + const appY = idxToY(idy) + iconSize / 2; + g.setFontAlign(0, 0, 0); + g.setFont("12x20"); + const rect = g.stringMetrics(selectedApp.name); + g.clearRect( + R.w / 2 - rect.width / 2, + appY - rect.height / 2, + R.w / 2 + rect.width / 2, + appY + rect.height / 2 + ); + g.drawString(selectedApp.name, R.w / 2, appY); + }; + let selectItem = function(id, e) { + const iconN = E.clip(Math.floor((e.x - R.x) / itemSize), 0, appsN - 1); + const appId = id * appsN + iconN; + if( settings.direct && launchCache.apps[appId]) + { + load(launchCache.apps[appId].src); + return; + } + if (appId == selectedItem && launchCache.apps[appId]) { + const app = launchCache.apps[appId]; + if (!app.src || s.read(app.src) === undefined) { + E.showMessage( /*LANG*/ "App Source\nNot found"); + } else { + load(app.src); + } + } + selectedItem = appId; + drawItems(); + }; + let idxToY = function(i) { + return i * itemSize + R.y - (scroll & ~1); + }; + let YtoIdx = function(y) { + return Math.floor((y + (scroll & ~1) - R.y) / itemSize); + }; + let drawItems = function() { + g.reset().clearRect(R.x, R.y, R.x2, R.y2); + g.setClipRect(R.x, R.y, R.x2, R.y2); + let a = YtoIdx(R.y); + let b = Math.min(YtoIdx(R.y2), 99); + for (let i = a; i <= b; i++) + drawItem(i, { x: R.x, y: idxToY(i), w: R.w, h: itemSize, }); - g.setClipRect(0, 0, g.getWidth() - 1, g.getHeight() - 1); -} - -drawItems(); -g.flip(); - -const itemsN = Math.ceil(apps.length / appsN); - -Bangle.setUI({ - mode: "custom", - drag: (e) => { + g.setClipRect(0, 0, g.getWidth() - 1, g.getHeight() - 1); + }; + drawItems(); + g.flip(); + const itemsN = Math.ceil(launchCache.apps.length / appsN); + let onDrag = function(e) { g.setColor(g.theme.fg); g.setBgColor(g.theme.bg); let dy = e.dy; @@ -186,7 +161,6 @@ Bangle.setUI({ y += itemSize; } } else { - // d>0 g.setClipRect(R.x, R.y, R.x2, R.y + dy); let i = YtoIdx(R.y + dy); let y = idxToY(i); @@ -202,10 +176,42 @@ Bangle.setUI({ } } g.setClipRect(0, 0, g.getWidth() - 1, g.getHeight() - 1); - }, - touch: (_, e) => { - if (e.y < R.y - 4) return; - var i = YtoIdx(e.y); - selectItem(i, e); - }, -}); + }; + let mode = { + mode: "custom", + drag: onDrag, + touch: (_, e) => { + if (e.y < R.y - 4) return; + let i = YtoIdx(e.y); + selectItem(i, e); + }, + swipe: (h,_) => { if(settings.swipeExit && h==1) { returnToClock(); } }, + }; + + const returnToClock = function() { + Bangle.setUI(); + delete launchCache; + delete launchHash; + delete drawItemAuto; + delete drawText; + delete selectItem; + delete onDrag; + delete drawItems; + delete drawItem; + delete returnToClock; + delete idxToY; + delete YtoIdx; + delete settings; + setTimeout(eval, 0, s.read(".bootcde")); + }; + + + if (settings.oneClickExit) mode.btn = returnToClock; + if (settings.timeOut!="Off"){ + let time=parseInt(settings.timeOut); //the "s" will be trimmed by the parseInt + setTimeout(returnToClock,time*1000); + } + + + Bangle.setUI(mode); +} diff --git a/apps/iconlaunch/metadata.json b/apps/iconlaunch/metadata.json index c7acc534f..13e7aee08 100644 --- a/apps/iconlaunch/metadata.json +++ b/apps/iconlaunch/metadata.json @@ -2,7 +2,7 @@ "id": "iconlaunch", "name": "Icon Launcher", "shortName" : "Icon launcher", - "version": "0.03", + "version": "0.10", "icon": "app.png", "description": "A launcher inspired by smartphones, with an icon-only scrollable menu.", "tags": "tool,system,launcher", @@ -12,6 +12,7 @@ { "name": "iconlaunch.app.js", "url": "app.js" }, { "name": "iconlaunch.settings.js", "url": "settings.js" } ], + "data": [{"name":"iconlaunch.json"},{"name":"iconlaunch.cache.json"}], "screenshots": [{ "url": "screenshot1.png" }, { "url": "screenshot2.png" }], "readme": "README.md" } diff --git a/apps/iconlaunch/settings.js b/apps/iconlaunch/settings.js index bd1a4a597..f4c0599f7 100644 --- a/apps/iconlaunch/settings.js +++ b/apps/iconlaunch/settings.js @@ -1,21 +1,29 @@ // make sure to enclose the function in parentheses (function(back) { + const s = require("Storage"); let settings = Object.assign({ showClocks: true, - fullscreen: false - }, require("Storage").readJSON("launch.json", true) || {}); + fullscreen: false, + direct: false, + oneClickExit: false, + swipeExit: false, + timeOut:"Off" + }, s.readJSON("iconlaunch.json", true) || {}); - let fonts = g.getFonts(); function save(key, value) { settings[key] = value; - require("Storage").write("launch.json",settings); + s.write("iconlaunch.json",settings); } + const timeOutChoices = [/*LANG*/"Off", "10s", "15s", "20s", "30s"]; const appMenu = { "": { "title": /*LANG*/"Launcher" }, /*LANG*/"< Back": back, /*LANG*/"Show Clocks": { value: settings.showClocks == true, - onchange: (m) => { save("showClocks", m) } + onchange: (m) => { + save("showClocks", m); + s.erase("iconlaunch.cache.json"); //delete the cache app list + } }, /*LANG*/"Fullscreen": { value: settings.fullscreen == true, @@ -28,7 +36,19 @@ /*LANG*/"One click exit": { value: settings.oneClickExit == true, onchange: (m) => { save("oneClickExit", m) } - } + }, + /*LANG*/"Swipe exit": { + value: settings.swipeExit == true, + onchange: m => { save("swipeExit", m) } + }, + /*LANG*/'Time Out': { + value: timeOutChoices.indexOf(settings.timeOut), + min: 0, max: timeOutChoices.length-1, + format: v => timeOutChoices[v], + onchange: m => { + save("timeOut", timeOutChoices[m]); + } + }, }; E.showMenu(appMenu); }); diff --git a/apps/imageclock/ChangeLog b/apps/imageclock/ChangeLog index af1b97b3d..d681176a7 100644 --- a/apps/imageclock/ChangeLog +++ b/apps/imageclock/ChangeLog @@ -8,3 +8,11 @@ 0.07: Allow wrapping drawing in timeouts to get faster reactions Show/Hide widgets with swipe up or down 0.08: Use default Bangle formatter for booleans +0.09: Support new fast app switching +0.10: Fix clock not correctly refreshing when drawing in timeouts option is not on +0.11: Additional option in customizer to force drawing directly + Fix some problems in handling timeouts +0.12: Use widget_utils module + Fix colorsetting in promises in generated code + Some performance improvements by caching lookups + Activate UI after first draw is complete to prevent drawing over launcher diff --git a/apps/imageclock/app.js b/apps/imageclock/app.js index 7b933b710..3d7a830d2 100644 --- a/apps/imageclock/app.js +++ b/apps/imageclock/app.js @@ -1,720 +1,771 @@ -var watchface = require("Storage").readJSON("imageclock.face.json"); -var watchfaceResources = require("Storage").readJSON("imageclock.resources.json"); -var precompiledJs = eval(require("Storage").read("imageclock.draw.js")); -var settings = require('Storage').readJSON("imageclock.json", true) || {}; +let unlockedDrawInterval = []; +let lockedDrawInterval = []; +let showWidgets = false; +let firstDraw = true; -var performanceLog = {}; +{ + let x = g.getWidth()/2; + let y = g.getHeight()/2; + g.setColor(g.theme.bg); + g.fillRect(x-49, y-19, x+49, y+19); + g.setColor(g.theme.fg); + g.drawRect(x-50, y-20, x+50, y+20); + y -= 4; + x -= 4*6; + g.setFont("6x8"); + g.setFontAlign(-1,-1); + g.drawString("Loading...", x, y); -var startPerfLog = () => {}; -var endPerfLog = () => {}; -var printPerfLog = () => print("Deactivated"); -var resetPerfLog = () => {performanceLog = {};}; + let watchface = require("Storage").readJSON("imageclock.face.json"); + let watchfaceResources = require("Storage").readJSON("imageclock.resources.json"); + let precompiledJs = eval(require("Storage").read("imageclock.draw.js")); + let settings = require('Storage').readJSON("imageclock.json", true) || {}; -var colormap={ -"#000":0, -"#00f":1, -"#0f0":2, -"#0ff":3, -"#f00":4, -"#f0f":5, -"#ff0":6, -"#fff":7 -}; + let performanceLog = {}; -var palette = new Uint16Array([ -0x0000, //black #000 -0x001f, //blue #00f -0x07e0, //green #0f0 -0x07ff, //cyan #0ff -0xf800, //red #f00 -0xf81f, //magenta #f0f -0xffe0, //yellow #ff0 -0xffff, //white #fff -0xffff, //white -0xffff, //white -0xffff, //white -0xffff, //white -0xffff, //white -0xffff, //white -0xffff, //white -0xffff, //white -]) + let startPerfLog = () => {}; + let endPerfLog = () => {}; + Bangle.printPerfLog = () => {print("Deactivated");}; + Bangle.resetPerfLog = () => {performanceLog = {};}; -var p0 = g; -var p1; - -if (settings.perflog){ - startPerfLog = function(name){ - var time = getTime(); - if (!performanceLog.start) performanceLog.start={}; - performanceLog.start[name] = time; - }; - endPerfLog = function (name){ - var time = getTime(); - if (!performanceLog.last) performanceLog.last={}; - var duration = time - performanceLog.start[name]; - performanceLog.last[name] = duration; - if (!performanceLog.cum) performanceLog.cum={}; - if (!performanceLog.cum[name]) performanceLog.cum[name] = 0; - performanceLog.cum[name] += duration; - if (!performanceLog.count) performanceLog.count={}; - if (!performanceLog.count[name]) performanceLog.count[name] = 0; - performanceLog.count[name]++; + let colormap={ + "#000":0, + "#00f":1, + "#0f0":2, + "#0ff":3, + "#f00":4, + "#f0f":5, + "#ff0":6, + "#fff":7 }; - printPerfLog = function(){ - var result = ""; - var keys = []; - for (var c in performanceLog.cum){ - keys.push(c); - } - keys.sort(); - for (var k of keys){ - print(k, "last:", (performanceLog.last[k] * 1000).toFixed(0), "average:", (performanceLog.cum[k]/performanceLog.count[k]*1000).toFixed(0), "count:", performanceLog.count[k], "total:", (performanceLog.cum[k] * 1000).toFixed(0)); - } - }; -} + let palette = new Uint16Array([ + 0x0000, //black #000 + 0x001f, //blue #00f + 0x07e0, //green #0f0 + 0x07ff, //cyan #0ff + 0xf800, //red #f00 + 0xf81f, //magenta #f0f + 0xffe0, //yellow #ff0 + 0xffff, //white #fff + 0xffff, //white + 0xffff, //white + 0xffff, //white + 0xffff, //white + 0xffff, //white + 0xffff, //white + 0xffff, //white + 0xffff, //white + ]); -function delay(t) { - return new Promise(function (resolve) { - setTimeout(resolve, t); - }); -} + let p0 = g; + let p1; -function prepareImg(resource){ - startPerfLog("prepareImg"); - //print("prepareImg: ", resource); - - if (resource.dataOffset !== undefined){ - resource.buffer = E.toArrayBuffer(require("Storage").read("imageclock.resources.data", resource.dataOffset, resource.dataLength)); - delete resource.dataOffset; - delete resource.dataLength; - if (resource.paletteData){ - result.palette = new Uint16Array(resource.paletteData); - delete resource.paletteData; - } - } - endPerfLog("prepareImg"); - return resource; -} - -function getByPath(object, path, lastElem){ - startPerfLog("getByPath"); - //print("getByPath", path,lastElem); - var current = object; - if (path.length) { - for (var c of path){ - if (!current[c]) return undefined; - current = current[c]; - } - } - if (lastElem!==undefined){ - if (!current["" + lastElem]) return undefined; - //print("Found by lastElem", lastElem); - current = current["" + lastElem]; - } - endPerfLog("getByPath"); - if (typeof current == "function"){ - //print("current was function"); - return undefined; - } - return current; -} - -function splitNumberToDigits(num){ - return String(num).split('').map(item => Number(item)); -} - -function isChangedNumber(element){ - return element.lastDrawnValue != getValue(element.Value); -} - -function isChangedMultistate(element){ - return element.lastDrawnValue != getMultistate(element.Value); -} - -function drawNumber(graphics, resources, element){ - startPerfLog("drawNumber"); - var number = getValue(element.Value); - var spacing = element.Spacing ? element.Spacing : 0; - var unit = element.Unit; - - var imageIndexMinus = element.ImageIndexMinus; - var imageIndexUnit = element.ImageIndexUnit; - var numberOfDigits = element.Digits; - - - //print("drawNumber: ", number, element); - if (number) number = number.toFixed(0); - - var isNegative; - var digits; - if (number == undefined){ - isNegative = true; - digits = []; - numberOfDigits = 0; - } else { - isNegative = number < 0; - if (isNegative) number *= -1; - digits = splitNumberToDigits(number); - } - - //print("digits: ", digits); - if (!numberOfDigits) numberOfDigits = digits.length; - var firstDigitX = element.X; - var firstDigitY = element.Y; - var imageIndex = element.ImageIndex ? element.ImageIndex : 0; - - var firstImage; - if (imageIndex){ - firstImage = getByPath(resources, [], "" + (0 + imageIndex)); - } else { - firstImage = getByPath(resources, element.ImagePath, 0); - } - - var minusImage; - if (imageIndexMinus){ - minusImage = getByPath(resources, [], "" + (0 + imageIndexMinus)); - } else { - minusImage = getByPath(resources, element.ImagePath, "minus"); - } - - var unitImage; - //print("Get image for unit", imageIndexUnit); - if (imageIndexUnit !== undefined){ - unitImage = getByPath(resources, [], "" + (0 + imageIndexUnit)); - //print("Unit image is", unitImage); - } else if (element.Unit){ - unitImage = getByPath(resources, element.ImagePath, getMultistate(element.Unit, "unknown")); - } - - var numberWidth = (numberOfDigits * firstImage.width) + (Math.max((numberOfDigits - 1),0) * spacing); - if (isNegative && minusImage){ - //print("Adding to width", minusImage); - numberWidth += minusImage.width + spacing; - } - if (unitImage){ - //print("Adding to width", unitImage); - numberWidth += unitImage.width + spacing; - } - //print("numberWidth:", numberWidth); - - if (element.Alignment == "Center") { - firstDigitX = Math.round(element.X - (numberWidth/2)) + 1; - firstDigitY = Math.round(element.Y - (firstImage.height/2)) + 1; - } else if (element.Alignment == "BottomRight"){ - firstDigitX = element.X - numberWidth + 1; - firstDigitY = element.Y - firstImage.height + 1; - } else if (element.Alignment == "TopRight") { - firstDigitX = element.X - numberWidth + 1; - firstDigitY = element.Y; - } else if (element.Alignment == "BottomLeft") { - firstDigitX = element.X; - firstDigitY = element.Y - firstImage.height + 1; - } - - var currentX = firstDigitX; - if (isNegative && minusImage){ - //print("Draw minus at", currentX); - if (imageIndexMinus){ - drawElement(graphics, resources, {X:currentX,Y:firstDigitY}, element, "" + (0 + imageIndexMinus)); - } else { - drawElement(graphics, resources, {X:currentX,Y:firstDigitY}, element, "minus"); - } - currentX += minusImage.width + spacing; - } - for (var d = 0; d < numberOfDigits; d++){ - var currentDigit; - var difference = numberOfDigits - digits.length; - if (d >= difference){ - currentDigit = digits[d-difference]; - } else { - currentDigit = 0; - } - //print("Digit " + currentDigit + " " + currentX); - drawElement(graphics, resources, {X:currentX,Y:firstDigitY}, element, currentDigit + imageIndex); - currentX += firstImage.width + spacing; - } - if (imageIndexUnit){ - //print("Draw unit at", currentX); - drawElement(graphics, resources, {X:currentX,Y:firstDigitY}, element, "" + (0 + imageIndexUnit)); - } else if (element.Unit){ - drawElement(graphics, resources, {X:currentX,Y:firstDigitY}, element, getMultistate(element.Unit,"unknown")); - } - element.lastDrawnValue = number; - - endPerfLog("drawNumber"); -} - -function drawElement(graphics, resources, pos, element, lastElem){ - startPerfLog("drawElement"); - var cacheKey = "_"+(lastElem?lastElem:"nole"); - if (!element.cachedImage) element.cachedImage={}; - if (!element.cachedImage[cacheKey]){ - var resource = getByPath(resources, element.ImagePath, lastElem); - if (resource){ - prepareImg(resource); - //print("lastElem", typeof resource) - if (resource) { - element.cachedImage[cacheKey] = resource; - //print("cache res ",typeof element.cachedImage[cacheKey]); - } else { - element.cachedImage[cacheKey] = null; - //print("cache null",typeof element.cachedImage[cacheKey]); - //print("Could not create image from", resource); - } - } else { - //print("Could not get resource from", element, lastElem); - } - } - - //print("cache ",typeof element.cachedImage[cacheKey], element.ImagePath, lastElem); - if(element.cachedImage[cacheKey]){ - //print("drawElement ",pos, path, lastElem); - //print("resource ", resource,pos, path, lastElem); - //print("drawImage from drawElement", image, pos); - var options={}; - if (element.RotationValue){ - options.rotate = radians(element); - } - if (element.Scale){ - options.scale = element.ScaleValue; - } - //print("options", options); - //print("Memory before drawing", process.memory(false)); - startPerfLog("drawElement_g.drawImage"); - try{ - graphics.drawImage(element.cachedImage[cacheKey] ,(pos.X ? pos.X : 0), (pos.Y ? pos.Y : 0), options);} catch (e) { - //print("Error", e, element.cachedImage[cacheKey]); - } - endPerfLog("drawElement_g.drawImage"); - } - endPerfLog("drawElement"); -} - -function getValue(value, defaultValue){ - if (typeof value == "string"){ - return numbers[value](); - } - if (value == undefined) return defaultValue; - return value; -} - -function getMultistate(name, defaultValue){ - if (typeof name == "string"){ - return multistates[name](); - } else { - if (name == undefined) return defaultValue; - } - return undefined; -} - -function drawScale(graphics, resources, scale){ - startPerfLog("drawScale"); - //print("drawScale", scale); - var segments = scale.Segments; - var imageIndex = scale.ImageIndex !== undefined ? scale.ImageIndex : 0; - - var value = scaledown(scale.Value, scale.MinValue, scale.MaxValue); - - //print("Value is ", value, "(", maxValue, ",", minValue, ")"); - - var segmentsToDraw = Math.ceil(value * segments.length); - - for (var i = 0; i < segmentsToDraw; i++){ - drawElement(graphics, resources, segments[i], scale, imageIndex + i); - } - scale.lastDrawnValue = segmentsToDraw; - - endPerfLog("drawScale"); -} - -function drawImage(graphics, resources, image, name){ - startPerfLog("drawImage"); - //print("drawImage", image.X, image.Y, name); - if (image.Value && image.Steps){ - var steps = Math.floor(scaledown(image.Value, image.MinValue, image.MaxValue) * (image.Steps - 1)); - //print("Step", steps, "of", image.Steps); - drawElement(graphics, resources, image, image, "" + steps); - } else if (image.ImageIndex !== undefined) { - drawElement(graphics, resources, image, image, image.ImageIndex); - } else { - drawElement(graphics, resources, image, image, name ? "" + name: undefined); - } - - endPerfLog("drawImage"); -} - -function drawCodedImage(graphics, resources, image){ - startPerfLog("drawCodedImage"); - var code = getValue(image.Value); - //print("drawCodedImage", image, code); - - if (image.ImagePath) { - var factor = 1; - var currentCode = code; - while (code / factor > 1){ - currentCode = Math.floor(currentCode/factor)*factor; - //print("currentCode", currentCode); - if (getByPath(resources, image.ImagePath, currentCode)){ - break; - } - factor *= 10; - } - if (code / factor > 1){ - //print("found match"); - drawImage(graphics, resources, image, currentCode); - } else { - //print("fallback"); - drawImage(graphics, resources, image, "fallback"); - } - } - image.lastDrawnValue = code; - - startPerfLog("drawCodedImage"); -} - -function getWeatherCode(){ - var jsonWeather = require("Storage").readJSON('weather.json'); - var weather = (jsonWeather && jsonWeather.weather) ? jsonWeather.weather : undefined; - - if (weather && weather.code){ - return weather.code; - } - return undefined; -} - -function getWeatherTemperature(){ - var jsonWeather = require("Storage").readJSON('weather.json'); - var weather = (jsonWeather && jsonWeather.weather) ? jsonWeather.weather : undefined; - - var result = { unit: "unknown"}; - if (weather && weather.temp){ - //print("Weather is", weather); - var temp = require('locale').temp(weather.temp-273.15); - result.value = Number(temp.match(/[\d\-]*/)[0]); - var unit; - if (temp.includes("C")){ - result.unit = "celsius"; - } else if (temp.includes("F")){ - result.unit = "fahrenheit"; - } - } - return result; -} - -function scaledown(value, min, max){ - //print("scaledown", value, min, max); - var scaled = E.clip(getValue(value),getValue(min,0),getValue(max,1)); - scaled -= getValue(min,0); - scaled /= getValue(max,1); - return scaled; -} - -function radians(rotation){ - var value = scaledown(rotation.RotationValue, rotation.MinRotationValue, rotation.MaxRotationValue); - value -= rotation.RotationOffset ? rotation.RotationOffset : 0; - value *= 360; - value *= Math.PI / 180; - return value; -} - -function drawPoly(graphics, resources, element){ - startPerfLog("drawPoly"); - var vertices = []; - - startPerfLog("drawPoly_transform"); - for (var c of element.Vertices){ - vertices.push(c.X); - vertices.push(c.Y); - } - var transform = { x: element.X ? element.X : 0, - y: element.Y ? element.Y : 0 + if (settings.perflog){ + startPerfLog = function(name){ + let time = getTime(); + if (!performanceLog.start) performanceLog.start={}; + performanceLog.start[name] = time; + }; + endPerfLog = function (name){ + let time = getTime(); + if (!performanceLog.last) performanceLog.last={}; + let duration = time - performanceLog.start[name]; + performanceLog.last[name] = duration; + if (!performanceLog.cum) performanceLog.cum={}; + if (!performanceLog.cum[name]) performanceLog.cum[name] = 0; + performanceLog.cum[name] += duration; + if (!performanceLog.count) performanceLog.count={}; + if (!performanceLog.count[name]) performanceLog.count[name] = 0; + performanceLog.count[name]++; }; - if (element.RotationValue){ - transform.rotate = radians(element); - } - vertices = graphics.transformVertices(vertices, transform); - endPerfLog("drawPoly_transform"); - - if (element.Filled){ - startPerfLog("drawPoly_g.fillPoly"); - graphics.fillPoly(vertices,true); - endPerfLog("drawPoly_g.fillPoly"); - } else { - startPerfLog("drawPoly_g.drawPoly"); - graphics.drawPoly(vertices,true); - endPerfLog("drawPoly_g.drawPoly"); - } - - endPerfLog("drawPoly"); -} - -function drawRect(graphics, resources, element){ - startPerfLog("drawRect"); - var vertices = []; - - if (element.Filled){ - startPerfLog("drawRect_g.fillRect"); - graphics.fillRect(element.X, element.Y, element.X + element.Width, element.Y + element.Height); - endPerfLog("drawRect_g.fillRect"); - } else { - startPerfLog("drawRect_g.fillRect"); - graphics.drawRect(element.X, element.Y, element.X + element.Width, element.Y + element.Height); - endPerfLog("drawRect_g.fillRect"); - } - endPerfLog("drawRect"); -} - -function drawCircle(graphics, resources, element){ - startPerfLog("drawCircle"); - - if (element.Filled){ - startPerfLog("drawCircle_g.fillCircle"); - graphics.fillCircle(element.X, element.Y, element.Radius); - endPerfLog("drawCircle_g.fillCircle"); - } else { - startPerfLog("drawCircle_g.drawCircle"); - graphics.drawCircle(element.X, element.Y, element.Radius); - endPerfLog("drawCircle_g.drawCircle"); - } - endPerfLog("drawCircle"); -} - -var numbers = {}; -numbers.Hour = () => { return new Date().getHours(); }; -numbers.HourTens = () => { return Math.floor(new Date().getHours()/10); }; -numbers.HourOnes = () => { return Math.floor(new Date().getHours()%10); }; -numbers.Hour12 = () => { return new Date().getHours()%12; }; -numbers.Hour12Analog = () => { var date = new Date(); return date.getHours()%12 + (date.getMinutes()/59); }; -numbers.Hour12Tens = () => { return Math.floor((new Date().getHours()%12)/10); }; -numbers.Hour12Ones = () => { return Math.floor((new Date().getHours()%12)%10); }; -numbers.Minute = () => { return new Date().getMinutes(); }; -numbers.MinuteAnalog = () => { var date = new Date(); return date.getMinutes() + (date.getSeconds()/59); }; -numbers.MinuteTens = () => { return Math.floor(new Date().getMinutes()/10); }; -numbers.MinuteOnes = () => { return Math.floor(new Date().getMinutes()%10); }; -numbers.Second = () => { return new Date().getSeconds(); }; -numbers.SecondAnalog = () => { var date = new Date(); return date.getSeconds() + (date.getMilliseconds()/999); }; -numbers.SecondTens = () => { return Math.floor(new Date().getSeconds()/10); }; -numbers.SecondOnes = () => { return Math.floor(new Date().getSeconds()%10); }; -numbers.WeekDay = () => { return new Date().getDay(); }; -numbers.WeekDayMondayFirst = () => { var day = (new Date().getDay() - 1); if (day < 0) day = 7 + day; return day; }; -numbers.Day = () => { return new Date().getDate(); }; -numbers.DayTens = () => { return Math.floor(new Date().getDate()/10); }; -numbers.DayOnes = () => { return Math.floor(new Date().getDate()%10); }; -numbers.Month = () => { return new Date().getMonth() + 1; }; -numbers.MonthTens = () => { return Math.floor((new Date().getMonth() + 1)/10); }; -numbers.MonthOnes = () => { return Math.floor((new Date().getMonth() + 1)%10); }; -numbers.Pulse = () => { return pulse; }; -numbers.Steps = () => { return Bangle.getHealthStatus ? Bangle.getHealthStatus("day").steps : undefined; }; -numbers.StepsGoal = () => { return settings.stepsgoal || 10000; }; -numbers.Temperature = () => { return temp; }; -numbers.Pressure = () => { return press; }; -numbers.Altitude = () => { return alt; }; -numbers.BatteryPercentage = E.getBattery; -numbers.BatteryVoltage = NRF.getBattery; -numbers.WeatherCode = getWeatherCode; -numbers.WeatherTemperature = () => { return getWeatherTemperature().value; }; - -var multistates = {}; -multistates.Lock = () => { return Bangle.isLocked() ? "on" : "off"; }; -multistates.Charge = () => { return Bangle.isCharging() ? "on" : "off"; }; -multistates.Notifications = () => { return ((require("Storage").readJSON("setting.json", 1) || {}).quiet|0) ? "off" : "vibrate"; }; -multistates.Alarm = () => { return (require('Storage').readJSON('alarm.json',1)||[]).some(alarm=>alarm.on) ? "on" : "off"; }; -multistates.Bluetooth = () => { return NRF.getSecurityStatus().connected ? "on" : "off"; }; -//TODO: Implement peripheral connection status -multistates.BluetoothPeripheral = () => { return NRF.getSecurityStatus().connected ? "on" : "off"; }; -multistates.HRM = () => { return Bangle.isHRMOn ? "on" : "off"; }; -multistates.Barometer = () => { return Bangle.isBarometerOn() ? "on" : "off"; }; -multistates.Compass = () => { return Bangle.isCompassOn() ? "on" : "off"; }; -multistates.GPS = () => { return Bangle.isGPSOn() ? "on" : "off"; }; -multistates.WeatherTemperatureNegative = () => { return getWeatherTemperature().value ? getWeatherTemperature().value : 0 < 0; }; -multistates.WeatherTemperatureUnit = () => { return getWeatherTemperature().unit; }; -multistates.StepsGoal = () => { return (numbers.Steps() >= (settings.stepsgoal || 10000)) ? "on": "off"; }; - -function drawMultiState(graphics, resources, element){ - startPerfLog("drawMultiState"); - //print("drawMultiState", element); - var value = multistates[element.Value](); - //print("drawImage from drawMultiState", element, value); - drawImage(graphics, resources, element, value); - element.lastDrawnValue = value; - endPerfLog("drawMultiState"); -} - -var pulse,alt,temp,press; - - -var requestedDraws = 0; -var isDrawing = false; - -var drawingTime; - -var start; - -function initialDraw(resources, face){ - //print("Free memory", process.memory(false).free); - requestedDraws++; - if (!isDrawing){ - //print(new Date().toISOString(), "Can draw,", requestedDraws, "draws requested so far"); - isDrawing = true; - resetPerfLog(); - requestedDraws = 0; - //print(new Date().toISOString(), "Drawing start"); - startPerfLog("initialDraw"); - //start = Date.now(); - drawingTime = 0; - //print("Precompiled"); - var promise = precompiledJs(watchfaceResources, watchface); - - promise.then(()=>{ - var currentDrawingTime = Date.now(); - if (showWidgets){ - //print("Draw widgets"); - Bangle.drawWidgets(); - g.setColor(g.theme.fg); - g.drawLine(0,24,g.getWidth(),24); + Bangle.printPerfLog = function(){ + let result = ""; + let keys = []; + for (let c in performanceLog.cum){ + keys.push(c); } - lastDrawTime = Date.now() - start; - drawingTime += Date.now() - currentDrawingTime; - //print(new Date().toISOString(), "Drawing done in", lastDrawTime.toFixed(0), "active:", drawingTime.toFixed(0)); - isDrawing=false; - firstDraw=false; - requestRefresh = false; - endPerfLog("initialDraw"); - }).catch((e)=>{ - print("Error during drawing", e); - }); - - if (requestedDraws > 0){ - //print(new Date().toISOString(), "Had deferred drawing left, drawing again"); - requestedDraws = 0; - setTimeout(()=>{initialDraw(resources, face);}, 10); - } - } //else { - //print("queued draw"); - //} -} - -function handleHrm(e){ - if (e.confidence > 70){ - pulse = e.bpm; - if (!redrawEvents || redrawEvents.includes("HRM") && !Bangle.isLocked()){ - //print("Redrawing on HRM"); - initialDraw(watchfaceResources, watchface); - } + keys.sort(); + for (let k of keys){ + print(k, "last:", (performanceLog.last[k] * 1000).toFixed(0), "average:", (performanceLog.cum[k]/performanceLog.count[k]*1000).toFixed(0), "count:", performanceLog.count[k], "total:", (performanceLog.cum[k] * 1000).toFixed(0)); + } + }; } -} -function handlePressure(e){ - alt = e.altitude; - temp = e.temperature; - press = e.pressure; - if (!redrawEvents || redrawEvents.includes("pressure") && !Bangle.isLocked()){ - //print("Redrawing on pressure"); - initialDraw(watchfaceResources, watchface); - } -} + startPerfLog("loadFunctions"); -function handleCharging(e){ - if (!redrawEvents || redrawEvents.includes("charging") && !Bangle.isLocked()){ - //print("Redrawing on charging"); - initialDraw(watchfaceResources, watchface); - } -} + let delayTimeouts = {}; + let timeoutCount = 0; + let delay = function(t) { + return new Promise(function (resolve) { + const i = timeoutCount++; + let timeout = setTimeout(()=>{ + resolve(); + delete delayTimeouts[i]; + }, t); + delayTimeouts[i] = timeout; + //print("Add delay timeout", delayTimeouts); + }); + }; -function getMatchedWaitingTime(time){ - var result = time - (Date.now() % time); - //print("Matched timeout", time, result); - return result; -} - - - -function setMatchedInterval(callable, time, intervalHandler, delay){ - //print("Setting matched interval for", time); - var matchedTime = getMatchedWaitingTime(time + delay); - setTimeout(()=>{ - var interval = setInterval(callable, time); - if (intervalHandler) intervalHandler(interval); - callable(); - }, matchedTime); -} - -var unlockedDrawInterval; -var lockedDrawInterval; - -var lastDrawTime = 0; -var firstDraw = true; - -var lockedRedraw = getByPath(watchface, ["Properties","Redraw","Locked"]) || 60000; -var unlockedRedraw = getByPath(watchface, ["Properties","Redraw","Unlocked"]) || 1000; -var defaultRedraw = getByPath(watchface, ["Properties","Redraw","Default"]) || "Always"; -var redrawEvents = getByPath(watchface, ["Properties","Redraw","Events"]); -var clearOnRedraw = getByPath(watchface, ["Properties","Redraw","Clear"]); -var events = getByPath(watchface, ["Properties","Events"]); - -//print("events", events); -//print("redrawEvents", redrawEvents); - -function handleLock(isLocked, forceRedraw){ - //print("isLocked", Bangle.isLocked()); - if (lockedDrawInterval) clearInterval(lockedDrawInterval); - if (unlockedDrawInterval) clearInterval(unlockedDrawInterval); - if (!isLocked){ - if (forceRedraw || !redrawEvents || (redrawEvents.includes("unlock"))){ - //print("Redrawing on unlock", isLocked); - initialDraw(watchfaceResources, watchface); + let cleanupDelays = function(){ + //print("Cleanup delays", delayTimeouts); + for (let t of delayTimeouts){ + clearTimeout(t); } - setMatchedInterval(()=>{ - //print("Redrawing on unlocked interval"); - initialDraw(watchfaceResources, watchface); - },unlockedRedraw, (v)=>{ - unlockedDrawInterval = v; - }, lastDrawTime); - if (!events || events.includes("HRM")) Bangle.setHRMPower(1, "imageclock"); - if (!events || events.includes("pressure")) Bangle.setBarometerPower(1, 'imageclock'); - } else { - if (forceRedraw || !redrawEvents || (redrawEvents.includes("lock"))){ - //print("Redrawing on lock", isLocked); - initialDraw(watchfaceResources, watchface); + delayTimeouts = {}; + }; + + let prepareImg = function(resource){ + startPerfLog("prepareImg"); + //print("prepareImg: ", resource); + + if (resource.dataOffset !== undefined){ + resource.buffer = E.toArrayBuffer(require("Storage").read("imageclock.resources.data", resource.dataOffset, resource.dataLength)); + delete resource.dataOffset; + delete resource.dataLength; + if (resource.paletteData){ + resource.palette = new Uint16Array(resource.paletteData); + delete resource.paletteData; + } } - setMatchedInterval(()=>{ - //print("Redrawing on locked interval"); - initialDraw(watchfaceResources, watchface); - },lockedRedraw, (v)=>{ - lockedDrawInterval = v; - }, lastDrawTime); - Bangle.setHRMPower(0, "imageclock"); - Bangle.setBarometerPower(0, 'imageclock'); - } -} + endPerfLog("prepareImg"); + return resource; + }; + + let getByPath = function(object, path, lastElem){ + startPerfLog("getByPath"); + //print("getByPath", path,lastElem); + let current = object; + if (path.length) { + for (let c of path){ + if (!current[c]) return undefined; + current = current[c]; + } + } + if (lastElem!==undefined){ + if (!current["" + lastElem]) return undefined; + //print("Found by lastElem", lastElem); + current = current["" + lastElem]; + } + endPerfLog("getByPath"); + if (typeof current == "function"){ + //print("current was function"); + return undefined; + } + return current; + }; + + let splitNumberToDigits = function(num){ + return String(num).split('').map(item => Number(item)); + }; + + let isChangedNumber = function(element){ + return element.lastDrawnValue != getValue(element.Value); + }; + + let isChangedMultistate = function(element){ + return element.lastDrawnValue != getMultistate(element.Value); + }; + + let drawNumber = function(graphics, resources, element){ + startPerfLog("drawNumber"); + let number = getValue(element.Value); + //print("drawNumber: ", number, element); + let spacing = element.Spacing ? element.Spacing : 0; + let unit = element.Unit; + + let imageIndexMinus = element.ImageIndexMinus; + let imageIndexUnit = element.ImageIndexUnit; + let numberOfDigits = element.Digits; -var showWidgets = false; -var showWidgetsChanged = false; -var currentDragDistance = 0; + if (number) number = number.toFixed(0); -Bangle.setUI("clock"); -Bangle.on('drag', (e)=>{ - currentDragDistance += e.dy; - if (Math.abs(currentDragDistance) < 10) return; - dragDown = currentDragDistance > 0; - currentDragDistance = 0; - if (!showWidgets && dragDown){ - //print("Enable widgets"); - if (WIDGETS && typeof WIDGETS === "object") { - for (let w in WIDGETS) { - var wd = WIDGETS[w]; - wd.draw = originalWidgetDraw[w]; - wd.area = originalWidgetArea[w]; + let isNegative; + let digits; + if (number == undefined){ + isNegative = true; + digits = []; + numberOfDigits = 0; + } else { + isNegative = number < 0; + if (isNegative) number *= -1; + digits = splitNumberToDigits(number); + } + + //print("digits: ", digits); + if (!numberOfDigits) numberOfDigits = digits.length; + let firstDigitX = element.X; + let firstDigitY = element.Y; + let imageIndex = element.ImageIndex ? element.ImageIndex : 0; + + let firstImage = element.cachedFirstImage; + if (!firstImage && !element.cachedFirstImageMissing){ + if (imageIndex){ + firstImage = getByPath(resources, [], "" + (0 + imageIndex)); + } else { + firstImage = getByPath(resources, element.ImagePath, 0); + } + element.cachedFirstImage = firstImage; + if (!firstImage) element.cachedFirstImageMissing = true; + } + + let minusImage = element.cachedMinusImage; + if (!minusImage && !element.cachedMinusImageMissing){ + if (imageIndexMinus){ + minusImage = getByPath(resources, [], "" + (0 + imageIndexMinus)); + } else { + minusImage = getByPath(resources, element.ImagePath, "minus"); + } + element.cachedMinusImage = minusImage; + if (!minusImage) element.cachedMinusImageMissing = true; + } + + let unitImage = element.cachedUnitImage; + //print("Get image for unit", imageIndexUnit); + if (!unitImage && !element.cachedUnitImageMissing){ + if (imageIndexUnit !== undefined){ + unitImage = getByPath(resources, [], "" + (0 + imageIndexUnit)); + //print("Unit image is", unitImage); + } else if (element.Unit){ + unitImage = getByPath(resources, element.ImagePath, getMultistate(element.Unit, "unknown")); + } + unitImage = element.cachedUnitImage; + if (!unitImage) element.cachedUnitImageMissing = true; + } + + let numberWidth = (numberOfDigits * firstImage.width) + (Math.max((numberOfDigits - 1),0) * spacing); + if (isNegative && minusImage){ + //print("Adding to width", minusImage); + numberWidth += minusImage.width + spacing; + } + if (unitImage){ + //print("Adding to width", unitImage); + numberWidth += unitImage.width + spacing; + } + //print("numberWidth:", numberWidth); + + if (element.Alignment == "Center") { + firstDigitX = Math.round(element.X - (numberWidth/2)) + 1; + firstDigitY = Math.round(element.Y - (firstImage.height/2)) + 1; + } else if (element.Alignment == "BottomRight"){ + firstDigitX = element.X - numberWidth + 1; + firstDigitY = element.Y - firstImage.height + 1; + } else if (element.Alignment == "TopRight") { + firstDigitX = element.X - numberWidth + 1; + firstDigitY = element.Y; + } else if (element.Alignment == "BottomLeft") { + firstDigitX = element.X; + firstDigitY = element.Y - firstImage.height + 1; + } + + let currentX = firstDigitX; + if (isNegative && minusImage){ + //print("Draw minus at", currentX); + if (imageIndexMinus){ + drawElement(graphics, resources, {X:currentX,Y:firstDigitY}, element, "" + (0 + imageIndexMinus)); + } else { + drawElement(graphics, resources, {X:currentX,Y:firstDigitY}, element, "minus"); + } + currentX += minusImage.width + spacing; + } + for (let d = 0; d < numberOfDigits; d++){ + let currentDigit; + let difference = numberOfDigits - digits.length; + if (d >= difference){ + currentDigit = digits[d-difference]; + } else { + currentDigit = 0; + } + //print("Digit", currentDigit, currentX); + drawElement(graphics, resources, {X:currentX,Y:firstDigitY}, element, currentDigit + imageIndex); + currentX += firstImage.width + spacing; + } + if (imageIndexUnit){ + //print("Draw unit at", currentX); + drawElement(graphics, resources, {X:currentX,Y:firstDigitY}, element, "" + (0 + imageIndexUnit)); + } else if (element.Unit){ + drawElement(graphics, resources, {X:currentX,Y:firstDigitY}, element, getMultistate(element.Unit,"unknown")); + } + element.lastDrawnValue = number; + + endPerfLog("drawNumber"); + }; + + let drawElement = function(graphics, resources, pos, element, lastElem){ + startPerfLog("drawElement"); + let cacheKey = "_"+(lastElem?lastElem:"nole"); + if (!element.cachedImage) element.cachedImage={}; + if (!element.cachedImage[cacheKey]){ + let resource = getByPath(resources, element.ImagePath, lastElem); + if (resource){ + prepareImg(resource); + //print("lastElem", typeof resource) + element.cachedImage[cacheKey] = resource; + } else { + //print("Could not get resource from", element, lastElem); + } + } + + //print("cache ", typeof element.cachedImage[cacheKey], element.ImagePath, lastElem); + if(element.cachedImage[cacheKey]){ + //print("drawElement ", pos, element, lastElem); + let options={}; + if (element.RotationValue){ + options.rotate = radians(element); + } + if (element.Scale){ + options.scale = element.ScaleValue; + } + //print("options", options); + //print("Memory before drawing", process.memory(false)); + startPerfLog("drawElement_g.drawImage"); + try{ + graphics.drawImage(element.cachedImage[cacheKey] ,(pos.X ? pos.X : 0), (pos.Y ? pos.Y : 0), options);} catch (e) { + //print("Error", e, element.cachedImage[cacheKey]); + } + endPerfLog("drawElement_g.drawImage"); + } + endPerfLog("drawElement"); + }; + + let getValue = function(value, defaultValue){ + startPerfLog("getValue"); + if (typeof value == "string"){ + return numbers[value](); + } + if (value == undefined) return defaultValue; + endPerfLog("getValue"); + return value; + }; + + let getMultistate = function(name, defaultValue){ + startPerfLog("getMultistate"); + if (typeof name == "string"){ + return multistates[name](); + } else { + if (name == undefined) return defaultValue; + } + endPerfLog("getMultistate"); + return undefined; + }; + + let drawScale = function(graphics, resources, scale){ + startPerfLog("drawScale"); + //print("drawScale", scale); + let segments = scale.Segments; + let imageIndex = scale.ImageIndex !== undefined ? scale.ImageIndex : 0; + + let value = scaledown(scale.Value, scale.MinValue, scale.MaxValue); + let segmentsToDraw = Math.ceil(value * segments.length); + + for (let i = 0; i < segmentsToDraw; i++){ + drawElement(graphics, resources, segments[i], scale, imageIndex + i); + } + scale.lastDrawnValue = segmentsToDraw; + + endPerfLog("drawScale"); + }; + + let drawImage = function(graphics, resources, image, name){ + startPerfLog("drawImage"); + //print("drawImage", image.X, image.Y, name); + if (image.Value && image.Steps){ + let steps = Math.floor(scaledown(image.Value, image.MinValue, image.MaxValue) * (image.Steps - 1)); + //print("Step", steps, "of", image.Steps); + drawElement(graphics, resources, image, image, "" + steps); + } else if (image.ImageIndex !== undefined) { + drawElement(graphics, resources, image, image, image.ImageIndex); + } else { + drawElement(graphics, resources, image, image, name ? "" + name: undefined); + } + + endPerfLog("drawImage"); + }; + + let drawCodedImage = function(graphics, resources, image){ + startPerfLog("drawCodedImage"); + let code = getValue(image.Value); + //print("drawCodedImage", image, code); + + if (image.ImagePath) { + let factor = 1; + let currentCode = code; + while (code / factor > 1){ + currentCode = Math.floor(currentCode/factor)*factor; + //print("currentCode", currentCode); + if (getByPath(resources, image.ImagePath, currentCode)){ + break; } + factor *= 10; } + if (code / factor > 1){ + //print("found match"); + drawImage(graphics, resources, image, currentCode); + } else { + //print("fallback"); + drawImage(graphics, resources, image, "fallback"); + } + } + image.lastDrawnValue = code; + + startPerfLog("drawCodedImage"); + }; + + let getWeatherCode = function(){ + let jsonWeather = require("Storage").readJSON('weather.json'); + let weather = (jsonWeather && jsonWeather.weather) ? jsonWeather.weather : undefined; + + if (weather && weather.code){ + return weather.code; + } + return undefined; + }; + + let getWeatherTemperature = function(){ + let jsonWeather = require("Storage").readJSON('weather.json'); + let weather = (jsonWeather && jsonWeather.weather) ? jsonWeather.weather : undefined; + + let result = { unit: "unknown"}; + if (weather && weather.temp){ + //print("Weather is", weather); + let temp = require('locale').temp(weather.temp-273.15); + result.value = Number(temp.match(/[\d\-]*/)[0]); + let unit; + if (temp.includes("C")){ + result.unit = "celsius"; + } else if (temp.includes("F")){ + result.unit = "fahrenheit"; + } + } + return result; + }; + + let scaledown = function(value, min, max){ + //print("scaledown", value, min, max); + let scaled = E.clip(getValue(value),getValue(min,0),getValue(max,1)); + scaled -= getValue(min,0); + scaled /= getValue(max,1); + return scaled; + }; + + let radians = function(rotation){ + let value = scaledown(rotation.RotationValue, rotation.MinRotationValue, rotation.MaxRotationValue); + value -= rotation.RotationOffset ? rotation.RotationOffset : 0; + value *= 360; + value *= Math.PI / 180; + return value; + }; + + let drawPoly = function(graphics, resources, element){ + startPerfLog("drawPoly"); + let vertices = []; + + startPerfLog("drawPoly_transform"); + for (let c of element.Vertices){ + vertices.push(c.X); + vertices.push(c.Y); + } + let transform = { x: element.X ? element.X : 0, + y: element.Y ? element.Y : 0 + }; + if (element.RotationValue){ + transform.rotate = radians(element); + } + vertices = graphics.transformVertices(vertices, transform); + + endPerfLog("drawPoly_transform"); + + if (element.Filled){ + startPerfLog("drawPoly_g.fillPoly"); + graphics.fillPoly(vertices,true); + endPerfLog("drawPoly_g.fillPoly"); + } else { + startPerfLog("drawPoly_g.drawPoly"); + graphics.drawPoly(vertices,true); + endPerfLog("drawPoly_g.drawPoly"); + } + + endPerfLog("drawPoly"); + }; + + let drawRect = function(graphics, resources, element){ + startPerfLog("drawRect"); + let vertices = []; + + if (element.Filled){ + startPerfLog("drawRect_g.fillRect"); + graphics.fillRect(element.X, element.Y, element.X + element.Width, element.Y + element.Height); + endPerfLog("drawRect_g.fillRect"); + } else { + startPerfLog("drawRect_g.fillRect"); + graphics.drawRect(element.X, element.Y, element.X + element.Width, element.Y + element.Height); + endPerfLog("drawRect_g.fillRect"); + } + endPerfLog("drawRect"); + }; + + let drawCircle = function(graphics, resources, element){ + startPerfLog("drawCircle"); + + if (element.Filled){ + startPerfLog("drawCircle_g.fillCircle"); + graphics.fillCircle(element.X, element.Y, element.Radius); + endPerfLog("drawCircle_g.fillCircle"); + } else { + startPerfLog("drawCircle_g.drawCircle"); + graphics.drawCircle(element.X, element.Y, element.Radius); + endPerfLog("drawCircle_g.drawCircle"); + } + endPerfLog("drawCircle"); + }; + + let numbers = {}; + numbers.Hour = () => { return new Date().getHours(); }; + numbers.HourTens = () => { return Math.floor(new Date().getHours()/10); }; + numbers.HourOnes = () => { return Math.floor(new Date().getHours()%10); }; + numbers.Hour12 = () => { return new Date().getHours()%12; }; + numbers.Hour12Analog = () => { let date = new Date(); return date.getHours()%12 + (date.getMinutes()/59); }; + numbers.Hour12Tens = () => { return Math.floor((new Date().getHours()%12)/10); }; + numbers.Hour12Ones = () => { return Math.floor((new Date().getHours()%12)%10); }; + numbers.Minute = () => { return new Date().getMinutes(); }; + numbers.MinuteAnalog = () => { let date = new Date(); return date.getMinutes() + (date.getSeconds()/59); }; + numbers.MinuteTens = () => { return Math.floor(new Date().getMinutes()/10); }; + numbers.MinuteOnes = () => { return Math.floor(new Date().getMinutes()%10); }; + numbers.Second = () => { return new Date().getSeconds(); }; + numbers.SecondAnalog = () => { let date = new Date(); return date.getSeconds() + (date.getMilliseconds()/999); }; + numbers.SecondTens = () => { return Math.floor(new Date().getSeconds()/10); }; + numbers.SecondOnes = () => { return Math.floor(new Date().getSeconds()%10); }; + numbers.WeekDay = () => { return new Date().getDay(); }; + numbers.WeekDayMondayFirst = () => { let day = (new Date().getDay() - 1); if (day < 0) day = 7 + day; return day; }; + numbers.Day = () => { return new Date().getDate(); }; + numbers.DayTens = () => { return Math.floor(new Date().getDate()/10); }; + numbers.DayOnes = () => { return Math.floor(new Date().getDate()%10); }; + numbers.Month = () => { return new Date().getMonth() + 1; }; + numbers.MonthTens = () => { return Math.floor((new Date().getMonth() + 1)/10); }; + numbers.MonthOnes = () => { return Math.floor((new Date().getMonth() + 1)%10); }; + numbers.Pulse = () => { return pulse; }; + numbers.Steps = () => { return Bangle.getHealthStatus ? Bangle.getHealthStatus("day").steps : undefined; }; + numbers.StepsGoal = () => { return settings.stepsgoal || 10000; }; + numbers.Temperature = () => { return temp; }; + numbers.Pressure = () => { return press; }; + numbers.Altitude = () => { return alt; }; + numbers.BatteryPercentage = E.getBattery; + numbers.BatteryVoltage = NRF.getBattery; + numbers.WeatherCode = getWeatherCode; + numbers.WeatherTemperature = () => { return getWeatherTemperature().value; }; + + let multistates = {}; + multistates.Lock = () => { return Bangle.isLocked() ? "on" : "off"; }; + multistates.Charge = () => { return Bangle.isCharging() ? "on" : "off"; }; + multistates.Notifications = () => { return ((require("Storage").readJSON("setting.json", 1) || {}).quiet|0) ? "off" : "vibrate"; }; + multistates.Alarm = () => { return (require('Storage').readJSON('alarm.json',1)||[]).some(alarm=>alarm.on) ? "on" : "off"; }; + multistates.Bluetooth = () => { return NRF.getSecurityStatus().connected ? "on" : "off"; }; + //TODO: Implement peripheral connection status + multistates.BluetoothPeripheral = () => { return NRF.getSecurityStatus().connected ? "on" : "off"; }; + multistates.HRM = () => { return Bangle.isHRMOn ? "on" : "off"; }; + multistates.Barometer = () => { return Bangle.isBarometerOn() ? "on" : "off"; }; + multistates.Compass = () => { return Bangle.isCompassOn() ? "on" : "off"; }; + multistates.GPS = () => { return Bangle.isGPSOn() ? "on" : "off"; }; + multistates.WeatherTemperatureNegative = () => { return getWeatherTemperature().value ? getWeatherTemperature().value : 0 < 0; }; + multistates.WeatherTemperatureUnit = () => { return getWeatherTemperature().unit; }; + multistates.StepsGoal = () => { return (numbers.Steps() >= (settings.stepsgoal || 10000)) ? "on": "off"; }; + + let drawMultiState = function(graphics, resources, element){ + startPerfLog("drawMultiState"); + //print("drawMultiState", element); + let value = multistates[element.Value](); + //print("drawImage from drawMultiState", element, value); + drawImage(graphics, resources, element, value); + element.lastDrawnValue = value; + endPerfLog("drawMultiState"); + }; + + let pulse,alt,temp,press; + + + let requestedDraws = 0; + let isDrawing = false; + + let start; + + let deferredTimout; + + let initialDraw = function(resources, face){ + //print("Free memory", process.memory(false).free); + requestedDraws++; + if (!isDrawing){ + cleanupDelays(); + //print(new Date().toISOString(), "Can draw,", requestedDraws, "draws requested so far"); + isDrawing = true; + requestedDraws = 0; + //print(new Date().toISOString(), "Drawing start"); + startPerfLog("initialDraw"); + //print("Precompiled"); + let promise = precompiledJs(watchfaceResources, watchface); + + promise.then(()=>{ + let currentDrawingTime = Date.now(); + if (showWidgets){ + restoreWidgetDraw(); + } + lastDrawTime = Date.now() - start; + isDrawing=false; + firstDraw=false; + requestRefresh = false; + endPerfLog("initialDraw"); + if (!Bangle.uiRemove) setUi(); + }).catch((e)=>{ + print("Error during drawing", e); + }); + + if (requestedDraws > 0){ + //print(new Date().toISOString(), "Had deferred drawing left, drawing again"); + requestedDraws = 0; + //print("Clear deferred timeout", deferredTimout); + clearTimeout(deferredTimeout); + deferredTimout = setTimeout(()=>{initialDraw(resources, face);}, 10); + } + } //else { + //print("queued draw"); + //} + }; + + let handleHrm = function(e){ + if (e.confidence > 70){ + pulse = e.bpm; + if (!redrawEvents || redrawEvents.includes("HRM") && !Bangle.isLocked()){ + //print("Redrawing on HRM"); + initialDraw(watchfaceResources, watchface); + } + } + }; + + let handlePressure = function(e){ + alt = e.altitude; + temp = e.temperature; + press = e.pressure; + if (!redrawEvents || redrawEvents.includes("pressure") && !Bangle.isLocked()){ + //print("Redrawing on pressure"); + initialDraw(watchfaceResources, watchface); + } + }; + + let handleCharging = function(e){ + if (!redrawEvents || redrawEvents.includes("charging") && !Bangle.isLocked()){ + //print("Redrawing on charging"); + initialDraw(watchfaceResources, watchface); + } + }; + + + let getMatchedWaitingTime = function(time){ + let result = time - (Date.now() % time); + //print("Matched wating time", time, result); + return result; + }; + + let setMatchedInterval = function(callable, time, intervalHandler, delay){ + //print("Setting matched interval for", time, intervalHandler); + if (!delay) delay = 0; + let matchedTime = getMatchedWaitingTime(time + delay); + return setTimeout(()=>{ + let interval = setInterval(callable, time); + //print("setMatchedInterval", interval); + if (intervalHandler) intervalHandler(interval); + callable(); + }, matchedTime); + }; + + endPerfLog("loadFunctions"); + + let lastDrawTime = 0; + + startPerfLog("loadProperties"); + let lockedRedraw = getByPath(watchface, ["Properties","Redraw","Locked"]) || 60000; + let unlockedRedraw = getByPath(watchface, ["Properties","Redraw","Unlocked"]) || 1000; + let defaultRedraw = getByPath(watchface, ["Properties","Redraw","Default"]) || "Always"; + let redrawEvents = getByPath(watchface, ["Properties","Redraw","Events"]); + let clearOnRedraw = getByPath(watchface, ["Properties","Redraw","Clear"]); + let events = getByPath(watchface, ["Properties","Events"]); + endPerfLog("loadProperties"); + + //print("events", events); + //print("redrawEvents", redrawEvents); + + let initialDrawTimeoutUnlocked; + let initialDrawTimeoutLocked; + + let handleLock = function(isLocked, forceRedraw){ + //print("isLocked", Bangle.isLocked()); + for (let i of unlockedDrawInterval){ + //print("Clearing unlocked", i); + clearInterval(i); + } + for (let i of lockedDrawInterval){ + //print("Clearing locked", i); + clearInterval(i); + } + unlockedDrawInterval = []; + lockedDrawInterval = []; + + if (!isLocked){ + if (forceRedraw || !redrawEvents || (redrawEvents.includes("unlock"))){ + //print("Redrawing on unlock", isLocked); + initialDraw(watchfaceResources, watchface); + } + if (initialDrawTimeoutUnlocked){ + //print("clear initialDrawTimeUnlocked timet", initialDrawTimeoutUnlocked); + clearTimeout(initialDrawTimeoutUnlocked); + } + initialDrawTimeoutUnlocked = setMatchedInterval(()=>{ + //print("Redrawing on unlocked interval"); + initialDraw(watchfaceResources, watchface); + },unlockedRedraw, (v)=>{ + //print("New matched unlocked interval", v); + unlockedDrawInterval.push(v); + }, lastDrawTime); + if (!events || events.includes("HRM")) Bangle.setHRMPower(1, "imageclock"); + if (!events || events.includes("pressure")) Bangle.setBarometerPower(1, 'imageclock'); + } else { + if (forceRedraw || !redrawEvents || (redrawEvents.includes("lock"))){ + //print("Redrawing on lock", isLocked); + initialDraw(watchfaceResources, watchface); + } + if (initialDrawTimeoutLocked){ + clearTimeout(initialDrawTimeoutLocked); + //print("clear initialDrawTimeLocked timet", initialDrawTimeoutLocked); + } + initialDrawTimeoutLocked = setMatchedInterval(()=>{ + //print("Redrawing on locked interval"); + initialDraw(watchfaceResources, watchface); + },lockedRedraw, (v)=>{ + //print("New matched locked interval", v); + lockedDrawInterval.push(v); + }, lastDrawTime); + Bangle.setHRMPower(0, "imageclock"); + Bangle.setBarometerPower(0, 'imageclock'); + } + }; + + + let showWidgetsChanged = false; + + let restoreWidgetDraw = function(){ + require("widget_utils").show(); + Bangle.drawWidgets(); + }; + + let handleSwipe = function(lr, ud){ + if (!showWidgets && ud == 1){ + //print("Enable widgets"); + restoreWidgetDraw(); showWidgetsChanged = true; } - if (showWidgets && !dragDown){ + if (showWidgets && ud == -1){ //print("Disable widgets"); clearWidgetsDraw(); firstDraw = true; @@ -723,52 +774,86 @@ Bangle.on('drag', (e)=>{ if (showWidgetsChanged){ showWidgetsChanged = false; //print("Draw after widget change"); - showWidgets = dragDown; + showWidgets = ud == 1; initialDraw(); } - } -); + }; -if (!events || events.includes("pressure")){ - Bangle.on('pressure', handlePressure); - try{ - Bangle.setBarometerPower(1, 'imageclock'); - } catch (e){ - print("Error during barometer power up", e); - } -} -if (!events || events.includes("HRM")) { - Bangle.on('HRM', handleHrm); - Bangle.setHRMPower(1, "imageclock"); -} -if (!events || events.includes("lock")) { - Bangle.on('lock', handleLock); -} -if (!events || events.includes("charging")) { - Bangle.on('charging', handleCharging); -} + Bangle.on('swipe', handleSwipe); -var originalWidgetDraw = {}; -var originalWidgetArea = {}; - -function clearWidgetsDraw(){ - //print("Clear widget draw calls"); - if (WIDGETS && typeof WIDGETS === "object") { - originalWidgetDraw = {}; - originalWidgetArea = {}; - for (let w in WIDGETS) { - var wd = WIDGETS[w]; - originalWidgetDraw[w] = wd.draw; - originalWidgetArea[w] = wd.area; - wd.draw = () => {}; - wd.area = ""; + if (!events || events.includes("pressure")){ + Bangle.on('pressure', handlePressure); + try{ + Bangle.setBarometerPower(1, 'imageclock'); + } catch (e){ + //print("Error during barometer power up", e); } } -} + if (!events || events.includes("HRM")) { + Bangle.on('HRM', handleHrm); + Bangle.setHRMPower(1, "imageclock"); + } + if (!events || events.includes("lock")) { + Bangle.on('lock', handleLock); + } + if (!events || events.includes("charging")) { + Bangle.on('charging', handleCharging); + } + + let originalWidgetDraw = {}; + let originalWidgetArea = {}; + + let clearWidgetsDraw = function(){ + //print("Clear widget draw calls"); + require("widget_utils").hide(); + } + + handleLock(Bangle.isLocked(), true); + + let setUi = function(){ + Bangle.setUI({ + mode : "clock", + remove : function() { + //print("remove calls"); + // Called to unload all of the clock app + Bangle.setHRMPower(0, "imageclock"); + Bangle.setBarometerPower(0, 'imageclock'); + + Bangle.removeListener('swipe', handleSwipe); + Bangle.removeListener('lock', handleLock); + Bangle.removeListener('charging', handleCharging); + Bangle.removeListener('HRM', handleHrm); + Bangle.removeListener('pressure', handlePressure); + + if (deferredTimout) clearTimeout(deferredTimout); + if (initialDrawTimeoutUnlocked) clearTimeout(initialDrawTimeoutUnlocked); + if (initialDrawTimeoutLocked) clearTimeout(initialDrawTimeoutLocked); + + for (let i of global.unlockedDrawInterval){ + //print("Clearing unlocked", i); + clearInterval(i); + } + delete global.unlockedDrawInterval; + for (let i of global.lockedDrawInterval){ + //print("Clearing locked", i); + clearInterval(i); + } + delete global.lockedDrawInterval; + delete global.showWidgets; + delete global.firstDraw; + + delete Bangle.printPerfLog; + if (settings.perflog){ + delete Bangle.resetPerfLog; + delete performanceLog; + } + + cleanupDelays(); + restoreWidgetDraw(); + } + }); + } -setTimeout(()=>{ Bangle.loadWidgets(); clearWidgetsDraw(); -}, 0); - -handleLock(Bangle.isLocked()); +} diff --git a/apps/imageclock/custom.html b/apps/imageclock/custom.html index af7a1835f..8bf7a7bd0 100644 --- a/apps/imageclock/custom.html +++ b/apps/imageclock/custom.html @@ -23,6 +23,8 @@ Options:

+ +

@@ -579,11 +581,8 @@ return result; } - function convertToCode(elements, properties, wrapInTimeouts){ + function convertToCode(elements, properties, wrapInTimeouts, forceUseOrigPlane){ var code = "(function (wr, wf) {\n"; - if (!wrapInTimeouts){ - code += "var ct=Date.now();\n"; - } code += "var lc;\n"; code += "var p = Promise.resolve();\n"; @@ -595,7 +594,7 @@ var c = elements[i].value; console.log("Check element", c); var name = c.Layer; - var plane = wrapInTimeouts ? 1 : 0; + var plane = (wrapInTimeouts && !forceUseOrigPlane) ? 1 : 0; if (typeof c.Plane == "number"){ plane = c.Plane; } @@ -610,8 +609,6 @@ console.log("Found planes", planes, "with numbers", planeNumbers) - if (wrapInTimeouts && planes == 0) planes = 1; - code += "p0 = g;\n"; for (var planeIndex = 0; planeIndex < planeNumbers.length; planeIndex++){ @@ -624,32 +621,25 @@ if (plane != 0) code += "if (!p" + plane + ") p" + plane + " = Graphics.createArrayBuffer(g.getWidth(),g.getHeight(),4,{msb:true});\n"; if (properties.Redraw && properties.Redraw.Clear){ - if (wrapInTimeouts && plane != 0){ + if (wrapInTimeouts && (plane != 0 || forceUseOrigPlane)){ code += "p = p.then(()=>delay(0)).then(()=>{\n"; } else { code += "p = p.then(()=>{\n"; } - code += "var ct=Date.now();\n" if (addDebug()) code += 'print("Clear for redraw of plane ' + p + '");'+"\n"; code += 'startPerfLog("initialDraw_g.clear");'+"\n"; code += "p" + plane + ".clear(true);\n"; code += 'endPerfLog("initialDraw_g.clear");'+ "\n"; - - code += "drawingTime += Date.now() - ct;\n"; code += "});\n"; } var previousPlane = plane + 1; if (previousPlane < planeNumbers.length){ code += "p = p.then(()=>{\n"; - code += "var ct=Date.now();\n"; if (addDebug()) code += 'print("Copying of plane ' + previousPlane + ' to display");'+"\n"; //code += "g.drawImage(p" + i + ".asImage());"; code += "p0.drawImage({width: p" + previousPlane + ".getWidth(), height: p" + previousPlane + ".getHeight(), bpp: p" + previousPlane + ".getBPP(), buffer: p" + previousPlane + ".buffer, palette: palette});\n"; - - - code += "drawingTime += Date.now() - ct;\n"; code += "});\n"; } @@ -660,12 +650,6 @@ console.log("Layer elements", layername, layerElements); //code for whole layer - if (wrapInTimeouts && plane != 0){ - code += "p = p.then(()=>delay(0)).then(()=>{\n"; - } else { - code += "p = p.then(()=>{\n"; - } - code += "var ct=Date.now();\n"; if (addDebug()) code += 'print("Starting layer ' + layername + '");' + "\n"; var checkForLayerChange = false; @@ -730,16 +714,19 @@ } if (addDebug()) code += 'print("Element condition is ' + condition + '");' + "\n"; - code += "" + colorsetting; code += (condition.length > 0 ? "if (" + condition + "){\n" : ""); + if (wrapInTimeouts && (plane != 0 || forceUseOrigPlane)){ + code += "p = p.then(()=>delay(0)).then(()=>{\n"; + } else { + code += "p = p.then(()=>{\n"; + } + code += "" + colorsetting; if (addDebug()) code += 'print("Drawing element ' + elementIndex + ' with type ' + c.type + ' on plane ' + planeName + '");' + "\n"; code += "draw" + c.type + "(" + planeName + ", wr, wf.Collapsed[" + elementIndex + "].value);\n"; + code += "});\n"; code += (condition.length > 0 ? "}\n" : ""); } - - code += "drawingTime += Date.now() - ct;\n"; - code += "});\n"; } console.log("Current plane is", plane); @@ -759,7 +746,7 @@ var properties = faceJson.Properties; faceJson = { Properties: properties, Collapsed: collapseTree(faceJson,{X:0,Y:0})}; console.log("After collapsing", faceJson); - precompiledJs = convertToCode(faceJson.Collapsed, properties, document.getElementById('timeoutwrap').checked); + precompiledJs = convertToCode(faceJson.Collapsed, properties, document.getElementById('timeoutwrap').checked, document.getElementById('forceOrigPlane').checked); console.log("After precompiling", precompiledJs); } @@ -1011,6 +998,10 @@ } } + document.getElementById("timeoutwrap").addEventListener("click", function() { + document.getElementById("forceOrigPlane").disabled = !document.getElementById("timeoutwrap").checked; + }); + document.getElementById("btnSave").addEventListener("click", function() { var h = document.createElement('a'); h.href = 'data:text/json;charset=utf-8,' + encodeURI(JSON.stringify(resultJson)); diff --git a/apps/imageclock/metadata.json b/apps/imageclock/metadata.json index c3ece0184..51257b435 100644 --- a/apps/imageclock/metadata.json +++ b/apps/imageclock/metadata.json @@ -2,7 +2,7 @@ "id": "imageclock", "name": "Imageclock", "shortName": "Imageclock", - "version": "0.08", + "version": "0.12", "type": "clock", "description": "BETA!!! File formats still subject to change --- This app is a highly customizable watchface. To use it, you need to select a watchface. You can build the watchfaces yourself without programming anything. All you need to do is write some json and create image files.", "icon": "app.png", diff --git a/apps/infoclk/ChangeLog b/apps/infoclk/ChangeLog new file mode 100644 index 000000000..4744f872a --- /dev/null +++ b/apps/infoclk/ChangeLog @@ -0,0 +1,3 @@ +0.01: New app! +0.02-0.07: Bug fixes +0.08: Submitted to the app loader \ No newline at end of file diff --git a/apps/infoclk/README.md b/apps/infoclk/README.md new file mode 100644 index 000000000..1dd563bec --- /dev/null +++ b/apps/infoclk/README.md @@ -0,0 +1,33 @@ +# Informational clock + +A configurable clock with extra info and shortcuts when unlocked, but large time when locked + +## Information + +The clock has two different screen arrangements, depending on whether the watch is locked or unlocked. The most commonly viewed piece of information is the time, so when the watch is locked it optimizes for the time being visible at a glance without the backlight. The hours and minutes take up nearly the entire top half of the display, with the date and seconds taking up nearly the entire bottom half. The day progress bar is between them if enabled, unless configured to be on the bottom row. The bottom row can be configured to display a weather summary, step count, step count and heart rate, the daily progress bar, or nothing. + +When the watch is unlocked, it can be assumed that the backlight is on and the user is actively looking at the watch, so instead we can optimize for information density. The bottom half of the display becomes shortcuts, and the top half of the display becomes 4 rows of information (date and time, step count and heart rate, 2 line weather summary) + an optional daily progress bar. (The daily progress bar can be independently enabled when locked and unlocked.) + +Most things are self-explanatory, but the day progress bar might not be. The day progress bar is intended to show approximately how far through the day you are, in the form of a progress bar. You might want to configure it to show how far you are through your waking hours, or you might want to use it to show how far you are through your work or school day. + +## Shortcuts + +There are generally a few apps that the user uses far more frequently than the others. For example, they might use a timer, alarm clock, and calculator every day, while everything else (such as the settings app) gets used only occasionally. This clock has space for 8 apps in the bottom half of the screen only one tap away, avoiding the need to wait for the launcher to open and then scroll through it. Tapping the top of the watch opens the launcher, eliminating the need for the button (which still opens the launcher due to bangle.js conventions). There is also handling for left, right, and vertical swipes. A vertical swipe by default opens the messages app, mimicking mobile operating systems which use a swipe down to view the notification shade. + +## Configurability + +Displaying the seconds allows for more precise timing, but waking up the CPU to refresh the display more often consumes battery. The user can enable or disable them completely, but can also configure them to be enabled or disabled automatically based on some hueristics: + +* They can be hidden while the display is locked, if the user expects to unlock their watch when they need the seconds. +* They can be hidden when the battery is too low, to make the last portion of the battery last a little bit longer. +* They can be hidden during a period of time such as when the user is asleep and therefore unlikely to need very much precision. + +The date format can be changed. + +As described earlier, the contents of the bottom row when locked can be changed. + +The 8 tap-based shortcuts on the bottom and the 3 swipe-based shortcuts can be changed to nothing, the launcher, or any app on the watch. + +The start and end time of the day progress bar can be changed. It can be enabled or disabled separately when the watch is locked and unlocked. The color can be changed. The time when it resets from full to empty can be changed. + +When the battery is below a defined point, the watch's color can change to another chosen color to help the user notice that the battery is low. \ No newline at end of file diff --git a/apps/infoclk/app.js b/apps/infoclk/app.js new file mode 100644 index 000000000..3d51191df --- /dev/null +++ b/apps/infoclk/app.js @@ -0,0 +1,405 @@ +const SETTINGS_FILE = "infoclk.json"; +const FONT = require('infoclk-font.js'); + +const storage = require("Storage"); +const locale = require("locale"); +const weather = require('weather'); + +let config = Object.assign({ + seconds: { + // Displaying the seconds can reduce battery life because the CPU must wake up more often to update the display. + // The seconds will be shown unless one of these conditions is enabled here, and currently true. + hideLocked: false, // Hide the seconds when the display is locked. + hideBattery: 20, // Hide the seconds when the battery is at or below a defined percentage. + hideTime: true, // Hide the seconds when between a certain period of time. Useful for when you are sleeping and don't need the seconds + hideStart: 2200, // The time when the seconds are hidden: first 2 digits are hours on a 24 hour clock, last 2 are minutes + hideEnd: 700, // The time when the seconds are shown again + hideAlways: false, // Always hide (never show) the seconds + }, + + date: { + // Settings related to the display of the date + mmdd: true, // If true, display the month first. If false, display the date first. + separator: '-', // The character that goes between the month and date + monthName: false, // If false, display the month as a number. If true, display the name. + monthFullName: false, // If displaying the name: If false, display an abbreviation. If true, display a full name. + dayFullName: false, // If false, display the day of the week's abbreviation. If true, display the full name. + }, + + bottomLocked: { + display: 'weather' // What to display in the bottom row when locked: + // 'weather': The current temperature and weather description + // 'steps': Step count + // 'health': Step count and bpm + // 'progress': Day progress bar + // false: Nothing + }, + + shortcuts: [ + //8 shortcuts, displayed in the bottom half of the screen (2 rows of 4 shortcuts) when unlocked + // false = no shortcut + // '#LAUNCHER' = open the launcher + // any other string = name of app to open + 'stlap', 'keytimer', 'pomoplus', 'alarm', + 'rpnsci', 'calendar', 'torch', 'weather' + ], + + swipe: { + // 3 shortcuts to launch upon swiping: + // false = no shortcut + // '#LAUNCHER' = open the launcher + // any other string = name of app to open + up: 'messages', // Swipe up or swipe down, due to limitation of event handler + left: '#LAUNCHER', + right: '#LAUNCHER', + }, + + dayProgress: { + // A progress bar representing how far through the day you are + enabledLocked: true, // Whether this bar is enabled when the watch is locked + enabledUnlocked: false, // Whether the bar is enabled when the watch is unlocked + color: [0, 0, 1], // The color of the bar + start: 700, // The time of day that the bar starts filling + end: 2200, // The time of day that the bar becomes full + reset: 300 // The time of day when the progress bar resets from full to empty + }, + + lowBattColor: { + // The text can change color to indicate that the battery is low + level: 20, // The percentage where this happens + color: [1, 0, 0] // The color that the text changes to + } +}, storage.readJSON(SETTINGS_FILE)); + +// Return whether the given time (as a date object) is between start and end (as a number where the first 2 digits are hours on a 24 hour clock and the last 2 are minutes), with end time wrapping to next day if necessary +function timeInRange(start, time, end) { + + // Convert the given date object to a time number + let timeNumber = time.getHours() * 100 + time.getMinutes(); + + // Normalize to prevent the numbers from wrapping around at midnight + if (end <= start) { + end += 2400; + if (timeNumber < start) timeNumber += 2400; + } + + return start <= timeNumber && timeNumber <= end; +} + +// Return whether settings should be displayed based on the user's configuration +function shouldDisplaySeconds(now) { + return !( + (config.seconds.hideAlways) || + (config.seconds.hideLocked && Bangle.isLocked()) || + (E.getBattery() <= config.seconds.hideBattery) || + (config.seconds.hideTime && timeInRange(config.seconds.hideStart, now, config.seconds.hideEnd)) + ); +} + +// Determine the font size needed to fit a string of the given length widthin maxWidth number of pixels, clamped between minSize and maxSize +function getFontSize(length, maxWidth, minSize, maxSize) { + let size = Math.floor(maxWidth / length); //Number of pixels of width available to character + size *= (20 / 12); //Convert to height, assuming 20 pixels of height for every 12 of width + + // Clamp to within range + if (size < minSize) return minSize; + else if (size > maxSize) return maxSize; + else return Math.floor(size); +} + +// Get the current day of the week according to user settings +function getDayString(now) { + if (config.date.dayFullName) return ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][now.getDay()]; + else return ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][now.getDay()]; +} + +// Pad a number with zeros to be the given number of digits +function pad(number, digits) { + let result = '' + number; + while (result.length < digits) result = '0' + result; + return result; +} + +// Get the current date formatted according to the user settings +function getDateString(now) { + let month; + if (!config.date.monthName) month = pad(now.getMonth() + 1, 2); + else if (config.date.monthFullName) month = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'][now.getMonth()]; + else month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][now.getMonth()]; + + if (config.date.mmdd) return `${month}${config.date.separator}${pad(now.getDate(), 2)}`; + else return `${pad(now.getDate(), 2)}${config.date.separator}${month}`; +} + +// Get a floating point number from 0 to 1 representing how far between the user-defined start and end points we are +function getDayProgress(now) { + let start = config.dayProgress.start; + let current = now.getHours() * 100 + now.getMinutes(); + let end = config.dayProgress.end; + let reset = config.dayProgress.reset; + + // Normalize + if (end <= start) end += 2400; + if (current < start) current += 2400; + if (reset < start) reset += 2400; + + // Convert an hhmm number into a floating-point hours + function toDecimalHours(time) { + let hours = Math.floor(time / 100); + let minutes = time % 100; + + return hours + (minutes / 60); + } + + start = toDecimalHours(start); + current = toDecimalHours(current); + end = toDecimalHours(end); + reset = toDecimalHours(reset); + + let progress = (current - start) / (end - start); + + if (progress < 0 || progress > 1) { + if (current < reset) return 1; + else return 0; + } else { + return progress; + } +} + +// Get a Gadgetbridge weather string +function getWeatherString() { + let current = weather.get(); + if (current) return locale.temp(current.temp - 273.15) + ', ' + current.txt; + else return 'Weather unknown!'; +} + +// Get a second weather row showing humidity, wind speed, and wind direction +function getWeatherRow2() { + let current = weather.get(); + if (current) return `${current.hum}%, ${locale.speed(current.wind)} ${current.wrose}`; + else return 'Check Gadgetbridge'; +} + +// Get a step string +function getStepsString() { + return '' + Bangle.getHealthStatus('day').steps + ' steps'; +} + +// Get a health string including daily steps and recent bpm +function getHealthString() { + return `${Bangle.getHealthStatus('day').steps} steps ${Bangle.getHealthStatus('last').bpm} bpm`; +} + +// Set the next timeout to draw the screen +let drawTimeout; +function setNextDrawTimeout() { + if (drawTimeout) { + clearTimeout(drawTimeout); + drawTimeout = undefined; + } + + let time; + let now = new Date(); + if (shouldDisplaySeconds(now)) time = 1000 - (now.getTime() % 1000); + else time = 60000 - (now.getTime() % 60000); + + drawTimeout = setTimeout(draw, time); +} + + +const DIGIT_WIDTH = 40; // How much width is allocated for each digit, 37 pixels + 3 pixels of space (which will go off of the screen on the right edge) +const COLON_WIDTH = 19; // How much width is allocated for the colon, 16 pixels + 3 pixels of space +const HHMM_TOP = 27; // 24 pixels for widgets + 3 pixels of space +const DIGIT_HEIGHT = 64; // How tall the digits are + +const SECONDS_TOP = HHMM_TOP + DIGIT_HEIGHT + 3; // The top edge of the seconds, top of hours and minutes + digit height + space +const SECONDS_LEFT = 2 * DIGIT_WIDTH + COLON_WIDTH; // The left edge of the seconds: displayed after 2 digits and the colon +const DATE_LETTER_HEIGHT = DIGIT_HEIGHT / 2; // Each letter of the day of week and date will be half the height of the time digits + +const DATE_CENTER_X = SECONDS_LEFT / 2; // Day of week and date will be centered between left edge of screen and where seconds start +const DOW_CENTER_Y = SECONDS_TOP + (DATE_LETTER_HEIGHT / 2); // Day of week will be the top row +const DATE_CENTER_Y = DOW_CENTER_Y + DATE_LETTER_HEIGHT; // Date will be the bottom row +const DOW_DATE_CENTER_Y = SECONDS_TOP + (DIGIT_HEIGHT / 2); // When displaying both on one row, center it +const BOTTOM_CENTER_Y = ((SECONDS_TOP + DIGIT_HEIGHT + 3) + g.getHeight()) / 2; + +// Draw the clock +function draw() { + //Prepare to draw + g.reset() + .setFontAlign(0, 0); + + if (E.getBattery() <= config.lowBattColor.level) { + let color = config.lowBattColor.color; + g.setColor(color[0], color[1], color[2]); + } + now = new Date(); + + if (Bangle.isLocked()) { //When the watch is locked + g.clearRect(0, 24, g.getWidth(), g.getHeight()); + + //Draw the hours and minutes + let x = 0; + + for (let digit of locale.time(now, 1)) { //apparently this is how you get an hh:mm time string adjusting for the user's 12/24 hour preference + if (digit != ' ') g.drawImage(FONT[digit], x, HHMM_TOP); + if (digit == ':') x += COLON_WIDTH; + else x += DIGIT_WIDTH; + } + if (storage.readJSON('setting.json')['12hour']) g.drawImage(FONT[(now.getHours() < 12) ? 'am' : 'pm'], 0, HHMM_TOP); + + //Draw the seconds if necessary + if (shouldDisplaySeconds(now)) { + let tens = Math.floor(now.getSeconds() / 10); + let ones = now.getSeconds() % 10; + g.drawImage(FONT[tens], SECONDS_LEFT, SECONDS_TOP) + .drawImage(FONT[ones], SECONDS_LEFT + DIGIT_WIDTH, SECONDS_TOP); + + // Draw the day of week and date assuming the seconds are displayed + + g.setFont('Vector', getFontSize(getDayString(now).length, SECONDS_LEFT, 6, DATE_LETTER_HEIGHT)) + .drawString(getDayString(now), DATE_CENTER_X, DOW_CENTER_Y) + .setFont('Vector', getFontSize(getDateString(now).length, SECONDS_LEFT, 6, DATE_LETTER_HEIGHT)) + .drawString(getDateString(now), DATE_CENTER_X, DATE_CENTER_Y); + + } else { + //Draw the day of week and date without the seconds + + let string = getDayString(now) + ' ' + getDateString(now); + g.setFont('Vector', getFontSize(string.length, g.getWidth(), 6, DATE_LETTER_HEIGHT)) + .drawString(string, g.getWidth() / 2, DOW_DATE_CENTER_Y); + } + + // Draw the bottom area + if (config.bottomLocked.display == 'progress') { + let color = config.dayProgress.color; + g.setColor(color[0], color[1], color[2]) + .fillRect(0, SECONDS_TOP + DIGIT_HEIGHT + 3, g.getWidth() * getDayProgress(now), g.getHeight()); + } else { + let bottomString; + + if (config.bottomLocked.display == 'weather') bottomString = getWeatherString(); + else if (config.bottomLocked.display == 'steps') bottomString = getStepsString(); + else if (config.bottomLocked.display == 'health') bottomString = getHealthString(); + else bottomString = ' '; + + g.setFont('Vector', getFontSize(bottomString.length, 176, 6, g.getHeight() - (SECONDS_TOP + DIGIT_HEIGHT + 3))) + .drawString(bottomString, g.getWidth() / 2, BOTTOM_CENTER_Y); + } + + // Draw the day progress bar between the rows if necessary + if (config.dayProgress.enabledLocked && config.bottomLocked.display != 'progress') { + let color = config.dayProgress.color; + g.setColor(color[0], color[1], color[2]) + .fillRect(0, HHMM_TOP + DIGIT_HEIGHT, g.getWidth() * getDayProgress(now), SECONDS_TOP); + } + } else { + + //If the watch is unlocked + g.clearRect(0, 24, g.getWidth(), g.getHeight() / 2); + rows = [ + `${getDayString(now)} ${getDateString(now)} ${locale.time(now, 1)}`, + getHealthString(), + getWeatherString(), + getWeatherRow2() + ]; + if (shouldDisplaySeconds(now)) rows[0] += ':' + pad(now.getSeconds(), 2); + if (storage.readJSON('setting.json')['12hour']) rows[0] += ((now.getHours() < 12) ? ' AM' : ' PM'); + + let maxHeight = ((g.getHeight() / 2) - HHMM_TOP) / (config.dayProgress.enabledUnlocked ? (rows.length + 1) : rows.length); + + let y = HHMM_TOP + maxHeight / 2; + for (let row of rows) { + let size = getFontSize(row.length, g.getWidth(), 6, maxHeight); + g.setFont('Vector', size) + .drawString(row, g.getWidth() / 2, y); + y += maxHeight; + } + + if (config.dayProgress.enabledUnlocked) { + let color = config.dayProgress.color; + g.setColor(color[0], color[1], color[2]) + .fillRect(0, y - maxHeight / 2, 176 * getDayProgress(now), y + maxHeight / 2); + } + } + + setNextDrawTimeout(); +} + +// Draw the icons. This is done separately from the main draw routine to avoid having to scale and draw a bunch of images repeatedly. +function drawIcons() { + g.reset().clearRect(0, 24, g.getWidth(), g.getHeight()); + for (let i = 0; i < 8; i++) { + let x = [0, 44, 88, 132, 0, 44, 88, 132][i]; + let y = [88, 88, 88, 88, 132, 132, 132, 132][i]; + let appId = config.shortcuts[i]; + let appInfo = storage.readJSON(appId + '.info', 1); + if (!appInfo) continue; + icon = storage.read(appInfo.icon); + g.drawImage(icon, x, y, { + scale: 0.916666666667 + }); + } +} + +weather.on("update", draw); +Bangle.on("step", draw); +Bangle.on('lock', locked => { + //If the watch is unlocked, draw the icons + if (!locked) drawIcons(); + draw(); +}); + +// Show launcher when middle button pressed +Bangle.setUI("clock"); +// Load widgets +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +// Launch an app given the current ID. Handles special cases: +// false: Do nothing +// '#LAUNCHER': Open the launcher +// nonexistent app: Do nothing +function launch(appId) { + if (appId == false) return; + else if (appId == '#LAUNCHER') { + Bangle.buzz(); + Bangle.showLauncher(); + } else { + let appInfo = storage.readJSON(appId + '.info', 1); + if (appInfo) { + Bangle.buzz(); + load(appInfo.src); + } + } +} + +//Set up touch to launch the selected app +Bangle.on('touch', function (button, xy) { + let x = Math.floor(xy.x / 44); + if (x < 0) x = 0; + else if (x > 3) x = 3; + + let y = Math.floor(xy.y / 44); + if (y < 0) y = -1; + else if (y > 3) y = 1; + else y -= 2; + + if (y < 0) { + Bangle.buzz(); + Bangle.showLauncher(); + } else { + let i = 4 * y + x; + launch(config.shortcuts[i]); + } +}); + +//Set up swipe handler +Bangle.on('swipe', function (direction) { + if (direction == -1) launch(config.swipe.left); + else if (direction == 0) launch(config.swipe.up); + else launch(config.swipe.right); +}); + +if (!Bangle.isLocked()) drawIcons(); + +draw(); \ No newline at end of file diff --git a/apps/infoclk/font.js b/apps/infoclk/font.js new file mode 100644 index 000000000..6063958e7 --- /dev/null +++ b/apps/infoclk/font.js @@ -0,0 +1,23 @@ +const heatshrink = require("heatshrink") + +function decompress(string) { + return heatshrink.decompress(atob(string)) +} + +exports = { + '0': decompress("ktAwIEB////EAj4EB+EDAYP/8E/AgWDAYX+CIX/+IDC//PBoYIDAAvwgEHAgOAgAnB/kAgIvCgEPAgJCBv5CCHwXAI4X+KAYk/En4kmAA4qBAAP7BAePAYX4BofBAYX8F4Q+BEwRHBIQI5BA"), + '1': decompress("ktAwIGDj/4AgX/4ADBg/+BAU/+ADBgP/wAEBh/8BoV/8ADBgf/En4k/En4k/EgQ="), + '2': decompress("ktAwMA/4AB/EHAgXwn4EC8IDC/+PAYX+v4EC+YND74NDBAYAE4A0Bg/+HIU/+ADBgP/wAEBh/8BoV/8ADBgf/BAUf/AECElQdBPA2HAYX8OYfHBAYRD8Z3Dj6TG/kPPYZm4EiwAHO4f7BAfPfI/xBoaTEPAfgQwY"), + '3': decompress("ktAwMA/4AB/EHAgXwn4EC8IDC/+PAYX+v4EC+YND74NDBAYAE4A0Bg/+HIU/+ADBgP/wAEBh/8BoV/8ADBgf/BAUf/AECElJWIAEpu/EhpgS34DC/IID54DC/l/AgXDAYX4j57DA"), + '4': decompress("ktAwMA//AgEf//+BYP///wgEHAgOAgE///8gEBBAPggEPAgIWBv///EAgYIBEn4kXABf9AgfnAgY4BAAP4BAfDAYX+EwfwIQRRCJIJRBJIRRBJIQICj5RBJIRRBJIJRCNwJRBNwQk/Ei4A=="), + '5': decompress("ktAwIEB/4AB/EfAgXDAYX+n4EC+YDC/+fAYX9BAfvAgYAJ+AwBgP/wAEBh/8H4V/8ADBgf/BAUf/AEC//AAYMH/wICn4kpPYUPAgXgv4EC4JfDg4DC/iFD8ANDwaTDCQfwEoZ2/EhrXNAAm/AYX5BAfPQoaTD4ahDj57DA=="), + '6': decompress("ktAwIEB/4AB/EfAgXDAYX+n4EC+YDC/+fAYX9BAfvAgYAJ+AwBgP/wAEBh/8H4V/8ADBgf/BAUf/AEC//AAYMH/wICn4kpPYUPAgXgv6AG/6JD/gID84ED358NJIIsCKIQ0BKIRJCFgJJCSYcHAgJuBXYJuBKIQkpAA58D/YIDx6PDBofBQoYvCHwImCI4KUCwA="), + '7': decompress("ktAwMA/4AB/EHAgXwn4EC8IDC/+PAYX+v4EC+YND74NDBAYAE4A0Bg/+HIU/+ADBgP/wAEBh/8BoV/8ADBgf/BAUf/AECEn4k/En4kVA"), + '8': decompress("ktAwIEB////EAj4EB+EDAYP/8E/AgWDAYX+CIX/+IDC//PBoYIDAAvwgEHAgOAgAnB/kAgIvCgEPAgJCBv5CCHwXAI4X+KAYkpAFpu/EhwAHFQIAB/YIDx4DC/AND4IDC/ieD4AmCI4JCBHIIA=="), + '9': decompress("ktAwIEB////EAj4EB+EDAYP/8E/AgWDAYX+CIX/+IDC//PBoYIDAAvwgEHAgOAgAnB/kAgIvCgEPAgJCBv5CCHwXAI4X+KAYkpABf9AgfnAgaFD/AID4Z8DEwfwIQRRCJIJRBJIRRBJIQICj5RBJIRRBJIJRCNwJRBNwQkoPhoAE34DC/L0H/iwBQAv4WAJ7CA=="), + + ':': decompress("iFAwITQg/gj/4n/8v/+AIP/ABQPDCoIZBDoJTfH94A=="), + + 'am': decompress("jFAwIEBngCEvwCH/4CFwEBAQkD//AgfnAQcH4fgAQsPwPwAQf/+Ef//4AQn8n0AvgCCHQN+vkAnwCC/EAj4CF+EAh4CCNIoLFC4v8gE/AQv+gF/AQpwB/4CDwICG+/D94CD8/v+fn54CC+P/x4CF+H/IgICFvwCEngCD"), + 'pm': decompress("jFAwMAn///l///+/4AE+EAh4CaEYoABFgX8BwMAAUwAFIIv4gEfAQX8OYICF/0Av4CF/8AKQICCwICG+/D94CD8/v+fn54CC+P/x4CF+H/IgICFvwCEngCDA") +} \ No newline at end of file diff --git a/apps/infoclk/font/am.png b/apps/infoclk/font/am.png new file mode 100644 index 000000000..a76ad25fc Binary files /dev/null and b/apps/infoclk/font/am.png differ diff --git a/apps/infoclk/font/colon.png b/apps/infoclk/font/colon.png new file mode 100644 index 000000000..878770dde Binary files /dev/null and b/apps/infoclk/font/colon.png differ diff --git a/apps/infoclk/font/digit0.png b/apps/infoclk/font/digit0.png new file mode 100644 index 000000000..5470154ee Binary files /dev/null and b/apps/infoclk/font/digit0.png differ diff --git a/apps/infoclk/font/digit1.png b/apps/infoclk/font/digit1.png new file mode 100644 index 000000000..26a35fd1b Binary files /dev/null and b/apps/infoclk/font/digit1.png differ diff --git a/apps/infoclk/font/digit2.png b/apps/infoclk/font/digit2.png new file mode 100644 index 000000000..92974daf3 Binary files /dev/null and b/apps/infoclk/font/digit2.png differ diff --git a/apps/infoclk/font/digit3.png b/apps/infoclk/font/digit3.png new file mode 100644 index 000000000..6751067c6 Binary files /dev/null and b/apps/infoclk/font/digit3.png differ diff --git a/apps/infoclk/font/digit4.png b/apps/infoclk/font/digit4.png new file mode 100644 index 000000000..fdb0c5f8d Binary files /dev/null and b/apps/infoclk/font/digit4.png differ diff --git a/apps/infoclk/font/digit5.png b/apps/infoclk/font/digit5.png new file mode 100644 index 000000000..5647ad00a Binary files /dev/null and b/apps/infoclk/font/digit5.png differ diff --git a/apps/infoclk/font/digit6.png b/apps/infoclk/font/digit6.png new file mode 100644 index 000000000..56c446881 Binary files /dev/null and b/apps/infoclk/font/digit6.png differ diff --git a/apps/infoclk/font/digit7.png b/apps/infoclk/font/digit7.png new file mode 100644 index 000000000..1fb6a6423 Binary files /dev/null and b/apps/infoclk/font/digit7.png differ diff --git a/apps/infoclk/font/digit8.png b/apps/infoclk/font/digit8.png new file mode 100644 index 000000000..a373205f1 Binary files /dev/null and b/apps/infoclk/font/digit8.png differ diff --git a/apps/infoclk/font/digit9.png b/apps/infoclk/font/digit9.png new file mode 100644 index 000000000..990a3a43b Binary files /dev/null and b/apps/infoclk/font/digit9.png differ diff --git a/apps/infoclk/font/pm.png b/apps/infoclk/font/pm.png new file mode 100644 index 000000000..a3db97eb8 Binary files /dev/null and b/apps/infoclk/font/pm.png differ diff --git a/apps/infoclk/icon.js b/apps/infoclk/icon.js new file mode 100644 index 000000000..ae230d8f4 --- /dev/null +++ b/apps/infoclk/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgOAA4YFS/4AKEf5BlABcAjAgBjAfBAuhH/Apo")) \ No newline at end of file diff --git a/apps/infoclk/icon.png b/apps/infoclk/icon.png new file mode 100644 index 000000000..24423fbd6 Binary files /dev/null and b/apps/infoclk/icon.png differ diff --git a/apps/infoclk/metadata.json b/apps/infoclk/metadata.json new file mode 100644 index 000000000..bb6dea3a4 --- /dev/null +++ b/apps/infoclk/metadata.json @@ -0,0 +1,38 @@ +{ + "id": "infoclk", + "name": "Informational clock", + "version": "0.08", + "description": "A configurable clock with extra info and shortcuts when unlocked, but large time when locked", + "readme": "README.md", + "icon": "icon.png", + "type": "clock", + "tags": "clock", + "supports": [ + "BANGLEJS2" + ], + "allow_emulator": true, + "storage": [ + { + "name": "infoclk.app.js", + "url": "app.js" + }, + { + "name": "infoclk.settings.js", + "url": "settings.js" + }, + { + "name": "infoclk-font.js", + "url": "font.js" + }, + { + "name": "infoclk.img", + "url": "icon.js", + "evaluate": true + } + ], + "data": [ + { + "name": "infoclk.json" + } + ] +} \ No newline at end of file diff --git a/apps/infoclk/settings.js b/apps/infoclk/settings.js new file mode 100644 index 000000000..0bc3d4b15 --- /dev/null +++ b/apps/infoclk/settings.js @@ -0,0 +1,571 @@ +(function (back) { + const SETTINGS_FILE = "infoclk.json"; + const storage = require('Storage'); + + let config = Object.assign({ + seconds: { + // Displaying the seconds can reduce battery life because the CPU must wake up more often to update the display. + // The seconds will be shown unless one of these conditions is enabled here, and currently true. + hideLocked: false, // Hide the seconds when the display is locked. + hideBattery: 20, // Hide the seconds when the battery is at or below a defined percentage. + hideTime: true, // Hide the seconds when between a certain period of time. Useful for when you are sleeping and don't need the seconds + hideStart: 2200, // The time when the seconds are hidden: first 2 digits are hours on a 24 hour clock, last 2 are minutes + hideEnd: 700, // The time when the seconds are shown again + hideAlways: false, // Always hide (never show) the seconds + }, + + date: { + // Settings related to the display of the date + mmdd: true, // If true, display the month first. If false, display the date first. + separator: '-', // The character that goes between the month and date + monthName: false, // If false, display the month as a number. If true, display the name. + monthFullName: false, // If displaying the name: If false, display an abbreviation. If true, display a full name. + dayFullName: false, // If false, display the day of the week's abbreviation. If true, display the full name. + }, + + bottomLocked: { + display: 'weather' // What to display in the bottom row when locked: + // 'weather': The current temperature and weather description + // 'steps': Step count + // 'health': Step count and bpm + // 'progress': Day progress bar + // false: Nothing + }, + + shortcuts: [ + //8 shortcuts, displayed in the bottom half of the screen (2 rows of 4 shortcuts) when unlocked + // false = no shortcut + // '#LAUNCHER' = open the launcher + // any other string = name of app to open + 'stlap', 'keytimer', 'pomoplus', 'alarm', + 'rpnsci', 'calendar', 'torch', 'weather' + ], + + swipe: { + // 3 shortcuts to launch upon swiping: + // false = no shortcut + // '#LAUNCHER' = open the launcher + // any other string = name of app to open + up: 'messages', // Swipe up or swipe down, due to limitation of event handler + left: '#LAUNCHER', + right: '#LAUNCHER', + }, + + dayProgress: { + // A progress bar representing how far through the day you are + enabledLocked: true, // Whether this bar is enabled when the watch is locked + enabledUnlocked: false, // Whether the bar is enabled when the watch is unlocked + color: [0, 0, 1], // The color of the bar + start: 700, // The time of day that the bar starts filling + end: 2200, // The time of day that the bar becomes full + reset: 300 // The time of day when the progress bar resets from full to empty + }, + + lowBattColor: { + // The text can change color to indicate that the battery is low + level: 20, // The percentage where this happens + color: [1, 0, 0] // The color that the text changes to + } + }, storage.readJSON(SETTINGS_FILE)); + + function saveSettings() { + storage.writeJSON(SETTINGS_FILE, config); + } + + function hourToString(hour) { + if (storage.readJSON('setting.json')['12hour']) { + if (hour == 0) return '12 AM'; + else if (hour < 12) return `${hour} AM`; + else if (hour == 12) return '12 PM'; + else return `${hour - 12} PM`; + } else return '' + hour; + } + + // The menu for configuring when the seconds are shown + function showSecondsMenu() { + E.showMenu({ + '': { + 'title': 'Seconds display', + 'back': showMainMenu + }, + 'Show seconds': { + value: !config.seconds.hideAlways, + onchange: value => { + config.seconds.hideAlways = !value; + saveSettings(); + } + }, + '...unless locked': { + value: config.seconds.hideLocked, + onchange: value => { + config.seconds.hideLocked = value; + saveSettings(); + } + }, + '...unless battery below': { + value: config.seconds.hideBattery, + min: 0, + max: 100, + format: value => `${value}%`, + onchange: value => { + config.seconds.hideBattery = value; + saveSettings(); + } + }, + '...unless between these 2 times...': () => { + E.showMenu({ + '': { + 'title': 'Hide seconds between', + 'back': showSecondsMenu + }, + 'Enabled': { + value: config.seconds.hideTime, + onchange: value => { + config.seconds.hideTime = value; + saveSettings(); + } + }, + 'Start hour': { + value: Math.floor(config.seconds.hideStart / 100), + format: hourToString, + min: 0, + max: 23, + wrap: true, + onchange: hour => { + minute = config.seconds.hideStart % 100; + config.seconds.hideStart = (100 * hour) + minute; + saveSettings(); + } + }, + 'Start minute': { + value: config.seconds.hideStart % 100, + min: 0, + max: 59, + wrap: true, + onchange: minute => { + hour = Math.floor(config.seconds.hideStart / 100); + config.seconds.hideStart = (100 * hour) + minute; + saveSettings(); + } + }, + 'End hour': { + value: Math.floor(config.seconds.hideEnd / 100), + format: hourToString, + min: 0, + max: 23, + wrap: true, + onchange: hour => { + minute = config.seconds.hideEnd % 100; + config.seconds.hideEnd = (100 * hour) + minute; + saveSettings(); + } + }, + 'End minute': { + value: config.seconds.hideEnd % 100, + min: 0, + max: 59, + wrap: true, + onchange: minute => { + hour = Math.floor(config.seconds.hideEnd / 100); + config.seconds.hideEnd = (100 * hour) + minute; + saveSettings(); + } + } + }); + } + }); + } + + // Available month/date separators + const SEPARATORS = [ + { name: 'Slash', char: '/' }, + { name: 'Dash', char: '-' }, + { name: 'Space', char: ' ' }, + { name: 'Comma', char: ',' }, + { name: 'None', char: '' } + ]; + + // Available bottom row display options + const BOTTOM_ROW_OPTIONS = [ + { name: 'Weather', val: 'weather' }, + { name: 'Step count', val: 'steps' }, + { name: 'Steps + BPM', val: 'health' }, + { name: 'Day progresss bar', val: 'progress' }, + { name: 'Nothing', val: false } + ]; + + // The menu for configuring which apps have shortcut icons + function showShortcutMenu() { + //Builds the shortcut options + let shortcutOptions = [ + { name: 'Nothing', val: false }, + { name: 'Launcher', val: '#LAUNCHER' }, + ]; + + let infoFiles = storage.list(/\.info$/).sort((a, b) => { + if (a.name < b.name) return -1; + else if (a.name > b.name) return 1; + else return 0; + }); + for (let infoFile of infoFiles) { + let appInfo = storage.readJSON(infoFile); + if (appInfo.src) shortcutOptions.push({ + name: appInfo.name, + val: appInfo.id + }); + } + + E.showMenu({ + '': { + 'title': 'Shortcuts', + 'back': showMainMenu + }, + 'Top first': { + value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[0]), + format: value => shortcutOptions[value].name, + min: 0, + max: shortcutOptions.length - 1, + wrap: false, + onchange: value => { + config.shortcuts[0] = shortcutOptions[value].val; + saveSettings(); + } + }, + 'Top second': { + value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[1]), + format: value => shortcutOptions[value].name, + min: 0, + max: shortcutOptions.length - 1, + wrap: false, + onchange: value => { + config.shortcuts[1] = shortcutOptions[value].val; + saveSettings(); + } + }, + 'Top third': { + value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[2]), + format: value => shortcutOptions[value].name, + min: 0, + max: shortcutOptions.length - 1, + wrap: false, + onchange: value => { + config.shortcuts[2] = shortcutOptions[value].val; + saveSettings(); + } + }, + 'Top fourth': { + value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[3]), + format: value => shortcutOptions[value].name, + min: 0, + max: shortcutOptions.length - 1, + wrap: false, + onchange: value => { + config.shortcuts[3] = shortcutOptions[value].val; + saveSettings(); + } + }, + 'Bottom first': { + value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[4]), + format: value => shortcutOptions[value].name, + min: 0, + max: shortcutOptions.length - 1, + wrap: false, + onchange: value => { + config.shortcuts[4] = shortcutOptions[value].val; + saveSettings(); + } + }, + 'Bottom second': { + value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[5]), + format: value => shortcutOptions[value].name, + min: 0, + max: shortcutOptions.length - 1, + wrap: false, + onchange: value => { + config.shortcuts[5] = shortcutOptions[value].val; + saveSettings(); + } + }, + 'Bottom third': { + value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[6]), + format: value => shortcutOptions[value].name, + min: 0, + max: shortcutOptions.length - 1, + wrap: false, + onchange: value => { + config.shortcuts[6] = shortcutOptions[value].val; + saveSettings(); + } + }, + 'Bottom fourth': { + value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[7]), + format: value => shortcutOptions[value].name, + min: 0, + max: shortcutOptions.length - 1, + wrap: false, + onchange: value => { + config.shortcuts[7] = shortcutOptions[value].val; + saveSettings(); + } + }, + 'Swipe up': { + value: shortcutOptions.map(item => item.val).indexOf(config.swipe.up), + format: value => shortcutOptions[value].name, + min: 0, + max: shortcutOptions.length - 1, + wrap: false, + onchange: value => { + config.swipe.up = shortcutOptions[value].val; + saveSettings(); + } + }, + 'Swipe left': { + value: shortcutOptions.map(item => item.val).indexOf(config.swipe.left), + format: value => shortcutOptions[value].name, + min: 0, + max: shortcutOptions.length - 1, + wrap: false, + onchange: value => { + config.swipe.left = shortcutOptions[value].val; + saveSettings(); + } + }, + 'Swipe right': { + value: shortcutOptions.map(item => item.val).indexOf(config.swipe.right), + format: value => shortcutOptions[value].name, + min: 0, + max: shortcutOptions.length - 1, + wrap: false, + onchange: value => { + config.swipe.right = shortcutOptions[value].val; + saveSettings(); + } + }, + }); + } + + const COLOR_OPTIONS = [ + { name: 'Black', val: [0, 0, 0] }, + { name: 'Blue', val: [0, 0, 1] }, + { name: 'Green', val: [0, 1, 0] }, + { name: 'Cyan', val: [0, 1, 1] }, + { name: 'Red', val: [1, 0, 0] }, + { name: 'Magenta', val: [1, 0, 1] }, + { name: 'Yellow', val: [1, 1, 0] }, + { name: 'White', val: [1, 1, 1] } + ]; + + // Workaround for being unable to use == on arrays: convert them into strings + function colorString(color) { + return `${color[0]} ${color[1]} ${color[2]}`; + } + + //Shows the top level menu + function showMainMenu() { + E.showMenu({ + '': { + 'title': 'Informational Clock', + 'back': back + }, + 'Seconds display': showSecondsMenu, + 'Day of week format': { + value: config.date.dayFullName, + format: value => value ? 'Full name' : 'Abbreviation', + onchange: value => { + config.date.dayFullName = value; + saveSettings(); + } + }, + 'Date format': () => { + E.showMenu({ + '': { + 'title': 'Date format', + 'back': showMainMenu, + }, + 'Order': { + value: config.date.mmdd, + format: value => value ? 'Month first' : 'Date first', + onchange: value => { + config.date.mmdd = value; + saveSettings(); + } + }, + 'Separator': { + value: SEPARATORS.map(item => item.char).indexOf(config.date.separator), + format: value => SEPARATORS[value].name, + min: 0, + max: SEPARATORS.length - 1, + wrap: true, + onchange: value => { + config.date.separator = SEPARATORS[value].char; + saveSettings(); + } + }, + 'Month format': { + // 0 = number only + // 1 = abbreviation + // 2 = full name + value: config.date.monthName ? (config.date.monthFullName ? 2 : 1) : 0, + format: value => ['Number', 'Abbreviation', 'Full name'][value], + min: 0, + max: 2, + wrap: true, + onchange: value => { + if (value == 0) config.date.monthName = false; + else { + config.date.monthName = true; + config.date.monthFullName = (value == 2); + } + saveSettings(); + } + } + }); + }, + 'Bottom row': { + value: BOTTOM_ROW_OPTIONS.map(item => item.val).indexOf(config.bottomLocked.display), + format: value => BOTTOM_ROW_OPTIONS[value].name, + min: 0, + max: BOTTOM_ROW_OPTIONS.length - 1, + wrap: true, + onchange: value => { + config.bottomLocked.display = BOTTOM_ROW_OPTIONS[value].val; + saveSettings(); + } + }, + 'Shortcuts': showShortcutMenu, + 'Day progress': () => { + E.showMenu({ + '': { + 'title': 'Day progress', + 'back': showMainMenu + }, + 'Enable while locked': { + value: config.dayProgress.enabledLocked, + onchange: value => { + config.dayProgress.enableLocked = value; + saveSettings(); + } + }, + 'Enable while unlocked': { + value: config.dayProgress.enabledUnlocked, + onchange: value => { + config.dayProgress.enabledUnlocked = value; + saveSettings(); + } + }, + 'Color': { + value: COLOR_OPTIONS.map(item => colorString(item.val)).indexOf(colorString(config.dayProgress.color)), + format: value => COLOR_OPTIONS[value].name, + min: 0, + max: COLOR_OPTIONS.length - 1, + wrap: false, + onchange: value => { + config.dayProgress.color = COLOR_OPTIONS[value].val; + saveSettings(); + } + }, + 'Start hour': { + value: Math.floor(config.dayProgress.start / 100), + format: hourToString, + min: 0, + max: 23, + wrap: true, + onchange: hour => { + minute = config.dayProgress.start % 100; + config.dayProgress.start = (100 * hour) + minute; + saveSettings(); + } + }, + 'Start minute': { + value: config.dayProgress.start % 100, + min: 0, + max: 59, + wrap: true, + onchange: minute => { + hour = Math.floor(config.dayProgress.start / 100); + config.dayProgress.start = (100 * hour) + minute; + saveSettings(); + } + }, + 'End hour': { + value: Math.floor(config.dayProgress.end / 100), + format: hourToString, + min: 0, + max: 23, + wrap: true, + onchange: hour => { + minute = config.dayProgress.end % 100; + config.dayProgress.end = (100 * hour) + minute; + saveSettings(); + } + }, + 'End minute': { + value: config.dayProgress.end % 100, + min: 0, + max: 59, + wrap: true, + onchange: minute => { + hour = Math.floor(config.dayProgress.end / 100); + config.dayProgress.end = (100 * hour) + minute; + saveSettings(); + } + }, + 'Reset hour': { + value: Math.floor(config.dayProgress.reset / 100), + format: hourToString, + min: 0, + max: 23, + wrap: true, + onchange: hour => { + minute = config.dayProgress.reset % 100; + config.dayProgress.reset = (100 * hour) + minute; + saveSettings(); + } + }, + 'Reset minute': { + value: config.dayProgress.reset % 100, + min: 0, + max: 59, + wrap: true, + onchange: minute => { + hour = Math.floor(config.dayProgress.reset / 100); + config.dayProgress.reset = (100 * hour) + minute; + saveSettings(); + } + } + }); + }, + 'Low battery color': () => { + E.showMenu({ + '': { + 'title': 'Low battery color', + back: showMainMenu + }, + 'Low battery threshold': { + value: config.lowBattColor.level, + min: 0, + max: 100, + format: value => `${value}%`, + onchange: value => { + config.lowBattColor.level = value; + saveSettings(); + } + }, + 'Color': { + value: COLOR_OPTIONS.map(item => colorString(item.val)).indexOf(colorString(config.lowBattColor.color)), + format: value => COLOR_OPTIONS[value].name, + min: 0, + max: COLOR_OPTIONS.length - 1, + wrap: false, + onchange: value => { + config.lowBattColor.color = COLOR_OPTIONS[value].val; + saveSettings(); + } + } + }); + }, + }); + } + + showMainMenu(); +}); \ No newline at end of file diff --git a/apps/kbmulti/ChangeLog b/apps/kbmulti/ChangeLog index 19739fa64..4ef8f7bda 100644 --- a/apps/kbmulti/ChangeLog +++ b/apps/kbmulti/ChangeLog @@ -2,3 +2,4 @@ 0.02: Introduce setting "Show help button?". Make setting firstLaunch invisible by removing corresponding code from settings.js. Add marker that shows when character selection timeout has run out. Display opened text on launch when editing existing text string. Perfect horizontal alignment of buttons. Tweak help message letter casing. 0.03: Use default Bangle formatter for booleans 0.04: Allow moving the cursor +0.05: Switch swipe directions for Caps Lock and moving cursor. diff --git a/apps/kbmulti/README.md b/apps/kbmulti/README.md index 80b2b077a..b6754711d 100644 --- a/apps/kbmulti/README.md +++ b/apps/kbmulti/README.md @@ -2,7 +2,7 @@ A library that provides the ability to input text in a style familiar to anyone who had a mobile phone before they went all touchscreen. -Swipe right for Space, left for Backspace, down for cursor moving mode, and up for Caps lock. Swipe left and right to move the cursor in moving mode. Tap the '?' button in the app if you need a reminder! +Swipe right for Space, left for Backspace, down for Caps lock switch, and up for cursor moving mode. Swipe left and right to move the cursor in moving mode. Tap the '?' button in the app if you need a reminder! At time of writing, only the [Noteify app](http://microco.sm/out/Ffe9i) uses a keyboard. diff --git a/apps/kbmulti/lib.js b/apps/kbmulti/lib.js index aa54dab9c..9b642a132 100644 --- a/apps/kbmulti/lib.js +++ b/apps/kbmulti/lib.js @@ -17,7 +17,7 @@ exports.input = function(options) { "4":"GHI4","5":"JKL5","6":"MNO6", "7":"PQRS7","8":"TUV80","9":"WXYZ9", }; - var helpMessage = 'Swipe:\nRight: Space\nLeft:Backspace\nUp: Caps lock\nDown:Move mode'; + var helpMessage = 'Swipe:\nRight: Space\nLeft:Backspace\nUp: Move mode\nDown:Caps lock'; var charTimeout; // timeout after a key is pressed var charCurrent; // current character (index in letters) @@ -122,10 +122,10 @@ exports.input = function(options) { function onSwipe(dirLeftRight, dirUpDown) { if (dirUpDown == -1) { - setCaps(); - } else if (dirUpDown == 1) { moveMode = !moveMode; displayText(false); + } else if (dirUpDown == 1) { + setCaps(); } else if (dirLeftRight == 1) { if (!moveMode){ text = text.slice(0, textIndex + 1) + " " + text.slice(++textIndex); diff --git a/apps/kbmulti/metadata.json b/apps/kbmulti/metadata.json index a1f6ffa81..510454f79 100644 --- a/apps/kbmulti/metadata.json +++ b/apps/kbmulti/metadata.json @@ -1,6 +1,6 @@ { "id": "kbmulti", "name": "Multitap keyboard", - "version":"0.04", + "version":"0.05", "description": "A library for text input via multitap/T9 style keypad", "icon": "app.png", "type":"textinput", diff --git a/apps/keytimer/ChangeLog b/apps/keytimer/ChangeLog new file mode 100644 index 000000000..c819919ed --- /dev/null +++ b/apps/keytimer/ChangeLog @@ -0,0 +1,2 @@ +0.01: New app! +0.02: Submitted to the app loader \ No newline at end of file diff --git a/apps/keytimer/app.js b/apps/keytimer/app.js new file mode 100644 index 000000000..7d235f9a8 --- /dev/null +++ b/apps/keytimer/app.js @@ -0,0 +1,27 @@ +Bangle.keytimer_ACTIVE = true; +const common = require("keytimer-com.js"); +const storage = require("Storage"); + +const keypad = require("keytimer-keys.js"); +const timerView = require("keytimer-tview.js"); + +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +//Save our state when the app is closed +E.on('kill', () => { + storage.writeJSON(common.STATE_PATH, common.state); +}); + +//Handle touch here. I would implement these separately in each view, but I can't figure out how to clear the event listeners. +Bangle.on('touch', (button, xy) => { + if (common.state.wasRunning) timerView.touch(button, xy); + else keypad.touch(button, xy); +}); + +Bangle.on('swipe', dir => { + if (!common.state.wasRunning) keypad.swipe(dir); +}); + +if (common.state.wasRunning) timerView.show(common); +else keypad.show(common); diff --git a/apps/keytimer/boot.js b/apps/keytimer/boot.js new file mode 100644 index 000000000..f202bcbdf --- /dev/null +++ b/apps/keytimer/boot.js @@ -0,0 +1,11 @@ +const keytimer_common = require("keytimer-com.js"); + +//Only start the timeout if the timer is running +if (keytimer_common.state.running) { + setTimeout(() => { + //Check now to avoid race condition + if (Bangle.keytimer_ACTIVE === undefined) { + load('keytimer-ring.js'); + } + }, keytimer_common.getTimeLeft()); +} \ No newline at end of file diff --git a/apps/keytimer/common.js b/apps/keytimer/common.js new file mode 100644 index 000000000..8c702de66 --- /dev/null +++ b/apps/keytimer/common.js @@ -0,0 +1,42 @@ +const storage = require("Storage"); +const heatshrink = require("heatshrink"); + +exports.STATE_PATH = "keytimer.state.json"; + +exports.BUTTON_ICONS = { + play: heatshrink.decompress(atob("jEYwMAkAGBnACBnwCBn+AAQPgAQPwAQP8AQP/AQXAAQPwAQP8AQP+AQgICBwQUCEAn4FggyBHAQ+CIgQ")), + pause: heatshrink.decompress(atob("jEYwMA/4BBAX4CEA")), + reset: heatshrink.decompress(atob("jEYwMA/4BB/+BAQPDAQPnAQIAKv///0///8j///EP//wAQQICBwQUCEhgyCHAQ+CIgI=")) +}; + +//Store the minimal amount of information to be able to reconstruct the state of the timer at any given time. +//This is necessary because it is necessary to write to flash to let the timer run in the background, so minimizing the writes is necessary. +exports.STATE_DEFAULT = { + wasRunning: false, //If the timer ever was running. Used to determine whether to display a reset button + running: false, //Whether the timer is currently running + startTime: 0, //When the timer was last started. Difference between this and now is how long timer has run continuously. + pausedTime: 0, //When the timer was last paused. Used for expiration and displaying timer while paused. + elapsedTime: 0, //How much time the timer had spent running before the current start time. Update on pause or user skipping stages. + setTime: 0, //How long the user wants the timer to run for + inputString: '0' //The string of numbers the user typed in. +}; +exports.state = storage.readJSON(exports.STATE_PATH); +if (!exports.state) { + exports.state = exports.STATE_DEFAULT; +} + +//Get the number of milliseconds until the timer expires +exports.getTimeLeft = function () { + if (!exports.state.wasRunning) { + //If the timer never ran, the time left is just the set time + return exports.setTime + } else if (exports.state.running) { + //If the timer is running, the time left is current time - start time + preexisting time + var runningTime = (new Date()).getTime() - exports.state.startTime + exports.state.elapsedTime; + } else { + //If the timer is not running, the same as above but use when the timer was paused instead of now. + var runningTime = exports.state.pausedTime - exports.state.startTime + exports.state.elapsedTime; + } + + return exports.state.setTime - runningTime; +} \ No newline at end of file diff --git a/apps/keytimer/icon.js b/apps/keytimer/icon.js new file mode 100644 index 000000000..a47eb21f8 --- /dev/null +++ b/apps/keytimer/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwcAkmSpICOggRPpEACJ9AgESCJxMBhu27dtARVgCIMBCJpxDmwRL7ARDgwRL4CWECJaoFjYRJ2ARFgYRJwDNGCJFsb46SIRgQAFSRAQHSRCMEAAqSGRgoAFRhaSKRgySKRg6SIRhCSIRhCSICBqSCRhSSGRhY2FkARPhMkCJ9JkiONgECCIOQCJsSCIOSCJuSCIVACBcECIdICJYOBCIVJRhYRFSRSMBCIiSKBwgCCSRCMCCIqSIRgYCFRhYCFSQyMEAQqSGBw6SIRgySKRgtO4iSJBAmT23bOIqSCRgvtCINsSQ4aEndtCINt2KSGIggOBCIW2JQlARgZECCIhKEpBEGCIpKEA==")) \ No newline at end of file diff --git a/apps/keytimer/icon.png b/apps/keytimer/icon.png new file mode 100644 index 000000000..7dcf44b88 Binary files /dev/null and b/apps/keytimer/icon.png differ diff --git a/apps/keytimer/img/pause.png b/apps/keytimer/img/pause.png new file mode 100644 index 000000000..ad31dadcf Binary files /dev/null and b/apps/keytimer/img/pause.png differ diff --git a/apps/keytimer/img/play.png b/apps/keytimer/img/play.png new file mode 100644 index 000000000..6c20c24c5 Binary files /dev/null and b/apps/keytimer/img/play.png differ diff --git a/apps/keytimer/img/reset.png b/apps/keytimer/img/reset.png new file mode 100644 index 000000000..7a317d097 Binary files /dev/null and b/apps/keytimer/img/reset.png differ diff --git a/apps/keytimer/keypad.js b/apps/keytimer/keypad.js new file mode 100644 index 000000000..a5edeb2f2 --- /dev/null +++ b/apps/keytimer/keypad.js @@ -0,0 +1,136 @@ +let common; + +function inputStringToTime(inputString) { + let number = parseInt(inputString); + let hours = Math.floor(number / 10000); + let minutes = Math.floor((number % 10000) / 100); + let seconds = number % 100; + + return 3600000 * hours + + 60000 * minutes + + 1000 * seconds; +} + +function pad(number) { + return ('00' + parseInt(number)).slice(-2); +} + +function inputStringToDisplayString(inputString) { + let number = parseInt(inputString); + let hours = Math.floor(number / 10000); + let minutes = Math.floor((number % 10000) / 100); + let seconds = number % 100; + + if (hours == 0 && minutes == 0) return '' + seconds; + else if (hours == 0) return `${pad(minutes)}:${pad(seconds)}`; + else return `${hours}:${pad(minutes)}:${pad(seconds)}`; +} + +class NumberButton { + constructor(number) { + this.label = '' + number; + } + + onclick() { + if (common.state.inputString == '0') common.state.inputString = this.label; + else common.state.inputString += this.label; + common.state.setTime = inputStringToTime(common.state.inputString); + feedback(true); + updateDisplay(); + } +} + +let ClearButton = { + label: 'Clr', + onclick: () => { + common.state.inputString = '0'; + common.state.setTime = 0; + updateDisplay(); + feedback(true); + } +}; + +let StartButton = { + label: 'Go', + onclick: () => { + common.state.startTime = (new Date()).getTime(); + common.state.elapsedTime = 0; + common.state.wasRunning = true; + common.state.running = true; + feedback(true); + require('keytimer-tview.js').show(common); + } +}; + +const BUTTONS = [ + [new NumberButton(7), new NumberButton(8), new NumberButton(9), ClearButton], + [new NumberButton(4), new NumberButton(5), new NumberButton(6), new NumberButton(0)], + [new NumberButton(1), new NumberButton(2), new NumberButton(3), StartButton] +]; + +function feedback(acceptable) { + if (acceptable) Bangle.buzz(50, 0.5); + else Bangle.buzz(200, 1); +} + +function drawButtons() { + g.reset().clearRect(0, 44, 175, 175).setFont("Vector", 15).setFontAlign(0, 0); + //Draw lines + for (let x = 44; x <= 176; x += 44) { + g.drawLine(x, 44, x, 175); + } + for (let y = 44; y <= 176; y += 44) { + g.drawLine(0, y, 175, y); + } + for (let row = 0; row < 3; row++) { + for (let col = 0; col < 4; col++) { + g.drawString(BUTTONS[row][col].label, 22 + 44 * col, 66 + 44 * row); + } + } +} + +function getFontSize(length) { + let size = Math.floor(176 / length); //Characters of width needed per pixel + size *= (20 / 12); //Convert to height + // Clamp to between 6 and 20 + if (size < 6) return 6; + else if (size > 20) return 20; + else return Math.floor(size); +} + +function updateDisplay() { + let displayString = inputStringToDisplayString(common.state.inputString); + g.clearRect(0, 24, 175, 43).setColor(storage.readJSON('setting.json').theme.fg2).setFontAlign(1, -1).setFont("Vector", getFontSize(displayString.length)).drawString(displayString, 176, 24); +} + +exports.show = function (callerCommon) { + common = callerCommon; + g.reset(); + drawButtons(); + updateDisplay(); +}; + +exports.touch = function (button, xy) { + let row = Math.floor((xy.y - 44) / 44); + let col = Math.floor(xy.x / 44); + if (row < 0) return; + if (row > 2) row = 2; + if (col < 0) col = 0; + if (col > 3) col = 3; + + BUTTONS[row][col].onclick(); +}; + +exports.swipe = function (dir) { + if (dir == -1) { + if (common.state.inputString.length == 1) common.state.inputString = '0'; + else common.state.inputString = common.state.inputString.substring(0, common.state.inputString.length - 1); + + common.state.setTime = inputStringToTime(common.state.inputString); + + feedback(true); + updateDisplay(); + } else if (dir == 0) { + EnterButton.onclick(); + } +}; \ No newline at end of file diff --git a/apps/keytimer/metadata.json b/apps/keytimer/metadata.json new file mode 100644 index 000000000..a982594f1 --- /dev/null +++ b/apps/keytimer/metadata.json @@ -0,0 +1,44 @@ +{ + "id": "keytimer", + "name": "Keypad Timer", + "version": "0.02", + "description": "A timer with a keypad that runs in the background", + "icon": "icon.png", + "type": "app", + "tags": "tools", + "supports": [ + "BANGLEJS2" + ], + "allow_emulator": true, + "storage": [ + { + "name": "keytimer.app.js", + "url": "app.js" + }, + { + "name": "keytimer.img", + "url": "icon.js", + "evaluate": true + }, + { + "name": "keytimer.boot.js", + "url": "boot.js" + }, + { + "name": "keytimer-com.js", + "url": "common.js" + }, + { + "name": "keytimer-ring.js", + "url": "ring.js" + }, + { + "name": "keytimer-keys.js", + "url": "keypad.js" + }, + { + "name": "keytimer-tview.js", + "url": "timerview.js" + } + ] +} \ No newline at end of file diff --git a/apps/keytimer/ring.js b/apps/keytimer/ring.js new file mode 100644 index 000000000..c42c11394 --- /dev/null +++ b/apps/keytimer/ring.js @@ -0,0 +1,28 @@ +const common = require('keytimer-com.js'); + +Bangle.loadWidgets() +Bangle.drawWidgets() + +Bangle.setLocked(false); +Bangle.setLCDPower(true); + +let brightness = 0; + +setInterval(() => { + Bangle.buzz(200); + Bangle.setLCDBrightness(1 - brightness); + brightness = 1 - brightness; +}, 400); +Bangle.buzz(200); + +function stopTimer() { + common.state.wasRunning = false; + common.state.running = false; + require("Storage").writeJSON(common.STATE_PATH, common.state); +} + +E.showAlert("Timer expired!").then(() => { + stopTimer(); + load(); +}); +E.on('kill', stopTimer); \ No newline at end of file diff --git a/apps/keytimer/timerview.js b/apps/keytimer/timerview.js new file mode 100644 index 000000000..48c896ba0 --- /dev/null +++ b/apps/keytimer/timerview.js @@ -0,0 +1,107 @@ +let common; + +function drawButtons() { + //Draw the backdrop + const BAR_TOP = g.getHeight() - 24; + g.setColor(0, 0, 1).setFontAlign(0, -1) + .clearRect(0, BAR_TOP, g.getWidth(), g.getHeight()) + .fillRect(0, BAR_TOP, g.getWidth(), g.getHeight()) + .setColor(1, 1, 1) + .drawLine(g.getWidth() / 2, BAR_TOP, g.getWidth() / 2, g.getHeight()) + + //Draw the buttons + .drawImage(common.BUTTON_ICONS.reset, g.getWidth() / 4, BAR_TOP); + if (common.state.running) { + g.drawImage(common.BUTTON_ICONS.pause, g.getWidth() * 3 / 4, BAR_TOP); + } else { + g.drawImage(common.BUTTON_ICONS.play, g.getWidth() * 3 / 4, BAR_TOP); + } +} + +function drawTimer() { + let timeLeft = common.getTimeLeft(); + g.reset() + .setFontAlign(0, 0) + .setFont("Vector", 36) + .clearRect(0, 24, 176, 152) + + //Draw the timer + .drawString((() => { + let hours = timeLeft / 3600000; + let minutes = (timeLeft % 3600000) / 60000; + let seconds = (timeLeft % 60000) / 1000; + + function pad(number) { + return ('00' + parseInt(number)).slice(-2); + } + + if (hours >= 1) return `${parseInt(hours)}:${pad(minutes)}:${pad(seconds)}`; + else return `${parseInt(minutes)}:${pad(seconds)}`; + })(), g.getWidth() / 2, g.getHeight() / 2) + + if (timeLeft <= 0) load('keytimer-ring.js'); +} + +let timerInterval; + +function setupTimerInterval() { + if (timerInterval !== undefined) { + clearInterval(timerInterval); + } + setTimeout(() => { + timerInterval = setInterval(drawTimer, 1000); + drawTimer(); + }, common.timeLeft % 1000); +} + +exports.show = function (callerCommon) { + common = callerCommon; + drawButtons(); + drawTimer(); + if (common.state.running) { + setupTimerInterval(); + } +} + +function clearTimerInterval() { + if (timerInterval !== undefined) { + clearInterval(timerInterval); + timerInterval = undefined; + } +} + +exports.touch = (button, xy) => { + if (xy.y < 152) return; + + if (button == 1) { + //Reset the timer + let setTime = common.state.setTime; + let inputString = common.state.inputString; + common.state = common.STATE_DEFAULT; + common.state.setTime = setTime; + common.state.inputString = inputString; + clearTimerInterval(); + require('keytimer-keys.js').show(common); + } else { + if (common.state.running) { + //Record the exact moment that we paused + let now = (new Date()).getTime(); + common.state.pausedTime = now; + + //Stop the timer + common.state.running = false; + clearTimerInterval(); + drawTimer(); + drawButtons(); + } else { + //Start the timer and record when we started + let now = (new Date()).getTime(); + common.state.elapsedTime += common.state.pausedTime - common.state.startTime; + common.state.startTime = now; + common.state.running = true; + drawTimer(); + setupTimerInterval(); + drawButtons(); + } + } +}; \ No newline at end of file diff --git a/apps/launch/ChangeLog b/apps/launch/ChangeLog index 36852e0b7..5da1b2661 100644 --- a/apps/launch/ChangeLog +++ b/apps/launch/ChangeLog @@ -15,3 +15,7 @@ 0.13: Add fullscreen mode 0.14: Use default Bangle formatter for booleans 0.15: Support for unload and quick return to the clock on 2v16 +0.16: Use a cache of app.info files to speed up loading the launcher +0.17: Don't display 'Loading...' now the watch has its own loading screen +0.18: Add 'back' icon in top-left to go back to clock +0.19: Fix regression after back button added (returnToClock was called twice!) diff --git a/apps/launch/app.js b/apps/launch/app.js index d53f0dcdf..b8e598f73 100644 --- a/apps/launch/app.js +++ b/apps/launch/app.js @@ -1,5 +1,6 @@ { // must be inside our own scope here so that when we are unloaded everything disappears let s = require("Storage"); +// handle customised launcher let scaleval = 1; let vectorval = 20; let font = g.getFonts().includes("12x20") ? "12x20" : "6x8:2"; @@ -7,70 +8,41 @@ let settings = Object.assign({ showClocks: true, fullscreen: false }, s.readJSON("launch.json", true) || {}); - -if ("vectorsize" in settings) { - vectorval = parseInt(settings.vectorsize); -} +if ("vectorsize" in settings) + vectorval = parseInt(settings.vectorsize); if ("font" in settings){ - if(settings.font == "Vector"){ - scaleval = vectorval/20; - font = "Vector"+(vectorval).toString(); - } - else{ - font = settings.font; - scaleval = (font.split("x")[1])/20; - } -} -let apps = s.list(/\.info$/).map(app=>{var a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src};}).filter(app=>app && (app.type=="app" || (app.type=="clock" && settings.showClocks) || !app.type)); -apps.sort((a,b)=>{ - var n=(0|a.sortorder)-(0|b.sortorder); - if (n) return n; // do sortorder first - if (a.nameb.name) return 1; - return 0; -}); -apps.forEach(app=>{ - if (app.icon) - app.icon = s.read(app.icon); // should just be a link to a memory area -}); -// FIXME: check not needed after 2v11 -if (g.wrapString) { - g.setFont(font); - apps.forEach(app=>app.name = g.wrapString(app.name, g.getWidth()-64).join("\n")); -} - -function drawApp(i, r) { - var app = apps[i]; - if (!app) return; - g.clearRect((r.x),(r.y),(r.x+r.w-1), (r.y+r.h-1)); - g.setFont(font).setFontAlign(-1,0).drawString(app.name,64*scaleval,r.y+(32*scaleval)); - if (app.icon) try {g.drawImage(app.icon,8*scaleval, r.y+(8*scaleval), {scale: scaleval});} catch(e){} -} - -g.clear(); - -if (!settings.fullscreen) { - Bangle.loadWidgets(); - Bangle.drawWidgets(); -} - -E.showScroller({ - h : 64*scaleval, c : apps.length, - draw : drawApp, - select : i => { - var app = apps[i]; - if (!app) return; - if (!app.src || require("Storage").read(app.src)===undefined) { - E.showMessage(/*LANG*/"App Source\nNot found"); - setTimeout(drawMenu, 2000); - } else { - E.showMessage(/*LANG*/"Loading..."); - load(app.src); - } + if(settings.font == "Vector"){ + scaleval = vectorval/20; + font = "Vector"+(vectorval).toString(); + } else{ + font = settings.font; + scaleval = (font.split("x")[1])/20; } -}); +} +// cache app list so launcher loads more quickly +let launchCache = s.readJSON("launch.cache.json", true)||{}; +let launchHash = require("Storage").hash(/\.info/); +if (launchCache.hash!=launchHash) { + launchCache = { + hash : launchHash, + apps : s.list(/\.info$/) + .map(app=>{var a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src};}) + .filter(app=>app && (app.type=="app" || (app.type=="clock" && settings.showClocks) || !app.type)) + .sort((a,b)=>{ + var n=(0|a.sortorder)-(0|b.sortorder); + if (n) return n; // do sortorder first + if (a.nameb.name) return 1; + return 0; + }) }; + s.writeJSON("launch.cache.json", launchCache); +} +let apps = launchCache.apps; +// Now apps list is loaded - render +if (!settings.fullscreen) + Bangle.loadWidgets(); -function returnToClock() { +let returnToClock = function() { // unload everything manually // ... or we could just call `load();` but it will be slower Bangle.setUI(); // remove scroller's handling @@ -80,20 +52,42 @@ function returnToClock() { setTimeout(eval,0,s.read(".bootcde")); } -// on bangle.js 2, the screen is used for navigating, so the single button goes back -// on bangle.js 1, the buttons are used for navigating -if (process.env.HWVERSION==2) { - setWatch(returnToClock, BTN1, {edge:"falling"}); -} +E.showScroller({ + h : 64*scaleval, c : apps.length, + draw : (i, r) => { + var app = apps[i]; + if (!app) return; + g.clearRect((r.x),(r.y),(r.x+r.w-1), (r.y+r.h-1)); + g.setFont(font).setFontAlign(-1,0).drawString(app.name,64*scaleval,r.y+(32*scaleval)); + if (app.icon) { + if (!app.img) app.img = s.read(app.icon); // load icon if it wasn't loaded + try {g.drawImage(app.img,8*scaleval, r.y+(8*scaleval), {scale: scaleval});} catch(e){} + } + }, + select : i => { + var app = apps[i]; + if (!app) return; + if (!app.src || require("Storage").read(app.src)===undefined) { + E.showMessage(/*LANG*/"App Source\nNot found"); + setTimeout(drawMenu, 2000); + } else { + load(app.src); + } + }, + back : returnToClock // button press or tap in top left calls returnToClock now +}); +g.flip(); // force a render before widgets have finished drawing // 10s of inactivity goes back to clock Bangle.setLocked(false); // unlock initially let lockTimeout; -function lockHandler(locked) { +let lockHandler = function(locked) { if (lockTimeout) clearTimeout(lockTimeout); lockTimeout = undefined; if (locked) lockTimeout = setTimeout(returnToClock, 10000); } Bangle.on("lock", lockHandler); +if (!settings.fullscreen) // finally draw widgets + Bangle.drawWidgets(); } diff --git a/apps/launch/metadata.json b/apps/launch/metadata.json index ec070e44e..ce9b1f801 100644 --- a/apps/launch/metadata.json +++ b/apps/launch/metadata.json @@ -2,7 +2,7 @@ "id": "launch", "name": "Launcher", "shortName": "Launcher", - "version": "0.15", + "version": "0.19", "description": "This is needed to display a menu allowing you to choose your own applications. You can replace this with a customised launcher.", "readme": "README.md", "icon": "app.png", @@ -13,6 +13,6 @@ {"name":"launch.app.js","url":"app.js"}, {"name":"launch.settings.js","url":"settings.js"} ], - "data": [{"name":"launch.json"}], + "data": [{"name":"launch.json"},{"name":"launch.cache.json"}], "sortorder": -10 } diff --git a/apps/linuxclock/ChangeLog b/apps/linuxclock/ChangeLog new file mode 100644 index 000000000..3f1ef5c55 --- /dev/null +++ b/apps/linuxclock/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App. +0.02: Performance improvements. \ No newline at end of file diff --git a/apps/linuxclock/README.md b/apps/linuxclock/README.md new file mode 100644 index 000000000..934ed2902 --- /dev/null +++ b/apps/linuxclock/README.md @@ -0,0 +1,13 @@ +# A Linux inspired clock + + +A linux inspired clock which also loads and shows clock_infos . +Simply click left/right to execute another command ;) +With up/down you can select an individual entry and with a click at the +center of the screen you can trigger an action if its supported (e.g. HomeAssistant). + +# Thanks +Icons from by Freepik - Flaticon + +## Creator +- [David Peer](https://github.com/peerdavid). \ No newline at end of file diff --git a/apps/linuxclock/app-icon.js b/apps/linuxclock/app-icon.js new file mode 100644 index 000000000..8a767a209 --- /dev/null +++ b/apps/linuxclock/app-icon.js @@ -0,0 +1 @@ +atob("JiaEAAAAAAAAAAAAD///AAAAAAAAAAAAAAAAAAAAD/////AAAAAAAAAAAAAAAAAAAA//////AAAAAAAAAAAAAAAAAAD///////AAAAAAAAAAAAAAAAAA///////wAAAAAAAAAAAAAAAAAPD/8AD/8AAAAAAAAAAAAAAAAADwD/AA//AAAAAAAAAAAAAAAAAADw/w8P/wAAAAAAAAAAAAAAAAAP////D/8AAAAAAAAAAAAAAAAAD/8AD///8AAAAAAAAAAAAAAAAA/wAAD///AAAAAAAAAAAAAAAAAP8AAP///wAAAAAAAAAAAAAAAADw8P8AD/8AAAAAAAAAAAAAAAAP8AAAAA//8AAAAAAAAAAAAAAADwAAAAAA//AAAAAAAAAAAAAAAP8AAAAAAP//AAAAAAAAAAAAAA/wAAAAAAAP//AAAAAAAAAAAAAP8AAAAAAAD///AAAAAAAAAAAA/wAAAAAAAA///wAAAAAAAAAAAP8AAAAAAAAA///wAAAAAAAAAA/wAAAAAAAAAP//8AAAAAAAAAAP8AAAAAAAAAD///8AAAAAAAAAD/AAAAAAAAAAD///AAAAAAAAAP8AAAAAAAAAAA///wAAAAAAAAD/AAAAAAAAAAAP//8AAAAAAAAA//AAAAAAAAAA////AAAAAAAAAP/wAAAAAAAAD////wAAAAAAAA8A/wAAAAAAAA////8AAAAAAA/wAA/wAAAAAAAPD///8AAAAA/wAAAP/wAAAAAADw//APAAAAAPAAAAAP/wAAAAAA8AAAD/AAAADwAAAAD/8AAAAAD/AAAAD/AAAA8AAAAAD/AAAAAP/wAAAADwAAAPAAAAAADwAAAP//AAAAD/AAAAD/AAAAAA///////wAAD/8AAAAAD///AAAP//////8AAP8AAAAAAAAAD//w/wAAAAAP8P8AAAAAAAAAAAAAD/AAAAAAAP/wAAAAAA") \ No newline at end of file diff --git a/apps/linuxclock/app.js b/apps/linuxclock/app.js new file mode 100644 index 000000000..02676310e --- /dev/null +++ b/apps/linuxclock/app.js @@ -0,0 +1,386 @@ + +/************************************************ + * Includes + */ + const clock_info = require("clock_info"); + const storage = require('Storage'); + const locale = require('locale'); + +/* + * Some vars + */ +var W = g.getWidth(); +var H = g.getHeight(); + + /************************************************ + * Settings + */ + const SETTINGS_FILE = "linuxclock.setting.json"; + let settings = { + menuPosX: 0, + menuPosY: 0, + }; + + let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings; + for (const key in saved_settings) { + settings[key] = saved_settings[key] + } + + + /************************************************ + * Assets + */ + Graphics.prototype.setFontUbuntuMono = function(scale) { + // Actual height 24 (27 - 4) + this.setFontCustom( + atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD+A4AP/ngA/+eAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAA/gAAD+AAAAAAAAAAAAD+AAAP4AAA8AAAAAAAAAAAAAAAAAAAAAAAMGAAAwfgAD/+AB//gAP/YAA/BgAAMH4AA3/gAP/8AD/+AAPwYAADBgAAAAAAAAAAAAAEAAfA4AD+DgAf4GABxwYA+HB8D4OHwBg4YAGDzgAcH8AAgPwAAAcAAAAAAA8AAAH8BgA9wOADBjwAOccAA/3gAA94AAAPeAAD3+AAcc4AHhhgA4H+ADAfwAAAeAAAAAAAAPgADx/AAf/eAD/wYAMHBgAw+GADneYAP4/AAfB8AAA/4AADzgAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/gAAD+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAB//AAP//AD4A+AeAA8DwAB4OAADwQAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAYOAABw8AAeB4ADwD4A+AD//wAH/8AAD/AAAAAAAAAAAAAAAAAAAAAAAAAA4AAABiAAAHcAAAPwAAP8AAA/wAAAPwAAB3AAAGIAAAYAAAAAAAAAAAAAAAAAABgAAAGAAAAYAAABgAAAGAAAP/wAA//AAAGAAAAYAAABgAAAGAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAABOAAAewAAB/AAAH4AAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAYAAABgAAAGAAAAYAAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAB4AAAHgAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAB8AAA/wAAf4AAP8AAH+AAD/AAB/gAAPwAAA4AAAAAAAAAAAAAAAAAAB/AAA//gAH//AA8AeADhg4AMPBgAw8GADhg4APAHgAf/8AA//gAAfwAAAAAAAAAAAAAAAAGAAAAYAYADgBgAcAGAB//4AP//gA//+AAAAYAAABgAAAGAAAAIAAAAAAAAAAAAAAAGADgA4A+ADgH4AMA9gAwHGADA4YAOHBgA/4GAB/AYAD4BgAAAGAAAAAAAAAAAAAAABgA4AOADgA4AGADBgYAMGBgAw4GADjw4AP/ngAfv8AA8fgAAAYAAAAAAAAAAAAA4AAAPgAAD+AAAeYAADhgAA8GAAHgYAA8BgAD//4AP//gAABgAAAGAAAAAAAAAAAAAAAAAADgA/4OAD/gYAP+BgAw4GADDgYAMGDgAweeADA/wAMB+AAABgAAAAAAAAAAAAPAAAH/gAB//AAP4eABzA4AGMBgA4wGADjgYAMOHgAwf8ADA/gAAB4AAAAAAAAAAAAAAAAwAAADAAAAMADgAwB+ADA/4AMP8AAz8AADfAAAPwAAA8AAADgAAAAAAAAAAAAAAHAAD5/AAf38AD344AMHBgAwYGADBwYAOHBgA9+OAB/fwAD5/AAABwAAAAAAAAAAAHgAAA/gYAH+BgA4cGADAw4AMDDgAwMcADgzwAPD+AAf/wAA/+AAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADweAAPB4AA8HgADweAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAADgA8HMADwfwAPB+AA8HwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAA8AAADwAAAPgAAB+AAAGYAAA5wAADDAAAcOAABw4AAGBgAAYGAAAAAAAAAAAAMYAAAxgAADGAAAMYAAAxgAADGAAAMYAAAxgAADGAAAMYAAAxgAADGAAAAAAAAAAAAGBgAAYHAABwYAAHDgAAMMAAA5wAABmAAAH4AAAfgAAA8AAADwAAAGAAAAAAAAAAAAAAAAAAAAAOAAAAwAMADAZ4AMHngAw8eADngAAP8AAAfgAAAYAAAAAAAAAAAAAAAAAAP+AAH/+AA//+AHgB8A4PhwDD/jgMP/GAxwcYDmAxgPYDGAf/8YA//wAAAAAAAAGAAAD4AAD/gAB/wAA/+AAP4YAA8BgADwGAAP8YAAP/gAAH/gAAD/gAAA+AAAAYAAAAAA//+AD//4AP//gAwYGADBgYAMGBgAwYGADjw4AP/DgAfv8AA8fgAAA8AAAAAAAAAAAAfwAAH/wAA//gAHgPAA8AOADgA4AMABgAwAGADAAYAOABgA4AOABAAwAAAAAAAAAAD//4AP//gA//+ADAAYAMABgAwAGADgA4AOADgAcAcAA//gAB/8AAB/AAAAAAAAAAAAAAAAD//4AP//gA//+ADBgYAMGBgAwYGADBgYAMGBgAwYGADBgYAIABgAAAAAAAAAAAAAAA//+AD//4AP//gAwYAADBgAAMGAAAwYAADBgAAMGAAAwYAADAAAAAAAAAAAAAAH8AAB/8AAP/4AB4DwAPADgA4AOADAAYAMABgAwAGADgf4AOD/gAQP+AAAAAAAAAAA//+AD//4AP//gAAYAAABgAAAGAAAAYAAABgAAAGAAA//+AD//4AP//gAAAAAAAAAAAAAAAwAGADAAYAMABgAwAGAD//4AP//gAwAGADAAYAMABgAwAGAAAAAAAAAAAAAAAAAAQAAADAAwAOADAAYAMABgAwAGADAAYAMADgA//8AD//wAP/8AAAAAAAAAAAAAAAAAAAAD//4AP//gAAcAAADwAAAfgAAHvAAA8eAAHg+AA8A8ADgB4AIADgAAACAAAAAAAAAAA//+AD//4AP//gAAAGAAAAYAAABgAAAGAAAAYAAABgAAAGAAAAYAAAAAAAAAAAP/4AP//gA/+AAD+AAAB/AAAA+AAAD4AAB/AAA/gAAD/4AAP//gAH/+AAAAAAAAAAA//+AD//4AP//gAfAAAA+AAAA+AAAA/AAAA/AAAA/AA//+AD//4AP//gAAAAAAAAAAB/8AAP/4AB+PwAOADgA4AOADAAYAMABgA4AOADgA4AH4/AAP/4AAf/AAAAAAAAAAAAAAAAP//gA//+AD//4AMBgAAwGAADAYAAODgAA4OAAB/wAAD+AAAHwAAAAAAAAAAAAD/wAA//wAH4fgA4APADgAcAMAA8AwAD4DgAfgPAD3Afh+MA//wwA/8CAAAAAAAAAAP//gA//+AD//4AMDAAAwMAADAwAAMDgAA4fgAD/vgAH+PgAPwOAAAAYAAAAAAAAAAAAAQAD4DAAfwOAD/AYAOOBgAwYGADBwYAMDDgA4OOADgfwAEB/AAABwAAAAAAAAAAAwAAADAAAAMAAAAwAAADAAAAP//gA//+ADAAAAMAAAAwAAADAAAAMAAAAAAAAAAAAAP/8AA//4AD//wAAADgAAAGAAAAYAAABgAAAGAAAA4AP//AA//4AD//AAAAAAAwAAAD4AAAP8AAAP/AAAD/gAAB/gAAAeAAAB4AAA/gAA/4AA/8AAP+AAA+AAADAAAAAAAAA//wAD//4AAf/gAAD+AAB/AAAPgAAA+AAAB/AAAA/gAB/+AD//4AP/4AAAAAAAAAAAIABgA4AeAD4H4AH5+AAH/gAAH4AAAfgAAH/gAB+fgAPgfgA4AeACAAYAAAAAAgAAADgAAAPgAAAfgAAAfgAAAfgAAAf+AAB/4AAfgAAH4AAB+AAAPgAAA4AAACAAAAAAAAAAAGADAB4AMAfgAwD+ADA+YAMHhgAx8GADPAYAP4BgA+AGADwAYAMABgAAAAAAAAAAAAAAAAAAAAAAAA////D///8MAAAwwAADDAAAMMAAAwwAADAAAAAAAAAAAAAAAAAAAAAAAA4AAAD8AAAH+AAAH/AAAD/gAAB/wAAAf4AAAP8AAAHwAAADAAAAAAAAAAAAAAAAAAAAAAAAwAADDAAAMMAAAwwAADDAAAMP///w////AAAAAAAAAAAAAAAAAAAAAAAAAGAAAA4AAAPgAAD4AAA+AAADwAAAPAAAA+AAAA+AAAA+AAAA4AAABgAAAAAAAAAAMAAAAwAAADAAAAMAAAAwAAADAAAAMAAAAwAAADAAAAMAAAAwAAADAAAAMAAAAwAAAAAAAAAAAAAAAAAAAAAAAOAAAA8AAAB4AAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAD8AAMPwAAxzgADGGAAMYYAAxhgADmGAAPYYAAf/gAA/+AAAAAAAAAAAAAAAAAAAA///gD//+AP//4AA4BgADAGAAMAYAA4DgADgeAAH/wAAP+AAAfwAAAAAAAAAAAAPgAAD/gAAf/AABwcAAOA4AA4DgADAGAAMAYAAwBgADAGAAOA4AAABgAAAAAAAAAAAH8AAA/4AAH/wAA4HgADgOAAMAYAAwBgADgGAH//4A///gD//+AAAAAAAAAAAAAAAAA/AAAP+AAB/8AAOZ4AA5jgADGGAAMYYAAxhgADmGAAH44AAfjgAAeAAAAAAAAAAAAAAAAAMAAAAwAAAf/+AH//4A///gDjAAAMMAAAwwAADDAAAMMAAA4AAABAAAAAAAAAAH8AAA/4cAH/xwA8DjADgOMAMAYwAwBjADAOcAP//wA//+AD//wAAAAAAAAAAAAAAAAAAA///gD//+AP//4AAwAAADAAAAMAAAA4AAAD4AAAH/4AAP/gAAAAAAAAAAAAAAADAAAAMAAAAwAADjAAAPP/gA8//ABj/+AAAA4AAABgAAAGAAAA4AAABAAAAAAAAAAAAAAAAAAAcAMABwAwADADAAMAMAAw8wAHDz//8PP//gY//8AAAAAAAAAAAAAAAAAAAAAAAA///gD//+AP//4AADwAAAfgAADvAAAeeAADw8AAOB4AAwDgACAGAAAAAAAAAADAAAAMAAAAwAAADAAAAP//gA///AD//+AAAA4AAABgAAAGAAAA4AAABAAAAAAAAAAAA//gAD/+AAOAAAAwAAADgAAAP8AAA/wAADAAAAMAAAA4AAAD/+AAH/4AAAAAAAAAAAAAAAA//gAD/+AAP/4AAwAAADAAAAMAAAA4AAAD4AAAH/4AAP/gAAAAAAAAAAAAAAAAfwAAD/gAAf/AADweAAOA4AAwBgADAGAAOA4AA8HgAB/8AAD/gAAH8AAAAAAAAAAAAAAAAD//8AP//wA///ADAOAAMAYAAwBgADgOAAPB4AAf/AAA/4AAB/AAAAAAAAAAAAB/AAAP+AAB/8AAPB4AA4DgADAGAAMAYAAwDgAD//8AP//wA///AAAAAAAAAAAAAAAAAAAAAAAAAf/gAD/+AAP/4AAwAAADAAAAMAAAAwAAADAAAAMAAAAAAAAAAAAAAAAAAAAAAA4OAAHw4AA/hgADOGAAMcYAAxxgADDGAAMO4AA4fAABB8AAAAAAAAAAAAAAAAAAAAAwAAADAAAD//AAP//AA//+AAMA4AAwBgADAGAAMAYAAwDgADAEAAAAAAAAAAAAAAAAP/gAA//AAAA+AAAA4AAABgAAAGAAAAYAA//gAD/+AAP/4AAAAAAAAAAAAAAAA4AAAD8AAAP8AAAH+AAAD+AAAB4AAAHgAAD8AAB/AAA/wAAD8AAAOAAAAAAAADAAAAP+AAA//gAAH+AAAD4AAB+AAAfAAAB8AAAD+AAAB+AAAP4AA//gAD/AAAMAAAAAAAACAGAAMA4AA8HgAB54AAD/AAAH4AAAPgAAD/AAAeeAADw+AAMA4AAgBgAAAAAAAAAAAgADAD4AMAP4AwAf8DAAH8cAAD/gAAD8AAB/AAB/wAA/4AAD8AAAMAAAAAAAAAAAAAAAAAAwDgADAeAAMH4AAw9gADPmAAN4YAA/BgAD4GAAPAYAAwBgAAAAAAAAAAAAAAAAAAAAAYAAABgAAAPAAD///Af/f+D/wf8MAAAwwAADDAAAMMAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///w////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAADDAAAMMAAAwwAADD/w/8H/3/gP//8AAPAAAAYAAABgAAAAAAAAAAAAAAAAADAAAA8AAADgAAAMAAAA4AAADgAAAHAAAAcAAAAwAAAHAAAA8AAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=='), + 32, + atob("Dg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4c"), + 28+(scale<<8)+(1<<16) + ); + return this; + } + + + + /************************************************ + * Menu + */ + var dateMenu = { + name: "date", + img: null, + items: [ + { name: "time", + get: () => ({ text: getTime(), img: null}), + show: function() { dateMenu.items[0].emit("redraw"); }, + hide: function () {} + }, + { name: "day", + get: () => ({ text: getDay(), img: null}), + show: function() { dateMenu.items[2].emit("redraw"); }, + hide: function () {} + }, + { name: "date", + get: () => ({ text: getDate(), img: null}), + show: function() { dateMenu.items[1].emit("redraw"); }, + hide: function () {} + }, + { name: "week", + get: () => ({ text: weekOfYear(), img: null}), + show: function() { dateMenu.items[3].emit("redraw"); }, + hide: function () {} + }, + ] + }; + + var menu = clock_info.load(); + menu = menu.concat(dateMenu); + + // Set draw functions for each item + menu.forEach((menuItm, x) => { + menuItm.items.forEach((item, y) => { + function drawItem() { + item.hide(); + + var info = item.get(); + drawText(item.name, info.text, (y%4)+1); + } + + item.on('redraw', drawItem); + }) + }); + + + // Ensure that our settings are still in range (e.g. app uninstall). Otherwise reset the position it. + if(settings.menuPosX >= menu.length || settings.menuPosY > menu[settings.menuPosX].items.length ){ + settings.menuPosX = 0; + settings.menuPosY = 0; + } + +function canRunMenuItem(){ + var menuEntry = menu[settings.menuPosX]; + var item = menuEntry.items[settings.menuPosY]; + return item.run !== undefined; +} + + +function runMenuItem(){ + var menuEntry = menu[settings.menuPosX]; + var item = menuEntry.items[settings.menuPosY]; + try{ + var ret = item.run(); + if(ret){ + Bangle.buzz(300, 0.6); + } + } catch (ex) { + // Simply ignore it... + } +} + +/************************************************ +* Helper +*/ +function getTime(){ + var date = new Date(); + return twoD(date.getHours())+ ":" + twoD(date.getMinutes()); +} + +function getDate(){ + var date = new Date(); + return twoD(date.getDate()) + "." + twoD(date.getMonth()); +} + +function getDay(){ + var date = new Date(); + return locale.dow(date, true); +} + +function weekOfYear() { + var date = new Date(); + date.setHours(0, 0, 0, 0); + // Thursday in current week decides the year. + date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7); + // January 4 is always in week 1. + var week1 = new Date(date.getFullYear(), 0, 4); + // Adjust to Thursday in week 1 and count number of weeks from date to week1. + return 1 + Math.round(((date.getTime() - week1.getTime()) / 86400000 + - 3 + (week1.getDay() + 6) % 7) / 7); +} + + + +/************************************************ +* Draw +*/ +function draw() { + queueDraw(); + + g.setFontUbuntuMono(); + g.setFontAlign(-1, -1); + + g.clearRect(0,24,W,H); + + drawMainScreen(); +} + + + +function drawMainScreen(){ + // Get menu item based on x + var menuItem = menu[settings.menuPosX]; + var cmd = menuItem.name.slice(0,5).toLowerCase(); + drawCmd(cmd); + + // Draw menu items depending on our y value + drawMenuItems(menuItem); + + // And draw the cursor + drawCursor(); +} + +function drawMenuItems(menuItem) { + var start = parseInt(settings.menuPosY / 4) * 4; + for (var i = start; i < start + 4; i++) { + if (i >= menuItem.items.length) { + continue; + } + lock_input++; + menuItem.items[i].show(); + } +} + +function drawCursor(){ + g.setFontUbuntuMono(); + g.setFontAlign(-1, -1); + g.setColor(g.theme.fg); + + g.clearRect(0, 27 + 28, 15, H); + if(!Bangle.isLocked()){ + g.drawString(">", -2, ((settings.menuPosY % 4) + 1) * 27 + 28); + } +} + +function drawText(key, value, line){ + var x = 15; + var y = line * 27 + 28; + + g.setFontUbuntuMono(); + g.setFontAlign(-1, -1); + g.setColor(g.theme.fg); + + if(key){ + key = (key.toLowerCase() + " ").slice(0, 4) + "|"; + } else { + key = "" + } + + value = String(value).replace("\n", " "); + g.drawString(key + value, x, y); + + lock_input -= 1; +} + + +function drawCmd(cmd){ + var c = 0; + var x = 10; + var y = 28; + + g.setColor("#0f0"); + g.drawString("bjs", x+c, y); + c += g.stringWidth("bjs"); + + g.setColor(g.theme.fg); + g.drawString(":", x+c, y); + c += g.stringWidth(":"); + + g.setColor("#0ff"); + g.drawString("$ ", x+c, y); + c += g.stringWidth("$ "); + + g.setColor(g.theme.fg); + g.drawString(cmd, x+c, y); +} + +function twoD(str){ + return ("0" + str).slice(-2) +} + + +/************************************************ +* Listener +*/ +// timeout used to update every minute +var drawTimeout; + +// schedule a draw for the next minute +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +} + + +// 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; + } +}); + + +Bangle.on('lock', function(isLocked) { + drawCursor(); +}); + + +Bangle.on('charging',function(charging) { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + + settings.menuPosX=0; + settings.menuPosY=0; + + draw(); +}); + +var lock_input = 0; + +Bangle.on('touch', function(btn, e){ + if(lock_input > 0){ + return; + } + lock_input = 0; + + var left = parseInt(g.getWidth() * 0.22); + var right = g.getWidth() - left; + var upper = parseInt(g.getHeight() * 0.22) + 20; + var lower = g.getHeight() - upper; + + var is_upper = e.y < upper; + var is_lower = e.y > lower; + var is_left = e.x < left && !is_upper && !is_lower; + var is_right = e.x > right && !is_upper && !is_lower; + var is_center = !is_upper && !is_lower && !is_left && !is_right; + + var oldYScreen = parseInt(settings.menuPosY/4); + if(is_lower){ + if(settings.menuPosY >= menu[settings.menuPosX].items.length-1){ + return; + } + + Bangle.buzz(40, 0.6); + settings.menuPosY++; + if(parseInt(settings.menuPosY/4) == oldYScreen){ + drawCursor(); + return; + } + } + + if(is_upper){ + if(e.y < 20){ // Reserved for widget clicks + return; + } + + if(settings.menuPosY <= 0){ + return; + } + Bangle.buzz(40, 0.6); + settings.menuPosY--; + settings.menuPosY = settings.menuPosY < 0 ? 0 : settings.menuPosY; + + if(parseInt(settings.menuPosY/4) == oldYScreen){ + drawCursor(); + return; + } + } + + if(is_right){ + Bangle.buzz(40, 0.6); + settings.menuPosX = (settings.menuPosX+1) % menu.length; + settings.menuPosY = 0; + } + + if(is_left){ + Bangle.buzz(40, 0.6); + settings.menuPosY = 0; + settings.menuPosX = settings.menuPosX-1; + settings.menuPosX = settings.menuPosX < 0 ? menu.length-1 : settings.menuPosX; + } + + if(is_center){ + if(!canRunMenuItem()){ + return; + } + runMenuItem(); + } + + draw(); +}); + +E.on("kill", function(){ + try{ + storage.write(SETTINGS_FILE, settings); + } catch(ex){ + // If this fails, we still kill the app... + } +}); + + +/************************************************ +* Startup Clock +*/ +// Show launcher when middle button pressed +Bangle.setUI("clock"); + +// Load and draw widgets +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +// Draw first time +draw(); diff --git a/apps/linuxclock/app.png b/apps/linuxclock/app.png new file mode 100644 index 000000000..3a09cd575 Binary files /dev/null and b/apps/linuxclock/app.png differ diff --git a/apps/linuxclock/metadata.json b/apps/linuxclock/metadata.json new file mode 100644 index 000000000..dfb17a315 --- /dev/null +++ b/apps/linuxclock/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "linuxclock", + "name": "Linux Clock", + "version": "0.02", + "description": "A Linux inspired clock.", + "readme": "README.md", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}, {"url":"screenshot_2.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"linuxclock.app.js","url":"app.js"}, + {"name":"linuxclock.img","url":"app-icon.js","evaluate":true}, + {"name":"linuxclock.settings.js","url":"settings.js"} + ] +} diff --git a/apps/linuxclock/screenshot.png b/apps/linuxclock/screenshot.png new file mode 100644 index 000000000..4bc7f9967 Binary files /dev/null and b/apps/linuxclock/screenshot.png differ diff --git a/apps/linuxclock/screenshot_2.png b/apps/linuxclock/screenshot_2.png new file mode 100644 index 000000000..abeba7a92 Binary files /dev/null and b/apps/linuxclock/screenshot_2.png differ diff --git a/apps/linuxclock/settings.js b/apps/linuxclock/settings.js new file mode 100644 index 000000000..116253fda --- /dev/null +++ b/apps/linuxclock/settings.js @@ -0,0 +1,50 @@ +(function(back) { + const SETTINGS_FILE = "bwclk.setting.json"; + + // initialize with default settings... + const storage = require('Storage') + let settings = { + screen: "Normal", + showLock: true, + hideColon: false, + }; + let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings; + for (const key in saved_settings) { + settings[key] = saved_settings[key] + } + + function save() { + storage.write(SETTINGS_FILE, settings) + } + + var screenOptions = ["Normal", "Dynamic", "Full"]; + E.showMenu({ + '': { 'title': 'BW Clock' }, + '< Back': back, + 'Screen': { + value: 0 | screenOptions.indexOf(settings.screen), + min: 0, max: 2, + format: v => screenOptions[v], + onchange: v => { + settings.screen = screenOptions[v]; + save(); + }, + }, + 'Show Lock': { + value: settings.showLock, + format: () => (settings.showLock ? 'Yes' : 'No'), + onchange: () => { + settings.showLock = !settings.showLock; + save(); + }, + }, + 'Hide Colon': { + value: settings.hideColon, + format: () => (settings.hideColon ? 'Yes' : 'No'), + onchange: () => { + settings.hideColon = !settings.hideColon; + save(); + }, + } + }); + }) diff --git a/apps/medicalinfo/ChangeLog b/apps/medicalinfo/ChangeLog new file mode 100644 index 000000000..e8739a121 --- /dev/null +++ b/apps/medicalinfo/ChangeLog @@ -0,0 +1 @@ +0.01: Initial Medical Information application! diff --git a/apps/medicalinfo/README.md b/apps/medicalinfo/README.md new file mode 100644 index 000000000..6dd19d4c6 --- /dev/null +++ b/apps/medicalinfo/README.md @@ -0,0 +1,27 @@ +# Medical Information + +This app displays basic medical information, and provides a common way to set up the `medicalinfo.json` file, which other apps can use if required. + +## Medical information JSON file + +When the app is loaded from the app loader, a file named `medicalinfo.json` is loaded along with the javascript etc. +The file has the following contents: + +``` +{ + "bloodType": "", + "height": "", + "weight": "", + "medicalAlert": [ "" ] +} +``` + +## Medical information editor + +Clicking on the download icon of `Medical Information` in the app loader invokes the editor. +The editor downloads and displays the current `medicalinfo.json` file, which can then be edited. +The edited `medicalinfo.json` file is uploaded to the Bangle by clicking the `Upload` button. + +## Creator + +James Taylor ([jt-nti](https://github.com/jt-nti)) diff --git a/apps/medicalinfo/app-icon.js b/apps/medicalinfo/app-icon.js new file mode 100644 index 000000000..1ae7916fb --- /dev/null +++ b/apps/medicalinfo/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwg+7kUiCykCC4MgFykgDIIXUAQgAMiMRiREBC4YABkILBCxEBC4pHCC4kQFxIXEAAgXCGBERif/+QXHl//mIXJj//+YXHn//+IXL/8yCwsjBIIXNABIX/C63d7oDB+czmaPPC7hHR/oWBAAPfC65HRC7qnXX/4XDABAXkIIQAFI5wXXL/5f/L/5fvC9sTC5cxC5IAOC48BCxsQC44wOCxAArA")) diff --git a/apps/medicalinfo/app.js b/apps/medicalinfo/app.js new file mode 100644 index 000000000..9c4941744 --- /dev/null +++ b/apps/medicalinfo/app.js @@ -0,0 +1,61 @@ +const medicalinfo = require('medicalinfo').load(); +// const medicalinfo = { +// bloodType: "O+", +// height: "166cm", +// weight: "73kg" +// }; + +function hasAlert(info) { + return (Array.isArray(info.medicalAlert)) && (info.medicalAlert[0]); +} + +// No space for widgets! +// TODO: no padlock widget visible so prevent screen locking? + +g.clear(); +const bodyFont = g.getFonts().includes("12x20") ? "12x20" : "6x8:2"; +g.setFont(bodyFont); + +const title = hasAlert(medicalinfo) ? "MEDICAL ALERT" : "Medical Information"; +var lines = []; + +lines = g.wrapString(title, g.getWidth() - 10); +var titleCnt = lines.length; +if (titleCnt) lines.push(""); // add blank line after title + +if (hasAlert(medicalinfo)) { + medicalinfo.medicalAlert.forEach(function (details) { + lines = lines.concat(g.wrapString(details, g.getWidth() - 10)); + }); + lines.push(""); // add blank line after medical alert +} + +if (medicalinfo.bloodType) { + lines = lines.concat(g.wrapString("Blood group: " + medicalinfo.bloodType, g.getWidth() - 10)); +} +if (medicalinfo.height) { + lines = lines.concat(g.wrapString("Height: " + medicalinfo.height, g.getWidth() - 10)); +} +if (medicalinfo.weight) { + lines = lines.concat(g.wrapString("Weight: " + medicalinfo.weight, g.getWidth() - 10)); +} + +lines.push(""); + +// TODO: display instructions for updating medical info if there is none! + +E.showScroller({ + h: g.getFontHeight(), // height of each menu item in pixels + c: lines.length, // number of menu items + // a function to draw a menu item + draw: function (idx, r) { + // FIXME: in 2v13 onwards, clearRect(r) will work fine. There's a bug in 2v12 + g.setBgColor(idx < titleCnt ? g.theme.bg2 : g.theme.bg). + setColor(idx < titleCnt ? g.theme.fg2 : g.theme.fg). + clearRect(r.x, r.y, r.x + r.w, r.y + r.h); + g.setFont(bodyFont).drawString(lines[idx], r.x, r.y); + } +}); + +// Show launcher when button pressed +setWatch(() => load(), process.env.HWVERSION === 2 ? BTN : BTN3, { repeat: false, edge: "falling" }); diff --git a/apps/medicalinfo/app.png b/apps/medicalinfo/app.png new file mode 100644 index 000000000..16204ea89 Binary files /dev/null and b/apps/medicalinfo/app.png differ diff --git a/apps/medicalinfo/interface.html b/apps/medicalinfo/interface.html new file mode 100644 index 000000000..9376be32f --- /dev/null +++ b/apps/medicalinfo/interface.html @@ -0,0 +1,135 @@ + + + + + + + +
+ + + + + +

+
+    
+    
+  
+
diff --git a/apps/medicalinfo/lib.js b/apps/medicalinfo/lib.js
new file mode 100644
index 000000000..683005359
--- /dev/null
+++ b/apps/medicalinfo/lib.js
@@ -0,0 +1,21 @@
+const storage = require('Storage');
+
+exports.load = function () {
+  const medicalinfo = storage.readJSON('medicalinfo.json') || {
+    bloodType: "",
+    height: "",
+    weight: "",
+    medicalAlert: [""]
+  };
+
+  // Don't return anything unexpected
+  const expectedMedicalinfo = [
+    "bloodType",
+    "height",
+    "weight",
+    "medicalAlert"
+  ].filter(key => key in medicalinfo)
+    .reduce((obj, key) => (obj[key] = medicalinfo[key], obj), {});
+
+  return expectedMedicalinfo;
+};
diff --git a/apps/medicalinfo/medicalinfo.json b/apps/medicalinfo/medicalinfo.json
new file mode 100644
index 000000000..8b49725cb
--- /dev/null
+++ b/apps/medicalinfo/medicalinfo.json
@@ -0,0 +1,6 @@
+{
+    "bloodType": "",
+    "height": "",
+    "weight": "",
+    "medicalAlert": [ "" ]
+}
diff --git a/apps/medicalinfo/metadata.json b/apps/medicalinfo/metadata.json
new file mode 100644
index 000000000..f1a0c145f
--- /dev/null
+++ b/apps/medicalinfo/metadata.json
@@ -0,0 +1,20 @@
+{ "id": "medicalinfo",
+  "name": "Medical Information",
+  "version":"0.01",
+  "description": "Provides 'medicalinfo.json' used by various health apps, as well as a way to edit it from the App Loader",
+  "icon": "app.png",
+  "tags": "health,medical",
+  "type": "app",
+  "supports" : ["BANGLEJS","BANGLEJS2"],
+  "readme": "README.md",
+  "screenshots": [{"url":"screenshot_light.png"}],
+  "interface": "interface.html",
+  "storage": [
+    {"name":"medicalinfo.app.js","url":"app.js"},
+    {"name":"medicalinfo.img","url":"app-icon.js","evaluate":true},
+    {"name":"medicalinfo","url":"lib.js"}
+  ],
+  "data": [
+    {"name":"medicalinfo.json","url":"medicalinfo.json"}
+  ]
+}
diff --git a/apps/medicalinfo/screenshot_light.png b/apps/medicalinfo/screenshot_light.png
new file mode 100644
index 000000000..42970f9fc
Binary files /dev/null and b/apps/medicalinfo/screenshot_light.png differ
diff --git a/apps/messages/ChangeLog b/apps/messages/ChangeLog
index da3b3ab5c..166ff64ae 100644
--- a/apps/messages/ChangeLog
+++ b/apps/messages/ChangeLog
@@ -70,4 +70,6 @@
 0.51: Emit "message events"
       Setting to hide widget
       Add custom event handlers to prevent default app form loading
-      Move WIDGETS.messages.buzz() to require("messages").buzz()
\ No newline at end of file
+      Move WIDGETS.messages.buzz() to require("messages").buzz()
+0.52: Fix require("messages").buzz() regression
+      Fix background color in messages list after one unread message is shown
diff --git a/apps/messages/app.js b/apps/messages/app.js
index 20fa8aaa3..f6226d178 100644
--- a/apps/messages/app.js
+++ b/apps/messages/app.js
@@ -68,8 +68,7 @@ function saveMessages() {
 
 function showMapMessage(msg) {
   active = "map";
-  var m;
-  var distance, street, target, eta;
+  var m, distance, street, target, eta;
   m=msg.title.match(/(.*) - (.*)/);
   if (m) {
     distance = m[1];
@@ -379,7 +378,7 @@ function checkMessages(options) {
     draw : function(idx, r) {"ram"
       var msg = MESSAGES[idx];
       if (msg && msg.new) g.setBgColor(g.theme.bgH).setColor(g.theme.fgH);
-      else g.setColor(g.theme.fg);
+      else g.setBgColor(g.theme.bg).setColor(g.theme.fg);
       g.clearRect(r.x,r.y,r.x+r.w, r.y+r.h);
       if (!msg) return;
       var x = r.x+2, title = msg.title, body = msg.body;
diff --git a/apps/messages/lib.js b/apps/messages/lib.js
index ed71ec04b..0188342ee 100644
--- a/apps/messages/lib.js
+++ b/apps/messages/lib.js
@@ -159,7 +159,7 @@ exports.buzz = function(msgSrc) {
     exports.buzzTimeout = setTimeout(()=>require("buzz").pattern(pattern), repeat*1000);
     var vibrateTimeout = (require('Storage').readJSON("messages.settings.json", true) || {}).vibrateTimeout;
     if (vibrateTimeout===undefined) vibrateTimeout=60;
-    if (vibrateTimeout && !exports.stopTimeout) exports.stopTimeout = setTimeout(exports.stopTimeout, vibrateTimeout*1000);
+    if (vibrateTimeout && !exports.stopTimeout) exports.stopTimeout = setTimeout(exports.stopBuzz, vibrateTimeout*1000);
   }
   return require("buzz").pattern(pattern);
 };
diff --git a/apps/messages/metadata.json b/apps/messages/metadata.json
index 057a95026..a31c21e03 100644
--- a/apps/messages/metadata.json
+++ b/apps/messages/metadata.json
@@ -1,7 +1,7 @@
 {
   "id": "messages",
   "name": "Messages",
-  "version": "0.51",
+  "version": "0.52",
   "description": "App to display notifications from iOS and Gadgetbridge/Android",
   "icon": "app.png",
   "type": "app",
diff --git a/apps/ncfrun/metadata.json b/apps/ncfrun/metadata.json
deleted file mode 100644
index 831ae3d4e..000000000
--- a/apps/ncfrun/metadata.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
-  "id": "ncfrun",
-  "name": "NCEU 5K Fun Run",
-  "version": "0.01",
-  "description": "Display a map of the NodeConf EU 2019 5K Fun Run route and your location on it",
-  "icon": "nceu-funrun.png",
-  "tags": "health",
-  "supports": ["BANGLEJS"],
-  "storage": [
-    {"name":"ncfrun.app.js","url":"nceu-funrun.js"},
-    {"name":"ncfrun.img","url":"nceu-funrun-icon.js","evaluate":true}
-  ]
-}
diff --git a/apps/ncfrun/nceu-funrun-icon.js b/apps/ncfrun/nceu-funrun-icon.js
deleted file mode 100644
index a13452a8b..000000000
--- a/apps/ncfrun/nceu-funrun-icon.js
+++ /dev/null
@@ -1 +0,0 @@
-require("heatshrink").decompress(atob("mEwwgurglEC6tDmYYUgkzAANAFygXKKYIADBwgXDkg8LBwwXMoQXEH4hHNC4s0O6BfECAKhDHYKnOghCB3cga6dEnYYBaScC2cznewC6W7OQU7BYyIFAAhFBAAYwGC5RFBC5QAJlY0FSIQAMkUjGgrTJRYoXFPQIXGLg8iAAJFDDgIXGgYXJGAWweQJHOC4jtBC6cidgQXUUQQXBogACDYR3HmQXHAAYzKU4IACC48kJBwFBgg7EMZYwDJAReDoh5PC4QARJAoARJAYXTJChtDoSgNAAaeEAAU0C5wqCC4q5LOYYvWgjOEaJ4AGoZGQPY6OPFw0yF34uFRlYXCFykAoQuVeIQWUAB4A="))
diff --git a/apps/ncfrun/nceu-funrun.js b/apps/ncfrun/nceu-funrun.js
deleted file mode 100644
index 30e587188..000000000
--- a/apps/ncfrun/nceu-funrun.js
+++ /dev/null
@@ -1,140 +0,0 @@
-var coordScale = 0.6068;
-var coords = new Int32Array([-807016,6918514,-807057,6918544,-807135,6918582,-807238,6918630,-807289,6918646,-807308,6918663,-807376,6918755,-807413,6918852,-807454,6919002,-807482,6919080,-807509,6919158,-807523,6919221,-807538,6919256,-807578,6919336,-807628,6919447,-807634,6919485,-807640,6919505,-807671,6919531,-807703,6919558,-807760,6919613,-807752,6919623,-807772,6919643,-807802,6919665,-807807,6919670,-807811,6919685,-807919,6919656,-807919,6919645,-807890,6919584,-807858,6919533,-807897,6919503,-807951,6919463,-807929,6919430,-807916,6919412,-807907,6919382,-807901,6919347,-807893,6919322,-807878,6919292,-807858,6919274,-807890,6919232,-807909,6919217,-807938,6919206,-807988,6919180,-807940,6919127,-807921,6919100,-807908,6919072,-807903,6919039,-807899,6919006,-807911,6918947,-807907,6918936,-807898,6918905,-807881,6918911,-807874,6918843,-807870,6918821,-807854,6918775,-807811,6918684,-807768,6918593,-807767,6918593,-807729,6918516,-807726,6918505,-807726,6918498,-807739,6918481,-807718,6918465,-807697,6918443,-807616,6918355,-807518,6918263,-807459,6918191,-807492,6918162,-807494,6918147,-807499,6918142,-807500,6918142,-807622,6918041,-807558,6917962,-807520,6917901,-807475,6917933,-807402,6917995,-807381,6918024,-807361,6918068,-807323,6918028,-807262,6918061,-807263,6918061,-807159,6918116,-807148,6918056,-807028,6918063,-807030,6918063,-806979,6918068,-806892,6918090,-806760,6918115,-806628,6918140,-806556,6918162,-806545,6918175,-806531,6918173,-806477,6918169,-806424,6918180,-806425,6918180,-806367,6918195,-806339,6918197,-806309,6918191,-806282,6918182,-806248,6918160,-806225,6918136,-806204,6918107,-806190,6918076,-806169,6917968,-806167,6917953,-806157,6917925,-806140,6917896,-806087,6917839,-806071,6917824,-805969,6917904,-805867,6917983,-805765,6918063,-805659,6918096,-805677,6918131,-805676,6918131,-805717,6918212,-805757,6918294,-805798,6918397,-805827,6918459,-805877,6918557,-805930,6918608,-805965,6918619,-806037,6918646,-806149,6918676,-806196,6918685,-806324,6918703,-806480,6918735,-806528,6918738,-806644,6918712,-806792,6918667,-806846,6918659,-806914,6918654,-806945,6918661,-806971,6918676,-806993,6918689,-806992,6918692,-807065,6918753,-807086,6918786,-807094,6918788,-807102,6918795,-807104,6918793,-807107,6918799,-807102,6918802,-807112,6918812,-807106,6918815,-807115,6918826,-807120,6918823,-807132,6918841,-807141,6918850,-807151,6918841,-807170,6918832,-807193,6918813,-807222,6918775,-807246,6918718,-807250,6918694,-807264,6918637,-807238,6918630,-807148,6918587,-807057,6918544,-806948,6918463]);
-
-var min = {"x":-807988,"y":6917824};
-var max = {"x":-805659,"y":6919685};
-var gcoords = new Uint8Array(coords.length);
-var coordDistance = new Uint16Array(coords.length/2);
-
-var PT_DISTANCE = 30; // distance to a point before we consider it complete
-
-function toScr(p) {
-  return {
-    x : 10 + (p.x-min.x)*100/(max.x-min.x),
-    y : 230 - (p.y-min.y)*100/(max.y-min.y)
-  };
-}
-
-var last;
-var totalDistance = 0;
-for (var i=0;i {
-      require('Storage').write('ncstart.json', {welcomed: true})
-      load('ncstart.app.js')
-    })
-  }
-})()
diff --git a/apps/ncstart/metadata.json b/apps/ncstart/metadata.json
deleted file mode 100644
index d2b3e2196..000000000
--- a/apps/ncstart/metadata.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
-  "id": "ncstart",
-  "name": "NCEU Startup",
-  "version": "0.06",
-  "description": "NodeConfEU 2019 'First Start' Sequence",
-  "icon": "start.png",
-  "tags": "start,welcome",
-  "supports": ["BANGLEJS"],
-  "storage": [
-    {"name":"ncstart.app.js","url":"start.js"},
-    {"name":"ncstart.boot.js","url":"boot.js"},
-    {"name":"ncstart.settings.js","url":"settings.js"},
-    {"name":"ncstart.img","url":"start-icon.js","evaluate":true},
-    {"name":"nc-bangle.img","url":"start-bangle.js","evaluate":true},
-    {"name":"nc-nceu.img","url":"start-nceu.js","evaluate":true},
-    {"name":"nc-nfr.img","url":"start-nfr.js","evaluate":true},
-    {"name":"nc-nodew.img","url":"start-nodew.js","evaluate":true},
-    {"name":"nc-tf.img","url":"start-tf.js","evaluate":true}
-  ],
-  "data": [{"name":"ncstart.json"}]
-}
diff --git a/apps/ncstart/settings.js b/apps/ncstart/settings.js
deleted file mode 100644
index 560fad8ba..000000000
--- a/apps/ncstart/settings.js
+++ /dev/null
@@ -1,14 +0,0 @@
-(function(back) {
-  let settings = require('Storage').readJSON('ncstart.json', 1)
-    || require('Storage').readJSON('setting.json', 1) || {}
-  E.showMenu({
-    '': { 'title': 'NCEU Startup' },
-    'Run on Next Boot': {
-      value: !settings.welcomed,
-      format: v => v ? 'OK' : 'No',
-      onchange: v => require('Storage').write('ncstart.json', {welcomed: !v}),
-    },
-    'Run Now': () => load('ncstart.app.js'),
-    '< Back': back,
-  })
-})
diff --git a/apps/ncstart/start-bangle.js b/apps/ncstart/start-bangle.js
deleted file mode 100644
index 26f38ae14..000000000
--- a/apps/ncstart/start-bangle.js
+++ /dev/null
@@ -1 +0,0 @@
-require("heatshrink").decompress(atob("s8wxH+AH4AQ/4AJJX5mmM/5m/AH5m/M34A/M35l/M35mqM/5m/AH5m/M34A/MqQKQJm5laOh7kNM35MGbiQxLM9osWIiZnGDI5m/VTBm/MsrOGM35maB4xm/MsoZFORZm/Fq5mDAAwUKBhAHBDJYLGAY4rOPShmRF44TIIoqlJCIxmKEZLMSBxY1GE5RTIJpwYSP5hmQZxodKLBKpIDBQZHMxS4MM1IKCMzKNQHJJmtFwbbUMy4AIM35mcJR5mbLCo1GZrxLOLZ6BMH5wOHMyAYRSRLOWGRY+MAxRmODCZeNMyLNMAA4TIBgpmPFA4YMHBZnPFIp/cADa0cC9Zm2J5YkKMtgsIGjZRTCYLMsFow0dDqJluGAgzhEJwxiAGpYLMn70hAA5N/M34A/M35mzJn5m/AH5nNJf5m/AH5m/M34A/M35m/MpgA="))
diff --git a/apps/ncstart/start-icon.js b/apps/ncstart/start-icon.js
deleted file mode 100644
index 0302cadbc..000000000
--- a/apps/ncstart/start-icon.js
+++ /dev/null
@@ -1 +0,0 @@
-require("heatshrink").decompress(atob("mEwxH+AHMPADQv/F+YxZYtb1wFto7SEbwwQBIsen0/ADU+jxfOjwtbAAYwDWZVWF79WfBAvEq4vfq4vIGQgviR44AEFz4vEGRQvnGA4v/F79YX9IHEq4aKh//jwvRrBcHG4ovL/4ABB5gAFRAwvVGIQveoAAIF4oABq0/CZIACF8BiBrAvTGIoaKF5AABIpVXd44AFJBQvKh4vOGBIvVL54vdX5iPhqztLoFYFpYvSh8/FxgABFpYvQRRgveoEP/8eFqAvbACi/CeA4IDP6IvUGIYGEF+EMADwvJR4ovmdoovnFoowDF8QsIF4dZF79ZF5RpCj1AFztAjy7JAAgwdFwbAFFwwAmF/4vhGFrxLFkoAvA="))
diff --git a/apps/ncstart/start-nceu.js b/apps/ncstart/start-nceu.js
deleted file mode 100644
index 89a9850cc..000000000
--- a/apps/ncstart/start-nceu.js
+++ /dev/null
@@ -1 +0,0 @@
-require("heatshrink").decompress(atob("o9HxH+AEOAwAkiIkIADIv5CEI/4/IJHbNLbPA4Iv1+JHREIwkmk2EJBBE2IoUnIwJHBCx5GoBA2DIgQACBw5G3aQQADwRG+wEmagQCBvxGufoQpDFxOCI4YNIDgxNeD4gDHCY+EwgMKBQIjGJDJlHA4YlKvzRHDRZHZDJQkMBZojVECb+OHJgkOZ6w6KCJAHJCgY1dK5wPDCg4GICYjDZBY9+vxGMArItLeRgWDwOEwmBJA5Ggv2GlMMwJGTwRFBI5JGfv2HlIACwRGRwBFDAAIUGIz+FIYMMI4R0CIxzSCRwhaMIBy2FAAaMBhmHI4QjIRqwUFIxxFJOgLTDlMGRqJHFwF+CpAWDIxgwJBgN+aoSMEIyAGBweDXBg6FABIWLAgOCw+GMhRGKByI9IIxYtQIywaJC5YTTIzwRGOyQqTIzLGNCTJGgXqIRTIzILIIzQvUI5a4EBgh6TDI7dKZJo7IAwQLFIzAjKIhwQGChBvMEhojLIqIjGBaZGPEbppOEerrLBYpGVEZrVOBpJjJIzCHNcpoqPI6gaUIywfSCLJGgXBYSZIzwRFCxoSGFSJGYCA4XLCRArQIywOJYxDPLFqA3OwFPp4HCy4lKHogAIM5uulukMIxGNy1MAAWW2JENFBJIMv8B0ksAAQQDIx2AptMpoCCChZGQGROYIocslsBIyGVIQNOp5HByhaMIxj9IAAWMIYUtRwiNPaIKNCpgUGIB4FNAAMXRq/+yhDBAAOUtJGlgKOCAAOvCJRGH2OVp1OypFGI0BHB0jUBzCMCIyAABtJEHI0RICIgYRMJBBGMCg4GICYgnPCBhHPBwQSIA5IUDGpxWOJBwgLfpgkOIhwVOEBj9WIipsKA4YiKgMBERojIIqphHAgYjKy+n1VpTJYjIADZlGEpOVlwABhTJKRL4oHFxIIEIgUKlula44/hShwIG1RFB02lJQJVII2zTC0iNBhVpI24vGgOmlpIBl2WagwWIJGFp1UKhRFGImI0FGouAaIoPIJGQMWJG5E7H5BE/I4pF/JA4kiA"))
diff --git a/apps/ncstart/start-nfr.js b/apps/ncstart/start-nfr.js
deleted file mode 100644
index 2a0ad70ea..000000000
--- a/apps/ncstart/start-nfr.js
+++ /dev/null
@@ -1 +0,0 @@
-require("heatshrink").decompress(atob("5EuxH+AAkPABIQFABIaKDiIA/AH6qaVpwbbAH4A/YzysMDbYA/AH7GfVhbHgChrr0MT5FoTDobOQijH/Y/6aYcqzH/Y/5EeZDLHmFxTH/Y/4TVY84uJY/7H7TibHuC4rH5XmiHRZC7HpDAjSQF5QpJCgYGJY6Y8MFR4bJBSrITY9RNJb6LFNY5ALFP6CsVY55PQTzDH6MhrGPY7opYY7IZFAgqfRY9xzIWx7GQY6QsYFTTHQZDLHlL44ONDxIfJdKS3PA54qSCRL1MWpDIRY8yLNCg5FICB7ZMHZwrKaB4bQEpTHZH5bHgRhiZNbCTZSY5qBNHiDHZZCbHsOZjHPRSTHYOZbHyZDLH/Y8pQRY+zIYY8xPLG6bHsDJjHuUiTbQdTjHfQBjHYVaLHyUqbHoKJC2KCBgRDBA7HeThbHvZETHdVxKKPTkzISfJLHpZELHeOZLGOY9g8OY+TIgY74OJLqDHqFZIMJY/7HuFxRcPYJbHeXi7HUKAqGYCSgdRAH4A/XC7IdY/4A/Y9rIZY/4A/Y/7H/AH7H2ZDDH/AH7HvZC7H/SMrH/Io7IZDCoVIBgwNFBSA7JBRoZOJ5jboY6IOBY9oWKDpYLFApZkNH6YIHJ5BMNY97IZY6yvTTJCGRBwQRIExYVKB4zH+ZDDHpBQ7HgH5Q+QY/7IYY9KDJY6QeKY6xDOY/7H7BhRiPCRQGHA4SsRCJDH4ZDJqUfpQiIBR6UNDRISQOJ52TY9DIYNSyvSIZLfOAxoaIY/7HVZC4SQQBSJUC57HTDIw9QGZzH/Y7xmINyTHTAAwfKHyzH/OBTH3CRg1LYxAUFD5Y+QY9RXLLxQaWY6yIYY6g5SH57kHY9StRcbZPQQJivRC6AKJEBpGHBxrH/DcbHUEpQKQBojSPH5gpIXx7HjVp4caJkbjRGv4AkA=="))
diff --git a/apps/ncstart/start-nodew.js b/apps/ncstart/start-nodew.js
deleted file mode 100644
index 287d49a1b..000000000
--- a/apps/ncstart/start-nodew.js
+++ /dev/null
@@ -1 +0,0 @@
-require("heatshrink").decompress(atob("5E1xH+AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A6hEICD4A/AH7FlY6TJ/AH7G1Y6bI/AH7FyY6rJ/AH7GyY6zI/AH7H/Y/4A/Y3DHXg5g/AH4Ak5jH/AH4A/Y9VXq5l/AH4Ag47HjZAJm/AH7GgY5S9KZBjHDZH4A/Y9S5KBxrH/AH7GkY5DGOCIrHJZA4pGBiQAPKpAQLHKQmPADRSQY6LFQCZLHPQJjIeQqwKVHTLHoEpDISY4rIGJRbH/IjBYXC67GCY5LGRY7CCaY8hEPV87Ha4zHEYyoXGY6SOKY9IQJIhRDUY+YdEY64YCAgTIBY6CDPMBxRIDpAvMIaZALV5z/NFRrHH5glGYyqOFY4LIJDJoHKaZolPMZIRMUZAyLHxotLLJzHJ4zGBY8TICY6KXJO6wdQWiJCHGRp+QJaTINYoRbQY6bICZINRY8RJQDhowPY8RMYY5YABgR9US6MHgIwGJ5QMLE44GIURY4NBSoyKIZQRObhrIMg7HkgQvIJBapSBzrBPCBhdJY5w+NeBgAFzO93rIFY8AFBxmGwydKFxSFOMJR6JFZhXLIKbHVPhbHPZALJBZA7HcAgLFBY5qFYY+KsLY/DICY4rIWC4kC/2MY6CGJOZjWPRBy8KY95MHY62ZAoLICY64/G/zGIMRxcQdB7HWBZqeQEZxcIY7e9Y4ZMHY/AwJIByrOY7JzLCJAbNY8jITCozHVURqDRDrY/MGSDHWPhTHOZAbHFZCgxHY4gxGIJbrLT6AQIDiRLQOp7ITGBbHcZB4wKY5o+IOxwWLBoQdUY54zMCJTTQBQ6GSZAjHGZCJCLY5IA/AH4AWY5L7LBpzHDNH4A/ZEDHIXQoALaZLH/AH7HsZB4WHgTHCM34A/AELHjY34A/AErHhAH4A/AEzH/AH4A/Y8xe/AH7IwCsgA/AH7IiCkYA/AH7IjCcQA/AH7JkCMAA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AHIA="))
diff --git a/apps/ncstart/start-tf.js b/apps/ncstart/start-tf.js
deleted file mode 100644
index d09185caf..000000000
--- a/apps/ncstart/start-tf.js
+++ /dev/null
@@ -1 +0,0 @@
-require("heatshrink").decompress(atob("4M7ghC/AH4A/AEcBiMQAgY+3iIACIAQhcAgYjdTzZgfYH5giYHo5BIIQGDAQYLHgMQBoZZFYAoYENGREDGgI6BAYYMDBYrXDA4RTDAobnFLgbNEYF5LFVAIGCAghsEBQYMFYAoMEC4TAxAAg5DLoqiCZQZ2CAQa2DYAiGFA4bAvXYj6DAIZkEVYizHLYhyDNoaGCL9zAEHw5aGOIwHJYAqBFL96zCHxJsEKwcBYAx5GYA4SCFobAuMohoDAQ4UCKgYCEYBR7FYGqsDIIwLFYAzUFYAhbEL+IyGIojAFBYbAEKYZYHCgghEYGoGFWoQGFJIYHBgRZDbQpsFYGiNuIP4AedooA/L/4A5gJf/AH4A/AH4A/AH4A/AH4A/AH4A/ACsN7oAJG+oJC6AoaL5QmbG7PQTLrA/YH7A/YH7AhFgxbrYA4JQFkTArWwzAlEQhnCA4bPDBQwLDHwwJH5oGBEAydNAwQHDEgggDcaJBDEY4JDJoYSEH5A7GBAbAPHg5aHCQoiSEYpNHBIxpIBIQHGTpwzGGQJlMESZCIFowTNCQLAVSZAXFGQnNL5AiNBJAeBBIYsDNJIJHTpwyMCQhGEQQwsFYo7kGAoQJDEQQyDS5ChDCgxRJGRJ4DUIpAFYBQiFJgwUGBIr3IIAy5EaJgyNcgoTGcZZCFWwxMLboxqLYBoUJGw5GIYBaSGFoo3GLApAOYCAUJQYycLYBCSHDIoUJYBfdYDwKDEwQFCBAbAfIwwHEFA6rKTpLAPEoQCDYEBgIFgzAMHwzAOXpDAkMIxALYD4wEAozAOaA7AKMIxAJVZjAWApLAOBxasGEYYZCIAyrOYCQlBYC4jGhpHCYBBpJAYSrSTp4FELQZmFYBoRDBQjAMGwvcHQYiEdBDANHgpTFApbALaYgWERpISGHYoJFYCo8JVBKAHFg5COAoY2JBI65IYBofHOYZmIYBxgGNIr4KSxJJHYCQfGCQhmIYBwjFPZDVKQg53IYCI8IBIgFIERgjEPZLVJEhHcAwUMYCo8IYBIfGAH4A/AHw"))
diff --git a/apps/ncstart/start.js b/apps/ncstart/start.js
deleted file mode 100644
index d2d713cb2..000000000
--- a/apps/ncstart/start.js
+++ /dev/null
@@ -1,120 +0,0 @@
-g.setFontAlign(1, 1, 0);
-const d = g.getWidth() - 18;
-function c(a) {
-  return {
-    width: 8,
-    height: a.length,
-    bpp: 1,
-    buffer: (new Uint8Array(a)).buffer
-  };
-}
-
-function welcome() {
-  var welcomes = [
-    'Welcome',
-    'Failte',
-    'Bienvenue',
-    'Willkommen',
-    'Bienvenido'
-  ];
-  function next() {
-    var n = welcomes.shift();
-    E.showMessage(n);
-    g.drawImage(c([0,8,12,14,255,14,12,8]),d,116);
-    welcomes.push(n);
-  }
-  return new Promise((res) => {
-    next();
-    var i = setInterval(next, 2000);
-    setWatch(() => {
-      clearInterval(i);
-      clearWatch();
-      E.showMessage('Loading...');
-      res();
-    }, BTN2, {repeat:false});
-  });
-}
-
-function logos() {
-  var logos = [
-    ['nfr', 20, 90, ()=>{}],
-    ['nceu', 20, 90, ()=>{
-      g.setFont("6x8", 2);
-      g.setColor(0,0,1);
-      g.drawString('Welcome To', 160, 110);
-      g.drawString('NodeConfEU', 160, 130);
-      g.drawString('2019', 200, 150);
-    }],
-    ['bangle', 70, 90, ()=>{}],
-    ['nodew', 20, 90, ()=>{}],
-    ['tf', 24, 90, ()=>{}],
-  ];
-  function next() {
-    var n = logos.shift();
-    var img = require("Storage").read("nc-"+n[0]+".img");
-    g.clear();
-    g.drawImage(img, n[1], n[2]);
-    n[3]();
-    g.drawImage(c([0,8,12,14,255,14,12,8]),d,116);
-    logos.push(n);
-  }
-  return new Promise((res) => {
-    next();
-    var i = setInterval(next, 2000);
-    setWatch(() => {
-      clearInterval(i);
-      clearWatch();
-      res();
-    }, BTN2, {repeat:false});
-  });
-}
-
-function info() {
-  var slides = [
-    () => E.showMessage('Visit\nnodewatch.dev\nfor info'),
-    () => E.showMessage('Visit\nbanglejs.com/apps\nfor apps'),
-    () => E.showMessage('Remember\nto charge\nyour watch!'),
-    () => {
-      g.clear();
-      g.setFont('6x8',2);
-      g.setColor(1,1,1);
-      g.drawImage(c([0,8,12,14,255,14,12,8]),d,40);
-      g.drawImage(c([0,8,12,14,255,14,12,8]),d,194);
-      g.drawImage(c([0,8,12,14,255,14,12,8]),d,116);
-      g.drawString('Menu Up', d - 50, 42);
-      g.drawString('Select', d - 40, 118);
-      g.drawString('Menu Down', d - 60, 196);
-    },
-    () => {
-      g.clear();
-      E.showMessage('Hold\nto return\nto clock');
-      g.drawImage(c([0,8,12,14,255,14,12,8]),d,194);
-    },
-    () => {
-      g.clear();
-      E.showMessage('Hold both\nto reboot');
-      g.drawImage(c([0,8,12,14,255,14,12,8]),d,40);
-      g.drawImage(c([0,8,12,14,255,14,12,8]),d,116);
-    },
-    () => E.showMessage('Open Settings\nto enable\nBluetooth')
-  ];
-  function next() {
-    var n = slides.shift();
-    n();
-    slides.push(n);
-  }
-  return new Promise((res) => {
-    next();
-    var i = setInterval(next, 2000);
-    setWatch(()=>{
-      clearInterval(i);
-      clearWatch();
-      res();
-    }, BTN2, {repeat:false});
-  });
-}
-
-welcome()
-  .then(logos)
-  .then(info)
-  .then(load);
diff --git a/apps/ncstart/start.png b/apps/ncstart/start.png
deleted file mode 100644
index 9df0974c8..000000000
Binary files a/apps/ncstart/start.png and /dev/null differ
diff --git a/apps/osmpoi/ChangeLog b/apps/osmpoi/ChangeLog
index 1c066f451..4e66dd684 100644
--- a/apps/osmpoi/ChangeLog
+++ b/apps/osmpoi/ChangeLog
@@ -1,3 +1,4 @@
 0.01: New App!
 0.02: Change img when no fix
 0.03: Add HTML class for Spectre.CSS
+0.04: Added adjustment for Bangle.js magnetometer heading fix
diff --git a/apps/osmpoi/custom.html b/apps/osmpoi/custom.html
index 6e61b68ee..1625acabe 100644
--- a/apps/osmpoi/custom.html
+++ b/apps/osmpoi/custom.html
@@ -187,7 +187,7 @@ Bangle.on('GPS', function(f) {
 
 Bangle.on('mag', function(m) {
   if (!Bangle.isLCDOn()) return;
-  var headingrad = m.heading*Math.PI/180; // in radians
+  var headingrad = (360-m.heading)*Math.PI/180; // in radians
   if (!isFinite(headingrad)) headingrad=0;
   if (nearest)
     g.drawImage(img_fix,120,120,{
diff --git a/apps/osmpoi/metadata.json b/apps/osmpoi/metadata.json
index 4abb07548..ad576e839 100644
--- a/apps/osmpoi/metadata.json
+++ b/apps/osmpoi/metadata.json
@@ -1,7 +1,7 @@
 {
   "id": "osmpoi",
   "name": "POI Compass",
-  "version": "0.03",
+  "version": "0.04",
   "description": "Uploads all the points of interest in an area onto your watch, same as Beer Compass with more p.o.i.",
   "icon": "app.png",
   "tags": "tool,outdoors,gps",
diff --git a/apps/owmweather/ChangeLog b/apps/owmweather/ChangeLog
index 5560f00bc..dba6a755a 100644
--- a/apps/owmweather/ChangeLog
+++ b/apps/owmweather/ChangeLog
@@ -1 +1,2 @@
 0.01: New App!
+0.02: Do first update request 5s after boot to boot up faster
diff --git a/apps/owmweather/boot.js b/apps/owmweather/boot.js
index 64d2df3e9..737413940 100644
--- a/apps/owmweather/boot.js
+++ b/apps/owmweather/boot.js
@@ -1,22 +1,25 @@
-(function() {
+{
   let waiting = false;
   let settings = require("Storage").readJSON("owmweather.json", 1) || {
     enabled: false
   };
   
-  function completion(){
+  let completion = function(){
     waiting = false;
   }
-  
+
   if (settings.enabled) {    
     let weather = require("Storage").readJSON('weather.json') || {};
     let lastUpdate;
     if (weather && weather.weather && weather.weather.time) lastUpdate = weather.weather.time;
+
     if (!lastUpdate || lastUpdate + settings.refresh * 1000 * 60 < Date.now()){
-      if (!waiting){
-        waiting = true;
-        require("owmweather").pull(completion);
-      }
+      setTimeout(() => {
+        if (!waiting){
+          waiting = true;
+          require("owmweather").pull(completion);
+        }
+      }, 5000);
     }
     setInterval(() => {
       if (!waiting && NRF.getSecurityStatus().connected){
@@ -25,4 +28,4 @@
       }
     }, settings.refresh * 1000 * 60);
   }
-})();
+}
diff --git a/apps/owmweather/metadata.json b/apps/owmweather/metadata.json
index 013d345a5..56f9afca7 100644
--- a/apps/owmweather/metadata.json
+++ b/apps/owmweather/metadata.json
@@ -1,7 +1,7 @@
 { "id": "owmweather",
   "name": "OpenWeatherMap weather provider",
   "shortName":"OWM Weather",
-  "version":"0.01",
+  "version":"0.02",
   "description": "Pulls weather from OpenWeatherMap (OWM) API",
   "icon": "app.png",
   "type": "bootloader",
diff --git a/apps/palikkainen/ChangeLog b/apps/palikkainen/ChangeLog
new file mode 100644
index 000000000..5560f00bc
--- /dev/null
+++ b/apps/palikkainen/ChangeLog
@@ -0,0 +1 @@
+0.01: New App!
diff --git a/apps/palikkainen/README.md b/apps/palikkainen/README.md
new file mode 100644
index 000000000..81d857209
--- /dev/null
+++ b/apps/palikkainen/README.md
@@ -0,0 +1,7 @@
+# Palikkainen
+
+By Jukio Kallio
+
+A minimal watch face consisting of blocks. Minutes fills the blocks, and after 12 hours it starts to empty them.
+
+![](screenshot1.png) 
diff --git a/apps/palikkainen/app-icon.js b/apps/palikkainen/app-icon.js
new file mode 100644
index 000000000..a99602121
--- /dev/null
+++ b/apps/palikkainen/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwkBiIA0/4AKCpMfCxYAB+ItTGJQuOGBAWPGAwuQGAwXvCyJgFC+PwgAAEh4X/C/6//A4gX/C/6//A4QX/C/6/vC6sfCyPxC+ZgSCwgwRFwowRCwwwPFw4xOCpIArA"))
diff --git a/apps/palikkainen/app.js b/apps/palikkainen/app.js
new file mode 100644
index 000000000..42013af69
--- /dev/null
+++ b/apps/palikkainen/app.js
@@ -0,0 +1,184 @@
+// Palikkainen
+//
+// Bangle.js 2 watch face
+// by Jukio Kallio
+// www.jukiokallio.com
+
+require("Font6x8").add(Graphics);
+
+// settings
+const watch = {  
+  x:0, y:0, w:0, h:0, 
+  bgcolor:g.theme.bg, 
+  fgcolor:g.theme.fg, 
+  font: "6x8", fontsize: 1,
+  finland:true, // change if you want Finnish style date, or US style
+};
+
+// set some additional settings
+watch.w = g.getWidth(); // size of the background
+watch.h = g.getHeight();
+watch.x = watch.w * 0.5; // position of the circles
+watch.y = watch.h * 0.45;
+
+const dateWeekday = { 0: "SUN", 1: "MON", 2: "TUE", 3: "WED", 4:"THU", 5:"FRI", 6:"SAT" }; // weekdays
+
+var wait = 60000; // wait time, normally a minute
+
+
+// timeout used to update every minute
+var drawTimeout;
+
+// schedule a draw for the next minute
+function queueDraw() {
+  if (drawTimeout) clearTimeout(drawTimeout);
+  drawTimeout = setTimeout(function() {
+    drawTimeout = undefined;
+    draw();
+  }, wait - (Date.now() % wait));
+}
+
+
+// main function
+function draw() {
+  // make date object
+  var date = new Date();
+  
+  // work out the date string
+  var dateDay = date.getDate();
+  var dateMonth = date.getMonth() + 1;
+  var dateYear = date.getFullYear();
+  var dateStr = dateWeekday[date.getDay()] + " " + dateMonth + "." + dateDay + "." + dateYear;
+  if (watch.finland) dateStr = dateWeekday[date.getDay()] + " " + dateDay + "." + dateMonth + "." + dateYear; // the true way of showing date
+
+  // Reset the state of the graphics library
+  g.reset();
+  
+  // Clear the area where we want to draw the time
+  g.setColor(watch.bgcolor);
+  g.fillRect(0, 0, watch.w, watch.h);
+  
+  // setup watch face
+  const block = {
+    w: watch.w / 2 - 6,
+    h: 18,
+    pad: 4,
+  };
+  
+  // get hours and minutes
+  var hour = date.getHours();
+  var minute = date.getMinutes();
+  
+  // calculate size of the block face
+  var facew = block.w * 2 + block.pad;
+  var faceh = (block.h + block.pad) * 6;
+  
+  
+  // loop through first 12 hours and draw blocks accordingly
+  g.setColor(watch.fgcolor); // set foreground color
+  
+  for (var i = 0; i < 12; i++) {
+    // where to draw
+    var x = watch.x - facew / 2; // starting position
+    var y = watch.y + faceh / 2 - block.h - block.pad / 2; // draw blocks from bottom up
+    if (i > 5) { 
+      // second column
+      x += block.w + block.pad;
+      y -= (block.h + block.pad) * (i - 6);
+    } else { 
+      // first column
+      x += 0;
+      y -= (block.h + block.pad) * i;
+    }
+    
+    if (i < hour) {
+      // draw full hour block
+      g.fillRect(x, y, x + block.w, y + block.h);
+    } else if (i == hour) {
+      // draw minutes
+      g.fillRect(x, y, x + block.w * (minute / 60), y + block.h);
+      
+      // minute reading help
+      for (var m = 1; m < 12; m++) {
+        // set color
+        if (m * 5 < minute) g.setColor(watch.bgcolor); else g.setColor(watch.fgcolor);
+
+        var mlineh = 1; // minute line height
+        if (m == 3 || m == 6 || m == 9) mlineh = 3; // minute line height at 15, 30 and 45 minutes
+
+        g.drawLine(x + (block.w / 12 * m), y + block.h / 2 - mlineh, x + (block.w / 12 * m), y + block.h / 2 + mlineh);
+      }
+    }
+  }
+  
+  
+  // loop through second 12 hours and draw blocks accordingly
+  if (hour >= 12) {
+    g.setColor(watch.bgcolor); // set foreground color
+    
+    for (var i2 = 0; i2 < 12; i2++) {
+      // where to draw
+      var x2 = watch.x - facew / 2; // starting position
+      var y2 = watch.y + faceh / 2 - block.h - block.pad / 2; // draw blocks from bottom up
+      if (i2 > 5) { 
+        // second column
+        x2 += block.w + block.pad;
+        y2 -= (block.h + block.pad) * (i2 - 6);
+      } else { 
+        // first column
+        x2 += 0;
+        y2 -= (block.h + block.pad) * i2;
+      }
+
+      if (i2 < hour % 12) {
+        // draw full hour block
+        g.fillRect(x2, y2, x2 + block.w, y2 + block.h);
+      } else if (i2 == hour % 12) {
+        // draw minutes
+        g.fillRect(x2, y2, x2 + block.w * (minute / 60), y2 + block.h);
+        
+        // minute reading help
+        for (var m2 = 1; m2 < 12; m2++) {
+          // set color
+          if (m2 * 5 < minute) g.setColor(watch.fgcolor); else g.setColor(watch.bgcolor);
+          
+          var mlineh2 = 1; // minute line height
+          if (m2 == 3 || m2 == 6 || m2 == 9) mlineh2 = 3; // minute line height at 15, 30 and 45 minutes
+          
+          g.drawLine(x2 + (block.w / 12 * m2), y2 + block.h / 2 - mlineh2, x2 + (block.w / 12 * m2), y2 + block.h / 2 + mlineh2);
+        }
+      }
+    }
+  }
+  
+  
+  // draw date
+  var datey = 11;
+  g.setFontAlign(0,-1).setFont(watch.font, watch.fontsize).setColor(watch.fgcolor);
+  g.drawString(dateStr, watch.x, watch.y + faceh / 2 + datey);
+  
+  
+  // queue draw
+  queueDraw();
+}
+
+
+// Clear the screen once, at startup
+g.clear();
+// draw immediately at first
+draw();
+
+
+// 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;
+  }
+});
+
+
+// Show launcher when middle button pressed
+Bangle.setUI("clock");
diff --git a/apps/palikkainen/app.png b/apps/palikkainen/app.png
new file mode 100644
index 000000000..142d429e9
Binary files /dev/null and b/apps/palikkainen/app.png differ
diff --git a/apps/palikkainen/metadata.json b/apps/palikkainen/metadata.json
new file mode 100644
index 000000000..4ed8be817
--- /dev/null
+++ b/apps/palikkainen/metadata.json
@@ -0,0 +1,16 @@
+{ "id": "palikkainen",
+  "name": "Palikkainen - A blocky watch face",
+  "shortName":"Palikkainen",
+  "version":"0.01",
+  "description": "A minimal watch face consisting of blocks.",
+  "icon": "app.png",
+  "screenshots": [{"url":"screenshot1.png"}],
+  "type": "clock",
+  "tags": "clock",
+  "supports" : ["BANGLEJS","BANGLEJS2"],  
+  "readme": "README.md",
+  "storage": [
+    {"name":"palikkainen.app.js","url":"app.js"},
+    {"name":"palikkainen.img","url":"app-icon.js","evaluate":true}
+  ]
+}
diff --git a/apps/palikkainen/screenshot1.png b/apps/palikkainen/screenshot1.png
new file mode 100644
index 000000000..43a630d59
Binary files /dev/null and b/apps/palikkainen/screenshot1.png differ
diff --git a/apps/pisteinen/ChangeLog b/apps/pisteinen/ChangeLog
new file mode 100644
index 000000000..5560f00bc
--- /dev/null
+++ b/apps/pisteinen/ChangeLog
@@ -0,0 +1 @@
+0.01: New App!
diff --git a/apps/pisteinen/README.md b/apps/pisteinen/README.md
new file mode 100644
index 000000000..20e8bf9a1
--- /dev/null
+++ b/apps/pisteinen/README.md
@@ -0,0 +1,7 @@
+# Pisteinen
+
+By Jukio Kallio
+
+A Minimal digital watch face consisting of dots. Big dots for hours, small dots for minutes.
+
+![](screenshot1.png) 
diff --git a/apps/pisteinen/app-icon.js b/apps/pisteinen/app-icon.js
new file mode 100644
index 000000000..d8ad05c50
--- /dev/null
+++ b/apps/pisteinen/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwkDmYA0/4AKCpM/CxYAB+YtTGJQuOGBAWPGAwuQGAwXvCyJgFC+UhiQDNC43ygEAl4DLC4/xBYMfAZYXfI653XX/6//X/6//O5gBKU5gGBAZAXfI66//C7s/CyPzC+ZgSCwgwRFwowRCwwwPFw4xOCpIArA=="))
diff --git a/apps/pisteinen/app.js b/apps/pisteinen/app.js
new file mode 100644
index 000000000..a455875ec
--- /dev/null
+++ b/apps/pisteinen/app.js
@@ -0,0 +1,121 @@
+// Pisteinen
+//
+// Bangle.js 2 watch face
+// by Jukio Kallio
+// www.jukiokallio.com
+
+
+// settings
+const watch = {  
+  x:0, y:0, w:0, h:0, 
+  bgcolor:g.theme.bg, 
+  fgcolor:g.theme.fg, 
+};
+
+// set some additional settings
+watch.w = g.getWidth(); // size of the background
+watch.h = g.getHeight();
+watch.x = watch.w * 0.5; // position of the circles
+watch.y = watch.h * 0.5;
+
+var wait = 60000; // wait time, normally a minute
+
+
+// timeout used to update every minute
+var drawTimeout;
+
+// schedule a draw for the next minute
+function queueDraw() {
+  if (drawTimeout) clearTimeout(drawTimeout);
+  drawTimeout = setTimeout(function() {
+    drawTimeout = undefined;
+    draw();
+  }, wait - (Date.now() % wait));
+}
+
+
+// main function
+function draw() {
+  // make date object
+  var date = new Date();
+
+  // Reset the state of the graphics library
+  g.reset();
+  
+  // Clear the area where we want to draw the time
+  g.setColor(watch.bgcolor);
+  g.fillRect(0, 0, watch.w, watch.h);
+  
+  // setup watch face
+  const hball = {
+    size: 9,
+    pad: 9,
+  };
+  const mball = {
+    size: 3,
+    pad: 4,
+    pad2: 2, 
+  };
+  
+  // get hours and minutes
+  var hour = date.getHours();
+  var minute = date.getMinutes();
+  
+  // calculate size of the hour face
+  var hfacew = (hball.size * 2 + hball.pad) * 6 - hball.pad;
+  var hfaceh = (hball.size * 2 + hball.pad) * 4 - hball.pad;
+  var mfacew = (mball.size * 2 + mball.pad) * 15 - mball.pad + mball.pad2 * 2;
+  var mfaceh = (mball.size * 2 + mball.pad) * 4 - mball.pad;
+  var faceh = hfaceh + mfaceh + hball.pad + mball.pad;
+  
+  g.setColor(watch.fgcolor); // set foreground color
+  
+  // draw hour balls
+  for (var i = 0; i < 24; i++) {
+    var x = ((hball.size * 2 + hball.pad) * (i % 6)) + (watch.x - hfacew / 2) + hball.size;
+    var y = watch.y - faceh / 2 + hball.size;
+    if (i >= 6) y += hball.size * 2 + hball.pad;
+    if (i >= 12) y += hball.size * 2 + hball.pad;
+    if (i >= 18) y += hball.size * 2 + hball.pad;
+    
+    if (i < hour) g.fillCircle(x, y, hball.size); else g.drawCircle(x, y, hball.size);
+  }
+  
+  // draw minute balls
+  for (var j = 0; j < 60; j++) {
+    var x2 = ((mball.size * 2 + mball.pad) * (j % 15)) + (watch.x - mfacew / 2) + mball.size;
+    if (j % 15 >= 5) x2 += mball.pad2;
+    if (j % 15 >= 10) x2 += mball.pad2;
+    var y2 = watch.y - faceh / 2 + hfaceh + mball.size + hball.pad + mball.pad;
+    if (j >= 15) y2 += mball.size * 2 + mball.pad;
+    if (j >= 30) y2 += mball.size * 2 + mball.pad;
+    if (j >= 45) y2 += mball.size * 2 + mball.pad;
+    
+    if (j < minute) g.fillCircle(x2, y2, mball.size); else g.drawCircle(x2, y2, mball.size);
+  }
+  
+  
+  // queue draw
+  queueDraw();
+}
+
+
+// Clear the screen once, at startup
+g.clear();
+// draw immediately at first
+draw();
+
+
+// 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;
+  }
+});
+
+
+// Show launcher when middle button pressed
+Bangle.setUI("clock");
diff --git a/apps/pisteinen/app.png b/apps/pisteinen/app.png
new file mode 100644
index 000000000..a6c441423
Binary files /dev/null and b/apps/pisteinen/app.png differ
diff --git a/apps/pisteinen/metadata.json b/apps/pisteinen/metadata.json
new file mode 100644
index 000000000..f1137e589
--- /dev/null
+++ b/apps/pisteinen/metadata.json
@@ -0,0 +1,16 @@
+{ "id": "pisteinen",
+  "name": "Pisteinen - Dotted watch face",
+  "shortName":"Pisteinen",
+  "version":"0.01",
+  "description": "A minimal digital watch face made with dots.",
+  "icon": "app.png",
+  "screenshots": [{"url":"screenshot1.png"}],
+  "type": "clock",
+  "tags": "clock",
+  "supports" : ["BANGLEJS","BANGLEJS2"],  
+  "readme": "README.md",
+  "storage": [
+    {"name":"pisteinen.app.js","url":"app.js"},
+    {"name":"pisteinen.img","url":"app-icon.js","evaluate":true}
+  ]
+}
diff --git a/apps/pisteinen/screenshot1.png b/apps/pisteinen/screenshot1.png
new file mode 100644
index 000000000..556c004c0
Binary files /dev/null and b/apps/pisteinen/screenshot1.png differ
diff --git a/apps/podadrem/ChangeLog b/apps/podadrem/ChangeLog
new file mode 100644
index 000000000..c26e40c0e
--- /dev/null
+++ b/apps/podadrem/ChangeLog
@@ -0,0 +1,6 @@
+0.01: Inital release.
+0.02: Misc fixes. Add Search and play.
+0.03: Simplify "Search and play" function after some bugfixes to Podcast
+Addict.
+0.04: New layout.
+0.05: Add widget field, tweak layout.
diff --git a/apps/podadrem/README.md b/apps/podadrem/README.md
new file mode 100644
index 000000000..3760e6b5b
--- /dev/null
+++ b/apps/podadrem/README.md
@@ -0,0 +1,21 @@
+Requires Gadgetbridge 71.0 or later. Allow intents in Gadgetbridge in order for this app to work.
+
+Touch input:
+
+Press the different ui elements to control Podcast Addict and open menus.
+Press left or right arrow to move backward/forward in current playlist.
+
+Swipe input:
+
+Swipe left/right to jump backward/forward within the current podcast episode.
+Swipe up/down to change the volume.
+
+It's possible to start a podcast by searching with the remote. It's also possible to change the playback speed.
+
+The swipe logic was inspired by the implementation in [rigrig](https://git.tubul.net/rigrig/)'s Scrolling Messages.
+
+Podcast Addict Remote was created by [thyttan](https://github.com/thyttan/).
+
+Podcast Addict is developed by [Xavier Guillemane](https://twitter.com/xguillem) and can be installed via the [Google Play Store](https://play.google.com/store/apps/details?id=com.bambuna.podcastaddict&hl=en_US&gl=US).
+
+The Podcast Addict icon is used with permission.
diff --git a/apps/podadrem/app-icon.js b/apps/podadrem/app-icon.js
new file mode 100644
index 000000000..fc4406666
--- /dev/null
+++ b/apps/podadrem/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwgHEhvdABnQDwwVNAAYtTGI4WSGAgWTGAYXUGAJGUGAQXXCyoXKmf/AAPznogQn4WCAAQYP6YWFDB4WFJQhFSA4gwMIYogEGBffLg0zKAYwKRgwTDBQP9Ix09n7DCpowBJBKNEBwIXBAQIsBMwgXKIQReCDoRgJOwYQDLQU/poMBC5B2DIAUzLwIKBnoXBPBAXEIQQVDA4IXNCIQXaWAgXNI4kzNQoXLO4wXLU4a+CU4gXR7ovBcIoXIBobMFPAgXILQKPDmgxCR5omDc4QAHC5ITCC6hgCC6hICC6owBC6phBC6zcFAAMzeogALdQjdBC6AZCeYfTmczAwfQhocOAAwXYgAXVgAXVFwMAJCgXCDCYWDJKYWEGKAtEA=="))
diff --git a/apps/podadrem/app.js b/apps/podadrem/app.js
new file mode 100644
index 000000000..b04d80b17
--- /dev/null
+++ b/apps/podadrem/app.js
@@ -0,0 +1,375 @@
+/*
+Bluetooth.println(JSON.stringify({t:"intent", action:"", flags:["flag1", "flag2",...], categories:["category1","category2",...], mimetype:"", data:"",  package:"", class:"", target:"", extra:{someKey:"someValueOrString"}}));
+
+Podcast Addict is developed by Xavier Guillemane and can be downloaded on Google Play Store: https://play.google.com/store/apps/details?id=com.bambuna.podcastaddict&hl=en_US&gl=US
+
+Podcast Addict can be controlled through the sending of remote commands called 'Intents'.
+Some 3rd parties apps specialized in task automation will then allow you to control Podcast Addict. For example, you will be able to wake up to the sound of your playlist or to start automatically playing when some NFC tag has been detected.
+In Tasker, you just need to copy/paste one of the following intent in the task Action field ("Misc" action type then select "Send Itent") .
+If you prefer Automate It, you can use the Podcast Addict plugin that will save you some configuration time (https://play.google.com/store/apps/details?id=com.smarterapps.podcastaddictplugin )
+Before using an intent make sure to set the following:
+Package: com.bambuna.podcastaddict
+Class (UPDATE intent only): com.bambuna.podcastaddict.receiver.PodcastAddictBroadcastReceiver
+Class (every other intent): com.bambuna.podcastaddict.receiver.PodcastAddictPlayerReceiver
+Here are the supported commands (Intents) :
+com.bambuna.podcastaddict.service.player.toggle – Toggle the playlist
+com.bambuna.podcastaddict.service.player.stop – Stop the player and release its resources
+com.bambuna.podcastaddict.service.player.play – Start playing the playlist
+com.bambuna.podcastaddict.service.player.pause – Pause the playlist
+com.bambuna.podcastaddict.service.player.nexttrack – Start playing next track
+com.bambuna.podcastaddict.service.player.previoustrack – Start playing previous track
+com.bambuna.podcastaddict.service.player.jumpforward – Jump 30s forward
+com.bambuna.podcastaddict.service.player.jumpbackward – Jump 15s backward
+com.bambuna.podcastaddict.service.player.1xspeed - Disable the variable playback speed
+com.bambuna.podcastaddict.service.player.1.5xspeed – Force the playback speed at 1.5x
+com.bambuna.podcastaddict.service.player.2xspeed – Force the playback speed at 2.0x
+com.bambuna.podcastaddict.service.player.stoptimer – Disable the timer
+com.bambuna.podcastaddict.service.player.15mntimer – Set the timer at 15 minutes
+com.bambuna.podcastaddict.service.player.30mntimer – Set the timer at 30 minutes
+com.bambuna.podcastaddict.service.player.60mntimer – Set the timer at 1 hour
+com.bambuna.podcastaddict.service.update – Trigger podcasts update
+com.bambuna.podcastaddict.openmainscreen – Open the app on the Main screen
+com.bambuna.podcastaddict.openplaylist – Open the app on the Playlist screen
+com.bambuna.podcastaddict.openplayer – Open the app on the Player screen
+com.bambuna.podcastaddict.opennewepisodes – Open the app on the New episodes screen
+com.bambuna.podcastaddict.opendownloadedepisodes – Open the app on the Downloaded episodes screen
+com.bambuna.podcastaddict.service.player.playfirstepisode – Start playing the first episode in the playlist
+com.bambuna.podcastaddict.service.player.customspeed – Select playback speed
+In order to use this intent you need to pass a float argument called "arg1". Valid values are within [0.1, 5.0]
+com.bambuna.podcastaddict.service.player.customtimer – Start a custom timer
+In order to use this intent you need to pass an int argument called "arg1" containing the number of minutes. Valid values are within [1, 1440]
+com.bambuna.podcastaddict.service.player.deletecurrentskipnexttrack – Delete the current episode and skip to the next one. It behaves the same way as long pressing on the player >| button, but doesn't display any confirmation popup.
+com.bambuna.podcastaddict.service.player.deletecurrentskipprevioustrack – Delete the current episode and skip to the previous one. It behaves the same way as long pressing on the player |< button, but doesn't display any confirmation popup.
+com.bambuna.podcastaddict.service.player.boostVolume – Toggle the Volume Boost audio effect
+You can pass a, optional boolean argument called "arg1" in order to create a ON or OFF button for the volume boost. Without this parameter the app will just toggle the current value
+com.bambuna.podcastaddict.service.player.quickBookmark – Creates a bookmark at the current playback position so you can easily retrieve it later.
+com.bambuna.podcastaddict.service.download.pause – Pause downloads
+com.bambuna.podcastaddict.service.download.resume – Resume downloads
+com.bambuna.podcastaddict.service. download.toggle – Toggle downloads
+com.bambuna.podcastaddict.service.player.favorite – Mark the current episode playing as favorite.
+com.bambuna.podcastaddict.openplaylist – Open the app on the Playlist screen
+You can pass an optional string argument called "arg1" in order to select the playlist to open. Without this parameter the app will open the current playlist
+Here's how it works:
+##AUDIO## will open the Audio playlist screen
+##VIDEO## will open the Video playlist screen
+##RADIO## will open the Radio screen
+Any other argument will be used as a CATEGORY name. The app will then open this category under the playlist CUSTOM tab
+You can pass an optional boolean argument called "arg2" in order to select if the app UI should be opened. Without this parameter the playlist will be displayed
+You can pass an optional boolean argument called "arg3" in order to select if the app should start playing the selected playlist. Without this parameter the playback won't start
+Since v2020.3
+com.bambuna.podcastaddict.service.full_backup – Trigger a full backup of the app data (relies on the app automatic backup settings for the folder and the # of backup to keep)
+This task takes a lot of resources and might take up to a minute to complete, so please avoid using the app at the same time
+Since v2020.15
+com.bambuna.podcastaddict.service.player.toggletimer – This will toggle the Sleep Timer using the last duration and parameter used in the app.
+Since v2020.16
+com.bambuna.podcastaddict.service.player.togglespeed – This will toggle the Playback speed for the episode currently playing (alternate between selected speed and 1.0x).
+*/
+
+var R;
+var backToMenu = false;
+var dark = g.theme.dark; // bool
+
+// The main layout of the app
+function gfx() {
+  //Bangle.drawWidgets();
+  R = Bangle.appRect;
+  marigin = 8;
+  // g.drawString(str, x, y, solid)
+  g.clearRect(R);
+  g.reset();
+  
+  if (dark) {g.setColor(0xFD20);} else {g.setColor(0xF800);} // Orange on dark theme, RED on light theme.
+  g.setFont("4x6:2");
+  g.setFontAlign(1, 0, 0);
+  g.drawString("->", R.x2 - marigin, R.y + R.h/2);
+
+  g.setFontAlign(-1, 0, 0);
+  g.drawString("<-", R.x + marigin, R.y + R.h/2);
+
+  g.setFontAlign(-1, 0, 1);
+  g.drawString("<-", R.x + R.w/2, R.y + marigin);
+
+  g.setFontAlign(1, 0, 1);
+  g.drawString("->", R.x + R.w/2, R.y2 - marigin);
+
+  g.setFontAlign(0, 0, 0);
+  g.drawString("Play\nPause", R.x + R.w/2, R.y + R.h/2);
+
+  g.setFontAlign(-1, -1, 0);
+  g.drawString("Menu", R.x + 2*marigin, R.y + 2*marigin);
+
+  g.setFontAlign(-1, 1, 0);
+  g.drawString("Wake", R.x + 2*marigin, R.y + R.h - 2*marigin);
+
+  g.setFontAlign(1, -1, 0);
+  g.drawString("Srch", R.x + R.w - 2*marigin, R.y + 2*marigin);
+
+  g.setFontAlign(1, 1, 0);
+  g.drawString("Speed", R.x + R.w - 2*marigin, R.y + R.h - 2*marigin);
+}
+
+// Touch handler for main layout
+function touchHandler(_, xy) {
+  x = xy.x;
+  y = xy.y;
+  len = (R.wb-1 instead of a>b.
+  if ((R.x-1 {
+      if (ud) Bangle.musicControl(ud>0 ? "volumedown" : "volumeup");
+    }
+  );
+  Bangle.on("touch", touchHandler);
+  Bangle.on("swipe", swipeHandler);
+}
+
+/*
+The functions for interacting with Android and the Podcast Addict app
+*/
+
+pkg = "com.bambuna.podcastaddict";
+standardCls = pkg + ".receiver.PodcastAddictPlayerReceiver";
+updateCls = pkg + ".receiver.PodcastAddictBroadcastReceiver";
+speed = 1.0;
+
+simpleSearch = "";
+
+function simpleSearchTerm() { // input a simple search term without tags, overrides search with tags (artist and track)
+  require("textinput").input({
+    text: simpleSearch
+  }).then(result => {
+    simpleSearch = result;
+  }).then(() => {
+    E.showMenu(searchMenu);
+  });
+}
+
+function searchPlayWOTags() { //make a search and play using entered terms
+  searchString = simpleSearch;
+  Bluetooth.println(JSON.stringify({
+    t: "intent",
+    action: "android.media.action.MEDIA_PLAY_FROM_SEARCH",
+    package: pkg,
+    target: "activity",
+    extra: {
+      query: searchString
+    },
+    flags: ["FLAG_ACTIVITY_NEW_TASK"]
+  }));
+}
+
+function gadgetbridgeWake() {
+  Bluetooth.println(JSON.stringify({
+    t: "intent",
+    target: "activity",
+    flags: ["FLAG_ACTIVITY_NEW_TASK", "FLAG_ACTIVITY_CLEAR_TASK", "FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS", "FLAG_ACTIVITY_NO_ANIMATION"],
+    package: "gadgetbridge",
+    class: "nodomain.freeyourgadget.gadgetbridge.activities.WakeActivity"
+  }));
+}
+
+// For stringing together the action for Podcast Addict to perform
+function actFn(actName, activOrServ) {
+  return "com.bambuna.podcastaddict." + (activOrServ == "service" ? "service." : "") + actName;
+}
+
+// Send the intent message to Gadgetbridge
+function btMsg(activOrServ, cls, actName, xtra) {
+
+  Bluetooth.println(JSON.stringify({
+    t: "intent",
+    action: actFn(actName, activOrServ),
+    package: pkg,
+    class: cls,
+    target: "broadcastreceiver",
+    extra: xtra
+  }));
+}
+
+// Get back to the main layout
+function backToGfx() {
+  E.showMenu();
+  g.clear();
+  g.reset();
+  Bangle.removeAllListeners("touch");
+  Bangle.removeAllListeners("swipe");
+  setUI();
+  gfx();
+  backToMenu = false;
+}
+
+// Podcast Addict Menu
+var paMenu = {
+  "": {
+    title: " ",
+    back: backToGfx
+  },
+  "Controls": () => {
+    E.showMenu(controlMenu);
+  },
+  "Speed Controls": () => {
+    E.showMenu(speedMenu);
+  },
+  "Search and play": () => {
+    E.showMenu(searchMenu);
+  },
+  "Navigate and play": () => {
+    E.showMenu(navigationMenu);
+  },
+  "Wake the android": () => {
+    gadgetbridgeWake();
+    gadgetbridgeWake();
+  },
+  "Exit PA Remote": ()=>{load();}
+};
+
+
+var controlMenu = {
+  "": {
+    title: " ",
+    back: () => {if (backToMenu) E.showMenu(paMenu);
+                 if (!backToMenu) backToGfx();
+                }
+  },
+  "Toggle Play/Pause": () => {
+    btMsg("service", standardCls, "player.toggle");
+  },
+  "Jump Backward": () => {
+    btMsg("service", standardCls, "player.jumpbackward");
+  },
+  "Jump Forward": () => {
+    btMsg("service", standardCls, "player.jumpforward");
+  },
+  "Previous": () => {
+    btMsg("service", standardCls, "player.previoustrack");
+  },
+  "Next": () => {
+    btMsg("service", standardCls, "player.nexttrack");
+  },
+  "Play": () => {
+    btMsg("service", standardCls, "player.play");
+  },
+  "Pause": () => {
+    btMsg("service", standardCls, "player.pause");
+  },
+  "Stop": () => {
+    btMsg("service", standardCls, "player.stop");
+  },
+  "Update": () => {
+    btMsg("service", updateCls, "update");
+  },
+  "Messages Music Controls": () => {
+    load("messagesmusic.app.js");
+  },
+};
+
+var speedMenu = {
+  "": {
+    title: " ",
+    back: () => {if (backToMenu) E.showMenu(paMenu);
+                 if (!backToMenu) backToGfx();
+                }
+  },
+  "Regular Speed": () => {
+    speed = 1.0;
+    btMsg("service", standardCls, "player.1xspeed");
+  },
+  "1.5x Regular Speed": () => {
+    speed = 1.5;
+    btMsg("service", standardCls, "player.1.5xspeed");
+  },
+  "2x Regular Speed": () => {
+    speed = 2.0;
+    btMsg("service", standardCls, "player.2xspeed");
+  },
+  //"Faster" : ()=>{speed+=0.1; speed=((speed>5.0)?5.0:speed); btMsg("service",standardCls,"player.customspeed",{arg1:speed});},
+  //"Slower" : ()=>{speed-=0.1; speed=((speed<0.1)?0.1:speed); btMsg("service",standardCls,"player.customspeed",{arg1:speed});},
+};
+
+var searchMenu = {
+  "": {
+    title: " ",
+
+    back: () => {if (backToMenu) E.showMenu(paMenu);
+                 if (!backToMenu) backToGfx();}
+
+  },
+  "Search term": () => {
+    simpleSearchTerm();
+  },
+  "Execute search and play": () => {
+    btMsg("service", standardCls, "player.play");
+    setTimeout(() => {
+      searchPlayWOTags();
+      setTimeout(() => {
+        btMsg("service", standardCls, "player.play");
+      }, 200);
+    }, 1500);
+  },
+  "Simpler search and play" : searchPlayWOTags,
+};
+
+var navigationMenu = {
+  "": {
+    title: " ",
+    back: () => {if (backToMenu) E.showMenu(paMenu);
+                 if (!backToMenu) backToGfx();}
+  },
+  "Open Main Screen": () => {
+    btMsg("activity", standardCls, "openmainscreen");
+  },
+  "Open Player Screen": () => {
+    btMsg("activity", standardCls, "openplayer");
+  },
+};
+
+Bangle.loadWidgets();
+setUI();
+gfx();
diff --git a/apps/podadrem/app.png b/apps/podadrem/app.png
new file mode 100644
index 000000000..b9cdf4fed
Binary files /dev/null and b/apps/podadrem/app.png differ
diff --git a/apps/podadrem/metadata.json b/apps/podadrem/metadata.json
new file mode 100644
index 000000000..929269762
--- /dev/null
+++ b/apps/podadrem/metadata.json
@@ -0,0 +1,18 @@
+{
+  "id": "podadrem",
+  "name": "Podcast Addict Remote",
+  "shortName": "PA Remote",
+  "version": "0.05",
+  "description": "Control Podcast Addict on your android device.",
+  "readme": "README.md",
+  "type": "app",
+  "tags": "remote,podcast,podcasts,radio,player,intent,intents,gadgetbridge,podadrem,pa remote", 
+  "icon": "app.png",
+  "screenshots" : [ {"url":"screenshot1.png"}, {"url":"screenshot2.png"} ],
+  "supports": ["BANGLEJS2"],
+  "dependencies": { "textinput":"type"},
+  "storage": [
+    {"name":"podadrem.app.js","url":"app.js"},
+    {"name":"podadrem.img","url":"app-icon.js","evaluate":true}
+  ]
+}
diff --git a/apps/podadrem/screenshot1.png b/apps/podadrem/screenshot1.png
new file mode 100644
index 000000000..50f3f17f1
Binary files /dev/null and b/apps/podadrem/screenshot1.png differ
diff --git a/apps/podadrem/screenshot2.png b/apps/podadrem/screenshot2.png
new file mode 100644
index 000000000..e20c808a2
Binary files /dev/null and b/apps/podadrem/screenshot2.png differ
diff --git a/apps/poikkipuinen/ChangeLog b/apps/poikkipuinen/ChangeLog
new file mode 100644
index 000000000..5560f00bc
--- /dev/null
+++ b/apps/poikkipuinen/ChangeLog
@@ -0,0 +1 @@
+0.01: New App!
diff --git a/apps/poikkipuinen/README.md b/apps/poikkipuinen/README.md
new file mode 100644
index 000000000..12f8d5d7e
--- /dev/null
+++ b/apps/poikkipuinen/README.md
@@ -0,0 +1,7 @@
+# Poikkipuinen
+
+By Jukio Kallio
+
+A Minimal digital watch face. Follows the theme colors.
+
+![](screenshot1.png) 
diff --git a/apps/poikkipuinen/app-icon.js b/apps/poikkipuinen/app-icon.js
new file mode 100644
index 000000000..d7ddba399
--- /dev/null
+++ b/apps/poikkipuinen/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwkEogA0/4AKCpNPCxYAB+gtTGJQuOGBAWPGAwuQGAwXamQULkYXGBQUgn4WJ+cCMAwXNiQXV+MBC6swh4XU+cAn4XU+IUBC6kgj4XUIwKnV+EDC6sQl4XU+UBd6q8BC6q8BC6i8CC6i8CC6a8DC6a8DC6a8DC6S8EC6S8EC6S8EC6K8FC6K8FC6C8BIwwXOXgwXQXgwXQkIWHd6IXPp4GBmQWJAAMjAQP0C4wAPC7hgDABwWEGCIuFGCIWGGB4uHGJwVJAFY="))
diff --git a/apps/poikkipuinen/app.js b/apps/poikkipuinen/app.js
new file mode 100644
index 000000000..0bf09c5e5
--- /dev/null
+++ b/apps/poikkipuinen/app.js
@@ -0,0 +1,158 @@
+// Poikkipuinen
+//
+// Bangle.js 2 watch face
+// by Jukio Kallio
+// www.jukiokallio.com
+
+require("Font5x9Numeric7Seg").add(Graphics);
+require("FontSinclair").add(Graphics);
+
+// settings
+const watch = {  
+  x:0, y:0, w:0, h:0, 
+  bgcolor:g.theme.bg, 
+  fgcolor:g.theme.fg, 
+  font: "5x9Numeric7Seg", fontsize: 1,
+  font2: "Sinclair", font2size: 1, 
+  finland:true, // change if you want Finnish style date, or US style
+};
+
+
+// set some additional settings
+watch.w = g.getWidth(); // size of the background
+watch.h = g.getHeight();
+watch.x = watch.w * 0.5; // position of the circles
+watch.y = watch.h * 0.41;
+
+const dateWeekday = { 0: "SUN", 1: "MON", 2: "TUE", 3: "WED", 4:"THU", 5:"FRI", 6:"SAT" }; // weekdays
+
+var wait = 60000; // wait time, normally a minute
+
+
+// timeout used to update every minute
+var drawTimeout;
+
+// schedule a draw for the next minute
+function queueDraw() {
+  if (drawTimeout) clearTimeout(drawTimeout);
+  drawTimeout = setTimeout(function() {
+    drawTimeout = undefined;
+    draw();
+  }, wait - (Date.now() % wait));
+}
+
+
+// main function
+function draw() {
+  // make date object
+  var date = new Date();
+  
+  // work out the date string
+  var dateDay = date.getDate();
+  var dateMonth = date.getMonth() + 1;
+  var dateYear = date.getFullYear();
+  var dateStr = dateMonth + "." + dateDay + "." + dateYear;
+  if (watch.finland) dateStr = dateDay + "." + dateMonth + "." + dateYear; // the true way of showing date
+  var dateStr2 = dateWeekday[date.getDay()];
+
+  // Reset the state of the graphics library
+  g.reset();
+  
+  // Clear the area where we want to draw the time
+  g.setColor(watch.bgcolor);
+  g.fillRect(0, 0, watch.w, watch.h);
+  
+  // set foreground color
+  g.setColor(watch.fgcolor);
+  g.setFontAlign(1,-1).setFont(watch.font, watch.fontsize);
+  
+  // watch face size
+  var facew, faceh; // halves of the size for easier calculation
+  facew = 50;
+  faceh = 59;
+  
+  // save hour and minute y positions
+  var houry, minutey;
+  
+  // draw hour meter
+  g.drawLine(watch.x - facew, watch.y - faceh, watch.x - facew, watch.y + faceh);
+  var lines = 13;
+  var lineh = faceh * 2 / (lines - 2);
+  for (var i = 1; i < lines; i++) {
+    var w = 3;
+    var y = faceh - lineh * (i - 1);
+    
+    if (i % 3 == 0) {
+      // longer line and numbers every 3
+      w = 5;
+      g.drawString(i, watch.x - facew - 2, y + watch.y);
+    }
+    
+    g.drawLine(watch.x - facew, y + watch.y, watch.x - facew + w, y + watch.y);
+    
+    // get hour y position
+    var hour = date.getHours() % 12; // modulate away the 24h
+    if (hour == 0) hour = 12; // fix a problem with 0-23 hours
+    //var hourMin = date.getMinutes() / 60; // move hour line by minutes
+    var hourMin = Math.floor(date.getMinutes() / 15) / 4; // move hour line by 15-minutes
+    if (hour == 12) hourMin = 0; // don't do minute moving if 12 (line ends there)
+    if (i == hour) houry = y - (lineh * hourMin);
+  }
+  
+  // draw minute meter
+  g.drawLine(watch.x + facew, watch.y - faceh, watch.x + facew, watch.y + faceh);
+  g.setFontAlign(-1,-1);
+  lines = 60;
+  lineh = faceh * 2 / (lines - 1);
+  for (i = 0; i < lines; i++) {
+    var mw = 3;
+    var my = faceh - lineh * i;
+    
+    if (i % 15 == 0 && i != 0) {
+      // longer line and numbers every 3
+      mw = 5;
+      g.drawString(i, watch.x + facew + 4, my + watch.y);
+    }
+    
+    //if (i % 2 == 0 || i == 15 || i == 45) 
+    g.drawLine(watch.x + facew, my + watch.y, watch.x + facew - mw, my + watch.y);
+    
+    // get minute y position
+    if (i == date.getMinutes()) minutey = my;
+  }
+  
+  // draw the time
+  var timexpad = 8;
+  g.drawLine(watch.x - facew + timexpad, watch.y + houry, watch.x + facew - timexpad, watch.y + minutey);
+  
+  // draw date
+  var datey = 14;
+  g.setFontAlign(0,-1);
+  g.drawString(dateStr, watch.x, watch.y + faceh + datey);
+  g.setFontAlign(0,-1).setFont(watch.font2, watch.font2size);
+  g.drawString(dateStr2, watch.x, watch.y + faceh + datey*2);
+  
+  // queue draw
+  queueDraw();
+}
+
+
+// Clear the screen once, at startup
+g.clear();
+// draw immediately at first
+draw();
+
+
+// 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;
+  }
+});
+
+
+// Show launcher when middle button pressed
+Bangle.setUI("clock");
diff --git a/apps/poikkipuinen/app.png b/apps/poikkipuinen/app.png
new file mode 100644
index 000000000..fa506c886
Binary files /dev/null and b/apps/poikkipuinen/app.png differ
diff --git a/apps/poikkipuinen/metadata.json b/apps/poikkipuinen/metadata.json
new file mode 100644
index 000000000..ec95ab7ce
--- /dev/null
+++ b/apps/poikkipuinen/metadata.json
@@ -0,0 +1,16 @@
+{ "id": "poikkipuinen",
+  "name": "Poikkipuinen - Minimal watch face",
+  "shortName":"Poikkipuinen",
+  "version":"0.01",
+  "description": "A minimal digital watch face.",
+  "icon": "app.png",
+  "screenshots": [{"url":"screenshot1.png"}],
+  "type": "clock",
+  "tags": "clock",
+  "supports" : ["BANGLEJS","BANGLEJS2"],  
+  "readme": "README.md",
+  "storage": [
+    {"name":"poikkipuinen.app.js","url":"app.js"},
+    {"name":"poikkipuinen.img","url":"app-icon.js","evaluate":true}
+  ]
+}
diff --git a/apps/poikkipuinen/screenshot1.png b/apps/poikkipuinen/screenshot1.png
new file mode 100644
index 000000000..23fcc348c
Binary files /dev/null and b/apps/poikkipuinen/screenshot1.png differ
diff --git a/apps/pomoplus/ChangeLog b/apps/pomoplus/ChangeLog
new file mode 100644
index 000000000..1a137aad0
--- /dev/null
+++ b/apps/pomoplus/ChangeLog
@@ -0,0 +1,3 @@
+0.01: New app!
+0.02-0.04: Bug fixes
+0.05: Submitted to the app loader
\ No newline at end of file
diff --git a/apps/pomoplus/app.js b/apps/pomoplus/app.js
new file mode 100644
index 000000000..73af5c935
--- /dev/null
+++ b/apps/pomoplus/app.js
@@ -0,0 +1,157 @@
+Bangle.POMOPLUS_ACTIVE = true;  //Prevent the boot code from running. To avoid having to reload on every interaction, we'll control the vibrations from here when the user is in the app.
+
+const storage = require("Storage");
+const common = require("pomoplus-com.js");
+
+//Expire the state if necessary
+if (
+  common.settings.pausedTimerExpireTime != 0 &&
+  !common.state.running &&
+  (new Date()).getTime() - common.state.pausedTime > common.settings.pausedTimerExpireTime
+) {
+  common.state = common.STATE_DEFAULT;
+}
+
+function drawButtons() {
+  //Draw the backdrop
+  const BAR_TOP = g.getHeight() - 24;
+  g.setColor(0, 0, 1).setFontAlign(0, -1)
+    .clearRect(0, BAR_TOP, g.getWidth(), g.getHeight())
+    .fillRect(0, BAR_TOP, g.getWidth(), g.getHeight())
+    .setColor(1, 1, 1);
+
+  if (!common.state.wasRunning) {  //If the timer was never started, only show a play button
+    g.drawImage(common.BUTTON_ICONS.play, g.getWidth() / 2, BAR_TOP);
+  } else {
+    g.drawLine(g.getWidth() / 2, BAR_TOP, g.getWidth() / 2, g.getHeight());
+    if (common.state.running) {
+      g.drawImage(common.BUTTON_ICONS.pause, g.getWidth() / 4, BAR_TOP)
+        .drawImage(common.BUTTON_ICONS.skip, g.getWidth() * 3 / 4, BAR_TOP);
+    } else {
+      g.drawImage(common.BUTTON_ICONS.reset, g.getWidth() / 4, BAR_TOP)
+        .drawImage(common.BUTTON_ICONS.play, g.getWidth() * 3 / 4, BAR_TOP);
+    }
+  }
+}
+
+function drawTimerAndMessage() {
+  g.reset()
+    .setFontAlign(0, 0)
+    .setFont("Vector", 36)
+    .clearRect(0, 24, 176, 152)
+
+    //Draw the timer
+    .drawString((() => {
+      let timeLeft = common.getTimeLeft();
+      let hours = timeLeft / 3600000;
+      let minutes = (timeLeft % 3600000) / 60000;
+      let seconds = (timeLeft % 60000) / 1000;
+
+      function pad(number) {
+        return ('00' + parseInt(number)).slice(-2);
+      }
+
+      if (hours >= 1) return `${parseInt(hours)}:${pad(minutes)}:${pad(seconds)}`;
+      else return `${parseInt(minutes)}:${pad(seconds)}`;
+    })(), g.getWidth() / 2, g.getHeight() / 2)
+
+    //Draw the phase label
+    .setFont("Vector", 12)
+    .drawString(((currentPhase, numShortBreaks) => {
+      if (!common.state.wasRunning) return "Not started";
+      else if (currentPhase == common.PHASE_WORKING) return `Work ${numShortBreaks + 1}/${common.settings.numShortBreaks + 1}`
+      else if (currentPhase == common.PHASE_SHORT_BREAK) return `Short break ${numShortBreaks + 1}/${common.settings.numShortBreaks}`;
+      else return "Long break!";
+    })(common.state.phase, common.state.numShortBreaks),
+      g.getWidth() / 2, g.getHeight() / 2 + 18);
+
+  //Update phase with vibation if needed
+  if (common.getTimeLeft() <= 0) {
+    common.nextPhase(true);
+  }
+}
+
+drawButtons();
+Bangle.on("touch", (button, xy) => {
+  //If we support full touch and we're not touching the keys, ignore.
+  //If we don't support full touch, we can't tell so just assume we are.
+  if (xy !== undefined && xy.y <= g.getHeight() - 24) return;
+
+  if (!common.state.wasRunning) {
+    //If we were never running, there is only one button: the start button
+    let now = (new Date()).getTime();
+    common.state = {
+      wasRunning: true,
+      running: true,
+      startTime: now,
+      pausedTime: now,
+      elapsedTime: 0,
+      phase: common.PHASE_WORKING,
+      numShortBreaks: 0
+    };
+    setupTimerInterval();
+    drawButtons();
+
+  } else if (common.state.running) {
+    //If we are running, there are two buttons: pause and skip
+    if (button == 1) {
+      //Record the exact moment that we paused
+      let now = (new Date()).getTime();
+      common.state.pausedTime = now;
+
+      //Stop the timer
+      common.state.running = false;
+      clearInterval(timerInterval);
+      timerInterval = undefined;
+      drawTimerAndMessage();
+      drawButtons();
+
+    } else {
+      common.nextPhase(false);
+    }
+
+  } else {
+    //If we are stopped, there are two buttons: Reset and continue
+    if (button == 1) {
+      //Reset the timer
+      common.state = common.STATE_DEFAULT;
+      drawTimerAndMessage();
+      drawButtons();
+
+    } else {
+      //Start the timer and record old elapsed time and when we started
+      let now = (new Date()).getTime();
+      common.state.elapsedTime += common.state.pausedTime - common.state.startTime;
+      common.state.startTime = now;
+      common.state.running = true;
+      drawTimerAndMessage();
+      setupTimerInterval();
+      drawButtons();
+    }
+  }
+});
+
+let timerInterval;
+
+function setupTimerInterval() {
+  if (timerInterval !== undefined) {
+    clearInterval(timerInterval);
+  }
+  setTimeout(() => {
+    timerInterval = setInterval(drawTimerAndMessage, 1000);
+    drawTimerAndMessage();
+  }, common.timeLeft % 1000);
+}
+
+drawTimerAndMessage();
+if (common.state.running) {
+  setupTimerInterval();
+}
+
+//Save our state when the app is closed
+E.on('kill', () => {
+  storage.writeJSON(common.STATE_PATH, common.state);
+});
+
+Bangle.loadWidgets();
+Bangle.drawWidgets();
\ No newline at end of file
diff --git a/apps/pomoplus/boot.js b/apps/pomoplus/boot.js
new file mode 100644
index 000000000..edc233853
--- /dev/null
+++ b/apps/pomoplus/boot.js
@@ -0,0 +1,19 @@
+const POMOPLUS_storage = require("Storage");
+const POMOPLUS_common = require("pomoplus-com.js");
+
+function setNextTimeout() {
+    setTimeout(() => {
+        //Make sure that the pomoplus app isn't in the foreground. The pomoplus app handles the vibrations when it is in the foreground in order to avoid having to reload every time the user changes state. That means that when the app is in the foreground, we shouldn't do anything here.
+        //We do this after the timer rather than before because the timer will start before the app executes.
+        if (Bangle.POMOPLUS_ACTIVE === undefined) {
+            POMOPLUS_common.nextPhase(true);
+            setNextTimeout();
+            POMOPLUS_storage.writeJSON(POMOPLUS_common.STATE_PATH, POMOPLUS_common.state)
+        }
+    }, POMOPLUS_common.getTimeLeft());
+}
+
+//Only start the timeout if the timer is running
+if (POMOPLUS_common.state.running) {
+    setNextTimeout();
+}
\ No newline at end of file
diff --git a/apps/pomoplus/common.js b/apps/pomoplus/common.js
new file mode 100644
index 000000000..b1cd42de8
--- /dev/null
+++ b/apps/pomoplus/common.js
@@ -0,0 +1,118 @@
+const storage = require("Storage");
+const heatshrink = require("heatshrink");
+
+exports.STATE_PATH = "pomoplus.state.json";
+exports.SETTINGS_PATH = "pomoplus.json";
+
+exports.PHASE_WORKING = 0;
+exports.PHASE_SHORT_BREAK = 1;
+exports.PHASE_LONG_BREAK = 2;
+
+exports.BUTTON_ICONS = {
+    play: heatshrink.decompress(atob("jEYwMAkAGBnACBnwCBn+AAQPgAQPwAQP8AQP/AQXAAQPwAQP8AQP+AQgICBwQUCEAn4FggyBHAQ+CIgQ")),
+    pause: heatshrink.decompress(atob("jEYwMA/4BBAX4CEA")),
+    reset: heatshrink.decompress(atob("jEYwMA/4BB/+BAQPDAQPnAQIAKv///0///8j///EP//wAQQICBwQUCEhgyCHAQ+CIgI=")),
+    skip: heatshrink.decompress(atob("jEYwMAwEIgHAhkA8EOgHwh8A/EPwH8h/A/0P8H/h/w/+P/H/5/8//v/3/AAoICBwQUCDQIgCEwQsCGQQ4CHwRECA"))
+};
+
+exports.settings = storage.readJSON(exports.SETTINGS_PATH);
+if (!exports.settings) {
+    exports.settings = {
+        workTime: 1500000,                  //Work for 25 minutes
+        shortBreak: 300000,                 //5 minute short break
+        longBreak: 900000,                  //15 minute long break
+        numShortBreaks: 3,                  //3 short breaks for every long break
+        pausedTimerExpireTime: 21600000,    //If the timer was left paused for >6 hours, reset it on next launch
+        widget: false                       //If a widget is added in the future, whether the user wants it
+    };
+}
+
+//Store the minimal amount of information to be able to reconstruct the state of the timer at any given time.
+//This is necessary because it is necessary to write to flash to let the timer run in the background, so minimizing the writes is necessary.
+exports.STATE_DEFAULT = {
+    wasRunning: false,              //If the timer ever was running. Used to determine whether to display a reset button
+    running: false,                 //Whether the timer is currently running
+    startTime: 0,                   //When the timer was last started. Difference between this and now is how long timer has run continuously.
+    pausedTime: 0,                  //When the timer was last paused. Used for expiration and displaying timer while paused.
+    elapsedTime: 0,                 //How much time the timer had spent running before the current start time. Update on pause or user skipping stages.
+    phase: exports.PHASE_WORKING,   //What phase the timer is currently in
+    numShortBreaks: 0               //Number of short breaks that have occured so far
+};
+exports.state = storage.readJSON(exports.STATE_PATH);
+if (!exports.state) {
+    exports.state = exports.STATE_DEFAULT;
+}
+
+//Get the number of milliseconds until the next phase change
+exports.getTimeLeft = function () {
+    if (!exports.state.wasRunning) {
+        //If the timer never ran, the time left is just the amount of work time.
+        return exports.settings.workTime;
+    } else if (exports.state.running) {
+        //If the timer is running, the time left is current time - start time + preexisting time
+        var runningTime = (new Date()).getTime() - exports.state.startTime + exports.state.elapsedTime;
+    } else {
+        //If the timer is not running, the same as above but use when the timer was paused instead of now.
+        var runningTime = exports.state.pausedTime - exports.state.startTime + exports.state.elapsedTime;
+    }
+
+    if (exports.state.phase == exports.PHASE_WORKING) {
+        return exports.settings.workTime - runningTime;
+    } else if (exports.state.phase == exports.PHASE_SHORT_BREAK) {
+        return exports.settings.shortBreak - runningTime;
+    } else {
+        return exports.settings.longBreak - runningTime;
+    }
+}
+
+//Get the next phase to change to
+exports.getNextPhase = function () {
+    if (exports.state.phase == exports.PHASE_WORKING) {
+        if (exports.state.numShortBreaks < exports.settings.numShortBreaks) {
+            return exports.PHASE_SHORT_BREAK;
+        } else {
+            return exports.PHASE_LONG_BREAK;
+        }
+    } else {
+        return exports.PHASE_WORKING;
+    }
+}
+
+//Change to the next phase and update numShortBreaks, and optionally vibrate. DOES NOT WRITE STATE CHANGE TO STORAGE!
+exports.nextPhase = function (vibrate) {
+    a = {
+        startTime: 0,                   //When the timer was last started. Difference between this and now is how long timer has run continuously.
+        pausedTime: 0,                  //When the timer was last paused. Used for expiration and displaying timer while paused.
+        elapsedTime: 0,                 //How much time the timer had spent running before the current start time. Update on pause or user skipping stages.
+        phase: exports.PHASE_WORKING,   //What phase the timer is currently in
+        numShortBreaks: 0               //Number of short breaks that have occured so far
+    }
+    let now = (new Date()).getTime();
+    exports.state.startTime = now;  //The timer is being reset, so say it starts now.
+    exports.state.pausedTime = now; //This prevents a paused timer from having the start time moved to the future and therefore having been run for negative time.
+    exports.state.elapsedTime = 0;  //Because we are resetting the timer, we no longer need to care about whether it was paused previously.
+
+    let oldPhase = exports.state.phase; //Cache the old phase because we need to remember it when counting the number of short breaks
+    exports.state.phase = exports.getNextPhase();
+
+    if (oldPhase == exports.PHASE_SHORT_BREAK) {
+        //If we just left a short break, increase the number of short breaks
+        exports.state.numShortBreaks++;
+    } else if (oldPhase == exports.PHASE_LONG_BREAK) {
+        //If we just left a long break, set the number of short breaks to zero
+        exports.state.numShortBreaks = 0;
+    }
+
+    if (vibrate) {
+        if (exports.state.phase == exports.PHASE_WORKING) {
+            Bangle.buzz(750, 1);
+        } else if (exports.state.phase == exports.PHASE_SHORT_BREAK) {
+            Bangle.buzz();
+            setTimeout(Bangle.buzz, 400);
+        } else {
+            Bangle.buzz();
+            setTimeout(Bangle.buzz, 400);
+            setTimeout(Bangle.buzz, 600);
+        }
+    }
+}
\ No newline at end of file
diff --git a/apps/pomoplus/icon.js b/apps/pomoplus/icon.js
new file mode 100644
index 000000000..e4ecc7d1c
--- /dev/null
+++ b/apps/pomoplus/icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwcBkmSpICG5AIHCLMmoQOKycJAoUyiQRLAgIOBAQQyKsmSpAROpgEBhmQzIHBC4JTIDIxcHDQYgCBQUSphEGpMJkwcEwgLByBoFCIMyCIgpDL4RQEBwWQ5ICBDoRKCBAIFBNYeSjJHDKYYaCR4YLBiYgDKYo4DEwQgECIpiECISqFkJlCCIILETwYRGDo1CsiJECIiPCdIaqCSoabFCgYRHAQ5iBCJ8hcAgRNKwOQgARLU4IRCvwRMa4QRPfwQR5YooR/cAYOGgAADvwEDCI8H/4AG/Ek5IRXGpMkzJZNoQGByYRNiQJCsgRLyAJDpgRQpIRLwgJEWxARBkIJFUg4RChIJGQA4RJNw4RKLg0kCJQ4DVoUACIY"))
\ No newline at end of file
diff --git a/apps/pomoplus/icon.png b/apps/pomoplus/icon.png
new file mode 100644
index 000000000..60d8023db
Binary files /dev/null and b/apps/pomoplus/icon.png differ
diff --git a/apps/pomoplus/img/pause.png b/apps/pomoplus/img/pause.png
new file mode 100644
index 000000000..ad31dadcf
Binary files /dev/null and b/apps/pomoplus/img/pause.png differ
diff --git a/apps/pomoplus/img/play.png b/apps/pomoplus/img/play.png
new file mode 100644
index 000000000..6c20c24c5
Binary files /dev/null and b/apps/pomoplus/img/play.png differ
diff --git a/apps/pomoplus/img/reset.png b/apps/pomoplus/img/reset.png
new file mode 100644
index 000000000..7a317d097
Binary files /dev/null and b/apps/pomoplus/img/reset.png differ
diff --git a/apps/pomoplus/img/skip.png b/apps/pomoplus/img/skip.png
new file mode 100644
index 000000000..375318069
Binary files /dev/null and b/apps/pomoplus/img/skip.png differ
diff --git a/apps/pomoplus/metadata.json b/apps/pomoplus/metadata.json
new file mode 100644
index 000000000..068eeed91
--- /dev/null
+++ b/apps/pomoplus/metadata.json
@@ -0,0 +1,37 @@
+{
+  "id": "pomoplus",
+  "name": "Pomodoro Plus",
+  "version": "0.05",
+  "description": "A configurable pomodoro timer that runs in the background.",
+  "icon": "icon.png",
+  "type": "app",
+  "tags": "pomodoro,cooking,tools",
+  "supports": [
+    "BANGLEJS",
+    "BANGLEJS2"
+  ],
+  "allow_emulator": true,
+  "storage": [
+    {
+      "name": "pomoplus.app.js",
+      "url": "app.js"
+    },
+    {
+      "name": "pomoplus.img",
+      "url": "icon.js",
+      "evaluate": true
+    },
+    {
+      "name": "pomoplus.boot.js",
+      "url": "boot.js"
+    },
+    {
+      "name": "pomoplus-com.js",
+      "url": "common.js"
+    },
+    {
+      "name": "pomoplus.settings.js",
+      "url": "settings.js"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/apps/pomoplus/settings.js b/apps/pomoplus/settings.js
new file mode 100644
index 000000000..1ff52340a
--- /dev/null
+++ b/apps/pomoplus/settings.js
@@ -0,0 +1,94 @@
+const SETTINGS_PATH = 'pomoplus.json';
+const storage = require("Storage");
+
+(function (back) {
+    let settings = storage.readJSON(SETTINGS_PATH);
+    if (!settings) {
+        settings = {
+            workTime: 1500000,                  //Work for 25 minutes
+            shortBreak: 300000,                 //5 minute short break
+            longBreak: 900000,                  //15 minute long break
+            numShortBreaks: 3,                  //3 short breaks for every long break
+            pausedTimerExpireTime: 21600000,    //If the timer was left paused for >6 hours, reset it on next launch
+            widget: false                       //If a widget is added in the future, whether the user wants it
+        };
+    }
+
+    function save() {
+        storage.writeJSON(SETTINGS_PATH, settings);
+    }
+
+    const menu = {
+        '': { 'title': 'Pomodoro Plus' },
+        '< Back': back,
+        'Work time': {
+            value: settings.workTime,
+            step: 60000,    //1 minute
+            min: 60000,
+            // max: 10800000,
+            // wrap: true,
+            onchange: function (value) {
+                settings.workTime = value;
+                save();
+            },
+            format: function (value) {
+                return '' + (value / 60000) + 'm'
+            }
+        },
+        'Short break time': {
+            value: settings.shortBreak,
+            step: 60000,
+            min: 60000,
+            // max: 10800000,
+            // wrap: true,
+            onchange: function (value) {
+                settings.shortBreak = value;
+                save();
+            },
+            format: function (value) {
+                return '' + (value / 60000) + 'm'
+            }
+        },
+        '# Short breaks': {
+            value: settings.numShortBreaks,
+            step: 1,
+            min: 0,
+            // max: 10800000,
+            // wrap: true,
+            onchange: function (value) {
+                settings.numShortBreaks = value;
+                save();
+            }
+        },
+        'Long break time': {
+            value: settings.longBreak,
+            step: 60000,
+            min: 60000,
+            // max: 10800000,
+            // wrap: true,
+            onchange: function (value) {
+                settings.longBreak = value;
+                save();
+            },
+            format: function (value) {
+                return '' + (value / 60000) + 'm'
+            }
+        },
+        'Timer expiration': {
+            value: settings.pausedTimerExpireTime,
+            step: 900000,   //15 minutes
+            min: 0,
+            // max: 10800000,
+            // wrap: true,
+            onchange: function (value) {
+                settings.pausedTimerExpireTime = value;
+                save();
+            },
+            format: function (value) {
+                if (value == 0) return "Off"
+                else return `${Math.floor(value / 3600000)}h ${(value % 3600000) / 60000}m`
+            }
+        },
+    };
+    E.showMenu(menu)
+})
\ No newline at end of file
diff --git a/apps/pooqroman/ChangeLog b/apps/pooqroman/ChangeLog
index c4f3171d3..b21b34b58 100644
--- a/apps/pooqroman/ChangeLog
+++ b/apps/pooqroman/ChangeLog
@@ -1,3 +1,4 @@
 0.01: Initial check-in.
 0.02: Make internal menu time out + small fixes.
 0.03: Autolight feature.
+0.04: Added adjustment for Bangle.js magnetometer heading fix
diff --git a/apps/pooqroman/app.js b/apps/pooqroman/app.js
index fcb2437e1..7bd749ac4 100644
--- a/apps/pooqroman/app.js
+++ b/apps/pooqroman/app.js
@@ -70,7 +70,7 @@ class Options {
             delay
         );
     }
-    
+
     bless(k) {
         Object.defineProperty(this, k, {
             get: () => this.backing[k],
@@ -103,7 +103,7 @@ class Options {
         if (this.bored) clearTimeout(this.bored);
         this.bored = setTimeout(_ => this.showMenu(), 15000);
     }
-    
+
     reset() {
         this.backing = {__proto__: this.constructor.defaults};
         this.writeBack(0);
@@ -145,7 +145,7 @@ class RomanOptions extends Options {
             Defaults: _ => {this.reset(); this.interact();}
         };
     }
-        
+
     interact() {this.showMenu(this.menu);}
 }
 
@@ -337,7 +337,7 @@ const events = {
     //            colour: colour, dramatic?: bool, event?: any}
     fixed: [{time: Number.POSITIVE_INFINITY}], // indexed by ms absolute
     wall: [{time: Number.POSITIVE_INFINITY}], // indexed by nominal ms + TZ ms
-    
+
     clean: function(now, l) {
         let o = now.getTimezoneOffset() * 60000;
         let tf = now.getTime() + l, tw = tf - o;
@@ -345,7 +345,7 @@ const events = {
         while (this.wall[0].time <= tw) this.wall.shift();
         while (this.fixed[0].time <= tf) this.fixed.shift();
     },
-    
+
     scan: function(now, from, to, f) {
         result = Infinity;
         let o = now.getTimezoneOffset() * 60000;
@@ -482,7 +482,7 @@ class Sidebar {
                 compassI,
                 this.x + 4 + imageWidth(compassI) / 2,
                 this.y + 4 + imageHeight(compassI) / 2,
-                a ? {rotate: c.heading / 180 * Math.PI} : undefined
+                a ? {rotate: (360-c.heading) / 180 * Math.PI} : undefined
             );
             this.y += 4 + imageHeight(compassI);
         }
@@ -535,13 +535,13 @@ class Roman {
     static pos(p, r) {
         let h = r * rectW / 2;
         let v = r * rectH / 2;
-        p = (p + 1) % 12;  
+        p = (p + 1) % 12;
         return p <= 2 ? [faceCX + h * (p - 1), faceCY - v]
             : p < 6 ? [faceCX + h, faceCY + v / 2 * (p - 4)]
             : p <= 8 ? [faceCX - h * (p - 7), faceCY + v]
             : [faceCX - h, faceCY - v / 2 * (p - 10)];
     }
-    
+
     alert(e, date, now, past) {
         const g = this.g;
         g.setColor(e.colour);
@@ -564,7 +564,7 @@ class Roman {
         }
         return Infinity;
     }
-    
+
     render(d, rate) {
         const g = this.g;
         const state = this.state || (g.clear(true), this.state = {});
@@ -625,7 +625,7 @@ class Roman {
             for (let h = keyHour; h < keyHour + 12; h++) {
                 g.drawString(
                     numeral(h % 24, options),
-                    faceX + layout[h % 12 * 2], 
+                    faceX + layout[h % 12 * 2],
                     faceY + layout[h % 12 * 2 + 1]
                 );
             }
@@ -643,7 +643,7 @@ class Roman {
             (e, t, p) => this.alert(e, t, d, p)
         );
         if (rate > requestedRate) rate = requestedRate;
-        
+
         // Hands
         // Here we are using incremental hands for hours and minutes.
         // If we quantised, we could use hand-crafted bitmaps, though.
@@ -668,7 +668,7 @@ class Clock {
         this.rates = {};
 
         this.options.on('done', () => this.start());
-        
+
         this.listeners = {
             charging: _ => {face.doIcons('charging'); this.active();},
             lock: _ => {face.doIcons('locked'); this.active();},
@@ -723,7 +723,7 @@ class Clock {
         this.face.reset(); // Cancel any ongoing background rendering
         return this;
     }
-    
+
     active() {
         const prev = this.rate;
         const now = Date.now();
diff --git a/apps/pooqroman/metadata.json b/apps/pooqroman/metadata.json
index 8cdbea728..0294e22a0 100644
--- a/apps/pooqroman/metadata.json
+++ b/apps/pooqroman/metadata.json
@@ -1,7 +1,7 @@
 { "id": "pooqroman",
   "name": "pooq Roman watch face",
   "shortName":"pooq Roman",
-  "version":"0.03",
+  "version":"0.04",
   "description": "A classic watch face with a certain dynamicity. Most amusing in 24h mode. Slide up to show more hands, down for less(!). By design does not support standard widgets, sorry!",
   "icon": "app.png",
   "type": "clock",
diff --git a/apps/powermanager/ChangeLog b/apps/powermanager/ChangeLog
index f0b60a45a..a83e8c676 100644
--- a/apps/powermanager/ChangeLog
+++ b/apps/powermanager/ChangeLog
@@ -1,3 +1,5 @@
 0.01: New App!
 0.02: Allow forcing monotonic battery voltage/percentage
 0.03: Use default Bangle formatter for booleans
+0.04: Remove calibration with current voltage (Calibrate->Auto) as it is now handled by settings app
+      Allow automatic calibration on every charge longer than 3 hours
diff --git a/apps/powermanager/README.md b/apps/powermanager/README.md
index 434ec814e..88b3c370a 100644
--- a/apps/powermanager/README.md
+++ b/apps/powermanager/README.md
@@ -3,8 +3,9 @@
 Manages settings for charging.
 Features:
 * Warning threshold to be able to disconnect the charger at a given percentage
-* Set the battery calibration offset.
+* Set the battery calibration offset
 * Force monotonic battery percentage or voltage
+* Automatic calibration on charging uninterrupted longer than 3 hours (reloads of the watch reset the timer).
 
 ## Internals
 
diff --git a/apps/powermanager/boot.js b/apps/powermanager/boot.js
index 077e24413..2bc2aaa35 100644
--- a/apps/powermanager/boot.js
+++ b/apps/powermanager/boot.js
@@ -5,7 +5,6 @@
   );
   
   if (settings.warnEnabled){
-    print("Charge warning enabled");
     var chargingInterval;
 
     function handleCharging(charging){
@@ -48,4 +47,13 @@
       return v;
     };
   }
+  
+  if (settings.autoCalibration){
+    let chargeStart;
+    Bangle.on("charging", (charging)=>{
+      if (charging) chargeStart = Date.now();
+      if (chargeStart && !charging && (Date.now() - chargeStart > 1000*60*60*3)) require("powermanager").setCalibration();
+      if (!charging) chargeStart = undefined;
+    });
+  }
 })();
diff --git a/apps/powermanager/lib.js b/apps/powermanager/lib.js
new file mode 100644
index 000000000..f4a7e3378
--- /dev/null
+++ b/apps/powermanager/lib.js
@@ -0,0 +1,6 @@
+// set battery calibration value by either applying the given value or setting the currently read battery voltage
+exports.setCalibration = function(calibration){
+  let s = require('Storage').readJSON("setting.json", true) || {};
+  s.batFullVoltage = calibration?calibration:((analogRead(D3) + analogRead(D3) + analogRead(D3) + analogRead(D3)) / 4);
+  require('Storage').writeJSON("setting.json", s);
+}
diff --git a/apps/powermanager/metadata.json b/apps/powermanager/metadata.json
index dd1727657..0777feee3 100644
--- a/apps/powermanager/metadata.json
+++ b/apps/powermanager/metadata.json
@@ -2,7 +2,7 @@
   "id": "powermanager",
   "name": "Power Manager",
   "shortName": "Power Manager",
-  "version": "0.03",
+  "version": "0.04",
   "description": "Allow configuration of warnings and thresholds for battery charging and display.",
   "icon": "app.png",
   "type": "bootloader",
@@ -12,6 +12,7 @@
   "storage": [
     {"name":"powermanager.boot.js","url":"boot.js"},
     {"name":"powermanager.settings.js","url":"settings.js"},
+    {"name":"powermanager","url":"lib.js"},
     {"name":"powermanager.default.json","url":"default.json"}
   ]
 }
diff --git a/apps/powermanager/settings.js b/apps/powermanager/settings.js
index 7cc683024..9eeb29e00 100644
--- a/apps/powermanager/settings.js
+++ b/apps/powermanager/settings.js
@@ -23,7 +23,6 @@
     '': {
       'title': 'Power Manager'
     },
-    '< Back': back,
     'Monotonic percentage': {
       value: !!settings.forceMonoPercentage,
       onchange: v => {
@@ -44,29 +43,29 @@
     }
   };
 
-
   function roundToDigits(number, stepsize) {
     return Math.round(number / stepsize) * stepsize;
   }
 
-  function getCurrentVoltageDirect() {
-    return (analogRead(D3) + analogRead(D3) + analogRead(D3) + analogRead(D3)) / 4;
-  }
-
   var stepsize = 0.0002;
-  var full = 0.32;
+  var full = 0.3144;
 
   function getInitialCalibrationOffset() {
     return roundToDigits(systemsettings.batFullVoltage - full, stepsize) || 0;
   }
 
-
   var submenu_calibrate = {
     '': {
-      title: "Calibrate"
+      title: "Calibrate",
+      back: function() {
+        E.showMenu(mainmenu);
+      },
     },
-    '< Back': function() {
-      E.showMenu(mainmenu);
+    'Autodetect': {
+      value: !!settings.autoCalibration,
+      onchange: v => {
+        writeSettings("autoCalibration", v);
+      }
     },
     'Offset': {
       min: -0.05,
@@ -75,25 +74,9 @@
       value: getInitialCalibrationOffset(),
       format: v => roundToDigits(v, stepsize).toFixed((""+stepsize).length - 2),
       onchange: v => {
-        print(typeof v);
-        systemsettings.batFullVoltage = v + full;
-        require("Storage").writeJSON("setting.json", systemsettings);
+        require("powermanager").setCalibration(v + full);
       }
     },
-    'Auto': function() {
-      var newVoltage = getCurrentVoltageDirect();
-      E.showAlert("Please charge fully before auto setting").then(() => {
-        E.showPrompt("Set current charge as full").then((r) => {
-          if (r) {
-            systemsettings.batFullVoltage = newVoltage;
-            require("Storage").writeJSON("setting.json", systemsettings);
-            //reset value shown in menu to the newly set one
-            submenu_calibrate.Offset.value = getInitialCalibrationOffset();
-            E.showMenu(mainmenu);
-          }
-        });
-      });
-    },
     'Clear': function() {
       E.showPrompt("Clear charging offset?").then((r) => {
         if (r) {
@@ -109,10 +92,10 @@
 
   var submenu_chargewarn = {
     '': {
-      title: "Charge warning"
-    },
-    '< Back': function() {
-      E.showMenu(mainmenu);
+      title: "Charge warning",
+      back: function() {
+        E.showMenu(mainmenu);
+      },
     },
     'Enabled': {
       value: !!settings.warnEnabled,
diff --git a/apps/powersave/ChangeLog b/apps/powersave/ChangeLog
new file mode 100644
index 000000000..28d913cc8
--- /dev/null
+++ b/apps/powersave/ChangeLog
@@ -0,0 +1,3 @@
+0.01: Initial release
+0.02: Removed accelerometer poll interval adjustment, fixed a few issues with detecting the current app
+0.03: Fix a couple of silly mistakes
\ No newline at end of file
diff --git a/apps/powersave/README.md b/apps/powersave/README.md
index 5be5e32b5..51ba044e1 100644
--- a/apps/powersave/README.md
+++ b/apps/powersave/README.md
@@ -6,7 +6,6 @@ Save your watch's battery power by halting foreground app execution while the sc
 - Stops foreground app processes
 - Background processes still run
 - Clears screen
-- Decreases accelerometer polls
 - Foreground app is returned to when screen is turned back on (app state is not preserved)
 
 ## Controls
@@ -14,7 +13,6 @@ Save your watch's battery power by halting foreground app execution while the sc
 - Deactivates when screen is turned back on
 
 ## Warnings
-- Due to an Espruino bug, this does not take affect immediately when installed. Switch apps for these features to take affect.
 - This is not compatible with apps that need to run in the foreground even while the screen is off, such as most stopwatch apps and some health trackers.
 - If you check your watch super often (like multiple times per minute), this may end of costing you more power than it saves since the app you are using will have to restart everytime you check it.
 
diff --git a/apps/powersave/boot.js b/apps/powersave/boot.js
index d170e9d59..f37fbc536 100644
--- a/apps/powersave/boot.js
+++ b/apps/powersave/boot.js
@@ -1,15 +1,20 @@
 var Storage = Storage || require("Storage");
 Bangle.on("lock", locked => {
   if(locked){
-    g.clear().reset();
-    Bangle.setLCDBrightness(0);
-    Bangle.setPollInterval(1000);
     load("powersave.screen.js");
   }else{
-    load(Storage.read("resumeaftersleep") || JSON.parse(Storage.read("setting.json")).clock);
+    const data = JSON.parse(Storage.read("powersave.json") || Storage.read("setting.json"));
+    load(data.app || data.clock);
   }
 });
 E.on("init", () => {
-  if(__FILE__ && __FILE__ !== "powersave.screen.js")
-  Storage.write("resumeaftersleep", __FILE__);
+  if("__FILE__" in global && __FILE__ !== "powersave.screen.js"){
+    Storage.write("powersave.json", {
+      app: __FILE__
+    });
+  }else if(!("__FILE__" in global)){
+    Storage.write("powersave.json", {
+      app: null
+    });
+  }
 });
\ No newline at end of file
diff --git a/apps/powersave/metadata.json b/apps/powersave/metadata.json
index 50603b2c2..705384058 100644
--- a/apps/powersave/metadata.json
+++ b/apps/powersave/metadata.json
@@ -1,7 +1,7 @@
 {
   "id": "powersave",
   "name": "Power Save",
-  "version": "0.01",
+  "version": "0.03",
   "description": "Halts foreground app execution while screen is off while still allowing background processes.",
   "readme": "README.md",
   "icon": "powersave.png",
@@ -10,6 +10,9 @@
   "supports": ["BANGLEJS2"],
   "storage": [
     {"name":"powersave.boot.js","url":"boot.js"},
-    {"name":"powersave.screen.js","url":"boot.js"}
+    {"name":"powersave.screen.js","url":"screen.js"}
+  ],
+  "data": [
+    {"name": "powersave.json"}
   ]
 }
\ No newline at end of file
diff --git a/apps/powersave/screen.js b/apps/powersave/screen.js
index f987f0bbb..c920b205d 100644
--- a/apps/powersave/screen.js
+++ b/apps/powersave/screen.js
@@ -1,7 +1,7 @@
-var Storage = Storage || require("Storage");
 g.clear();
 Bangle.setLCDBrightness(0);
-Bangle.setPollInterval(1000);
 if(!Bangle.isLocked()){
-  load(Storage.read("resumeaftersleep") || JSON.parse(Storage.read("setting.json")).clock);
+  var Storage = Storage || require("Storage");
+  const data = JSON.parse(Storage.read("powersave.json") || Storage.read("setting.json"));
+  load(data.app || data.clock);
 }
\ No newline at end of file
diff --git a/apps/primetime/README.md b/apps/primetime/README.md
new file mode 100644
index 000000000..a07c19f52
--- /dev/null
+++ b/apps/primetime/README.md
@@ -0,0 +1,10 @@
+# App Name
+
+Watchface that displays time and the prime factors of the "military time" (i.e. 21:05 => 2105, shows prime factors of 2105 which are 5 & 421). Displays "Prime Time!" if prime. 
+
+![image](https://user-images.githubusercontent.com/115424919/194777279-7f5e4d2a-f475-4099-beaf-38db5b460714.png)
+
+
+## Creator
+
+Adapted from simplestclock by [Eve Bury](https://www.github.com/eveeeon)
diff --git a/apps/primetime/app.png b/apps/primetime/app.png
new file mode 100644
index 000000000..5024727fb
Binary files /dev/null and b/apps/primetime/app.png differ
diff --git a/apps/primetime/metadata.json b/apps/primetime/metadata.json
new file mode 100644
index 000000000..d796d290c
--- /dev/null
+++ b/apps/primetime/metadata.json
@@ -0,0 +1,15 @@
+{ "id": "primetime",
+  "name": "Prime Time Clock",
+  "version": "0.01",  
+  "type": "clock",
+  "description": "A clock that tells you the primes of the time",
+  "icon": "app.png",
+  "screenshots": [{"url":"screenshot.png"}],  
+  "tags": "clock",
+  "supports": ["BANGLEJS2"],  
+  "readme": "README.md",
+  "storage": [
+    {"name":"primetime.app.js","url":"primetime.js"},
+    {"name":"primetime.img","url":"primetime-icon.js","evaluate":true}
+  ]
+}
diff --git a/apps/primetime/primetime-icon.js b/apps/primetime/primetime-icon.js
new file mode 100644
index 000000000..57969a68b
--- /dev/null
+++ b/apps/primetime/primetime-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwgVVABVADJMBBf4L/Bf4LMgtQgIHCitAqoHBoEv+EHwALBv/S//4BYO//svwELoP//X/+gLB2E93+Ah9B9f+//QBYMVvv3C4XvvwLDl/0q+AgsB998qt4F4XgHYIXB/1+6ALC//93/4F4I7CI4QLBAIMLoF/6ABBBYNVqgBBgprCAIKz0qkAooLHgP8gXvvALH/EL7e4BY+tz/+vovH3PR1++L9YL/BYdVABQ="))
diff --git a/apps/primetime/primetime.js b/apps/primetime/primetime.js
new file mode 100644
index 000000000..bba63bc48
--- /dev/null
+++ b/apps/primetime/primetime.js
@@ -0,0 +1,89 @@
+const h = g.getHeight();
+const w = g.getWidth();
+
+
+
+// creates a list of prime factors of n and outputs them as a string, if n is prime outputs "Prime Time!"
+function primeFactors(n) {
+  const factors = [];
+  let divisor = 2;
+
+  while (n >= 2) {
+    if (n % divisor == 0) {
+      factors.push(divisor);
+      n = n / divisor;
+    } else {
+      divisor++;
+    }
+  }
+  if (factors.length === 1) {
+    return "Prime Time!";
+  }
+  else
+    return factors.toString();
+}
+
+
+// converts time HR:MIN to integer HRMIN e.g. 15:35 => 1535
+function timeToInt(t) {
+    var arr = t.split(':');
+    var intTime = parseInt(arr[0])*100+parseInt(arr[1]);
+
+    return intTime;
+}
+
+
+
+function draw() {
+  var date = new Date();
+  var timeStr = require("locale").time(date,1);
+  var primeStr = primeFactors(timeToInt(timeStr));
+
+  g.reset();
+  g.setColor(0,0,0);
+  g.fillRect(Bangle.appRect);
+
+  g.setFont("6x8", w/30);
+  g.setFontAlign(0, 0);
+  g.setColor(100,100,100);
+  g.drawString(timeStr, w/2, h/2);
+  g.setFont("6x8", w/60);
+  g.drawString(primeStr, w/2, 3*h/4);
+  queueDraw();
+}
+
+// timeout used to update every minute
+var drawTimeout;
+
+// schedule a draw for the next minute
+function queueDraw() {
+  if (drawTimeout) clearTimeout(drawTimeout);
+  drawTimeout = setTimeout(function() {
+    drawTimeout = undefined;
+    draw();
+  }, 60000 - (Date.now() % 60000));
+}
+
+// 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;
+  }
+});
+
+g.clear();
+
+// Show launcher when middle button pressed
+// Bangle.setUI("clock");
+// use  clockupdown as it tests for issue #1249
+Bangle.setUI("clockupdown", btn=> {
+  draw();
+});
+
+// Load widgets
+Bangle.loadWidgets();
+Bangle.drawWidgets();
+draw();
diff --git a/apps/primetime/screenshot.png b/apps/primetime/screenshot.png
new file mode 100644
index 000000000..cb625a9b6
Binary files /dev/null and b/apps/primetime/screenshot.png differ
diff --git a/apps/primetimelato/ChangeLog b/apps/primetimelato/ChangeLog
new file mode 100644
index 000000000..46690e360
--- /dev/null
+++ b/apps/primetimelato/ChangeLog
@@ -0,0 +1,2 @@
+0.01: first release
+0.02: added option to buzz on prime, with settings
diff --git a/apps/primetimelato/README.md b/apps/primetimelato/README.md
new file mode 100644
index 000000000..924a6fae6
--- /dev/null
+++ b/apps/primetimelato/README.md
@@ -0,0 +1,19 @@
+# Prime Time Lato (clock)
+
+A watchface that displays time and its prime factors in the Lato font.
+For example when the time is 21:05, the prime factors are 5,421.
+Displays 'Prime Time!' when the time is a prime number.
+
+There is a settings option added in the Settings App.  If 'Buzz on
+Prime' is ticked then the buzzer will sound when 'Prime Time!' is
+detected.  Note the buzzer is limited to between 8am and 8pm so it
+should not go off when you want to sleep.
+
+
+![](screenshot.jpg)
+
+Written by: [Hugh Barney](https://github.com/hughbarney)
+
+Adapted from primetime by [Eve Bury](https://www.github.com/eveeeon)
+
+For support and discussion please post in the [Bangle JS Forum](http://forum.espruino.com/microcosms/1424/)
diff --git a/apps/primetimelato/app.js b/apps/primetimelato/app.js
new file mode 100644
index 000000000..817da7cda
--- /dev/null
+++ b/apps/primetimelato/app.js
@@ -0,0 +1,132 @@
+const h = g.getHeight();
+const w = g.getWidth();
+const SETTINGS_FILE = "primetimelato.json";
+let settings;
+
+Graphics.prototype.setFontLato = function(scale) {
+  // Actual height 41 (43 - 3)
+  this.setFontCustom(
+    E.toString(require('heatshrink').decompress(atob('ACEfAokB/AGEn+AAocDBgsfBgkB+A6Yg4FEgYgF/4FEv/gHIhAEh/+DwgYEgP/4AeJn4eF/hDEDwxrE/4eFKAgeFgJDERQ5DEXJI0Eh//IIZlB/4pDAoP/HgQYBAAIaBPARDDv4JBj5WBh5ZCv4CBPATPCeQcPwDYECAIMGPId4gEeSIYMHDIYMCExZAGh6ICn5QEK4RnGOgqBGaoKOECYSiDn78FAFgxFR4bcCKISOBgF+RwYCBTgQMCOIQMCj5eCBgIfDBgQfCBgSbDWoSHC/61CAwYMUAAYzCAAZNCBkYAfgYmBgJ7CTYsPTYQMBgJnBgYMCn4MBv64CBgMPXYSBBD4cfBgQIBgf3RwK7CvybCGQXPBgIfBGQIMBeoU/DoIMCF4IMDgP8BgQmBn8AEwb+BBgQIBIQJAC4BYBIgTNBDYIMBg///xRDn//OoIMBcYISBBgUHNATpCgjCngIEDFIM+AoV8TYKYCMIJQBAQLCCgZcBYQIJBX4UHCwSJBYQaSCMYQDCYQabEBgngW4V4WoQBBOgIMN+AoCEwY+BGYZfBYwQIBv+AG4X+j/8K4TCB+bECM4P+YgcD//B/67Ch/4h//K4SSBv5IBAAdAWqMYAokDD4t+AokfBgi0DAAX/Bgkf7wSE/IME/6PBCQfBBgcD/AME/ypCCQXABgYSBBgg+BBgYSBBgatBBggSBBgYSBBgcB/4ACZ4QGDZ4IMLFARAEAAJaEBjQAUhAnFMgIABuD5CVwWAboUDLwZ0DNYKLBM4IMBh50C55rBCoWACAIMC+EPFIN4EQQMBg4MSHgYzEBgIzBIAUAvhND+EH8DZCBAN/bIfwMQP/OgIMBLgalCBgKlDg//V4kfCQIAqWgYzC/gFDIAJgBZoS3CAwV//4TDh/+j5UCCQOAn4SD8DPCCQSbCCQR/BNAV/3i1CB4PzAYLCBgP8AYIMCv+HBgcP+AMDCQMHEwb2BYQLPDgYMBIAKoBOYLPCwDNBZ4UQBQPhJ4J0EDYJbCZ4R7EAoZiDSoaUDADBNBFQj5EKwQMEGAoMEOgQFCnAMEQIYFBgaOCBgTRBBgc/AYIMCaIQMCgb2CBgX/JQIMCDAQMCh/8JoYYBJoiNDBgIYDBgIYDBgPzUwkfUwisBOokfDAYMCQIq/ERwwAcn4pCgfwg42D//B/6hBCAP+KwYQBMQKbBgF//9+g5EBh4YB4CfC/EHDwK1Dn7PD8A0BgF4gEeAIUHBgQBBBi4mEGYpAEAIMP4BNELQpnGOgM/ZYaBFGQMPYos/JAIAuj4xEKgJrBfoX//hEE/4TDCQJSCCoN/gZfBjCBCj+AgaOCAIiKBg4OCgKKBvgbCWYMDToK1CgE8JIQMC4ZCBBgU4HYTNCz4JBEwV7KoQzCUIYvBLYZNBn60CLQPfCQcDM4LHCEALHDZ4TaCCYaODHYK8fh6FDEwKSCF4Uf4COCBgJsBn4MDDIJPDVgYAZA='))),
+    46,
+    atob("CxMeHh4eHh4eHh4eDQ=="),
+    52+(scale<<8)+(1<<16)
+  );
+  return this;
+};
+
+Graphics.prototype.setFontLatoSmall = function(scale) {
+  // Actual height 21 (20 - 0)
+  this.setFontCustom(
+    E.toString(require('heatshrink').decompress(atob('AB0B/+ch/88EAgQPHg/AgE+A4cPwEAvwTHoEAscQgc/wE//EP/0Au0wgEz/ED/+A//gg9jgEDiEAAIIACgcBwF+h0H+EwmPBwOf/4AB8Ng4cDg84hkfwFAvA/EgZfBneAwOEjkMkPAuccgPzwAbBCQJeBuZBBKYNxxkOhkgsFjgUD+B5BNox+Bgf4g/Y8F/wcDjkYjFw4HB40Gg8wjkfQgJLBj6bB84fBjCQDU5AAFj/wg//+A1B9xEGhkAg18gPx//+bgJmBAAckAQOgDQa+BgI2BjQ0HsBoBJogSBZgX/DQIrBG4IUFAAs7CQKVBFJ0GB4kEAQNwYQYACEIKaBRYX4RoQGCCoIADMwMf/kB8HwdQPA4EGGAMwmEBwPAjkHwfACgMAv4iBAAxICuEMSQNguEDgf//Ef/5ZEmCDDAAkBgHgnEOgfA8Eeg0B2EwjOBweMh04sE/wZlBfYomCjkPEIM8VwNgsI+BhkYjHg4HnFoPv8EOQYIAFQwMHJAN8gEOW4PjAgMYQwUPcIN/cI4ACkCNBgP8hkPsFgsY+BHoMYD4PHjkGj/gkAxBAAoHBh/wgPxV4J8B7EwnnBwPGhkMnHggLUBg/gXI6oDCgLiBsE+gb0BjD7B74bDni1CAAk8gP3+EP/ZTBfYMYfYI+Bw0Mh148E+HwPj+A6DAAIpBj6HB/0EhkwPwPPgcE+EYt4ZB8EDDwMP4EAiAeDgUCgE4nEB4INBAAcBKQMcjuA8BhBAAh2BgY5BjwUBJAMMDwPGJoMYf4ImFAAoUCswhBEgMZEgPMDgL7BmYpBzAUDABnBwEDhhTBFIiIBh4PBuDKCCwR6BgITBDAKSB88Dh84jAKB/geBHAQcBj/wgPhcIMDgOBhkYn0w4exw0wwkhxkhxHBhl/hFj7BKBo0Mg0wnnjgF+LIkEbQc/gEH8EBHgM/sEH4bGBjEB/HAgf2JIKvBCgICBJ4MOaYv//kP//gsHDga/BjEw4HBw0GjkwnHhwH9/kH54hBnggDkB1D//AjydBPIIyBFIMDgcAFIILB4EGg0AFIILBCgLLHj4kB//+CgUwTwOAhi8DFIccZANghkD4Pgh/+D4L7IFIpuBmHBwOGFIMwsFhwcDaAJTCwECSRasBJgIjBgDXBgxMBEYMBwUAhAdEn+Ag//gF8vkDwHgPoJoIXgnhw0Dh/gjF/Mo8D//4NASDBIgICBPQJLBIgJZBwEAF4IwBgB9BIYPwE44pHBYoiHg4EBvAUBwAZBExMAv//FIQbCKYU8AgJABnPggeHwE4jyKBnC8CgC8LEQZ2PNBA4BgIgCDYMHCwMfJIYNCnwMBB4N4gEPBYY+CNAJyHU4U/QoIxCFwQNBjwCBnACBHgpoGAAz4BB4PAj5OB8E4hwDBsEDPwMYSQXAgx+BmE4TwIUBPAOAh65Bj5wBAAxTBwI+BhgiBsHAgYiBjChB4OAg8cDwJVBvgfHB4KCBvl8HQmBwEMXoNgKY/ghwUBPAP+SoPhwF8ghOH//+U4UwsEBwbnBKYXwgcPRQPngF+jzMBuAbBOYnAn8GgP4nEc8HA4aSBjkwmHhwPDzkGNwMgNwYwBDoMAQgKoBcYIqBfYhoBChQAFv/4YIPwewIgBboIoFSIJnBgEHAgPwj6nBDIQVEAwMH8BBBAQQ6BdIUeCAc/BgR4BHwI6BCYJTCEIQCBj4CBh4CCE4QVBDQP+dYYLBnwcBBIMBAQoUBg/Agf8KwIZBHYIoBNIQSBfYUcIQP4nkB8YZBDwMPLoK5BJIXz8EPg+A8EeU4KmCXgYFBngCBDwL2BLAJGBMwIHCB4JKBgF4BwIWBiBGCgQpBuEwg+BwF8hkPsFg+cDj0YjPg4HcgwhBmBXBCgJnDAALmB/+f//8JYMkIoIMCHYJqCAQUfBYICCUYU4EgkIJINAgETFIgPEgAhBHoJtEgb3DQoacBSAQAGkBLDGYMAGYMCQ4gYBgiLCAQMYAQIzBDQQAFsbZBMYMxzEBxlAhlmgFikCIBwEPLowABn//4P/aIMwCgJUBjBpB4FgWYISBMIP/OYYAEg4MBv/Ag4UBmEYEAPAhjlBsEwgLyCAAcBIYMf+EB4IUFHwcIj//8C5BLA4cB4EBBgMdjkA5BTBocAmQ+ByBGBx0AjlgDISoCfwJ4BQ4P4jKwB5kAgwTDsEP3+A//mg03mEwzOBwnMU4Ngv8zgfh+EYPAKEEFIIuB4BnBEoMAPgTcEHgMAh/4NQ8gBwOf/kcPoKWEyEAhvP//uv7UBQgiSETgJ4BAgLJBnPAgeHcweAGALDGfYMP/4XBAAxmBSoP4FYQgBNAsPcIN/8CBCmBADCgV/dwP/BAIpIYgQpHLoIpCdwT7Mj08EIL7BDoMwfYOA4EOhwxBT4KUFHwXwZ4ITB4EMaQNgmEDDoMYH4LeBgZtBgZdFHwscCgI+HwOAUoPgv5eIPp8QCgcD4YUBFIOYKYPGgFjKYMfwEIvAUCgi8Dj5qBcwJoGGQQADn7JCH4J9BbRHAKYoAEuBLBVIMHAQMPEIMPBoIUBJYIMCvhoDAAQZFDgkeBQUBDwIGBEYUBXgIKCgYUBNAM/wA+CSQi8CnE4gH3B4PwFIINBIIMPSQPh8AUCAAMBLQR3Bn4KBn4SBv4fDDgJlBgI2BTw0AU4XBFIMfgEx7EB3nAh+GgF4XgOBF4JRCGAP/8f+/+YbAINBkArGbwIQB/5BBAArwBgLSBhP/v/H/6QBBAIACjhrDKwVgdAwHBRQThBgIbD'))),
+    32,
+    atob("BAcIDAwRDwUGBggMBAcECAwMDAwMDAwMDAwFBQwMDAgRDg4OEAwMDxAGCQ4LExARDREOCwwPDhUODQ0GCAYMCAYLDAoMCwcLDAUFCwURDAwMDAgJCAwLEAsLCgYGBgwA"),
+    21+(scale<<8)+(1<<16)
+  );
+  return this;
+};
+
+function loadSettings() {
+  settings = require("Storage").readJSON(SETTINGS_FILE,1)||{};
+  settings.buzz_on_prime = (settings.buzz_on_prime === undefined ? false : settings.buzz_on_prime);
+}
+	
+// creates a list of prime factors of n and outputs them as a string, if n is prime outputs "Prime Time!"
+function primeFactors(n) {
+  const factors = [];
+  let divisor = 2;
+
+  while (n >= 2) {
+    if (n % divisor == 0) {
+      factors.push(divisor);
+      n = n / divisor;
+    } else {
+      divisor++;
+    }
+  }
+  if (factors.length === 1) {
+    return "Prime Time!";
+  }
+  else
+    return factors.toString();
+}
+
+
+// converts time HR:MIN to integer HRMIN e.g. 15:35 => 1535
+function timeToInt(t) {
+    var arr = t.split(':');
+    var intTime = parseInt(arr[0])*100+parseInt(arr[1]);
+
+    return intTime;
+}
+
+function draw() {
+  var date = new Date();
+  var timeStr = require("locale").time(date,1);
+  var intTime = timeToInt(timeStr);
+  var primeStr = primeFactors(intTime);
+
+  g.reset();
+  g.setColor(0,0,0);
+  g.fillRect(Bangle.appRect);
+
+  g.setFontLato();
+  g.setFontAlign(0, 0);
+  g.setColor(100,100,100);
+  g.drawString(timeStr, w/2, h/2);
+
+  g.setFontLatoSmall();
+  g.drawString(primeStr, w/2, 3*h/4);
+
+  // Buzz if Prime Time and between 8am and 8pm
+  if (settings.buzz_on_prime && primeStr == "Prime Time!" && intTime >= 800 && intTime <= 2000)
+      buzzer(2);
+  queueDraw();
+}
+
+// timeout for multi-buzzer
+var buzzTimeout;
+
+// n buzzes
+function buzzer(n) {
+  if (n-- < 1) return;
+  Bangle.buzz(250);
+  
+  if (buzzTimeout) clearTimeout(buzzTimeout);
+  buzzTimeout = setTimeout(function() {
+    buzzTimeout = undefined;
+    buzzer(n);
+  }, 500);
+}
+	
+// timeout used to update every minute
+var drawTimeout;
+
+// schedule a draw for the next minute
+function queueDraw() {
+  if (drawTimeout) clearTimeout(drawTimeout);
+  drawTimeout = setTimeout(function() {
+    drawTimeout = undefined;
+    draw();
+  }, 60000 - (Date.now() % 60000));
+}
+
+// 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;
+  }
+});
+
+
+loadSettings();
+g.clear();
+// Show launcher when middle button pressed
+Bangle.setUI("clock");
+
+// Load widgets
+Bangle.loadWidgets();
+Bangle.drawWidgets();
+draw();
diff --git a/apps/primetimelato/app.png b/apps/primetimelato/app.png
new file mode 100644
index 000000000..2a84c62a0
Binary files /dev/null and b/apps/primetimelato/app.png differ
diff --git a/apps/primetimelato/icon.js b/apps/primetimelato/icon.js
new file mode 100644
index 000000000..06f93e2ef
--- /dev/null
+++ b/apps/primetimelato/icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwIdah/wAof//4ECgYFB4AFBg4FB8AFBj/wh/4AoM/wEB/gFBvwCEBAU/AQP4gfAj8AgPwAoMPwED8AFBg/AAYIBDA4ngg4TB4EBApkPKgJSBJQIFTMgIFCJIIFDKoIFEvgFBGoMAnw7DP4IFEh+BAoItBg+DNIQwBMIaeCKoKxCPoIzCEgKVHUIqtFXIrFFaIrdFdIwAV"))
diff --git a/apps/primetimelato/metadata.json b/apps/primetimelato/metadata.json
new file mode 100644
index 000000000..6b292c380
--- /dev/null
+++ b/apps/primetimelato/metadata.json
@@ -0,0 +1,18 @@
+{ "id": "primetimelato",
+  "name": "Prime Time Lato Clock",
+  "version": "0.02",  
+  "type": "clock",
+  "description": "A clock that tells you the primes of the time in the Lato font",
+  "icon": "app.png",
+  "screenshots": [{"url":"screenshot.png"}],  
+  "tags": "clock",
+  "supports": ["BANGLEJS2"],  
+  "readme": "README.md",
+  "storage": [
+    {"name":"primetimelato.app.js","url":"app.js"},
+    {"name":"primetimelato.img","url":"icon.js","evaluate":true},
+    {"name":"primetimelato.settings.js","url":"settings.js"}
+
+  ],
+  "data": [{"name":"primetimelato.json"}]
+}
diff --git a/apps/primetimelato/screenshot.png b/apps/primetimelato/screenshot.png
new file mode 100644
index 000000000..7f6e7cc0d
Binary files /dev/null and b/apps/primetimelato/screenshot.png differ
diff --git a/apps/primetimelato/settings.js b/apps/primetimelato/settings.js
new file mode 100644
index 000000000..5550055eb
--- /dev/null
+++ b/apps/primetimelato/settings.js
@@ -0,0 +1,34 @@
+(function(back) {
+  const SETTINGS_FILE = "primetimelato.json";
+
+  // initialize with default settings...
+  let s = {
+    'buzz_on_prime': true
+  }
+
+  // ...and overwrite them with any saved values
+  // This way saved values are preserved if a new version adds more settings
+  const storage = require('Storage')
+  let settings = storage.readJSON(SETTINGS_FILE, 1) || {}
+  const saved = settings || {}
+  for (const key in saved) {
+    s[key] = saved[key]
+  }
+
+  function save() {
+      settings = s;
+      storage.write(SETTINGS_FILE, settings);
+  }
+
+  E.showMenu({
+    '': { 'title': 'Prime Time Lato' },
+    '< Back': back,
+    'Buzz on Prime': {
+      value: !!s.buzz_on_prime,
+      onchange: v => {
+        s.buzz_on_prime = v;
+        save();
+      },
+    }
+  })
+})
diff --git a/apps/quicklaunch/ChangeLog b/apps/quicklaunch/ChangeLog
index ae1d4a848..0ab99632a 100644
--- a/apps/quicklaunch/ChangeLog
+++ b/apps/quicklaunch/ChangeLog
@@ -1,2 +1,4 @@
 0.01: Initial version
 0.02: Moved settings from launcher to settings->apps menu
+0.03: Better performance by not scanning on every boot
+0.04: Better performace by not scanning on boot at all
diff --git a/apps/quicklaunch/boot.js b/apps/quicklaunch/boot.js
index 3670c4776..4ab238293 100644
--- a/apps/quicklaunch/boot.js
+++ b/apps/quicklaunch/boot.js
@@ -1,67 +1,24 @@
-(function() {
-  var settings = Object.assign(require("Storage").readJSON("quicklaunch.json", true) || {});
+{
+  let settings = require("Storage").readJSON("quicklaunch.json", true) || {};
+  const storage = require("Storage");
 
-  //list all sources
-  var apps = require("Storage").list(/\.info$/).map(app=>{var a=require("Storage").readJSON(app,1);return a&&{src:a.src};});
-  
-  //populate empty app list
-      
-  if (!settings.leftapp) {
-    settings["leftapp"] = {"name":"(none)"};
-    require("Storage").write("quicklaunch.json",settings);
-  }
-  if (!settings.rightapp) {
-    settings["rightapp"] = {"name":"(none)"};
-    require("Storage").write("quicklaunch.json",settings);
-  }
-    if (!settings.upapp) {
-    settings["upapp"] = {"name":"(none)"};
-    require("Storage").write("quicklaunch.json",settings);
-  }
-    if (!settings.downapp) {
-    settings["downapp"] = {"name":"(none)"};
-    require("Storage").write("quicklaunch.json",settings);
-  }
-    if (!settings.tapapp) {
-    settings["tapapp"] = {"name":"(none)"};
-    require("Storage").write("quicklaunch.json",settings);
-  }
+  let reset = function(name){
+    if (!settings[name]) settings[name] = {"name":"(none)"};
+    if (!require("Storage").read(settings[name].src)) settings[name] = {"name":"(none)"};
+    storage.write("quicklaunch.json", settings);
+  };
 
-  //activate on clock faces
-  var sui = Bangle.setUI;
-  Bangle.setUI = function(mode, cb) {
-    sui(mode,cb);
-    if(!mode) return;
-    if ("object"==typeof mode) mode = mode.mode;
-    if (!mode.startsWith("clock")) return;
-
-  function tap() {
-    //tap, check if source exists, launch
-    if ((settings.tapapp.src) && apps.some(e => e.src === settings.tapapp.src)) load (settings.tapapp.src);
-  }
-    
-  let drag;
-  let e;
-  
-  Bangle.on("touch",tap);
-  Bangle.on("drag", e => {
-    if (!drag) { // start dragging
-      drag = {x: e.x, y: e.y};
-    } else if (!e.b) { // released
-      const dx = e.x-drag.x, dy = e.y-drag.y;
-      drag = null;
-      //horizontal swipes, check if source exists, launch
-      if (Math.abs(dx)>Math.abs(dy)+10) {
-        if ((settings.leftapp.src) && apps.some(e => e.src === settings.leftapp.src) && dx<0) load(settings.leftapp.src);
-        if ((settings.rightapp.src) && apps.some(e => e.src === settings.rightapp.src) && dx>0) load(settings.rightapp.src);
-      } 
-      //vertical swipes, check if source exists, launch
-      else if (Math.abs(dy)>Math.abs(dx)+10) {
-        if ((settings.upapp.src) && apps.some(e => e.src === settings.upapp.src) && dy<0) load(settings.upapp.src);
-        if ((settings.downapp.src) && apps.some(e => e.src === settings.downapp.src) && dy>0) load(settings.downapp.src);
-      }
-    }
+  Bangle.on("touch", () => {
+    if (!Bangle.CLOCK) return;
+    if (settings.tapapp.src){ if (!storage.read(settings.tapapp.src)) reset("tapapp"); else load(settings.tapapp.src); }
   });
 
-  };
-})();
+  Bangle.on("swipe", (lr,ud) => {
+    if (!Bangle.CLOCK) return;
+
+    if (lr == -1 && settings.leftapp && settings.leftapp.src){ if (!storage.read(settings.leftapp.src)) reset("leftapp"); else load(settings.leftapp.src); }
+    if (lr == 1 && settings.rightapp && settings.rightapp.src){ if (!storage.read(settings.rightapp.src)) reset("rightapp"); else load(settings.rightapp.src); }
+    if (ud == -1 && settings.upapp && settings.upapp.src){ if (!storage.read(settings.upapp.src)) reset("upapp"); else load(settings.upapp.src); }
+    if (ud == 1 && settings.downapp && settings.downapp.src){ if (!storage.read(settings.downapp.src)) reset("downapp"); else load(settings.downapp.src); }
+  });
+}
diff --git a/apps/quicklaunch/metadata.json b/apps/quicklaunch/metadata.json
index 49eafdd35..ccad02c96 100644
--- a/apps/quicklaunch/metadata.json
+++ b/apps/quicklaunch/metadata.json
@@ -2,7 +2,7 @@
   "id": "quicklaunch",
   "name": "Quick Launch",
   "icon": "app.png",
-  "version":"0.02",
+  "version":"0.04",
   "description": "Tap or swipe left/right/up/down on your clock face to launch up to five apps of your choice. Configurations can be accessed through Settings->Apps.",
   "type": "bootloader",
   "tags": "tools, system",
diff --git a/apps/quicklaunch/settings.js b/apps/quicklaunch/settings.js
index ac4cc5805..7aac60a94 100644
--- a/apps/quicklaunch/settings.js
+++ b/apps/quicklaunch/settings.js
@@ -1,6 +1,10 @@
 (function(back) {
 var settings = Object.assign(require("Storage").readJSON("quicklaunch.json", true) || {});
 
+for (let c of ["leftapp","rightapp","upapp","downapp","tapapp"]){
+  if (!settings[c]) settings[c] = {"name":"(none)"};
+}
+
 var apps = require("Storage").list(/\.info$/).map(app=>{var a=require("Storage").readJSON(app,1);return a&&{name:a.name,type:a.type,sortorder:a.sortorder,src:a.src};}).filter(app=>app && (app.type=="app" || app.type=="launch" || app.type=="clock" || !app.type));
 
 apps.sort((a,b)=>{
@@ -29,11 +33,11 @@ function showMainMenu() {
   mainmenu["Up: "+settings.upapp.name] = function() { E.showMenu(upmenu); };
   mainmenu["Down: "+settings.downapp.name] = function() { E.showMenu(downmenu); };
   mainmenu["Tap: "+settings.tapapp.name] = function() { E.showMenu(tapmenu); };
-  
+
   return E.showMenu(mainmenu);
 }
-  
-//Left swipe menu  
+
+//Left swipe menu
 var leftmenu = {
   "" : { "title" : "Left Swipe" },
   "< Back" : showMainMenu
@@ -119,4 +123,4 @@ apps.forEach((a)=>{
 });
 
 showMainMenu();
-});
\ No newline at end of file
+})
diff --git a/apps/random/ChangeLog b/apps/random/ChangeLog
new file mode 100644
index 000000000..c819919ed
--- /dev/null
+++ b/apps/random/ChangeLog
@@ -0,0 +1,2 @@
+0.01: New app!
+0.02: Submitted to the app loader
\ No newline at end of file
diff --git a/apps/random/app.js b/apps/random/app.js
new file mode 100644
index 000000000..c3001a6d1
--- /dev/null
+++ b/apps/random/app.js
@@ -0,0 +1,205 @@
+let n = 1;
+let diceSides = 6;
+let replacement = false;
+let min = 1;
+let max = 10;
+
+Bangle.loadWidgets();
+Bangle.drawWidgets();
+
+function showCoinMenu() {
+  E.showMenu({
+    '': {
+      'title': 'Coin flip',
+      'back': showMainMenu
+    },
+    '# of coins': {
+      value: n,
+      step: 1,
+      min: 1,
+      onchange: value => n = value
+    },
+    'Go': () => {
+      let resultMenu = {
+        '': {
+          'title': 'Result',
+          'back': showCoinMenu
+        }
+      };
+      let heads = 0;
+      for (let i = 0; i < n; i++) {
+        let coin = Math.random() < 0.5;
+        if (coin) heads++;
+        resultMenu[`${i + 1}: ${coin ? 'Heads' : 'Tails'}`] = () => { };
+      }
+      let tails = n - heads;
+      resultMenu[`${heads} heads, ${Math.round(100 * heads / n)}%`] = () => { };
+      resultMenu[`${tails} tails, ${Math.round(100 * tails / n)}%`] = () => { };
+
+      E.showMenu(resultMenu);
+    }
+  });
+}
+
+
+function showDiceMenu() {
+  E.showMenu({
+    '': {
+      'title': 'Dice roll',
+      'back': showMainMenu
+    },
+    '# of dice': {
+      value: n,
+      step: 1,
+      min: 1,
+      onchange: value => n = value
+    },
+    '# of sides': {
+      value: diceSides,
+      step: 1,
+      min: 2,
+      onchange: value => diceSides = value
+    },
+    'Go': () => {
+      let resultMenu = {
+        '': {
+          'title': 'Result',
+          'back': showDiceMenu
+        }
+      };
+      let sum = 0;
+      let min = diceSides + 1;
+      let max = 0;
+      for (let i = 0; i < n; i++) {
+        let roll = Math.floor(Math.random() * diceSides + 1);
+        sum += roll;
+        if (roll < min) min = roll;
+        if (roll > max) max = roll;
+        resultMenu[`${i + 1}: ${roll}`] = () => { };
+      }
+      resultMenu[`Sum: ${sum}`] = () => { };
+      resultMenu[`Min: ${min}`] = () => { };
+      resultMenu[`Max: ${max}`] = () => { };
+      resultMenu[`Average: ${sum / n}`] = () => { };
+
+      E.showMenu(resultMenu);
+    }
+  });
+}
+
+
+function showCardMenu() {
+  E.showMenu({
+    '': {
+      'title': 'Card draw',
+      'back': showMainMenu
+    },
+    '# of cards': {
+      value: Math.min(52, n),
+      step: 1,
+      min: 1,
+      max: 52,
+      onchange: value => n = value
+    },
+    'Replacement': {
+      value: replacement,
+      onchange: value => {
+        replacement = value;
+        if (replacement && n > 52) n = 52;
+      }
+    },
+    'Go': () => {
+      n = Math.min(n, 52);
+      SUITS = ['Spades', 'Diamonds', 'Clubs', 'Hearts'];
+      RANKS = ['Ace', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'Jack', 'Queen', 'King'];
+      class Card {
+        constructor(suit, rank) {
+          this.suit = suit;
+          this.rank = rank;
+        }
+
+        //Can't use == to check equality, so using Java-inspired .equals()
+        equals(other) {
+          return this.suit == other.suit && this.rank == other.rank;
+        }
+      }
+
+      let resultMenu = {
+        '': {
+          'title': 'Result',
+          'back': showCardMenu
+        }
+      };
+      let cards = [];
+      for (let i = 0; i < n; i++) {
+        let newCard;
+        while (true) {
+          newCard = new Card(
+            SUITS[Math.floor(Math.random() * SUITS.length)],
+            RANKS[Math.floor(Math.random() * RANKS.length)]);
+
+          if (replacement) break; //If we are doing replacement, skip the check for duplicates and stop looping
+
+          if (!cards.map(card => card.equals(newCard)).includes(true)) break; //If there are no duplicates found, stop looping
+        }
+
+        cards.push(newCard);
+        resultMenu[`${newCard.rank} of ${newCard.suit}`] = () => { };
+      }
+
+      E.showMenu(resultMenu);
+    }
+  });
+}
+
+function showNumberMenu() {
+  E.showMenu({
+    '': {
+      'title': 'Number choice',
+      'back': showMainMenu
+    },
+    'Minimum': {
+      value: min,
+      step: 1,
+      onchange: value => min = value
+    },
+    'Maximum': {
+      value: max,
+      step: 1,
+      onchange: value => max = value
+    },
+    '# of choices': {
+      value: n,
+      min: 1,
+      step: 1,
+      onchange: value => n = value
+    },
+    'Go': () => {
+      let resultMenu = {
+        '': {
+          'title': 'Result',
+          'back': showNumberMenu
+        }
+      };
+      for (let i = 0; i < n; i++) {
+        let value = Math.floor(min + Math.random() * (max - min + 1));
+        resultMenu[`${i + 1}: ${value}`] = () => { };
+      }
+      E.showMenu(resultMenu);
+    }
+  });
+}
+
+function showMainMenu() {
+  E.showMenu({
+    '': {
+      'title': 'Random'
+    },
+    'Coin': showCoinMenu,
+    'Dice': showDiceMenu,
+    'Card': showCardMenu,
+    'Number': showNumberMenu
+  });
+}
+
+showMainMenu();
\ No newline at end of file
diff --git a/apps/random/icon.js b/apps/random/icon.js
new file mode 100644
index 000000000..de84a0893
--- /dev/null
+++ b/apps/random/icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwIEBgf///AAoMHAoPgAoMPAoPwAoMfAoP4AoM/AoP8AoN/AoP+AoIEBAAMAgIbBD4OAAoPgFYIFC4A3BAoQCFEAQFBEwV/AoIyCn+ALYYFFCIIFDDoIECFIQFCGoQFCIIQFCJoQFCNoIuEHwQuCHwQuCQYQuCR4QuCTYQuGAoIcDg4oEg4oEg6mCAoQuDAoIuDAFQvFAsIA=="))
\ No newline at end of file
diff --git a/apps/random/icon.png b/apps/random/icon.png
new file mode 100644
index 000000000..e4af7d7a1
Binary files /dev/null and b/apps/random/icon.png differ
diff --git a/apps/random/metadata.json b/apps/random/metadata.json
new file mode 100644
index 000000000..1af4bc8ac
--- /dev/null
+++ b/apps/random/metadata.json
@@ -0,0 +1,24 @@
+{
+  "id": "random",
+  "name": "Random",
+  "version": "0.02",
+  "description": "Flip coins, roll dice, draw a card, or choose random numbers",
+  "icon": "icon.png",
+  "tags": "tool",
+  "supports": [
+    "BANGLEJS",
+    "BANGLEJS2"
+  ],
+  "allow_emulator": true,
+  "storage": [
+    {
+      "name": "random.app.js",
+      "url": "app.js"
+    },
+    {
+      "name": "random.img",
+      "url": "icon.js",
+      "evaluate": true
+    }
+  ]
+}
\ No newline at end of file
diff --git a/apps/rebble/ChangeLog b/apps/rebble/ChangeLog
index c392cc74b..21dd44e77 100644
--- a/apps/rebble/ChangeLog
+++ b/apps/rebble/ChangeLog
@@ -8,4 +8,5 @@
 0.08: removed unused font, fix autocycle,  imported suncalc and trimmed, removed pedometer dependency, "tap to cycle" setting
 0.09: fix battery icon size
 0.10: Tell clock widgets to hide.
-0.11: fix issue https://github.com/espruino/BangleApps/issues/2128 (#2128) ( settings undefined )
\ No newline at end of file
+0.11: fix issue https://github.com/espruino/BangleApps/issues/2128 (#2128) ( settings undefined )
+0.12: implemented widget_utils 
\ No newline at end of file
diff --git a/apps/rebble/metadata.json b/apps/rebble/metadata.json
index 9134ccd23..dfc0703c0 100644
--- a/apps/rebble/metadata.json
+++ b/apps/rebble/metadata.json
@@ -2,7 +2,7 @@
   "id": "rebble",
   "name": "Rebble Clock",
   "shortName": "Rebble",
-  "version": "0.11",
+  "version": "0.12",
   "description": "A Pebble style clock, with configurable background, three sidebars including steps, day, date, sunrise, sunset, long live the rebellion",
   "readme": "README.md",
   "icon": "rebble.png",
diff --git a/apps/rebble/rebble.app.js b/apps/rebble/rebble.app.js
index 2ddd3a9b9..82e4a62d7 100644
--- a/apps/rebble/rebble.app.js
+++ b/apps/rebble/rebble.app.js
@@ -309,16 +309,8 @@ else{
 }
 
 
-g.clear();
 Bangle.loadWidgets();
-/*
- * we are not drawing the widgets as we are taking over the whole screen
- * so we will blank out the draw() functions of each widget and change the
- * area to the top bar doesn't get cleared.
- */
-for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
-
-
+require("widget_utils").hide();
 
 
 
diff --git a/apps/rinkulainen/ChangeLog b/apps/rinkulainen/ChangeLog
new file mode 100644
index 000000000..5560f00bc
--- /dev/null
+++ b/apps/rinkulainen/ChangeLog
@@ -0,0 +1 @@
+0.01: New App!
diff --git a/apps/rinkulainen/README.md b/apps/rinkulainen/README.md
new file mode 100644
index 000000000..4b32ada0a
--- /dev/null
+++ b/apps/rinkulainen/README.md
@@ -0,0 +1,14 @@
+# Rinkulainen
+
+By Jukio Kallio
+
+A Minimal & stylish watch face, with rings or disks for hours and minutes. Date underneath. With easy to mod source code for making your own themes. Some example themes included.
+
+![](screenshot2.png) 
+Default Colorful theme
+
+![](screenshot1.png) 
+Grayscale theme
+
+![](screenshot3.png) 
+Maze theme
diff --git a/apps/rinkulainen/app-icon.js b/apps/rinkulainen/app-icon.js
new file mode 100644
index 000000000..0618f7891
--- /dev/null
+++ b/apps/rinkulainen/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwkEogA0/4AKCpNPCxYAB+gtTGJQuOGBAWPGAwuQGAwXG+chiMRiU/C6HyiDpDgMvC5ItFCoYADGIoXIFoIqDGgUBC5nxB4IoE+YYBj4XLBwJxGJ4IwEC4wuBiYEBmUhiUjAoMxGAgXGmAuCDYIACCYIwBgYXJBYSQGD4IjBC5HyE4QOBgU/+cgEAQ3BTAQXFBQImBN4p/BHARgCC4swCYIaBT4gGDiBgCC4syQ4JVENIsggTvKBgYHG+BRCC5KdDWIYXOiEPC4oUCC8hHUmTJBO44XMCgSnH+SnLa5IABfILXJCgINBgA9CAAnzEIYXF+QKCJAMCn/zkQXCEgJtBR479CEwIADCQRpEC4wLBJAInBAAQ3BD4KxDC4wTBiatCkMSkYFBmKAEa48QGAR1GP4gXHGAMBDAnzEAKvEC44wCgJzC+QGCBwgXIRwoACJ4oXDp4JEFQQACGgYAC+gXJGIMhiMRiR9GC5YALC4hgFABgWEGCIuFGCIWGGB4uHGJwVJAFY"))
diff --git a/apps/rinkulainen/app.js b/apps/rinkulainen/app.js
new file mode 100644
index 000000000..b487c9a0d
--- /dev/null
+++ b/apps/rinkulainen/app.js
@@ -0,0 +1,146 @@
+// Rinkulainen
+//
+// Bangle.js 2 watch face
+// by Jukio Kallio
+// www.jukiokallio.com
+
+// settings
+const watch = { 
+  theme: "default", 
+  x:0, y:0, w:0, h:0, 
+  color:"#000000", // change background color
+  finland:true, // change if you want Finnish style date, or US style
+  
+    // default theme "colorful"
+    hour: { size:60, weight:8, color:"#00FFFF", cursor:10 },
+    minute: { size:40, weight:16, color:"#FFFF00", cursor:6 },
+    second: { on: false, cursor:2 }, // if on, uses a lot more battery
+    date: { font:"6x8", size:1, y:15, color:"#FFFF00" }
+};
+
+// more themes
+if (watch.theme == "grayscale") {
+  watch.hour = { size:60, weight:20, color:"#999999", cursor:8 };
+  watch.minute = { size:40, weight:20, color:"#dddddd", cursor:8 };
+  watch.second = { on: false, cursor:2 }; // if on, uses a lot more battery
+  watch.date = { font:"6x8", size:1, y:15, color:"#ffffff" };
+} else if (watch.theme == "maze") {
+  watch.hour = { size:50, weight:7, color:"#ffffff", cursor:6 };
+  watch.minute = { size:30, weight:7, color:"#ffffff", cursor:6 };
+  watch.second = { on: false, cursor:2 }; // if on, uses a lot more battery
+  watch.date = { font:"6x8", size:1, y:15, color:"#ffffff" };
+} else if (watch.theme == "disks") {
+  watch.hour = { size:72, weight:30, color:"#00ff66", cursor:4 };
+  watch.minute = { size:36, weight:32, color:"#0066ff", cursor:4 };
+  watch.second = { on: false, cursor:2 }; // if on, uses a lot more battery
+  watch.date = { font:"6x8", size:1, y:10, color:"#ffffff" };
+}
+
+// set some additional settings
+watch.w = g.getWidth(); // size of the background
+watch.h = g.getHeight();
+watch.x = watch.w * 0.5; // position of the circles
+watch.y = watch.h * 0.46;
+watch.date.y = watch.date.y + watch.y + watch.hour.size; // final position of the date
+
+const dateWeekday = { 0: "Sunday", 1: "Monday", 2: "Tuesday", 3: "Wednesday", 4:"Thursday", 5:"Friday", 6:"Saturday" }; // weekdays
+
+var wait = 60000; // wait time, normally a minute
+if (watch.second.on) wait = 1000; // a second if seconds are used
+
+
+// timeout used to update every minute
+var drawTimeout;
+
+// schedule a draw for the next minute
+function queueDraw() {
+  if (drawTimeout) clearTimeout(drawTimeout);
+  drawTimeout = setTimeout(function() {
+    drawTimeout = undefined;
+    draw();
+  }, wait - (Date.now() % wait));
+}
+
+
+// main function
+function draw() {
+  // make date object
+  var date = new Date();
+  
+  // work out the date string
+  var dateDay = date.getDate();
+  var dateMonth = date.getMonth() + 1;
+  var dateYear = date.getFullYear();
+  var dateStr = dateWeekday[date.getDay()] + " " + dateMonth + "." + dateDay + "." + dateYear;
+  if (watch.finland) dateStr = dateWeekday[date.getDay()] + " " + dateDay + "." + dateMonth + "." + dateYear; // the true way of showing date
+
+  // Reset the state of the graphics library
+  g.reset();
+  
+  // Clear the area where we want to draw the time
+  g.setColor(watch.color);
+  g.fillRect(0, 0, watch.w, watch.h);
+  
+  // variables for vertex transformation
+  var tver, tobj, tran;
+  
+  // draw hour circle
+  g.setColor(watch.hour.color).fillCircle(watch.x, watch.y, watch.hour.size);
+  g.setColor(watch.color).fillCircle(watch.x, watch.y, watch.hour.size - watch.hour.weight);
+  // draw hour line
+  g.setColor(watch.color);
+  var thour = (date.getHours() / 12) * (Math.PI * 2);
+  tver = [-watch.hour.cursor, 0, watch.hour.cursor, 0, watch.hour.cursor, -watch.hour.size*1.05, -watch.hour.cursor, -watch.hour.size*1.05];
+  tobj = { x:watch.x, y:watch.y, scale:1, rotate:thour };
+  tran = g.transformVertices(tver, tobj);
+  g.fillPoly(tran);
+  
+  // draw minute circle
+  g.setColor(watch.minute.color).fillCircle(watch.x, watch.y, watch.minute.size);
+  g.setColor(watch.color).fillCircle(watch.x, watch.y, watch.minute.size - watch.minute.weight);
+  // draw minute line
+  g.setColor(watch.color);
+  var tmin = (date.getMinutes() / 60) * (Math.PI * 2);
+  tver = [-watch.minute.cursor, 0, watch.minute.cursor, 0, watch.minute.cursor, -watch.minute.size*1.05, -watch.minute.cursor, -watch.minute.size*1.05];
+  tobj = { x:watch.x, y:watch.y, scale:1, rotate:tmin };
+  tran = g.transformVertices(tver, tobj);
+  g.fillPoly(tran);
+  
+  // draw seconds line, if the feature is on
+  if (watch.second.on) {
+    g.setColor(watch.color);
+    var tsec = (date.getSeconds() / 60) * (Math.PI * 2);
+    tver = [-watch.second.cursor, 0, watch.second.cursor, 0, watch.second.cursor, -watch.second.size*1.045, -watch.second.cursor, -watch.second.size*1.045];
+    tobj = { x:watch.x, y:watch.y, scale:1, rotate:tsec };
+    tran = g.transformVertices(tver, tobj);
+    g.fillPoly(tran);
+  }
+  
+  // draw date
+  g.setFontAlign(0,0).setFont(watch.date.font, 1).setColor(watch.date.color);
+  g.drawString(dateStr, watch.x, watch.date.y + watch.date.size + 2);
+  
+  // queue draw
+  queueDraw();
+}
+
+
+// Clear the screen once, at startup
+g.clear();
+// draw immediately at first
+draw();
+
+
+// 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;
+  }
+});
+
+
+// Show launcher when middle button pressed
+Bangle.setUI("clock");
diff --git a/apps/rinkulainen/app.png b/apps/rinkulainen/app.png
new file mode 100644
index 000000000..50782c48d
Binary files /dev/null and b/apps/rinkulainen/app.png differ
diff --git a/apps/rinkulainen/metadata.json b/apps/rinkulainen/metadata.json
new file mode 100644
index 000000000..f0a51af87
--- /dev/null
+++ b/apps/rinkulainen/metadata.json
@@ -0,0 +1,16 @@
+{ "id": "rinkulainen",
+  "name": "Rinkulainen - Minimal & Stylish watch face",
+  "shortName":"Rinkulainen",
+  "version":"0.01",
+  "description": "A minimal watch face, with rings/disks for hours and minutes. Date underneath. With easy to mod source code for making your own themes. Some example themes included.",
+  "icon": "app.png",
+  "screenshots": [{"url":"screenshot2.png"}, {"url":"screenshot1.png"}, {"url":"screenshot3.png"}],
+  "type": "clock",
+  "tags": "clock",
+  "supports" : ["BANGLEJS","BANGLEJS2"],  
+  "readme": "README.md",
+  "storage": [
+    {"name":"rinkulainen.app.js","url":"app.js"},
+    {"name":"rinkulainen.img","url":"app-icon.js","evaluate":true}
+  ]
+}
diff --git a/apps/rinkulainen/screenshot1.png b/apps/rinkulainen/screenshot1.png
new file mode 100644
index 000000000..f897c5a4e
Binary files /dev/null and b/apps/rinkulainen/screenshot1.png differ
diff --git a/apps/rinkulainen/screenshot2.png b/apps/rinkulainen/screenshot2.png
new file mode 100644
index 000000000..354618167
Binary files /dev/null and b/apps/rinkulainen/screenshot2.png differ
diff --git a/apps/rinkulainen/screenshot3.png b/apps/rinkulainen/screenshot3.png
new file mode 100644
index 000000000..ef1385288
Binary files /dev/null and b/apps/rinkulainen/screenshot3.png differ
diff --git a/apps/route/ChangeLog b/apps/route/ChangeLog
index f8c97a57b..2c66bf30a 100644
--- a/apps/route/ChangeLog
+++ b/apps/route/ChangeLog
@@ -1,3 +1,4 @@
 0.01: New App!
 0.02: Change color from red->yellow to ease readability (fix #710)
 0.03: Color/positioning change to allow it to work with Bangle.js 1 (although not pretty)
+0.04: Added adjustment for Bangle.js magnetometer heading fix
diff --git a/apps/route/custom.html b/apps/route/custom.html
index fd3148e2b..9e721a95b 100644
--- a/apps/route/custom.html
+++ b/apps/route/custom.html
@@ -231,7 +231,7 @@ document.getElementById("upload").addEventListener("click", function() {
     if (!Bangle.isLCDOn()) return;
 
     arrow(oldHeading,g.theme.bg);
-    var heading = m.heading + nextAngle;
+    var heading = (360-m.heading) + nextAngle;
     arrow(heading,"#f00");
     oldHeading = heading;
   }
diff --git a/apps/route/metadata.json b/apps/route/metadata.json
index dc980dada..0738f682f 100644
--- a/apps/route/metadata.json
+++ b/apps/route/metadata.json
@@ -1,7 +1,7 @@
 {
   "id": "route",
   "name": "Route Viewer",
-  "version": "0.03",
+  "version": "0.04",
   "description": "Upload a KML file of a route, and have your watch display a map with how far around it you are",
   "icon": "app.png",
   "tags": "",
diff --git a/apps/rpnsci/ChangeLog b/apps/rpnsci/ChangeLog
new file mode 100644
index 000000000..35ba8b130
--- /dev/null
+++ b/apps/rpnsci/ChangeLog
@@ -0,0 +1,3 @@
+0.01: New app!
+0.02: Bug fixes
+0.03: Submitted to the app loader
\ No newline at end of file
diff --git a/apps/rpnsci/README.md b/apps/rpnsci/README.md
new file mode 100644
index 000000000..437985c7a
--- /dev/null
+++ b/apps/rpnsci/README.md
@@ -0,0 +1,38 @@
+# Scientific calculator
+
+This is a reverse polish notation scientific calculator with memory function.
+
+## General operation
+
+In order to fit all of the functions, the calculator only displays 12 keys at a time. The top right of these keys is always a mode button. This will take you into number mode if you are in operation mode, and operation mode if you are in any other mode. The calculator starts out in number mode, where you can enter numbers. Operation mode lets you perform basic operations and RPN stack manipulation. In operation mode, you can press "Sci" to switch to Scientific mode. This provides trigonometric, logarithmic, and memory operations, as well as the constants e and pi.
+
+In any mode, the calculator also accepts swipes. A swipe up or down will function as the enter key (see below), and a swipe left will delete the last character if entry has not been terminated (see below) or function as the clear key if it has.
+
+The calculator will vibrate when performing an operation. If the operation is invalid, it will do a long vibration.
+
+## Reverse polish notation
+
+To save keystrokes and avoid the need for parentheses keys while still allowing you to control the order of operations, this calculator uses Reverse Polish Notation (RPN). There is a stack of 4 registers: x (the displayed value), y, z, and t (top). Pressing Enter will lift the stack. The value of z will be copied to t, y to z, and x to y. (The old value of t is lost.) This also terminates input, making the next numerical key press clear the value in x before typing its value. This enables you to enter a value into the stack multiple times by pressing Enter multiple times.
+
+Performing an operation will also terminate entry, and can either simply replace the value of x (if it is a one-number operation), or drop the stack (if it is a two number operation). Dropping the stack causes the existing values of x and y to be lost, replacing x with the result of the operation, y with the old value of z, and z with the old value of t. t remains the same.
+
+Effectively, to do an operation, you type the first operand, press Enter, and then type the second operand. If you want to do multiple operations, start with the one that you want to do first, and then continue operating on the result without pressing enter. For example, 3 Enter 2 Times 1 Plus computes (3\*2) + 1. 3 Enter 1 Plus 2 Times computes (3+1) \* 2. If you wish to compute something independently, simply press enter before starting the independent operation. For example, to compute (3 \* 2) + (4 \* 5), first compute 3 \* 2. Then press enter and compute 4 \* 5. You will have 6 in the y register and 20 in the x register. Press Plus to add them.
+
+You can also rotate the stack down with the Rot key. x gets set to the value of y, y gets set to the value of z, z gets set to the value of t, and t gets set to the old value of x. And you can swap x and y with Swp. I find this to be most handy when I want to subtract the result of an operation from another value, but I forget to enter another value first. For example, 20 - (2 \* 3) should usually be computed as 20 Enter 2 Enter 3 Times Minus. But if you compute 2 \* 3 first, you can enter 20, swap the values, and then subtract. (I do this more often than I would like to admit.)
+
+## Memory
+The calculator has 10 variables that you can store values in. In Scientific mode, press Sto to store the value of the x register in one of the values (which you then choose by pressing a number), or Rcl to read a value (which you choose by pressing a number) into the x register. These values are preserved when the calculator is closed.
+
+## Clearing
+
+A swipe left will delete one character, unless the number is already zero in which case it will emulate a press of the clear button (Clr). The clear button will set the value of x to 0 if it is not zero. If x=0, y, z, and t will be cleared to zero. And if they are already zero, pressing Clear again will clear the memory.
+
+## Limitations
+
+* This calculator uses Javascript's floating point numbers. These are fast, space efficient, and less complicated to code for (producing a smaller app), but they sacrifice some precision. You might see stuff like 0.1 + 0.2 adding to 0.30000000000000004, or the sine of pi being a very low value but not quite zero.
+
+* This calculator performs trigonometric operations in radians. If you wish to convert degrees to radians, multiply by (pi/180). If you wish to convert radians to degrees, multiply by (180 / pi).
+
+* This calculator performs logarithms in base 10. If you would like to perform logarithms in another base, divide the log of the number by the log of the base. For example, to compute log base 2 of 8, divide log(8) by log(2). (To get the natural log or ln, divide by log(e)).
+
+* This calculator considers 0^0 to be 1, a behavior inherited from Javascripts Math.pow() function. In reality, it is undefined because two mathematical rules give conflicting answers: anything^0 = 1, but 0^anything = 0.
\ No newline at end of file
diff --git a/apps/rpnsci/app.js b/apps/rpnsci/app.js
new file mode 100644
index 000000000..5c98770c4
--- /dev/null
+++ b/apps/rpnsci/app.js
@@ -0,0 +1,403 @@
+const MEMORY_FILE = "rpnsci.mem.json";
+const storage = require("Storage");
+
+class NumberButton {
+    constructor(number) {
+        this.label = '' + number;
+    }
+
+    onclick() {
+        if (entryTerminated) {
+            if (liftOnNumberPress) liftStack();
+            x = this.label;
+            entryTerminated = false;
+            liftOnNumberPress = false;
+        } else {
+            if (x == '0') x = this.label;
+            else x += this.label;
+        }
+        feedback(true);
+        updateDisplay();
+    }
+}
+
+let DecimalPointButton = {
+    label: '.',
+    onclick: () => {
+        if (entryTerminated) {
+            if (liftOnNumberPress) liftStack();
+            x = '0.';
+            entryTerminated = false;
+            liftOnNumberPress = false;
+            feedback(true);
+            updateDisplay();
+        } else if (!x.includes('.')) {
+            x += '.';
+            feedback(true);
+            updateDisplay();
+        } else {
+            feedback(false);
+        }
+    }
+};
+
+class ModeButton {
+    constructor(currentMode) {
+        if (currentMode == 'memstore' || currentMode == 'memrec') {
+            this.label = 'Exit';
+        } else if (currentMode == 'operation') {
+            this.label = 'Num';
+        } else {
+            this.label = 'Op';
+        }
+    }
+
+    onclick() {
+        if (mode == 'memstore' || mode == 'memrec') {
+            mode = 'operation';
+        } else if (mode == 'operation') {
+            mode = 'number';
+        } else {
+            mode = 'operation';
+        }
+        feedback(true);
+        drawButtons();
+    }
+}
+
+class OperationButton {
+    constructor(label) {
+        this.label = label;
+    }
+
+    onclick() {
+        if (this.label == '/' && parseFloat(x) == 0) {
+            feedback(false);
+            return;
+        }
+        let result = this.getResult();
+        x = '' + result;
+        y = z;
+        z = t;
+        entryTerminated = true;
+        liftOnNumberPress = true;
+        feedback(true);
+        updateDisplay();
+    }
+
+    getResult() {
+        let numX = parseFloat(x);
+        return {
+            '+': y + numX,
+            '-': y - numX,
+            '/': y / numX,
+            '*': y * numX,
+            '^': Math.pow(y, numX)
+        }[this.label];
+    }
+}
+
+class OneNumOpButton {
+    constructor(label) {
+        this.label = label;
+    }
+
+    onclick() {
+        result = {
+            '+-': '' + -parseFloat(x),
+            'Sin': '' + Math.sin(parseFloat(x)),
+            'Cos': '' + Math.cos(parseFloat(x)),
+            'Tan': '' + Math.tan(parseFloat(x)),
+            'Asin': '' + Math.asin(parseFloat(x)),
+            'Acos': '' + Math.acos(parseFloat(x)),
+            'Atan': '' + Math.atan(parseFloat(x)),
+            'Log': '' + (Math.log(parseFloat(x)) / Math.log(10))
+        }[this.label];
+        if (isNaN(result) || result == 'NaN') feedback(false);
+        else {
+            x = result;
+            entryTerminated = true;
+            liftOnNumberPress = true;
+            feedback(true);
+            updateDisplay();
+        }
+    }
+}
+
+let ClearButton = {
+    label: 'Clr',
+    onclick: () => {
+        if (x != '0') {
+            x = '0';
+            updateDisplay();
+        } else if (y != 0 || z != 0 || t != 0) {
+            y = 0;
+            z = 0;
+            t = 0;
+            E.showMessage('Registers cleared!');
+            setTimeout(() => {
+                drawButtons();
+                updateDisplay();
+            }, 250);
+        } else {
+            memory = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
+            storage.writeJSON(MEMORY_FILE, memory);
+            E.showMessage('Memory cleared!');
+            setTimeout(() => {
+                drawButtons();
+                updateDisplay();
+            }, 250);
+        }
+        entryTerminated = false;
+        liftOnNumberPress = false;
+        feedback(true);
+    }
+};
+
+let SwapButton = {
+    label: 'Swp',
+    onclick: () => {
+        oldX = x;
+        x = '' + y;
+        y = parseFloat(oldX);
+        entryTerminated = true;
+        liftOnNumberPress = true;
+        feedback(true);
+        updateDisplay();
+    }
+};
+
+let RotateButton = {
+    label: 'Rot',
+    onclick: () => {
+        oldX = x;
+        x = '' + y;
+        y = z;
+        z = t;
+        t = parseFloat(oldX);
+        entryTerminated = true;
+        liftOnNumberPress = true;
+        feedback(true);
+        updateDisplay();
+    }
+};
+
+let EnterButton = {
+    label: 'Ent',
+    onclick: () => {
+        liftStack();
+        entryTerminated = true;
+        liftOnNumberPress = false;
+        feedback(true);
+        updateDisplay();
+    }
+};
+
+let ScientificButton = {
+    label: 'Sci',
+    onclick: () => {
+        mode = 'scientific';
+        feedback(true);
+        drawButtons();
+    }
+};
+
+class ConstantButton {
+    constructor(label, value) {
+        this.label = label;
+        this.value = value;
+    }
+
+    onclick() {
+        if (entryTerminated && liftOnNumberPress) liftStack();
+        x = '' + this.value;
+        entryTerminated = true;
+        liftOnNumberPress = true;
+        feedback(true);
+        updateDisplay();
+    }
+}
+
+let MemStoreButton = {
+    label: 'Sto',
+    onclick: () => {
+        mode = 'memstore';
+        feedback(true);
+        drawButtons();
+    }
+};
+
+let MemRecallButton = {
+    label: 'Rec',
+    onclick: () => {
+        mode = 'memrec';
+        feedback(true);
+        drawButtons();
+    }
+};
+
+class MemStoreIn {
+    constructor(register) {
+        this.register = register;
+        this.label = '' + register;
+    }
+
+    onclick() {
+        memory[this.register] = parseFloat(x);
+        storage.writeJSON(MEMORY_FILE, memory);
+        mode = 'scientific';
+        entryTerminated = true;
+        liftOnNumberPress = true;
+        feedback(true);
+        drawButtons();
+    }
+}
+
+class MemRecFrom {
+    constructor(register) {
+        this.register = register;
+        this.label = '' + register;
+    }
+
+    onclick() {
+        x = '' + memory[this.register];
+        mode = 'scientific';
+        entryTerminated = true;
+        liftOnNumberPress = true;
+        feedback(true);
+        updateDisplay();
+        drawButtons();
+    }
+}
+
+const BUTTONS = {
+    'number': [
+        [new NumberButton(7), new NumberButton(8), new NumberButton(9), new ModeButton('number')],
+        [new NumberButton(4), new NumberButton(5), new NumberButton(6), new NumberButton(0)],
+        [new NumberButton(1), new NumberButton(2), new NumberButton(3), DecimalPointButton]
+    ],
+    'operation': [
+        [new OperationButton('+'), new OperationButton('-'), ClearButton, new ModeButton('operation')],
+        [new OperationButton('*'), new OperationButton('/'), SwapButton, EnterButton],
+        [new OperationButton('^'), new OneNumOpButton('+-'), RotateButton, ScientificButton]
+    ],
+    'scientific': [
+        [new OneNumOpButton('Sin'), new OneNumOpButton('Cos'), new OneNumOpButton('Tan'), new ModeButton('scientific')],
+        [new OneNumOpButton('Asin'), new OneNumOpButton('Acos'), new OneNumOpButton('Atan'), MemStoreButton],
+        [new OneNumOpButton('Log'), new ConstantButton('e', Math.E), new ConstantButton('pi', Math.PI), MemRecallButton]
+    ],
+    'memstore': [
+        [new MemStoreIn(7), new MemStoreIn(8), new MemStoreIn(9), new ModeButton('memstore')],
+        [new MemStoreIn(4), new MemStoreIn(5), new MemStoreIn(6), new MemStoreIn(0)],
+        [new MemStoreIn(1), new MemStoreIn(2), new MemStoreIn(3), new ModeButton('memstore')]
+    ],
+    'memrec': [
+        [new MemRecFrom(7), new MemRecFrom(8), new MemRecFrom(9), new ModeButton('memrec')],
+        [new MemRecFrom(4), new MemRecFrom(5), new MemRecFrom(6), new MemRecFrom(0)],
+        [new MemRecFrom(1), new MemRecFrom(2), new MemRecFrom(3), new ModeButton('memrec')]
+    ],
+};
+
+let x = '0';
+let y = 0;
+let z = 0;
+let t = 0;
+let memJSON = storage.readJSON(MEMORY_FILE);
+if (memJSON) {
+    let memory = memJSON;
+} else {
+    let memory = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
+}
+let mode = 'number';
+let entryTerminated = false;
+let liftOnNumberPress = false;
+
+function liftStack() {
+    t = z;
+    z = y;
+    y = parseFloat(x);
+}
+
+function feedback(acceptable) {
+    if (acceptable) Bangle.buzz(50, 0.5);
+    else Bangle.buzz(200, 1);
+}
+
+function drawButtons() {
+    g.reset().clearRect(0, 44, 175, 175).setFont("Vector", 15).setFontAlign(0, 0);
+    //Draw lines
+    for (let x = 44; x <= 176; x += 44) {
+        g.drawLine(x, 44, x, 175);
+    }
+    for (let y = 44; y <= 176; y += 44) {
+        g.drawLine(0, y, 175, y);
+    }
+    for (let row = 0; row < 3; row++) {
+        for (let col = 0; col < 4; col++) {
+            g.drawString(BUTTONS[mode][row][col].label, 22 + 44 * col, 66 + 44 * row);
+        }
+    }
+}
+
+function getFontSize(length) {
+    let size = Math.floor(176 / length);  //Characters of width needed per pixel
+    size *= (20 / 12);  //Convert to height
+    // Clamp to between 6 and 20
+    if (size < 6) return 6;
+    else if (size > 20) return 20;
+    else return Math.floor(size);
+}
+
+function updateDisplay() {
+    g.clearRect(0, 24, 175, 43).setColor(storage.readJSON('setting.json').theme.fg2).setFontAlign(1, -1).setFont("Vector", getFontSize(x.length)).drawString(x, 176, 24);
+}
+
+Bangle.on("touch", (button, xy) => {
+    let row = Math.floor((xy.y - 44) / 44);
+    let col = Math.floor(xy.x / 44);
+    if (row < 0) {  // Tap number to show registers
+        g.clearRect(0, 24, 175, 43).setColor(storage.readJSON('setting.json').theme.fg2).setFontAlign(1, -1)
+            .setFont("Vector", getFontSize(x.length)).drawString('' + t, 176, 24);
+
+        g.clearRect(0, 44, 175, 63).setColor(storage.readJSON('setting.json').theme.fg2).setFontAlign(1, -1)
+            .setFont("Vector", getFontSize(x.length)).drawString('' + z, 176, 44);
+
+        g.clearRect(0, 64, 175, 83).setColor(storage.readJSON('setting.json').theme.fg2).setFontAlign(1, -1)
+            .setFont("Vector", getFontSize(x.length)).drawString('' + y, 176, 64);
+
+        g.clearRect(0, 84, 175, 103).setColor(storage.readJSON('setting.json').theme.fg2).setFontAlign(1, -1)
+            .setFont("Vector", getFontSize(x.length)).drawString(x, 176, 84);
+
+        setTimeout(() => {
+            drawButtons();
+            updateDisplay();
+        }, 500);
+    } else {
+        if (row > 2) row = 2;
+        if (col < 0) col = 0;
+        if (col > 3) col = 3;
+
+        BUTTONS[mode][row][col].onclick();
+    }
+});
+
+Bangle.on("swipe", dir => {
+    if (dir == -1) {
+        if (entryTerminated) ClearButton.onclick();
+        else if (x.length == 1) x = '0';
+        else x = x.substring(0, x.length - 1);
+
+        feedback(true);
+        updateDisplay();
+    } else if (dir == 0) {
+        EnterButton.onclick();
+    }
+});
+
+g.clear().reset();
+
+drawButtons();
+updateDisplay();
+
+Bangle.loadWidgets();
+Bangle.drawWidgets();
\ No newline at end of file
diff --git a/apps/rpnsci/icon.js b/apps/rpnsci/icon.js
new file mode 100644
index 000000000..24ea29035
--- /dev/null
+++ b/apps/rpnsci/icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwcCkmSpICEpEEBAwCICP4CCk/yCP4RVyf/AAXkCK0///5Nf4RffcYR/AQkAAERr/CKn+CK9//+f/41O/mT5IRO/+eLJ8/CIw+BAAP8CIkn+QRQMQY1MCKM8z5rP8mf/KzO8mTCJ1/CIP/8j7pCP4RMA=="))
\ No newline at end of file
diff --git a/apps/rpnsci/icon.png b/apps/rpnsci/icon.png
new file mode 100644
index 000000000..474abf7e3
Binary files /dev/null and b/apps/rpnsci/icon.png differ
diff --git a/apps/rpnsci/icon.xcf b/apps/rpnsci/icon.xcf
new file mode 100644
index 000000000..9a41dbe32
Binary files /dev/null and b/apps/rpnsci/icon.xcf differ
diff --git a/apps/rpnsci/metadata.json b/apps/rpnsci/metadata.json
new file mode 100644
index 000000000..0c52aa8a7
--- /dev/null
+++ b/apps/rpnsci/metadata.json
@@ -0,0 +1,24 @@
+{
+    "id": "rpnsci",
+    "name": "RPN Scientific Calculator",
+    "shortName": "Calculator",
+    "icon": "icon.png",
+    "version": "0.03",
+    "description": "RPN scientific calculator with memory function.",
+    "tags": "",
+    "supports": [
+        "BANGLEJS2"
+    ],
+    "readme": "README.md",
+    "storage": [
+        {
+            "name": "rpnsci.app.js",
+            "url": "app.js"
+        },
+        {
+            "name": "rpnsci.img",
+            "url": "icon.js",
+            "evaluate": "true"
+        }
+    ]
+}
\ No newline at end of file
diff --git a/apps/sensortools/ChangeLog b/apps/sensortools/ChangeLog
new file mode 100644
index 000000000..eb1ab5cbc
--- /dev/null
+++ b/apps/sensortools/ChangeLog
@@ -0,0 +1,2 @@
+0.01: New App!
+0.02: Less time used during boot if disabled
diff --git a/apps/sensortools/README.md b/apps/sensortools/README.md
new file mode 100644
index 000000000..8b89add7c
--- /dev/null
+++ b/apps/sensortools/README.md
@@ -0,0 +1,44 @@
+# Sensor tools
+
+This allows to simulate sensor behaviour for development purposes
+
+
+## Per Sensor settings:
+
+enabled:
+  true or false
+mode:
+  emulate: Completely craft events for this sensor
+  modify: Take existing events from real sensor and modify their data
+name:
+  name of the emulation or modification mode
+power:
+  emulate: Simulate Bangle._PWR changes, but do not call real power function
+  nop: Do nothing, ignore all power calls for this sensor but return true
+  passthrough: Just pass all power calls unmodified
+  on: Do not allow switching the sensor off, all calls are switching the real sensor on
+
+### HRM
+
+Modes: modify, emulate
+Modification:
+  bpmtrippled: Multiply the bpm value of the original HRM values with 3
+Emulation:
+  sin: Calculate bpm changes by using sin
+
+### GPS
+
+Modes: emulate
+Emulation:
+  staticfix: static complete fix with all values
+  route: A square route starting in the SW corner and moving SW->NW->NO->SW...
+  routeFuzzy: Roughly the same square as route, but with 100m seqments with some variaton in course
+  nofix: All values NaN but time,sattelites,fix and fix == 0
+  changingfix: A fix with randomly changing values
+
+### Compass
+
+Modes: emulate
+Emulation:
+  static: All values but heading are 1, heading == 0
+  rotate: All values but heading are 1, heading rotates 360°
diff --git a/apps/sensortools/boot.js b/apps/sensortools/boot.js
new file mode 100644
index 000000000..fa210f1a0
--- /dev/null
+++ b/apps/sensortools/boot.js
@@ -0,0 +1 @@
+if ((require('Storage').readJSON("sensortools.json", true) || {}).enabled) require("sensortools").enable();
diff --git a/apps/sensortools/default.json b/apps/sensortools/default.json
new file mode 100644
index 000000000..a85e1ddeb
--- /dev/null
+++ b/apps/sensortools/default.json
@@ -0,0 +1,18 @@
+{
+  "enabled": false,
+  "mag": {
+    "enabled": false,
+    "mode": "emulate",
+    "name": "static"
+  },
+  "hrm": {
+    "enabled": false,
+    "mode": "modify",
+    "name": "bpmtrippled"
+  },
+  "gps": {
+    "enabled": false,
+    "mode": "emulate",
+    "name": "changingfix"
+  }
+}
diff --git a/apps/sensortools/icon.png b/apps/sensortools/icon.png
new file mode 100644
index 000000000..b7b5ec9ea
Binary files /dev/null and b/apps/sensortools/icon.png differ
diff --git a/apps/sensortools/lib.js b/apps/sensortools/lib.js
new file mode 100644
index 000000000..460245c2f
--- /dev/null
+++ b/apps/sensortools/lib.js
@@ -0,0 +1,348 @@
+exports.enable = () => {
+  let settings = Object.assign(
+    require('Storage').readJSON("sensortools.default.json", true) || {},
+    require('Storage').readJSON("sensortools.json", true) || {}
+  );
+
+  let log = function(text, param) {
+    let logline = new Date().toISOString() + " - " + "Sensortools - " + text;
+    if (param) logline += ": " + JSON.stringify(param);
+    print(logline);
+  };
+  
+  log("Enabled");
+  const POWER_DELAY = 10000;
+
+  let onEvents = [];
+
+  Bangle.sensortoolsOrigOn = Bangle.on;
+  Bangle.sensortoolsOrigEmit = Bangle.emit;
+  Bangle.sensortoolsOrigRemoveListener = Bangle.removeListener;
+
+  Bangle.on = function(name, callback) {
+    if (onEvents[name]) {
+      log("Redirecting listener for", name, "to", name + "_mod");
+      Bangle.sensortoolsOrigOn(name + "_mod", callback);
+      Bangle.sensortoolsOrigOn(name, (e) => {
+        log("Redirected event for", name, "to", name + "_mod");
+        Bangle.sensortoolsOrigEmit(name + "_mod", onEvents[name](e));
+      });
+    } else {
+      log("Pass through on call for", name, callback);
+      Bangle.sensortoolsOrigOn(name, callback);
+    }
+  };
+
+  Bangle.removeListener = function(name, callback) {
+    if (onEvents[name]) {
+      log("Removing augmented listener for", name, onEvents[name]);
+      Bangle.sensortoolsOrigRemoveListener(name + "_mod", callback);
+    } else {
+      log("Pass through remove listener for", name);
+      Bangle.sensortoolsOrigRemoveListener(name, callback);
+    }
+  };
+
+  Bangle.emit = function(name, event) {
+    if (onEvents[name]) {
+      log("Augmenting emit call for", name, onEvents[name]);
+      Bangle.sensortoolsOrigEmit(name + "_mod", event);
+    } else {
+      log("Pass through emit call for", name);
+      Bangle.sensortoolsOrigEmit(name, event);
+    }
+  };
+
+  let createPowerFunction = function(type, name, origPower) {
+    return function(isOn, app) {
+      if (type == "nop") {
+        return true;
+      }else if (type == "delay") {
+        setTimeout(() => {
+          origPower(isOn, app);
+        }, POWER_DELAY);
+      } else if (type == "on") {
+        origPower(1, "sensortools_force_on");
+      } else if (type == "passthrough"){
+        origPower(isOn, "app");
+      } else if (type == "emulate"){
+        if (!Bangle._PWR) Bangle._PWR={};
+        if (!Bangle._PWR[name]) Bangle._PWR[name] = [];
+        if (!app) app="?";
+        if (isOn) {
+          Bangle._PWR[name].push(app);
+          return true;
+        } else {
+          Bangle._PWR[name] = Bangle._PWR[name].filter((v)=>{return v == app;});
+          return false;
+        }
+      }
+    };
+  };
+
+  if (settings.hrm && settings.hrm.enabled) {
+    log("HRM", settings.hrm);
+    if (settings.hrm.power) {
+      log("HRM power");
+      Bangle.sensortoolsOrigSetHRMPower = Bangle.setHRMPower;
+      Bangle.setHRMPower = createPowerFunction(settings.hrm.power, "HRM", Bangle.sensortoolsOrigSetHRMPower);
+    }
+    if (settings.hrm.mode == "modify") {
+      if (settings.hrm.name == "bpmtrippled") {
+        onEvents.HRM = (e) => {
+          return {
+            bpm: e.bpm * 3
+          };
+        };
+      }
+    } else if (settings.hrm.mode == "emulate") {
+      if (settings.hrm.name == "sin") {
+        setInterval(() => {
+          Bangle.sensortoolsOrigEmit(60 + 3 * Math.sin(Date.now() / 10000));
+        }, 1000);
+      }
+    }
+  }
+  if (settings.gps && settings.gps.enabled) {
+    log("GPS", settings.gps);
+    let modGps = function(dataProvider) {
+      Bangle.getGPSFix = dataProvider;
+      setInterval(() => {
+        Bangle.sensortoolsOrigEmit("GPS", dataProvider());
+      }, 1000);
+    };
+    if (settings.gps.power) {
+      Bangle.sensortoolsOrigSetGPSPower = Bangle.setGPSPower;
+      Bangle.setGPSPower = createPowerFunction(settings.gps.power, "GPS", Bangle.sensortoolsOrigSetGPSPower);
+    }
+    if (settings.gps.mode == "emulate") {
+          function radians(a) {
+            return a*Math.PI/180;
+          }
+
+          function degrees(a) {
+            let d = a*180/Math.PI;
+            return (d+360)%360;
+          }
+
+          function bearing(a,b){
+            if (!a || !b || !a.lon || !a.lat || !b.lon || !b.lat) return Infinity;
+            let delta = radians(b.lon-a.lon);
+            let alat = radians(a.lat);
+            let blat = radians(b.lat);
+            let y = Math.sin(delta) * Math.cos(blat);
+            let x = Math.cos(alat)*Math.sin(blat) -
+                  Math.sin(alat)*Math.cos(blat)*Math.cos(delta);
+            return Math.round(degrees(Math.atan2(y, x)));
+          }
+      
+          function interpolate(a,b,progress){
+            return {
+              lat: a.lat * progress + b.lat * (1-progress),
+              lon: a.lon * progress + b.lon * (1-progress),
+              ele: a.ele * progress + b.ele * (1-progress)
+            }
+          }
+          
+          function getSquareRoute(){
+            return [
+              {lat:"47.2577411",lon:"11.9927442",ele:2273},
+              {lat:"47.266761",lon:"11.9926673",ele:2166},
+              {lat:"47.2667605",lon:"12.0059511",ele:2245},
+              {lat:"47.2577516",lon:"12.0059925",ele:1994}
+            ];
+          }
+          function getSquareRouteFuzzy(){
+            return [
+              {lat:"47.2578455",lon:"11.9929891",ele:2265},
+              {lat:"47.258592",lon:"11.9923341",ele:2256},
+              {lat:"47.2594506",lon:"11.9927412",ele:2230},
+              {lat:"47.2603323",lon:"11.9924949",ele:2219},
+              {lat:"47.2612056",lon:"11.9928175",ele:2199},
+              {lat:"47.2621002",lon:"11.9929817",ele:2182},
+              {lat:"47.2629025",lon:"11.9923915",ele:2189},
+              {lat:"47.2637828",lon:"11.9926486",ele:2180},
+              {lat:"47.2646733",lon:"11.9928167",ele:2191},
+              {lat:"47.2655617",lon:"11.9930357",ele:2185},
+              {lat:"47.2662862",lon:"11.992252",ele:2186},
+              {lat:"47.2669305",lon:"11.993173",ele:2166},
+              {lat:"47.266666",lon:"11.9944419",ele:2171},
+              {lat:"47.2667579",lon:"11.99576",ele:2194},
+              {lat:"47.2669409",lon:"11.9970579",ele:2207},
+              {lat:"47.2666562",lon:"11.9983128",ele:2212},
+              {lat:"47.2666027",lon:"11.9996335",ele:2262},
+              {lat:"47.2667245",lon:"12.0009395",ele:2278},
+              {lat:"47.2668457",lon:"12.002256",ele:2297},
+              {lat:"47.2666126",lon:"12.0035373",ele:2303},
+              {lat:"47.2664554",lon:"12.004841",ele:2251},
+              {lat:"47.2669461",lon:"12.005948",ele:2245},
+              {lat:"47.2660877",lon:"12.006323",ele:2195},
+              {lat:"47.2652729",lon:"12.0057552",ele:2163},
+              {lat:"47.2643926",lon:"12.0060123",ele:2131},
+              {lat:"47.2634978",lon:"12.0058302",ele:2095},
+              {lat:"47.2626129",lon:"12.0060759",ele:2066},
+              {lat:"47.2617325",lon:"12.0058188",ele:2037},
+              {lat:"47.2608668",lon:"12.0061784",ele:1993},
+              {lat:"47.2600155",lon:"12.0057392",ele:1967},
+              {lat:"47.2591203",lon:"12.0058233",ele:1949},
+              {lat:"47.2582307",lon:"12.0059718",ele:1972},
+              {lat:"47.2578014",lon:"12.004804",ele:2011},
+              {lat:"47.2577232",lon:"12.0034834",ele:2044},
+              {lat:"47.257745",lon:"12.0021656",ele:2061},
+              {lat:"47.2578682",lon:"12.0008597",ele:2065},
+              {lat:"47.2577082",lon:"11.9995526",ele:2071},
+              {lat:"47.2575917",lon:"11.9982348",ele:2102},
+              {lat:"47.2577401",lon:"11.996924",ele:2147},
+              {lat:"47.257715",lon:"11.9956061",ele:2197},
+              {lat:"47.2578996",lon:"11.9943081",ele:2228}
+            ];
+          }
+      
+      if (settings.gps.name == "staticfix") {
+        modGps(() => { return {
+          "lat": 52,
+          "lon": 8,
+          "alt": 100,
+          "speed": 10,
+          "course": 12,
+          "time": Date.now(),
+          "satellites": 7,
+          "fix": 1,
+          "hdop": 1
+        };});
+      } else if (settings.gps.name.includes("route")) {
+        let route;
+        let interpSteps;
+        if (settings.gps.name == "routeFuzzy"){
+          route = getSquareRouteFuzzy();
+          interpSteps = 5;
+        } else {
+          route = getSquareRoute();
+          interpSteps = 50;
+        }
+
+        let step = 0;
+        let routeIndex = 0;
+        modGps(() => {
+          let newIndex = (routeIndex + 1)%route.length;
+          
+          let result = {
+            "speed": Math.random() * 3 + 2,
+            "time": Date.now(),
+            "satellites": Math.floor(Math.random()*5)+3,
+            "fix": 1,
+            "hdop": Math.floor(Math.random(30)+1)
+          };
+          
+          let oldPos = route[routeIndex];
+          if (step != 0){
+            oldPos = interpolate(route[routeIndex], route[newIndex], E.clip(0,1,step/interpSteps));
+          }
+          let newPos = route[newIndex];
+          if (step < interpSteps - 1){
+            newPos = interpolate(route[routeIndex], route[newIndex], E.clip(0,1,(step+1)%interpSteps/interpSteps));
+          }
+
+          if (step == interpSteps - 1){
+            let followingIndex = (routeIndex + 2)%route.length;
+            newPos = interpolate(route[newIndex], route[followingIndex], E.clip(0,1,1/interpSteps));
+          }
+          
+          result.lat = oldPos.lat;
+          result.lon = oldPos.lon;
+          result.alt = oldPos.ele;
+          
+          result.course = bearing(oldPos,newPos);
+          
+          step++;
+          if (step == interpSteps){
+            routeIndex = (routeIndex + 1) % route.length;
+            step = 0;
+          }
+          return result;
+        });
+      } else if (settings.gps.name == "nofix") {
+        modGps(() => { return {
+          "lat": NaN,
+          "lon": NaN,
+          "alt": NaN,
+          "speed": NaN,
+          "course": NaN,
+          "time": Date.now(),
+          "satellites": 2,
+          "fix": 0,
+          "hdop": NaN
+        };});
+      } else if (settings.gps.name == "changingfix") {
+        let currentSpeed=1;
+        let currentLat=20;
+        let currentLon=10;
+        let currentCourse=10;
+        let currentAlt=-100;
+        let currentSats=5;
+        modGps(() => {
+          currentLat += 0.1;
+          if (currentLat > 50) currentLat = 20;
+          currentLon += 0.1;
+          if (currentLon > 20) currentLon = 10;
+          currentSpeed *= 10;
+          if (currentSpeed > 1000) currentSpeed = 1;
+          currentCourse += 12;
+          if (currentCourse > 360) currentCourse -= 360;
+          currentSats += 1;
+          if (currentSats > 10) currentSats = 5;
+          currentAlt *= 10;
+          if (currentAlt > 1000) currentAlt = -100;
+          return {
+          "lat": currentLat,
+          "lon": currentLon,
+          "alt": currentAlt,
+          "speed": currentSpeed,
+          "course": currentCourse,
+          "time": Date.now(),
+          "satellites": currentSats,
+          "fix": 1,
+          "hdop": 1
+        };});
+      }
+    }
+  }
+
+  if (settings.mag && settings.mag.enabled) {
+    log("MAG", settings.mag);
+    let modMag = function(data) {
+      setInterval(() => {
+        Bangle.getCompass = data;
+        Bangle.sensortoolsOrigEmit("mag", data());
+      }, 100);
+    };
+    if (settings.mag.power) {
+      Bangle.sensortoolsOrigSetCompassPower = Bangle.setCompassPower;
+      Bangle.setCompassPower = createPowerFunction(settings.mag.power, "Compass", Bangle.sensortoolsOrigSetCompassPower);
+    }
+    if (settings.mag.mode == "emulate") {
+      if (settings.mag.name == "static") {
+        modMag(()=>{return {
+          x: 1,
+          y: 1,
+          z: 1,
+          dx: 1,
+          dy: 1,
+          dz: 1,
+          heading: 0
+        };});
+      } else if (settings.mag.name == "rotate"){
+        let last = 0;
+        modMag(()=>{return {
+          x: 1,
+          y: 1,
+          z: 1,
+          dx: 1,
+          dy: 1,
+          dz: 1,
+          heading: last = (last+1)%360,
+        };});
+      }
+    }
+  }
+};
diff --git a/apps/sensortools/metadata.json b/apps/sensortools/metadata.json
new file mode 100644
index 000000000..59a129ec1
--- /dev/null
+++ b/apps/sensortools/metadata.json
@@ -0,0 +1,18 @@
+{
+  "id": "sensortools",
+  "name": "Sensor tools",
+  "shortName": "Sensor tools",
+  "version": "0.02",
+  "description": "Tools for testing and debugging apps that use sensor input",
+  "icon": "icon.png",
+  "type": "bootloader",
+  "tags": "tool,boot,debug",
+  "supports": ["BANGLEJS","BANGLEJS2"],
+  "readme": "README.md",
+  "storage": [
+    {"name":"sensortools.0.boot.js","url":"boot.js"},
+    {"name":"sensortools.settings.js","url":"settings.js"},
+    {"name":"sensortools","url":"lib.js"},
+    {"name":"sensortools.default.json","url":"default.json"}
+  ]
+}
diff --git a/apps/sensortools/settings.js b/apps/sensortools/settings.js
new file mode 100644
index 000000000..231ab8467
--- /dev/null
+++ b/apps/sensortools/settings.js
@@ -0,0 +1,99 @@
+(function(back) {
+  function writeSettings(key, value) {
+    var s = require('Storage').readJSON(FILE, true) || {};
+    s[key] = value;
+    require('Storage').writeJSON(FILE, s);
+    readSettings();
+  }
+
+  function writeSettingsParent(parent, key, value) {
+    var s = require('Storage').readJSON(FILE, true) || {};
+    if (!s[parent]) s[parent] = {};
+    s[parent][key] = value;
+    require('Storage').writeJSON(FILE, s);
+    readSettings();
+  }
+
+  function readSettings(){
+    settings = Object.assign(
+      require('Storage').readJSON("sensortools.default.json", true) || {},
+      require('Storage').readJSON(FILE, true) || {}
+    );
+  }
+
+  var FILE="sensortools.json";
+  var settings;
+  readSettings();
+
+
+  let modes = ["nop", "emulate", "modify"];
+  let modesPower = ["nop", "emulate", "passthrough", "delay", "on"];
+
+  function showSubMenu(name,key,typesEmulate,typesModify){
+    var menu = {
+      '': { 'title': name,
+          back: ()=>{E.showMenu(buildMainMenu());}},
+      'Enabled': {
+        value: !!settings[key].enabled,
+        onchange: v => {
+          writeSettingsParent(key, "enabled",v);
+        }
+      },
+      'Mode': {
+        value: modes.indexOf(settings[key].mode||"nop"),
+        min: 0, max: modes.length-1,
+        format: v => { return modes[v]; },
+        onchange: v => {
+          writeSettingsParent(key,"mode",modes[v]);
+          showSubMenu(name,key,typesEmulate,typesModify);
+        }
+      },
+      'Name': {},
+      'Power': {
+        value: modesPower.indexOf(settings[key].power||"nop"),
+        min: 0, max: modesPower.length-1,
+        format: v => { return modesPower[v]; },
+        onchange: v => {
+          writeSettingsParent(key,"power",modesPower[v]);
+        }
+      },
+    };
+    
+    if (settings[key].mode != "nop"){
+      let types = typesEmulate;
+      if (settings[key].mode == "modify") types = typesModify;
+      menu.Name = {
+        value: types.indexOf(settings[key].name||"static"),
+        min: 0, max: types.length-1,
+        format: v => { return types[v]; },
+        onchange: v => {
+          writeSettingsParent(key,"name",types[v]);
+        }
+      };
+    } else {
+      delete menu.Name;
+    }
+    
+    E.showMenu(menu);
+  }
+
+
+  function buildMainMenu(){
+    var mainmenu = {
+      '': { 'title': 'Sensor tools' },
+      '< Back': back,
+      'Enabled': {
+        value: !!settings.enabled,
+        onchange: v => {
+          writeSettings("enabled",v);
+        },
+      },
+      'GPS': ()=>{showSubMenu("GPS","gps",["nop", "staticfix", "nofix", "changingfix", "route", "routeFuzzy"],[]);},
+      'Compass': ()=>{showSubMenu("Compass","mag",["nop", "static", "rotate"],[]);},
+      'HRM': ()=>{showSubMenu("HRM","hrm",["nop", "static"],["bpmtrippled"],["sin"]);}
+    };
+    return mainmenu;
+  }
+
+  E.showMenu(buildMainMenu());
+});
diff --git a/apps/setting/ChangeLog b/apps/setting/ChangeLog
index 9b88f0073..55b61c46a 100644
--- a/apps/setting/ChangeLog
+++ b/apps/setting/ChangeLog
@@ -54,3 +54,8 @@
       Improve "Turn Off" user experience
 0.48: Allow reading custom themes from files
 0.49: Now reloads settings properly after 'Calibrate Battery'
+0.50: Add Bangle.js 2 touchscreen calibration - for 2v16 or 2v15 cutting edge builds
+0.51: Add setting for configuring a launcher
+0.52: Add option for left-handed users
+0.53: Ensure that when clock is set, clockHasWidgets is set correctly too
+0.54: If setting.json is corrupt, ensure it gets re-written
diff --git a/apps/setting/README.md b/apps/setting/README.md
index 657b96f71..e5ea2b43d 100644
--- a/apps/setting/README.md
+++ b/apps/setting/README.md
@@ -29,10 +29,12 @@ This is Bangle.js's settings menu
 
 * **LCD Brightness** set how bright the LCD is. Due to hardware limitations in the LCD backlight, you may notice flicker if the LCD is not at 100% brightness.
 * **LCD Timeout** how long should the LCD stay on for if no activity is detected. 0=stay on forever
+* **Rotation** allows you to rotate (or mirror) what's displayed on the screen, eg. for left-handed wearers (needs 2v16 or 2v15 cutting edge firmware to work reliably)
 * **Wake on X** should the given activity wake up the Bangle.js LCD?
   * On Bangle.js 2 when locked the touchscreen is turned off to save power. Because of this,
     `Wake on Touch` actually uses the accelerometer, and you need to actually tap the display to wake Bangle.js.
 * **Twist X** these options adjust the sensitivity of `Wake on Twist` to ensure Bangle.js wakes up with just the right amount of wrist movement.
+* **Calibrate** on Bangle.js 2, pop up a screen allowing you to calibrate the touchscreen (calibration only works on 2v16 or 2v15 cutting edge builds)
 
 ## Locale
 
diff --git a/apps/setting/metadata.json b/apps/setting/metadata.json
index d209eafc3..08544cff6 100644
--- a/apps/setting/metadata.json
+++ b/apps/setting/metadata.json
@@ -1,7 +1,7 @@
 {
   "id": "setting",
   "name": "Settings",
-  "version": "0.49",
+  "version": "0.54",
   "description": "A menu for setting up Bangle.js",
   "icon": "settings.png",
   "tags": "tool,system",
diff --git a/apps/setting/settings.js b/apps/setting/settings.js
index 77e3fdd87..2350a8965 100644
--- a/apps/setting/settings.js
+++ b/apps/setting/settings.js
@@ -39,6 +39,7 @@ function resetSettings() {
     timezone: 0,                    // Set the timezone for the device
     HID: false,                     // BLE HID mode, off by default
     clock: null,                    // a string for the default clock's name
+    // clockHasWidgets: false,      // Does the clock in 'clock' contain the string 'Bangle.loadWidgets'
     "12hour" : false,               // 12 or 24 hour clock?
     firstDayOfWeek: 0,              // 0 -> Sunday (default), 1 -> Monday
     brightness: 1,                  // LCD brightness from 0 to 1
@@ -59,7 +60,9 @@ function resetSettings() {
 }
 
 settings = storage.readJSON('setting.json', 1);
-if (!settings) resetSettings();
+if (("object" != typeof settings) ||
+    ("object" != typeof settings.options))
+  resetSettings();
 
 const boolFormat = v => v ? /*LANG*/"On" : /*LANG*/"Off";
 
@@ -87,6 +90,7 @@ function showSystemMenu() {
     /*LANG*/'LCD': ()=>showLCDMenu(),
     /*LANG*/'Locale': ()=>showLocaleMenu(),
     /*LANG*/'Select Clock': ()=>showClockMenu(),
+    /*LANG*/'Select Launcher': ()=>showLauncherMenu(),
     /*LANG*/'Date & Time': ()=>showSetTimeMenu()
   };
 
@@ -235,9 +239,9 @@ function showThemeMenu() {
     }
   };
 
-  require("Storage").list(/^.*\.theme$/).forEach(
+  storage.list(/^.*\.theme$/).forEach(
     n => {
-      let newTheme = require("Storage").readJSON(n);
+      let newTheme = storage.readJSON(n);
       themesMenu[newTheme.name ? newTheme.name : n] = () => {
         upd({
         fg:cl(newTheme.fg), bg:cl(newTheme.bg),
@@ -383,6 +387,8 @@ function showLCDMenu() {
   // converts Espruino internal unit to g
   function internalToG(u) { return u / 8192; }
 
+  var rotNames = [/*LANG*/"No",/*LANG*/"Rotate CW",/*LANG*/"Left Handed",/*LANG*/"Rotate CCW",/*LANG*/"Mirror"];
+
   const lcdMenu = {
     '': { 'title': 'LCD' },
     '< Back': ()=>showSystemMenu(),
@@ -408,6 +414,18 @@ function showLCDMenu() {
         Bangle.setLCDTimeout(settings.timeout);
       }
     },
+    /*LANG*/'Rotate': {
+      value: 0|settings.rotate,
+      min: 0,
+      max: rotNames.length-1,
+      format: v=> rotNames[v],
+      onchange: v => {
+        settings.rotate = 0 | v;
+        updateSettings();
+        g.setRotation(settings.rotate&3,settings.rotate>>2).clear();
+        Bangle.drawWidgets();
+      }
+    },
     /*LANG*/'Wake on BTN1': {
       value: settings.options.wakeOnBTN1,
       format: boolFormat,
@@ -491,6 +509,10 @@ function showLCDMenu() {
       }
     }
   });
+  if (BANGLEJS2)
+    Object.assign(lcdMenu, {
+    /*LANG*/'Calibrate': () => showTouchscreenCalibration()
+    });
   return E.showMenu(lcdMenu)
 }
 
@@ -547,11 +569,11 @@ function showUtilMenu() {
     },
     /*LANG*/'Compact Storage': () => {
       E.showMessage(/*LANG*/"Compacting...\nTakes approx\n1 minute",{title:/*LANG*/"Storage"});
-      require("Storage").compact();
+      storage.compact();
       showUtilMenu();
     },
     /*LANG*/'Rewrite Settings': () => {
-      require("Storage").write(".boot0","eval(require('Storage').read('bootupdate.js'));");
+      storage.write(".boot0","eval(require('Storage').read('bootupdate.js'));");
       load("setting.app.js");
     },
     /*LANG*/'Flatten Battery': () => {
@@ -572,9 +594,9 @@ function showUtilMenu() {
     menu[/*LANG*/'Calibrate Battery'] = () => {
       E.showPrompt(/*LANG*/"Is the battery fully charged?",{title:/*LANG*/"Calibrate"}).then(ok => {
         if (ok) {
-          var s=require("Storage").readJSON("setting.json");
+          var s=storage.readJSON("setting.json");
           s.batFullVoltage = (analogRead(D3)+analogRead(D3)+analogRead(D3)+analogRead(D3))/4;
-          require("Storage").writeJSON("setting.json",s);
+          storage.writeJSON("setting.json",s);
           E.showAlert(/*LANG*/"Calibrated!").then(() => load("setting.app.js"));
         } else {
           E.showAlert(/*LANG*/"Please charge Bangle.js for 3 hours and try again").then(() => load("settings.app.js"));
@@ -639,7 +661,7 @@ function makeConnectable() {
   });
 }
 function showClockMenu() {
-  var clockApps = require("Storage").list(/\.info$/)
+  var clockApps = storage.list(/\.info$/)
     .map(app => {var a=storage.readJSON(app, 1);return (a&&a.type == "clock")?a:undefined})
     .filter(app => app) // filter out any undefined apps
     .sort((a, b) => a.sortorder - b.sortorder);
@@ -655,11 +677,10 @@ function showClockMenu() {
       label = "* " + label;
     }
     clockMenu[label] = () => {
-      if (settings.clock !== app.src) {
-        settings.clock = app.src;
-        updateSettings();
-        showMainMenu();
-      }
+      settings.clock = app.src;
+      settings.clockHasWidgets = storage.read(app.src).includes("Bangle.loadWidgets");
+      updateSettings();
+      showMainMenu();
     };
   });
   if (clockApps.length === 0) {
@@ -667,6 +688,33 @@ function showClockMenu() {
   }
   return E.showMenu(clockMenu);
 }
+function showLauncherMenu() {
+  var launcherApps = storage.list(/\.info$/)
+    .map(app => {var a=storage.readJSON(app, 1);return (a&&a.type == "launch")?a:undefined})
+    .filter(app => app) // filter out any undefined apps
+    .sort((a, b) => a.sortorder - b.sortorder);
+  const launcherMenu = {
+    '': {
+      'title': /*LANG*/'Select Launcher',
+    },
+    '< Back': ()=>showSystemMenu(),
+  };
+  launcherApps.forEach((app, index) => {
+    var label = app.name;
+    if ((!settings.launcher && index === 0) || (settings.launcher === app.src)) {
+      label = "* " + label;
+    }
+    launcherMenu[label] = () => {
+      settings.launcher = app.src;
+      updateSettings();
+      showMainMenu();
+    };
+  });
+  if (launcherApps.length === 0) {
+    launcherMenu[/*LANG*/"No Launchers Found"] = () => { };
+  }
+  return E.showMenu(launcherMenu);
+}
 
 function showSetTimeMenu() {
   d = new Date();
@@ -774,4 +822,85 @@ function showAppSettings(app) {
   }
 }
 
+function showTouchscreenCalibration() {
+  Bangle.setUI();
+  // disable touchscreen calibration (passed coords right through)
+  Bangle.setOptions({touchX1: 0, touchY1: 0, touchX2: g.getWidth(), touchY2: g.getHeight() });
+
+  var P = 32;
+  var corners = [
+    [P,P],
+    [g.getWidth()-P,P],
+    [g.getWidth()-P,g.getHeight()-P],
+    [P,g.getHeight()-P],
+  ];
+  var currentCorner = 0;
+  var currentTry = 0;
+  var pt = {
+    x1 : 0, y1 : 0, x2 : 0, y2 : 0
+  };
+
+  function showTapSpot() {
+    var spot = corners[currentCorner];
+    g.clear(1);
+    g.drawLine(spot[0]-32,spot[1],spot[0]+32,spot[1]);
+    g.drawLine(spot[0],spot[1]-32,spot[0],spot[1]+32);
+    g.drawCircle(spot[0],spot[1], 16);
+    var tapsLeft = (1-currentTry)*4+(4-currentCorner);
+    g.setFont("6x8:2").setFontAlign(0,0).drawString(tapsLeft+" taps\nto go", g.getWidth()/2, g.getHeight()/2);
+  }
+
+  function calcCalibration() {
+    g.clear(1);
+    // we should now have 4 of each tap in 'pt'
+    pt.x1 /= 4;
+    pt.y1 /= 4;
+    pt.x2 /= 4;
+    pt.y2 /= 4;
+    // work out final values
+    var calib = {
+      x1 : Math.round(pt.x1 - (pt.x2-pt.x1)*P/(g.getWidth()-P*2)),
+      y1 : Math.round(pt.y1 - (pt.y2-pt.y1)*P/(g.getHeight()-P*2)),
+      x2 : Math.round(pt.x2 + (pt.x2-pt.x1)*P/(g.getWidth()-P*2)),
+      y2 : Math.round(pt.y2 + (pt.y2-pt.y1)*P/(g.getHeight()-P*2))
+    };
+    Bangle.setOptions({
+      touchX1: calib.x1, touchY1: calib.y1, touchX2: calib.x2, touchY2: calib.y2
+    });
+    var s = storage.readJSON("setting.json",1)||{};
+    s.touch = calib;
+    storage.writeJSON("setting.json",s);
+    g.setFont("6x8:2").setFontAlign(0,0).drawString("Calibrated!", g.getWidth()/2, g.getHeight()/2);
+    // now load the main menu again
+    setTimeout(showLCDMenu, 500);
+  }
+
+  function touchHandler(_,e) {
+    var spot = corners[currentCorner];
+    // store averages
+    if (spot[0]*2 < g.getWidth())
+      pt.x1 += e.x;
+    else
+      pt.x2 += e.x;
+    if (spot[1]*2 < g.getHeight())
+      pt.y1 += e.y;
+    else
+      pt.y2 += e.y;
+    // go to next corner
+    currentCorner++;
+    if (currentCorner>=corners.length) {
+      currentCorner = 0;
+      currentTry++;
+      if (currentTry==2) {
+        Bangle.removeListener('touch', touchHandler);
+        return calcCalibration();
+      }
+    }
+    showTapSpot();
+  }
+  Bangle.on('touch', touchHandler);
+
+  showTapSpot();
+}
+
 showMainMenu();
diff --git a/apps/sleepphasealarm/ChangeLog b/apps/sleepphasealarm/ChangeLog
index 6bf296342..9f2b07d49 100644
--- a/apps/sleepphasealarm/ChangeLog
+++ b/apps/sleepphasealarm/ChangeLog
@@ -10,4 +10,4 @@
 0.09: Vibrate with configured pattern
       Add setting to defer start of algorithm
       Add setting to disable scheduler alarm
-
+0.10: Fix: Do not wake when falling asleep
diff --git a/apps/sleepphasealarm/README.md b/apps/sleepphasealarm/README.md
index ecb3feb06..574e84e1e 100644
--- a/apps/sleepphasealarm/README.md
+++ b/apps/sleepphasealarm/README.md
@@ -9,6 +9,8 @@ The display shows:
 - Time difference between current time and alarm time (ETA).
 - Current state of the ESS algorithm, "Sleep" or "Awake", useful for debugging. State can also be "Deferred", see the "Run before alarm"-option.
 
+Replacing the watch strap with a more comfortable one (e.g. made of nylon) is recommended.
+
 ## Settings
 
 * **Keep alarm enabled**
@@ -16,7 +18,7 @@ The display shows:
   - No: No action at configured alarm time from scheduler.
 * **Run before alarm**
   - disabled: (default) The ESS algorithm starts immediately when the application starts.
-  - 1..23: The ESS algorithm starts the configured time before the alarm. E.g. when set to 1h for an alarm at 7:00 the ESS algorithm will start at 6:00. This improves battery life.
+  - 1..23: The ESS algorithm starts the configured time before the alarm. E.g. when set to 1h for an alarm at 7:00 the ESS algorithm will start at 6:00. This increases battery life.
 
 ## Logging
 
diff --git a/apps/sleepphasealarm/app.js b/apps/sleepphasealarm/app.js
index b19799c4b..b3aacc80d 100644
--- a/apps/sleepphasealarm/app.js
+++ b/apps/sleepphasealarm/app.js
@@ -168,7 +168,7 @@ if (nextAlarmDate !== undefined) {
         // The alarm widget should handle this one
         addLog(now, "alarm");
         setTimeout(load, 1000);
-      } else if (measure && now >= minAlarm && swest_last === false) {
+      } else if (measure && now >= minAlarm && swest === false) {
         addLog(now, "alarm");
         buzz();
         measure = false;
diff --git a/apps/sleepphasealarm/interface.html b/apps/sleepphasealarm/interface.html
index f45c183e1..8c8cea990 100644
--- a/apps/sleepphasealarm/interface.html
+++ b/apps/sleepphasealarm/interface.html
@@ -30,7 +30,7 @@ function getData() {
     // remove window
     Util.hideModal();
 
-    logs = logs.filter(log => log != null);
+    logs = logs.filter(log => log != null && log.filter(entry => entry.type === "alarm").length > 0);
     logs.sort(function(a, b) {return new Date(b?.filter(entry => entry.type === "alarm")[0]?.time) - new Date(a?.filter(entry => entry.type === "alarm")[0]?.time)}); // sort by alarm date desc
     logs.forEach((log, i) => {
       const timeStr = log.filter(entry => entry.type === "alarm")[0]?.time;
diff --git a/apps/sleepphasealarm/metadata.json b/apps/sleepphasealarm/metadata.json
index 6ec5f4180..35eea7466 100644
--- a/apps/sleepphasealarm/metadata.json
+++ b/apps/sleepphasealarm/metadata.json
@@ -2,7 +2,7 @@
   "id": "sleepphasealarm",
   "name": "SleepPhaseAlarm",
   "shortName": "SleepPhaseAlarm",
-  "version": "0.09",
+  "version": "0.10",
   "description": "Uses the accelerometer to estimate sleep and wake states with the principle of Estimation of Stationary Sleep-segments (ESS, see https://ubicomp.eti.uni-siegen.de/home/datasets/ichi14/index.html.en). This app will read the next alarm from the alarm application and will wake you up to 30 minutes early at the best guessed time when you are almost already awake.",
   "icon": "app.png",
   "tags": "alarm",
diff --git a/apps/slidingtext/ChangeLog b/apps/slidingtext/ChangeLog
index 1b45c36cb..5c4a9fa75 100644
--- a/apps/slidingtext/ChangeLog
+++ b/apps/slidingtext/ChangeLog
@@ -8,3 +8,4 @@
 0.08: Removed "wake LCD on face-up"-feature: A watch-face should not set things like "wake LCD on face-up".
 0.09: Added button control toggle and other live controls to new settings screen.
 0.10: Tell clock widgets to hide.
+0.11: Added new styling and watch faces
diff --git a/apps/slidingtext/README.md b/apps/slidingtext/README.md
index d5a561634..dde2b62af 100644
--- a/apps/slidingtext/README.md
+++ b/apps/slidingtext/README.md
@@ -1,54 +1,81 @@
 # Sliding Text Clock - See the time in different languages
 
-Inspired by the Pebble sliding clock, old times are scrolled off the screen and new times on. You are also able to change language on the fly so you can see the time written in other languages using button 1. Please use the upload page to choose which languages you want loaded.
+Inspired by the Pebble sliding clock, previous times are scrolled off the screen and new times scrolled on. There are a variety of colours schemes, clock faces and languages available through the settings menu 
 
 ![](app.png)
 
-## Usage
+## Settings
 
-### Bangle 2
+Please go to the sliding text clock menu under the settings menu to customise clock. Settings -> Apps -> Sliding Clock
 
-The Bangle 2 has Live Controls switched **off** by default so the colour and language have to be changed from the setting Menu.
-Please locate the Sliding Text clock under the setting->apps menu.
 
-With the Live Controls switched on:
-#### Bottom right hand corner press
-press the bottom right hand corner of the screen to change the colour
 
-| White                | Black                | Gray                 | Red                  |
-|----------------------|----------------------|----------------------|----------------------|
-| ![](b2_color-01.jpg) | ![](b2_color-02.jpg) | ![](b2_color-03.jpg) | ![](b2_color-04.jpg) |
+## Colour
 
-#### Top right hand corner press
-press the top right hand corner of the screen to change the language
+The colour selection allows to select between different colour schemes. Colour schemes that are currently available are:
 
-### Bangle 1
+- White background  with black lettering
+- Black background with red and white lettering
+- Red background with yellow and white lettering.
+- Grey background with black and white lettering
+- Purple with yellow and white lettering
+- Blue with yellow and white lettering
 
-By Default the Live Controls (The side buttons) are switched on, which means the clock face can be controlled dynamically using the 2 side buttons on the right hand side
+## Live Control
 
-#### Button 1
+Live control allows you to change the colour scheme of the clock by pressing 
 
-Use Button 1 (the top right button) to change the language
+- The bottom right hand corner of the screen for a bangle 2
+- Button 3 on on a bangle 1
 
-|   English   |  English (Traditional)    |  French    | Japanese (Romanji) |
-| ---- | ---- | ---- | ---- |
-|   ![](format-01.jpg)   | ![](format-02.jpg)     |  ![](format-03.jpg) |![](format-04.jpg)    |
-|   **German**   |  **Spanish**    |      |  |
-|   ![](format-05.jpg)   | ![](format-06.jpg)     | |    |
+When select the watch will move to the next colour in the scheme. The selected colour will not be saved so it will will revert to the last colour select in the menu when the clock is restarted. This option is included to help select the preferred colour with having to continuously go back to the settings menu.
 
-#### Button 3
-Button 3 (bottom right button) is used to change the colour
+The Live Control is turned off by default on a bangle 2, but is on by default for a bangle 1
 
-|  Black   |  Red    |  Gray    |  Purple    |
-| ---- | ---- | ---- | ---- |
-|   ![](b1_color-01.jpg) | ![](b1_color-02.jpg) |  ![](b1_color-03.jpg)   | ![](b1_color-04.jpg)   |
+## Style
 
-#### Settings
+Style controls the clock face.
+
+
+
+### English
+
+| Style  | English 1                                       | English 1 Alternative                                        | English 2                                | English 2 Alternative                                  | English Hybrid                                            |
+| ------ | ----------------------------------------------- | ------------------------------------------------------------ | ---------------------------------------- | ------------------------------------------------------ | --------------------------------------------------------- |
+| Screen | ![](slidingtext-screenshot.english.png)         | ![](slidingtext-screenshot.english_alt.png)                  | ![](slidingtext-screenshot.english2.png) | ![](slidingtext-screenshot.english2_alt.png)           | ![](slidingtext-screenshot.hybrid.png)                    |
+| Notes  | Straight 12 hour English time and Date in words | Straight 12 hour English time and Date in words in alternative style | Traditional English Time                 | Traditional English Time and Date in alternative style | 24 Hour clock  in numbers with minutes  and date in words |
+
+### French
+| Style  | French         |
+| ------ | -------------- |
+| Screen | ![](slidingtext-screenshot.french.png) |
+
+### Spanish
+| Style  | Spanish         |
+| ------ | -------------- |
+| Screen | ![](slidingtext-screenshot.spanish.png) |
+
+### German 
+
+| Style  | German 12 Hour | German 24 Hour |
+| ------ | -------------- | -------------- |
+| Screen | ![](slidingtext-screenshot.german.png) |![](slidingtext-screenshot.german24.png) |
+| Notes  | 12 Hour German clock in words | 24 Hour German clock in words |
+
+### Japanese
+
+| Style  | Japanese                                 |
+| ------ | ---------------------------------------- |
+| Screen | ![](slidingtext-screenshot.japanese.png) |
+| Notes  | Simplified Romanji Japanese Clock.       |
+
+### Digital
+
+| Style  | Digits                                  |
+| ------ | --------------------------------------- |
+| Screen | ![](slidingtext-screenshot.digital.png) |
+| Notes  | Sliding version of a digital clock      |
 
-To turn off the Live Controls and change the settings statically please visit the settings menu. The settings menu will allow you to:
-- Colour Scheme
-- Language
-- Live Controls (On or Off)
 
 ## Further Details
 
@@ -56,7 +83,7 @@ For further details of design and working please visit [The Project Page](https:
 
 ## Requests
 
-Reach out to adrian@adriankirk.com if you have feature requests or notice bugs.
+Thank you so much for the feedback so far. Please reach out to adrian@adriankirk.com if you have feature requests or notice bugs.
 
 ## Creator
 
diff --git a/apps/slidingtext/b1_color-01.jpg b/apps/slidingtext/b1_color-01.jpg
deleted file mode 100644
index 49efb0481..000000000
Binary files a/apps/slidingtext/b1_color-01.jpg and /dev/null differ
diff --git a/apps/slidingtext/b1_color-02.jpg b/apps/slidingtext/b1_color-02.jpg
deleted file mode 100644
index 446491cc4..000000000
Binary files a/apps/slidingtext/b1_color-02.jpg and /dev/null differ
diff --git a/apps/slidingtext/b1_color-03.jpg b/apps/slidingtext/b1_color-03.jpg
deleted file mode 100644
index 0b26419a5..000000000
Binary files a/apps/slidingtext/b1_color-03.jpg and /dev/null differ
diff --git a/apps/slidingtext/b1_color-04.jpg b/apps/slidingtext/b1_color-04.jpg
deleted file mode 100644
index 385c42a90..000000000
Binary files a/apps/slidingtext/b1_color-04.jpg and /dev/null differ
diff --git a/apps/slidingtext/b2_color-01.jpg b/apps/slidingtext/b2_color-01.jpg
deleted file mode 100644
index 7428f7623..000000000
Binary files a/apps/slidingtext/b2_color-01.jpg and /dev/null differ
diff --git a/apps/slidingtext/b2_color-02.jpg b/apps/slidingtext/b2_color-02.jpg
deleted file mode 100644
index 7e3b8666f..000000000
Binary files a/apps/slidingtext/b2_color-02.jpg and /dev/null differ
diff --git a/apps/slidingtext/b2_color-03.jpg b/apps/slidingtext/b2_color-03.jpg
deleted file mode 100644
index 96c8655cf..000000000
Binary files a/apps/slidingtext/b2_color-03.jpg and /dev/null differ
diff --git a/apps/slidingtext/b2_color-04.jpg b/apps/slidingtext/b2_color-04.jpg
deleted file mode 100644
index dac36365a..000000000
Binary files a/apps/slidingtext/b2_color-04.jpg and /dev/null differ
diff --git a/apps/slidingtext/custom.html b/apps/slidingtext/custom.html
deleted file mode 100644
index 5e89e230b..000000000
--- a/apps/slidingtext/custom.html
+++ /dev/null
@@ -1,71 +0,0 @@
-
-  
-    
-  
-  
-      
-      

Please select watch languages (Max 3, only the first 3 selected will be loaded)

- - - - - - -
EnabledName
- -

Click

- - - - - - diff --git a/apps/slidingtext/format-01.jpg b/apps/slidingtext/format-01.jpg deleted file mode 100644 index b8bc4552e..000000000 Binary files a/apps/slidingtext/format-01.jpg and /dev/null differ diff --git a/apps/slidingtext/format-02.jpg b/apps/slidingtext/format-02.jpg deleted file mode 100644 index c8b7a5e60..000000000 Binary files a/apps/slidingtext/format-02.jpg and /dev/null differ diff --git a/apps/slidingtext/format-03.jpg b/apps/slidingtext/format-03.jpg deleted file mode 100644 index 5dfdd8b23..000000000 Binary files a/apps/slidingtext/format-03.jpg and /dev/null differ diff --git a/apps/slidingtext/format-04.jpg b/apps/slidingtext/format-04.jpg deleted file mode 100644 index 19b01fd64..000000000 Binary files a/apps/slidingtext/format-04.jpg and /dev/null differ diff --git a/apps/slidingtext/format-05.jpg b/apps/slidingtext/format-05.jpg deleted file mode 100644 index d6bd2b9aa..000000000 Binary files a/apps/slidingtext/format-05.jpg and /dev/null differ diff --git a/apps/slidingtext/format-06.jpg b/apps/slidingtext/format-06.jpg deleted file mode 100644 index 493777d23..000000000 Binary files a/apps/slidingtext/format-06.jpg and /dev/null differ diff --git a/apps/slidingtext/metadata.json b/apps/slidingtext/metadata.json index 49ffaf16f..098fdb747 100644 --- a/apps/slidingtext/metadata.json +++ b/apps/slidingtext/metadata.json @@ -1,15 +1,15 @@ { "id": "slidingtext", "name": "Sliding Clock", - "version": "0.10", + "version": "0.11", "description": "Inspired by the Pebble sliding clock, old times are scrolled off the screen and new times on. You are also able to change language on the fly so you can see the time written in other languages using button 1. Currently English, French, Japanese, Spanish and German are supported", "icon": "slidingtext.png", + "screenshots": [{"url":"slidingtext-screenshot.english.png"},{"url":"slidingtext-screenshot.english2.png"},{"url":"slidingtext-screenshot.hybrid.png"}], "type": "clock", "tags": "clock", "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", - "custom": "custom.html", - "allow_emulator": false, + "allow_emulator": true, "storage": [ {"name":"slidingtext.app.js","url":"slidingtext.js"}, {"name":"slidingtext.settings.js","url":"slidingtext.settings.js"}, @@ -20,7 +20,11 @@ {"name":"slidingtext.locale.es.js","url":"slidingtext.locale.es.js"}, {"name":"slidingtext.locale.fr.js","url":"slidingtext.locale.fr.js"}, {"name":"slidingtext.locale.jp.js","url":"slidingtext.locale.jp.js"}, + {"name":"slidingtext.utils.de.js","url":"slidingtext.utils.de.js"}, {"name":"slidingtext.locale.de.js","url":"slidingtext.locale.de.js"}, + {"name":"slidingtext.locale.de2.js","url":"slidingtext.locale.de2.js"}, + {"name":"slidingtext.locale.dgt.js","url":"slidingtext.locale.dgt.js"}, + {"name":"slidingtext.locale.hyb.js","url":"slidingtext.locale.hyb.js"}, {"name":"slidingtext.dtfmt.js","url":"slidingtext.dtfmt.js"} ], "data": [{"name": "slidingtext.settings.json"}] diff --git a/apps/slidingtext/slidingtext-screenshot.digital.png b/apps/slidingtext/slidingtext-screenshot.digital.png new file mode 100644 index 000000000..b06d2ef18 Binary files /dev/null and b/apps/slidingtext/slidingtext-screenshot.digital.png differ diff --git a/apps/slidingtext/slidingtext-screenshot.english.png b/apps/slidingtext/slidingtext-screenshot.english.png new file mode 100644 index 000000000..14c91ba43 Binary files /dev/null and b/apps/slidingtext/slidingtext-screenshot.english.png differ diff --git a/apps/slidingtext/slidingtext-screenshot.english2.png b/apps/slidingtext/slidingtext-screenshot.english2.png new file mode 100644 index 000000000..3005d19ee Binary files /dev/null and b/apps/slidingtext/slidingtext-screenshot.english2.png differ diff --git a/apps/slidingtext/slidingtext-screenshot.english2_alt.png b/apps/slidingtext/slidingtext-screenshot.english2_alt.png new file mode 100644 index 000000000..88131afa4 Binary files /dev/null and b/apps/slidingtext/slidingtext-screenshot.english2_alt.png differ diff --git a/apps/slidingtext/slidingtext-screenshot.english_alt.png b/apps/slidingtext/slidingtext-screenshot.english_alt.png new file mode 100644 index 000000000..2ce813710 Binary files /dev/null and b/apps/slidingtext/slidingtext-screenshot.english_alt.png differ diff --git a/apps/slidingtext/slidingtext-screenshot.french.png b/apps/slidingtext/slidingtext-screenshot.french.png new file mode 100644 index 000000000..8f04fb8dc Binary files /dev/null and b/apps/slidingtext/slidingtext-screenshot.french.png differ diff --git a/apps/slidingtext/slidingtext-screenshot.german.png b/apps/slidingtext/slidingtext-screenshot.german.png new file mode 100644 index 000000000..0726f575d Binary files /dev/null and b/apps/slidingtext/slidingtext-screenshot.german.png differ diff --git a/apps/slidingtext/slidingtext-screenshot.german24.png b/apps/slidingtext/slidingtext-screenshot.german24.png new file mode 100644 index 000000000..99d93b475 Binary files /dev/null and b/apps/slidingtext/slidingtext-screenshot.german24.png differ diff --git a/apps/slidingtext/slidingtext-screenshot.hybrid.png b/apps/slidingtext/slidingtext-screenshot.hybrid.png new file mode 100644 index 000000000..2ae7721fa Binary files /dev/null and b/apps/slidingtext/slidingtext-screenshot.hybrid.png differ diff --git a/apps/slidingtext/slidingtext-screenshot.japanese.png b/apps/slidingtext/slidingtext-screenshot.japanese.png new file mode 100644 index 000000000..80c9cdee9 Binary files /dev/null and b/apps/slidingtext/slidingtext-screenshot.japanese.png differ diff --git a/apps/slidingtext/slidingtext-screenshot.spanish.png b/apps/slidingtext/slidingtext-screenshot.spanish.png new file mode 100644 index 000000000..7160f29d6 Binary files /dev/null and b/apps/slidingtext/slidingtext-screenshot.spanish.png differ diff --git a/apps/slidingtext/slidingtext.dtfmt.js b/apps/slidingtext/slidingtext.dtfmt.js index 266ed0b35..2543610c1 100644 --- a/apps/slidingtext/slidingtext.dtfmt.js +++ b/apps/slidingtext/slidingtext.dtfmt.js @@ -3,14 +3,20 @@ class DateFormatter { * A pure virtual class which all the other date formatters will * inherit from. * The name will be used to declare the date format when selected - * and the date formatDate methid will return the time formated + * and the date formatDate method will return the time formated * to the lines of text on the screen */ - name(){return "no name";} - shortName(){return "no short name"} - formatDate(date){ - return ["no","date","defined"]; - } + formatDate(date){ return ["no","date","defined"]; } + + /** + * returns a map of the different row types + */ + defaultRowTypes(){} + + /** + * returns a list of row definitions (1 definition can cover m + */ + defaultRowDefs(){ return [];} } module.exports = DateFormatter; \ No newline at end of file diff --git a/apps/slidingtext/slidingtext.js b/apps/slidingtext/slidingtext.js index 2f24e5596..47a24ea6a 100644 --- a/apps/slidingtext/slidingtext.js +++ b/apps/slidingtext/slidingtext.js @@ -5,18 +5,18 @@ */ const color_schemes = [ + { + name: "black", + background : [0.0,0.0,0.0], + main_bar: [1.0,0.0,0.0], + other_bars: [0.9,0.9,0.9], + }, { name: "white", background : [1.0,1.0,1.0], main_bar: [0.0,0.0,0.0], other_bars: [0.1,0.1,0.1], }, - { - name: "black", - background : [0.0,0.0,0.0], - main_bar: [1.0,1.0,1.0], - other_bars: [0.9,0.9,0.9], - }, { name: "red", background : [1.0,0.0,0.0], @@ -31,14 +31,14 @@ const color_schemes = [ }, { name: "purple", - background : [1.0,0.0,1.0], + background : [0.3,0.0,0.6], main_bar: [1.0,1.0,0.0], other_bars: [0.85,0.85,0.85] }, { name: "blue", - background : [0.4,0.7,1.0], - main_bar: [1.0,1.0,1.0], + background : [0.1,0.2,1.0], + main_bar: [1.0,1.0,0.0], other_bars: [0.9,0.9,0.9] } ]; @@ -66,17 +66,12 @@ let command_stack_high_priority = []; let command_stack_low_priority = []; function next_command(){ - command = command_stack_high_priority.pop(); + var command = command_stack_high_priority.pop(); if(command == null){ - //console.log("Low priority command"); command = command_stack_low_priority.pop(); - } else { - //console.log("High priority command"); } if(command != null){ command.call(); - } else { - //console.log("no command"); } } @@ -102,7 +97,9 @@ class ShiftText { constructor(x,y,txt,font_name, font_size,speed_x,speed_y,freq_millis, color, - bg_color){ + bg_color, + row_context, + rotation){ this.x = x; this.tgt_x = x; this.init_x = x; @@ -118,17 +115,15 @@ class ShiftText { this.freq_millis = freq_millis; this.color = color; this.bg_color = bg_color; + this.row_context = row_context; + this.rotation = rotation; this.finished_callback=null; this.timeoutId = null; } - setColor(color){ - this.color = color; - } - setBgColor(bg_color){ - this.bg_color = bg_color; - } + getRowContext(){ return this.row_context;} + setColor(color){ this.color = color; } + setBgColor(bg_color){ this.bg_color = bg_color; } reset(hard_reset) { - //console.log("reset"); this.hide(); this.x = this.init_x; this.y = this.init_y; @@ -141,13 +136,13 @@ class ShiftText { } } show() { - g.setFontAlign(-1,-1,0); + g.setFontAlign(-1,-1,this.rotation); g.setFont(this.font_name,this.font_size); g.setColor(this.color[0],this.color[1],this.color[2]); g.drawString(this.txt, this.x, this.y); } hide(){ - g.setFontAlign(-1,-1,0); + g.setFontAlign(-1,-1,this.rotation); g.setFont(this.font_name,this.font_size); //console.log("bgcolor:" + this.bg_color); g.setColor(this.bg_color[0],this.bg_color[1],this.bg_color[2]); @@ -188,6 +183,36 @@ class ShiftText { this.tgt_y = new_y; this._doMove(); } + scrollInFromBottom(txt,to_y){ + if(to_y == null) + to_y = this.init_y; + + this.setTextPosition(txt, this.init_x, g.getHeight()*2); + this.moveTo(this.init_x,to_y); + } + scrollInFromLeft(txt,to_x){ + if(to_x == null) + to_x = this.init_x; + + this.setTextPosition(txt, -txt.length * this.font_size - this.font_size, this.init_y); + this.moveTo(to_x,this.init_y); + } + scrollInFromRight(txt,to_x){ + if(to_x == null) + to_x = this.init_x; + + this.setTextPosition(txt, g.getWidth() + this.font_size, this.init_y); + this.moveTo(to_x,this.init_y); + } + scrollOffToLeft(){ + this.moveTo(-this.txt.length * this.font_size, this.init_y); + } + scrollOffToRight(){ + this.moveTo(g.getWidth() + this.font_size, this.init_y); + } + scrollOffToBottom(){ + this.moveTo(this.init_x,g.getHeight()*2); + } onFinished(finished_callback){ this.finished_callback = finished_callback; } @@ -230,135 +255,234 @@ class ShiftText { if(!finished){ this.timeoutId = setTimeout(this._doMove.bind(this), this.freq_millis); } else if(this.finished_callback != null){ - //console.log("finished - calling:" + this.finished_callback); this.finished_callback.call(); this.finished_callback = null; } } } -const CLOCK_TEXT_SPEED_X = 10; -// a list of display rows -let row_displays; -function setRowDisplays(y, heights) { - var cols = [ - main_color(), other_color(), other_color(), other_color(), main_color() - ]; - row_displays = []; - for (var i=0;i200)? 1 : 2; } -if (bangleVersion()<2) - setRowDisplays(50, [40,30,30,30,40]); -else - setRowDisplays(34, [35,25,25,25,35]); +let row_displays; +function initDisplay(settings) { + if(row_displays != null){ + return; + } + if(settings == null){ + settings = {}; + } + var row_types = { + large: { + color: 'major', + speed: 'medium', + angle_to_horizontal: 0, + scroll_off: ['left'], + scroll_in: ['right'], + size: 'large' + }, + medium: { + color: 'minor', + speed: 'slow', + angle_to_horizontal: 0, + scroll_off: ['left'], + scroll_in: ['right'], + size: 'medium' + }, + small: { + color: 'minor', + speed: 'superslow', + angle_to_horizontal: 0, + scroll_off: ['left'], + scroll_in: ['right'], + size: 'small' + } + }; + + function mergeMaps(map1,map2){ + if(map2 == null){ + return; + } + Object.keys(map2).forEach(key => { + if(map1.hasOwnProperty(key)){ + map1[key] = mergeObjects(map1[key], map2[key]); + } else { + map1[key] = map2[key]; + } + }); + } + + function mergeObjects(obj1, obj2){ + const result = {}; + Object.keys(obj1).forEach(key => result[key] = (obj2.hasOwnProperty(key))? obj2[key] : obj1[key]); + return result; + } + + const row_type_overide = date_formatter.defaultRowTypes(); + mergeMaps(row_types,row_type_overide); + mergeMaps(row_types,settings.row_types); + var row_defs = (settings.row_defs != null && settings.row_defs.length > 0)? + settings.row_defs : date_formatter.defaultRowDefs(); + + var heights = { + vvsmall: [15,13], + vsmall: [20,15], + ssmall: [22,17], + small: [25,20], + msmall: [29,22], + medium: [40,25], + mlarge: [45,35], + large: [50,40], + vlarge: [60,50], + slarge: [110,90] + }; + + var rotations = { + 0: 0, + 90: 3, + 180: 2, + 270: 1, + }; + + var speeds = { + fast: 20, + medium: 10, + slow: 5, + vslow: 2, + superslow: 1 + }; + + function create_row_type(row_type, row_def){ + const speed = speeds[row_type.speed]; + const rotation = rotations[row_type.angle_to_horizontal]; + const height = heights[row_type.size]; + const scroll_ins = []; + if(row_type.scroll_in.includes('left')){ + scroll_ins.push((row_display,txt)=> row_display.scrollInFromLeft(txt)); + } + if(row_type.scroll_in.includes('right')){ + scroll_ins.push((row_display,txt)=> row_display.scrollInFromRight(txt)); + } + if(row_type.scroll_in.includes('up')){ + scroll_ins.push((row_display,txt)=> row_display.scrollInFromBottom(txt)); + } + var scroll_in; + if(scroll_ins.length === 0){ + scroll_in = (row_display,txt)=> row_display.scrollInFromLeft(txt); + } else if(scroll_ins.length === 1){ + scroll_in = scroll_ins[0]; + } else { + scroll_in = (row_display,txt) =>{ + const idx = (Math.random() * scroll_ins.length) | 0; + return scroll_ins[idx](row_display,txt); + }; + } + + const scroll_offs = []; + if(row_type.scroll_off.includes('left')){ + scroll_offs.push((row_display)=> row_display.scrollOffToLeft()); + } + if(row_type.scroll_off.includes('right')){ + scroll_offs.push((row_display)=> row_display.scrollOffToRight()); + } + if(row_type.scroll_off.includes('down')){ + scroll_offs.push((row_display)=> row_display.scrollOffToBottom()); + } + var scroll_off; + if(scroll_offs.length === 0){ + scroll_off = (row_display)=> row_display.scrollOffToLeft(); + } else if(scroll_offs.length === 1){ + scroll_off = scroll_offs[0]; + } else { + scroll_off = (row_display) =>{ + var idx = (Math.random() * scroll_off.length) | 0; + return scroll_offs[idx](row_display); + }; + } + + var text_formatter = (txt)=>txt; + const SPACES = ' '; + if(row_def.hasOwnProperty("alignment")){ + const alignment = row_def.alignment; + if(alignment.startsWith("centre")){ + const padding = parseInt(alignment.split("-")[1]); + if(padding > 0){ + text_formatter = (txt) => { + const front_spaces = (padding - txt.length)/2 | 0; + return front_spaces > 0? SPACES.substring(0,front_spaces + 1) + txt : txt; + }; + } + } + } + + const version = bangleVersion() - 1; + const Y_RESERVED = 20; + return { + row_speed: speed, + row_height: height[version], + row_rotation: rotation, + x: (row_no) => row_def.init_coords[0] * g.getWidth() + row_def.row_direction[0] * height[version] * row_no, + y: (row_no) => Y_RESERVED + row_def.init_coords[1] * (g.getHeight() - Y_RESERVED) + row_def.row_direction[1] * height[version] * row_no, + scroll_in: scroll_in, + scroll_off: scroll_off, + fg_color: () => (row_type.color === 'major')? main_color(): other_color(), + row_text_formatter : text_formatter + }; + } + row_displays = []; + row_defs.forEach(row_def =>{ + const row_type = create_row_type(row_types[row_def.type],row_def); + // we now create the number of rows specified of that type + for(var row_no=0; row_no row_displays.length){ + if(color_scheme_index >= color_schemes.length){ color_scheme_index = 0; } - setColorScheme(color_schemes[color_scheme_index]); - reset_clock(true); - draw_clock(); + updateColorScheme(); + resetClock(true); + drawClock(); } -function setColorScheme(color_scheme){ - setColor(color_scheme.main_bar, - color_scheme.other_bars, - color_scheme.background); -} - -function setColor(main_color,other_color,bg_color){ - row_displays[0].setColor(main_color); - row_displays[0].setBgColor(bg_color); - for(var i=1; i= date_formatters.length){ - date_formatter_idx = 0; - } - console.log("changing to formatter " + date_formatter_idx); - date_formatter = date_formatters[date_formatter_idx]; - reset_clock(true); - draw_clock(); - command_stack_high_priority.unshift( - function() { - //console.log("move in new:" + txt); - // first select the top or bottom to display the formatter name - // We choose the first spare row without text - var format_name_display = row_displays[row_displays.length - 1]; - if (format_name_display.txt != '') { - format_name_display = row_displays[0]; - } - if (format_name_display.txt != ''){ - return; - } - format_name_display.speed_x = 3; - format_name_display.onFinished(function(){ - format_name_display.speed_x = CLOCK_TEXT_SPEED_X; - console.log("return speed to:" + format_name_display.speed_x) - next_command(); - }); - format_name_display.setTextXPosition(date_formatter.name(),220); - format_name_display.moveToX(-date_formatter.name().length * format_name_display.font_size); - } - ); - -} - -var DISPLAY_TEXT_X = 20; -function reset_clock(hard_reset){ +function resetClock(hard_reset){ console.log("reset_clock hard_reset:" + hard_reset); - setColorScheme(color_schemes[color_scheme_index]); + updateColorScheme(); if(!hard_reset && last_draw_time != null){ // If its not a hard reset then we want to reset the // rows set to the last time. If the last time is too long @@ -366,15 +490,14 @@ function reset_clock(hard_reset){ // In this way the watch wakes by scrolling // off the last time and scroll on the new time var reset_time = last_draw_time; - var last_minute_millis = Date.now() - 60000; + const last_minute_millis = Date.now() - 60000; if(reset_time.getTime() < last_minute_millis){ reset_time = display_time(new Date(last_minute_millis)); } - var rows = date_formatter.formatDate(reset_time); + const rows = date_formatter.formatDate(reset_time); for (var i = 0; i < rows.length; i++) { row_displays[i].hide(); - row_displays[i].speed_x = CLOCK_TEXT_SPEED_X; - row_displays[i].x = DISPLAY_TEXT_X; + row_displays[i].x = row_displays[i].init_x; row_displays[i].y = row_displays[i].init_y; if(row_displays[i].timeoutId != null){ clearTimeout(row_displays[i].timeoutId); @@ -384,12 +507,8 @@ function reset_clock(hard_reset){ } } else { // do a hard reset and clear everything out - for (var i = 0; i < row_displays.length; i++) { - row_displays[i].speed_x = CLOCK_TEXT_SPEED_X; - row_displays[i].reset(hard_reset); - } + row_displays.forEach(row_display => row_display.reset(hard_reset)); } - reset_commands(); } @@ -405,13 +524,13 @@ function display_time(date){ } } -function draw_clock(){ +function drawClock(){ var date = new Date(); // we don't want the time to be displayed // and then immediately be trigger another time if(last_draw_time != null && - Date.now() - last_draw_time.getTime() < next_minute_boundary_secs * 1000 && + date.getTime() - last_draw_time.getTime() < next_minute_boundary_secs * 1000 && has_commands() ){ console.log("skipping draw clock"); return; @@ -420,65 +539,54 @@ function draw_clock(){ } reset_commands(); date = display_time(date); - console.log("draw_clock:" + last_draw_time.toISOString() + " display:" + date.toISOString()); + const mem = process.memory(false); + console.log("draw_clock:" + last_draw_time.toISOString() + " display:" + date.toISOString() + + " memory:" + mem.usage / mem.total); - var rows = date_formatter.formatDate(date); - var display; + const rows = date_formatter.formatDate(date); for (var i = 0; i < rows.length; i++) { - display = row_displays[i]; - var txt = rows[i]; - //console.log(i + "->" + txt); - display_row(display,txt); + const display = row_displays[i]; + if(display != null){ + const txt = display.getRowContext().row_text_formatter(rows[i]); + display_row(display,txt); + } } // If the dateformatter has not returned enough - // rows then treat the reamining rows as empty + // rows then treat the remaining rows as empty for (var j = i; j < row_displays.length; j++) { - display = row_displays[j]; - //console.log(i + "->''(empty)"); + const display = row_displays[j]; display_row(display,''); } next_command(); - //console.log(date); } function display_row(display,txt){ if(display == null) { - console.log("no display for text:" + txt) return; } - if(display.txt == null || display.txt == ''){ - if(txt != '') { - command_stack_high_priority.unshift( - function () { - //console.log("move in new:" + txt); + if(display.txt == null || display.txt === ''){ + if(txt !== '') { + command_stack_high_priority.unshift(()=>{ display.onFinished(next_command); - display.setTextXPosition(txt, 240); - display.moveToX(DISPLAY_TEXT_X); + display.getRowContext().scroll_in(display,txt); } ); } - } else if(txt != display.txt && display.txt != null){ - command_stack_high_priority.push( - function(){ - //console.log("move out:" + txt); + } else if(txt !== display.txt && display.txt != null){ + command_stack_high_priority.push(()=>{ display.onFinished(next_command); - display.moveToX(-display.txt.length * display.font_size); + display.getRowContext().scroll_off(display); } ); - command_stack_low_priority.push( - function(){ - //console.log("move in:" + txt); + command_stack_low_priority.push(() => { display.onFinished(next_command); - display.setTextXPosition(txt,240); - display.moveToX(DISPLAY_TEXT_X); + display.getRowContext().scroll_in(display,txt); } ); } else { - command_stack_high_priority.push( - function(){ - //console.log("move in2:" + txt); - display.setTextXPosition(txt,DISPLAY_TEXT_X); + command_stack_high_priority.push(() => { + display.setTextPosition(txt,display.init_x, display.init_y); next_command(); } ); @@ -489,26 +597,92 @@ function display_row(display,txt){ * called from load_settings on startup to * set the color scheme to named value */ -function set_colorscheme(colorscheme_name){ +function setColorScheme(colorscheme_name){ console.log("setting color scheme:" + colorscheme_name); for (var i=0; i < color_schemes.length; i++) { - if(color_schemes[i].name == colorscheme_name){ + if(color_schemes[i].name === colorscheme_name){ color_scheme_index = i; - console.log("match"); - setColorScheme(color_schemes[color_scheme_index]); + updateColorScheme(); break; } } } -function set_dateformat(dateformat_name){ - console.log("setting date format:" + dateformat_name); - for (var i=0; i < date_formatters.length; i++) { - if(date_formatters[i].shortName() == dateformat_name){ - date_formatter_idx = i; - date_formatter = date_formatters[date_formatter_idx]; - console.log("match"); +var date_formatter; +function setDateformat(shortname){ + /** + * Demonstration Date formatter so that we can see the + * clock working in the emulator + */ + class DigitDateTimeFormatter { + constructor() {} + + format00(num){ + const value = (num | 0); + if(value > 99 || value < 0) + throw "must be between in range 0-99"; + if(value < 10) + return "0" + value.toString(); + else + return value.toString(); } + + formatDate(now){ + const hours = now.getHours() ; + const time_txt = this.format00(hours) + ":" + this.format00(now.getMinutes()); + const date_txt = require('locale').dow(now,1) + " " + this.format00(now.getDate()); + const month_txt = require('locale').month(now); + return [time_txt, date_txt, month_txt]; + } + + defaultRowTypes(){ + return { + large: { + scroll_off: ['left', 'right', 'down'], + scroll_in: ['left', 'right', 'up'], + size: 'vlarge' + }, + small: { + angle_to_horizontal: 90, + scroll_off: ['down'], + scroll_in: ['up'], + size: 'vvsmall' + } + }; + } + + defaultRowDefs() { + return [ + { + type: 'large', + row_direction: [0.0,1.0], + init_coords: [0.1,0.35], + rows: 1 + }, + { + type: 'small', + row_direction: [1.0,0], + init_coords: [0.85,0.99], + rows: 2 + } + ]; + } + } + console.log("setting date format:" + shortname); + try { + if (date_formatter == null) { + if(shortname === "default"){ + date_formatter = new DigitDateTimeFormatter(); + } else { + const date_formatter_class = require("slidingtext.locale." + shortname + ".js"); + date_formatter = new date_formatter_class(); + } + } + } catch(e){ + console.log("not loaded:" + shortname); + } + if(date_formatter == null){ + date_formatter = new DigitDateTimeFormatter(); } } @@ -517,64 +691,44 @@ const PREFERENCE_FILE = "slidingtext.settings.json"; /** * Called on startup to set the watch to the last preference settings */ -function load_settings(){ - var setScheme = false; - try{ - var settings = require("Storage").readJSON(PREFERENCE_FILE); - if(settings != null){ - console.log("loaded:" + JSON.stringify(settings)); - if(settings.color_scheme != null){ - set_colorscheme(settings.color_scheme); - setScheme = true; - } - if(settings.date_format != null){ - set_dateformat(settings.date_format); - } - if(settings.enable_live_controls == null){ - settings.enable_live_controls = (bangleVersion() <= 1); - } - enable_live_controls = settings.enable_live_controls; - } else { - console.log("no settings to load"); - enable_live_controls = (bangleVersion() <= 1); +function loadSettings() { + try { + const settings = Object.assign({}, + require('Storage').readJSON(PREFERENCE_FILE, true) || {}); + if (settings.date_formatter == null) { + settings.date_formatter = "en"; } + console.log("loaded settings:" + JSON.stringify(settings)); + setDateformat(settings.date_formatter); + initDisplay(settings); + if (settings.color_scheme != null) { + setColorScheme(settings.color_scheme); + } else { + setColorScheme("black"); + } + if (settings.enable_live_controls == null) { + settings.enable_live_controls = (bangleVersion() <= 1); + } + enable_live_controls = settings.enable_live_controls; console.log("enable_live_controls=" + enable_live_controls); - } catch(e){ + } catch (e) { console.log("failed to load settings:" + e); } // just set up as default - if (!setScheme) - setColorScheme(color_schemes[color_scheme_index]); -} - -/** - * Called on button press to save down the last preference settings - */ -function save_settings(){ - var settings = { - date_format : date_formatter.shortName(), - color_scheme : color_schemes[color_scheme_index].name, - enable_live_controls: enable_live_controls - }; - console.log("saving:" + JSON.stringify(settings)); - require("Storage").writeJSON(PREFERENCE_FILE,settings); -} - -function button1pressed() { - console.log("button1pressed"); - if (enable_live_controls) { - changeFormatter(); - save_settings(); + if (row_displays === undefined) { + setDateformat("default"); + initDisplay(); + updateColorScheme(); } + const mem = process.memory(true); + console.log("init complete memory:" + mem.usage / mem.total); } function button3pressed() { - console.log("button3pressed"); if (enable_live_controls) { nextColorTheme(); - reset_clock(true); - draw_clock(); - save_settings(); + resetClock(true); + drawClock(); } } @@ -589,12 +743,11 @@ function clearTimers(){ } function startTimers(){ - var date = new Date(); - var secs = date.getSeconds(); - var nextMinuteStart = 60 - secs; - //console.log("scheduling clock draw in " + nextMinuteStart + " seconds"); + const date = new Date(); + const secs = date.getSeconds(); + const nextMinuteStart = 60 - secs; setTimeout(scheduleDrawClock,nextMinuteStart * 1000); - draw_clock(); + drawClock(); } /** @@ -613,17 +766,17 @@ function scheduleDrawClock(){ if (Bangle.isLCDOn()) { console.log("schedule draw of clock"); intervalRef = setInterval(() => { - if (!shouldRedraw()) { - console.log("draw clock callback - skipped redraw"); - } else { - console.log("draw clock callback"); - draw_clock() - } - }, 60 * 1000 + if (!shouldRedraw()) { + console.log("draw clock callback - skipped redraw"); + } else { + console.log("draw clock callback"); + drawClock(); + } + }, 60 * 1000 ); if (shouldRedraw()) { - draw_clock(); + drawClock(); } else { console.log("scheduleDrawClock - skipped redraw"); } @@ -636,20 +789,19 @@ Bangle.on('lcdPower', (on) => { if (on) { console.log("lcdPower: on"); Bangle.drawWidgets(); - reset_clock(false); + resetClock(false); startTimers(); } else { console.log("lcdPower: off"); - reset_clock(false); + resetClock(false); clearTimers(); } }); g.clear(); -load_settings(); +loadSettings(); // Show launcher when button pressed Bangle.setUI("clockupdown", d=>{ - if (d<0) button1pressed(); if (d>0) button3pressed(); }); Bangle.loadWidgets(); diff --git a/apps/slidingtext/slidingtext.locale.de.js b/apps/slidingtext/slidingtext.locale.de.js index da5c2f01d..7be61e965 100644 --- a/apps/slidingtext/slidingtext.locale.de.js +++ b/apps/slidingtext/slidingtext.locale.de.js @@ -1,95 +1,43 @@ -var DateFormatter = require("slidingtext.dtfmt.js"); - -const germanNumberStr = [ ["NULL",""], // 0 - ["EINS",""], // 1 - ["ZWEI",""], //2 - ["DREI",''], //3 - ["VIER",''], //4 - ["FÜNF",''], //5 - ["SECHS",''], //6 - ["SIEBEN",''], //7 - ["ACHT",''], //8 - ["NEUN",''], // 9, - ["ZEHN",''], // 10 - ["ELF",''], // 11, - ["ZWÖLF",''], // 12 - ["DREI",'ZEHN'], // 13 - ["VIER",'ZEHN'], // 14 - ["FÜNF",'ZEHN'], // 15 - ["SECH",'ZEHN'], // 16 - ["SIEB",'ZEHN'], // 17 - ["ACHT",'ZEHN'], // 18 - ["NEUN",'ZEHN'], // 19 -]; - -const germanTensStr = ["NULL",//0 - "ZEHN",//10 - "ZWANZIG",//20 - "DREIßIG",//30 - "VIERZIG",//40 - "FÜNFZIG",//50 - "SECHZIG"//60 -] - -const germanUnit = ["",//0 - "EINUND",//1 - "ZWEIUND",//2 - "DREIUND",//3 - "VIERUND", //4 - "FÜNFUND", //5 - "SECHSUND", //6 - "SIEBENUND", //7 - "ACHTUND", //8 - "NEUNUND" //9 -] - -function germanHoursToText(hours){ - hours = hours % 12; - if(hours == 0){ - hours = 12; - } - return germanNumberStr[hours][0]; -} - -function germanMinsToText(mins) { - if (mins < 20) { - return germanNumberStr[mins]; - } else { - var tens = (mins / 10 | 0); - var word1 = germanTensStr[tens]; - var remainder = mins - tens * 10; - var word2 = germanUnit[remainder]; - return [word2, word1]; - } -} +const DateFormatter = require("slidingtext.dtfmt.js"); +const germanHoursToText = require("slidingtext.utils.de.js").germanHoursToText; +const germanMinsToText = require("slidingtext.utils.de.js").germanMinsToText; +/** + * German 12 hour clock + */ class GermanDateFormatter extends DateFormatter { - constructor() { super();} - name(){return "German";} - shortName(){return "de"} + constructor() { + super(); + } formatDate(date){ - var mins = date.getMinutes(); - var hourOfDay = date.getHours(); - var hours = germanHoursToText(hourOfDay); - //console.log('hourOfDay->' + hourOfDay + ' hours text->' + hours) - // Deal with the special times first - if(mins == 0){ - var hours = germanHoursToText(hourOfDay); + const mins = date.getMinutes(); + const hourOfDay = date.getHours(); + const hours = germanHoursToText(hourOfDay); + if(mins === 0){ return [hours,"UHR", "","",""]; - } /*else if(mins == 30){ - var hours = germanHoursToText(hourOfDay+1); - return ["", "", "HALB","", hours]; - } else if(mins == 15){ - var hours = germanHoursToText(hourOfDay); - return ["", "", "VIERTEL", "NACH",hours]; - } else if(mins == 45) { - var hours = germanHoursToText(hourOfDay+1); - return ["", "", "VIERTEL", "VOR",hours]; - } */ else { - var mins_txt = germanMinsToText(mins); + } else { + const mins_txt = germanMinsToText(mins); return [hours, "UHR", mins_txt[0],mins_txt[1]]; } } + defaultRowTypes(){ return {};} + + defaultRowDefs(){ + return [ + { + type: 'large', + init_coords: [0.05,0.1], + row_direction: [0.0,1.0], + rows: 1 + }, + { + type: 'medium', + init_coords: [0.05,0.4], + row_direction: [0.0,1.0], + rows: 3 + } + ]; + } } module.exports = GermanDateFormatter; diff --git a/apps/slidingtext/slidingtext.locale.de2.js b/apps/slidingtext/slidingtext.locale.de2.js new file mode 100644 index 000000000..a4c8c2fa6 --- /dev/null +++ b/apps/slidingtext/slidingtext.locale.de2.js @@ -0,0 +1,49 @@ +const DateFormatter = require("slidingtext.dtfmt.js"); +const german24HoursToText = require("slidingtext.utils.de.js").german24HoursToText; +const germanMinsToText = require("slidingtext.utils.de.js").germanMinsToText; + +/** + * German 24 hour clock + */ +class German24HourDateFormatter extends DateFormatter { + constructor() { + super(); + } + formatDate(date){ + const mins = date.getMinutes(); + const hourOfDay = date.getHours(); + const hours = german24HoursToText(hourOfDay); + const display_hours = (hours[1] === '')? ["", hours[0]] : hours; + if(mins === 0){ + return [display_hours[0],display_hours[1],"UHR", "","",""]; + } else { + const mins_txt = germanMinsToText(mins); + + return [display_hours[0],display_hours[1], "UHR", mins_txt[0],mins_txt[1]]; + } + } + defaultRowTypes(){ return { + large:{ + size: 'mlarge' + } + };} + + defaultRowDefs(){ + return [ + { + type: 'large', + init_coords: [0.05,0.06], + row_direction: [0.0,1.0], + rows: 2 + }, + { + type: 'medium', + init_coords: [0.05,0.5], + row_direction: [0.0,1.0], + rows: 3 + } + ]; + } +} + +module.exports = German24HourDateFormatter; diff --git a/apps/slidingtext/slidingtext.locale.dgt.js b/apps/slidingtext/slidingtext.locale.dgt.js new file mode 100644 index 000000000..446a4cd50 --- /dev/null +++ b/apps/slidingtext/slidingtext.locale.dgt.js @@ -0,0 +1,63 @@ +const Locale = require('locale'); + +class DigitDateTimeFormatter { + constructor() {} + + format00(num){ + const value = (num | 0); + if(value > 99 || value < 0) + throw "must be between in range 0-99"; + if(value < 10) + return "0" + value.toString(); + else + return value.toString(); + } + + formatDate(now){ + const hours = now.getHours() ; + const time_txt = this.format00(hours) + ":" + this.format00(now.getMinutes()); + const date_txt = Locale.dow(now,1) + " " + this.format00(now.getDate()); + return [time_txt[0], time_txt[1],time_txt[2], time_txt[3],time_txt[4],date_txt]; + } + + defaultRowTypes(){ + return { + large: { + scroll_off: ['down'], + scroll_in: ['up'], + size: 'vlarge', + speed: 'medium' + }, + small: { + angle_to_horizontal: 0, + scroll_off: ['left'], + scroll_in: ['right'], + } + }; + } + + defaultRowDefs() { + return [ + { + type: 'large', + row_direction: [0.7,0.0], + init_coords: [0.1,0.35], + rows: 3 + }, + { + type: 'large', + row_direction: [0.7,0.0], + init_coords: [0.6,0.35], + rows: 2 + }, + { + type: 'small', + row_direction: [0.0,1.0], + init_coords: [0.1,0.05], + rows: 1 + } + ]; + } +} + +module.exports = DigitDateTimeFormatter; \ No newline at end of file diff --git a/apps/slidingtext/slidingtext.locale.en.js b/apps/slidingtext/slidingtext.locale.en.js index 6414ef7a9..545cc9f09 100644 --- a/apps/slidingtext/slidingtext.locale.en.js +++ b/apps/slidingtext/slidingtext.locale.en.js @@ -1,15 +1,55 @@ -var DateFormatter = require("slidingtext.dtfmt.js"); +const DateFormatter = require("slidingtext.dtfmt.js"); const hoursToText = require("slidingtext.utils.en.js").hoursToText; const numberToText = require("slidingtext.utils.en.js").numberToText; +const dayOfWeek = require("slidingtext.utils.en.js").dayOfWeek; +const numberToDayNumberText = require("slidingtext.utils.en.js").numberToDayNumberText; +const monthToText = require("slidingtext.utils.en.js").monthToText; class EnglishDateFormatter extends DateFormatter { - constructor() { super();} - name(){return "English";} - shortName(){return "en"} + constructor() { + super(); + } formatDate(date){ - var hours_txt = hoursToText(date.getHours()); - var mins_txt = numberToText(date.getMinutes()); - return [hours_txt,mins_txt[0],mins_txt[1]]; + const hours_txt = hoursToText(date.getHours()); + const mins_txt = numberToText(date.getMinutes()); + const day_of_week = dayOfWeek(date); + const date_txt = numberToDayNumberText(date.getDate()).join(' '); + const month = monthToText(date); + return [hours_txt,mins_txt[0],mins_txt[1],day_of_week,date_txt,month]; + } + defaultRowTypes() { + return { + small: {size: 'ssmall'} + }; + } + + defaultRowDefs(){ + const row_defs = [ + { + type: 'large', + init_coords: [0.05,0.07], + row_direction: [0.0,1.0], + rows: 1 + }, + { + type: 'medium', + init_coords: [0.05,0.31], + row_direction: [0.0,1.0], + rows: 2 + } + ]; + const bangleVersion = (g.getHeight()>200)? 1 : 2; + if(bangleVersion > 1){ + row_defs.push( + { + type: 'small', + init_coords: [0.05,0.75], + row_direction: [0.0,1.0], + rows: 2 + } + ) + } + return row_defs; } } diff --git a/apps/slidingtext/slidingtext.locale.en2.js b/apps/slidingtext/slidingtext.locale.en2.js index d7d7ff6a8..ac2ae02ff 100644 --- a/apps/slidingtext/slidingtext.locale.en2.js +++ b/apps/slidingtext/slidingtext.locale.en2.js @@ -1,4 +1,6 @@ -var DateFormatter = require("slidingtext.dtfmt.js"); +const DateFormatter = require("slidingtext.dtfmt.js"); +const dayOfWeekShort = require("slidingtext.utils.en.js").dayOfWeekShort; +const numberToDayNumberText = require("slidingtext.utils.en.js").numberToDayNumberText; const hoursToText = require("slidingtext.utils.en.js").hoursToText; const numberToText = require("slidingtext.utils.en.js").numberToText; @@ -6,24 +8,24 @@ class EnglishTraditionalDateFormatter extends DateFormatter { constructor() { super(); } - name(){return "English (Traditional)";} - shortName(){return "en2"} formatDate(date){ - var mins = date.getMinutes(); + const day_of_week = dayOfWeekShort(date); + const date_txt = numberToDayNumberText(date.getDate()).join(' '); + const mins = date.getMinutes(); var hourOfDay = date.getHours(); if(mins > 30){ hourOfDay += 1; } - var hours = hoursToText(hourOfDay); + const hours = hoursToText(hourOfDay); // Deal with the special times first - if(mins == 0){ - return [hours,"", "O'","CLOCK"]; - } else if(mins == 30){ - return ["","HALF", "PAST", "", hours]; - } else if(mins == 15){ - return ["","QUARTER", "PAST", "", hours]; - } else if(mins == 45) { - return ["", "QUARTER", "TO", "", hours]; + if(mins === 0){ + return [hours,"", "O'","CLOCK","", day_of_week, date_txt]; + } else if(mins === 30){ + return ["","HALF", "PAST", "", hours, day_of_week, date_txt]; + } else if(mins === 15){ + return ["","QUARTER", "PAST", "", hours, day_of_week, date_txt]; + } else if(mins === 45) { + return ["", "QUARTER", "TO", "", hours, day_of_week, date_txt]; } var mins_txt; var from_to; @@ -37,18 +39,57 @@ class EnglishTraditionalDateFormatter extends DateFormatter { from_to = "PAST"; mins_txt = numberToText(mins_value); } - if(mins_txt[1] != '') { - return ['', mins_txt[0], mins_txt[1], from_to, hours]; + + if(mins_txt[1] !== '') { + return ['', mins_txt[0], mins_txt[1], from_to, hours, day_of_week, date_txt]; } else { - if(mins_value % 5 == 0) { - return ['', mins_txt[0], from_to, '', hours]; - } else if(mins_value == 1){ - return ['', mins_txt[0], 'MINUTE', from_to, hours]; + if(mins_value % 5 === 0) { + return ['', mins_txt[0], from_to, '', hours, day_of_week, date_txt]; + } else if(mins_value === 1){ + return ['', mins_txt[0], 'MINUTE', from_to, hours, day_of_week, date_txt]; } else { - return ['', mins_txt[0], 'MINUTES', from_to, hours]; + return ['', mins_txt[0], 'MINUTES', from_to, hours, day_of_week, date_txt]; } } } + defaultRowTypes(){ + return { + small: { + speed: 'medium', + scroll_off: ['left','right'], + scroll_in: ['left','right'], + }, + large: { + speed: 'medium', + color: 'major', + scroll_off: ['left','right'], + scroll_in: ['left','right'] + } + }; + } + + defaultRowDefs(){ + return [ + { + type: 'large', + init_coords: [0.05,0.1], + row_direction: [0.0,1.0], + rows: 1 + }, + { + type: 'small', + init_coords: [0.05,0.35], + row_direction: [0.0,1.0], + rows: 3 + }, + { + type: 'large', + init_coords: [0.05,0.75], + row_direction: [0.0,1.0], + rows: 1 + } + ]; + } } module.exports = EnglishTraditionalDateFormatter; \ No newline at end of file diff --git a/apps/slidingtext/slidingtext.locale.es.js b/apps/slidingtext/slidingtext.locale.es.js index 62c68b64d..041497e04 100644 --- a/apps/slidingtext/slidingtext.locale.es.js +++ b/apps/slidingtext/slidingtext.locale.es.js @@ -34,7 +34,7 @@ const spanishNumberStr = [ ["ZERO"], // 0 function spanishHoursToText(hours){ hours = hours % 12; - if(hours == 0){ + if(hours === 0){ hours = 12; } return spanishNumberStr[hours][0]; @@ -45,34 +45,52 @@ function spanishMinsToText(mins){ } class SpanishDateFormatter extends DateFormatter { - constructor() { super();} - name(){return "Spanish";} - shortName(){return "es"} + constructor() { + super(); + } formatDate(date){ - var mins = date.getMinutes(); + const mins = date.getMinutes(); var hourOfDay = date.getHours(); if(mins > 30){ hourOfDay += 1; } - var hours = spanishHoursToText(hourOfDay); + const hours = spanishHoursToText(hourOfDay); //console.log('hourOfDay->' + hourOfDay + ' hours text->' + hours) // Deal with the special times first - if(mins == 0){ + if(mins === 0){ return [hours,"", "","",""]; - } else if(mins == 30){ + } else if(mins === 30){ return [hours, "Y", "MEDIA",""]; - } else if(mins == 15){ + } else if(mins === 15){ return [hours, "Y", "CUARTO",""]; - } else if(mins == 45) { + } else if(mins === 45) { return [hours, "MENOS", "CUARTO",""]; } else if(mins > 30){ - var mins_txt = spanishMinsToText(60-mins); + const mins_txt = spanishMinsToText(60-mins); return [hours, "MENOS", mins_txt[0],mins_txt[1]]; } else { - var mins_txt = spanishMinsToText(mins); + const mins_txt = spanishMinsToText(mins); return [hours, "Y", mins_txt[0],mins_txt[1]]; } } + defaultRowTypes(){ return {};} + + defaultRowDefs(){ + return [ + { + type: 'large', + init_coords: [0.05,0.1], + row_direction: [0.0,1.0], + rows: 1 + }, + { + type: 'medium', + init_coords: [0.05,0.4], + row_direction: [0.0,1.0], + rows: 3 + } + ]; + } } module.exports = SpanishDateFormatter; diff --git a/apps/slidingtext/slidingtext.locale.fr.js b/apps/slidingtext/slidingtext.locale.fr.js index d4c1dc9d6..6da0e232d 100644 --- a/apps/slidingtext/slidingtext.locale.fr.js +++ b/apps/slidingtext/slidingtext.locale.fr.js @@ -14,14 +14,14 @@ const frenchNumberStr = [ "ZERO", "UNE", "DEUX", "TROIS", "QUATRE", function frenchHoursToText(hours){ hours = hours % 12; - if(hours == 0){ + if(hours === 0){ hours = 12; } return frenchNumberStr[hours]; } function frenchHeures(hours){ - if(hours % 12 == 1){ + if(hours % 12 === 1){ return 'HEURE'; } else { return 'HEURES'; @@ -29,33 +29,33 @@ function frenchHeures(hours){ } class FrenchDateFormatter extends DateFormatter { - constructor() { super(); } - name(){return "French";} - shortName(){return "fr"} + constructor() { + super(); + } formatDate(date){ var hours = frenchHoursToText(date.getHours()); var heures = frenchHeures(date.getHours()); - var mins = date.getMinutes(); - if(mins == 0){ - if(hours == 0){ + const mins = date.getMinutes(); + if(mins === 0){ + if(hours === 0){ return ["MINUIT", "",""]; - } else if(hours == 12){ + } else if(hours === 12){ return ["MIDI", "",""]; } else { return [hours, heures,""]; } - } else if(mins == 30){ + } else if(mins === 30){ return [hours, heures,'ET DEMIE']; - } else if(mins == 15){ + } else if(mins === 15){ return [hours, heures,'ET QUART']; - } else if(mins == 45){ + } else if(mins === 45){ var next_hour = date.getHours() + 1; hours = frenchHoursToText(next_hour); heures = frenchHeures(next_hour); return [hours, heures,"MOINS",'LET QUART']; } if(mins > 30){ - var to_mins = 60-mins; + const to_mins = 60-mins; var mins_txt = frenchNumberStr[to_mins]; next_hour = date.getHours() + 1; hours = frenchHoursToText(next_hour); @@ -66,6 +66,30 @@ class FrenchDateFormatter extends DateFormatter { return [ hours, heures , mins_txt ]; } } + defaultRowTypes(){ + return { + small: { + speed: 'vslow' + } + }; + } + + defaultRowDefs(){ + return [ + { + type: 'large', + init_coords: [0.05,0.1], + row_direction: [0.0,1.0], + rows: 1 + }, + { + type: 'small', + init_coords: [0.05,0.4], + row_direction: [0.0,1.0], + rows: 3 + } + ]; + } } module.exports = FrenchDateFormatter; \ No newline at end of file diff --git a/apps/slidingtext/slidingtext.locale.hyb.js b/apps/slidingtext/slidingtext.locale.hyb.js new file mode 100644 index 000000000..aa47b349f --- /dev/null +++ b/apps/slidingtext/slidingtext.locale.hyb.js @@ -0,0 +1,80 @@ +const DateFormatter = require("slidingtext.dtfmt.js"); +const numberToText = require("slidingtext.utils.en.js").numberToText; +const dayOfWeek = require("slidingtext.utils.en.js").dayOfWeekShort; + +class EnglishDateFormatter extends DateFormatter { + constructor() { + super(); + } + + format00(num){ + const value = (num | 0); + if(value > 99 || value < 0) + throw "must be between in range 0-99"; + if(value < 10) + return "0" + value.toString(); + else + return value.toString(); + } + + formatDate(date){ + + const hours_txt = this.format00(date.getHours()); + const mins_txt = numberToText(date.getMinutes()); + const day_of_week = dayOfWeek(date); + const date_txt = day_of_week + " " + this.format00(date.getDate()); + return [hours_txt,mins_txt[0],mins_txt[1],date_txt]; + } + defaultRowTypes() { + return { + large: { + size: 'slarge', + scroll_off: ['left','down'], + scroll_in: ['up','left'], + }, + medium: { + size: 'msmall', + scroll_off: ['down'], + scroll_in: ['up'], + angle_to_horizontal: 90 + }, + small: { + size: 'ssmall', + scroll_off: ['left'], + scroll_in: ['left'], + } + }; + } + + defaultRowDefs(){ + const row_defs = [ + { + type: 'large', + init_coords: [0.05,0.35], + row_direction: [0.0,1.0], + rows: 1 + }, + { + type: 'medium', + init_coords: [0.68,0.95], + row_direction: [1.0,0.0], + angle_to_horizontal: 90, + rows: 2 + }]; + + const bangleVersion = (g.getHeight()>200)? 1 : 2; + if(bangleVersion > 1){ + row_defs.push( + { + type: 'small', + init_coords: [0.05, 0.1], + row_direction: [0.0, 1.0], + rows: 1 + } + ) + } + return row_defs; + } +} + +module.exports = EnglishDateFormatter; \ No newline at end of file diff --git a/apps/slidingtext/slidingtext.locale.jp.js b/apps/slidingtext/slidingtext.locale.jp.js index 0f6e46a21..b2f9106a2 100644 --- a/apps/slidingtext/slidingtext.locale.jp.js +++ b/apps/slidingtext/slidingtext.locale.jp.js @@ -1,4 +1,4 @@ -var DateFormatter = require("slidingtext.dtfmt.js"); +const DateFormatter = require("slidingtext.dtfmt.js"); /** * Japanese date formatting @@ -28,21 +28,21 @@ const japaneseMinuteStr = [ ["", "PUN"], function japaneseHoursToText(hours){ hours = hours % 12; - if(hours == 0){ + if(hours === 0){ hours = 12; } return japaneseHourStr[hours]; } function japaneseMinsToText(mins){ - if(mins == 0){ + if(mins === 0){ return ["",""]; - } else if(mins == 30) + } else if(mins === 30) return ["HAN",""]; else { - var units = mins % 10; - var mins_txt = japaneseMinuteStr[units]; - var tens = mins /10 | 0; + const units = mins % 10; + const mins_txt = japaneseMinuteStr[units]; + const tens = mins /10 | 0; if(tens > 0){ var tens_txt = tensPrefixStr[tens]; var minutes_txt; @@ -59,14 +59,32 @@ function japaneseMinsToText(mins){ } class JapaneseDateFormatter extends DateFormatter { - constructor() { super(); } - name(){return "Japanese (Romanji)";} - shortName(){return "jp"} + constructor() { + super(); + } formatDate(date){ - var hours_txt = japaneseHoursToText(date.getHours()); - var mins_txt = japaneseMinsToText(date.getMinutes()); + const hours_txt = japaneseHoursToText(date.getHours()); + const mins_txt = japaneseMinsToText(date.getMinutes()); return [hours_txt,"JI", mins_txt[0], mins_txt[1] ]; } + defaultRowTypes(){ return {}; } + + defaultRowDefs(){ + return [ + { + type: 'large', + init_coords: [0.05,0.1], + row_direction: [0.0,1.0], + rows: 1 + }, + { + type: 'medium', + init_coords: [0.05,0.4], + row_direction: [0.0,1.0], + rows: 3 + } + ]; + } } module.exports = JapaneseDateFormatter; \ No newline at end of file diff --git a/apps/slidingtext/slidingtext.settings.js b/apps/slidingtext/slidingtext.settings.js index d1006990e..e13c857fd 100644 --- a/apps/slidingtext/slidingtext.settings.js +++ b/apps/slidingtext/slidingtext.settings.js @@ -1,33 +1,151 @@ (function(back) { const PREFERENCE_FILE = "slidingtext.settings.json"; - var settings = Object.assign({}, + const settings = Object.assign({}, require('Storage').readJSON(PREFERENCE_FILE, true) || {}); + const bangleVersion = (g.getHeight()>200)? 1 : 2; // the screen controls are defaulted on for a bangle 1 and off for a bangle 2 if(settings.enable_live_controls == null){ - settings.enable_live_controls = (g.getHeight()> 200); + settings.enable_live_controls = bangleVersion < 2; } console.log("loaded:" + JSON.stringify(settings)); + const locale_mappings = (bangleVersion > 1)? { + 'english' : { date_formatter: 'en' }, + 'english alt': { + date_formatter: 'en', + row_types: { + large:{ + size: 'mlarge', + angle_to_horizontal: 90, + scroll_off: ['down'], + scroll_in: ['up'], + speed: 'vslow' + }, + medium: { + size: 'msmall', + scroll_off: ['right'], + scroll_in: ['right'], + }, + small: { + scroll_off: ['right'], + scroll_in: ['right'], + } + }, + row_defs: [ + { + type: 'large', + init_coords: [0.05,0.99], + row_direction: [1.0,0.0], + alignment: 'centre-6', + rows: 1 + }, + { + type: 'medium', + init_coords: [0.26,0.1], + row_direction: [0.0,1.0], + rows: 2 + }, + { + type: 'small', + init_coords: [0.26,0.65], + row_direction: [0.0,1.0], + rows: 3 + } + ] + }, + 'english2': { date_formatter: 'en2' }, + 'english2 alt': { date_formatter: 'en2', + row_types: { + vsmall: { + color: 'minor', + speed: 'superslow', + angle_to_horizontal: 0, + scroll_off: ['left'], + scroll_in: ['left'], + size: 'ssmall' + }, + small: { + scroll_off: ['left'], + scroll_in: ['left'] + }, + large: { + size: 'mlarge', + angle_to_horizontal: 90, + color: 'major', + scroll_off: ['down'], + scroll_in: ['up'] + } + }, + row_defs: [ + { + type: 'large', + init_coords: [0.8,0.99], + row_direction: [0.0,1.0], + alignment: 'centre-6', + rows: 1 + }, + { + type: 'small', + init_coords: [0.05,0.45], + row_direction: [0.0,1.0], + rows: 3 + }, + { + type: 'large', + init_coords: [0.8,0.99], + row_direction: [0.0,1.0], + alignment: 'centre-6', + rows: 1 + }, + { + type: 'vsmall', + init_coords: [0.05,0.1], + row_direction: [0.0,1.0], + rows: 2 + }, + ] + }, + 'french': { date_formatter:'fr'}, + 'german': { date_formatter: 'de'}, + 'german 24h': { date_formatter: 'de2'}, + 'spanish': { date_formatter: 'es'}, + 'japanese': { date_formatter: 'jp'}, + 'hybrid': { date_formatter: 'hyb'}, + 'digits': { date_formatter: 'dgt'}, + } : { + 'english' : { date_formatter: 'en' }, + 'french': { date_formatter:'fr'}, + 'german': { date_formatter: 'de'}, + 'spanish': { date_formatter: 'es'}, + 'japanese': { date_formatter: 'jp'}, + 'hybrid': { date_formatter: 'hyb'}, + 'digits': { date_formatter: 'dgt'}, + } - const LANGUAGES_FILE = "slidingtext.languages.json"; - const LANGUAGES_DEFAULT = ["en","en2"]; - var locales = null; - try { - locales = require("Storage").readJSON(LANGUAGES_FILE); - } catch(e) { - console.log("failed to load languages:" + e); - } - if(locales == null || locales.length == 0){ - locales = LANGUAGES_DEFAULT; - console.log("defaulting languages to locale:" + locales); - } + const locales = Object.keys(locale_mappings); function writeSettings() { + if(settings.date_format == null){ + settings.date_format = 'en'; + } + const styling = locale_mappings[settings.date_format]; + if(styling.date_formatter != null) + settings.date_formatter = styling.date_formatter; + + settings.row_types = {}; + if(styling.row_types != null) + settings.row_types = styling.row_types; + + settings.row_defs = []; + if(styling.row_defs != null) + settings.row_defs = styling.row_defs; + console.log("saving:" + JSON.stringify(settings)); + require('Storage').writeJSON(PREFERENCE_FILE, settings); } // Helper method which uses int-based menu item for set of string values - function stringItems(startvalue, writer, values) { + function stringItems(startvalue, writer, values, value_mapping) { return { value: (startvalue === undefined ? 0 : values.indexOf(startvalue)), format: v => values[v], @@ -36,7 +154,8 @@ wrap: true, step: 1, onchange: v => { - writer(values[v]); + const write_value = (value_mapping == null)? values[v] : value_mapping(values[v]); + writer(write_value); writeSettings(); } }; @@ -51,8 +170,8 @@ E.showMenu({ "" : { "title" : "Sliding Text" }, "< Back" : () => back(), - "Colour": stringInSettings("color_scheme", ["white", "black", "red","grey","purple","blue"]), - "Languages": stringInSettings("date_format", locales), + "Colour": stringInSettings("color_scheme", ["black","white", "red","grey","purple","blue"]), + "Style": stringInSettings("date_format", locales, (l)=>locale_mappings[l] ), "Live Control": { value: (settings.enable_live_controls !== undefined ? settings.enable_live_controls : true), format: v => v ? "On" : "Off", diff --git a/apps/slidingtext/slidingtext.utils.de.js b/apps/slidingtext/slidingtext.utils.de.js new file mode 100644 index 000000000..a240f8dd8 --- /dev/null +++ b/apps/slidingtext/slidingtext.utils.de.js @@ -0,0 +1,86 @@ +const germanNumberStr = [ ["NULL",""], // 0 + ["EINS",""], // 1 + ["ZWEI",""], //2 + ["DREI",""], //3 + ["VIER",""], //4 + ["FÜNF",""], //5 + ["SECHS",""], //6 + ["SIEBEN",""], //7 + ["ACHT",""], //8 + ["NEUN",""], // 9, + ["ZEHN",""], // 10 + ["ELF",""], // 11, + ["ZWÖLF",""], // 12 + ["DREI","ZEHN"], // 13 + ["VIER","ZEHN"], // 14 + ["FÜNF","ZEHN"], // 15 + ["SECH","ZEHN"], // 16 + ["SIEB","ZEHN"], // 17 + ["ACHT","ZEHN"], // 18 + ["NEUN","ZEHN"], // 19 + ["ZWANZIG",""], // 20 + ["EIN","UNDZWANZIG"], // 21 + ["ZWEI","UNDZWANZIG"], //22 + ["DREI","UNDZWANZIG"], // 23 + ["VIER","UNDZWANZIG"] // 24 +]; + +const germanTensStr = ["NULL",//0 + "ZEHN",//10 + "ZWANZIG",//20 + "DREIßIG",//30 + "VIERZIG",//40 + "FÜNFZIG",//50 + "SECHZIG"//60 +] + +const germanUnit = ["",//0 + "EINUND",//1 + "ZWEIUND",//2 + "DREIUND",//3 + "VIERUND", //4 + "FÜNFUND", //5 + "SECHSUND", //6 + "SIEBENUND", //7 + "ACHTUND", //8 + "NEUNUND" //9 +] + +function germanHoursToText(hours){ + hours = hours % 12; + if(hours === 0){ + hours = 12; + } + if(hours === 1){ + return "EIN" + } else { + return germanNumberStr[hours][0]; + } +} +function german24HoursToText(hours){ + hours = hours % 24; + if(hours === 0){ + return hours[24] ; + } else if(hours === 1){ + return ["EIN",""]; + } else { + return germanNumberStr[hours]; + } +} + + +function germanMinsToText(mins) { + if (mins < 20) { + return germanNumberStr[mins]; + } else { + const tens = (mins / 10 | 0); + const word1 = germanTensStr[tens]; + const remainder = mins - tens * 10; + const word2 = germanUnit[remainder]; + return [word2, word1]; + } +} + +exports.germanMinsToText = germanMinsToText; +exports.germanHoursToText = germanHoursToText; +exports.german24HoursToText = german24HoursToText; \ No newline at end of file diff --git a/apps/slidingtext/slidingtext.utils.en.js b/apps/slidingtext/slidingtext.utils.en.js index a91fcbd16..a66ae456c 100644 --- a/apps/slidingtext/slidingtext.utils.en.js +++ b/apps/slidingtext/slidingtext.utils.en.js @@ -3,12 +3,26 @@ const numberStr = ["ZERO","ONE", "TWO", "THREE", "FOUR", "FIVE", "ELEVEN", "TWELVE", "THIRTEEN", "FOURTEEN", "FIFTEEN", "SIXTEEN", "SEVENTEEN", "EIGHTEEN", "NINETEEN", "TWENTY"]; -const tensStr = ["ZERO", "TEN", "TWENTY", "THIRTY", "FOURTY", - "FIFTY"]; +const tensStr = ["ZERO", "TEN", "TWENTY", "THIRTY", "FORTY", "FIFTY"]; +const dayNames = ["SUNDAY", "MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY"]; +const monthStr = [ + "JANUARY", "FEBRUARY", "MARCH", "APRIL", "MAY", "JULY", + "AUGUST", "SEPTEMBER", "OCTOBER", "NOVEMBER", "DECEMBER" +] + +const dateNumberStr = ["ZEROTH", "FIRST", "SECOND", "THIRD", "FORTH", "FIFTH", + "SIXTH","SEVENTH","EIGHTH","NINTH","TENTH","ELEVENTH","TWELFTH","THIRTEENTH", + "FOURTEENTH", "FIFTEENTH", "SIXTEENTH", "SEVENTEENTH", "EIGHTEENTH", "NINETEENTH", + "TWENTIETH" +] + +const dayOfWeek = (date) => dayNames[date.getDay()]; +const dayOfWeekShort = (date) => dayNames[date.getDay()].substring(0,3); +const monthToText = (date)=>monthStr[date.getMonth()-1]; const hoursToText = (hours)=>{ hours = hours % 12; - if(hours == 0){ + if(hours === 0){ hours = 12; } return numberStr[hours]; @@ -18,9 +32,9 @@ const numberToText = (value)=> { var word1 = ''; var word2 = ''; if(value > 20){ - var tens = (value / 10 | 0); + const tens = (value / 10 | 0); word1 = tensStr[tens]; - var remainder = value - tens * 10; + const remainder = value - tens * 10; if(remainder > 0){ word2 = numberStr[remainder]; } @@ -30,5 +44,27 @@ const numberToText = (value)=> { return [word1,word2]; } +const numberToDayNumberText = (value) => { + var word1 = ''; + var word2 = ''; + if(value === 30) { + word1 = "THIRTIETH"; + } else if(value > 20){ + const tens = (value / 10 | 0); + word1 = tensStr[tens]; + const remainder = value - tens * 10; + if(remainder > 0){ + word2 = dateNumberStr[remainder]; + } + } else if(value > 0) { + word1 = dateNumberStr[value]; + } + return [word1,word2]; +} + +exports.monthToText = monthToText; exports.hoursToText = hoursToText; -exports.numberToText = numberToText; \ No newline at end of file +exports.numberToText = numberToText; +exports.numberToDayNumberText = numberToDayNumberText; +exports.dayOfWeek = dayOfWeek; +exports.dayOfWeekShort = dayOfWeekShort; \ No newline at end of file diff --git a/apps/slopeclock/ChangeLog b/apps/slopeclock/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/slopeclock/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/slopeclock/app-icon.js b/apps/slopeclock/app-icon.js new file mode 100644 index 000000000..528758a7a --- /dev/null +++ b/apps/slopeclock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4X/AAMfBIWSyhL/ACkD1cAlWq1WABYk6BYnABQcO1QLa3//FwgLEHIoABlQLO3WsBZHqHYwLGEooLF0ALHnW/BZMDDII8DmoLDAAILDmtVBZHV+oLEmEA1Ws6vAgNVh5EB+Eq/2lq4cCqqPBIYMqytVoALCioLD6tVBYMDqALEr4KBqotBqkAgsP/9VvkMhoLBhoLCKwQrCIoQLCBQITBM4QPB+ALBFYYLBhpHDBY0BIoILFuEPBYNcBY1XP4fAmgLENIYDBI4JGCNIlXqp3CCof/F4UPIIILEI4wtEIQVwAYIzCO4oLCSgIXFqpfCIwjTBCQXAEQgA/AAoA=")) diff --git a/apps/slopeclock/app.js b/apps/slopeclock/app.js new file mode 100644 index 000000000..dba455cff --- /dev/null +++ b/apps/slopeclock/app.js @@ -0,0 +1,116 @@ +Graphics.prototype.setFontPaytoneOne = function(scale) { + // Actual height 81 (91 - 11) + this.setFontCustom( + E.toString(require('heatshrink').decompress(atob('AH8AgP/BpcD//gBpn4Bpn+Bpn/wANMHBRTB//wBphGLBoJGLv4OBBpU/KhkfBoPABpMPMRkHMRh+CMRRwC/hwmMQQNKMQTTNBpRGCRhSpCBpY4BFJY4BBpcAjgMLAHUwBpl4BhcBd5Z/Bd5abCBpa3BTZd/YpcBcIPgBpMHBoPwIhf//BEL/5wKIgP/OBJECAAJELAAJwIIgQABOBBECOBRECOBJEEOBBEEOBBEEOBBEEOBBEEOA5EFBo5EFFI5EFKY5EGN4woGTIpEpj5EMDYzeGG4xEFgEDWZhhFbo59FfI7QFIgynGIgxwGBg5wEIhBwE+ANIOAZEIOAhEIOAgMJOAREJOAZEJOAZEJOAZEKOAQMKOAJELOAJELAAJELAH0EBhaQBSJa6BZJbkCDhMDBof4XJIADBpvAKRIqKBov+Bo0fBogqHBozpGBoyAGBoxjGBo44FBo44FMIpxHBo5xFBo7HFU4pGHBpBGEBpB/EdohGIgINHIwgNJIwgWEn4EC8ANGQ4SNHv4VEQgRUEEgQxCHwRUEYgRNDEQQNKFQRUDAwQNDQoRUDTQQUDHASpDCgR3EHAJiDCgR3ELYJiEBow/BMQgiBbQ4iFSYg/CLYZwBGAg/COAwNGOAwiDJoRwUKggNBOAwGEBoJwEcIT2GaYw4DAoINEMQQ/CHwRbEMQQHCLQTaHI4QvCNIoHCAArMEJoQAFO4gkDBpJUCAAraHBpRUDAAihEIxANFIw4NFIw7EEIxANFRo4NGcQQNKHAwNGHAwNGHAwNHHAoNHf4YNJVQqLFFQ7DEFRDtEKpHgBpCADwANIDgRSHKwvABpQA/AFp7BZwkfXIyXFVoLVFv//bArxFBoLBDga6GfgK0DHwIiEH4TrEcgw/BJogwBa4g/BJogwBEQgNGOAxNBAAwUEJoQAFOAoNHOAoNHOApbBAAxwEBpBwENIIAGOAgNIOAh3BOBYNIOAi2BOBYNIOAgNJOAbEBOBbEIOAjEIOAoNIOAioIOAiaIOAiMIOH5wLAAw/BOAgAGH4JwEAAw/CBpQ/COAYAHWAJwDAA6wBOAYAHWAJwEAAywBODIA/ABsDUBYNBOwpwGZgIcEcIwNBDggNBcIraFBoQjEbQK+DBoThEBoIqDBoThEdAJNDBoThEBpBNEewJbDBoRwEewINGOAiFBNIYNCOAgNJO5INDOAaaBAwYNDOAgGEBoZwEBpBwEVAgNDOAiMBCgQNDOAiMBCgRnCOAqMEBohwDPwgNEOAZ+EBohwDPwQGBFwJwJAwINEOAxUBLAP/+5wHIwIDC/ZwHHAInC/JwHAAn4OBAAD/g/BOAwNEHYJwGBog/BOAgiBAAf+H4JwELwQNDH4JwEMQQNDH4JwEMQv+H4QNDKgoYBOApUGJoRwDKgxNCOAZUGJoRwEIwoGCOAhGFWARwEIwoUCOAhGEBIJwGRogXCOAriEBoRwGHAZBCOAxxDBoRwGFQZrCOAxADEgRwGCwZOCOA4A/AEMBXggAISQ0AjCZFZYgjBTQt/AwqgBBoraFfozgBbQgNBGIgNGEQIGEewJVECgIGEHwJGEAxr9BKggGBewImBfoRUEAwQ7CBIJUFgINCFoIJBO4oNCwAtBBIJ3JFoIJBFoJNEEQQfBBIJNDRgwJCJoaMGBIQ/DPwgNBFoJiHRgYtBMQ4+DFoJiHHwYfBMQbFDPwoJBXww+CFoZwGHwQtDOAz2CFoZwGUIQJCTwRwGGAIJBTwRwGEQICBKAIRDOAngAQJCBJoJwGAAfhD4ZwEAAxwGBpZiBAA4NDMQIAHPwZiCAAx+DMQQNKKhKMDKhKMDKhINEKgf7BoaaDIwn5BpCpD/A8DVAhGD/g8DBooJC/g8DBoqNC/A8DWwg4DIAINIe4k/BpA0BPAI4CBowmBWAI4CBo4uFKYoAFM4KLEAAxZBWogA/ADSMBRZaaCBpTlCwANMXYIAIaQXgBpioKBoTEKaILgLBoRwKn4NBOBQNDOBINDOBN/BoRwJBoZwJBgRwKBoZwJBoZwIgILCOBINDJAJwHfQX8OQJwHBoaqBOA4NC/DUBOA8HBoQDBOA4NC+AfBOA76C8BXBOA4NDQIQNJLwJwILoINCOBANCC4JwIfQQNBOBAbCMwZwGIoQAGJAZ9CAAxIDU4QAGJAbfCAAxIEBpBIEQ4IAGXIhwCAAq5EOAQAGOH5w/OH5wvBoYAELIInEAA4ZKLIiYDAA5ZBTAYAHLIKYDAA5ZBTAgAGZQKYEAAzKBTAhwjAH4A8U4LRCh7xGS4LRCcYwGBAATDBAwLjEBojDBeILVEAwIADwA7Baoj4BAAfAcYLVECgIADGgIRCfAgAD/EAn5UFBohUIv4OEKg4iBKghNBKghwEGgJNCOBJCBD4RwIIQI/BMQZwHH4JUDOArFDOgJwHBIJiGOAQtBBoJiGSYQNBC4JiGSYTPDH4RiDGAP4Z4jFFGAImBBoY/BYoYmDEoZwIRAhwIwDrDBoJwG4AXDJoJwHRAbMCOAzICZgZwGRAXADYRwGK4X4EQLhGOAYADPwZwFcopwHcopwHBpBwEAAaMEOAoACRgjhFBo7hFAAYNDOAZiFBoZwDKgqoDOAZUFBohwCW4QNHfQYNEWwZwDCIQNHGgINBIwgNEOAIDDBo8DLAoNGAAg4DBpJxDMIgAEXAYNJFQYMJXgTtEAA8HIhIA/ACp9BN5SZD8B7JBoX+YZjSJb4f//ANMYpF/BogqHBovwBowMEKpANF/+ABpiAGBoxjGBoyrGBoxxGBo5xFBo5xFPopGHBo5/FBo5GFYYpGHBpCNEj5UMBpCNEh4ICw//g5UGA4X8AYOAHwQNG/EDBoIGCcQYJBH4IDB4EBKgoGCBoQJBQoJUDBoYDBBIJbBVIgNGHAJiEEQIUBAQQtBMQhbBBoQXBGISMFBQN/C4RiFRgIKBD4IxDYoY+BBoIfBC4IRBOAZ+CBoQJBAYJwGwAtBBIIDBOA3AFoIJBOBHgNgY/DOAiMCHYLFCOAp+CFoZwGPwQRBAwINEGAb6CAAR+DGgYtBAAZ+DGgYmCBo5iCIQQACRgZiGAASMEKgYNJKgYtBAASaEYoZiEBohUIVAhUIBoomB/BUEBopUIBoipIBogmBDYJGEBogmBO4JmCBo8/V4QNJh7nCHAYNFgxYEMIxKGBpYqCU4oAFOoLtEAA8PBhYA/AB9///AQ5jFCABEfQ47MCYAbvBXQgiEUYKxFg4iEgbNGh4UEbgRNFCgoNBH4hpBOBYUBAwhwFHwJ3FOApaBNIpwFCYJpFOAovBNIpwFBgJbFOAgECKgwUDIgQABTYhwDJQIACKghwDKQRGGOAYfBAAZwHBghUEOASXCAAaiF/xSEKgprCIgibGAwO/BopUEKApwJAAyMEGoyoGSwhvHWQqLHOARgKbgpSHfAqYGOBJSEOBAMFOAyXEOBBEGOAyXEOBBEGOAyXEOA5EHOAqXFOA5EHOAqXGOAxEIOAgMIOAZEJOAaXHMQpEJAH4AOn6QJbIaDKQgYcKUATXJVxwNCZQ8fCwIND4C4H4ANDHAzUCBoY4GBAP+MIQEBBo//4IDCOIoXD+ANDewozDBoZGFBIZXBIw4NDAAZGFBo6NFEoYAERogNIKgk/Bo5UEBpBUEj5UMh5UMBpKpDg4KFAwRUDbgP4JARCBKgrEB/AsC/BNCAYINEfYQJBCQJiEBIQpDCQJiEv4JBHAT2DRggTBQIReBWAJiDBQJlDYIIgBYoY+BwBGCLwIVBOAYYBCYJUFOAYYBCYIzBHgIVBOAoTBKgYVBOA6NCwAVBOA6zEOAwlDSIhwF4ANCEAJKBOAvwcgYNCOAv/TQQYBGILhFAAn4DYJwDHwQAGBogUBAAx+ERIQAFPwiJCAAwNDL4YNJPYQAGRgZUJRgZUJBoiKC/wNETQZGEMwiaDIwhmEBohGDMwgNFEwS7EVAiNDLAgNFDARYDBowqBWAJGDBo0DH4JYDaQgAFDZKRGBpRxCBpQqCPooAFKoLDEAA8cBhYA/ACM/8AMKcQYAJaASXKWYTdDgwNI/+AawSyHAAJHCn64FBobeCHgwND/xLCeAoNDHAIFBCIINI8BnCKZA0BQYRGEBohxBv5YDBow0Bn5UFGIRGFSIYNG4AiBKgg/CKhQNFPYJUGBohUIBohUICgIADSYSpECgJiEKgwNCKAXAKg0fCgRCCLYWAYggNBCIJiHGAYDBBoJiFGAINBEwJwBMQowCOgQtFPwh0DH4TFEJgYYBOA4XBJgIYBaYRwEHwJMBBQLTDOAYlBJgIKBPwZwFHwIKB+ANCOA5KBD4INBOAwwBTQhwGGAN/BpBiBEQM/HYINBPwhiBS4X8GAR+EMQI4BBoJvCPwiFC/kPAIINGCof//oEDRgYxCAAwNDKgQAGTQZUCBpZUCAAqoDKgYNKKggADWwapDBpZGHBopGHBopGHBoqNHBoqNHBow4GBow4GBow4GBow4GTIgACfIYNJFQrREFRD7EKo/+Bg7HE/ANJDgQ2IeYZRHAH4AmgaYDn50HRgKLCv/8BpD6CZQINIC4QNBVgy2CBoYgCIojEDBoI4GBoRQBn7yHgLuDBoJGGBoQlBj7zIBAIlBh4uDAAhBBEoJYCKgwzCwBKCHgIAEGYY8EAAgzEHgaMHGYI8DPw5wEwBwTEoJwLUgatEMQ4uDPwzhNC4RPBEAKMGC4QNBEAINHC4INBEAIpGKAQgDBo8AnASDRYoAnA='))), + 46, + atob("ITZOMzs7SDxHNUdGIQ=="), + 113+(scale<<8)+(1<<16) + ); + return this; +}; + +{ // must be inside our own scope here so that when we are unloaded everything disappears + // we also define functions using 'let fn = function() {..}' for the same reason. function decls are global +let drawTimeout; + +let g2 = Graphics.createArrayBuffer(g.getWidth(),90,1,{msb:true}); +let g2img = { + width:g2.getWidth(), height:g2.getHeight(), bpp:1, + buffer:g2.buffer, transparent:0 +}; +const slope = 20; +const offsy = 20; // offset of numbers from middle +const fontBorder = 4; // offset from left/right +const slopeBorder = 10, slopeBorderUpper = 4; // fudge-factor to move minutes down from slope +let R,x,y; // middle of the clock face +let dateStr = ""; +let bgColors = g.theme.dark ? ["#ff0","#0ff","#f0f"] : ["#f00","#0f0","#00f"]; +let bgColor = bgColors[(Math.random()*bgColors.length)|0]; + + +// Draw the hour, and the minute into an offscreen buffer +let draw = function() { + R = Bangle.appRect; + x = R.w / 2; + y = R.y + R.h / 2 - 12; // 12 = room for date + var date = new Date(); + var hourStr = date.getHours(); + var minStr = date.getMinutes().toString().padStart(2,0); + dateStr = require("locale").dow(date, 1).toUpperCase()+ " "+ + require("locale").date(date, 0).toUpperCase(); + + // Draw hour + g.reset().clearRect(R); // clear whole background (w/o widgets) + g.setFontAlign(-1, 0).setFont("PaytoneOne"); + g.drawString(hourStr, fontBorder, y-offsy); + // add slope in background color + g.setColor(g.theme.bg).fillPoly([0,y+slope-slopeBorderUpper, R.w,y-slope-slopeBorderUpper, + R.w,y-slope, 0,y+slope]); + // Draw minute to offscreen buffer + g2.setColor(0).fillRect(0,0,g2.getWidth(),g2.getHeight()).setFontAlign(1, 0).setFont("PaytoneOne"); + g2.setColor(1).drawString(minStr, g2.getWidth()-fontBorder, g2.getHeight()/2); + g2.setColor(0).fillPoly([0,0, g2.getWidth(),0, 0,slope*2]); + // start the animation *in* + animate(true); + + // queue next draw + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + animate(false, function() { + draw(); + }); + }, 60000 - (Date.now() % 60000)); +}; + +let isAnimIn = true; +let animInterval; +// Draw *just* the minute image +let drawMinute = function() { + var yo = slopeBorder + offsy + y - 2*slope*minuteX/R.w; + // draw over the slanty bit + g.setColor(bgColor).fillPoly([0,y+slope, R.w,y-slope, R.w,R.h+R.y, 0,R.h+R.y]); + // draw the minutes + g.setColor(g.theme.bg).drawImage(g2img, x+minuteX-(g2.getWidth()/2), yo-(g2.getHeight()/2)); +}; +let animate = function(isIn, callback) { + if (animInterval) clearInterval(animInterval); + isAnimIn = isIn; + minuteX = isAnimIn ? -g2.getWidth() : 0; + drawMinute(); + animInterval = setInterval(function() { + minuteX += 8; + let stop = false; + if (isAnimIn && minuteX>=0) { + minuteX=0; + stop = true; + } else if (!isAnimIn && minuteX>=R.w) + stop = true; + drawMinute(); + if (stop) { + clearInterval(animInterval); + animInterval=undefined; + if (isAnimIn) // draw the date + g.setColor(g.theme.bg).setFontAlign(0, 0).setFont("6x15").drawString(dateStr, R.x + R.w/2, R.y+R.h-9); + if (callback) callback(); + } + }, 20); +}; + + +// Show launcher when middle button pressed +Bangle.setUI({ + mode : "clock", + remove : function() { + // Called to unload all of the clock app + if (animInterval) clearInterval(animInterval); + animInterval = undefined; + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + delete Graphics.prototype.setFontPaytoneOne; + }}); +// Load widgets +Bangle.loadWidgets(); +draw(); +setTimeout(Bangle.drawWidgets,0); +} diff --git a/apps/slopeclock/app.png b/apps/slopeclock/app.png new file mode 100644 index 000000000..d72b8faf9 Binary files /dev/null and b/apps/slopeclock/app.png differ diff --git a/apps/slopeclock/metadata.json b/apps/slopeclock/metadata.json new file mode 100644 index 000000000..d880a0cf6 --- /dev/null +++ b/apps/slopeclock/metadata.json @@ -0,0 +1,14 @@ +{ "id": "slopeclock", + "name": "Slope Clock", + "version":"0.01", + "description": "A clock where hours and minutes are divided by a sloping line. When the minute changes, the numbers slide off the screen", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "clock", + "tags": "clock", + "supports" : ["BANGLEJS2"], + "storage": [ + {"name":"slopeclock.app.js","url":"app.js"}, + {"name":"slopeclock.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/slopeclock/screenshot.png b/apps/slopeclock/screenshot.png new file mode 100644 index 000000000..4a59f5c4a Binary files /dev/null and b/apps/slopeclock/screenshot.png differ diff --git a/apps/slopeclockpp/ChangeLog b/apps/slopeclockpp/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/slopeclockpp/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/slopeclockpp/app-icon.js b/apps/slopeclockpp/app-icon.js new file mode 100644 index 000000000..bd62b928d --- /dev/null +++ b/apps/slopeclockpp/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4P/AAMA/Ayq8EH8AEBgfgj4zCj/gn/8Aod//wFDvk/gEEAoP4AoMAEIP4j4FFwAFC/gFEv//ApM/74FDg4XBgZLCFIMzAoU4g8BK4dwgMP+Ewg+AgMfK4PhAoXwh+B/0Bj0B/4FBgYnB/8B/kDgf/+ED/kHn//HgIFBW4IFB/AFDgf4h4FB+EBFgLKCAoInBAAOAAoqkBAgPAWAIuBAoXAn+zCAMB4F/8YFBgYFB4YFBRgY7BYwIoCABX4zkY74FB/mMiALC/3mug6CAAgA==")) diff --git a/apps/slopeclockpp/app.js b/apps/slopeclockpp/app.js new file mode 100644 index 000000000..25fd307eb --- /dev/null +++ b/apps/slopeclockpp/app.js @@ -0,0 +1,220 @@ +Graphics.prototype.setFontPaytoneOne = function(scale) { + // Actual height 81 (91 - 11) + this.setFontCustom( + E.toString(require('heatshrink').decompress(atob('AH8AgP/BpcD//gBpn4Bpn+Bpn/wANMHBRTB//wBphGLBoJGLv4OBBpU/KhkfBoPABpMPMRkHMRh+CMRRwC/hwmMQQNKMQTTNBpRGCRhSpCBpY4BFJY4BBpcAjgMLAHUwBpl4BhcBd5Z/Bd5abCBpa3BTZd/YpcBcIPgBpMHBoPwIhf//BEL/5wKIgP/OBJECAAJELAAJwIIgQABOBBECOBRECOBJEEOBBEEOBBEEOBBEEOBBEEOA5EFBo5EFFI5EFKY5EGN4woGTIpEpj5EMDYzeGG4xEFgEDWZhhFbo59FfI7QFIgynGIgxwGBg5wEIhBwE+ANIOAZEIOAhEIOAgMJOAREJOAZEJOAZEJOAZEKOAQMKOAJELOAJELAAJELAH0EBhaQBSJa6BZJbkCDhMDBof4XJIADBpvAKRIqKBov+Bo0fBogqHBozpGBoyAGBoxjGBo44FBo44FMIpxHBo5xFBo7HFU4pGHBpBGEBpB/EdohGIgINHIwgNJIwgWEn4EC8ANGQ4SNHv4VEQgRUEEgQxCHwRUEYgRNDEQQNKFQRUDAwQNDQoRUDTQQUDHASpDCgR3EHAJiDCgR3ELYJiEBow/BMQgiBbQ4iFSYg/CLYZwBGAg/COAwNGOAwiDJoRwUKggNBOAwGEBoJwEcIT2GaYw4DAoINEMQQ/CHwRbEMQQHCLQTaHI4QvCNIoHCAArMEJoQAFO4gkDBpJUCAAraHBpRUDAAihEIxANFIw4NFIw7EEIxANFRo4NGcQQNKHAwNGHAwNGHAwNHHAoNHf4YNJVQqLFFQ7DEFRDtEKpHgBpCADwANIDgRSHKwvABpQA/AFp7BZwkfXIyXFVoLVFv//bArxFBoLBDga6GfgK0DHwIiEH4TrEcgw/BJogwBa4g/BJogwBEQgNGOAxNBAAwUEJoQAFOAoNHOAoNHOApbBAAxwEBpBwENIIAGOAgNIOAh3BOBYNIOAi2BOBYNIOAgNJOAbEBOBbEIOAjEIOAoNIOAioIOAiaIOAiMIOH5wLAAw/BOAgAGH4JwEAAw/CBpQ/COAYAHWAJwDAA6wBOAYAHWAJwEAAywBODIA/ABsDUBYNBOwpwGZgIcEcIwNBDggNBcIraFBoQjEbQK+DBoThEBoIqDBoThEdAJNDBoThEBpBNEewJbDBoRwEewINGOAiFBNIYNCOAgNJO5INDOAaaBAwYNDOAgGEBoZwEBpBwEVAgNDOAiMBCgQNDOAiMBCgRnCOAqMEBohwDPwgNEOAZ+EBohwDPwQGBFwJwJAwINEOAxUBLAP/+5wHIwIDC/ZwHHAInC/JwHAAn4OBAAD/g/BOAwNEHYJwGBog/BOAgiBAAf+H4JwELwQNDH4JwEMQQNDH4JwEMQv+H4QNDKgoYBOApUGJoRwDKgxNCOAZUGJoRwEIwoGCOAhGFWARwEIwoUCOAhGEBIJwGRogXCOAriEBoRwGHAZBCOAxxDBoRwGFQZrCOAxADEgRwGCwZOCOA4A/AEMBXggAISQ0AjCZFZYgjBTQt/AwqgBBoraFfozgBbQgNBGIgNGEQIGEewJVECgIGEHwJGEAxr9BKggGBewImBfoRUEAwQ7CBIJUFgINCFoIJBO4oNCwAtBBIJ3JFoIJBFoJNEEQQfBBIJNDRgwJCJoaMGBIQ/DPwgNBFoJiHRgYtBMQ4+DFoJiHHwYfBMQbFDPwoJBXww+CFoZwGHwQtDOAz2CFoZwGUIQJCTwRwGGAIJBTwRwGEQICBKAIRDOAngAQJCBJoJwGAAfhD4ZwEAAxwGBpZiBAA4NDMQIAHPwZiCAAx+DMQQNKKhKMDKhKMDKhINEKgf7BoaaDIwn5BpCpD/A8DVAhGD/g8DBooJC/g8DBoqNC/A8DWwg4DIAINIe4k/BpA0BPAI4CBowmBWAI4CBo4uFKYoAFM4KLEAAxZBWogA/ADSMBRZaaCBpTlCwANMXYIAIaQXgBpioKBoTEKaILgLBoRwKn4NBOBQNDOBINDOBN/BoRwJBoZwJBgRwKBoZwJBoZwIgILCOBINDJAJwHfQX8OQJwHBoaqBOA4NC/DUBOA8HBoQDBOA4NC+AfBOA76C8BXBOA4NDQIQNJLwJwILoINCOBANCC4JwIfQQNBOBAbCMwZwGIoQAGJAZ9CAAxIDU4QAGJAbfCAAxIEBpBIEQ4IAGXIhwCAAq5EOAQAGOH5w/OH5wvBoYAELIInEAA4ZKLIiYDAA5ZBTAYAHLIKYDAA5ZBTAgAGZQKYEAAzKBTAhwjAH4A8U4LRCh7xGS4LRCcYwGBAATDBAwLjEBojDBeILVEAwIADwA7Baoj4BAAfAcYLVECgIADGgIRCfAgAD/EAn5UFBohUIv4OEKg4iBKghNBKghwEGgJNCOBJCBD4RwIIQI/BMQZwHH4JUDOArFDOgJwHBIJiGOAQtBBoJiGSYQNBC4JiGSYTPDH4RiDGAP4Z4jFFGAImBBoY/BYoYmDEoZwIRAhwIwDrDBoJwG4AXDJoJwHRAbMCOAzICZgZwGRAXADYRwGK4X4EQLhGOAYADPwZwFcopwHcopwHBpBwEAAaMEOAoACRgjhFBo7hFAAYNDOAZiFBoZwDKgqoDOAZUFBohwCW4QNHfQYNEWwZwDCIQNHGgINBIwgNEOAIDDBo8DLAoNGAAg4DBpJxDMIgAEXAYNJFQYMJXgTtEAA8HIhIA/ACp9BN5SZD8B7JBoX+YZjSJb4f//ANMYpF/BogqHBovwBowMEKpANF/+ABpiAGBoxjGBoyrGBoxxGBo5xFBo5xFPopGHBo5/FBo5GFYYpGHBpCNEj5UMBpCNEh4ICw//g5UGA4X8AYOAHwQNG/EDBoIGCcQYJBH4IDB4EBKgoGCBoQJBQoJUDBoYDBBIJbBVIgNGHAJiEEQIUBAQQtBMQhbBBoQXBGISMFBQN/C4RiFRgIKBD4IxDYoY+BBoIfBC4IRBOAZ+CBoQJBAYJwGwAtBBIIDBOA3AFoIJBOBHgNgY/DOAiMCHYLFCOAp+CFoZwGPwQRBAwINEGAb6CAAR+DGgYtBAAZ+DGgYmCBo5iCIQQACRgZiGAASMEKgYNJKgYtBAASaEYoZiEBohUIVAhUIBoomB/BUEBopUIBoipIBogmBDYJGEBogmBO4JmCBo8/V4QNJh7nCHAYNFgxYEMIxKGBpYqCU4oAFOoLtEAA8PBhYA/AB9///AQ5jFCABEfQ47MCYAbvBXQgiEUYKxFg4iEgbNGh4UEbgRNFCgoNBH4hpBOBYUBAwhwFHwJ3FOApaBNIpwFCYJpFOAovBNIpwFBgJbFOAgECKgwUDIgQABTYhwDJQIACKghwDKQRGGOAYfBAAZwHBghUEOASXCAAaiF/xSEKgprCIgibGAwO/BopUEKApwJAAyMEGoyoGSwhvHWQqLHOARgKbgpSHfAqYGOBJSEOBAMFOAyXEOBBEGOAyXEOBBEGOAyXEOA5EHOAqXFOA5EHOAqXGOAxEIOAgMIOAZEJOAaXHMQpEJAH4AOn6QJbIaDKQgYcKUATXJVxwNCZQ8fCwIND4C4H4ANDHAzUCBoY4GBAP+MIQEBBo//4IDCOIoXD+ANDewozDBoZGFBIZXBIw4NDAAZGFBo6NFEoYAERogNIKgk/Bo5UEBpBUEj5UMh5UMBpKpDg4KFAwRUDbgP4JARCBKgrEB/AsC/BNCAYINEfYQJBCQJiEBIQpDCQJiEv4JBHAT2DRggTBQIReBWAJiDBQJlDYIIgBYoY+BwBGCLwIVBOAYYBCYJUFOAYYBCYIzBHgIVBOAoTBKgYVBOA6NCwAVBOA6zEOAwlDSIhwF4ANCEAJKBOAvwcgYNCOAv/TQQYBGILhFAAn4DYJwDHwQAGBogUBAAx+ERIQAFPwiJCAAwNDL4YNJPYQAGRgZUJRgZUJBoiKC/wNETQZGEMwiaDIwhmEBohGDMwgNFEwS7EVAiNDLAgNFDARYDBowqBWAJGDBo0DH4JYDaQgAFDZKRGBpRxCBpQqCPooAFKoLDEAA8cBhYA/ACM/8AMKcQYAJaASXKWYTdDgwNI/+AawSyHAAJHCn64FBobeCHgwND/xLCeAoNDHAIFBCIINI8BnCKZA0BQYRGEBohxBv5YDBow0Bn5UFGIRGFSIYNG4AiBKgg/CKhQNFPYJUGBohUIBohUICgIADSYSpECgJiEKgwNCKAXAKg0fCgRCCLYWAYggNBCIJiHGAYDBBoJiFGAINBEwJwBMQowCOgQtFPwh0DH4TFEJgYYBOA4XBJgIYBaYRwEHwJMBBQLTDOAYlBJgIKBPwZwFHwIKB+ANCOA5KBD4INBOAwwBTQhwGGAN/BpBiBEQM/HYINBPwhiBS4X8GAR+EMQI4BBoJvCPwiFC/kPAIINGCof//oEDRgYxCAAwNDKgQAGTQZUCBpZUCAAqoDKgYNKKggADWwapDBpZGHBopGHBopGHBoqNHBoqNHBow4GBow4GBow4GBow4GTIgACfIYNJFQrREFRD7EKo/+Bg7HE/ANJDgQ2IeYZRHAH4AmgaYDn50HRgKLCv/8BpD6CZQINIC4QNBVgy2CBoYgCIojEDBoI4GBoRQBn7yHgLuDBoJGGBoQlBj7zIBAIlBh4uDAAhBBEoJYCKgwzCwBKCHgIAEGYY8EAAgzEHgaMHGYI8DPw5wEwBwTEoJwLUgatEMQ4uDPwzhNC4RPBEAKMGC4QNBEAINHC4INBEAIpGKAQgDBo8AnASDRYoAnA='))), + 46, + atob("ITZOMzs7SDxHNUdGIQ=="), + 113+(scale<<8)+(1<<16) + ); + return this; +}; + +{ // must be inside our own scope here so that when we are unloaded everything disappears + // we also define functions using 'let fn = function() {..}' for the same reason. function decls are global +let drawTimeout; + +let g2 = Graphics.createArrayBuffer(g.getWidth(),90,1,{msb:true}); +let g2img = { + width:g2.getWidth(), height:g2.getHeight(), bpp:1, + buffer:g2.buffer, transparent:0 +}; +const slope = 20; +const offsy = 20; // offset of numbers from middle +const fontBorder = 4; // offset from left/right +const slopeBorder = 10, slopeBorderUpper = 4; // fudge-factor to move minutes down from slope +let R,x,y; // middle of the clock face +let dateStr = ""; +let bgColors = g.theme.dark ? ["#ff0","#0ff","#f0f"] : ["#f00","#0f0","#00f"]; +let bgColor = bgColors[(Math.random()*bgColors.length)|0]; + + +// Draw the hour, and the minute into an offscreen buffer +let draw = function() { + R = Bangle.appRect; + x = R.w / 2; + y = R.y + R.h / 2 - 12; // 12 = room for date + var date = new Date(); + var hourStr = date.getHours(); + var minStr = date.getMinutes().toString().padStart(2,0); + dateStr = require("locale").dow(date, 1).toUpperCase()+ " "+ + require("locale").date(date, 0).toUpperCase(); + + // Draw hour + g.reset().clearRect(R); // clear whole background (w/o widgets) + g.setFontAlign(-1, 0).setFont("PaytoneOne"); + g.drawString(hourStr, fontBorder, y-offsy); + // add slope in background color + g.setColor(g.theme.bg).fillPoly([0,y+slope-slopeBorderUpper, R.w,y-slope-slopeBorderUpper, + R.w,y-slope, 0,y+slope]); + + // Draw minute to offscreen buffer + g2.setColor(0).fillRect(0,0,g2.getWidth(),g2.getHeight()).setFontAlign(1, 0).setFont("PaytoneOne"); + g2.setColor(1).drawString(minStr, g2.getWidth()-fontBorder, g2.getHeight()/2); + g2.setColor(0).fillPoly([0,0, g2.getWidth(),0, 0,slope*2]); + // start the animation *in* + animate(true); + + // queue next draw + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + animate(false, function() { + draw(); + }); + }, 60000 - (Date.now() % 60000)); +}; + +let isAnimIn = true; +let animInterval; +// Draw *just* the minute image +let drawMinute = function() { + var yo = slopeBorder + offsy + y - 2*slope*minuteX/R.w; + // draw over the slanty bit + g.setColor(bgColor).fillPoly([0,y+slope, R.w,y-slope, R.w,R.h+R.y, 0,R.h+R.y]); + // draw the minutes + g.setColor(g.theme.bg).drawImage(g2img, x+minuteX-(g2.getWidth()/2), yo-(g2.getHeight()/2)); +}; +let animate = function(isIn, callback) { + if (animInterval) clearInterval(animInterval); + isAnimIn = isIn; + minuteX = isAnimIn ? -g2.getWidth() : 0; + drawMinute(); + animInterval = setInterval(function() { + minuteX += 8; + let stop = false; + if (isAnimIn && minuteX>=0) { + minuteX=0; + stop = true; + } else if (!isAnimIn && minuteX>=R.w) + stop = true; + drawMinute(); + if (stop) { + clearInterval(animInterval); + animInterval=undefined; + if (isAnimIn) { + // draw the date + g.setColor(g.theme.bg).setFontAlign(0, 0).setFont("6x15").drawString(dateStr, R.x + R.w/2, R.y+R.h-9); + + // draw steps to bottom left + const steps = getSteps(); + if (steps > 0) + g.setFontAlign(-1, 0).drawString(shortValue(steps), 3, R.y+R.h-30); + + // draw weather to top right + const weather = getWeather(); + const tempString = weather ? require("locale").temp(weather.temp - 273.15) : undefined; + const code = weather ? weather.code : -1; + if (code > -1) { + g.setColor(g.theme.fg).setFontAlign(1, 0).drawString(tempString, R.w - 3, y-slope-slopeBorderUpper); + const icon = getWeatherIconByCode(code); + if (icon) g.drawImage(icon, R.w - 3 - 15, y-slope-slopeBorderUpper - 15 - 15); + } + } + if (callback) callback(); + } + }, 20); +}; + +let getSteps = function() { + if (Bangle.getHealthStatus) { + return Bangle.getHealthStatus("day").steps; + } + if (WIDGETS && WIDGETS.wpedom !== undefined) { + return WIDGETS.wpedom.getSteps(); + } + return 0; +}; + +let shortValue = function(v) { + if (isNaN(v)) return '-'; + if (v <= 999) return v; + if (v >= 1000 && v < 10000) { + v = Math.floor(v / 100) * 100; + return (v / 1000).toFixed(1).replace(/\.0$/, '') + 'k'; + } + if (v >= 10000) { + v = Math.floor(v / 1000) * 1000; + return (v / 1000).toFixed(1).replace(/\.0$/, '') + 'k'; + } +}; + +let getWeather = function() { + let jsonWeather = require("Storage").readJSON('weather.json'); + return jsonWeather && jsonWeather.weather ? jsonWeather.weather : undefined; +}; + +/* + * Choose weather icon to display based on weather conditition code + * https://openweathermap.org/weather-conditions#Weather-Condition-Codes-2 + */ +let getWeatherIconByCode = function(code) { + let codeGroup = Math.round(code / 100); + + // weather icons: + let weatherCloudy = atob("EBCBAAAAAAAAAAfgD/Af8H/4//7///////9//z/+AAAAAAAA"); + let weatherSunny = atob("EBCBAAAAAYAQCBAIA8AH4A/wb/YP8A/gB+ARiBAIAYABgAAA"); + let weatherMoon = atob("EBCBAAAAAYAP8B/4P/w//D/8f/5//j/8P/w//B/4D/ABgAAA"); + let weatherPartlyCloudy = atob("EBCBAAAAAAAYQAMAD8AIQBhoW+AOYBwwOBBgHGAGP/wf+AAA"); + let weatherRainy = atob("EBCBAAAAAYAH4AwwOBBgGEAOQAJBgjPOEkgGYAZgA8ABgAAA"); + let weatherPartlyRainy = atob("EBCBAAAAEEAQAAeADMAYaFvoTmAMMDgQIBxhhiGGG9wDwAGA"); + let weatherSnowy = atob("EBCBAAAAAAADwAGAEYg73C50BCAEIC50O9wRiAGAA8AAAAAA"); + let weatherFoggy = atob("EBCBAAAAAAADwAZgDDA4EGAcQAZAAgAAf74AAAAAd/4AAAAA"); + let weatherStormy = atob("EBCBAAAAAYAH4AwwOBBgGEAOQMJAgjmOGcgAgACAAAAAAAAA"); + let unknown = undefined; + + switch (codeGroup) { + case 2: + return weatherStormy; + case 3: + return weatherCloudy; + case 5: + switch (code) { + case 511: + return weatherSnowy; + case 520: + return weatherPartlyRainy; + case 521: + return weatherPartlyRainy; + case 522: + return weatherPartlyRainy; + case 531: + return weatherPartlyRainy; + default: + return weatherRainy; + } + case 6: + return weatherSnowy; + case 7: + return weatherFoggy; + case 8: + switch (code) { + case 800: + return weatherSunny; + case 801: + return weatherPartlyCloudy; + case 802: + return weatherPartlyCloudy; + default: + return weatherCloudy; + } + default: + return unknown; + } +} + +// Show launcher when middle button pressed +Bangle.setUI({ + mode : "clock", + remove : function() { + // Called to unload all of the clock app + if (animInterval) clearInterval(animInterval); + animInterval = undefined; + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + delete Graphics.prototype.setFontPaytoneOne; + }}); +// Load widgets +Bangle.loadWidgets(); +draw(); +setTimeout(Bangle.drawWidgets,0); +} diff --git a/apps/slopeclockpp/app.png b/apps/slopeclockpp/app.png new file mode 100644 index 000000000..2f5912fcf Binary files /dev/null and b/apps/slopeclockpp/app.png differ diff --git a/apps/slopeclockpp/metadata.json b/apps/slopeclockpp/metadata.json new file mode 100644 index 000000000..9e28424a8 --- /dev/null +++ b/apps/slopeclockpp/metadata.json @@ -0,0 +1,14 @@ +{ "id": "slopeclockpp", + "name": "Slope Clock ++", + "version":"0.01", + "description": "A clock where hours and minutes are divided by a sloping line. When the minute changes, the numbers slide off the screen. This is a clone of the original Slope Clock which shows weather and steps.", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "clock", + "tags": "clock", + "supports" : ["BANGLEJS2"], + "storage": [ + {"name":"slopeclockpp.app.js","url":"app.js"}, + {"name":"slopeclockpp.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/slopeclockpp/screenshot.png b/apps/slopeclockpp/screenshot.png new file mode 100644 index 000000000..93970956c Binary files /dev/null and b/apps/slopeclockpp/screenshot.png differ diff --git a/apps/smpltmr/ChangeLog b/apps/smpltmr/ChangeLog index 572aaa91e..b09100f50 100644 --- a/apps/smpltmr/ChangeLog +++ b/apps/smpltmr/ChangeLog @@ -1,3 +1,4 @@ 0.01: Release 0.02: Rewrite with new interface -0.03: Added clock infos to expose timer functionality to clocks. \ No newline at end of file +0.03: Added clock infos to expose timer functionality to clocks. +0.04: Improvements of clock infos. \ No newline at end of file diff --git a/apps/smpltmr/clkinfo.js b/apps/smpltmr/clkinfo.js index dfc70aab9..1a63a9b7e 100644 --- a/apps/smpltmr/clkinfo.js +++ b/apps/smpltmr/clkinfo.js @@ -69,7 +69,7 @@ img: img, items: [ { - name: "Timer", + name: null, get: () => ({ text: getAlarmMinutesText() + (isAlarmEnabled() ? " min" : ""), img: null}), show: function() { smpltmrItems.items[0].emit("redraw"); }, hide: function () {}, @@ -78,17 +78,18 @@ ] }; - var offsets = [+1,+5,-1,-5]; + var offsets = [+5,-5]; offsets.forEach((o, i) => { smpltmrItems.items = smpltmrItems.items.concat({ - name: String(o), - get: () => ({ text: getAlarmMinutesText() + " (" + (o > 0 ? "+" : "") + o + ")", img: null}), + name: null, + get: () => ({ text: (o > 0 ? "+" : "") + o + " min.", img: null}), show: function() { smpltmrItems.items[i+1].emit("redraw"); }, hide: function () {}, run: function() { if(o > 0) increaseAlarm(o); else decreaseAlarm(Math.abs(o)); this.show(); + return true; } }); }); diff --git a/apps/smpltmr/metadata.json b/apps/smpltmr/metadata.json index 4a219fad2..5f1329dfc 100644 --- a/apps/smpltmr/metadata.json +++ b/apps/smpltmr/metadata.json @@ -2,7 +2,7 @@ "id": "smpltmr", "name": "Simple Timer", "shortName": "Simple Timer", - "version": "0.03", + "version": "0.04", "description": "A very simple app to start a timer.", "icon": "app.png", "tags": "tool,alarm,timer", diff --git a/apps/spotrem/ChangeLog b/apps/spotrem/ChangeLog new file mode 100644 index 000000000..8e3d8b652 --- /dev/null +++ b/apps/spotrem/ChangeLog @@ -0,0 +1,5 @@ +0.01: New app. +0.02: Restructure menu. +0.03: change handling of intent extras. +0.04: New layout. +0.05: Add widgets field. Tweak layout. diff --git a/apps/spotrem/README.md b/apps/spotrem/README.md new file mode 100644 index 000000000..346ec9eba --- /dev/null +++ b/apps/spotrem/README.md @@ -0,0 +1,21 @@ +Requires Gadgetbridge 71.0 or later. Allow intents in Gadgetbridge in order for this app to work. + +Touch input: + +Press the different ui elements to control Podcast Addict and open menus. Press left or right arrow to go to previous/next track. + +Swipe input: + +Swipe left/right to go to previous/next track. Swipe up/down to change the volume. + +It's possible to start tracks by searching with the remote. Search term without tags will override search with tags. + +To start playing 'from cold' the command for previous/next track via touch or swipe can be used. Pressing just play/pause is not guaranteed to initiate spotify in all circumstances (this will probably change with subsequent releases). + +In order to search to play or start music from the 'Saved' menu the Android device must be awake and unlocked. The remote can wake and unlock the device if the Bangle.js has been added as a 'trusted device' under Android Settings->Security->Smart Lock->Trusted devices. + +The swipe logic was inspired by the implementation in [rigrig](https://git.tubul.net/rigrig/)'s Scrolling Messages. + +Spotify Remote was created by [thyttan](https://github.com/thyttan/). + +Spotify icon by Icons8 diff --git a/apps/spotrem/app-icon.js b/apps/spotrem/app-icon.js new file mode 100644 index 000000000..8da55b9a5 --- /dev/null +++ b/apps/spotrem/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AFV3AAQVVDKQWHDB0HC5NwCyoYMCxZJKFxgwKCxowJC6xGOJBAWPGA4MGogXOIwdCmf/AAczkhIKC4VzCogAD+YZEC49PC5AABmgXO+czJYoYDC4gfCuRYGoUjDAZ4GUJlyn4XNukjIwMzmVHBAU/+YXKoZ0GmQLCDgQXIU5IVDC5JVCIwIECDA5HIR4hkBDAX0C5YAHOoIXJa4QRDoUikiOEm7vKE4YADmZ1FC5N/R48nC5tzFQMiokimYYHC4h4KJwX3Ow6QMOwoXGSAoAKIwrBNFxIXZJBxGHGB4WIGBouJDBgWLJJYWMDBIWODIwVRAH4AXA=")) diff --git a/apps/spotrem/app.js b/apps/spotrem/app.js new file mode 100644 index 000000000..7e76d84bc --- /dev/null +++ b/apps/spotrem/app.js @@ -0,0 +1,281 @@ +/* +Bluetooth.println(JSON.stringify({t:"intent", action:"", flags:["flag1", "flag2",...], categories:["category1","category2",...], mimetype:"", data:"", package:"", class:"", target:"", extra:{someKey:"someValueOrString"}})); +*/ + +var R; +var backToMenu = false; +var isPaused = true; +var dark = g.theme.dark; // bool + +// The main layout of the app +function gfx() { + //Bangle.drawWidgets(); + R = Bangle.appRect; + marigin = 8; + // g.drawString(str, x, y, solid) + g.clearRect(R); + g.reset(); + + if (dark) {g.setColor(0x07E0);} else {g.setColor(0x03E0);} // Green on dark theme, DarkGreen on light theme. + g.setFont("4x6:2"); + g.setFontAlign(1, 0, 0); + g.drawString("->", R.x2 - marigin, R.y + R.h/2); + + g.setFontAlign(-1, 0, 0); + g.drawString("<-", R.x + marigin, R.y + R.h/2); + + g.setFontAlign(-1, 0, 1); + g.drawString("<-", R.x + R.w/2, R.y + marigin); + + g.setFontAlign(1, 0, 1); + g.drawString("->", R.x + R.w/2, R.y2 - marigin); + + g.setFontAlign(0, 0, 0); + g.drawString("Play\nPause", R.x + R.w/2, R.y + R.h/2); + + g.setFontAlign(-1, -1, 0); + g.drawString("Menu", R.x + 2*marigin, R.y + 2*marigin); + + g.setFontAlign(-1, 1, 0); + g.drawString("Wake", R.x + 2*marigin, R.y + R.h - 2*marigin); + + g.setFontAlign(1, -1, 0); + g.drawString("Srch", R.x + R.w - 2*marigin, R.y + 2*marigin); + + g.setFontAlign(1, 1, 0); + g.drawString("Saved", R.x + R.w - 2*marigin, R.y + R.h - 2*marigin); +} + +// Touch handler for main layout +function touchHandler(_, xy) { + x = xy.x; + y = xy.y; + len = (R.wb-1 instead of a>b. + if ((R.x-1 { + if (ud) Bangle.musicControl(ud>0 ? "volumedown" : "volumeup"); + } + ); + Bangle.on("touch", touchHandler); + Bangle.on("swipe", swipeHandler); +} + + + +// Get back to the main layout +function backToGfx() { + E.showMenu(); + g.clear(); + g.reset(); + Bangle.removeAllListeners("touch"); + Bangle.removeAllListeners("swipe"); + setUI(); + gfx(); + backToMenu = false; +} + +/* +The functions for interacting with Android and the Spotify app +*/ + +simpleSearch = ""; +function simpleSearchTerm() { // input a simple search term without tags, overrides search with tags (artist and track) + require("textinput").input({text:simpleSearch}).then(result => {simpleSearch = result;}).then(() => {E.showMenu(searchMenu);}); +} + +artist = ""; +function artistSearchTerm() { // input artist to search for + require("textinput").input({text:artist}).then(result => {artist = result;}).then(() => {E.showMenu(searchMenu);}); +} + +track = ""; +function trackSearchTerm() { // input track to search for + require("textinput").input({text:track}).then(result => {track = result;}).then(() => {E.showMenu(searchMenu);}); +} + +album = ""; +function albumSearchTerm() { // input album to search for + require("textinput").input({text:album}).then(result => {album = result;}).then(() => {E.showMenu(searchMenu);}); +} + +function searchPlayWOTags() {//make a spotify search and play using entered terms + searchString = simpleSearch; + Bluetooth.println(JSON.stringify({t:"intent", action:"android.media.action.MEDIA_PLAY_FROM_SEARCH", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", target:"activity", extra:{query:searchString}, flags:["FLAG_ACTIVITY_NEW_TASK"]})); +} + +function searchPlayWTags() {//make a spotify search and play using entered terms + searchString = (artist=="" ? "":("artist:\""+artist+"\"")) + ((artist!="" && track!="") ? " ":"") + (track=="" ? "":("track:\""+track+"\"")) + (((artist!="" && album!="") || (track!="" && album!="")) ? " ":"") + (album=="" ? "":(" album:\""+album+"\"")); + Bluetooth.println(JSON.stringify({t:"intent", action:"android.media.action.MEDIA_PLAY_FROM_SEARCH", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", target:"activity", extra:{query:searchString}, flags:["FLAG_ACTIVITY_NEW_TASK"]})); +} + +function playVreden() {//Play the track "Vreden" by Sara Parkman via spotify uri-link + Bluetooth.println(JSON.stringify({t:"intent", action:"android.intent.action.VIEW", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", data:"spotify:track:5QEFFJ5tAeRlVquCUNpAJY:play", target:"activity" , flags:["FLAG_ACTIVITY_NEW_TASK", "FLAG_ACTIVITY_NO_ANIMATION"/*, "FLAG_ACTIVITY_CLEAR_TOP", "FLAG_ACTIVITY_PREVIOUS_IS_TOP"*/]})); +} + +function playVredenAlternate() {//Play the track "Vreden" by Sara Parkman via spotify uri-link + Bluetooth.println(JSON.stringify({t:"intent", action:"android.intent.action.VIEW", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", data:"spotify:track:5QEFFJ5tAeRlVquCUNpAJY:play", target:"activity" , flags:["FLAG_ACTIVITY_NEW_TASK"]})); +} + +function searchPlayVreden() {//Play the track "Vreden" by Sara Parkman via search and play + Bluetooth.println(JSON.stringify({t:"intent", action:"android.media.action.MEDIA_PLAY_FROM_SEARCH", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", target:"activity", extra:{query:'artist:"Sara Parkman" track:"Vreden"'}, flags:["FLAG_ACTIVITY_NEW_TASK"]})); +} + +function openAlbum() {//Play EP "The Blue Room" by Coldplay + Bluetooth.println(JSON.stringify({t:"intent", action:"android.intent.action.VIEW", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", data:"spotify:album:3MVb2CWB36x7VwYo5sZmf2", target:"activity", flags:["FLAG_ACTIVITY_NEW_TASK"]})); +} + +function searchPlayAlbum() {//Play EP "The Blue Room" by Coldplay via search and play + Bluetooth.println(JSON.stringify({t:"intent", action:"android.media.action.MEDIA_PLAY_FROM_SEARCH", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", target:"activity", extra:{query:'album:"The blue room" artist:"Coldplay"', "android.intent.extra.focus":"vnd.android.cursor.item/album"}, flags:["FLAG_ACTIVITY_NEW_TASK"]})); +} + +function spotifyWidget(action) { + Bluetooth.println(JSON.stringify({t:"intent", action:("com.spotify.mobile.android.ui.widget."+action), package:"com.spotify.music", target:"broadcastreceiver"})); +} + +function gadgetbridgeWake() { + Bluetooth.println(JSON.stringify({t:"intent", target:"activity", flags:["FLAG_ACTIVITY_NEW_TASK", "FLAG_ACTIVITY_CLEAR_TASK", "FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS", "FLAG_ACTIVITY_NO_ANIMATION"], package:"gadgetbridge", class:"nodomain.freeyourgadget.gadgetbridge.activities.WakeActivity"})); +} + +function spotifyPlaylistDW() { + Bluetooth.println(JSON.stringify({t:"intent", action:"android.intent.action.VIEW", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", data:"spotify:user:spotify:playlist:37i9dQZEVXcRfaeEbxXIgb:play", target:"activity" , flags:["FLAG_ACTIVITY_NEW_TASK", "FLAG_ACTIVITY_NO_ANIMATION"/*, "FLAG_ACTIVITY_CLEAR_TOP", "FLAG_ACTIVITY_PREVIOUS_IS_TOP"*/]})); +} + +function spotifyPlaylistDM1() { + Bluetooth.println(JSON.stringify({t:"intent", action:"android.intent.action.VIEW", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", data:"spotify:user:spotify:playlist:37i9dQZF1E365VyzxE0mxF:play", target:"activity" , flags:["FLAG_ACTIVITY_NEW_TASK", "FLAG_ACTIVITY_NO_ANIMATION"/*, "FLAG_ACTIVITY_CLEAR_TOP", "FLAG_ACTIVITY_PREVIOUS_IS_TOP"*/]})); +} + +function spotifyPlaylistDM2() { + Bluetooth.println(JSON.stringify({t:"intent", action:"android.intent.action.VIEW", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", data:"spotify:user:spotify:playlist:37i9dQZF1E38LZHLFnrM61:play", target:"activity" , flags:["FLAG_ACTIVITY_NEW_TASK", "FLAG_ACTIVITY_NO_ANIMATION"/*, "FLAG_ACTIVITY_CLEAR_TOP", "FLAG_ACTIVITY_PREVIOUS_IS_TOP"*/]})); +} + +function spotifyPlaylistDM3() { + Bluetooth.println(JSON.stringify({t:"intent", action:"android.intent.action.VIEW", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", data:"spotify:user:spotify:playlist:37i9dQZF1E36RU87qzgBFP:play", target:"activity" , flags:["FLAG_ACTIVITY_NEW_TASK", "FLAG_ACTIVITY_NO_ANIMATION"/*, "FLAG_ACTIVITY_CLEAR_TOP", "FLAG_ACTIVITY_PREVIOUS_IS_TOP"*/]})); +} + +function spotifyPlaylistDM4() { + Bluetooth.println(JSON.stringify({t:"intent", action:"android.intent.action.VIEW", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", data:"spotify:user:spotify:playlist:37i9dQZF1E396gGyCXEBFh:play", target:"activity" , flags:["FLAG_ACTIVITY_NEW_TASK", "FLAG_ACTIVITY_NO_ANIMATION"/*, "FLAG_ACTIVITY_CLEAR_TOP", "FLAG_ACTIVITY_PREVIOUS_IS_TOP"*/]})); +} + +function spotifyPlaylistDM5() { + Bluetooth.println(JSON.stringify({t:"intent", action:"android.intent.action.VIEW", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", data:"spotify:user:spotify:playlist:37i9dQZF1E37a0Tt6CKJLP:play", target:"activity" , flags:["FLAG_ACTIVITY_NEW_TASK", "FLAG_ACTIVITY_NO_ANIMATION"/*, "FLAG_ACTIVITY_CLEAR_TOP", "FLAG_ACTIVITY_PREVIOUS_IS_TOP"*/]})); +} + +function spotifyPlaylistDM6() { + Bluetooth.println(JSON.stringify({t:"intent", action:"android.intent.action.VIEW", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", data:"spotify:user:spotify:playlist:37i9dQZF1E36UIQLQK79od:play", target:"activity" , flags:["FLAG_ACTIVITY_NEW_TASK", "FLAG_ACTIVITY_NO_ANIMATION"/*, "FLAG_ACTIVITY_CLEAR_TOP", "FLAG_ACTIVITY_PREVIOUS_IS_TOP"*/]})); +} + +function spotifyPlaylistDD() { + Bluetooth.println(JSON.stringify({t:"intent", action:"android.intent.action.VIEW", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", data:"spotify:user:spotify:playlist:37i9dQZF1EfWFiI7QfIAKq:play", target:"activity" , flags:["FLAG_ACTIVITY_NEW_TASK", "FLAG_ACTIVITY_NO_ANIMATION"/*, "FLAG_ACTIVITY_CLEAR_TOP", "FLAG_ACTIVITY_PREVIOUS_IS_TOP"*/]})); +} + +function spotifyPlaylistRR() { + Bluetooth.println(JSON.stringify({t:"intent", action:"android.intent.action.VIEW", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", data:"spotify:user:spotify:playlist:37i9dQZEVXbs0XkE2V8sMO:play", target:"activity" , flags:["FLAG_ACTIVITY_NEW_TASK", "FLAG_ACTIVITY_NO_ANIMATION"/*, "FLAG_ACTIVITY_CLEAR_TOP", "FLAG_ACTIVITY_PREVIOUS_IS_TOP"*/]})); +} + +// Spotify Remote Menu +var spotifyMenu = { + "" : { title : " ", + back: backToGfx }, + "Controls" : ()=>{E.showMenu(controlMenu);}, + "Search and play" : ()=>{E.showMenu(searchMenu);}, + "Saved music" : ()=>{E.showMenu(savedMenu);}, + "Wake the android" : function() {gadgetbridgeWake();gadgetbridgeWake();}, + "Exit Spotify Remote" : ()=>{load();} +}; + + +var controlMenu = { + "" : { title : " ", + back: () => {if (backToMenu) E.showMenu(spotifyMenu); + if (!backToMenu) backToGfx();} }, + "Play" : ()=>{Bangle.musicControl("play");}, + "Pause" : ()=>{Bangle.musicControl("pause");}, + "Previous" : ()=>{spotifyWidget("PREVIOUS");}, + "Next" : ()=>{spotifyWidget("NEXT");}, + "Play (widget, next then previous)" : ()=>{spotifyWidget("NEXT"); spotifyWidget("PREVIOUS");}, + "Messages Music Controls" : ()=>{load("messagesmusic.app.js");}, +}; + +var searchMenu = { + "" : { title : " ", + back: () => {if (backToMenu) E.showMenu(spotifyMenu); + if (!backToMenu) backToGfx();} }, + "Search term w/o tags" : ()=>{simpleSearchTerm();}, + "Execute search and play w/o tags" : ()=>{searchPlayWOTags();}, + "Search term w tag \"artist\"" : ()=>{artistSearchTerm();}, + "Search term w tag \"track\"" : ()=>{trackSearchTerm();}, + "Search term w tag \"album\"" : ()=>{albumSearchTerm();}, + "Execute search and play with tags" : ()=>{searchPlayWTags();}, +}; + +var savedMenu = { + "" : { title : " ", + back: () => {if (backToMenu) E.showMenu(spotifyMenu); + if (!backToMenu) backToGfx();} }, + "Play Discover Weekly" : ()=>{spotifyPlaylistDW();}, + "Play Daily Mix 1" : ()=>{spotifyPlaylistDM1();}, + "Play Daily Mix 2" : ()=>{spotifyPlaylistDM2();}, + "Play Daily Mix 3" : ()=>{spotifyPlaylistDM3();}, + "Play Daily Mix 4" : ()=>{spotifyPlaylistDM4();}, + "Play Daily Mix 5" : ()=>{spotifyPlaylistDM5();}, + "Play Daily Mix 6" : ()=>{spotifyPlaylistDM6();}, + "Play Daily Drive" : ()=>{spotifyPlaylistDD();}, + "Play Release Radar" : ()=>{spotifyPlaylistRR();}, + "Play \"Vreden\" by Sara Parkman via uri-link" : ()=>{playVreden();}, + "Open \"The Blue Room\" EP (no autoplay)" : ()=>{openAlbum();}, + "Play \"The Blue Room\" EP via search&play" : ()=>{searchPlayAlbum();}, +}; + +Bangle.loadWidgets(); +setUI(); +gfx(); diff --git a/apps/spotrem/app.png b/apps/spotrem/app.png new file mode 100644 index 000000000..3c0d65eee Binary files /dev/null and b/apps/spotrem/app.png differ diff --git a/apps/spotrem/metadata.json b/apps/spotrem/metadata.json new file mode 100644 index 000000000..a0261ba13 --- /dev/null +++ b/apps/spotrem/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "spotrem", + "name": "Remote for Spotify", + "version": "0.05", + "description": "Control spotify on your android device.", + "readme": "README.md", + "type": "app", + "tags": "spotify,music,player,remote,control,intent,intents,gadgetbridge,spotrem", + "icon": "app.png", + "screenshots" : [ {"url":"screenshot1.png"}, {"url":"screenshot2.png"} ], + "supports": ["BANGLEJS2"], + "dependencies": { "textinput":"type"}, + "storage": [ + {"name":"spotrem.app.js","url":"app.js"}, + {"name":"spotrem.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/spotrem/screenshot1.png b/apps/spotrem/screenshot1.png new file mode 100644 index 000000000..730e98547 Binary files /dev/null and b/apps/spotrem/screenshot1.png differ diff --git a/apps/spotrem/screenshot2.png b/apps/spotrem/screenshot2.png new file mode 100644 index 000000000..7801a3034 Binary files /dev/null and b/apps/spotrem/screenshot2.png differ diff --git a/apps/stlap/ChangeLog b/apps/stlap/ChangeLog new file mode 100644 index 000000000..35ba8b130 --- /dev/null +++ b/apps/stlap/ChangeLog @@ -0,0 +1,3 @@ +0.01: New app! +0.02: Bug fixes +0.03: Submitted to the app loader \ No newline at end of file diff --git a/apps/stlap/app.js b/apps/stlap/app.js new file mode 100644 index 000000000..0bd18311f --- /dev/null +++ b/apps/stlap/app.js @@ -0,0 +1,304 @@ +const storage = require("Storage"); +const heatshrink = require("heatshrink"); +const STATE_PATH = "stlap.state.json"; +g.setFont("Vector", 24); +const BUTTON_ICONS = { + play: heatshrink.decompress(atob("jEYwMAkAGBnACBnwCBn+AAQPgAQPwAQP8AQP/AQXAAQPwAQP8AQP+AQgICBwQUCEAn4FggyBHAQ+CIgQ")), + pause: heatshrink.decompress(atob("jEYwMA/4BBAX4CEA")), + reset: heatshrink.decompress(atob("jEYwMA/4BB/+BAQPDAQPnAQIAKv///0///8j///EP//wAQQICBwQUCEhgyCHAQ+CIgI=")) +}; + +let state = storage.readJSON(STATE_PATH); +const STATE_DEFAULT = { + wasRunning: false, //If the stopwatch was ever running since being reset + sessionStart: 0, //When the stopwatch was first started + running: false, //Whether the stopwatch is currently running + startTime: 0, //When the stopwatch was last started. + pausedTime: 0, //When the stopwatch was last paused. + elapsedTime: 0 //How much time was spent running before the current start time. Update on pause. +}; +if (!state) { + state = STATE_DEFAULT; +} + +let lapFile; +let lapHistory; +if (state.wasRunning) { + lapFile = 'stlap-' + state.sessionStart + '.json'; + lapHistory = storage.readJSON(lapFile); + if (!lapHistory) + lapHistory = { + final: false, //Whether the stopwatch has been reset. It is expected that the stopwatch app will create a final split when reset. If this is false, it is expected that this hasn't been done, and that the current time should be used as the "final split" + splits: [] //List of times when the Lap button was pressed + }; +} else + lapHistory = { + final: false, //Whether the stopwatch has been reset. It is expected that the stopwatch app will create a final split when reset. If this is false, it is expected that this hasn't been done, and that the current time should be used as the "final split" + splits: [] //List of times when the Lap button was pressed + }; + +//Get the number of milliseconds that stopwatch has run for +function getTime() { + if (!state.wasRunning) { + //If the timer never ran, zero ms have passed + return 0; + } else if (state.running) { + //If the timer is running, the time left is current time - start time + preexisting time + return (new Date()).getTime() - state.startTime + state.elapsedTime; + } else { + //If the timer is not running, the same as above but use when the timer was paused instead of now. + return state.pausedTime - state.startTime + state.elapsedTime; + } +} + +let gestureMode = false; + +function drawButtons() { + //Draw the backdrop + const BAR_TOP = g.getHeight() - 48; + const BUTTON_Y = BAR_TOP + 12; + const BUTTON_LEFT = g.getWidth() / 4 - 12; //For the buttons, we have to subtract 12 because images do not obey alignment, but their size is known in advance + const TEXT_LEFT = g.getWidth() / 4; //For text, we do not have to subtract 12 because they do obey alignment. + const BUTTON_MID = g.getWidth() / 2 - 12; + const TEXT_MID = g.getWidth() / 2; + const BUTTON_RIGHT = g.getHeight() * 3 / 4 - 12; + + g.setColor(0, 0, 1).setFontAlign(0, -1) + .clearRect(0, BAR_TOP, g.getWidth(), g.getHeight()) + .fillRect(0, BAR_TOP, g.getWidth(), g.getHeight()) + .setColor(1, 1, 1); + + if (gestureMode) + g.setFont('Vector', 16) + .drawString('Button: Lap/Reset\nSwipe: Start/stop\nTap: Light', TEXT_MID, BAR_TOP); + else { + g.setFont('Vector', 24); + if (!state.wasRunning) { //If the timer was never running: + if (storage.read('stlapview.app.js') !== undefined) //If stlapview is installed, there should be a button to open it and a button to start the timer + g.drawLine(g.getWidth() / 2, BAR_TOP, g.getWidth() / 2, g.getHeight()) + .drawString("Laps", TEXT_LEFT, BUTTON_Y) + .drawImage(BUTTON_ICONS.play, BUTTON_RIGHT, BUTTON_Y); + else g.drawImage(BUTTON_ICONS.play, BUTTON_MID, BUTTON_Y); //Otherwise, only a button to start the timer + } else { //If the timer was running: + g.drawLine(g.getWidth() / 2, BAR_TOP, g.getWidth() / 2, g.getHeight()); + if (state.running) { //If it is running now, have a lap button and a pause button + g.drawString("LAP", TEXT_LEFT, BUTTON_Y) + .drawImage(BUTTON_ICONS.pause, BUTTON_RIGHT, BUTTON_Y); + } else { //If it is not running now, have a reset button and a + g.drawImage(BUTTON_ICONS.reset, BUTTON_LEFT, BUTTON_Y) + .drawImage(BUTTON_ICONS.play, BUTTON_RIGHT, BUTTON_Y); + } + } + } +} + +function drawTime() { + function pad(number) { + return ('00' + parseInt(number)).slice(-2); + } + + let time = getTime(); + g.reset(0, 0, 0) + .setFontAlign(0, 0) + .setFont("Vector", 36) + .clearRect(0, 24, g.getWidth(), g.getHeight() - 48) + + //Draw the time + .drawString((() => { + let hours = Math.floor(time / 3600000); + let minutes = Math.floor((time % 3600000) / 60000); + let seconds = Math.floor((time % 60000) / 1000); + let hundredths = Math.floor((time % 1000) / 10); + + if (hours >= 1) return `${hours}:${pad(minutes)}:${pad(seconds)}`; + else return `${minutes}:${pad(seconds)}:${pad(hundredths)}`; + })(), g.getWidth() / 2, g.getHeight() / 2); + + //Draw the lap labels if necessary + if (lapHistory.splits.length >= 1) { + let lastLap = lapHistory.splits.length; + let curLap = lastLap + 1; + + g.setFont("Vector", 12) + .drawString((() => { + let lapTime = time - lapHistory.splits[lastLap - 1]; + let hours = Math.floor(lapTime / 3600000); + let minutes = Math.floor((lapTime % 3600000) / 60000); + let seconds = Math.floor((lapTime % 60000) / 1000); + let hundredths = Math.floor((lapTime % 1000) / 10); + + if (hours == 0) return `Lap ${curLap}: ${pad(minutes)}:${pad(seconds)}:${pad(hundredths)}`; + else return `Lap ${curLap}: ${hours}:${pad(minutes)}:${pad(seconds)}:${pad(hundredths)}`; + })(), g.getWidth() / 2, g.getHeight() / 2 + 18) + .drawString((() => { + let lapTime; + if (lastLap == 1) lapTime = lapHistory.splits[lastLap - 1]; + else lapTime = lapHistory.splits[lastLap - 1] - lapHistory.splits[lastLap - 2]; + let hours = Math.floor(lapTime / 3600000); + let minutes = Math.floor((lapTime % 3600000) / 60000); + let seconds = Math.floor((lapTime % 60000) / 1000); + let hundredths = Math.floor((lapTime % 1000) / 10); + + if (hours == 0) return `Lap ${lastLap}: ${pad(minutes)}:${pad(seconds)}:${pad(hundredths)}`; + else return `Lap ${lastLap}: ${hours}:${pad(minutes)}:${pad(seconds)}:${pad(hundredths)}`; + })(), g.getWidth() / 2, g.getHeight() / 2 + 30); + } +} + +drawButtons(); + +function firstTimeStart(now, time) { + state = { + wasRunning: true, + sessionStart: Math.floor(now), + running: true, + startTime: now, + pausedTime: 0, + elapsedTime: 0, + }; + lapFile = 'stlap-' + state.sessionStart + '.json'; + setupTimerInterval(); + Bangle.buzz(200); + drawButtons(); +} + +function split(now, time) { + lapHistory.splits.push(time); + Bangle.buzz(); +} + +function pause(now, time) { + //Record the exact moment that we paused + state.pausedTime = now; + + //Stop the timer + state.running = false; + stopTimerInterval(); + Bangle.buzz(200); + drawTime(); + drawButtons(); +} + +function reset(now, time) { + //Record the time + lapHistory.splits.push(time); + lapHistory.final = true; + storage.writeJSON(lapFile, lapHistory); + + //Reset the timer + state = STATE_DEFAULT; + lapHistory = { + final: false, + splits: [] + }; + Bangle.buzz(500); + drawTime(); + drawButtons(); +} + +function start(now, time) { + //Start the timer and record when we started + state.elapsedTime += (state.pausedTime - state.startTime); + state.startTime = now; + state.running = true; + setupTimerInterval(); + Bangle.buzz(200); + drawTime(); + drawButtons(); +} + +Bangle.on("touch", (button, xy) => { + //In gesture mode, just turn on the light and then return + if (gestureMode) { + Bangle.setLCDPower(true); + return; + } + + //If we support full touch and we're not touching the keys, ignore. + //If we don't support full touch, we can't tell so just assume we are. + if (xy !== undefined && xy.y <= g.getHeight() - 48) return; + + let now = (new Date()).getTime(); + let time = getTime(); + + if (!state.wasRunning) { + if (storage.read('stlapview.app.js') !== undefined) { + //If we were never running and stlapview is installed, there are two buttons: open stlapview and start the timer + if (button == 1) load('stlapview.app.js'); + else firstTimeStart(now, time); + } + //If stlapview there is only one button: the start button + else firstTimeStart(now, time); + } else if (state.running) { + //If we are running, there are two buttons: lap and pause + if (button == 1) split(now, time); + else pause(now, time); + + } else { + //If we are stopped, there are two buttons: reset and continue + if (button == 1) reset(now, time); + else start(now, time); + } +}); + +Bangle.on('swipe', direction => { + let now = (new Date()).getTime(); + let time = getTime(); + + if (gestureMode) { + Bangle.setLCDPower(true); + if (!state.wasRunning) firstTimeStart(now, time); + else if (state.running) pause(now, time); + else start(now, time); + } else { + gestureMode = true; + Bangle.setOptions({ + lockTimeout: 0 + }); + drawTime(); + drawButtons(); + } +}); + +setWatch(() => { + let now = (new Date()).getTime(); + let time = getTime(); + + if (gestureMode) { + Bangle.setLCDPower(true); + if (state.running) split(now, time); + else reset(now, time); + } +}, BTN1, { repeat: true }); + +let timerInterval; + +function setupTimerInterval() { + if (timerInterval !== undefined) { + clearInterval(timerInterval); + } + timerInterval = setInterval(drawTime, 10); +} + +function stopTimerInterval() { + if (timerInterval !== undefined) { + clearInterval(timerInterval); + timerInterval = undefined; + } +} + +drawTime(); +if (state.running) { + setupTimerInterval(); +} + +//Save our state when the app is closed +E.on('kill', () => { + storage.writeJSON(STATE_PATH, state); + if (state.wasRunning) { + storage.writeJSON(lapFile, lapHistory); + } +}); + +Bangle.loadWidgets(); +Bangle.drawWidgets(); \ No newline at end of file diff --git a/apps/stlap/icon.js b/apps/stlap/icon.js new file mode 100644 index 000000000..eb0ec64c7 --- /dev/null +++ b/apps/stlap/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwcCpMkyQCCB4oLFAQsf/4AC/ARvIYM//hHNAQIRBBxgRliQECCJgHFCJWJA4slCJEiDI+UIhYCFAw1IJ5NSAwtECI8/yVKNBYCC/5uGyIRHp/8AoMpCJvkCIyMGAQN/CISSECI+T/6kHPQ+f/IFD0gRL5IRP/4RHag8n/zaHCI8/+QRQfxARHYQQRNYQYROWAQ1OAQwR4YpACFa5YRbxIRLkoGDCKORCJcpCKuSCJYGFogRJpQGFpARJqQJGiQRIDY7aIagYCFSQyMEAQxoLJRQLG")) \ No newline at end of file diff --git a/apps/stlap/icon.png b/apps/stlap/icon.png new file mode 100644 index 000000000..6b9ba85bb Binary files /dev/null and b/apps/stlap/icon.png differ diff --git a/apps/stlap/img/pause.png b/apps/stlap/img/pause.png new file mode 100644 index 000000000..ad31dadcf Binary files /dev/null and b/apps/stlap/img/pause.png differ diff --git a/apps/stlap/img/play.png b/apps/stlap/img/play.png new file mode 100644 index 000000000..6c20c24c5 Binary files /dev/null and b/apps/stlap/img/play.png differ diff --git a/apps/stlap/img/reset.png b/apps/stlap/img/reset.png new file mode 100644 index 000000000..7a317d097 Binary files /dev/null and b/apps/stlap/img/reset.png differ diff --git a/apps/stlap/metadata.json b/apps/stlap/metadata.json new file mode 100644 index 000000000..1ecdc5b6e --- /dev/null +++ b/apps/stlap/metadata.json @@ -0,0 +1,24 @@ +{ + "id": "stlap", + "name": "Stopwatch", + "version": "0.03", + "description": "A stopwatch that remembers its state, with a lap timer and a gesture mode (enable by swiping)", + "icon": "icon.png", + "type": "app", + "tags": "tools,app", + "supports": [ + "BANGLEJS2" + ], + "allow_emulator": true, + "storage": [ + { + "name": "stlap.app.js", + "url": "app.js" + }, + { + "name": "stlap.img", + "url": "icon.js", + "evaluate": true + } + ] +} \ No newline at end of file diff --git a/apps/stlapview/ChangeLog b/apps/stlapview/ChangeLog new file mode 100644 index 000000000..c819919ed --- /dev/null +++ b/apps/stlapview/ChangeLog @@ -0,0 +1,2 @@ +0.01: New app! +0.02: Submitted to the app loader \ No newline at end of file diff --git a/apps/stlapview/app.js b/apps/stlapview/app.js new file mode 100644 index 000000000..7a58b5782 --- /dev/null +++ b/apps/stlapview/app.js @@ -0,0 +1,110 @@ +const storage = require("Storage"); + +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +function pad(number) { + return ('00' + parseInt(number)).slice(-2); +} + +function fileNameToDateString(fileName) { + let timestamp = 0; + let foundDigitYet = false; + for (let character of fileName) { + if ('1234567890'.includes(character)) { + foundDigitYet = true; + timestamp *= 10; + timestamp += parseInt(character); + } else if (foundDigitYet) break; + } + let date = new Date(timestamp); + + let dayOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][date.getDay()]; + let completed = storage.readJSON(fileName).final; + + return `${dayOfWeek} ${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}` + (completed ? '' : ' (running)'); +} + +function msToHumanReadable(ms) { + + let hours = Math.floor(ms / 3600000); + let minutes = Math.floor((ms % 3600000) / 60000); + let seconds = Math.floor((ms % 60000) / 1000); + let hundredths = Math.floor((ms % 1000) / 10); + + return `${hours}:${pad(minutes)}:${pad(seconds)}:${pad(hundredths)}`; +} + +function view(fileName) { + let lapTimes = []; + let fileData = storage.readJSON(fileName).splits; + for (let i = 0; i < fileData.length; i++) { + if (i == 0) lapTimes.push(fileData[i]); + else lapTimes.push(fileData[i] - fileData[i - 1]); + } + + let fastestIndex = 0; + let slowestIndex = 0; + for (let i = 0; i < lapTimes.length; i++) { + if (lapTimes[i] < lapTimes[fastestIndex]) fastestIndex = i; + else if (lapTimes[i] > lapTimes[slowestIndex]) slowestIndex = i; + } + + let lapMenu = { + '': { + 'title': fileNameToDateString(fileName), + 'back': () => { E.showMenu(mainMenu); } + }, + }; + lapMenu[`Total time: ${msToHumanReadable(fileData[fileData.length - 1])}`] = () => { }; + lapMenu[`Fastest lap: ${fastestIndex + 1}: ${msToHumanReadable(lapTimes[fastestIndex])}`] = () => { }; + lapMenu[`Slowest lap: ${slowestIndex + 1}: ${msToHumanReadable(lapTimes[slowestIndex])}`] = () => { }; + lapMenu[`Average lap: ${msToHumanReadable(fileData[fileData.length - 1] / fileData.length)}`] = () => { }; + + for (let i = 0; i < lapTimes.length; i++) { + lapMenu[`Lap ${i + 1}: ${msToHumanReadable(lapTimes[i])}`] = () => { }; + } + + lapMenu.Delete = () => { + E.showMenu({ + '': { + 'title': 'Are you sure?', + 'back': () => { E.showMenu(lapMenu); } + }, + 'Yes': () => { + storage.erase(fileName); + showMainMenu(); + }, + 'No': () => { E.showMenu(lapMenu); } + }); + }; + + E.showMenu(lapMenu); +} + +function showMainMenu() { + let LAP_FILES = storage.list(/stlap-[0-9]*\.json/); + LAP_FILES.sort(); + LAP_FILES.reverse(); + + let mainMenu = { + '': { + 'title': 'Sessions' + } + }; + + //I know eval is evil, but I can't think of any other way to do this. + for (let lapFile of LAP_FILES) { + mainMenu[fileNameToDateString(lapFile)] = eval(`(function() { + view('${lapFile}'); + })`); + } + + if (LAP_FILES.length == 0) { + mainMenu['No data'] = _ => { load(); }; + } + + E.showMenu(mainMenu); +} + +showMainMenu(); \ No newline at end of file diff --git a/apps/stlapview/icon.js b/apps/stlapview/icon.js new file mode 100644 index 000000000..9e10b10a8 --- /dev/null +++ b/apps/stlapview/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwcCpMkyQCCB4oLFAQsf/4AC/ARvIYM//hHNAQIRBBxgRliQECCJgHFCJWJA4slCJEiDI+UIhYCFAw1IJ5NSAwtECI8/yVKNBOQYYMCpP/Nw2RAgYQBAAMP/gIBlIRHCAYAB8gRGRgVICIsESQwRCoANBA4OAAgIRGJgQRBSQWeCIck0gRFBYmf4AXDCI5BCkn/5ARGagSMBCIUn/xfBSQLaDCIiwD+SDBCJyVCCJrCCCJtPYQQROYQQ1OboYRKR4bdCCJChFCIShCCI7FDbooRCdJAXFa4wRBgAFBwARLIIIAFfY2JO4YAFNYMlQ4YRDkgSGCIuRAgaSBEAI7ChMpCJCzFgEBCImSCJEgCQPpBIlECI5NCjoJEpARISQMDPoYCBWAYCEL4V0BIjaDAQmeCI6SFAQMlkvACI5uGAYO4CJBKEBAWX//0yQ=")) \ No newline at end of file diff --git a/apps/stlapview/icon.png b/apps/stlapview/icon.png new file mode 100644 index 000000000..be24b8e6f Binary files /dev/null and b/apps/stlapview/icon.png differ diff --git a/apps/stlapview/metadata.json b/apps/stlapview/metadata.json new file mode 100644 index 000000000..aa54a7b67 --- /dev/null +++ b/apps/stlapview/metadata.json @@ -0,0 +1,27 @@ +{ + "id": "stlapview", + "name": "Stopwatch laps", + "version": "0.02", + "description": "Optional lap viewer for my stopwatch app", + "icon": "icon.png", + "type": "app", + "tags": "tools,app", + "supports": [ + "BANGLEJS2" + ], + "allow_emulator": true, + "storage": [ + { + "name": "stlapview.app.js", + "url": "app.js" + }, + { + "name": "stlapview.img", + "url": "icon.js", + "evaluate": true + } + ], + "dependencies": { + "stlap": "app" + } +} \ No newline at end of file diff --git a/apps/swiperclocklaunch/ChangeLog b/apps/swiperclocklaunch/ChangeLog index 244b602b5..e7ad4555c 100644 --- a/apps/swiperclocklaunch/ChangeLog +++ b/apps/swiperclocklaunch/ChangeLog @@ -1,3 +1,4 @@ 0.01: New App! 0.02: Fix issue with mode being undefined 0.03: Update setUI to work with new Bangle.js 2v13 menu style +0.04: Update to work with new 'fast switch' clock->launcher functionality diff --git a/apps/swiperclocklaunch/boot.js b/apps/swiperclocklaunch/boot.js index bb285ea94..ea00a6735 100644 --- a/apps/swiperclocklaunch/boot.js +++ b/apps/swiperclocklaunch/boot.js @@ -1,19 +1,19 @@ -// clock -> launcher (function() { - var sui = Bangle.setUI; - Bangle.setUI = function(mode, cb) { - sui(mode,cb); - if(!mode) return; - if ("object"==typeof mode) mode = mode.mode; - if (!mode.startsWith("clock")) return; - Bangle.swipeHandler = dir => { if (dir<0) Bangle.showLauncher(); }; - Bangle.on("swipe", Bangle.swipeHandler); - }; -})(); -// launcher -> clock -setTimeout(function() { - if (global.__FILE__ && __FILE__.endsWith(".app.js") && (require("Storage").readJSON(__FILE__.slice(0,-6)+"info",1)||{}).type=="launch") { + var sui = Bangle.setUI; + Bangle.setUI = function(mode, cb) { + sui(mode,cb); + if(!mode) return; + if ("object"==typeof mode) mode = mode.mode; + if (mode.startsWith("clock")) { + // clock -> launcher + Bangle.swipeHandler = dir => { if (dir<0) Bangle.showLauncher(); }; + Bangle.on("swipe", Bangle.swipeHandler); + } else { + if (global.__FILE__ && __FILE__.endsWith(".app.js") && (require("Storage").readJSON(__FILE__.slice(0,-6)+"info",1)||{}).type=="launch") { + // launcher -> clock Bangle.swipeHandler = dir => { if (dir>0) load(); }; Bangle.on("swipe", Bangle.swipeHandler); + } } -}, 10); + }; +})(); diff --git a/apps/swiperclocklaunch/metadata.json b/apps/swiperclocklaunch/metadata.json index 5e4a0d648..4f27da528 100644 --- a/apps/swiperclocklaunch/metadata.json +++ b/apps/swiperclocklaunch/metadata.json @@ -1,7 +1,7 @@ { "id": "swiperclocklaunch", "name": "Swiper Clock Launch", - "version": "0.03", + "version": "0.04", "description": "Navigate between clock and launcher with Swipe action", "icon": "swiperclocklaunch.png", "type": "bootloader", diff --git a/apps/torch/ChangeLog b/apps/torch/ChangeLog index fd904b6e8..22b1c179a 100644 --- a/apps/torch/ChangeLog +++ b/apps/torch/ChangeLog @@ -3,3 +3,4 @@ 0.03: Add Color Changing Settings 0.04: Add Support For Bangle.js 2 0.05: Default full Brightness +0.06: Press upper left corner to exit on Bangle.js 2 diff --git a/apps/torch/README.md b/apps/torch/README.md new file mode 100644 index 000000000..e06861071 --- /dev/null +++ b/apps/torch/README.md @@ -0,0 +1,2 @@ +On Bangle.js 2, pressing the upper left corner where the red back button would be exits the app if the screen is unlocked. + diff --git a/apps/torch/app.js b/apps/torch/app.js index 2af35fdb6..418463144 100644 --- a/apps/torch/app.js +++ b/apps/torch/app.js @@ -17,4 +17,8 @@ g.fillRect(0,0,g.getWidth(),g.getHeight()); setWatch(()=>load(), BTN1); if (global.BTN2) setWatch(()=>load(), BTN2); if (global.BTN3) setWatch(()=>load(), BTN3); - +// Pressing upper left corner turns off (where red back button would be) +Bangle.setUI({ + mode : 'custom', + back : load +}); diff --git a/apps/torch/metadata.json b/apps/torch/metadata.json index af85370ac..580ae4e8a 100644 --- a/apps/torch/metadata.json +++ b/apps/torch/metadata.json @@ -2,7 +2,7 @@ "id": "torch", "name": "Torch", "shortName": "Torch", - "version": "0.05", + "version": "0.06", "description": "Turns screen white to help you see in the dark. Select from the launcher or press BTN1,BTN3,BTN1,BTN3 quickly to start when in any app that shows widgets on Bangle.js 1. You can also set the color through the app's setting menu.", "icon": "app.png", "tags": "tool,torch", diff --git a/apps/waypointer/ChangeLog b/apps/waypointer/ChangeLog index 292d77a99..8613ef799 100644 --- a/apps/waypointer/ChangeLog +++ b/apps/waypointer/ChangeLog @@ -2,3 +2,6 @@ 0.02: Make Bangle.js 2 compatible 0.03: Silently use built in heading when no magnav calibration file is present 0.04: Move waypoints.json (and editor) to 'waypoints' app +0.05: Fix not displaying of wpindex = 0 +0.06: Added adjustment for Bangle.js magnetometer heading fix +0.07: Add settings file with the option to disable the slow direction updates diff --git a/apps/waypointer/app.js b/apps/waypointer/app.js index 9fd288c9a..de6bfdcaa 100644 --- a/apps/waypointer/app.js +++ b/apps/waypointer/app.js @@ -8,6 +8,11 @@ var buf1 = Graphics.createArrayBuffer(160*scale,160*scale,1, {msb:true}); var buf2 = Graphics.createArrayBuffer(g.getWidth()/3,40*scale,1, {msb:true}); var arrow_img = require("heatshrink").decompress(atob("lEowIPMjAEDngEDvwED/4DCgP/wAEBgf/4AEBg//8AEBh//+AEBj///AEBn///gEBv///wmCAAImCAAIoBFggE/AkaaEABo=")); +var settings = Object.assign({ + // default values + smoothDirection: true, +}, require('Storage').readJSON("waypointer.json", true) || {}); + function flip1(x,y) { g.drawImage({width:160*scale,height:160*scale,bpp:1,buffer:buf1.buffer, palette:pal_by},x,y); buf1.clear(); @@ -68,7 +73,12 @@ function newHeading(m,h){ var delta = (m>h)?1:-1; if (s>=180){s=360-s; delta = -delta;} if (s<2) return h; - var hd = h + delta*(1 + Math.round(s/5)); + var hd; + if (settings.smoothDirection) { + hd = h + delta*(1 + Math.round(s/5)); + } else { + hd = h + delta*s; + } if (hd<0) hd+=360; if (hd>360)hd-= 360; return hd; @@ -80,7 +90,7 @@ function tiltfixread(O,S){ var m = Bangle.getCompass(); if (O === undefined || S === undefined) { // no valid calibration from magnav, use built in - return 360-m.heading; + return m.heading; } var g = Bangle.getAccel(); m.dx =(m.x-O.x)*S.x; m.dy=(m.y-O.y)*S.y; m.dz=(m.z-O.z)*S.z; @@ -263,7 +273,7 @@ function nextwp(inc){ } function doselect(){ - if (selected && wpindex!=0 && waypoints[wpindex].lat===undefined && savedfix.fix) { + if (selected && wpindex>=0 && waypoints[wpindex].lat===undefined && savedfix.fix) { waypoints[wpindex] ={name:"@"+wp.name, lat:savedfix.lat, lon:savedfix.lon}; wp = waypoints[wpindex]; require("waypoints").save(waypoints); diff --git a/apps/waypointer/metadata.json b/apps/waypointer/metadata.json index 8b923c604..0bbc42322 100644 --- a/apps/waypointer/metadata.json +++ b/apps/waypointer/metadata.json @@ -1,7 +1,7 @@ { "id": "waypointer", "name": "Way Pointer", - "version": "0.04", + "version": "0.07", "description": "Navigate to a waypoint using the GPS for bearing and compass to point way, uses the same waypoint interface as GPS Navigation", "icon": "waypointer.png", "tags": "tool,outdoors,gps", @@ -10,6 +10,8 @@ "readme": "README.md", "storage": [ {"name":"waypointer.app.js","url":"app.js"}, + {"name":"waypointer.settings.js","url":"settings.js"}, {"name":"waypointer.img","url":"icon.js","evaluate":true} - ] + ], + "data": [{"name":"waypointer.json"}] } diff --git a/apps/waypointer/settings.js b/apps/waypointer/settings.js new file mode 100644 index 000000000..c8b06b9f9 --- /dev/null +++ b/apps/waypointer/settings.js @@ -0,0 +1,25 @@ +(function(back) { + var FILE = "waypointer.json"; + // Load settings + var settings = Object.assign({ + smoothDirection: true, + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + // Show the menu + E.showMenu({ + "" : { "title" : "Way Pointer" }, + "< Back" : () => back(), + 'Smooth arrow rot': { + value: !!settings.smoothDirection, + format: v => v?"Yes":"No", + onchange: v => { + settings.smoothDirection = v; + writeSettings(); + } + }, + }); +}) diff --git a/apps/weather/readme.md b/apps/weather/readme.md index b37d0b38e..2187ef061 100644 --- a/apps/weather/readme.md +++ b/apps/weather/readme.md @@ -14,6 +14,34 @@ You can view the full report through the app: If using the `Bangle.js Gadgetbridge` app on your phone (as opposed to the standard F-Droid `Gadgetbridge`) you need to set the package name to `com.espruino.gadgetbridge.banglejs` in the settings of the weather app (`settings -> gadgetbridge support -> package name`). +## Android Weather Apps + +There are two weather apps for Android that can connect with Gadgetbridge + * Tiny Weather Forecast Germany + ** F-Droid - https://f-droid.org/en/packages/de.kaffeemitkoffein.tinyweatherforecastgermany/ + ** Source code - https://codeberg.org/Starfish/TinyWeatherForecastGermany + * QuickWeather + ** F-Droid - https://f-droid.org/en/packages/com.ominous.quickweather/ + ** Google Play - https://play.google.com/store/apps/details?id=com.ominous.quickweather + ** Source code - https://github.com/TylerWilliamson/QuickWeather + + ### Tiny Weather Forecast Germany + Even though Tiny Weather Forecast Germany is made for Germany, it can be used around the world. To do this: + +1. Tap on the three dots in the top right hand corner and go to settings +2. Go down to Location and tap on the checkbox labeled "Use location services". You may also want to check on the "Check Location checkbox". Alternatively, you may select the "manual" checkbox and choose your location. +3. Scroll down further to the "other" section and tap "Gadgetbridge support". Then tap on "Enable". You may also choose to tap on "Send current time". +4. If you're using the specific Gadgetbridge for Bangle.JS app, you'll want to tap on "Package name." In the dialog box that appears, you'll want to put in "com.espruino.gadgetbridge.banglejs" without the quotes. If you're using the original Gadgetbridge, leave this as the default. + + +### QuickWeather +QuickWeather requires an OpenWeatherMap API. You will need the "One Call By Call" plan, which is free if you're not making too many calls. Sign up or get more information at https://openweathermap.org/api + +1. When you first load QuickWeather, it will take you through the setup process. You will fill out all the required information as well as put your API key in. If you do not have the "One Call By Call", or commonly known as "One Call", API, you will need to sign up for that. QuickWeather will work automatically with both the main version of Gadgetbridge and Gadgetbridge for bangle.JS. + +### Weather Notification +* Note - at one time, the Weather Notification app also worked with Gadgetbridge. However, many users are reporting it's no longer seeing the OpenWeatherMap API key as valid. The app has not received any updates since August of 2020, and may be unmaintained. + ## Settings * Expiration timespan can be set after which the local weather data is considered as invalid diff --git a/apps/wid_edit/ChangeLog b/apps/wid_edit/ChangeLog index 2fa857bd8..d8e165029 100644 --- a/apps/wid_edit/ChangeLog +++ b/apps/wid_edit/ChangeLog @@ -1 +1,4 @@ -0.01: new Widget Editor! \ No newline at end of file +0.01: new Widget Editor! +0.02: Wrap loadWidgets instead of replacing to keep original functionality intact + Change back entry to menu option + Allow changing widgets into all areas, including bottom widget bar diff --git a/apps/wid_edit/boot.js b/apps/wid_edit/boot.js index 872965c97..3cb545a34 100644 --- a/apps/wid_edit/boot.js +++ b/apps/wid_edit/boot.js @@ -1,24 +1,20 @@ -Bangle.loadWidgets = function() { - global.WIDGETS={}; - require("Storage").list(/\.wid\.js$/) - .forEach(w=>{ - try { eval(require("Storage").read(w)); } - catch (e) { print(w, e); } - }); - const s = require("Storage").readJSON("wid_edit.json", 1) || {}, - c = s.custom || {}; +Bangle.loadWidgets = (o => ()=>{ + o(); + const s = require("Storage").readJSON("wid_edit.json", 1) || {}; + const c = s.custom || {}; for (const w in c){ if (!(w in WIDGETS)) continue; // widget no longer exists // store defaults of customized values in _WIDGETS - global._WIDGETS=global._WIDGETS||{}; - _WIDGETS[w] = {}; - Object.keys(c[w]).forEach(k => _WIDGETS[w][k] = WIDGETS[w][k]); - Object.assign(WIDGETS[w], c[w]); + if (!global._WIDGETS) global._WIDGETS = {}; + if (!global._WIDGETS[w]) global._WIDGETS[w] = {}; + Object.keys(c[w]).forEach(k => global._WIDGETS[w][k] = global.WIDGETS[w][k]); + //overide values in widget with configured ones + Object.assign(global.WIDGETS[w], c[w]); } - const W = WIDGETS; - WIDGETS = {}; + const W = global.WIDGETS; + global.WIDGETS = {}; Object.keys(W) .sort() .sort((a, b) => (0|W[b].sortorder)-(0|W[a].sortorder)) - .forEach(k => WIDGETS[k] = W[k]); -} \ No newline at end of file + .forEach(k => global.WIDGETS[k] = W[k]); +})(Bangle.loadWidgets); diff --git a/apps/wid_edit/metadata.json b/apps/wid_edit/metadata.json index 66d0192f6..d963a53d0 100644 --- a/apps/wid_edit/metadata.json +++ b/apps/wid_edit/metadata.json @@ -1,6 +1,6 @@ { "id": "wid_edit", - "version": "0.01", + "version": "0.02", "name": "Widget Editor", "icon": "icon.png", "description": "Customize widget locations", diff --git a/apps/wid_edit/settings.js b/apps/wid_edit/settings.js index 0969ed533..1d34ae0ca 100644 --- a/apps/wid_edit/settings.js +++ b/apps/wid_edit/settings.js @@ -9,7 +9,7 @@ let cleanup = false; for (const id in settings.custom) { - if (!(id in WIDGETS)) { + if (!(id in global.WIDGETS)) { // widget which no longer exists cleanup = true; delete settings.custom[id]; @@ -24,12 +24,12 @@ * Sort & redraw all widgets */ function redrawWidgets() { - let W = WIDGETS; + let W = global.WIDGETS; global.WIDGETS = {}; Object.keys(W) .sort() .sort((a, b) => (0|W[b].sortorder)-(0|W[a].sortorder)) - .forEach(k => {WIDGETS[k] = W[k]}); + .forEach(k => {global.WIDGETS[k] = W[k];}); Bangle.drawWidgets(); } @@ -52,9 +52,9 @@ } function edit(id) { - let WIDGET = WIDGETS[id], + let WIDGET = global.WIDGETS[id], def = {area: WIDGET.area, sortorder: WIDGET.sortorder|0}; // default values - Object.assign(def, _WIDGETS[id]||{}); // defaults were saved in _WIDGETS + Object.assign(def, global._WIDGETS[id]||{}); // defaults were saved in _WIDGETS settings.custom = settings.custom||{}; let saved = settings.custom[id] || {}, @@ -102,7 +102,7 @@ settings.custom = settings.custom || {}; settings.custom[id] = saved; } else if (settings.custom) { - delete settings.custom[id] + delete settings.custom[id]; } if (!Object.keys(settings.custom).length) delete settings.custom; require("Storage").writeJSON("wid_edit.json", settings); @@ -112,8 +112,8 @@ let _W = {}; if (saved.area) _W.area = def.area; if ('sortorder' in saved) _W.sortorder = def.sortorder; - if (Object.keys(_W).length) _WIDGETS[id] = _W; - else delete _WIDGETS[id]; + if (Object.keys(_W).length) global._WIDGETS[id] = _W; + else delete global._WIDGETS[id]; // drawWidgets won't clear e.g. bottom bar if we just disabled the last bottom widget redrawWidgets(); @@ -122,17 +122,23 @@ m.draw(); } + const AREA_NAMES = [ "Top left", "Top right", "Bottom left", "Bottom right" ]; + const AREAS = [ "tl", "tr", "bl", "br" ]; + const menu = { - "": {"title": name(id)}, - /*LANG*/"< Back": () => { - redrawWidgets(); - mainMenu(); - }, - /*LANG*/"Side": { - value: (area === 'tl'), - format: tl => tl ? /*LANG*/"Left" : /*LANG*/"Right", - onchange: tl => { - area = tl ? "tl" : "tr"; + "": {"title": name(id), + back: () => { + redrawWidgets(); + mainMenu(); + } }, + /*LANG*/"Position": { + value: AREAS.indexOf(area), + format: v => AREA_NAMES[v], + min: 0, + max: AREAS.length - 1, + onchange: v => { + print("v", v); + area = AREAS[v]; save(); } }, @@ -149,7 +155,7 @@ save(); mainMenu(); // changing multiple values made the rest of the menu wrong, so take the easy out } - } + }; let m = E.showMenu(menu); } @@ -157,31 +163,32 @@ function mainMenu() { let menu = { - "": {"title": /*LANG*/"Widgets"}, + "": { + "title": /*LANG*/"Widgets", + back: ()=>{ + if (!Object.keys(global._WIDGETS).length) delete global._WIDGETS; // no defaults to remember + back(); + }}, }; - menu[/*LANG*/"< Back"] = ()=>{ - if (!Object.keys(_WIDGETS).length) delete _WIDGETS; // no defaults to remember - back(); - }; - Object.keys(WIDGETS).forEach(id=>{ + Object.keys(global.WIDGETS).forEach(id=>{ // mark customized widgets with asterisk - menu[name(id)+((id in _WIDGETS) ? " *" : "")] = () => edit(id); + menu[name(id)+((id in global._WIDGETS) ? " *" : "")] = () => edit(id); }); - if (Object.keys(_WIDGETS).length) { // only show reset if there is anything to reset + if (Object.keys(global._WIDGETS).length) { // only show reset if there is anything to reset menu[/*LANG*/"Reset All"] = () => { E.showPrompt(/*LANG*/"Reset all widgets?").then(confirm => { if (confirm) { delete settings.custom; require("Storage").writeJSON("wid_edit.json", settings); - for(let id in _WIDGETS) { - Object.assign(WIDGETS[id], _WIDGETS[id]) // restore defaults + for(let id in global._WIDGETS) { + Object.assign(global.WIDGETS[id], global._WIDGETS[id]); // restore defaults } global._WIDGETS = {}; redrawWidgets(); } mainMenu(); // reload with reset widgets - }) - } + }); + }; } E.showMenu(menu); diff --git a/apps/widbars/ChangeLog b/apps/widbars/ChangeLog index 61e28e6e4..0065c1b27 100644 --- a/apps/widbars/ChangeLog +++ b/apps/widbars/ChangeLog @@ -1,3 +1,4 @@ 0.01: New Widget! 0.02: Battery bar turns yellow on charge Memory status bar does not trigger garbage collect +0.03: Set battery color on load diff --git a/apps/widbars/metadata.json b/apps/widbars/metadata.json index a9981305c..06965e77e 100644 --- a/apps/widbars/metadata.json +++ b/apps/widbars/metadata.json @@ -1,7 +1,7 @@ { "id": "widbars", "name": "Bars Widget", - "version": "0.02", + "version": "0.03", "description": "Display several measurements as vertical bars.", "icon": "icon.png", "screenshots": [{"url":"screenshot.png"}], diff --git a/apps/widbars/widget.js b/apps/widbars/widget.js index cceeb0897..0a4bba76c 100644 --- a/apps/widbars/widget.js +++ b/apps/widbars/widget.js @@ -42,7 +42,7 @@ if (top) g .clearRect(x,y, x+w-1,y+top-1); // erase above bar if (f) g.setColor(col).fillRect(x,y+top, x+w-1,y+h-1); // even for f=0.001 this is still 1 pixel high } - let batColor='#0f0'; + let batColor=Bangle.isCharging()?'#ff0':'#0f0'; function draw() { g.reset(); const x = this.x, y = this.y, diff --git a/apps/widclk/ChangeLog b/apps/widclk/ChangeLog index c74857ab4..1a89e2780 100644 --- a/apps/widclk/ChangeLog +++ b/apps/widclk/ChangeLog @@ -3,3 +3,4 @@ 0.04: Fix regression stopping correct widget updates 0.05: Don't show clock widget if already showing clock app 0.06: Use 7 segment font, update *on* the minute, use less memory +0.07: allow turning on/off when quick-switching apps diff --git a/apps/widclk/metadata.json b/apps/widclk/metadata.json index 6996f4080..b7bc74e11 100644 --- a/apps/widclk/metadata.json +++ b/apps/widclk/metadata.json @@ -1,7 +1,7 @@ { "id": "widclk", "name": "Digital clock widget", - "version": "0.06", + "version": "0.07", "description": "A simple digital clock widget", "icon": "widget.png", "type": "widget", diff --git a/apps/widclk/widget.js b/apps/widclk/widget.js index 9e035ca9a..7c281f761 100644 --- a/apps/widclk/widget.js +++ b/apps/widclk/widget.js @@ -1,10 +1,13 @@ /* Simple clock that appears in the widget bar if no other clock is running. We update once per minute, but don't bother stopping if the */ - -// don't show widget if we know we have a clock app running -if (!Bangle.CLOCK) WIDGETS["wdclk"]={area:"tl",width:52/* g.stringWidth("00:00") */,draw:function() { - g.reset().setFontCustom(atob("AAAAAAAAAAIAAAQCAQAAAd0BgMBdwAAAAAAAdwAB0RiMRcAAAERiMRdwAcAQCAQdwAcERiMRBwAd0RiMRBwAAEAgEAdwAd0RiMRdwAcERiMRdwAFAAd0QiEQdwAdwRCIRBwAd0BgMBAAABwRCIRdwAd0RiMRAAAd0QiEQAAAAAAAAAA="), 32, atob("BgAAAAAAAAAAAAAAAAYCAAYGBgYGBgYGBgYCAAAAAAAABgYGBgYG"), 512+9); +WIDGETS["wdclk"]={area:"tl",width:Bangle.CLOCK?0:52/* g.stringWidth("00:00") */,draw:function() { + if (!Bangle.CLOCK == !this.width) { // if we're the wrong size for if we have a clock or not... + this.width = Bangle.CLOCK?0:52; + return setTimeout(Bangle.drawWidgets,1); // widget changed size - redraw + } + if (!this.width) return; // if size not right, return +g.reset().setFontCustom(atob("AAAAAAAAAAIAAAQCAQAAAd0BgMBdwAAAAAAAdwAB0RiMRcAAAERiMRdwAcAQCAQdwAcERiMRBwAd0RiMRBwAAEAgEAdwAd0RiMRdwAcERiMRdwAFAAd0QiEQdwAdwRCIRBwAd0BgMBAAABwRCIRdwAd0RiMRAAAd0QiEQAAAAAAAAAA="), 32, atob("BgAAAAAAAAAAAAAAAAYCAAYGBgYGBgYGBgYCAAAAAAAABgYGBgYG"), 512+9); var time = require("locale").time(new Date(),1); g.drawString(time, this.x, this.y+3, true); // 5 * 6*2 = 60 // queue draw in one minute diff --git a/apps/widclkbttm/ChangeLog b/apps/widclkbttm/ChangeLog index 326169af5..9dc8f8d2c 100644 --- a/apps/widclkbttm/ChangeLog +++ b/apps/widclkbttm/ChangeLog @@ -1,4 +1,4 @@ 0.01: Fork of widclk v0.04 github.com/espruino/BangleApps/tree/master/apps/widclk 0.02: Modification for bottom widget area and text color 0.03: based in widclk v0.05 compatible at same time, bottom area and color - +0.04: refactored to use less memory, and allow turning on/off when quick-switching apps diff --git a/apps/widclkbttm/metadata.json b/apps/widclkbttm/metadata.json index 9e92f7c46..7c5fe4b63 100644 --- a/apps/widclkbttm/metadata.json +++ b/apps/widclkbttm/metadata.json @@ -2,8 +2,8 @@ "id": "widclkbttm", "name": "Digital clock (Bottom) widget", "shortName": "Digital clock Bottom Widget", - "version": "0.03", - "description": "Displays time in the bottom area.", + "version": "0.04", + "description": "Displays time in the bottom of the screen (may not be compatible with some apps)", "icon": "widclkbttm.png", "type": "widget", "tags": "widget", diff --git a/apps/widclkbttm/widclkbttm.wid.js b/apps/widclkbttm/widclkbttm.wid.js index c27906786..c5e85318c 100644 --- a/apps/widclkbttm/widclkbttm.wid.js +++ b/apps/widclkbttm/widclkbttm.wid.js @@ -1,31 +1,16 @@ -(function() { - // don't show widget if we know we have a clock app running - if (Bangle.CLOCK) return; - - let intervalRef = null; - var width = 5 * 6*2; - var text_color=0x07FF;//cyan - - function draw() { - g.reset().setFont("6x8", 2).setFontAlign(-1, 0).setColor(text_color); - var time = require("locale").time(new Date(),1); - g.drawString(time, this.x, this.y+11, true); // 5 * 6*2 = 60 +WIDGETS["wdclkbttm"]={area:"br",width:Bangle.CLOCK?0:60,draw:function() { + if (!Bangle.CLOCK == !this.width) { // if we're the wrong size for if we have a clock or not... + this.width = Bangle.CLOCK?0:60; + return setTimeout(Bangle.drawWidgets,1); // widget changed size - redraw } - function clearTimers(){ - if(intervalRef) { - clearInterval(intervalRef); - intervalRef = null; - } - } - function startTimers(){ - intervalRef = setInterval(()=>WIDGETS["wdclkbttm"].draw(), 60*1000); - WIDGETS["wdclkbttm"].draw(); - } - Bangle.on('lcdPower', (on) => { - clearTimers(); - if (on) startTimers(); - }); - - WIDGETS["wdclkbttm"]={area:"br",width:width,draw:draw}; - if (Bangle.isLCDOn) intervalRef = setInterval(()=>WIDGETS["wdclkbttm"].draw(), 60*1000); -})() + if (!this.width) return; // if size not right, return + g.reset().setFont("6x8", 2).setFontAlign(-1, 0).setColor("#0ff"); // cyan + var time = require("locale").time(new Date(),1); + g.drawString(time, this.x, this.y+11, true); // 5 * 6*2 = 60 + // queue draw in one minute + if (this.drawTimeout) clearTimeout(this.drawTimeout); + this.drawTimeout = setTimeout(()=>{ + this.drawTimeout = undefined; + this.draw(); + }, 60000 - (Date.now() % 60000)); +}}; diff --git a/apps/widclose/ChangeLog b/apps/widclose/ChangeLog index 4be6afb16..1e0c86fc6 100644 --- a/apps/widclose/ChangeLog +++ b/apps/widclose/ChangeLog @@ -1 +1,2 @@ -0.01: New widget! \ No newline at end of file +0.01: New widget! +0.02: allow turning on/off when quick-switching apps diff --git a/apps/widclose/metadata.json b/apps/widclose/metadata.json index e044a2d39..19009fd82 100644 --- a/apps/widclose/metadata.json +++ b/apps/widclose/metadata.json @@ -1,7 +1,7 @@ { "id": "widclose", "name": "Close Button", - "version": "0.01", + "version": "0.02", "description": "A button to close the current app", "readme": "README.md", "icon": "icon.png", diff --git a/apps/widclose/widget.js b/apps/widclose/widget.js index 3a354018b..aeba1de00 100644 --- a/apps/widclose/widget.js +++ b/apps/widclose/widget.js @@ -1,14 +1,17 @@ -if (!Bangle.CLOCK) WIDGETS.close = { - area: "tr", width: 24, sortorder: 10, // we want the right-most spot please +WIDGETS.close = { + area: "tr", width: Bangle.CLOCK?0:24, sortorder: 10, // we want the right-most spot please draw: function() { - Bangle.removeListener("touch", this.touch); - Bangle.on("touch", this.touch); - g.reset().setColor("#f00").drawImage(atob( // hardcoded red to match setUI back button + if (!Bangle.CLOCK == !this.width) { // if we're the wrong size for if we have a clock or not... + this.width = Bangle.CLOCK?0:24; + return setTimeout(Bangle.drawWidgets,1); // widget changed size - redraw + } + if (this.width) g.reset().setColor("#f00").drawImage(atob( // red to match setUI back button // b/w version of preview.png, 24x24 "GBgBABgAAf+AB//gD//wH//4P//8P//8fn5+fjx+fxj+f4H+/8P//8P/f4H+fxj+fjx+fn5+P//8P//8H//4D//wB//gAf+AABgA" ), this.x, this.y); - }, touch: function(_, c) { - const w = WIDGETS.close; - if (w && c.x>=w.x && c.x<=w.x+24 && c.y>=w.y && c.y<=w.y+24) load(); + }, touch: function(_, c) { // if touched + const w = WIDGETS.close; // if in range, go back to the clock + if (w && c.x>=w.x && c.x=w.y && c.y<=w.y+24) load(); } -}; \ No newline at end of file +}; +Bangle.on("touch", WIDGETS.close.touch); diff --git a/apps/widcloselaunch/ChangeLog b/apps/widcloselaunch/ChangeLog new file mode 100644 index 000000000..1e0c86fc6 --- /dev/null +++ b/apps/widcloselaunch/ChangeLog @@ -0,0 +1,2 @@ +0.01: New widget! +0.02: allow turning on/off when quick-switching apps diff --git a/apps/widcloselaunch/README.md b/apps/widcloselaunch/README.md new file mode 100644 index 000000000..1eb384ce1 --- /dev/null +++ b/apps/widcloselaunch/README.md @@ -0,0 +1,9 @@ +# Close Button Launcher + +Adds a ![X](preview.png) button to close the current app and go back to the launcher. +(Widget is not visible on the clock screen) + +Copied from widclose by @rigrig and slightly modified. + +![Light theme screenshot](screenshot_light.png) +![Dark theme screenshot](screenshot_dark.png) diff --git a/apps/widcloselaunch/icon.png b/apps/widcloselaunch/icon.png new file mode 100644 index 000000000..1d95ba0ce Binary files /dev/null and b/apps/widcloselaunch/icon.png differ diff --git a/apps/widcloselaunch/metadata.json b/apps/widcloselaunch/metadata.json new file mode 100644 index 000000000..b5c83e37e --- /dev/null +++ b/apps/widcloselaunch/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "widcloselaunch", + "name": "Close Button to launcher", + "version": "0.02", + "description": "A button to close the current app and go to launcher", + "readme": "README.md", + "icon": "icon.png", + "type": "widget", + "tags": "widget,tools", + "supports": ["BANGLEJS2"], + "screenshots": [{"url":"screenshot_light.png"},{"url":"screenshot_dark.png"}], + "storage": [ + {"name":"widcloselaunch.wid.js","url":"widget.js"} + ] +} diff --git a/apps/widcloselaunch/preview.png b/apps/widcloselaunch/preview.png new file mode 100644 index 000000000..d90a3b4c5 Binary files /dev/null and b/apps/widcloselaunch/preview.png differ diff --git a/apps/widcloselaunch/screenshot_dark.png b/apps/widcloselaunch/screenshot_dark.png new file mode 100644 index 000000000..58067a3b9 Binary files /dev/null and b/apps/widcloselaunch/screenshot_dark.png differ diff --git a/apps/widcloselaunch/screenshot_light.png b/apps/widcloselaunch/screenshot_light.png new file mode 100644 index 000000000..32817ea8d Binary files /dev/null and b/apps/widcloselaunch/screenshot_light.png differ diff --git a/apps/widcloselaunch/widget.js b/apps/widcloselaunch/widget.js new file mode 100644 index 000000000..2c9147d16 --- /dev/null +++ b/apps/widcloselaunch/widget.js @@ -0,0 +1,17 @@ +WIDGETS.close = { + area: "tr", width: Bangle.CLOCK?0:24, sortorder: 10, // we want the right-most spot please + draw: function() { + if (!Bangle.CLOCK == !this.width) { // if we're the wrong size for if we have a clock or not... + this.width = Bangle.CLOCK?0:24; + return setTimeout(Bangle.drawWidgets,1); // widget changed size - redraw + } + if (this.width) g.reset().setColor("#f00").drawImage(atob( // red to match setUI back button + // b/w version of preview.png, 24x24 + "GBgBABgAAf+AB//gD//wH//4P//8P//8fn5+fjx+fxj+f4H+/8P//8P/f4H+fxj+fjx+fn5+P//8P//8H//4D//wB//gAf+AABgA" + ), this.x, this.y); + }, touch: function(_, c) { + const w = WIDGETS.close; + if (w && c.x>=w.x && c.x=w.y && c.y<=w.y+24) Bangle.showLauncher(); + } +}; +Bangle.on("touch", WIDGETS.close.touch); diff --git a/apps/widdst/metadata.json b/apps/widdst/metadata.json index f280c96eb..144c02998 100644 --- a/apps/widdst/metadata.json +++ b/apps/widdst/metadata.json @@ -5,7 +5,7 @@ "icon": "icon.png", "type": "widget", "tags": "widget,tool", - "supports" : ["BANGLEJS2"], + "supports" : ["BANGLEJS","BANGLEJS2"], "readme": "README.md", "storage": [ {"name":"widdst.wid.js","url":"widget.js"}, diff --git a/apps/widmeda/ChangeLog b/apps/widmeda/ChangeLog index 7415150e6..6ac092a85 100644 --- a/apps/widmeda/ChangeLog +++ b/apps/widmeda/ChangeLog @@ -1 +1,2 @@ 0.01: Initial Medical Alert Widget! +0.02: Use Medical Information app for medical alert text, and to display details diff --git a/apps/widmeda/README.md b/apps/widmeda/README.md index 0bbfc4dc3..151b11058 100644 --- a/apps/widmeda/README.md +++ b/apps/widmeda/README.md @@ -11,12 +11,12 @@ Implemented: - Basic medical alert logo and message - Only display bottom widget on clocks - High contrast colours depending on theme +- Configure medical alert text (using Medical Information app) +- Show details when touched (using Medical Information app) Future: - Configure when to show bottom widget (always/never/clocks) -- Configure medical alert text -- Show details when touched ## Creator diff --git a/apps/widmeda/metadata.json b/apps/widmeda/metadata.json index 346306b8e..8ce051dd4 100644 --- a/apps/widmeda/metadata.json +++ b/apps/widmeda/metadata.json @@ -1,10 +1,11 @@ { "id": "widmeda", "name": "Medical Alert Widget", "shortName":"Medical Alert", - "version":"0.01", + "version":"0.02", "description": "Display a medical alert in the bottom widget section.", "icon": "widget.png", "type": "widget", + "dependencies" : { "medicalinfo":"app" }, "tags": "health,medical,tools,widget", "supports" : ["BANGLEJS2"], "readme": "README.md", diff --git a/apps/widmeda/widget.js b/apps/widmeda/widget.js index a2d109596..2758ae11f 100644 --- a/apps/widmeda/widget.js +++ b/apps/widmeda/widget.js @@ -1,30 +1,47 @@ (() => { + function getAlertText() { + const medicalinfo = require("medicalinfo").load(); + const alertText = ((Array.isArray(medicalinfo.medicalAlert)) && (medicalinfo.medicalAlert[0])) ? medicalinfo.medicalAlert[0] : ""; + return (g.wrapString(alertText, g.getWidth()).length === 1) ? alertText : "MEDICAL ALERT"; + } + // Top right star of life logo - WIDGETS["widmedatr"]={ + WIDGETS["widmedatr"] = { area: "tr", width: 24, - draw: function() { + draw: function () { g.reset(); g.setColor("#f00"); g.drawImage(atob("FhYBAAAAA/AAD8AAPwAc/OD/P8P8/x/z/n+/+P5/wP58A/nwP5/x/v/n/P+P8/w/z/Bz84APwAA/AAD8AAAAAA=="), this.x + 1, this.y + 1); } }; - // Bottom medical alert message - WIDGETS["widmedabl"]={ + // Bottom medical alert text + WIDGETS["widmedabl"] = { area: "bl", - width: Bangle.CLOCK?Bangle.appRect.w:0, - draw: function() { + width: Bangle.CLOCK ? Bangle.appRect.w : 0, + draw: function () { // Only show the widget on clocks if (!Bangle.CLOCK) return; g.reset(); g.setBgColor(g.theme.dark ? "#fff" : "#f00"); g.setColor(g.theme.dark ? "#f00" : "#fff"); - g.setFont("Vector",18); - g.setFontAlign(0,0); + g.setFont("Vector", 16); + g.setFontAlign(0, 0); g.clearRect(this.x, this.y, this.x + this.width - 1, this.y + 23); - g.drawString("MEDICAL ALERT", this.width / 2, this.y + ( 23 / 2 )); + + const alertText = getAlertText(); + g.drawString(alertText, this.width / 2, this.y + (23 / 2)); } }; + + Bangle.on("touch", (_, c) => { + const bl = WIDGETS.widmedabl; + const tr = WIDGETS.widmedatr; + if ((bl && c.x >= bl.x && c.x < bl.x + bl.width && c.y >= bl.y && c.y <= bl.y + 24) + || (tr && c.x >= tr.x && c.x < tr.x + tr.width && c.y >= tr.y && c.y <= tr.y + 24)) { + load("medicalinfo.app.js"); + } + }); })(); diff --git a/apps/widmsggrid/ChangeLog b/apps/widmsggrid/ChangeLog new file mode 100644 index 000000000..4be6afb16 --- /dev/null +++ b/apps/widmsggrid/ChangeLog @@ -0,0 +1 @@ +0.01: New widget! \ No newline at end of file diff --git a/apps/widmsggrid/README.md b/apps/widmsggrid/README.md new file mode 100644 index 000000000..86a80c403 --- /dev/null +++ b/apps/widmsggrid/README.md @@ -0,0 +1,28 @@ +# Messages Grid Widget + +Widget that displays multiple notification icons in a grid. +The widget has a fixed size: if there are multiple notifications it uses smaller +icons. +It shows a single icon per application, so if you have two SMS messages, the +grid only has one SMS icon. +If there are multiple messages waiting, the total number is shown in the +bottom-right corner. + +Example: one SMS, one Signal, and two WhatsApp messages: +![screenshot](screenshot.png) + +## Installation +This widget needs the [`messages`](/?id=messages) app to handle notifications. + +You probably want to disable the default widget, to do so: +1. Open `Settings` +2. Navigate to `Apps`>`Messages` +3. Scroll down to the `Widget messages` entry, and change it to `Hide` + +## Settings +This widget uses the `Widget` settings from the `messages` app: + +### Widget +* `Flash icon` Toggle flashing of the widget icons. + +* `Widget messages` Not used by this widget, but you should select `Hide` to hide the default widget. \ No newline at end of file diff --git a/apps/widmsggrid/metadata.json b/apps/widmsggrid/metadata.json new file mode 100644 index 000000000..b624f5c23 --- /dev/null +++ b/apps/widmsggrid/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "widmsggrid", + "name": "Messages Grid Widget", + "version": "0.01", + "description": "Widget that display notification icons in a grid", + "icon": "widget.png", + "type": "widget", + "dependencies": {"messages":"app"}, + "tags": "tool,system", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"widmsggrid.wid.js","url":"widget.js"} + ], + "screenshots": [{"url":"screenshot.png"}] +} diff --git a/apps/widmsggrid/screenshot.png b/apps/widmsggrid/screenshot.png new file mode 100644 index 000000000..b1cdb2a5a Binary files /dev/null and b/apps/widmsggrid/screenshot.png differ diff --git a/apps/widmsggrid/widget.js b/apps/widmsggrid/widget.js new file mode 100644 index 000000000..7c5882e6c --- /dev/null +++ b/apps/widmsggrid/widget.js @@ -0,0 +1,92 @@ +(function () { + if (global.MESSAGES) return; // don't load widget while in the app + let settings = require('Storage').readJSON("messages.settings.json", true) || {}; + const s = { + flash: (settings.flash === undefined) ? true : !!settings.flash, + showRead: !!settings.showRead, + }; + delete settings; + WIDGETS["msggrid"] = { + area: "tl", width: 0, + flash: s.flash, + showRead: s.showRead, + init: function() { + // runs on first draw + delete w.init; // don't run again + Bangle.on("touch", w.touch); + Bangle.on("message", w.listener); + w.listener(); // update status now + }, + draw: function () { + if (w.init) w.init(); + // If we had a setTimeout queued from the last time we were called, remove it + if (w.t) { + clearTimeout(w.t); + delete w.t; + } + if (!w.width) return; + const b = w.flash && w.status === "new" && ((Date.now() / 1000) & 1), // Blink(= inverse colors) on this second? + // show multiple icons in a grid, by scaling them down + cols = Math.ceil(Math.sqrt(w.srcs.length - 0.1)); // cols===rows, -0.1 to work around rounding error + g.reset().clearRect(w.x, w.y, w.x + w.width - 1, w.y + 24) + .setClipRect(w.x, w.y, w.x + w.width - 1, w.y + 24); // guard against oversized icons + let r = 0, c = 0; // row, column + const offset = pos => Math.floor(pos / cols * 24); // pixel offset for position in row/column + w.srcs.forEach(src => { + const appColor = require("messages").getMessageImageCol(src, require("messages").getMessageImageCol("alert")); + let colors = [g.theme.bg, g.setColor(appColor).getColor()]; + if (b) { + if (colors[1] == g.theme.fg) colors = colors.reverse(); + else colors[1] = g.theme.fg; + } + g.setColor(colors[1]).setBgColor(colors[0]); + g.drawImage(require("messages").getMessageImage(src, "alert"), w.x+offset(c), w.y+offset(r), { scale: 1 / cols }); + if (++c >= cols) { + c = 0; + r++; + } + }); + if (w.total > 1) { + // show total number of messages in bottom-right corner + g.reset(); + if (w.total < 10) g.fillCircle(w.x + w.width - 5, w.y + 20, 4); // single digits get a round background, double digits fill their rectangle + g.setColor(g.theme.bg).setBgColor(g.theme.fg) + .setFont('6x8').setFontAlign(1, 1) + .drawString(w.total, w.x + w.width - 1, w.y + 24, w.total > 9); + } + if (w.flash && w.status === "new") w.t = setTimeout(w.draw, 1000); // schedule redraw while blinking + }, show: function () { + w.width = 24; + w.srcs = require("messages").getMessages() + .filter(m => !['call', 'map', 'music'].includes(m.id)) + .filter(m => m.new || w.showRead) + .map(m => m.src); + w.total = w.srcs.length; + w.srcs = w.srcs.filter((src, i, uniq) => uniq.indexOf(src) === i); // keep unique entries only + Bangle.drawWidgets(); + Bangle.setLCDPower(1); // turns screen on + }, hide: function () { + w.width = 0; + w.srcs = []; + w.total = 0; + Bangle.drawWidgets(); + }, touch: function (b, c) { + if (!w || !w.width) return; // widget not shown + if (process.env.HWVERSION < 2) { + // Bangle.js 1: open app when on clock we touch the side with widget + if (!Bangle.CLOCK) return; + const m = Bangle.appRect / 2; + if ((w.x < m && b !== 1) || (w.x > m && b !== 2)) return; + } + // Bangle.js 2: open app when touching the widget + else if (c.x < w.x || c.x > w.x + w.width || c.y < w.y || c.y > w.y + 24) return; + load("messages.app.js"); + }, listener: function () { + w.status = require("messages").status(); + if (w.status === "new" || (w.status === "old" && w.showRead)) w.show(); + else w.hide(); + } + }; + delete s; + const w = WIDGETS["msggrid"]; +})(); \ No newline at end of file diff --git a/apps/widmsggrid/widget.png b/apps/widmsggrid/widget.png new file mode 100644 index 000000000..ce6e7b7ac Binary files /dev/null and b/apps/widmsggrid/widget.png differ diff --git a/apps/wpmoto/ChangeLog b/apps/wpmoto/ChangeLog index 2f35c81fe..63f4bf24c 100644 --- a/apps/wpmoto/ChangeLog +++ b/apps/wpmoto/ChangeLog @@ -1,3 +1,4 @@ ... 0.02: First update with ChangeLog Added 0.03: Move waypoints.json (and editor) to 'waypoints' app +0.04: Added adjustment for Bangle.js magnetometer heading fix diff --git a/apps/wpmoto/app.js b/apps/wpmoto/app.js index c8e30a583..f08cb8279 100644 --- a/apps/wpmoto/app.js +++ b/apps/wpmoto/app.js @@ -134,7 +134,7 @@ function read_heading() { Bangle.setCompassPower(1); var d = 0; var m = Bangle.getCompass(); - if (!isNaN(m.heading)) d = -m.heading; + if (!isNaN(m.heading)) d = m.heading; heading = d; } diff --git a/apps/wpmoto/metadata.json b/apps/wpmoto/metadata.json index 01ca4edd8..32c41d757 100644 --- a/apps/wpmoto/metadata.json +++ b/apps/wpmoto/metadata.json @@ -2,7 +2,7 @@ "id": "wpmoto", "name": "Waypointer Moto", "shortName": "Waypointer Moto", - "version": "0.03", + "version": "0.04", "description": "Waypoint-based motorcycle navigation aid", "icon": "wpmoto.png", "tags": "tool,outdoors,gps", diff --git a/bin/firmwaremaker_c.js b/bin/firmwaremaker_c.js index 87065dab4..e56e28ad9 100755 --- a/bin/firmwaremaker_c.js +++ b/bin/firmwaremaker_c.js @@ -172,7 +172,7 @@ Promise.all(APPS.map(appid => { // Generated by BangleApps/bin/build_bangles_c.js const int jsfStorageInitialContentLength = ${storageContent.length}; -const char jsfStorageInitialContents[] = { +const unsigned char jsfStorageInitialContents[] = { `; for (var i=0;i