diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index a3469e7bb..7c0cfca3a 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -22,9 +22,6 @@ jobs: - name: Install typescript dependencies working-directory: ./typescript run: npm ci - - name: Build types - working-directory: ./typescript - run: npm run build:types - name: Build all TS apps and widgets working-directory: ./typescript run: npm run build diff --git a/.gitignore b/.gitignore index a9398e871..f4588ac6f 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ tests/Layout/testresult.bmp apps.local.json _site .jekyll-cache +.owncloudsync.log +Desktop.ini +.sync_*.db* diff --git a/README.md b/README.md index ea485da86..d2f7022e9 100644 --- a/README.md +++ b/README.md @@ -324,7 +324,7 @@ and which gives information about the app for the Launcher. ``` * name, icon and description present the app in the app loader. -* tags is used for grouping apps in the library, separate multiple entries by comma. Known tags are `tool`, `system`, `clock`, `game`, `sound`, `gps`, `widget`, `launcher` or empty. +* tags is used for grouping apps in the library, separate multiple entries by comma. Known tags are `tool`, `system`, `clock`, `game`, `sound`, `gps`, `widget`, `launcher`, `bluetooth` or empty. * storage is used to identify the app files and how to handle them * data is used to clean up files when the app is uninstalled diff --git a/apps/7x7dotsclock/7x7dotsclock.app.js b/apps/7x7dotsclock/7x7dotsclock.app.js index aa174b2d2..aa6672a4f 100644 --- a/apps/7x7dotsclock/7x7dotsclock.app.js +++ b/apps/7x7dotsclock/7x7dotsclock.app.js @@ -149,11 +149,11 @@ function drawHSeg(x1,y1,x2,y2,Num,Color,Size) { if (Color == "fg") { g.setColor(g.theme.fg); } else { - g.setColor(mColor[0],mColor[1],mColor[2]); + g.setColor(mColor[0],mColor[1],mColor[2]); } g.fillCircle(x1+Dx+(i-1)*(x2-x1)/7,y1+Dy+(j-1)*(y2-y1)/7,Size); } else { - g.setColor(bColor[0],bColor[1],bColor[2]); + g.setColor(bColor[0],bColor[1],bColor[2]); g.fillCircle(x1+Dx+(i-1)*(x2-x1)/7,y1+Dy+(j-1)*(y2-y1)/7,1); } } @@ -166,7 +166,7 @@ function drawSSeg(x1,y1,x2,y2,Num,Color,Size) { for (let j = 1; j < 8; j++) { if (Font[Num][j-1][i-1] == 1) { if (Color == "fg") { - g.setColor(sColor[0],sColor[1],sColor[2]); + g.setColor(sColor[0],sColor[1],sColor[2]); } else { g.setColor(g.theme.fg); //g.setColor(0.7,0.7,0.7); @@ -253,8 +253,8 @@ function actions(v){ if(BTN1.read() === true) { print("BTN pressed"); Bangle.showLauncher(); - } - + } + if(v==-1){ print("up swipe event"); if(settings.swupApp != "") load(settings.swupApp); @@ -269,7 +269,7 @@ function actions(v){ } // Get Messages status -var messages = require("Storage").readJSON("messages.json",1)||[]; +var messages_installed = require("Storage").read("messages") !== undefined; //var BTconnected = NRF.getSecurityStatus().connected; //NRF.on('connect',BTconnected = NRF.getSecurityStatus().connected); @@ -289,27 +289,27 @@ function drawWidgeds() { g.setColor((g.getBPP()>8) ? "#07f" : (g.theme.dark ? "#0ff" : "#00f")); else g.setColor(g.theme.dark ? "#666" : "#999"); - g.drawImage(atob("CxQBBgDgFgJgR4jZMawfAcA4D4NYybEYIwTAsBwDAA=="),x1Bt,y1Bt); + g.drawImage(atob("CxQBBgDgFgJgR4jZMawfAcA4D4NYybEYIwTAsBwDAA=="),x1Bt,y1Bt); + - //Battery //print(E.getBattery()); //print(Bangle.isCharging()); - + var x1B = 130; var y1B = 2; var x2B = x1B + 20; var y2B = y1B + 15; - + g.setColor(g.theme.bg); g.clearRect(x1B,y1B,x2B,y2B); - + g.setColor(g.theme.fg); g.drawRect(x1B,y1B,x2B,y2B); g.fillRect(x1B,y1B,x1B+(E.getBattery()*(x2B-x1B)/100),y2B); g.fillRect(x2B,y1B+(y2B-y1B)/2-3,x2B+4,y1B+(y2B-y1B)/2+3); - + //Messages @@ -318,25 +318,25 @@ function drawWidgeds() { var x2M = x1M + 25; var y2M = y2B; - if (messages.some(m=>m.new)) { + if (messages_installed && require("messages").status() == "new") { g.setColor(g.theme.fg); g.fillRect(x1M,y1M,x2M,y2M); g.setColor(g.theme.bg); g.drawLine(x1M,y1M,x1M+(x2M-x1M)/2,y1M+(y2M-y1M)/2); g.drawLine(x1M+(x2M-x1M)/2,y1M+(y2M-y1M)/2,x2M,y1M); } - + var strDow = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat']; var d = new Date(); var dow = d.getDay(),day = d.getDate(), month = d.getMonth() + 1, year = d.getFullYear(); print(strDow[dow] + ' ' + day + '.' + month + ' ' + year); - + g.setColor(g.theme.fg); g.setFontAlign(-1, -1,0); g.setFont("Vector", 20); g.drawString(strDow[dow] + ' ' + day, 0, 0, true); - + } @@ -354,7 +354,7 @@ function SetFull(on) { } else { Ys = 30; Bangle.setUI("updown",actions); - Bangle.on('swipe', function(direction) { + Bangle.on('swipe', function(direction) { switch (direction) { case 1: print("swipe left event"); @@ -362,7 +362,7 @@ function SetFull(on) { print(settings.swleftApp); break; case -1: - print("swipe right event"); + print("swipe right event"); if(settings.swrightApp != "") load(settings.swrightApp); print(settings.swrightApp); break; @@ -374,7 +374,7 @@ function SetFull(on) { SegH = (Ye-Ys)/2; Dy = SegH/16; - + draw(); if (on != true) { diff --git a/apps/7x7dotsclock/ChangeLog b/apps/7x7dotsclock/ChangeLog index d2c98a472..5e8e48b0b 100644 --- a/apps/7x7dotsclock/ChangeLog +++ b/apps/7x7dotsclock/ChangeLog @@ -1,2 +1,3 @@ 0.01: Initial version for upload -0.02: better theme support, configurable colors, small improvements +0.02: Better theme support, configurable colors, small improvements +0.03: Use `messages` library to check for new messages \ No newline at end of file diff --git a/apps/7x7dotsclock/metadata.json b/apps/7x7dotsclock/metadata.json index 41f0836d3..ba1996544 100644 --- a/apps/7x7dotsclock/metadata.json +++ b/apps/7x7dotsclock/metadata.json @@ -1,7 +1,7 @@ { "id": "7x7dotsclock", "name": "7x7 Dots Clock", "shortName":"7x7 Dots Clock", - "version":"0.02", + "version":"0.03", "description": "A clock with a big 7x7 dots Font", "icon": "dotsfontclock.png", "tags": "clock", diff --git a/apps/90sclk/ChangeLog b/apps/90sclk/ChangeLog index feb008f5f..057d6ff73 100644 --- a/apps/90sclk/ChangeLog +++ b/apps/90sclk/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App! -0.02: Fullscreen settings. \ No newline at end of file +0.02: Fullscreen settings. +0.03: Tell clock widgets to hide. diff --git a/apps/90sclk/app.js b/apps/90sclk/app.js index 6babbfec2..351c235e0 100644 --- a/apps/90sclk/app.js +++ b/apps/90sclk/app.js @@ -115,6 +115,9 @@ function draw() { } } +// Show launcher when middle button pressed +Bangle.setUI("clock"); + Bangle.loadWidgets(); // Clear the screen once, at startup @@ -140,5 +143,3 @@ Bangle.on('lock', function(isLocked) { }); -// Show launcher when middle button pressed -Bangle.setUI("clock"); diff --git a/apps/90sclk/metadata.json b/apps/90sclk/metadata.json index fb2824a6f..59b627427 100644 --- a/apps/90sclk/metadata.json +++ b/apps/90sclk/metadata.json @@ -1,7 +1,7 @@ { "id": "90sclk", "name": "90s Clock", - "version": "0.02", + "version": "0.03", "description": "A 90s style watch-face", "readme": "README.md", "icon": "app.png", diff --git a/apps/a_dndtoggle/ChangeLog b/apps/a_dndtoggle/ChangeLog new file mode 100644 index 000000000..ec66c5568 --- /dev/null +++ b/apps/a_dndtoggle/ChangeLog @@ -0,0 +1 @@ +0.01: Initial version diff --git a/apps/a_dndtoggle/README.md b/apps/a_dndtoggle/README.md new file mode 100644 index 000000000..bd0981c5b --- /dev/null +++ b/apps/a_dndtoggle/README.md @@ -0,0 +1,13 @@ +# a_dndtoggle - Toggle Quiet Mode of the watch + +When Quiet mode is off, just start this app to set quiet mode. Start it again to turn off quiet mode. +Work in progress. + +#ToDo +Settings page, current status indicator. + +## Creator + +Hank - contact at http://forum.espruino.com + + diff --git a/apps/a_dndtoggle/a_dndtoggle.app.js b/apps/a_dndtoggle/a_dndtoggle.app.js new file mode 100644 index 000000000..c0b968f2c --- /dev/null +++ b/apps/a_dndtoggle/a_dndtoggle.app.js @@ -0,0 +1,43 @@ + +const modeNames = [/*LANG*/"Noisy", /*LANG*/"Alarms", /*LANG*/"Silent"]; +let bSettings = require('Storage').readJSON('setting.json',true)||{}; +let current = 0|bSettings.quiet; +//0 off +//1 alarms +//2 silent + +console.log("old: " + current); + +switch (current) { + case 0: + bSettings.quiet = 2; + Bangle.buzz(); + setTimeout('Bangle.buzz();',500); + break; + case 1: + bSettings.quiet = 0; + Bangle.buzz(); + break; + case 2: + bSettings.quiet = 0; + Bangle.buzz(); + break; + default: + bSettings.quiet = 0; + Bangle.buzz(); +} + +console.log("new: " + bSettings.quiet); + +E.showMessage(modeNames[current] + " -> " + modeNames[bSettings.quiet]); +setTimeout('exitApp();', 2000); + + +function exitApp(){ + +require("Storage").writeJSON("setting.json", bSettings); +// reload clocks with new theme, otherwise just wait for user to switch apps + +load() + +} \ No newline at end of file diff --git a/apps/a_dndtoggle/a_dndtoggle.png b/apps/a_dndtoggle/a_dndtoggle.png new file mode 100644 index 000000000..4c8b74c0c Binary files /dev/null and b/apps/a_dndtoggle/a_dndtoggle.png differ diff --git a/apps/a_dndtoggle/app-icon.js b/apps/a_dndtoggle/app-icon.js new file mode 100644 index 000000000..0b08cc65b --- /dev/null +++ b/apps/a_dndtoggle/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwJC/AAl/Agf/AAUAgIFDwEHAofgh/g/0Ag/wj+AnwVB/EegEfEIN4nkAh+AgE8vgVBAoV4Aoce/EAgfADQIFcjwpFHYIFCnxBFJopZBn5ZCMopxFPoqJFSowA/gA=")) \ No newline at end of file diff --git a/apps/a_dndtoggle/metadata.json b/apps/a_dndtoggle/metadata.json new file mode 100644 index 000000000..f5ae9cc31 --- /dev/null +++ b/apps/a_dndtoggle/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "a_dndtoggle", + "name": "a_dndtoggle - Toggle Quiet Mode of the watch", + "shortName": "A_DND Toggle", + "version": "0.01", + "description": "Toggle Quiet Mode of the watch just by starting this app.", + "icon": "a_dndtoggle.png", + "type": "app", + "tags": "tool", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"a_dndtoggle.app.js","url":"a_dndtoggle.app.js"}, + {"name":"a_dndtoggle.img","url":"app-icon.js","evaluate":true} + ], + "readme": "README.md" +} diff --git a/apps/about/ChangeLog b/apps/about/ChangeLog index f5638fdd2..ffe9de081 100644 --- a/apps/about/ChangeLog +++ b/apps/about/ChangeLog @@ -10,3 +10,4 @@ 0.10: Added separate Bangle.js 2 file with Bangle.js 2 kickstarter pixels (as of 28 Oct 2021) 0.11: Bangle.js2: New pixels, btn1 to exit 0.12: Actual pixels as of 29th Nov 2021 +0.13: Bangle.js 2: Use setUI to add software back button diff --git a/apps/about/app-bangle2.js b/apps/about/app-bangle2.js index 978d36193..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); @@ -69,4 +69,7 @@ function drawImage() { // TODO: a nice little animation before setTimeout(drawInfo, 1000); -setWatch(_=>load(), BTN1); +Bangle.setUI({ + mode : "custom", + back : load +}); diff --git a/apps/about/metadata.json b/apps/about/metadata.json index 6c22bdc56..648576576 100644 --- a/apps/about/metadata.json +++ b/apps/about/metadata.json @@ -1,7 +1,7 @@ { "id": "about", "name": "About", - "version": "0.12", + "version": "0.13", "description": "Bangle.js About page - showing software version, stats, and a collaborative mural from the Bangle.js KickStarter backers", "icon": "app.png", "tags": "tool,system", 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/activityreminder/ChangeLog b/apps/activityreminder/ChangeLog index 37820dce6..3811425ac 100644 --- a/apps/activityreminder/ChangeLog +++ b/apps/activityreminder/ChangeLog @@ -6,3 +6,5 @@ 0.06: Add a temperature threshold to detect (and not alert) if the BJS isn't worn. Better support for the peoples using the app at night 0.07: Fix bug on the cutting edge firmware 0.08: Use default Bangle formatter for booleans +0.09: New app screen (instead of showing settings or the alert) and some optimisations +0.10: Add software back button via setUI diff --git a/apps/activityreminder/alert.js b/apps/activityreminder/alert.js new file mode 100644 index 000000000..96a9b76c4 --- /dev/null +++ b/apps/activityreminder/alert.js @@ -0,0 +1,37 @@ +(function () { + // load variable before defining functions cause it can trigger a ReferenceError + const activityreminder = require("activityreminder"); + const storage = require("Storage"); + let activityreminder_data = activityreminder.loadData(); + + function run() { + E.showPrompt("Inactivity detected", { + title: "Activity reminder", + buttons: { "Ok": 1, "Dismiss": 2, "Pause": 3 } + }).then(function (v) { + if (v == 1) { + activityreminder_data.okDate = new Date(); + } + if (v == 2) { + activityreminder_data.dismissDate = new Date(); + } + if (v == 3) { + activityreminder_data.pauseDate = new Date(); + } + activityreminder.saveData(activityreminder_data); + load(); + }); + + // Obey system quiet mode: + if (!(storage.readJSON('setting.json', 1) || {}).quiet) { + Bangle.buzz(400); + } + setTimeout(load, 20000); + } + + g.clear(); + Bangle.loadWidgets(); + Bangle.drawWidgets(); + run(); + +})(); \ No newline at end of file diff --git a/apps/activityreminder/app.js b/apps/activityreminder/app.js index c2b626fb3..81e10d8dd 100644 --- a/apps/activityreminder/app.js +++ b/apps/activityreminder/app.js @@ -1,46 +1,58 @@ (function () { - // load variable before defining functions cause it can trigger a ReferenceError - const activityreminder = require("activityreminder"); - const storage = require("Storage"); - const activityreminder_settings = activityreminder.loadSettings(); - let activityreminder_data = activityreminder.loadData(); + // load variable before defining functions cause it can trigger a ReferenceError + const activityreminder = require("activityreminder"); + let activityreminder_data = activityreminder.loadData(); + let W = g.getWidth(); + // let H = g.getHeight(); - function drawAlert() { - E.showPrompt("Inactivity detected", { - title: "Activity reminder", - buttons: { "Ok": 1, "Dismiss": 2, "Pause": 3 } - }).then(function (v) { - if (v == 1) { - activityreminder_data.okDate = new Date(); - } - if (v == 2) { - activityreminder_data.dismissDate = new Date(); - } - if (v == 3) { - activityreminder_data.pauseDate = new Date(); - } - activityreminder.saveData(activityreminder_data); - load(); - }); - - // Obey system quiet mode: - if (!(storage.readJSON('setting.json', 1) || {}).quiet) { - Bangle.buzz(400); - } - setTimeout(load, 20000); - } - - function run() { - if (activityreminder.mustAlert(activityreminder_data, activityreminder_settings)) { - drawAlert(); - } else { - eval(storage.read("activityreminder.settings.js"))(() => load()); - } - } + function getHoursMins(date){ + var h = date.getHours(); + var m = date.getMinutes(); + return (""+h).substr(-2) + ":" + ("0"+m).substr(-2); + } + function drawData(name, value, y){ + g.drawString(name, 10, y); + g.drawString(value, 100, y); + } + + function drawInfo() { + var h=18, y = h; + g.setColor(g.theme.fg); + g.setFont("Vector",h).setFontAlign(-1,-1); + + // Header + g.drawLine(0,25,W,25); + g.drawLine(0,26,W,26); + + g.drawString("Current Cycle", 10, y+=h); + drawData("Start", getHoursMins(activityreminder_data.stepsDate), y+=h); + drawData("Steps", getCurrentSteps(), y+=h); + + /* + g.drawString("Button Press", 10, y+=h*2); + drawData("Ok", getHoursMins(activityreminder_data.okDate), y+=h); + drawData("Dismiss", getHoursMins(activityreminder_data.dismissDate), y+=h); + drawData("Pause", getHoursMins(activityreminder_data.pauseDate), y+=h); + */ + } + + function getCurrentSteps(){ + let health = Bangle.getHealthStatus("day"); + return health.steps - activityreminder_data.stepsOnDate; + } + + function run() { g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); - run(); - -})(); \ No newline at end of file + drawInfo(); + Bangle.setUI({ + mode : "custom", + back : load + }) + } + + run(); + +})(); diff --git a/apps/activityreminder/boot.js b/apps/activityreminder/boot.js index f97cf274d..5a11d73b8 100644 --- a/apps/activityreminder/boot.js +++ b/apps/activityreminder/boot.js @@ -1,70 +1,81 @@ (function () { - // load variable before defining functions cause it can trigger a ReferenceError - const activityreminder = require("activityreminder"); - const activityreminder_settings = activityreminder.loadSettings(); - let activityreminder_data = activityreminder.loadData(); - - if (activityreminder_data.firstLoad) { - activityreminder_data.firstLoad = false; + // load variable before defining functions cause it can trigger a ReferenceError + const activityreminder = require("activityreminder"); + const activityreminder_settings = activityreminder.loadSettings(); + let activityreminder_data = activityreminder.loadData(); + + if (activityreminder_data.firstLoad) { + activityreminder_data.firstLoad = false; + activityreminder.saveData(activityreminder_data); + } + + function run() { + if (isNotWorn()) return; + let now = new Date(); + let h = now.getHours(); + + if (isDuringAlertHours(h)) { + let health = Bangle.getHealthStatus("day"); + if (health.steps - activityreminder_data.stepsOnDate >= activityreminder_settings.minSteps // more steps made than needed + || health.steps < activityreminder_data.stepsOnDate) { // new day or reboot of the watch + activityreminder_data.stepsOnDate = health.steps; + activityreminder_data.stepsDate = now; activityreminder.saveData(activityreminder_data); - } - - function run() { - if (isNotWorn()) return; - let now = new Date(); - let h = now.getHours(); - - if (isDuringAlertHours(h)) { - let health = Bangle.getHealthStatus("day"); - if (health.steps - activityreminder_data.stepsOnDate >= activityreminder_settings.minSteps // more steps made than needed - || health.steps < activityreminder_data.stepsOnDate) { // new day or reboot of the watch - activityreminder_data.stepsOnDate = health.steps; - activityreminder_data.stepsDate = now; - activityreminder.saveData(activityreminder_data); - /* todo in a futur release - Add settimer to trigger like 30 secs after going in this part cause the person have been walking - (pass some argument to run() to handle long walks and not triggering so often) - */ - } - - if (activityreminder.mustAlert(activityreminder_data, activityreminder_settings)) { - load('activityreminder.app.js'); - } - } - - } - - function isNotWorn() { - return (Bangle.isCharging() || activityreminder_settings.tempThreshold >= E.getTemperature()); - } - - function isDuringAlertHours(h) { - if (activityreminder_settings.startHour < activityreminder_settings.endHour) { // not passing through midnight - return (h >= activityreminder_settings.startHour && h < activityreminder_settings.endHour); - } else { // passing through midnight - return (h >= activityreminder_settings.startHour || h < activityreminder_settings.endHour); - } - } - - Bangle.on('midnight', function () { - /* - Usefull trick to have the app working smothly for people using it at night - */ - let now = new Date(); - let h = now.getHours(); - if (activityreminder_settings.enabled && isDuringAlertHours(h)) { - // updating only the steps and keeping the original stepsDate on purpose - activityreminder_data.stepsOnDate = 0; - activityreminder.saveData(activityreminder_data); - } - }); - - - if (activityreminder_settings.enabled) { - setInterval(run, 60000); /* todo in a futur release - increase setInterval time to something that is still sensible (5 mins ?) - when we added a settimer + Add settimer to trigger like 30 secs after going in this part cause the person have been walking + (pass some argument to run() to handle long walks and not triggering so often) */ + } + + if (mustAlert(now)) { + load('activityreminder.alert.js'); + } } + + } + + function isNotWorn() { + return (Bangle.isCharging() || activityreminder_settings.tempThreshold >= E.getTemperature()); + } + + function isDuringAlertHours(h) { + if (activityreminder_settings.startHour < activityreminder_settings.endHour) { // not passing through midnight + return (h >= activityreminder_settings.startHour && h < activityreminder_settings.endHour); + } else { // passing through midnight + return (h >= activityreminder_settings.startHour || h < activityreminder_settings.endHour); + } + } + + function mustAlert(now) { + if ((now - activityreminder_data.stepsDate) / 60000 > activityreminder_settings.maxInnactivityMin) { // inactivity detected + if ((now - activityreminder_data.okDate) / 60000 > 3 && // last alert anwsered with ok was more than 3 min ago + (now - activityreminder_data.dismissDate) / 60000 > activityreminder_settings.dismissDelayMin && // last alert was more than dismissDelayMin ago + (now - activityreminder_data.pauseDate) / 60000 > activityreminder_settings.pauseDelayMin) { // last alert was more than pauseDelayMin ago + return true; + } + } + return false; + } + + Bangle.on('midnight', function () { + /* + Usefull trick to have the app working smothly for people using it at night + */ + let now = new Date(); + let h = now.getHours(); + if (activityreminder_settings.enabled && isDuringAlertHours(h)) { + // updating only the steps and keeping the original stepsDate on purpose + activityreminder_data.stepsOnDate = 0; + activityreminder.saveData(activityreminder_data); + } + }); + + + if (activityreminder_settings.enabled) { + setInterval(run, 60000); + /* todo in a futur release + increase setInterval time to something that is still sensible (5 mins ?) + when we added a settimer + */ + } })(); diff --git a/apps/activityreminder/lib.js b/apps/activityreminder/lib.js index 704d35641..a5c35190c 100644 --- a/apps/activityreminder/lib.js +++ b/apps/activityreminder/lib.js @@ -1,56 +1,44 @@ exports.loadSettings = function () { - return Object.assign({ - enabled: true, - startHour: 9, - endHour: 20, - maxInnactivityMin: 30, - dismissDelayMin: 15, - pauseDelayMin: 120, - minSteps: 50, - tempThreshold: 27 - }, require("Storage").readJSON("activityreminder.s.json", true) || {}); + return Object.assign({ + enabled: true, + startHour: 9, + endHour: 20, + maxInnactivityMin: 30, + dismissDelayMin: 15, + pauseDelayMin: 120, + minSteps: 50, + tempThreshold: 27 + }, require("Storage").readJSON("activityreminder.s.json", true) || {}); }; exports.writeSettings = function (settings) { - require("Storage").writeJSON("activityreminder.s.json", settings); + require("Storage").writeJSON("activityreminder.s.json", settings); }; exports.saveData = function (data) { - require("Storage").writeJSON("activityreminder.data.json", data); + require("Storage").writeJSON("activityreminder.data.json", data); }; exports.loadData = function () { - let health = Bangle.getHealthStatus("day"); - let data = Object.assign({ - firstLoad: true, - stepsDate: new Date(), - stepsOnDate: health.steps, - okDate: new Date(1970), - dismissDate: new Date(1970), - pauseDate: new Date(1970), - }, + let health = Bangle.getHealthStatus("day"); + let data = Object.assign({ + firstLoad: true, + stepsDate: new Date(), + stepsOnDate: health.steps, + okDate: new Date(1970), + dismissDate: new Date(1970), + pauseDate: new Date(1970), + }, require("Storage").readJSON("activityreminder.data.json") || {}); - if(typeof(data.stepsDate) == "string") - data.stepsDate = new Date(data.stepsDate); - if(typeof(data.okDate) == "string") - data.okDate = new Date(data.okDate); - if(typeof(data.dismissDate) == "string") - data.dismissDate = new Date(data.dismissDate); - if(typeof(data.pauseDate) == "string") - data.pauseDate = new Date(data.pauseDate); + if (typeof (data.stepsDate) == "string") + data.stepsDate = new Date(data.stepsDate); + if (typeof (data.okDate) == "string") + data.okDate = new Date(data.okDate); + if (typeof (data.dismissDate) == "string") + data.dismissDate = new Date(data.dismissDate); + if (typeof (data.pauseDate) == "string") + data.pauseDate = new Date(data.pauseDate); - return data; + return data; }; - -exports.mustAlert = function(activityreminder_data, activityreminder_settings) { - let now = new Date(); - if ((now - activityreminder_data.stepsDate) / 60000 > activityreminder_settings.maxInnactivityMin) { // inactivity detected - if ((now - activityreminder_data.okDate) / 60000 > 3 && // last alert anwsered with ok was more than 3 min ago - (now - activityreminder_data.dismissDate) / 60000 > activityreminder_settings.dismissDelayMin && // last alert was more than dismissDelayMin ago - (now - activityreminder_data.pauseDate) / 60000 > activityreminder_settings.pauseDelayMin) { // last alert was more than pauseDelayMin ago - return true; - } - } - return false; -} \ No newline at end of file diff --git a/apps/activityreminder/metadata.json b/apps/activityreminder/metadata.json index 75ebf80b2..a7fb0c487 100644 --- a/apps/activityreminder/metadata.json +++ b/apps/activityreminder/metadata.json @@ -3,7 +3,7 @@ "name": "Activity Reminder", "shortName":"Activity Reminder", "description": "A reminder to take short walks for the ones with a sedentary lifestyle", - "version":"0.08", + "version":"0.10", "icon": "app.png", "type": "app", "tags": "tool,activity", @@ -13,11 +13,12 @@ {"name": "activityreminder.app.js", "url":"app.js"}, {"name": "activityreminder.boot.js", "url": "boot.js"}, {"name": "activityreminder.settings.js", "url": "settings.js"}, + {"name": "activityreminder.alert.js", "url": "alert.js"}, {"name": "activityreminder", "url": "lib.js"}, {"name": "activityreminder.img", "url": "app-icon.js", "evaluate": true} ], "data": [ {"name": "activityreminder.s.json"}, - {"name": "activityreminder.data.json"} + {"name": "activityreminder.data.json", "storageFile": true} ] } diff --git a/apps/activityreminder/settings.js b/apps/activityreminder/settings.js index de490b796..051c0dcd8 100644 --- a/apps/activityreminder/settings.js +++ b/apps/activityreminder/settings.js @@ -1,80 +1,86 @@ (function (back) { - // Load settings - const activityreminder = require("activityreminder"); - let settings = activityreminder.loadSettings(); + // Load settings + const activityreminder = require("activityreminder"); + let settings = activityreminder.loadSettings(); - // Show the menu - E.showMenu({ - "": { "title": "Activity Reminder" }, - "< Back": () => back(), - 'Enable': { - value: settings.enabled, - onchange: v => { - settings.enabled = v; - activityreminder.writeSettings(settings); - } - }, - 'Start hour': { - value: settings.startHour, - min: 0, max: 24, - onchange: v => { - settings.startHour = v; - activityreminder.writeSettings(settings); - } - }, - 'End hour': { - value: settings.endHour, - min: 0, max: 24, - onchange: v => { - settings.endHour = v; - activityreminder.writeSettings(settings); - } - }, - 'Max inactivity': { - value: settings.maxInnactivityMin, - min: 15, max: 120, - onchange: v => { - settings.maxInnactivityMin = v; - activityreminder.writeSettings(settings); - }, - format: x => x + "m" - }, - 'Dismiss delay': { - value: settings.dismissDelayMin, - min: 5, max: 60, - onchange: v => { - settings.dismissDelayMin = v; - activityreminder.writeSettings(settings); - }, - format: x => x + "m" - }, - 'Pause delay': { - value: settings.pauseDelayMin, - min: 30, max: 240, step: 5, - onchange: v => { - settings.pauseDelayMin = v; - activityreminder.writeSettings(settings); - }, - format: x => { - return x + "m"; - } - }, - 'Min steps': { - value: settings.minSteps, - min: 10, max: 500, step: 10, - onchange: v => { - settings.minSteps = v; - activityreminder.writeSettings(settings); - } - }, - 'Temp Threshold': { - value: settings.tempThreshold, - min: 20, max: 40, step: 0.5, - format: v => v + "°C", - onchange: v => { - settings.tempThreshold = v; - activityreminder.writeSettings(settings); - } + function getMainMenu(){ + var mainMenu = { + "": { "title": "Activity Reminder" }, + "< Back": () => back(), + 'Enable': { + value: settings.enabled, + onchange: v => { + settings.enabled = v; + activityreminder.writeSettings(settings); } - }); + }, + 'Start hour': { + value: settings.startHour, + min: 0, max: 24, + onchange: v => { + settings.startHour = v; + activityreminder.writeSettings(settings); + } + }, + 'End hour': { + value: settings.endHour, + min: 0, max: 24, + onchange: v => { + settings.endHour = v; + activityreminder.writeSettings(settings); + } + }, + 'Max inactivity': { + value: settings.maxInnactivityMin, + min: 15, max: 120, + onchange: v => { + settings.maxInnactivityMin = v; + activityreminder.writeSettings(settings); + }, + format: x => x + "m" + }, + 'Dismiss delay': { + value: settings.dismissDelayMin, + min: 5, max: 60, + onchange: v => { + settings.dismissDelayMin = v; + activityreminder.writeSettings(settings); + }, + format: x => x + "m" + }, + 'Pause delay': { + value: settings.pauseDelayMin, + min: 30, max: 240, step: 5, + onchange: v => { + settings.pauseDelayMin = v; + activityreminder.writeSettings(settings); + }, + format: x => { + return x + "m"; + } + }, + 'Min steps': { + value: settings.minSteps, + min: 10, max: 500, step: 10, + onchange: v => { + settings.minSteps = v; + activityreminder.writeSettings(settings); + } + }, + 'Temp Threshold': { + value: settings.tempThreshold, + min: 20, max: 40, step: 0.5, + format: v => v + "°C", + onchange: v => { + settings.tempThreshold = v; + activityreminder.writeSettings(settings); + } + } + }; + + return mainMenu; + } + + // Show the menu + E.showMenu(getMainMenu()); }) diff --git a/apps/advcasio/ChangeLog b/apps/advcasio/ChangeLog index 7de176672..a1b528cf6 100644 --- a/apps/advcasio/ChangeLog +++ b/apps/advcasio/ChangeLog @@ -1 +1,3 @@ 0.01: AdvCasio first version +0.02: Remove un-needed fonts to improve memory usage +0.03: Tell clock widgets to hide. diff --git a/apps/advcasio/app.js b/apps/advcasio/app.js index 8c27b7823..8cb904f90 100644 --- a/apps/advcasio/app.js +++ b/apps/advcasio/app.js @@ -1,7 +1,5 @@ const storage = require('Storage'); -require("Font6x12").add(Graphics); -require("Font6x8").add(Graphics); require("Font8x12").add(Graphics); require("Font7x11Numeric7Seg").add(Graphics); @@ -257,7 +255,6 @@ function draw() { g.setColor(0, 0, 0); - g.setFont("6x12"); if(dataJson && dataJson.weather) drawWeather(dataJson.weather); if(dataJson && dataJson.tasks) drawTasks(dataJson.tasks); @@ -297,9 +294,10 @@ Bangle.on("lock", (locked) => { }); +Bangle.setUI("clock"); + // Load widgets, but don't show them Bangle.loadWidgets(); -Bangle.setUI("clock"); g.reset(); g.clear(); diff --git a/apps/advcasio/metadata.json b/apps/advcasio/metadata.json index 0f0c75c07..152f47132 100644 --- a/apps/advcasio/metadata.json +++ b/apps/advcasio/metadata.json @@ -1,7 +1,7 @@ { "id": "advcasio", "name": "Advanced Casio Clock", "shortName":"advcasio", - "version":"0.01", + "version":"0.03", "description": "An over-engineered clock inspired by Casio watches. It has a 4 days weather, a timer using swipe and a scratchpad. Can be updated using a dedicated webapp.", "icon": "app.png", "tags": "clock", diff --git a/apps/agenda/ChangeLog b/apps/agenda/ChangeLog index ae650deeb..0a7916810 100644 --- a/apps/agenda/ChangeLog +++ b/apps/agenda/ChangeLog @@ -1,2 +1,7 @@ 0.01: Basic agenda with events from GB 0.02: Added settings page to force calendar sync +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. +0.07: Clkinfo improvements. \ No newline at end of file diff --git a/apps/agenda/README.md b/apps/agenda/README.md index a546e0a89..1a0ec9264 100644 --- a/apps/agenda/README.md +++ b/apps/agenda/README.md @@ -1,3 +1,30 @@ # Agenda -Basic agenda reading the events synchronised from GadgetBridge +Basic agenda reading the events synchronised from GadgetBridge. + +### Functionalities + +* List all events in the next week (or whatever is synchronized) +* Optionally view past events (until GB removes them) +* Show start time and location of the events in the list +* 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). diff --git a/apps/agenda/agenda.clkinfo.js b/apps/agenda/agenda.clkinfo.js new file mode 100644 index 000000000..6c2ddb3da --- /dev/null +++ b/apps/agenda/agenda.clkinfo.js @@ -0,0 +1,29 @@ +(function() { + var agendaItems = { + name: "Agenda", + img: atob("GBiBAf////////85z/AAAPAAAPgAAP////AAAPAAAPAAAPAAAOAAAeAAAeAAAcAAA8AAAoAABgAADP//+P//8PAAAPAAAPgAAf///w=="), + items: [] + }; + + var now = new Date(); + var agenda = storage.readJSON("android.calendar.json") + .filter(ev=>ev.timestamp + ev.durationInSeconds > now/1000) + .sort((a,b)=>a.timestamp - b.timestamp); + + agenda.forEach((entry, i) => { + + var title = entry.title.slice(0,18); + var date = new Date(entry.timestamp*1000); + var dateStr = locale.date(date).replace(/\d\d\d\d/,""); + dateStr += entry.durationInSeconds < 86400 ? "/ " + locale.time(date,1) : ""; + + agendaItems.items.push({ + name: null, + get: () => ({ text: title + "\n" + dateStr, img: null}), + show: function() { agendaItems.items[i].emit("redraw"); }, + hide: function () {} + }); + }); + + return agendaItems; +}) \ No newline at end of file diff --git a/apps/agenda/agenda.js b/apps/agenda/agenda.js index f39e31c75..9cffe0265 100644 --- a/apps/agenda/agenda.js +++ b/apps/agenda/agenda.js @@ -6,6 +6,8 @@ title, description, location, + color:int, + calName, allDay: bool, } */ @@ -24,19 +26,23 @@ var fontLarge = g.getFonts().includes("6x15")?"6x15:2":"6x8:4"; //FIXME maybe write the end from GB already? Not durationInSeconds here (or do while receiving?) var CALENDAR = require("Storage").readJSON("android.calendar.json",true)||[]; +var settings = require("Storage").readJSON("agenda.settings.json",true)||{}; -CALENDAR=CALENDAR.sort((a,b)=>a.timestamp - b.timestamp) +CALENDAR=CALENDAR.sort((a,b)=>a.timestamp - b.timestamp); function getDate(timestamp) { return new Date(timestamp*1000); } -function formatDateLong(date, includeDay) { - if(includeDay) - return Locale.date(date)+" "+Locale.time(date,1); - return Locale.time(date,1); +function formatDateLong(date, includeDay, allDay) { + let shortTime = Locale.time(date,1)+Locale.meridian(date); + if(allDay) shortTime = ""; + if(includeDay || allDay) + return Locale.date(date)+" "+shortTime; + return shortTime; } -function formatDateShort(date) { - return Locale.date(date).replace(/\d\d\d\d/,"")+Locale.time(date,1); +function formatDateShort(date, allDay) { + return Locale.date(date).replace(/\d\d\d\d/,"")+(allDay? + "" : Locale.time(date,1)+Locale.meridian(date)); } var lines = []; @@ -45,7 +51,7 @@ function showEvent(ev) { if(!ev) return; g.setFont(bodyFont); //var lines = []; - if (ev.title) lines = g.wrapString(ev.title, g.getWidth()-10) + if (ev.title) lines = g.wrapString(ev.title, g.getWidth()-10); var titleCnt = lines.length; var start = getDate(ev.timestamp); var end = getDate((+ev.timestamp) + (+ev.durationInSeconds)); @@ -53,22 +59,24 @@ function showEvent(ev) { if (titleCnt) lines.push(""); // add blank line after title if(start.getDay() == end.getDay() && start.getMonth() == end.getMonth()) includeDay = false; - if(includeDay) { + if(includeDay || ev.allDay) { lines = lines.concat( /*LANG*/"Start:", - g.wrapString(formatDateLong(start, includeDay), g.getWidth()-10), + g.wrapString(formatDateLong(start, includeDay, ev.allDay), g.getWidth()-10), /*LANG*/"End:", - g.wrapString(formatDateLong(end, includeDay), g.getWidth()-10)); + g.wrapString(formatDateLong(end, includeDay, ev.allDay), g.getWidth()-10)); } else { lines = lines.concat( g.wrapString(Locale.date(start), g.getWidth()-10), - g.wrapString(/*LANG*/"Start"+": "+formatDateLong(start, includeDay), g.getWidth()-10), - g.wrapString(/*LANG*/"End"+": "+formatDateLong(end, includeDay), g.getWidth()-10)); + g.wrapString(/*LANG*/"Start"+": "+formatDateLong(start, includeDay, ev.allDay), g.getWidth()-10), + g.wrapString(/*LANG*/"End"+": "+formatDateLong(end, includeDay, ev.allDay), g.getWidth()-10)); } if(ev.location) lines = lines.concat(/*LANG*/"Location"+": ", g.wrapString(ev.location, g.getWidth()-10)); if(ev.description) lines = lines.concat("",g.wrapString(ev.description, g.getWidth()-10)); + if(ev.calName) + lines = lines.concat(/*LANG*/"Calendar"+": ", g.wrapString(ev.calName, g.getWidth()-10)); lines = lines.concat(["",/*LANG*/"< Back"]); E.showScroller({ h : g.getFontHeight(), // height of each menu item in pixels @@ -89,6 +97,12 @@ function showEvent(ev) { } function showList() { + //it might take time for GB to delete old events, decide whether to show them grayed out or hide entirely + if(!settings.pastEvents) { + let now = new Date(); + //TODO add threshold here? + CALENDAR = CALENDAR.filter(ev=>ev.timestamp + ev.durationInSeconds > now/1000); + } if(CALENDAR.length == 0) { E.showMessage("No events"); return; @@ -101,24 +115,21 @@ function showList() { g.setColor(g.theme.fg); g.clearRect(r.x,r.y,r.x+r.w, r.y+r.h); if (!ev) return; - var isPast = ev.timestamp + ev.durationInSeconds < (new Date())/1000; + var isPast = false; var x = r.x+2, title = ev.title; - var body = formatDateShort(getDate(ev.timestamp))+"\n"+ev.location; - var m = ev.title+"\n"+ev.location, longBody=false; + var body = formatDateShort(getDate(ev.timestamp),ev.allDay)+"\n"+(ev.location?ev.location:/*LANG*/"No location"); + if(settings.pastEvents) isPast = ev.timestamp + ev.durationInSeconds < (new Date())/1000; if (title) g.setFontAlign(-1,-1).setFont(fontBig) - .setColor(isPast ? "#888" : g.theme.fg).drawString(title, x,r.y+2); + .setColor(isPast ? "#888" : g.theme.fg).drawString(title, x+4,r.y+2); if (body) { g.setFontAlign(-1,-1).setFont(fontMedium).setColor(isPast ? "#888" : g.theme.fg); - var l = g.wrapString(body, r.w-(x+14)); - if (l.length>3) { - l = l.slice(0,3); - l[l.length-1]+="..."; - } - longBody = l.length>2; - g.drawString(l.join("\n"), x+10,r.y+20); + g.drawString(body, x+10,r.y+20); } - //if (!longBody && msg.src) g.setFontAlign(1,1).setFont("6x8").drawString(msg.src, r.x+r.w-2, r.y+r.h-2); g.setColor("#888").fillRect(r.x,r.y+r.h-1,r.x+r.w-1,r.y+r.h-1); // dividing line between items + if(ev.color) { + g.setColor("#"+(0x1000000+Number(ev.color)).toString(16).padStart(6,"0")); + g.fillRect(r.x,r.y+4,r.x+3, r.y+r.h-4); + } }, select : idx => showEvent(CALENDAR[idx]), back : () => load() diff --git a/apps/agenda/metadata.json b/apps/agenda/metadata.json index ce8438686..6d91455f0 100644 --- a/apps/agenda/metadata.json +++ b/apps/agenda/metadata.json @@ -1,7 +1,7 @@ { "id": "agenda", "name": "Agenda", - "version": "0.02", + "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"}], @@ -12,6 +12,8 @@ "storage": [ {"name":"agenda.app.js","url":"agenda.js"}, {"name":"agenda.settings.js","url":"settings.js"}, + {"name":"agenda.clkinfo.js","url":"agenda.clkinfo.js"}, {"name":"agenda.img","url":"agenda-icon.js","evaluate":true} - ] + ], + "data": [{"name":"agenda.settings.json"}] } diff --git a/apps/agenda/settings.js b/apps/agenda/settings.js index fe9dab2d8..4220fcb63 100644 --- a/apps/agenda/settings.js +++ b/apps/agenda/settings.js @@ -3,6 +3,10 @@ Bluetooth.println(""); Bluetooth.println(JSON.stringify(message)); } + var settings = require("Storage").readJSON("agenda.settings.json",1)||{}; + function updateSettings() { + require("Storage").writeJSON("agenda.settings.json", settings); + } var CALENDAR = require("Storage").readJSON("android.calendar.json",true)||[]; var mainmenu = { "" : { "title" : "Agenda" }, @@ -32,6 +36,13 @@ E.showAlert(/*LANG*/"You are not connected").then(()=>E.showMenu(mainmenu)); } }, + /*LANG*/"Show past events" : { + value : !!settings.pastEvents, + onchange: v => { + settings.pastEvents = v; + updateSettings(); + } + }, }; E.showMenu(mainmenu); }) diff --git a/apps/agpsdata/ChangeLog b/apps/agpsdata/ChangeLog index c17eac852..89844a132 100644 --- a/apps/agpsdata/ChangeLog +++ b/apps/agpsdata/ChangeLog @@ -1 +1,4 @@ 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 diff --git a/apps/agpsdata/README.md b/apps/agpsdata/README.md index 93cc94259..57bb055a1 100644 --- a/apps/agpsdata/README.md +++ b/apps/agpsdata/README.md @@ -1,18 +1,19 @@ # A-GPS Data -Load assisted GPS data directly to the watch using the new http requests on Android GadgetBridge. +Load assisted GPS (A-GPS) data directly to your Bangle.js using the new http requests on Android GadgetBridge. + +Will download A-GPS data in background (if enabled in settings). + +The GNSS type can be configured in the settings. Make sure: * your GadgetBridge version supports http requests * turn on internet access in GadgetBridge settings -Currently proof of concept on Bangle2 only. Will eventually add a widget for automatic download. - -![](screenshot.png) -![](screenshot2.png) -![](screenshot3.png) -![](screenshot4.png) -![](screenshot5.png) +Currently proof of concept on Bangle.js 2 only. ## Creator [@pidajo](https://github.com/pidajo) + +## Contributor +[@myxor](https://github.com/myxor) diff --git a/apps/agpsdata/app.js b/apps/agpsdata/app.js index 825eda273..647723bb4 100644 --- a/apps/agpsdata/app.js +++ b/apps/agpsdata/app.js @@ -1,125 +1,54 @@ -var _GB = global.GB; -var counter = 0; - -function GB(msg) { - console.log(msg); - if (msg.t == "http") { - display("Received", "(" + msg.resp.length + ") Touch to apply", () => { - display("Apply data..", ""); - setTimeout(() => { - if (setAGPS(msg.resp)) { - display("Success", "Touch for restart", httpTest); - } - else { - display("Error", "Touch for restart", httpTest); - } - }, 1); - }); - } - if (_GB) { - _GB(msg); - } -} - -function setAGPS(data) { - var js = jsFromBase64(data); - console.log(js); - try { - eval(js); - return true; - } - catch(e) { - console.log("Error:", e); - } - return false; -} - -function jsFromBase64(b64) { - var bin = atob(b64); - var chunkSize = 128; - var js = "Bangle.setGPSPower(1);\n"; // turn GPS on - var gnss_select="1"; - js += `Serial1.println("${CASIC_CHECKSUM("$PCAS04,"+gnss_select)}")\n`; // set GNSS mode - // What about: - // NAV-TIMEUTC (0x01 0x10) - // NAV-PV (0x01 0x03) - // or AGPS.zip uses AID-INI (0x0B 0x01) - - for (var i=0;i { - display("Request...", "Touch for restart", httpTest); - if (Bluetooth.println) { - console.log("On device"); - Bluetooth.println(JSON.stringify({t:"info", msg:"HTTP Request"})); - Bluetooth.println(JSON.stringify({t:"http", url:"https://www.espruino.com/agps/casic.base64"})); - } - else { - console.log("Testing on Emulator"); - setTimeout(() => { - GB({t:"http", resp:testData}); - }, 1); - } - }); -} - -var nextStep = null; - -Bangle.on("touch", () => { - if (nextStep) { - nextStep(); - } -}); - -httpTest(); - // Show launcher when middle button pressed // Load widgets Bangle.loadWidgets(); Bangle.drawWidgets(); +let waiting = false; -/* -require("Storage").write("httptest.info",{ - "id":"httptest", - "name":"Http Test", - "src":"httptest.js", - "icon":"wristlight.img" -}); -*/ +function start() { + g.reset(); + g.clear(); + waiting = false; + display("Retry?", "touch to retry"); + Bangle.on("touch", () => { updateAgps(); }); +} -var testData = "QUdOU1MgZGF0YSBmcm9tIENBU0lDLgpEYXRhTGVuZ3RoOiAyNTk4LgpMaW1pdGF0aW9uOiAzLzEwMDAuCrrOSAAIB7YdxSr+Sg2h8NYlBux1jiUgQbrXgJk/KJvFZVv8pP//uy3i/PH6rv9EMQH6SwBfAOxepgDsXgAAlCULALv/AAtCAAAAAQMAALQ7kly6zkgACAdBzVam9HANoXGycgoqGmnG5X9h3mKrWicvBKhXAp7//+00U/9j/jP/SDHM/Vn/JADrXqYA614AAF6d6v8DAADaEAAAAAIDAADKmrVTus5IAAgHTUirJrjvDKHJDjACcmXJJ+8Lv6rw5LQnl4OChUCt//9XKG3/+u6ZE84ZQuwDAMf/7F6mAOxeAADa0Pb/mP8ABDUAAAADAwAA4pBeVLrOSAAIB5291DTIzAyhJGfzAJ7pCIcIUQMgcfEoJ2TIjrHkqv//YjDTCKj/wRPdGIz/8/9NAOxepgDsXgAAl9/6/yQAAPbhAAAABAMAAIJ7sXC6zkgACAeKKDOgmwEOocKrFwP8Jmgqq4lOQ1VtLSfEi8yDVqn//5MskP6E7WQRPxzv6vv/0//sXqYA7F4AAM4w/f/0/wDoJwAAAAUDAABcUW5Hus5IAAgHu+Va48nxDaFaw0UBkVc73Y3FB+HFmDgoUWIPW+Ck//+iLcf9X/t5/ikzgPrT/wgA7F6mAOxeAAADVgsAiQAACB0AAAAGAwAAvsu9zbrOSAAIB0OVp/7+JQ2hFANPCF+F6KOOMwe9/nC5JsX0CtthqP//WzhzADr/dgqbIRj/nwDj/+xepgDsXgAAP4oKAPv/AOg4AAAABwMAAM4qVwS6zkgACAey9J9b+zwOofLIzwObDg0HLdEmMOA3PydWaUQvr6b//0wxwf/7EJ8K/yLREhkANADsXqYA7F4AALug/f/y/wALLAAAAAgDAACs6Ue+us5IAAgHm7NJLLhaDKEumRQBAFtVTExfuke3AeEmNg9cr2Cq//92MTIJm/63FCkXPv4gABgA616mAOteAACQQfX/HgAAAzUAAAAJAwAAfmebX7rOSAAIB9fgVnnchw2hNlTrAyWnJ5rnBMWFV9GyJzPfZYV8rf//Oyh2AA3xmhLdGtXu/f/Z/+xepgDsXgAA8gXx/37/AAU/AAAACgMAAPbBtfm6zkgACAc+F7Ct97wMoVEVPQCJJG1yeakXMm80PSet+BBd+aH//5AzPf2J+uX97jG7+fj/DgDsXqYA7F4AAGqE//8WAADufQIAAAsDAADELmhius5IAAgHW3g6rwRJDqFpPmYEPf8lNRy9lKO/IX8nJPRjCLOt//90Lir7QQcEGFYUTAguAA4A7F6mAOxeAADrZfj/zP8A5SsAAAAMAwAA/vB8ZbrOSAAIB1mVSsfiXw2hUX8RA60JSyUgLkEgC910JykTq7Usqv//8S9rBw//HhIpGyT/+v8fAOxepgDsXgAAitgKAD4AAOcpAAAADQMAAPoqnZW6zkgACAcCLzz8Q1ANocBvBQFuUWqAxVSgoRjR0SaS5AkH3qr//7cx3vlcB2AYPROjCOr/vf/sXqYA7F4AAA5Y/P/7/wDvGwMAAA4DAABMXoD/us5IAAgH+RiyseCLDKGnOjkHATiXLOud6AnbGuclr2roqg2l///FNxcHi/0hFLoW4fyj/10A7F6mAOxeAAANRf7/GgAA6TQAAAAPAwAAOjJsarrOSAAIB1/+xFM3Xg2hVDKBBg6Tsx25u5hYROV9J/QyJQmLrf//zC1e+7QGxRfmFOAHhf+s/+xepgDsXgAAaE/v/+j/AOonAAAAEAMAAAb9ka66zkgACAc74z4AUZEMofLN6wbbUEjD/w54MWPC3SfOFa4yQ6f//4wtrwH5EOwKOCRTFLz/UP/sXqYA7F4AAOaAFAApAADoOQAAABEDAAC+xoUHus5IAAgH1yXSbYfZDaFOrzwBIM5keona3uYD8JYnC6ekWw+l//8JMC/9ivsaAGEwM/ssAN//7F6mAOxeAAAymwQAof8A7l8AAAASAwAA9kus4rrOSAAIB/9znedCsA2h5VDBBLdHG1Uw8P6P93bRJw5UgTR9qf//LS1BAj0TAwkqJioWBABHAOxepgDsXgAAD54FACwAAN6xAAAAEwMAAEboQta6zkgACAdVJvSzetsMoRclcgKU4/OAvUl5A73wdCYbpBB/P6X//08yQ/4N7voNgx5160gAFwDsXqYA7F4AADa+EADk/wDuLwAAABQDAADyTPBuus5IAAgHhpZXHJnuDaGfsmkMbSLS2QeTnDZBLR8ntwGPV8mj//+SM6n8rftj/EUyZfw7AaX/7F6mAOxeAAAvSQUAAAAA6j4AAAAVAwAAVC23P7rOSAAIB8FSSGuHmw2hXoTeBoU+tLS9TdsrYGopJ+h3h7O9p///mjEIB3X/GhN5GXP/c//V/+xepgDsXgAASREJADgAAO4rAAAAFgMAAMqlmN26zkgACAeGsPJwF8ENoU2cJwHhxiN62PG7uVyKfydPWVyEG6z//8spSgCQ8NkRnxsl7sr/EQDsXqYA7F4AAI0S///u/wDudQEAABcDAABUYe3ous5IAAgHtHJyvWVdDaFr1mwGwPVWIZcaxOQXwAwmeHqW1+Sh//+PPnAAQgAOCxwhof8sAGwA7F6mAOxeAAAhMQcAtf8ABjsAAAAYAwAAsOXsgbrOSAAIB4nxObhoSQ2hOjBcBf2n5ijflOaX/Iz8JpPiNAUTrP//ajEL++oEchfzE9AFSgDT/+tepgDrXgAAohMLACkAAAweAAAAGQMAAFrje3e6zkgACAexAdJ/MyYOoeXvjwPazb0PcXcXfIVaNCZ2mjMDkqn//z02W/qPBMcW7BM7BcX/6//sXqYA7F4AABv4BgAVAAAPIgAAABoDAACqA6wGus5IAAgHxYz/b8dNDaH6/WQF3AILHLKDgTJ+VponRocZMKSm//8bLycAMRANDG8i/RHR/2wA7F6mAOxeAAArEwcAHAAABEgAAQAbAwAA0hkH57rOSAAIB5HXkJcOjA2h8B4kAebzvl2gmkU1K1TzJ86wODP6qP//0CzCAM4OmQrkI1UR7f/n/+xepgDsXgAAcs/u/9//AOplAAAAHQMAAGqvKTa6zkgACAekogIGh+8NoYBTBQMDtQeTiKQ4u0KYHiYkF4HbzaT//7A80v9N/gQLmSC4/icA6v/sXqYA7F4AAB2V7v/2/wAIGQAAAB4DAACQRQ0Tus5IAAgHUsOCUJ71DaHkPFgFdJcpEDguP6ldR+UmgbrM2+6m//9vOPT/S/7vC1sh6/49AJH/7F6mAOxeAAAQCfr/8/8A4wwAAAAfAwAA7IYNqLrOSAAIByZDZIfa4AyhxlEVA9QtFKN+R+1NPIIIJ8xm1q8Qqv//djDzB9H+pBNQGGj+rv++/+xepgDsXgAA3f/6/67/AAFUAAAAIAMAAJSG0BW6zhQACAWVGZOmAAAAAPr///8SEpCmiQcDAD4zLlK6zhAACAZIDf33DwP+/jYK//gDAAAAoBoC9g=="; +function updateAgps() { + g.reset(); + g.clear(); + if (!waiting) { + waiting = true; + display("Updating A-GPS..."); + require("agpsdata").pull(function() { + waiting = false; + display("A-GPS updated.", "touch to close"); + Bangle.on("touch", () => { load(); }); + }, + function(error) { + waiting = false; + E.showAlert(error, "Error") + .then(() => { start(); }); + }); + } else { + display("Waiting..."); + } +} +updateAgps(); diff --git a/apps/agpsdata/boot.js b/apps/agpsdata/boot.js new file mode 100644 index 000000000..2b1e6819c --- /dev/null +++ b/apps/agpsdata/boot.js @@ -0,0 +1,26 @@ +(function() { + let waiting = false; + let settings = require("Storage").readJSON("agpsdata.settings.json", 1) || { + enabled: true, + refresh: 1440 + }; + + if (settings.refresh == undefined) settings.refresh = 1440; + + function successCallback(){ + waiting = false; + } + + function errorCallback(){ + waiting = false; + } + + if (settings.enabled) { + setInterval(() => { + if (!waiting && NRF.getSecurityStatus().connected){ + waiting = true; + require("agpsdata").pull(successCallback, errorCallback); + } + }, settings.refresh * 1000 * 60); + } +})(); diff --git a/apps/agpsdata/default.json b/apps/agpsdata/default.json new file mode 100644 index 000000000..0b6e0cecf --- /dev/null +++ b/apps/agpsdata/default.json @@ -0,0 +1 @@ +{"enabled":true,"refresh":1440,"gnsstype":1} diff --git a/apps/agpsdata/lib.js b/apps/agpsdata/lib.js new file mode 100644 index 000000000..7d9758c0a --- /dev/null +++ b/apps/agpsdata/lib.js @@ -0,0 +1,75 @@ +function readSettings() { + settings = Object.assign( + require('Storage').readJSON("agpsdata.default.json", true) || {}, + require('Storage').readJSON(FILE, true) || {}); +} + +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 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) + + 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); + }); + } else { + console.log("error: No http method found"); + if (failureCallback) failureCallback(/*LANG*/"No http method"); + } +}; diff --git a/apps/agpsdata/metadata.json b/apps/agpsdata/metadata.json index af51f3a10..1ce299532 100644 --- a/apps/agpsdata/metadata.json +++ b/apps/agpsdata/metadata.json @@ -1,16 +1,24 @@ { "id": "agpsdata", - "name": "A-GPS Data", - "shortName":"AGPS Data", + "name": "A-GPS Data Downloader App", + "shortName":"A-GPS Data", "icon": "agpsdata.png", - "version":"0.01", - "description": "Download assisted GPS data directly to watch", - "tags": "assisted,gps,agps,http", + "version":"0.03", + "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, "supports": ["BANGLEJS2"], "readme":"README.md", - "screenshots" : [ { "url":"screenshot.png" }, { "url":"screenshot2.png" }, { "url":"screenshot3.png" }, { "url":"screenshot4.png" }, { "url":"screenshot5.png" } ], + "screenshots" : [ { "url":"screenshot.png" }, { "url":"screenshot2.png" } ], "storage": [ {"name":"agpsdata.app.js","url":"app.js"}, - {"name":"agpsdata.img","url":"agpsdata-icon.js","evaluate":true} + {"name":"agpsdata.img","url":"agpsdata-icon.js","evaluate":true}, + {"name":"agpsdata.default.json","url":"default.json"}, + {"name":"agpsdata.boot.js","url":"boot.js"}, + {"name":"agpsdata","url":"lib.js"}, + {"name":"agpsdata.settings.js","url":"settings.js"} + ], + "data": [ + {"name": "agpsdata.json"}, + {"name": "agpsdata.settings.json"} ] } diff --git a/apps/agpsdata/screenshot.png b/apps/agpsdata/screenshot.png index fae53ba85..1fcb2d8ee 100644 Binary files a/apps/agpsdata/screenshot.png and b/apps/agpsdata/screenshot.png differ diff --git a/apps/agpsdata/screenshot2.png b/apps/agpsdata/screenshot2.png index 7cdba1487..7c546e4b5 100644 Binary files a/apps/agpsdata/screenshot2.png and b/apps/agpsdata/screenshot2.png differ diff --git a/apps/agpsdata/screenshot3.png b/apps/agpsdata/screenshot3.png deleted file mode 100644 index be152ba28..000000000 Binary files a/apps/agpsdata/screenshot3.png and /dev/null differ diff --git a/apps/agpsdata/screenshot4.png b/apps/agpsdata/screenshot4.png deleted file mode 100644 index 305a166d0..000000000 Binary files a/apps/agpsdata/screenshot4.png and /dev/null differ diff --git a/apps/agpsdata/screenshot5.png b/apps/agpsdata/screenshot5.png deleted file mode 100644 index 6468a1872..000000000 Binary files a/apps/agpsdata/screenshot5.png and /dev/null differ diff --git a/apps/agpsdata/settings.js b/apps/agpsdata/settings.js new file mode 100644 index 000000000..64fa25330 --- /dev/null +++ b/apps/agpsdata/settings.js @@ -0,0 +1,71 @@ +(function(back) { +function writeSettings(key, value) { + var s = Object.assign( + require('Storage').readJSON(settingsDefaultFile, true) || {}, + require('Storage').readJSON(settingsFile, true) || {}); + s[key] = value; + require('Storage').writeJSON(settingsFile, s); + readSettings(); +} + +function readSettings() { + settings = Object.assign( + require('Storage').readJSON(settingsDefaultFile, true) || {}, + require('Storage').readJSON(settingsFile, true) || {}); +} + +var settingsFile = "agpsdata.settings.json"; +var settingsDefaultFile = "agpsdata.default.json"; + +var settings; +readSettings(); + +const gnsstypes = [ + "", "GPS", "BDS", "GPS+BDS", "GLONASS", "GPS+GLONASS", "BDS+GLONASS", + "GPS+BDS+GLON." +]; + +function buildMainMenu() { + var mainmenu = { + '' : {'title' : 'AGPS download'}, + '< Back' : back, + "Enabled" : { + value : !!settings.enabled, + onchange : v => { writeSettings("enabled", v); } + }, + "Refresh every" : { + value : settings.refresh / 60, + min : 6, + max : 168, + step : 1, + format : v => v + "h", + onchange : v => { writeSettings("refresh", Math.round(v * 60)); } + }, + "GNSS type" : { + value : settings.gnsstype, + min : 1, + max : 7, + step : 1, + format : v => gnsstypes[v], + onchange : x => writeSettings('gnsstype', x) + }, + "Force refresh" : () => { + E.showMessage("Loading A-GPS data"); + require("agpsdata") + .pull( + function() { + E.showAlert("Success").then( + () => { E.showMenu(buildMainMenu()); }); + }, + function(error) { + E.showAlert(error, "Error") + .then(() => { E.showMenu(buildMainMenu()); }); + }); + } + }; + + return mainmenu; +} + +E.showMenu(buildMainMenu()); +}); 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 76658af49..a65326941 100644 --- a/apps/android/ChangeLog +++ b/apps/android/ChangeLog @@ -11,4 +11,6 @@ 0.10: Fix SMS bug 0.12: Use default Bangle formatter for booleans 0.13: Added Bangle.http function (see Readme file for more info) -0.14: Fix timeout of http function not beeing cleaned up +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 99d4dbb2c..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) { @@ -150,6 +152,9 @@ //send the request var req = {t: "http", url:url, id:options.id}; if (options.xpath) req.xpath = options.xpath; + if (options.method) req.method = options.method; + if (options.body) req.body = options.body; + if (options.headers) req.headers = options.headers; gbSend(req); //create the promise var promise = new Promise(function(resolve,reject) { diff --git a/apps/android/metadata.json b/apps/android/metadata.json index 91f74c36f..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.14", + "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/animclk/ChangeLog b/apps/animclk/ChangeLog index 348448c34..76d15bdb1 100644 --- a/apps/animclk/ChangeLog +++ b/apps/animclk/ChangeLog @@ -1,3 +1,5 @@ 0.01: New App! 0.02: Fix bug if image clock wasn't installed 0.03: Update to use setUI +0.04: Tell clock widgets to hide. Move loadWidgets() so it only runs on +startup and not on every draw. diff --git a/apps/animclk/app.js b/apps/animclk/app.js index 4bf63daf6..bdc399fbe 100644 --- a/apps/animclk/app.js +++ b/apps/animclk/app.js @@ -87,7 +87,6 @@ if (g.drawImages) { draw(); var secondInterval = setInterval(draw,100); // load widgets - Bangle.loadWidgets(); Bangle.drawWidgets(); // Stop when LCD goes off Bangle.on('lcdPower',on=>{ @@ -104,3 +103,5 @@ if (g.drawImages) { } // Show launcher when button pressed Bangle.setUI("clock"); + +Bangle.loadWidgets(); diff --git a/apps/animclk/metadata.json b/apps/animclk/metadata.json index 31dfe453f..0b426a37d 100644 --- a/apps/animclk/metadata.json +++ b/apps/animclk/metadata.json @@ -2,7 +2,7 @@ "id": "animclk", "name": "Animated Clock", "shortName": "Anim Clock", - "version": "0.03", + "version": "0.04", "description": "An animated clock face using Mark Ferrari's amazing 8 bit game art and palette cycling: http://www.markferrari.com/art/8bit-game-art", "icon": "app.png", "type": "clock", diff --git a/apps/antonclk/ChangeLog b/apps/antonclk/ChangeLog index f7e95b5fa..4ef0cee75 100644 --- a/apps/antonclk/ChangeLog +++ b/apps/antonclk/ChangeLog @@ -10,4 +10,7 @@ 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 \ No newline at end of file +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 4b1e71bda..528866588 100644 --- a/apps/antonclk/app.js +++ b/apps/antonclk/app.js @@ -1,230 +1,45 @@ // Clock with large digits using the "Anton" bold font - -const SETTINGSFILE = "antonclk.json"; - 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 +let drawTimeout; -// variables defined from settings -var secondsMode; -var secondsColoured; -var secondsWithColon; -var dateOnMain; -var dateOnSecs; -var weekDay; -var calWeek; -var upperCase; -var vectorFont; +// 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); -// dynamic variables -var drawTimeout; -var queueMillis = 1000; -var secondsScreen = true; - -var isBangle1 = (process.env.HWVERSION == 1); - -//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 +// 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; - } -} - -function isoStr(date) { - return date.getFullYear() + "-" + ("0" + (date.getMonth() + 1)).slice(-2) + "-" + ("0" + date.getDate()).slice(-2); -} - -var 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', on => { - updateState(); -}); -Bangle.on('lock', on => { - updateState(); -}); -// Show launcher when middle button pressed -Bangle.setUI("clock"); + delete Graphics.prototype.setFontAnton; + }}); // Load widgets Bangle.loadWidgets(); -Bangle.drawWidgets(); - -// end of file \ No newline at end of file +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 16bdf3aa8..b8242f11a 100644 --- a/apps/antonclk/metadata.json +++ b/apps/antonclk/metadata.json @@ -1,9 +1,8 @@ { "id": "antonclk", "name": "Anton Clock", - "version": "0.09", - "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/assistedgps/metadata.json b/apps/assistedgps/metadata.json index 1dbc42c87..4c91dcd35 100644 --- a/apps/assistedgps/metadata.json +++ b/apps/assistedgps/metadata.json @@ -1,11 +1,12 @@ { "id": "assistedgps", - "name": "Assisted GPS Update (AGPS)", + "name": "Assisted GPS Updater (AGPS)", "version": "0.03", - "description": "Downloads assisted GPS (AGPS) data to Bangle.js 1 or 2 for faster GPS startup and more accurate fixes. **No app will be installed**, this just uploads new data to the GPS chip.", + "description": "Downloads assisted GPS (AGPS) data to Bangle.js for faster GPS startup and more accurate fixes. **No app will be installed**, this just uploads new data to the GPS chip.", + "sortorder": -1, "icon": "app.png", "type": "RAM", - "tags": "tool,outdoors,agps", + "tags": "tool,outdoors,agps,gps,a-gps", "supports": ["BANGLEJS","BANGLEJS2"], "custom": "custom.html", "customConnect": true, diff --git a/apps/astral/ChangeLog b/apps/astral/ChangeLog index a51c96760..747e5ac2e 100644 --- a/apps/astral/ChangeLog +++ b/apps/astral/ChangeLog @@ -1,3 +1,5 @@ 0.01: Create astral clock app 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 c445463f2..a435ca9e3 100644 --- a/apps/astral/app.js +++ b/apps/astral/app.js @@ -767,6 +767,24 @@ function draw() { g.clear(); current_moonphase = getMoonPhase(); +Bangle.setUI("clockupdown", btn => { + if (btn==0) { + if (!processing) { + if (!modeswitch) { + modeswitch = true; + if (mode == "planetary") mode = "extras"; + else mode = "planetary"; + } + else + modeswitch = false; + } + } else { + if (!processing) + ready_to_compute = true; + } +}); + + // Load widgets Bangle.loadWidgets(); Bangle.drawWidgets(); @@ -799,23 +817,6 @@ Bangle.setGPSPower(1); // Show launcher when button pressed Bangle.setClockMode(); -Bangle.setUI("clockupdown", btn => { - if (btn==0) { - if (!processing) { - if (!modeswitch) { - modeswitch = true; - if (mode == "planetary") mode = "extras"; - else mode = "planetary"; - } - else - modeswitch = false; - } - } else { - if (!processing) - ready_to_compute = true; - } -}); - setWatch(function () { if (!astral_settings.astral_default) { colours_switched = true; @@ -833,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 3317092db..647066a13 100644 --- a/apps/astral/metadata.json +++ b/apps/astral/metadata.json @@ -1,7 +1,7 @@ { "id": "astral", "name": "Astral Clock", - "version": "0.03", + "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/banglexercise/ChangeLog b/apps/banglexercise/ChangeLog index 5f1d3bd7d..6cf589541 100644 --- a/apps/banglexercise/ChangeLog +++ b/apps/banglexercise/ChangeLog @@ -2,3 +2,4 @@ 0.02: Add sit ups Add more feedback to the user about the exercises Clean up code +0.03: Add software back button on main menu diff --git a/apps/banglexercise/app.js b/apps/banglexercise/app.js index bc6e35f07..9659ee81f 100644 --- a/apps/banglexercise/app.js +++ b/apps/banglexercise/app.js @@ -71,7 +71,8 @@ function showMainMenu() { let menu; menu = { "": { - title: "BanglExercise" + title: "BanglExercise", + back: load } }; @@ -381,4 +382,5 @@ Bangle.on('HRM', function(hrm) { }); g.clear(1); +Bangle.loadWidgets(); showMainMenu(); diff --git a/apps/banglexercise/metadata.json b/apps/banglexercise/metadata.json index 9bb93f112..f4ce1894b 100644 --- a/apps/banglexercise/metadata.json +++ b/apps/banglexercise/metadata.json @@ -1,7 +1,7 @@ { "id": "banglexercise", "name": "BanglExercise", "shortName":"BanglExercise", - "version":"0.02", + "version":"0.03", "description": "Can automatically track exercises while wearing the Bangle.js watch.", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], diff --git a/apps/barclock/ChangeLog b/apps/barclock/ChangeLog index ba44ecef8..a00ae9325 100644 --- a/apps/barclock/ChangeLog +++ b/apps/barclock/ChangeLog @@ -12,3 +12,4 @@ 0.12: Add settings to hide date,widgets 0.13: Add font setting 0.14: Use ClockFace_menu.addItems +0.15: Add Power saving option \ No newline at end of file diff --git a/apps/barclock/README.md b/apps/barclock/README.md index ff66a5cbb..28572e37c 100644 --- a/apps/barclock/README.md +++ b/apps/barclock/README.md @@ -7,4 +7,5 @@ A simple digital clock showing seconds as a horizontal bar. ## Settings * `Show date`: display date at the bottom of screen -* `Font`: choose between bitmap or vector fonts \ No newline at end of file +* `Font`: choose between bitmap or vector fonts +* `Power saving`: (Bangle.js 2 only) don't draw the seconds bar while the watch is locked \ No newline at end of file diff --git a/apps/barclock/clock-bar.js b/apps/barclock/clock-bar.js index 61ce07dfb..5a7dfc8c0 100644 --- a/apps/barclock/clock-bar.js +++ b/apps/barclock/clock-bar.js @@ -13,16 +13,20 @@ let locale = require("locale"); locale.hasMeridian = (locale.meridian(date)!==""); } +let barW = 0, prevX = 0; function renderBar(l) { - if (!this.fraction) { - // zero-size fillRect stills draws one line of pixels, we don't want that - return; - } - const width = this.fraction*l.w; - g.fillRect(l.x, l.y, l.x+width-1, l.y+l.height-1); + "ram"; + if (l) prevX = 0; // called from Layout: drawing area was cleared + else l = clock.layout.bar; + let x2 = l.x+barW; + if (clock.powerSave && Bangle.isLocked()) x2 = 0; // hide bar + if (x2===prevX) return; // nothing to do + if (x2===0) x2--; // don't leave 1px line + if (x21; if (this.is12Hour && locale.hasMeridian) { @@ -89,17 +93,32 @@ const ClockFace = require("ClockFace"), this.layout.time.font = "6x8:"+thickness; } this.layout.update(); + bar.y2 = bar.y+bar.height-1; }, update: function(date, c) { + "ram"; if (c.m) this.layout.time.label = timeText(date); if (c.h) this.layout.ampm.label = ampmText(date); if (c.d && this.showDate) this.layout.date.label = dateText(date); - const SECONDS_PER_MINUTE = 60; - if (c.s) this.layout.bar.fraction = date.getSeconds()/SECONDS_PER_MINUTE; - this.layout.render(); + if (c.m) this.layout.render(); + if (c.s) { + barW = Math.round(date.getSeconds()/60*this.layout.bar.w); + renderBar(); + } }, resume: function() { + prevX = 0; // force redraw of bar this.layout.forgetLazyState(); }, }); + +// power saving: only update once a minute while locked, hide bar +if (clock.powerSave) { + Bangle.on("lock", lock => { + clock.precision = lock ? 60 : 1; + clock.tick(); + renderBar(); // hide/redraw bar right away + }); +} + clock.start(); diff --git a/apps/barclock/metadata.json b/apps/barclock/metadata.json index 0c227dc52..5b783dbda 100644 --- a/apps/barclock/metadata.json +++ b/apps/barclock/metadata.json @@ -1,7 +1,7 @@ { "id": "barclock", "name": "Bar Clock", - "version": "0.14", + "version": "0.15", "description": "A simple digital clock showing seconds as a bar", "icon": "clock-bar.png", "screenshots": [{"url":"screenshot.png"},{"url":"screenshot_pm.png"}], diff --git a/apps/barclock/settings.js b/apps/barclock/settings.js index dfe25581c..7b88b7021 100644 --- a/apps/barclock/settings.js +++ b/apps/barclock/settings.js @@ -17,10 +17,14 @@ onchange: v => save("font", v), }, }; - require("ClockFace_menu").addItems(menu, save, { + let items = { showDate: s.showDate, loadWidgets: s.loadWidgets, - }); - + }; + // Power saving for Bangle.js 1 doesn't make sense (no updates while screen is off anyway) + if (process.env.HWVERSION>1) { + items.powerSave = s.powerSave; + } + require("ClockFace_menu").addItems(menu, save, items); E.showMenu(menu); }); diff --git a/apps/barcode/ChangeLog b/apps/barcode/ChangeLog index 4f99f15ac..7ab5d8587 100644 --- a/apps/barcode/ChangeLog +++ b/apps/barcode/ChangeLog @@ -7,3 +7,4 @@ 0.07: Step count resets at midnight 0.08: Step count stored in memory to survive reloads. Now shows step count daily and since last reboot. 0.09: NOW it really should reset daily (instead of every other day...) +0.10: Tell clock widgets to hide. diff --git a/apps/barcode/barcode.app.js b/apps/barcode/barcode.app.js index 89419f33c..0d9df78d5 100644 --- a/apps/barcode/barcode.app.js +++ b/apps/barcode/barcode.app.js @@ -416,13 +416,13 @@ var layout = new Layout( { // Clear the screen once, at startup g.clear(); +Bangle.setUI("clock"); Bangle.loadWidgets(); Bangle.drawWidgets(); -Bangle.setUI("clock"); layout.render(); Bangle.on('lock', function(locked) { if(!locked) { layout.render(); } -}); \ No newline at end of file +}); diff --git a/apps/barcode/metadata.json b/apps/barcode/metadata.json index cef267b2b..3f6bf06e6 100644 --- a/apps/barcode/metadata.json +++ b/apps/barcode/metadata.json @@ -2,7 +2,7 @@ "name": "Barcode clock", "shortName":"Barcode clock", "icon": "barcode.icon.png", - "version":"0.09", + "version":"0.10", "description": "EAN-8 compatible barcode clock.", "tags": "barcode,ean,ean-8,watchface,clock,clockface", "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/batclock/ChangeLog b/apps/batclock/ChangeLog index e6e21b146..2a2d91b74 100644 --- a/apps/batclock/ChangeLog +++ b/apps/batclock/ChangeLog @@ -1,2 +1,3 @@ 0.01: App Created! 0.02: Update to use Bangle.setUI instead of setWatch +0.03: Tell clock widgets to hide. diff --git a/apps/batclock/bat-clock.app.js b/apps/batclock/bat-clock.app.js index 31b8f5b9b..dc649160f 100644 --- a/apps/batclock/bat-clock.app.js +++ b/apps/batclock/bat-clock.app.js @@ -249,6 +249,9 @@ g.clear(); g.setColor(0, 0.5, 0).drawImage(bg_crack); g.setColor(1, 1, 1).drawImage(batman); +// Show launcher when button pressed +Bangle.setUI("clock"); + Bangle.loadWidgets(); Bangle.drawWidgets(); @@ -256,5 +259,3 @@ Bangle.drawWidgets(); timeInterval = setInterval(showTime, 1000); showTime(); -// Show launcher when button pressed -Bangle.setUI("clock"); diff --git a/apps/batclock/metadata.json b/apps/batclock/metadata.json index 8aa115780..e6520cb90 100644 --- a/apps/batclock/metadata.json +++ b/apps/batclock/metadata.json @@ -2,7 +2,7 @@ "id": "batclock", "name": "Bat Clock", "shortName": "Bat Clock", - "version": "0.02", + "version": "0.03", "description": "Morphing Clock, with an awesome \"The Dark Knight\" themed logo.", "icon": "bat-clock.png", "screenshots": [{"url":"screenshot.png"}], diff --git a/apps/bclock/ChangeLog b/apps/bclock/ChangeLog index 5b2cf598c..79c198431 100644 --- a/apps/bclock/ChangeLog +++ b/apps/bclock/ChangeLog @@ -1,2 +1,3 @@ 0.02: Modified for use with new bootloader and firmware 0.03: Update to use Bangle.setUI instead of setWatch +0.04: Tell clock widgets to hide. diff --git a/apps/bclock/clock-binary.js b/apps/bclock/clock-binary.js index fdf945ee6..c08a7abe6 100644 --- a/apps/bclock/clock-binary.js +++ b/apps/bclock/clock-binary.js @@ -100,10 +100,12 @@ Bangle.on('lcdPower', on => { if (on) drawClock(); }); +// Show launcher when button pressed +Bangle.setUI("clock"); + g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); setInterval(() => { drawClock(); }, 1000); drawClock(); -// Show launcher when button pressed -Bangle.setUI("clock"); + diff --git a/apps/bclock/metadata.json b/apps/bclock/metadata.json index 94219a30b..c6a24d89f 100644 --- a/apps/bclock/metadata.json +++ b/apps/bclock/metadata.json @@ -1,7 +1,7 @@ { "id": "bclock", "name": "Binary Clock", - "version": "0.03", + "version": "0.04", "description": "A simple binary clock watch face", "icon": "clock-binary.png", "type": "clock", 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/bigdclock/ChangeLog b/apps/bigdclock/ChangeLog index 09cc978fb..c92d139bb 100644 --- a/apps/bigdclock/ChangeLog +++ b/apps/bigdclock/ChangeLog @@ -3,3 +3,5 @@ 0.03: Internationalisation; bug fix - battery icon responds promptly to charging state 0.04: bug fix 0.05: proper fix for the race condition in queueDraw() +0.06: Tell clock widgets to hide. +0.07: Better battery graphic - now has green, yellow and red sections; battery status reflected in the bar across the middle of the screen; current battery state checked only once every 15 minutes, leading to longer-lasting battery charge diff --git a/apps/bigdclock/bigdclock.app.js b/apps/bigdclock/bigdclock.app.js index c013c6188..a8e2b38df 100644 --- a/apps/bigdclock/bigdclock.app.js +++ b/apps/bigdclock/bigdclock.app.js @@ -11,6 +11,8 @@ Graphics.prototype.setFontOpenSans = function(scale) { }; var drawTimeout; +var lastBattCheck = 0; +var width = 0; function queueDraw(millis_now) { if (drawTimeout) clearTimeout(drawTimeout); @@ -24,12 +26,15 @@ function draw() { var date = new Date(); var h = date.getHours(), m = date.getMinutes(); - var d = date.getDate(), - w = date.getDay(); // d=1..31; w=0..6 - const level = E.getBattery(); - const width = level + (level/2); + var d = date.getDate(); var is12Hour = (require("Storage").readJSON("setting.json", 1) || {})["12hour"]; - var dows = require("date_utils").dows(0,1); + var dow = require("date_utils").dows(0,1)[date.getDay()]; + + if ((date.getTime() >= lastBattCheck + 15*60000) || Bangle.isCharging()) { + lastBattcheck = date.getTime(); + width = E.getBattery(); + width += width/2; + } g.reset(); g.clear(); @@ -47,24 +52,35 @@ function draw() { g.drawString(d, g.getWidth() -6, 98); g.setFont('Vector', 52); g.setFontAlign(-1, -1); - g.drawString(dows[w].slice(0,2).toUpperCase(), 6, 103); + g.drawString(dow.slice(0,2).toUpperCase(), 6, 103); g.fillRect(9,159,166,171); g.fillRect(167,163,170,167); if (Bangle.isCharging()) { g.setColor(1,1,0); - } else if (level > 40) { - g.setColor(0,1,0); + g.fillRect(12,162,12+width,168); } else { g.setColor(1,0,0); + g.fillRect(12,162,57,168); + g.setColor(1,1,0); + g.fillRect(58,162,72,168); + g.setColor(0,1,0); + g.fillRect(73,162,162,168); } - g.fillRect(12,162,12+width,168); - if (level < 100) { + if (width < 150) { g.setColor(g.theme.bg); g.fillRect(12+width+1,162,162,168); } - g.setColor(0, 1, 0); + if (Bangle.isCharging()) { + g.setColor(1,1,0); + } else if (width <= 45) { + g.setColor(1,0,0); + } else if (width <= 60) { + g.setColor(1,1,0); + } else { + g.setColor(0, 1, 0); + } g.fillRect(0, 90, g.getWidth(), 94); // widget redraw @@ -85,7 +101,8 @@ Bangle.on('charging', (charging) => { draw(); }); +Bangle.setUI("clock"); + Bangle.loadWidgets(); draw(); -Bangle.setUI("clock"); diff --git a/apps/bigdclock/metadata.json b/apps/bigdclock/metadata.json index 7359bcf20..30352ca1a 100644 --- a/apps/bigdclock/metadata.json +++ b/apps/bigdclock/metadata.json @@ -1,7 +1,7 @@ { "id": "bigdclock", "name": "Big digit clock containing just the essentials", "shortName":"Big digit clk", - "version":"0.05", + "version":"0.07", "description": "A clock containing just the essentials, made as easy to read as possible for those of us that need glasses. It contains the time, the day-of-week, the day-of-month, and the current battery state-of-charge.", "icon": "bigdclock.png", "type": "clock", diff --git a/apps/bigdclock/screenshot.png b/apps/bigdclock/screenshot.png index 8a12b266e..acac53ea9 100644 Binary files a/apps/bigdclock/screenshot.png and b/apps/bigdclock/screenshot.png differ diff --git a/apps/binclock/ChangeLog b/apps/binclock/ChangeLog index dc4ed8308..7c31cc0d3 100644 --- a/apps/binclock/ChangeLog +++ b/apps/binclock/ChangeLog @@ -1,3 +1,4 @@ 0.01: New App! 0.02: Fixed bug where screen didn't clear so incorrect time displayed. 0.03: Update to use Bangle.setUI instead of setWatch +0.04: Tell clock widgets to hide. diff --git a/apps/binclock/app.js b/apps/binclock/app.js index f8cbe8dd5..d9c74e6ce 100644 --- a/apps/binclock/app.js +++ b/apps/binclock/app.js @@ -164,9 +164,6 @@ Bangle.on('lcdPower',on=>{ draw(); // draw immediately } }); -// Load widgets -Bangle.loadWidgets(); -Bangle.drawWidgets(); // Show launcher when button pressed Bangle.setUI("clockupdown", btn=>{ if (btn!=1) return; @@ -176,3 +173,6 @@ Bangle.setUI("clockupdown", btn=>{ displayTime = 0; } }); +// Load widgets +Bangle.loadWidgets(); +Bangle.drawWidgets(); diff --git a/apps/binclock/metadata.json b/apps/binclock/metadata.json index d17045868..2ca2755a6 100644 --- a/apps/binclock/metadata.json +++ b/apps/binclock/metadata.json @@ -2,7 +2,7 @@ "id": "binclock", "name": "Binary Clock", "shortName": "Binary Clock", - "version": "0.03", + "version": "0.04", "description": "A binary clock with hours and minutes. BTN1 toggles a digital clock.", "icon": "app.png", "type": "clock", diff --git a/apps/binwatch/ChangeLog b/apps/binwatch/ChangeLog index 1e54f489c..e355155b3 100644 --- a/apps/binwatch/ChangeLog +++ b/apps/binwatch/ChangeLog @@ -2,3 +2,5 @@ 0.02: first running version for BangleJs2 0.03: corrected icon, added screen shot, extended description 0.04: corrected format of background image (raw binary) +0.05: move setUI() up before draw() as to not have a false positive 'sanity +check' when building on github. diff --git a/apps/binwatch/app.js b/apps/binwatch/app.js index 28d7a06a5..153bebb32 100644 --- a/apps/binwatch/app.js +++ b/apps/binwatch/app.js @@ -334,6 +334,7 @@ function setRuntimeValues(resolution) { var hour = 0, minute = 1, second = 50; var batVLevel = 20; +Bangle.setUI("clock"); function draw() { var d = new Date(); @@ -371,7 +372,6 @@ function draw() { } // Show launcher when button pressed -Bangle.setUI("clock"); setRuntimeValues(g.getWidth()); g.reset().clear(); Bangle.loadWidgets(); diff --git a/apps/binwatch/metadata.json b/apps/binwatch/metadata.json index 0b5fb2c72..0b4dbc697 100644 --- a/apps/binwatch/metadata.json +++ b/apps/binwatch/metadata.json @@ -3,7 +3,7 @@ "shortName":"BinWatch", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], - "version":"0.04", + "version":"0.05", "supports": ["BANGLEJS2"], "readme": "README.md", "allow_emulator":true, diff --git a/apps/blobclk/ChangeLog b/apps/blobclk/ChangeLog index 9c4ef5b7b..193eb5024 100644 --- a/apps/blobclk/ChangeLog +++ b/apps/blobclk/ChangeLog @@ -5,3 +5,4 @@ 0.04: Modified to account for changes in the behavior of Graphics.fillPoly 0.05: Slight increase to draw speed after LCD on 0.06: Update to use Bangle.setUI instead of setWatch, allow themes and different size screens +0.07: Tell clock widgets to hide. diff --git a/apps/blobclk/clock-blob.js b/apps/blobclk/clock-blob.js index c84b8a1e6..d23e18ff9 100644 --- a/apps/blobclk/clock-blob.js +++ b/apps/blobclk/clock-blob.js @@ -99,6 +99,10 @@ function startTimers() { Bangle.drawWidgets(); intervalRef = setInterval(redraw,1000); } + +// Show launcher when button pressed +Bangle.setUI("clock"); + Bangle.loadWidgets(); startTimers(); Bangle.on('lcdPower',function(on) { @@ -108,5 +112,3 @@ Bangle.on('lcdPower',function(on) { clearTimers(); } }); -// Show launcher when button pressed -Bangle.setUI("clock"); diff --git a/apps/blobclk/metadata.json b/apps/blobclk/metadata.json index 85d7deabe..3ae8de222 100644 --- a/apps/blobclk/metadata.json +++ b/apps/blobclk/metadata.json @@ -2,7 +2,7 @@ "id": "blobclk", "name": "Large Digit Blob Clock", "shortName": "Blob Clock", - "version": "0.06", + "version": "0.07", "description": "A clock with big digits", "icon": "clock-blob.png", "type": "clock", diff --git a/apps/boldclk/ChangeLog b/apps/boldclk/ChangeLog index 30ac31c61..0c6e8cb52 100644 --- a/apps/boldclk/ChangeLog +++ b/apps/boldclk/ChangeLog @@ -3,3 +3,4 @@ 0.04: Work with themes, smaller screens 0.05: Adjust hand lengths to be within 'tick' points 0.06: Removed "wake LCD on face-up"-feature: A watch-face should not set things like "wake LCD on face-up". +0.07: Tell clock widgets to hide. diff --git a/apps/boldclk/bold_clock.js b/apps/boldclk/bold_clock.js index 9d3ea0756..763530a32 100644 --- a/apps/boldclk/bold_clock.js +++ b/apps/boldclk/bold_clock.js @@ -130,9 +130,10 @@ Bangle.on('lcdPower', (on) => { } }); +// Show launcher when button pressed +Bangle.setUI("clock"); + g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); startTimers(); -// Show launcher when button pressed -Bangle.setUI("clock"); diff --git a/apps/boldclk/metadata.json b/apps/boldclk/metadata.json index cf961347d..086203142 100644 --- a/apps/boldclk/metadata.json +++ b/apps/boldclk/metadata.json @@ -1,7 +1,7 @@ { "id": "boldclk", "name": "Bold Clock", - "version": "0.06", + "version": "0.07", "description": "Simple, readable and practical clock", "icon": "bold_clock.png", "screenshots": [{"url":"screenshot_bold.png"}], 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..a70ae3f8a 100644 --- a/apps/bthrm/ChangeLog +++ b/apps/bthrm/ChangeLog @@ -29,3 +29,4 @@ 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 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/lib.js b/apps/bthrm/lib.js new file mode 100644 index 000000000..9e2f0fe63 --- /dev/null +++ b/apps/bthrm/lib.js @@ -0,0 +1,633 @@ +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(()=>{ + 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)); + } + }); + } +}; diff --git a/apps/bthrm/metadata.json b/apps/bthrm/metadata.json index 4d2cb811b..7eedd223c 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.13", "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/bwclk/ChangeLog b/apps/bwclk/ChangeLog index 8a6fe4f9a..546c83894 100644 --- a/apps/bwclk/ChangeLog +++ b/apps/bwclk/ChangeLog @@ -10,4 +10,14 @@ 0.10: HomeAssistant integration if HomeAssistant is installed. 0.11: Performance improvements. 0.12: Implements a 2D menu. -0.13: Clicks < 24px are for widgets, if fullscreen mode is disabled. \ No newline at end of file +0.13: Clicks < 24px are for widgets, if fullscreen mode is disabled. +0.14: Adds humidity to weather data. +0.15: Added option for a dynamic mode to show widgets only if unlocked. +0.16: You can now show your agenda if your calendar is synced with Gadgetbridge. +0.17: Fix - Step count was no more shown in the menu. +0.18: Set timer for an agenda entry by simply clicking in the middle of the screen. Only one timer can be set. +0.19: Fix - Compatibility with "Digital clock widget" +0.20: Better handling of async data such as getPressure. +0.21: On the default menu the week of year can be shown. +0.22: Use the new clkinfo module for the menu. +0.23: Feedback of apps after run is now optional and decided by the corresponding clkinfo. \ No newline at end of file diff --git a/apps/bwclk/README.md b/apps/bwclk/README.md index eb5356a7e..d869fa2cf 100644 --- a/apps/bwclk/README.md +++ b/apps/bwclk/README.md @@ -1,46 +1,49 @@ # BW Clock -A very minimalistic clock with date and time in focus. +A very minimalistic clock. ![](screenshot.png) ## Features -The BW clock provides many features as well as 3rd party integrations: +The BW clock implements features that are exposed by other apps through the `clkinfo` module. +For example, if you install the HomeAssistant app, this menu item will be shown if you click right +and additionally allows you to send triggers directly from the clock (select triggers via up/down and +send via click center). Here are examples of other apps that are integrated: + - Bangle data such as steps, heart rate, battery or charging state. -- A timer can be set directly. *Requirement: Scheduler library* +- Show agenda entries. A timer for an agenda entry can also be set by simply clicking in the middle of the screen. This can be used to not forget a meeting etc. Note that only one agenda-timer can be set at a time. *Requirement: Gadgetbridge calendar sync enabled* - Weather temperature as well as the wind speed can be shown. *Requirement: Weather app* - HomeAssistant triggers can be executed directly. *Requirement: HomeAssistant app* Note: If some apps are not installed (e.gt. weather app), then this menu item is hidden. -## Menu -2D menu allows you to display lots of different data including data from 3rd party apps and it's also possible to control things e.g. to set a timer or send a HomeAssistant trigger. - -Simply click left / right to go through the menu entries such as Bangle, Timer etc. -and click up/down to move into this sub-menu. You can then click in the middle of the screen -to e.g. send a trigger via HomeAssistant once you selected it. - -``` - +5min - | - Bangle -- Timer[Optional] -- Weather[Optional] -- HomeAssistant [Optional] - | | | | - Bpm -5min Temperature Trigger1 - | | | - Steps ... ... - | - Battery -``` - ## Settings -- Fullscreen on/off (widgets are still loaded). -- Enable/disable lock icon in the settings. Useful if fullscreen is on. +- Screen: Normal (widgets shown), Dynamic (widgets shown if unlocked) or Full (widgets are hidden). +- Enable/disable lock icon in the settings. Useful if fullscreen mode is on. - The colon (e.g. 7:35 = 735) can be hidden in the settings for an even larger time font to improve readability further. -- There are no design settings, as your bangle sys settings are used. +- Your bangle uses the sys color settings so you can change the color too. + +## Menu structure +2D menu allows you to display lots of different data including data from 3rd party apps and it's also possible to control things e.g. to trigger HomeAssistant. + +Simply click left / right to go through the menu entries such as Bangle, Weather etc. +and click up/down to move into this sub-menu. You can then click in the middle of the screen +to e.g. send a trigger via HomeAssistant once you selected it. The actions really depend +on the app that provide this sub-menu through the `clkinfo` module. + +``` + Bangle -- Agenda -- Weather -- HomeAssistant + | | | | + Battery Entry 1 Temperature Trigger1 + | | | | + Steps ... ... ... + | + ... +``` ## Thanks to -Icons created by Flaticon - +- Thanks to Gordon Williams not only for the great BangleJs, but specifically also for the implementation of `clkinfo` which simplified the BWClock a lot and moved complexety to the apps where it should be located. +- Icons created by Flaticon ## Creator [David Peer](https://github.com/peerdavid) diff --git a/apps/bwclk/app.js b/apps/bwclk/app.js index 2d629abe8..7dcca9d75 100644 --- a/apps/bwclk/app.js +++ b/apps/bwclk/app.js @@ -1,22 +1,25 @@ -/************ +/************************************************ * Includes */ const locale = require('locale'); const storage = require('Storage'); +const clock_info = require("clock_info"); -/************ - * Statics + +/************************************************ + * Globals */ const SETTINGS_FILE = "bwclk.setting.json"; -const TIMER_IDX = "bwclk"; const W = g.getWidth(); const H = g.getHeight(); +var lock_input = false; -/************ + +/************************************************ * Settings */ let settings = { - fullscreen: false, + screen: "Normal", showLock: true, hideColon: false, menuPosX: 0, @@ -28,22 +31,10 @@ for (const key in saved_settings) { settings[key] = saved_settings[key] } - -/************ +/************************************************ * Assets */ // Manrope font -Graphics.prototype.setXLargeFont = function(scale) { - // Actual height 53 (55 - 3) - this.setFontCustom( - E.toString(require('heatshrink').decompress(atob('AHM/8AIG/+AA4sD/wQGh/4EWQA/AC8YA40HNA0BRY8/RY0P/6LFgf//4iFA4IiFj4HBEQkHCAQiDHIIZGv4HCFQY5BDAo5CAAIpDDAfACA3wLYv//hsFKYxcCMgoiBOooiBQwwiBS40AHIgA/ACS/DLYjYCBAjQEBAYQDBAgHDUAbyDZQi3CegoHEVQQZFagUfW4Y0DaAgECaIJSEFYMPbIYNDv5ACGAIrBCgJ1EFYILCAAQWCj4zDGgILCegcDEQRNDHIIiCHgZ2BEQShFIqUDFYidCh5ODg4NCn40DAgd/AYR5BDILZEAAIMDAAYVCh7aHdYhKDbQg4Dv7rGBAihFCAwIDCAgA/AB3/eoa7GAAk/dgbVGDJrvCDK67DDIjaGdYpbCdYonCcQjjDEVUBEQ4A/AEMcAYV/NAUHcYUDawd/cYUPRYSmBBgaLBToP8BgYiBSgIiCj4iCg//EQSuDW4IMDVwYiCBgIiBBgrRDCATeBaIYqCv70DCgT4CEQMfIgQZBBoRnDv/3EQIvBDIffEQMHFwReBRYUfOgX/+IiDKIeHEQRRECwUHKwIuB8AiDIoJEBCwZFCv/4HIZaBIgPAEQS2CUYQiCD4SABEQcfOwIZBEQaHBO4RcEAAI/BEQQgBSIQiDTIRZBEQZuBVYQiDHoKWCEQQICFQIiDBAQeCEQQA/AANwA40BLIJ5BO4JWCBAUPAYR5En7RBUIQECN4SYCQQIiEh6CCEQk/BoQiBgYeCBoTrCAgT0CCgIfCFYQiBg4IBGgIiDj6rBg4rCBYLRDFYIiBbYIfBLgQiBIQYiD4JCCLgf/bQIWDBYV/EQV/BYXz/5FBgIiD5//IowZBD4M/NAX/BIPgDIJoC//5GgKUDn//4f/8KLE/wTBAAI8BEQPwj4HBVwYmBDgIZDN4QZCGYKJCHQP/JoSgCBATrCh5dBKITVDG4gICAAbvDAH5SCL4QADK4J5CCAiTCCAp1BCAqCDCAgiGCAIiFCAQiFeoIiFg6/FCAgiECAXnEQgQB/kfEQYQC4F/EQYQCgIiDfoIQBg4iDCAUAEQZUCcgIiDDIIQBEQhuBBoIiENoYiFDwQiECAQiFwEBPQQNCAQKDDEYMDDoMfRh4iGUwqvEESBiBaQ5oEbgr0FNAo+EEIwA+oAHGgJoFRAMHe4L0CAALNBBAT0BfwScDCAXweAL0DWgUPQYQiDwF/QYQiC/zTB+C0FBAL0CEQYIBGgMPCgIxBg4rCJIKsCh5IBBwTPCj4WBgYLBZ4V/MAIiBBQQrBEQYtCBYQiCO4QLFCwgiDIQIiGIoMHEQpFBn5FFD4JoENwRoGDgSUCAoKfBw//DgIiCT4auCFwN/T4RRET4TaCEQKoCDIQiCGgK/DAAQICdYQACHoIqCBAoQFEwIhFAH4AFQIROEj4IGXwIIGNwIACbgIhEBAiRCVwoqDTogHEW4QZFXgIZB/z9Cv49CF4MPBwI0Ca4LlB8ATCJoP4AoINDfQPAg7PBg4cBBwUfD4MfFYILCCwgOCf4QLEwEPCwILCgJaBn4WBBYQxCIQQiD+EDCYI5CBYRQBIo4fBMQIuBC4N/NAv8AoIcBSgU/FYIIBZIYrCW4hOCXIQZCgYUBv7jEh4uBZAscewZ8CgEgUYT0EEoQIBA4gICFQQIEHYQA+KQzdDAArdCAArpCEScHaIQiEvwiGe4QiFUwQiEbgIiFYIL0DEQTkBEQrJEEQc/cYYiCg4HBDIQiCfoRoEHQLaDEQQHBbQYiBCAT8Dn/BCAoXBJYP/OgZKC/6OEEARLCEQZLEEQZLEEQjKFEQI6EEQZLDEQbsGEQLjGYYYA/JIxzEg/AfgJSDAoPgfgiDC8COFAoPnaQj6CAAR+CW4TCFA4i6CDIqhCDIfwHoYHCYIN/GgKuBJ4JDBFYUf/C5CBYIZBv/Ag4ZBg4rBBYQTBAQIcBg4FBn5UBAQUfFwIfCEQeAgYfBAQUBFAKbCAQQiCGwIiE+A2BwBFNwE/AoM/EQJoIWwKCCh4cBFYKUERYV/W46uHFYIZGaJA0B/glBGYT0JIITiEMIJvCFQQAEHYQA/ABBlEOIhdGQAIRFSgQIBgQICn4IB8EAjiBCUYglCbQYeBEoQZCTwM/CYIZD/gEBUwIzBJ4UHYAU/EwIrBh4rCAoIXCn4rBCgUDAQN/FYMfBYIXBCYJnCBYXggf8HgQLCwEPEQQuBgJOECwILDCwgiLHIUHBYJFGD4IxBgYWCn4rBBwJoFDIYNBCgPADgKHBRYfDBQN/GAIrBToTLDVwYACDILiCWAb8DAAYzBYAjTCAAI9BAARNCBAoqCBAgQDFgbYCAH4AufgQACf4T8CAAT/CfgQACBwITCAAYOBCYQioh4iEAHQA=='))), - 46, - atob("FR4uHyopKyksJSssGA=="), - 70+(scale<<8)+(1<<16) - ); -}; - - Graphics.prototype.setLargeFont = function(scale) { // Actual height 47 (48 - 2) this.setFontCustom( @@ -55,14 +46,12 @@ Graphics.prototype.setLargeFont = function(scale) { return this; }; - Graphics.prototype.setMediumFont = function(scale) { // Actual height 41 (42 - 2) this.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAA/AAAAAAAA/AAAAAAAA/AAAAAAAA/AAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAAAAAAB/AAAAAAAP/AAAAAAD//AAAAAA///AAAAAP///AAAAB///8AAAAf///AAAAH///wAAAB///+AAAAH///gAAAAH//4AAAAAH/+AAAAAAH/wAAAAAAH8AAAAAAAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///8AAAAH////AAAAP////wAAAf////4AAA/////8AAB/////+AAD/gAAH+AAD+AAAD/AAH8AAAB/AAH4AAAA/gAH4AAAAfgAH4AAAAfgAPwAAAAfgAPwAAAAfgAPwAAAAfgAHwAAAAfgAH4AAAAfgAH4AAAA/gAH8AAAA/AAD+AAAD/AAD/gAAH/AAB/////+AAB/////8AAA/////4AAAf////wAAAH////gAAAB///+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAAAAfwAAAAAAA/gAAAAAAA/AAAAAAAB/AAAAAAAD+AAAAAAAD8AAAAAAAH8AAAAAAAH//////AAH//////AAH//////AAH//////AAH//////AAH//////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AAA/AAAP4AAB/AAAf4AAD/AAA/4AAD/AAB/4AAH/AAD/4AAP/AAH/AAAf/AAH8AAA//AAH4AAB//AAP4AAD//AAPwAAH+/AAPwAAP8/AAPwAAf4/AAPwAA/4/AAPwAA/w/AAPwAB/g/AAPwAD/A/AAP4AH+A/AAH8AP8A/AAH/A/4A/AAD///wA/AAD///gA/AAB///AA/AAA//+AA/AAAP/8AA/AAAD/wAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAH4AAAHwAAH4AAAH4AAH4AAAH8AAH4AAAP+AAH4AAAH+AAH4A4AB/AAH4A+AA/AAH4B/AA/gAH4D/AAfgAH4H+AAfgAH4P+AAfgAH4f+AAfgAH4/+AAfgAH5/+AAfgAH5//AAfgAH7+/AA/gAH/8/gB/AAH/4f4H/AAH/wf//+AAH/gP//8AAH/AH//8AAH+AD//wAAH8AB//gAAD4AAf+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAAAAD/AAAAAAAP/AAAAAAB//AAAAAAH//AAAAAAf//AAAAAB///AAAAAH///AAAAAf/8/AAAAB//w/AAAAH/+A/AAAA//4A/AAAD//gA/AAAH/+AA/AAAH/4AA/AAAH/gAA/AAAH+AAA/AAAHwAAA/AAAHAAf///AAEAAf///AAAAAf///AAAAAf///AAAAAf///AAAAAf///AAAAAAA/AAAAAAAA/AAAAAAAA/AAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAAP/AHgAAH///AP4AAH///gP8AAH///gP8AAH///gP+AAH///gD/AAH/A/AB/AAH4A/AA/gAH4A+AAfgAH4B+AAfgAH4B+AAfgAH4B8AAfgAH4B8AAfgAH4B+AAfgAH4B+AAfgAH4B+AA/gAH4B/AA/AAH4A/gD/AAH4A/4H+AAH4Af//+AAH4AP//8AAH4AP//4AAHwAD//wAAAAAB//AAAAAAAf8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///8AAAAD////AAAAP////wAAAf////4AAA/////8AAB/////+AAD/gP4H+AAD/AfgD/AAH8A/AB/AAH8A/AA/gAH4B+AAfgAH4B+AAfgAPwB8AAfgAPwB8AAfgAPwB+AAfgAPwB+AAfgAH4B+AAfgAH4B/AA/gAH8B/AB/AAH+A/wD/AAD+A/8P+AAB8Af//+AAB4AP//8AAAwAH//4AAAAAD//gAAAAAA//AAAAAAAP4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAPwAAAAAAAPwAAAAAAAPwAAAAAAAPwAAAAHAAPwAAAA/AAPwAAAD/AAPwAAAf/AAPwAAB//AAPwAAP//AAPwAA//8AAPwAH//wAAPwAf/+AAAPwB//4AAAPwP//AAAAPw//8AAAAP3//gAAAAP//+AAAAAP//wAAAAAP//AAAAAAP/4AAAAAAP/gAAAAAAP+AAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+AAAAH+A//gAAAf/h//4AAA//z//8AAB/////+AAD/////+AAD///+H/AAH+H/4B/AAH8B/wA/gAH4A/gAfgAH4A/gAfgAPwA/AAfgAPwA/AAfgAPwA/AAfgAPwA/AAfgAH4A/gAfgAH4A/gAfgAH8B/wA/gAH/H/4B/AAD///+H/AAD/////+AAB/////+AAA//z//8AAAf/h//4AAAH+A//gAAAAAAH+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAAD/8AAAAAAP/+AAAAAAf//AAcAAA///gA8AAB///wB+AAD/x/4B/AAD+AP4B/AAH8AH8A/gAH4AH8A/gAH4AD8AfgAP4AD8AfgAPwAB8AfgAPwAB8AfgAPwAB8AfgAPwAB8AfgAH4AD8AfgAH4AD4A/gAH8AH4B/AAD+APwD/AAD/g/wP+AAB/////+AAA/////8AAAf////4AAAP////wAAAH////AAAAA///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8APwAAAAD8APwAAAAD8APwAAAAD8APwAAAAD8APwAAAAD8APwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="), 46, atob("DxcjFyAfISAiHCAiEg=="), 54+(scale<<8)+(1<<16)); return this; }; - Graphics.prototype.setSmallFont = function(scale) { // Actual height 28 (27 - 0) this.setFontCustom( @@ -74,6 +63,16 @@ Graphics.prototype.setSmallFont = function(scale) { return this; }; +Graphics.prototype.setMiniFont = function(scale) { + // Actual height 16 (15 - 0) + this.setFontCustom( + atob('AAAAAAAAAAAAAP+w/5AAAAAA4ADgAOAA4AAAAAAAAAABgBmAGbAb8D+A+YDZ8B/wf4D5gJmAGQAQAAAAAAAeOD8cMwzxj/GPMYwc/Az4AAAAAHAA+DDIYMjA+YBzAAYADeA7MHMw4zDD4ADAAAAz4H/wzjDHMMMwwbBj4APgADAAAAAA4ADgAAAAAAAAAAfwH/54B+ABAAAAAOABeAcf/gfwAAAAACAAaAD4APgAOABgAAAAAAACAAIAAgA/wAMAAgACAAAAAAAAPAA4AAAAAAIAAgACAAIAAgAAAAAAADAAMAAAAAAAcAfwf4D4AIAAAAA/wH/gwDDAMMAwwDB/4D/AAAAAAGAAwAD/8P/wAAAAAHAw8HDA8MHww7DnMH4wGBAAAMBgyHDcMPww/DDv4MfAAAAAAAHgD+A+YPhgwGAH8AfwAEAAAAAA/GD8cMwwzDDMMM5wx+ABgAAAP8B/4MwwzDDMMMwwx+ADwAAAgADAAMBwwfDPgP4A8ADAAAAAe+D/8M4wxjDGMP5wf+ABwAAAfAB+cMYwwjDCMMYwf+A/wAAAAAAAAAxgBCAAAAAAAAAYPBA4AAAAAAAAAgAHAA+AHMAYYAAAAAAAAAAAAAAJAAkACQAJAAkACQAJAAkAAAAAAAAAAAAAABhgHMAPgAcAAgAAAAAAAABgAOAAwbDDsMYA/AA4AAAAAAAD4A/wGBgxzDPsMyQjJDPkM+wYIBxgD+AAAAAAABAA8A/gf8DwwODA/sAfwAHwADAAAP/w//DGMMYwxjDOMP9we+ABwA8AP8Bw4MAwwDDAMMAwwDDgcHDgMMAAAAAA//D/8MAwwDDAMMAw4HB/4D/AAAAAAP/w//DGMMYwxjDGMMQwgBAAAP/w//DDAMMAwwDDAMAADwA/wHDgwDDAMMAwwDDCMOJwc+ADwAAA//D/8AMAAwADAAMAAwD/8P/wAAAAAP/w//AAAABgAHAAMAAwAHD/4P+AAAAAAP/w//AOAB+AOcBw4MBwgDAAEAAA//D/8AAwADAAMAAwADAAAP/w//A8AA8AA+AA8AHwB8AeAHgA//D/8AAAAAD/8P/wcAAcAA8AA4AB4P/w//AAAA8AP8Bw4MAwwDDAMMAwwDDgcH/gP8AAAAAA//D/8MMAwwDDAMYA7gB8ABgADwA/wHDgwDDAMMAwwDDA8ODwf/A/8AAAAAD/8P/wwwDDAMMAx4Dv4HxwEBAAAHjg/HDMMMYwxjDGMONwc+ABwMAAwADAAMAA//D/8MAAwADAAIAAAAD/wP/gAHAAMAAwADAAMAHg/8AAAMAA+AA/AAfgAPAA8AfgPwD4AMAAwAD4AD+AA/AA8A/g/gDwAP4AH8AB8APwH8D8AMAAgBDAMPDgO8APAB8AOcDw8MAwgBCAAOAAeAAeAAfwH/B4AOAAwAAAAMAwwPDB8Mew3jD4MPAwwDAAAAAAAAB//3//QABAAAAAAADgAP4AH+AB8AAQAABAAEAAf/9//wAAAAAAAAAAGAAwAGAAwABgADAAGAAAAAAAAAQABAAEAAQABAAEAAQABAAEAAQABAAAAAAAAAAAAAAAAAAAAAAAQA3wHbAZMBswGzAf4A/wAAAAAP/w//AYMBgwGDAYMA/gB8AAAAEAD+Ae8BgwGDAYMBgwDGAAAAMAD+Ae8BgwGDAYMBhw//D/8AAAAYAP4B/wGTAZMBkwGTAP4AcAEAAYAP/w//CQAJAAAwAP4hz3GDMQMxAzGHcf/h/8AAAAAP/w//AYABgAGAAYAA/wB/AAAAAA3/Df8AAAAAOf/9//AAAAAP/w//ADgAfADGAYMBAQAAD/8P/wAAAAAB/wH/AYABgAGAAf8A/wGAAYABgAH/AP8AAAAAAf8B/wGAAYABgAGAAP8AfwAAADAA/gHvAYMBgwGDAYMA/gB8AAAAAAH/8f/xgwGDAYMBgwD+AHwAAAAwAP4B7wGDAYMBgwGHAf/x//AAAAAB/wH/AYABgAEAAAAA5gHzAbMBkwGbAd8AzgEAAYAP/wf/AQMBAwAAAAAB/gH/AAMAAwADAAcB/wH/AAABAAHgAPwAHwAPAH4B8AGAAQAB8AB+AA8APwHwAeAA/AAPAD8B+AHAAQEBgwHOAHwAOAD+AccBAwAAAQAB4AD4EB/wB8A/APgBwAAAAAEBgwGPAZ8B8wHjAcMBAQAAAAAABgf/9/n2AAAAAAAP/w//AAAEAAYAB/nz//AGAAAAAAAAAAAAcABgAGAAcAAwAHAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'), + 32, + atob("AwUHDwoOCwQHBwcJBAcEBgoGCQkKCQoICQoFBQoMCgkPCgoMCwkICwsECAoIDgsMCgwKCgoLCg8KCQoHBgcLCwgJCgkKCQYKCgQECAQOCgoKCgYIBwoIDAkJCAcEBwsQ"), + 16+(scale<<8)+(1<<16) + ); + return this; +}; function imgLock(){ return { @@ -83,279 +82,100 @@ function imgLock(){ } } -function imgSteps(){ - return { - width : 24, height : 24, bpp : 1, - transparent : 1, - buffer : require("heatshrink").decompress(atob("/H///wv4CBn4CD8ACCj4IBj8f+Eeh/wjgCBngCCg/4nEH//4h/+jEP/gRBAQX+jkf/wgB//8GwP4FoICDHgICCBwIA==")) - } -} -function imgBattery(){ - return { - width : 24, height : 24, bpp : 1, - transparent : 1, - buffer : require("heatshrink").decompress(atob("/4AN4EAg4TBgd///9oEAAQv8ARQRDDQQgCEwQ4OA")) - } -} - -function imgCharging() { - return { - width : 24, height : 24, bpp : 1, - transparent : 1, - buffer : require("heatshrink").decompress(atob("//+v///k///4AQPwBANgBoMxBoMb/P+h/w/kH8H4gfB+EBwfggHH4EAt4CBn4CBj4CBh4FCCIO/8EB//Agf/wEH/8Gh//x////fAQIA=")) - } -} - -function imgBpm() { - return { - width : 24, height : 24, bpp : 1, - transparent : 1, - buffer : require("heatshrink").decompress(atob("/4AOn4CD/wCCjgCCv/8jF/wGYgOA5MB//BC4PDAQnjAQPnAQgANA")) - } -} - -function imgTemperature() { - return { - width : 24, height : 24, bpp : 1, - transparent : 1, - buffer : require("heatshrink").decompress(atob("//D///wICBjACBngCNkgCP/0kv/+s1//nDn/8wICEBAIOC/08v//IYJECA==")) - } -} - -function imgWeather(){ - return { - width : 24, height : 24, bpp : 1, - transparent : 0, - buffer : require("heatshrink").decompress(atob("AAcYAQ0MgEwAQUAngLB/8AgP/wACCgf/4Fz//OAQQICCIoaCEAQpGHA4ACA=")) - } -} - -function imgWind () { - return { - width : 24, height : 24, bpp : 1, - transparent : 1, - buffer : require("heatshrink").decompress(atob("/0f//8h///Pn//zAQXzwf/88B//mvGAh18gEevn/DIICB/PwgEBAQMHBAIADFwM/wEAGAP/54CD84CE+eP//wIQU/A==")) - } -} - -function imgTimer() { - return { - width : 24, height : 24, bpp : 1, - transparent : 1, - buffer : require("heatshrink").decompress(atob("/+B/4CD84CEBAPygFP+F+h/x/+P+fz5/n+HnAQNn5/wuYCBmYCC5kAAQfOgFz80As/ngHn+fD54mC/F+j/+gF/HAQA==")) - } -} - -function imgWatch() { - return { - width : 24, height : 24, bpp : 1, - transparent : 1, - buffer : require("heatshrink").decompress(atob("/8B//+ARANB/l4//5/1/+f/n/n5+fAQnf9/P44CC8/n7/n+YOB/+fDQQgCEwQsCHBBEC")) - } -} - -function imgHomeAssistant() { - return { - width : 48, height : 48, bpp : 1, - transparent : 0, - buffer : require("heatshrink").decompress(atob("AD8BwAFDg/gAocP+AFDj4FEn/8Aod//wFD/1+FAf4j+8AoMD+EPDAUH+OPAoUP+fPAoUfBYk/C4l/EYIwC//8n//FwIFEgYFD4EH+E8nkP8BdBAonjjk44/wj/nzk58/4gAFDF4PgCIMHAoPwhkwh4FB/EEkEfIIWAHwIFC4A+BAoXgg4FDL4IFDL4IFDLIYFkAEQA==")) - } -} - - -/************ - * 2D MENU with entries of: - * [name, icon, opt[customDownFun], opt[customUpFun], opt[customCenterFun]] - * +/************************************************ + * Menu */ -var menu = [ - [ - function(){ return [ null, null ] }, - ], - [ - function(){ return [ "Bangle", imgWatch() ] }, - function(){ return [ E.getBattery() + "%", Bangle.isCharging() ? imgCharging() : imgBattery() ] }, - function(){ return [ getSteps(), imgSteps() ] }, - function(){ return [ Math.round(Bangle.getHealthStatus("last").bpm) + " bpm", imgBpm()] }, +// Custom bwItems menu - therefore, its added here and not in a clkinfo.js file. +var bwItems = { + name: null, + img: null, + items: [ + { name: "WeekOfYear", + get: () => ({ text: "Week " + weekOfYear(), img: null}), + show: function() { bwItems.items[0].emit("redraw"); }, + hide: function () {} + }, ] -] +}; -/* - * Timer Menu - */ -try{ - require('sched'); - menu.push([ - function(){ - var text = isAlarmEnabled() ? getAlarmMinutes() + " min." : "Timer"; - return [text, imgTimer(), () => decreaseAlarm(), () => increaseAlarm(), null ] - }, - ]); -} catch(ex) { - // If sched is not installed, we hide this menu item -} - -/* - * WEATHER MENU - */ -if(storage.readJSON('weather.json') !== undefined){ - menu.push([ - function(){ return [ "Weather", imgWeather() ] }, - function(){ return [ getWeather().temp, imgTemperature() ] }, - function(){ return [ getWeather().wind, imgWind() ] }, - ]); +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); } -/* - * HOME ASSISTANT MENU - */ -try{ - var triggers = require("ha.lib.js").getTriggers(); - var haMenu = [ - function(){ return [ "Home", imgHomeAssistant() ] }, - ]; +// Load menu +var menu = clock_info.load(); +menu = menu.concat(bwItems); - triggers.forEach(trigger => { - haMenu.push(function(){ - return [trigger.display, trigger.getIcon(), () => {}, () => {}, function(){ - var ha = require("ha.lib.js"); - ha.sendTrigger("TRIGGER_BW"); - ha.sendTrigger(trigger.trigger); - }] - }); + +// 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; +} + +// Set draw functions for each item +menu.forEach((menuItm, x) => { + menuItm.items.forEach((item, y) => { + function drawItem() { + // For the clock, we have a special case, as we don't wanna redraw + // immediately when something changes. Instead, we update data each minute + // to save some battery etc. Therefore, we hide (and disable the listener) + // immedeately after redraw... + item.hide(); + + // After drawing the item, we enable inputs again... + lock_input = false; + + var info = item.get(); + drawMenuItem(info.text, info.img); + } + + item.on('redraw', drawItem); }) - menu.push(haMenu); -} catch(ex){ - // If HomeAssistant is not installed, we hide this item -} +}); -function getMenuEntry(){ - // In case the user removes HomeAssistant entries, showInfo - // could be larger than infoArray.length... - settings.menuPosX = settings.menuPosX % menu.length; - settings.menuPosY = settings.menuPosY % menu[settings.menuPosX].length; - return menu[settings.menuPosX][settings.menuPosY](); -} - - -/************ - * Helper - */ -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. +function canRunMenuItem(){ + if(settings.menuPosY == 0){ + return false; } - steps = Math.round(steps/100) / 10; // This ensures that we do not show e.g. 15.0k and 15k instead - return steps + "k"; + var menuEntry = menu[settings.menuPosX]; + var item = menuEntry.items[settings.menuPosY-1]; + return item.run !== undefined; } -function getWeather(){ - var weatherJson; - - try { - weatherJson = storage.readJSON('weather.json'); - var weather = weatherJson.weather; - - // Temperature - weather.temp = locale.temp(weather.temp-273.15); - - // Humidity - weather.hum = weather.hum + "%"; - - // Wind - const wind = locale.speed(weather.wind).match(/^(\D*\d*)(.*)$/); - weather.wind = Math.round(wind[1]) + "kph"; - - return weather - - } catch(ex) { - // Return default +function runMenuItem(){ + if(settings.menuPosY == 0){ + return; } - return { - temp: " ? ", - hum: " ? ", - txt: " ? ", - wind: " ? ", - wdir: " ? ", - wrose: " ? " - }; -} - - -function isAlarmEnabled(){ + var menuEntry = menu[settings.menuPosX]; + var item = menuEntry.items[settings.menuPosY-1]; try{ - var alarm = require('sched'); - var alarmObj = alarm.getAlarm(TIMER_IDX); - if(alarmObj===undefined || !alarmObj.on){ - return false; + var ret = item.run(); + if(ret){ + Bangle.buzz(300, 0.6); } - - return true; - - } catch(ex){ } - return false; -} - - -function getAlarmMinutes(){ - if(!isAlarmEnabled()){ - return -1; + } catch (ex) { + // Simply ignore it... } - - var alarm = require('sched'); - var alarmObj = alarm.getAlarm(TIMER_IDX); - return Math.round(alarm.getTimeToAlarm(alarmObj)/(60*1000)); } -function increaseAlarm(){ - try{ - var minutes = isAlarmEnabled() ? getAlarmMinutes() : 0; - var alarm = require('sched') - alarm.setAlarm(TIMER_IDX, { - timer : (minutes+5)*60*1000, - }); - alarm.reload(); - } catch(ex){ } -} - - -function decreaseAlarm(){ - try{ - var minutes = getAlarmMinutes(); - minutes -= 5; - - var alarm = require('sched') - alarm.setAlarm(TIMER_IDX, undefined); - - if(minutes > 0){ - alarm.setAlarm(TIMER_IDX, { - timer : minutes*60*1000, - }); - } - - alarm.reload(); - } catch(ex){ } -} - - -/************ - * DRAW +/************************************************ + * Draw */ function draw() { // Queue draw again @@ -363,7 +183,7 @@ function draw() { // Draw clock drawDate(); - drawTime(); + drawMenuAndTime(); drawLock(); drawWidgets(); } @@ -371,12 +191,12 @@ function draw() { function drawDate(){ // Draw background - var y = H/5*2; - g.reset().clearRect(0,0,W,W); + var y = H/5*2 + (isFullscreen() ? 0 : 8); + g.reset().clearRect(0,0,W,y); // Draw date y = parseInt(y/2)+4; - y += settings.fullscreen ? 0 : 13; + y += isFullscreen() ? 0 : 8; var date = new Date(); var dateStr = date.getDate(); dateStr = ("0" + dateStr).substr(-2); @@ -395,15 +215,12 @@ function drawDate(){ g.setMediumFont(); g.setColor(g.theme.fg); - g.drawString(dateStr, W/2 - fullDateW / 2, y+1); + g.drawString(dateStr, W/2 - fullDateW / 2, y+2); } -function drawTime(){ +function drawTime(y, smallText){ // Draw background - var y = H/5*2 + (settings.fullscreen ? 0 : 8); - g.setColor(g.theme.fg); - g.fillRect(0,y,W,H); var date = new Date(); // Draw time @@ -419,45 +236,65 @@ function drawTime(){ // Set y coordinates correctly y += parseInt((H - y)/2) + 5; - var menuEntry = getMenuEntry(); - var menuName = menuEntry[0]; - var menuImg = menuEntry[1]; - var printImgLeft = settings.menuPosY != 0; - // Show large or small time depending on info entry - if(menuName == null){ - if(settings.hideColon){ - g.setXLargeFont(); - } else { - g.setLargeFont(); - } - } else { + if(smallText){ y -= 15; g.setMediumFont(); + } else { + g.setLargeFont(); } g.drawString(timeStr, W/2, y); +} - // Draw menu if set - if(menuName == null){ +function drawMenuItem(text, image){ + // First clear the time region + var y = H/5*2 + (isFullscreen() ? 0 : 8); + + g.setColor(g.theme.fg); + g.fillRect(0,y,W,H); + + // Draw menu text + var hasText = (text != null && text != ""); + if(hasText){ + g.setFontAlign(0,0); + + // For multiline text we show an even smaller font... + text = String(text); + if(text.split('\n').length > 1){ + g.setMiniFont(); + } else { + g.setSmallFont(); + } + + var imgWidth = image == null ? 0 : 24; + var strWidth = g.stringWidth(text); + g.setColor(g.theme.fg).fillRect(0, 149-14, W, H); + g.setColor(g.theme.bg).drawString(text, W/2 + imgWidth/2 + 2, 149+3); + + if(image != null){ + var scale = imgWidth / image.width; + g.drawImage(image, W/2 + -strWidth/2-4 - parseInt(imgWidth/2), 149 - parseInt(imgWidth/2), {scale: scale}); + } + } + + // Draw time + drawTime(y, hasText); +} + + +function drawMenuAndTime(){ + var menuEntry = menu[settings.menuPosX]; + + // The first entry is the overview... + if(settings.menuPosY == 0){ + drawMenuItem(menuEntry.name, menuEntry.img); return; } - y += 35; - g.setFontAlign(0,0); - g.setSmallFont(); - var imgWidth = 0; - if(menuImg !== undefined){ - imgWidth = 24.0; - var strWidth = g.stringWidth(menuName); - var scale = imgWidth / menuImg.width; - g.drawImage( - menuImg, - W/2 + (printImgLeft ? -strWidth/2-2 : strWidth/2+2) - parseInt(imgWidth/2), - y - parseInt(imgWidth/2), - { scale: scale } - ); - } - g.drawString(menuName, printImgLeft ? W/2 + imgWidth/2 + 2 : W/2 - imgWidth/2 - 2, y+3); + // Draw item if needed + lock_input = true; + var item = menuEntry.items[settings.menuPosY-1]; + item.show(); } @@ -470,7 +307,7 @@ function drawLock(){ function drawWidgets(){ - if(settings.fullscreen){ + if(isFullscreen()){ for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";} } else { Bangle.drawWidgets(); @@ -478,9 +315,19 @@ function drawWidgets(){ } +function isFullscreen(){ + var s = settings.screen.toLowerCase(); + if(s == "dynamic"){ + return Bangle.isLocked() + } else { + return s == "full" + } +} -/* - * Draw timeout + + +/************************************************ + * Listener */ // timeout used to update every minute var drawTimeout; @@ -508,6 +355,13 @@ Bangle.on('lcdPower',on=>{ Bangle.on('lock', function(isLocked) { if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = undefined; + + if(!isLocked && settings.screen.toLowerCase() == "dynamic"){ + // If we have to show the widgets again, we load it from our + // cache and not through Bangle.loadWidgets as its much faster! + for (let wd of WIDGETS) {wd.draw=wd._draw;wd.area=wd._area;} + } + draw(); }); @@ -516,13 +370,13 @@ Bangle.on('charging',function(charging) { drawTimeout = undefined; // Jump to battery - settings.menuPosX = 1; + settings.menuPosX = 0; settings.menuPosY = 1; draw(); }); Bangle.on('touch', function(btn, e){ - var widget_size = settings.fullscreen ? 0 : 20; // Its not exactly 24px -- empirically it seems that 20 worked better... + var widget_size = isFullscreen() ? 0 : 20; // Its not exactly 24px -- empirically it seems that 20 worked better... var left = parseInt(g.getWidth() * 0.22); var right = g.getWidth() - left; var upper = parseInt(g.getHeight() * 0.22) + widget_size; @@ -534,17 +388,15 @@ Bangle.on('touch', function(btn, e){ var is_right = e.x > right && !is_upper && !is_lower; var is_center = !is_upper && !is_lower && !is_left && !is_right; + if(lock_input){ + return; + } + if(is_lower){ Bangle.buzz(40, 0.6); - settings.menuPosY = (settings.menuPosY+1) % menu[settings.menuPosX].length; + settings.menuPosY = (settings.menuPosY+1) % (menu[settings.menuPosX].items.length+1); - // Handle custom menu entry function - var menuEntry = getMenuEntry(); - if(menuEntry.length > 2){ - menuEntry[2](); - } - - drawTime(); + drawMenuAndTime(); } if(is_upper){ @@ -554,22 +406,16 @@ Bangle.on('touch', function(btn, e){ Bangle.buzz(40, 0.6); settings.menuPosY = settings.menuPosY-1; - settings.menuPosY = settings.menuPosY < 0 ? menu[settings.menuPosX].length-1 : settings.menuPosY; + settings.menuPosY = settings.menuPosY < 0 ? menu[settings.menuPosX].items.length : settings.menuPosY; - // Handle custom menu entry function - var menuEntry = getMenuEntry(); - if(menuEntry.length > 3){ - menuEntry[3](); - } - - drawTime(); + drawMenuAndTime(); } if(is_right){ Bangle.buzz(40, 0.6); settings.menuPosX = (settings.menuPosX+1) % menu.length; settings.menuPosY = 0; - drawTime(); + drawMenuAndTime(); } if(is_left){ @@ -577,23 +423,12 @@ Bangle.on('touch', function(btn, e){ settings.menuPosY = 0; settings.menuPosX = settings.menuPosX-1; settings.menuPosX = settings.menuPosX < 0 ? menu.length-1 : settings.menuPosX; - drawTime(); + drawMenuAndTime(); } if(is_center){ - var menuEntry = getMenuEntry(); - if(menuEntry.length > 4){ - Bangle.buzz(80, 0.6).then(()=>{ - try{ - menuEntry[4](); - setTimeout(()=>{ - Bangle.buzz(80, 0.6); - }, 250); - } catch(ex){ - // In case it fails, we simply ignore it. - } - } - ); + if(canRunMenuItem()){ + runMenuItem(); } } }); @@ -608,17 +443,24 @@ E.on("kill", function(){ }); -/* - * Draw clock the first time +/************************************************ + * Startup Clock */ + // The upper part is inverse i.e. light if dark and dark if light theme // is enabled. In order to draw the widgets correctly, we invert the // dark/light theme as well as the colors. g.setTheme({bg:g.theme.fg,fg:g.theme.bg, dark:!g.theme.dark}).clear(); -// Load widgets and draw clock the first time -Bangle.loadWidgets(); -draw(); - // Show launcher when middle button pressed Bangle.setUI("clock"); + +// Load widgets and draw clock the first time +Bangle.loadWidgets(); + +// Cache draw function for dynamic screen to hide / show widgets +// Bangle.loadWidgets() could also be called later on but its much slower! +for (let wd of WIDGETS) {wd._draw=wd.draw; wd._area=wd.area;} + +// Draw first time +draw(); diff --git a/apps/bwclk/metadata.json b/apps/bwclk/metadata.json index 45d337ebf..fbae0e1e7 100644 --- a/apps/bwclk/metadata.json +++ b/apps/bwclk/metadata.json @@ -1,8 +1,8 @@ { "id": "bwclk", "name": "BW Clock", - "version": "0.13", - "description": "A very minimalistic clock with date and time in focus.", + "version": "0.23", + "description": "A very minimalistic clock to mainly show date and time.", "readme": "README.md", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}, {"url":"screenshot_2.png"}, {"url":"screenshot_3.png"}, {"url":"screenshot_4.png"}], diff --git a/apps/bwclk/screenshot_3.png b/apps/bwclk/screenshot_3.png index f9a9a7d3f..8d982cac4 100644 Binary files a/apps/bwclk/screenshot_3.png and b/apps/bwclk/screenshot_3.png differ diff --git a/apps/bwclk/settings.js b/apps/bwclk/settings.js index a421e81a9..116253fda 100644 --- a/apps/bwclk/settings.js +++ b/apps/bwclk/settings.js @@ -4,7 +4,7 @@ // initialize with default settings... const storage = require('Storage') let settings = { - fullscreen: false, + screen: "Normal", showLock: true, hideColon: false, }; @@ -17,15 +17,16 @@ storage.write(SETTINGS_FILE, settings) } - + var screenOptions = ["Normal", "Dynamic", "Full"]; E.showMenu({ '': { 'title': 'BW Clock' }, '< Back': back, - 'Fullscreen': { - value: settings.fullscreen, - format: () => (settings.fullscreen ? 'Yes' : 'No'), - onchange: () => { - settings.fullscreen = !settings.fullscreen; + 'Screen': { + value: 0 | screenOptions.indexOf(settings.screen), + min: 0, max: 2, + format: v => screenOptions[v], + onchange: v => { + settings.screen = screenOptions[v]; save(); }, }, diff --git a/apps/calclock/ChangeLog b/apps/calclock/ChangeLog new file mode 100644 index 000000000..f4a0c96f5 --- /dev/null +++ b/apps/calclock/ChangeLog @@ -0,0 +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/README.md b/apps/calclock/README.md new file mode 100644 index 000000000..2b4e93a0c --- /dev/null +++ b/apps/calclock/README.md @@ -0,0 +1,9 @@ +# Calendar Clock - Your day at a glance + +This clock shows a chronological view of your current and future events. +It uses events synced from Gadgetbridge to achieve this. + +The current time and date is highlighted in cyan. + +## Screenshot +![](screenshot.png) diff --git a/apps/calclock/calclock-icon.js b/apps/calclock/calclock-icon.js new file mode 100644 index 000000000..9d5514d80 --- /dev/null +++ b/apps/calclock/calclock-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwgpm5gAB4AVRhgWCAAQWWDCARC/4ACJR4uB54WDAAP8DBotFGIgXLFwv4GAouQC4gwMLooXF/gXJOowXGJBIXBCIgXQxgXLMAIXXMAmIC5OIx4XJhH/wAXIxnIC78IxGIHoIABI44MBC4wQBEQIDB5gXGPAJgEC6IxBC5oABC4wwDa4YTCxAWD5nPDAzvGFYgAB5AXWJBK+GcAq5CGBIuBC5X4GBIJBdoQXB/GIx4CDPJAuEC5JoCDAgWBFwYXJxCBIFwYXKYwoACCwZ3IPQoWIC5YABGYIABCwpHKAQYMBCwwX/C5QAMC8R3/R/4XNhAXNwAXHgGIABgWIAFwA==")) diff --git a/apps/calclock/calclock.js b/apps/calclock/calclock.js new file mode 100644 index 000000000..a55dc05f9 --- /dev/null +++ b/apps/calclock/calclock.js @@ -0,0 +1,119 @@ +var calendar = []; +var current = []; +var next = []; + +function updateCalendar() { + calendar = require("Storage").readJSON("android.calendar.json",true)||[]; + calendar = calendar.filter(e => isActive(e) || getTime() <= e.timestamp); + calendar.sort((a,b) => a.timestamp - b.timestamp); + + current = calendar.filter(isActive); + next = calendar.filter(e=>!isActive(e)); +} + +function isActive(event) { + var timeActive = getTime() - event.timestamp; + return timeActive >= 0 && timeActive <= event.durationInSeconds; +} +function zp(str) { + return ("0"+str).substr(-2); +} + +function drawEventHeader(event, y) { + g.setFont("Vector", 24); + + var time = isActive(event) ? new Date() : new Date(event.timestamp * 1000); + var timeStr = zp(time.getHours()) + ":" + zp(time.getMinutes()); + g.drawString(timeStr, 5, y); + y += 24; + + g.setFont("12x20", 1); + if (isActive(event)) { + g.drawString(zp(time.getDate())+". " + require("locale").month(time,1),15*timeStr.length,y-21); + } else { + var offset = 0-time.getTimezoneOffset()/1440; + var days = Math.floor((time.getTime()/1000)/86400+offset)-Math.floor(getTime()/86400+offset); + if(days > 0) { + var daysStr = days===1?/*LANG*/"tomorrow":/*LANG*/"in "+days+/*LANG*/" days"; + g.drawString(daysStr,15*timeStr.length,y-21); + } + } + return y; +} + +function drawEventBody(event, y) { + g.setFont("12x20", 1); + var lines = g.wrapString(event.title, g.getWidth()-10); + if (lines.length > 2) { + lines = lines.slice(0,2); + lines[1] = lines[1].slice(0,-3)+"..."; + } + g.drawString(lines.join('\n'), 5, y); + y+=20 * lines.length; + if(event.location) { + g.drawImage(atob("DBSBAA8D/H/nDuB+B+B+B3Dn/j/B+A8A8AYAYAYAAAAAAA=="),5,y); + g.drawString(event.location, 20, y); + y+=20; + } + y+=5; + return y; +} + +function drawEvent(event, y) { + y = drawEventHeader(event, y); + y = drawEventBody(event, y); + return y; +} + +var curEventHeight = 0; + +function drawCurrentEvents(y) { + g.setColor(g.theme.dark ? "#0ff" : "#0000ff"); + g.clearRect(5, y, g.getWidth() - 5, y + curEventHeight); + curEventHeight = y; + + if(current.length === 0) { + y = drawEvent({timestamp: getTime(), durationInSeconds: 100}, y); + } else { + y = drawEventHeader(current[0], y); + for (var e of current) { + y = drawEventBody(e, y); + } + } + curEventHeight = y - curEventHeight; + return y; +} + +function drawFutureEvents(y) { + g.setColor(g.theme.fg); + for (var e of next) { + y = drawEvent(e, y); + if(y>g.getHeight())break; + } + return y; +} + +function fullRedraw() { + g.clearRect(5,24,g.getWidth()-5,g.getHeight()); + updateCalendar(); + var y = 30; + y = drawCurrentEvents(y); + drawFutureEvents(y); +} + +function redraw() { + g.reset(); + if (current.find(e=>!isActive(e)) || next.find(isActive)) { + fullRedraw(); + } else { + drawCurrentEvents(30); + } +} + +g.clear(); +fullRedraw(); +var minuteInterval = setInterval(redraw, 60 * 1000); + +Bangle.setUI("clock"); +Bangle.loadWidgets(); +Bangle.drawWidgets(); diff --git a/apps/calclock/calclock.png b/apps/calclock/calclock.png new file mode 100644 index 000000000..5f953c1ee Binary files /dev/null and b/apps/calclock/calclock.png differ diff --git a/apps/calclock/location.png b/apps/calclock/location.png new file mode 100644 index 000000000..619e55775 Binary files /dev/null and b/apps/calclock/location.png differ diff --git a/apps/calclock/metadata.json b/apps/calclock/metadata.json new file mode 100644 index 000000000..3aab55186 --- /dev/null +++ b/apps/calclock/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "calclock", + "name": "Calendar Clock", + "shortName": "CalClock", + "version": "0.04", + "description": "Show the current and upcoming events synchronized from Gadgetbridge", + "icon": "calclock.png", + "type": "clock", + "tags": "clock agenda", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"calclock.app.js","url":"calclock.js"}, + {"name":"calclock.img","url":"calclock-icon.js","evaluate":true} + ], + "screenshots": [{"url":"screenshot.png"}] +} diff --git a/apps/calclock/screenshot.png b/apps/calclock/screenshot.png new file mode 100644 index 000000000..4ab503f2b Binary files /dev/null and b/apps/calclock/screenshot.png differ diff --git a/apps/calculator/ChangeLog b/apps/calculator/ChangeLog index a08a0f5a7..f3ce3a928 100644 --- a/apps/calculator/ChangeLog +++ b/apps/calculator/ChangeLog @@ -3,3 +3,4 @@ 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 diff --git a/apps/calculator/app.js b/apps/calculator/app.js index 40953254e..571a5b27f 100644 --- a/apps/calculator/app.js +++ b/apps/calculator/app.js @@ -402,6 +402,7 @@ if (process.env.HWVERSION==1) { swipeEnabled = false; drawGlobal(); } else { // touchscreen? + setWatch(_ => {load();}, BTN1, {edge:'falling'}); // Exit with a short press to physical button selected = "NONE"; swipeEnabled = true; prepareScreen(numbers, numbersGrid, COLORS.DEFAULT); @@ -419,26 +420,20 @@ if (process.env.HWVERSION==1) { } } }); - var lastX = 0, lastY = 0; - Bangle.on('drag', (e) => { - if (!e.b) { - if (lastX > 50) { // right - drawSpecials(); - } else if (lastX < -50) { // left - drawOperators(); - } else if (lastY > 50) { // down - drawNumbers(); - } else if (lastY < -50) { // up - drawNumbers(); - } - lastX = 0; - lastY = 0; - } else { - lastX = lastX + e.dx; - lastY = lastY + e.dy; + Bangle.on('swipe', (LR, UD) => { + if (LR == 1) { // right + drawSpecials(); + } + if (LR == -1) { // left + drawOperators(); + } + if (UD == 1) { // down + drawNumbers(); + } + if (UD == -1) { // up + drawNumbers(); } }); } - displayOutput(0); diff --git a/apps/calculator/metadata.json b/apps/calculator/metadata.json index e78e4d54f..536a6955e 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.06", "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/cassioWatch/ChangeLog b/apps/cassioWatch/ChangeLog index 419810021..883bd2585 100644 --- a/apps/cassioWatch/ChangeLog +++ b/apps/cassioWatch/ChangeLog @@ -8,4 +8,5 @@ 0.7: Update Rocket Sequences Scope to not use memory all time 0.8: Update Some Variable Scopes to not use memory until need 0.9: Remove ESLint spaces -0.10: Show daily steps, heartrate and the temperature if weather information is available. \ No newline at end of file +0.10: Show daily steps, heartrate and the temperature if weather information is available. +0.11: Tell clock widgets to hide. diff --git a/apps/cassioWatch/app.js b/apps/cassioWatch/app.js index 6bbb9e823..91f6737ad 100644 --- a/apps/cassioWatch/app.js +++ b/apps/cassioWatch/app.js @@ -165,11 +165,11 @@ Bangle.on("lock", (locked) => { } }); +Bangle.setUI("clock"); // Load widgets, but don't show them Bangle.loadWidgets(); -Bangle.setUI("clock"); g.reset(); g.clear(); -draw(); \ No newline at end of file +draw(); diff --git a/apps/cassioWatch/metadata.json b/apps/cassioWatch/metadata.json index dabdc2c93..abfee9b93 100644 --- a/apps/cassioWatch/metadata.json +++ b/apps/cassioWatch/metadata.json @@ -4,7 +4,7 @@ "description": "Animated Clock with Space Cassio Watch Style", "screenshots": [{ "url": "screens/screen_night.png" },{ "url": "screens/screen_day.png" }], "icon": "app.png", - "version": "0.10", + "version": "0.11", "type": "clock", "tags": "clock, weather, cassio, retro", "supports": ["BANGLEJS2"], diff --git a/apps/chimer/ChangeLog b/apps/chimer/ChangeLog new file mode 100644 index 000000000..01bd00a0a --- /dev/null +++ b/apps/chimer/ChangeLog @@ -0,0 +1,2 @@ +0.01: Initial Creation +0.02: Fixed some sleep bugs. Added a sleep mode toggle \ No newline at end of file diff --git a/apps/chimer/README.MD b/apps/chimer/README.MD new file mode 100644 index 000000000..a78c677f2 --- /dev/null +++ b/apps/chimer/README.MD @@ -0,0 +1,11 @@ +# Chimer - For the BangleJS + +A fork of [Hour Chime](https://github.com/espruino/BangleApps/tree/master/apps/widchime) that adds extra features such as: + +- Buzz or beep on every 60, 30 or 15 minutes. +- Repeat Chime up to 3 times +- Set hours to disable chime + +Setting the hours you don't want your watch to chime for is done by setting the hour you want it to stop, and the hour you want it to start. + +Hours range from 0 - 23. diff --git a/apps/chimer/icon.txt b/apps/chimer/icon.txt new file mode 100644 index 000000000..cc969bc81 --- /dev/null +++ b/apps/chimer/icon.txt @@ -0,0 +1,2 @@ + +widget.png: "https://icons8.com/icon/114436/alarm" \ No newline at end of file diff --git a/apps/chimer/metadata.json b/apps/chimer/metadata.json new file mode 100644 index 000000000..d5bc04950 --- /dev/null +++ b/apps/chimer/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "chimer", + "name": "Chimer", + "version": "0.02", + "description": "A fork of Hour Chime that adds extra features such as: \n - Buzz or beep on every 60, 30 or 15 minutes. \n - Reapeat Chime up to 3 times \n - Set hours to disable chime", + "icon": "widget.png", + "type": "widget", + "tags": "widget", + "supports": ["BANGLEJS", "BANGLEJS2"], + "readme": "README.MD", + "storage": [ + { "name": "chimer.wid.js", "url": "widget.js" }, + { "name": "chimer.settings.js", "url": "settings.js" } + ], + "data": [{ "name": "chimer.json" }] +} diff --git a/apps/chimer/settings.js b/apps/chimer/settings.js new file mode 100644 index 000000000..55160c9be --- /dev/null +++ b/apps/chimer/settings.js @@ -0,0 +1,94 @@ +/** + * @param {function} back Use back() to return to settings menu + */ + +(function (back) { + // default to buzzing + var FILE = "chimer.json"; + var settings = {}; + const chimes = ["Off", "Buzz", "Beep", "Both"]; + const frequency = ["60 min", "30 min", "15 min", "1 min"]; + + var showMainMenu = () => { + E.showMenu({ + "": { title: "Chimer" }, + "< Back": () => back(), + "Chime Type": { + value: settings.type, + min: 0, + max: 2, // both is just silly + format: (v) => chimes[v], + onchange: (v) => { + settings.type = v; + writeSettings(settings); + }, + }, + Frequency: { + value: settings.freq, + min: 0, + max: 2, + format: (v) => frequency[v], + onchange: (v) => { + settings.freq = v; + writeSettings(settings); + }, + }, + Repetition: { + value: settings.repeat, + min: 1, + max: 5, + format: (v) => v, + onchange: (v) => { + settings.repeat = v; + writeSettings(settings); + }, + }, + "Sleep Mode": { + value: !!settings.sleep, + onchange: (v) => { + settings.sleep = v; + writeSettings(settings); + }, + }, + "Sleep Start": { + value: settings.start, + min: 0, + max: 23, + format: (v) => v, + onchange: (v) => { + settings.start = v; + writeSettings(settings); + }, + }, + "Sleep End": { + value: settings.end, + min: 0, + max: 23, + format: (v) => v, + onchange: (v) => { + settings.end = v; + writeSettings(settings); + }, + }, + }); + }; + + var readSettings = () => { + var settings = require("Storage").readJSON(FILE, 1) || { + type: 1, + freq: 0, + repeat: 1, + sleep: true, + start: 6, + end: 22, + }; + return settings; + }; + + var writeSettings = (settings) => { + require("Storage").writeJSON(FILE, settings); + }; + + settings = readSettings(); + showMainMenu(); +}); diff --git a/apps/chimer/widget.js b/apps/chimer/widget.js new file mode 100644 index 000000000..18358df9e --- /dev/null +++ b/apps/chimer/widget.js @@ -0,0 +1,134 @@ +(function () { + // 0: off, 1: buzz, 2: beep, 3: both + var FILE = "chimer.json"; + + var readSettings = () => { + var settings = require("Storage").readJSON(FILE, 1) || { + type: 1, + freq: 0, + repeat: 1, + sleep: true, + start: 6, + end: 22, + }; + return settings; + }; + + var settings = readSettings(); + + function sleep(milliseconds) { + const date = Date.now(); + let currentDate = null; + do { + currentDate = Date.now(); + } while (currentDate - date < milliseconds); + } + + function chime() { + for (var i = 0; i < settings.repeat; i++) { + if (settings.type === 1) { + Bangle.buzz(100); + } else if (settings.type === 2) { + Bangle.beep(); + } else { + return; + } + sleep(150); + } + } + + let lastHour = new Date().getHours(); + let lastMinute = new Date().getMinutes(); // don't chime when (re)loaded at a whole hour + function check() { + const now = new Date(), + h = now.getHours(), + m = now.getMinutes(), + s = now.getSeconds(), + ms = now.getMilliseconds(); + if ( + (settings.sleep && h > settings.end) || + (settings.sleep && h >= settings.end && m !== 0) || + (settings.sleep && h < settings.start) + ) { + var mLeft = 60 - m, + sLeft = mLeft * 60 - s, + msLeft = sLeft * 1000 - ms; + setTimeout(check, msLeft); + return; + } + if (settings.freq === 1) { + if ((m !== lastMinute && m === 0) || (m !== lastMinute && m === 30)) + chime(); + lastHour = h; + lastMinute = m; + // check again in 30 minutes + switch (true) { + case m / 30 >= 1: + var mLeft = 30 - (m - 30), + sLeft = mLeft * 60 - s, + msLeft = sLeft * 1000 - ms; + break; + case m / 30 < 1: + var mLeft = 30 - m, + sLeft = mLeft * 60 - s, + msLeft = sLeft * 1000 - ms; + break; + } + setTimeout(check, msLeft); + } else if (settings.freq === 2) { + if ( + (m !== lastMinute && m === 0) || + (m !== lastMinute && m === 15) || + (m !== lastMinute && m === 30) || + (m !== lastMinute && m === 45) + ) + chime(); + lastHour = h; + lastMinute = m; + // check again in 15 minutes + switch (true) { + case m / 15 >= 3: + var mLeft = 15 - (m - 45), + sLeft = mLeft * 60 - s, + msLeft = sLeft * 1000 - ms; + break; + case m / 15 >= 2: + var mLeft = 15 - (m - 30), + sLeft = mLeft * 60 - s, + msLeft = sLeft * 1000 - ms; + break; + case m / 15 >= 1: + var mLeft = 15 - (m - 15), + sLeft = mLeft * 60 - s, + msLeft = sLeft * 1000 - ms; + break; + case m / 15 < 1: + var mLeft = 15 - m, + sLeft = mLeft * 60 - s, + msLeft = sLeft * 1000 - ms; + break; + } + setTimeout(check, msLeft); + } else if (settings.freq === 3) { + if (m !== lastMinute) chime(); + lastHour = h; + lastMinute = m; + // check again in 1 minute + + var mLeft = 1, + sLeft = mLeft * 60 - s, + msLeft = sLeft * 1000 - ms; + setTimeout(check, msLeft); + } else { + if (h !== lastHour && m === 0) chime(); + lastHour = h; + // check again in 60 minutes + var mLeft = 60 - m, + sLeft = mLeft * 60 - s, + msLeft = sLeft * 1000 - ms; + setTimeout(check, msLeft); + } + } + + check(); +})(); diff --git a/apps/chimer/widget.png b/apps/chimer/widget.png new file mode 100644 index 000000000..14edf4150 Binary files /dev/null and b/apps/chimer/widget.png differ 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/clockcal/ChangeLog b/apps/clockcal/ChangeLog index 20a46b5b7..27d4fc7f4 100644 --- a/apps/clockcal/ChangeLog +++ b/apps/clockcal/ChangeLog @@ -2,3 +2,5 @@ 0.02: Added scrollable calendar and swipe gestures 0.03: Configurable drag gestures 0.04: Use default Bangle formatter for booleans +0.05: Improved colors (connected vs disconnected) +0.06: Tell clock widgets to hide. diff --git a/apps/clockcal/app.js b/apps/clockcal/app.js index 5e8c7f796..58ddd7ef5 100644 --- a/apps/clockcal/app.js +++ b/apps/clockcal/app.js @@ -1,3 +1,4 @@ +Bangle.setUI("clock"); Bangle.loadWidgets(); var s = Object.assign({ @@ -123,7 +124,7 @@ function drawMinutes() { var d = new Date(); var hours = s.MODE24 ? d.getHours().toString().padStart(2, ' ') : ((d.getHours() + 24) % 12 || 12).toString().padStart(2, ' '); var minutes = d.getMinutes().toString().padStart(2, '0'); - var textColor = NRF.getSecurityStatus().connected ? '#fff' : '#f00'; + var textColor = NRF.getSecurityStatus().connected ? '#99f' : '#fff'; var size = 50; var clock_x = (w - 20) / 2; if (dimSeconds) { @@ -307,4 +308,4 @@ NRF.on('disconnect', BTevent); dimSeconds = Bangle.isLocked(); drawWatch(); -Bangle.setUI("clock"); + diff --git a/apps/clockcal/metadata.json b/apps/clockcal/metadata.json index 6d547a7a3..872211495 100644 --- a/apps/clockcal/metadata.json +++ b/apps/clockcal/metadata.json @@ -1,7 +1,7 @@ { "id": "clockcal", "name": "Clock & Calendar", - "version": "0.04", + "version": "0.06", "description": "Clock with Calendar", "readme":"README.md", "icon": "app.png", diff --git a/apps/colorful_clock/ChangeLog b/apps/colorful_clock/ChangeLog index c72634017..54ee389e3 100644 --- a/apps/colorful_clock/ChangeLog +++ b/apps/colorful_clock/ChangeLog @@ -1,2 +1,3 @@ ... 0.03: First update with ChangeLog Added +0.04: Tell clock widgets to hide. diff --git a/apps/colorful_clock/app.js b/apps/colorful_clock/app.js index afc6b321f..ba6272e9b 100644 --- a/apps/colorful_clock/app.js +++ b/apps/colorful_clock/app.js @@ -3,6 +3,8 @@ let outerRadius = Math.min(CenterX,CenterY) * 0.9; + Bangle.setUI('clock'); + Bangle.loadWidgets(); /**** updateClockFaceSize ****/ @@ -241,7 +243,3 @@ refreshDisplay(); } }); - - Bangle.loadWidgets(); - - Bangle.setUI('clock'); diff --git a/apps/colorful_clock/metadata.json b/apps/colorful_clock/metadata.json index 5b6dbe87e..237acf81c 100644 --- a/apps/colorful_clock/metadata.json +++ b/apps/colorful_clock/metadata.json @@ -1,7 +1,7 @@ { "id": "colorful_clock", "name": "Colorful Analog Clock", "shortName":"Colorful Clock", - "version":"0.03", + "version":"0.04", "description": "a colorful analog clock", "icon": "app-icon.png", "type": "clock", 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/configurable_clock/ChangeLog b/apps/configurable_clock/ChangeLog index 84e7affed..9d55c1a91 100644 --- a/apps/configurable_clock/ChangeLog +++ b/apps/configurable_clock/ChangeLog @@ -1,2 +1,3 @@ ... 0.02: First update with ChangeLog Added +0.03: Tell clock widgets to hide. diff --git a/apps/configurable_clock/app.js b/apps/configurable_clock/app.js index 157d57741..45c86c7e9 100644 --- a/apps/configurable_clock/app.js +++ b/apps/configurable_clock/app.js @@ -5,6 +5,7 @@ let ScreenWidth = g.getWidth(), CenterX; let ScreenHeight = g.getHeight(), CenterY, outerRadius; + Bangle.setUI('clock'); Bangle.loadWidgets(); /**** updateClockFaceSize ****/ @@ -1377,4 +1378,3 @@ } }); - Bangle.setUI('clock'); diff --git a/apps/configurable_clock/metadata.json b/apps/configurable_clock/metadata.json index 28feae7e4..687a5b212 100644 --- a/apps/configurable_clock/metadata.json +++ b/apps/configurable_clock/metadata.json @@ -1,7 +1,7 @@ { "id": "configurable_clock", "name": "Configurable Analog Clock", "shortName":"Configurable Clock", - "version":"0.02", + "version":"0.03", "description": "an analog clock with several kinds of faces, hands and colors to choose from", "icon": "app-icon.png", "type": "clock", diff --git a/apps/counter/ChangeLog b/apps/counter/ChangeLog index f3f1c4eac..8402b3467 100644 --- a/apps/counter/ChangeLog +++ b/apps/counter/ChangeLog @@ -1,3 +1,4 @@ 0.01: New App! 0.02: Added decrement and touch functions 0.03: Set color - ensures widgets don't end up coloring the counter's text +0.04: Adopted for BangleJS 2 diff --git a/apps/counter/counter.js b/apps/counter/counter.js index 3e0687944..0054ada6d 100644 --- a/apps/counter/counter.js +++ b/apps/counter/counter.js @@ -1,45 +1,104 @@ var counter = 0; +const BANGLEJS2 = process.env.HWVERSION == 2; + +if (BANGLEJS2) { + var drag; + var y = 45; + var x = 5; +} else { + var y = 100; + var x = 25; +} function updateScreen() { - g.clearRect(0, 50, 250, 150); - g.setColor(0xFFFF); + if (BANGLEJS2) { + g.clearRect(0, 50, 250, 130); + } else { + g.clearRect(0, 50, 250, 150); + } + g.setBgColor(g.theme.bg).setColor(g.theme.fg); g.setFont("Vector",40).setFontAlign(0,0); g.drawString(Math.floor(counter), g.getWidth()/2, 100); - g.drawString('-', 45, 100); - g.drawString('+', 185, 100); + if (!BANGLEJS2) { + g.drawString('-', 45, 100); + g.drawString('+', 185, 100); + } } -// add a count by using BTN1 or BTN5 -setWatch(() => { - counter += 1; - updateScreen(); -}, BTN1, {repeat:true}); +if (BANGLEJS2) { + setWatch(() => { + counter = 0; + updateScreen(); + }, BTN1, {repeat:true}); + 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; + if (Math.abs(dx)>Math.abs(dy)+10) { + // horizontal + if (dx < dy) { + //console.log("left " + dx + " " + dy); + } else { + //console.log("right " + dx + " " + dy); + } + } else if (Math.abs(dy)>Math.abs(dx)+10) { + // vertical + if (dx < dy) { + //console.log("down " + dx + " " + dy); + if (counter > 0) counter -= 1; + updateScreen(); + } else { + //console.log("up " + dx + " " + dy); + counter += 1; + updateScreen(); + } + } else { + //console.log("tap " + e.x + " " + e.y); + } + } + }); + } else { -setWatch(() => { - counter += 1; - updateScreen(); -}, BTN5, {repeat:true}); + // add a count by using BTN1 or BTN5 + setWatch(() => { + counter += 1; + updateScreen(); + }, BTN1, {repeat:true}); + + setWatch(() => { + counter += 1; + updateScreen(); + }, BTN5, {repeat:true}); + + // subtract a count by using BTN3 or BTN4 + setWatch(() => { + if (counter > 0) counter -= 1; + updateScreen(); + }, BTN4, {repeat:true}); + + setWatch(() => { + if (counter > 0) counter -= 1; + updateScreen(); + }, BTN3, {repeat:true}); + + // reset by using BTN2 + setWatch(() => { + counter = 0; + updateScreen(); + }, BTN2, {repeat:true}); +} -// subtract a count by using BTN3 or BTN4 -setWatch(() => { - counter -= 1; - updateScreen(); -}, BTN4, {repeat:true}); - -setWatch(() => { - counter -= 1; - updateScreen(); -}, BTN3, {repeat:true}); - -// reset by using BTN2 -setWatch(() => { - counter = 0; - updateScreen(); -}, BTN2, {repeat:true}); g.clear(1).setFont("6x8"); -g.drawString('Tap right or BTN1 to increase\nTap left or BTN3 to decrease\nPress BTN2 to reset.', 25, 200); +g.setBgColor(g.theme.bg).setColor(g.theme.fg); +if (BANGLEJS2) { + g.drawString('Swipe up to increase\nSwipe down to decrease\nPress button to reset.', x, 100 + y); +} else { + g.drawString('Tap right or BTN1 to increase\nTap left or BTN3 to decrease\nPress BTN2 to reset.', x, 100 + y); +} Bangle.loadWidgets(); Bangle.drawWidgets(); diff --git a/apps/counter/metadata.json b/apps/counter/metadata.json index e455fda95..daba58d39 100644 --- a/apps/counter/metadata.json +++ b/apps/counter/metadata.json @@ -1,11 +1,11 @@ { "id": "counter", "name": "Counter", - "version": "0.03", + "version": "0.04", "description": "Simple counter", "icon": "counter_icon.png", "tags": "tool", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS", "BANGLEJS2"], "screenshots": [{"url":"bangle1-counter-screenshot.png"}], "allow_emulator": true, "storage": [ diff --git a/apps/crowclk/ChangeLog b/apps/crowclk/ChangeLog index 4f48bdd14..1c4f6f43b 100644 --- a/apps/crowclk/ChangeLog +++ b/apps/crowclk/ChangeLog @@ -1,3 +1,4 @@ 0.01: New App! 0.02: Removed "wake LCD on face-up"-feature: A watch-face should not set things like "wake LCD on face-up". 0.03: Fix the clock for dark mode. +0.04: Tell clock widgets to hide. diff --git a/apps/crowclk/crow_clock.js b/apps/crowclk/crow_clock.js index eee1653cb..7e608ef19 100644 --- a/apps/crowclk/crow_clock.js +++ b/apps/crowclk/crow_clock.js @@ -136,9 +136,9 @@ Bangle.on('lcdPower', (on) => { g.clear(); +// Show launcher when button pressed +Bangle.setUI("clock"); Bangle.loadWidgets(); Bangle.drawWidgets(); startTimers(); -// Show launcher when button pressed -Bangle.setUI("clock"); diff --git a/apps/crowclk/metadata.json b/apps/crowclk/metadata.json index 6985cf11a..265a0398b 100644 --- a/apps/crowclk/metadata.json +++ b/apps/crowclk/metadata.json @@ -1,7 +1,7 @@ { "id": "crowclk", "name": "Crow Clock", - "version": "0.03", + "version": "0.04", "description": "A simple clock based on Bold Clock that has MST3K's Crow T. Robot for a face", "icon": "crow_clock.png", "screenshots": [{"url":"screenshot_crow.png"}], diff --git a/apps/daisy/ChangeLog b/apps/daisy/ChangeLog index 829ff3d13..b13ce261b 100644 --- a/apps/daisy/ChangeLog +++ b/apps/daisy/ChangeLog @@ -5,3 +5,4 @@ 0.05: changed text to uppercase, just looks better, removed colons on text 0.06: better contrast for light theme, use fg color instead of dithered for ring 0.07: Use default Bangle formatter for booleans +0.08: fix idle timer always getting set to true diff --git a/apps/daisy/app.js b/apps/daisy/app.js index 7c513726f..848cd1801 100644 --- a/apps/daisy/app.js +++ b/apps/daisy/app.js @@ -83,7 +83,7 @@ function loadSettings() { settings = require("Storage").readJSON(SETTINGS_FILE,1)||{}; settings.gy = settings.gy||'#020'; settings.fg = settings.fg||'#0f0'; - settings.idle_check = settings.idle_check||true; + settings.idle_check = (settings.idle_check === undefined ? true : settings.idle_check); assignPalettes(); } diff --git a/apps/daisy/metadata.json b/apps/daisy/metadata.json index 802ba6834..c6cc93620 100644 --- a/apps/daisy/metadata.json +++ b/apps/daisy/metadata.json @@ -1,6 +1,6 @@ { "id": "daisy", "name": "Daisy", - "version":"0.07", + "version":"0.08", "dependencies": {"mylocation":"app"}, "description": "A beautiful digital clock with large ring guage, idle timer and a cyclic information line that includes, day, date, steps, battery, sunrise and sunset times", "icon": "app.png", diff --git a/apps/deko/Building_Typeface.ttf b/apps/deko/Building_Typeface.ttf new file mode 100644 index 000000000..d5a3933ab Binary files /dev/null and b/apps/deko/Building_Typeface.ttf differ diff --git a/apps/deko/ChangeLog b/apps/deko/ChangeLog new file mode 100644 index 000000000..9db0e26c5 --- /dev/null +++ b/apps/deko/ChangeLog @@ -0,0 +1 @@ +0.01: first release diff --git a/apps/deko/README.md b/apps/deko/README.md new file mode 100644 index 000000000..91e83bd23 --- /dev/null +++ b/apps/deko/README.md @@ -0,0 +1,10 @@ +# Deko Clock + +A simple clock with an Art Deko font + +The font was obtained from https://dafonttop.com/building.font and is free for personal use + + +![](screenshot.png) + +Written by: [Hugh Barney](https://github.com/hughbarney) For support and discussion please post in the [Bangle JS Forum](http://forum.espruino.com/microcosms/1424/) diff --git a/apps/deko/app-icon.js b/apps/deko/app-icon.js new file mode 100644 index 000000000..06f93e2ef --- /dev/null +++ b/apps/deko/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwIdah/wAof//4ECgYFB4AFBg4FB8AFBj/wh/4AoM/wEB/gFBvwCEBAU/AQP4gfAj8AgPwAoMPwED8AFBg/AAYIBDA4ngg4TB4EBApkPKgJSBJQIFTMgIFCJIIFDKoIFEvgFBGoMAnw7DP4IFEh+BAoItBg+DNIQwBMIaeCKoKxCPoIzCEgKVHUIqtFXIrFFaIrdFdIwAV")) diff --git a/apps/deko/app.js b/apps/deko/app.js new file mode 100644 index 000000000..8ae2c1d31 --- /dev/null +++ b/apps/deko/app.js @@ -0,0 +1,64 @@ +Graphics.prototype.setFontBuildingTypeface = function(scale) { + // Actual height 100 (102 - 3) + this.setFontCustom( + atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAH/gAAAAAAAAAAAAAAAAAH//gAAAAAAAAAAAAAAAAD///gAAAAAAAAAAAAAAAD////gAAAAAAAAAAAAAAD/////gAAAAAAAAAAAAAB//////gAAAAAAAAAAAAB///////gAAAAAAAAAAAB////////gAAAAAAAAAAA////////4AAAAAAAAAAA////////4AAAAAAAAAAAf///////8AAAAAAAAAAAf///////8AAAAAAAAAAAf///////8AAAAAAAAAAAP///////+AAAAAAAAAAAP///////+AAAAAAAAAAAP///////+AAAAAAAAAAAH////////AAAAAAAAAAAAH///////AAAAAAAAAAAAAH//////AAAAAAAAAAAAAAH/////gAAAAAAAAAAAAAAH////gAAAAAAAAAAAAAAAH///wAAAAAAAAAAAAAAAAH//wAAAAAAAAAAAAAAAAAH/wAAAAAAAAAAAAAAAAAAH4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////////////AAAAAAAD/////////////wAAAAAAP/////////////4AAAAAAP/////////////8AAAAAAf/////////////+AAAAAAf/////////////+AAAAAA///////////////AAAAAA///////////////AAAAAA///////////////AAAAAA/4AAAAAAAAAAAH/AAAAAA/4AAAAAAAAAAAH/AAAAAA/4AAAAAAAAAAAH/AAAAAA/4AAAAAAAAAAAH/AAAAAA/4AAAAAAAAAAAH/AAAAAA/4AAAAAAAAAAAH/AAAAAA/4AAAAAAAAAAAH/AAAAAA/4AAAAAAAAAAAH/AAAAAA/4AAAAAAAAAAAH/AAAAAA/4AAAAAAAAAAAH/AAAAAA/4AAAAAAAAAAAH/AAAAAA/4AAAAAAAAAAAH/AAAAAA///////////////AAAAAA///////////////AAAAAA///////////////AAAAAA///////////////AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAP/////////////8AAAAAAH/////////////4AAAAAAD/////////////wAAAAAAA/////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP4AAAAAAAAAAAAAAAAAAAf4AAAAAAAAAAAAAAAAAAA/4AAAAAAAAAAAAAAAAAAB/4AAAAAAAAAAAAAAAAAAD/4AAAAAAAAAAAAAAAAAAH/4AAAAAAAAAAAAAAAAAAf/4AAAAAAAAAAAAAAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/wAAf///////+AAAAAAB//wAB////////+AAAAAAH//wAD////////+AAAAAAH//wAH////////+AAAAAAP//wAP////////+AAAAAAf//wAP////////+AAAAAAf//wAP////////+AAAAAAf//wAf////////+AAAAAAf//wAf////////+AAAAAAf8AAAf8AAAAAAP+AAAAAAf8AAAf8AAAAAAP+AAAAAAf8AAAf8AAAAAAP+AAAAAAf8AAAf8AAAAAAP+AAAAAAf8AAAf8AAAAAAP+AAAAAAf8AAAf8AAAAAAP+AAAAAAf8AAAf8AAAAAAP+AAAAAAf8AAAf8AAAAAAP+AAAAAAf8AAAf8AAAAAAP+AAAAAAf8AAAf8AAAAAAP+AAAAAAf8AAAf8AAAAAAP+AAAAAAf8AAAf8AAAAAAP+AAAAAAf/////8AAAA///+AAAAAAf/////8AAAA///+AAAAAAf/////8AAAA///+AAAAAAf/////8AAAA///+AAAAAAP/////8AAAA///+AAAAAAH/////8AAAA///+AAAAAAH/////8AAAA///+AAAAAAB/////8AAAA///+AAAAAAAf////8AAAA///+AAAAAAAAAAAAAAAAA///+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf//gAAAAAAAf//+AAAAAAf//wAAAAAAA///+AAAAAAf//wAAAAAAA///+AAAAAAf//wAAAAAAA///+AAAAAAf//wAAAAAAA///+AAAAAAf//wAAAAAAA///+AAAAAAf//wAAAAAAA///+AAAAAAf//wAAAAAAA///+AAAAAAf//wAAAAAAA///+AAAAAAf//wAAAAAAA///+AAAAAAf8AAAAAAAAAAAP+AAAAAAf8AAAAAAAAAAAP+AAAAAAf8AAAAAAAAAAAP+AAAAAAf8AAAAEAAAAAAP+AAAAAAf8AAAB8AAAAAAP+AAAAAAf8AAAP8AAAAAAP+AAAAAAf8AAD/8AAAAAAP+AAAAAAf8AA//8AAAAAAP+AAAAAAf8AP//8AAAAAAP+AAAAAAf8B///8AAAAAAP+AAAAAAf8f///8AAAAAAP+AAAAAAf/////8AAAAAAP+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf///8P////////+AAAAAAf///gH////////+AAAAAAf//4AD////////+AAAAAAf/+AAB////////+AAAAAAf/gAAA////////+AAAAAAfwAAAAD///////+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/gAAAAAAAAAAAAAAAAAB//gAAAAAAAAAAAAAAAAAH//gAAAAAAAAAAAAAAAAA///gAAAAAAAAAAAAAAAAH///gAAAAAAAAAAAAAAAAf///gAAAAAAAAAAAAAAAD////gAAAAAAAAAAAAAAAf////gAAAAAAAAAAAAAAB/////gAAAAAAAAAAAAAAP///z/gAAAAAAAAAAAAAB///+D/gAAAAAAAAAAAAAH///wD/gAAAAAAAAAAAAA///+AD/gAAAAAAAAAAAAH///wAD/gAAAAAAAAAAAAf//+AAD/gAAAAAAAAAAAD///wAAD/gAAAAAAAAAAAf///AAAD/gAAAAAAAAAAB///4AAAD/gAAAAAAAAAAP///AAAAD/gAAAAAAAAAB///4AAAAD/gAAAAAAAAAH///AAAAAD/gAAAAAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAAAAAAAAAAD/gAAAAAAAAAAAAAAAAAAD/gAAAAAAAAAAAAAAAAAAD/gAAAAAAAAAAAAAAAAAAD/gAAAAAAAAAAAAAAAAAAD/gAAAAAAAAAAAAAAAAAAD/gAAAAAAAAAAAAAAAAAAD/gAAAAAAAAAAAAAAAAAAD/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//+AAAAAAAf/////8AAAA///wAAAAAAf/////8AAAA///4AAAAAAf/////8AAAA///8AAAAAAf/////8AAAA///+AAAAAAf/////8AAAA///+AAAAAAf/////8AAAA///+AAAAAAf/////8AAAA////AAAAAAf/////8AAAA////AAAAAAf/////8AAAA////AAAAAAf8AAAf8AAAAAAH/AAAAAAf8AAAf8AAAAAAH/AAAAAAf8AAAf8AAAAAAH/AAAAAAf8AAAf8AAAAAAH/AAAAAAf8AAAf8AAAAAAH/AAAAAAf8AAAf8AAAAAAH/AAAAAAf8AAAf8AAAAAAH/AAAAAAf8AAAf8AAAAAAH/AAAAAAf8AAAf8AAAAAAH/AAAAAAf8AAAf8AAAAAAH/AAAAAAf8AAAf8AAAAAAH/AAAAAAf//wAf8AAAAAAH/AAAAAAf//wAf/////////AAAAAAf//wAf/////////AAAAAAf//wAf/////////AAAAAAf//wAf////////+AAAAAAf//wAP////////+AAAAAAf//wAP////////8AAAAAAf//wAH////////8AAAAAAf//wAD////////wAAAAAAf//wAA////////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////////////AAAAAAAD/////////////wAAAAAAP/////////////4AAAAAAP/////////////8AAAAAAf/////////////+AAAAAAf/////////////+AAAAAA///////////////AAAAAA///////////////AAAAAA///////////////AAAAAA/4AAAf8AAAAAAH/AAAAAA/4AAAf8AAAAAAH/AAAAAA/4AAAf8AAAAAAH/AAAAAA/4AAAf8AAAAAAH/AAAAAA/4AAAf8AAAAAAH/AAAAAA/4AAAf8AAAAAAH/AAAAAA/4AAAf8AAAAAAH/AAAAAA/4AAAf8AAAAAAH/AAAAAA/4AAAf8AAAAAAH/AAAAAA/4AAAf8AAAAAAH/AAAAAA/4AAAf8AAAAAAH/AAAAAA///wAf8AAAAAAH/AAAAAA///wAf/////////AAAAAA///wAf/////////AAAAAA///wAf/////////AAAAAAf//wAf/////////AAAAAAf//wAP////////+AAAAAAP//wAP////////+AAAAAAH//wAH////////8AAAAAAB//wAD////////4AAAAAAAf/wAB////////wAAAAAAAAAAAAf///////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf//wAAAAAAAAAAAAAAAAAf//wAAAAAAAAAAAAAAAAAf//wAAAAAAAAAAeAAAAAAf//wAAAAAAAAAP+AAAAAAf//wAAAAAAAAD/+AAAAAAf//wAAAAAAAA//+AAAAAAf//wAAAAAAAf//+AAAAAAf//wAAAAAAH///+AAAAAAf//wAAAAAD////+AAAAAAf8AAAAAAA/////+AAAAAAf8AAAAAAP/////+AAAAAAf8AAAAAH//////8AAAAAAf8AAAAB///////AAAAAAAf8AAAAf//////wAAAAAAAf8AAAP//////4AAAAAAAAf8AAD//////+AAAAAAAAAf8AB///////gAAAAAAAAAf8Af//////4AAAAAAAAAAf8H//////+AAAAAAAAAAAf////////gAAAAAAAAAAAf///////wAAAAAAAAAAAAf//////8AAAAAAAAAAAAAf//////AAAAAAAAAAAAAAf/////wAAAAAAAAAAAAAAf////8AAAAAAAAAAAAAAAf////AAAAAAAAAAAAAAAAf///wAAAAAAAAAAAAAAAAf//4AAAAAAAAAAAAAAAAAf/+AAAAAAAAAAAAAAAAAAf/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///gf//////+AAAAAAAB////4////////wAAAAAAH////9////////4AAAAAAP/////////////8AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAA///////////////AAAAAA///////////////AAAAAA//////4AAAAAAH/AAAAAA/4AAA/4AAAAAAH/AAAAAA/4AAA/4AAAAAAH/AAAAAA/4AAA/4AAAAAAH/AAAAAA/4AAA/4AAAAAAH/AAAAAA/4AAA/4AAAAAAH/AAAAAA/4AAA/4AAAAAAH/AAAAAA/4AAA/4AAAAAAH/AAAAAA/4AAA/4AAAAAAH/AAAAAA/4AAA/4AAAAAAH/AAAAAA/4AAA/4AAAAAAH/AAAAAA/4AAA/4AAAAAAH/AAAAAA///////////////AAAAAA///////////////AAAAAA///////////////AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAP/////////////+AAAAAAP/////////////8AAAAAAH////9////////4AAAAAAB////4////////gAAAAAAAH///gP//////+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///////+AAAAAAAAAAAAD////////gAD//AAAAAAAP////////wAD//wAAAAAAP////////4AD//4AAAAAAf////////8AD//8AAAAAAf////////8AD//+AAAAAA/////////+AD//+AAAAAA/////////+AD///AAAAAA/////////+AD///AAAAAA/4AAAAAAP+AD///AAAAAA/4AAAAAAP+AAAH/AAAAAA/4AAAAAAP+AAAH/AAAAAA/4AAAAAAP+AAAH/AAAAAA/4AAAAAAP+AAAH/AAAAAA/4AAAAAAP+AAAH/AAAAAA/4AAAAAAP+AAAH/AAAAAA/4AAAAAAP+AAAH/AAAAAA/4AAAAAAP+AAAH/AAAAAA/4AAAAAAP+AAAH/AAAAAA/4AAAAAAP+AAAH/AAAAAA/4AAAAAAP+AAAH/AAAAAA/4AAAAAAP+AAAH/AAAAAA///////////////AAAAAA///////////////AAAAAA///////////////AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAP/////////////8AAAAAAP/////////////4AAAAAAD/////////////wAAAAAAA/////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/wAAAf+AAAAAAAAAAAAAD/wAAAf+AAAAAAAAAAAAAD/wAAAf+AAAAAAAAAAAAAD/wAAAf+AAAAAAAAAAAAAD/wAAAf+AAAAAAAAAAAAAD/wAAAf+AAAAAAAAAAAAAD/wAAAf+AAAAAAAAAAAAAD/wAAAf+AAAAAAAAAAAAAD/wAAAf+AAAAAAAAAAAAAD/wAAAf+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='), + 46, + atob("FCYpGigoKigoJykoFA=="), + 126+(scale<<8)+(1<<16) + ); + return this; +}; + + + +// 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)); +} + + +function draw() { + var x = g.getWidth()/2; + var y = g.getHeight()/2; + g.reset(); + var date = new Date(); + var timeStr = require("locale").time(date,1); + var dateStr = require("locale").date(date).toUpperCase(); + // draw time + g.setFontAlign(0,0).setFont("BuildingTypeface"); + g.clearRect(0, 24, g.getWidth(), y+35); // clear the background + g.drawString(timeStr,x,y); + // draw date + y += 60; + g.setFontAlign(0,0).setFont("6x8",2); + g.clearRect(0,y-8,g.getWidth(),y+8); // clear the background + g.drawString(dateStr,x,y); + // queue draw in one minute + queueDraw(); +} + +// Clear the screen once, at startup +g.clear(); +// draw immediately at first, queue update +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"); +// Load widgets +Bangle.loadWidgets(); +Bangle.drawWidgets(); diff --git a/apps/deko/app.png b/apps/deko/app.png new file mode 100644 index 000000000..6f11e7019 Binary files /dev/null and b/apps/deko/app.png differ diff --git a/apps/deko/metadata.json b/apps/deko/metadata.json new file mode 100644 index 000000000..9bdd15429 --- /dev/null +++ b/apps/deko/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "deko", + "name": "Deko Clock", + "version": "0.01", + "description": "Clock with Art Deko font", + "readme": "README.md", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"deko.app.js","url":"app.js"}, + {"name":"deko.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/deko/screenshot.png b/apps/deko/screenshot.png new file mode 100644 index 000000000..91ce2ea38 Binary files /dev/null and b/apps/deko/screenshot.png differ diff --git a/apps/distortclk/ChangeLog b/apps/distortclk/ChangeLog new file mode 100644 index 000000000..4c7291526 --- /dev/null +++ b/apps/distortclk/ChangeLog @@ -0,0 +1,2 @@ +0.01: New face! +0.02: Improved clock diff --git a/apps/distortclk/README.md b/apps/distortclk/README.md new file mode 100644 index 000000000..8c7c433c1 --- /dev/null +++ b/apps/distortclk/README.md @@ -0,0 +1,17 @@ +# Distort Watchface +Was playing around with custom fonts and made something with it +Made for Bangle.js 2 + +![screenshot (3)](https://user-images.githubusercontent.com/44651387/157507228-100452bf-94a6-476f-aec6-d13d5dad86d5.png) + +## Features + +Has a dark mode + +## Requests + +If you have any issues or would like to suggest a feature, click here to send a message -> [here](https://github.com/elykittytee/BangleApps/issues/new?title=Poketch%20Clock%20Bug). + +## Creator + +Eleanor Tayam diff --git a/apps/distortclk/app-icon.js b/apps/distortclk/app-icon.js new file mode 100644 index 000000000..c375de96e --- /dev/null +++ b/apps/distortclk/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("j0ewkBiIAxHIQMJiBJEIxAaCAIQfHDgIUFDwwNCHYgVFiAVBHYgIDEghKCCIQGCFYoaDAYgORGIJ2DBwYIBHgQOPgAOIPIYOGAgQOFFgh7DHZQeDBwhoFQgh3JEAgOFFoqkHYRzgOfx4bCJ4gNGSIaJEABA7EAGA")) diff --git a/apps/distortclk/app.js b/apps/distortclk/app.js new file mode 100644 index 000000000..a9fdd1ef2 --- /dev/null +++ b/apps/distortclk/app.js @@ -0,0 +1,65 @@ +Graphics.prototype.setFontSixCaps = function(scale) { + // Actual height 60 (59 - 0) + this.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADMzMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADMzMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0VniarM3u4AAAAAAAAAAAAAAAAAAAAAAAAAAABEZoiqzN3u////////8AAAAAAAAAAAAAAAAAAAJFZ4iqzN3u////////////////8AAAAAAAAAAARGZ4mrzd7v////////////////////////8AAAAAAAqszd7v////////////////////////////7u3MoAAAAAAA//////////////////////////7t3MqohmRCAAAAAAAAAA//////////////////7t3MqohmRAAAAAAAAAAAAAAAAAAA/////////+7dzKqYdlQwAAAAAAAAAAAAAAAAAAAAAAAAAA/+7dzKqIZkQQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZkQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWazMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMqVAAAAAAAAAa7//////////////////////////////////+oQAAAAAAC/////////////////////////////////////+wAAAAAAf//////////////////////////////////////3AAAAAA3///7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u///9AAAAAA///GREREREREREREREREREREREREREREREREbP//AAAAAA//8wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///AAAAAA///GREREREREREREREREREREREREREREREREbP//AAAAAA3///7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u///9AAAAAAf//////////////////////////////////////3AAAAAAC/////////////////////////////////////+wAAAAAAAa7//////////////////////////////////+oQAAAAAAAAWazMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMqVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAqqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACqoAAAAAAA//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/8AAAAAAA//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/8AAAAAAA//3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3/8AAAAAAA//////////////////////////////////////8AAAAAAA//////////////////////////////////////8AAAAAAA//////////////////////////////////////8AAAAAAA3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADu4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAes3d3d3d0AAAAAAAAAAAAAAAAAAAAAAAADVorAAAAAAAA8////////8AAAAAAAAAAAAAAAAAAAAlaKze///wAAAAAAHf////////8AAAAAAAAAAAAAAAFGis3v///////wAAAAAAn/////////8AAAAAAAAAAARom83v///////////wAAAAAA7//+3d3d3d0AAAAAAEV5rN7//////////////v/wAAAAAA//xTAAAAAAAAA1eKze//////////////7cqXVP/wAAAAAA//UAAAAAFGis3v/////////////+3Kl2QAAAAP/wAAAAAA//6XZoq83v/////////////+26hkEAAAAAAAAP/wAAAAAA3//////////////////tyoZSAAAAAAAAAAAAAP/wAAAAAAb//////////////tuoZAAAAAAAAAAAAAAAAAAP/wAAAAAACv/////////tuXUwAAAAAAAAAAAAAAAAAAAAAP/wAAAAAAAI3////9yoZAAAAAAAAAAAAAAAAAAAAAAAAAAP/wAAAAAAAAJ4qodRAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqgAAAAAAAAWazMzMzMzMzAAAAAAAAAAAAAzMzMzMzMzMqVAAAAAAAAAa7//////////wAAAAAAAAAAAA///////////+oQAAAAAADP///////////wAAAAAAAAAAAA/////////////AAAAAAAf////////////wAAAAiIgAAAAA/////////////3AAAAAA3///7u7u7u7u7gAAAA//8AAAAA7u7u7u7u7u///9AAAAAA//11RERERERERAAAAA//8AAAAAREREREREREV9//AAAAAA//QAAAAAAAAAAAAAAB//8QAAAAAAAAAAAAAAAE//AAAAAA//11RERERERERERERa//+lREREREREREREREV9//AAAAAA3///7u7u7u7u7u7u7/////7u7u7u7u7u7u7u///9AAAAAAf//////////////////////////////////////3AAAAAAC/////////////////+q//////////////////+wAAAAAAAa7///////////////0i3////////////////+oQAAAAAAAAWazMzMzMzMzMzMyoIAKKzMzMzMzMzMzMzMqVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkRGZniIqqoAAAAAAAAAAAAAAAAAAAAAAkRGZniIqqrMzd3u7v//////8AAAAAAAAAAAAAAAqqrMzd3u7v////////////////////8AAAAAAAAAAAAAAA//////////////////////////////8AAAAAAAAAAAAAAA//////////////////////////////8AAAAAAAAAAAAAAA///////////////+7t3czKqpiHZm//8AAAAAAAAAAAAAAA//7u3dzMuqqIhmZUQwAAAAAAAAAA//8AAAAAAAAAAAAAAAZmREAAAAAAAAAARERERERERERERE//9EREREREQAAAAAAAAAAAAAAAAAAAAA7u7u7u7u7u7u7u///u7u7u7u4AAAAAAAAAAAAAAAAAAAAA////////////////////////8AAAAAAAAAAAAAAAAAAAAA////////////////////////8AAAAAAAAAAAAAAAAAAAAA////////////////////////8AAAAAAAAAAAAAAAAAAAAAzMzMzMzMzMzMzMzMzMzMzMzMwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARERERERERERERERERERAAABEREREREREREREAAAAAAAAAA7u7u7u7u7u7u7u7u7u7gAADu7u7u7u7u7u7u2TAAAAAAAA///////////////////wAAD//////////////+YAAAAAAA///////////////////wAAD///////////////4wAAAAAA///////////////////wAAD///////////////+gAAAAAA//zMzMzMzMzMzMzM3//AAADMzMzMzMzMzMzM7//wAAAAAA//AAAAAAAAAAAAAG//cAAAAAAAAAAAAAAAAAKf/wAAAAAA//AAAAAAAAAAAAAN//UAAAAAAAAAAAAAAAAAB//wAAAAAA//AAAAAAAAAAAAAP//6qqqqqqqqqqqqqqqqqv//wAAAAAA//AAAAAAAAAAAAAP///////////////////////AAAAAAA//AAAAAAAAAAAAAN//////////////////////9AAAAAAA//AAAAAAAAAAAAAF7/////////////////////gAAAAAAA//AAAAAAAAAAAAAAWu//////////////////7GAAAAAAAAZmAAAAAAAAAAAAAAADZmZmZmZmZmZmZmZmZmQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADREREREREREREREREREREREREREREREREMAAAAAAAAAADne7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7ZMAAAAAAABd////////////////////////////////////1QAAAAAALv/////////////////////////////////////iAAAAAAr//////////////////////////////////////6AAAAAA///szMzMzMzMzMzMzP//zMzMzMzMzMzMzMzM3///AAAAAA//ogAAAAAAAAAAAAC//5AAAAAAAAAAAAAAAACf//AAAAAA//cAAAAAAAAAAAAAD//zAAAAAAAAAAAAAAAABf//AAAAAA//+6qqqqqqqqAAAAD//8qqqqqqqqqqqqqqqqrv//AAAAAAz///////////AAAAD//////////////////////8AAAAAAT///////////AAAADv/////////////////////0AAAAAACP//////////AAAAB/////////////////////+AAAAAAAAGzv////////AAAAAGzv/////////////////sYAAAAAAAAABGZmZmZmZmAAAAAABGZmZmZmZmZmZmZmZmZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZmAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQAAAAAAA//AAAAAAAAAAAAAAAAAAAAAAAAACRGZ4iqrM3e4AAAAAAA//AAAAAAAAAAAAAAACRGZ4iqrM3e7v////////8AAAAAAA//AAAAACRGZ4iqrM3e7v//////////////////8AAAAAAA//iqrM3e7v////////////////////////////8AAAAAAA//////////////////////////////////7u3cwAAAAAAA///////////////////////+7t3MyqmIZmRCAAAAAAAAAA/////////////u3dzLqoiGZUQgAAAAAAAAAAAAAAAAAAAA//7t3cyqqIhmVEEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZlRAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWazMzMzMqphjAAAAAAAAAARniqrMzMzMzMqVAAAAAAAAAa7//////////+yWEAAAAVi97////////////+oQAAAAAAC///////////////2VAGrf////////////////+wAAAAAAf////////////////+vP///////////////////3AAAAAA3///7u7u7u7/////////////////7u7u7u7u///9AAAAAA///GRERERERmis3////////typhmREREREREbP//AAAAAA//8wAAAAAAAAAAJ8/////8hgAAAAAAAAAAAAA///AAAAAA///GRERERERWis3////////typhmREREREREbP//AAAAAA3///7u7u7u7/////////////////7u7u7u7u///9AAAAAAf////////////////+vP///////////////////3AAAAAAC///////////////2VAGrf////////////////+wAAAAAAAa7//////////+yWIAAAAVi97////////////+oQAAAAAAAAWazMzMzMqphjAAAAAAAAAARniqrMzMzMzMqVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1ZmZmZmZmZmZmZmZmQgAAAAAAZmZmZmZmYwAAAAAAAAAFvv////////////////7rUAAAAA/////////rUAAAAAAAB+////////////////////9gAAAA//////////9wAAAAAAP//////////////////////gAAAA///////////zAAAAAAv//////////////////////wAAAA///////////7AAAAAA///7qqqqqqqqqqqqqqqq3//wAAAAqqqqqqqqrP//AAAAAA//9wAAAAAAAAAAAAAAAAT//wAAAAAAAAAAAAAH//AAAAAA//9wAAAAAAAAAAAAAAAAn//AAAAAAAAAAAAAAZ//AAAAAA///8zMzMzMzMzMzMzMzM///MzMzMzMzMzMzMzf//AAAAAAv//////////////////////////////////////7AAAAAAP//////////////////////////////////////zAAAAAABu////////////////////////////////////5gAAAAAAAEre7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7aQAAAAAAAAAAUREREREREREREREREREREREREREREREREQQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMzMwAAAAAAAAAAAzMzAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//8AAAAAAAAAAA///wAAAAAAAAAAAAAAAAAAAAAAAAAAAP//8AAAAAAAAAAA///wAAAAAAAAAAAAAAAAAAAAAAAAAAAP//8AAAAAAAAAAA///wAAAAAAAAAAAAAAAAAAAAAAAAAAAP//8AAAAAAAAAAA///wAAAAAAAAAAAAAAAAAAAAAAAAAAAMzMwAAAAAAAAAAAzMzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="), 46, atob("CAwODQ4PDw8QDA8QCA=="), 69+(scale<<8)+(4<<16)); + return this; +}; + +const offset = 25; +const width = g.getWidth(); +const height = g.getHeight(); + +var drawTimeout; +var fgTime = 0xf800; +var bgTime = 0x3333ff; +var dayDate = 0x000; + +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +} + +function time() { + require("Font4x5").add(Graphics); + var d = new Date(); + var day = d.getDate(); + var time = require("locale").time(d,1); + var date = require("locale").date(d); + var mo = require("date_utils").month(d.getMonth()+1,0); + + g.setFontAlign(0,0); + g.setFontSixCaps(2).setColor(fgTime).drawString(time, width/2, height/2+10); + + g.setFont("4x5",2); + g.setFontAlign(0,0); + g.setColor(dayDate).drawString(mo,width-55, height-16); + g.drawString(day,width-10, height-16); +} + +function draw() { + g.setColor(bgTime).fillRect(0,40,width,height-offset); + time(); + queueDraw(); +} + +//program start +g.clear(); // Clear the screen once, at startup + +if (g.theme.dark==true){ + dayDate = 0xffff; +} +else { + dayDate=0x000; +} + +draw(); // draw immediately at first + + + +// Show launcher when middle button pressed +Bangle.setUI("clock"); +// Load widgets +Bangle.loadWidgets(); +Bangle.drawWidgets(); diff --git a/apps/distortclk/app.png b/apps/distortclk/app.png new file mode 100644 index 000000000..b82a0913e Binary files /dev/null and b/apps/distortclk/app.png differ diff --git a/apps/distortclk/metadata.json b/apps/distortclk/metadata.json new file mode 100644 index 000000000..125dac590 --- /dev/null +++ b/apps/distortclk/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "distortclk", + "name": "Distort Clock", + "shortName":"Distort Clock", + "version": "0.02", + "description": "A clockface", + "icon": "app.png", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "readme":"README.md", + "storage": [ + {"name":"distortclk.app.js","url":"app.js"}, + {"name":"distortclk.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/distortclk/screenshot.png b/apps/distortclk/screenshot.png new file mode 100644 index 000000000..3207b4e1e Binary files /dev/null and b/apps/distortclk/screenshot.png differ 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/drinkcounter/ChangeLog b/apps/drinkcounter/ChangeLog index 69faa7904..d8d174c4c 100644 --- a/apps/drinkcounter/ChangeLog +++ b/apps/drinkcounter/ChangeLog @@ -1,3 +1,4 @@ 0.10: Initial release - still work in progress 0.15: Added settings and calculations -0.20: Added status saving \ No newline at end of file +0.20: Added status saving +0.25: Adopted for Bangle.js 1 - kind of \ No newline at end of file diff --git a/apps/drinkcounter/README.md b/apps/drinkcounter/README.md index f1700531b..5638ee066 100644 --- a/apps/drinkcounter/README.md +++ b/apps/drinkcounter/README.md @@ -1,11 +1,14 @@ # Drink Counter -Development still in progress. Counts drinks you had for science. Calculates BAC. +Counts drinks you had for science. Calculates BAC. ## Usage Swipe left/right to select drink. Swipe up/down to add/remove drinks. +## Important notes + +No warranty whatsoever. Use at your own risk. Calculations might be wrong. Do not drink and drive - even if BAC is low. ## Creator diff --git a/apps/drinkcounter/app.js b/apps/drinkcounter/app.js index 47a4feb13..323d9fb41 100644 --- a/apps/drinkcounter/app.js +++ b/apps/drinkcounter/app.js @@ -3,6 +3,7 @@ Bangle.loadWidgets(); Bangle.drawWidgets(); require("Font8x16").add(Graphics); +const BANGLEJS2 = process.env.HWVERSION == 2; const SETTINGSFILE = "drinkcounter.json"; setting = require("Storage").readJSON("setting.json",1); E.setTimeZone(setting.timezone); // timezone = 1 for MEZ, = 2 for MESZ @@ -154,7 +155,9 @@ function updateDrinks(){ } g.setBgColor(g.theme.bg).setColor(g.theme.fg); - g.drawImage(icoReset,145,145); + if (BANGLEJS2) { + g.drawImage(icoReset,145,145); + } drinkStatus.firstDrinkTime = firstDrinkTime; settings_file = require("Storage").open("drinkcounter.status.json", "w"); @@ -184,6 +187,10 @@ function addDrink(){ function removeDrink(){ if (drinks[activeDrink] > 0) drinks[activeDrink] = drinks[activeDrink] - 1; updateDrinks(); + + if ((!BANGLEJS2) && (drinks[0] == 0) && (drinks[1] == 0) && (drinks[2] == 0)) { + resetDrinksFn() + } } function previousDrink(){ @@ -203,61 +210,74 @@ function showDrinks() { g.drawImage(icoShot,80,100); } -function initDragEvents() { - 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; - if (Math.abs(dx)>Math.abs(dy)+10) { - // horizontal - if (dx < dy) { - //console.log("left " + dx + " " + dy); - previousDrink(); - } else { - //console.log("right " + dx + " " + dy); - nextDrink(); - } - } else if (Math.abs(dy)>Math.abs(dx)+10) { - // vertical - if (dx < dy) { - //console.log("down " + dx + " " + dy); - removeDrink(); - } else { - //console.log("up " + dx + " " + dy); - addDrink(); - } - } else { - //console.log("tap " + e.x + " " + e.y); - if (e.x > 145 && e.y > 145) { - g.clearRect(0,34,176,176); //Clear - resetDrinks = E.showPrompt("Reset drinks?", { - title: "Confirm", - buttons: { Yes: true, No: false }, - }); - resetDrinks.then((confirm) => { - if (confirm) { - for (let i = 0; i <= maxDrinks; i++) { - drinks[i] = 0; - } - //console.log("reset to default"); - } - //console.log("reset " + confirm); - firstDrinkTime = null; - showDrinks(); - updateDrinks(); - updateTime(); - updateFirstDrinkTime(); - }); - } - } - } -}); +function resetDrinksFn() { + g.clearRect(0,34,176,176); //Clear + resetDrinks = E.showPrompt("Reset drinks?", { + title: "Confirm", + buttons: { Yes: true, No: false }, + }); + resetDrinks.then((confirm) => { + if (confirm) { + for (let i = 0; i <= maxDrinks; i++) { + drinks[i] = 0; + } + //console.log("reset to default"); + } + //console.log("reset " + confirm); + firstDrinkTime = null; + showDrinks(); + updateDrinks(); + updateTime(); + updateFirstDrinkTime(); + }); } -loadMySettings(); +function initDragEvents() { + +if (BANGLEJS2) { + 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; + if (Math.abs(dx)>Math.abs(dy)+10) { + // horizontal + if (dx < dy) { + //console.log("left " + dx + " " + dy); + previousDrink(); + } else { + //console.log("right " + dx + " " + dy); + nextDrink(); + } + } else if (Math.abs(dy)>Math.abs(dx)+10) { + // vertical + if (dx < dy) { + //console.log("down " + dx + " " + dy); + removeDrink(); + } else { + //console.log("up " + dx + " " + dy); + addDrink(); + } + } else { + //console.log("tap " + e.x + " " + e.y); + if (e.x > 145 && e.y > 145) { + resetDrinksFn(); + } + } + } + }); + } else { + setWatch(addDrink, BTN1, { repeat: true, debounce:50 }); + setWatch(removeDrink, BTN3, { repeat: true, debounce:50 }); + setWatch(previousDrink, BTN4, { repeat: true, debounce:50 }); + setWatch(nextDrink, BTN5, { repeat: true, debounce:50 }); + } +} + + +loadMySettings(); showDrinks(); diff --git a/apps/drinkcounter/metadata.json b/apps/drinkcounter/metadata.json index 3d1a167f0..2b8d7fe71 100644 --- a/apps/drinkcounter/metadata.json +++ b/apps/drinkcounter/metadata.json @@ -2,14 +2,14 @@ "id": "drinkcounter", "name": "Drink Counter", "shortName": "Drink Counter", - "version": "0.20", + "version": "0.25", "description": "Counts drinks you had for science. Calculates blood alcohol content (BAC)", "allow_emulator":true, "icon": "drinkcounter.png", "type": "app", "tags": "health", "screenshots": [{"url":"screenshot_drnkcnt.png"}], - "supports": ["BANGLEJS2"], + "supports": ["BANGLEJS", "BANGLEJS2"], "readme": "README.md", "storage": [ {"name":"drinkcounter.app.js","url":"app.js"}, 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/espruinoctrl/README.md b/apps/espruinoctrl/README.md index a7bca662c..7b2e434e7 100644 --- a/apps/espruinoctrl/README.md +++ b/apps/espruinoctrl/README.md @@ -17,7 +17,7 @@ showing available Espruino devices is popped up. device being connected to. Use this if you want to print data - eg: `print(E.getBattery())` When done, click 'Upload'. Your changes will be saved to local storage -so they'll be remembered next time you upload from the same device.s +so they'll be remembered next time you upload from the same device. ## Usage diff --git a/apps/espruinoctrl/app-icon.js b/apps/espruinoctrl/app-icon.js index 70d2dd062..3f9572f72 100644 --- a/apps/espruinoctrl/app-icon.js +++ b/apps/espruinoctrl/app-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mEwhH+AH4A/AH4A/AH4AFwIuuAAIllAAYIGF041IF34AKqwuuAANXF9QuCAANdGHqQgGBwvdGCIud5mjGB4udAAIwPFz3MSR61VFxQwNci4vGeh4uXGAguHGBK3WGA4AIegtXc69dGBxoBGAouWO4IwNe4gwZa4YwLFwikEFzAwLFwwwCFzQwKFw68YGB4AdF5AwmF5IwlF5QwkF5Yw/F8IwEL9WBB4IuuADwuzGxAugFAgliGBYutAH4A/AH4A/ADA=")) +require("heatshrink").decompress(atob("mEw4UA///muVt9TgH+Jf4AQgILKgtABI9VqkVqAgHqoABC48FBYQKGhEVBQNUBY0qyoLJ1WlEZMq1ILJhWqBZMC1QwCBY0PGAYLGn/qGAQLG/4wDBIkggf8GARfF1ED+BhCTQgTBgfAMISaF1WAAYM61SBG0ADB/wLFgNq1EAHoIcDXYVaCYMP+EqC4kVqwTBn/AhDqFqowBn72HqowCBZAwCBZAwCBZAwCBZIwIiowKBYVWC5VUkAvJXYiaDBYS7FTQVUgr2HC4IgHAAYgHAH4AJA==")) diff --git a/apps/espruinoctrl/metadata.json b/apps/espruinoctrl/metadata.json index 253307fa0..5107bc6ae 100644 --- a/apps/espruinoctrl/metadata.json +++ b/apps/espruinoctrl/metadata.json @@ -5,7 +5,7 @@ "version": "0.01", "description": "Send commands to other Espruino devices via the Bluetooth UART interface. Customisable commands!", "icon": "app.png", - "tags": "", + "tags": "tool,bluetooth", "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", "custom": "custom.html", diff --git a/apps/espruinoprog/ChangeLog b/apps/espruinoprog/ChangeLog new file mode 100644 index 000000000..6fdcad1d6 --- /dev/null +++ b/apps/espruinoprog/ChangeLog @@ -0,0 +1,4 @@ +0.01: New App! +0.02: Add 'pre' code that can erase the device + Wait more between sending code snippets + Now force use of 'Storage' (assume 2v00 or later) diff --git a/apps/espruinoprog/README.md b/apps/espruinoprog/README.md new file mode 100644 index 000000000..aef4cccad --- /dev/null +++ b/apps/espruinoprog/README.md @@ -0,0 +1,43 @@ +# Espruino Programmer + +Finds Bluetooth devices with a specific name (eg `Puck.js`), connects and uploads code. Great for programming many devices at once! + +**WARNING:** This will reprogram **any matching Espruino device within range** while +the app is running. Unless you are careful to remove other devices from the area or +turn them off, you could find some of your devices unexpectedly get programmed! + +## Customising + +Click on the Customise button in the app loader to set up the programmer. + +* First you need to choose the kind of devices you want to upload to. This is +the text that should match the Bluetooth advertising name. So `Puck.js` for Puck.js +devices, or `Bangle.js` for Bangles. +* In the next box, you have code to run before the upload of the main code. By default +the code `require("Storage").list().forEach(f=>require("Storage").erase(f));reset();` will +erase all files on the device and reset it. +* Now paste in the code you want to write to the device. This is automatically +written to flash (`.bootcde`). See https://www.espruino.com/Saving#save-on-send-to-flash- +for more information. +* Now enter the code that should be sent **after** programming. This code +should make the device so it doesn't advertise on Bluetooth with the Bluetooth +name you entered for the first item. It may also help if it indicates to you that +the device is programmed properly. + * You could turn advertising off with `NRF.sleep()` + * You could change the advertising name with `NRF.setAdvertising({},{name:"Ok"});` + * On a Bangle, you could turn it off with `Bangle.off()` +* Finally scroll down and click `Upload` +* Now you can run the new `Programmer` app on the Bangle. + +## Usage + +Just run the app, and as soon as it starts it'll start scanning for +devices to upload to! + +To stop scanning, long-press the button to return to the clock. + +## Notes + +* This assumes the device being written to is at least version 2v00 of Espruino +* Currently, code is not minified before upload (so you need to supply pre-minified + code if you want that) diff --git a/apps/espruinoprog/app-icon.js b/apps/espruinoprog/app-icon.js new file mode 100644 index 000000000..532c60eea --- /dev/null +++ b/apps/espruinoprog/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4cA/4AB7wJB8/5uX+7uUgH41lSKf4AKpMkyQCCggEDAQVtCAMCCNWUx9JufSrmkCJeKqsiytICJtFkWRCJWAEaARCI5BkEoAGBymJ9eSvXkCJZ9JCLI1DyM9uQRLNYWRpRZMR5ARWAwSPCuWR9MuCJZZIgARGPouTCIcSA4OQAoMW7dt2wCEEZECCI1oCJAADrZyBAAcDuQRByOABQkKDAvbtwRBxu24AMFAAcGCIY/B7AQIhpOC3MjKIVsCJe3jYRCwiPEkARBQg227ieDAQO0CJPhCKHJCK1N0ARI28JCIjUDEY4OBzWRfAoRG3ARBygRH3oPBswRB4QjFfAYgCt9pYoJoEkmbCJONCI1ACJGSiQRE7TXDCIuQEYkmCIhpDEYSCFCIj2DCIOTrYRE6ARDAH4AHA")) diff --git a/apps/espruinoprog/app.js b/apps/espruinoprog/app.js new file mode 100644 index 000000000..58fac4a0b --- /dev/null +++ b/apps/espruinoprog/app.js @@ -0,0 +1,100 @@ +var uart; // require("ble_uart") +var device; // BluetoothDevice +var uploadTimeout; // a timeout used during upload - if we disconnect, kill this +Bangle.loadWidgets(); + +var json = require("Storage").readJSON("espruinoprog.json",1); +/*var json = { // for example + namePrefix : "Puck.js ", + code : "E.setBootCode('digitalPulse(LED2,1,100);')", + post : "LED.set();NRF.sleep()", +};*/ + +if ("object" != typeof json) { + E.showAlert("JSON not found","Programmer").then(() => load()); + throw new Error("JSON not found"); + // stops execution +} + +// Set up terminal +var R = Bangle.appRect; +var termg = Graphics.createArrayBuffer(R.w, R.h, 1, {msb:true}); +termg.setFont("6x8"); +var term; + +function showTerminal() { + E.showMenu(); // clear anything that was drawn + if (term) term.print(""); // redraw terminal +} + +function scanAndConnect() { + termg.clear(); + term = require("VT100").connect(termg, { + charWidth : 6, + charHeight : 8 + }); + term.print = str => { + for (var i of str) term.char(i); + g.reset().drawImage(termg,R.x,R.y); + }; + term.print(`\r\nScanning...\r\n`); + NRF.requestDevice({ filters: [{ namePrefix: json.namePrefix }] }).then(function(dev) { + term.print(`Found ${dev.name||dev.id.substr(0,17)}\r\n`); + device = dev; + + term.print(`Connect to ${dev.name||dev.id.substr(0,17)}...\r\n`); + device.removeAllListeners(); + device.on('gattserverdisconnected', function(reason) { + if (!uart) return; + term.print(`\r\nDISCONNECTED (${reason})\r\n`); + uart = undefined; + device = undefined; + if (uploadTimeout) clearTimeout(uploadTimeout); + uploadTimeout = undefined; + setTimeout(scanAndConnect, 1000); + }); + require("ble_uart").connect(device).then(function(u) { + uart = u; + term.print("Connected...\r\n"); + uart.removeAllListeners(); + uart.on('data', function(d) { term.print(d); }); + term.print("Upload initial...\r\n"); + uart.write((json.pre||"")+"\n").then(() => { + term.print("\r\Done.\r\n"); + uploadTimeout = setTimeout(function() { + uploadTimeout = undefined; + term.print("\r\nUpload Code...\r\n"); + uart.write((json.code||"")+"\n").then(() => { + term.print("\r\Done.\r\n"); + // main upload completed - wait a bit + uploadTimeout = setTimeout(function() { + uploadTimeout = undefined; + term.print("\r\Upload final...\r\n"); + // now upload the code to run after... + uart.write((json.post||"")+"\n").then(() => { + term.print("\r\nDone.\r\n"); + // now wait and disconnect (if not already done!) + uploadTimeout = setTimeout(function() { + uploadTimeout = undefined; + term.print("\r\nDisconnecting...\r\n"); + if (uart) uart.disconnect(); + }, 500); + }); + }, 2000); + }); + }, 2000); + }); + }); + }).catch(err => { + if (err.toString().startsWith("No device found")) { + // expected - try again + scanAndConnect(); + } else + term.print(`\r\ERROR ${err.toString()}\r\n`); + }); +} + +// now start +Bangle.drawWidgets(); +showTerminal(); +scanAndConnect(); diff --git a/apps/espruinoprog/app.png b/apps/espruinoprog/app.png new file mode 100644 index 000000000..b2b435f04 Binary files /dev/null and b/apps/espruinoprog/app.png differ diff --git a/apps/espruinoprog/custom.html b/apps/espruinoprog/custom.html new file mode 100644 index 000000000..a12189707 --- /dev/null +++ b/apps/espruinoprog/custom.html @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + +

Upload code to devices with names starting with:

+

+

Enter the code to send before upload here:

+

+

Enter your program to upload here:

+

+

Enter the code to send after upload here:

+

+

Then click  

+

Click here to reset to defaults.

+ + + + diff --git a/apps/espruinoprog/metadata.json b/apps/espruinoprog/metadata.json new file mode 100644 index 000000000..ebb55b23d --- /dev/null +++ b/apps/espruinoprog/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "espruinoprog", + "name": "Espruino Programmer", + "shortName": "Programmer", + "version": "0.02", + "description": "Finds Bluetooth devices with a specific name (eg 'Puck.js'), connects and uploads code. Great for programming many devices at once!", + "icon": "app.png", + "tags": "tool,bluetooth", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "custom": "custom.html", + "storage": [ + {"name":"espruinoprog.app.js","url":"app.js"}, + {"name":"espruinoprog.img","url":"app-icon.js","evaluate":true}, + {"name":"espruinoprog.json"} + ] +} diff --git a/apps/ncfrun/ChangeLog b/apps/espruinoterm/ChangeLog similarity index 100% rename from apps/ncfrun/ChangeLog rename to apps/espruinoterm/ChangeLog diff --git a/apps/espruinoterm/README.md b/apps/espruinoterm/README.md new file mode 100644 index 000000000..df26d59a0 --- /dev/null +++ b/apps/espruinoterm/README.md @@ -0,0 +1,22 @@ +# Espruino Terminal + +Send commands to other Espruino devices via the Bluetooth UART interface and +see the result on a terminal. + +## Customising + +Once installed and you're connected to the Bangle you can click the button next to the app in the app loader +to change the commands (they will be read from the device). + +When done, click `Save to Bangle.js` and your changes will be saved to the same device. + +## Usage + +* Load the app and after a few seconds you'll see a menu with Espruino devices +in the vicinity. +* Tap on the device you want to connect to +* A terminal will pop up showing `Connecting...` and then `Connected` +* Now tap on the right (or press the button) to bring up a menu with options for commands, or the option to disconnect. + +You can also choose `Custom` in which case a keyboard (using the currently installed text input method) will +be displayed and you can enter the command you would like to send. diff --git a/apps/espruinoterm/app-icon.js b/apps/espruinoterm/app-icon.js new file mode 100644 index 000000000..f566aedf7 --- /dev/null +++ b/apps/espruinoterm/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwcCpMkyQC/AVW//4AK/oR/COD8LCP4R/CK8DCKNsCKFt2BHPhu2CJ8BCKAjQI4OQNaIUB23bsCPMCJzp/CP4Rf/4AKCKwC/AVIA==")) diff --git a/apps/espruinoterm/app.js b/apps/espruinoterm/app.js new file mode 100644 index 000000000..348190db4 --- /dev/null +++ b/apps/espruinoterm/app.js @@ -0,0 +1,101 @@ +var uart; // require("ble_uart") +var device; // BluetoothDevice +var customCommand = ""; +// Set up terminal +Bangle.loadWidgets(); +var R = Bangle.appRect; +var termg = Graphics.createArrayBuffer(R.w, R.h, 1, {msb:true}); +var termVisible = false; +termg.setFont("6x8"); +term = require("VT100").connect(termg, { + charWidth : 6, + charHeight : 8 +}); +term.print = str => { + for (var i of str) term.char(i); + if (termVisible) g.reset().drawImage(termg,R.x,R.y).setFont("6x8").setFontAlign(0,-1,1).drawString("MORE",R.w-1,(R.y+R.y2)/2); +}; + +function showConnectMenu() { + termVisible = false; + var m = { "" : {title:"Devices"} }; + E.showMessage("Scanning..."); + NRF.findDevices(devices => { + devices.forEach(dev=>{ + m[dev.name||dev.id.substr(0,17)] = ()=>{ + connectTo(dev); + }; + }); + m["< Back"] = () => showConnectMenu(); + E.showMenu(m); + },{filters:[ + { namePrefix: 'Puck.js' }, + { namePrefix: 'Pixl.js' }, + { namePrefix: 'MDBT42Q' }, + { namePrefix: 'Bangle.js' }, + { namePrefix: 'Espruino' }, + { services: [ "6e400001-b5a3-f393-e0a9-e50e24dcca9e" ] } + ],active:true,timeout:4000}); +} + +function showOptionsMenu() { + if (!uart) showConnectMenu(); + termVisible = false; + var menu = {"":{title:/*LANG*/"Options"}, + "< Back" : () => showTerminal(), + }; + var json = require("Storage").readJSON("espruinoterm.json",1); + if (Array.isArray(json)) { + json.forEach(j => { menu[j.title] = () => sendCommand(j.cmd); }); + } else { + Object.assign(menu,{ + "Version" : () => sendCommand("process.env.VERSION"), + "Battery" : () => sendCommand("E.getBattery()"), + "Flash LED" : () => sendCommand("LED.set();setTimeout(()=>LED.reset(),1000);") + }); + } + menu[/*LANG*/"Custom"] = () => { require("textinput").input({text:customCommand}).then(result => { + customCommand = result; + sendCommand(customCommand); + })}; + menu[/*LANG*/"Disconnect"] = () => { showTerminal(); term.print("\r\nDisconnecting...\r\n"); uart.disconnect(); } + + E.showMenu(menu); +} + +function showTerminal() { + E.showMenu(); + Bangle.setUI({ + mode : "custom", + btn : n=> { showOptionsMenu(); }, + touch : (n,e) => { if (n==2) showOptionsMenu(); } + }); + termVisible = true; + term.print(""); // redraw terminal +} + +function sendCommand(cmd) { + showTerminal(); + uart.write(cmd+"\n"); +} + +function connectTo(dev) { + device = dev; + showTerminal(); + term.print(`\r\nConnect to ${dev.name||dev.id.substr(0,17)}...\r\n`); + device.on('gattserverdisconnected', function(reason) { + term.print(`\r\nDISCONNECTED (${reason})\r\n`); + uart = undefined; + device = undefined; + setTimeout(showConnectMenu, 1000); + }); + require("ble_uart").connect(device).then(function(u) { + uart = u; + term.print("Connected...\r\n"); + uart.on('data', function(d) { term.print(d); }); + }); +} + +// now start +Bangle.drawWidgets(); +showConnectMenu(); diff --git a/apps/espruinoterm/app.json b/apps/espruinoterm/app.json new file mode 100644 index 000000000..72a12e635 --- /dev/null +++ b/apps/espruinoterm/app.json @@ -0,0 +1,5 @@ +[ + {"title":"Version", "cmd":"process.env.VERSION"}, + {"title":"Battery", "cmd":"E.getBattery()"}, + {"title":"Flash LED", "cmd":"LED.set();setTimeout(()=>LED.reset(),1000);"} +] diff --git a/apps/espruinoterm/app.png b/apps/espruinoterm/app.png new file mode 100644 index 000000000..e9a8c3758 Binary files /dev/null and b/apps/espruinoterm/app.png differ diff --git a/apps/espruinoterm/interface.html b/apps/espruinoterm/interface.html new file mode 100644 index 000000000..660b3a86c --- /dev/null +++ b/apps/espruinoterm/interface.html @@ -0,0 +1,104 @@ + + + + + + + + +

Enter the menu items you'd like to see appear in the app below. When finished, click `Save to Bangle.js` to save the JavaScript back.

+
+ + + + + + + + + + +
TitleCommand
+
+

+ + + + + + diff --git a/apps/espruinoterm/metadata.json b/apps/espruinoterm/metadata.json new file mode 100644 index 000000000..25e6183e1 --- /dev/null +++ b/apps/espruinoterm/metadata.json @@ -0,0 +1,20 @@ +{ + "id": "espruinoterm", + "name": "Espruino Terminal", + "shortName": "Espruino Term", + "version": "0.01", + "description": "Send commands to other Espruino devices via the Bluetooth UART interface, and see the result on a VT100 terminal. Customisable commands!", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], + "tags": "tool,bluetooth", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "interface": "interface.html", + "dependencies": {"textinput":"type"}, + "storage": [ + {"name":"espruinoterm.app.js","url":"app.js"}, + {"name":"espruinoterm.img","url":"app-icon.js","evaluate":true} + ],"data": [ + {"name":"espruinoterm.json","url":"app.json"} + ] +} diff --git a/apps/espruinoterm/screenshot.png b/apps/espruinoterm/screenshot.png new file mode 100644 index 000000000..cce881a37 Binary files /dev/null and b/apps/espruinoterm/screenshot.png differ diff --git a/apps/fclock/ChangeLog b/apps/fclock/ChangeLog index 30e049f69..7e7307c59 100644 --- a/apps/fclock/ChangeLog +++ b/apps/fclock/ChangeLog @@ -1,2 +1,3 @@ 0.01: First published version of app 0.02: Move to Bangle.setUI to launcher support +0.03: Tell clock widgets to hide. diff --git a/apps/fclock/fclock.app.js b/apps/fclock/fclock.app.js index afa0c5e2d..838a5578d 100644 --- a/apps/fclock/fclock.app.js +++ b/apps/fclock/fclock.app.js @@ -173,6 +173,9 @@ const drawHR = function () { } }; +// Show launcher when button pressed +Bangle.setUI("clock"); + // clean app screen g.clear(); Bangle.loadWidgets(); @@ -198,6 +201,3 @@ Bangle.on('HRM', function (d) { // draw now drawClock(); - -// Show launcher when button pressed -Bangle.setUI("clock"); diff --git a/apps/fclock/metadata.json b/apps/fclock/metadata.json index da553e110..dffb197a2 100644 --- a/apps/fclock/metadata.json +++ b/apps/fclock/metadata.json @@ -2,7 +2,7 @@ "id": "fclock", "name": "fclock", "shortName": "F Clock", - "version": "0.02", + "version": "0.03", "description": "Simple design of a digital clock", "icon": "app.png", "type": "clock", diff --git a/apps/ftclock/ChangeLog b/apps/ftclock/ChangeLog index 83ec21ee6..c30dae69f 100644 --- a/apps/ftclock/ChangeLog +++ b/apps/ftclock/ChangeLog @@ -1,3 +1,4 @@ 0.01: first release 0.02: RAM efficient version of `fourTwentyTz.js` (as suggested by @gfwilliams). 0.03: `mkFourTwentyTz.js` now handles new timezonedb.com CSV format +0.04: Tell clock widgets to hide. diff --git a/apps/ftclock/app.js b/apps/ftclock/app.js index b12db10f1..4f2cef895 100644 --- a/apps/ftclock/app.js +++ b/apps/ftclock/app.js @@ -33,6 +33,8 @@ function draw() { // Clear the screen once, at startup g.clear(); +// Show launcher when middle button pressed +Bangle.setUI("clock"); // Load widgets Bangle.loadWidgets(); Bangle.drawWidgets(); @@ -47,5 +49,4 @@ Bangle.on('lcdPower',on=>{ drawTimeout = undefined; } }); -// Show launcher when middle button pressed -Bangle.setUI("clock"); + diff --git a/apps/ftclock/metadata.json b/apps/ftclock/metadata.json index 876feb1bb..96a4f84b9 100644 --- a/apps/ftclock/metadata.json +++ b/apps/ftclock/metadata.json @@ -1,7 +1,7 @@ { "id": "ftclock", "name": "Four Twenty Clock", - "version": "0.03", + "version": "0.04", "description": "A clock that tells when and where it's going to be 4:20 next", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}, {"url":"screenshot1.png"}], diff --git a/apps/fwupdate/custom.html b/apps/fwupdate/custom.html index 3f8f50b3f..8ef117933 100644 --- a/apps/fwupdate/custom.html +++ b/apps/fwupdate/custom.html @@ -83,6 +83,7 @@ function onInit(device) { if (crc==3508163280 || crc==1418074094) version = "2v12"; if (crc==4056371285) version = "2v13"; if (crc==1038322422) version = "2v14"; + if (crc==2560806221) version = "2v15"; if (!ok) { version += `(⚠ update required)`; } diff --git a/apps/gallifr/ChangeLog b/apps/gallifr/ChangeLog index 0e1f45042..32c1057b0 100644 --- a/apps/gallifr/ChangeLog +++ b/apps/gallifr/ChangeLog @@ -1,2 +1,3 @@ 0.01: First released version 0.02: Changed setWatch to Bangle.setUI +0.03: Tell clock widgets to hide. diff --git a/apps/gallifr/app.js b/apps/gallifr/app.js index d327bcdc1..8468eee48 100644 --- a/apps/gallifr/app.js +++ b/apps/gallifr/app.js @@ -238,10 +238,12 @@ Bangle.on('lcdPower', (on) => { } }); +// Show launcher when button pressed +Bangle.setUI("clock"); + g.clear(); startTimers(); Bangle.loadWidgets(); drawAll(); -// Show launcher when button pressed -Bangle.setUI("clock"); + diff --git a/apps/gallifr/metadata.json b/apps/gallifr/metadata.json index 9ce7d7f97..96fe243ed 100644 --- a/apps/gallifr/metadata.json +++ b/apps/gallifr/metadata.json @@ -2,7 +2,7 @@ "id": "gallifr", "name": "Time Traveller's Chronometer", "shortName": "Time Travel Clock", - "version": "0.02", + "version": "0.03", "description": "A clock for time travellers. The light pie segment shows the minutes, the black circle, the hour. The dial itself reads 'time' just in case you forget.", "icon": "gallifr.png", "screenshots": [{"url":"screenshot_time.png"}], diff --git a/apps/geissclk/ChangeLog b/apps/geissclk/ChangeLog index 7458fadee..cd46173f7 100644 --- a/apps/geissclk/ChangeLog +++ b/apps/geissclk/ChangeLog @@ -1,3 +1,4 @@ 0.01: New App! 0.02: BTN2->launcher, use smaller text to allow "20:00" to fit on screen 0.03: Changed setWatch to Bangle.setUI +0.04: Tell clock widgets to hide. diff --git a/apps/geissclk/clock.js b/apps/geissclk/clock.js index f14ea5f39..5401fb142 100644 --- a/apps/geissclk/clock.js +++ b/apps/geissclk/clock.js @@ -142,11 +142,13 @@ Bangle.on('lcdPower',function(on) { animInterval = setInterval(iterate, 50); } }); -g.clear(); + +// Show launcher when button pressed +Bangle.setUI("clock");g.clear(); + Bangle.loadWidgets(); Bangle.drawWidgets(); iterate(); animInterval = setInterval(iterate, 50); -// Show launcher when button pressed -Bangle.setUI("clock"); + diff --git a/apps/geissclk/metadata.json b/apps/geissclk/metadata.json index 456854dbd..68bd2a970 100644 --- a/apps/geissclk/metadata.json +++ b/apps/geissclk/metadata.json @@ -1,7 +1,7 @@ { "id": "geissclk", "name": "Geiss Clock", - "version": "0.03", + "version": "0.04", "description": "7 segment clock with animated background in the style of Ryan Geiss' music visualisation. NOTE: The first run will take ~1 minute to do some precalculation", "icon": "clock.png", "type": "clock", diff --git a/apps/glbasic/ChangeLog b/apps/glbasic/ChangeLog index 89aee01f8..d97fd44d5 100644 --- a/apps/glbasic/ChangeLog +++ b/apps/glbasic/ChangeLog @@ -1,2 +1,3 @@ 0.20: New App! +0.21: Tell clock widgets to hide. diff --git a/apps/glbasic/glbasic.app.js b/apps/glbasic/glbasic.app.js index ff7837ada..c1f82f74c 100644 --- a/apps/glbasic/glbasic.app.js +++ b/apps/glbasic/glbasic.app.js @@ -178,6 +178,8 @@ function draw() { //////////////////////////////////////////////////// // Bangle.setBarometerPower(true); +Bangle.setUI("clock"); + Bangle.loadWidgets(); draw(); @@ -197,6 +199,5 @@ Bangle.on('lcdPower', on => { } }); -Bangle.setUI("clock"); Bangle.drawWidgets(); diff --git a/apps/glbasic/metadata.json b/apps/glbasic/metadata.json index 7c79097da..6d4c562a3 100644 --- a/apps/glbasic/metadata.json +++ b/apps/glbasic/metadata.json @@ -2,7 +2,7 @@ "id": "glbasic", "name": "GLBasic Clock", "shortName": "GLBasic", - "version": "0.20", + "version": "0.21", "description": "A clock with large numbers", "dependencies": {"widpedom":"app"}, "icon": "icon48.png", 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 b4eaf5472..840f9ecbc 100644 --- a/apps/gpsnav/ChangeLog +++ b/apps/gpsnav/ChangeLog @@ -3,4 +3,6 @@ 0.03: Add Waypoint Editor 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 +0.08: Fix not displaying of wpindex = 0, correct compass drawing and nm calculation on b2 diff --git a/apps/gpsnav/README.md b/apps/gpsnav/README.md index af239b233..2b67799b8 100644 --- a/apps/gpsnav/README.md +++ b/apps/gpsnav/README.md @@ -4,9 +4,12 @@ The app is aimed at small boat navigation although it can also be used to mark t The app displays direction of travel (course), speed, direction to waypoint (bearing) and distance to waypoint. The screen shot below is before the app has got a GPS fix. +[Bangle.js 2] Button mappings in brackests. One additional feature: +On swiping on the main screen you can change the displayed metrics: Right changes to nautical metrics, left to the default locale metrics. + ![](first_screen.jpg) -The large digits are the course and speed. The top of the display is a linear compass which displays the direction of travel when a fix is received and you are moving. The blue text is the name of the current waypoint. NONE means that there is no waypoint set and so bearing and distance will remain at 0. To select a waypoint, press BTN2 (middle) and wait for the blue text to turn white. Then use BTN1 and BTN3 to select a waypoint. The waypoint choice is fixed by pressing BTN2 again. In the screen shot below a waypoint giving the location of Stone Henge has been selected. +The large digits are the course and speed. The top of the display is a linear compass which displays the direction of travel when a fix is received and you are moving. The blue text is the name of the current waypoint. NONE means that there is no waypoint set and so bearing and distance will remain at 0. To select a waypoint, press BTN2 (middle) [touch / BTN] and wait for the blue text to turn white. Then use BTN1 and BTN3 [swipe left/right] to select a waypoint. The waypoint choice is fixed by pressing BTN2 [touch / BTN] again. In the screen shot below a waypoint giving the location of Stone Henge has been selected. ![](waypoint_screen.jpg) @@ -18,7 +21,7 @@ The app lets you mark your current location as follows. There are vacant slots i ![](select_screen.jpg) -Bearing and distance are both zero as WP1 has currently no GPS location associated with it. To mark the location, press BTN2. +Bearing and distance are both zero as WP1 has currently no GPS location associated with it. To mark the location, press BTN2 [touch / BTN]. ![](marked_screen.jpg) diff --git a/apps/gpsnav/app.js b/apps/gpsnav/app.js index 8903e07fb..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; @@ -51,10 +51,10 @@ function drawCompass(course) { //displayed heading var heading = 0; -function newHeading(m,h){ +function newHeading(m,h){ var s = Math.abs(m - h); var delta = (m>h)?1:-1; - if (s>=180){s=360-s; delta = -delta;} + if (s>=180){s=360-s; delta = -delta;} if (s<2) return h; var hd = h + delta*(1 + Math.round(s/5)); if (hd<0) hd+=360; @@ -125,7 +125,7 @@ function drawN(){ g.setColor(0,0,0); g.fillRect(10,230,60,239); g.setColor(1,1,1); - g.drawString("Sats " + satellites.toString(),10,230); + g.drawString("Sats " + satellites.toString(),10,230); } var savedfix; @@ -193,11 +193,11 @@ var SCREENACCESS = { }, release:function(){ this.withApp=true; - startdraw(); + startdraw(); setButtons(); } -} - +} + Bangle.on('lcdPower',function(on) { if (!SCREENACCESS.withApp) return; if (on) { @@ -207,7 +207,7 @@ Bangle.on('lcdPower',function(on) { } }); -var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"NONE"}]; +var waypoints = require("waypoints").load(); wp=waypoints[0]; function nextwp(inc){ @@ -220,10 +220,10 @@ 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("Storage").writeJSON("waypoints.json", waypoints); + require("waypoints").save(waypoints); } selected=!selected; drawN(); diff --git a/apps/gpsnav/app.min.js b/apps/gpsnav/app.min.js index 7771e2b98..a01b251e0 100644 --- a/apps/gpsnav/app.min.js +++ b/apps/gpsnav/app.min.js @@ -6,6 +6,6 @@ function drawN(){var a=loc.speed(speed);buf.setColor(1);buf.setFont("6x8",2);buf 0,30);buf.setColor(selected?1:2);buf.drawString(wp.name,140,0);buf.setColor(1);buf.drawString(a,60,0);buf.drawString(loc.distance(dist),60,30);flip(buf,Yoff+130);g.setFont("6x8",1);g.setColor(0,0,0);g.fillRect(10,230,60,239);g.setColor(1,1,1);g.drawString("Sats "+satellites.toString(),10,230)}var savedfix; function onGPS(a){savedfix=a;void 0!==a&&(course=isNaN(a.course)?course:Math.round(a.course),speed=isNaN(a.speed)?speed:a.speed,satellites=a.satellites);candraw&&(void 0!==a&&1==a.fix&&(dist=distance(a,wp),isNaN(dist)&&(dist=0),brg=bearing(a,wp),isNaN(brg)&&(brg=0)),drawN())}var intervalRef;function stopdraw(){candraw=!1;intervalRef&&clearInterval(intervalRef)} function startTimers(){candraw=!0;intervalRefSec=setInterval(function(){heading=newHeading(course,heading);course!=heading&&drawCompass(heading)},200)}function drawAll(){g.setColor(1,.5,.5);g.fillPoly([120,Yoff+50,110,Yoff+70,130,Yoff+70]);g.setColor(1,1,1);drawN();drawCompass(heading)}function startdraw(){g.clear();Bangle.drawWidgets();startTimers();drawAll()} -function setButtons(){setWatch(nextwp.bind(null,-1),BTN1,{repeat:!0,edge:"falling"});setWatch(doselect,BTN2,{repeat:!0,edge:"falling"});setWatch(nextwp.bind(null,1),BTN3,{repeat:!0,edge:"falling"})}var SCREENACCESS={withApp:!0,request:function(){this.withApp=!1;stopdraw();clearWatch()},release:function(){this.withApp=!0;startdraw();setButtons()}};Bangle.on("lcdPower",function(a){SCREENACCESS.withApp&&(a?startdraw():stopdraw())});var waypoints=require("Storage").readJSON("waypoints.json")||[{name:"NONE"}]; -wp=waypoints[0];function nextwp(a){selected&&(wpindex+=a,wpindex>=waypoints.length&&(wpindex=0),0>wpindex&&(wpindex=waypoints.length-1),wp=waypoints[wpindex],drawN())}function doselect(){selected&&0!=wpindex&&void 0===waypoints[wpindex].lat&&savedfix.fix&&(waypoints[wpindex]={name:"@"+wp.name,lat:savedfix.lat,lon:savedfix.lon},wp=waypoints[wpindex],require("Storage").writeJSON("waypoints.json",waypoints));selected=!selected;drawN()}g.clear();Bangle.setLCDBrightness(1);Bangle.loadWidgets();Bangle.drawWidgets(); +function setButtons(){setWatch(nextwp.bind(null,-1),BTN1,{repeat:!0,edge:"falling"});setWatch(doselect,BTN2,{repeat:!0,edge:"falling"});setWatch(nextwp.bind(null,1),BTN3,{repeat:!0,edge:"falling"})}var SCREENACCESS={withApp:!0,request:function(){this.withApp=!1;stopdraw();clearWatch()},release:function(){this.withApp=!0;startdraw();setButtons()}};Bangle.on("lcdPower",function(a){SCREENACCESS.withApp&&(a?startdraw():stopdraw())});var waypoints=require("waypoints").load(); +wp=waypoints[0];function nextwp(a){selected&&(wpindex+=a,wpindex>=waypoints.length&&(wpindex=0),0>wpindex&&(wpindex=waypoints.length-1),wp=waypoints[wpindex],drawN())}function doselect(){selected&&0!=wpindex&&void 0===waypoints[wpindex].lat&&savedfix.fix&&(waypoints[wpindex]={name:"@"+wp.name,lat:savedfix.lat,lon:savedfix.lon},wp=waypoints[wpindex],require("waypoints").save(waypoints));selected=!selected;drawN()}g.clear();Bangle.setLCDBrightness(1);Bangle.loadWidgets();Bangle.drawWidgets(); Bangle.setGPSPower(1);drawAll();startTimers();Bangle.on("GPS",onGPS);setButtons(); diff --git a/apps/gpsnav/app_b2.js b/apps/gpsnav/app_b2.js new file mode 100644 index 000000000..ee6519c92 --- /dev/null +++ b/apps/gpsnav/app_b2.js @@ -0,0 +1,265 @@ +var candraw = true; +var brg = 0; +var wpindex = 0; +var locindex = 0; +const labels = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]; +var loc = { + speed: [ + require("locale").speed, + (kph) => { + return (kph / 1.852).toFixed(1) + "kn "; + } + ], + distance: [ + require("locale").distance, + (m) => { + return (m / 1852).toFixed(3) + "nm "; + } + ] +}; + + +function drawCompass(course) { + if (!candraw) return; + g.reset().clearRect(0, 24, 175, 71); + g.setFont("Vector", 18); + var start = course - 90; + if (start < 0) start += 360; + g.fillRect(14, 67, 162, 71); + var xpos = 16; + var frag = 15 - start % 15; + if (frag < 15) xpos += Math.floor((frag * 4) / 5); + else frag = 0; + for (var i = frag; i <= 180 - frag; i += 15) { + var res = start + i; + if (res % 90 == 0) { + g.drawString(labels[Math.floor(res / 45) % 8], xpos - 6, 28); + g.fillRect(xpos - 2, 47, xpos + 2, 67); + } else if (res % 45 == 0) { + g.drawString(labels[Math.floor(res / 45) % 8], xpos - 9, 28); + g.fillRect(xpos - 2, 52, xpos + 2, 67); + } else if (res % 15 == 0) { + g.fillRect(xpos, 58, xpos + 1, 67); + } + xpos += 12; + } + if (wpindex >= 0) { + var bpos = brg - course; + if (bpos > 180) bpos -= 360; + if (bpos < -180) bpos += 360; + bpos = Math.round((bpos * 4) / 5) + 88; + if (bpos < 16) bpos = 7; + if (bpos > 160) bpos = 169; + g.setColor(g.theme.bgH); + g.fillCircle(bpos, 63, 8); + } +} + +//displayed heading +var heading = 0; + +function newHeading(m, h) { + var s = Math.abs(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)); + if (hd < 0) hd += 360; + if (hd > 360) hd -= 360; + return hd; +} + +var course = 0; +var speed = 0; +var satellites = 0; +var wp; +var dist = 0; + +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) { + 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) { + 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); +} + +var selected = false; + +function drawN() { + g.reset().clearRect(0, 89, 175, 175); + var txt = loc.speed[locindex](speed); + g.setFont("6x8", 2); + g.drawString("o", 68, 87); + g.setFont("6x8", 1); + g.drawString(txt.substring(txt.length - 3), 156, 119); + g.setFont("Vector", 36); + var cs = course.toString().padStart(3, "0"); + g.drawString(cs, 2, 89); + g.drawString(txt.substring(0, txt.length - 3), 92, 89); + g.setFont("Vector", 18); + var bs = brg.toString().padStart(3, "0"); + g.drawString("Brg:", 1, 128); + g.drawString("Dist:", 1, 148); + g.setColor(selected ? g.theme.bgH : g.theme.bg); + g.fillRect(90, 127, 175, 143); + g.setColor(selected ? g.theme.fgH : g.theme.fg); + g.drawString(wp.name, 92, 128); + g.setColor(g.theme.fg); + g.drawString(bs, 42, 128); + g.drawString(loc.distance[locindex](dist), 42, 148); + g.setFont("6x8", 0.5); + g.drawString("o", 75, 127); + g.setFont("6x8", 1); + g.setColor(satellites ? g.theme.bg : g.theme.bgH); + g.fillRect(0, 167, 75, 175); + g.setColor(satellites ? g.theme.fg : g.theme.fgH); + g.drawString("Sats:", 1, 168); + g.drawString(satellites.toString(), 42, 168); +} + +var savedfix; + +function onGPS(fix) { + savedfix = fix; + if (fix !== undefined) { + course = isNaN(fix.course) ? course : Math.round(fix.course); + speed = isNaN(fix.speed) ? speed : fix.speed; + satellites = fix.satellites; + } + if (candraw) { + if (fix !== undefined && fix.fix == 1) { + dist = distance(fix, wp); + if (isNaN(dist)) dist = 0; + brg = bearing(fix, wp); + if (isNaN(brg)) brg = 0; + } + drawN(); + } +} + +var intervalRef; + +function stopdraw() { + candraw = false; + if (intervalRef) { + clearInterval(intervalRef); + } +} + +function startTimers() { + candraw = true; + intervalRefSec = setInterval(function() { + heading = newHeading(course, heading); + if (course != heading) drawCompass(heading); + }, 200); +} + +function drawAll() { + g.setColor(1, 0, 0); + g.fillPoly([88, 71, 78, 88, 98, 88]); + drawN(); + drawCompass(heading); +} + +function startdraw() { + g.clear(); + Bangle.drawWidgets(); + startTimers(); + drawAll(); +} + +function setButtons() { + Bangle.setUI("leftright", btn => { + if (!btn) { + doselect(); + } else { + nextwp(btn); + } + }); +} + +var SCREENACCESS = { + withApp: true, + request: function() { + this.withApp = false; + stopdraw(); + clearWatch(); + }, + release: function() { + this.withApp = true; + startdraw(); + setButtons(); + } +}; + +Bangle.on('lcdPower', function(on) { + if (!SCREENACCESS.withApp) return; + if (on) { + startdraw(); + } else { + stopdraw(); + } +}); + +var waypoints = require("waypoints").load(); +wp = waypoints[0]; + +function nextwp(inc) { + if (selected) { + wpindex += inc; + if (wpindex >= waypoints.length) wpindex = 0; + if (wpindex < 0) wpindex = waypoints.length - 1; + wp = waypoints[wpindex]; + drawN(); + } else { + locindex = inc > 0 ? 1 : 0; + drawN(); + } +} + +function doselect() { + 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); + } + selected = !selected; + print("selected = " + selected); + drawN(); +} + +g.clear(); +Bangle.setLCDBrightness(1); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +// load widgets can turn off GPS +Bangle.setGPSPower(1); +drawAll(); +startTimers(); +Bangle.on('GPS', onGPS); +// Toggle selected +setButtons(); diff --git a/apps/gpsnav/metadata.json b/apps/gpsnav/metadata.json index 5c1830318..bc46a733c 100644 --- a/apps/gpsnav/metadata.json +++ b/apps/gpsnav/metadata.json @@ -1,16 +1,17 @@ { "id": "gpsnav", "name": "GPS Navigation", - "version": "0.05", + "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", "tags": "tool,outdoors,gps", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", - "interface": "waypoints.html", + "dependencies" : { "waypoints":"type" }, "storage": [ - {"name":"gpsnav.app.js","url":"app.min.js"}, + {"name":"gpsnav.app.js","url":"app.min.js","supports":["BANGLEJS"]}, + {"name":"gpsnav.app.js","url":"app_b2.js","supports":["BANGLEJS2"]}, {"name":"gpsnav.img","url":"app-icon.js","evaluate":true} - ], - "data": [{"name":"waypoints.json","url":"waypoints.json"}] + ] } diff --git a/apps/gpsnav/screenshot-b2.png b/apps/gpsnav/screenshot-b2.png new file mode 100644 index 000000000..34ad23679 Binary files /dev/null and b/apps/gpsnav/screenshot-b2.png differ diff --git a/apps/gpsnav/waypoints.html b/apps/gpsnav/waypoints.html deleted file mode 100644 index d02260732..000000000 --- a/apps/gpsnav/waypoints.html +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - -

List of waypoints

- - - - - - - - - - - - -
NameLat.Long.Actions
-
-

Add a new waypoint

-
-
-
- -
-
- -
-
- -
-
-
-
- -
-
- -
-
-
-
- - - - - - - diff --git a/apps/gpsnav/waypoints.json b/apps/gpsnav/waypoints.json deleted file mode 100644 index 98a670c0d..000000000 --- a/apps/gpsnav/waypoints.json +++ /dev/null @@ -1,20 +0,0 @@ -[ - { - "name":"NONE" - }, - { - "name":"No10", - "lat":51.5032, - "lon":-0.1269 - }, - { - "name":"Stone", - "lat":51.1788, - "lon":-1.8260 - }, - { "name":"WP0" }, - { "name":"WP1" }, - { "name":"WP2" }, - { "name":"WP3" }, - { "name":"WP4" } -] \ No newline at end of file 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 e78b4ccd0..a4865be3f 100644 --- a/apps/ha/ChangeLog +++ b/apps/ha/ChangeLog @@ -1,2 +1,5 @@ 0.01: Release -0.02: Includeas the ha.lib.js library that can be used by other apps or clocks. \ No newline at end of file +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. +0.05: Clkinfo improvements. \ No newline at end of file diff --git a/apps/ha/ha.clkinfo.js b/apps/ha/ha.clkinfo.js new file mode 100644 index 000000000..1b1e468d7 --- /dev/null +++ b/apps/ha/ha.clkinfo.js @@ -0,0 +1,26 @@ +(function() { + var ha = require("ha.lib.js"); + var triggers = ha.getTriggers(); + + var haItems = { + name: "Home", + img: atob("GBiBAf/////////n///D//+B//8A//48T/wkD/gkD/A8D+AYB8AYA4eZ4QyZMOyZN+fb5+D/B+B+B+A8B+AYB+AYB+AYB+AYB+A8Bw=="), + items: [] + }; + + triggers.forEach((trigger, i) => { + haItems.items.push({ + name: null, + get: () => ({ text: trigger.display, img: trigger.getIcon()}), + show: function() { haItems.items[i].emit("redraw"); }, + hide: function () {}, + run: function() { + ha.sendTrigger("TRIGGER_BW"); + ha.sendTrigger(trigger.trigger); + return true; + } + }); + }); + + return haItems; +}) \ No newline at end of file diff --git a/apps/ha/metadata.json b/apps/ha/metadata.json index 63308b933..052e82fe0 100644 --- a/apps/ha/metadata.json +++ b/apps/ha/metadata.json @@ -1,7 +1,7 @@ { "id": "ha", "name": "HomeAssistant", - "version": "0.02", + "version": "0.05", "description": "Integrates your BangleJS into HomeAssistant.", "icon": "ha.png", "type": "app", @@ -20,6 +20,7 @@ "storage": [ {"name":"ha.app.js","url":"ha.app.js"}, {"name":"ha.lib.js","url":"ha.lib.js"}, + {"name":"ha.clkinfo.js","url":"ha.clkinfo.js"}, {"name":"ha.img","url":"ha.icon.js","evaluate":true} ] } diff --git a/apps/hcclock/ChangeLog b/apps/hcclock/ChangeLog index f70653d58..289c7ac2d 100644 --- a/apps/hcclock/ChangeLog +++ b/apps/hcclock/ChangeLog @@ -1,3 +1,4 @@ 0.01: Base code 0.02: Saved settings when switching color scheme -0.03: Added Button 3 opening messages (if app is installed) \ No newline at end of file +0.03: Added Button 3 opening messages (if app is installed) +0.04: Use `messages` library to check for new messages \ No newline at end of file diff --git a/apps/hcclock/hcclock.app.js b/apps/hcclock/hcclock.app.js index de5163996..9558c052b 100644 --- a/apps/hcclock/hcclock.app.js +++ b/apps/hcclock/hcclock.app.js @@ -3,7 +3,7 @@ // Numbers Rect order (left, top, right, bottom) // Each number defines a set of rects to draw -const numbers = +const numbers = [ [// Zero [0, 0, 1, 0.2], @@ -64,7 +64,7 @@ const numbers = [0, 0.8, 1, 1], [0, 0, 0.1, 0.6], [0.9, 0, 1, 1] - ] + ] ]; const months = [ "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" ]; @@ -103,7 +103,7 @@ function updateTime() let mo = now.getMonth(); let y = now.getFullYear(); let d = now.getDate(); - + if(h != hour) { hour = h; @@ -127,7 +127,7 @@ function updateTime() day = d; g.setFont("6x8", 2); g.setFontAlign(0, -1, 0); - g.drawString(fmtDate(d,mo,y,hour), 120, 120); + g.drawString(fmtDate(d,mo,y,hour), 120, 120); } drawMessages(); } @@ -136,7 +136,7 @@ function drawDigits(x, value) { if(!Bangle.isLCDOn()) // No need to draw when LCD Off return; - + drawChar(Math.floor(value/10), 15, x, 115, x+50); if(value%10 == 1) drawChar(value%10, 55, x, 155, x+50); @@ -228,27 +228,18 @@ function flipColors() // MESSAGE HANDLING() // -let messages_installed = require("Storage").read("messages.app.js") != undefined; +let messages_installed = require("Storage").read("messages") !== undefined; function handleMessages() { - if(messages_installed && hasMessages() > 0) - { - E.showMessage("Loading Messages..."); - load("messages.app.js"); - } + if(!hasMessages()) return; + E.showMessage("Loading Messages..."); + load("messages.app.js"); } function hasMessages() { - if(!messages_installed) - return false; - - var messages = require("Storage").readJSON("messages.json",1)||[]; - if (messages.some(m=>m.new)) - return true; - else - return false; + return messages_installed && require("messages").status() === 'new'; } let msg = atob("GBiBAAAAAAAAAAAAAAAAAAAAAB//+DAADDAADDAADDwAPD8A/DOBzDDn/DA//DAHvDAPvjAPvjAPvjAPvh///gf/vAAD+AAB8AAAAA=="); @@ -256,20 +247,21 @@ let had_messages = false; function drawMessages() { - if(!had_messages && hasMessages()) { + const has_messages = hasMessages(); + if(has_messages === had_messages) return; + if(has_messages) { g.setColor(255,255,255); g.drawImage(msg, 184, 212); g.setFont("6x8", 2); g.setFontAlign(0, -1, 0); g.drawString(">", 224, 216); - had_messages = true; - } - else if (had_messages && !hasMessages()) + } + else { g.setColor(0,0,0); g.fillRect(180, 210, 240, 240); - had_messages = false; } + had_messages = has_messages; } ////////////////////////////////////////// diff --git a/apps/hcclock/metadata.json b/apps/hcclock/metadata.json index 0d4cbe0cd..b8f8c14b9 100644 --- a/apps/hcclock/metadata.json +++ b/apps/hcclock/metadata.json @@ -1,7 +1,7 @@ { "id": "hcclock", "name": "Hi-Contrast Clock", - "version": "0.03", + "version": "0.04", "description": "Hi-Contrast Clock : A simple yet very bold clock that aims to be readable in high luninosity environments. Uses big 10x5 pixel digits. Use BTN 1 to switch background and foreground colors.", "icon": "hcclock-icon.png", "type": "clock", diff --git a/apps/henkinen/ChangeLog b/apps/henkinen/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/henkinen/ChangeLog @@ -0,0 +1 @@ +0.01: New App! 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/hrmaccevents/README.md b/apps/hrmaccevents/README.md index 188ca325a..ecd619152 100644 --- a/apps/hrmaccevents/README.md +++ b/apps/hrmaccevents/README.md @@ -13,6 +13,21 @@ This app can use [BTHRM](https://banglejs.com/apps/#bthrm) as a reference. * (Recording to file) Stop the recording with a long press of the button and download log.csv with the Espruino IDE. * (Recording to browser) Click "Stop" followed by "Save" and store the resulting file on your device. + +## CSV data format + +The CSV data contains the following columns: + +* Time - Current time (milliseconds since 1970) +* Acc_x,Acc_y,Acc_z - X,Y,Z acceleration in Gs +* HRM_b - BPM figure reported by internal HRM algorithm in Bangle.js +* HRM_c - BPM confidence figure (0..100%) reported by internal HRM algorithm in Bangle.js +* HRM_r - `e.raw` from the `Bangle.on("HRM-raw"` event. This is the value that gets passed to the HRM algorithm. +* HRM_f - `e.filt` from the `Bangle.on("HRM-raw"` event. This is the filtered value that comes from the Bangle's HRM algorithm and which is used for peak detection +* PPG_r - `e.vcPPG` from the `Bangle.on("HRM-raw"` event. This is the PPG value direct from the sensor +* PPG_o - `e.vcPPGoffs` from the `Bangle.on("HRM-raw"` event. This is the PPG offset used to map `e.vcPPG` to `e.raw` so there are no glitches when the exposure values in the sensor change. +* BTHRM - BPM figure from external Bluetooth HRM device (this is our reference BPM) + ## Creator [halemmerich](https://github.com/halemmerich) diff --git a/apps/hwid_a_battery_widget/ChangeLog b/apps/hwid_a_battery_widget/ChangeLog index da289abff..cbdccfecf 100644 --- a/apps/hwid_a_battery_widget/ChangeLog +++ b/apps/hwid_a_battery_widget/ChangeLog @@ -4,4 +4,5 @@ 0.04: Increase screen update rate when charging 0.05: Deleting Background - making Font larger 0.06: Fixing refresh issues -0.07: Fixed position after unlocking \ No newline at end of file +0.07: Fixed position after unlocking +0.08: Handling exceptions \ No newline at end of file diff --git a/apps/hwid_a_battery_widget/metadata.json b/apps/hwid_a_battery_widget/metadata.json index 38fd503a2..29b0540c2 100644 --- a/apps/hwid_a_battery_widget/metadata.json +++ b/apps/hwid_a_battery_widget/metadata.json @@ -3,7 +3,7 @@ "name": "A Battery Widget (with percentage) - Hanks Mod", "shortName":"H Battery Widget", "icon": "widget.png", - "version":"0.07", + "version":"0.08", "type": "widget", "supports": ["BANGLEJS", "BANGLEJS2"], "readme": "README.md", diff --git a/apps/hwid_a_battery_widget/widget.js b/apps/hwid_a_battery_widget/widget.js index bd1c4e0c5..e42c15355 100644 --- a/apps/hwid_a_battery_widget/widget.js +++ b/apps/hwid_a_battery_widget/widget.js @@ -29,9 +29,9 @@ var y = this.y; if ((typeof x === 'undefined') || (typeof y === 'undefined')) { } else { - const l = E.getBattery(); + const l = E.getBattery(); // debug: Math.floor(Math.random() * 101); let xl = x+4+l*(s-12)/100; - if (l != old_l){ // Delete the old value from screen + if ((l != old_l) && (typeof old_l != 'undefined') ){ // Delete the old value from screen let xl_old = x+4+old_l*(s-12)/100; g.setColor(COLORS.white); // g.fillRect(x+2,y+5,x+s-6,y+18); @@ -41,9 +41,9 @@ //g.fillRect(old_x,old_y,old_x+4+l*(s-12)/100,old_y+16+3); // clear (lazy) g.drawString(old_l, old_x + 14, old_y + 10); g.fillRect(x+4,y+14+3,xl_old,y+16+3); // charging bar - old_l = l; + } - + old_l = l; //console.log(old_x); g.setColor(levelColor(l)); @@ -64,7 +64,7 @@ } Bangle.on('charging',function(charging) { draw(); }); - var id = setInterval(()=>WIDGETS["wid_a_battery_widget"].draw(), intervalLow); + var id = setInterval(()=>WIDGETS["hwid_a_battery_widget"].draw(), intervalLow); - WIDGETS["wid_a_battery_widget"]={area:"tr",width:30,draw:draw}; + WIDGETS["hwid_a_battery_widget"]={area:"tr",width:30,draw:draw}; })(); diff --git a/apps/hworldclock/README.md b/apps/hworldclock/README.md index 170a2aa8b..25cc368ca 100644 --- a/apps/hworldclock/README.md +++ b/apps/hworldclock/README.md @@ -1,16 +1,16 @@ # Hanks World Clock - See the time in four locations -In addition to the main clock and date in your current location, you can add up to three other locations. Great for travel or remote working. -Additionally we show the sunset/sunrise and seconds for the current location and the day name is shown in your locale. +In addition to the main clock and date in your current location, you can add up to three other locations. Great for travel or remote working. +Additionally we show the sunset/sunrise and seconds for the current location and the day name is shown in your locale. If watch is locked, seconds get refreshed every 10 seconds. ![](hworldclock.png) ## Usage -Location for sun set / rise set with mylocation app. +Location for sun set / rise set with mylocation app. -Provide names and the UTC offsets for up to three other timezones in the app store. These are stored in a json file on your watch. UTC offsets can be decimal (e.g., 5.5 for India). +Provide names and the UTC offsets for up to three other timezones in the app store. These are stored in a json file on your watch. UTC offsets can be decimal (e.g., 5.5 for India). The clock does not handle summer time / daylight saving time changes automatically. If one of your three locations changes its UTC offset, you can simply change the setting in the app store and update. Currently the clock only supports 24 hour time format for the additional time zones. @@ -23,5 +23,5 @@ Please use [the Espruino Forum](http://forum.espruino.com/microcosms/1424/) if y Created by Hank. -Based on the great work of "World Clock - 4 time zones". Made by [Scott Hale](https://www.github.com/computermacgyver), based upon the [Simple Clock](https://github.com/espruino/BangleApps/tree/master/apps/sclock). +Based on the great work of "World Clock - 4 time zones". Made by [Scott Hale](https://www.github.com/computermacgyver), based upon the [Simple Clock](https://github.com/espruino/BangleApps/tree/master/apps/sclock). And Sun Clock [Sun Clock](https://github.com/espruino/BangleApps/tree/master/apps/sunclock) \ No newline at end of file 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 2e8b285ad..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,7 +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", - "sortorder": -10 + "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..f81bbf185 100644 --- a/apps/imageclock/ChangeLog +++ b/apps/imageclock/ChangeLog @@ -8,3 +8,7 @@ 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 diff --git a/apps/imageclock/app.js b/apps/imageclock/app.js index 7b933b710..ff3f5a62d 100644 --- a/apps/imageclock/app.js +++ b/apps/imageclock/app.js @@ -1,717 +1,777 @@ -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); - }); + 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)); + } + }; + } - if (requestedDraws > 0){ - //print(new Date().toISOString(), "Had deferred drawing left, drawing again"); + startPerfLog("loadFunctions"); + + 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); + }); + }; + + let cleanupDelays = function(){ + //print("Cleanup delays", delayTimeouts); + for (let t of delayTimeouts){ + clearTimeout(t); + } + 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; + } + } + 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; + + + if (number) number = number.toFixed(0); + + 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; + if (imageIndex){ + firstImage = getByPath(resources, [], "" + (0 + imageIndex)); + } else { + firstImage = getByPath(resources, element.ImagePath, 0); + } + + let minusImage; + if (imageIndexMinus){ + minusImage = getByPath(resources, [], "" + (0 + imageIndexMinus)); + } else { + minusImage = getByPath(resources, element.ImagePath, "minus"); + } + + let 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")); + } + + 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) + 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, 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; - setTimeout(()=>{initialDraw(resources, face);}, 10); - } - } //else { - //print("queued draw"); - //} -} + //print(new Date().toISOString(), "Drawing start"); + startPerfLog("initialDraw"); + //print("Precompiled"); + let promise = precompiledJs(watchfaceResources, watchface); -function handleHrm(e){ - if (e.confidence > 70){ - pulse = e.bpm; - if (!redrawEvents || redrawEvents.includes("HRM") && !Bangle.isLocked()){ - //print("Redrawing on HRM"); + promise.then(()=>{ + let currentDrawingTime = Date.now(); + if (showWidgets && global.WIDGETS){ + //print("Draw widgets"); + restoreWidgetDraw(); + Bangle.drawWidgets(); + g.setColor(g.theme.fg); + g.drawLine(0,24,g.getWidth(),24); + } + lastDrawTime = Date.now() - start; + 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; + //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); } - } -} + }; -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); - } -} - -function handleCharging(e){ - if (!redrawEvents || redrawEvents.includes("charging") && !Bangle.isLocked()){ - //print("Redrawing on charging"); - initialDraw(watchfaceResources, watchface); - } -} - - -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); + let handleCharging = function(e){ + if (!redrawEvents || redrawEvents.includes("charging") && !Bangle.isLocked()){ + //print("Redrawing on charging"); initialDraw(watchfaceResources, watchface); } - 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); + }; + + + 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); } - setMatchedInterval(()=>{ - //print("Redrawing on locked interval"); - initialDraw(watchfaceResources, watchface); - },lockedRedraw, (v)=>{ - lockedDrawInterval = v; - }, lastDrawTime); - Bangle.setHRMPower(0, "imageclock"); - Bangle.setBarometerPower(0, 'imageclock'); - } -} + 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'); + } + }; -var showWidgets = false; -var showWidgetsChanged = false; -var currentDragDistance = 0; + let showWidgetsChanged = false; + let currentDragDistance = 0; -Bangle.setUI("clock"); -Bangle.on('drag', (e)=>{ + let restoreWidgetDraw = function(){ + if (global.WIDGETS) { + for (let w in global.WIDGETS) { + let wd = global.WIDGETS[w]; + wd.draw = originalWidgetDraw[w]; + wd.area = originalWidgetArea[w]; + } + } + }; + + let handleDrag = function(e){ + //print("handleDrag"); 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]; - } - } + restoreWidgetDraw(); showWidgetsChanged = true; } if (showWidgets && !dragDown){ @@ -726,49 +786,91 @@ Bangle.on('drag', (e)=>{ showWidgets = dragDown; 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('drag', handleDrag); -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"); + if (global.WIDGETS) { + originalWidgetDraw = {}; + originalWidgetArea = {}; + for (let w in global.WIDGETS) { + let wd = global.WIDGETS[w]; + originalWidgetDraw[w] = wd.draw; + originalWidgetArea[w] = wd.area; + wd.draw = () => {}; + wd.area = ""; + } + } + } + + handleLock(Bangle.isLocked(), true); + + 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('drag', handleDrag); + 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 unlockedDrawInterval){ + //print("Clearing unlocked", i); + clearInterval(i); + } + delete unlockedDrawInterval; + for (let i of lockedDrawInterval){ + //print("Clearing locked", i); + clearInterval(i); + } + delete lockedDrawInterval; + delete showWidgets; + delete 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..e595b51ca 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; @@ -732,14 +716,17 @@ 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"; + } 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..e068b9fa7 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.11", "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/imgclock/ChangeLog b/apps/imgclock/ChangeLog index 01a6a4248..0895bb66d 100644 --- a/apps/imgclock/ChangeLog +++ b/apps/imgclock/ChangeLog @@ -7,3 +7,5 @@ 0.06: Support 12 hour time 0.07: Don't cut off wide date formats 0.08: Use Bangle.setUI for button/launcher handling +0.09: Bangle.js 2 compatibility +0.10: Tell clock widgets to hide. diff --git a/apps/imgclock/app.js b/apps/imgclock/app.js index 0e4435638..7d74bee82 100644 --- a/apps/imgclock/app.js +++ b/apps/imgclock/app.js @@ -10,8 +10,8 @@ var IX = inf.x, IY = inf.y, IBPP = inf.bpp; var IW = 174, IH = 45, OY = 24; var bgwidth = img.charCodeAt(0); var bgoptions; -if (bgwidth<240) - bgoptions = { scale : 240/bgwidth }; +if (bgwidth{ draw(); } }); -// Show launcher when button pressed -Bangle.setUI("clock"); + diff --git a/apps/imgclock/b2_122240.png b/apps/imgclock/b2_122240.png new file mode 100644 index 000000000..1a3f4daaa Binary files /dev/null and b/apps/imgclock/b2_122240.png differ diff --git a/apps/imgclock/b2_122271.png b/apps/imgclock/b2_122271.png new file mode 100644 index 000000000..31733fb2c Binary files /dev/null and b/apps/imgclock/b2_122271.png differ diff --git a/apps/imgclock/b2_explode.png b/apps/imgclock/b2_explode.png new file mode 100644 index 000000000..5252bbcd2 Binary files /dev/null and b/apps/imgclock/b2_explode.png differ diff --git a/apps/imgclock/b2_thisisfine.png b/apps/imgclock/b2_thisisfine.png new file mode 100644 index 000000000..1b7daaf60 Binary files /dev/null and b/apps/imgclock/b2_thisisfine.png differ diff --git a/apps/imgclock/custom.html b/apps/imgclock/custom.html index 2511f8a54..1d8e06c07 100644 --- a/apps/imgclock/custom.html +++ b/apps/imgclock/custom.html @@ -5,6 +5,7 @@
+ Please wait...
@@ -12,13 +13,51 @@ diff --git a/apps/imgclock/metadata.json b/apps/imgclock/metadata.json index 799d11acc..94dff5f17 100644 --- a/apps/imgclock/metadata.json +++ b/apps/imgclock/metadata.json @@ -2,18 +2,19 @@ "id": "imgclock", "name": "Image background clock", "shortName": "Image Clock", - "version": "0.08", + "version": "0.10", "description": "A clock with an image as a background", "icon": "app.png", "type": "clock", "tags": "clock", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS","BANGLEJS2"], "custom": "custom.html", + "customConnect": true, "storage": [ {"name":"imgclock.app.js","url":"app.js"}, {"name":"imgclock.img","url":"app-icon.js","evaluate":true}, {"name":"imgclock.face.img"}, {"name":"imgclock.face.json"}, - {"name":"imgclock.face.bg","content":""} + {"name":"imgclock.face.bg","content":"X"} ] } diff --git a/apps/impwclock/ChangeLog b/apps/impwclock/ChangeLog index 6555fcc8f..0af7c99d6 100644 --- a/apps/impwclock/ChangeLog +++ b/apps/impwclock/ChangeLog @@ -3,3 +3,4 @@ 0.03: Move to Bangle.setUI to launcher support 0.04: Tweaks for compatibility with BangleJS2 0.05: Time-word now readable on Bangle.js 2 +0.06: Tell clock widgets to hide. diff --git a/apps/impwclock/clock-impword.js b/apps/impwclock/clock-impword.js index c42dbda44..04421017b 100644 --- a/apps/impwclock/clock-impword.js +++ b/apps/impwclock/clock-impword.js @@ -154,6 +154,9 @@ Bangle.on('lcdPower', function(on) { if (on) drawWordClock(); }); +// Show launcher when button pressed +Bangle.setUI("clock"); + g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); @@ -172,5 +175,4 @@ Bangle.on('touch',e=>{ } }); -// Show launcher when button pressed -Bangle.setUI("clock"); + diff --git a/apps/impwclock/metadata.json b/apps/impwclock/metadata.json index 733dbb957..1b92ea3ae 100644 --- a/apps/impwclock/metadata.json +++ b/apps/impwclock/metadata.json @@ -1,7 +1,7 @@ { "id": "impwclock", "name": "Imprecise Word Clock", - "version": "0.05", + "version": "0.06", "description": "Imprecise word clock for vacations, weekends, and those who never need accurate time.", "icon": "clock-impword.png", "type": "clock", diff --git a/apps/isoclock/ChangeLog b/apps/isoclock/ChangeLog index 809091ce4..7b57ecfa9 100644 --- a/apps/isoclock/ChangeLog +++ b/apps/isoclock/ChangeLog @@ -1,2 +1,3 @@ 0.01: Created app based on digiclock with some small tweaks. 0.02: Swap to Bangle.setUI for launcher/buttons +0.03: Tell clock widgets to hide. diff --git a/apps/isoclock/checkout b/apps/isoclock/checkout new file mode 100644 index 000000000..e69de29bb diff --git a/apps/isoclock/isoclock.js b/apps/isoclock/isoclock.js index 59f28e66e..7526660b9 100644 --- a/apps/isoclock/isoclock.js +++ b/apps/isoclock/isoclock.js @@ -89,8 +89,8 @@ Bangle.on('lcdPower',on=>{ } }); -Bangle.loadWidgets(); -Bangle.drawWidgets(); - // Show launcher when button pressed Bangle.setUI("clock"); + +Bangle.loadWidgets(); +Bangle.drawWidgets(); diff --git a/apps/isoclock/metadata.json b/apps/isoclock/metadata.json index 313153dde..488afcb41 100644 --- a/apps/isoclock/metadata.json +++ b/apps/isoclock/metadata.json @@ -2,7 +2,7 @@ "id": "isoclock", "name": "ISO Compliant Clock Face", "shortName": "ISO Clock", - "version": "0.02", + "version": "0.03", "description": "Tweaked fork of digiclock for ISO date and time", "icon": "isoclock.png", "type": "clock", diff --git a/apps/kanawatch/ChangeLog b/apps/kanawatch/ChangeLog index ce7cac123..f2c991fd0 100644 --- a/apps/kanawatch/ChangeLog +++ b/apps/kanawatch/ChangeLog @@ -1,3 +1,5 @@ 0.01: First release 0.02: Improve battery life, sprite resolution, fix launcher issue and unaligned text bug 0.03: Reduce code size, refresh once a minute and faster refresh +0.04: Show a random kana every minute to improve learning +0.05: Tell clock widgets to hide. diff --git a/apps/kanawatch/README.md b/apps/kanawatch/README.md index 1fdf1927c..e213949dc 100644 --- a/apps/kanawatch/README.md +++ b/apps/kanawatch/README.md @@ -3,10 +3,17 @@ A simple watchface design with hiragana and katakana cards for learning. +## Changelog + +0.01: First release +0.02: Improve battery life, sprite resolution, fix launcher issue and unaligned text bug +0.03: Reduce code size, refresh once a minute and faster refresh +0.04: Show a random kana every minute to improve learning + ## Author Written by pancake in 2022, powered by insomnia ## Screenshots -![hiragana and katakana](screenshot.jpg) +![hiragana and katakana](screenshot.png) diff --git a/apps/kanawatch/app.js b/apps/kanawatch/app.js index d08b644a0..088dab785 100644 --- a/apps/kanawatch/app.js +++ b/apps/kanawatch/app.js @@ -127,6 +127,18 @@ function next () { updateWatch(ohhmm); } +function randKana() { + try { + const keys = Object.keys(katakana); + const total = keys.length; + let index = 0 | (Math.random() * total); + curkana = keys[index]; + kana = hiramode ? hiragana[curkana] : katakana[curkana]; + } catch (e) { + randKana(); + } +} + function prev () { let oldk = ''; let count = 0; @@ -233,6 +245,7 @@ function tickWatch () { } const hhmm = zpad(now.getHours()) + ':' + zpad(now.getMinutes()); if (hhmm !== ohhmm) { + randKana(); updateWatch(hhmm); ohhmm = hhmm; } @@ -251,9 +264,10 @@ Bangle.on('touch', function (tap, top) { }); g.clear(true); +// show launcher when button pressed +Bangle.setUI('clock'); Bangle.loadWidgets(); tickWatch(); setInterval(tickWatch, 1000 * 60); -// show launcher when button pressed -Bangle.setUI('clock'); + diff --git a/apps/kanawatch/metadata.json b/apps/kanawatch/metadata.json index 8593c8f82..b14703979 100644 --- a/apps/kanawatch/metadata.json +++ b/apps/kanawatch/metadata.json @@ -2,7 +2,7 @@ "id": "kanawatch", "name": "Kanawatch", "shortName": "Kanawatch", - "version": "0.03", + "version": "0.05", "type": "clock", "description": "Learn Hiragana and Katakana", "icon": "app.png", diff --git a/apps/kbmorse/ChangeLog b/apps/kbmorse/ChangeLog index f62348ec8..c85361374 100644 --- a/apps/kbmorse/ChangeLog +++ b/apps/kbmorse/ChangeLog @@ -1 +1,2 @@ -0.01: New Keyboard! \ No newline at end of file +0.01: New Keyboard! +0.02: Temporarily fix because of firmware bug. diff --git a/apps/kbmorse/lib.js b/apps/kbmorse/lib.js index 8bc177a46..997f2cb16 100644 --- a/apps/kbmorse/lib.js +++ b/apps/kbmorse/lib.js @@ -82,6 +82,36 @@ exports.input = function(options) { } return new Promise((resolve, reject) => { + const Layout = require("Layout"); + let layout = new Layout({ + type: "h", c: [ + { + type: "v", width: Bangle.appRect.w-8, bgCol: g.theme.bg, c: [ + {id: "dots", type: "txt", font: "6x8:2", label: "", fillx: 1, bgCol: g.theme.bg}, + {filly: 1, bgCol: g.theme.bg}, + { + type: "h", fillx: 1, c: [ + {id: "del", type: "txt", font: "6x8", label: " + ({type: "txt", font: "6x8", height: Math.floor(Bangle.appRect.h/3), r: 1, label: l}) + ) + } + ] + }); function update() { let dots = [], dashes = []; @@ -157,36 +187,6 @@ exports.input = function(options) { } } - const Layout = require("Layout"); - let layout = new Layout({ - type: "h", c: [ - { - type: "v", width: Bangle.appRect.w-8, bgCol: g.theme.bg, c: [ - {id: "dots", type: "txt", font: "6x8:2", label: "", fillx: 1, bgCol: g.theme.bg}, - {filly: 1, bgCol: g.theme.bg}, - { - type: "h", fillx: 1, c: [ - {id: "del", type: "txt", font: "6x8", label: " - ({type: "txt", font: "6x8", height: Math.floor(Bangle.appRect.h/3), r: 1, label: l}) - ) - } - ] - }); g.reset().clear(); update(); @@ -244,4 +244,4 @@ exports.input = function(options) { }; Bangle.on("swipe", Bangle.swipeHandler); }); -}; \ No newline at end of file +}; diff --git a/apps/kbmorse/metadata.json b/apps/kbmorse/metadata.json index f9c5354f1..9111d514d 100644 --- a/apps/kbmorse/metadata.json +++ b/apps/kbmorse/metadata.json @@ -1,7 +1,7 @@ { "id": "kbmorse", "name": "Morse keyboard", - "version": "0.01", + "version": "0.02", "description": "A library for text input as morse code", "icon": "app.png", "type": "textinput", 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/kbtouch/metadata.json b/apps/kbtouch/metadata.json index f6d6d5228..89d121d63 100644 --- a/apps/kbtouch/metadata.json +++ b/apps/kbtouch/metadata.json @@ -6,10 +6,11 @@ "type":"textinput", "tags": "keyboard", "supports" : ["BANGLEJS2"], - "screenshots": [{"url":"screenshot.png"}], + "screenshots": [{"url":"screenshot.png"}], "readme": "README.md", "storage": [ {"name":"textinput","url":"lib.js"}, {"name":"kbtouch.settings.js","url":"settings.js"} - ] + ], + "sortorder":-1 } diff --git a/apps/kitchen/ChangeLog b/apps/kitchen/ChangeLog index 3767a9548..4e8c49c50 100644 --- a/apps/kitchen/ChangeLog +++ b/apps/kitchen/ChangeLog @@ -11,3 +11,4 @@ 0.11: Detect when waypoints.json is not present, error E-WPT 0.12: Added stepo2 as a replacement for stepo and digi 0.13: Added long press BTN2 toggle gpsrec status in GPS clock +0.14: Move waypoints.json (and editor) to 'waypoints' app diff --git a/apps/kitchen/README.md b/apps/kitchen/README.md index 102881d15..3049d9c6d 100644 --- a/apps/kitchen/README.md +++ b/apps/kitchen/README.md @@ -60,7 +60,7 @@ The following buttons depend on which face is currently in use ![](screenshot_stepo.jpg) - now replaced by Stepo2 but still available if you install manually -- Requires one of the pedominter widgets to be installed +- Requires one of the pedominter widgets to be installed - Displays the time in large font - Display current step count in a doughnut gauge - Show step count in the middle of the doughnut gauge @@ -208,14 +208,8 @@ which will obviously limit this. ### Waypoint Editor -Clicking on the download icon of gpsnav in the app loader invokes the -waypoint editor. The editor downloads and displays the current -`waypoints.json` file. Clicking the `Edit` button beside an entry -causes the entry to be deleted from the list and displayed in the -edit boxes. It can be restored - by clicking the `Add waypoint` -button. A new markable entry is created by using the `Add name` -button. The edited `waypoints.json` file is uploaded to the Bangle by -clicking the `Upload` button. +Clicking on the download icon of `Waypoints` in the app loader invokes the +waypoint editor. See the `Waypoints` app for more information. ### Calibration of the Compass diff --git a/apps/kitchen/kitchen.app.js b/apps/kitchen/kitchen.app.js index 5564b2807..2c2cebaef 100644 --- a/apps/kitchen/kitchen.app.js +++ b/apps/kitchen/kitchen.app.js @@ -23,7 +23,7 @@ function nextFace(){ iface += 1 iface = iface % FACES.length; face = FACES[iface](); - + g.clear(); g.reset(); face.init(gpsObj, swObj, hrmObj, tripObject); @@ -64,7 +64,7 @@ function buttonReleased(btn) { clearInterval(pressTimer); pressTimer = undefined; } - + if ( dur >= 1.5 ) { switch(btn) { case 1: @@ -165,11 +165,11 @@ GPS.prototype.getLastFix = function() { GPS.prototype.determineGPSState = function() { this.log_debug("determineGPSState"); gpsPowerState = Bangle.isGPSOn(); - + //this.log_debug("last_fix.fix " + this.last_fix.fix); //this.log_debug("gpsPowerState " + this.gpsPowerState); //this.log_debug("last_fix.satellites " + this.last_fix.satellites); - + if (!gpsPowerState) { this.gpsState = this.GPS_OFF; this.resetLastFix(); @@ -178,9 +178,9 @@ GPS.prototype.determineGPSState = function() { } else { this.gpsState = this.GPS_SATS; } - + this.log_debug("gpsState=" + this.gpsState); - + if (this.gpsState !== this.GPS_OFF) { if (this.listenerCount === 0) { Bangle.on('GPS', processFix); @@ -196,9 +196,9 @@ GPS.prototype.determineGPSState = function() { } }; -GPS.prototype.getGPSTime = function() { +GPS.prototype.getGPSTime = function() { var time; - + if (this.last_fix !== undefined && this.last_fix.time !== undefined && this.last_fix.time.toUTCString !== undefined && (this.gpsState == this.GPS_SATS || this.gpsState == this.GPS_RUNNING)) { time = this.last_fix.time.toUTCString().split(" "); @@ -216,7 +216,7 @@ GPS.prototype.toggleGPSPower = function() { this.gpsPowerState = Bangle.isGPSOn(); this.gpsPowerState = !this.gpsPowerState; Bangle.setGPSPower((this.gpsPowerState ? 1 : 0), 'kitchen'); - + this.resetLastFix(); this.determineGPSState(); @@ -247,11 +247,11 @@ GPS.prototype.processFix = function(fix) { //this.log_debug("GPS:processFix()"); //this.log_debug(fix); this.last_fix.time = fix.time; - + if (this.gpsState == this.GPS_TIME) { this.gpsState = this.GPS_SATS; } - + if (fix.fix) { //this.log_debug("Got fix - setting state to GPS_RUNNING"); this.gpsState = this.GPS_RUNNING; @@ -271,10 +271,10 @@ GPS.prototype.formatTime = function(now) { GPS.prototype.timeSince = function(t) { var hms = t.split(":"); var now = new Date(); - + var sn = 3600*(now.getHours()) + 60*(now.getMinutes()) + 1*(now.getSeconds()); var st = 3600*(hms[0]) + 60*(hms[1]) + 1*(hms[2]); - + return (sn - st); }; @@ -313,7 +313,7 @@ GPS.prototype.getWPdistance = function() { GPS.prototype.getWPbearing = function() { //log_debug(this.last_fix); //log_debug(this.wp_current); - + if (this.wp_current.name === "E-WPT" || this.wp_current.name === "NONE" || this.wp_current.lat === undefined || this.wp_current.lat === 0) return 0; else @@ -321,7 +321,7 @@ GPS.prototype.getWPbearing = function() { } GPS.prototype.loadFirstWaypoint = function() { - var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"E-WPT"}]; + var waypoints = require("waypoints").load(); this.wp_index = 0; this.wp_current = waypoints[this.wp_index]; log_debug(this.wp_current); @@ -345,10 +345,10 @@ GPS.prototype.markWaypoint = function() { return; log_debug("GPS::markWaypoint()"); - - var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"E-WPT"}]; + + var waypoints = require("waypoints").load(); this.wp_current = waypoints[this.wp_index]; - + if (this.waypointHasLocation()) { waypoints[this.wp_index] = {name:this.wp_current.name, lat:0, lon:0}; } else { @@ -356,12 +356,12 @@ GPS.prototype.markWaypoint = function() { } this.wp_current = waypoints[this.wp_index]; - require("Storage").writeJSON("waypoints.json", waypoints); + require("waypoints").save(waypoints); log_debug("GPS::markWaypoint() written"); } GPS.prototype.nextWaypoint = function(inc) { - var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"E-WPT"}]; + var waypoints = require("waypoints").load(); this.wp_index+=inc; if (this.wp_index>=waypoints.length) this.wp_index=0; if (this.wp_index<0) this.wp_index = waypoints.length-1; @@ -520,7 +520,7 @@ function STOPWATCH() { this.redrawLaps = true; this.redrawTime = true; } - + STOPWATCH.prototype.log_debug = function(o) { //console.log(o); } @@ -531,7 +531,7 @@ STOPWATCH.prototype.timeToText = function(t) { let secs = Math.floor(t/1000)%60; let text; - if (hrs === 0) + if (hrs === 0) text = ("0"+mins).substr(-2) + ":" + ("0"+secs).substr(-2); else text = (""+hrs) + ":" + ("0"+mins).substr(-2) + ":" + ("0"+secs).substr(-2); @@ -551,7 +551,7 @@ STOPWATCH.prototype.stopStart = function() { if (this.running) this.tStart = Date.now() + this.tStart - this.tCurrent; - + this.tTotal = Date.now() + this.tTotal - this.tCurrent; this.tCurrent = Date.now(); this.redrawButtons = true; @@ -623,7 +623,7 @@ STOPWATCH.prototype.drawLaptimes = function() { g.setFont("Vector",24); g.setFontAlign(-1,-1); g.clearRect(4, 205, 239, 229); // clear the last line of the lap times - + let laps = 0; for (let i in this.lapTimes) { g.drawString(this.lapTimes.length-i + ": " + this.timeToText(this.lapTimes[i]), 4, this.timeY + 40 + i*24); @@ -645,7 +645,7 @@ STOPWATCH.prototype.drawTime = function() { g.setFont("Vector",38); g.setFontAlign(0,0); g.clearRect(0, this.timeY-21, 200, this.timeY+21); - g.setColor(0xFFC0); + g.setColor(0xFFC0); g.drawString(txtTotal, xTotal, this.timeY); // current lap time @@ -691,7 +691,7 @@ function HRM() { this.bpm = 0; this.confidence = 0; } - + HRM.prototype.log_debug = function(o) { //console.log(o); } @@ -782,7 +782,7 @@ Debug Object function DEBUG() { this.logfile = require("Storage").open("debug.log","a"); } - + DEBUG.prototype.log = function(msg) { let timestamp = new Date().toString().split(" ")[4]; let line = timestamp + ", " + msg + "\n"; diff --git a/apps/kitchen/metadata.json b/apps/kitchen/metadata.json index ab2e7183c..9c9f7b2ec 100644 --- a/apps/kitchen/metadata.json +++ b/apps/kitchen/metadata.json @@ -1,14 +1,14 @@ { "id": "kitchen", "name": "Kitchen Combo", - "version": "0.13", + "version": "0.14", "description": "Combination of the Stepo, Walkersclock, Arrow and Waypointer apps into a multiclock format. 'Everything but the kitchen sink'", "icon": "kitchen.png", "type": "clock", "tags": "tool,outdoors,gps", "supports": ["BANGLEJS"], "readme": "README.md", - "interface": "waypoints.html", + "dependencies" : { "waypoints":"type" }, "storage": [ {"name":"kitchen.app.js","url":"kitchen.app.js"}, {"name":"stepo2.kit.js","url":"stepo2.kit.js"}, @@ -16,6 +16,5 @@ {"name":"gps.kit.js","url":"gps.kit.js"}, {"name":"compass.kit.js","url":"compass.kit.js"}, {"name":"kitchen.img","url":"kitchen.icon.js","evaluate":true} - ], - "data": [{"name":"waypoints.json","url":"waypoints.json"}] + ] } diff --git a/apps/kitchen/waypoints.html b/apps/kitchen/waypoints.html deleted file mode 100644 index d02260732..000000000 --- a/apps/kitchen/waypoints.html +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - -

List of waypoints

- - - - - - - - - - - - -
NameLat.Long.Actions
-
-

Add a new waypoint

-
-
-
- -
-
- -
-
- -
-
-
-
- -
-
- -
-
-
-
- - - - - - - diff --git a/apps/kitchen/waypoints.json b/apps/kitchen/waypoints.json deleted file mode 100644 index 98a670c0d..000000000 --- a/apps/kitchen/waypoints.json +++ /dev/null @@ -1,20 +0,0 @@ -[ - { - "name":"NONE" - }, - { - "name":"No10", - "lat":51.5032, - "lon":-0.1269 - }, - { - "name":"Stone", - "lat":51.1788, - "lon":-1.8260 - }, - { "name":"WP0" }, - { "name":"WP1" }, - { "name":"WP2" }, - { "name":"WP3" }, - { "name":"WP4" } -] \ No newline at end of file diff --git a/apps/launch/ChangeLog b/apps/launch/ChangeLog index 44866b9f3..955061e19 100644 --- a/apps/launch/ChangeLog +++ b/apps/launch/ChangeLog @@ -14,3 +14,7 @@ Add /*LANG*/ tags for internationalisation 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 diff --git a/apps/launch/app.js b/apps/launch/app.js index 556e61bfd..e9f99d8f5 100644 --- a/apps/launch/app.js +++ b/apps/launch/app.js @@ -1,61 +1,69 @@ -var s = require("Storage"); -var scaleval = 1; -var vectorval = 20; -var font = g.getFonts().includes("12x20") ? "12x20" : "6x8:2"; +{ // 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"; 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; - } + if(settings.font == "Vector"){ + scaleval = vectorval/20; + font = "Vector"+(vectorval).toString(); + } else{ + font = settings.font; + scaleval = (font.split("x")[1])/20; + } } -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};}).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")); +// 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); } - -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) { +let apps = launchCache.apps; +// Now apps list is loaded - render +if (!settings.fullscreen) Bangle.loadWidgets(); - Bangle.drawWidgets(); + +let returnToClock = function() { + // unload everything manually + // ... or we could just call `load();` but it will be slower + Bangle.setUI(); // remove scroller's handling + if (lockTimeout) clearTimeout(lockTimeout); + Bangle.removeListener("lock", lockHandler); + // now load the default clock - just call .bootcde as this has the code already + setTimeout(eval,0,s.read(".bootcde")); } E.showScroller({ h : 64*scaleval, c : apps.length, - draw : drawApp, + 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; @@ -63,24 +71,31 @@ E.showScroller({ E.showMessage(/*LANG*/"App Source\nNot found"); setTimeout(drawMenu, 2000); } else { - E.showMessage(/*LANG*/"Loading..."); load(app.src); } - } + }, + back : returnToClock }); +g.flip(); // force a render before widgets have finished drawing + + // 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(_=>load(), BTN1, {edge:"falling"}); + setWatch(returnToClock, BTN1, {edge:"falling"}); } // 10s of inactivity goes back to clock Bangle.setLocked(false); // unlock initially -var lockTimeout; -Bangle.on("lock", locked => { +let lockTimeout; +let lockHandler = function(locked) { if (lockTimeout) clearTimeout(lockTimeout); lockTimeout = undefined; if (locked) - lockTimeout = setTimeout(_=>load(), 10000); -}); + 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 19ca74e73..d6770524c 100644 --- a/apps/launch/metadata.json +++ b/apps/launch/metadata.json @@ -2,7 +2,7 @@ "id": "launch", "name": "Launcher", "shortName": "Launcher", - "version": "0.14", + "version": "0.18", "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/lcars/ChangeLog b/apps/lcars/ChangeLog index 9a8ac4008..f97ddf540 100644 --- a/apps/lcars/ChangeLog +++ b/apps/lcars/ChangeLog @@ -21,3 +21,4 @@ 0.21: Add custom theming. 0.22: Fix alarm and add build in function for step counting. 0.23: Add warning for low flash memory +0.24: Add ability to disable alarm functionality \ No newline at end of file diff --git a/apps/lcars/lcars.app.js b/apps/lcars/lcars.app.js index e81c0d6f3..06a89a957 100644 --- a/apps/lcars/lcars.app.js +++ b/apps/lcars/lcars.app.js @@ -12,6 +12,7 @@ let settings = { themeColor1BG: "#FF9900", themeColor2BG: "#FF00DC", themeColor3BG: "#0094FF", + disableAlarms: false, }; let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings; for (const key in saved_settings) { @@ -722,12 +723,12 @@ Bangle.on('touch', function(btn, e){ } if(lcarsViewPos == 0){ - if(is_upper){ + if(is_upper && !settings.disableAlarms){ feedback(); increaseAlarm(); drawState(); return; - } if(is_lower){ + } if(is_lower && !settings.disableAlarms){ feedback(); decreaseAlarm(); drawState(); diff --git a/apps/lcars/lcars.settings.js b/apps/lcars/lcars.settings.js index b64feb30e..e4b9b0a78 100644 --- a/apps/lcars/lcars.settings.js +++ b/apps/lcars/lcars.settings.js @@ -13,6 +13,7 @@ themeColor1BG: "#FF9900", themeColor2BG: "#FF00DC", themeColor3BG: "#0094FF", + disableAlarms: false, }; let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings; for (const key in saved_settings) { @@ -102,6 +103,14 @@ settings.themeColor3BG = bg_code[v]; save(); }, - } + }, + 'Disable alarm functionality': { + value: settings.disableAlarms, + format: () => (settings.disableAlarms ? 'Yes' : 'No'), + onchange: () => { + settings.disableAlarms = !settings.disableAlarms; + save(); + }, + }, }); }) diff --git a/apps/lcars/metadata.json b/apps/lcars/metadata.json index 62a1c67db..6533ddd52 100644 --- a/apps/lcars/metadata.json +++ b/apps/lcars/metadata.json @@ -3,7 +3,7 @@ "name": "LCARS Clock", "shortName":"LCARS", "icon": "lcars.png", - "version":"0.23", + "version":"0.24", "readme": "README.md", "supports": ["BANGLEJS2"], "description": "Library Computer Access Retrieval System (LCARS) clock.", diff --git a/apps/limelight/ChangeLog b/apps/limelight/ChangeLog index 9db0e26c5..8fe3a0b2c 100644 --- a/apps/limelight/ChangeLog +++ b/apps/limelight/ChangeLog @@ -1 +1,2 @@ 0.01: first release +0.02: Tell clock widgets to hide. diff --git a/apps/limelight/limelight.app.js b/apps/limelight/limelight.app.js index 20d79deeb..84ded1039 100644 --- a/apps/limelight/limelight.app.js +++ b/apps/limelight/limelight.app.js @@ -10,6 +10,8 @@ * */ +Bangle.setUI('clock'); + g.clear(); const SETTINGS_FILE = "limelight.json"; @@ -259,5 +261,4 @@ Bangle.on('lcdPower',on=>{ } }); -Bangle.setUI('clock'); draw(); diff --git a/apps/limelight/metadata.json b/apps/limelight/metadata.json index 7c3736e1a..e484a2825 100644 --- a/apps/limelight/metadata.json +++ b/apps/limelight/metadata.json @@ -1,7 +1,7 @@ { "id": "limelight", "name": "Limelight", - "version": "0.01", + "version": "0.02", "description": "Simple analogue clock (with configurable fonts) based on the work of @Andreas_Rozek (Simple_Clock)", "icon": "limelight.png", "readme":"README.md", 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/macwatch2/ChangeLog b/apps/macwatch2/ChangeLog index a60193ba7..5eafe64d2 100644 --- a/apps/macwatch2/ChangeLog +++ b/apps/macwatch2/ChangeLog @@ -1,3 +1,5 @@ 0.01: Created first version of the app with numeric date, only works in light mode 0.02: New icon, shimmied date right a bit 0.03: Incorporated improvements from Peer David for accuracy, fix dark mode, widgets run in background +0.04: Changed clock to use 12/24 hour format based on locale +0.05: Tell clock widgets to hide. diff --git a/apps/macwatch2/app.js b/apps/macwatch2/app.js index 3b78d5baf..4556e06ac 100644 --- a/apps/macwatch2/app.js +++ b/apps/macwatch2/app.js @@ -30,15 +30,15 @@ function draw() { g.setFontAlign(0, -1, 0); g.setColor(0,0,0); var d = new Date(); - var da = d.toString().split(" "); - hh = da[4].substr(0,2); - mi = da[4].substr(3,2); + var dt = require("locale").time(d, 1); + var hh = dt.split(":")[0]; + var mm = dt.split(":")[1]; + g.drawString(hh, 52, 65, true); + g.drawString(mm, 132, 65, true); + g.drawString(':', 93,65); dd = ("0"+(new Date()).getDate()).substr(-2); mo = ("0"+((new Date()).getMonth()+1)).substr(-2); yy = ("0"+((new Date()).getFullYear())).substr(-2); - g.drawString(hh, 52, 65, true); - g.drawString(mi, 132, 65, true); - g.drawString(':', 93,65); g.setFontCustom(font, 48, 8, 521); g.drawString(dd + ':' + mo + ':' + yy, 88, 120, true); @@ -57,8 +57,8 @@ Bangle.on('lcdPower',on=>{ } }); +Bangle.setUI("clock"); // Load widgets but hide them Bangle.loadWidgets(); draw(); -Bangle.setUI("clock"); diff --git a/apps/macwatch2/metadata.json b/apps/macwatch2/metadata.json index 09ec01e06..14c48c749 100644 --- a/apps/macwatch2/metadata.json +++ b/apps/macwatch2/metadata.json @@ -2,7 +2,7 @@ "name": "MacWatch2", "shortName":"MacWatch2", "icon": "app.png", - "version":"0.03", + "version":"0.05", "description": "Classic Mac Finder clock", "type": "clock", "tags": "clock", diff --git a/apps/matrixclock/ChangeLog b/apps/matrixclock/ChangeLog index 5d88618bd..02f7d109b 100644 --- a/apps/matrixclock/ChangeLog +++ b/apps/matrixclock/ChangeLog @@ -3,3 +3,5 @@ 0.03: Keep the date from being overwritten, use correct colour from theme for clearing 0.04: Removed "wake LCD on face-up"-feature: A watch-face should not set things like "wake LCD on face-up". 0.05: Added support to other color themes (other then black) +0.06: Added support for 24 hour clock enabled from settings +0.07: Tell clock widgets to hide. diff --git a/apps/matrixclock/README.md b/apps/matrixclock/README.md index a2add957a..01aef6544 100644 --- a/apps/matrixclock/README.md +++ b/apps/matrixclock/README.md @@ -5,10 +5,11 @@ ## Settings Please use the setting->App->Matrix Clock Menu to change the settings -| Setting | Description | -|-----------|--------------------------------------------------------------------------------------------------------------------| -| Color | by default set to **'theme'** to follow the theme colors. Selector also offers a selection of other colour schemes | -| Intensity | Changes the number of matrix streams that are falling | +| Setting | Description | +|-------------|--------------------------------------------------------------------------------------------------------------------| +| Color | By default set to **'theme'** to follow the theme colors. Selector also offers a selection of other colour schemes | +| Time Format | Choose between 12 hour and 24 hour time format | +| Intensity | Changes the number of matrix streams that are falling | ## Colour Themes diff --git a/apps/matrixclock/matrixclock.js b/apps/matrixclock/matrixclock.js index 5477f64fb..9618c3a47 100644 --- a/apps/matrixclock/matrixclock.js +++ b/apps/matrixclock/matrixclock.js @@ -10,9 +10,16 @@ const Locale = require('locale'); const PREFERENCE_FILE = "matrixclock.settings.json"; -const settings = Object.assign({color: "theme", intensity: 'light'}, +const settings = Object.assign({color: "theme", time_format: '12 hour', intensity: 'light'}, require('Storage').readJSON(PREFERENCE_FILE, true) || {}); +var format_time; +if(settings.time_format == '24 hour'){ + format_time = (t) => format_time_24_hour(t); +} else { + format_time = (t) => format_time_12_hour(t); +} + const colors = { 'gray' :[0.5,0.5,0.5], 'green': [0,1.0,0], @@ -247,8 +254,14 @@ function format_date(now){ return Locale.dow(now,1) + " " + format00(now.getDate()); } +function format_time_24_hour(now){ + var time = new Date(now.getTime()); + var hours = time.getHours() ; -function format_time(now){ + return format00(hours) + ":" + format00(time.getMinutes()); +} + +function format_time_12_hour(now){ var time = new Date(now.getTime()); var hours = time.getHours() % 12; if(hours < 1){ @@ -315,11 +328,9 @@ Bangle.on('lcdPower', (on) => { } }); +Bangle.setUI("clock"); g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); startTimers(); -Bangle.setUI("clock"); - - diff --git a/apps/matrixclock/matrixclock.settings.js b/apps/matrixclock/matrixclock.settings.js index 0f625df4d..1f22a045f 100644 --- a/apps/matrixclock/matrixclock.settings.js +++ b/apps/matrixclock/matrixclock.settings.js @@ -1,6 +1,6 @@ (function(back) { const PREFERENCE_FILE = "matrixclock.settings.json"; - var settings = Object.assign({color : "theme", intensity: "light"}, + var settings = Object.assign({color : "theme", time_format: '12 hour', intensity: "light"}, require('Storage').readJSON(PREFERENCE_FILE, true) || {}); console.log("loaded:" + JSON.stringify(settings)); @@ -44,6 +44,7 @@ 'white on red', 'white on blue' ]), + "Time Format": stringInSettings("time_format", ['12 hour','24 hour']), "Intensity": stringInSettings("intensity", ['light', 'medium', 'high']) diff --git a/apps/matrixclock/metadata.json b/apps/matrixclock/metadata.json index 1312210dd..718b878e5 100644 --- a/apps/matrixclock/metadata.json +++ b/apps/matrixclock/metadata.json @@ -1,7 +1,7 @@ { "id": "matrixclock", "name": "Matrix Clock", - "version": "0.05", + "version": "0.07", "description": "inspired by The Matrix, a clock of the same style", "icon": "matrixclock.png", "screenshots": [{"url":"matrix_green_on_black.jpg"}], diff --git a/apps/mclock/ChangeLog b/apps/mclock/ChangeLog index 05b422406..e3b164942 100644 --- a/apps/mclock/ChangeLog +++ b/apps/mclock/ChangeLog @@ -5,3 +5,4 @@ Fix issue where first digit could get stuck going from "2x:xx" to " x:xx" (fix #365) 0.06: Support 12 hour time 0.07: Use Bangle.setUI for button/launcher handling +0.08: Tell clock widgets to hide. diff --git a/apps/mclock/clock-morphing.js b/apps/mclock/clock-morphing.js index f1254860b..bd133206e 100644 --- a/apps/mclock/clock-morphing.js +++ b/apps/mclock/clock-morphing.js @@ -209,6 +209,9 @@ Bangle.on('lcdPower',function(on) { } }); +// Show launcher when button pressed +Bangle.setUI("clock"); + g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); @@ -216,5 +219,3 @@ Bangle.drawWidgets(); timeInterval = setInterval(showTime, 1000); showTime(); -// Show launcher when button pressed -Bangle.setUI("clock"); diff --git a/apps/mclock/metadata.json b/apps/mclock/metadata.json index 513f823a1..a7d56f752 100644 --- a/apps/mclock/metadata.json +++ b/apps/mclock/metadata.json @@ -1,7 +1,7 @@ { "id": "mclock", "name": "Morphing Clock", - "version": "0.07", + "version": "0.08", "description": "7 segment clock that morphs between minutes and hours", "icon": "clock-morphing.png", "type": "clock", diff --git a/apps/messages/ChangeLog b/apps/messages/ChangeLog index c3d63a199..166ff64ae 100644 --- a/apps/messages/ChangeLog +++ b/apps/messages/ChangeLog @@ -58,3 +58,18 @@ 0.43: Add new Icons (Airbnb, warnwetter) 0.44: Separate buzz pattern for incoming calls 0.45: Added new app colors and icons +0.46: Add 'Vibrate Timer' option to set how long to vibrate for, and fix Repeat:off + Fix message removal from widget bar (previously caused exception as .hide has been removed) +0.47: Add new Icons (Nextbike, Mattermost, etc.) +0.48: When getting new message from the clock, only buzz once the messages app is loaded +0.49: Change messages icon (to fit within 24px) and ensure widget renders icons centrally +0.50: Add `getMessages` and `status` functions to library + Option to disable auto-open of messages + Option to make message icons monochrome (not colored) + messages widget buzz now returns a promise +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() +0.52: Fix require("messages").buzz() regression + Fix background color in messages list after one unread message is shown diff --git a/apps/messages/README.md b/apps/messages/README.md index aadd1c304..72a989146 100644 --- a/apps/messages/README.md +++ b/apps/messages/README.md @@ -14,7 +14,9 @@ and `Messages`: * `Vibrate` - This is the pattern of buzzes that should be made when a new message is received * `Vibrate for calls` - This is the pattern of buzzes that should be made when an incoming call is received * `Repeat` - How often should buzzes repeat - the default of 4 means the Bangle will buzz every 4 seconds -* `Unread Timer` - When a new message is received we go into the Messages app. +* `Vibrate Timer` - When a new message is received when in a non-clock app, we display the message icon and +buzz every `Repeat` seconds. This is how long we continue to do that. +* `Unread Timer` - When a new message is received when showing the clock we go into the Messages app. If there is no user input for this amount of time then the app will exit and return to the clock where a ringing bell will be shown in the Widget bar. * `Min Font` - The minimum font size used when displaying messages on the screen. A bigger font @@ -23,7 +25,7 @@ it starts getting clipped. * `Auto-Open Music` - Should the app automatically open when the phone starts playing music? * `Unlock Watch` - Should the app unlock the watch when a new message arrives, so you can touch the buttons at the bottom of the app? * `Flash Icon` - Toggle flashing of the widget icon. -* `Widget messages` - The maximum amount of message icons to show on the widget. +* `Widget messages` - The maximum amount of message icons to show on the widget, or `Hide` the widget completely. ## New Messages @@ -54,6 +56,24 @@ _2. What the notify icon looks like (it's touchable on Bangle.js2!)_ ![](screenshot-notify.gif) +## Events (for app/widget developers) + +When a new message arrives, a `"message"` event is emitted, you can listen for +it like this: + +```js +myMessageListener = Bangle.on("message", (type, message)=>{ + if (message.handled) return; // another app already handled this message + // is one of "text", "call", "alarm", "map", "music", or "clearAll" + if (type === "clearAll") return; // not a message + // see `messages/lib.js` for possible formats + // message.t could be "add", "modify" or "remove" + E.showMessage(`${message.title}\n${message.body}`, `${message.t} ${type} message`); + // You can prevent the default `message` app from loading by setting `message.handled = true`: + message.handled = true; +}); +``` + ## Requests diff --git a/apps/messages/app-newmessage.js b/apps/messages/app-newmessage.js new file mode 100644 index 000000000..328927c70 --- /dev/null +++ b/apps/messages/app-newmessage.js @@ -0,0 +1,5 @@ +/* Called when we have a new message when we're in the clock... +BUZZ_ON_NEW_MESSAGE is set so when messages.app.js loads it knows +that it should buzz */ +global.BUZZ_ON_NEW_MESSAGE = true; +eval(require("Storage").read("messages.app.js")); diff --git a/apps/messages/app.js b/apps/messages/app.js index 6b61740aa..f6226d178 100644 --- a/apps/messages/app.js +++ b/apps/messages/app.js @@ -48,14 +48,13 @@ we should start a timeout for settings.unreadTimeout to return to the clock. */ var unreadTimeout; /// List of all our messages -var MESSAGES = require("Storage").readJSON("messages.json",1)||[]; +var MESSAGES = require("messages").getMessages(); if (!Array.isArray(MESSAGES)) MESSAGES=[]; var onMessagesModified = function(msg) { // TODO: if new, show this new one if (msg && msg.id!=="music" && msg.new && active!="map" && !((require('Storage').readJSON('setting.json', 1) || {}).quiet)) { - if (WIDGETS["messages"]) WIDGETS["messages"].buzz(msg.src); - else Bangle.buzz(); + require("messages").buzz(msg.src); } if (msg && msg.id=="music") { if (msg.state && msg.state!="play") openMusic = false; // no longer playing music to go back to @@ -69,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]; @@ -286,6 +284,7 @@ function showMessage(msgid) { } } function goBack() { + layout = undefined; msg.new = false; saveMessages(); // read mail cancelReloadTimeout(); // don't auto-reload to clock now checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:0,openMusic:openMusic}); @@ -353,8 +352,18 @@ function checkMessages(options) { // we have >0 messages var newMessages = MESSAGES.filter(m=>m.new&&m.id!="music"); // If we have a new message, show it - if (options.showMsgIfUnread && newMessages.length) - return showMessage(newMessages[0].id); + if (options.showMsgIfUnread && newMessages.length) { + showMessage(newMessages[0].id); + // buzz after showMessage, so being busy during layout doesn't affect the buzz pattern + if (global.BUZZ_ON_NEW_MESSAGE) { + // this is set if we entered the messages app by loading `messages.new.js` + // ... but only buzz the first time we view a new message + global.BUZZ_ON_NEW_MESSAGE = false; + // messages.buzz respects quiet mode - no need to check here + require("messages").buzz(newMessages[0].src); + } + return; + } // no new messages: show playing music? (only if we have playing music to show) if (options.openMusic && MESSAGES.some(m=>m.id=="music" && m.track && m.state=="play")) return showMessage('music'); @@ -369,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 50df70ea8..0188342ee 100644 --- a/apps/messages/lib.js +++ b/apps/messages/lib.js @@ -8,15 +8,11 @@ function openMusic() { /* Push a new message onto messages queue, event is: {t:"add",id:int, src,title,subject,body,sender,tel, important:bool, new:bool} {t:"add",id:int, id:"music", state, artist, track, etc} // add new - {t:"remove-",id:int} // remove + {t:"remove",id:int} // remove {t:"modify",id:int, title:string} // modified */ exports.pushMessage = function(event) { - var messages, inApp = "undefined"!=typeof MESSAGES; - if (inApp) - messages = MESSAGES; // we're in an app that has already loaded messages - else // no app - load messages - messages = require("Storage").readJSON("messages.json",1)||[]; + var messages = exports.getMessages(); // now modify/delete as appropriate var mIdx = messages.findIndex(m=>m.id==event.id); if (event.t=="remove") { @@ -35,99 +31,173 @@ exports.pushMessage = function(event) { else Object.assign(messages[mIdx], event); if (event.id=="music" && messages[mIdx].state=="play") { messages[mIdx].new = true; // new track, or playback (re)started + type = 'music'; } } require("Storage").writeJSON("messages.json",messages); + var message = mIdx<0 ? {id:event.id, t:'remove'} : messages[mIdx]; // if in app, process immediately - if (inApp) return onMessagesModified(mIdx<0 ? {id:event.id} : messages[mIdx]); - // if we've removed the last new message, hide the widget - if (event.t=="remove" && !messages.some(m=>m.new)) { - if (global.WIDGETS && WIDGETS.messages) WIDGETS.messages.hide(); + if ("undefined"!=typeof MESSAGES) return onMessagesModified(message); + // emit message event + var type = 'text'; + if (["call", "music", "map"].includes(message.id)) type = message.id; + if (message.src && message.src.toLowerCase().startsWith("alarm")) type = "alarm"; + Bangle.emit("message", type, message); + // update the widget icons shown + if (global.WIDGETS && WIDGETS.messages) WIDGETS.messages.update(messages,true); + var handleMessage = () => { // if no new messages now, make sure we don't load the messages app - if (exports.messageTimeout && !messages.some(m=>m.new)) + if (event.t=="remove" && exports.messageTimeout && !messages.some(m => m.new)) { clearTimeout(exports.messageTimeout); - } - // ok, saved now - if (event.id=="music" && Bangle.CLOCK && messages[mIdx].new && openMusic()) { - // just load the app to display music: no buzzing - load("messages.app.js"); - } else if (event.t!="add") { - // we only care if it's new - return; - } else if(event.new == false) { - return; - } - // otherwise load messages/show widget - var loadMessages = Bangle.CLOCK || event.important; - var quiet = (require('Storage').readJSON('setting.json',1)||{}).quiet; - var appSettings = require('Storage').readJSON('messages.settings.json',1)||{}; - var unlockWatch = appSettings.unlockWatch; - var quietNoAutOpn = appSettings.quietNoAutOpn; - delete appSettings; - // don't auto-open messages in quiet mode if quietNoAutOpn is true - if(quiet && quietNoAutOpn) { - loadMessages = false; - } - // first, buzz - if (!quiet && loadMessages && global.WIDGETS && WIDGETS.messages){ - WIDGETS.messages.buzz(event.src); - if(unlockWatch != false){ - Bangle.setLocked(false); - Bangle.setLCDPower(1); // turn screen on - } - } - // after a delay load the app, to ensure we have all the messages - if (exports.messageTimeout) clearTimeout(exports.messageTimeout); - exports.messageTimeout = setTimeout(function() { - exports.messageTimeout = undefined; - // if we're in a clock or it's important, go straight to messages app - if (loadMessages){ - return load("messages.app.js"); + delete exports.messageTimeout; } - if (!quiet && (!global.WIDGETS || !WIDGETS.messages)) return Bangle.buzz(); // no widgets - just buzz to let someone know - if (global.WIDGETS && WIDGETS.messages) WIDGETS.messages.update(messages); - }, 500); + // ok, saved now + if (event.id=="music" && Bangle.CLOCK && messages[mIdx].new && openMusic()) { + // just load the app to display music: no buzzing + load("messages.app.js"); + } else if (event.t!="add") { + // we only care if it's new + return; + } else if (event.new==false) { + return; + } + // otherwise load messages/show widget + var loadMessages = Bangle.CLOCK || event.important; + var quiet = (require('Storage').readJSON('setting.json', 1) || {}).quiet; + var appSettings = require('Storage').readJSON('messages.settings.json', 1) || {}; + var unlockWatch = appSettings.unlockWatch; + // don't auto-open messages in quiet mode if quietNoAutOpn is true + if ((quiet && appSettings.quietNoAutOpn) || appSettings.noAutOpn) + loadMessages = false; + delete appSettings; + // after a delay load the app, to ensure we have all the messages + if (exports.messageTimeout) clearTimeout(exports.messageTimeout); + exports.messageTimeout = setTimeout(function() { + exports.messageTimeout = undefined; + // if we're in a clock or it's important, go straight to messages app + if (loadMessages) { + if (!quiet && unlockWatch) { + Bangle.setLocked(false); + Bangle.setLCDPower(1); // turn screen on + } + // we will buzz when we enter the messages app + return load("messages.new.js"); + } + if (global.WIDGETS && WIDGETS.messages) WIDGETS.messages.update(messages); + exports.buzz(message.src); + }, 500); + }; + setTimeout(()=>{ + if (!message.handled) handleMessage(); + },0); } /// Remove all messages -exports.clearAll = function(event) { - var messages, inApp = "undefined"!=typeof MESSAGES; - if (inApp) { +exports.clearAll = function() { + if ("undefined"!= typeof MESSAGES) { // we're in a messages app, clear that as well MESSAGES = []; - messages = MESSAGES; // we're in an app that has already loaded messages - } else // no app - empty messages - messages = []; - // Save all messages - require("Storage").writeJSON("messages.json",messages); - // update app if in app - if (inApp) return onMessagesModified(); + } + // Clear all messages + require("Storage").writeJSON("messages.json", []); // if we have a widget, update it if (global.WIDGETS && WIDGETS.messages) - WIDGETS.messages.update(messages); + WIDGETS.messages.update([]); + // let message listeners know + Bangle.emit("message", "clearAll", {}); // guarantee listeners an object as `message` + // clearAll cannot be marked as "handled" + // update app if in app + if ("function"== typeof onMessagesModified) onMessagesModified(); } +/** + * @returns {array} All messages + */ +exports.getMessages = function() { + if ("undefined"!=typeof MESSAGES) return MESSAGES; // loaded/managed by app + return require("Storage").readJSON("messages.json",1)||[]; +} + +/** + * Check if there are any messages + * @returns {string} "new"/"old"/"none" + */ + exports.status = function() { + try { + let status= "none"; + for(const m of exports.getMessages()) { + if (["music", "map"].includes(m.id)) continue; + if (m.new) return "new"; + status = "old"; + } + return status; + } catch(e) { + return "none"; // don't bother e.g. the widget with errors + } +}; + +/** + * Start buzzing for new message + * @param {string} msgSrc Message src to buzz for + * @return {Promise} Resolves when initial buzz finishes (there might be repeat buzzes later) + */ +exports.buzz = function(msgSrc) { + exports.stopBuzz(); // cancel any previous buzz timeouts + if ((require('Storage').readJSON('setting.json',1)||{}).quiet) return Promise.resolve(); // never buzz during Quiet Mode + + var pattern; + if (msgSrc && msgSrc.toLowerCase() === "phone") { + // special vibration pattern for incoming calls + pattern = (require('Storage').readJSON("messages.settings.json", true) || {}).vibrateCalls; + } else { + pattern = (require('Storage').readJSON("messages.settings.json", true) || {}).vibrate; + } + if (pattern === undefined) { pattern = ":"; } // pattern may be "", so we can't use || ":" here + if (!pattern) return Promise.resolve(); + + var repeat = (require('Storage').readJSON("messages.settings.json", true) || {}).repeat; + if (repeat===undefined) repeat=4; // repeat may be zero + if (repeat) { + 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.stopBuzz, vibrateTimeout*1000); + } + return require("buzz").pattern(pattern); +}; +/** + * Stop buzzing + */ +exports.stopBuzz = function() { + if (exports.buzzTimeout) clearTimeout(exports.buzzTimeout); + delete exports.buzzTimeout; + if (exports.stopTimeout) clearTimeout(exports.stopTimeout); + delete exports.stopTimeout; +}; + exports.getMessageImage = function(msg) { /* - * icons should be 24x24px with 1bpp colors and 'Transparency to Color' + * icons should be 24x24px or less with 1bpp colors and 'Transparency to Color' * http://www.espruino.com/Image+Converter */ if (msg.img) return atob(msg.img); - var s = (msg.src||"").toLowerCase(); + const s = (("string"=== typeof msg) ? msg : (msg.src || "")).toLowerCase(); if (s=="airbnb") return atob("GBgBAAAAAAAAAAAAADwAAH4AAGYAAMMAAIEAAYGAAYGAAzzAA2bABmZgBmZgDGYwDDwwCDwQCBgQDDwwB+fgA8PAAAAAAAAAAAAA"); if (s=="alarm" || s =="alarmclockreceiver") return atob("GBjBAP////8AAAAAAAACAEAHAOAefng5/5wTgcgHAOAOGHAMGDAYGBgYGBgYGBgYGBgYDhgYBxgMATAOAHAHAOADgcAB/4AAfgAAAAAAAAA="); if (s=="bibel") return atob("GBgBAAAAA//wD//4D//4H//4H/f4H/f4H+P4H4D4H4D4H/f4H/f4H/f4H/f4H/f4H//4H//4H//4GAAAEAAAEAAACAAAB//4AAAA"); if (s=="bring") return atob("GBgBAAAAAAAAAAAAAAAAAHwAAFoAAf+AA/+AA/+AA/+AA/eAA+eAA0+AAx+AA7+AA/+AA//AA/+AAf8AAAIAAAAAAAAAAAAAAAAA"); - if (s=="calendar") return atob("GBiBAAAAAAAAAAAAAA//8B//+BgAGBgAGBgAGB//+B//+B//+B9m2B//+B//+Btm2B//+B//+Btm+B//+B//+A//8AAAAAAAAAAAAA=="); + if (s=="calendar" || s=="etar") return atob("GBiBAAAAAAAAAAAAAA//8B//+BgAGBgAGBgAGB//+B//+B//+B9m2B//+B//+Btm2B//+B//+Btm+B//+B//+A//8AAAAAAAAAAAAA=="); if (s=="corona-warn") return atob("GBgBAAAAABwAAP+AAf/gA//wB/PwD/PgDzvAHzuAP8EAP8AAPAAAPMAAP8AAH8AAHzsADzuAB/PAB/PgA//wAP/gAH+AAAwAAAAA"); if (s=="discord") return atob("GBgBAAAAAAAAAAAAAIEABwDgDP8wH//4H//4P//8P//8P//8Pjx8fhh+fzz+f//+f//+e//ePH48HwD4AgBAAAAAAAAAAAAAAAAA"); if (s=="facebook" || s=="messenger") return atob("GBiBAAAAAAAAAAAYAAD/AAP/wAf/4A/48A/g8B/g+B/j+B/n+D/n/D8A/B8A+B+B+B/n+A/n8A/n8Afn4APnwADnAAAAAAAAAAAAAA=="); if (s=="gmx") return atob("GBgBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEJmfmd8Zuc85v847/88Z9s8fttmHIHiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); - if (s=="google home") return atob("GBiCAAAAAAAAAAAAAAAAAAAAAoAAAAAACqAAAAAAKqwAAAAAqroAAAACquqAAAAKq+qgAAAqr/qoAACqv/6qAAKq//+qgA6r///qsAqr///6sAqv///6sAqv///6sAqv///6sA6v///6sA6v///qsA6qqqqqsA6qqqqqsA6qqqqqsAP7///vwAAAAAAAAAAAAAAAAA=="); - if (s=="hangouts") return atob("FBaBAAH4AH/gD/8B//g//8P//H5n58Y+fGPnxj5+d+fmfj//4//8H//B//gH/4A/8AA+AAHAABgAAAA="); + if (s=="google") return atob("GBiBAAAAAAD/AAP/wAf/4A/D4B8AwDwAADwAAHgAAHgAAHAAAHAH/nAH/nAH/ngH/ngAHjwAPDwAfB8A+A/D8Af/4AP/wAD/AAAAAA=="); + if (s=="google home") return atob("GBiCAAAAAAAAAAAAAAAAAAAAAoAAAAAACqAAAAAAKqwAAAAAqroAAAACquqAAAAKq+qgAAAqr/qoAACqv/6qAAKq//+qgA6r///qsAqr///6sAqv///6sAqv///6sAqv///6sA6v///6sA6v///qsA6qqqqqsA6qqqqqsA6qqqqqsAP7///vwAAAAAAAAAAAAAAAAA=="); // 2 bit unpaletted if (s=="home assistant") return atob("FhaBAAAAAADAAAeAAD8AAf4AD/3AfP8D7fwft/D/P8ec572zbzbNsOEhw+AfD8D8P4fw/z/D/P8P8/w/z/AAAAA="); if (s=="instagram") return atob("GBiBAAAAAAAAAAAAAAAAAAP/wAYAYAwAMAgAkAh+EAjDEAiBEAiBEAiBEAiBEAjDEAh+EAgAEAwAMAYAYAP/wAAAAAAAAAAAAAAAAA=="); if (s=="kalender") return atob("GBgBBgBgBQCgff++RQCiRgBiQAACf//+QAACQAACR//iRJkiRIEiR//iRNsiRIEiRJkiR//iRIEiRIEiR//iQAACQAACf//+AAAA"); if (s=="lieferando") return atob("GBgBABgAAH5wAP9wAf/4A//4B//4D//4H//4P/88fV8+fV4//V4//Vw/HVw4HVw4HBg4HBg4HBg4HDg4Hjw4Hj84Hj44Hj44Hj44"); + if (s=="mattermost") return atob("GBgBAAAAAPAAA+EAB4MADgcYHAcYOA8MOB8OeD8GcD8GcH8GcD8HcD8HeBwHeAAOfAAOfgAePwA8P8D8H//4D//wB//gAf/AAH4A"); if (s=="n26") return atob("GBgBAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAOIAAOIAAPIAANoAANoAAM4AAMYAAMYAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAA"); + if (s=="nextbike") return atob("GBgBAAAAAAAAAAAAAAAAAAAAAACAfgDAPwDAP4HAH4N4H8f8D82GMd8CMDsDMGMDMGGGGMHOD4D8AAAAAAAAAAAAAAAAAAAAAAAA"); if (s=="nina") return atob("GBgBAAAABAAQCAAICAAIEAAEEgAkJAgSJBwSKRxKSj4pUn8lVP+VVP+VUgAlSgApKQBKJAASJAASEgAkEAAECAAICAAIBAAQAAAA"); if (s=="outlook mail") return atob("HBwBAAAAAAAAAAAIAAAfwAAP/gAB/+AAP/5/A//v/D/+/8P/7/g+Pv8Dye/gPd74w5znHDnOB8Oc4Pw8nv/Dwe/8Pj7/w//v/D/+/8P/7/gf/gAA/+AAAfwAAACAAAAAAAAAAAA="); if (s=="paypal") return atob("GBgBAAAAAAAAAAAAAf+AAf/AAf/gA//gA//gA//wA//wA//wA//wB//wB//wB//gB/+AB/gAB/gAB/gAAPgAAPgAAAAAAAAAAAAA"); @@ -141,23 +211,27 @@ exports.getMessageImage = function(msg) { if (s=="teams") return atob("GBgBAAAAAAAAAAQAAB4AAD8IAA8cP/M+f/scf/gIeDgAfvvefvvffvvffvvffvvff/vff/veP/PeAA/cAH/AAD+AAD8AAAQAAAAA"); if (s=="telegram" || s=="telegram foss") return atob("GBiBAAAAAAAAAAAAAAAAAwAAHwAA/wAD/wAf3gD/Pgf+fh/4/v/z/P/H/D8P/Acf/AM//AF/+AF/+AH/+ADz+ADh+ADAcAAAMAAAAA=="); if (s=="threema") return atob("GBjB/4Yx//8AAAAAAAAAAAAAfgAB/4AD/8AH/+AH/+AP//AP2/APw/APw/AHw+AH/+AH/8AH/4AH/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="); - if (s=="to do") return atob("GBgBAAAAAAAAAAAwAAB4AAD8AAH+AAP/DAf/Hg//Px/+f7/8///4///wf//gP//AH/+AD/8AB/4AA/wAAfgAAPAAAGAAAAAAAAAA"); + if (s=="to do" || s=="opentasks") return atob("GBgBAAAAAAAAAAAwAAB4AAD8AAH+AAP/DAf/Hg//Px/+f7/8///4///wf//gP//AH/+AD/8AB/4AA/wAAfgAAPAAAGAAAAAAAAAA"); if (s=="twitch") return atob("GBgBH//+P//+P//+eAAGeAAGeAAGeDGGeDOGeDOGeDOGeDOGeDOGeDOGeAAOeAAOeAAcf4/4f5/wf7/gf//Af/+AA/AAA+AAAcAA"); if (s=="twitter") return atob("GhYBAABgAAB+JgA/8cAf/ngH/5+B/8P8f+D///h///4f//+D///g///wD//8B//+AP//gD//wAP/8AB/+AB/+AH//AAf/AAAYAAA"); if (s=="warnapp") return atob("GBgBAAAAAAAAAAAAAH4AAP8AA//AA//AD//gP//gf//4f//+/+P+/8H//8n//4n/fxh/fzg+Pj88Dn44AA4AAAwAAAwAAAgAAAAA"); if (s=="whatsapp") return atob("GBiBAAB+AAP/wAf/4A//8B//+D///H9//n5//nw//vw///x///5///4///8e//+EP3/APn/wPn/+/j///H//+H//8H//4H//wMB+AA=="); if (s=="wordfeud") return atob("GBgCWqqqqqqlf//////9v//////+v/////++v/////++v8///Lu+v8///L++v8///P/+v8v//P/+v9v//P/+v+fx/P/+v+Pk+P/+v/PN+f/+v/POuv/+v/Ofdv/+v/NvM//+v/I/Y//+v/k/k//+v/i/w//+v/7/6//+v//////+v//////+f//////9Wqqqqqql"); - if (s=="youtube") return atob("GBgBAAAAAAAAAAAAAAAAAf8AH//4P//4P//8P//8P5/8P4/8f4P8f4P8P4/8P5/8P//8P//8P//4H//4Af8AAAAAAAAAAAAAAAAA"); + if (s=="youtube" || s=="newpipe") return atob("GBgBAAAAAAAAAAAAAAAAAf8AH//4P//4P//8P//8P5/8P4/8f4P8f4P8P4/8P5/8P//8P//8P//4H//4Af8AAAAAAAAAAAAAAAAA"); if (msg.id=="music") return atob("FhaBAH//+/////////////h/+AH/4Af/gB/+H3/7/f/v9/+/3/7+f/vB/w8H+Dwf4PD/x/////////////3//+A="); // if (s=="sms message" || s=="mail" || s=="gmail") // .. default icon (below) - return atob("HBKBAD///8H///iP//8cf//j4//8f5//j/x/8//j/H//H4//4PB//EYj/44HH/Hw+P4//8fH//44///xH///g////A=="); + return atob("FhKBAH//+P//yf/+c//z5/+fz/z/n+f/Pz/+ef/8D///////////////////////f//4///A"); }; exports.getMessageImageCol = function(msg,def) { + let iconColorMode = (require('Storage').readJSON("messages.settings.json", 1) || {}).iconColorMode; + if (iconColorMode == 'mono') + return g.theme.fg; + const s = (("string"=== typeof msg) ? msg : (msg.src || "")).toLowerCase(); return { // generic colors, using B2-safe colors + // DO NOT USE BLACK OR WHITE HERE, just leave the declaration out and then the theme's fg color will be used "airbnb": "#f00", - "alarm": "#fff", "mail": "#ff0", "music": "#f0f", "phone": "#0f0", @@ -167,17 +241,22 @@ exports.getMessageImageCol = function(msg,def) { "bibel": "#54342c", "bring": "#455a64", "discord": "#738adb", + "etar": "#36a18b", "facebook": "#4267b2", "gmail": "#ea4335", "gmx": "#1c449b", + "google": "#4285F4", "google home": "#fbbc05", - "hangouts": "#1ba261", - "home assistant": "#fff", // ha-blue is #41bdf5, but that's the background +// "home assistant": "#41bdf5", // ha-blue is #41bdf5, but that's the background "instagram": "#dd2a7b", "lieferando": "#ee5c00", "messenger": "#0078ff", + "mattermost": "#00f", "n26": "#36a18b", + "nextbike": "#00f", + "newpipe": "#f00", "nina": "#e57004", + "opentasks": "#409f8f", "outlook mail": "#0072c6", "paypal": "#003087", "post & dhl": "#f2c101", @@ -189,12 +268,11 @@ exports.getMessageImageCol = function(msg,def) { "teams": "#464eb8", "telegram": "#0088cc", "telegram foss": "#0088cc", - "threema": "#000", "to do": "#3999e5", "twitch": "#6441A4", "twitter": "#1da1f2", "whatsapp": "#4fce5d", "wordfeud": "#e7d3c7", "youtube": "#f00", - }[(msg.src||"").toLowerCase()]||(def !== undefined?def:g.theme.fg); + }[s]||(def !== undefined?def:g.theme.fg); }; diff --git a/apps/messages/metadata.json b/apps/messages/metadata.json index c514e5256..a31c21e03 100644 --- a/apps/messages/metadata.json +++ b/apps/messages/metadata.json @@ -1,7 +1,7 @@ { "id": "messages", "name": "Messages", - "version": "0.45", + "version": "0.52", "description": "App to display notifications from iOS and Gadgetbridge/Android", "icon": "app.png", "type": "app", @@ -10,6 +10,7 @@ "readme": "README.md", "storage": [ {"name":"messages.app.js","url":"app.js"}, + {"name":"messages.new.js","url":"app-newmessage.js"}, {"name":"messages.settings.js","url":"settings.js"}, {"name":"messages.img","url":"app-icon.js","evaluate":true}, {"name":"messages.wid.js","url":"widget.js"}, diff --git a/apps/messages/settings.js b/apps/messages/settings.js index bcd227ece..09c9db455 100644 --- a/apps/messages/settings.js +++ b/apps/messages/settings.js @@ -1,11 +1,15 @@ (function(back) { + const iconColorModes = ['color', 'mono']; + function settings() { let settings = require('Storage').readJSON("messages.settings.json", true) || {}; if (settings.vibrate===undefined) settings.vibrate=":"; if (settings.vibrateCalls===undefined) settings.vibrateCalls=":"; if (settings.repeat===undefined) settings.repeat=4; + if (settings.vibrateTimeout===undefined) settings.vibrateTimeout=60; if (settings.unreadTimeout===undefined) settings.unreadTimeout=60; if (settings.maxMessages===undefined) settings.maxMessages=3; + if (settings.iconColorMode === undefined) settings.iconColorMode = iconColorModes[0]; settings.unlockWatch=!!settings.unlockWatch; settings.openMusic=!!settings.openMusic; settings.maxUnreadTimeout=240; @@ -29,6 +33,12 @@ format: v => v?v+"s":/*LANG*/"Off", onchange: v => updateSetting("repeat", v) }, + /*LANG*/'Vibrate timer': { + value: settings().vibrateTimeout, + min: 0, max: settings().maxUnreadTimeout, step : 10, + format: v => v?v+"s":/*LANG*/"Off", + onchange: v => updateSetting("vibrateTimeout", v) + }, /*LANG*/'Unread timer': { value: settings().unreadTimeout, min: 0, max: settings().maxUnreadTimeout, step : 10, @@ -57,10 +67,21 @@ value: !!settings().quietNoAutOpn, onchange: v => updateSetting("quietNoAutOpn", v) }, + /*LANG*/'Disable auto-open': { + value: !!settings().noAutOpn, + onchange: v => updateSetting("noAutOpn", v) + }, /*LANG*/'Widget messages': { value:0|settings().maxMessages, - min: 1, max: 5, + min: 0, max: 5, + format: v => v ? v :/*LANG*/"Hide", onchange: v => updateSetting("maxMessages", v) + }, + /*LANG*/'Icon color mode': { + value: Math.max(0,iconColorModes.indexOf(settings().iconColorMode)), + min: 0, max: iconColorModes.length - 1, + format: v => iconColorModes[v], + onchange: v => updateSetting("iconColorMode", iconColorModes[v]) } }; E.showMenu(mainmenu); diff --git a/apps/messages/widget.js b/apps/messages/widget.js index c8ae8d570..c8d132f82 100644 --- a/apps/messages/widget.js +++ b/apps/messages/widget.js @@ -1,17 +1,13 @@ (() => { - -function getMessages() { - if ("undefined"!=typeof MESSAGES) return MESSAGES; - return require("Storage").readJSON("messages.json",1)||[]; -} +if ((require('Storage').readJSON("messages.settings.json", true) || {}).maxMessages===0) return; function filterMessages(msgs) { return msgs.filter(msg => msg.new && msg.id != "music") + .map(m => m.src) // we only need this for icon/color .filter((msg, i, arr) => arr.findIndex(nmsg => msg.src == nmsg.src) == i); } -WIDGETS["messages"]={area:"tl", width:0, iconwidth:24, -draw:function(recall) { +WIDGETS["messages"]={area:"tl", width:0, draw:function(recall) { // If we had a setTimeout queued from the last time we were called, remove it if (WIDGETS["messages"].i) { clearTimeout(WIDGETS["messages"].i); @@ -19,16 +15,14 @@ draw:function(recall) { } Bangle.removeListener('touch', this.touch); if (!this.width) return; - var c = (Date.now()-this.t)/1000; - let settings = require('Storage').readJSON("messages.settings.json", true) || {}; - if (settings.flash===undefined) settings.flash = true; + let settings = Object.assign({flash:true, maxMessages:3},require('Storage').readJSON("messages.settings.json", true) || {}); if (recall !== true || settings.flash) { var msgsShown = E.clip(this.msgs.length, 0, settings.maxMessages); g.reset().clearRect(this.x, this.y, this.x+this.width, this.y+23); for(let i = 0;i < msgsShown;i++) { const msg = this.msgs[i]; const colors = [g.theme.bg, g.setColor(require("messages").getMessageImageCol(msg)).getColor()]; - if (settings.flash && (c&1)) { + if (settings.flash && ((Date.now()/1000)&1)) { if (colors[1] == g.theme.fg) { colors.reverse(); } else { @@ -36,51 +30,26 @@ draw:function(recall) { } } g.setColor(colors[1]).setBgColor(colors[0]); - g.drawImage(i == (settings.maxMessages - 1) && msgs.length > settings.maxMessages ? atob("GBgBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH4H4H4H4H4H4H4H4H4H4H4H4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") : require("messages").getMessageImage(msg), this.x + i * this.iconwidth, this.y - 1); + // draw the icon, or '...' if too many messages + g.drawImage(i == (settings.maxMessages - 1) && this.msgs.length > settings.maxMessages ? atob("EASBAGGG88/zz2GG") : require("messages").getMessageImage(msg), + this.x + 12 + i * 24, this.y + 12, {rotate:0/*force centering*/}); } } - if (settings.repeat===undefined) settings.repeat = 4; - if (c<120 && (Date.now()-this.l)>settings.repeat*1000) { - this.l = Date.now(); - WIDGETS["messages"].buzz(); // buzz every 4 seconds - } WIDGETS["messages"].i=setTimeout(()=>WIDGETS["messages"].draw(true), 1000); if (process.env.HWVERSION>1) Bangle.on('touch', this.touch); -},update:function(rawMsgs, quiet) { - const settings = require('Storage').readJSON("messages.settings.json", true) || {}; - msgs = filterMessages(rawMsgs); - if (msgs.length === 0) { - delete WIDGETS["messages"].t; - delete WIDGETS["messages"].l; - } else { - WIDGETS["messages"].t=Date.now(); // first time - WIDGETS["messages"].l=Date.now()-10000; // last buzz - if (quiet) WIDGETS["messages"].t -= 500000; // if quiet, set last time in the past so there is no buzzing - } - WIDGETS["messages"].width=this.iconwidth * E.clip(msgs.length, 0, settings.maxMessages); - WIDGETS["messages"].msgs = msgs; +},update:function(rawMsgs) { + const settings = Object.assign({maxMessages:3},require('Storage').readJSON("messages.settings.json", true) || {}); + this.msgs = filterMessages(rawMsgs); + this.width = 24 * E.clip(this.msgs.length, 0, settings.maxMessages); Bangle.drawWidgets(); -},buzz:function(msgSrc) { - if ((require('Storage').readJSON('setting.json',1)||{}).quiet) return; // never buzz during Quiet Mode - var pattern; - if (msgSrc != undefined && msgSrc.toLowerCase() == "phone") { - // special vibration pattern for incoming calls - pattern = (require('Storage').readJSON("messages.settings.json", true) || {}).vibrateCalls; - } else { - pattern = (require('Storage').readJSON("messages.settings.json", true) || {}).vibrate; - } - if (pattern === undefined) { pattern = ":"; } // pattern may be "", so we can't use || ":" here - require("buzz").pattern(pattern); },touch:function(b,c) { var w=WIDGETS["messages"]; - if (!w||!w.width||c.xw.x+w.width||c.yw.y+w.iconwidth) return; + if (!w||!w.width||c.xw.x+w.width||c.yw.y+24) return; load("messages.app.js"); }}; /* We might have returned here if we were in the Messages app for a -message but then the watch was never viewed. In that case we don't -want to buzz but should still show that there are unread messages. */ +message but then the watch was never viewed. */ if (global.MESSAGES===undefined) - WIDGETS["messages"].update(getMessages(), true); - + WIDGETS["messages"].update(require("messages").getMessages()); })(); diff --git a/apps/miclock/ChangeLog b/apps/miclock/ChangeLog index e92bad2e3..d1ac3e388 100644 --- a/apps/miclock/ChangeLog +++ b/apps/miclock/ChangeLog @@ -2,3 +2,4 @@ 0.03: Localization 0.04: move jshint to the top 0.05: Use Bangle.setUI for button/launcher handling +0.06: Tell clock widgets to hide. diff --git a/apps/miclock/clock-mixed.js b/apps/miclock/clock-mixed.js index b3d6bea8d..cb3235406 100644 --- a/apps/miclock/clock-mixed.js +++ b/apps/miclock/clock-mixed.js @@ -77,11 +77,13 @@ Bangle.on('lcdPower', function(on) { drawMixedClock(); }); +// Show launcher when button pressed +Bangle.setUI("clock"); + g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); setInterval(drawMixedClock, 5E3); drawMixedClock(); -// Show launcher when button pressed -Bangle.setUI("clock"); + diff --git a/apps/miclock/metadata.json b/apps/miclock/metadata.json index 6eece46b0..2c216dc33 100644 --- a/apps/miclock/metadata.json +++ b/apps/miclock/metadata.json @@ -1,7 +1,7 @@ { "id": "miclock", "name": "Mixed Clock", - "version": "0.05", + "version": "0.06", "description": "A mix of analog and digital Clock", "icon": "clock-mixed.png", "type": "clock", diff --git a/apps/minimal_clock/ChangeLog b/apps/minimal_clock/ChangeLog index c72634017..54ee389e3 100644 --- a/apps/minimal_clock/ChangeLog +++ b/apps/minimal_clock/ChangeLog @@ -1,2 +1,3 @@ ... 0.03: First update with ChangeLog Added +0.04: Tell clock widgets to hide. diff --git a/apps/minimal_clock/app.js b/apps/minimal_clock/app.js index d78790347..47eca3c66 100644 --- a/apps/minimal_clock/app.js +++ b/apps/minimal_clock/app.js @@ -3,6 +3,7 @@ let outerRadius = Math.min(CenterX,CenterY) * 0.9; + Bangle.setUI('clock'); Bangle.loadWidgets(); /**** updateClockFaceSize ****/ @@ -225,6 +226,5 @@ } }); - Bangle.loadWidgets(); - Bangle.setUI('clock'); + Bangle.loadWidgets(); diff --git a/apps/minimal_clock/metadata.json b/apps/minimal_clock/metadata.json index 1702d97a9..3089780ce 100644 --- a/apps/minimal_clock/metadata.json +++ b/apps/minimal_clock/metadata.json @@ -1,7 +1,7 @@ { "id": "minimal_clock", "name": "Minimal Analog Clock", "shortName":"Minimal Clock", - "version":"0.03", + "version":"0.04", "description": "a minimal analog clock - just with some hands and no clock face", "icon": "app-icon.png", "type": "clock", diff --git a/apps/minionclk/ChangeLog b/apps/minionclk/ChangeLog index a8b6efc81..5949a786d 100644 --- a/apps/minionclk/ChangeLog +++ b/apps/minionclk/ChangeLog @@ -3,3 +3,4 @@ 0.03: Fixed rendering for Espruino v2.06 0.04: Fixed overlapped rendering of dates 0.05: Use Bangle.setUI for button/launcher handling +0.06: Tell clock widgets to hide. diff --git a/apps/minionclk/app b/apps/minionclk/app new file mode 100644 index 000000000..e69de29bb diff --git a/apps/minionclk/app.js b/apps/minionclk/app.js index 9648e3d89..c61f8d3bf 100644 --- a/apps/minionclk/app.js +++ b/apps/minionclk/app.js @@ -78,8 +78,10 @@ Bangle.on('lcdPower', (on) => { } }); +// Show launcher when button pressed +Bangle.setUI("clock"); + Bangle.loadWidgets(); startDrawing(); -// Show launcher when button pressed -Bangle.setUI("clock"); + diff --git a/apps/minionclk/metadata.json b/apps/minionclk/metadata.json index 44fc2a82d..4df2ddc6b 100644 --- a/apps/minionclk/metadata.json +++ b/apps/minionclk/metadata.json @@ -1,7 +1,7 @@ { "id": "minionclk", "name": "Minion clock", - "version": "0.05", + "version": "0.06", "description": "Minion themed clock.", "icon": "minionclk.png", "type": "clock", diff --git a/apps/mysticclock/ChangeLog b/apps/mysticclock/ChangeLog index b486a29a1..cd91abe00 100644 --- a/apps/mysticclock/ChangeLog +++ b/apps/mysticclock/ChangeLog @@ -1,2 +1,3 @@ 1.00: First published version. 1.01: Use Bangle.setUI for Launcher/buttons +1.02: Tell clock widgets to hide. diff --git a/apps/mysticclock/metadata.json b/apps/mysticclock/metadata.json index 571a55ecd..bd2df2f8d 100644 --- a/apps/mysticclock/metadata.json +++ b/apps/mysticclock/metadata.json @@ -1,7 +1,7 @@ { "id": "mysticclock", "name": "Mystic Clock", - "version": "1.01", + "version": "1.02", "description": "A retro-inspired watchface featuring time, date, and an interactive data display line.", "icon": "mystic-clock.png", "type": "clock", diff --git a/apps/mysticclock/mystic-clock-app.js b/apps/mysticclock/mystic-clock-app.js index 2d95633fe..d7f4ab1c3 100644 --- a/apps/mysticclock/mystic-clock-app.js +++ b/apps/mysticclock/mystic-clock-app.js @@ -189,6 +189,13 @@ Bangle.on('touch', (button) => { if (button === 3 && Bangle.isLCDOn()) Bangle.setLCDPower(false); }); +// Show launcher when button pressed +Bangle.setUI("clockupdown", btn=>{ + if (btn<0) prevInfo(); + if (btn>0) nextInfo(); + drawAll(); +}); + // clean app screen g.clear(); Bangle.loadWidgets(); @@ -200,9 +207,3 @@ if (Bangle.isLCDOn()) { drawAll(); // draw immediately } -// Show launcher when button pressed -Bangle.setUI("clockupdown", btn=>{ - if (btn<0) prevInfo(); - if (btn>0) nextInfo(); - drawAll(); -}); 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 { } }); +// Show launcher when button pressed +Bangle.setUI("clock"); + g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); drawHands(true); - -// Show launcher when button pressed -Bangle.setUI("clock"); diff --git a/apps/ncrclk/metadata.json b/apps/ncrclk/metadata.json index b50b554e1..fdab77450 100644 --- a/apps/ncrclk/metadata.json +++ b/apps/ncrclk/metadata.json @@ -2,7 +2,7 @@ "id": "ncrclk", "name": "NCR Clock", "shortName": "NCR Clock", - "version": "0.02", + "version": "0.03", "description": "NodeConf Remote clock", "icon": "app.png", "type": "clock", diff --git a/apps/ncstart/ChangeLog b/apps/ncstart/ChangeLog deleted file mode 100644 index 152fdc9d1..000000000 --- a/apps/ncstart/ChangeLog +++ /dev/null @@ -1,9 +0,0 @@ -0.02: Modified for use with new bootloader and firmware - Renamed as nodeconf-specific -0.03: Move configuration into App/widget settings - Move loader into welcome.boot.js -0.04: Run again when updated - Don't run again when settings app is updated (or absent) - Add "Run Now" option to settings -0.05: Don't overwrite existing settings on app update -0.06: Allow welcome to run after a fresh install diff --git a/apps/ncstart/boot.js b/apps/ncstart/boot.js deleted file mode 100644 index 62ac962f6..000000000 --- a/apps/ncstart/boot.js +++ /dev/null @@ -1,9 +0,0 @@ -(function() { - let s = require('Storage').readJSON('ncstart.json', 1) || {}; - if (!s.welcomed) { - setTimeout(() => { - 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/noteify/README.md b/apps/noteify/README.md index c846709de..dbdceb399 100644 --- a/apps/noteify/README.md +++ b/apps/noteify/README.md @@ -1,6 +1,6 @@ # WARNING -This app uses the [Scheduler library](https://banglejs.com/apps/?id=sched) and requires a keyboard such as [Swipe keyboard](https://banglejs.com/apps/?id=kbswipe). +This app uses the [Scheduler library](https://banglejs.com/apps/?id=sched) and requires a [keyboard library](https://banglejs.com/apps/?c=textinput#). ## Usage diff --git a/apps/noteify/metadata.json b/apps/noteify/metadata.json index eb6dc695a..fbd5a88f1 100644 --- a/apps/noteify/metadata.json +++ b/apps/noteify/metadata.json @@ -5,7 +5,7 @@ "description": "Write notes using an onscreen keyboard and use them as custom messages for alarms or timers.", "icon": "app.png", "tags": "tool,alarm", - "supports": ["BANGLEJS2"], + "supports": ["BANGLEJS", "BANGLEJS2"], "readme": "README.md", "storage": [ {"name":"noteify.app.js","url":"app.js"}, diff --git a/apps/novaclock/ChangeLog b/apps/novaclock/ChangeLog index b9a5425d1..8b05ff9ec 100644 --- a/apps/novaclock/ChangeLog +++ b/apps/novaclock/ChangeLog @@ -1,2 +1,3 @@ ... 0.10: First update with ChangeLog Added +0.11: Tell clock widgets to hide. diff --git a/apps/novaclock/app.js b/apps/novaclock/app.js index e5bd37b06..52bee0dbd 100644 --- a/apps/novaclock/app.js +++ b/apps/novaclock/app.js @@ -249,13 +249,13 @@ var open = false; var timemode = true; var clockmode; var novaYPos = -7; +Bangle.setUI("clock"); g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); g.drawImage(nova(), -10, -10, { scale: 2.2 }); -Bangle.setUI("clock"); g.drawImage(star(), 5, -5, {scale:0.8}); g.drawImage(star(), -10, 120, {scale:0.8}); diff --git a/apps/novaclock/metadata.json b/apps/novaclock/metadata.json index a8a2c0af1..69b7627f8 100644 --- a/apps/novaclock/metadata.json +++ b/apps/novaclock/metadata.json @@ -3,7 +3,7 @@ "shortName":"Nova Clock", "icon": "app.png", "type": "clock", - "version":"0.10", + "version":"0.11", "description": "A clock inspired by the Kirby series", "tags": "clock", "supports": ["BANGLEJS2"], diff --git a/apps/numberchaser/app-icon.js b/apps/numberchaser/app-icon.js new file mode 100644 index 000000000..a4bb5054d --- /dev/null +++ b/apps/numberchaser/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkA/4AC+c/AoYAR+QTTj4DBkYXS+YUB+cSl4YS+P/mUiL4RLQ+chiUziPyAAIXPmJEC+Ui+UhO6QABj8iVKocEACUTC60j+YXWPoZHTkcyC6sibYaPpC63ziXzkYXUkXyO6nykMykIXUl8xmcykQ2BSZ4XBkTXB+LdBMgPyDRnzj8vmf/AIMyDgMjAoIALiQSCVYI0CD4QAL+MTIILFCI4TJOmMRkUzn40CGQRLBMRipBiIABkR2DTAIAQmURF4KcBZScxn5qBACgWWbwUhMCT7CmU/WQQAR+awBF6jdBVggXSPCwXXVAPyJCkimUieKinBI6sxAQIvUeAMyMQIXT+MjI6iNC+RIT+bAB+cSYiZdBMQMzSCkf+ZIUGAMiYKsjkTbVkMSl4A==")) diff --git a/apps/numberchaser/app.js b/apps/numberchaser/app.js new file mode 100644 index 000000000..f68119fb2 --- /dev/null +++ b/apps/numberchaser/app.js @@ -0,0 +1,104 @@ +var randomNumber; +var guessNumber = 1; + +function mathRandomInt(a, b) { + if (a > b) { + // Swap a and b to ensure a is smaller. + var c = a; + a = b; + b = c; + } + return Math.floor(Math.random() * (b - a + 1) + a); +} + +/** + * Describe this function... + */ +function game() { + + g.drawString('',0,20,true); + E.showMenu(numMenu); + console.log(randomNumber); +} + +var numMenu = { + "" : { + "title" : "Number Chaser", + }, + "Guess Number" : { + value : guessNumber, + min:1,max:100,step:1, + onchange : v => { guessNumber=v; } + }, + "OK" : function () { + g.clear(); + if (guessNumber == randomNumber) { + //if guess is correct + g.setFont("Vector",13);g.setFontAlign(-1,-1); + status = "You won! "; + gameOver(); + } else { + //if guess is incorrect + g.setFont("Vector",13);g.setFontAlign(-1,-1); + if (guessNumber > randomNumber) { + //Decreases number if guess is greater + randomNumber = randomNumber - 1; + status = "Too high!"; + } else if (guessNumber < randomNumber) { + //Increases number if guess is lower + status = "Too low!"; + randomNumber = randomNumber + 1; + } + if (randomNumber < 0 || randomNumber > 100) { + //You lose when the number is out of the 1 to 100 range + g.setFont("Vector",13);g.setFontAlign(-1,-1); + g.drawString('You have lost\nNumber is out\nof range.',10,10,true); + status = "You lost!"; + } else { + g.drawString(status+"\nTry again!",10,10); + Bangle.on('tap', function() { + delay(3000).then(() => game()); + } + ); + } + } + } +}; + +function gameOver() +{ + E.showPrompt(status+'Play again?',{title:""+'Number Chaser'}).then(function(a) { + if (a) { + randomNumber = mathRandomInt(1, 100); + game(); + } else { + load(); + } + } + ); +} + +function delay(time) { + return new Promise(resolve => setTimeout(resolve, time)); +} + +function instructions() +{ + g.setFont("Vector",13);g.setFontAlign(-1,-1); + g.drawString('Guess the number\nbetween 1 and 100.\nGuess too high, it\ndecreases by 1.\nToo low, it increases\nby 1.\nIf the number\ngoes below 0 or\nabove 100, it\nis out of range\nand you have\nlost.',10,10,true); + randomNumber = mathRandomInt(1, 100); + delay(10000).then(() => game()); +} + + +g.clear(); +E.showPrompt('Do you need instructions?',{title:""+'Number Chaser'}).then(function(a) + { if (a) { + instructions(); + } else + { + randomNumber = mathRandomInt(1, 100); + game(); + } + } +); diff --git a/apps/numberchaser/metadata.json b/apps/numberchaser/metadata.json new file mode 100644 index 000000000..f9b6ff4b2 --- /dev/null +++ b/apps/numberchaser/metadata.json @@ -0,0 +1,13 @@ +{ "id": "numberchaser", + "name": "Number Chaser", + "shortName":"Number Chaser", + "version":"0.01", + "description": "A number guessing game, but the number goes up or down based on if you're guessing too high or too low.", + "icon": "numberchaser.png", + "tags": "game,fun", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"numberchaser.app.js","url":"app.js"}, + {"name":"numberchaser.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/numberchaser/numberchaser.png b/apps/numberchaser/numberchaser.png new file mode 100644 index 000000000..2042cc22b Binary files /dev/null and b/apps/numberchaser/numberchaser.png differ diff --git a/apps/openstmap/ChangeLog b/apps/openstmap/ChangeLog index 6cb9d061e..2a6c15a09 100644 --- a/apps/openstmap/ChangeLog +++ b/apps/openstmap/ChangeLog @@ -10,3 +10,5 @@ 0.10: Improve scale factor calculation to fix scaling issues (#984) 0.11: Add slight offset to OSM data to align it properly (fix #984) Fix alignment of satellite info text +0.12: switch to using normal OpenStreetMap tiles (opentopomap was too slow) +0.13: Use a single image file with 'frames' of data (drastically reduces file count, possibility of >1 map on device) diff --git a/apps/openstmap/custom.html b/apps/openstmap/custom.html index 6e79a6e9a..3bba997e7 100644 --- a/apps/openstmap/custom.html +++ b/apps/openstmap/custom.html @@ -64,21 +64,22 @@ TODO: var OSMTILECOUNT = 3; // how many tiles do we download in each direction (Math.floor(MAPSIZE / OSMTILESIZE)+1) /* Can see possible tiles on http://leaflet-extras.github.io/leaflet-providers/preview/ However some don't allow cross-origin use */ - var TILELAYER = 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png'; // simple, high contrast - var PREVIEWTILELAYER = 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'; + //var TILELAYER = 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png'; // simple, high contrast, TOO SLOW //var TILELAYER = 'http://a.tile.stamen.com/toner/{z}/{x}/{y}.png'; // black and white + var TILELAYER = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'; + var PREVIEWTILELAYER = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'; // Create map and try and set the location to where the browser thinks we are var map = L.map('map').locate({setView: true, maxZoom: 16, enableHighAccuracy:true}); // Tiles used for Bangle.js itself var bangleTileLayer = L.tileLayer(TILELAYER, { maxZoom: 18, - attribution: 'Map data © OpenStreetMap contributors' + attribution: 'Map data © OpenStreetMap contributors' }); // Tiles used for the may the user sees (faster) var previewTileLayer = L.tileLayer(PREVIEWTILELAYER, { maxZoom: 18, - attribution: 'Map data © OpenStreetMap contributors' + attribution: 'Map data © OpenStreetMap contributors' }); // Could optionally overlay trails: https://wiki.openstreetmap.org/wiki/Tiles @@ -128,7 +129,7 @@ TODO: var min = Math.min(rgba[i+0],rgba[i+1],rgba[i+2]); var max = Math.max(rgba[i+0],rgba[i+1],rgba[i+2]); var d = max-min; - if (max<120) { // black + if (max<120 || (d<8 && max<215)) { // black, or a darker grey rgba[i+0]=0; rgba[i+1]=0; rgba[i+2]=0; @@ -147,7 +148,7 @@ TODO: console.log("Compression options", options); var w = Math.round(width / TILESIZE); var h = Math.round(height / TILESIZE); - var tiles = []; + var tiledImage; for (var y=0;y { document.getElementById("uploadbuttons").style.display=""; mapFiles = tilesLoaded(ctx, canvas.width, canvas.height); - mapFiles.unshift({name:"openstmap.json",content:JSON.stringify({ + mapFiles.unshift({name:"openstmap.0.json",content:JSON.stringify({ imgx : canvas.width, imgy : canvas.height, tilesize : TILESIZE, scale : scale, // how much of Bangle.project(latlon) does one pixel equate to? lat : centerlatlon.lat, - lon : centerlatlon.lng + lon : centerlatlon.lng, + w : Math.round(canvas.width / TILESIZE), // width in tiles + h : Math.round(canvas.height / TILESIZE), // height in tiles + fn : "openstmap.0.img" })}); console.log(mapFiles); }); diff --git a/apps/openstmap/metadata.json b/apps/openstmap/metadata.json index 2dc9bd427..32093f70e 100644 --- a/apps/openstmap/metadata.json +++ b/apps/openstmap/metadata.json @@ -2,7 +2,7 @@ "id": "openstmap", "name": "OpenStreetMap", "shortName": "OpenStMap", - "version": "0.11", + "version": "0.13", "description": "Loads map tiles from OpenStreetMap onto your Bangle.js and displays a map of where you are. Once installed this also adds map functionality to `GPS Recorder` and `Recorder` apps", "icon": "app.png", "tags": "outdoors,gps,osm", diff --git a/apps/openstmap/openstmap.js b/apps/openstmap/openstmap.js index d995aca25..2bd7d2e2e 100644 --- a/apps/openstmap/openstmap.js +++ b/apps/openstmap/openstmap.js @@ -22,7 +22,7 @@ function center() { */ -var map = require("Storage").readJSON("openstmap.json"); +var map = require("Storage").readJSON("openstmap.0.json"); map.center = Bangle.project({lat:map.lat,lon:map.lon}); exports.map = map; exports.lat = map.lat; // actual position of middle of screen @@ -30,7 +30,7 @@ exports.lon = map.lon; // actual position of middle of screen var m = exports; exports.draw = function() { - var s = require("Storage"); + var img = require("Storage").read(map.fn); var cx = g.getWidth()/2; var cy = g.getHeight()/2; var p = Bangle.project({lat:m.lat,lon:m.lon}); @@ -41,13 +41,11 @@ exports.draw = function() { var ty = 0|(iy/map.tilesize); var ox = (tx*map.tilesize)-ix; var oy = (ty*map.tilesize)-iy; - for (var x=ox,ttx=tx;x=0 && ttx=0 && tty { + 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/pastel/ChangeLog b/apps/pastel/ChangeLog index f4640426b..28dcc0c28 100644 --- a/apps/pastel/ChangeLog +++ b/apps/pastel/ChangeLog @@ -18,3 +18,4 @@ added setting to enable/disable idle timer warning 0.16: make check_idle boolean setting work properly with new B2 menu 0.17: Use default Bangle formatter for booleans +0.18: fix idle option always getting defaulted to true diff --git a/apps/pastel/metadata.json b/apps/pastel/metadata.json index 1fe176d5f..860ed833b 100644 --- a/apps/pastel/metadata.json +++ b/apps/pastel/metadata.json @@ -2,7 +2,7 @@ "id": "pastel", "name": "Pastel Clock", "shortName": "Pastel", - "version": "0.17", + "version": "0.18", "description": "A Configurable clock with custom fonts, background and weather display. Has a cyclic information line that includes, day, date, battery, sunrise and sunset times", "icon": "pastel.png", "dependencies": {"mylocation":"app","weather":"app"}, diff --git a/apps/pastel/pastel.app.js b/apps/pastel/pastel.app.js index 605b78ad0..05c0e2367 100644 --- a/apps/pastel/pastel.app.js +++ b/apps/pastel/pastel.app.js @@ -34,7 +34,7 @@ function loadSettings() { settings = require("Storage").readJSON(SETTINGS_FILE,1)||{}; settings.grid = settings.grid||false; settings.font = settings.font||"Lato"; - settings.idle_check = settings.idle_check||true; + settings.idle_check = (settings.idle_check === undefined ? true : settings.idle_check); } // requires the myLocation app diff --git a/apps/pebble/ChangeLog b/apps/pebble/ChangeLog index 274c34a34..ac894fc00 100644 --- a/apps/pebble/ChangeLog +++ b/apps/pebble/ChangeLog @@ -8,3 +8,4 @@ 0.08: Add theme options and optional lock symbol 0.09: Add support for internationalization (LANG placeholders + "locale" module) Get steps from built-in step counter (widpedom no more needed, fix #1697) +0.10: Tell clock widgets to hide. diff --git a/apps/pebble/metadata.json b/apps/pebble/metadata.json index f3c1fcc12..c5faa8857 100644 --- a/apps/pebble/metadata.json +++ b/apps/pebble/metadata.json @@ -2,7 +2,7 @@ "id": "pebble", "name": "Pebble Clock", "shortName": "Pebble", - "version": "0.09", + "version": "0.10", "description": "A pebble style clock to keep the rebellion going", "readme": "README.md", "icon": "pebble.png", diff --git a/apps/pebble/pebble.app.js b/apps/pebble/pebble.app.js index 774b24c3f..729d0a896 100644 --- a/apps/pebble/pebble.app.js +++ b/apps/pebble/pebble.app.js @@ -133,6 +133,8 @@ Bangle.on('lock', function(on) { drawLock(); }); +Bangle.setUI("clock"); + g.clear(); Bangle.loadWidgets(); @@ -149,4 +151,3 @@ loadThemeColors(); setInterval(draw, 15000); // refresh every 15s draw(); -Bangle.setUI("clock"); diff --git a/apps/pebbled/ChangeLog b/apps/pebbled/ChangeLog index 9db0e26c5..8fe3a0b2c 100644 --- a/apps/pebbled/ChangeLog +++ b/apps/pebbled/ChangeLog @@ -1 +1,2 @@ 0.01: first release +0.02: Tell clock widgets to hide. diff --git a/apps/pebbled/metadata.json b/apps/pebbled/metadata.json index c16025f6f..70820f2c8 100644 --- a/apps/pebbled/metadata.json +++ b/apps/pebbled/metadata.json @@ -2,7 +2,7 @@ "id": "pebbled", "name": "Pebble Clock with distance", "shortName": "Pebble + distance", - "version": "0.01", + "version": "0.02", "description": "Fork of Pebble Clock with distance in KM. Both step count and the distance are on the main screen. Default step length = 0.75m (can be changed in settings).", "readme": "README.md", "icon": "pebbled.png", diff --git a/apps/pebbled/pebbled.app.js b/apps/pebbled/pebbled.app.js index bbe98823f..472ac8fff 100644 --- a/apps/pebbled/pebbled.app.js +++ b/apps/pebbled/pebbled.app.js @@ -115,6 +115,7 @@ function getSteps() { return '0'; } +Bangle.setUI("clock"); g.clear(); Bangle.loadWidgets(); /* @@ -126,4 +127,3 @@ for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";} loadSettings(); setInterval(draw, 15000); // refresh every 15s draw(); -Bangle.setUI("clock"); 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/pokeclk/ChangeLog b/apps/pokeclk/ChangeLog index 8e506ce50..5838e596d 100644 --- a/apps/pokeclk/ChangeLog +++ b/apps/pokeclk/ChangeLog @@ -1,2 +1,3 @@ 0.01: New face :) 0.02: Color image compressed +0.03: Improved clock diff --git a/apps/pokeclk/app.js b/apps/pokeclk/app.js index 17a487bc0..7e495f7d2 100644 --- a/apps/pokeclk/app.js +++ b/apps/pokeclk/app.js @@ -5,7 +5,7 @@ const width = g.getWidth(); const height = g.getHeight(); const font = "Vector:12"; -const locale = require("locale"); +var drawTimeout; var img = { width : 176, height : 149, bpp : 4, @@ -20,34 +20,12 @@ var night= { buffer : (atob("ERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERABEREREREREAEREREREREREREREREREREREREREREREREREREREREREREREQABERERERHwABEREREREREREREREREREREREREREREREREREREREREREREREQ/xERERER/wERERERERERERERERERERERERERERERERERERERERERERERERH//xERERH//xERERERERERERERERERERERERERERERERERERERERERERERERH//xEREf/xERERERERERERERERERERERERERERERERERERERERERERERERERH///////ERERERERERERERERERERERERERERERERERERERERERERERERERER////////8RERERERERERERERERERERERERERERERERERERERERERERERERH/////////ERERERERERERERERERERERERERERERERERERERERERERERERER////D///DxERERERERERERERERERERERERERERERERERERERERERERERERH////w///w8RERERERERERERERERERERERERERERERERERERERER//ERERER//AA///w/w8REREREREREREREREREREREREREREREREREREREREf////EREf/wAP/wAA8PERERERERERERERERERERERERERERERERERERERERH////xERH/8AD/////DxERERERERERERERERERERERERERERERERERERERER////8REf//////////ERERERERERERERERERERERERERERERERERERERERERERH/8R/////////xERERERERERERERERERERERERERERERERERERERER////////Ef////////////////////////////////////////////////////////////////////////////////////////////////////////////8RERERH////////////xERERERERERERERERERERERERERERERERERERERERERERERH///////////EREREREREREREREREREREREREREREREREREREREREf/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////w==")) }; -var time= "10:20"; - -function time() { //numbers - // work out how to display the current time - const d = new Date(); - const h = d.getHours(), - m = d.getMinutes(); - const time = h + ":" + ("0" + m).substr(-2); - const day = Date.now(); - const mo = d.getMonth()+1; - const damo = d.getDate(); - - var dayMonth = mo+"-"+damo; - - // time - require("Font4x5").add(Graphics); - isDark(); - g.setFontAlign(0,0); - //g.setFont("6x8:4x5"); - g.setFont("4x5",7); - g.drawString(time, width/2, height/2); - // date - require("Font4x5").add(Graphics); - g.setFontAlign(1,1); - //g.setFont("4x6",2); - g.setFont("4x5",3); - g.drawString(dayMonth, width/2+60, height/2+40); - +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); } function isDark(){ @@ -59,6 +37,22 @@ function isDark(){ } } +function time() { + var d = new Date(); + var day = d.getDate(); + var time = require("locale").time(d,1); + var date = require("locale").date(d); + var mo = require("date_utils").month(d.getMonth()+1,1); + + require("Font4x5").add(Graphics); // time + isDark(); + g.setFontAlign(0,0); + g.setFont("4x5",7.5).drawString(time, width/2, height/2); + + g.setFontAlign(1,1); + g.setFont("4x5",3).drawString(mo+" "+day, width-15, height-35); +} + function draw() { //poketch background if (g.theme.dark==true){ g.drawImage(night, 0, 25, {scale:2}); //poketch is life @@ -67,20 +61,13 @@ function draw() { //poketch background g.drawImage(img, 0, 25); //poketch is life } time(); + queueDraw(); } //program start g.clear(); draw(); -var secondInterval = setInterval(draw, 1000); // Stop updates when LCD is off, restart when on -Bangle.on('lcdPower',on=>{ - if (secondInterval) clearInterval(secondInterval); - secondInterval = undefined; - if (on) { - secondInterval = setInterval(draw, 1000); - draw(); // draw immediately - } -}); + // Show launcher when middle button pressed Bangle.setUI("clock"); // Load widgets diff --git a/apps/pokeclk/metadata.json b/apps/pokeclk/metadata.json index 433077efe..c022868ec 100644 --- a/apps/pokeclk/metadata.json +++ b/apps/pokeclk/metadata.json @@ -2,7 +2,7 @@ "id": "pokeclk", "name": "Poketch Clock", "shortName":"Poketch Clock", - "version": "0.02", + "version": "0.03", "description": "A clock based on the Poketch electronic device found in Sinnoh", "icon": "app.png", "type": "clock", 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/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 new file mode 100644 index 000000000..51ba044e1 --- /dev/null +++ b/apps/powersave/README.md @@ -0,0 +1,25 @@ +# Power Saver + +Save your watch's battery power by halting foreground app execution while the screen is off. + +## Features +- Stops foreground app processes +- Background processes still run +- Clears screen +- Foreground app is returned to when screen is turned back on (app state is not preserved) + +## Controls +- Automatically activates when screen times out, timing can be adjusted using normal timeout settings +- Deactivates when screen is turned back on + +## Warnings +- 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. + +## Requests + +[Contact information is on my website](https://kyleplo.com/#contact) + +## Creator + +[kyleplo](https://kyleplo.com) \ No newline at end of file diff --git a/apps/powersave/boot.js b/apps/powersave/boot.js new file mode 100644 index 000000000..f37fbc536 --- /dev/null +++ b/apps/powersave/boot.js @@ -0,0 +1,20 @@ +var Storage = Storage || require("Storage"); +Bangle.on("lock", locked => { + if(locked){ + load("powersave.screen.js"); + }else{ + const data = JSON.parse(Storage.read("powersave.json") || Storage.read("setting.json")); + load(data.app || data.clock); + } +}); +E.on("init", () => { + 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 new file mode 100644 index 000000000..705384058 --- /dev/null +++ b/apps/powersave/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "powersave", + "name": "Power Save", + "version": "0.03", + "description": "Halts foreground app execution while screen is off while still allowing background processes.", + "readme": "README.md", + "icon": "powersave.png", + "type": "bootloader", + "tags": "tool", + "supports": ["BANGLEJS2"], + "storage": [ + {"name":"powersave.boot.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/powersave.png b/apps/powersave/powersave.png new file mode 100644 index 000000000..fa0399b73 Binary files /dev/null and b/apps/powersave/powersave.png differ diff --git a/apps/powersave/screen.js b/apps/powersave/screen.js new file mode 100644 index 000000000..c920b205d --- /dev/null +++ b/apps/powersave/screen.js @@ -0,0 +1,7 @@ +g.clear(); +Bangle.setLCDBrightness(0); +if(!Bangle.isLocked()){ + 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/presentation_timer/ChangeLog b/apps/presentation_timer/ChangeLog index 9db0e26c5..2ed460931 100644 --- a/apps/presentation_timer/ChangeLog +++ b/apps/presentation_timer/ChangeLog @@ -1 +1,2 @@ 0.01: first release +0.02: added interface for configuration from app loader diff --git a/apps/presentation_timer/README.md b/apps/presentation_timer/README.md index b24426672..4539fc2f9 100644 --- a/apps/presentation_timer/README.md +++ b/apps/presentation_timer/README.md @@ -12,11 +12,10 @@ when the time for the last slide is approaching, the button becomes red, when it passed, the time will go on for another half a minute and stop automatically. -The only way to upload personalized timings is -by uploading a CSV to the bangle (i.e. from the IDE), -in the future I'll possibly figure out a better way. +You can set personalized timings from the web interface +by uploading a CSV to the bangle (floppy disk button in the app loader). -Each line in the file (which must be called `presentation_timer.csv`) +Each line in the file (`presentation_timer.csv`) contains the time in minutes at which the slide is supposed to finish and the slide number, separated by a semicolon. @@ -25,8 +24,7 @@ is lasting until 1 minutes 30 seconds (yes it's decimal), after another slide will start. The only requirement is that timings are increasing, so slides number don't have to be consecutive, -some can be skipped and they can even be short texts -(be careful with that, I didn't test it). +some can be skipped and they can even be short texts. At the moment the app is just quick and dirty but it should do its job. diff --git a/apps/presentation_timer/interface.html b/apps/presentation_timer/interface.html new file mode 100644 index 000000000..137ea8475 --- /dev/null +++ b/apps/presentation_timer/interface.html @@ -0,0 +1,136 @@ + + + + + + + +
+ + + + + +

+
+    
+    
+  
+
diff --git a/apps/presentation_timer/metadata.json b/apps/presentation_timer/metadata.json
index 7a61ffbf0..8790d6208 100644
--- a/apps/presentation_timer/metadata.json
+++ b/apps/presentation_timer/metadata.json
@@ -1,15 +1,17 @@
 {
   "id": "presentation_timer",
   "name": "Presentation Timer",
-  "version": "0.01",
+  "version": "0.02",
   "description": "A touch based presentation timer for Bangle JS 2",
   "icon": "presentation_timer.png",
   "screenshots": [{"url":"screenshot1.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"},{"url":"screenshot4.png"}],
   "tags": "tools,app",
   "supports": ["BANGLEJS2"],
   "readme": "README.md",
+  "interface": "interface.html",
   "storage": [
     {"name":"presentation_timer.app.js","url":"presentation_timer.app.js"},
     {"name":"presentation_timer.img","url":"presentation_timer.icon.js","evaluate":true}
-  ]
+  ],
+  "data": [{ "name": "presentation_timer.csv" }]
 }
diff --git a/apps/presentation_timer/presentation_timer.app.js b/apps/presentation_timer/presentation_timer.app.js
index af2b35dd5..1d0e5945d 100644
--- a/apps/presentation_timer/presentation_timer.app.js
+++ b/apps/presentation_timer/presentation_timer.app.js
@@ -19,10 +19,10 @@ const margin = 0.5; //half a minute tolerance
 
 //dummy default values
 var slides = [
-  [0.3, 1],
-  [0.5, 2],
-  [0.7, 3],
-  [1,4]
+  [1.5, 1],
+  [2, 2],
+  [2.5, 3],
+  [3,4]
 ];
 
 function log_debug(o) {
@@ -267,6 +267,6 @@ g.fillRect(0,0,w,h);
 
 Bangle.loadWidgets();
 Bangle.drawWidgets();
+readSlides();
 draw();
 setWatch(() => load(), BTN, { repeat: false, edge: "falling" });
-readSlides();
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/ptlaunch/ChangeLog b/apps/ptlaunch/ChangeLog
index eec3610ed..5871b1fdc 100644
--- a/apps/ptlaunch/ChangeLog
+++ b/apps/ptlaunch/ChangeLog
@@ -6,3 +6,4 @@
 0.12: Improve pattern detection code readability by PaddeK http://forum.espruino.com/profiles/117930/
 0.13: Improve pattern rendering by HughB http://forum.espruino.com/profiles/167235/
 0.14: Update setUI to work with new Bangle.js 2v13 menu style
+0.15: Update to support clocks in custom setUI mode
diff --git a/apps/ptlaunch/boot.js b/apps/ptlaunch/boot.js
index 748d564f3..885962761 100644
--- a/apps/ptlaunch/boot.js
+++ b/apps/ptlaunch/boot.js
@@ -76,13 +76,8 @@
   var sui = Bangle.setUI;
   Bangle.setUI = function (mode, cb) {
     sui(mode, cb);
-    if ("object"==typeof mode) mode = mode.mode;
-    if (!mode) {
-      Bangle.removeListener("drag", dragHandler);
-      storedPatterns = {};
-      return;
-    }
-    if (!mode.startsWith("clock")) {
+    if (typeof mode === "object") mode = (mode.clock ? "clock" : "") + mode.mode;
+    if (!mode || !mode.startsWith("clock")) {
       storedPatterns = {};
       Bangle.removeListener("drag", dragHandler);
       return;
diff --git a/apps/ptlaunch/metadata.json b/apps/ptlaunch/metadata.json
index 0b6dce3d1..6f8a9e16f 100644
--- a/apps/ptlaunch/metadata.json
+++ b/apps/ptlaunch/metadata.json
@@ -2,7 +2,7 @@
   "id": "ptlaunch",
   "name": "Pattern Launcher",
   "shortName": "Pattern Launcher",
-  "version": "0.14",
+  "version": "0.15",
   "description": "Directly launch apps from the clock screen with custom patterns.",
   "icon": "app.png",
   "screenshots": [{"url":"manage_patterns_light.png"}],
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/ratchet_launch/metadata.json b/apps/ratchet_launch/metadata.json
index 14ffec34a..45057b0b9 100644
--- a/apps/ratchet_launch/metadata.json
+++ b/apps/ratchet_launch/metadata.json
@@ -11,6 +11,5 @@
   "storage": [
     {"name":"ratchet_launch.app.js","url":"app.js"}
   ],
-  "sortorder": -10,
   "readme":"README.md"
 }
diff --git a/apps/rclock/ChangeLog b/apps/rclock/ChangeLog
index 915fbc5d7..0a86ee3e3 100644
--- a/apps/rclock/ChangeLog
+++ b/apps/rclock/ChangeLog
@@ -5,3 +5,4 @@
 0.05: Changes which circle show minutes and seconds
 0.06: Avoid function wrapper, use setUI for launcher
       Clock face smaller so no longer breaks widgets
+0.07: Tell clock widgets to hide.
diff --git a/apps/rclock/metadata.json b/apps/rclock/metadata.json
index 77a036481..e5478aa6a 100644
--- a/apps/rclock/metadata.json
+++ b/apps/rclock/metadata.json
@@ -2,7 +2,7 @@
   "id": "rclock",
   "name": "Round clock with seconds,  minutes and date",
   "shortName": "Round Clock",
-  "version": "0.06",
+  "version": "0.07",
   "description": "Designed round clock with ticks for minutes and seconds and heart rate indication",
   "icon": "app.png",
   "type": "clock",
diff --git a/apps/rclock/rclock.app.js b/apps/rclock/rclock.app.js
index 9c219ab3d..2d6f84dc4 100644
--- a/apps/rclock/rclock.app.js
+++ b/apps/rclock/rclock.app.js
@@ -196,6 +196,9 @@ const drawHR = function () {
   }
 };
 
+// Show launcher when button pressed
+Bangle.setUI("clock");
+
 // clean app screen
 g.clear();
 Bangle.loadWidgets();
@@ -222,5 +225,3 @@ Bangle.on('HRM', function (d) {
 // draw now
 drawClock();
 
-// Show launcher when button pressed
-Bangle.setUI("clock");
diff --git a/apps/rebble/ChangeLog b/apps/rebble/ChangeLog
index 4e2e76484..21dd44e77 100644
--- a/apps/rebble/ChangeLog
+++ b/apps/rebble/ChangeLog
@@ -6,4 +6,7 @@
 0.06: Add 12h support and autocycle control
 0.07: added localization, removed deprecated code
 0.08: removed unused font, fix autocycle,  imported suncalc and trimmed, removed pedometer dependency, "tap to cycle" setting
-0.09: fix battery icon size
\ No newline at end of file
+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 )
+0.12: implemented widget_utils 
\ No newline at end of file
diff --git a/apps/rebble/metadata.json b/apps/rebble/metadata.json
index ec7650f53..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.09",
+  "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 fc91fe0ac..82e4a62d7 100644
--- a/apps/rebble/rebble.app.js
+++ b/apps/rebble/rebble.app.js
@@ -292,19 +292,10 @@ function queueDraw() {
 
 
 log_debug("starting..");
-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="";}
 loadSettings();
 loadLocation();
 
 
-
 if(settings.autoCycle || settings.sideTap==0)
 {
   Bangle.setUI("clockupdown", btn=> {
@@ -318,6 +309,9 @@ else{
 }
 
 
+Bangle.loadWidgets();
+require("widget_utils").hide();
+
 
 
 draw();  // queues the next draw for a minutes time
@@ -331,4 +325,4 @@ Bangle.on('charging', function(charging) {
       drawSideBar2();
       break;
   }
-});
\ No newline at end of file
+});
diff --git a/apps/recorder/interface.html b/apps/recorder/interface.html
index 8cf339e85..cc9762d20 100644
--- a/apps/recorder/interface.html
+++ b/apps/recorder/interface.html
@@ -12,9 +12,22 @@
     
+    
+  
+
\ No newline at end of file
diff --git a/apps/sleeplog/lib.js b/apps/sleeplog/lib.js
index 752139e27..1919e7483 100644
--- a/apps/sleeplog/lib.js
+++ b/apps/sleeplog/lib.js
@@ -1,199 +1,465 @@
+// define accessable functions
 exports = {
   // define en-/disable function, restarts the service to make changes take effect
-  setEnabled: function(enable, logfile, powersaving) {
-    // check if sleeplog is available
-    if (typeof global.sleeplog !== "object") return;
-
-    // set default logfile
-    if ((typeof logfile !== "string" || !logfile.endsWith(".log")) &&
-      logfile !== false) logfile = "sleeplog.log";
-
+  setEnabled: function(enable) {
     // stop if enabled
-    if (global.sleeplog.enabled) global.sleeplog.stop();
+    if (global.sleeplog && sleeplog.enabled) sleeplog.stop();
 
-    // define storage and filename
-    var storage = require("Storage");
-    var filename = "sleeplog.json";
+    // define settings filename
+    var settings = "sleeplog.json";
 
     // change enabled value in settings
-    storage.writeJSON(filename, Object.assign(storage.readJSON(filename, true) || {}, {
-      enabled: enable,
-      logfile: logfile,
-      powersaving: powersaving || false
-    }));
+    require("Storage").writeJSON(settings, Object.assign(
+      require("Storage").readJSON(settings, true) || {}, {
+        enabled: enable
+      }
+    ));
 
     // force changes to take effect by executing the boot script
-    eval(storage.read("sleeplog.boot.js"));
+    eval(require("Storage").read("sleeplog.boot.js"));
 
-    // clear variables
-    storage = undefined;
-    filename = undefined;
     return true;
   },
 
-  // define read log function
-  // sorting: latest first, format:
-  // [[number, int, float, string], [...], ... ]
-  // - number // timestamp in ms
-  // - int    // status: 0 = unknown, 1 = not worn, 2 = awake, 3 = sleeping
-  // - float  // internal temperature
-  // - string // additional information
-  readLog: function(logfile, since, until) {
-    // check/set logfile
-    if (typeof logfile !== "string" || !logfile.endsWith(".log")) {
-      logfile = (global.sleeplog || {}).logfile || "sleeplog.log";
+  // define read log function, returns log array
+  // sorting: ascending (latest first), format:
+  // [[number, int, int], [...], ... ]
+  // - number // timestamp in 10min
+  // - int    // status: 0 = unknown, 1 = not worn, 2 = awake, 3 = light sleep, 4 = deep sleep
+  // - int    // consecutive: 0 = unknown, 1 = no consecutive sleep, 2 = consecutive sleep
+  readLog: function(since, until) {
+    // set now and check if now is before since
+    var now = Date.now();
+    if (now < since) return [];
+
+    // set defaults and convert since, until and now to 10min steps
+    since = Math.floor((since || 0) / 6E5);
+    until = Math.ceil((until || now) / 6E5);
+    now = Math.ceil(now / 6E5);
+
+    // define output log
+    var log = [];
+
+    // open StorageFile
+    var file = require("Storage").open("sleeplog.log", "r");
+    // cache StorageFile size
+    var storageFileSize = file.getLength();
+    // check if a Storage File needs to be read
+    if (storageFileSize) {
+      // define previous line cache
+      var prevLine;
+      // loop through StorageFile entries
+      while (true) {
+        // cache new line
+        var line = file.readLine();
+        // exit loop if all lines are read
+        if (!line) break;
+        // skip lines starting with ","
+        if (line.startsWith(",")) continue;
+        // parse line
+        line = line.trim().split(",").map(e => parseInt(e));
+        // exit loop if new line timestamp is not before until
+        if (line[0] >= until) break;
+        // check if new line timestamp is 24h before since or not after since
+        if (line[0] + 144 < since) {
+          // skip roughly the next 10 lines
+          file.read(118);
+          file.readLine();
+        } else if (line[0] <= since) {
+          // cache line for next cycle
+          prevLine = line;
+        } else {
+          // add previous line if it was cached
+          if (prevLine) log.push(prevLine);
+          // add new line at the end of log
+          log.push(line);
+          // clear previous line cache
+          prevLine = undefined;
+        }
+      }
+      // add previous line if it was cached
+      if (prevLine) log.push(prevLine);
+      // set unknown consecutive statuses
+      log = log.reverse().map((entry, index) => {
+        if (entry[2] === 0) entry[2] = (log[index - 1] || [])[2] || 0;
+        return entry;
+      }).reverse();
+      // remove duplicates
+      log = log.filter((entry, index) =>
+        !(index > 0 && entry[1] === log[index - 1][1] && entry[2] === log[index - 1][2])
+      );
     }
 
-    // check if since is in the future
-    if (since > Date()) return [];
+    // check if log empty or first entry is after since
+    if (!log[0] || log[0][0] > since) {
+      // look for all needed storage files
+      var files = require("Storage").list(/^sleeplog_\d\d\d\d\.log$/, {
+        sf: false
+      });
 
-    // read logfile
-    var log = require("Storage").read(logfile);
-    // return empty log
-    if (!log) return [];
-    // decode data if needed 
-    if (log[0] !== "[") log = atob(log);
-    // do a simple check before parsing
-    if (!log.startsWith("[[") || !log.endsWith("]]")) return [];
-    log = JSON.parse(log) || [];
+      // check if any file available
+      if (files.length) {
+        // generate start and end times in 10min steps
+        files = files.map(file => {
+          var start = this.fnToMs(parseInt(file.substr(9, 4))) / 6E5;
+          return {
+            name: file,
+            start: start,
+            end: start + 2016
+          };
+        }).sort((a, b) => b.start - a.start);
 
-    // check if filtering is needed
-    if (since || until) {
-      // search for latest entry befor since
-      if (since) since = (log.find(element => element[0] <= since) || [0])[0];
-      // filter selected time period
-      log = log.filter(element => (element[0] >= since) && (element[0] <= (until || 1E14)));
+        // read all neccessary files
+        var filesLog = [];
+        files.some(file => {
+          // exit loop if since after end
+          if (since >= file.end) return true;
+          // read file if until after start and since before end
+          if (until > file.start || since < file.end) {
+            var thisLog = require("Storage").readJSON(file.name, 1) || [];
+            if (thisLog.length) filesLog = thisLog.concat(filesLog);
+          }
+        });
+        // free ram
+        files = undefined;
+
+        // check if log from files is available 
+        if (filesLog.length) {
+          // remove unwanted entries
+          filesLog = filesLog.filter((entry, index, filesLog) => (
+            (filesLog[index + 1] || [now])[0] >= since && entry[0] <= until
+          ));
+          // add to log as previous entries
+          log = filesLog.concat(log);
+        }
+        // free ram
+        filesLog = undefined;
+      }
     }
 
-    // output log
+    // define last index
+    var lastIndex = log.length - 1;
+    // set timestamp of first entry to since if first entry before since
+    if (log[0] && log[0][0] < since) log[0][0] = since;
+    // add timestamp at now with unknown status if until after now
+    if (until > now) log.push([now, 0, 0]);
+
     return log;
   },
 
-  // define write log function, append or replace log depending on input
-  // append input if array length >1 and element[0] >9E11
-  // replace log with input if at least one entry like above is inside another array
-  writeLog: function(logfile, input) {
-    // check/set logfile
-    if (typeof logfile !== "string" || !logfile.endsWith(".log")) {
-      if (!global.sleeplog || sleeplog.logfile === false) return;
-      logfile = sleeplog.logfile || "sleeplog.log";
+  // define move log function, move StorageFile content into files seperated by fortnights
+  moveLog: function(force) {
+    /** convert old logfile (< v0.10) if present **/
+    if (require("Storage").list("sleeplog.log", {
+        sf: false
+      }).length) {
+      convertOldLog();
     }
+    /** may be removed in later versions **/
 
-    // check if input is an array
-    if (typeof input !== "object" || typeof input.length !== "number") return;
+    // first day of this fortnight period
+    var thisFirstDay = this.fnToMs(this.msToFn(Date.now()));
 
-    // check for entry plausibility
-    if (input.length > 1 && input[0] * 1 > 9E11) {
-      // read log
-      var log = this.readLog(logfile);
+    // read timestamp of the first StorageFile entry
+    var firstDay = (require("Storage").open("sleeplog.log", "r").read(47) || "").match(/\n\d*/);
+    // calculate the first day of the fortnight period
+    if (firstDay) firstDay = this.fnToMs(this.msToFn(parseInt(firstDay[0].trim()) * 6E5));
 
-      // remove last state if it was unknown and less then 5min ago
-      if (log.length > 0 && log[0][1] === 0 &&
-        Math.floor(Date.now()) - log[0][0] < 3E5) log.shift();
+    // check if moving is neccessary or forced
+    if (force || firstDay && firstDay < thisFirstDay) {
+      // read log for each fortnight period
+      while (firstDay) {
+        // calculate last day
+        var lastDay = firstDay + 12096E5;
+        // read log of the fortnight period
+        var log = require("sleeplog").readLog(firstDay, lastDay);
 
-      // add entry at the first position if it has changed
-      if (log.length === 0 || input.some((e, index) => index > 0 && input[index] !== log[0][index])) log.unshift(input);
+        // check if before this fortnight period
+        if (firstDay < thisFirstDay) {
+          // write log in seperate file
+          require("Storage").writeJSON("sleeplog_" + this.msToFn(firstDay) + ".log", log);
+          // set last day as first
+          firstDay = lastDay;
+        } else {
+          // rewrite StorageFile
+          require("Storage").open("sleeplog.log", "w").write(log.map(e => e.join(",")).join("\n"));
+          // clear first day to exit loop
+          firstDay = undefined;
+        }
 
-      // map log as input
-      input = log;
-    }
-
-    // check and if neccessary reduce logsize to prevent low mem
-    if (input.length > 750) input = input.slice(-750);
-
-    // simple check for log plausibility
-    if (input[0].length > 1 && input[0][0] * 1 > 9E11) {
-      // write log to storage
-      require("Storage").write(logfile, btoa(JSON.stringify(input)));
-      return true;
-    }
-  },
-
-  // define log to humanreadable string function
-  // sorting: latest last, format:
-  // "{substring of ISO date} - {status} for {duration}min\n..."
-  getReadableLog: function(printLog, since, until, logfile) {
-    // read log and check
-    var log = this.readLog(logfile, since, until);
-    if (!log.length) return;
-    // reverse array to set last timestamp to the end
-    log.reverse();
-
-    // define status description and log string
-    var statusText = ["unknown ", "not worn", "awake   ", "sleeping"];
-    var logString = [];
-
-    // rewrite each entry
-    log.forEach((element, index) => {
-      logString[index] = "" +
-        Date(element[0] - Date().getTimezoneOffset() * 6E4).toISOString().substr(0, 19).replace("T", " ") + " - " +
-        statusText[element[1]] +
-        (index === log.length - 1 ?
-          element.length < 3 ? "" : " ".repeat(12) :
-          " for " + ("" + Math.round((log[index + 1][0] - element[0]) / 60000)).padStart(4) + "min"
-        ) +
-        (element[2] ? " | Temp: " + ("" + element[2]).padEnd(5) + "°C" : "") +
-        (element[3] ? " | " + element[3] : "");
-    });
-    logString = logString.join("\n");
-
-    // if set print and return string
-    if (printLog) {
-      print(logString);
-      print("- first", Date(log[0][0]));
-      print("-  last", Date(log[log.length - 1][0]));
-      var period = log[log.length - 1][0] - log[0][0];
-      print("-     period= " + Math.floor(period / 864E5) + "d " + Math.floor(period % 864E5 / 36E5) + "h " + Math.floor(period % 36E5 / 6E4) + "min");
-    }
-    return logString;
-  },
-
-  // define function to eliminate some errors inside the log
-  restoreLog: function(logfile) {
-    // read log and check
-    var log = this.readLog(logfile);
-    if (!log.length) return;
-
-    // define output variable to show number of changes
-    var output = log.length;
-
-    // remove non decremental entries
-    log = log.filter((element, index) => log[index][0] >= (log[index + 1] || [0])[0]);
-
-    // write log
-    this.writeLog(logfile, log);
-
-    // return difference in length
-    return output - log.length;
-  },
-
-  // define function to reinterpret worn status based on given temperature threshold
-  reinterpretTemp: function(logfile, tempthresh) {
-    // read log and check
-    var log = this.readLog(logfile);
-    if (!log.length) return;
-
-    // set default tempthresh
-    tempthresh = tempthresh || (global.sleeplog ? sleeplog.tempthresh : 27);
-
-    // define output variable to show number of changes
-    var output = 0;
-
-    // remove non decremental entries
-    log = log.map(element => {
-      if (element[2]) {
-        var tmp = element[1];
-        element[1] = element[2] > tempthresh ? 3 : 1;
-        if (tmp !== element[1]) output++;
+        // free ram
+        log = undefined;
       }
-      return element;
+    }
+  },
+
+  // define function to return stats from the last date [ms] for a specific duration [ms] or for the complete log
+  getStats: function(until, duration, log) {
+    // define stats variable
+    var stats = {
+      calculatedAt: //   [date] timestamp of the calculation
+        Math.round(Date.now()),
+      deepSleep: 0, //   [min] deep sleep duration
+      lightSleep: 0, //  [min] light sleep duration
+      awakeSleep: 0, //  [min] awake duration inside consecutive sleep
+      consecSleep: 0, // [min] consecutive sleep duration
+      awakeTime: 0, //   [min] awake duration outside consecutive sleep
+      notWornTime: 0, // [min] duration of not worn status
+      unknownTime: 0, // [min] duration of unknown status
+      logDuration: 0, // [min] duration of all entries taken into account
+      firstDate: undefined, // [date] first entry taken into account
+      lastDate: undefined // [date] last entry taken into account
+    };
+
+    // set default inputs
+    until = until || stats.calculatedAt;
+    if (!duration) duration = 864E5;
+
+    // read log for the specified duration or complete log if not handed over
+    if (!log) log = this.readLog(duration ? until - duration : 0, until);
+
+    // check if log not empty or corrupted
+    if (log && log.length && log[0] && log[0].length === 3) {
+      // calculate and set first log date from 10min steps
+      stats.firstDate = log[0][0] * 6E5;
+      stats.lastDate = log[log.length - 1][0] * 6E5;
+
+      // cycle through log to calculate sums til end or duration is exceeded
+      log.forEach((entry, index, log) => {
+        // calculate duration of this entry from 10min steps to minutes
+        var duration = ((log[index + 1] || [until / 6E5 | 0])[0] - entry[0]) * 10;
+
+        // check if duration greater 0
+        if (duration) {
+          // calculate sums
+          if (entry[1] === 4) stats.deepSleep += duration;
+          else if (entry[1] === 3) stats.lightSleep += duration;
+          else if (entry[1] === 2) {
+            if (entry[2] === 2) stats.awakeSleep += duration;
+            else if (entry[2] === 1) stats.awakeTime += duration;
+          }
+          if (entry[2] === 2) stats.consecSleep += duration;
+          if (entry[1] === 1) stats.notWornTime += duration;
+          if (entry[1] === 0) stats.unknownTime += duration;
+          stats.logDuration += duration;
+        }
+      });
+    }
+
+    // free ram
+    log = undefined;
+
+    // return stats of the last day
+    return stats;
+  },
+
+  // define function to return last break time of day from date or now (default: 12 o'clock)
+  getLastBreak: function(date, ToD) {
+    // set default date or correct date type if needed
+    if (!date || !date.getDay) date = date ? new Date(date) : new Date();
+    // set default ToD as set in sleeplog.conf or settings if available
+    if (ToD === undefined) ToD = (global.sleeplog && sleeplog.conf ? sleeplog.conf.breakToD :
+      (require("Storage").readJSON("sleeplog.json", true) || {}).breakToD) || 12;
+    // calculate last break time and return
+    return new Date(date.getFullYear(), date.getMonth(), date.getDate(), ToD);
+  },
+
+  // define functions to convert ms to the number of fortnights since the first Sunday at noon: 1970-01-04T12:00
+  fnToMs: function(no) {
+    return (no + 0.25) * 12096E5;
+  },
+  msToFn: function(ms) {
+    return (ms / 12096E5 - 0.25) | 0;
+  },
+
+  // define set debug function, options:
+  //  enable as boolean, start/stop debugging
+  //  duration in hours, generate csv log if set, max: 96h
+  setDebug: function(enable, duration) {
+    // check if global variable accessable
+    if (!global.sleeplog) return new Error("sleeplog: Can't set debugging, global object missing!");
+
+    // check if nothing has to be changed
+    if (!duration &&
+      (enable && sleeplog.debug === true) ||
+      (!enable && !sleeplog.debug)) return;
+
+    // check if en- or disable debugging
+    if (enable) {
+      // define debug object
+      sleeplog.debug = {};
+
+      // check if a file should be generated
+      if (typeof duration === "number") {
+        // check duration boundaries, 0 => 8
+        duration = duration > 96 ? 96 : duration || 12;
+        // calculate and set writeUntil in 10min steps
+        sleeplog.debug.writeUntil = ((Date.now() / 6E5 | 0) + duration * 6) * 6E5;
+        // set fileid to "{hours since 1970}"
+        sleeplog.debug.fileid = Date.now() / 36E5 | 0;
+        // write csv header on empty file
+        var file = require("Storage").open("sleeplog_" + sleeplog.debug.fileid + ".csv", "a");
+        if (!file.getLength()) file.write(
+          "timestamp,movement,status,consecutive,asleepSince,awakeSince,bpm,bpmConfidence\n"
+        );
+        // free ram
+        file = undefined;
+      } else {
+        // set debug as active
+        sleeplog.debug = true;
+      }
+    } else {
+      // disable debugging
+      delete sleeplog.debug;
+    }
+
+    // save status forced
+    sleeplog.saveStatus(true);
+  },
+
+  // define debugging function, called after logging if debug is set
+  debug: function(data) {
+    // check if global variable accessable and debug active
+    if (!global.sleeplog || !sleeplog.debug) return;
+
+    // set functions to convert timestamps
+    function localTime(timestamp) {
+      return timestamp ? Date(timestamp).toString().split(" ")[4].substr(0, 5) : "- - -";
+    }
+    function officeTime(timestamp) {
+      // days since 30.12.1899
+      return timestamp / 864E5 + 25569;
+    }
+
+    // generate console output
+    var console = "sleeplog: " +
+      localTime(data.timestamp) + " > " +
+      "movement: " + ("" + data.movement).padStart(4) + ", " +
+      "unknown    ,non consec.,consecutive".split(",")[sleeplog.consecutive] + " " +
+      "unknown,not worn,awake,light sleep,deep sleep".split(",")[data.status].padEnd(12) + ", " +
+      "asleep since: " + localTime(sleeplog.info.asleepSince) + ", " +
+      "awake since: " + localTime(sleeplog.info.awakeSince);
+    // add bpm if set
+    if (data.bpm) console += ", " +
+      "bpm: " + ("" + data.bpm).padStart(3) + ", " +
+      "confidence: " + data.bpmConfidence;
+    // output to console
+    print(console);
+
+    // check if debug is set as object with a file id and it is not past writeUntil
+    if (typeof sleeplog.debug === "object" && sleeplog.debug.fileid &&
+      Date.now() < sleeplog.debug.writeUntil) {
+      // generate next csv line
+      var csv = [
+        officeTime(data.timestamp),
+        data.movement,
+        data.status,
+        sleeplog.consecutive,
+        sleeplog.info.asleepSince ? officeTime(sleeplog.info.asleepSince) : "",
+        sleeplog.info.awakeSince ? officeTime(sleeplog.info.awakeSince) : "",
+        data.bpm || "",
+        data.bpmConfidence || ""
+      ].join(",");
+      // write next line to log if set
+      require("Storage").open("sleeplog_" + sleeplog.debug.fileid + ".csv", "a").write(csv + "\n");
+    } else {
+      // clear file setting in debug
+      sleeplog.debug = true;
+    }
+
+  },
+
+  // print log as humanreadable output similar to debug output
+  printLog: function(since, until) {
+    // set default until
+    until = until || Date.now();
+    // print each entry inside log
+    this.readLog(since, until).forEach((entry, index, log) => {
+      // calculate duration of this entry from 10min steps to minutes
+      var duration = ((log[index + 1] || [until / 6E5 | 0])[0] - entry[0]) * 10;
+      // print this entry
+      print((index + ")").padStart(4) + " " +
+        Date(entry[0] * 6E5).toString().substr(0, 21) + " > " +
+        "unknown    ,non consec.,consecutive".split(",")[entry[2]] + " " +
+        "unknown,not worn,awake,light sleep,deep sleep".split(",")[entry[1]].padEnd(12) +
+        "for" + (duration + "min").padStart(8));
     });
+  },
 
-    // write log
-    this.writeLog(logfile, log);
+  /** convert old (< v0.10) to new logfile data **/
+  convertOldLog: function() {
+    // read old logfile
+    var oldLog = require("Storage").read("sleeplog.log") || "";
+    // decode data if needed 
+    if (!oldLog.startsWith("[")) oldLog = atob(oldLog);
+    // delete old logfile and return if it is empty or corrupted
+    if (!oldLog.startsWith("[[") || !oldLog.endsWith("]]")) {
+      require("Storage").erase("sleeplog.log");
+      return;
+    }
 
-    // return output
-    return output;
+    // transform into StorageFile and clear oldLog to have more free ram accessable
+    require("Storage").open("sleeplog_old.log", "w").write(JSON.parse(oldLog).reverse().join("\n"));
+    oldLog = undefined;
+
+    // calculate fortnight from now
+    var fnOfNow = this.msToFn(Date.now());
+
+    // open StorageFile with old log data
+    var file = require("Storage").open("sleeplog_old.log", "r");
+    // define active fortnight and file cache
+    var activeFn = true;
+    var fileCache = [];
+    // loop through StorageFile entries
+    while (activeFn) {
+      // define fortnight for this entry
+      var thisFn = false;
+      // cache new line
+      var line = file.readLine();
+      // check if line is filled
+      if (line) {
+        // parse line
+        line = line.substr(0, 15).split(",").map(e => parseInt(e));
+        // calculate fortnight for this entry
+        thisFn = this.msToFn(line[0]);
+        // convert timestamp into 10min steps
+        line[0] = line[0] / 6E5 | 0;
+        // set consecutive to unknown
+        line.push(0);
+      }
+      // check if active fortnight and file cache is set, fortnight has changed and
+      //  active fortnight is not fortnight from now
+      if (activeFn && fileCache.length && activeFn !== thisFn && activeFn !== fnOfNow) {
+        // write file cache into new file according to fortnight
+        require("Storage").writeJSON("sleeplog_" + activeFn + ".log", fileCache);
+        // clear file cache
+        fileCache = [];
+      }
+      // add line to file cache if it is filled
+      if (line) fileCache.push(line);
+      // set active fortnight
+      activeFn = thisFn;
+    }
+    // check if entries are leftover
+    if (fileCache.length) {
+      // format fileCache entries into a string
+      fileCache = fileCache.map(e => e.join(",")).join("\n");
+      // read complete new log StorageFile as string
+      file = require("Storage").open("sleeplog.log", "r");
+      var newLogString = file.read(file.getLength());
+      // add entries at the beginning of the new log string
+      newLogString = fileCache + "\n" + newLogString;
+      // rewrite new log StorageFile
+      require("Storage").open("sleeplog.log", "w").write(newLogString);
+    }
+
+    // free ram
+    file = undefined;
+    fileCache = undefined;
+
+    // clean up old files
+    require("Storage").erase("sleeplog.log");
+    require("Storage").open("sleeplog_old.log", "w").erase();
   }
-
+  /** may be removed in later versions **/
 };
diff --git a/apps/sleeplog/metadata.json b/apps/sleeplog/metadata.json
index c4dbe8631..f6ce661e8 100644
--- a/apps/sleeplog/metadata.json
+++ b/apps/sleeplog/metadata.json
@@ -2,27 +2,32 @@
   "id":"sleeplog",
   "name":"Sleep Log",
   "shortName": "SleepLog",
-  "version": "0.06",
-  "description": "Log and view your sleeping habits. This app derived from SleepPhaseAlarm and uses also the principe of Estimation of Stationary Sleep-segments (ESS). It also provides a power saving mode using the built in movement calculation.",
+  "version": "0.11",
+  "description": "Log and view your sleeping habits. This app is using the built in movement calculation.",
   "icon": "app.png",
   "type": "app",
   "tags": "tool,boot",
   "supports": ["BANGLEJS2"],
   "readme": "README.md",
+  "interface": "interface.html",
   "storage": [
     {"name": "sleeplog.app.js", "url": "app.js"},
-    {"name": "sleeplog.img", "url": "app-icon.js", "evaluate":true},
+    {"name": "sleeplog.img", "url": "app-icon.js", "evaluate": true},
     {"name": "sleeplog.boot.js", "url": "boot.js"},
     {"name": "sleeplog", "url": "lib.js"},
     {"name": "sleeplog.settings.js", "url": "settings.js"}
   ],
   "data": [
-    {"name": "sleeplog.json"},
-    {"name": "sleeplog.log"}
+    {"name": "sleeplog.json"}
   ],
   "screenshots": [
-    {"url": "screenshot1.png"},
-    {"url": "screenshot2.png"},
-    {"url": "screenshot3.png"}
-   ]
+    {"url": "screenshot-1_app_light.png"},
+    {"url": "screenshot-2_day_light.png"},
+    {"url": "screenshot-3_graph_light.png"},
+    {"url": "screenshot-4_graph2_light.png"},
+    {"url": "screenshot-5_app_dark.png"},
+    {"url": "screenshot-6_day_dark.png"},
+    {"url": "screenshot-7_graph_dark.png"},
+    {"url": "screenshot-8_graph2_dark.png"}
+  ]
 }
diff --git a/apps/sleeplog/nolog.png b/apps/sleeplog/nolog.png
deleted file mode 100644
index b153b5769..000000000
Binary files a/apps/sleeplog/nolog.png and /dev/null differ
diff --git a/apps/sleeplog/off_20x20.png b/apps/sleeplog/off_20x20.png
new file mode 100644
index 000000000..abf3d3bfc
Binary files /dev/null and b/apps/sleeplog/off_20x20.png differ
diff --git a/apps/sleeplog/powersaving.png b/apps/sleeplog/powersaving.png
deleted file mode 100644
index ea487b48c..000000000
Binary files a/apps/sleeplog/powersaving.png and /dev/null differ
diff --git a/apps/sleeplog/screenshot-1_app_light.png b/apps/sleeplog/screenshot-1_app_light.png
new file mode 100644
index 000000000..f4c01773c
Binary files /dev/null and b/apps/sleeplog/screenshot-1_app_light.png differ
diff --git a/apps/sleeplog/screenshot-2_day_light.png b/apps/sleeplog/screenshot-2_day_light.png
new file mode 100644
index 000000000..61e0a60f6
Binary files /dev/null and b/apps/sleeplog/screenshot-2_day_light.png differ
diff --git a/apps/sleeplog/screenshot-3_graph_light.png b/apps/sleeplog/screenshot-3_graph_light.png
new file mode 100644
index 000000000..4b74afd25
Binary files /dev/null and b/apps/sleeplog/screenshot-3_graph_light.png differ
diff --git a/apps/sleeplog/screenshot-4_graph2_light.png b/apps/sleeplog/screenshot-4_graph2_light.png
new file mode 100644
index 000000000..300be5d05
Binary files /dev/null and b/apps/sleeplog/screenshot-4_graph2_light.png differ
diff --git a/apps/sleeplog/screenshot-5_app_dark.png b/apps/sleeplog/screenshot-5_app_dark.png
new file mode 100644
index 000000000..82e1f8c2f
Binary files /dev/null and b/apps/sleeplog/screenshot-5_app_dark.png differ
diff --git a/apps/sleeplog/screenshot-6_day_dark.png b/apps/sleeplog/screenshot-6_day_dark.png
new file mode 100644
index 000000000..a727f73e0
Binary files /dev/null and b/apps/sleeplog/screenshot-6_day_dark.png differ
diff --git a/apps/sleeplog/screenshot-7_graph_dark.png b/apps/sleeplog/screenshot-7_graph_dark.png
new file mode 100644
index 000000000..71612fa8e
Binary files /dev/null and b/apps/sleeplog/screenshot-7_graph_dark.png differ
diff --git a/apps/sleeplog/screenshot-8_graph2_dark.png b/apps/sleeplog/screenshot-8_graph2_dark.png
new file mode 100644
index 000000000..09c19e95c
Binary files /dev/null and b/apps/sleeplog/screenshot-8_graph2_dark.png differ
diff --git a/apps/sleeplog/screenshot1.png b/apps/sleeplog/screenshot1.png
deleted file mode 100644
index 200a305c4..000000000
Binary files a/apps/sleeplog/screenshot1.png and /dev/null differ
diff --git a/apps/sleeplog/screenshot2.png b/apps/sleeplog/screenshot2.png
deleted file mode 100644
index 61f580336..000000000
Binary files a/apps/sleeplog/screenshot2.png and /dev/null differ
diff --git a/apps/sleeplog/screenshot3.png b/apps/sleeplog/screenshot3.png
deleted file mode 100644
index 4a29b5008..000000000
Binary files a/apps/sleeplog/screenshot3.png and /dev/null differ
diff --git a/apps/sleeplog/settings.js b/apps/sleeplog/settings.js
index 11c7c0adb..9bf37ed69 100644
--- a/apps/sleeplog/settings.js
+++ b/apps/sleeplog/settings.js
@@ -1,144 +1,431 @@
 (function(back) {
+  // define settings filename
   var filename = "sleeplog.json";
+  // define logging prompt display status
+  var thresholdsPrompt = true;
 
-  // set storage and load settings
-  var storage = require("Storage");
-  var settings = Object.assign({
-    breaktod: 10, // time of day when to start/end graphs
-    maxawake: 36E5, // 60min in ms
-    minconsec: 18E5, // 30min in ms
-    tempthresh: 27, // every temperature above ist registered as worn
-    powersaving: false, // disables ESS and uses build in movement detection
-    maxmove: 100, // movement threshold on power saving mode
-    nomothresh: 0.012, // values lower than 0.008 getting triggert by noise
-    sleepthresh: 577, // 577 times no movement * 1.04s window width > 10min
-    winwidth: 13, // 13 values, read with 12.5Hz = every 1.04s
-    enabled: true, // en-/disable completely
-    logfile: "sleeplog.log", // logfile
-  }, storage.readJSON(filename, true) || {});
+  // define default vaules
+  var defaults = {
+    // main settings
+    enabled: true, //   en-/disable completely
+    // threshold settings
+    maxAwake: 36E5, //  [ms] maximal awake time to count for consecutive sleep
+    minConsec: 18E5, // [ms] minimal time to count for consecutive sleep
+    deepTh: 100, //     threshold for deep sleep
+    lightTh: 200, //    threshold for light sleep
+    // app settings
+    breakToD: 12, //    [h] time of day when to start/end graphs
+    appTimeout: 0 //   lock and backlight timeouts for the app
+  };
 
-  // write change to global.sleeplog and storage
-  function writeSetting(key, value) {
-    // change key in global.sleeplog
-    if (typeof global.sleeplog === "object") global.sleeplog[key] = value;
-    // reread settings to only change key
-    settings = Object.assign(settings, storage.readJSON(filename, true) || {});
-    // change the value of key
-    settings[key] = value;
-    // write to storage
-    storage.writeJSON(filename, settings);
+  // assign loaded settings to default values
+  var settings = Object.assign(defaults, require("Storage").readJSON(filename, true) || {});
+
+  // write change to storage
+  function writeSetting() {
+    require("Storage").writeJSON(filename, settings);
   }
 
-  // define function to change values that need a restart of the service
-  function changeRestart() {
-    require("sleeplog").setEnabled(settings.enabled, settings.logfile, settings.powersaving);
+  // plot a debug file
+  function plotDebug(filename) {    
+    // handle swipe events
+    function swipeHandler(x, y) {
+      if (x) {
+        start -= x;
+        if (start < 0 || maxStart && start > maxStart) {
+          start = start < 0 ? 0 : maxStart;
+        } else {
+          drawGraph();
+        }
+      } else {
+        minMove += y * 10;
+        if (minMove < 0 || minMove > 300) {
+          minMove = minMove < 0 ? 0 : 300;
+        } else {
+          drawGraph();
+        }
+      }
+    }
+    // handle touch events
+    function touchHandler() {
+      invert = !invert;
+      drawGraph();
+    }
+
+    // read required entries
+    function readEntries(count) {
+      // extract usabble data from line
+      function extract(line) {
+        if (!line) return;
+        line = line.trim().split(",");
+        return [Math.round((parseFloat(line[0]) - 25569) * 144), parseInt(line[1])];
+      }
+
+      // open debug file
+      var file = require("Storage").open(filename, "r");
+      // skip title
+      file.readLine();
+      // skip past entries
+      for (var i = 0; i < start * count; i++) { file.readLine(); }
+      // define data with first entry
+      var data = [extract(file.readLine())];
+      // get start time in 10min steps
+      var start10min = data[0][0];
+      // read first required entry
+      var line = extract(file.readLine());
+      
+      // read next count entries from file
+      while (data.length < count) {
+        // check if line is immediately after the last entry
+        if (line[0] === start10min + data.length) {
+          // add line to data
+          data.push(line);
+          // read new line
+          line = extract(file.readLine());
+          // stop if no more data available
+          if (!line) break;
+        } else {
+          // add line with unknown movement
+          data.push([start10min + data.length, 0]);
+        }
+      }
+
+      // free ram
+      file = undefined
+      // set this start as max, if less entries than expected
+      if (data.length < count) maxStart = start;
+      return data;
+    }
+
+    // draw graph at starting point
+    function drawGraph() {
+      // set correct or inverted drawing
+      function rect(fill, x0, y0, x1, y1) {
+        if (fill ^ invert) {
+          g.fillRect(x0, y0, x1, y1);
+        } else {
+          g.clearRect(x0, y0, x1, y1);
+        }
+      }
+
+      // set witdh
+      var width = g.getWidth();
+      // calculate entries to display (+ set width zero based)
+      var count = (width--) / 4;
+      // read required entries
+      var data = readEntries(count);
+
+      // clear app area
+      g.reset().clearRect(0, width - 13, width, width);
+      rect(false, 0, 24, width, width - 14);
+      // draw x axis
+      g.drawLine(0, width - 13, width, width - 13);
+      // draw x label
+      data.forEach((e, i) => {
+        var startTime = new Date(e[0] * 6E5);
+        if (startTime.getMinutes() === 0) {
+          g.fillRect(4 * i, width - 12, 4 * i, width - 9);
+          g.setFontAlign(-1, -1).setFont("6x8")
+            .drawString(startTime.getHours(), 4 * i + 1, width - 8);
+        } else if (startTime.getMinutes() === 30) {
+          g.fillRect(4 * i, width - 12, 4 * i, width - 11);
+        }
+      });
+
+      // calculate max height
+      var height = width - 38;
+      // cycle through entries
+      data.forEach((e, i) => {
+        // check if movement available 
+        if (e[1]) {
+          // set color depending on recognised status
+          var color = e[1] < deepTh ? 31 : e[1] < lightTh ? 2047 : 2016;
+          // correct according to min movement
+          e[1] -= minMove;
+          // keep movement in bounderies
+          e[1] = e[1] < 0 ? 0 : e[1] > height ? height : e[1];
+          // draw line and rectangle
+          g.reset();
+          rect(true, 4 * i, width - 14, 4 * i, width - 14 - e[1]);
+          g.setColor(color).fillRect(4 * i + 1, width - 14, 4 * i + 3, width - 14 - e[1]);
+        } else {
+          // draw error in red
+          g.setColor(63488).fillRect(4 * i, width - 14, 4 * i, width - 14 - height);
+        }
+      });
+      // draw threshold lines
+      [deepTh, lightTh].forEach(th => {
+        th -= minMove;
+        if (th > 0 && th < height) {
+          // draw line
+          g.reset();
+          rect(true, 0, width - 14 - th, width, width - 14 - th);
+          // draw value above or below line
+          var yAlign = th < height / 2 ? -1 : 1;
+          if (invert) g.setColor(1);
+          g.setFontAlign(1, yAlign).setFont("6x8")
+            .drawString(th + minMove, width - 2, width - 13 - th + 10 * yAlign);
+        }
+      });
+
+      // free ram
+      data = undefined;
+    }
+
+    // get thresholds
+    var deepTh = global.sleeplog ? sleeplog.conf.deepTh : defaults.deepTh;
+    var lightTh = global.sleeplog ? sleeplog.conf.lightTh : defaults.lightTh;
+    // set lowest movement displayed
+    var minMove = deepTh - 20;
+    // set start point
+    var start = 0;
+    // define max start point value
+    var maxStart = 0;
+    // define inverted color status
+    var invert = false;
+
+    // setup UI
+    Bangle.setUI({
+      mode: "custom",
+      back: selectDebug,
+      touch: touchHandler,
+      swipe: swipeHandler
+    });
+
+    // first draw
+    drawGraph(start);
   }
 
-  // calculate sleepthresh factor
-  var stFactor = settings.winwidth / 12.5 / 60;
+  // select a debug logfile
+  function selectDebug() {
+    // load debug files
+    var files = require("Storage").list(/^sleeplog_\d\d\d\d\d\d\.csv$/, {sf:true});
+
+    // check if no files found
+    if (!files.length) {
+      // show prompt
+      E.showPrompt( /*LANG*/"No debug files found.", {
+        title: /*LANG*/"Debug log",
+        buttons: {
+          /*LANG*/"Back": 0
+        }
+      }).then(showDebug);
+    } else {
+      // prepare scroller
+      const H = 40;
+      var menuIcon = "\0\f\f\x81\0\xFF\xFF\xFF\0\0\0\0\x0F\xFF\xFF\xF0\0\0\0\0\xFF\xFF\xFF";
+      // show scroller
+      E.showScroller({
+        h: H, c: files.length,
+        back: showDebug,
+        scrollMin : -24, scroll : -24, // title is 24px, rendered at -1
+          draw : (idx, r) => {
+            if (idx < 0) {
+              return g.setFont("12x20").setFontAlign(-1,0).drawString(menuIcon + " Select file", r.x + 12, r.y + H - 12);
+            } else {
+              g.setColor(g.theme.bg2).fillRect({x: r.x + 4, y: r.y + 2, w: r.w - 8, h: r.h - 4, r: 5});
+              var name = new Date(parseInt(files[idx].match(/\d\d\d\d\d\d/)[0]) * 36E5);
+              name = name.toString().slice(0, -12).split(" ").filter((e, i) => i !== 3).join(" ");
+              g.setColor(g.theme.fg2).setFont("12x20").setFontAlign(-1, 0).drawString(name, r.x + 12, r.y + H / 2);
+            }
+          },
+        select: (idx) => plotDebug(files[idx])
+      });
+    }
+  }
+
+  // show menu or promt to change debugging
+  function showDebug() {
+    // check if sleeplog is available
+    if (global.sleeplog) {
+      // get debug status, file and duration
+      var enabled = !!sleeplog.debug;
+      var file = typeof sleeplog.debug === "object";
+      var duration = 0;
+      // setup debugging menu
+      var debugMenu = {
+        "": {
+          title: /*LANG*/"Debugging"
+        },
+        /*LANG*/"< Back": () => {
+          // check if some value has changed
+          if (enabled !== !!sleeplog.debug || file !== (typeof sleeplog.debug === "object") || duration)
+            require("sleeplog").setDebug(enabled, file ? duration || 12 : undefined);
+          // redraw main menu
+          showMain(7);
+        },
+        /*LANG*/"View log": () => selectDebug(),
+        /*LANG*/"Enable": {
+          value: enabled,
+          onchange: v => enabled = v
+        },
+        /*LANG*/"write File": {
+          value: file,
+          onchange: v => file = v
+        },
+        /*LANG*/"Duration": {
+          value: file ? (sleeplog.debug.writeUntil - Date.now()) / 36E5 | 0 : 12,
+          min: 1,
+          max: 96,
+          wrap: true,
+          format: v => v + /*LANG*/ "h",
+          onchange: v => duration = v
+        },
+        /*LANG*/"Cancel": () => showMain(7),
+      };
+      // show menu
+      var menu = E.showMenu(debugMenu);
+    } else {
+      // show error prompt
+      E.showPrompt("Sleeplog" + /*LANG*/"not enabled!", {
+        title: /*LANG*/"Debugging",
+        buttons: {
+          /*LANG*/"Back": 7
+        }
+      }).then(showMain);
+    }
+  }
+
+  // show menu to change thresholds
+  function showThresholds() {
+    // setup logging menu
+    var menu;
+    var thresholdsMenu = {
+      "": {
+        title: /*LANG*/"Thresholds"
+      },
+      /*LANG*/"< Back": () => showMain(2),
+      /*LANG*/"Max Awake": {
+        value: settings.maxAwake / 6E4,
+        step: 10,
+        min: 10,
+        max: 120,
+        wrap: true,
+        noList: true,
+        format: v => v + /*LANG*/"min",
+        onchange: v => {
+          settings.maxAwake = v * 6E4;
+          writeSetting();
+        }
+      },
+      /*LANG*/"Min Consecutive": {
+        value: settings.minConsec / 6E4,
+        step: 10,
+        min: 10,
+        max: 120,
+        wrap: true,
+        noList: true,
+        format: v => v + /*LANG*/"min",
+        onchange: v => {
+          settings.minConsec = v * 6E4;
+          writeSetting();
+        }
+      },
+      /*LANG*/"Deep Sleep": {
+        value: settings.deepTh,
+        step: 1,
+        min: 30,
+        max: 200,
+        wrap: true,
+        noList: true,
+        onchange: v => {
+          settings.deepTh = v;
+          writeSetting();
+        }
+      },
+      /*LANG*/"Light Sleep": {
+        value: settings.lightTh,
+        step: 10,
+        min: 100,
+        max: 400,
+        wrap: true,
+        noList: true,
+        onchange: v => {
+          settings.lightTh = v;
+          writeSetting();
+        }
+      },
+      /*LANG*/"Reset to Default": () => {
+        settings.maxAwake = defaults.maxAwake;
+        settings.minConsec = defaults.minConsec;
+        settings.deepTh = defaults.deepTh;
+        settings.lightTh = defaults.lightTh;
+        writeSetting();
+        showThresholds();
+      }
+    };
+
+    // display info/warning prompt or menu
+    if (thresholdsPrompt) {
+      thresholdsPrompt = false;
+      E.showPrompt("Changes take effect from now on, not retrospective", {
+        title: /*LANG*/"Thresholds",
+        buttons: {
+          /*LANG*/"Ok": 0
+        }
+      }).then(() => menu = E.showMenu(thresholdsMenu));
+    } else {
+      menu = E.showMenu(thresholdsMenu);
+    }
+  }
 
   // show main menu
   function showMain(selected) {
+    // set debug image
+    var debugImg = !global.sleeplog ?
+      "FBSBAOAAfwAP+AH3wD4+B8Hw+A+fAH/gA/wAH4AB+AA/wAf+APnwHw+D4Hx8A++AH/AA/gAH" : // X
+      typeof sleeplog.debug === "object" ?
+      "FBSBAB/4AQDAF+4BfvAX74F+CBf+gX/oFJKBf+gUkoF/6BSSgX/oFJ6Bf+gX/oF/6BAAgf/4" : // file
+      sleeplog.debug ?
+      "FBSBAP//+f/V///4AAGAABkAAZgAGcABjgAYcAGDgBhwAY4AGcABmH+ZB/mAABgAAYAAH///" : // console
+      0; // off
+    debugImg = debugImg ? "\0" + atob(debugImg) : false;
+    // set menu
     var mainMenu = {
       "": {
         title: "Sleep Log",
         selected: selected
       },
-      "Exit": () => load(),
-      "< Back": () => back(),
-      "Break Tod": {
-        value: settings.breaktod,
+      /*LANG*/"< Back": () => back(),
+      /*LANG*/"Thresholds": () => showThresholds(),
+      /*LANG*/"Break ToD": {
+        value: settings.breakToD,
         step: 1,
         min: 0,
         max: 23,
         wrap: true,
-        onchange: v => writeSetting("breaktod", v),
-      },
-      "Max Awake": {
-        value: settings.maxawake / 6E4,
-        step: 5,
-        min: 15,
-        max: 120,
-        wrap: true,
-        format: v => v + "min",
-        onchange: v => writeSetting("maxawake", v * 6E4),
-      },
-      "Min Consec": {
-        value: settings.minconsec / 6E4,
-        step: 5,
-        min: 15,
-        max: 120,
-        wrap: true,
-        format: v => v + "min",
-        onchange: v => writeSetting("minconsec", v * 6E4),
-      },
-      "Temp Thresh": {
-        value: settings.tempthresh,
-        step: 0.5,
-        min: 20,
-        max: 40,
-        wrap: true,
-        format: v => v + "°C",
-        onchange: v => writeSetting("tempthresh", v),
-      },
-      "Power Saving": {
-        value: settings.powersaving,
-        format: v => v ? "on" : "off",
-        onchange: function(v) {
-          settings.powersaving = v;
-          changeRestart();
-          // redraw menu with changed entries subsequent to onchange
-          // https://github.com/espruino/Espruino/issues/2149
-          setTimeout(showMain, 1, 6);
+        noList: true,
+        format: v => v + ":00",
+        onchange: v => {
+          settings.breakToD = v;
+          writeSetting();
         }
       },
-      "Max Move": {
-        value: settings.maxmove,
-        step: 1,
-        min: 50,
-        max: 200,
+      /*LANG*/"App Timeout": {
+        value: settings.appTimeout / 1E3,
+        step: 10,
+        min: 0,
+        max: 120,
         wrap: true,
-        onchange: v => writeSetting("maxmove", v),
+        noList: true,
+        format: v => v ? v + "s" : "-",
+        onchange: v => {
+          settings.appTimeout = v * 1E3;
+          writeSetting();
+        }
       },
-      "NoMo Thresh": {
-        value: settings.nomothresh,
-        step: 0.001,
-        min: 0.006,
-        max: 0.02,
-        wrap: true,
-        format: v => ("" + v).padEnd(5, "0"),
-        onchange: v => writeSetting("nomothresh", v),
-      },
-      "Min Duration": {
-        value: Math.floor(settings.sleepthresh * stFactor),
-        step: 1,
-        min: 5,
-        max: 15,
-        wrap: true,
-        format: v => v + "min",
-        onchange: v => writeSetting("sleepthresh", Math.ceil(v / stFactor)),
-      },
-      "Enabled": {
+      /*LANG*/"Enabled": {
         value: settings.enabled,
-        format: v => v ? "on" : "off",
-        onchange: function(v) {
+        onchange: v => {
           settings.enabled = v;
-          changeRestart();
+          require("sleeplog").setEnabled(v);
         }
       },
-      "Logfile ": {
-        value: settings.logfile === "sleeplog.log" ? true : (settings.logfile || "").endsWith(".log") ? "custom" : false,
-        format: v => v === true ? "default" : v ? "custom" : "off",
-        onchange: function(v) {
-          if (v !== "custom") {
-            settings.logfile = v ? "sleeplog.log" : false;
-            changeRestart();
-          }
-        }
+      /*LANG*/"Debugging": {
+        value: debugImg,
+        onchange: () => setTimeout(showDebug, 10)
       }
     };
-    // check power saving mode to delete unused entries
-    (settings.powersaving ? ["NoMo Thresh", "Min Duration"] : ["Max Move"]).forEach(property => delete mainMenu[property]);
     var menu = E.showMenu(mainMenu);
   }
 
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 b3cf16ac7..5c4a9fa75 100644
--- a/apps/slidingtext/ChangeLog
+++ b/apps/slidingtext/ChangeLog
@@ -7,3 +7,5 @@
 0.07: Support for Bangle.js 2 and themes
 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 15707c1ad..098fdb747 100644 --- a/apps/slidingtext/metadata.json +++ b/apps/slidingtext/metadata.json @@ -1,15 +1,15 @@ { "id": "slidingtext", "name": "Sliding Clock", - "version": "0.09", + "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 ae6571121..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,23 +789,23 @@ 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) button3pressed(); +}); Bangle.loadWidgets(); Bangle.drawWidgets(); startTimers(); -// Show launcher when button pressed -Bangle.setUI("clockupdown", d=>{ - if (d<0) button1pressed(); - if (d>0) button3pressed(); -}); + 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 bf128e2fb..b09100f50 100644 --- a/apps/smpltmr/ChangeLog +++ b/apps/smpltmr/ChangeLog @@ -1,2 +1,4 @@ 0.01: Release -0.02: Rewrite with new interface \ No newline at end of file +0.02: Rewrite with new interface +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 new file mode 100644 index 000000000..1a63a9b7e --- /dev/null +++ b/apps/smpltmr/clkinfo.js @@ -0,0 +1,98 @@ +(function() { + const TIMER_IDX = "smpltmr"; + + function isAlarmEnabled(){ + try{ + var alarm = require('sched'); + var alarmObj = alarm.getAlarm(TIMER_IDX); + if(alarmObj===undefined || !alarmObj.on){ + return false; + } + + return true; + + } catch(ex){ } + return false; + } + + function getAlarmMinutes(){ + if(!isAlarmEnabled()){ + return -1; + } + + var alarm = require('sched'); + var alarmObj = alarm.getAlarm(TIMER_IDX); + return Math.round(alarm.getTimeToAlarm(alarmObj)/(60*1000)); + } + + function getAlarmMinutesText(){ + var min = getAlarmMinutes(); + if(min < 0){ + return "OFF"; + } + + return "T-" + String(min); + } + + function increaseAlarm(t){ + try{ + var minutes = isAlarmEnabled() ? getAlarmMinutes() : 0; + var alarm = require('sched') + alarm.setAlarm(TIMER_IDX, { + timer : (minutes+t)*60*1000, + }); + alarm.reload(); + } catch(ex){ } + } + + function decreaseAlarm(t){ + try{ + var minutes = getAlarmMinutes(); + minutes -= t; + + var alarm = require('sched') + alarm.setAlarm(TIMER_IDX, undefined); + + if(minutes > 0){ + alarm.setAlarm(TIMER_IDX, { + timer : minutes*60*1000, + }); + } + + alarm.reload(); + } catch(ex){ } + } + + var img = atob("GBiBAeAAB+AAB/v/3/v/3/v/3/v/3/v/n/n/H/z+P/48//85//+b//+b//8p//4E//yCP/kBH/oAn/oAX/oAX/oAX/oAX+AAB+AABw==") + var smpltmrItems = { + name: "Timer", + img: img, + items: [ + { + name: null, + get: () => ({ text: getAlarmMinutesText() + (isAlarmEnabled() ? " min" : ""), img: null}), + show: function() { smpltmrItems.items[0].emit("redraw"); }, + hide: function () {}, + run: function() { } + }, + ] + }; + + var offsets = [+5,-5]; + offsets.forEach((o, i) => { + smpltmrItems.items = smpltmrItems.items.concat({ + 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; + } + }); + }); + + return smpltmrItems; +}) \ No newline at end of file diff --git a/apps/smpltmr/metadata.json b/apps/smpltmr/metadata.json index cb1ef6eab..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.02", + "version": "0.04", "description": "A very simple app to start a timer.", "icon": "app.png", "tags": "tool,alarm,timer", @@ -12,6 +12,7 @@ "readme": "README.md", "storage": [ {"name":"smpltmr.app.js","url":"app.js"}, + {"name":"smpltmr.clkinfo.js","url":"clkinfo.js"}, {"name":"smpltmr.img","url":"app-icon.js","evaluate":true} ] } diff --git a/apps/speedalt/README.md b/apps/speedalt/README.md index 6f0d4efe5..e629f94bb 100644 --- a/apps/speedalt/README.md +++ b/apps/speedalt/README.md @@ -2,7 +2,7 @@ You can switch between three display modes. One showing speed and altitude (A), one showing speed and distance to waypoint (D) and a large dispay of time and selected waypoint. -Within the [A]ltitude and [D]istance displays modes one figure is displayed on the watch face using the largest possible characters depending on the number of digits. The other is in a smaller characters below that. Both are always visible. You can display the current or maximum observed speed/altitude values. Current time is always displayed. +Within the [A]ltitude and [D]istance displays modes one figure is displayed on the watch face using the largest possible characters depending on the number of digits. The other is in a smaller characters below that. Both are always visible. You can display the current or maximum observed speed/altitude values. Current time is always displayed. The waypoints list is the same as that used with the [GPS Navigation](https://banglejs.com/apps/#gps%20navigation) app so the same set of waypoints can be used across both apps. Refer to that app for waypoint file information. @@ -36,7 +36,7 @@ BTN4 : Left Display Tap : Swaps which figure is in the large display. You can ha ## App Settings -Select the desired display units. Speed can be as per the default locale, kph, knots, mph or m/s. Distance can be km, miles or nautical miles. Altitude can be feet or metres. Select one of three colour schemes. Default (three colours), high contrast (all white on black) or night ( all red on black ). +Select the desired display units. Speed can be as per the default locale, kph, knots, mph or m/s. Distance can be km, miles or nautical miles. Altitude can be feet or metres. Select one of three colour schemes. Default (three colours), high contrast (all white on black) or night ( all red on black ). ## Kalman Filter @@ -67,13 +67,11 @@ This app will work quite happily on its own but will use the [GPS Setup App](htt When using the GPS Setup App this app switches the GPS to SuperE (default) mode while the display is lit and showing fix information. This ensures that that fixes are updated every second or so. 10 seconds after the display is blanked by the watch this app will switch the GPS to PSMOO mode and will only attempt to get a fix every two minutes. This improves power saving while the display is off and the delay gives an opportunity to restore the display before the GPS power mode is switched. -The MAX values continue to be collected with the display off so may appear a little odd after the intermittent fixes of the low power mode. +The MAX values continue to be collected with the display off so may appear a little odd after the intermittent fixes of the low power mode. ## Waypoints -Waypoints are used in [D]istance mode. Create a file waypoints.json and write to storage on the Bangle.js using the IDE. The first 6 characters of the name are displayed in Speed+[D]istance mode. - -The [GPS Navigation](https://banglejs.com/apps/#gps%20navigation) app in the App Loader has a really nice waypoints file editor. (Must be connected to your Bangle.JS and then click on the Download icon.) +Waypoints are used in Distance and VMG modes. See the `Waypoints` app for information on how to create/edit waypoints. The first 6 characters of the name are displayed in Speed+[D]istance mode. Sample waypoints.json (My sailing waypoints) @@ -149,5 +147,3 @@ Developed for my use in sailing, cycling and motorcycling. If you find this soft Many thanks to Gordon Williams. Awesome job. Special thanks also to @jeffmer, for the [GPS Navigation](https://banglejs.com/apps/#gps%20navigation) app and @hughbarney for the Low power GPS code development and Wouter Bulten for the Kalman filter code. - - diff --git a/apps/speedalt/app.js b/apps/speedalt/app.js index 79db932db..4587252c2 100644 --- a/apps/speedalt/app.js +++ b/apps/speedalt/app.js @@ -209,7 +209,7 @@ function nxtWp(inc){ } function loadWp() { - var w = require("Storage").readJSON('waypoints.json')||[{name:"NONE"}]; + var w = require("waypoints").load(); if (cfg.wp>=w.length) cfg.wp=0; if (cfg.wp<0) cfg.wp = w.length-1; savSettings(); diff --git a/apps/speedalt/metadata.json b/apps/speedalt/metadata.json index b23d2692c..89bfd4a57 100644 --- a/apps/speedalt/metadata.json +++ b/apps/speedalt/metadata.json @@ -8,12 +8,12 @@ "type": "app", "tags": "tool,outdoors", "supports": ["BANGLEJS","BANGLEJS2"], + "dependencies" : { "waypoints":"type" }, "readme": "README.md", "allow_emulator": true, "storage": [ {"name":"speedalt.app.js","url":"app.js"}, {"name":"speedalt.img","url":"app-icon.js","evaluate":true}, {"name":"speedalt.settings.js","url":"settings.js"} - ], - "data": [{"name":"speedalt.json"}] + ] } diff --git a/apps/speedalt2/ChangeLog b/apps/speedalt2/ChangeLog index 0e54d5db3..ec76c6a16 100644 --- a/apps/speedalt2/ChangeLog +++ b/apps/speedalt2/ChangeLog @@ -15,3 +15,4 @@ 0.15: Droidscript mirroring prog automatically uses last connection address. Auto connects when run. 0.16: Add configuration item Wpt File Suffix. A one character suffix to append to the waypoints.json file. A number of other apps also use this file name. Using the file name suffix allows the speedalt2 waypoints to be retained if one of these other apps is installed for a different use. 0.17: Use default Bangle formatter for booleans +0.18: Move waypoints.json to 'waypoints' app (with editor) diff --git a/apps/speedalt2/README.md b/apps/speedalt2/README.md index a86f787cf..d6a5e1d71 100644 --- a/apps/speedalt2/README.md +++ b/apps/speedalt2/README.md @@ -25,7 +25,7 @@ Touch functions as BTN1 short press. ## App Settings -Select the desired display units. Speed can be as per the default locale, kph, knots, mph or m/s. Distance can be km, miles or nautical miles. Altitude can be feet or metres. Select one of three colour schemes. Default (three colours), high contrast (all white on black) or night ( all red on black ). +Select the desired display units. Speed can be as per the default locale, kph, knots, mph or m/s. Distance can be km, miles or nautical miles. Altitude can be feet or metres. Select one of three colour schemes. Default (three colours), high contrast (all white on black) or night ( all red on black ). ## Kalman Filter @@ -43,7 +43,7 @@ This app will work quite happily on its own but will use the [GPS Setup App](htt When using the GPS Setup App this app switches the GPS to SuperE (default) mode while the display is lit and showing fix information. This ensures that that fixes are updated every second or so. 10 seconds after the display is blanked by the watch this app will switch the GPS to PSMOO mode and will only attempt to get a fix every two minutes. This improves power saving while the display is off and the delay gives an opportunity to restore the display before the GPS power mode is switched. -The MAX values continue to be collected with the display off so may appear a little odd after the intermittent fixes of the low power mode. +The MAX values continue to be collected with the display off so may appear a little odd after the intermittent fixes of the low power mode. ## Velocity Made Good - VMG @@ -63,9 +63,9 @@ The Droidscript script file is called : **GPS Adv Sports II.js** **Important Gotcha :** For the BLE comms to find and connect to the Bangle.js it must be paired with the Android device but **NOT** connected. The Bangle.js must have been set in the Bluetooth settings as connectable. -Start/Stop buttons tell the Bangle.js to start or stop sending BLE data packets to the Android device. While stopped the Bangle.js reverts to full power saving mode when the screen is asleep. +Start/Stop buttons tell the Bangle.js to start or stop sending BLE data packets to the Android device. While stopped the Bangle.js reverts to full power saving mode when the screen is asleep. -When running a blue 'led' will flash each time a data packet is recieved to refresh the android display. +When running a blue 'led' will flash each time a data packet is recieved to refresh the android display. An orange 'led' will flash for each reconnection attempt if no data is received for 30 seconds. It will keep trying to reconnect so you can restart the Bangle, run another Bangle app or temprarily turn off bluetooth. The android mirror display will automatically reconnect when the GPS Adv Sports II app is running on the Bangle again. ( Designed to leave the Android device running as the display mirror in a sealed case all day while retaining the ability to do other functions on the Bangle.js and returning to the GPS Speed Alt II app. ) @@ -74,14 +74,12 @@ Android Screen Mirroring:
## Waypoints -Waypoints are used in Distance and VMG modes. Create a file waypoints.json and write to storage on the Bangle.js using the IDE. The first 6 characters of the name are displayed in Speed+[D]istance mode. - -The [GPS Navigation](https://banglejs.com/apps/#gps%20navigation) app in the App Loader has a really nice waypoints file editor. (Must be connected to your Bangle.JS and then click on the Download icon.) +Waypoints are used in Distance and VMG modes. See the `Waypoints` app for information on how to create/edit waypoints. The first 6 characters of the name are displayed in Speed+[D]istance mode. By default the waypoints file is called waypoints.json -**Note** : The waypoints.json file is used by a number of different gps apps. The setting 'Wpt File Suffix' allows one of waypoints1.json, waypoints2.json or waypoints3.json to be used instead. This allows the other apps to be used with a different set of waypoints without losing the speedalt2 waypoint set. - +**Note** : The waypoints.json file is used by a number of different gps apps. The setting 'Wpt File Suffix' allows one of waypoints.1.json, waypoints.2.json or waypoints.3.json to be used instead. This allows the other apps to be used with a different set of waypoints without losing the speedalt2 waypoint set. + Sample waypoints.json (My sailing waypoints)
@@ -156,4 +154,3 @@ Developed for my use in sailing, cycling and motorcycling. If you find this soft
 Many thanks to Gordon Williams. Awesome job.
 
 Special thanks also to @jeffmer, for the [GPS Navigation](https://banglejs.com/apps/#gps%20navigation) app and @hughbarney for the Low power GPS code development and Wouter Bulten for the Kalman filter code.
-
diff --git a/apps/speedalt2/app.js b/apps/speedalt2/app.js
index 4cdf71913..839ac63eb 100644
--- a/apps/speedalt2/app.js
+++ b/apps/speedalt2/app.js
@@ -185,7 +185,7 @@ let LED = // LED as minimal and only definition (as instance / singleton)
 , reset: function() { this.set(false); } // turn off
 , write: function(v) { this.set(v); }  // turn on w/ no arg or truey, else off
 , toggle: function() { this.set( ! this.isOn); } // toggle the LED
-}, LED1 = LED; // LED1 as 'synonym' for LED 
+}, LED1 = LED; // LED1 as 'synonym' for LED
 
 
 var lf = {fix:0,satellites:0};
@@ -210,7 +210,7 @@ function nxtWp(){
 }
 
 function loadWp() {
-  var w = require("Storage").readJSON('waypoints'+cfg.wptSfx+'.json')||[{name:"NONE"}];
+  var w = require("waypoints").load(cfg.wptSfx);
   if (cfg.wp>=w.length) cfg.wp=0;
   if (cfg.wp<0) cfg.wp = w.length-1;
   savSettings();
@@ -239,7 +239,7 @@ function bearing(a,b){
 function distance(a,b){
   var x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2));
   var y = radians(b.lat-a.lat);
-  
+
   // Distance in metres
   var d = Math.sqrt(x*x + y*y) * 6371000;
   return d;
@@ -251,38 +251,38 @@ function drawScrn(dat) {
 
   buf.clear();
   buf.setBgColor(0);
-  
+
   var n;
   n = dat.val.toString();
-  
+
   var s=50;    // Font size
   var l=n.length;
-  
+
   if ( l <= 7 ) s=55;
   if ( l <= 6 ) s=60;
   if ( l <= 5 ) s=80;
   if ( l <= 4 ) s=100;
   if ( l <= 3 ) s=120;
-        
-  buf.setFontAlign(0,0); //Centre 
-  buf.setColor(1);  
+
+  buf.setFontAlign(0,0); //Centre
+  buf.setColor(1);
   buf.setFontVector(s);
   buf.drawString(n,126,52);
 
-  
+
   // Primary Units
   buf.setFontAlign(-1,1); //left, bottom
-  buf.setColor(2);  
+  buf.setColor(2);
   buf.setFontVector(35);
-  buf.drawString(dat.unit,5,164);  
-  
+  buf.drawString(dat.unit,5,164);
+
   drawMax(dat.max); // MAX display indicator
   drawWP(dat.wp);  // Waypoint name
   drawSats(dat.sats);
-   
+
   g.reset();
   g.drawImage(img,0,40);
-  
+
   LED1.write(!pwrSav);
 
 }
@@ -295,7 +295,7 @@ function drawPosn(dat) {
   var x, y;
   x=210;
   y=0;
-  buf.setFontAlign(1,-1); 
+  buf.setFontAlign(1,-1);
   buf.setFontVector(60);
   buf.setColor(1);
 
@@ -320,45 +320,45 @@ function drawPosn(dat) {
 
 function drawClock() {
   if (!canDraw) return;
-  
+
   buf.clear();
   buf.setBgColor(0);
-  
+
   var x, y;
   x=185;
   y=0;
-  buf.setFontAlign(1,-1); 
+  buf.setFontAlign(1,-1);
   buf.setFontVector(94);
   time = require("locale").time(new Date(),1);
-  
+
   buf.setColor(1);
-  
+
   buf.drawString(time.substring(0,2),x,y);
   buf.drawString(time.substring(3,5),x,y+80);
-  
+
   g.reset();
   g.drawImage(img,0,40);
-  
+
   LED1.write(!pwrSav);
 }
 
 function drawWP(wp) {
-  buf.setColor(3);  
+  buf.setColor(3);
   buf.setFontAlign(0,1); //left, bottom
   buf.setFontVector(40);
-  buf.drawString(wp,120,132);  
+  buf.drawString(wp,120,132);
 }
 
 function drawSats(sats) {
-  buf.setColor(3);  
+  buf.setColor(3);
   buf.setFont("6x8", 2);
   buf.setFontAlign(1,1); //right, bottom
-  buf.drawString(sats,240,160);  
+  buf.drawString(sats,240,160);
 }
 
 function drawMax(max) {
   buf.setFontVector(30);
-  buf.setColor(2); 
+  buf.setColor(2);
   buf.setFontAlign(0,1); //centre, bottom
   buf.drawString(max,120,164);
 }
@@ -369,7 +369,7 @@ if ( emulator ) {
     fix.speed = 10 + (Math.random()*5);
     fix.alt = 354 + (Math.random()*50);
     fix.lat = -38.92;
-    fix.lon = 175.7613350;   
+    fix.lon = 175.7613350;
     fix.course = 245;
     fix.satellites = 12;
     fix.time = new Date();
@@ -378,7 +378,7 @@ if ( emulator ) {
 
   var m;
 
-  var sp = '---';        
+  var sp = '---';
   var al = sp;
   var di = sp;
   var brg = ''; // bearing
@@ -390,15 +390,15 @@ if ( emulator ) {
   var lon = '---.--';
   var sats = sp;
   var vmg = sp;
-  
-  
+
+
   // Waypoint name
   var wpName = wp.name;
   if ( wpName == undefined || wpName == 'NONE' ) wpName = '';
-  wpName = wpName.substring(0,8);  
+  wpName = wpName.substring(0,8);
 
   if (fix.fix) lf = fix;
-  
+
   if (lf.fix) {
 
     // Smooth data
@@ -408,10 +408,10 @@ if ( emulator ) {
       lf.smoothed = 1;
       if ( maxN <= 15 ) maxN++;
     }
-    
+
     // Bearing to waypoint
     brg = bearing(lf,wp);
-    
+
     // Current course
     crs = lf.course;
 
@@ -447,19 +447,19 @@ if ( emulator ) {
     if ( di >= 100 ) di = parseFloat(di).toFixed(1);
     if ( di >= 1000 ) di = parseFloat(di).toFixed(0);
     if (isNaN(di)) di = '------';
-    
+
     // Age of last fix (secs)
     age = Math.max(0,Math.round(getTime())-(lf.time.getTime()/1000));
 
     // Lat / Lon
     ns = 'N';
     if ( lf.lat < 0 ) ns = 'S';
-    lat = Math.abs(lf.lat.toFixed(2)); 
+    lat = Math.abs(lf.lat.toFixed(2));
 
     ew = 'E';
     if ( lf.lon < 0 ) ew = 'W';
-    lon = Math.abs(lf.lon.toFixed(2)); 
-    
+    lon = Math.abs(lf.lon.toFixed(2));
+
     // Sats
     if ( age > 10 ) {
       sats = 'Age:'+Math.round(age);
@@ -573,7 +573,7 @@ if ( emulator ) {
         ew:ew
       });
   }
-  
+
   if ( cfg.modeA == 5 )  {
     // Large clock
     drawClock();
@@ -585,14 +585,14 @@ function prevScrn() {
     cfg.modeA = cfg.modeA-1;
     if ( cfg.modeA < 0 ) cfg.modeA = 5;
     savSettings();
-    onGPS(lf); 
+    onGPS(lf);
 }
 
 function nextScrn() {
     cfg.modeA = cfg.modeA+1;
     if ( cfg.modeA > 5 ) cfg.modeA = 0;
     savSettings();
-    onGPS(lf); 
+    onGPS(lf);
 }
 
 // Next function on a screen
@@ -610,7 +610,7 @@ function nextFunc(dur) {
 function updateClock() {
   if (!canDraw) return;
   if ( cfg.modeA != 5 )  return;
-  drawClock(); 
+  drawClock();
   if ( emulator ) {maxSpd++;maxAlt++;}
 }
 
@@ -661,10 +661,10 @@ function setButtons(){
     var dur = e.time - e.lastTime;
     nextFunc(dur);
   }, BTN1, { edge:"falling",repeat:true});
-  
-  // Power saving on/off 
+
+  // Power saving on/off
   setWatch(function(e){
-    pwrSav=!pwrSav; 
+    pwrSav=!pwrSav;
     if ( pwrSav ) {
       var s = require('Storage').readJSON('setting.json',1)||{};
       var t = s.timeout||10;
@@ -676,7 +676,7 @@ function setButtons(){
     }
       LED1.write(!pwrSav);
   }, BTN2, {repeat:true,edge:"falling"});
-  
+
   // BTN3 - next screen
   setWatch(function(e){
     nextScrn();
@@ -685,7 +685,7 @@ function setButtons(){
 
 Bangle.on('lcdPower',function(on) {
   if (!SCREENACCESS.withApp) return;
-  if (on) startDraw(); 
+  if (on) startDraw();
   else stopDraw();
 });
 
@@ -702,7 +702,7 @@ Bangle.on('touch', function(button){
 
 // == Main Prog
 
-// Read settings. 
+// Read settings.
 let cfg = require('Storage').readJSON('speedalt2.json',1)||{};
 
 cfg.spd = cfg.spd||1;  // Multiplier for speed unit conversions. 0 = use the locale values for speed
@@ -713,13 +713,13 @@ cfg.dist = cfg.dist||1000;// Multiplier for distnce unit conversions.
 cfg.dist_unit = cfg.dist_unit||'km';  // Displayed altitude units
 cfg.colour = cfg.colour||0;          // Colour scheme.
 cfg.wp = cfg.wp||0;        // Last selected waypoint for dist
-cfg.modeA = cfg.modeA||0;    // 0=Speed 1=Alt 2=Dist 3 = vmg 4=Position 5=Clock 
+cfg.modeA = cfg.modeA||0;    // 0=Speed 1=Alt 2=Dist 3 = vmg 4=Position 5=Clock
 cfg.primSpd = cfg.primSpd||0;    // 1 = Spd in primary, 0 = Spd in secondary
 
-cfg.spdFilt = cfg.spdFilt==undefined?true:cfg.spdFilt; 
+cfg.spdFilt = cfg.spdFilt==undefined?true:cfg.spdFilt;
 cfg.altFilt = cfg.altFilt==undefined?true:cfg.altFilt;
 cfg.touch = cfg.touch==undefined?true:cfg.touch;
-cfg.wptSfx = cfg.wptSfx==undefined?'':cfg.wptSfx; 
+cfg.wptSfx = cfg.wptSfx==undefined?'':cfg.wptSfx;
 
 if ( cfg.spdFilt ) var spdFilter = new KalmanFilter({R: 0.1 , Q: 1 });
 if ( cfg.altFilt ) var altFilter = new KalmanFilter({R: 0.01, Q: 2 });
@@ -749,7 +749,7 @@ var SCREENACCESS = {
       withApp:true,
       request:function(){this.withApp=false;stopDraw();},
       release:function(){this.withApp=true;startDraw();}
-}; 
+};
 
 var gpssetup;
 try {
diff --git a/apps/speedalt2/metadata.json b/apps/speedalt2/metadata.json
index ae513acd5..2c8d37f79 100644
--- a/apps/speedalt2/metadata.json
+++ b/apps/speedalt2/metadata.json
@@ -2,12 +2,13 @@
   "id": "speedalt2",
   "name": "GPS Adventure Sports II",
   "shortName":"GPS Adv Sport II",
-  "version":"0.17",
+  "version":"0.18",
   "description": "GPS speed, altitude and distance to waypoint display. Designed for easy viewing and use during outdoor activities such as para-gliding, hang-gliding, sailing, cycling etc.",
   "icon": "app.png",
   "type": "app",
   "tags": "tool,outdoors",
   "supports": ["BANGLEJS"],
+  "dependencies" : { "waypoints":"type" },
   "readme": "README.md",
   "allow_emulator": true,
   "storage": [
@@ -16,10 +17,6 @@
     {"name":"speedalt2.settings.js","url":"settings.js"}
   ],
   "data": [
-    {"name":"speedalt2.json"},
-    {"name":"waypoints.json"},
-    {"name":"waypoints1.json"},
-    {"name":"waypoints2.json"},
-    {"name":"waypoints3.json"}
+    {"name":"speedalt2.json"}
   ]
 }
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/stardateclock/ChangeLog b/apps/stardateclock/ChangeLog
index 1b1719352..bb6430b65 100644
--- a/apps/stardateclock/ChangeLog
+++ b/apps/stardateclock/ChangeLog
@@ -1,2 +1,3 @@
 0.01: Initial release on the app repository for Bangle.js 1 and 2
 0.02: Fixed let/const usage while using firmware version >=2v14
+0.03: Tell clock widgets to hide.
diff --git a/apps/stardateclock/app.js b/apps/stardateclock/app.js
index cdc730970..adf2c14c7 100644
--- a/apps/stardateclock/app.js
+++ b/apps/stardateclock/app.js
@@ -334,10 +334,10 @@ updateStardate();
 updateConventionalTime();
 // Make sure widgets can be shown.
 //g.setColor("#FF0000"); g.fillRect(0, 0, g.getWidth(), widgetsHeight); // debug
-Bangle.loadWidgets();
-Bangle.drawWidgets();
 // Show launcher on button press as usual for a clock face
 Bangle.setUI("clock", Bangle.showLauncher);
+Bangle.loadWidgets();
+Bangle.drawWidgets();
 // Stop updates when LCD is off, restart when on
 Bangle.on('lcdPower', on => {
   if (on) {
diff --git a/apps/stardateclock/metadata.json b/apps/stardateclock/metadata.json
index 9569d3a53..d27b14512 100644
--- a/apps/stardateclock/metadata.json
+++ b/apps/stardateclock/metadata.json
@@ -3,7 +3,7 @@
   "name":"Stardate Clock",
   "shortName":"Stardate Clock",
   "description": "A clock displaying a stardate along with a 'standard' digital/analog clock in LCARS design",
-  "version":"0.02",
+  "version":"0.03",
   "icon": "app.png",
   "type":"clock",
   "tags": "clock",
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/swscroll/ChangeLog b/apps/swscroll/ChangeLog
new file mode 100644
index 000000000..c650baf72
--- /dev/null
+++ b/apps/swscroll/ChangeLog
@@ -0,0 +1 @@
+0.01: Inital release.
diff --git a/apps/swscroll/README.md b/apps/swscroll/README.md
new file mode 100644
index 000000000..f97d59b71
--- /dev/null
+++ b/apps/swscroll/README.md
@@ -0,0 +1,9 @@
+This first release seems servicable in testing so far.
+
+To get the standard menu scrolling back, just remove this app from your Bangle.
+
+TODO:
+- Maybe have how much of "trailing space" there are after the last entry should be dynamic in size, now it's always 8 pixels which corresponds to if there are a widget field and a menu title present.
+- I want to change the size of menu entries to be a little bigger vertically. 
+
+Drag List Down icon by Icons8
diff --git a/apps/swscroll/app.png b/apps/swscroll/app.png
new file mode 100644
index 000000000..7abd582c2
Binary files /dev/null and b/apps/swscroll/app.png differ
diff --git a/apps/swscroll/boot.js b/apps/swscroll/boot.js
new file mode 100644
index 000000000..fc5650cad
--- /dev/null
+++ b/apps/swscroll/boot.js
@@ -0,0 +1,100 @@
+E.showScroller = (function(options) {
+  /* options = {
+    h = height
+    c = # of items
+    scroll = initial scroll position
+    scrollMin = minimum scroll amount (can be negative)
+    draw = function(idx, rect)
+    select = function(idx)
+  }
+  
+  returns {
+    draw  = draw all
+    drawItem(idx) = draw specific item
+  }
+  */
+if (!options) return Bangle.setUI(); // remove existing handlers
+
+var menuShowing = false;
+var R = Bangle.appRect;
+var Y = Bangle.appRect.y;
+var n = Math.ceil(R.h/options.h);
+var menuScrollMin = 0|options.scrollMin;
+var menuScrollMax = options.h*options.c - R.h;
+if (menuScrollMax {
+  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),options.c-1);
+  for (var i=a;i<=b;i++)
+    options.draw(i, {x:R.x,y:idxToY(i),w:R.w,h:options.h});
+  g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
+}, drawItem : i => {
+  var y = idxToY(i);
+  g.reset().setClipRect(R.x,y,R.x2,y+options.h);
+  options.draw(i, {x:R.x,y:y,w:R.w,h:options.h});
+  g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
+}};
+var rScroll = s.scroll&~1; // rendered menu scroll (we only shift by 2 because of dither)
+s.draw(); // draw the full scroller
+g.flip(); // force an update now to make this snappier
+Bangle.setUI({
+  mode : "custom",
+  back : options.back,
+  swipe : (hor,ver)=>{
+    pixels = 120;
+    var dy = ver*pixels;
+    if (s.scroll - dy > menuScrollMax)
+      dy = s.scroll - menuScrollMax-8; // Makes it so the last 'page' has the same position as previous pages. This should be done dynamically (change the static 8 to be a variable) so the offset is correct even when no widget field or title field is present.
+    if (s.scroll - dy < menuScrollMin)
+      dy = s.scroll - menuScrollMin;
+    s.scroll -= dy;
+    var oldScroll = rScroll;
+    rScroll = s.scroll &~1;
+    dy = oldScroll-rScroll;
+    if (!dy || options.c<=3) return; //options.c<=3 should maybe be dynamic, so 3 would be replaced by a variable dependent on R=Bangle.appRect. It's here so we don't try to scroll if all entries fit in the app rectangle.
+    g.reset().setClipRect(R.x,R.y,R.x2,R.y2);
+    g.scroll(0,dy);
+    var d = ver*pixels;
+    if (d < 0) {
+      g.setClipRect(R.x,R.y2-(1-d),R.x2,R.y2);
+      let i = YtoIdx(R.y2-(1-d));
+      let y = idxToY(i);
+      //print(i, options.c, options.c-i); //debugging info
+      while (y < R.y2 - (options.h*((options.c-i)<=0)) ) { //- (options.h*((options.c-i)<=0)) makes sure we don't go beyond the menu entries in the menu object "options". This has to do with "dy = s.scroll - menuScrollMax-8" above.
+        options.draw(i, {x:R.x,y:y,w:R.w,h:options.h});
+        i++;
+        y += options.h;
+      }
+    } else { // d>0
+      g.setClipRect(R.x,R.y,R.x2,R.y+d);
+      let i = YtoIdx(R.y+d);
+      let y = idxToY(i);
+      //print(i, options.c, options.c-i); //debugging info
+      while (y > R.y-options.h) {
+        options.draw(i, {x:R.x,y:y,w:R.w,h:options.h});
+        y -= options.h;
+        i--;
+      }
+    }
+    g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
+  }, touch : (_,e)=>{
+    if (e.y=0) && i{if (p) onTemperature(p);});
   } else {
     onTemperature({
       temperature : E.getTemperature()
@@ -34,8 +34,11 @@ function drawTemperature() {
 
 setInterval(function() {
   drawTemperature();
-}, 10000);
+}, 5000);
+Bangle.loadWidgets();
+Bangle.setUI({
+  mode : "custom",
+  back : function() {load();}
+});
 E.showMessage("Reading temperature...");
 drawTemperature();
-Bangle.loadWidgets();
-Bangle.drawWidgets();
diff --git a/apps/thermom/metadata.json b/apps/thermom/metadata.json
index 381f85e17..a215df624 100644
--- a/apps/thermom/metadata.json
+++ b/apps/thermom/metadata.json
@@ -1,7 +1,7 @@
 {
   "id": "thermom",
   "name": "Thermometer",
-  "version": "0.05",
+  "version": "0.07",
   "description": "Displays the current temperature in degree Celsius/Fahrenheit (depending on locale), updates every 10 seconds with average of last 5 readings.",
   "icon": "app.png",
   "tags": "tool",
diff --git a/apps/tinyVario/ChangeLog b/apps/tinyVario/ChangeLog
index 6c352f82b..a201ee465 100644
--- a/apps/tinyVario/ChangeLog
+++ b/apps/tinyVario/ChangeLog
@@ -1 +1,5 @@
 0.01: Initial Version
+0.02: Touch data fields to configure them.
+0.03: Changed menu layout, fixed automatic flight time detection.
+0.04: flight time detection should work without GPS now. New vario display available.
+0.05: some bugs fiexed, no new features.
diff --git a/apps/tinyVario/README.md b/apps/tinyVario/README.md
index e23f3af66..c375585c2 100644
--- a/apps/tinyVario/README.md
+++ b/apps/tinyVario/README.md
@@ -1,5 +1,7 @@
 # Turn your Bangle.js2 into a flight computer!
 
+All settings can be accessed by touching the corresponding data fields. I made all interface objects as big as possible to make it easier to use with gloves. 
+
 ## This is a work in progress. Working features so far:
 - Altimeter
 - Variometer
@@ -7,8 +9,9 @@
 - Ground speed
 - Flying time with automatic take-off detection
 
+
 ## Planned features:
-- Settings page to adjust QNH, change units 
+- glide slope display
 - final glide computer
 - waypoint navigation
 - flight log (possibly IGC file export)
diff --git a/apps/tinyVario/app.js b/apps/tinyVario/app.js
index cf7bc286d..b9a87c821 100644
--- a/apps/tinyVario/app.js
+++ b/apps/tinyVario/app.js
@@ -1,204 +1,465 @@
 /*
 To do:
-  -setup page
-  -different units
   -flight log
   -statistics page
-  -nav page
+  -navigation
 */
-Bangle.setBarometerPower(true, "tinyVario");
-Bangle.setGPSPower(true, "tinyVario");
+
+getAltitude = (p,baseP) => (44330 * (1.0 - Math.pow(p/baseP, 0.1903)));
+getFL = () => (44330 * (1.0 - Math.pow(pressure/1013.25, 0.1903))).toFixed(0);
+getTimeString = () => (settings.localTime) ? (require("locale").time(Date(),1)):(Date().toUTCString().slice(Date().toUTCString().length-12,Date().toUTCString().length-7));
+
+var fg=g.getColor();
+var bg=g.getBgColor();
+var red="#F00",green="#0F0";
 
 const unitsRoc=[
   {name:"m/s", factor:1, precision:1, layoutCode:{type:"v", halign:1, c: [
             {type:"txt", font:"12%", halign:0, filly:0, label:"m"},
-            {type:"", height:1,width:"20", bgCol:"#FFF"},
+            {type:"", height:1,width:"20", bgCol:fg},
             {type:"txt", font:"12%", halign:0, filly:0, label:"s"}]}},
   {name:"ft/m", factor:196.85039370078738, precision:0, layoutCode:{type:"v", halign:1, c: [
             {type:"txt", font:"12%", halign:0, filly:0, label:"ft"},
-            {type:"", height:1,width:"30", bgCol:"#FFF"},
+            {type:"", height:1,width:"30", bgCol:fg},
             {type:"txt", font:"12%", halign:0, filly:0, label:"min"}]}},
   {name:"kt", factor:1.9438444924406, precision:1, layoutCode:
             {type:"txt", font:"12%", halign:0, filly:0, label:"kt"}}
   ];
-
 const unitsGs=[
   {name:"km/h", factor:1, precision:1, layoutCode:{type:"v", halign:1, c: [
             {type:"txt", font:"12%", halign:0, filly:0, label:"km"},
-            {type:"", height:1,width:"30", bgCol:"#FFF"},
+            {type:"", height:1,width:"30", bgCol:fg},
             {type:"txt", font:"12%", halign:0, filly:0, label:"h"}]}},
-  {name:"kt", factor:196.85039370078738, precision:0, layoutCode:{type:"txt", font:"12%", halign:0, filly:0, label:"kt"}},
+  {name:"kt", factor:0.5399568, precision:0, layoutCode:{type:"txt", font:"12%", halign:0, filly:0, label:"kt"}},
   {name:"m/s", factor:0.2777777777777778, precision:1, layoutCode:{type:"v", halign:1, c: [
             {type:"txt", font:"12%", halign:0, filly:0, label:"m"},
-            {type:"", height:1,width:"20", bgCol:"#FFF"},
+            {type:"", height:1,width:"20", bgCol:fg},
             {type:"txt", font:"12%", halign:0, filly:0, label:"s"}]}}
   ];
-
 const unitsAlt=[
   {name:"m", factor:1, precision:0, layoutCode:{type:"txt", font:"12%", halign:0, filly:0, label:"m"}},
   {name:"ft", factor:3.280839895013123, precision:0, layoutCode:{type:"txt", font:"12%", halign:0, filly:0, label:"ft"}}
   ];
-
-var intTime=10,pressureInterval=100;
-var altH = [];
-var altRaw=-9999, altFast=0, altSlow=0;
-var fastGain=0.2, slowGain=0.168;
-var roc=0,rocAvg=0;
-var gs;
-var lastPressure = Date.now();
-var flying=false;
-var takeoffTime, flyingTime;
-var Layout = require("Layout");
-var speedUnit="km/h", speedFactor=1;
-var altUnit="m", altFactor=1;
-var rocUnit=unitsRoc[0];
-var altUnit=unitsAlt[0];
-var gsUnit=unitsGs[0];
-
-function drawVario() {
-  var p = pfd.vario;
-  g.reset();
-  g.clearRect(p.x,p.y,p.x+p.w-1,p.y+p.h-1);
-  if (roc>0.1) g.setColor(0,1,0);
-  if (roc<-1) g.setColor(1,0,0);
-  var y=p.y+p.h/2-roc*(p.h/2)/5;
-  g.fillRect(p.x,p.y+(p.h/2),p.x+p.w-1,Math.clip(y,p.y,p.y+p.h-1));
-}
-
-function updateText(t) {
-  if (t.halign==1) 
-    g.setFont(t.font).setFontAlign(1,0,0).drawString(t.label, t.x+t.w, t.y+(t.h>>1));
-  else if (t.halign==-1)
-    g.setFont(t.font).setFontAlign(-1,0,0).drawString(t.label, t.x, t.y+(t.h>>1));
-  else 
-    g.setFont(t.font).setFontAlign(0,0,0).drawString(t.label, t.x+(t.w>>1), t.y+(t.h>>1));
-}
-
-unitROC={type:"v", halign:1, c: [
+const unitROC={type:"v", halign:1, c: [
             {type:"txt", font:"12%", halign:0, filly:0, label:"m"},
-            {type:"", height:1,width:"20", bgCol:"#FFF"},
+            {type:"", height:1,width:"20", bgCol:fg},
             {type:"txt", font:"12%", halign:0, filly:0, label:"s"}
           ]};
 
-var pfd = new Layout(
-  {type:"v",c: [
-    {type:"h",c: [
-      {type:"", fillx:1, height:"1"}
-      ]},
-    {type:"h", c: [
-      /*{type:"v", c:[
-        {type:"", width:"3", height:"3", bgCol:"#FFF"},
-        {type:"", filly:1},
-        {type:"", width:"3", height:"3", bgCol:"#FFF"},
-        {type:"", filly:1},
-        {type:"", width:"3", height:"3", bgCol:"#FFF"},
-        {type:"", filly:1},
-        {type:"", width:"3", height:"3", bgCol:"#FFF"},
-        {type:"", filly:1},
-        {type:"", width:"3", height:"3", bgCol:"#FFF"},
-        {type:"", filly:1},
-        {type:"", width:"3", height:"3", bgCol:"#FFF"},
-        {type:"", filly:1},
-        {type:"", width:"3", height:"3", bgCol:"#FFF"},
-        {type:"", filly:1},
-        {type:"", width:"3", height:"3", bgCol:"#FFF"},
-        {type:"", filly:1},
-        {type:"", width:"3", height:"3", bgCol:"#FFF"},
-        {type:"", filly:1},
-        {type:"", width:"3", height:"3", bgCol:"#FFF"},
-        {type:"", filly:1},
-        {type:"", width:"3", height:"3", bgCol:"#FFF"}
-      ]},*/
-      {type:"custom", width:"25", render:drawVario, id:"vario",filly:1 },
-      {type:"", filly:1, width:1, bgCol:"#FFF"},
-      {type:"v",fillx:1, c: [
-        {type:"h", halign:1, c:[
-          {type:"txt",font:"22%", halign:1, filly:1, label:"19999", id:"alt"},
-          altUnit.layoutCode 
-        ]},
-        {type:"", fillx:1, height:"1", bgCol:"#FFF"},
-        {type:"h", halign:1, c:[
-          {type:"txt", font:"25%", halign:1, filly:1, label:"-99.9", id:"avg" },
-          rocUnit.layoutCode
-        ]},
-        {type:"", fillx:1, height:"1", bgCol:"#FFF"},
-        {type:"h", halign:1, c:[
-          {type:"txt", font:"25%", halign:1, filly:1, label:"XXX", id:"gs" },
-          gsUnit.layoutCode
+const ground=0, flying=1, landed=2, maybeFlying=3, maybeLanded=4;
+
+var settings = Object.assign({
+  rocU: 0,
+  altU: 0,
+  gsU:0,
+  intTime:10,
+  localTime:true,
+  autoDetect:true,
+  bargraph:false
+}, require('Storage').readJSON("tinyVario.json", true) || {});
+
+var qnh=Math.floor(Bangle.getOptions().seaLevelPressure) || 1013;
+var pfdHandle;
+var rawP=0, samples=0;
+var altH = [];
+var altRaw=-9999, altFast=0, altSlow=0;
+var fastGain=0.5, slowGain=0.3;
+var roc=0,rocAvg=0, gs;
+var lastPressure = Date.now();
+var pressure = 1000;
+var state=ground;
+var takeoffTime=0, landingTime=0, flyingTime;
+var Layout = require("Layout");
+var oldSettings;
+
+//var delta=0;//TESTING
+
+
+function updateText(t) {
+  g.reset();
+  g.clearRect(t.x,t.y,t.x+t.w-1,t.y+t.h-1);
+  if (t.col) g.setColor(t.col);
+  else g.setColor(fg);
+  if (t.halign==1)
+    g.setFont(t.font).setFontAlign(1,0,0).drawString(t.label, t.x+t.w, t.y+(t.h>>1));
+  else if (t.halign==-1)
+    g.setFont(t.font).setFontAlign(-1,0,0).drawString(t.label, t.x, t.y+(t.h>>1));
+  else
+    g.setFont(t.font).setFontAlign(0,0,0).drawString(t.label, t.x+(t.w>>1), t.y+(t.h>>1));
+}
+
+function initPFD() {
+  Bangle.setUI();
+  var pfd = new Layout(
+    {type:"v",c: [
+      /*{type:"h",c: [
+        {type:"", fillx:1, height:"1"}
+        ]},*/
+      {type:"h",filly:1, c: [
+        {type:"custom", width:"25", render:()=>{
+          var p = pfd.vario;
+          if (roc>0.1) g.setColor(0,1,0);
+          if (roc<-1) g.setColor(1,0,0);
+          var y=p.y+p.h/2-roc*(p.h/2)/5;
+          if (settings.bargraph==false) {
+            g.clearRect(p.x,p.y,p.x+p.w-1,p.y+p.h-1);
+            g.fillRect(p.x,p.y+(p.h/2),p.x+p.w-1,Math.clip(y,p.y,p.y+p.h-1));
+          } else {
+            g.setClipRect(p.x,p.y,p.x+p.w-1,p.y+p.h-1);
+            g.scroll(-1,0);
+            g.drawLine(p.x+p.w-1,p.y+(p.h/2),p.x+p.w-1,Math.clip(y,p.y,p.y+p.h-1));
+          }
+          g.reset();
+        }, id:"vario",filly:1, cb:()=>initVarioMenu()},
+        {type:"", filly:1, width:1, bgCol:fg},
+        {type:"v",fillx:1, c: [
+          {type:"h", halign:1, c:[
+            {type:"txt", font:"22%", halign:1, filly:1, fillx:1, label:"9999", id:"alt", cb:()=>initAltMenu()},
+            unitsAlt[settings.altU].layoutCode
+          ]},
+          {type:"", fillx:1, height:"1", bgCol:fg},
+          {type:"h", halign:1, c:[
+            {type:"txt", font:"25%", halign:1, filly:1, fillx:1, label:"-9.9", id:"avg", cb:()=>initAvgMenu()},
+            unitsRoc[settings.rocU].layoutCode
+          ]},
+          {type:"", fillx:1, height:"1", bgCol:fg},
+          {type:"h", halign:1, c:[
+            {type:"txt", font:"25%", halign:1, filly:1, fillx:1, label:"XXX", id:"gs", cb:()=>initGsMenu()},
+            unitsGs[settings.gsU].layoutCode
+          ]}
         ]}
+      ]},
+      {type:"", fillx:1, height:"1", bgCol:fg},
+      {type:"h",c: [
+        {type:"txt",pad:0, halign:0, font:"15%",fillx:1, label:"99:99", id:"time", cb:()=>initTimeMenu()},
+        {type:"", width:1,height:g.getHeight()*0.15, bgCol:fg},
+        {type:"txt",pad:0, halign:0, font:"15%", fillx:1, label:"--:--", id:"flyingtime", cb:()=>initFlyingTimeMenu() }
       ]}
-    ]},
-    {type:"", fillx:1, height:"1", bgCol:"#FFF"},
-    {type:"h",c: [
-      {type:"txt",pad:"1", halign:0, font:"15%",fillx:"1", label:"99:99", id:"time"},
-      {type:"", width:"1", height:g.getHeight()*0.15+2, bgCol:"#FFF"},
-      {type:"txt",pad:"1", halign:0, font:"15%", fillx:"1", label:"99:99", id:"flyingtime" }
-    ]}
-  ]}//,{lazy:"true"}
-);
-pfd.update();
+    ]},{lazy:true}
+  );
+  g.clear();
+  pfd.render();
+  //-------testing------
+  //rawP=1000;
+  //samples=1;
+  //--------------------
+  pfdHandle = setInterval(function() {
+    t1=Date().getTime();
+    //process pressure readings
+    if (samples) {
+      pressure=rawP/samples;
+      samples=0;
+      rawP=0;
+      if (altRaw==-9999) {//first measurement)
+        altRaw=getAltitude(pressure,qnh);
+        altFast=altRaw;
+        altSlow=altRaw;
+        for (let i = 0; i < settings.intTime*4+1; i++) altH.push(altRaw);
+      }
+    }
+    //altRaw=altRaw+delta;getAltitude(pressure,qnh);//TESTING
+    altRaw=getAltitude(pressure,qnh);
+    altFast=altFast+(altRaw-altFast)*fastGain;
+    altSlow=altSlow+(altRaw-altSlow)*slowGain;
+    altH.push(altFast);
+    while (altH.length>settings.intTime*4) {
+      rocAvg=(altH[altH.length-1]-altH[0])/settings.intTime;
+      altH.shift();
+    }
+    roc=(altFast-altSlow)/((0.25/slowGain)-(0.25/fastGain));
+
+    if (settings.autoDetect==true) switch (state) {
+      case ground:
+        if ((gs>=5) || (roc>=1) || (roc<=-1)) {
+          state=maybeFlying;
+          takeoffTime=Date().getTime();
+        }
+        break;
+      case maybeFlying:
+        if (!(gs>=5) && (roc<1) && (roc>-1)) state=ground;
+        else if (Date().getTime()-takeoffTime>60000) state=flying;
+        break;
+      case flying:
+        if (!(gs>=5) && (roc<1) && (roc>-1)) {
+          state=maybeLanded;
+          landingTime=Date().getTime();
+        }
+        break;
+      case maybeLanded:
+        if ((gs>=5) || (roc>=1) || (roc<=-1)) state=flying;
+        else if (Date().getTime()-landingTime>60000) state=landed;
+        break;
+    }
+    if ((state==flying) || (state==maybeLanded)) {
+      flyingTime=Date().getTime()-takeoffTime;
+      pfd.flyingtime.label=(flyingTime / 3600000).toFixed(0)+":"+(flyingTime / 60000 % 60).toFixed(0).padStart(2,'0');
+      pfd.flyingtime.col=fg;
+    } else if (state==landed) {
+      flyingTime=landingTime-takeoffTime;
+      pfd.flyingtime.label=(flyingTime / 3600000).toFixed(0)+":"+(flyingTime / 60000 % 60).toFixed(0).padStart(2,'0');
+      pfd.flyingtime.col=green;
+    }
+
+    pfd.alt.label=(altRaw*unitsAlt[settings.altU].factor).toFixed(unitsAlt[settings.altU].precision);
+    pfd.avg.col=(rocAvg<-1) ? (red):((rocAvg>0.1) ? (green):(fg));
+    pfd.avg.label=(rocAvg*unitsRoc[settings.rocU].factor).toFixed(unitsRoc[settings.rocU].precision);
+
+    var gps = Bangle.getGPSFix();
+    if (gps!=undefined) {
+      pfd.gs.label=(gps.speed*unitsGs[settings.gsU].factor).toFixed(unitsGs[settings.gsU].precision);
+      updateText(pfd.gs);
+      gs=gps.speed;
+    } //else gs=0;
+    
+    pfd.time.label=getTimeString();
+    updateText(pfd.alt);
+    updateText(pfd.avg);
+    updateText(pfd.time);
+    updateText(pfd.flyingtime);
+
+    pfd.vario.render();
+    //print(Date().getTime()-t1);
+  }, 250);
+
+}
+
+function initAltMenu() {
+  var oldQnh=qnh;
+  function updateAltMenu() {
+    altMenu.clear();
+    altMenu.alt.label=
+      (getAltitude(pressure,qnh)*unitsAlt[settings.altU].factor).toFixed(unitsAlt[settings.altU].precision)
+      +unitsAlt[settings.altU].name;
+    altMenu.qnh.label=qnh;
+    altMenu.render();
+  }
+  oldSettings=Object.assign({},settings);
+  clearInterval(pfdHandle);
+  var altMenu = new Layout ({
+    type:"v", c: [{
+      type:"v", width:180, c: [
+        {type:"h", c: [
+          {type:"btn", font:"15%", pad:1, fillx:1, filly:1, label:"-", cb:l=>{qnh--; updateAltMenu();}},
+          {type:"btn", font:"15%", pad:1, fillx:1, filly:1, label:"+", cb:l=>{qnh++; updateAltMenu();}}
+        ]},
+        {type:"v", c: [
+          {type:"h", c: [
+            {type:"txt", font:"13%", fillx:1, filly:1, label:"QNH: "},
+            {type:"txt", font:"13%", fillx:1, filly:1, id:"qnh", label:"    "},
+          ]},
+          {type:"h", c: [
+            {type:"txt", font:"13%", fillx:1, filly:1, label:"Alt: "},
+            {type:"txt", font:"13%", fillx:1, filly:1, id:"alt", label: "      "},
+          ]}
+        ]},
+        {type:"btn", font:"12%", id:"units", pad:2, fillx:1, filly:1, label:"Units: "+unitsAlt[settings.altU].name, cb:()=>{
+          settings.altU=(settings.altU+1)%unitsAlt.length;
+          altMenu.units.label="Units: "+unitsAlt[settings.altU].name;
+          altMenu.render();
+        }},
+      ]},
+      {type:"h", c: [
+        {type:"btn", font:"16%", pad:1, fillx:1, label:"BACK", cb: ()=>{
+          settings=Object.assign({},oldSettings);
+          print("old settings restored");
+          initPFD();
+        }},
+        {type:"btn", font:"16%", pad:1, fillx:1, label:"SAVE", cb: ()=>{
+          require('Storage').writeJSON("tinyVario.json", settings);
+          Bangle.setOptions({seaLevelPressure:qnh});
+          initPFD();
+        }}
+      ]}
+    ], lazy:true});
+  g.clear();
+  altMenu.render();
+  updateAltMenu();
+}
+
+function initAvgMenu() {
+  oldSettings=Object.assign({},settings);
+  clearInterval(pfdHandle);
+  var avgMenu = new Layout ({
+    type:"v", c: [{
+      type:"v", width:180, c: [
+        {type:"h", c: [
+          {type:"btn", font:"12%", pad:2, fillx:1, filly:1, label:"-", cb:l=>{
+            settings.intTime=Math.clip(settings.intTime-1,1,60);
+            avgMenu.clear();
+            avgMenu.interval.label="Interval: "+settings.intTime+"s";
+            avgMenu.render();
+          }},
+          {type:"btn", font:"12%", pad:1, fillx:1, filly:1, label:"+", cb:l=>{
+            settings.intTime=Math.clip(settings.intTime+1,1,60);
+            avgMenu.clear();
+            avgMenu.interval.label="Interval: "+settings.intTime+"s";
+            avgMenu.render();
+          }}
+        ]},
+        {type:"txt", id:"interval", font:"10%", pad:1, fillx:1, filly:1, label:"Interval: "+settings.intTime+"s"},
+        {type:"btn", font:"12%", id:"units", pad:1, fillx:1, filly:1, label:"Units: "+unitsRoc[settings.rocU].name, cb:()=>{
+          settings.rocU=(settings.rocU+1)%unitsRoc.length;
+          avgMenu.units.label="Units: "+unitsRoc[settings.rocU].name;
+          avgMenu.render();
+        }},
+      ]},
+      {type:"h", c: [
+        {type:"btn", font:"16%", pad:1, fillx:1, label:"BACK", cb: ()=>{
+          settings=Object.assign({},oldSettings);
+          initPFD();
+        }},
+        {type:"btn", font:"16%", pad:1, fillx:1, label:"SAVE", cb: ()=>{
+          require('Storage').writeJSON("tinyVario.json", settings);
+          initPFD();
+        }}
+      ]}
+    ], lazy:true});
+  g.clear();
+  avgMenu.render();
+}
+
+function initGsMenu() {
+  oldSettings=Object.assign({},settings);
+  clearInterval(pfdHandle);
+  var gsMenu = new Layout ({
+    type:"v", c: [
+      {type:"btn", font:"20%", id:"units", pad:1, fillx:1, filly:1, label:"Units:\n"+unitsGs[settings.gsU].name, cb:()=>{
+        settings.gsU=(settings.gsU+1)%unitsGs.length;
+        gsMenu.units.label="Units:\n"+unitsGs[settings.gsU].name;
+        gsMenu.render();
+      }},
+      {type:"h", c: [
+        {type:"btn", font:"16%", pad:1, fillx:1, label:"BACK", cb: ()=>{
+          settings=Object.assign({},oldSettings);
+          print("old settings restored");
+          initPFD();
+        }},
+        {type:"btn", font:"16%", pad:1, fillx:1, label:"SAVE", cb: ()=>{
+          require('Storage').writeJSON("tinyVario.json", settings);
+          initPFD();
+        }}
+      ]}
+    ], lazy:true});
+  g.clear();
+  gsMenu.render();
+}
+
+function initTimeMenu() {
+  oldSettings=Object.assign({},settings);
+  clearInterval(pfdHandle);
+  var timeMenu = new Layout ({
+    type:"v", c: [
+      {type:"btn", font:"20%", id:"format", pad:1, fillx:1, filly:1, label:"Time:\n"+((settings.localTime==true) ? ("LCL") : ("UTC")), cb:()=>{
+        settings.localTime=!settings.localTime;
+        timeMenu.format.label="Time:\n"+((settings.localTime==true) ? ("LCL") : ("UTC"));
+        timeMenu.render();
+      }},
+      {type:"h", c: [
+        {type:"btn", font:"16%", pad:1, fillx:1, label:"BACK", cb: ()=>{
+          settings=Object.assign({},oldSettings);
+          initPFD();
+        }},
+        {type:"btn", font:"16%", pad:1, fillx:1, label:"SAVE", cb: ()=>{
+          require('Storage').writeJSON("tinyVario.json", settings);
+          initPFD();
+        }}
+      ]}
+    ], lazy:true});
+  g.clear();
+  timeMenu.render();
+}
+
+function initVarioMenu() {
+  oldSettings=Object.assign({},settings);
+  clearInterval(pfdHandle);
+  var varioMenu = new Layout ({
+    type:"v", c: [
+      {type:"btn", font:"20%", id:"format", pad:1, fillx:1, filly:1, label:"Display:\n"+((settings.bargraph==true) ? ("graph") : ("simple")), cb:()=>{
+        settings.bargraph=!settings.bargraph;
+        varioMenu.format.label="Display:\n"+((settings.bargraph==true) ? ("graph") : ("simple"));
+        varioMenu.render();
+      }},
+      {type:"h", c: [
+        {type:"btn", font:"16%", pad:1, fillx:1, label:"BACK", cb: ()=>{
+          settings=Object.assign({},oldSettings);
+          initPFD();
+        }},
+        {type:"btn", font:"16%", pad:1, fillx:1, label:"SAVE", cb: ()=>{
+          require('Storage').writeJSON("tinyVario.json", settings);
+          initPFD();
+        }}
+      ]}
+    ], lazy:true});
+  g.clear();
+  varioMenu.render();
+}
+function initFlyingTimeMenu() {
+  oldSettings=Object.assign({},settings);
+  clearInterval(pfdHandle);
+  var ftMenu = new Layout (
+    {type:"v", c: [
+      {type:"v", c: [
+        {type:"h", c: [
+          {type:"btn", font:"12%", pad:1, fillx:1, filly:1, label:"Toggle\nAuto", cb:()=>{
+            settings.autoDetect=!settings.autoDetect;
+            ftMenu.manual.label= (settings.autoDetect==true)? 
+              ("AUTO"):((state==flying) ? ("LAND") : ("TAKE\nOFF"));
+            ftMenu.render();
+          }},
+          {type:"btn", font:"12%", id:"manual", pad:1, fillx:1, filly:1, label:(settings.autoDetect==true)? 
+            ("AUTO"):((state==flying) ? ("LAND") : ("TAKE\nOFF")), cb:()=>{
+              if (settings.autoDetect==false) {
+                if (state!=flying) {
+                  E.showPrompt("Take off now?").then((v)=> {
+                    if (v) {
+                      state=flying;
+                      takeoffTime=Date().getTime();
+                      initPFD();
+                    }
+                  });
+                } else {
+                  E.showPrompt("Land now?").then((v)=> {
+                    if (v) {
+                      state=landed;
+                      landingTime=Date().getTime();
+                      initPFD();
+                    }
+                  });
+                }
+              }
+            }
+          }
+        ]},
+        {type:"btn", font:"12%", pad:1, fillx:1, filly:1, label:"Reset", cb:()=>{
+          E.showPrompt("Reset Flight?").then((v)=> {
+            state=ground;
+            initPFD();
+          });
+        }}
+      ]},
+      {type:"h", c: [
+        {type:"btn", font:"16%", pad:1, fillx:1, label:"BACK", cb: ()=>{
+          settings=Object.assign({},oldSettings);
+          initPFD();
+        }},
+        {type:"btn", font:"16%", pad:1, fillx:1, label:"SAVE", cb: ()=>{
+          require('Storage').writeJSON("tinyVario.json", settings);
+          initPFD();
+        }}
+      ]}
+    ], lazy:true});
+  g.clear();
+  ftMenu.render();
+}
+
+Bangle.setGPSPower(true, "tinyVario");
+Bangle.setBarometerPower(true, "tinyVario");
 
 Bangle.on('pressure', function(e) {
-  if (altRaw==-9999) {
-    altFast=e.altitude;
-    altSlow=e.altitude;
-    altRaw=e.altitude;
+  if (samples<10) { //no need to gather more samples when stuck in a menu
+    rawP+=e.pressure;
+    samples++;
   }
-  altRaw=altRaw+(e.altitude-altRaw)*0.2;
 });
 
-Bangle.on('GPS', function(fix) {
-  gs=fix.speed;
-});
-          
-/*setWatch(function() {
-  
-}, BTN1);*/
-
-setInterval(function () { 
-  altFast=altFast+(altRaw-altFast)*fastGain;
-  altSlow=altSlow+(altRaw-altSlow)*0.09093;
-  altH.push(altSlow);
-  if (altH.length>intTime*1000/pressureInterval) {
-    altH.shift();
-    rocAvg=(altH[altH.length-1]-altH[0])/intTime;
-  }
-}, pressureInterval);
-
-g.clear();
-pfd.render();
-
-setInterval(function() {
-  pfd.clear(pfd.alt);
-  pfd.clear(pfd.avg);
-  pfd.clear(pfd.time);
-  if ((!flying) && ((rocAvg>1) || (rocAvg<-1) || (gs>10))) { //take-off detected
-    takeoffTime=Date().getTime();
-    flying=true;
-  } 
-  if (flying) {
-    pfd.clear(pfd.flyingtime);
-    flyingTime=Date().getTime()-takeoffTime;
-    pfd.flyingtime.label=(flyingTime / 3600000).toFixed(0)+":"+(flyingTime / 60000 % 60).toFixed(0).padStart(2,'0');
-  } 
-  roc=(altFast-altSlow)/(pressureInterval/1000/slowGain)-(pressureInterval/1000/fastGain);
-  pfd.alt.label=(altSlow).toFixed(0);
-  if (rocAvg>0.1) pfd.avg.col="#0f0";
-    else if (rocAvg<-1) pfd.avg.col="#f00";
-    else pfd.avg.col="#fff";
-  pfd.avg.label=(rocAvg*rocUnit.factor).toFixed(rocUnit.precision);
-  if (!isNaN(gs)) {
-    pfd.gs.label=gs.toFixed(0);
-    pfd.clear(pfd.gs);
-  } 
-  pfd.time.label=require("locale").time(Date(),1);
-  updateText(pfd.alt);
-  updateText(pfd.avg);
-  updateText(pfd.gs);
-  updateText(pfd.time);
-  updateText(pfd.flyingtime);
-  drawVario();
- // pfd.debug(pfd.roc);
-  //pfd.render();  
-}, 250);
-
+initPFD();
diff --git a/apps/tinyVario/metadata.json b/apps/tinyVario/metadata.json
index c8bc3659e..f038e7515 100644
--- a/apps/tinyVario/metadata.json
+++ b/apps/tinyVario/metadata.json
@@ -1,7 +1,7 @@
 { "id": "tinyVario",
   "name": "Tiny Vario",
   "shortName" : "tinyVario",
-  "version":"0.01",
+  "version":"0.05",
   "icon": "app.png",
   "readme": "README.md",
   "description": "A very simple app for gliding / paragliding / hang gliding etc.",
@@ -10,5 +10,6 @@
   "storage": [
     {"name":"tinyVario.app.js","url":"app.js"},
     {"name":"tinyVario.img","url":"app-icon.js","evaluate":true}
-    ]
+    ],
+  "data": [{"name":"tinyVario.json"}]
 }
diff --git a/apps/torch/ChangeLog b/apps/torch/ChangeLog
index 4d8f47500..fd904b6e8 100644
--- a/apps/torch/ChangeLog
+++ b/apps/torch/ChangeLog
@@ -2,3 +2,4 @@
 0.02: Change start sequence to BTN1/3/1/3 to avoid accidental turning on (fix #342)
 0.03: Add Color Changing Settings
 0.04: Add Support For Bangle.js 2
+0.05: Default full Brightness
diff --git a/apps/torch/app.js b/apps/torch/app.js
index 864efb883..2af35fdb6 100644
--- a/apps/torch/app.js
+++ b/apps/torch/app.js
@@ -7,6 +7,7 @@ function loadSettings() {
 
 loadSettings();
 
+Bangle.setLCDBrightness(1);
 Bangle.setLCDPower(1);
 Bangle.setLCDTimeout(0);
 g.reset();
diff --git a/apps/torch/metadata.json b/apps/torch/metadata.json
index 37e6f6b95..af85370ac 100644
--- a/apps/torch/metadata.json
+++ b/apps/torch/metadata.json
@@ -2,7 +2,7 @@
   "id": "torch",
   "name": "Torch",
   "shortName": "Torch",
-  "version": "0.04",
+  "version": "0.05",
   "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/twenties/ChangeLog b/apps/twenties/ChangeLog
new file mode 100644
index 000000000..89eb9547f
--- /dev/null
+++ b/apps/twenties/ChangeLog
@@ -0,0 +1,4 @@
+0.01: New Widget!
+0.02: Fix calling null on draw
+0.03: Only vibrate during work
+0.04: Convert to boot code
\ No newline at end of file
diff --git a/apps/twenties/README.md b/apps/twenties/README.md
new file mode 100644
index 000000000..8ee917b0e
--- /dev/null
+++ b/apps/twenties/README.md
@@ -0,0 +1,17 @@
+# Twenties
+
+Follow the [20-20-20 rule](https://www.aoa.org/AOA/Images/Patients/Eye%20Conditions/20-20-20-rule.pdf) with discrete reminders. Your Bangle will buzz every 20 minutes for you to look away from your screen, and then buzz 20 seconds later to look back. Additionally, alternate between standing and sitting every 20 minutes to be standing for [more than 30 minutes](https://uwaterloo.ca/kinesiology-health-sciences/how-long-should-you-stand-rather-sit-your-work-station) per hour.
+
+## Usage
+
+Download this app and it will automatically run in the background.
+
+## Features
+
+Vibrates to remind you to stand up and look away for healthy living.
+
+Only vibrates during work days and hours.
+
+## Creator
+
+[@splch](https://github.com/splch/)
diff --git a/apps/twenties/app.png b/apps/twenties/app.png
new file mode 100644
index 000000000..7c6b02055
Binary files /dev/null and b/apps/twenties/app.png differ
diff --git a/apps/twenties/boot.js b/apps/twenties/boot.js
new file mode 100644
index 000000000..722af43bc
--- /dev/null
+++ b/apps/twenties/boot.js
@@ -0,0 +1,19 @@
+(() => {
+  const move = 20 * 60 * 1000; // 20 minutes
+  const look = 20 * 1000;      // 20 seconds
+
+  const buzz = _ => {
+    const date = new Date();
+    const day = date.getDay();
+    const hour = date.getHours();
+    // buzz at work
+    if (day >= 1 && day <= 5 &&
+      hour >= 8 && hour <= 17) {
+      Bangle.buzz().then(_ => {
+        setTimeout(Bangle.buzz, look);
+      });
+    }
+  };
+
+  setInterval(buzz, move); // buzz to stand / sit
+})();
diff --git a/apps/twenties/metadata.json b/apps/twenties/metadata.json
new file mode 100644
index 000000000..b1dfe2134
--- /dev/null
+++ b/apps/twenties/metadata.json
@@ -0,0 +1,13 @@
+{
+  "id": "twenties",
+  "name": "Twenties",
+  "shortName": "twenties",
+  "version": "0.04",
+  "description": "Buzzes every 20m to stand / sit and look 20ft away for 20s.",
+  "icon": "app.png",
+  "type": "bootloader",
+  "tags": "alarm,tool",
+  "supports": ["BANGLEJS", "BANGLEJS2"],
+  "readme": "README.md",
+  "storage": [{ "name": "twenties.boot.js", "url": "boot.js" }]
+}
diff --git a/apps/verticalface/ChangeLog b/apps/verticalface/ChangeLog
index 99ab68ec4..dc6c11eab 100644
--- a/apps/verticalface/ChangeLog
+++ b/apps/verticalface/ChangeLog
@@ -4,3 +4,4 @@
 0.07: Added leading zero to hours and minutes
 0.08: Show step count by calling wpedom.getSteps() or activepedom.getSteps()
 0.09: Fix time when minutes<10 and hours>9 (fix #767)
+0.10: Tell clock widgets to hide.
diff --git a/apps/verticalface/app.js b/apps/verticalface/app.js
index 4fcae5642..178481047 100644
--- a/apps/verticalface/app.js
+++ b/apps/verticalface/app.js
@@ -97,6 +97,28 @@ function drawBattery() {
 // Clear the screen once, at startup
 g.clear();
 
+// Show launcher when button pressed
+Bangle.setUI("clockupdown", btn=>{
+  if (btn!=0) return;
+  //HRM Controller.
+  if(!HRMstate){
+    //console.log("Toggled HRM");
+    //Turn on.
+    Bangle.buzz();
+    Bangle.setHRMPower(1);
+    currentHRM = "CALC";
+    HRMstate = true;
+  } else if(HRMstate){
+    //console.log("Toggled HRM");
+    //Turn off.
+    Bangle.buzz();
+    Bangle.setHRMPower(0);
+    HRMstate = false;
+    currentHRM = [];
+  }
+  drawBPM(HRMstate);
+});
+
 // Load and draw widgets
 Bangle.loadWidgets();
 Bangle.drawWidgets();
@@ -128,28 +150,6 @@ Bangle.on('lcdPower',on=>{
   }
 });
 
-// Show launcher when button pressed
-Bangle.setUI("clockupdown", btn=>{
-  if (btn!=0) return;
-  //HRM Controller.
-  if(!HRMstate){
-    //console.log("Toggled HRM");
-    //Turn on.
-    Bangle.buzz();
-    Bangle.setHRMPower(1);
-    currentHRM = "CALC";
-    HRMstate = true;
-  } else if(HRMstate){
-    //console.log("Toggled HRM");
-    //Turn off.
-    Bangle.buzz();
-    Bangle.setHRMPower(0);
-    HRMstate = false;
-    currentHRM = [];
-  }
-  drawBPM(HRMstate);
-});
-
 Bangle.on('touch', function(button) {
   if(button == 1 || button == 2){
     Bangle.showLauncher();
diff --git a/apps/verticalface/metadata.json b/apps/verticalface/metadata.json
index da41b3f0d..273070022 100644
--- a/apps/verticalface/metadata.json
+++ b/apps/verticalface/metadata.json
@@ -2,7 +2,7 @@
   "id": "verticalface",
   "name": "Vertical watch face",
   "shortName": "Vertical Face",
-  "version": "0.09",
+  "version": "0.10",
   "description": "A simple vertical watch face with the date. Heart rate monitor is toggled with BTN1",
   "icon": "app.png",
   "type": "clock",
diff --git a/apps/waypointer/ChangeLog b/apps/waypointer/ChangeLog
index 7ccad08ea..8613ef799 100644
--- a/apps/waypointer/ChangeLog
+++ b/apps/waypointer/ChangeLog
@@ -1,3 +1,7 @@
 0.01: New app!
 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/README.md b/apps/waypointer/README.md
index c0b4c5125..241d70a65 100644
--- a/apps/waypointer/README.md
+++ b/apps/waypointer/README.md
@@ -61,58 +61,10 @@ The app indicates that WP2 is now marked by adding the prefix @ to
 it's name. The distance should be small as shown in the screen shot
 as you have just marked your current location.
 
-## Waypoint JSON file
-
-When the app is loaded from the app loader, a file named
-`waypoints.json` is loaded along with the javascript etc. The file
-has the following contents:
-
-
-```
-[
-  {
-  "name":"NONE"
-  },
-  {
-  "name":"No10",
-  "lat":51.5032,
-  "lon":-0.1269
-  },
-  {
-  "name":"Stone",
-  "lat":51.1788,
-  "lon":-1.8260
-  },
-  { "name":"WP0" },
-  { "name":"WP1" },
-  { "name":"WP2" },
-  { "name":"WP3" },
-  { "name":"WP4" }
-]
-```
-
-The file contains the initial NONE waypoint which is useful if you
-just want to display course and speed. The next two entries are
-waypoints to No 10 Downing Street and to Stone Henge - obtained from
-Google Maps. The last five entries are entries which can be *marked*.
-
-You add and delete entries using the Web IDE to load and then save
-the file from and to watch storage. The app itself does not limit the
-number of entries although it does load the entire file into RAM
-which will obviously limit this.
-
-
-## Waypoint Editor
-
-Clicking on the download icon of gpsnav in the app loader invokes the
-waypoint editor.  The editor downloads and displays the current
-`waypoints.json` file. Clicking the `Edit` button beside an entry
-causes the entry to be deleted from the list and displayed in the
-edit boxes. It can be restored - by clicking the `Add waypoint`
-button. A new markable entry is created by using the `Add name`
-button. The edited `waypoints.json` file is uploaded to the Bangle by
-clicking the `Upload` button.
+## Setting Waypoints
 
+Check out the documentation for the `Waypoints` app. This provides
+the ability to set waypoints from your browser.
 
 ## Calibration of the Compass
 
diff --git a/apps/waypointer/app.js b/apps/waypointer/app.js
index bdb6f6857..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();
@@ -50,7 +55,7 @@ function drawCompass(course) {
   if(!candraw) return;
   if (Math.abs(previous.course - course) < 9) return; // reduce number of draws due to compass jitter
   previous.course = course;
-  
+
   buf1.setColor(1);
   buf1.fillCircle(buf1.getWidth()/2,buf1.getHeight()/2,79*scale);
   buf1.setColor(0);
@@ -63,12 +68,17 @@ function drawCompass(course) {
 /***** COMPASS CODE ***********/
 
 var heading = 0;
-function newHeading(m,h){ 
+function newHeading(m,h){
     var s = Math.abs(m - h);
     var delta = (m>h)?1:-1;
-    if (s>=180){s=360-s; delta = -delta;} 
+    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;
@@ -147,9 +157,9 @@ function drawN(){
   var bs = wp_bearing.toString();
   bs = wp_bearing<10?"00"+bs : wp_bearing<100 ?"0"+bs : bs;
   var dst = loc.distance(dist);
-  
+
   // -1=left (default), 0=center, 1=right
-  
+
   // show distance on the left
   if (previous.dst !== dst) {
     previous.dst = dst;
@@ -159,7 +169,7 @@ function drawN(){
     buf2.drawString(dst,0,0);
     flip2_bw(0, g.getHeight()-40*scale);
   }
-  
+
   // bearing, place in middle at bottom of compass
   if (previous.bs !== bs) {
     previous.bs = bs;
@@ -192,7 +202,7 @@ function onGPS(fix) {
   if (fix!==undefined){
     satellites = fix.satellites;
   }
-  
+
   if (candraw) {
     if (fix!==undefined && fix.fix==1){
       dist = distance(fix,wp);
@@ -240,7 +250,7 @@ function setButtons(){
     else { doselect(); }
   });
 }
- 
+
 Bangle.on('lcdPower',function(on) {
   if (on) {
     clear_previous();
@@ -250,7 +260,7 @@ Bangle.on('lcdPower',function(on) {
   }
 });
 
-var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"NONE"}];
+var waypoints = require("waypoints").load();
 wp=waypoints[0];
 
 function nextwp(inc){
@@ -263,10 +273,10 @@ 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("Storage").writeJSON("waypoints.json", waypoints);
+     require("waypoints").save(waypoints);
   }
   selected=!selected;
   drawN();
diff --git a/apps/waypointer/metadata.json b/apps/waypointer/metadata.json
index 707da94cf..0bbc42322 100644
--- a/apps/waypointer/metadata.json
+++ b/apps/waypointer/metadata.json
@@ -1,16 +1,17 @@
 {
   "id": "waypointer",
   "name": "Way Pointer",
-  "version": "0.03",
+  "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",
   "supports": ["BANGLEJS", "BANGLEJS2"],
+  "dependencies" : { "waypoints":"type" },
   "readme": "README.md",
-  "interface": "waypoints.html",
   "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":"waypoints.json","url":"waypoints.json"}]
+  "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/waypointer/waypoints.html b/apps/waypointer/waypoints.html
deleted file mode 100644
index 7a65821a2..000000000
--- a/apps/waypointer/waypoints.html
+++ /dev/null
@@ -1,170 +0,0 @@
-
-  
-    
-    
-  
-  
-
-    

List of waypoints

- - - - - - - - - - - - -
NameLat.Long.Actions
-
-

Add a new waypoint

-
-
-
- -
-
- -
-
- -
-
-
-
- -
-
- -
-
-
-
- - - - - - - diff --git a/apps/waypointer/waypoints.json b/apps/waypointer/waypoints.json deleted file mode 100644 index 98a670c0d..000000000 --- a/apps/waypointer/waypoints.json +++ /dev/null @@ -1,20 +0,0 @@ -[ - { - "name":"NONE" - }, - { - "name":"No10", - "lat":51.5032, - "lon":-0.1269 - }, - { - "name":"Stone", - "lat":51.1788, - "lon":-1.8260 - }, - { "name":"WP0" }, - { "name":"WP1" }, - { "name":"WP2" }, - { "name":"WP3" }, - { "name":"WP4" } -] \ No newline at end of file diff --git a/apps/waypoints/ChangeLog b/apps/waypoints/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/waypoints/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/waypoints/README.md b/apps/waypoints/README.md new file mode 100644 index 000000000..e252054f2 --- /dev/null +++ b/apps/waypoints/README.md @@ -0,0 +1,56 @@ +# Waypoints + +This app provides a common way to set up the `waypoints.json` file, +which several other apps rely on for navigation. + +## Waypoint JSON file + +When the app is loaded from the app loader, a file named +`waypoints.json` is loaded along with the javascript etc. The file +has the following contents: + + +``` +[ + { + "name":"NONE" + }, + { + "name":"No10", + "lat":51.5032, + "lon":-0.1269 + }, + { + "name":"Stone", + "lat":51.1788, + "lon":-1.8260 + }, + { "name":"WP0" }, + { "name":"WP1" }, + { "name":"WP2" }, + { "name":"WP3" }, + { "name":"WP4" } +] +``` + +The file contains the initial NONE waypoint which is useful if you +just want to display course and speed. The next two entries are +waypoints to No 10 Downing Street and to Stone Henge - obtained from +Google Maps. The last five entries are entries which can be *marked*. + +You add and delete entries using the Web IDE to load and then save +the file from and to watch storage. The app itself does not limit the +number of entries although it does load the entire file into RAM +which will obviously limit this. + + +## Waypoint Editor + +Clicking on the download icon of `Waypoints` in the app loader invokes the +waypoint editor. The editor downloads and displays the current +`waypoints.json` file. Clicking the `Edit` button beside an entry +causes the entry to be deleted from the list and displayed in the +edit boxes. It can be restored - by clicking the `Add waypoint` +button. A new markable entry is created by using the `Add name` +button. The edited `waypoints.json` file is uploaded to the Bangle by +clicking the `Upload` button. diff --git a/apps/waypoints/app-icon.js b/apps/waypoints/app-icon.js new file mode 100644 index 000000000..49232b838 --- /dev/null +++ b/apps/waypoints/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwJC/AH4A/AH4AgA==")) diff --git a/apps/waypoints/app.js b/apps/waypoints/app.js new file mode 100644 index 000000000..06c254a36 --- /dev/null +++ b/apps/waypoints/app.js @@ -0,0 +1,34 @@ +// place your const, vars, functions or classes here + +// clear the screen +g.clear(); + +var n = 0; + +// redraw the screen +function draw() { + g.reset().clearRect(Bangle.appRect); + g.setFont("6x8").setFontAlign(0,0).drawString("Up / Down",g.getWidth()/2,g.getHeight()/2 - 20); + g.setFont("Vector",60).setFontAlign(0,0).drawString(n,g.getWidth()/2,g.getHeight()/2 + 30); +} + +// Respond to user input +Bangle.setUI({mode: "updown"}, function(dir) { + if (dir<0) { + n--; + draw(); + } else if (dir>0) { + n++; + draw(); + } else { + n = 0; + draw(); + } +}); + +// First draw... +draw(); + +// Load widgets +Bangle.loadWidgets(); +Bangle.drawWidgets(); diff --git a/apps/waypoints/app.png b/apps/waypoints/app.png new file mode 100644 index 000000000..0b9344405 Binary files /dev/null and b/apps/waypoints/app.png differ diff --git a/apps/wpmoto/wpmoto.html b/apps/waypoints/interface.html similarity index 60% rename from apps/wpmoto/wpmoto.html rename to apps/waypoints/interface.html index 9966f51f6..48e47df57 100644 --- a/apps/wpmoto/wpmoto.html +++ b/apps/waypoints/interface.html @@ -4,6 +4,7 @@ + @@ -11,6 +12,8 @@ html, body { height: 100% } .flex-col { display:flex; flex-direction:column; height:100% } #map { width:100%; height:100% } + #tab-map { width:100%; height:100% } + #tab-list { width:100%; height:100% } /* https://stackoverflow.com/a/58686215 */ .arrow-icon { @@ -23,6 +26,7 @@ transform-origin: center center; font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif; } + @@ -32,8 +36,57 @@ +
+ +
+
+
+
@@ -44,8 +97,23 @@ - + + diff --git a/apps/waypoints/lib.js b/apps/waypoints/lib.js new file mode 100644 index 000000000..88b402a73 --- /dev/null +++ b/apps/waypoints/lib.js @@ -0,0 +1,7 @@ +exports.load = (num) => { + return require("Storage").readJSON(`waypoints${num?"."+num:""}.json`)||[{name:"NONE"}]; +}; + +exports.save = (waypoints,num) => { + require("Storage").writeJSON(`waypoints${num?"."+num:""}.json`, waypoints); +}; diff --git a/apps/waypoints/metadata.json b/apps/waypoints/metadata.json new file mode 100644 index 000000000..d7fa00f7e --- /dev/null +++ b/apps/waypoints/metadata.json @@ -0,0 +1,20 @@ +{ "id": "waypoints", + "name": "Waypoints", + "version":"0.01", + "description": "Provides 'waypoints.json' used by various navigation apps, as well as a way to edit it from the App Loader with maps or a list", + "icon": "app.png", + "tags": "tool,outdoors,gps", + "type": "waypoints", + "supports" : ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "interface": "interface.html", + "storage": [ + {"name":"waypoints","url":"lib.js"} + ], + "data": [ + {"name":"waypoints.json","url":"waypoints.json"}, + {"name":"waypoints.1.json"}, + {"name":"waypoints.2.json"}, + {"name":"waypoints.3.json"} + ] +} diff --git a/apps/waypoints/waypoints.json b/apps/waypoints/waypoints.json new file mode 100644 index 000000000..066f05c10 --- /dev/null +++ b/apps/waypoints/waypoints.json @@ -0,0 +1,12 @@ +[ + { + "name":"No10", + "lat":51.5032, + "lon":-0.1269 + }, + { + "name":"Stone", + "lat":51.1788, + "lon":-1.8260 + } +] diff --git a/apps/wclock/ChangeLog b/apps/wclock/ChangeLog index 9a2ebdd5f..e50ee6842 100644 --- a/apps/wclock/ChangeLog +++ b/apps/wclock/ChangeLog @@ -1,2 +1,3 @@ 0.02: Modified for use with new bootloader and firmware 0.03: setUI and support for different screens +0.04: Tell clock widgets to hide. diff --git a/apps/wclock/clock-word.js b/apps/wclock/clock-word.js index aff134273..7ddb7bc35 100644 --- a/apps/wclock/clock-word.js +++ b/apps/wclock/clock-word.js @@ -122,11 +122,11 @@ Bangle.on('lcdPower', function(on) { if (on) drawWordClock(); }); +// Show launcher when button pressed +Bangle.setUI("clock"); + g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); setInterval(drawWordClock, 1E4); drawWordClock(); - -// Show launcher when button pressed -Bangle.setUI("clock"); diff --git a/apps/wclock/metadata.json b/apps/wclock/metadata.json index f22b53dc1..820af7ac0 100644 --- a/apps/wclock/metadata.json +++ b/apps/wclock/metadata.json @@ -1,7 +1,7 @@ { "id": "wclock", "name": "Word Clock", - "version": "0.03", + "version": "0.04", "description": "Display Time as Text", "icon": "clock-word.png", "screenshots": [{"url":"screenshot_word.png"}], diff --git a/apps/weather/ChangeLog b/apps/weather/ChangeLog index 101da48e1..da28d8d5a 100644 --- a/apps/weather/ChangeLog +++ b/apps/weather/ChangeLog @@ -12,3 +12,5 @@ 0.13: Tweak Bangle.js 2 light theme colors 0.14: Use weather condition code for icon selection 0.15: Fix widget icon +0.16: Don't mark app as clock +0.17: Added clkinfo for clocks. \ No newline at end of file diff --git a/apps/weather/app.js b/apps/weather/app.js index efd9b0209..f63b226b9 100644 --- a/apps/weather/app.js +++ b/apps/weather/app.js @@ -101,7 +101,11 @@ weather.on("update", update); update(); -// Show launcher when middle button pressed +// We want this app to behave like a clock: +// i.e. show launcher when middle button pressed Bangle.setUI("clock"); +// But the app is not actually a clock +// This matters for widgets that hide themselves for clocks, like widclk or widclose +delete Bangle.CLOCK; Bangle.drawWidgets(); diff --git a/apps/weather/clkinfo.js b/apps/weather/clkinfo.js new file mode 100644 index 000000000..8e502b7fc --- /dev/null +++ b/apps/weather/clkinfo.js @@ -0,0 +1,43 @@ +(function() { + var weather = { + temp: "?", + hum: "?", + wind: "?", + }; + + var weatherJson = storage.readJSON('weather.json'); + if(weatherJson !== undefined && weatherJson.weather !== undefined){ + weather = weatherJson.weather; + weather.temp = locale.temp(weather.temp-273.15); + weather.hum = weather.hum + "%"; + weather.wind = locale.speed(weather.wind).match(/^(\D*\d*)(.*)$/); + weather.wind = Math.round(weather.wind[1]) + "kph"; + } + + var weatherItems = { + name: "Weather", + img: atob("GBiBAf+///u5//n7//8f/9wHP8gDf/gB//AB/7AH/5AcP/AQH/DwD/uAD84AD/4AA/wAAfAAAfAAAfAAAfgAA/////+bP/+zf/+zfw=="), + items: [ + { + name: "temperature", + get: () => ({ text: weather.temp, img: atob("GBiBAf/D//+B//8Y//88//88//88//88//88//8k//8k//8k//8k//8k//8k//4kf/5mf/zDP/yBP/yBP/zDP/5mf/48f/8A///D/w==")}), + show: function() { weatherItems.items[0].emit("redraw"); }, + hide: function () {} + }, + { + name: "humidity", + get: () => ({ text: weather.hum, img: atob("GBiBAf/7///z///x///g///g///Af//Af/3Af/nA//jg//B/v/B/H+A/H8A+D8AeB8AcB4AYA8AYA8AYA+A4A/B4A//4A//8B///Dw==")}), + show: function() { weatherItems.items[1].emit("redraw"); }, + hide: function () {} + }, + { + name: "wind", + get: () => ({ text: weather.wind, img: atob("GBiBAf4f//wP//nn//Pn//Pzg//nAf/meIAOfAAefP///P//+fAAAfAAB////////wAAP4AAH///z///z//nz//nz//zj//wH//8Pw==")}), + show: function() { weatherItems.items[2].emit("redraw"); }, + hide: function () {} + }, + ] + }; + + return weatherItems; +}) \ No newline at end of file diff --git a/apps/weather/metadata.json b/apps/weather/metadata.json index 1d0b6b469..125041ec4 100644 --- a/apps/weather/metadata.json +++ b/apps/weather/metadata.json @@ -1,7 +1,7 @@ { "id": "weather", "name": "Weather", - "version": "0.15", + "version": "0.17", "description": "Show Gadgetbridge weather report", "icon": "icon.png", "screenshots": [{"url":"screenshot.png"}], @@ -13,7 +13,8 @@ {"name":"weather.wid.js","url":"widget.js"}, {"name":"weather","url":"lib.js"}, {"name":"weather.img","url":"icon.js","evaluate":true}, - {"name":"weather.settings.js","url":"settings.js"} + {"name":"weather.settings.js","url":"settings.js"}, + {"name":"weather.clkinfo.js","url":"clkinfo.js"} ], "data": [{"name":"weather.json"}] } 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/widagps/metadata.json b/apps/widagps/metadata.json index 4607ddaa1..ee1eb9f08 100644 --- a/apps/widagps/metadata.json +++ b/apps/widagps/metadata.json @@ -1,10 +1,10 @@ { "id": "widagps", - "name": "AGPS Widget", + "name": "AGPS Widget (automatic download)", "shortName":"AGPS Widget", "icon": "widget.png", "type": "widget", "version":"0.01", - "description": "Load AGPS data in the background", + "description": "Once installed, this widget allows your Bangle.js 2 to load AGPS data in the background **via Gadgetbridge on an Android phone** so it is always up to date. If you just want to upload the latest AGPS data from this app loader, please use the `Assisted GPS Update (AGPS)` app.", "readme": "README.md", "tags": "widget,agps,http", "supports": ["BANGLEJS2"], diff --git a/apps/widalt/ChangeLog b/apps/widalt/ChangeLog new file mode 100644 index 000000000..ec66c5568 --- /dev/null +++ b/apps/widalt/ChangeLog @@ -0,0 +1 @@ +0.01: Initial version diff --git a/apps/widalt/README.md b/apps/widalt/README.md new file mode 100644 index 000000000..08ee4f83d --- /dev/null +++ b/apps/widalt/README.md @@ -0,0 +1,2 @@ +# Altimeter widget +Displays barometric altitude in the top right corner. diff --git a/apps/widalt/metadata.json b/apps/widalt/metadata.json new file mode 100644 index 000000000..309c5bf2c --- /dev/null +++ b/apps/widalt/metadata.json @@ -0,0 +1,18 @@ + +{ + "id": "widalt", + "name": "Altimeter widget", + "version": "0.01", + "description": "Displays barometric altitude", + "readme": "README.md", + "icon": "widalt.png", + "type": "widget", + "tags": "widget", + "supports": ["BANGLEJS2"], + "allow_emulator":false, + "storage": [ + {"name":"widalt.wid.js","url":"widalt.wid.js"}, + {"name":"widalt.settings.js","url":"widalt.settings.js"} + ], + "data": [{"name":"widalt.json"}] + } diff --git a/apps/widalt/widalt.png b/apps/widalt/widalt.png new file mode 100644 index 000000000..43e5465bc Binary files /dev/null and b/apps/widalt/widalt.png differ diff --git a/apps/widalt/widalt.settings.js b/apps/widalt/widalt.settings.js new file mode 100644 index 000000000..57993474e --- /dev/null +++ b/apps/widalt/widalt.settings.js @@ -0,0 +1,27 @@ +(function(back) { + var settings = Object.assign({ + interval: 5000, + }, require('Storage').readJSON("widalt.json", true) || {}); + o=Bangle.getOptions(); + Bangle.getPressure().then((p)=>{ + E.showMenu({ + "" : { "title" : "Altimeter Widget" }, + "< Back" : () => back(), + 'QNH': { + value: Math.floor(o.seaLevelPressure), + min: 100, max: 10000, + format: v=>(v+"hPa\nAlt: "+(44330 * (1.0 - Math.pow(p.pressure/v, 0.1903))).toFixed(0)+"m"), + onchange: v => {Bangle.setOptions({seaLevelPressure:v});} + }, + 'update Interval': { + value: settings.interval/1000, + min: 1, max: 60, + format: v=>(v+"s"), + onchange: v => { + settings.interval=v*1000; + require('Storage').writeJSON("widalt.json", settings); + } + } + }); + }); +}) diff --git a/apps/widalt/widalt.wid.js b/apps/widalt/widalt.wid.js new file mode 100644 index 000000000..dbd1a763e --- /dev/null +++ b/apps/widalt/widalt.wid.js @@ -0,0 +1,38 @@ +(()=>{ + var alt=""; + var lastAlt=0; + var settings = Object.assign({ + interval: 5000, + }, require('Storage').readJSON("widalt.json", true) || {}); + Bangle.setBarometerPower(true,"widalt"); + Bangle.on("pressure", (p)=>{ + if (Math.floor(p.altitude)!=lastAlt) { + lastAlt=Math.floor(p.altitude); + alt=p.altitude.toFixed(0); + var w = WIDGETS["widalt"].width; + WIDGETS["widalt"].width = 1 + (alt.length)*12+16; + if (w!=WIDGETS["widalt"].width) Bangle.drawWidgets(); + else WIDGETS["widalt"].draw(); + } + Bangle.setBarometerPower(false,"widalt") + setTimeout(()=>{Bangle.setBarometerPower(true,"widalt");},settings.interval); + }); + + function draw() { + if (!Bangle.isLCDOn()) return; + g.reset(); + g.setColor(g.theme.bg); + g.fillRect(this.x, this.y, this.x + this.width, this.y + 23); + g.setColor(g.theme.fg); + g.drawImage(atob("EBCBAAAAAAAIAAwgFXAX0BCYIIggTD/EYPZADkACf/4AAAAA"), this.x, this.y+4); +g.setFontCustom(atob("AAAAABwAAOAAAgAAHAADwAD4AB8AB8AA+AAeAADAAAAOAAP+AH/8B4DwMAGBgAwMAGBgAwOAOA//gD/4AD4AAAAAAAABgAAcAwDAGAwAwP/+B//wAAGAAAwAAGAAAAAAAAIAwHgOA4DwMA+BgOwMDmBg4wOeGA/gwDwGAAAAAAAAAGAHA8A4DwMAGBhAwMMGBjgwOcOA+/gDj4AAAAABgAAcAAHgADsAA5gAOMAHBgBwMAP/+B//wABgAAMAAAAAAAgD4OB/AwOYGBjAwMYGBjBwMe8Bh/AIHwAAAAAAAAAfAAP8AHxwB8GAdgwPMGBxgwMOOAB/gAH4AAAAAAABgAAMAABgAwMAeBgPgMHwBj4AN8AB+AAPAABAAAAAAAMfAH38B/xwMcGBhgwMMGBjgwP+OA+/gDj4AAAAAAAAOAAH4AA/gQMMGBgzwME8BhvAOPgA/4AD8AAEAAAAAAGAwA4OAHBwAAA="), 46, atob("BAgMDAwMDAwMDAwMBQ=="), 21+(1<<8)+(1<<16)); + g.setFontAlign(-1, 0); + g.drawString(alt, this.x+16, this.y + 12); + } + WIDGETS["widalt"] = { + area: "tr", + width: 6, + draw: draw + }; + +})(); diff --git a/apps/widbt_notify/ChangeLog b/apps/widbt_notify/ChangeLog index 8f1dab908..b9ecd7b7d 100644 --- a/apps/widbt_notify/ChangeLog +++ b/apps/widbt_notify/ChangeLog @@ -11,4 +11,5 @@ 0.12: Prevent repeated execution of `draw()` from the current app. 0.13: Added "connection restored" notification. Fixed restoring of the watchface. 0.14: Added configuration option -0.15: Added option to hide widget when connected \ No newline at end of file +0.15: Added option to hide widget when connected +0.16: Simplify code, add option to disable displaying a message \ No newline at end of file diff --git a/apps/widbt_notify/metadata.json b/apps/widbt_notify/metadata.json index 566a51b57..626ffcb8b 100644 --- a/apps/widbt_notify/metadata.json +++ b/apps/widbt_notify/metadata.json @@ -1,8 +1,8 @@ { "id": "widbt_notify", "name": "Bluetooth Widget with Notification", - "version": "0.15", - "description": "Show the current Bluetooth connection status in the top right of the clock and vibrate when disconnected.", + "version": "0.16", + "description": "Show the current Bluetooth connection status with some optional features: show message, buzz on connect/loss, hide always/if connected.", "icon": "widget.png", "type": "widget", "tags": "widget,bluetooth", diff --git a/apps/widbt_notify/settings.js b/apps/widbt_notify/settings.js index 1e0d5036b..5c67fed7b 100644 --- a/apps/widbt_notify/settings.js +++ b/apps/widbt_notify/settings.js @@ -1,69 +1,57 @@ (function(back) { - var FILE = "widbt_notify.json"; + + var filename = "widbt_notify.json"; + + // set Storage and load settings + var storage = require("Storage"); var settings = Object.assign({ - secondsOnUnlock: false, - }, require('Storage').readJSON(FILE, true) || {}); + showWidget: true, + buzzOnConnect: true, + buzzOnLoss: true, + hideConnected: true, + showMessage: true, + nextBuzz: 30000 + }, storage.readJSON(filename, true) || {}); - function writeSettings() { - require('Storage').writeJSON(FILE, settings); - } - - // Helper method which uses int-based menu item for set of string values - function stringItems(startvalue, writer, values) { + // setup boolean menu entries + function boolEntry(key) { return { - value: (startvalue === undefined ? 0 : values.indexOf(startvalue)), - format: v => values[v], - min: 0, - max: values.length - 1, - wrap: true, - step: 1, + value: settings[key], onchange: v => { - writer(values[v]); - writeSettings(); + // change the value of key + settings[key] = v; + // write to storage + storage.writeJSON(filename, settings); } }; } - // Helper method which breaks string set settings down to local settings object - function stringInSettings(name, values) { - return stringItems(settings[name], v => settings[name] = v, values); - } - - var mainmenu = { + // setup menu + var menu = { "": { "title": "Bluetooth Widget WN" }, "< Back": () => back(), - "Show Widget": { - value: (settings.showWidget !== undefined ? settings.showWidget : true), + "Show Widget": boolEntry("showWidget"), + "Buzz on connect": boolEntry("buzzOnConnect"), + "Buzz on loss": boolEntry("buzzOnLoss"), + "Hide connected": boolEntry("hideConnected"), + "Show Message": boolEntry("showMessage"), + "Next Buzz": { + value: settings.nextBuzz, + step: 1000, + min: 1000, + max: 120000, + wrap: true, + format: v => (v / 1000) + "s", onchange: v => { - settings.showWidget = v; - writeSettings(); + settings.nextBuzz = v; + storage.writeJSON(filename, settings); } - }, - "Buzz on Connect": { - value: (settings.buzzOnConnect !== undefined ? settings.buzzOnConnect : true), - onchange: v => { - settings.buzzOnConnect = v; - writeSettings(); - } - }, - "Buzz on loss": { - value: (settings.buzzOnLoss !== undefined ? settings.buzzOnLoss : true), - onchange: v => { - settings.buzzOnLoss = v; - writeSettings(); - } - }, - "Hide connected": { - value: (settings.hideConnected !== undefined ? settings.hideConnected : false), - onchange: v => { - settings.hideConnected = v; - writeSettings(); - } - } + } }; - E.showMenu(mainmenu); + // draw main menu + E.showMenu(menu); -}); +}) \ No newline at end of file diff --git a/apps/widbt_notify/widget.js b/apps/widbt_notify/widget.js index de2baa3cf..1b192412a 100644 --- a/apps/widbt_notify/widget.js +++ b/apps/widbt_notify/widget.js @@ -1,110 +1,90 @@ -WIDGETS.bluetooth_notify = { +(function() { + // load settings + var settings = Object.assign({ + showWidget: true, + buzzOnConnect: true, + buzzOnLoss: true, + hideConnected: true, + showMessage: true, + nextBuzz: 30000 + }, require("Storage").readJSON("widbt_notify.json", true) || {}); + + // setup widget with to hide if connected and option set + var widWidth = settings.hideConnected && NRF.getSecurityStatus().connected ? 0 : 15; + + // write widget with loaded settings + WIDGETS.bluetooth_notify = Object.assign(settings, { + + // set area and width area: "tr", - width: 15, + width: widWidth, + + // setup warning status warningEnabled: 1, - // ------------ Settings -------- very lame - need to improve - readshowWidget: function() { - var showWidget; - const SETTINGSFILE = "widbt_notify.json"; - function def (value, def) {return value !== undefined ? value : def;} - var settings = require('Storage').readJSON(SETTINGSFILE, true) || {}; - showWidget = def(settings.showWidget, true); - return showWidget; - }, - - readBuzzOnConnect: function() { - var buzzOnConnect; - const SETTINGSFILE = "widbt_notify.json"; - function def (value, def) {return value !== undefined ? value : def;} - var settings = require('Storage').readJSON(SETTINGSFILE, true) || {}; - buzzOnConnect = def(settings.buzzOnConnect, true); - return buzzOnConnect; - }, - - readBuzzOnLoss: function() { - var buzzOnLoss; - const SETTINGSFILE = "widbt_notify.json"; - function def (value, def) {return value !== undefined ? value : def;} - var settings = require('Storage').readJSON(SETTINGSFILE, true) || {}; - buzzOnLoss = def(settings.buzzOnLoss, true); - return buzzOnLoss; - }, - - readHideConnected: function() { - var hideConnected; - const SETTINGSFILE = "widbt_notify.json"; - function def (value, def) {return value !== undefined ? value : def;} - var settings = require('Storage').readJSON(SETTINGSFILE, true) || {}; - hideConnected = def(settings.hideConnected, true); - return hideConnected; - }, - - - // ------------ Settings -------- - draw: function() { - if (WIDGETS.bluetooth_notify.readshowWidget()){ - g.reset(); - if (NRF.getSecurityStatus().connected) { - if (!WIDGETS.bluetooth_notify.readHideConnected()) { - g.setColor((g.getBPP() > 8) ? "#07f" : (g.theme.dark ? "#0ff" : "#00f")); - g.drawImage(atob("CxQBBgDgFgJgR4jZMawfAcA4D4NYybEYIwTAsBwDAA=="), 2 + this.x, 2 + this.y); - } - } else { - // g.setColor(g.theme.dark ? "#666" : "#999"); - g.setColor("#f00"); // red is easier to distinguish from blue - g.drawImage(atob("CxQBBgDgFgJgR4jZMawfAcA4D4NYybEYIwTAsBwDAA=="), 2 + this.x, 2 + this.y); - } + if (this.showWidget) { + g.reset(); + if (NRF.getSecurityStatus().connected) { + if (!this.hideConnected) { + g.setColor((g.getBPP() > 8) ? "#07f" : (g.theme.dark ? "#0ff" : "#00f")); + g.drawImage(atob("CxQBBgDgFgJgR4jZMawfAcA4D4NYybEYIwTAsBwDAA=="), 2 + this.x, 2 + this.y); + } + } else { + // g.setColor(g.theme.dark ? "#666" : "#999"); + g.setColor("#f00"); // red is easier to distinguish from blue + g.drawImage(atob("CxQBBgDgFgJgR4jZMawfAcA4D4NYybEYIwTAsBwDAA=="), 2 + this.x, 2 + this.y); } + } }, - - redrawCurrentApp: function(){ - if(typeof(draw)=='function'){ - g.clear(); - draw(); - Bangle.loadWidgets(); - Bangle.drawWidgets(); - }else{ - load(); // fallback. This might reset some variables + + redrawCurrentApp: function() { + if (typeof(draw) == 'function') { + g.clear(); + draw(); + Bangle.loadWidgets(); + Bangle.drawWidgets(); + } else { + load(); // fallback. This might reset some variables + } + }, + + onNRF: function(connect) { + // setup widget with and reload widgets to show/hide if hideConnected is enabled + if (this.hideConnected) { + this.width = connect ? 0 : 15; // ensures correct redraw + Bangle.drawWidgets(); + } else { + // redraw widget + this.draw(); + } + + if (this.warningEnabled) { + if (this.showMessage) { + E.showMessage( /*LANG*/ 'Connection\n' + (connect ? /*LANG*/ 'restored.' : /*LANG*/ 'lost.'), 'Bluetooth'); + setTimeout(() => { + WIDGETS.bluetooth_notify.redrawCurrentApp(); + }, 3000); // clear message - this will reload the widget, resetting 'warningEnabled'. } - }, - - connect: function() { - if(WIDGETS.bluetooth_notify.warningEnabled == 1){ - E.showMessage(/*LANG*/'Connection\nrestored.', 'Bluetooth'); - setTimeout(()=>{WIDGETS.bluetooth_notify.redrawCurrentApp();}, 3000); // clear message - this will reload the widget, resetting 'warningEnabled'. - - WIDGETS.bluetooth_notify.warningEnabled = 0; - setTimeout('WIDGETS.bluetooth_notify.warningEnabled = 1;', 30000); // don't buzz for the next 30 seconds. - - var quiet = (require('Storage').readJSON('setting.json',1)||{}).quiet; - if(!quiet && WIDGETS.bluetooth_notify.readBuzzOnConnect()){ - Bangle.buzz(700, 1); // buzz on connection resume - } - } - WIDGETS.bluetooth_notify.draw(); + this.warningEnabled = 0; + setTimeout('WIDGETS.bluetooth_notify.warningEnabled = 1;', this.nextBuzz); // don't buzz for the next X seconds. - }, - - disconnect: function() { - if(WIDGETS.bluetooth_notify.warningEnabled == 1){ - E.showMessage(/*LANG*/ 'Connection\nlost.', 'Bluetooth'); - setTimeout(()=>{WIDGETS.bluetooth_notify.redrawCurrentApp();}, 3000); // clear message - this will reload the widget, resetting 'warningEnabled'. - - WIDGETS.bluetooth_notify.warningEnabled = 0; - setTimeout('WIDGETS.bluetooth_notify.warningEnabled = 1;', 30000); // don't buzz for the next 30 seconds. - - var quiet = (require('Storage').readJSON('setting.json',1)||{}).quiet; - if(!quiet && WIDGETS.bluetooth_notify.readBuzzOnLoss()){ - Bangle.buzz(700, 1); // buzz on connection loss - } - } - - WIDGETS.bluetooth_notify.draw(); + var quiet = (require('Storage').readJSON('setting.json', 1) || {}).quiet; + if (!quiet && (connect ? this.buzzOnConnect : this.buzzOnLoss)) { + Bangle.buzz(700, 1); // buzz on connection resume or loss + } + } } -}; -NRF.on('connect', WIDGETS.bluetooth_notify.connect); -NRF.on('disconnect', WIDGETS.bluetooth_notify.disconnect); + }); + + // clear variables + settings = undefined; + widWidth = undefined; + + // setup bluetooth connection events + NRF.on('connect', (addr) => WIDGETS.bluetooth_notify.onNRF(addr)); + NRF.on('disconnect', () => WIDGETS.bluetooth_notify.onNRF()); + +})() \ No newline at end of file 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/widcw/ChangeLog b/apps/widcw/ChangeLog index a4bc24d1a..07b8f7424 100644 --- a/apps/widcw/ChangeLog +++ b/apps/widcw/ChangeLog @@ -1 +1,2 @@ -0.01: First version \ No newline at end of file +0.01: First version +0.02: Fix memory leak \ No newline at end of file diff --git a/apps/widcw/metadata.json b/apps/widcw/metadata.json index 653b093ec..467ab1729 100644 --- a/apps/widcw/metadata.json +++ b/apps/widcw/metadata.json @@ -1,7 +1,7 @@ { "id": "widcw", "name": "Calendar Week Widget", - "version": "0.01", + "version": "0.02", "description": "Widget which shows the current calendar week", "icon": "widget.png", "type": "widget", diff --git a/apps/widcw/widget.js b/apps/widcw/widget.js index ef43a4551..e33ad0aad 100644 --- a/apps/widcw/widget.js +++ b/apps/widcw/widget.js @@ -34,8 +34,8 @@ } // redraw when date changes - setTimeout(()=>WIDGETS["widcw"].draw(), (86401 - Math.floor(date/1000) % 86400)*1000); - + if (WIDGETS["widcw"].to) clearTimeout(WIDGETS["widcw"].to); + WIDGETS["widcw"].to = setTimeout(()=>WIDGETS["widcw"].draw(), (86401 - Math.floor(date/1000) % 86400)*1000); } // add your widget diff --git a/apps/widdst/ChangeLog b/apps/widdst/ChangeLog index ec66c5568..e350137ee 100644 --- a/apps/widdst/ChangeLog +++ b/apps/widdst/ChangeLog @@ -1 +1,2 @@ 0.01: Initial version +0.02: Checks for correct firmware; E.setDST(...) moved to boot.js diff --git a/apps/widdst/boot.js b/apps/widdst/boot.js new file mode 100644 index 000000000..b0a844532 --- /dev/null +++ b/apps/widdst/boot.js @@ -0,0 +1,15 @@ +(() => { + + if (E.setDST) { + var dstSettings = require('Storage').readJSON('widdst.json',1)||{}; + if (dstSettings.has_dst) { + E.setDST(60*dstSettings.dst_size, 60*dstSettings.tz, dstSettings.dst_start.dow_number, dstSettings.dst_start.dow, + dstSettings.dst_start.month, dstSettings.dst_start.day_offset, 60*dstSettings.dst_start.at, + dstSettings.dst_end.dow_number, dstSettings.dst_end.dow, dstSettings.dst_end.month, dstSettings.dst_end.day_offset, + 60*dstSettings.dst_end.at); + } else { + E.setDST(0,0,0,0,0,0,0,0,0,0,0,0); + } + } + +})() diff --git a/apps/widdst/metadata.json b/apps/widdst/metadata.json index 16cc6c94f..144c02998 100644 --- a/apps/widdst/metadata.json +++ b/apps/widdst/metadata.json @@ -1,14 +1,15 @@ { "id": "widdst", "name": "Daylight Saving", - "version":"0.01", + "version":"0.02", "description": "Widget to set daylight saving rules. Requires Espruino 2v14.49 or later - see the instructions below for more information.", "icon": "icon.png", "type": "widget", "tags": "widget,tool", - "supports" : ["BANGLEJS2"], + "supports" : ["BANGLEJS","BANGLEJS2"], "readme": "README.md", "storage": [ {"name":"widdst.wid.js","url":"widget.js"}, + {"name":"widdst.boot.js","url":"boot.js"}, {"name":"widdst.settings.js","url":"settings.js"} ], "data": [{"name":"widdst.json"}] diff --git a/apps/widdst/widget.js b/apps/widdst/widget.js index f9a5b7f81..f690cc68f 100644 --- a/apps/widdst/widget.js +++ b/apps/widdst/widget.js @@ -40,26 +40,16 @@ clear(); } } - - function setDst() { - var dstSettings = require('Storage').readJSON('widdst.json',1)||{}; - if (dstSettings.has_dst) { - E.setDST(60*dstSettings.dst_size, 60*dstSettings.tz, dstSettings.dst_start.dow_number, dstSettings.dst_start.dow, - dstSettings.dst_start.month, dstSettings.dst_start.day_offset, 60*dstSettings.dst_start.at, - dstSettings.dst_end.dow_number, dstSettings.dst_end.dow, dstSettings.dst_end.month, dstSettings.dst_end.day_offset, - 60*dstSettings.dst_end.at); - } else { - E.setDST(0,0,0,0,0,0,0,0,0,0,0,0); - } + + // Register ourselves + if (E.setDST) { + WIDGETS["widdst"] = { + area: "tl", + width: 0, + draw: draw + }; + } else { + E.showAlert("Firmware update needed to support Daylight Saving Time"); } - setDst(); - - // Register ourselves - WIDGETS["widdst"] = { - area: "tl", - width: 0, - draw: draw - }; - })() diff --git a/apps/widgps/ChangeLog b/apps/widgps/ChangeLog index 0eb9e5692..b530843e7 100644 --- a/apps/widgps/ChangeLog +++ b/apps/widgps/ChangeLog @@ -4,3 +4,6 @@ 0.04: Show GPS fix status 0.05: Don't poll for GPS status, override setGPSPower handler (fix #1456) 0.06: Periodically update so the always on display does show current GPS fix +0.07: Alternative marker icon (configurable via settings) +0.08: Add ability to hide the icon when GPS is off, for a cleaner appearance. +0.09: Do not take widget space if icon is hidden diff --git a/apps/widgps/README.md b/apps/widgps/README.md index 37e088bcf..98f0ba6f7 100644 --- a/apps/widgps/README.md +++ b/apps/widgps/README.md @@ -6,12 +6,22 @@ The GPS can quickly run the battery down if it is on all the time so it is useful to know if it has been switched on or not. - Uses Bangle.isGPSOn() -- Shows in grey when the GPS is off -- Shows in amber when the GPS is on but has no fix -- Shows in green when the GPS is on and has a fix +There are two icons which can be used to visualize the GPS/GNSS status: +1. A cross colored depending on the GPS/GNSS status + - Shows in grey when the GPS is off + - Shows in amber when the GPS is on but has no fix + - Shows in green when the GPS is on and has a fix +2. Different place markers depending on GPS/GNSS status + +You can also choose to hide the icon when the GPS is off in the settings. + Written by: [Hugh Barney](https://github.com/hughbarney) For support and discussion please post in the [Bangle JS Forum](http://forum.espruino.com/microcosms/1424/) Extended by Marco ([myxor](https://github.com/myxor)) + +Extended by khromov ([khromov](https://github.com/khromov)) + +Place marker icons from [icons8.com](https://icons8.com/icon/set/maps/material-outlined). diff --git a/apps/widgps/default.json b/apps/widgps/default.json new file mode 100644 index 000000000..28482ddb0 --- /dev/null +++ b/apps/widgps/default.json @@ -0,0 +1 @@ +{"crossIcon": true, "hideWhenGpsOff": false} \ No newline at end of file diff --git a/apps/widgps/metadata.json b/apps/widgps/metadata.json index b135c77bd..cfd35f5bb 100644 --- a/apps/widgps/metadata.json +++ b/apps/widgps/metadata.json @@ -1,14 +1,19 @@ { "id": "widgps", "name": "GPS Widget", - "version": "0.06", - "description": "Tiny widget to show the power and fix status of the GPS", + "version": "0.09", + "description": "Tiny widget to show the power and fix status of the GPS/GNSS", "icon": "widget.png", "type": "widget", "tags": "widget,gps", "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", "storage": [ - {"name":"widgps.wid.js","url":"widget.js"} + {"name":"widgps.wid.js","url":"widget.js"}, + {"name":"widgps.default.json","url":"default.json"}, + {"name":"widgps.settings.js","url":"settings.js"} + ], + "data": [ + {"name":"widgps.json"} ] } diff --git a/apps/widgps/settings.js b/apps/widgps/settings.js new file mode 100644 index 000000000..7a1c186c9 --- /dev/null +++ b/apps/widgps/settings.js @@ -0,0 +1,32 @@ +(function(back) { +function writeSettings(key, value) { + var s = require('Storage').readJSON(FILE, true) || {}; + s[key] = value; + require('Storage').writeJSON(FILE, s); + readSettings(); +} + +function readSettings() { + settings = Object.assign( + require('Storage').readJSON("widgps.default.json", true) || {}, + require('Storage').readJSON(FILE, true) || {}); +} + +var FILE = "widgps.json"; +var settings; +readSettings(); + +var mainmenu = { + '' : {'title' : 'GPS widget'}, + '< Back' : back, + "Cross icon" : { + value : settings.crossIcon , + onchange : v => { writeSettings("crossIcon", v); } + }, + "Hide icon when GPS off" : { + value : settings.hideWhenGpsOff , + onchange : v => { writeSettings("hideWhenGpsOff", v); } + }, +}; +E.showMenu(mainmenu); +}); \ No newline at end of file diff --git a/apps/widgps/widget.js b/apps/widgps/widget.js index 206096013..73351eaa6 100644 --- a/apps/widgps/widget.js +++ b/apps/widgps/widget.js @@ -1,36 +1,85 @@ -(function(){ - var interval; +(function() { - // override setGPSPower so we know if GPS is on or off - var oldSetGPSPower = Bangle.setGPSPower; - Bangle.setGPSPower = function(on,id) { - var isGPSon = oldSetGPSPower(on,id); - WIDGETS.gps.draw(); - return isGPSon; - } +let settings = Object.assign( + require('Storage').readJSON("widgps.default.json", true) || {}, + require('Storage').readJSON("widgps.json", true) || {} +); - WIDGETS.gps={area:"tr",width:24,draw:function() { +var interval; + +// override setGPSPower so we know if GPS is on or off +var oldSetGPSPower = Bangle.setGPSPower; +Bangle.setGPSPower = function(on, id) { + var isGPSon = oldSetGPSPower(on, id); + WIDGETS.gps.width = !isGPSon && settings.hideWhenGpsOff ? 0 : 24; + Bangle.drawWidgets(); + return isGPSon; +}; + +WIDGETS.gps = { + area : "tr", + width : !Bangle.isGPSOn() && settings.hideWhenGpsOff ? 0 : 24, + draw : function() { g.reset(); - if (Bangle.isGPSOn()) { - const gpsObject = Bangle.getGPSFix(); - if (gpsObject && gpsObject.fix > 0) { - g.setColor("#0F0"); // on and has fix = green - } else { - g.setColor("#FD0"); // on but no fix = amber - } - } else { - g.setColor("#888"); // off = grey - } // check if we need to update the widget periodically if (Bangle.isGPSOn() && interval === undefined) { - interval = setInterval(function() { - WIDGETS.gps.draw(WIDGETS.gps); - }, 10*1000); // update every 10 seconds to show gps fix/no fix + interval = setInterval( + function() { WIDGETS.gps.draw(WIDGETS.gps); }, + 10 * 1000); // update every 10 seconds to show gps fix/no fix } else if (!Bangle.isGPSOn() && interval !== undefined) { clearInterval(interval); interval = undefined; } - g.drawImage(atob("GBiBAAAAAAAAAAAAAA//8B//+BgYGBgYGBgYGBgYGBgYGBgYGB//+B//+BgYGBgYGBgYGBgYGBgYGBgYGB//+A//8AAAAAAAAAAAAA=="), this.x, 2+this.y); - }}; + if (settings.crossIcon) { + if (Bangle.isGPSOn()) { + const gpsObject = Bangle.getGPSFix(); + if (gpsObject && gpsObject.fix > 0) { + g.setColor("#0F0"); // on and has fix = green + } else { + g.setColor("#FD0"); // on but no fix = amber + } + + g.drawImage( + atob( + "GBiBAAAAAAAAAAAAAA//8B//+BgYGBgYGBgYGBgYGBgYGBgYGB//+B//+BgYGBgYGBgYGBgYGBgYGBgYGB//+A//8AAAAAAAAAAAAA=="), + this.x, 2 + this.y); + + } else { + if(!settings.hideWhenGpsOff) { + g.setColor("#888"); // off = grey + + g.drawImage( + atob( + "GBiBAAAAAAAAAAAAAA//8B//+BgYGBgYGBgYGBgYGBgYGBgYGB//+B//+BgYGBgYGBgYGBgYGBgYGBgYGB//+A//8AAAAAAAAAAAAA=="), + this.x, 2 + this.y); + } + } + + } else { // marker icons + if (Bangle.isGPSOn()) { + const gpsObject = Bangle.getGPSFix(); + if (gpsObject && gpsObject.fix > 0) { + // on and has fix + g.drawImage( + atob("GBiBAAAAAAAAAAB+AAD/AAHDgAMAwAcAwAY8YAY8YAY8YAY8YAMAwAMAwAOBwAGBgAHDgADDAABmAAB+AAA8AAAYAAAAAAAAAAAAAA=="), + this.x, 2 + this.y); + + } else { + // GNSS on but no fix + g.drawImage( + atob("GBiBAAAAAAAAAAh+AA3/gA+B4A8AcA+AMAA8GAB+GADnDADDDADDDDDDADBmADB+ABg8ABgYAAwB8A4A8AeB8AH/sAB+EAAAAAAAAA=="), + this.x, 2 + this.y); + } + } else { + // GNSS off + if(!settings.hideWhenGpsOff) { + g.drawImage( + atob("GBiBAAAAAAAAAAB+ABj/ABxDgA4AwAcAwAeMYAfEYAbgYAZwYAM4wAMcQAOOAAGHAAHDgADDwABm4AB+cAA8OAAYGAAAAAAAAAAAAA=="), + this.x, 2 + this.y); + } + } + } + } +}; })(); diff --git a/apps/widmeda/ChangeLog b/apps/widmeda/ChangeLog new file mode 100644 index 000000000..7415150e6 --- /dev/null +++ b/apps/widmeda/ChangeLog @@ -0,0 +1 @@ +0.01: Initial Medical Alert Widget! diff --git a/apps/widmeda/README.md b/apps/widmeda/README.md new file mode 100644 index 000000000..0bbfc4dc3 --- /dev/null +++ b/apps/widmeda/README.md @@ -0,0 +1,23 @@ +# Medical Alert Widget + +Shows a medical alert logo in the top right widget area, and a medical alert message in the bottom widget area. + +**Note:** this is not a replacement for a medical alert band but hopefully a useful addition. + +## Features + +Implemented: + +- Basic medical alert logo and message +- Only display bottom widget on clocks +- High contrast colours depending on theme + +Future: + +- Configure when to show bottom widget (always/never/clocks) +- Configure medical alert text +- Show details when touched + +## Creator + +James Taylor ([jt-nti](https://github.com/jt-nti)) diff --git a/apps/widmeda/metadata.json b/apps/widmeda/metadata.json new file mode 100644 index 000000000..346306b8e --- /dev/null +++ b/apps/widmeda/metadata.json @@ -0,0 +1,15 @@ +{ "id": "widmeda", + "name": "Medical Alert Widget", + "shortName":"Medical Alert", + "version":"0.01", + "description": "Display a medical alert in the bottom widget section.", + "icon": "widget.png", + "type": "widget", + "tags": "health,medical,tools,widget", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "screenshots": [{"url":"screenshot_light.png"}], + "storage": [ + {"name":"widmeda.wid.js","url":"widget.js"} + ] +} diff --git a/apps/widmeda/screenshot_light.png b/apps/widmeda/screenshot_light.png new file mode 100644 index 000000000..bf92d753d Binary files /dev/null and b/apps/widmeda/screenshot_light.png differ diff --git a/apps/widmeda/widget.js b/apps/widmeda/widget.js new file mode 100644 index 000000000..a2d109596 --- /dev/null +++ b/apps/widmeda/widget.js @@ -0,0 +1,30 @@ +(() => { + // Top right star of life logo + WIDGETS["widmedatr"]={ + area: "tr", + width: 24, + 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"]={ + area: "bl", + 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.clearRect(this.x, this.y, this.x + this.width - 1, this.y + 23); + g.drawString("MEDICAL ALERT", this.width / 2, this.y + ( 23 / 2 )); + } + }; +})(); diff --git a/apps/widmeda/widget.png b/apps/widmeda/widget.png new file mode 100644 index 000000000..249bf15bf Binary files /dev/null and b/apps/widmeda/widget.png differ 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/widram/ChangeLog b/apps/widram/ChangeLog index e7b406081..7b00c8a48 100644 --- a/apps/widram/ChangeLog +++ b/apps/widram/ChangeLog @@ -1,2 +1,3 @@ 0.01: New Widget! 0.02: Now also visible on Bangle.js 2 +0.03: Remove global declaration of BANGLEJS2 var (fix #2123) diff --git a/apps/widram/metadata.json b/apps/widram/metadata.json index 19ae6d311..ebf23742b 100644 --- a/apps/widram/metadata.json +++ b/apps/widram/metadata.json @@ -2,7 +2,7 @@ "id": "widram", "name": "RAM Widget", "shortName": "RAM Widget", - "version": "0.02", + "version": "0.03", "description": "Display your Bangle's RAM usage percentage in a widget", "icon": "widget.png", "type": "widget", diff --git a/apps/widram/widget.js b/apps/widram/widget.js index 210c85357..07b7c0a5f 100644 --- a/apps/widram/widget.js +++ b/apps/widram/widget.js @@ -1,6 +1,6 @@ (() => { function draw() { - BANGLEJS2 = process.env.HWVERSION==2; + const BANGLEJS2 = process.env.HWVERSION==2; g.reset(); var m = process.memory(); var percent = Math.round(m.usage*100/m.total); diff --git a/apps/wpmoto/ChangeLog b/apps/wpmoto/ChangeLog index 84e7affed..63f4bf24c 100644 --- a/apps/wpmoto/ChangeLog +++ b/apps/wpmoto/ChangeLog @@ -1,2 +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 7deacb6ca..f08cb8279 100644 --- a/apps/wpmoto/app.js +++ b/apps/wpmoto/app.js @@ -1,6 +1,6 @@ var loc = require("locale"); -var waypoints = require("Storage").readJSON("waypoints.json") || []; +var waypoints = require("waypoints").load(); var wp = waypoints[0]; if (wp == undefined) wp = {name:"NONE"}; var wp_bearing = 0; @@ -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; } @@ -196,7 +196,7 @@ function addCurrentWaypoint() { } function saveWaypoints() { - require("Storage").writeJSON("waypoints.json", waypoints); + require("waypoints").save(waypoints); } function deleteWaypoint(w) { diff --git a/apps/wpmoto/metadata.json b/apps/wpmoto/metadata.json index f562b23ba..32c41d757 100644 --- a/apps/wpmoto/metadata.json +++ b/apps/wpmoto/metadata.json @@ -2,17 +2,16 @@ "id": "wpmoto", "name": "Waypointer Moto", "shortName": "Waypointer Moto", - "version": "0.02", + "version": "0.04", "description": "Waypoint-based motorcycle navigation aid", "icon": "wpmoto.png", "tags": "tool,outdoors,gps", "supports": ["BANGLEJS","BANGLEJS2"], "screenshots": [{"url":"screenshot.png"},{"url":"screenshot-menu.png"},{"url":"screenshot-delete.png"}], "readme": "README.md", - "interface": "wpmoto.html", + "dependencies" : { "waypoints":"type" }, "storage": [ {"name":"wpmoto.app.js","url":"app.js"}, {"name":"wpmoto.img","url":"icon.js","evaluate":true} - ], - "data": [{"name":"waypoints.json","url":"waypoints.json"}] + ] } diff --git a/apps/wpmoto/waypoints.json b/apps/wpmoto/waypoints.json deleted file mode 100644 index 8a4ab83b8..000000000 --- a/apps/wpmoto/waypoints.json +++ /dev/null @@ -1,5 +0,0 @@ -[ - { - "name":"NONE" - }, -] diff --git a/backup.js b/backup.js index 8a894666e..06d98a366 100644 --- a/backup.js +++ b/backup.js @@ -101,6 +101,7 @@ function bangleUpload() { return Comms.showMessage(`Restoring...`); }) .then(() => Comms.write("\x10"+Comms.getProgressCmd()+"\n")) .then(() => Comms.uploadCommandList(cmds, 0, cmds.length)) + .then(() => getInstalledApps(true)) .then(() => Comms.showMessage(Const.MESSAGE_RELOAD)) .then(() => { Progress.hide({sticky:true}); diff --git a/bin/apploader.js b/bin/apploader.js index 427a0ef99..26c4c1f09 100755 --- a/bin/apploader.js +++ b/bin/apploader.js @@ -1,4 +1,4 @@ -#!/usr/bin/nodejs +#!/usr/bin/env node /* Simple Command-line app loader for Node.js =============================================== @@ -8,35 +8,44 @@ as a normal dependency) because we want `sanitycheck.js` to be able to run *quickly* in travis for every commit, and we don't want NPM pulling in (and compiling native modules) for Noble. + */ var SETTINGS = { pretokenise : true }; var APPSDIR = __dirname+"/../apps/"; -var Utils = require("../core/js/utils.js"); -var AppInfo = require("../core/js/appinfo.js"); var noble; -try { - noble = require('@abandonware/noble'); -} catch (e) {} -if (!noble) try { - noble = require('noble'); -} catch (e) { } +["@abandonware/noble", "noble"].forEach(module => { + if (!noble) try { + noble = require(module); + } catch(e) { + if (e.code !== 'MODULE_NOT_FOUND') { + throw e; + } + } +}); if (!noble) { console.log("You need to:") console.log(" npm install @abandonware/noble") console.log("or:") console.log(" npm install noble") + process.exit(1); } - -var apps = []; - function ERROR(msg) { console.error(msg); process.exit(1); } +//eval(require("fs").readFileSync(__dirname+"../core/js/utils.js")); +var AppInfo = require("../core/js/appinfo.js"); +global.Const = { + /* Are we only putting a single app on a device? If so + apps should all be saved as .bootcde and we write info + about the current app into app.info */ + SINGLE_APP_ONLY : false, +}; +var deviceId = "BANGLEJS2"; var apps = []; var dirs = require("fs").readdirSync(APPSDIR, {withFileTypes: true}); dirs.forEach(dir => { @@ -54,6 +63,10 @@ dirs.forEach(dir => { var args = process.argv; +var bangleParam = args.findIndex(arg => /-b\d/.test(arg)); +if (bangleParam!==-1) { + deviceId = "BANGLEJS"+args.splice(bangleParam, 1)[0][2]; +} if (args.length==3 && args[2]=="list") cmdListApps(); else if (args.length==3 && args[2]=="devices") cmdListDevices(); else if (args.length==4 && args[2]=="install") cmdInstallApp(args[3]); @@ -68,7 +81,10 @@ apploader.js list - list available apps apploader.js devices - list available device addresses -apploader.js install appname [de:vi:ce:ad:dr:es] +apploader.js install [-b1] appname [de:vi:ce:ad:dr:es] + +NOTE: By default this App Loader expects the device it uploads to +(deviceId) to be BANGLEJS2, pass '-b1' for it to work with Bangle.js 1 `); process.exit(0); } @@ -104,7 +120,10 @@ function cmdInstallApp(appId, deviceAddress) { fileGetter:function(url) { console.log(__dirname+"/"+url); return Promise.resolve(require("fs").readFileSync(__dirname+"/../"+url).toString("binary")); - }, settings : SETTINGS}).then(files => { + }, + settings : SETTINGS, + device : { id : deviceId } + }).then(files => { //console.log(files); var command = files.map(f=>f.cmd).join("\n")+"\n"; bangleSend(command, deviceAddress).then(() => process.exit(0)); diff --git a/bin/create_app_supports_field.js b/bin/create_app_supports_field.js index d6aada357..24d6694f2 100644 --- a/bin/create_app_supports_field.js +++ b/bin/create_app_supports_field.js @@ -1,4 +1,4 @@ -#!/usr/bin/nodejs +#!/usr/bin/env nodejs /* Quick hack to add proper 'supports' field to apps.json */ diff --git a/bin/create_apps_json.sh b/bin/create_apps_json.sh index 30c58bc93..c9f310e57 100755 --- a/bin/create_apps_json.sh +++ b/bin/create_apps_json.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # ================================================================ # apps.json used to contain the metadata for every app. Now the # metadata is stored in each apps's directory - app/yourapp/metadata.js diff --git a/bin/firmwaremaker.js b/bin/firmwaremaker.js index 1c2d9cb77..1dc5ec073 100755 --- a/bin/firmwaremaker.js +++ b/bin/firmwaremaker.js @@ -1,4 +1,4 @@ -#!/usr/bin/nodejs +#!/usr/bin/env nodejs /* Mashes together a bunch of different apps to make a single firmware JS file which can be uploaded. diff --git a/bin/firmwaremaker_c.js b/bin/firmwaremaker_c.js index 4aa8e087f..87065dab4 100755 --- a/bin/firmwaremaker_c.js +++ b/bin/firmwaremaker_c.js @@ -1,4 +1,4 @@ -#!/usr/bin/node +#!/usr/bin/env node /* Mashes together a bunch of different apps into a big binary blob. We then store this *inside* the Bangle.js firmware and can use it diff --git a/bin/pre-publish.sh b/bin/pre-publish.sh index ee73968d7..710160ded 100755 --- a/bin/pre-publish.sh +++ b/bin/pre-publish.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash cd `dirname $0`/.. nodejs bin/sanitycheck.js || exit 1 diff --git a/bin/sanitycheck.js b/bin/sanitycheck.js index 53ef3403d..c7dbb0d03 100755 --- a/bin/sanitycheck.js +++ b/bin/sanitycheck.js @@ -1,4 +1,4 @@ -#!/usr/bin/node +#!/usr/bin/env node /* Checks for any obvious problems in apps.json */ @@ -30,7 +30,11 @@ function ERROR(msg, opt) { function WARN(msg, opt) { // file=app.js,line=1,col=5,endColumn=7 opt = opt||{}; - console.log(`::warning${Object.keys(opt).length?" ":""}${Object.keys(opt).map(k=>k+"="+opt[k]).join(",")}::${msg}`); + if (KNOWN_WARNINGS.includes(msg)) { + console.log(`Known warning : ${msg}`); + } else { + console.log(`::warning${Object.keys(opt).length?" ":""}${Object.keys(opt).map(k=>k+"="+opt[k]).join(",")}::${msg}`); + } warningCount++; } @@ -74,13 +78,22 @@ const APP_KEYS = [ 'supports', 'allow_emulator', 'dependencies' ]; -const STORAGE_KEYS = ['name', 'url', 'content', 'evaluate', 'noOverwite', 'supports']; +const STORAGE_KEYS = ['name', 'url', 'content', 'evaluate', 'noOverwite', 'supports', 'noOverwrite']; const DATA_KEYS = ['name', 'wildcard', 'storageFile', 'url', 'content', 'evaluate']; const SUPPORTS_DEVICES = ["BANGLEJS","BANGLEJS2"]; // device IDs allowed for 'supports' -const METADATA_TYPES = ["app","clock","widget","bootloader","RAM","launch","textinput","scheduler","notify","locale","settings"]; // values allowed for "type" field +const METADATA_TYPES = ["app","clock","widget","bootloader","RAM","launch","textinput","scheduler","notify","locale","settings","waypoints"]; // values allowed for "type" field const FORBIDDEN_FILE_NAME_CHARS = /[,;]/; // used as separators in appid.info const VALID_DUPLICATES = [ '.tfmodel', '.tfnames' ]; const GRANDFATHERED_ICONS = ["s7clk", "snek", "astral", "alpinenav", "slomoclock", "arrow", "pebble", "rebble"]; +const INTERNAL_FILES_IN_APP_TYPE = { // list of app types and files they SHOULD provide... + 'textinput' : ['textinput'], + 'waypoints' : ['waypoints'], + // notify? +}; +/* These are warnings we know about but don't want in our output */ +var KNOWN_WARNINGS = [ +"App gpsrec data file wildcard .gpsrc? does not include app ID" +]; function globToRegex(pattern) { const ESCAPE = '.*+-?^${}()|[]\\'; @@ -163,6 +176,7 @@ apps.forEach((app,appIdx) => { } else ERROR(`App ${app.id} 'dependencies' must be an object`, {file:metadataFile}); } + var fileNames = []; app.storage.forEach((file) => { if (!file.name) ERROR(`App ${app.id} has a file with no name`, {file:metadataFile}); @@ -179,7 +193,12 @@ apps.forEach((app,appIdx) => { ERROR(`App ${app.id} file ${file.name} has unknown device in 'supports' field - ${dev}`, {file:metadataFile}); }); fileNames.push(file.name); - allFiles.push({app: app.id, file: file.name}); + var fileInternal = false; + if (app.type && INTERNAL_FILES_IN_APP_TYPE[app.type]) { + if (INTERNAL_FILES_IN_APP_TYPE[app.type].includes(file.name)) + fileInternal = true; + } + allFiles.push({app: app.id, file: file.name, internal:fileInternal}); if (file.url) if (!fs.existsSync(appDir+file.url)) ERROR(`App ${app.id} file ${file.url} doesn't exist`, {file:metadataFile}); if (!file.url && !file.content && !app.custom) ERROR(`App ${app.id} file ${file.name} has no contents`, {file:metadataFile}); var fileContents = ""; @@ -214,6 +233,13 @@ apps.forEach((app,appIdx) => { console.log("====================================================="); ERROR(`App ${app.id}'s ${file.name} is a JS file but isn't valid JS`, {file:appDirRelative+file.url}); } + // clock app checks + if (app.type=="clock") { + var a = fileContents.indexOf("Bangle.loadWidgets()"); + var b = fileContents.indexOf("Bangle.setUI("); + if (a>=0 && b>=0 && a { for (const key in app) { if (!APP_KEYS.includes(key)) ERROR(`App ${app.id} has unknown key ${key}`, {file:metadataFile}); } + if (app.type && INTERNAL_FILES_IN_APP_TYPE[app.type]) { + INTERNAL_FILES_IN_APP_TYPE[app.type].forEach(fileName => { + if (!fileNames.includes(fileName)) + ERROR(`App ${app.id} should include file named ${fileName} but it doesn't`, {file:metadataFile}); + }); + } }); @@ -313,7 +345,7 @@ while(fileA=allFiles.pop()) { if (globA.test(nameB)||globB.test(nameA)) { if (isGlob(nameA)||isGlob(nameB)) ERROR(`App ${fileB.app} ${typeB} file ${nameB} matches app ${fileA.app} ${typeB} file ${nameA}`); - else if (fileA.app != fileB.app) + else if (fileA.app != fileB.app && (!fileA.internal) && (!fileB.internal)) WARN(`App ${fileB.app} ${typeB} file ${nameB} is also listed as ${typeA} file for app ${fileA.app}`); } }) diff --git a/bin/thumbnailer.js b/bin/thumbnailer.js index b6862741a..0895098e9 100755 --- a/bin/thumbnailer.js +++ b/bin/thumbnailer.js @@ -1,4 +1,4 @@ -#!/usr/bin/node +#!/usr/bin/env node /* var EMULATOR = "banglejs2"; @@ -37,7 +37,8 @@ var SETTINGS = { var Const = { }; module = undefined; -eval(require("fs").readFileSync(__dirname + "/../core/lib/espruinotools.js").toString()); +var Espruino = require(__dirname + "/../core/lib/espruinotools.js"); +//eval(require("fs").readFileSync(__dirname + "/../core/lib/espruinotools.js").toString()); eval(require("fs").readFileSync(__dirname + "/../core/js/utils.js").toString()); eval(require("fs").readFileSync(__dirname + "/../core/js/appinfo.js").toString()); var apps = JSON.parse(require("fs").readFileSync(__dirname+"/../apps.json")); @@ -78,7 +79,7 @@ function getThumbnail(appId, imageFn) { settings : SETTINGS, device : { id : DEVICEID } }).then(files => { - console.log("AppInfo returned");//, files); + console.log(`AppInfo returned for ${appId}`);//, files); flashMemory.set(factoryFlashMemory); jsTransmitString("reset()\n"); console.log("Uploading..."); @@ -88,6 +89,7 @@ function getThumbnail(appId, imageFn) { appLog = ""; jsTransmitString(command); console.log("Done."); + jsTransmitString("Bangle.setLCDMode();clearInterval();clearTimeout();\n"); jsStopIdle(); var rgba = new Uint8Array(GFX_WIDTH*GFX_HEIGHT*4); @@ -96,7 +98,7 @@ function getThumbnail(appId, imageFn) { var firstPixel = rgba32[0]; var blankImage = rgba32.every(col=>col==firstPixel) - if (appLog.indexOf("Uncaught")>=0) + if (appLog.replace("Uncaught Storage Updated!", "").indexOf("Uncaught")>=0) erroredApps.push( { id : app.id, log : appLog } ); if (!blankImage) { diff --git a/core b/core index c46b4edd2..87cf54203 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit c46b4edd2052d0df37fea41f8839af8175a78ec9 +Subproject commit 87cf5420322cd78a13e3d2b76cd12c9fdbddc7d2 diff --git a/css/main.css b/css/main.css index 96a102119..ceb9fcd17 100644 --- a/css/main.css +++ b/css/main.css @@ -30,7 +30,7 @@ } a.mr-2{ - display: flex; + display: flex; align-items: center; } @@ -102,12 +102,23 @@ a.btn.btn-link.dropdown-toggle { content: url("data:image/svg+xml,%3Csvg fill='rgb(87, 85, 217)' xmlns='http://www.w3.org/2000/svg' viewBox='4 4 40 40' width='1em' height='1em'%3E%3Cpath d='M 8.5 5 C 6.0324991 5 4 7.0324991 4 9.5 L 4 30.5 C 4 32.967501 6.0324991 35 8.5 35 L 17 35 L 17 40 L 13.5 40 A 1.50015 1.50015 0 1 0 13.5 43 L 18.253906 43 A 1.50015 1.50015 0 0 0 18.740234 43 L 29.253906 43 A 1.50015 1.50015 0 0 0 29.740234 43 L 34.5 43 A 1.50015 1.50015 0 1 0 34.5 40 L 31 40 L 31 35 L 39.5 35 C 41.967501 35 44 32.967501 44 30.5 L 44 9.5 C 44 7.0324991 41.967501 5 39.5 5 L 8.5 5 z M 8.5 8 L 39.5 8 C 40.346499 8 41 8.6535009 41 9.5 L 41 30.5 C 41 31.346499 40.346499 32 39.5 32 L 29.746094 32 A 1.50015 1.50015 0 0 0 29.259766 32 L 18.746094 32 A 1.50015 1.50015 0 0 0 18.259766 32 L 8.5 32 C 7.6535009 32 7 31.346499 7 30.5 L 7 9.5 C 7 8.6535009 7.6535009 8 8.5 8 z M 17.5 12 C 16.136406 12 15 13.136406 15 14.5 L 15 25.5 C 15 26.863594 16.136406 28 17.5 28 L 30.5 28 C 31.863594 28 33 26.863594 33 25.5 L 33 14.5 C 33 13.136406 31.863594 12 30.5 12 L 17.5 12 z M 18 18 L 30 18 L 30 25 L 18 25 L 18 18 z M 20 35 L 28 35 L 28 40 L 20 40 L 20 35 z'/%3E%3C/svg%3E"); } .icon.icon-favourite { text-indent: 0px; } /*override spectre*/ +.icon.icon-favourite-active { text-indent: 0px; } /*override spectre*/ .icon.icon-favourite::before { - content: "\02661"; /* 0x2661 = empty heart; 0x2606 = empty star */ + content: url("data:image/svg+xml,%3Csvg fill='rgb(255, 0, 0)' xmlns='http://www.w3.org/2000/svg' viewBox='0 -3 50 47' width='1.5em' height='1.5em'%3E%3Cpath d='M 16.375 9 C 10.117188 9 5 14.054688 5 20.28125 C 5 33.050781 19.488281 39.738281 24.375 43.78125 L 25 44.3125 L 25.625 43.78125 C 30.511719 39.738281 45 33.050781 45 20.28125 C 45 14.054688 39.882813 9 33.625 9 C 30.148438 9 27.085938 10.613281 25 13.0625 C 22.914063 10.613281 19.851563 9 16.375 9 Z M 16.375 11 C 19.640625 11 22.480469 12.652344 24.15625 15.15625 L 25 16.40625 L 25.84375 15.15625 C 27.519531 12.652344 30.359375 11 33.625 11 C 38.808594 11 43 15.144531 43 20.28125 C 43 31.179688 30.738281 37.289063 25 41.78125 C 19.261719 37.289063 7 31.179688 7 20.28125 C 7 15.144531 11.1875 11 16.375 11 Z'/%3E%3C/svg%3E"); } .icon.icon-favourite-active::before { - content: "\02665"; /* 0x2665 = solid heart; 0x2605 = solid star */ + content: url("data:image/svg+xml,%3Csvg fill='rgb(255, 0, 0)' xmlns='http://www.w3.org/2000/svg' viewBox='0 -3 50 47' width='1.5em' height='1.5em'%3E%3Cpath d='M 25 44.296875 L 24.363281 43.769531 C 23.363281 42.941406 22.019531 42.027344 20.46875 40.96875 C 14.308594 36.765625 5 30.414063 5 20.285156 C 5 14.0625 10.097656 9 16.363281 9 C 19.714844 9 22.851563 10.457031 25 12.957031 C 27.148438 10.457031 30.289063 9 33.636719 9 C 39.902344 9 45 14.0625 45 20.285156 C 45 30.414063 35.691406 36.765625 29.53125 40.96875 C 27.976563 42.027344 26.636719 42.941406 25.636719 43.769531 Z'/%3E%3C/svg%3E"); } +.icon.icon-favourite span { + font-size: 50%; + color : #F66; + position:relative; + top:-0.7em; +} +.icon.icon-favourite-active span { + color : white; +} + .icon.icon-interface {text-indent: 0px;} /*override spectre*/ .icon.icon-interface::before { position: absolute; left: 50%; top: 70%; diff --git a/index.html b/index.html index b141cffc9..e037ee7aa 100644 --- a/index.html +++ b/index.html @@ -88,8 +88,10 @@ @@ -151,6 +153,10 @@ Bluetooth Compatibility mode (limit to 20 byte writes) +