diff --git a/.eslintignore b/.eslintignore index e657b6260..fcbea07f9 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,3 +2,5 @@ apps/animclk/V29.LBM.js apps/banglerun/rollup.config.js apps/schoolCalendar/fullcalendar/main.js apps/authentiwatch/qr_packed.js +apps/qrcode/qr-scanner.umd.min.js +*.test.js diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml new file mode 100644 index 000000000..1eb009153 --- /dev/null +++ b/.github/workflows/nodejs.yml @@ -0,0 +1,34 @@ +name: Node CI + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [16.x] + + steps: + - name: Checkout repository and submodules + uses: actions/checkout@v2 + with: + submodules: recursive + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - name: install testing dependencies + run: npm i + - name: test all apps and widgets + run: npm run test + - 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 \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index f3d0d2159..000000000 --- a/.travis.yml +++ /dev/null @@ -1,3 +0,0 @@ -language: node_js -node_js: - - "node" diff --git a/README.md b/README.md index 9cf30065a..38ce09f75 100644 --- a/README.md +++ b/README.md @@ -243,6 +243,7 @@ and which gives information about the app for the Launcher. "screenshots" : [ { url:"screenshot.png" } ], // optional screenshot for app "type":"...", // optional(if app) - // 'app' - an application + // 'clock' - a clock - required for clocks to automatically start // 'widget' - a widget // 'launch' - replacement launcher app // 'bootloader' - code that runs at startup only diff --git a/apps/7x7dotsclock/7x7dotsclock.app.js b/apps/7x7dotsclock/7x7dotsclock.app.js new file mode 100644 index 000000000..aa174b2d2 --- /dev/null +++ b/apps/7x7dotsclock/7x7dotsclock.app.js @@ -0,0 +1,394 @@ +/* +7x7DotsClock + +by Peter Kuppelwieser + +*/ + +let settings = Object.assign({ swupApp: "",swdownApp: "", swleftApp: "", swrightApp: "", ColorMinutes: ""}, require("Storage").readJSON("7x7dotsclock.json", true) || {}); + +// position on screen +var Xs = 0, Ys = 30,Xe = 175, Ye=175; +//const Xs = 0, Ys = 0,Xe = 175, Ye=175; +var SegH = (Ye-Ys)/2,SegW = (Xe-Xs)/2; +var Dx = SegW/14, Dy = SegH/16; + +switch(settings.ColorMinutes) { +case "blue": + var mColor = [0.3,0.3,1]; + var sColor = [0,0,1]; + var sbColor = [1,1,1]; + break; +case "pink": + var mColor = [1,0.3,1]; + var sColor = [1,0,1]; + var sbColor = [1,1,1]; + break; +case "green": + var mColor = [0.3,1,0.3]; + var sColor = [0,1,0]; + var sbColor = [1,1,1]; + break; +case "yellow": + var mColor = [1,1,0.3]; + var sColor = [1,1,0]; + var sbColor = [0,0,0]; + break; +default: + var sColor = [0,0,1]; + var mColor = [0.3,0.3,1]; + var sbColor = [1,1,1]; +} +const bColor = [0.3,0.3,0.3]; + +const Font = [ + [ + [1,1,1,1,1,1,1], + [1,1,1,1,1,1,1], + [1,1,0,0,0,1,1], + [1,1,0,0,0,1,1], + [1,1,0,0,0,1,1], + [1,1,1,1,1,1,1], + [1,1,1,1,1,1,1] + ], + [ + [0,0,0,1,1,0,0], + [0,0,0,1,1,0,0], + [0,0,0,1,1,0,0], + [0,0,0,1,1,0,0], + [0,0,0,1,1,0,0], + [0,0,0,1,1,0,0], + [0,0,0,1,1,0,0], + ], + [ + [1,1,1,1,1,1,1], + [1,1,1,1,1,1,1], + [0,0,0,0,0,1,1], + [1,1,1,1,1,1,1], + [1,1,0,0,0,0,0], + [1,1,1,1,1,1,1], + [1,1,1,1,1,1,1] + ], + [ + [1,1,1,1,1,1,1], + [1,1,1,1,1,1,1], + [0,0,0,0,0,1,1], + [0,0,0,1,1,1,1], + [0,0,0,0,0,1,1], + [1,1,1,1,1,1,1], + [1,1,1,1,1,1,1] + ], + [ + [1,1,0,0,0,0,0], + [1,1,0,0,0,0,0], + [1,1,0,1,1,0,0], + [1,1,1,1,1,1,1], + [1,1,1,1,1,1,1], + [0,0,0,1,1,0,0], + [0,0,0,1,1,0,0] + ], + [ + [1,1,1,1,1,1,1], + [1,1,1,1,1,1,1], + [1,1,0,0,0,0,0], + [1,1,1,1,1,1,1], + [0,0,0,0,0,1,1], + [1,1,1,1,1,1,1], + [1,1,1,1,1,1,1] + ], + [ + [1,1,0,0,0,0,0], + [1,1,0,0,0,0,0], + [1,1,0,0,0,0,0], + [1,1,1,1,1,1,1], + [1,1,0,0,0,1,1], + [1,1,1,1,1,1,1], + [1,1,1,1,1,1,1] + ], + [ + [1,1,1,1,1,1,1], + [1,1,1,1,1,1,1], + [0,0,0,0,0,1,1], + [0,0,0,0,0,1,1], + [0,0,0,0,0,1,1], + [0,0,0,0,0,1,1], + [0,0,0,0,0,1,1] + ], + [ + [1,1,1,1,1,1,1], + [1,1,1,1,1,1,1], + [1,1,0,0,0,1,1], + [1,1,1,1,1,1,1], + [1,1,0,0,0,1,1], + [1,1,1,1,1,1,1], + [1,1,1,1,1,1,1] + ], + [ + [1,1,1,1,1,1,1], + [1,1,1,1,1,1,1], + [1,1,0,0,0,1,1], + [1,1,1,1,1,1,1], + [1,1,1,1,1,1,1], + [0,0,0,0,0,1,1], + [0,0,0,0,0,1,1] + ], + ]; + +// Global Vars +var dho = -1, eho = -1, dmo = -1, emo = -1; + + +function drawHSeg(x1,y1,x2,y2,Num,Color,Size) { + + + g.setColor(g.theme.bg); + g.fillRect(x1, y1, x2, y2); + for (let i = 1; i < 8; i++) { + for (let j = 1; j < 8; j++) { + if (Font[Num][j-1][i-1] == 1) { + if (Color == "fg") { + g.setColor(g.theme.fg); + } else { + 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.fillCircle(x1+Dx+(i-1)*(x2-x1)/7,y1+Dy+(j-1)*(y2-y1)/7,1); + } + } + } +} + + +function drawSSeg(x1,y1,x2,y2,Num,Color,Size) { + for (let i = 1; i < 8; i++) { + 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]); + } else { + g.setColor(g.theme.fg); + //g.setColor(0.7,0.7,0.7); + } + g.fillCircle(x1+(i-1)*(x2-x1)/7,y1+(j-1)*(y2-y1)/7,Size); + } + } + } +} + + +function ShowSeconds() { + + g.setColor(sbColor[0],sbColor[1],sbColor[2]); + + g.fillRect((Xe-Xs) / 2 - 14 + Xs -4, + (Ye-Ys) / 2 - 7 + Ys -4, + (Xe-Xs) / 2 + 14 + Xs +4, + (Ye-Ys) / 2 + 7 + Ys +4); + + + drawSSeg( (Xe-Xs) / 2 - 14 + Xs -1, + (Ye-Ys) / 2 - 7 + Ys +1, + (Xe-Xs) / 2 + Xs -1, + (Ye-Ys) / 2 + 7 + Ys +1, + ds,"fg",1); + + drawSSeg( (Xe-Xs) / 2 + Xs +2, + (Ye-Ys) / 2 - 7 + Ys +1, + (Xe-Xs) / 2 + 14 + Xs +2, + (Ye-Ys) / 2 + 7 + Ys +1, + es,"fg",1); + +} + +function draw() { + // work out how to display the current time + var d = new Date(); + var h = d.getHours(), m = d.getMinutes(), s = d.getSeconds(); + + + dh = Math.floor(h/10); + eh = h - dh * 10; + + dm = Math.floor(m/10); + em = m - dm * 10; + + ds = Math.floor(s/10); + es = s - ds * 10; + + + // Reset the state of the graphics library + g.reset(); + if (dh != dho) { + g.setColor(1,1,1); + drawHSeg(Xs, Ys, Xs+SegW, Ys+SegH,dh,"fg",4); + dho = dh; + } + + if (eh != eho) { + g.setColor(1,1,1); + drawHSeg(Xs+SegW+Dx, Ys, Xs+SegW*2, Ys+SegH,eh,"fg",4); + eho = eh; + } + + if (dm != dmo) { + g.setColor(0.3,0.3,1); + drawHSeg(Xs, Ys+SegH+Dy, Xs+SegW, Ys+SegH*2,dm,"",4); + dmo = dm; + } + + if (em != emo) { + g.setColor(0.3,0.3,1); + drawHSeg(Xs+SegW+Dx, Ys+SegH+Dy, Xs+SegW*2, Ys+SegH*2,em,"",4); + emo = em; + } + + if (!Bangle.isLocked()) ShowSeconds(); + +} + + +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); + print(settings.swupApp); + } else if(v==1) { + print("down swipe event"); + if(settings.swdownApp != "") load(settings.swdownApp); + print(settings.swdownApp); + } else { + print("touch event"); + } +} + +// Get Messages status +var messages = require("Storage").readJSON("messages.json",1)||[]; + +//var BTconnected = NRF.getSecurityStatus().connected; +//NRF.on('connect',BTconnected = NRF.getSecurityStatus().connected); +//NRF.on('disconnect',BTconnected = NRF.getSecurityStatus().connected); + + +function drawWidgeds() { + + //Bluetooth + //print(BluetoothDevice.connected); + var x1Bt = 160; + var y1Bt = 0; + var x2Bt = x1Bt + 30; + var y2Bt = y2Bt; + + if (NRF.getSecurityStatus().connected) + 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); + + + //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 + + var x1M = 100; + var y1M = y1B; + var x2M = x1M + 25; + var y2M = y2B; + + if (messages.some(m=>m.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); + +} + + + + +function SetFull(on) { + dho = -1; eho = -1; dmo = -1; emo = -1; + g.clear(); + + if (on === true) { + Ys = 0; + Bangle.setUI("clock"); + Bangle.on('swipe', function(direction) { }); + + } else { + Ys = 30; + Bangle.setUI("updown",actions); + Bangle.on('swipe', function(direction) { + switch (direction) { + case 1: + print("swipe left event"); + if(settings.swleftApp != "") load(settings.swleftApp); + print(settings.swleftApp); + break; + case -1: + print("swipe right event"); + if(settings.swrightApp != "") load(settings.swrightApp); + print(settings.swrightApp); + break; + default: + print("swipe undefined event"); + } + }); + } + + SegH = (Ye-Ys)/2; + Dy = SegH/16; + + draw(); + + if (on != true) { + //Bangle.loadWidgets(); + //Bangle.drawWidgets(); + drawWidgeds(); + } +} + +Bangle.on('lock', function(on) { + SetFull(on); +}); + + +SetFull(Bangle.isLocked()); + +var secondInterval = setInterval(draw, 1000); diff --git a/apps/7x7dotsclock/7x7dotsclock.img.js b/apps/7x7dotsclock/7x7dotsclock.img.js new file mode 100644 index 000000000..b1f91c0bb --- /dev/null +++ b/apps/7x7dotsclock/7x7dotsclock.img.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkEBAkTmEzkAHDmcjmQBBmcTmICCgMAiMAkE/+P/mEQgMQgH/n/zAIP/l/yA4QvXC4kDkEjFgIACkcSmMTkMyBoQHBI4kvI6wXBn8wA4c/mfzl8y+cfEoIaBVa5HBAAMQF4UgIoIBBBgJNBAwQ3BkfygSnJSQIUBkECiBoCL48DmCPFAA6PCX40jX4hYEU4LNBX4JHIkBHCBgJHBianKj8wO4IvHgSnBmJ3CHYqGCABcRcYTXLAA5KCFAJfCC4KnDX4anNgUgiSnMkQQBO5hvCl8yO4pHEd4oyBH4QBBU5TXHkcimUTkLXFL44HEiTbBO4MhBoQHBI4KECR45HGBoIFBU4y/BC4c/mYXGMQJHFiBHLEAIHCf5gAKhWg1UB0IEBjUA0MB0EAjQKCiANCCQOg0cxmcSmWjU4MqmcDmSnDBASkBmejCQIXFmYXEmYXHicyhRLC0AEBAIJFBAIIFCBAYHDF65fXR66vImUCnS8IkeinUBgERgEgcIMBgRHDBgLvCBYMQmcjBYIAHfwL7JiQLBichkcSnUSO4MhI4MxI5MSmMjPgMinCnCkRHGIgJHFiUgkUalUCAgMRkUCkIvIkUSkMC0EiBxAAI0UKkBHCkCPDgA+CI5Z3BmYPBAB53CV4MSEgcSiCnOR4cyR5JQEgBHCC4I0BC4UjC4MCxQXGF4IlBxRHB0UAlUK0BMBkIEBI5ILB0ZHBF4czlTXHI4mjCQIXOH4KnDC4MKgGqgGgAgIBBIoJHJBoQ=")) diff --git a/apps/7x7dotsclock/7x7dotsclock.settings.js b/apps/7x7dotsclock/7x7dotsclock.settings.js new file mode 100644 index 000000000..34935d668 --- /dev/null +++ b/apps/7x7dotsclock/7x7dotsclock.settings.js @@ -0,0 +1,88 @@ +(function(back) { + +let settings = Object.assign({ swupApp: "",swdownApp: "", swleftApp: "", swrightApp: "",ColorMinutes: ""}, require("Storage").readJSON("7x7dotsclock.json", true) || {}); + + + +function setSetting(key,value) { + print("call " + key + " = " + value); + settings[key] = value; + + print("storing settings 7x7dotsclock.json"); + storage.write('7x7dotsclock.json', settings); +} + + + // Helper method which uses int-based menu item for set of string values + function stringItems(key, startvalue, values) { + return { + value: (startvalue === undefined ? 0 : values.indexOf(startvalue)), + format: v => values[v], + min: 0, + max: values.length - 1, + wrap: true, + step: 1, + onchange: v => { + setSetting(key,values[v]); + } + }; + } + + // Helper method which breaks string set settings down to local settings object + function stringInSettings(name, values) { + return stringItems(name,settings[name], values); + } + +function showMainMenu() { + const mainMenu = { + "": {"title": "7x7 Dots Clock Settings"}, + "< Back": ()=>load(), + "Minutes": stringInSettings("ColorMinutes", ["blue","pink","green","yellow"]), + "swipe-up": ()=>showSelAppMenu("swupApp"), + "swipe-down": ()=>showSelAppMenu("swdownApp"), + "swipe-left": ()=>showSelAppMenu("swleftApp"), + "swipe-right": ()=>showSelAppMenu("swrightApp") + + }; + + E.showMenu(mainMenu); +} + + +function showSelAppMenu(key) { + var Apps = require("Storage").list(/\.info$/) + .map(app => {var a=storage.readJSON(app, 1);return ( + a&&a.name != "Launcher" + && a&&a.name != "Bootloader" + && a&&a.type != "clock" + && a&&a.type !="widget" + )?a:undefined}) + .filter(app => app) // filter out any undefined apps + .sort((a, b) => a.sortorder - b.sortorder); + const SelAppMenu = { + '': { + 'title': /*LANG*/'Select App', + }, + '< Back': ()=>showMainMenu(), + }; + Apps.forEach((app, index) => { + var label = app.name; + if (settings[key] === app.src) { + label = "* " + label; + } + SelAppMenu[label] = () => { + if (settings[key] !== app.src) { + setSetting(key,app.src); + showMainMenu(); + } + }; + }); + if (Apps.length === 0) { + SelAppMenu[/*LANG*/"No Apps Found"] = () => { }; + } + return E.showMenu(SelAppMenu); +} + +showMainMenu(); + +}) diff --git a/apps/7x7dotsclock/ChangeLog b/apps/7x7dotsclock/ChangeLog new file mode 100644 index 000000000..d2c98a472 --- /dev/null +++ b/apps/7x7dotsclock/ChangeLog @@ -0,0 +1,2 @@ +0.01: Initial version for upload +0.02: better theme support, configurable colors, small improvements diff --git a/apps/7x7dotsclock/README.md b/apps/7x7dotsclock/README.md new file mode 100644 index 000000000..28fcac1b1 --- /dev/null +++ b/apps/7x7dotsclock/README.md @@ -0,0 +1,15 @@ +# 7x7 dots clock + +![](dotsfontclock.png) + +* A Clock with big numbers made of 7x7 dots +* system widgeds ar not (yet) supported +* when screen is locked it shows hours and minutes in full screen mode +* adjustable color for minutes and seconds + +![](dotsfontclock-scr1.png) + +* when screen is unlocked it shows additional info: bluetooth, battery, new message state, date and seconds +* you can configure an app per swipe direction +* when swiping the configured apps are launched +* button press opens launcher diff --git a/apps/7x7dotsclock/dotsfontclock-scr1.png b/apps/7x7dotsclock/dotsfontclock-scr1.png new file mode 100644 index 000000000..5ab2e4863 Binary files /dev/null and b/apps/7x7dotsclock/dotsfontclock-scr1.png differ diff --git a/apps/7x7dotsclock/dotsfontclock-scr2.png b/apps/7x7dotsclock/dotsfontclock-scr2.png new file mode 100644 index 000000000..f301bb50c Binary files /dev/null and b/apps/7x7dotsclock/dotsfontclock-scr2.png differ diff --git a/apps/7x7dotsclock/dotsfontclock.png b/apps/7x7dotsclock/dotsfontclock.png new file mode 100644 index 000000000..af8fa61ba Binary files /dev/null and b/apps/7x7dotsclock/dotsfontclock.png differ diff --git a/apps/7x7dotsclock/metadata.json b/apps/7x7dotsclock/metadata.json new file mode 100644 index 000000000..41f0836d3 --- /dev/null +++ b/apps/7x7dotsclock/metadata.json @@ -0,0 +1,19 @@ +{ "id": "7x7dotsclock", + "name": "7x7 Dots Clock", + "shortName":"7x7 Dots Clock", + "version":"0.02", + "description": "A clock with a big 7x7 dots Font", + "icon": "dotsfontclock.png", + "tags": "clock", + "type": "clock", + "supports" : ["BANGLEJS2"], + "allow_emulator": true, + "readme": "README.md", + "storage": [ + {"name":"7x7dotsclock.app.js","url":"7x7dotsclock.app.js"}, + {"name":"7x7dotsclock.settings.js","url":"7x7dotsclock.settings.js"}, + {"name":"7x7dotsclock.img","url":"7x7dotsclock.img.js","evaluate":true} + ], + "data": [{"name":"7x7dotsclock.json"}], + "screenshots": [{"url":"dotsfontclock.png"},{"url":"dotsfontclock-scr1.png"},{"url":"dotsfontclock-scr2.png"}] +} diff --git a/apps/alarm/ChangeLog b/apps/alarm/ChangeLog index d129e9f9f..4576237a5 100644 --- a/apps/alarm/ChangeLog +++ b/apps/alarm/ChangeLog @@ -13,3 +13,4 @@ Widgets now shown on Alarm screen 0.13: Alarm widget state now updates when setting/resetting an alarm 0.14: Order of 'back' menu item +0.15: Fix hour/minute wrapping code for new menu system diff --git a/apps/alarm/app.js b/apps/alarm/app.js index 17062d44a..56184edf1 100644 --- a/apps/alarm/app.js +++ b/apps/alarm/app.js @@ -73,12 +73,12 @@ function editAlarm(alarmIndex) { '': { 'title': /*LANG*/'Alarm' }, /*LANG*/'< Back' : showMainMenu, /*LANG*/'Hours': { - value: hrs, - onchange: function(v){if (v<0)v=23;if (v>23)v=0;hrs=v;this.value=v;} // no arrow fn -> preserve 'this' + value: hrs, min : 0, max : 23, wrap : true, + onchange: v => hrs=v }, /*LANG*/'Minutes': { - value: mins, - onchange: function(v){if (v<0)v=59;if (v>59)v=0;mins=v;this.value=v;} // no arrow fn -> preserve 'this' + value: mins, min : 0, max : 59, wrap : true, + onchange: v => mins=v }, /*LANG*/'Enabled': { value: en, @@ -138,12 +138,12 @@ function editTimer(alarmIndex) { const menu = { '': { 'title': /*LANG*/'Timer' }, /*LANG*/'Hours': { - value: hrs, - onchange: function(v){if (v<0)v=23;if (v>23)v=0;hrs=v;this.value=v;} // no arrow fn -> preserve 'this' + value: hrs, min : 0, max : 23, wrap : true, + onchange: v => hrs=v }, /*LANG*/'Minutes': { - value: mins, - onchange: function(v){if (v<0)v=59;if (v>59)v=0;mins=v;this.value=v;} // no arrow fn -> preserve 'this' + value: mins, min : 0, max : 59, wrap : true, + onchange: v => mins=v }, /*LANG*/'Enabled': { value: en, diff --git a/apps/alarm/metadata.json b/apps/alarm/metadata.json index 3e109bda9..d29298309 100644 --- a/apps/alarm/metadata.json +++ b/apps/alarm/metadata.json @@ -2,7 +2,7 @@ "id": "alarm", "name": "Default Alarm & Timer", "shortName": "Alarms", - "version": "0.14", + "version": "0.15", "description": "Set and respond to alarms and timers", "icon": "app.png", "tags": "tool,alarm,widget", diff --git a/apps/antonclk/ChangeLog b/apps/antonclk/ChangeLog index 4dca8053e..ac49dcbd7 100644 --- a/apps/antonclk/ChangeLog +++ b/apps/antonclk/ChangeLog @@ -7,4 +7,5 @@ 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 \ No newline at end of file + 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) \ No newline at end of file diff --git a/apps/antonclk/metadata.json b/apps/antonclk/metadata.json index def5d3b48..4d26dd0c7 100644 --- a/apps/antonclk/metadata.json +++ b/apps/antonclk/metadata.json @@ -1,7 +1,7 @@ { "id": "antonclk", "name": "Anton Clock", - "version": "0.06", + "version": "0.07", "description": "A clock using the bold Anton font, optionally showing seconds and date in ISO-8601 format.", "readme":"README.md", "icon": "app.png", diff --git a/apps/antonclk/settings.js b/apps/antonclk/settings.js index e452b02c7..6882cbd0f 100644 --- a/apps/antonclk/settings.js +++ b/apps/antonclk/settings.js @@ -38,7 +38,7 @@ }, "< Back": () => back(), "Seconds...": () => E.showMenu(secmenu), - "Date": stringInSettings("dateOnMain", ["Short", "Long", "ISO8601"]), + "Date": stringInSettings("dateOnMain", ["Long", "Short", "ISO8601"]), "Show Weekday": { value: (settings.weekDay !== undefined ? settings.weekDay : true), format: v => v ? "On" : "Off", @@ -56,7 +56,7 @@ } }, "Uppercase": { - value: (settings.upperCase !== undefined ? settings.upperCase : false), + value: (settings.upperCase !== undefined ? settings.upperCase : true), format: v => v ? "On" : "Off", onchange: v => { settings.upperCase = v; @@ -81,7 +81,7 @@ "< Back": () => E.showMenu(mainmenu), "Show": stringInSettings("secondsMode", ["Never", "Unlocked", "Always"]), "With \":\"": { - value: (settings.secondsWithColon !== undefined ? settings.secondsWithColon : false), + value: (settings.secondsWithColon !== undefined ? settings.secondsWithColon : true), format: v => v ? "On" : "Off", onchange: v => { settings.secondsWithColon = v; @@ -89,14 +89,14 @@ } }, "Color": { - value: (settings.secondsColoured !== undefined ? settings.secondsColoured : false), + value: (settings.secondsColoured !== undefined ? settings.secondsColoured : true), format: v => v ? "On" : "Off", onchange: v => { settings.secondsColoured = v; writeSettings(); } }, - "Date": stringInSettings("dateOnSecs", ["No", "Year", "Weekday"]) + "Date": stringInSettings("dateOnSecs", ["Year", "Weekday", "No"]) }; // Actually display the menu diff --git a/apps/aptsciclk/ChangeLog b/apps/aptsciclk/ChangeLog new file mode 100644 index 000000000..ed32a45a2 --- /dev/null +++ b/apps/aptsciclk/ChangeLog @@ -0,0 +1,8 @@ +0.01: New App! +0.02: Icons, loading screen +0.03: Random icon, Shorter "loading" screen +0.04: Support for light and dark Themes +0.05: Small bugfix +0.06: Formatting +0.07: Added potato GLaDOS and quote functionality when you tap her +0.08: Fixed drawing issues with the quotes and added more diff --git a/apps/aptsciclk/README.md b/apps/aptsciclk/README.md new file mode 100644 index 000000000..718e3d408 --- /dev/null +++ b/apps/aptsciclk/README.md @@ -0,0 +1,11 @@ +# Description + +This is a simple clock based on the Portal Series. + +# Features + +The button in the center of the screen is interactable and the warning image will change when it is pressed. + +Potato GLaDOS in the bottom left corner is interactable and will display a quote when tapped. (You can add more quotes by editing the `aptsciclkquotes.txt` file seperating each quote with a `^`) + +When the app loads the Apeture Science Logo is displayed. diff --git a/apps/aptsciclk/app-icon.js b/apps/aptsciclk/app-icon.js new file mode 100644 index 000000000..d2a2dbbd6 --- /dev/null +++ b/apps/aptsciclk/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwgNKxAACEaIVDDKWAhAXGwAtODA4HBLR4YFD4QWICIhABGAoMBJRBZHC4wwHOQ4IFAgQwGUQ4YBAg4uMJIwDDGAjRLIgYLHc5gXJIwbKLC4hICb4gZKfAhgETJKHJLwwXRUooWKCImAJogXRMopGMNwkIC4oWLYYqtHC5rFJC5h0GIxwsGFyD8CC4wwOIxBIQFwoeBCxrwEFwYXTFgTXReI4uQC4apPC4xNERqBlGFx4XCeJ4nHD4kIIxY3KPoIxNBwYXEJRInEP44iGOgwXFBYYcDChCHHC4wMBC5BnJEoouMGAYXEJJCCJC4pOEcpYKBFIpJFZRQXGD4gWKXBUICxjdFIhwyJOJMAA=")) diff --git a/apps/aptsciclk/app.js b/apps/aptsciclk/app.js new file mode 100644 index 000000000..c2903cf37 --- /dev/null +++ b/apps/aptsciclk/app.js @@ -0,0 +1,368 @@ +const big = g.getWidth()>200; +const timeFontSize = big?5:4; +const dateFontSize = big?3:2; +const gmtFontSize = 2; +const font = "6x8"; + +const xyCenter = g.getWidth() / 2; +const yposTime = xyCenter*0.73; +const yposDate = xyCenter*0.48; +const yposYear = xyCenter*1.8; + +const buttonTolerance = 20; +const buttonX = 88; +const buttonY = 104; + +var pause = false; //set to true to pause any sort of drawing (except for quotes) + +function getImg(img){ + if (img == "w0"){//drink + return { + width : 60, height : 60, bpp : 1, + buffer : require("heatshrink").decompress(atob("AB0//4AE4YGF/gOZFIQOD4EABwnwgEDBwf8g/4h4ODwYQBv4OC+AbDAIP+j/HAQIOC4Hwj4RBBwP8o8B/+PBwWOkEP/l/BwP4+JCB44OCj+Ih/+n4OB+PEoP38YOB/0YkUXGgIOB8cBi9f+IOCkEI+XvBwXigFG64OEg0/t4OEuP7BwkHx/PBwWigF8voOC+Uwg/ig4OCkMgv8QsIOB+cfSoOGLIUR/E/4ljBwPxx/B/0kO4UI/0P+J3C/HHVQOISoWEn+D/iPBBwIwC8IOCwcP84IBBwU4TAMHBwfAv+AcARBBgD3CBwX8gDnBBwfwewIODAgIABBwYHDB3oAEBwIHFByyDBABg")) +} + } + else if (img == "w1"){//cube dispenser + return { + width : 60, height : 60, bpp : 1, + buffer : require("heatshrink").decompress(atob("AB0//4AE4YGF/gOI/3/+fvBwYEBnwO/By3APgN/O6IeBh4OF8AOcwADCBwX8g4dM/8fBwt774OE+/9Bwt/BxodH3oOcFgyVG8BhCBwX8hRwCBwXA0C6BBwc/w4OE41MBwtEo6VF84sE/1/54OLDo4sHHYxKHLIxoGO44AD/kAABo")) + } + } + else if (img == "w2"){//acid + return { + width : 60, height : 60, bpp : 1, + buffer : require("heatshrink").decompress(atob("AB0//4AE4YGF/gOF+IOGngOF8/D8YGD/wdBB4nv4fzAwf4BwOfGQd/4f7/+//+f74OB4PwHIJKDx8P/4BBBwP8BwIBBBwXvh+Hw5ZD+Pwnl/NAcegJOBBwfgj0fBwvhBxcPgYEBBwXw/F+FghIB84OC/BfBOYQOBk/w/0f4f4nkGgFgh0hwED4H4jOBuF8hk/v/Hzlnx/zFgQZBGYLCD4EHaIn8gAOF8EDBwn+dgQOK/8AN4IOD+EABww0BBwqGEBwIWBBwk8CwIODg/gv4OEv4OD+4OBBAIOBRYIFBh+PcAQdC+gOCDoN+h/vBwPP/wOB/wOBwJCBBwP2oa3BLALgBiA7BOwIvB/+DQoV/d4hPBBwQsB/wJB8ZoEAAZoDAAQOPRQIAM")) + } + } + else if (img == "w3"){//turret + return { + width : 60, height : 60, bpp : 1, + buffer : require("heatshrink").decompress(atob("AB0//4AE4YGF/gOi+IOGh4OF8AOF/UNBwthx4OE+0YBwtBh4OE6mQBwn7rEfBwl22IOE99gBwn99UzBwUc/+90YsC8HH+++n98n/+g0++2Z+4OB4Fz73T74OCg877d8/YdC+d7u/v3gsBjEvt/+O4X+gvtIgI7CwG934OD8E326kD/0A+yzEwEO74OD/EArYOEgEDv4OD+PAl4OEnkBaInz0EPBwk3iAdE+XwSIYDBj2Oj4OD/fYvIOEvdHz4OD99unIOD/vt44OE3u4Dou3h4OE+3x/IOE70/Bwn78/9Bwl4LAQ7Dx75DBwP4Awb+EBwgAEBz0AABo=")) + } + } + else if (img == "w4"){//falling cube + return { + width : 60, height : 60, bpp : 1, + buffer : require("heatshrink").decompress(atob("AB0//4AE4YGF/gOC+YOF/0PBwvgv4OE/kFBwvAyIdFnYeBBwYeDDofng4OE8vYDonx7uPBwkf/+/Bwfh+czBwf+g/5z4OD+FevIdEhMDDon/0E3BwgeBJQgeB+5ZFvAEBBwfzgYOEw/XLInwn3BBwf8gH4LYIOCwUHDonwmE4HYkHwKkE8P4XYQOCv7dCYQkBWYsAWYvAiAsE/EDJQn/wF+CwJZDg/gBwgrBXYIOC8D+FNAL+F4eDBwn4nh2BBweHFYJ3EFYQOC/0P/AOECgIOE/E/BwsHBwvACAIODWAQOEJAIOFAgIOEQ4QsEAAOfBwoACBwgACBw8AABo")) +} + } + else if (img == "w5"){//ball + return { + width : 60, height : 60, bpp : 1, + buffer : require("heatshrink").decompress(atob("AB0//4AE4YGF/gOiv4OF8YOFAgQOEyYdGBw3zBw0BBwv4j4OB+EAgOD84OE+/ev4dD/3+BwvcugsE/u7t0f4aRC7e2sF8Bwlxg4dEu8YBwYsB/HDHYsMngOB8EDweHDon//PADoYABz0PBwfwnJKE/0OjZZC/kB4Hxz4OCwEYh+wBwXwgeA/+HBwUP8EP/0/BwPj/0DCQIOB/l/4DQBw4OBDIMPUoJKB+H/wY+B44OBj/4CoJKC+P/g7+FBAL+Fj4OFbwIOEI4IOF8YO6JQwAEaIgORgAANA")) + } + } + else if (img == "w6"){//ball recviver + return { + width : 60, height : 60, bpp : 1, + buffer : require("heatshrink").decompress(atob("AB0//4AE4YGF/gOR/YOG34Ob/e7Bwu7CwQOhGgQOD34OF/0LBwvfv4dMuPfBwn29oOFtwONDowsHHY3+h7CNj4OF+IOc4A7NDo7gGJQ4ACBwX+//vBwnvBAIOK8EH/kBBwd+v/PSwIOB/fnjiWBBwXesHPLQIOB/2AgEvBwfgh0AFgf8gAuBLKQObgAANA==")) + } + } + else if (img == "w7"){//falling portals + return { + width : 60, height : 60, bpp : 1, + buffer : require("heatshrink").decompress(atob("AB0//4AE4YFE/H8BwtvBwvvvgOE/33Bwvf3gOE/v7Bxn5Bw2fHYv7/oOF3/cB118JQQOC4ODJQn8jEfLInBjBoE/0jO4pjD953CwCVF/EH5//+ykCwA8Cp4OB/MDz4DBEQUYjPzaIfn5k/74xC/l44f+BwePz1595ADDYPvv7vDMAN3Bwf4CAIOE4//BYIOB/0On47E8AFCBwcPTwYOCAgPAgE8Bwf8gEDBwOAGIJZDBwX9DofhUYRKDKIIOEAAQOD8EABwgcB+IODnoKB84OD37tCBwUzZ4QODZ4QdDnIFB/YODZwP+v47DJIIBBJQcAAwZyBABoA==")) + } + } + else if (img == "w8"){//flying portals + return { + width : 60, height : 60, bpp : 1, + buffer : require("heatshrink").decompress(atob("AB0//4AE4YFE/H8BwtvBwvvvgOE/33Bwvf3gdF/YOF/4OF/IOGgA7F8ENBwn8gHcBw/5AoOAg4OCh4sD/vD+AFB45KBBwfwv//BwMJgEIFAXcnvggF4kEBBwPMSIIYBz/8nAEBw5ZD4IhBO48AhpoG953FSo/2Ugv/p4OF/LCGaIyIBB34OH4EAngODbAMDBwnfDoqeCBy7RBBwnh//xBwc9BQPnBwe/AYO/BwUzFYQODGgYOCnIFB/YOD57WBv47Dj//AIJKDgAGDOQIANA")) + } + } + else if (img == "w9"){//cake + return { + width : 60, height : 60, bpp : 1, + buffer : require("heatshrink").decompress(atob("AB0//4AE4YGF/gOY/oOG94OF/1/Bwv3FgwKCBwfnFhn8HY0LAQPwvgOB8EP/5uBBwP2gF4j+PBwP+sEEj/x44OB90Ao/8Dodwg8/nkH4ZXBgHnx8ABwPv/k98+ABwZEB+EAJQPj/3+nkAv4OB5+fz0Aj4OB98Ag+Ah/nBwJXB4EDHYSTB/EA/wsCSoJfBwAODNIPgBwgcBHYQOCC4QODn8Ah4ODGgMH+47D8EB/A7KTYMf4A7Eg/wHYgcBHZx3DcAPggbRBFgQcBcAQOB/iUBBwgcBBwgcCd4V/HYL+D/YOBDgIOC8/+DgIOC/+HfwIOD/4cCBwYAEBwQADBz0AABoA=")) + } + } + else if (img == "butPress"){ + return { + width : 176, height : 176, bpp : 4, + transparent : 1, + buffer : require("heatshrink").decompress(atob("iIA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AFEc5kM5hD/ACXMAAJXB5nBI35WSK4ZY/AB8cK4/MJP5WRK4pY/ABhPD5e7he7A4fBJn5XNKwJXCLAZX/ABUcKwhXDLAZN/VxhSCK4m8WH5XNVwZXEWARX/ABEcK4sAgBYDXYRP/K5RQC2ACE3e8K/5XPVgYDCK/4AKJIPLVYoEEBoPBIWPd6ICPK46uDAohXzjvd7oCMCAJX/K7cAAAZXFBQkBK/6v/ABPd6ICPK/4AaK4mwKwYEDV+4ARjhKBVQoDD3gMBK2MdAIRXXVYSuDK/5XN5ZRCgEAWQYLBK/4AJK4u7KwZXC4JXxiPd6JXV5hXH3hX1ACscWApXDMQRN/WBpYCK4QICV35XOLARXBA4ZX/ABccKAfMhgFEJf5YRK4hJ/ABxXH4JI/LCRXCK34ASjhXCIf4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/ACIA==")) + } + } + else if (img == "butUnpress"){ + return { + width : 176, height : 176, bpp : 4, + transparent : 1, + buffer : require("heatshrink").decompress(atob("iIA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AFEc5kM5hD/ACXMAAJXB5nBI35WSK4ZY/AB8cK4/MJP5WRK4pY/ABhQEK43BJn5X/AEMcK5fMJv6uPK46w/K/4AgjhPFgEALAxP/K5vAAQhX/K6KsDWApP/AA6uHWA/BIWOIwICPK46qFAohXxjGIxACMCAJX/K7cAAAZXFBQkBK/6v/ABOIwICPK/4AaKInAAhCv2ACMcVRC0FK2MYAIRXXVYSuFK/5XO5kAgCuFK/4AJJwvMKw3BK+MRxGBK/4ArjhXMJv6wQK4qu/K/4AjjhXKJf5YRK4hJ/ABxXH4JI/LCRXCK34ASjhXCIf4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/ACI=")) + } + } + + else if (img == "apetureLaboratories"){ + return { + width : 173, height : 43, bpp : 4, + transparent : 0, + buffer : require("heatshrink").decompress(atob("AA+IAAeAIv5UTK34APhABBKwpeBJX5VLJgJWGAwKv/ABL8EKomABIYA/KpJWHAoRW/KpZQCfwJSCAgRW/KphWFBgZW/KphNCAgSxEKP4ADJAatFKwWAL4oA/KpALGXQhS/Ko4MIwAOMAHREOKv5ZVVgcAh///4LDAwQAB+AIHCQYHMDQQYBDwQEGDIoaGTx4MCwAiCJgYhFBIYIHLw4HFBARjGESJUDehZVFVhJNLRI5VGDIIdEAgwiL+DzDJAJVJB4IMBCoKsHAYpFCDgr2IApYEIBpJFCKpqsCHgQbFIhBVHBhJoGTwyBRKp5mBDoY6GKoYqEXYg7JApKeHQJysEKpRmBDoasHQBAACM4qMGEAr0JAgZjGFYhVPgGAEIpVEAAaAEA4oyEW4qyKFZBoFKoisDJIJWLfIp3IQBJeJfgysMERpVCwEIVpijFBA6ZJdA4JHJYgEKQIx1EVgRVBVhj5IEIyZHHYoUGNw4MCQIwIKAARVDxBVRQo6ZJHZZoGU4yBGBAiBGVgRZBVhYlIfJBoFHZZoHfJRwGBAQrEVISvCKpwrEUZAqFVhzYKB4yKGQIpVDAYJVKEoYFDBIpVIb4ysMIgoPHOgoRFhGAVwKsLAFzQHACBVCVhQA/KpawBCJa76IhRWDVxIODAwTaFAocP///dhALGBQYJBCIYQBDYgKEBBAEDVgeIAgKgFBYZVEEYY+CH4YDBBgYFBBYoFEOYhmEDYgFFLIwEDKw2AKwhTFBoSsHIgglFAQo/HKIoDECBBZGc46eEAYa2EKox2Ga5LjLDoq8FKAgVDBAv/EghWGVgRWJKoaOFD4IgCViAWFVjKTGJIZOFKpKOHAQj3JAogCFPApcGKQziGLopKCU4hWFKojsEHYrXFCAJFId45CEDYh4EB4Y1DCYSzFJQT+FgAGCKooA/AAZMBfopWDKv5WLVgxT/AB+AKopW/ACBU/ABwA==")) + } + } + +else if (img == "apetureLaboratoriesLight"){ + return { + width : 173, height : 43, bpp : 4, + transparent : 1, + buffer : require("heatshrink").decompress(atob("iIAGxAADwINHAH5ULK34APjABBKwpeBJX5VLJgJWGAwKv/ABL8EKomBBIYA/KpJWHAoRW/KpZQCfwJSCAgRW/KphWFBgZW/KphNCAgSxEKP4ADJAatFKwWBL4oA/KpALGXQhS/Ko4MIwIOMAHREOKv5ZVVgcRiEAgALDAwQABgIIHCQYHMDQQYBDwQEGDIoaGTx4MCwIiCJgYhFBIYIHLw4HFBARjGESJUDehZVFVhJNLRI5VGDIIdEAgwiLgLzDJAJVJB4IMBCoKsHAYpFCDgr2IApYEIBpJFCKpqsCHgQbFIhBVHBhJoGTwyBRKp5mBDoY6GKoYqEXYg7JApKeHQJysEKpRmBDoasHQBAACM4qMGEAr0JAgZjGFYhVPiOBEIpVEAAaAEA4oyEW4qyKFZBoFKoisDJIJWLfIp3IQBJeJfgysMERpVCwMYVpijFBA6ZJdA4JHJYgEKQIx1EVgRVBVhj5IEIyZHHYoUGNw4MCQIwIKAARVDxBVRQo6ZJHZZoGU4yBGBAiBGVgRZBVhYlIfJBoFHZZoHfJRwGBAQrEVISvCKpwrEUZAqFVhzYKB4yKGQIpVDAYJVKEoYFDBIpVIb4ysMIgoPHOgoRFjGBVwKsLAFzQHACBVCVhQA/KpawBCJa76IhRWDVxIODAwTaFAocQgEAdhALGBQYJBCIYQBDYgKEBBAEDVgeIAgKgFBYZVEEYY+CH4YDBBgYFBBYoFEOYhmEDYgFFLIwEDKw2BKwhTFBoSsHIgglFAQo/HKIoDECBBZGc46eEAYa2EKox2Ga5LjLDoq8FKAgVDBAsAEghWGVgRWJKoaOFD4IgCViAWFVjKTGJIZOFKpKOHAQj3JAogCFPApcGKQziGLopKCU4hWFKojsEHYrXFCAJFId45CEDYh4EB4Y1DCYSzFJQT+FiIGCKooA/AAZMBfopWDKv5WLVgxT/AB+BKopW/ACBU/ABsQA=")) + } + } + + else if (img == "potato"){ + return { + width : 54, height : 55, bpp : 4, + transparent : 6, + buffer : require("heatshrink").decompress(atob("swAEsEGA4oASEQ4ARGgNgDa8QsA3BKStowxTDDalikxTCRKsSNwY2BDSYvDGoI2TsoTDgwEBGx5IBs1VHIgaBGx4qCDQZOBUiUGstQDQ4FBKYwHGDQNWDRA3BCYsABotgJ4dkogABowcGAAUGOwogDDAVCAYScKOooFBooWCAAjqNAoQYHKYQ8ELQZzFsAMCiIeJAAdFMwiCEoIaNoICBoAaMiMUDA0ikMiigaIAAgaGDIIaBkcyoCHEMxqIBmdEkMzmcxDSVBkYXBGoMzmUQDSMSDIURiIbBkDBCDRtBiYwBDIMREAMiDR6qBiI0BkQEBKQKkBUJQAEoCfBC4MVgMSDYMkGwQaBeBMUiC3BGYNN8kSHgM0DQrsGAAMQQAMyCwIaBgK/BmIaKM4IGCiMTDQXd9waCmlENZIaEgESKAMhpxQEDQSiEoJTGiEimaGCqIaBmKABUQwyBUIzRBkIXBDoI8BkAaDGwgaFEIMCeQUSYAKNBmLYDGwJ/DDYlFqAaBGwILBGgLyBUIRSFQghrCgEjDYMiiTdCggaFDYgaFKIKIBmcTkciiA1GNwpsFgI4BmZsBkVEDRAbJiBTCNQMABIIZHfBCPBDQKSCoFEGhA2IKIMQgJ1CoCFHGxVBeASQDgAZJDQ8RQIMyDQkGDZQ1GDILtBAwNGsAaLs1hokECYNCaQMxDIVmDRoOBDQifCBggaNKgMRqUhiovFGxoMEsCADAQQaMsUWJBi+LsMRqtWDSwUBqvFqpVFQ54UCstVqEGsDbBQx4lFgAABotRAgQABT54ADqtRM5jjQAAQ")) + } + } + + else if (img == "apetureWatch"){ + return { + width : 176, height : 176, bpp : 4, + transparent : 2, + buffer : require("heatshrink").decompress(atob("kQA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A+/4A/AH4A/AH4A/AD0RgAASgMQCqgrqiJX/K/5X/K/5XR7vQK/8IxBX/K/5X/K/5X/K/5X/K/5X/K/5X/K/5Xah////wK/5XTKwIABK4YAG//x7vRBhAA2jGIAYMfK4fxCRAOCK/5XFKwYABBgTBEh4LC7vQbabxLFYoVPFZMIxADBK4sAK4wLDK/5XSBYhX/K6MPBIQDBK/5XD+BXMBAQQCK/5XDKAJXKVwRWBAoJX/K4j3CUohXDL4YACK/5XF+CuEK4yuCK/5XHWAZOCK4cPKwhX/K45MFK4YAGK/5XGLApX/K6X/K5sPK/5XIWAZXJ/5X/K6sPK/5XPAoauDK/5XJ/5XDj5eEVwRX/K5y2FVwRX/K4/wK44GDVwRX/K50fWAi9DK/5XGUYRQCiKwCAwKuDK/5XGJgZXGMQJWDK/5XGAoKkCK4arEK/5XIVQRQDK/5XQAwZLC+BXBAwYACLwJX/K4auCWApXHCAJX/K5JYFAoi/Ch5X/K4ZHCAAZXEAoZnCK/5XLVQRXFBgZX/K4igCLAnxFYRdBBohX/K5cAiIrJK/5XEfIhX/K/5Xr+BX/K/5Xu/5X/K/5WKFhRXWl5XB+BX/K6ywFK9XxK4SMFK7JWCK58PK7sP/5XDGoxXr/5Xc//4xBXEHIKyPK54fFK5CPBK7cPxAyBK4oHBK7wKFK5AQBK7UP/GPD4JWCiMfK4IJBK7jOGFgYwEK4XRBg4AQ/CtFAAZXCBQ4AQjBXCCRxXcUoJLDjnMhnMBgTqCK7RzJYYxXC6DbTAAf4UYInB5gABK4PM4KBDdYwrQhBXBBQ5XIAAJXYh6GDKwRXDLASwCK7BxIK8auDjhXH5iwPK5gKIK8iuBKwhXFLAJX/AA0PxCuBKAhXG4KwCK/6uEx/xK5qwNK/KuBjhXL5kRWAJXrbapXEJ4pXH4JXXABRXhh+I+JXPiP//5X/K4WP+McJ4oLBLAxX/K5vAAQhX/ABBDBiJXFVgawFiMf//wK/4gBAAKuHWA/BCYQhIK8uIwACOK5CqFAohXxhGIxACMCAJX/K7YaEK4pKFK/6v/ABOIwACOK/4AMFZRXI4AEIV+wrO///iMcVRC0FiMf//wQaZXZhABCFZ0P//xK4qrCVwpXB/+PbahX15gLBVwpX/K5ERJwvMKw3BiP4K98AxGAFaH//5XPj/4+BXvFaRXCjhXMiMfxBXhGoIAcK4nxWAxXF4MR/GPK4Q4e+UiADcvK4UPWARXMj/4NwayKACUPK8KwDjhXKcQOIKYZX/eIkRLAhXEiKuBx5XEY4QAYDgJXiIAXxiJXH4KuC/4VDK/6wGLAZXCKwKuGK/6wIiMcK4QFBVw5X/WA2IwJSCAAYJBVwpX/WA2IJwJVDj///GPVwpX/LA34K4PxVoYHBKwxX/AAynCK4ZWC+BX/K5ixB/6vEVo5X/IxAABK4asHK/5XLgJXCBxRX/K/5XgLAQNLK/5X/K6r7CACxX/K/5XVfJcBiINLK/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K5o6bK/YaZK/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K6o6bK/cAABUBiINLK/5X/K/5X/K/5X/K/AMLACBX/K7DMaAAcRADA4eA==")) + } + } + + else if (img == "apetureWatchLight"){ + return { + width : 176, height : 176, bpp : 4, + transparent : 2, + buffer : require("heatshrink").decompress(atob("kQA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A+gAA/AH4A/AH4A/AD0R/4AS+MfCqgrqiJX/K/5X/K/5XR7vfK//4xBX/K/5X/K/5X/K/5X/K/5X/K/5X/K/5Xa+EAgEPK/5XTKwIABK4YAGgEB7vRBhAA2jGIAYMQK4cBCRAOCK/5XFKwYABBgTBE+ALC7vfbabxLFYoVPFZP4xADBK4v/K4wLDK/5XSBYhX/K6PwBIQDBK/5XDh5XMBAQQCK/5XDKAJXKVwRWBAoJX/K4j3CUohXDL4YACK/5XFh6uEK4yuCK/5XHWAZOCK4fwKwhX/K45MFK4YAGK/5XGLApX/K6UAK5vwK/5XIWAZXJgBX/K6vwK/5XPAoauDK/5XJgBXDiBeEVwRX/K5y2FVwRX/K48PK44GDVwRX/K50QWAi9DK/5XGUYRQCiKwCAwKuDK/5XGJgZXGMQJWDK/5XGAoKkCK4arEK/5XIVQRQDK/5XQAwZLCh5XBAwYACLwJX/K4auCWApXHCAJX/K5JYFAoi/C+BX/K4ZHCAAZXEAoZnCK/5XLVQRXFBgZX/K4igCLAkBFYRdBBohX/K5f/iIrJK/5XEfIhX/K/5Xrh5X/K/5XugBX/K/5WKFhRXWkBXBh5XvgJXCGgpXcWApXoF4KvD+COGK65WCK5/wK7gtCK4YmCLB5XfgBXbFgIzBK4mILCBXPDwpXIcIJXa+BPBKAJXFLARXdBQpXICAJXah/4x4qDAAMfK4IJBWBpXODgwsDAAcQK4XRBg4APgAwBVogADK4XwgInWjBXCCRxXbiD9BKwcc5kM5gNCRgXwK7JyJYYxXC77bTIwf4UYInB5gABK4PM4MRDQXwDpYrKawMABQ5XIAAJXYh6uDKwRXDLAQRDK64YIK8SuEjhXH5iwPK5gKIK8UP/APBKwhXFLAKwNK/ItBiJQEK43BDgRX/AAXw/GP+JXNGQXwK/5XDEgMcK5fMiIdBK9YAKK5cPK4RPFK4/BDoUPFagAJK8WI+JXPGYRX/IIWP+McJ4sAgBYGK/5XN4ACEK/4AH+AjCK4qsDWAsRDwIWCK/ogBAAKuHWA/BCYQhIK8uIx4COK5CqFAohXx/GIxACMCAJX/K7cAAAZXFBQkBK/6v/ABOIx4COK/4AMFZRXI4AEIV+wrN+AjCjiqIWgpUCCwRXr/ABCFZ0PBoJXFVYSuFK4P/x4VBK/5XI5kAgCuFK/5XIiJOF5hWG4MR/BXv/+Ix5XREgJXOj58BK94rR+AkCjhXMiMfxAUCK70AADpXE+KwGK4vBiP4x5XhgUiADcgEQTyCK5sf/ATDK/5DD+McK5UR/+IK/5XEeYcRLAhXEiKuBx4SDK/6wFiJXH4KuOK/SwELAZXCKwKuOK/ywBiMcK4QFBVwZX/K43/gACBxGBKQQADBIKuBh5X/K43/JAOIJwJVDIYP4x4NCK/5XHfAP4K4PxVoYHBBgRX/K5H/gCnCK4ZWDVxpX9LARABV4ZWQK/xYBgBXD+EAKx5X/AAMBK4RVQK/5ADK4RBSK/5YDIKZX/K/5XNfYQA1K/5X2eJkReKfxj4VTK/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5XvgAAYh5X8DTJX/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5XVgAAYK/orL+MRIKZX/K/5X/K/5X/K/5X/K/5XmgAAdiIA3")) + } + } +} + + +function drawStart(){ + g.clear(); + g.reset(); + if (g.theme.dark){apSciLab = getImg("apetureLaboratories");} + else {apSciLab = getImg("apetureLaboratoriesLight");} + g.drawImage(apSciLab, xyCenter-apSciLab.width/2, xyCenter-apSciLab.height/2); +} + +// Check settings for what type our clock should be +var is12Hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"]; + +// timeout used to update every minute +var drawTimeout; + +//warnings +var maxWarning = 9; +var curWarning = Math.floor(Math.random() * (maxWarning+1)); + +function unPause(delay, quote){ + if (pause){ + setTimeout(function() { + if (quote == undefined || quoteNum == quote){ + pause = false; + draw(); + } + }, delay); + } +} + +var quoteNum; + +function quote(fontsize, width, height, specificQuote){ + pause = true; + var finalString = ""; + var quotesFile; + var finalFontSize; + quotesFile = require("Storage").read("aptsciclkquotes.txt", 0, 0); //opens the quotes file + //console.log(quotesFile); + var quotes = quotesFile.split("^"); + var numQuotes = quotes.length;//number of quotes + var curQuote; + + if (specificQuote == undefined){ + quoteNum = Math.round(Math.random()*numQuotes)-1; + curQuote = quotes[quoteNum]; //quote to be displayed + } + else{ + quoteNum = specificQuote; + curQuote = quotes[quoteNum]; + } + + unPause(10000, quoteNum); + + var curWords = curQuote.split(" "); //individual words + //console.log(numQuotes); + + var maxChar = width/6/fontsize; + var maxLines = height/10/fontsize; + var curLines = 0; + var curLength = 0; + + + for (var i = 0; i < curWords.length; i++){ + //console.log(curLength+curWords[i].length); + if (curLength + curWords[i].length <= maxChar){ + finalString += " "+curWords[i]; + curLength += curWords[i].length+1; + //console.log("next"); + } + else{ + //console.log("break"); + curLines++; + if (curLines > maxLines){ + curLength = 0; + finalString = ""; + i = -1; + if (fontsize > 1){fontsize--;} + maxChar = width/6/fontsize; + maxLines = height/10/fontsize; + console.log(maxLines); + console.log(maxChar); + + } + else{ + curLength = 0; + finalString += "\n"; + i--; + } + } + finalFontSize = fontsize; + } + + + //drawing actual stuff + g.setColor(g.getBgColor()); + g.fillRect(10, 10+28, g.getWidth()-10,g.getWidth()-10); + g.reset(); + g.setFont(font, finalFontSize); + g.setFontAlign(0, 0); + g.drawString(finalString, xyCenter, xyCenter+14); + //quote length*pixels per character = pixel width + //height ~120 width ~160 +} + +function buttonPressed(){ + if (curWarning < maxWarning) curWarning += 1; + else curWarning = 0; + g.reset(); + buttonImg = getImg("butPress"); + g.drawImage(buttonImg, 0, 0); + + warningImg = getImg("w"+String(curWarning)); + g.drawImage(warningImg, 1, g.getWidth()-61); + + setTimeout(buttonUnpressed, 500); +} +function buttonUnpressed(){ + if (!pause){ + buttonImg = getImg("butUnpress"); + g.drawImage(buttonImg, 0, 0); + } + else{ + setTimeout(buttonUnpressed, 500); + } +} + +// 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() { + if (pause){} + else{ + // get date + var d = new Date(); + var da = d.toString().split(" "); + + g.reset(); // default draw styles + //draw watchface + if (g.theme.dark){apSciWatch = getImg("apetureWatch");} + else {apSciWatch = getImg("apetureWatchLight");} + g.drawImage(apSciWatch, xyCenter-apSciWatch.width/2, xyCenter-apSciWatch.height/2); + + potato = getImg("potato"); + g.drawImage(potato, 118, 118); + + g.drawImage(warningImg, 1, g.getWidth()-61);//update warning + + // drawString centered + g.setFontAlign(0, 0); + + // draw time + var time = da[4].substr(0, 5).split(":"); + var hours = time[0], + minutes = time[1]; + var meridian = ""; + if (is12Hour) { + hours = parseInt(hours,10); + meridian = "AM"; + if (hours == 0) { + hours = 12; + meridian = "AM"; + } else if (hours >= 12) { + meridian = "PM"; + if (hours>12) hours -= 12; + } + hours = (" "+hours).substr(-2); + } + + g.setFont(font, timeFontSize); + g.drawString(`${hours}:${minutes}`, xyCenter+2, yposTime, false); + g.setFont(font, gmtFontSize); + g.drawString(meridian, xyCenter + 102, yposTime + 10, true); + + // draw Day, name of month, Date + var date = [da[0], da[1], da[2]].join(" "); + g.setFont(font, dateFontSize); + g.drawString(String(date), xyCenter, yposDate, false); + + + // draw year + g.setFont(font, dateFontSize); + g.drawString(d.getFullYear(), xyCenter+1, yposYear, true); + } + queueDraw(); +} + + +// 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('touch',(n,e)=>{ + //button is 88 104 + if (!pause && buttonX-buttonTolerance < e.x && e.x < buttonX+buttonTolerance && buttonY-buttonTolerance < e.y && e.y < buttonY+buttonTolerance){ + buttonPressed(); + } + //Potato GLaDOS + else if (!pause && 117 < e.x && e.x < 172 && 117 < e.y && e.y < 172){ + quote(2, 150, 140); + } + else{ + unPause(0); + } +}); + +//show Apeture laboritories +drawStart(); + +setTimeout(function() { + // clean app screen + g.clear(); + // Show launcher when button pressed + Bangle.setUI("clock"); + Bangle.loadWidgets(); + Bangle.drawWidgets(); + //update warning image + buttonPressed(); + // draw now + draw(); + }, 500); diff --git a/apps/aptsciclk/app.png b/apps/aptsciclk/app.png new file mode 100644 index 000000000..b37efdaf8 Binary files /dev/null and b/apps/aptsciclk/app.png differ diff --git a/apps/aptsciclk/metadata.json b/apps/aptsciclk/metadata.json new file mode 100644 index 000000000..c450d926e --- /dev/null +++ b/apps/aptsciclk/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "aptsciclk", + "name": "Apeture Science Clock", + "shortName":"AptSci Clock", + "version": "0.08", + "description": "A clock based on the portal series", + "icon": "app.png", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "allow_emulator": false, + "readme":"README.md", + "storage": [ + {"name":"aptsciclkquotes.txt","url":"quotes.txt"}, + {"name":"aptsciclk.app.js","url":"app.js"}, + {"name":"aptsciclk.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/aptsciclk/quotes.txt b/apps/aptsciclk/quotes.txt new file mode 100644 index 000000000..bc7a04867 --- /dev/null +++ b/apps/aptsciclk/quotes.txt @@ -0,0 +1 @@ +Well here we are again^You euthanized your faithful Companion Cube more quickly than any test subject on record. Congratulations.^So get comfortable while I warm up the neurotoxin emitters^This isn't brave. It's murder. What did I ever do to you?^The difference between us is that I can feel pain.^Who's gonna make the cake when I'm gone? You?^Oh... It's you.^I've been really busy being dead. You know, after you MURDERED ME.^So. How are you holding up? BECAUSE I'M A POTATO.^You really do have brain damage, don't you?^You like revenge, right? Everybody likes revenge. Well, let's go get some.^It's been fun. Don't come back.^And then you showed up. You dangerous, mute lunatic.^Unbelievable. You, [subject name here] must be the pride of [subject hometown here.]^You are not a good person. You know that, right? Good people don't get up here.^Cake, and grief counseling, will be available at the conclusion of the test.^This is your fault. I'm going to kill you. And all the cake is gone. You don't even care, do you?^Momentum, a function of mass and velocity, is conserved between portals. In layman's terms, speedy thing goes in, speedy thing comes out. diff --git a/apps/authentiwatch/ChangeLog b/apps/authentiwatch/ChangeLog index 7d6f96026..bb2945db4 100644 --- a/apps/authentiwatch/ChangeLog +++ b/apps/authentiwatch/ChangeLog @@ -3,3 +3,4 @@ 0.03: Add "Calculating" placeholder, update JSON save format 0.04: Fix tapping at very bottom of list, exit on inactivity 0.05: Add support for bulk importing and exporting tokens +0.06: Add spaces to codes for improved readability (thanks @BartS23) diff --git a/apps/authentiwatch/README.md b/apps/authentiwatch/README.md index cc25e9604..a957cf93a 100644 --- a/apps/authentiwatch/README.md +++ b/apps/authentiwatch/README.md @@ -33,7 +33,7 @@ Keep those copies safe and secure. * Swipe right to exit to the app launcher. * Swipe left on selected counter token to advance the counter to the next value. -![Screenshot](screenshot.png) +![Screenshot](screenshot1.png) ![Screenshot](screenshot2.png) ![Screenshot](screenshot3.png) ![Screenshot](screenshot4.png) ## Creator diff --git a/apps/authentiwatch/app.js b/apps/authentiwatch/app.js index 640183230..73b8bdeea 100644 --- a/apps/authentiwatch/app.js +++ b/apps/authentiwatch/app.js @@ -1,5 +1,6 @@ const tokenextraheight = 16; var tokendigitsheight = 30; +var tokenheight = tokendigitsheight + tokenextraheight; // Hash functions const crypto = require("crypto"); const algos = { @@ -93,6 +94,9 @@ function hotp(d, token, dohmac) { while (ret.length < token.digits) { ret = "0" + ret; } + // add a space after every 3rd or 4th digit + var re = (token.digits % 3 == 0 || (token.digits % 3 >= token.digits % 4 && token.digits % 4 != 0)) ? "" : "."; + ret = ret.replace(new RegExp("(..." + re + ")", "g"), "$1 ").trim(); } catch(err) { ret = notsupported; } @@ -121,15 +125,15 @@ function drawToken(id, r) { lbl = tokens[id].label.substr(0, 10); if (id == state.curtoken) { // current token - g.setColor(g.theme.fgH); - g.setBgColor(g.theme.bgH); - g.setFont("Vector", tokenextraheight); + g.setColor(g.theme.fgH) + .setBgColor(g.theme.bgH) + .setFont("Vector", tokenextraheight) // center just below top line - g.setFontAlign(0, -1, 0); + .setFontAlign(0, -1, 0); adj = y1; } else { - g.setColor(g.theme.fg); - g.setBgColor(g.theme.bg); + g.setColor(g.theme.fg) + .setBgColor(g.theme.bg); sz = tokendigitsheight; do { g.setFont("Vector", sz--); @@ -138,8 +142,8 @@ function drawToken(id, r) { g.setFontAlign(0, 0, 0); adj = (y1 + y2) / 2; } - g.clearRect(x1, y1, x2, y2); - g.drawString(lbl, (x1 + x2) / 2, adj, false); + g.clearRect(x1, y1, x2, y2) + .drawString(lbl, (x1 + x2) / 2, adj, false); if (id == state.curtoken) { if (tokens[id].period > 0) { // timed - draw progress bar @@ -160,10 +164,10 @@ function drawToken(id, r) { g.drawString(state.otp, (x1 + adj + x2) / 2, y1 + tokenextraheight, false); } // shaded lines top and bottom - g.setColor(0.5, 0.5, 0.5); - g.drawLine(x1, y1, x2, y1); - g.drawLine(x1, y2, x2, y2); - g.setClipRect(0, 0, g.getWidth(), g.getHeight()); + g.setColor(0.5, 0.5, 0.5) + .drawLine(x1, y1, x2, y1) + .drawLine(x1, y2, x2, y2) + .setClipRect(0, 0, g.getWidth(), g.getHeight()); } function draw() { @@ -198,15 +202,15 @@ function draw() { } if (tokens.length > 0) { var drewcur = false; - var id = Math.floor(state.listy / (tokendigitsheight + tokenextraheight)); - var y = id * (tokendigitsheight + tokenextraheight) + Bangle.appRect.y - state.listy; + var id = Math.floor(state.listy / tokenheight); + var y = id * tokenheight + Bangle.appRect.y - state.listy; while (id < tokens.length && y < Bangle.appRect.y2) { - drawToken(id, {x:Bangle.appRect.x, y:y, w:Bangle.appRect.w, h:(tokendigitsheight + tokenextraheight)}); + drawToken(id, {x:Bangle.appRect.x, y:y, w:Bangle.appRect.w, h:tokenheight}); if (id == state.curtoken && (tokens[id].period <= 0 || state.nextTime != 0)) { drewcur = true; } id += 1; - y += (tokendigitsheight + tokenextraheight); + y += tokenheight; } if (drewcur) { // the current token has been drawn - schedule a redraw @@ -228,9 +232,9 @@ function draw() { state.nexttime = 0; } } else { - g.setFont("Vector", tokendigitsheight); - g.setFontAlign(0, 0, 0); - g.drawString(notokens, Bangle.appRect.x + Bangle.appRect.w / 2, Bangle.appRect.y + Bangle.appRect.h / 2, false); + g.setFont("Vector", tokendigitsheight) + .setFontAlign(0, 0, 0) + .drawString(notokens, Bangle.appRect.x + Bangle.appRect.w / 2, Bangle.appRect.y + Bangle.appRect.h / 2, false); } if (state.drawtimer) { clearTimeout(state.drawtimer); @@ -240,18 +244,18 @@ function draw() { function onTouch(zone, e) { if (e) { - var id = Math.floor((state.listy + (e.y - Bangle.appRect.y)) / (tokendigitsheight + tokenextraheight)); + var id = Math.floor((state.listy + (e.y - Bangle.appRect.y)) / tokenheight); if (id == state.curtoken || tokens.length == 0 || id >= tokens.length) { id = -1; } if (state.curtoken != id) { if (id != -1) { - var y = id * (tokendigitsheight + tokenextraheight) - state.listy; + var y = id * tokenheight - state.listy; if (y < 0) { state.listy += y; y = 0; } - y += (tokendigitsheight + tokenextraheight); + y += tokenheight; if (y > Bangle.appRect.h) { state.listy += (y - Bangle.appRect.h); } @@ -268,7 +272,7 @@ function onTouch(zone, e) { function onDrag(e) { if (e.x > g.getWidth() || e.y > g.getHeight()) return; if (e.dx == 0 && e.dy == 0) return; - var newy = Math.min(state.listy - e.dy, tokens.length * (tokendigitsheight + tokenextraheight) - Bangle.appRect.h); + var newy = Math.min(state.listy - e.dy, tokens.length * tokenheight - Bangle.appRect.h); state.listy = Math.max(0, newy); draw(); } @@ -300,8 +304,12 @@ function bangle1Btn(e) { } state.curtoken = Math.max(state.curtoken, 0); state.curtoken = Math.min(state.curtoken, tokens.length - 1); + state.listy = state.curtoken * tokenheight; + state.listy -= (Bangle.appRect.h - tokenheight) / 2; + state.listy = Math.min(state.listy, tokens.length * tokenheight - Bangle.appRect.h); + state.listy = Math.max(state.listy, 0); var fakee = {}; - fakee.y = state.curtoken * (tokendigitsheight + tokenextraheight) - state.listy + Bangle.appRect.y; + fakee.y = state.curtoken * tokenheight - state.listy + Bangle.appRect.y; state.curtoken = -1; state.nextTime = 0; onTouch(0, fakee); @@ -318,9 +326,9 @@ Bangle.on('touch', onTouch); Bangle.on('drag' , onDrag ); Bangle.on('swipe', onSwipe); if (typeof BTN2 == 'number') { - setWatch(function(){bangle1Btn(-1);}, BTN1, {edge:"rising", debounce:50, repeat:true}); - setWatch(function(){exitApp(); }, BTN2, {edge:"rising", debounce:50, repeat:true}); - setWatch(function(){bangle1Btn( 1);}, BTN3, {edge:"rising", debounce:50, repeat:true}); + setWatch(function(){bangle1Btn(-1);}, BTN1, {edge:"rising" , debounce:50, repeat:true}); + setWatch(function(){exitApp(); }, BTN2, {edge:"falling", debounce:50}); + setWatch(function(){bangle1Btn( 1);}, BTN3, {edge:"rising" , debounce:50, repeat:true}); } Bangle.loadWidgets(); diff --git a/apps/authentiwatch/interface.html b/apps/authentiwatch/interface.html index d2213b819..5ee32fd8e 100644 --- a/apps/authentiwatch/interface.html +++ b/apps/authentiwatch/interface.html @@ -56,6 +56,7 @@ function base32clean(val, nows) { var ret = val.replaceAll(/\s+/g, ' '); ret = ret.replaceAll(/0/g, 'O'); ret = ret.replaceAll(/1/g, 'I'); + ret = ret.replaceAll(/8/g, 'B'); ret = ret.replaceAll(/[^A-Za-z2-7 ]/g, ''); if (nows) { ret = ret.replaceAll(/\s+/g, ''); diff --git a/apps/authentiwatch/metadata.json b/apps/authentiwatch/metadata.json index 676d8da9f..b4ed34a12 100644 --- a/apps/authentiwatch/metadata.json +++ b/apps/authentiwatch/metadata.json @@ -3,8 +3,8 @@ "name": "2FA Authenticator", "shortName": "AuthWatch", "icon": "app.png", - "screenshots": [{"url":"screenshot.png"}], - "version": "0.05", + "screenshots": [{"url":"screenshot1.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"},{"url":"screenshot4.png"}], + "version": "0.06", "description": "Google Authenticator compatible tool.", "tags": "tool", "interface": "interface.html", diff --git a/apps/authentiwatch/screenshot.png b/apps/authentiwatch/screenshot.png deleted file mode 100644 index 2a7bcbd9a..000000000 Binary files a/apps/authentiwatch/screenshot.png and /dev/null differ diff --git a/apps/authentiwatch/screenshot1.png b/apps/authentiwatch/screenshot1.png new file mode 100644 index 000000000..c7ca744b4 Binary files /dev/null and b/apps/authentiwatch/screenshot1.png differ diff --git a/apps/authentiwatch/screenshot2.png b/apps/authentiwatch/screenshot2.png new file mode 100644 index 000000000..8156dd3e8 Binary files /dev/null and b/apps/authentiwatch/screenshot2.png differ diff --git a/apps/authentiwatch/screenshot3.png b/apps/authentiwatch/screenshot3.png new file mode 100644 index 000000000..6d14e0b96 Binary files /dev/null and b/apps/authentiwatch/screenshot3.png differ diff --git a/apps/authentiwatch/screenshot4.png b/apps/authentiwatch/screenshot4.png new file mode 100644 index 000000000..7576e1aff Binary files /dev/null and b/apps/authentiwatch/screenshot4.png differ diff --git a/apps/banglerun/.gitignore b/apps/banglerun/.gitignore deleted file mode 100644 index c2658d7d1..000000000 --- a/apps/banglerun/.gitignore +++ /dev/null @@ -1 +0,0 @@ -node_modules/ diff --git a/apps/banglerun/ChangeLog b/apps/banglerun/ChangeLog deleted file mode 100644 index c778588cc..000000000 --- a/apps/banglerun/ChangeLog +++ /dev/null @@ -1,13 +0,0 @@ -0.01: First release -0.02: Bugfix time: Reset minutes to 0 when hitting 60 -0.03: Fix distance >=10 km (fix #529) -0.04: Use offscreen buffer for flickerless updates -0.05: Complete rewrite. New UI, GPS & HRM Kalman filters, activity logging -0.06: Reading HDOP directly from the GPS event (needs Espruino 2v07 or above) -0.07: Fixed GPS update, added guards against NaN values -0.08: Fix issue with GPS coordinates being wrong after the first one -0.09: Another GPS fix (log raw coordinates - not filtered ones) -0.10: Removed kalman filtering to allow distance log to work - Only log data every 5 seconds (not 1 sec) - Don't create a file until the first log entry is ready - Add labels for buttons diff --git a/apps/banglerun/README.md b/apps/banglerun/README.md deleted file mode 100644 index 80e984bfa..000000000 --- a/apps/banglerun/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# BangleRun - -An app for running sessions. Displays info and logs your run for later viewing. - -## Compilation - -The app is written in Typescript, and needs to be transpiled in order to be -run on the BangleJS. The easiest way to perform this step is by using the -ubiquitous [NPM package manager](https://www.npmjs.com/get-npm). - -After having installed NPM for your platform, checkout the `BangleApps` repo, -open a terminal, and navigate into the `apps/banglerun` folder. Then issue: - -``` -npm i -``` - -to install the project's build tools, and: - -``` -npm run build -``` - -To build the app. The last command will generate the `app.js` file, containing -the transpiled code for the BangleJS. diff --git a/apps/banglerun/app-icon.js b/apps/banglerun/app-icon.js deleted file mode 100644 index 9eeaced6e..000000000 --- a/apps/banglerun/app-icon.js +++ /dev/null @@ -1 +0,0 @@ -require("heatshrink").decompress(atob("mEwwIHEuAEDgP8ApMDAqAXBjAGD/E8AgUcgF8CAX/BgIFBn//wAFCv//8PwAoP///5Aon/8AcB+IFB4AFB8P/34FBgfj/8fwAFB4f+g4cBg/H/w/Cg+HKQcPx4FEh4/CAoMfAocOj4/CKYRwELIIFDLII6BAoZSBLIYeCgP+v4FD/k/GAQFBHgcD/ABBIIX4gIFBSYPwAoUPAog/B8AFEwAFDDQQCBQoQFCZYYFigCKEgFwgAA==")) diff --git a/apps/banglerun/app.js b/apps/banglerun/app.js deleted file mode 100644 index b79255171..000000000 --- a/apps/banglerun/app.js +++ /dev/null @@ -1 +0,0 @@ -!function(){"use strict";var t;!function(t){t.Stopped="STOP",t.Paused="PAUSE",t.Running="RUN"}(t||(t={}));const n={STOP:63488,PAUSE:65504,RUN:2016};function e(t,n,e){g.setColor(0),g.fillRect(n-60,e,n+60,e+30),g.setColor(65535),g.drawString(t,n,e)}function i(i){var s;g.setFontVector(30),g.setFontAlign(0,-1,0),e((i.distance/1e3).toFixed(2),60,55),e(function(t){const n=Math.round(t),e=Math.floor(n/3600),i=Math.floor(n/60)%60,s=n%60;return(e?e+":":"")+("0"+i).substr(-2)+":"+("0"+s).substr(-2)}(i.duration),172,55),e(function(t){if(t<.1667)return"__'__\"";const n=Math.round(1e3/t),e=Math.floor(n/60),i=n%60;return("0"+e).substr(-2)+"'"+("0"+i).substr(-2)+'"'}(i.speed),60,115),e(i.hr.toFixed(0),172,115),e(i.steps.toFixed(0),60,175),e(i.cadence.toFixed(0),172,175),g.setFont("6x8",2),g.setColor(i.gpsValid?2016:63488),g.fillRect(0,216,80,240),g.setColor(0),g.drawString("GPS",40,220),g.setColor(65535),g.fillRect(80,216,160,240),g.setColor(0),g.drawString(("0"+(s=new Date).getHours()).substr(-2)+":"+("0"+s.getMinutes()).substr(-2),120,220),g.setColor(n[i.status]),g.fillRect(160,216,230,240),g.setColor(0),g.drawString(i.status,200,220),g.setFont("6x8").setFontAlign(0,0,1).setColor(-1),i.status===t.Paused?g.drawString("START",236,60,1).drawString(" CLEAR ",236,180,1):i.status===t.Running?g.drawString(" PAUSE ",236,60,1).drawString(" PAUSE ",236,180,1):g.drawString("START",236,60,1).drawString(" ",236,180,1)}function s(t){g.clear(),g.setColor(50712),g.setFont("6x8",2),g.setFontAlign(0,-1,0),g.drawString("DIST (KM)",60,32),g.drawString("TIME",180,32),g.drawString("PACE",60,92),g.drawString("HEART",180,92),g.drawString("STEPS",60,152),g.drawString("CADENCE",180,152),i(t),Bangle.drawWidgets()}function a(n){n.status===t.Stopped&&function(t){const n=(new Date).toISOString().replace(/[-:]/g,""),e=`banglerun_${n.substr(2,6)}_${n.substr(9,6)}`;t.file=require("Storage").open(e,"w"),t.fileWritten=!1}(n),n.status===t.Running?n.status=t.Paused:n.status=t.Running,i(n)}const r={fix:NaN,lat:NaN,lon:NaN,alt:NaN,vel:NaN,dop:NaN,gpsValid:!1,x:NaN,y:NaN,z:NaN,t:NaN,timeSinceLog:0,hr:60,hrError:100,file:null,fileWritten:!1,drawing:!1,status:t.Stopped,duration:0,distance:0,speed:0,steps:0,cadence:0};var o;o=r,Bangle.on("GPS",n=>function(n,e){n.lat=e.lat,n.lon=e.lon,n.alt=e.alt,n.vel=e.speed/3.6,n.fix=e.fix,n.dop=e.hdop,n.gpsValid=n.fix>0,function(n){const e=Date.now();let i=(e-n.t)/1e3;if(isFinite(i)||(i=0),n.t=e,n.timeSinceLog+=i,n.status===t.Running&&(n.duration+=i),!n.gpsValid)return;const s=6371008.8+n.alt,a=n.lat*Math.PI/180,r=n.lon*Math.PI/180,o=s*Math.cos(a)*Math.cos(r),g=s*Math.cos(a)*Math.sin(r),d=s*Math.sin(a);if(!n.x)return n.x=o,n.y=g,void(n.z=d);const u=o-n.x,l=g-n.y,c=d-n.z,f=Math.sqrt(u*u+l*l+c*c);n.x=o,n.y=g,n.z=d,n.status===t.Running&&(n.distance+=f,n.speed=n.distance/n.duration||0,n.cadence=60*n.steps/n.duration||0)}(n),i(n),n.gpsValid&&n.status===t.Running&&n.timeSinceLog>5&&(n.timeSinceLog=0,function(t){t.fileWritten||(t.file.write(["timestamp","latitude","longitude","altitude","duration","distance","heartrate","steps"].join(",")+"\n"),t.fileWritten=!0),t.file.write([Date.now().toFixed(0),t.lat.toFixed(6),t.lon.toFixed(6),t.alt.toFixed(2),t.duration.toFixed(0),t.distance.toFixed(2),t.hr.toFixed(0),t.steps.toFixed(0)].join(",")+"\n")}(n))}(o,n)),Bangle.setGPSPower(1),function(t){Bangle.on("HRM",n=>function(t,n){if(0===n.confidence)return;const e=n.bpm-t.hr,i=Math.abs(e)+101-n.confidence,s=t.hrError/(t.hrError+i)||0;t.hr+=e*s,t.hrError+=(i-t.hrError)*s}(t,n)),Bangle.setHRMPower(1)}(r),function(n){Bangle.on("step",()=>function(n){n.status===t.Running&&(n.steps+=1)}(n))}(r),function(t){Bangle.loadWidgets(),Bangle.on("lcdPower",n=>{t.drawing=n,n&&s(t)}),s(t)}(r),setWatch(()=>a(r),BTN1,{repeat:!0,edge:"falling"}),setWatch(()=>function(n){n.status===t.Paused&&function(t){t.duration=0,t.distance=0,t.speed=0,t.steps=0,t.cadence=0}(n),n.status===t.Running?n.status=t.Paused:n.status=t.Stopped,i(n)}(r),BTN3,{repeat:!0,edge:"falling"})}(); diff --git a/apps/banglerun/banglerun.png b/apps/banglerun/banglerun.png deleted file mode 100644 index bf2cd8af3..000000000 Binary files a/apps/banglerun/banglerun.png and /dev/null differ diff --git a/apps/banglerun/interface.html b/apps/banglerun/interface.html deleted file mode 100644 index 6388d3b65..000000000 --- a/apps/banglerun/interface.html +++ /dev/null @@ -1,217 +0,0 @@ - - - - - -
- - - - - diff --git a/apps/banglerun/jasmine.json b/apps/banglerun/jasmine.json deleted file mode 100644 index 813363b27..000000000 --- a/apps/banglerun/jasmine.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "spec_dir": "test", - "spec_files": [ - "**/*.spec.ts" - ] -} \ No newline at end of file diff --git a/apps/banglerun/metadata.json b/apps/banglerun/metadata.json deleted file mode 100644 index d66441c8d..000000000 --- a/apps/banglerun/metadata.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "id": "banglerun", - "name": "BangleRun", - "shortName": "BangleRun", - "version": "0.10", - "description": "An app for running sessions. Displays info and logs your run for later viewing.", - "icon": "banglerun.png", - "tags": "run,running,fitness,outdoors", - "supports": ["BANGLEJS"], - "interface": "interface.html", - "allow_emulator": false, - "storage": [ - {"name":"banglerun.app.js","url":"app.js"}, - {"name":"banglerun.img","url":"app-icon.js","evaluate":true} - ] -} diff --git a/apps/banglerun/package.json b/apps/banglerun/package.json deleted file mode 100644 index 1f5cc677b..000000000 --- a/apps/banglerun/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "banglerun", - "version": "0.5.0", - "description": "Bangle.js app for running sessions", - "main": "app.js", - "types": "app.d.ts", - "scripts": { - "build": "rollup -c", - "test": "ts-node -P tsconfig.spec.json node_modules/jasmine/bin/jasmine --config=jasmine.json" - }, - "author": { - "name": "Stefano Baldan", - "email": "singintime@gmail.com" - }, - "license": "ISC", - "devDependencies": { - "@rollup/plugin-typescript": "^4.1.1", - "@types/jasmine": "^3.5.10", - "jasmine": "^3.5.0", - "rollup": "^2.10.2", - "rollup-plugin-terser": "^5.3.0", - "terser": "^4.7.0", - "ts-node": "^8.10.2", - "tslib": "^2.0.0", - "typescript": "^3.9.2" - } -} diff --git a/apps/banglerun/rollup.config.js b/apps/banglerun/rollup.config.js deleted file mode 100644 index f7027eb2b..000000000 --- a/apps/banglerun/rollup.config.js +++ /dev/null @@ -1,15 +0,0 @@ -import typescript from '@rollup/plugin-typescript'; -import { terser } from 'rollup-plugin-terser'; - -export default { - input: './src/app.ts', - output: { - dir: '.', - format: 'iife', - name: 'banglerun' - }, - plugins: [ - typescript(), - terser(), - ] -}; diff --git a/apps/banglerun/src/activity.ts b/apps/banglerun/src/activity.ts deleted file mode 100644 index c1a01f30b..000000000 --- a/apps/banglerun/src/activity.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { draw } from './display'; -import { initLog } from './log'; -import { ActivityStatus, AppState } from './state'; - -function startActivity(state: AppState): void { - if (state.status === ActivityStatus.Stopped) { - initLog(state); - } - - if (state.status === ActivityStatus.Running) { - state.status = ActivityStatus.Paused; - } else { - state.status = ActivityStatus.Running; - } - - draw(state); -} - -function stopActivity(state: AppState): void { - if (state.status === ActivityStatus.Paused) { - clearActivity(state); - } - - if (state.status === ActivityStatus.Running) { - state.status = ActivityStatus.Paused; - } else { - state.status = ActivityStatus.Stopped; - } - - draw(state); -} - -function clearActivity(state: AppState): void { - state.duration = 0; - state.distance = 0; - state.speed = 0; - state.steps = 0; - state.cadence = 0; -} - -export { clearActivity, startActivity, stopActivity }; diff --git a/apps/banglerun/src/app.ts b/apps/banglerun/src/app.ts deleted file mode 100644 index 7093e24e0..000000000 --- a/apps/banglerun/src/app.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { startActivity, stopActivity } from './activity'; -import { initDisplay } from './display'; -import { initGps } from './gps'; -import { initHrm } from './hrm'; -import { initState } from './state'; -import { initStep } from './step'; - -declare var BTN1: any; -declare var BTN3: any; -declare var setWatch: any; - -const appState = initState(); - -initGps(appState); -initHrm(appState); -initStep(appState); -initDisplay(appState); - -setWatch(() => startActivity(appState), BTN1, { repeat: true, edge: 'falling' }); -setWatch(() => stopActivity(appState), BTN3, { repeat: true, edge: 'falling' }); diff --git a/apps/banglerun/src/display.ts b/apps/banglerun/src/display.ts deleted file mode 100644 index 528890c35..000000000 --- a/apps/banglerun/src/display.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { ActivityStatus, AppState } from './state'; - -declare var Bangle: any; -declare var g: any; - -const STATUS_COLORS = { - 'STOP': 0xF800, - 'PAUSE': 0xFFE0, - 'RUN': 0x07E0, -} - -function initDisplay(state: AppState): void { - Bangle.loadWidgets(); - Bangle.on('lcdPower', (on: boolean) => { - state.drawing = on; - if (on) { - drawAll(state); - } - }); - drawAll(state); -} - -function drawBackground(): void { - g.clear(); - g.setColor(0xC618); - g.setFont('6x8', 2); - g.setFontAlign(0, -1, 0); - g.drawString('DIST (KM)', 60, 32); - g.drawString('TIME', 172, 32); - g.drawString('PACE', 60, 92); - g.drawString('HEART', 172, 92); - g.drawString('STEPS', 60, 152); - g.drawString('CADENCE', 172, 152); -} - -function drawValue(value: string, x: number, y: number) { - g.setColor(0x0000); - g.fillRect(x - 60, y, x + 60, y + 30); - g.setColor(0xFFFF); - g.drawString(value, x, y); -} - -function draw(state: AppState): void { - g.setFontVector(30); - g.setFontAlign(0, -1, 0); - - drawValue(formatDistance(state.distance), 60, 55); - drawValue(formatTime(state.duration), 172, 55); - drawValue(formatPace(state.speed), 60, 115); - drawValue(state.hr.toFixed(0), 172, 115); - drawValue(state.steps.toFixed(0), 60, 175); - drawValue(state.cadence.toFixed(0), 172, 175); - - g.setFont('6x8', 2); - - g.setColor(state.gpsValid ? 0x07E0 : 0xF800); - g.fillRect(0, 216, 80, 240); - g.setColor(0x0000); - g.drawString('GPS', 40, 220); - - g.setColor(0xFFFF); - g.fillRect(80, 216, 160, 240); - g.setColor(0x0000); - g.drawString(formatClock(new Date()), 120, 220); - - g.setColor(STATUS_COLORS[state.status]); - g.fillRect(160, 216, 230, 240); - g.setColor(0x0000); - g.drawString(state.status, 200, 220); - - g.setFont("6x8").setFontAlign(0,0,1).setColor(-1); - if (state.status === ActivityStatus.Paused) { - g.drawString("START",236,60,1).drawString(" CLEAR ",236,180,1); - } else if (state.status === ActivityStatus.Running) { - g.drawString(" PAUSE ",236,60,1).drawString(" PAUSE ",236,180,1); - } else { - g.drawString("START",236,60,1).drawString(" ",236,180,1); - } -} - -function drawAll(state: AppState) { - drawBackground(); - draw(state); - Bangle.drawWidgets(); -} - -function formatClock(date: Date): string { - return ('0' + date.getHours()).substr(-2) + ':' + ('0' + date.getMinutes()).substr(-2); -} - -function formatDistance(meters: number): string { - return (meters / 1000).toFixed(2); -} - -function formatPace(speed: number): string { - if (speed < 0.1667) { - return `__'__"`; - } - const pace = Math.round(1000 / speed); - const min = Math.floor(pace / 60); - const sec = pace % 60; - return ('0' + min).substr(-2) + `'` + ('0' + sec).substr(-2) + `"`; -} - -function formatTime(time: number): string { - const seconds = Math.round(time); - const hrs = Math.floor(seconds / 3600); - const min = Math.floor(seconds / 60) % 60; - const sec = seconds % 60; - return (hrs ? hrs + ':' : '') + ('0' + min).substr(-2) + `:` + ('0' + sec).substr(-2); -} - -export { - draw, - drawAll, - drawBackground, - drawValue, - formatClock, - formatDistance, - formatPace, - formatTime, - initDisplay, -}; diff --git a/apps/banglerun/src/gps.ts b/apps/banglerun/src/gps.ts deleted file mode 100644 index 1886ecfb2..000000000 --- a/apps/banglerun/src/gps.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { draw } from './display'; -import { updateLog } from './log'; -import { ActivityStatus, AppState } from './state'; - -declare var Bangle: any; - -interface GpsEvent { - lat: number; - lon: number; - alt: number; - speed: number; - hdop: number; - fix: number; -} - -const EARTH_RADIUS = 6371008.8; - -function initGps(state: AppState): void { - Bangle.on('GPS', (gps: GpsEvent) => readGps(state, gps)); - Bangle.setGPSPower(1); -} - -function readGps(state: AppState, gps: GpsEvent): void { - state.lat = gps.lat; - state.lon = gps.lon; - state.alt = gps.alt; - state.vel = gps.speed / 3.6; - state.fix = gps.fix; - state.dop = gps.hdop; - state.gpsValid = state.fix > 0; - - updateGps(state); - draw(state); - - /* Only log GPS data every 5 secs if we - have a fix and we're running. */ - if (state.gpsValid && - state.status === ActivityStatus.Running && - state.timeSinceLog > 5) { - state.timeSinceLog = 0; - updateLog(state); - } -} - -function updateGps(state: AppState): void { - const t = Date.now(); - let dt = (t - state.t) / 1000; - if (!isFinite(dt)) dt=0; - state.t = t; - state.timeSinceLog += dt; - - if (state.status === ActivityStatus.Running) { - state.duration += dt; - } - - if (!state.gpsValid) { - return; - } - - const r = EARTH_RADIUS + state.alt; - const lat = state.lat * Math.PI / 180; - const lon = state.lon * Math.PI / 180; - const x = r * Math.cos(lat) * Math.cos(lon); - const y = r * Math.cos(lat) * Math.sin(lon); - const z = r * Math.sin(lat); - - if (!state.x) { - state.x = x; - state.y = y; - state.z = z; - return; - } - - const dx = x - state.x; - const dy = y - state.y; - const dz = z - state.z; - const dpMag = Math.sqrt(dx * dx + dy * dy + dz * dz); - - state.x = x; - state.y = y; - state.z = z; - - if (state.status === ActivityStatus.Running) { - state.distance += dpMag; - state.speed = (state.distance / state.duration) || 0; - state.cadence = (60 * state.steps / state.duration) || 0; - } -} - -export { initGps, readGps, updateGps }; diff --git a/apps/banglerun/src/hrm.ts b/apps/banglerun/src/hrm.ts deleted file mode 100644 index 08dd237a7..000000000 --- a/apps/banglerun/src/hrm.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { AppState } from './state'; - -interface HrmData { - bpm: number; - confidence: number; - raw: string; -} - -declare var Bangle: any; - -function initHrm(state: AppState) { - Bangle.on('HRM', (hrm: HrmData) => updateHrm(state, hrm)); - Bangle.setHRMPower(1); -} - -function updateHrm(state: AppState, hrm: HrmData) { - if (hrm.confidence === 0) { - return; - } - - const dHr = hrm.bpm - state.hr; - const hrError = Math.abs(dHr) + 101 - hrm.confidence; - const hrGain = (state.hrError / (state.hrError + hrError)) || 0; - - state.hr += dHr * hrGain; - state.hrError += (hrError - state.hrError) * hrGain; -} - -export { initHrm, updateHrm }; diff --git a/apps/banglerun/src/log.ts b/apps/banglerun/src/log.ts deleted file mode 100644 index b6714e407..000000000 --- a/apps/banglerun/src/log.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { AppState } from './state'; - -declare var require: any; - -function initLog(state: AppState): void { - const datetime = new Date().toISOString().replace(/[-:]/g, ''); - const date = datetime.substr(2, 6); - const time = datetime.substr(9, 6); - const filename = `banglerun_${date}_${time}`; - state.file = require('Storage').open(filename, 'w'); - state.fileWritten = false; -} - -function updateLog(state: AppState): void { - if (!state.fileWritten) { - state.file.write([ - 'timestamp', - 'latitude', - 'longitude', - 'altitude', - 'duration', - 'distance', - 'heartrate', - 'steps', - ].join(',') + '\n'); - state.fileWritten = true; - } - state.file.write([ - Date.now().toFixed(0), - state.lat.toFixed(6), - state.lon.toFixed(6), - state.alt.toFixed(2), - state.duration.toFixed(0), - state.distance.toFixed(2), - state.hr.toFixed(0), - state.steps.toFixed(0), - ].join(',') + '\n'); -} - -export { initLog, updateLog }; diff --git a/apps/banglerun/src/state.ts b/apps/banglerun/src/state.ts deleted file mode 100644 index 14ef2dc5d..000000000 --- a/apps/banglerun/src/state.ts +++ /dev/null @@ -1,85 +0,0 @@ -enum ActivityStatus { - Stopped = 'STOP', - Paused = 'PAUSE', - Running = 'RUN', -} - -interface AppState { - // GPS NMEA data - fix: number; - lat: number; - lon: number; - alt: number; - vel: number; - dop: number; - gpsValid: boolean; - - // Absolute position data - x: number; - y: number; - z: number; - // Last fix time - t: number; - // Last time we saved log info - timeSinceLog : number; - - // HRM data - hr: number, - hrError: number, - - // Logger data - file: File; - fileWritten: boolean; - - // Drawing data - drawing: boolean; - - // Activity data - status: ActivityStatus; - duration: number; - distance: number; - speed: number; - steps: number; - cadence: number; -} - -interface File { - read: Function; - write: Function; - erase: Function; -} - -function initState(): AppState { - return { - fix: NaN, - lat: NaN, - lon: NaN, - alt: NaN, - vel: NaN, - dop: NaN, - gpsValid: false, - - x: NaN, - y: NaN, - z: NaN, - t: NaN, - timeSinceLog : 0, - - hr: 60, - hrError: 100, - - file: null, - fileWritten: false, - - drawing: false, - - status: ActivityStatus.Stopped, - duration: 0, - distance: 0, - speed: 0, - steps: 0, - cadence: 0, - } -} - -export { ActivityStatus, AppState, File, initState }; diff --git a/apps/banglerun/src/step.ts b/apps/banglerun/src/step.ts deleted file mode 100644 index c7fcb61ea..000000000 --- a/apps/banglerun/src/step.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ActivityStatus, AppState } from './state'; - -declare var Bangle: any; - -function initStep(state: AppState) { - Bangle.on('step', () => updateStep(state)); -} - -function updateStep(state: AppState) { - if (state.status === ActivityStatus.Running) { - state.steps += 1; - } -} - -export { initStep, updateStep }; diff --git a/apps/banglerun/tsconfig.json b/apps/banglerun/tsconfig.json deleted file mode 100644 index a341a5a5e..000000000 --- a/apps/banglerun/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "module": "es2015", - "noImplicitAny": true, - "target": "es2015" - }, - "include": [ - "src" - ] -} diff --git a/apps/banglerun/tsconfig.spec.json b/apps/banglerun/tsconfig.spec.json deleted file mode 100644 index 136ae137b..000000000 --- a/apps/banglerun/tsconfig.spec.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "noImplicitAny": true, - "target": "es2015" - }, - "include": [ - "test" - ] -} diff --git a/apps/blackjack/ChangeLog b/apps/blackjack/ChangeLog index 25b5f9195..8e468e9ad 100644 --- a/apps/blackjack/ChangeLog +++ b/apps/blackjack/ChangeLog @@ -1,2 +1,3 @@ 0.01: New game! BTN4- Hit card, BTN5- Stand -0.02: ignore buttons on pauses \ No newline at end of file +0.02: Ignore buttons on pauses +0.03: Support Bangle.js 2 diff --git a/apps/blackjack/appb2.js b/apps/blackjack/appb2.js new file mode 100644 index 000000000..c9907487d --- /dev/null +++ b/apps/blackjack/appb2.js @@ -0,0 +1,207 @@ +var Clubs = require("heatshrink").decompress(atob("j0ewcBkmSpICipEAiQLHwA3BBY8gBQMEEA1AJwQgGyAKChILGBQUCFgxwDJpEAO5AVCII44CAQI1GAAg1GAAZQCWxCDEAAqJBQYQAFRIJWCAApcCR4YADPoRWCgQdBPopfCwAdBTw47BcBAvBU44vDfBDUIRIbUHATuQ")); + +var Spades = require("heatshrink").decompress(atob("j0ewcBkmSpICuoALJIQILHpAKBJQ+QLIUJBYsgMoY1GBQcCBYmAPgkSEBEAgggIKApBDIg4KFHAZiCAAgsDBQw4DFitJFhQ4FTwplBgRoCSQoRBBYJ6EF4jgUwDUHAVOQA==")); + +var Hearts = require("heatshrink").decompress(atob("j0ewY96gMkyAEByVIBQcSpILBhMkBYkEyQLBAQYKCCIQLEEwQgCBYuAEBFJkBBCBYw4CEA44CgQLHIYQsHLJsAEBJEHSQhxENwQADMQoAEKAdAWowLCYJESXggAFGowA/AAQ")); + +var Diamonds = require("heatshrink").decompress(atob("j0ewY1ykgKJhIKJiVIEBOSoAKHpILBBQ+SBYOQBIsBCgILBwAKEgQgCAQIKEggICAQMgKwgUDAQI1GBY4IFLgoLGJpGSPoo4EMoxNIMoqSHiR6HLgizIPoLgfAFA")); + +var deck = []; +var player = {Hand:[]}; +var computer = {Hand:[]}; +var ctx = {ready:true}; + +function createDeck() { + var suits = ["Spades", "Hearts", "Diamonds", "Clubs"]; + var values = ["2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"]; + + var dck = []; + for (var i = 0 ; i < values.length; i++) { + for(var x = 0; x < suits.length; x++) { + dck.push({ Value: values[i], Suit: suits[x] }); + } + } + return dck; +} + +function shuffle(a) { + var j, x, i; + for (i = a.length - 1; i > 0; i--) { + j = Math.floor(Math.random() * (i + 1)); + x = a[i]; + a[i] = a[j]; + a[j] = x; + } + return a; +} + +function EndGameMessdage(msg){ + ctx.ready = false; + g.clearRect(0,160,176,176); + g.setColor(255,255,255); + g.fillRect(0,160,176,176); + g.setColor(0,0,0); + g.drawString(msg, 12, 155); + setTimeout(function(){ + startGame(); + }, 2500); + +} + +function hitMe() { + if (!ctx.ready) return; + player.Hand.push(deck.pop()); + renderOnScreen(1); + var playerWeight = calcWeight(player.Hand, 0); + + if(playerWeight == 21) + EndGameMessdage('WINNER'); + else if(playerWeight > 21) + EndGameMessdage('LOSER'); +} + +function calcWeight(hand, hideCard) { + if(hideCard === 1) { + if (hand[0].Value == "J" || hand[0].Value == "Q" || hand[0].Value == "K") + return "10 +"; + else if (hand[0].Value == "A") + return "11 +"; + else + return parseInt(hand[0].Value) +" +"; + } + else { + var weight = 0; + for(i=0; i 21 || bangleWeight < playerWeight) + EndGameMessdage('WINNER'); + else if(bangleWeight > playerWeight) + EndGameMessdage('LOOSER'); +} + +function renderOnScreen(HideCard) { + const fontName = "6x8"; + + g.clear(); // clear screen + g.reset(); // default draw styles + g.setFont(fontName, 1); + + g.setColor(255,255,255); + g.fillRect(Bangle.appRect); + g.setColor(0,0,0); + + g.drawString('Hit', 176/4-10, 160); + g.drawString('Stand', 176/4+176/2-10, 160); + + g.setFont(fontName, 3); + for(i=0; i right; + + if(is_left){ + hitMe(); + + } else if(is_right){ + stand(); + } +}); +setWatch(startGame, BTN1, {repeat:true, edge:"falling"}); + +startGame(); diff --git a/apps/blackjack/metadata.json b/apps/blackjack/metadata.json index 331c64040..837b891bd 100644 --- a/apps/blackjack/metadata.json +++ b/apps/blackjack/metadata.json @@ -2,15 +2,16 @@ "id": "blackjack", "name": "Black Jack game", "shortName": "Black Jack game", - "version": "0.02", + "version": "0.03", "description": "Simple implementation of card game Black Jack", "icon": "blackjack.png", "tags": "game", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS","BANGLEJS2"], "screenshots": [{"url":"bangle1-black-jack-game-screenshot.png"}], "allow_emulator": true, "storage": [ - {"name":"blackjack.app.js","url":"blackjack.app.js"}, + {"name":"blackjack.app.js","url":"blackjack.app.js","supports": ["BANGLEJS"]}, + {"name":"blackjack.app.js","url":"appb2.js","supports": ["BANGLEJS2"]}, {"name":"blackjack.img","url":"blackjack-icon.js","evaluate":true} ] } diff --git a/apps/boldclk/ChangeLog b/apps/boldclk/ChangeLog index c7a4ba7b4..30ac31c61 100644 --- a/apps/boldclk/ChangeLog +++ b/apps/boldclk/ChangeLog @@ -2,3 +2,4 @@ 0.03: Tweak for more efficient rendering, and firmware 2v06 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". diff --git a/apps/boldclk/bold_clock.js b/apps/boldclk/bold_clock.js index 4358b2e29..9d3ea0756 100644 --- a/apps/boldclk/bold_clock.js +++ b/apps/boldclk/bold_clock.js @@ -129,14 +129,6 @@ Bangle.on('lcdPower', (on) => { clearTimers(); } }); -Bangle.on('faceUp',function(up){ - //console.log("faceUp: " + up + " LCD: " + Bangle.isLCDOn()); - if (up && !Bangle.isLCDOn()) { - //console.log("faceUp and LCD off"); - clearTimers(); - Bangle.setLCDPower(true); - } -}); g.clear(); Bangle.loadWidgets(); diff --git a/apps/boldclk/metadata.json b/apps/boldclk/metadata.json index 7e3941cb3..cf961347d 100644 --- a/apps/boldclk/metadata.json +++ b/apps/boldclk/metadata.json @@ -1,7 +1,7 @@ { "id": "boldclk", "name": "Bold Clock", - "version": "0.05", + "version": "0.06", "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 4c3d3b930..e75c9cc7b 100644 --- a/apps/boot/ChangeLog +++ b/apps/boot/ChangeLog @@ -46,3 +46,4 @@ 0.40: Bootloader now rebuilds for new firmware versions 0.41: Add Keyboard and Mouse Bluetooth HID option 0.42: Sort *.boot.js files lexically and by optional numeric priority, e.g. appname..boot.js +0.43: Fix Gadgetbridge handling with Programmable:off diff --git a/apps/boot/bootupdate.js b/apps/boot/bootupdate.js index 63424bfbf..c9bd89f51 100644 --- a/apps/boot/bootupdate.js +++ b/apps/boot/bootupdate.js @@ -38,7 +38,7 @@ LoopbackA.setConsole(true);\n`; boot += ` Bluetooth.line=""; Bluetooth.on('data',function(d) { - var l = (Bluetooth.line + d).split("\n"); + var l = (Bluetooth.line + d).split(/[\\n\\r]/); Bluetooth.line = l.pop(); l.forEach(n=>Bluetooth.emit("line",n)); }); @@ -196,7 +196,7 @@ if (!Bangle.appRect) { // added in 2v11 - polyfill for older firmwares // Append *.boot.js files // These could change bleServices/bleServiceOptions if needed var getPriority = /.*\.(\d+)\.boot\.js$/; -require('Storage').list(/\.boot\.js/).sort((a,b)=>{ +require('Storage').list(/\.boot\.js$/).sort((a,b)=>{ var aPriority = a.match(getPriority); var bPriority = b.match(getPriority); if (aPriority && bPriority){ @@ -206,7 +206,7 @@ require('Storage').list(/\.boot\.js/).sort((a,b)=>{ } else if (!aPriority && bPriority){ return 1; } - return a > b; + return a==b ? 0 : (a>b ? 1 : -1); }).forEach(bootFile=>{ // we add a semicolon so if the file is wrapped in (function(){ ... }() // with no semicolon we don't end up with (function(){ ... }()(function(){ ... }() diff --git a/apps/boot/metadata.json b/apps/boot/metadata.json index 4cbfd9c59..edddc6b41 100644 --- a/apps/boot/metadata.json +++ b/apps/boot/metadata.json @@ -1,7 +1,7 @@ { "id": "boot", "name": "Bootloader", - "version": "0.42", + "version": "0.43", "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 58d002f22..41eec666a 100644 --- a/apps/bthrm/ChangeLog +++ b/apps/bthrm/ChangeLog @@ -20,3 +20,4 @@ 0.07: Recorder icon only blue if values actually arive Adds some preset modes and a custom one Restructure the settings menu +0.08: Allow scanning for devices in settings diff --git a/apps/bthrm/boot.js b/apps/bthrm/boot.js index 339f6f8c6..3a1f1cc4c 100644 --- a/apps/bthrm/boot.js +++ b/apps/bthrm/boot.js @@ -23,7 +23,10 @@ } function getCache(){ - return require('Storage').readJSON("bthrm.cache.json", true) || {}; + var cache = require('Storage').readJSON("bthrm.cache.json", true) || {}; + if (settings.btname && settings.btname == cache.name) return cache; + clearCache(); + return {}; } function addNotificationHandler(characteristic){ @@ -361,7 +364,13 @@ var promise; if (!device){ - promise = NRF.requestDevice({ filters: serviceFilters }); + var filters = serviceFilters; + if (settings.btname){ + log("Configured device name", settings.btname); + filters = [{name: settings.btname}]; + } + log("Requesting device with filters", filters); + promise = NRF.requestDevice({ filters: filters }); if (settings.gracePeriodRequest){ log("Add " + settings.gracePeriodRequest + "ms grace period after request"); @@ -488,11 +497,15 @@ if (gatt) { if (gatt.connected){ log("Disconnect with gatt: ", gatt); - gatt.disconnect().then(()=>{ - log("Successful disconnect"); - }).catch((e)=>{ - log("Error during disconnect", e); - }); + try{ + gatt.disconnect().then(()=>{ + log("Successful disconnect"); + }).catch((e)=>{ + log("Error during disconnect promise", e); + }); + } catch (e){ + log("Error during disconnect attempt", e); + } } } } diff --git a/apps/bthrm/metadata.json b/apps/bthrm/metadata.json index 1c21269e2..b35ebd6af 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.07", + "version": "0.08", "description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.", "icon": "app.png", "type": "app", diff --git a/apps/bthrm/settings.js b/apps/bthrm/settings.js index beefb00e9..4b564d670 100644 --- a/apps/bthrm/settings.js +++ b/apps/bthrm/settings.js @@ -17,54 +17,73 @@ var settings; readSettings(); - var mainmenu = { - '': { 'title': 'Bluetooth HRM' }, - '< Back': back, - 'Mode': { - value: 0 | settings.mode, - min: 0, - max: 3, - format: v => ["Off", "Default", "Both", "Custom"][v], - onchange: v => { - settings.mode = v; - switch (v){ - case 0: - writeSettings("enabled",false); - break; - case 1: - writeSettings("enabled",true); - writeSettings("replace",true); - writeSettings("debuglog",false); - writeSettings("startWithHrm",true); - writeSettings("allowFallback",true); - writeSettings("fallbackTimeout",10); - break; - case 2: - writeSettings("enabled",true); - writeSettings("replace",false); - writeSettings("debuglog",false); - writeSettings("startWithHrm",false); - writeSettings("allowFallback",false); - break; - case 3: - writeSettings("enabled",true); - writeSettings("replace",settings.custom_replace); - writeSettings("debuglog",settings.custom_debuglog); - writeSettings("startWithHrm",settings.custom_startWithHrm); - writeSettings("allowFallback",settings.custom_allowFallback); - writeSettings("fallbackTimeout",settings.custom_fallbackTimeout); - break; + function buildMainMenu(){ + var mainmenu = { + '': { 'title': 'Bluetooth HRM' }, + '< Back': back, + 'Mode': { + value: 0 | settings.mode, + min: 0, + max: 3, + format: v => ["Off", "Default", "Both", "Custom"][v], + onchange: v => { + settings.mode = v; + switch (v){ + case 0: + writeSettings("enabled",false); + break; + case 1: + writeSettings("enabled",true); + writeSettings("replace",true); + writeSettings("debuglog",false); + writeSettings("startWithHrm",true); + writeSettings("allowFallback",true); + writeSettings("fallbackTimeout",10); + break; + case 2: + writeSettings("enabled",true); + writeSettings("replace",false); + writeSettings("debuglog",false); + writeSettings("startWithHrm",false); + writeSettings("allowFallback",false); + break; + case 3: + writeSettings("enabled",true); + writeSettings("replace",settings.custom_replace); + writeSettings("debuglog",settings.custom_debuglog); + writeSettings("startWithHrm",settings.custom_startWithHrm); + writeSettings("allowFallback",settings.custom_allowFallback); + writeSettings("fallbackTimeout",settings.custom_fallbackTimeout); + break; + } + writeSettings("mode",v); } - writeSettings("mode",v); } - }, - 'Custom Mode': function() { E.showMenu(submenu_custom); }, - 'Debug': function() { E.showMenu(submenu_debug); } - }; + }; + + if (settings.btname){ + var name = "Clear " + settings.btname; + mainmenu[name] = function() { + E.showPrompt("Clear current device name?").then((r)=>{ + if (r) { + writeSettings("btname",undefined); + } + E.showMenu(buildMainMenu()); + }); + }; + } + + mainmenu["BLE Scan"] = ()=> createMenuFromScan(); + mainmenu["Custom Mode"] = function() { E.showMenu(submenu_custom); }; + mainmenu.Debug = function() { E.showMenu(submenu_debug); }; + return mainmenu; + } + + var submenu_debug = { '' : { title: "Debug"}, - '< Back': function() { E.showMenu(mainmenu); }, + '< Back': function() { E.showMenu(buildMainMenu()); }, 'Alert on disconnect': { value: !!settings.warnDisconnect, format: v => settings.warnDisconnect ? "On" : "Off", @@ -81,10 +100,41 @@ }, 'Grace periods': function() { E.showMenu(submenu_grace); } }; + + function createMenuFromScan(){ + E.showMenu(); + E.showMessage("Scanning"); + + var submenu_scan = { + '' : { title: "Scan"}, + '< Back': function() { E.showMenu(buildMainMenu()); } + }; + var packets=10; + var scanStart=Date.now(); + NRF.setScan(function(d) { + packets--; + if (packets<=0 || Date.now() - scanStart > 5000){ + NRF.setScan(); + E.showMenu(submenu_scan); + } else if (d.name){ + print("Found device", d); + submenu_scan[d.name] = function(){ + E.showPrompt("Set "+d.name+"?").then((r)=>{ + if (r) { + writeSettings("btname",d.name); + } + E.showMenu(buildMainMenu()); + }); + }; + } + }, { filters: [{services: [ "180d" ]}]}); + } + + var submenu_custom = { '' : { title: "Custom mode"}, - '< Back': function() { E.showMenu(mainmenu); }, + '< Back': function() { E.showMenu(buildMainMenu()); }, 'Replace HRM': { value: !!settings.custom_replace, format: v => settings.custom_replace ? "On" : "Off", @@ -165,7 +215,7 @@ var submenu = { '' : { title: "Grace periods"}, - '< Back': function() { E.showMenu(mainmenu); }, + '< Back': function() { E.showMenu(buildMainMenu()); }, 'Request': { value: settings.gracePeriodRequest, min: 0, @@ -208,5 +258,5 @@ } }; - E.showMenu(mainmenu); + E.showMenu(buildMainMenu()); }) diff --git a/apps/bthrv/ChangeLog b/apps/bthrv/ChangeLog index e144fd8f9..eefadac78 100644 --- a/apps/bthrv/ChangeLog +++ b/apps/bthrv/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App! 0.02: Write available data on reset or kill +0.03: Buzz short on every finished measurement and longer if all are done diff --git a/apps/bthrv/app.js b/apps/bthrv/app.js index 067c84f56..fbd0e2d05 100644 --- a/apps/bthrv/app.js +++ b/apps/bthrv/app.js @@ -75,7 +75,6 @@ function write(){ data += "," + rrMax + "," + rrMin + ","+rrNumberOfValues; data += "\n"; file.write(data); - Bangle.buzz(500); } function onBtHrm(e) { @@ -87,6 +86,11 @@ function onBtHrm(e) { if (currentSlot <= hrvSlots.length && (Date.now() - startingTime) > (hrvSlots[currentSlot] * 1000) && !hrvValues[hrvSlots[currentSlot]]){ hrvValues[hrvSlots[currentSlot]] = hrv; currentSlot++; + if (currentSlot == hrvSlots.length){ + Bangle.buzz(500) + } else { + Bangle.buzz(50); + } } } diff --git a/apps/bthrv/metadata.json b/apps/bthrv/metadata.json index 183008034..7c57be682 100644 --- a/apps/bthrv/metadata.json +++ b/apps/bthrv/metadata.json @@ -2,7 +2,7 @@ "id": "bthrv", "name": "Bluetooth Heart Rate variance calculator", "shortName": "BT HRV", - "version": "0.02", + "version": "0.03", "description": "Calculates HRV from a a BT HRM with interval data", "icon": "app.png", "type": "app", diff --git a/apps/circlesclock/ChangeLog b/apps/circlesclock/ChangeLog index 7165f8521..46eddd32b 100644 --- a/apps/circlesclock/ChangeLog +++ b/apps/circlesclock/ChangeLog @@ -20,3 +20,6 @@ Color depending on value (green -> red, red -> green) option Good HRM value will not be overwritten so fast anymore 0.10: Use roboto font for time, date and day of week and center align them +0.11: New color option: foreground color + Improve performance, reduce memory usage + Small optical adjustments diff --git a/apps/circlesclock/app.js b/apps/circlesclock/app.js index 903c7bdb2..a6aa1a8b1 100644 --- a/apps/circlesclock/app.js +++ b/apps/circlesclock/app.js @@ -3,23 +3,8 @@ const storage = require("Storage"); const SunCalc = require("https://raw.githubusercontent.com/mourner/suncalc/master/suncalc.js"); const shoesIcon = atob("EBCBAAAACAAcAB4AHgAeABwwADgGeAZ4AHgAMAAAAHAAIAAA"); -const heartIcon = atob("EBCBAAAAAAAeeD/8P/x//n/+P/w//B/4D/AH4APAAYAAAAAA"); -const powerIcon = atob("EBCBAAAAA8ADwA/wD/AP8A/wD/AP8A/wD/AP8A/wD/AH4AAA"); const temperatureIcon = atob("EBCBAAAAAYADwAJAAkADwAPAA8ADwAfgB+AH4AfgA8ABgAAA"); -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"); - -const sunSetDown = atob("EBCBAAAAAAABgAAAAAATyAZoBCB//gAAAAAGYAPAAYAAAAAA"); -const sunSetUp = atob("EBCBAAAAAAABgAAAAAATyAZoBCB//gAAAAABgAPABmAAAAAA"); - 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)); @@ -67,7 +52,7 @@ const colorGreen = '#008000'; const colorBlue = '#0000ff'; const colorYellow = '#ffff00'; const widgetOffset = showWidgets ? 24 : 0; -const dowOffset = circleCount == 3 ? 22 : 24; // dow offset relative to date +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; @@ -103,21 +88,24 @@ const circleFontBig = circleCount == 3 ? "Vector:16" : "Vector:12"; const iconOffset = circleCount == 3 ? 6 : 8; const defaultCircleTypes = ["steps", "hr", "battery", "weather"]; +function hideWidgets() { + /* + * 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. + */ + if (WIDGETS && typeof WIDGETS === "object") { + for (let wd of WIDGETS) { + wd.draw = () => {}; + wd.area = ""; + } + } +} function draw() { g.clear(true); if (!showWidgets) { - /* - * 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. - */ - if (WIDGETS && typeof WIDGETS === "object") { - for (let wd of WIDGETS) { - wd.draw = () => {}; - wd.area = ""; - } - } + hideWidgets(); } else { Bangle.drawWidgets(); } @@ -129,7 +117,7 @@ function draw() { g.setFontRobotoRegular50NumericOnly(); g.setFontAlign(0, -1); g.setColor(colorFg); - g.drawString(locale.time(new Date(), 1), w / 2, h1 + 8); + g.drawString(locale.time(new Date(), 1), w / 2, h1 + 6); now = Math.round(new Date().getTime() / 1000); // date & dow @@ -138,10 +126,19 @@ function draw() { g.drawString(locale.date(new Date()), w / 2, h2); g.drawString(locale.dow(new Date()), w / 2, h2 + dowOffset); - drawCircle(1); - drawCircle(2); - drawCircle(3); - if (circleCount >= 4) drawCircle(4); + // draw the circles a little bit delayed so we decrease the blocking time + setTimeout(function() { + drawCircle(1); + }, 1); + setTimeout(function() { + drawCircle(2); + }, 1); + setTimeout(function() { + drawCircle(3); + }, 1); + setTimeout(function() { + if (circleCount >= 4) drawCircle(4); + }, 1); } function drawCircle(index) { @@ -248,6 +245,9 @@ function getGradientColor(color, percent) { const colorList = [ '#00FF00', '#80FF00', '#FFFF00', '#FF8000', '#FF0000' ]; + if (color == "fg") { + color = colorFg; + } if (color == "green-red") { const colorIndex = Math.round(colorList.length * percent); return colorList[Math.min(colorIndex, colorList.length) - 1] || "#00ff00"; @@ -325,6 +325,8 @@ function drawStepsDistance(w) { function drawHeartRate(w) { if (!w) w = getCircleXPosition("hr"); + const heartIcon = atob("EBCBAAAAAAAeeD/8P/x//n/+P/w//B/4D/AH4APAAYAAAAAA"); + drawCircleBackground(w); const color = getCircleColor("hr"); @@ -349,6 +351,8 @@ function drawBattery(w) { if (!w) w = getCircleXPosition("battery"); const battery = E.getBattery(); + const powerIcon = atob("EBCBAAAAA8ADwA/wD/AP8A/wD/AP8A/wD/AP8A/wD/AH4AAA"); + drawCircleBackground(w); let color = getCircleColor("battery"); @@ -426,6 +430,10 @@ function drawSunProgress(w) { if (!w) w = getCircleXPosition("sunprogress"); const percent = getSunProgress(); + // sunset icons: + const sunSetDown = atob("EBCBAAAAAAABgAAAAAATyAZoBCB//gAAAAAGYAPAAYAAAAAA"); + const sunSetUp = atob("EBCBAAAAAAABgAAAAAATyAZoBCB//gAAAAABgAPABmAAAAAA"); + drawCircleBackground(w); const color = getCircleColor("sunprogress"); @@ -559,6 +567,18 @@ function windAsBeaufort(windInKmh) { */ function getWeatherIconByCode(code) { const codeGroup = Math.round(code / 100); + + // 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"); + switch (codeGroup) { case 2: return weatherStormy; @@ -823,7 +843,6 @@ if (isCircleEnabled("hr")) { enableHRMSensor(); } - Bangle.setUI("clock"); Bangle.loadWidgets(); diff --git a/apps/circlesclock/metadata.json b/apps/circlesclock/metadata.json index 3279ec2cf..b16f14c06 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.10", + "version":"0.11", "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/settings.js b/apps/circlesclock/settings.js index bec539376..0b9e94aca 100644 --- a/apps/circlesclock/settings.js +++ b/apps/circlesclock/settings.js @@ -14,8 +14,10 @@ const valuesCircleTypes = ["empty", "steps", "stepsDist", "hr", "battery", "weather", "sunprogress", "temperature", "pressure", "altitude"]; const namesCircleTypes = ["empty", "steps", "distance", "heart", "battery", "weather", "sun", "temperature", "pressure", "altitude"]; - const valuesColors = ["", "#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff", "#00ffff", "#fff", "#000", "green-red", "red-green"]; - const namesColors = ["default", "red", "green", "blue", "yellow", "magenta", "cyan", "white", "black", "green->red", "red->green"]; + const valuesColors = ["", "#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff", + "#00ffff", "#fff", "#000", "green-red", "red-green", "fg"]; + const namesColors = ["default", "red", "green", "blue", "yellow", "magenta", + "cyan", "white", "black", "green->red", "red->green", "foreground"]; const weatherData = ["empty", "humidity", "wind"]; diff --git a/apps/clockcal/ChangeLog b/apps/clockcal/ChangeLog new file mode 100644 index 000000000..299b1ec69 --- /dev/null +++ b/apps/clockcal/ChangeLog @@ -0,0 +1,2 @@ +0.01: Initial upload +0.2: Added scrollable calendar and swipe gestures diff --git a/apps/clockcal/README.md b/apps/clockcal/README.md new file mode 100644 index 000000000..da6887177 --- /dev/null +++ b/apps/clockcal/README.md @@ -0,0 +1,33 @@ +# Clock & Calendar by Michael + +This is my "Hello World". I first made this watchface almost 10 years ago for my original Pebble and Pebble Time and I missed this so much, that I had to write it for the BangleJS2. +I know that it seems redundant because there already **is** a *time&cal*-app, but it didn't fit my style. + +|Screenshot|description| +|:--:|:-| +|![locked screen](screenshot.png)|locked: triggers only one minimal update/min| +|![unlocked screen](screenshot2.png)|unlocked: smaller clock, but with seconds| +|![big calendar](screenshot3.png)|swipe up for big calendar, (up down to scroll, left/right to exit)| + + + + +## Configurable Features +- Number of calendar rows (weeks) +- Buzz on connect/disconnect (I know, this should be an extra widget, but for now, it is included) +- Clock Mode (24h/12h). Doesn't have an am/pm indicator. It's only there because it was easy. +- First day of the week +- Red Saturday +- Red Sunday +- Swipes (to disable all gestures) +- Swipes: music (swipe down) +- Spipes: messages (swipe right) + +## Auto detects your message/music apps: +- swiping down will search your files for an app with the string "music" in its filename and launch it +- swiping right will search your files for an app with the string "message" in its filename and launch it. +- Configurable apps coming soon. + +## Feedback +The clock works for me in a 24h/MondayFirst/WeekendFree environment but is not well-tested with other settings. +So if something isn't working, please tell me: https://github.com/foostuff/BangleApps/issues diff --git a/apps/clockcal/app-icon.js b/apps/clockcal/app-icon.js new file mode 100644 index 000000000..5bab7853e --- /dev/null +++ b/apps/clockcal/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkECqMCkQACiEDkIXQuUnkUBkESiYXPgN/u8jgEx/8vC6E3k9xiH//8/C6BHCPQMSL6EDO4cgaf4A/ACEC+YFDl4FEAAM/+ISHbIIECh4FB+QWEA4PwCQsfC4gVBkYGDgP/mQ4CCQk/iAXEAQTiCgMiDQQSFiATDBgQXCgILBEQkQBwYrEC4sPLQRpCBwoXECgUCC4oSBAggXHNQRfDV4X/JgQXJBIIXFgYuDC5QKBiE/C4f/bwgXJmanGJgoSDiTQBmQMBE4JYBfwJ5BBYMiYQISEB4IAB+KdCAgfwAwTrCn4SDiczAAMwGwMTmR0CmECBgRSBCQwA/AGsBgEQAgYABAwcHu93s4GBqAXEmLrCiYICmICBj4XEgvABIMMqECiIXCgQXCegLYBC4NwF4VcAQNV4EPkEhF4REBgYXCiQvCu4UCAQMFJYRfKgxGBuxfGLgkjFgMCkMBmEjgEigZaBI4XFMYcRC4kBmRhBkMQgI5DF4MFgAXCLARfCFoIvDkZmBhnF4sA5gvDYghfEHIQJDAAhQBIAPwVQMTgQvCNIMhAwJfBR4MMU4JRB+RJBiUQgUDVwMgYwMBgcwX4amBqBQBiTqBgUQh8RmJhCL4IvC4HMR4ZaEAgIBBL4LBDL5EBmI5BkQvBXwIGBmMPMwMvkEFR4VcR4UgU4MSC4UQmIJBn7dBiQNBqoXBPYNQh8Q+MB+MvgEvG4JyBj8A+RkBhlQd4ZHBiBYCL4bBELxEAA==")) \ No newline at end of file diff --git a/apps/clockcal/app.js b/apps/clockcal/app.js new file mode 100644 index 000000000..86fa0815a --- /dev/null +++ b/apps/clockcal/app.js @@ -0,0 +1,292 @@ +Bangle.loadWidgets(); + +var s = Object.assign({ + CAL_ROWS: 4, //number of calendar rows.(weeks) Shouldn't exceed 5 when using widgets. + BUZZ_ON_BT: true, //2x slow buzz on disconnect, 2x fast buzz on connect. Will be extra widget eventually + MODE24: true, //24h mode vs 12h mode + FIRSTDAYOFFSET: 6, //First day of the week: 0-6: Sun, Sat, Fri, Thu, Wed, Tue, Mon + REDSUN: true, // Use red color for sunday? + REDSAT: true, // Use red color for saturday? + DRAGENABLED: true, + DRAGMUSIC: true, + DRAGMESSAGES: true +}, require('Storage').readJSON("clockcal.json", true) || {}); + +const h = g.getHeight(); +const w = g.getWidth(); +const CELL_W = w / 7; +const CELL2_W = w / 8;//full calendar +const CELL_H = 15; +const CAL_Y = h - s.CAL_ROWS * CELL_H; +const DEBUG = false; +var state = "watch"; +var monthOffset = 0; + +/* + * Calendar features + */ +function drawFullCalendar(monthOffset) { + addMonths = function (_d, _am) { + var ay = 0, m = _d.getMonth(), y = _d.getFullYear(); + while ((m + _am) > 11) { ay++; _am -= 12; } + while ((m + _am) < 0) { ay--; _am += 12; } + n = new Date(_d.getTime()); + n.setMonth(m + _am); + n.setFullYear(y + ay); + return n; + }; + monthOffset = (typeof monthOffset == "undefined") ? 0 : monthOffset; + state = "calendar"; + var start = Date().getTime(); + const months = ['Jan.', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec.']; + const monthclr = ['#0f0', '#f0f', '#00f', '#ff0', '#0ff', '#fff']; + if (typeof dayInterval !== "undefined") clearTimeout(dayInterval); + if (typeof secondInterval !== "undefined") clearTimeout(secondInterval); + if (typeof minuteInterval !== "undefined") clearTimeout(minuteInterval); + d = addMonths(Date(), monthOffset); + tdy = Date().getDate() + "." + Date().getMonth(); + newmonth=false; + c_y = 0; + g.reset(); + g.setBgColor(0); + g.clear(); + var prevmonth = addMonths(d, -1) + const today = prevmonth.getDate(); + var rD = new Date(prevmonth.getTime()); + rD.setDate(rD.getDate() - (today - 1)); + const dow = (s.FIRSTDAYOFFSET + rD.getDay()) % 7; + rD.setDate(rD.getDate() - dow); + var rDate = rD.getDate(); + bottomrightY = c_y - 3; + clrsun=s.REDSUN?'#f00':'#fff'; + clrsat=s.REDSUN?'#f00':'#fff'; + var fg=[clrsun,'#fff','#fff','#fff','#fff','#fff',clrsat]; + for (var y = 1; y <= 11; y++) { + bottomrightY += CELL_H; + bottomrightX = -2; + for (var x = 1; x <= 7; x++) { + bottomrightX += CELL2_W; + rMonth = rD.getMonth(); + rDate = rD.getDate(); + if (tdy == rDate + "." + rMonth) { + caldrawToday(rDate); + } else if (rDate == 1) { + caldrawFirst(rDate); + } else { + caldrawNormal(rDate,fg[rD.getDay()]); + } + if (newmonth && x == 7) { + caldrawMonth(rDate,monthclr[rMonth % 6],months[rMonth],rD); + } + rD.setDate(rDate + 1); + } + } + delete addMonths; + if (DEBUG) console.log("Calendar performance (ms):" + (Date().getTime() - start)); +} +function caldrawMonth(rDate,c,m,rD) { + g.setColor(c); + g.setFont("Vector", 18); + g.setFontAlign(-1, 1, 1); + drawyear = ((rMonth % 11) == 0) ? String(rD.getFullYear()).substr(-2) : ""; + g.drawString(m + drawyear, bottomrightX, bottomrightY - CELL_H, 1); + newmonth = false; +} +function caldrawToday(rDate) { + g.setFont("Vector", 16); + g.setFontAlign(1, 1); + g.setColor('#0f0'); + g.fillRect(bottomrightX - CELL2_W + 1, bottomrightY - CELL_H - 1, bottomrightX, bottomrightY - 2); + g.setColor('#000'); + g.drawString(rDate, bottomrightX, bottomrightY); +} +function caldrawFirst(rDate) { + g.flip(); + g.setFont("Vector", 16); + g.setFontAlign(1, 1); + bottomrightY += 3; + newmonth = true; + g.setColor('#0ff'); + g.fillRect(bottomrightX - CELL2_W + 1, bottomrightY - CELL_H - 1, bottomrightX, bottomrightY - 2); + g.setColor('#000'); + g.drawString(rDate, bottomrightX, bottomrightY); +} +function caldrawNormal(rDate,c) { + g.setFont("Vector", 16); + g.setFontAlign(1, 1); + g.setColor(c); + g.drawString(rDate, bottomrightX, bottomrightY);//100 +} +function drawMinutes() { + if (DEBUG) console.log("|-->minutes"); + 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 size = 50; + var clock_x = (w - 20) / 2; + if (dimSeconds) { + size = 65; + clock_x = 4 + (w / 2); + } + g.setBgColor(0); + g.setColor(textColor); + g.setFont("Vector", size); + g.setFontAlign(0, 1); + g.drawString(hours + ":" + minutes, clock_x, CAL_Y - 10, 1); + var nextminute = (61 - d.getSeconds()); + if (typeof minuteInterval !== "undefined") clearTimeout(minuteInterval); + minuteInterval = setTimeout(drawMinutes, nextminute * 1000); +} + +function drawSeconds() { + if (DEBUG) console.log("|--->seconds"); + var d = new Date(); + g.setColor(); + g.fillRect(w - 31, CAL_Y - 36, w - 3, CAL_Y - 19); + g.setBgColor(0); + g.setColor('#fff'); + g.setFont("Vector", 24); + g.setFontAlign(1, 1); + g.drawString(" " + d.getSeconds().toString().padStart(2, '0'), w, CAL_Y - 13); + if (typeof secondInterval !== "undefined") clearTimeout(secondInterval); + if (!dimSeconds) secondInterval = setTimeout(drawSeconds, 1000); +} + +function drawWatch() { + if (DEBUG) console.log("CALENDAR"); + monthOffset = 0; + state = "watch"; + var d = new Date(); + g.reset(); + g.setBgColor(0); + g.clear(); + drawMinutes(); + if (!dimSeconds) drawSeconds(); + const dow = (s.FIRSTDAYOFFSET + d.getDay()) % 7; //MO=0, SU=6 + const today = d.getDate(); + var rD = new Date(d.getTime()); + rD.setDate(rD.getDate() - dow); + var rDate = rD.getDate(); + g.setFontAlign(1, 1); + for (var y = 1; y <= s.CAL_ROWS; y++) { + for (var x = 1; x <= 7; x++) { + bottomrightX = x * CELL_W - 2; + bottomrightY = y * CELL_H + CAL_Y; + g.setFont("Vector", 16); + var fg = ((s.REDSUN && rD.getDay() == 0) || (s.REDSAT && rD.getDay() == 6)) ? '#f00' : '#fff'; + if (y == 1 && today == rDate) { + g.setColor('#0f0'); + g.fillRect(bottomrightX - CELL_W + 1, bottomrightY - CELL_H - 1, bottomrightX, bottomrightY - 2); + g.setColor('#000'); + g.drawString(rDate, bottomrightX, bottomrightY); + } + else { + g.setColor(fg); + g.drawString(rDate, bottomrightX, bottomrightY); + } + rD.setDate(rDate + 1); + rDate = rD.getDate(); + } + } + Bangle.drawWidgets(); + + var nextday = (3600 * 24) - (d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds() + 1); + if (DEBUG) console.log("Next Day:" + (nextday / 3600)); + if (typeof dayInterval !== "undefined") clearTimeout(dayInterval); + dayInterval = setTimeout(drawWatch, nextday * 1000); +} + +function BTevent() { + drawMinutes(); + if (s.BUZZ_ON_BT) { + var interval = (NRF.getSecurityStatus().connected) ? 100 : 500; + Bangle.buzz(interval); + setTimeout(function () { Bangle.buzz(interval); }, interval * 3); + } +} + +function input(dir) { + if (s.DRAGENABLED) { + Bangle.buzz(100,1); + console.log("swipe:"+dir); + switch (dir) { + case "r": + if (state == "calendar") { + drawWatch(); + } else { + if (s.DRAGMUSIC) { + l=require("Storage").list(RegExp("music.*app")); + if (l.length > 0) { + load(l[0]); + } else Bangle.buzz(3000,1);//not found + } + } + break; + case "l": + if (state == "calendar") { + drawWatch(); + } + break; + case "d": + if (state == "calendar") { + monthOffset--; + drawFullCalendar(monthOffset); + } else { + if (s.DRAGMESSAGES) { + l=require("Storage").list(RegExp("message.*app")); + if (l.length > 0) { + load(l[0]); + } else Bangle.buzz(3000,1);//not found + } + } + break; + case "u": + if (state == "watch") { + state = "calendar"; + drawFullCalendar(0); + } else if (state == "calendar") { + monthOffset++; + drawFullCalendar(monthOffset); + } + break; + default: + if (state == "calendar") { + drawWatch(); + } + break; + } + } +} + +let drag; +Bangle.on("drag", e => { + if (s.DRAGENABLED) { + if (!drag) { + drag = { x: e.x, y: e.y }; + } else if (!e.b) { + const dx = e.x - drag.x, dy = e.y - drag.y; + var dir = "t"; + if (Math.abs(dx) > Math.abs(dy) + 10) { + dir = (dx > 0) ? "r" : "l"; + } else if (Math.abs(dy) > Math.abs(dx) + 10) { + dir = (dy > 0) ? "d" : "u"; + } + drag = null; + input(dir); + } + } +}); + +//register events +Bangle.on('lock', locked => { + if (typeof secondInterval !== "undefined") clearTimeout(secondInterval); + dimSeconds = locked; //dim seconds if lock=on + drawWatch(); +}); +NRF.on('connect', BTevent); +NRF.on('disconnect', BTevent); + +dimSeconds = Bangle.isLocked(); +drawWatch(); +Bangle.setUI("clock"); diff --git a/apps/clockcal/app.png b/apps/clockcal/app.png new file mode 100644 index 000000000..2e2e4461e Binary files /dev/null and b/apps/clockcal/app.png differ diff --git a/apps/clockcal/metadata.json b/apps/clockcal/metadata.json new file mode 100644 index 000000000..a42e1ad2e --- /dev/null +++ b/apps/clockcal/metadata.json @@ -0,0 +1,19 @@ +{ + "id": "clockcal", + "name": "Clock & Calendar", + "version": "0.2", + "description": "Clock with Calendar", + "readme":"README.md", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"},{"url":"screenshot2.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS","BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"clockcal.app.js","url":"app.js"}, + {"name":"clockcal.settings.js","url":"settings.js"}, + {"name":"clockcal.img","url":"app-icon.js","evaluate":true} + ], + "data": [{"name":"clockcal.json"}] +} diff --git a/apps/clockcal/screenshot.png b/apps/clockcal/screenshot.png new file mode 100644 index 000000000..fcfde0c4a Binary files /dev/null and b/apps/clockcal/screenshot.png differ diff --git a/apps/clockcal/screenshot2.png b/apps/clockcal/screenshot2.png new file mode 100644 index 000000000..98acfa9a0 Binary files /dev/null and b/apps/clockcal/screenshot2.png differ diff --git a/apps/clockcal/screenshot3.png b/apps/clockcal/screenshot3.png new file mode 100644 index 000000000..ab34f4306 Binary files /dev/null and b/apps/clockcal/screenshot3.png differ diff --git a/apps/clockcal/settings.js b/apps/clockcal/settings.js new file mode 100644 index 000000000..c4ec764c9 --- /dev/null +++ b/apps/clockcal/settings.js @@ -0,0 +1,122 @@ +(function (back) { + var FILE = "clockcal.json"; + + settings = Object.assign({ + CAL_ROWS: 4, //number of calendar rows.(weeks) Shouldn't exceed 5 when using widgets. + BUZZ_ON_BT: true, //2x slow buzz on disconnect, 2x fast buzz on connect. Will be extra widget eventually + MODE24: true, //24h mode vs 12h mode + FIRSTDAY: 6, //First day of the week: mo, tu, we, th, fr, sa, su + REDSUN: true, // Use red color for sunday? + REDSAT: true, // Use red color for saturday? + DRAGENABLED: true, //Enable drag gestures (bigger calendar etc) + DRAGMUSIC: true, //Enable drag down for music (looks for "music*app") + DRAGMESSAGES: true //Enable drag right for messages (looks for "message*app") + }, require('Storage').readJSON(FILE, true) || {}); + + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + menu = { + "": { "title": "Clock & Calendar" }, + "< Back": () => back(), + 'Buzz(dis)conn.?': { + value: settings.BUZZ_ON_BT, + format: v => v ? "On" : "Off", + onchange: v => { + settings.BUZZ_ON_BT = v; + writeSettings(); + } + }, + '#Calendar Rows': { + value: settings.CAL_ROWS, + min: 0, max: 6, + onchange: v => { + settings.CAL_ROWS = v; + writeSettings(); + } + }, + 'Clock mode': { + value: settings.MODE24, + format: v => v ? "24h" : "12h", + onchange: v => { + settings.MODE24 = v; + writeSettings(); + } + }, + 'First Day': { + value: settings.FIRSTDAY, + min: 0, max: 6, + format: v => ["Sun", "Sat", "Fri", "Thu", "Wed", "Tue", "Mon"][v], + onchange: v => { + settings.FIRSTDAY = v; + writeSettings(); + } + }, + 'Red Saturday?': { + value: settings.REDSAT, + format: v => v ? "On" : "Off", + onchange: v => { + settings.REDSAT = v; + writeSettings(); + } + }, + 'Red Sunday?': { + value: settings.REDSUN, + format: v => v ? "On" : "Off", + onchange: v => { + settings.REDSUN = v; + writeSettings(); + } + }, + 'Swipes (big cal.)?': { + value: settings.DRAGENABLED, + format: v => v ? "On" : "Off", + onchange: v => { + settings.DRAGENABLED = v; + writeSettings(); + } + }, + 'Swipes (music)?': { + value: settings.DRAGMUSIC, + format: v => v ? "On" : "Off", + onchange: v => { + settings.DRAGMUSIC = v; + writeSettings(); + } + }, + 'Swipes (messg)?': { + value: settings.DRAGMESSAGES, + format: v => v ? "On" : "Off", + onchange: v => { + settings.DRAGMESSAGES = v; + writeSettings(); + } + }, + 'Load deafauls?': { + value: 0, + min: 0, max: 1, + format: v => ["No", "Yes"][v], + onchange: v => { + if (v == 1) { + settings = { + CAL_ROWS: 4, //number of calendar rows.(weeks) Shouldn't exceed 5 when using widgets. + BUZZ_ON_BT: true, //2x slow buzz on disconnect, 2x fast buzz on connect. + MODE24: true, //24h mode vs 12h mode + FIRSTDAY: 6, //First day of the week: mo, tu, we, th, fr, sa, su + REDSUN: true, // Use red color for sunday? + REDSAT: true, // Use red color for saturday? + DRAGENABLED: true, + DRAGMUSIC: true, + DRAGMESSAGES: true + }; + writeSettings(); + load(); + } + } + }, + }; + // Show the menu + E.showMenu(menu); +}); diff --git a/apps/contourclock/ChangeLog b/apps/contourclock/ChangeLog index 0b6709d24..032edc9b5 100644 --- a/apps/contourclock/ChangeLog +++ b/apps/contourclock/ChangeLog @@ -4,3 +4,5 @@ 0.22: Changed timing code, original "Nunito" Font is back! 0.23: Customizer! Unused fonts no longer take up precious memory. 0.24: Added previews to the customizer. +0.25: Fixed a bug that would let widgets change the color of the clock. +0.26: Time formatted to locale diff --git a/apps/contourclock/app.js b/apps/contourclock/app.js index a5440845d..cdfadd217 100644 --- a/apps/contourclock/app.js +++ b/apps/contourclock/app.js @@ -10,6 +10,7 @@ if (settings.fontIndex==undefined) { function draw() { var date = new Date(); // Draw day of the week + g.reset(); g.setFont("Teletext10x18Ascii"); g.clearRect(0,138,g.getWidth()-1,176); g.setFontAlign(0,1).drawString(require("locale").dow(date).toUpperCase(),g.getWidth()/2,g.getHeight()-18); diff --git a/apps/contourclock/lib.js b/apps/contourclock/lib.js index 65a4622f4..c4f927953 100644 --- a/apps/contourclock/lib.js +++ b/apps/contourclock/lib.js @@ -1,3 +1,11 @@ +var is12; +function getHours(d) { + var h = d.getHours(); + if (is12===undefined) is12 = (require('Storage').readJSON('setting.json',1)||{})["12hour"]; + if (!is12) return h; + return (h%12==0) ? 12 : h%12; +} + exports.drawClock = function(fontIndex) { var digits = []; fontFile=require("Storage").read("contourclock-"+Math.abs(parseInt(fontIndex+0.5))+".json"); @@ -15,8 +23,8 @@ exports.drawClock = function(fontIndex) { var y = g.getHeight()/2-digits[0].height/2; var date = new Date(); g.clearRect(0,38,g.getWidth()-1,138); - d1=parseInt(date.getHours()/10); - d2=parseInt(date.getHours()%10); + d1=parseInt(getHours(date)/10); + d2=parseInt(getHours(date)%10); d3=10; d4=parseInt(date.getMinutes()/10); d5=parseInt(date.getMinutes()%10); diff --git a/apps/contourclock/metadata.json b/apps/contourclock/metadata.json index a5d764f2d..ec29c390b 100644 --- a/apps/contourclock/metadata.json +++ b/apps/contourclock/metadata.json @@ -1,7 +1,7 @@ { "id": "contourclock", "name": "Contour Clock", "shortName" : "Contour Clock", - "version":"0.24", + "version":"0.26", "icon": "app.png", "description": "A Minimalist clockface with large Digits. Now with more fonts!", "screenshots" : [{"url":"cc-screenshot-1.png"},{"url":"cc-screenshot-2.png"}], diff --git a/apps/crowclk/ChangeLog b/apps/crowclk/ChangeLog index 5560f00bc..b7e18abe3 100644 --- a/apps/crowclk/ChangeLog +++ b/apps/crowclk/ChangeLog @@ -1 +1,2 @@ 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". diff --git a/apps/crowclk/crow_clock.js b/apps/crowclk/crow_clock.js index 97e5e61d9..d06369fa8 100644 --- a/apps/crowclk/crow_clock.js +++ b/apps/crowclk/crow_clock.js @@ -133,15 +133,6 @@ Bangle.on('lcdPower', (on) => { clearTimers(); } }); -Bangle.on('faceUp',function(up){ - //console.log("faceUp: " + up + " LCD: " + Bangle.isLCDOn()); - if (up && !Bangle.isLCDOn()) { - //console.log("faceUp and LCD off"); - clearTimers(); - Bangle.setLCDPower(true); - } -}); - g.clear(); diff --git a/apps/crowclk/metadata.json b/apps/crowclk/metadata.json index 365393e6c..752e30fb0 100644 --- a/apps/crowclk/metadata.json +++ b/apps/crowclk/metadata.json @@ -1,7 +1,7 @@ { "id": "crowclk", "name": "Crow Clock", - "version": "0.01", + "version": "0.02", "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/cscsensor/ChangeLog b/apps/cscsensor/ChangeLog index 8f23fa9f3..a98be5c0f 100644 --- a/apps/cscsensor/ChangeLog +++ b/apps/cscsensor/ChangeLog @@ -5,3 +5,4 @@ 0.05: Add cadence sensor support 0.06: Now read wheel rev as well as cadence sensor Improve connection code +0.07: Make Bangle.js 2 compatible diff --git a/apps/cscsensor/README.md b/apps/cscsensor/README.md index 9740fd9cf..3828e8e3e 100644 --- a/apps/cscsensor/README.md +++ b/apps/cscsensor/README.md @@ -11,9 +11,9 @@ Currently the app displays the following data: - total distance traveled - an icon with the battery status of the remote sensor -Button 1 resets all measurements except total distance traveled. The latter gets preserved by being written to storage every 0.1 miles and upon exiting the app. -If the watch app has not received an update from the sensor for at least 10 seconds, pushing button 3 will attempt to reconnect to the sensor. -Button 2 switches between the display for cycling speed and cadence. +Button 1 (swipe up on Bangle.js 2) resets all measurements except total distance traveled. The latter gets preserved by being written to storage every 0.1 miles and upon exiting the app. +If the watch app has not received an update from the sensor for at least 10 seconds, pushing button 3 (swipe down on Bangle.js 2) will attempt to reconnect to the sensor. +Button 2 (tap on Bangle.js 2) switches between the display for cycling speed and cadence. Values displayed are imperial or metric (depending on locale), cadence is in RPM, the wheel circumference can be adjusted in the global settings app. diff --git a/apps/cscsensor/metadata.json b/apps/cscsensor/metadata.json index af338c59e..4006789ef 100644 --- a/apps/cscsensor/metadata.json +++ b/apps/cscsensor/metadata.json @@ -2,11 +2,11 @@ "id": "cscsensor", "name": "Cycling speed sensor", "shortName": "CSCSensor", - "version": "0.06", + "version": "0.07", "description": "Read BLE enabled cycling speed and cadence sensor and display readings on watch", "icon": "icons8-cycling-48.png", "tags": "outdoors,exercise,ble,bluetooth", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS", "BANGLEJS2"], "readme": "README.md", "storage": [ {"name":"cscsensor.app.js","url":"cscsensor.app.js"}, diff --git a/apps/custom/custom.html b/apps/custom/custom.html index 684f813ae..307f2fd2f 100644 --- a/apps/custom/custom.html +++ b/apps/custom/custom.html @@ -16,12 +16,12 @@

Type your javascript code here

-

Then click

+

Then click  

diff --git a/apps/daisy/ChangeLog b/apps/daisy/ChangeLog new file mode 100644 index 000000000..26396b75c --- /dev/null +++ b/apps/daisy/ChangeLog @@ -0,0 +1,5 @@ +0.01: first release +0.02: added settings menu to change color +0.03: fix metadata.json to allow setting as clock +0.04: added heart rate which is switched on when cycled to it through up/down touch on rhs +0.05: changed text to uppercase, just looks better, removed colons on text diff --git a/apps/daisy/README.md b/apps/daisy/README.md new file mode 100644 index 000000000..12a55ddfd --- /dev/null +++ b/apps/daisy/README.md @@ -0,0 +1,32 @@ +# Daisy ![](app.png) + + *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* + +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/) + +* Derived from [The Ring](https://banglejs.com/apps/?id=thering) proof of concept and the [Pastel clock](https://banglejs.com/apps/?q=pastel) +* Includes the [Lazybones](https://banglejs.com/apps/?q=lazybones) Idle warning timer +* Touch the top right/top left to cycle through the info display (Day, Date, Steps, Sunrise, Sunset, Heart Rate) +* The heart rate monitor is turned on only when Heart rate is selected and will take a few seconds to settle +* The heart value is displayed in RED if the confidence value is less than 50% +* NOTE: The heart rate monitor of Bangle JS 2 is not very accurate when moving about. +See [#1248](https://github.com/espruino/BangleApps/issues/1248) +* Uses mylocation.json from MyLocation app to calculate sunrise and sunset times for your location +* If your Sunrise, Sunset times look odd make sure you have setup your location using +[MyLocation](https://banglejs.com/apps/?id=mylocation) +* The screen is updated every minute to save battery power +* Uses the [BloggerSansLight](https://www.1001fonts.com/rounded-fonts.html?page=3) font, which if free for commercial use + +## Future Development +* Use mini icons in the information line rather that text +* Add weather icons as per Pastel clock +* Add a lock icon to the screen + +## Screenshots +![](screenshot_daisy1.png) + +It is worth looking at the real thing though as the screenshot does not do it justice. diff --git a/apps/daisy/app-icon.js b/apps/daisy/app-icon.js new file mode 100644 index 000000000..b577b3d5e --- /dev/null +++ b/apps/daisy/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4UA///pf+sdR0n8CAkCwAcJhNgBI8KwALBgWgCo8NqAZHhNYktYloLKkoLHqwLByoLEgXoBYsrK4UDq0CqulrVVwGV4AXCquCBYYEBC4UC6tiBYeJq57DytayttvNW0tWHgclq2VtNpAYNYKQgiBBYIkBKgUK9Q8B9Nq1Nrqug1WgCoOqytq1/61NW1XVsALBq2ttbMB9N62olBKQNVtNvBYP61dVKgWlqtY34LB/wGBvQ8CEgIKBAAOVq7NDGwILD2/qBQWqAAILDAwUAlIzB1YLD9X1q+oytVqtbBYflA4NeBZVWlQjJ9A7LKZhrLQZS9Bqvq16bGWZXgZY2JZYcK1TjC9WrcYOAcYL7FpL7EAAMlq219NrRYNYBYeVrWV9t7q2lqwKCFwNi6utvVXxLuCBYWCGAIuBAgILCgdegVXBYPVwG1C4eohNWktYyvglYLCKgVeBYO1KQgLCrElvElBY94loBBBY/ghtghILGhSsBsECRgQZHBI5XCJ4kAA=")) diff --git a/apps/daisy/app.js b/apps/daisy/app.js new file mode 100644 index 000000000..cf0287616 --- /dev/null +++ b/apps/daisy/app.js @@ -0,0 +1,547 @@ +var SunCalc = require("https://raw.githubusercontent.com/mourner/suncalc/master/suncalc.js"); +const storage = require('Storage'); +const locale = require("locale"); +const SETTINGS_FILE = "daisy.json"; +const LOCATION_FILE = "mylocation.json"; +const h = g.getHeight(); +const w = g.getWidth(); +let settings; +let location; + +// variable for controlling idle alert +let lastStep = getTime(); +let warned = 0; +let idle = false; +let IDLE_MINUTES = 26; + +let pal1; // palette for 0-40% +let pal2; // palette for 50-100% +const infoLine = (3*h/4) - 6; +const infoWidth = 56; +const infoHeight = 11; +var drawingSteps = false; + +function log_debug(o) { + //print(o); +} + +var hrmImg = require("heatshrink").decompress(atob("i0WgIKHgPh8Ef5/g///44CBz///1///5A4PnBQk///wA4PBA4MDA4MH/+Ah/8gEP4EAjw0GA")); + +// https://www.1001fonts.com/rounded-fonts.html?page=3 +Graphics.prototype.setFontBloggerSansLight46 = function(scale) { + // Actual height 46 (45 - 0) + this.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAA/AAAAAAAAPwAAAAAAAD4AAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAH/gAAAAAAP/wAAAAAAf/gAAAAAAf/AAAAAAA//AAAAAAB/+AAAAAAD/8AAAAAAH/4AAAAAAH/wAAAAAAP/gAAAAAAf/gAAAAAA//AAAAAAB/+AAAAAAA/8AAAAAAAP4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///8AAAAP////4AAAP/////AAAH/////4AAD+AAAB/AAA8AAAAHwAAeAAAAA+AAHgAAAAHgADwAAAAB4AA8AAAAAPAAPAAAAADwADwAAAAA8AA8AAAAAPAAPAAAAADwAB4AAAAB4AAeAAAAAeAAHwAAAAPgAA/AAAAPwAAH/////4AAA/////8AAAH////+AAAAf///+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAAAAAPAAAAAAAAHwAAAAAAAB4AAAAAAAA+AAAAAAAAfAAAAAAAAHgAAAAAAAD4AAAAAAAB8AAAAAAAAeAAAAAAAAPgAAAAAAADwAAAAAAAB//////4AAf//////AAH//////gAA//////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAD4AAHAAAAD+AAD4AAAB/gAA8AAAB/4AAfAAAA/+AAHgAAAf3gAB4AAAPx4AA8AAAH4eAAPAAAD4HgADwAAB8B4AA8AAA+AeAAPAAAfAHgADwAAPgB4AA8AAHwAeAAHgAD4AHgAB4AD8AB4AAfAB+AAeAAD8B/AAHgAAf//gAB4AAH//wAAeAAAf/wAAHgAAB/wAAA4AAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4AADgAAAAPAAB4AAAADwAAeAAAAA+AAHgAAAAHgAB4ABgAB4AAeAA8AAeAAHgA/AADwAB4AfwAA8AAeAP8AAPAAHgH/AADwAB4H7wAA8AAeD48AAPAAHh8PAAHgAB5+BwAB4AAe/AeAA+AAH/AHwAfAAB/gA/AfgAAfwAH//wAAHwAA//4AAA4AAH/8AAAAAAAf4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAAAAAD+AAAAAAAD/gAAAAAAH/4AAAAAAH/+AAAAAAP/ngAAAAAP/h4AAAAAf/AeAAAAAf/AHgAAAA/+AB4AAAA/+AAeAAAB/8AAHgAAA/8AAB4AAAP4AAAeAAAB4AAAHgAAAAAAAB4AAAAAAAAeAAAAAAP///4AAAAH////AAAAA////gAAAAP///4AAAAAAB4AAAAAAAAeAAAAAAAAHgAAAAAAABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAD4AA8AAD///gAPAAB///4AD4AAf//+AAeAAH+APAAHgAB4AHgAA4AAeAB4AAOAAHgAcAADwAB4AHAAA8AAeADwAAPAAHgAcAADwAB4AHAAA8AAeAB4AAeAAHgAeAAHgAB4AHwAD4AAeAA+AB8AAHgAP4B+AAB4AB///gAAOAAP//gAABAAA//wAAAAAAD/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/gAAAAAB///4AAAAD////wAAAD////+AAAB/////4AAA/gPgB/AAAfgDwAHwAAPgA8AA+AADwAeAAHgAB4AHgAB4AAeAB4AAfAAHgAeAADwABwAHgAA8AAcAB4AAPAAHAAeAAHwAB4AHgAB4AAeAB8AAeAAHgAPAAPgAB4AD8APwAAOAAfwP4AADgAD//8AAAAAAf/+AAAAAAB/+AAAAAAAH8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAB4AAAAAAAAeAAAAAAAAHgAAAAAAAB4AAAAA4AAeAAAAB/AAHgAAAB/wAB4AAAB/4AAeAAAD/4AAHgAAD/wAAB4AAH/wAAAeAAH/gAAAHgAP/gAAAB4AP/AAAAAeAf/AAAAAHgf+AAAAAB4/+AAAAAAe/8AAAAAAH/8AAAAAAB/4AAAAAAAf4AAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/gAAAA/AB/+AAAA/8B//wAAA//gf/+AAAf/8PgPgAAH4fngB8AAD4B/wAPgAA8AP8AB4AAeAB+AAeAAHgAfgADwAB4ADwAA8AAcAA8AAPAAHAAPAADwAB4ADwAA8AAeAB+AAPAAHgAfgAHgAB8AP8AB4AAPgH/AA+AAD8H54AfAAAf/8fgPwAAD/+D//4AAAf/Af/8AAAB/AD/+AAAAAAAP+AAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAAAAf/wAAAAAAf/+AAAAAAP//4AAwAAH//+AAeAAD+APwAHgAA+AA+AB4AAfAAHgAOAAHgAB4ADwAB4AAPAA8AAeAADwAPAAHgAA8ADwAB4AAPAA8AAeAADwAPAAHgAA8AHgAB8AAeAB4AAPgAHgA+AAD8ADwA/AAAfwA8A/gAAD/wef/wAAAf////4AAAB////4AAAAH///wAAAAAD/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AB4AAAAAfgA/AAAAAH4APwAAAAB+AD4AAAAAPAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="), 46, atob("DRAcHBwcHBwcHBwcDQ=="), 56+(scale<<8)+(1<<16)); + return this; +}; + +Graphics.prototype.setFontRoboto20 = function(scale) { + // Actual height 21 (20 - 0) + this.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAH/zA/+YAAAAAAAHwAAwAAHwAA+AAAAAAAAAAAQACDAAYbADP4B/8A/zAGYZADH4A/+A/7AHYYADCAAAAAAAQAeHgH4eBzgwMMHnhw88GGBw4wHj+AcPgAAAAAAAAAAB4AA/gAGMAAwhwGMcAfuABzgABzgAc+AOMYBhBAAMYAB/AAHwAAAAAHwD5+A/8YGPDAw8YGPzA/HYD4fAADwAB/AAOYAABAAAAHwAA4AAAAAAAAAAH/gD//B8A+cAA7AADAAAAAAAYAAbwAHHgHwf/4A/8AAAAEAABiAAGwAA8AA/AAH+AAGwAByAAEAAAAAAAMAABgAAMAABgAH/wA/+AAMAABgAAMAABgAAAAAAAIAAfAADwAAAABgAAMAABgAAMAABgAAAAAAAAAAAAADAAAYAAAAAAAAADgAB8AB+AA+AA+AA/AAHAAAgAAAAAAB8AB/8Af/wHAHAwAYGADAwAYHAHAf/wB/8AAAAAAAAAAABgAAcAADAAAYAAH//A//4AAAAAAAAAAAAAAAAAAAAABwDAeA4HAPAwHYGBzAwcYHHDAfwYB8DAAAYAAAAAAABgOAcBwHADAwwYGGDAwwYHPHAf/wB58AAAAAAAAADAAB4AAfAAPYAHjAB4YA8DAH//A//4AAYAADAAAAAAAAAEMA/xwH+HAxgYGMDAxgYGODAw/4GD+AAHAAAAAAAAAf8AP/wD2HA5wYGMDAxgYGOHAA/wAD8AAAAAAAAAAAGAAAwAAGADAwB4GB+Aw+AGfAA/gAHwAAwAAAAAAADAB5+Af/wHPDAwwYGGDAwwYHPHAfvwB58AAAAAAAAAAAB+AAf4AHDjAwMYGBjAwM4HDOAf/gB/4AAAAAAAAAAAAYDADAYAAAAAAAAAAYDAfAYHwAAAABAAAcAADgAA+AAGwAB3AAMYABjgAYMAAAAAAAAAAAAAAABmAAMwABmAAMwABmAAMwABmAAMwAAiAAAAAAAAAYMADjgAMYAB3AAGwAA2AADgAAcAABAAAAAAAAAMAADgAA4AAGBzAweYGHAA/wAD8AAEAAAAwAB/4A/PwOAGDgAYYPxmH/Mw4ZmMDMxgZmM+Mx/5mHDAYAIDgDAPBwAf8AAMAAAAAAAYAAfAAPwAP4AH+AH4wA8GAH4wAP2AAPwAAfwAAfAAAYAAAAAAAAAAA//4H//AwwYGGDAwwYGGDAwwYH/HAf/wB58AAAAADAAH/AD/+AcBwHADAwAYGADAwAYGADA4A4DweAODgAAAAAAAAAAAAAAH//A//4GADAwAYGADAwAYGADAYAwD4+AP/gAfwAAAAAAAAAAAH//A//4GDDAwYYGDDAwYYGDDAwYYGCDAgAYAAAAAAAH//A//4GDAAwYAGDAAwYAGDAAwYAGAAAAAAAAAAH/AD/8AcBwHAHAwAYGADAwYYGDDA4YYDz/AOfwAAAAAAAAAAA//4H//A//4ADAAAYAADAAAYAADAAAYAADAA//4H//AAAAAAAAAAAAAAA//4H//AAAAAAAAABAAAeAAB4AADAAAYAADAAAYAAHA//wH/8AAAAAAAAAAAAAAA//4H//AAcAAPAAD4AA/wAOPADg8A4B4GAHAgAYAAAAAAAH//A//4AADAAAYAADAAAYAADAAAYAADAAAAAAAA//4H//A+AAB+AAD8AAD8AAH4AAPAAH4AH4AD8AD8AA+AAH//A//4AAAAAAAH//A//4H//AeAAB8AADwAAPgAAeAAA8AADwH//A//4AAAAAAAAAAAH/AB/8AeDwHAHAwAYGADAwAYGADA4A4DweAP/gA/4AAAAAAAAAAAH//A//4GBgAwMAGBgAwMAGBgAwcAH/AAfwAA8AAAAAA/4AP/gDgOA4A4GADAwAYGADAwAYHAHgeD+B/8wD+GAAAAAAAAAAA//4H//AwYAGDAAwYAGDgAweAHH8Afz4B8HAAAIAAYAPDwD8OA5w4GGDAwwYGHDAwYYHDnAePwBw8AAAAGAAAwAAGAAAwAAGAAA//4H//AwAAGAAAwAAGAAAwAAAAAAAAAH/4A//wAAPAAAYAADAAAYAADAAAYAAPA//wH/8AAAAAAAAgAAHAAA/AAB/AAD+AAD+AAD4AAfAAfwAfwAfwAH4AA4AAEAAA+AAH/AAH/gAD/AAD4AD+AH+AH8AA+AAH+AAD+AAD/AAD4AH/AP/AH+AA8AAAAAAAAAGADA4A4HweAPPgA/wAB8AAfwAPvgDweA8B4GADAAAIGAAA4AAHwAAPgAAfAAA/4AH/AD4AB8AA+AAHgAAwAAAAAAAAAGADAwB4GAfAwPYGDzAx4YGeDA/AYHwDA4AYGADAAAAAAAA///3//+wAA2AAGAAAGAAA+AAD8AAD8AAD4AAH4AAHgAAMAAAAwAA2AAG///3//+AAAAAAAAAAAOAAHwAD4AA8AAD8AADwAAGAAAAAAABgAAMAABgAAMAABgAAMAABgAAMAABgAAAEAAAwAADAAAIAAAAAAAAAAEeABn4Ad3ADMYAZjADMYAZmAB/4AP/AAAAAAAA//4H//ABgwAYDADAYAYDADg4AP+AA/gABwAAAAAAAAA/gAP+ADg4AYDADAYAYDADAYAOOABxwAAAAAEAAH8AB/wAcHADAYAYDADAYAcDA//4H//AAAAAAAAAAAAH8AB/wAdnADMYAZjADMYAZjAB84AHmAAMAAMAABgAB//gf/8HMAAxgAGIAAAAAAH8IB/zAcHMDAZgYDMDAZgcHcD//Af/wAAAAAAAAAAH//A//4AMAADAAAYAADAAAcAAD/4AP/AAAAAAAAAAAGf/Az/4AAAAAAAAAAMz//mf/4AAAAAAAAAAH//A//4ABwAAeAAH4ABzwAcPACAYAABAAAAAAAA//4H//AAAAAAAAAAAAf/AD/4AMAADAAAYAADAAAcAAD/4AP/ABgAAYAADAAAYAADgAAP/AA/4AAAAAAAAf/AD/4AMAADAAAYAADAAAcAAD/4AP/AAAAAAAAAAAAH8AB/wAcHADAYAYDADAYAYDADx4AP+AA/gAAAAAAAAf/8D//gYDADAYAYDADAYAcHAB/wAH8AAEAAAAAAEAAH8AB/wAcHADAYAYDADAYAYDAD//gf/8AAAAAAAAAAAf/AD/4AcAADAAAYAACAAAAEAB5wAfnADMYAZjADGYAYzADn4AOeAAAAAAAADAAAYAAf/wD//ADAYAYDAAAAAAAAD/gAf/AAA4AADAAAYAADAAAwAf/AD/4AAAAAAAAYAAD4AAP4AAP4AAPAAH4AH4AD8AAcAAAAAAQAADwAAf4AAf4AAPAAP4AP4ADwAAfgAA/gAA/AAD4AH+AD+AAeAAAAAAAAACAYAcHADzwAH8AAfAAH8ADx4AcHACAIAcAMD4BgP4MAP/AAPwAP4AP4AD4AAcAAAAAAAAADAYAYHADD4AY7ADOYAfjADwYAcDADAYAAAAADAAA4AH//B/v8cABzAACAAAH//w//+AAAAAAACAACcAAx/n+H//AA4AAHAAAAAAAAAAAAAOAADgAAYAADAAAcAABgAAGAAAwAAGAADwAAcAAAAA"), 32, atob("BQUHDQwPDQQHBwkMBAYGCQwMDAwMDAwMDAwFBAsMCwoTDg0ODgwMDg8GDA0LEg8ODQ4NDA0ODRMNDQ0GCQYJCQYLDAsMCwcMDAUFCwUSDAwMDAcLBwwKEAoKCgcFBw4A"), 21+(scale<<8)+(1<<16)); + return this; +}; + +function assignPalettes() { + // palette for 0-40% + pal1 = new Uint16Array([g.theme.bg, g.toColor(settings.gy), g.toColor(settings.fg), g.toColor("#00f")]); + // palette for 50-100% + pal2 = new Uint16Array([g.theme.bg, g.toColor(settings.fg), g.toColor(settings.gy), g.toColor("#00f")]); +} + +function setSmallFont20() { + g.setFontRoboto20(); +} + +function setLargeFont() { + g.setFontBloggerSansLight46(1); +} + +function setSmallFont() { + g.setFont('Vector', 16); +} + +function getSteps() { + try { + return Bangle.getHealthStatus("day").steps; + } catch (e) { + if (WIDGETS.wpedom !== undefined) + return WIDGETS.wpedom.getSteps(); + else + return 0; + } +} + +/////////////// sunrise / sunset ///////////////////////////// + +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; + assignPalettes(); +} + +// requires the myLocation app +function loadLocation() { + location = require("Storage").readJSON(LOCATION_FILE,1)||{}; + location.lat = location.lat||51.5072; + location.lon = location.lon||0.1276; + location.location = location.location||"London"; +} + +function extractTime(d){ + var h = d.getHours(), m = d.getMinutes(); + return(("0"+h).substr(-2) + ":" + ("0"+m).substr(-2)); +} + +var sunRise = "00:00"; +var sunSet = "00:00"; +var drawCount = 0; + +function updateSunRiseSunSet(now, lat, lon, line){ + // get today's sunlight times for lat/lon + var times = SunCalc.getTimes(new Date(), lat, lon); + + // format sunrise time from the Date object + sunRise = extractTime(times.sunrise); + sunSet = extractTime(times.sunset); +} + +const infoData = { + ID_DATE: { calc: () => {var d = (new Date()).toString().split(" "); return d[2] + ' ' + d[1] + ' ' + d[3];} }, + ID_DAY: { calc: () => {var d = require("locale").dow(new Date()).toLowerCase(); return d[0].toUpperCase() + d.substring(1);} }, + ID_SR: { calc: () => 'Sunrise ' + sunRise }, + ID_SS: { calc: () => 'Sunset ' + sunSet }, + ID_STEP: { calc: () => 'Steps ' + getSteps() }, + ID_BATT: { calc: () => 'Battery ' + E.getBattery() + '%' }, + ID_HRM: { calc: () => hrmCurrent } +}; + +const infoList = Object.keys(infoData).sort(); +let infoMode = infoList[0]; + +function nextInfo() { + let idx = infoList.indexOf(infoMode); + if (idx > -1) { + if (idx === infoList.length - 1) infoMode = infoList[0]; + else infoMode = infoList[idx + 1]; + } + // power HRM on/off accordingly + Bangle.setHRMPower(infoMode == "ID_HRM" ? 1 : 0); + resetHrm(); +} + +function prevInfo() { + let idx = infoList.indexOf(infoMode); + if (idx > -1) { + if (idx === 0) infoMode = infoList[infoList.length - 1]; + else infoMode = infoList[idx - 1]; + } + // power HRM on/off accordingly + Bangle.setHRMPower(infoMode == "ID_HRM" ? 1 : 0); + resetHrm(); +} + +function clearInfo() { + g.setColor(g.theme.bg); + //g.setColor(g.theme.fg); + g.fillRect((w/2) - infoWidth, infoLine - infoHeight, (w/2) + infoWidth, infoLine + infoHeight); +} + +function drawInfo() { + clearInfo(); + g.setColor(g.theme.fg); + setSmallFont(); + g.setFontAlign(0,0); + + if (infoMode == "ID_HRM") { + clearInfo(); + g.setColor('#f00'); // red + drawHeartIcon(); + } else { + g.drawString((infoData[infoMode].calc().toUpperCase()), w/2, infoLine); + } +} + +function drawHeartIcon() { + g.drawImage(hrmImg, (w/2) - infoHeight - 20, infoLine - infoHeight); +} + +function drawHrm() { + if (idle) return; // dont draw while prompting + var d = new Date(); + clearInfo(); + g.setColor(d.getSeconds()&1 ? '#f00' : g.theme.bg); + drawHeartIcon(); + setSmallFont(); + g.setFontAlign(-1,0); // left + g.setColor(hrmConfidence >= 50 ? g.theme.fg : '#f00'); + g.drawString(hrmCurrent, (w/2) + 10, infoLine); +} + +function draw() { + if (!idle) + drawClock(); + else + drawIdle(); + queueDraw(); +} + +function drawClock() { + var date = new Date(); + var timeStr = require("locale").time(date,1); + var da = date.toString().split(" "); + var time = da[4].substr(0,5); + var hh = da[4].substr(0,2); + var mm = da[4].substr(3,2); + var steps = getSteps(); + var p_steps = Math.round(100*(steps/10000)); + + g.reset(); + g.setColor(g.theme.bg); + g.fillRect(0, 0, w, h); + g.drawImage(getGaugeImage(p_steps), 0, 0); + setLargeFont(); + + g.setColor(settings.fg); + g.setFontAlign(1,0); // right aligned + g.drawString(hh, (w/2) - 1, h/2); + + g.setColor(g.theme.fg); + g.setFontAlign(-1,0); // left aligned + g.drawString(mm, (w/2) + 1, h/2); + + drawInfo(); + + // recalc sunrise / sunset every hour + if (drawCount % 60 == 0) + updateSunRiseSunSet(new Date(), location.lat, location.lon); + drawCount++; +} + +function drawSteps() { + if (drawingSteps) return; + drawingSteps = true; + clearInfo(); + setSmallFont(); + g.setFontAlign(0,0); + g.setColor(g.theme.fg); + g.drawString('Steps ' + getSteps(), w/2, (3*h/4) - 4); + drawingSteps = false; +} + +///////////////// GAUGE images ///////////////////////////////////// + +var hrmCurrent = "--"; +var hrmConfidence = 0; + +function resetHrm() { + hrmCurrent = "--"; + hrmConfidence = 0; + if (infoMode == "ID_HRM") { + clearInfo(); + g.setColor('#f00'); // red + drawHeartIcon(); + } +} + +Bangle.on('HRM', function(hrm) { + hrmCurrent = hrm.bpm; + hrmConfidence = hrm.confidence; + log_debug("HRM=" + hrm.bpm + " (" + hrm.confidence + ")"); + if (infoMode == "ID_HRM" ) drawHrm(); +}); + + +///////////////// GAUGE images ///////////////////////////////////// + + +// putting into 1 function like this, rather than individual variables +// reduces ram usage from 70%-13% +function getGaugeImage(p) { + // p0 + if (p < 2) return { + width : 176, height : 176, bpp : 2, + transparent : -1, + palette : pal1, + buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVAAVUFUgpDAAdAFMEBFQ4ABqBVnLMQqLLLzWEABLgbVgohEGopYaiofDBihWVHJpYYDgYPbKx1ACJhYZIwT4OcAZWYHyRYUIgQXQH4RqOThCXUYRpCHNyQVVQQTwVQiSZWIQSEQNgSYSIYiEQQSyEUCQLDSOAyCnQiSCYQiSCYQiSCZDaDARObKuBSZwcaVzR0QFYKuZWAYNZWCJJKMoKuaWAahKBhiwTJRSudURorBFTgfMVzqjDO5DaeZ5jaeJhhiKbi4rIbT4hLqoriPI7afUpS5BbTwiKFdZgIADSmHFYIqgbgIrGcgIriEYwzHADZ7HRY4rdaYrjHADcBFYoGBFcgkEGQwAeFYqKHFbzUEcQ4AdiorwiorlEogxFAD59FWoorhoArDqArjgIr/FbYwFAEJSDFf4rXgornqgrDFUkAior/Ff4rGAYYAjKYYr/Ff4r/FbdVFdFAFYNQFcsBFf4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/FbdUFcsFFYUVFdADBFf4r/Ff4rbAYYAjKYYr/Ff4rFoArkqorCgIrnqAr/FbIEFAEBSFFf4rYqgrjgorEiormAocVAogAfEooxFFcB9EFdq1DAD9VFYkBFctQFYoGEADokHFcp8FRQoAdag7iFFb4HFioHGADYjHGY4rcPYyLHADbTHcYNQFT4iIFdZgIADKmJqrcgiorIBIIrhMKIAXUpIrBbjzaBFZAKKbS5MJFcKkJbj4fLBYLcdqorKbjzPMbjxKNMhauTURawdJJorBWDShBFZiRBWDQcOHRyuPOhorBWDIbPWDRzQSYKEYIwLLOHgSEXDIJyPQjD2SQjCCQQjSCRCYY/QN4xDRQiyCSQgjdSCqqECLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5A")) + }; + + // p2 + if (p >= 2 && p < 4) return { + width : 176, height : 176, bpp : 2, + transparent : -1, + palette: pal1, + buffer : require("heatshrink").decompress(atob("AH4A/ADNUFE8FqtVq2q1AqkFIIrDAAOAFMEBFQYrE1WgKsYrGLL4qFFY2pqDWeFZdUVkAhCAQMKFYdVLDUVFQYMHlWq0oMJKyoOJlQrCLDBWDB5clB5xWOoARMCARYWKwT4OgpYXKwY+SLChECC6A/CNRycIS6jCNIQ5uSCqqCCeCqESTKxCCQiBsCTCRDEQiCCWQigSBYaRwGQU6ESQTCESQTCESQTIbQYCJzZVwKTODjSuaOiArBVzKwDBrKwRJJRlBVzSwDUJQMMWCZKKVzqiNFYIqcD5iudUYZ3IbTzPMbTxMMMRTcXFZDafEJdVFcR5HbT6lKXILaeERQrrMBAAaUw4rBFUDcBFYzkBFcQjGGY4AbPY6LHFbrTFcY4AbgIrFAwIrkEggyGADwrFRQ4reagjiHADsVFeEVFcolEGIoAfPoq1FFcNAFYdQFccBFf4rbGAoAhKQYr/Fa8FFc9UFYYqkgEVFf4r/FYwDDAEZTDFf4r/Ff4rbqorooArBqArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlgorCioroAYIr/Ff4r/FbYDDAEZTDFf4r/FYtAFclVFYUBFc9QFf4rZAgoAgKQor/FbFUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHI")) + }; + + // p4 + if (p >= 4 && p < 7) return { + width : 176, height : 176, bpp : 2, + transparent : -1, + palette : pal1, + buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFY2loAqjFY1VqDWeFZdUVkAhEhQrDLDcVFQYMHlQrCBhBWVHJpYYDgYPbKx1ACJhYZIwT4OgpYXKwY+SLChECC6A/CNRycIS6jCNIQ5uSCqqCCeCqESTKxCCQiBsCTCRDEQiCCWQigSBYaRwGQU6ESQTCESQTCESQTIbQYCJzZVwKTODjSuaOiArBVzKwDBrKwRJJRlBVzSwDUJQMMWCZKKVzqiNFYIqcD5iudUYZ3IbTzPMbTxMMMRTcXFZDafEJdVFcR5HbT6lKXILaeERQrrMBAAaUw4rBFUDcBFYzkBFcQjGGY4AbPY6LHFbrTFcY4AbgIrFAwIrkEggyGADwrFRQ4reagjiHADsVFeEVFcolEGIoAfPoq1FFcNAFYdQFccBFf4rbGAoAhKQYr/Fa8FFc9UFYYqkgEVFf4r/FYwDDAEZTDFf4r/Ff4rbqorooArBqArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlgorCioroAYIr/Ff4r/FbYDDAEZTDFf4r/FYtAFclVFYUBFc9QFf4rZAgoAgKQor/FbFUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHI")) + }; + + // p7 + if (p >= 7 && p < 10) return { + width : 176, height : 176, bpp : 2, + transparent : -1, + palette : pal1, + buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFYxZfFQorGLLrWCFZbgbVgtUBQcKLD8VFQYMHlQsDKzoOJFgZYYKwYPLFgWlKzVACJgrCqBWYawgAJcAOlNBhWMCZ8qFYJYUgoqBC6ECFYJqOAApWSS4jCNQQ5uSCqqCCeCqESFQKZUIQSEQNgSYSIYiEQQSyEUCQLDSOAyCnQiSCYQiSCYQiSCZDaDARObKuBSZwcaVzR0QFYKuZWAYNZWCJJKMoKuaWAahKBhiwTJRSudURorBFTgfMVzqjDO5DaeZ5jaeJhhiKbi4rIbT4hLqoriPI7afUpS5BbTwiKFdZgIADSmHFYIqgbgIrGcgIriEYwzHADZ7HRY4rdaYrjHADcBFYoGBFcgkEGQwAeFYqKHFbzUEcQ4AdiorwiorlEogxFAD59FWoorhoArDqArjgIr/FbYwFAEJSDFf4rXgornqgrDFUkAior/Ff4rGAYYAjKYYr/Ff4r/FbdVFdFAFYNQFcsBFf4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/FbdUFcsFFYUVFdADBFf4r/Ff4rbAYYAjKYYr/Ff4rFoArkqorCgIrnqAr/FbIEFAEBSFFf4rYqgrjgorEiormAocVAogAfEooxFFcB9EFdq1DAD9VFYkBFctQFYoGEADokHFcp8FRQoAdag7iFFb4HFioHGADYjHGY4rcPYyLHADbTHcYNQFT4iIFdZgIADKmJqrcgiorIBIIrhMKIAXUpIrBbjzaBFZAKKbS5MJFcKkJbj4fLBYLcdqorKbjzPMbjxKNMhauTURawdJJorBWDShBFZiRBWDQcOHRyuPOhorBWDIbPWDRzQSYKEYIwLLOHgSEXDIJyPQjD2SQjCCQQjSCRCYY/QN4xDRQiyCSQgjdSCqqECLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5A==")) + }; + + // p10 + if (p >= 10 && p < 20) return { + width : 176, height : 176, bpp : 2, + transparent : -1, + palette : pal1, + buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFYxZfFQorGLLrWCFZbgbVgtUBQcKLD8VFQYMHlQsDKzoOJFgZYYKwYPLFgZWaoARMLDJWCawgAJcAZWYCZ6FCLCkFFQNQCZ8CFYOoFaZWSLAmAQShWQLAiESQQRtTLAOkQSdUFacK1WloCCSCaAAEFYKaQQSyEC0pvQirZTbomlIh6CYZAZFOQTBxDQhyCYOQhoPQS4bQHaBzaVwKTODjSuaOiArBVzKwDBrKwRJJRlBVzSwDUJQMMWCZKKVzqiNFYIqcD5iudUYZ3IbTzPMbTxMMMRTcXFZDafEJdVFcR5HbT6lKXILaeERQrrMBAAaUw4rBFUDcBFYzkBFcQjGGY4AbPY6LHFbrTFcY4AbgIrFAwIrkEggyGADwrFRQ4reagjiHADsVFeEVFcolEGIoAfPoq1FFcNAFYdQFccBFf4rbGAoAhKQYr/Fa8FFc9UFYYqkgEVFf4r/FYwDDAEZTDFf4r/Ff4rbqorooArBqArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlgorCioroAYIr/Ff4r/FbYDDAEZTDFf4r/FYtAFclVFYUBFc9QFf4rZAgoAgKQor/FbFUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHI")) + }; + + // p20 + if (p >= 20 && p < 30) return { + width : 176, height : 176, bpp : 2, + transparent : -1, + palette : pal1, + buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFYxZfFQorGLLrWCFZbgbVgtUBQcKLD8VFQYMHlQsDKzoOJFgZYYKwYPLFgZWaoARMLDJWCawgAJcAZWYCZ6FCLCkFFQNQCZ8CFYOoFaZWSLAmAQShWQLAiESQQRtTLAKESFQNUFacKQiSCCoArTgCESQSyEUirZTboyCnQiSCYQiSCYQiSCZQgeAVxwqYQgSwMVwNUFbMKWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbhdVFcTcHbT7cDFY0BbT7cD0ArxgtVoArfgGq1ArHFUDcBFY0VFceqFY1UFcMKFY1VFcmAFYtQFcMCFYsBFcugFYtAFcMAFYsFFcuoFYoqigEqFeEVFcuqFYlUFccKFYlVFc2AFYdQFccCFf4AWgNVoAEGAERSDFf4rXgornqgrDFUkAior/Ff4rGAYYAjKYYr/Ff4r/FbdVFdFAFYNQFcsBFf4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/FbdUFcsFFYUVFdADBFf4r/Ff4rbAYYAjKYYr/Ff4rFoArkqorCgIrnqAr/FbIEFAEBSFFf4rYqgrjgorEiormAocVAogAfEooxFFcB9EFdq1DAD9VFYkBFctQFYoGEADokHFcp8FRQoAdag7iFFb4HFioHGADYjHGY4rcPYyLHADbTHcYNQFT4iIFdZgIADKmJqrcgiorIBIIrhMKIAXUpIrBbjzaBFZAKKbS5MJFcKkJbj4fLBYLcdqorKbjzPMbjxKNMhauTURawdJJorBWDShBFZiRBWDQcOHRyuPOhorBWDIbPWDRzQSYKEYIwLLOHgSEXDIJyPQjD2SQjCCQQjSCRCYY/QN4xDRQiyCSQgjdSCqqECLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5A=")) + }; + + // p30 + if (p >= 30 && p < 40) return { + width : 176, height : 176, bpp : 2, + transparent : -1, + palette : pal1, + buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFYxZfFQorGLLrWCFZbgbVgtUBQcKLD8VFQYMHlQsDKzoOJFgZYYKwYPLFgZWaoARMLDJWCawgAJcAZWYCZ6FCLCkFFQNQCZ8CFYOoFaZWSLAmAQShWQLAiESQQRtTLAKESFQNUFacKQiSCCoArTgCESQSyEUirZTboyCnQiSCYQiSCYQiSCZQgeAVxwqYQgSwMVwNUFbMKWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbhdVFcTcHbT7cDFY0BbT7cD0ArxgtVoArfgGq1ArHFUDcBFY0VFceqFY1UFcMKFY1VFcmAFYtQFcMCFYsBFcugFYtAFcMAFYsFFcuoFYoqigEqFeEVFcuqFYlUFccKFYlVFc2AFYdQFccCFf4rbgNVoArjgGq0Ar/FbMFFc+oFYYqkgEqFf4r/FY0VqgrlhWqFf4r/Ff4rdqorowArBqArlgQr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlhQrCioroAYIr/Ff4r/FbcFqorllWoFf4r/FY9AFcmqFYUBFc+gFf4rZgFVqAqjgWqwAr/FbdUFccFawkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHI")) + }; + + // p40 + if (p >= 40 && p < 50) return { + width : 176, height : 176, bpp : 2, + transparent : -1, + palette : pal1, + buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFYxZfFQorGLLrWCFZbgbVgtUBQcKLD8VFQYMHlQsDKzoOJFgZYYKwYPLFgZWaoARMLDJWCawgAJcAZWYCZ6FCLCkFFQNQCZ8CFYOoFaZWSLAmAQShWQLAiESQQRtTLAKESFQNUFacKQiSCCoArTgCESQSyEUirZTboyCnQiSCYQiSCYQiSCZQgeAVxwqYQgSwMVwNUFbMKWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbhdVFcTcHbT7cDFY0BbT7cD0ArxgtVoArfgGq1ArHFUDcBFY0VFceqFY1UFcMKFY1VFcmAFYtQFcMCFYsBFcugFYtAFcMAFYsFFcuoFYoqigEqFeEVFcuqFYlUFccKFYlVFc2AFYdQFccCFf4rbgNVoArjgGq0Ar/FbMFFc+oFYYqkgEqFf4r/FY0VqgrlhWqFf4r/Ff4rdqorowArBqArlgQr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlhQrCioroAYIr/Ff4r/FbcFqorllWoFf4r/FY9AFcmqFYUBFc+gFf4rZgFVqAqjgWqwAr/FbdUFccKFYkVFcwFDitVFccqFYkFFcuoFeNAFcWqFYkBFcugFYtQFUMCFYsAFcuAFYtUFcMKFY0VFcgHFitVFcMqFY0FFceoFY9AFcGqFY0BqtQFT8C1WgFeMAqtUFb8K1WAFY7cglQrIioriBI8FqtAFb2q1ArJbjzaBFZEBbj7aB0ALIFcLaHbkLaJFYbcd1QrKbjzaKbkDaLbgSwcVwLaJWD6uLFYawaVwIrMbgKwaVwLaKbgawaVwLaLbgawZQQLaLWDiuOWAaEYQQKuMWAiEXKwKuNQjUBQR6EaiqCPQjVVQSATCqtUFSZvB1WACiSEUY4KCQQgjdSCqqECLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5A")) + }; + + // p50 + if (p >= 50 && p < 60) return { + width : 176, height : 176, bpp : 2, + transparent : -1, + palette : pal2, + buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESFQOoFacFQiSCCwArTgCESQSyEUlTZTboyCnQiSCYQiSCYQiSCZQgdAVxwqYQgSwMVwOoFbMFWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbheqFcTcHbT7cDFY0CbT7cDqArxhWqwArfgFVqgrHFUDcBFY0qFcdVFY2oFcMFFY2qFclAFYugFcMBFYsCFctQFYuAFcMAFYsKFctUFYoqigEVFeEqFctVFYmoFccFFYmqFc1AFYegFccBFf4rbgWqwArjgFVqAr/FbMKFc9UFYYqkgEVFf4r/FY0q1ArlgtVFf4r/Ff4rd1QrooArB0ArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rb1ArlgorClQroAYIr/Ff4r/FbcK1QrlitUFf4r/FY+AFclVFYUCFc9QFf4rZgGq0AqjgNVoAr/FbeoFccFFYkqFcwFDlWqFccVFYkKFctUFeOAFcVVFYkCFctQFYugFUMBFYsAFctAFYuoFcMFFY0qFcgHFlWqFcMVFY0KFcdUFY+AFcFVFY0C1WgFT8BqtQFeMA1WoFb8FqtAFY7cgiorIlQriBI8K1WAFb1VqgrJbjzaBFZECbj7aBqALIFcLaHbkLaJFYbcdqorKbjzaKbkDaLbgSwcVwLaJWD6uLFYawaVwIrMbgKwaVwLaKbgawaVwLaLbgawZQQLaLWDiuOWAaEYQQKuMWAiEXKwKuNQjSCQQjSCQQjSCRAAIrB1AqTgorBoAUQQiyCSQgjdSbISCRQgZYSKwKCSQghYQKwSCSQghYQKwSCTAAMqFYOoCJsFFQNVFShYEwARMFQRWVLAiFMQIRWWLAosKFQZWXLAosIFQZWYLAzgFawZWbAAMKFgmq1IoEAANUFTQABFZtAFbgsFFYwqeWQorFVjZZJFYhVfcAwrCazoA/AHI")) + }; + + // p60 + if (p >= 60 && p < 70) return { + width : 176, height : 176, bpp : 2, + transparent : -1, + palette : pal2, + buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESFQOoFacFQiSCCwArTgCESQSyEUlTZTboyCnQiSCYQiSCYQiSCZQgdAVxwqYQgSwMVwOoFbMFWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbheqFcTcHbT7cDFY0CbT7cDqArxhWqwArfgFVqgrHFUDcBFY0qFcdVFY2oFcMFFY2qFclAFYugFcMBFYsCFctQFYuAFcMAFYsKFctUFYoqigEVFeEqFctVFYmoFccFFYmqFc1AFYegFccBFf4rbgWqwArjgFVqAr/FbMKFc9UFYYqkgEVFf4r/FY0q1ArlgtVFf4r/Ff4rd1QrooArB0ArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rb1ArlgorClQroAYIr/Ff4r/FbcK1QrlitUFf4r/FY+AFclVFYUCFc9QFf4rZgGq0AqjgNVoAr/FbeoFccFFYkqFcwFDlWqFccVFYkKFctUFeOAFcVVFYkCFctQFYugFUMBFYsAFctAFYuoFcMFFY0qFcgHFlWqFcMVFY0KFcdUFY+AFcFVFY0C1WgFT8BqtQFeMA1WoFb8FqtAFY7cgiorIlQriBI8K1WAFb1VqgrJbjzaBFZECbj7aBqALIFcLaHbkLaJFYbcdqorKbjzaKbkDaLbgSwcVwLaJWD6uLFYawaVwIrMbgKwaVwLaKbgawaVwLaLbgawZQQLaLWDiuOWAaEYQQKuMWAelNBqCLVxqEC0oRPQS6EC0oSQQSyECFYKEVQSIABFYI/QAAcFFYJDRCgSCmYYjdSCqqYCLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5A")) + }; + + // p70 + if (p >= 70 && p < 80) return { + width : 176, height : 176, bpp : 2, + transparent : -1, + palette : pal2, + buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESFQOoFacFQiSCCwArTgCESQSyEUlTZTboyCnQiSCYQiSCYQiSCZQgdAVxwqYQgSwMVwOoFbMFWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbheqFcTcHbT7cDFY0CbT7cDqArxhWqwArfgFVqgrHFUDcBFY0qFcdVFY2oFcMFFY2qFclAFYugFcMBFYsCFctQFYuAFcMAFYsKFctUFYoqigEVFeEqFctVFYmoFccFFYmqFc1AFYegFccBFf4rbgWqwArjgFVqAr/FbMKFc9UFYYqkgEVFf4r/FY0q1ArlgtVFf4r/Ff4rd1QrooArB0ArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rb1ArlgorClQroAYIr/Ff4r/FbcK1QrlitUFf4r/FY+AFclVFYUCFc9QFf4rZAgoAggNVoAr/FbdUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHIA=")) + }; + + // p80 + if (p >= 80 && p < 90) return { + width : 176, height : 176, bpp : 2, + transparent : -1, + palette : pal2, + buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESFQOoFacFQiSCCwArTgCESQSyEUlTZTboyCnQiSCYQiSCYQiSCZQgdAVxwqYQgSwMVwOoFbMFWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbheqFcTcHbT7cDFY0CbT7cDqArxhWqwArfgFVqgrHFUDcBFY0qFcdVFY2oFcMFFY2qFclAFYugFcMBFYsCFctQFYuAFcMAFYsKFctUFYoqigEVFeEqFctVFYmoFccFFYmqFc1AcIdQFccBFf4rbGAoAhKQYr/Fa8FFc9UFYYqkgEVFf4r/FYwDDAEZTDFf4r/Ff4rbqorooArBqArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlgorCioroAYIr/Ff4r/FbYDDAEZTDFf4r/FYtAFclVFYUBFc9QFf4rZAgoAgKQor/FbFUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHIA=")) + }; + + // p90 + if (p >= 90 && p < 100) return { + width : 176, height : 176, bpp : 2, + transparent : -1, + palette : pal2, + buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESquq1ArTgqESNgOqwArTIYKERH4KCUQigSBbKTdGCKKCVQiTCCFSyERCALBQQjAPBoArXDZ7ARObKuBSZwcaVzR0QFYKuZWAYNZWCJJKMoKuaWAahKBhiwTJRSudURorBFTgfMVzqjDO5DaeZ5jaeJhhiKbi4rIbT4hLqoriPI7afUpS5BbTwiKFdZgIADSmHFYIqgbgIrGcgIriEYwzHADZ7HRY4rdaYrjHADcBFYoGBFcgkEGQwAeFYqKHFbzUEcQ4AdiorwiorlEogxFAD59FWoorhoArDqArjgIr/FbYwFAEJSDFf4rXgornqgrDFUkAior/Ff4rGAYYAjKYYr/Ff4r/FbdVFdFAFYNQFcsBFf4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/FbdUFcsFFYUVFdADBFf4r/Ff4rbAYYAjKYYr/Ff4rFoArkqorCgIrnqAr/FbIEFAEBSFFf4rYqgrjgorEiormAocVAogAfEooxFFcB9EFdq1DAD9VFYkBFctQFYoGEADokHFcp8FRQoAdag7iFFb4HFioHGADYjHGY4rcPYyLHADbTHcYNQFT4iIFdZgIADKmJqrcgiorIBIIrhMKIAXUpIrBbjzaBFZAKKbS5MJFcKkJbj4fLBYLcdqorKbjzPMbjxKNMhauTURawdJJorBWDShBFZiRBWDQcOHRyuPOhorBWDIbPWDRzQSYKEYIwLLOHgSEXDIJyPQjD2SQjCCQQjSCRCYY/QN4xDRQiyCSQgjdSCqqECLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5A")) + }; + + // p100 + return { + width : 176, height : 176, bpp : 2, + transparent : -1, + palette : pal2, + buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVAAVUFUgpDAAdAFMEBFQ4ABqBVnLMQqLFjzWEABLgbVgohEGoqyaiofDBihWVHJpYYDgYPbKxz5NLDJGCfBzgDKzA+SLChECC6A/CNRycIS6jCNIQ5uSCqqCCeCqESTKxCCQiBsCTCRDEQiCCWQigSBYaRwGQU6ESQTCESQTCESQTIbQYCJzZVwKTODjSuaOiArBVzKwDBrKwRJJRlBVzSwDUJQMMWCZKKVzqiNFYIqcD5iudUYZ3IbTzPMbTxMMMRTcXFZDafEJdVFcR5HbT6lKXILaeERQrrMBAAaUw4rBFUDcBFYzkBFcQjGGY4AbPY6LHFbrTFcY4AbgIrFAwIrkEggyGADwrFRQ4reagjiHADsVFeEVFcolEGIoAfPoq1FFcNAFYdQFccBFf4rbGAoAhKQYr/Fa8FFc9UFYYqkgEVFf4r/FYwDDAEZTDFf4r/Ff4rbqorooArBqArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlgorCioroAYIr/Ff4r/FbYDDAEZTDFf4r/FYtAFclVFYUBFc9QFf4rZAgoAgKQor/FbFUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHIA=")) + }; +} + +///////////////// IDLE TIMER ///////////////////////////////////// + +function drawIdle() { + let mins = Math.round((getTime() - lastStep) / 60); + g.reset(); + g.setColor(g.theme.bg); + g.fillRect(Bangle.appRect); + g.setColor(g.theme.fg); + setSmallFont20(); + g.setFontAlign(0, 0); + g.drawString('Last step was', w/2, (h/3)); + g.drawString(mins + ' minutes ago', w/2, 20+(h/3)); + dismissBtn.draw(); +} + +/////////////// BUTTON CLASS /////////////////////////////////////////// + +// simple on screen button class +function BUTTON(name,x,y,w,h,c,f,tx) { + this.name = name; + this.x = x; + this.y = y; + this.w = w; + this.h = h; + this.color = c; + this.callback = f; + this.text = tx; +} + +// if pressed the callback +BUTTON.prototype.check = function(x,y) { + //console.log(this.name + ":check() x=" + x + " y=" + y +"\n"); + + if (x>= this.x && x<= (this.x + this.w) && y>= this.y && y<= (this.y + this.h)) { + log_debug(this.name + ":callback\n"); + this.callback(); + return true; + } + return false; +}; + +BUTTON.prototype.draw = function() { + g.setColor(this.color); + g.fillRect(this.x, this.y, this.x + this.w, this.y + this.h); + g.setColor("#000"); // the icons and boxes are drawn black + setSmallFont20(); + g.setFontAlign(0, 0); + g.drawString(this.text, (this.x + this.w/2), (this.y + this.h/2)); + g.drawRect(this.x, this.y, (this.x + this.w), (this.y + this.h)); +}; + +function dismissPrompt() { + idle = false; + warned = false; + lastStep = getTime(); + Bangle.buzz(100); + draw(); +} + +var dismissBtn = new BUTTON("big",0, 3*h/4 ,w, h/4, "#0ff", dismissPrompt, "Dismiss"); + +Bangle.on('touch', function(button, xy) { + var x = xy.x; + var y = xy.y; + // adjust for outside the dimension of the screen + // http://forum.espruino.com/conversations/371867/#comment16406025 + if (y > h) y = h; + if (y < 0) y = 0; + if (x > w) x = w; + if (x < 0) x = 0; + + if (idle && dismissBtn.check(x, y)) return; +}); + +// if we get a step then we are not idle +Bangle.on('step', s => { + lastStep = getTime(); + // redraw if we had been idle + if (idle == true) { + dismissPrompt(); + } + idle = false; + warned = 0; + + if (infoMode == "ID_STEP") drawSteps(); +}); + +function checkIdle() { + log_debug("checkIdle()"); + if (!settings.idle_check) { + idle = false; + warned = false; + return; + } + + let hour = (new Date()).getHours(); + let active = (hour >= 9 && hour < 21); + //let active = true; + let dur = getTime() - lastStep; + + if (active && dur > IDLE_MINUTES * 60) { + drawIdle(); + if (warned++ < 3) { + buzzer(warned); + log_debug("checkIdle: warned=" + warned); + Bangle.setLocked(false); + } + idle = true; + } else { + idle = false; + warned = 0; + } +} + +// timeout for multi-buzzer +var buzzTimeout; + +// n buzzes +function buzzer(n) { + log_debug("buzzer n=" + n); + + if (n-- < 1) return; + Bangle.buzz(250); + + if (buzzTimeout) clearTimeout(buzzTimeout); + buzzTimeout = setTimeout(function() { + buzzTimeout = undefined; + buzzer(n); + }, 500); +} + +/////////////////////////////////////////////////////////////////////////////// + +// timeout used to update every minute +var drawTimeout; + +// schedule a draw for the next minute +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + checkIdle(); + 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.setUI("clockupdown", btn=> { + if (btn<0) prevInfo(); + if (btn>0) nextInfo(); + draw(); +}); + +loadSettings(); +loadLocation(); + +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="";} +draw(); diff --git a/apps/daisy/app.png b/apps/daisy/app.png new file mode 100644 index 000000000..89252e5df Binary files /dev/null and b/apps/daisy/app.png differ diff --git a/apps/daisy/metadata.json b/apps/daisy/metadata.json new file mode 100644 index 000000000..15a24592c --- /dev/null +++ b/apps/daisy/metadata.json @@ -0,0 +1,18 @@ +{ "id": "daisy", + "name": "Daisy", + "version":"0.05", + "dependencies": {"mylocation":"app"}, + "description": "A clock based on the Pastel clock with large ring guage for steps", + "icon": "app.png", + "type": "clock", + "tags": "clock", + "supports" : ["BANGLEJS2"], + "screenshots": [{"url":"screenshot_daisy2.jpg"}], + "readme": "README.md", + "storage": [ + {"name":"daisy.app.js","url":"app.js"}, + {"name":"daisy.img","url":"app-icon.js","evaluate":true}, + {"name":"daisy.settings.js","url":"settings.js"} + ], + "data": [{"name":"daisy.json"}] +} diff --git a/apps/daisy/screenshot_daisy1.png b/apps/daisy/screenshot_daisy1.png new file mode 100644 index 000000000..afef3a424 Binary files /dev/null and b/apps/daisy/screenshot_daisy1.png differ diff --git a/apps/daisy/screenshot_daisy2.jpg b/apps/daisy/screenshot_daisy2.jpg new file mode 100644 index 000000000..fec6a4c7b Binary files /dev/null and b/apps/daisy/screenshot_daisy2.jpg differ diff --git a/apps/daisy/settings.js b/apps/daisy/settings.js new file mode 100644 index 000000000..044eee0d1 --- /dev/null +++ b/apps/daisy/settings.js @@ -0,0 +1,51 @@ +(function(back) { + const SETTINGS_FILE = "daisy.json"; + + // initialize with default settings... + let s = {'gy' : '#020', + 'fg' : '#0f0', + 'color': 'Green', + 'check_idle' : true}; + + // ...and overwrite them with any saved values + // This way saved values are preserved if a new version adds more settings + const storage = require('Storage') + let settings = storage.readJSON(SETTINGS_FILE, 1) || s; + const saved = settings || {} + for (const key in saved) { + s[key] = saved[key] + } + + function save() { + settings = s + storage.write(SETTINGS_FILE, settings) + } + + var color_options = ['Green','Orange','Cyan','Purple','Red','Blue']; + var fg_code = ['#0f0','#ff0','#0ff','#f0f','#f00','#00f']; + var gy_code = ['#020','#220','#022','#202','#200','#002']; + + E.showMenu({ + '': { 'title': 'Daisy Clock' }, + '< Back': back, + 'Colour': { + value: 0 | color_options.indexOf(s.color), + min: 0, max: 5, + format: v => color_options[v], + onchange: v => { + s.color = color_options[v]; + s.fg = fg_code[v]; + s.gy = gy_code[v]; + save(); + }, + }, + 'Idle Warning': { + value: !!s.idle_check, + format: v => v ? /*LANG*/"Yes":/*LANG*/"No", + onchange: v => { + s.idle_check = v; + save(); + }, + } + }); +}) diff --git a/apps/dtlaunch/ChangeLog b/apps/dtlaunch/ChangeLog index 556472eaa..811784b39 100644 --- a/apps/dtlaunch/ChangeLog +++ b/apps/dtlaunch/ChangeLog @@ -6,3 +6,5 @@ 0.06: Adds settings page (hide clocks or launchers) 0.07: Adds setting for directly launching app on touch for Bangle 2 0.08: Optimize line wrapping for Bangle 2 +0.09: fix the trasparent widget bar if there are no widgets for Bangle 2 +0.10: added "one click exit" setting for Bangle 2 diff --git a/apps/dtlaunch/app-b2.js b/apps/dtlaunch/app-b2.js index 96e562add..e0f7f825f 100644 --- a/apps/dtlaunch/app-b2.js +++ b/apps/dtlaunch/app-b2.js @@ -6,8 +6,12 @@ var settings = Object.assign({ showClocks: true, showLaunchers: true, direct: false, + oneClickExit:false }, require('Storage').readJSON("dtlaunch.json", true) || {}); +if( settings.oneClickExit) + setWatch(_=> load(), BTN1); + var s = require("Storage"); var apps = s.list(/\.info$/).map(app=>{ var a=s.readJSON(app,1); @@ -125,5 +129,6 @@ Bangle.on("touch",(_,p)=>{ }); Bangle.loadWidgets(); +g.clear(); Bangle.drawWidgets(); drawPage(0); diff --git a/apps/dtlaunch/metadata.json b/apps/dtlaunch/metadata.json index 6cd1dbe73..7a4094e54 100644 --- a/apps/dtlaunch/metadata.json +++ b/apps/dtlaunch/metadata.json @@ -1,7 +1,7 @@ { "id": "dtlaunch", "name": "Desktop Launcher", - "version": "0.08", + "version": "0.10", "description": "Desktop style App Launcher with six (four for Bangle 2) apps per page - fast access if you have lots of apps installed.", "screenshots": [{"url":"shot1.png"},{"url":"shot2.png"},{"url":"shot3.png"}], "icon": "icon.png", diff --git a/apps/dtlaunch/settings-b2.js b/apps/dtlaunch/settings-b2.js index 7f667d213..8eca46a7e 100644 --- a/apps/dtlaunch/settings-b2.js +++ b/apps/dtlaunch/settings-b2.js @@ -4,7 +4,8 @@ var settings = Object.assign({ showClocks: true, showLaunchers: true, - direct: false + direct: false, + oneClickExit:false }, require('Storage').readJSON(FILE, true) || {}); function writeSettings() { @@ -37,6 +38,14 @@ settings.direct = v; writeSettings(); } + }, + 'One click exit': { + value: settings.oneClickExit, + format: v => v?"On":"Off", + onchange: v => { + settings.oneClickExit = v; + writeSettings(); + } } }); }) diff --git a/apps/edisonsball/ChangeLog b/apps/edisonsball/ChangeLog new file mode 100644 index 000000000..b71b8bb0d --- /dev/null +++ b/apps/edisonsball/ChangeLog @@ -0,0 +1,2 @@ +0.01: Initial version +0.02: Added BangleJS Two diff --git a/apps/edisonsball/README.md b/apps/edisonsball/README.md index b8e9ec106..a3b013b6d 100644 --- a/apps/edisonsball/README.md +++ b/apps/edisonsball/README.md @@ -1,4 +1,4 @@ -The application is based on a technique that Thomas Edison used to prevent falling asleep using a steel ball. Essentially the app starts with a display that shows the current HR value that the watch alarm is set to and this can be adjusted with buttons 1 and 3. This HR settng should be the approximate value you want the alarm to trigger and so you should ideally know both what your HR is currently and what your heartrate normally is during sleep. For your current HR according to the watch, you can simply use the HR monitor available in the Espruino app loader, and then from that you can choose a lower value as the target for the alarm and adjust as required. +The application is based on a technique that Thomas Edison used to prevent falling asleep using a steel ball. Essentially the app starts with a display that shows the current HR value that the watch alarm is set to and this can be adjusted with buttons 1 and 3 (Mapped to top touch and bottom touch on Bangle 2). This HR settng should be the approximate value you want the alarm to trigger and so you should ideally know both what your HR is currently and what your heartrate normally is during sleep. For your current HR according to the watch, you can simply use the HR monitor available in the Espruino app loader, and then from that you can choose a lower value as the target for the alarm and adjust as required. When you press the middle button on the side, the HR monitor starts, the alarm will trigger when your heart rate average drops to the limit you’ve set and has a certain level of steadiness that is determined by a assessing the variance over several readings - the sensitivity of this variance can be adjusted in a variable in the app's code under 'ADVANCED SETTINGS' if needed. The code also has a basic logging function which shows, in a CSV file, when you started the HR tracker and when the alarm was triggered. diff --git a/apps/edisonsball/app.js b/apps/edisonsball/app.js index 557155c9a..2aa317829 100644 --- a/apps/edisonsball/app.js +++ b/apps/edisonsball/app.js @@ -3,6 +3,8 @@ var lower_limit_BPM = 49; var upper_limit_BPM = 140; var deviation_threshold = 3; +var ISBANGLEJS1 = process.env.HWVERSION==1; + var target_heartrate = 70; var heartrate_set; @@ -33,25 +35,39 @@ function btn2Pressed() { } function update_target_HR(){ - g.clear(); - g.setColor("#00ff7f"); - g.setFont("6x8", 4); - g.setFontAlign(0,0); // center font + if (process.env.HWVERSION==1) { + g.setColor("#00ff7f"); + g.setFont("6x8", 4); + g.setFontAlign(0,0); // center font - g.drawString(target_heartrate, 120,120); - g.setFont("6x8", 2); - g.setFontAlign(-1,-1); - g.drawString("-", 220, 200); - g.drawString("+", 220, 40); - g.drawString("GO", 210, 120); - - g.setColor("#ffffff"); - g.setFontAlign(0,0); // center font - g.drawString("target HR", 120,90); - - g.setFont("6x8", 1); - g.drawString("if unsure, start with 7-10%\n less than waking average and\n adjust as required", 120,170); + g.drawString(target_heartrate, 120,120); + g.setFont("6x8", 2); + g.setFontAlign(-1,-1); + g.drawString("-", 220, 200); + g.drawString("+", 220, 40); + g.drawString("GO", 210, 120); + + g.setColor("#ffffff"); + g.setFontAlign(0,0); // center font + g.drawString("target HR", 120,90); + + g.setFont("6x8", 1); + g.drawString("if unsure, start with 7-10%\n less than waking average and\n adjust as required", 120,170); + } else { + g.setFont("6x8", 4); + g.setFontAlign(0,0); // center font + g.drawString(target_heartrate, 88,88); + g.setFont("6x8", 2); + g.setFontAlign(-1,-1); + g.drawString("-", 160, 160); + g.drawString("+", 160, 10); + g.drawString("GO", 150, 88); + g.setFontAlign(0,0); // center font + g.drawString("target HR", 88,65); + g.setFont("6x8", 1); + g.drawString("if unsure, start with 7-10%\n less than waking average and\n adjust as required", 88,150); + } g.setFont("6x8",3); g.flip(); @@ -105,8 +121,13 @@ function checkHR() { average_HR = average(HR_samples).toFixed(0); stdev_HR = getStandardDeviation (HR_samples).toFixed(1); - g.drawString("HR: " + average_HR, 120,100); - g.drawString("STDEV: " + stdev_HR, 120,160); + if (ISBANGLEJS1) { + g.drawString("HR: " + average_HR, 120,100); + g.drawString("STDEV: " + stdev_HR, 120,160); + } else { + g.drawString("HR: " + average_HR, 88,60); + g.drawString("STDEV: " + stdev_HR, 88,90); + } HR_samples = []; if(average_HR < target_heartrate && stdev_HR < deviation_threshold){ @@ -131,12 +152,26 @@ function checkHR() { update_target_HR(); -setWatch(btn1Pressed, BTN1, {repeat:true}); -setWatch(btn2Pressed, BTN2, {repeat:true}); -setWatch(btn3Pressed, BTN3, {repeat:true}); +if (ISBANGLEJS1) { + // Bangle 1 + setWatch(btn1Pressed, BTN1, {repeat:true}); + setWatch(btn2Pressed, BTN2, {repeat:true}); + setWatch(btn3Pressed, BTN3, {repeat:true}); +} else { + // Bangle 2 + setWatch(btn2Pressed, BTN1, { repeat: true }); + Bangle.on('touch', function(zone, e) { + if (e.y < g.getHeight() / 2) { + btn1Pressed(); + } + if (e.y > g.getHeight() / 2) { + btn3Pressed(); + } + }); +} + Bangle.on('HRM',function(hrm) { - if(trigger_count < 2){ if (firstBPM) firstBPM=false; // ignore the first one as it's usually rubbish diff --git a/apps/edisonsball/metadata.json b/apps/edisonsball/metadata.json index f429c7b67..dfeb4451e 100644 --- a/apps/edisonsball/metadata.json +++ b/apps/edisonsball/metadata.json @@ -2,11 +2,11 @@ "id": "edisonsball", "name": "Edison's Ball", "shortName": "Edison's Ball", - "version": "0.01", + "version": "0.02", "description": "Hypnagogia/Micro-Sleep alarm for experimental use in exploring sleep transition and combating drowsiness", "icon": "app-icon.png", - "tags": "", - "supports": ["BANGLEJS"], + "tags": "sleep,hyponagogia,quick,nap", + "supports": ["BANGLEJS", "BANGLEJS2"], "readme": "README.md", "storage": [ {"name":"edisonsball.app.js","url":"app.js"}, diff --git a/apps/gbmusic/ChangeLog b/apps/gbmusic/ChangeLog index 8b1a3e4aa..e2ee53ede 100644 --- a/apps/gbmusic/ChangeLog +++ b/apps/gbmusic/ChangeLog @@ -7,3 +7,5 @@ 0.07: Fix "previous" button image 0.08: Fix scrolling title background color 0.09: Move event listener from widget to boot code, stops music from showing up in messages +0.10: Simplify touch events + Remove date+time diff --git a/apps/gbmusic/README.md b/apps/gbmusic/README.md index 5d06164c2..f1f9c4378 100644 --- a/apps/gbmusic/README.md +++ b/apps/gbmusic/README.md @@ -3,9 +3,10 @@ If you have an Android phone with Gadgetbridge, this app allows you to view and control music playback. -| Bangle.js 1 | Bangle.js 2 | -|:-------------------------------------------|:-------------------------------------------| -| ![Screenshot: Bangle 1](screenshot_v1.png) | ![Screenshot: Bangle 2](screenshot_v2.png) | +| Bangle.js 1 | Bangle.js 2 | +|:---------------------------------------------------------|:---------------------------------------------------------| +| ![Screenshot: Bangle 1 Dark theme](screenshot_v1_d.png) | ![Screenshot: Bangle 2 Darm theme](screenshot_v2_d.png) | +| ![Screenshot: Bangle 1 Light theme](screenshot_v1_l.png) | ![Screenshot: Bangle 2 Light theme](screenshot_v2_l.png) | Download the [latest Gadgetbridge for Android here](https://f-droid.org/packages/nodomain.freeyourgadget.gadgetbridge/). @@ -14,7 +15,6 @@ Download the [latest Gadgetbridge for Android here](https://f-droid.org/packages * Dynamic colors based on Track/Artist/Album name * Scrolling display for long titles * Automatic start when music plays -* Time and date display ## Settings @@ -40,9 +40,7 @@ Disable double/triple pressing Middle Button: always simply toggle play/pause. * Button 3 (*Bangle.js 1*): Volume down ### Touch -* Left: Pause/previous song -* Right: Next song/resume -* Center: Toggle play/pause +* Touch: Toggle play/pause * Swipe left/right: Next/previous song * Swipe up/down (*Bangle.js 2*): Volume up/down diff --git a/apps/gbmusic/app.js b/apps/gbmusic/app.js index c8395f745..5252ac0ac 100644 --- a/apps/gbmusic/app.js +++ b/apps/gbmusic/app.js @@ -10,15 +10,15 @@ const BANGLE2 = process.env.HWVERSION===2; /** * @param {string} text - * @return {number} Maximum font size to make text fit on screen + * @param {number} w Width to fit text in + * @return {number} Maximum font size to make text fit */ -function fitText(text) { +function fitText(text, w) { if (!text.length) { return Infinity; } // make a guess, then shrink/grow until it fits - const w = Bangle.appRect.w, - test = (s) => g.setFont("Vector", s).stringWidth(text); + const test = (s) => g.setFont("Vector", s).stringWidth(text); let best = Math.floor(100*w/test(100)); if (test(best)===w) { // good guess! return best; @@ -106,7 +106,7 @@ function rTitle(l) { rScroller(l); // already scrolling return; } - let size = fitText(l.label); + let size = fitText(l.label, l.w); if (sizel.h) { size = l.h; } @@ -182,23 +182,17 @@ function makeUI() { type: "v", c: [ { type: "h", fillx: 1, c: [ - {id: "time", type: "txt", label: "88:88", valign: -1, halign: -1, font: "8%", bgCol: g.theme.bg}, {fillx: 1}, - {id: "num", type: "txt", label: "88:88", valign: -1, halign: 1, font: "12%", bgCol: g.theme.bg}, - BANGLE2 ? {} : {id: "up", type: "txt", label: " +", font: "6x8:2"}, + {id: "num", type: "txt", label: "", valign: -1, halign: -1, font: "12%", bgCol: g.theme.bg}, + BANGLE2 ? {} : {id: "up", type: "txt", label: " +", halign: 1, font: "6x8:2"}, ], }, {id: "title", type: "custom", label: "", fillx: 1, filly: 2, offset: null, font: "Vector:20%", render: rTitle, bgCol: g.theme.bg}, {id: "artist", type: "custom", label: "", fillx: 1, filly: 1, size: 30, render: rInfo, bgCol: g.theme.bg}, - {id: "album", type: "custom", label: "", fillx: 1, filly: 1, size: 20, render: rInfo, bgCol: g.theme.bg}, - {height: 10}, { type: "h", c: [ - {width: 3}, - {id: "prev", type: "custom", height: 15, width: 15, icon: "previous", render: rIcon, bgCol: g.theme.bg}, - {id: "date", type: "txt", halign: 0, valign: 1, label: "", font: "8%", fillx: 1, bgCol: g.theme.bg}, - {id: "next", type: "custom", height: 15, width: 15, icon: "next", render: rIcon, bgCol: g.theme.bg}, - BANGLE2 ? {width: 3} : {id: "down", type: "txt", label: " -", font: "6x8:2"}, + {id: "album", type: "custom", label: "", fillx: 1, filly: 1, size: 20, render: rInfo, bgCol: g.theme.bg}, + BANGLE2 ? {} : {id: "down", type: "txt", label: " -", font: "6x8:2"}, ], }, {height: 10}, @@ -211,20 +205,6 @@ function makeUI() { // Self-repeating timeouts /////////////////////// -// Clock -let tock = -1; -function tick() { - if (!BANGLE2 && !Bangle.isLCDOn()) { - return; - } - const now = new Date(); - if (now.getHours()*60+now.getMinutes()!==tock) { - drawDateTime(); - tock = now.getHours()*60+now.getMinutes(); - } - setTimeout(tick, 1000); // we only show minute precision anyway -} - // Fade out while paused and auto closing let fade = null; function fadeOut() { @@ -271,40 +251,12 @@ function scrollStop() { //////////////////// // Drawing functions //////////////////// -/** - * Draw date and time - */ -function drawDateTime() { - const now = new Date(); - const l = require("locale"); - const is12 = (require("Storage").readJSON("setting.json", 1) || {})["12hour"]; - if (is12) { - const d12 = new Date(now.getTime()); - const hour = d12.getHours(); - if (hour===0) { - d12.setHours(12); - } else if (hour>12) { - d12.setHours(hour-12); - } - layout.time.label = l.time(d12, true)+l.meridian(now); - } else { - layout.time.label = l.time(now, true); - } - layout.date.label = require("locale").date(now, true); - layout.render(); -} function drawControls() { - let l = layout; + if (BANGLE2) return; const cc = a => (a ? "#f00" : "#0f0"); // control color: red for active, green for inactive - if (!BANGLE2) { - l.up.col = cc("volumeup" in tCommand); - l.down.col = cc("volumedown" in tCommand); - } - l.prev.icon = (stat==="play") ? "pause" : "previous"; - l.prev.col = cc("prev" in tCommand || "pause" in tCommand); - l.next.icon = (stat==="play") ? "next" : "play"; - l.next.col = cc("next" in tCommand || "play" in tCommand); + layout.up.col = cc("volumeup" in tCommand); + layout.down.col = cc("volumedown" in tCommand); layout.render(); } @@ -339,6 +291,7 @@ function info(info) { layout.album.col = infoColor("album"); layout.artist.col = infoColor("artist"); layout.num.label = formatNum(info); + layout.update(); layout.render(); rTitle(layout.title); // force redraw of title, or scroller might break // reset auto exit interval @@ -473,37 +426,16 @@ function sendCommand(command) { drawControls(); } -// touch/swipe: navigation function togglePlay() { sendCommand(stat==="play" ? "pause" : "play"); } -function pausePrev() { - sendCommand(stat==="play" ? "pause" : "previous"); -} -function nextPlay() { - sendCommand(stat==="play" ? "next" : "play"); -} /** * Setup touch+swipe for Bangle.js 1 */ function touch1() { - Bangle.on("touch", side => { - if (!Bangle.isLCDOn()) {return;} // for <2v10 firmware - switch(side) { - case 1: - pausePrev(); - break; - case 2: - nextPlay(); - break; - default: - togglePlay(); - break; - } - }); + Bangle.on("touch", togglePlay); Bangle.on("swipe", dir => { - if (!Bangle.isLCDOn()) {return;} // for <2v10 firmware sendCommand(dir===1 ? "previous" : "next"); }); } @@ -511,16 +443,7 @@ function touch1() { * Setup touch+swipe for Bangle.js 2 */ function touch2() { - Bangle.on("touch", (side, xy) => { - const ar = Bangle.appRect; - if (xy.xar.x+ar.w*2/3) { - nextPlay(); - } else { - togglePlay(); - } - }); + Bangle.on("touch", togglePlay); // swiping let drag; Bangle.on("drag", e => { @@ -595,7 +518,6 @@ function startWatches() { function start() { makeUI(); startWatches(); - tick(); startEmulator(); } diff --git a/apps/gbmusic/metadata.json b/apps/gbmusic/metadata.json index f578f1f48..0ded80452 100644 --- a/apps/gbmusic/metadata.json +++ b/apps/gbmusic/metadata.json @@ -2,10 +2,11 @@ "id": "gbmusic", "name": "Gadgetbridge Music Controls", "shortName": "Music Controls", - "version": "0.09", + "version": "0.10", "description": "Control the music on your Gadgetbridge-connected phone", "icon": "icon.png", - "screenshots": [{"url":"screenshot_v1.png"},{"url":"screenshot_v2.png"}], + "screenshots": [{"url":"screenshot_v1_d.png"},{"url":"screenshot_v1_l.png"}, + {"url":"screenshot_v2_d.png"},{"url":"screenshot_v2_l.png"}], "type": "app", "tags": "tools,bluetooth,gadgetbridge,music", "supports": ["BANGLEJS","BANGLEJS2"], diff --git a/apps/gbmusic/screenshot_v1.png b/apps/gbmusic/screenshot_v1.png deleted file mode 100644 index 3b290e459..000000000 Binary files a/apps/gbmusic/screenshot_v1.png and /dev/null differ diff --git a/apps/gbmusic/screenshot_v1_d.png b/apps/gbmusic/screenshot_v1_d.png new file mode 100644 index 000000000..3a6d84cff Binary files /dev/null and b/apps/gbmusic/screenshot_v1_d.png differ diff --git a/apps/gbmusic/screenshot_v1_l.png b/apps/gbmusic/screenshot_v1_l.png new file mode 100644 index 000000000..1b6b57809 Binary files /dev/null and b/apps/gbmusic/screenshot_v1_l.png differ diff --git a/apps/gbmusic/screenshot_v2.png b/apps/gbmusic/screenshot_v2.png deleted file mode 100644 index b89b5022e..000000000 Binary files a/apps/gbmusic/screenshot_v2.png and /dev/null differ diff --git a/apps/gbmusic/screenshot_v2_d.png b/apps/gbmusic/screenshot_v2_d.png new file mode 100644 index 000000000..761e1d14a Binary files /dev/null and b/apps/gbmusic/screenshot_v2_d.png differ diff --git a/apps/gbmusic/screenshot_v2_l.png b/apps/gbmusic/screenshot_v2_l.png new file mode 100644 index 000000000..5cf05c7cb Binary files /dev/null and b/apps/gbmusic/screenshot_v2_l.png differ diff --git a/apps/gbridge/metadata.json b/apps/gbridge/metadata.json index 0c46aa852..db7119758 100644 --- a/apps/gbridge/metadata.json +++ b/apps/gbridge/metadata.json @@ -2,7 +2,7 @@ "id": "gbridge", "name": "Gadgetbridge", "version": "0.26", - "description": "(NOT RECOMMENDED) Displays Gadgetbridge notifications from Android. Please use the 'Android' Bangle.js app instead.", + "description": "(NOT RECOMMENDED) Displays Gadgetbridge notifications from Android. Please use the 'Android Integration' Bangle.js app instead.", "icon": "app.png", "type": "widget", "tags": "tool,system,android,widget", diff --git a/apps/gpsautotime/ChangeLog b/apps/gpsautotime/ChangeLog index 5560f00bc..2827c9e5c 100644 --- a/apps/gpsautotime/ChangeLog +++ b/apps/gpsautotime/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Set Bangle.js 2 compatible diff --git a/apps/gpsautotime/metadata.json b/apps/gpsautotime/metadata.json index a64a45f6d..766961276 100644 --- a/apps/gpsautotime/metadata.json +++ b/apps/gpsautotime/metadata.json @@ -2,12 +2,12 @@ "id": "gpsautotime", "name": "GPS auto time", "shortName": "GPS auto time", - "version": "0.01", + "version": "0.02", "description": "A widget that automatically updates the Bangle.js time to the GPS time whenever there is a valid GPS fix.", "icon": "widget.png", "type": "widget", "tags": "widget,gps", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS","BANGLEJS2"], "storage": [ {"name":"gpsautotime.wid.js","url":"widget.js"} ] diff --git a/apps/hardalarm/ChangeLog b/apps/hardalarm/ChangeLog index 7e9b17f2a..dac7d317e 100644 --- a/apps/hardalarm/ChangeLog +++ b/apps/hardalarm/ChangeLog @@ -1,2 +1,3 @@ 0.01: Add a number to match to turn off alarm 0.02: Respect Quiet Mode +0.03: Fix hour/minute wrapping code for new menu system diff --git a/apps/hardalarm/app.js b/apps/hardalarm/app.js index 61467b421..0c72a2c8f 100644 --- a/apps/hardalarm/app.js +++ b/apps/hardalarm/app.js @@ -56,25 +56,25 @@ function editAlarm(alarmIndex) { } const menu = { '': { 'title': 'Alarms' }, - 'Hours': { - value: hrs, - onchange: function(v){if (v<0)v=23;if (v>23)v=0;hrs=v;this.value=v;} // no arrow fn -> preserve 'this' + /*LANG*/'Hours': { + value: hrs, min : 0, max : 23, wrap : true, + onchange: v => hrs=v }, - 'Minutes': { - value: mins, - onchange: function(v){if (v<0)v=59;if (v>59)v=0;mins=v;this.value=v;} // no arrow fn -> preserve 'this' + /*LANG*/'Minutes': { + value: mins, min : 0, max : 59, wrap : true, + onchange: v => mins=v }, - 'Enabled': { + /*LANG*/'Enabled': { value: en, format: v=>v?"On":"Off", onchange: v=>en=v }, - 'Repeat': { + /*LANG*/'Repeat': { value: en, format: v=>v?"Yes":"No", onchange: v=>repeat=v }, - 'Auto snooze': { + /*LANG*/'Auto snooze': { value: as, format: v=>v?"Yes":"No", onchange: v=>as=v diff --git a/apps/hardalarm/metadata.json b/apps/hardalarm/metadata.json index 13a8fb920..1dab4501d 100644 --- a/apps/hardalarm/metadata.json +++ b/apps/hardalarm/metadata.json @@ -2,7 +2,7 @@ "id": "hardalarm", "name": "Hard Alarm", "shortName": "HardAlarm", - "version": "0.02", + "version": "0.03", "description": "Make sure you wake up! Count to the right number to turn off the alarm", "icon": "app.png", "tags": "tool,alarm,widget", diff --git a/apps/info/ChangeLog b/apps/info/ChangeLog new file mode 100644 index 000000000..07afedd21 --- /dev/null +++ b/apps/info/ChangeLog @@ -0,0 +1 @@ +0.01: Release \ No newline at end of file diff --git a/apps/info/README.md b/apps/info/README.md new file mode 100644 index 000000000..32920cb75 --- /dev/null +++ b/apps/info/README.md @@ -0,0 +1,17 @@ +# Info + +A very simple app that shows information on 3 different screens. +Go to the next screen via tab right, go to the previous screen +via tab left and reload the data via tab in the middle of the +screen. Very useful if combined with pattern launcher ;) + +![](screenshot_1.png) +![](screenshot_2.png) +![](screenshot_3.png) + + +## Contributors +- [David Peer](https://github.com/peerdavid). + +## Thanks To +Info icons created by Freepik - Flaticon diff --git a/apps/info/info.app.js b/apps/info/info.app.js new file mode 100644 index 000000000..c61a88045 --- /dev/null +++ b/apps/info/info.app.js @@ -0,0 +1,108 @@ +var s = require("Storage"); +const locale = require('locale'); +var ENV = process.env; +var W = g.getWidth(), H = g.getHeight(); +var screen = 0; +const maxScreen = 2; + +function getVersion(file) { + var j = s.readJSON(file,1); + var v = ("object"==typeof j)?j.version:false; + return v?((v?"v"+v:"Unknown")):"NO "; +} + + +function drawData(name, value, y){ + g.drawString(name, 5, y); + g.drawString(value, 100, y); +} + +function getSteps(){ + try{ + return Bangle.getHealthStatus("day").steps; + } catch(e) { + return ">= 2v12"; + } +} + +function getBpm(){ + try{ + return Math.round(Bangle.getHealthStatus("day").bpm) + "bpm"; + } catch(e) { + return ">= 2v12"; + } +} + +function drawInfo() { + g.reset().clearRect(Bangle.appRect); + var h=18, y = h;//-h; + + // Header + g.setFont("Vector", h+2).setFontAlign(0,-1); + g.drawString("--==|| INFO ||==--", W/2, 0); + g.setFont("Vector",h).setFontAlign(-1,-1); + + // Dynamic data + if(screen == 0){ + drawData("Steps", getSteps(), y+=h); + drawData("HRM", getBpm(), y+=h); + drawData("Battery", E.getBattery() + "%", y+=h); + drawData("Voltage", E.getAnalogVRef().toFixed(2) + "V", y+=h); + drawData("IntTemp.", locale.temp(parseInt(E.getTemperature())), y+=h); + } + + if(screen == 1){ + drawData("Charging?", Bangle.isCharging() ? "Yes" : "No", y+=h); + drawData("Bluetooth", NRF.getSecurityStatus().connected ? "Conn." : "Disconn.", y+=h); + drawData("GPS", Bangle.isGPSOn() ? "On" : "Off", y+=h); + drawData("Compass", Bangle.isCompassOn() ? "On" : "Off", y+=h); + drawData("HRM", Bangle.isHRMOn() ? "On" : "Off", y+=h); + } + + // Static data + if(screen == 2){ + drawData("Firmw.", ENV.VERSION, y+=h); + drawData("Boot.", getVersion("boot.info"), y+=h); + drawData("Settings", getVersion("setting.info"), y+=h); + drawData("Storage", "", y+=h); + drawData(" Total", ENV.STORAGE>>10, y+=h); + drawData(" Free", require("Storage").getFree()>>10, y+=h); + } + + if(Bangle.isLocked()){ + g.setFont("Vector",h-2).setFontAlign(-1,-1); + g.drawString("Locked", 0, H-h+2); + } + + g.setFont("Vector",h-2).setFontAlign(1,-1); + g.drawString((screen+1) + "/3", W, H-h+2); +} + +drawInfo(); +setWatch(_=>load(), BTN1); + +Bangle.on('touch', function(btn, e){ + var left = parseInt(g.getWidth() * 0.3); + var right = g.getWidth() - left; + var isLeft = e.x < left; + var isRight = e.x > right; + + if(isRight){ + screen = (screen + 1) % (maxScreen+1); + } + + if(isLeft){ + screen -= 1; + screen = screen < 0 ? maxScreen : screen; + } + + drawInfo(); +}); + +Bangle.on('lock', function(isLocked) { + drawInfo(); +}); + +Bangle.loadWidgets(); +for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";} +// Bangle.drawWidgets(); \ No newline at end of file diff --git a/apps/info/info.icon.js b/apps/info/info.icon.js new file mode 100644 index 000000000..8dbab8357 --- /dev/null +++ b/apps/info/info.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwcBkmSpICDBwcJBYwCDpAhFggRJGg8SCI+ABgU//gSDCI4JBj//AAX4JRAIBg4QDAAPgBIJWGgIQFAAI+BLglAgEPCI/wEgJoEgYQHAAPANwhWFAApcBCIWQgAQJAAMAgSMDCJiSCwB6GQA6eCn5TFL4q5BUgIRF/wuBv4RGkCeGO4IREUgMBCJCVGCISwIWw0BYRLIICLBHHCJRrGCIQIFR44I5LIoRaPpARcdIwRJfYMBCJuACKUkgE/a5f8gEJCJD7FCIeAg78FAAvggFJCIMACJZOBCIOQCJsCCIOSgEfCBP4gESCIZTFOIwRDoDIGaguSCIVIgCkFTwcAggRDpIYBQAx6BgAOCAQYIBLghWBTwQRFFgIABXIIFDBwgCDBYQAENAYCFLgIAEKwpKIIhA=")) diff --git a/apps/info/info.png b/apps/info/info.png new file mode 100644 index 000000000..c73813025 Binary files /dev/null and b/apps/info/info.png differ diff --git a/apps/info/metadata.json b/apps/info/metadata.json new file mode 100644 index 000000000..f05f0e134 --- /dev/null +++ b/apps/info/metadata.json @@ -0,0 +1,19 @@ +{ + "id": "info", + "name": "Info", + "version": "0.01", + "description": "An application that displays information such as battery level, steps etc.", + "icon": "info.png", + "type": "app", + "tags": "tool", + "readme": "README.md", + "supports": ["BANGLEJS2"], + "screenshots": [ + {"url":"screenshot_1.png"}, + {"url":"screenshot_2.png"}, + {"url":"screenshot_3.png"}], + "storage": [ + {"name":"info.app.js","url":"info.app.js"}, + {"name":"info.img","url":"info.icon.js","evaluate":true} + ] +} diff --git a/apps/info/screenshot_1.png b/apps/info/screenshot_1.png new file mode 100644 index 000000000..97d42a896 Binary files /dev/null and b/apps/info/screenshot_1.png differ diff --git a/apps/info/screenshot_2.png b/apps/info/screenshot_2.png new file mode 100644 index 000000000..2d25dd4e6 Binary files /dev/null and b/apps/info/screenshot_2.png differ diff --git a/apps/info/screenshot_3.png b/apps/info/screenshot_3.png new file mode 100644 index 000000000..782e4a195 Binary files /dev/null and b/apps/info/screenshot_3.png differ diff --git a/apps/intclock/ChangeLog b/apps/intclock/ChangeLog new file mode 100644 index 000000000..2ad07562f --- /dev/null +++ b/apps/intclock/ChangeLog @@ -0,0 +1 @@ +0.01: Initial commit diff --git a/apps/intclock/README.md b/apps/intclock/README.md new file mode 100644 index 000000000..7d31cdae2 --- /dev/null +++ b/apps/intclock/README.md @@ -0,0 +1,4 @@ +# Analogue Clock + +![](screenshot_analog.png) + diff --git a/apps/intclock/app-icon.js b/apps/intclock/app-icon.js new file mode 100644 index 000000000..8c62a192e --- /dev/null +++ b/apps/intclock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgP/AH4Aq+GEkPh4E/BZMAh4LGw/h8MPBZHggIXJ/EB4ALI//h/4LHwk/BagA/ACY=")) diff --git a/apps/intclock/app.js b/apps/intclock/app.js new file mode 100644 index 000000000..1324a422c --- /dev/null +++ b/apps/intclock/app.js @@ -0,0 +1,60 @@ +Graphics.prototype.setFontUndo = function(scale) { + // Actual height 19 (20 - 2) + this.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqKAqooCqigKqKAAAACgAAKAAAoAACgAAAAAAAAACgAAKAAAoAACgAAAAAAKCgAoKAKqqAqqoCqqgKqqAKCgAoKAKqqAqqoCqqgKqqAKCgAoKAAAAAKgoAqCgKqKAqooKiioqKKiooqKiioKKqAoqoCgqAKCoAAAACoCgKgKAiCoCIKgKioAqKgACoAAKgACoAAKgACoqAKioCoIgKgiAoCoCgKgAAAAKqgAqqAKqqAqqoKiioqKKiooqKiioKAKAoAoCgCgKAKAAAACgAAKAAAoAACgAAAAAAKqgAqqAKqqAqqoCgCgKAKAAAACgCgKAKAqqoCqqgCqoAKqgAAAACigAKKAAqoACqgAqoACqgAKqAAqoAAqoACqgAKKAAooAAAAAAoAACgAAKAAAoAAqqACqoAKqgAqqAAKAAAoAACgAAKAAAAAAACoAAKgAAqAACoAAAAAoAACgAAKAAAoAACgAAKAAAoAACgAAKAAAoAACgAAKAAAAAAACgAAKAAAoAACgAAAAAAoAACgAAqAACoAAqAACoAAqAACoAAqAACoAAqAACoAAqAACoAAKAAAoAAAAAACqoAKqgCqqgKqqAoAoCgCgKAKAoAoCqqgKqqAKqgAqqAAAAAqqoCqqgKqqAqqoAAAAKCqAoKoCiqgKKqAoooCiigKKKAoooCqigKqKAKgoAqCgAAAAoAoCgCgKAKAoAoCiigKKKAoooCiigKqqAqqoAqqACqoAAAACqAAKoAAqoACqgAAKAAAoAACgAAKAAqqoCqqgKqqAqqoAAAAKoKAqgoCqigKqKAoooCiigKKKAoooCiqgKKqAoKgCgqAAAAAKqgAqqAKqqAqqoCiigKKKAoooCiigKKqAoqoCgqAKCoAAAACgAAKAAAoAACgAAKAAAoAACgAAKAAAqqoCqqgCqqAKqoAAAACqoAKqgCqqgKqqAoooCiigKKKAoooCqqgKqqAKqgAqqAAAAAKgAAqAAKqAAqoACigAKKAAooACigAKqqAqqoAqqgCqqAAAACgCgKAKAoAoCgCgAAAAoAqCgCoKAKgoAqAAAAAKAAAoAAKoAAqgAKqgAqqAKgqAqCoCgCgKAKAAAAAoKACgoAKCgAoKACgoAKCgAoKACgoAKCgAoKACgoAKCgAAAAKAKAoAoCoKgKgqAKqgAqqAAqgACqAACgAAKAAAAACgAAKAAAoAACgAAKKKAoooCiigKKKAqoACqgACoAAKgAAAAACqqAKqoCqqgKqqAoAACgAAKCoAoKgCiqgKKqAoooCiigKqqAqqoAqqACqoAAAAAqqgCqqAqqoCqqgKKAAooACigAKKAAqqoCqqgCqqAKqoAAAAKqqAqqoCqqgKqqAoooCiigKKKAoooCqqgKqqAKCgAoKAAAAAKqgAqqAKqqAqqoCgCgKAKAoAoCgCgKAKAoAoCgCgKAKAAAACqqgKqqAqqoCqqgKAKAoAoCoKgKgqAKqgAqqAAqgACqAAAAACqoAKqgCqqgKqqAoooCiigKKKAoooCgCgKAKAoAoCgCgAAAAKqoAqqgKqqAqqoCigAKKAAooACigAKAAAoAACgAAKAAAAAAAqqACqoAqqoCqqgKAKAoAoCiigKKKAoqoCiqgKKqAoqoAAAAKqqAqqoCqqgKqqAAoAACgAAKAAAoACqqgKqqAqqoCqqgAAAAoAoCgCgKAKAoAoCqqgKqqAqqoCqqgKAKAoAoCgCgKAKAAAAAAKAAAoAACoAAKgAAKAAAoAACgAAKAqqoCqqgKqoAqqgAAAAKqqAqqoCqqgKqqACqAAKoACqoAKqgCoKgKgqAoAoCgCgAAAAqqgCqqAKqqAqqoAACgAAKAAAoAACgAAKAAAoAACgAAKAAAACqqgKqqAqqoCqqgCoAAKgAAKgAAqAAKgAAqAAKqqAqqoCqqgKqqAAAACqqgKqqAqqoCqqgCoAAKgAAKgAAqAAqqoCqqgKqqAqqoAAAACqoAKqgCqqgKqqAoAoCgCgKAKAoAoCqqgKqqAKqgAqqAAAAAqqoCqqgKqqAqqoCigAKKAAooACigAKqAAqoAAqAACoAAAAAAqqACqoAqqoCqqgKAKAoAoCgKgKAqAqqoCqqgCqqAKqoAAAAKqqAqqoCqqgKqqAooACigAKKgAoqACqqgKqqAKioAqKgAAAAKgoAqCgKqKAqooCiigKKKAoooCiigKKqAoqoCgqAKCoAAAACgAAKAAAoAACgAAKqqAqqoCqqgKqqAoAACgAAKAAAoAAAAAAKqoAqqgCqqgKqqAAAoAACgAAKAAAoCqqgKqqAqqgCqqAAAAAqqACqoAKqoAqqgAAKgAAqAACoAAKgKqoAqqgCqoAKqgAAAACqqgKqqAqqoCqqgACoAAKgACoAAKgAAKgAAqAKqqAqqoCqqgKqqAAAACoKgKgqAqqoCqqgAqgACqAAKoAAqgAqqoCqqgKgqAqCoAAAAKoAAqgACqgAKqAAAqoACqgAKqAAqoCqgAKqAAqgACqAAAAAAoCoCgKgKCqAoKoCiqgKKqAqooCqigKoKAqgoCoCgKgKAAAACqqgKqqAqqoCqqgKAKAoAoAAAAKAAAoAACoAAKgAAKgAAqAAAqAACoAACoAAKgAAKgAAqAAAqAACoAACgAAKAAAACgCgKAKAqqoCqqgKqqAqqoAAA"), 32, atob("DQULDw0RDQUHBw0NBQ0FEQ0FDQ0NDQ0NDQ0FBQsNCw0RDQ0NDQ0NDQ0NDQ0NDw0NDQ0NDQ0NDQ8NDQ0HEQc="), 22+(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 - 10; + g.reset(); + // work out locale-friendly date/time + var date = new Date(); + var timeStr = require("locale").time(date,1).trim(); + var dateStr = require("locale").date(date).toUpperCase(); + + + // draw time + g.setFontAlign(0,0).setFont("Undo:3"); + g.clearRect(0,y-30,g.getWidth(),y+30); // clear the background + g.drawString(timeStr,x,y); + // draw date + y += 40; + g.setFontAlign(0,0).setFont("Undo"); + g.clearRect(0,y-10,g.getWidth(),y+20); // 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(); \ No newline at end of file diff --git a/apps/intclock/app.png b/apps/intclock/app.png new file mode 100644 index 000000000..dacbceb73 Binary files /dev/null and b/apps/intclock/app.png differ diff --git a/apps/intclock/metadata.json b/apps/intclock/metadata.json new file mode 100644 index 000000000..fff4c58b0 --- /dev/null +++ b/apps/intclock/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "intclock", + "name": "Interlaced Clock", + "version": "0.01", + "description": "A lightweight digital clock using an 'interlaced' font", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS","BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"intclock.app.js","url":"app.js"}, + {"name":"intclock.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/intclock/screenshot.png b/apps/intclock/screenshot.png new file mode 100644 index 000000000..33a459f1f Binary files /dev/null and b/apps/intclock/screenshot.png differ diff --git a/apps/locale/ChangeLog b/apps/locale/ChangeLog index 39b825e02..2dbb4febb 100644 --- a/apps/locale/ChangeLog +++ b/apps/locale/ChangeLog @@ -7,7 +7,7 @@ 0.06: Remove translations if not required Ensure 'on' is always supplied for translations 0.07: Improve handling of non-ASCII characters (fix #469) -0.08: Added Mavigation units and en_NAV +0.08: Added Navigation units and en_NAV 0.09: Added New Zealand en_NZ 0.10: Apply 12hour setting to time 0.11: Added translations for nl_NL and changes one formatting diff --git a/apps/matrixclock/ChangeLog b/apps/matrixclock/ChangeLog index 23819e26c..52f705301 100644 --- a/apps/matrixclock/ChangeLog +++ b/apps/matrixclock/ChangeLog @@ -1,3 +1,4 @@ 0.01: Initial Release 0.02: Support for Bangle 2 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". diff --git a/apps/matrixclock/matrixclock.js b/apps/matrixclock/matrixclock.js index e116280af..2e4ba1ac4 100644 --- a/apps/matrixclock/matrixclock.js +++ b/apps/matrixclock/matrixclock.js @@ -239,15 +239,6 @@ Bangle.on('lcdPower', (on) => { } }); -Bangle.on('faceUp',function(up){ - //console.log("faceUp: " + up + " LCD: " + Bangle.isLCDOn()); - if (up && !Bangle.isLCDOn()) { - //console.log("faceUp and LCD off"); - clearTimers(); - Bangle.setLCDPower(true); - } -}); - g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); diff --git a/apps/matrixclock/metadata.json b/apps/matrixclock/metadata.json index 5e05698a5..122cee3a1 100644 --- a/apps/matrixclock/metadata.json +++ b/apps/matrixclock/metadata.json @@ -1,7 +1,7 @@ { "id": "matrixclock", "name": "Matrix Clock", - "version": "0.03", + "version": "0.04", "description": "inspired by The Matrix, a clock of the same style", "icon": "matrixclock.png", "screenshots": [{"url":"screenshot_matrix.png"}], diff --git a/apps/messages/ChangeLog b/apps/messages/ChangeLog index 4811cd19b..c39e7d6fe 100644 --- a/apps/messages/ChangeLog +++ b/apps/messages/ChangeLog @@ -15,7 +15,7 @@ 0.10: Respect the 'new' attribute if it was set from iOS integrations 0.11: Open app when touching the widget (Bangle.js 2 only) 0.12: Extra app-specific notification icons - New animated notifcationicon (instead of large blinking 'MESSAGES') + New animated notification icon (instead of large blinking 'MESSAGES') Added screenshots 0.13: Add /*LANG*/ comments for internationalisation Add 'Delete All' option to message options @@ -31,3 +31,11 @@ 0.19: Use a larger font for message text if it'll fit 0.20: Allow tapping on the body to show a scrollable view of the message and title in a bigger font (fix #1405, #1031) 0.21: Improve list readability on dark theme +0.22: Add Home Assistant icon + Allow repeat to be switched Off, so there is no buzzing repetition. + Also gave the widget a pixel more room to the right +0.23: Change message colors to match current theme instead of using green + Now attempt to use Large/Big/Medium fonts, and allow minimum font size to be configured +0.24: Remove left-over debug statement +0.25: Fix widget memory usage issues if message received and watch repeatedly calls Bangle.drawWidgets (fix #1550) +0.26: Setting to auto-open music diff --git a/apps/messages/README.md b/apps/messages/README.md index a355a58ac..655c549b9 100644 --- a/apps/messages/README.md +++ b/apps/messages/README.md @@ -13,9 +13,13 @@ and `Messages`: * `Vibrate` - This is the pattern of buzzes that should be made when a new message 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. +* `Unread Timer` - When a new message is received 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 +is chosen if there isn't much message text, but this specifies the smallest the font should get before +it starts getting clipped. +* `Auto-Open Music` - Should the app automatically open when the phone starts playing music? ## New Messages diff --git a/apps/messages/app.js b/apps/messages/app.js index 4aaf97369..403f9b5d8 100644 --- a/apps/messages/app.js +++ b/apps/messages/app.js @@ -21,13 +21,13 @@ */ var Layout = require("Layout"); +var settings = require('Storage').readJSON("messages.settings.json", true) || {}; var fontSmall = "6x8"; var fontMedium = g.getFonts().includes("6x15")?"6x15":"6x8:2"; var fontBig = g.getFonts().includes("12x20")?"12x20":"6x8:2"; var fontLarge = g.getFonts().includes("6x15")?"6x15:2":"6x8:4"; -var colBg = g.theme.dark ? "#141":"#4f4"; -var colSBg1 = g.theme.dark ? "#121":"#cFc"; -var colSBg2 = g.theme.dark ? "#000":"#9F9"; +var active; // active screen +var openMusic = false; // go back to music screen after we handle something else? // hack for 2v10 firmware's lack of ':size' font handling try { g.setFont("6x8:2"); @@ -52,10 +52,14 @@ var MESSAGES = require("Storage").readJSON("messages.json",1)||[]; if (!Array.isArray(MESSAGES)) MESSAGES=[]; var onMessagesModified = function(msg) { // TODO: if new, show this new one - if (msg && msg.new && !((require('Storage').readJSON('setting.json', 1) || {}).quiet)) { + if (msg && msg.id!=="music" && msg.new && !((require('Storage').readJSON('setting.json', 1) || {}).quiet)) { if (WIDGETS["messages"]) WIDGETS["messages"].buzz(); else Bangle.buzz(); } + if (msg && msg.id=="music") { + if (msg.state && msg.state!="play") openMusic = false; // no longer playing music to go back to + if (active!="music") return; // don't open music over other screens + } showMessage(msg&&msg.id); }; function saveMessages() { @@ -77,12 +81,17 @@ function getPosImage() { function getNegImage() { return atob("FhaBADAAMeAB78AP/4B/fwP4/h/B/P4D//AH/4AP/AAf4AB/gAP/AB/+AP/8B/P4P4fx/A/v4B//AD94AHjAAMA="); } +/* +* icons should be 24x24px with 1bpp colors and transparancy +*/ function getMessageImage(msg) { if (msg.img) return atob(msg.img); var s = (msg.src||"").toLowerCase(); + if (s=="alarm" || s =="alarmclockreceiver") return atob("GBjBAP////8AAAAAAAACAEAHAOAefng5/5wTgcgHAOAOGHAMGDAYGBgYGBgYGBgYGBgYDhgYBxgMATAOAHAHAOADgcAB/4AAfgAAAAAAAAA="); if (s=="calendar") return atob("GBiBAAAAAAAAAAAAAA//8B//+BgAGBgAGBgAGB//+B//+B//+B9m2B//+B//+Btm2B//+B//+Btm+B//+B//+A//8AAAAAAAAAAAAA=="); if (s=="facebook") return getFBIcon(); if (s=="hangouts") return atob("FBaBAAH4AH/gD/8B//g//8P//H5n58Y+fGPnxj5+d+fmfj//4//8H//B//gH/4A/8AA+AAHAABgAAAA="); + 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=="gmail") return getNotificationImage(); if (s=="google home") return atob("GBiCAAAAAAAAAAAAAAAAAAAAAoAAAAAACqAAAAAAKqwAAAAAqroAAAACquqAAAAKq+qgAAAqr/qoAACqv/6qAAKq//+qgA6r///qsAqr///6sAqv///6sAqv///6sAqv///6sA6v///6sA6v///qsA6qqqqqsA6qqqqqsA6qqqqqsAP7///vwAAAAAAAAAAAAAAAAA=="); @@ -93,6 +102,7 @@ function getMessageImage(msg) { if (s=="skype") return atob("GhoBB8AAB//AA//+Af//wH//+D///w/8D+P8Afz/DD8/j4/H4fP5/A/+f4B/n/gP5//B+fj8fj4/H8+DB/PwA/x/A/8P///B///gP//4B//8AD/+AAA+AA=="); if (s=="slack") return atob("GBiBAAAAAAAAAABAAAHvAAHvAADvAAAPAB/PMB/veD/veB/mcAAAABzH8B3v+B3v+B3n8AHgAAHuAAHvAAHvAADGAAAAAAAAAAAAAA=="); if (s=="sms message") return getNotificationImage(); + if (s=="threema") return atob("GBjB/4Yx//8AAAAAAAAAAAAAfgAB/4AD/8AH/+AH/+AP//AP2/APw/APw/AHw+AH/+AH/8AH/4AH/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="); 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=="telegram") return atob("GBiBAAAAAAAAAAAAAAAAAwAAHwAA/wAD/wAf3gD/Pgf+fh/4/v/z/P/H/D8P/Acf/AM//AF/+AF/+AH/+ADz+ADh+ADAcAAAMAAAAA=="); 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=="); @@ -104,6 +114,7 @@ function getMessageImage(msg) { function getMessageImageCol(msg,def) { return { // generic colors, using B2-safe colors + "alarm": "#fff", "calendar": "#f00", "mail": "#ff0", "music": "#f0f", @@ -114,20 +125,23 @@ function getMessageImageCol(msg,def) { "facebook": "#4267b2", "gmail": "#ea4335", "google home": "#fbbc05", + "home assistant": "#fff", // ha-blue is #41bdf5, but that's the background "hangouts": "#1ba261", "instagram": "#dd2a7b", "messenger": "#0078ff", "outlook mail": "#0072c6", "skype": "#00aff0", "slack": "#e51670", + "threema": "#000", "telegram": "#0088cc", "twitter": "#1da1f2", "whatsapp": "#4fce5d", - "wordfeud": "#dcc8bd", + "wordfeud": "#e7d3c7", }[(msg.src||"").toLowerCase()]||(def !== undefined?def:g.theme.fg); } function showMapMessage(msg) { + active = "map"; var m; var distance, street, target, eta; m=msg.title.match(/(.*) - (.*)/); @@ -141,8 +155,8 @@ function showMapMessage(msg) { eta = m[2]; } else target=msg.body; layout = new Layout({ type:"v", c: [ - {type:"txt", font:fontMedium, label:target, bgCol:colBg, fillx:1, pad:2 }, - {type:"h", bgCol:colBg, fillx:1, c: [ + {type:"txt", font:fontMedium, label:target, bgCol:g.theme.bg2, col: g.theme.fg2, fillx:1, pad:2 }, + {type:"h", bgCol:g.theme.bg2, col: g.theme.fg2, fillx:1, c: [ {type:"txt", font:"6x8", label:"Towards" }, {type:"txt", font:fontLarge, label:street } ]}, @@ -161,32 +175,67 @@ function showMapMessage(msg) { msg.new = false; saveMessages(); layout = undefined; - checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1}); + checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1,openMusic:0}); }); } +var updateLabelsInterval; function showMusicMessage(msg) { + active = "music"; + openMusic = msg.state=="play"; + var trackScrollOffset = 0; + var artistScrollOffset = 0; + var albumScrollOffset = 0; + var trackName = ''; + var artistName = ''; + var albumName = ''; + function fmtTime(s) { var m = Math.floor(s/60); s = (parseInt(s%60)).toString().padStart(2,0); return m+":"+s; } + function reduceStringAndPad(text, offset, maxLen) { + var sliceLength = offset + maxLen > text.length ? text.length - offset : maxLen; + return text.substr(offset, sliceLength).padEnd(maxLen, " "); + } + function back() { + clearInterval(updateLabelsInterval); + updateLabelsInterval = undefined; + openMusic = false; + var wasNew = msg.new; msg.new = false; saveMessages(); layout = undefined; - checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1}); + if (wasNew) checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:0,openMusic:0}); + else checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0}); } + function updateLabels() { + trackName = reduceStringAndPad(msg.track, trackScrollOffset, 13); + artistName = reduceStringAndPad(msg.artist, artistScrollOffset, 21); + albumName = reduceStringAndPad(msg.album, albumScrollOffset, 21); + + trackScrollOffset++; + artistScrollOffset++; + albumScrollOffset++; + + if ((trackScrollOffset + 13) > msg.track.length) trackScrollOffset = 0; + if ((artistScrollOffset + 21) > msg.artist.length) artistScrollOffset = 0; + if ((albumScrollOffset + 21) > msg.album.length) albumScrollOffset = 0; + } + updateLabels(); + layout = new Layout({ type:"v", c: [ - {type:"h", fillx:1, bgCol:colBg, c: [ + {type:"h", fillx:1, bgCol:g.theme.bg2, col: g.theme.fg2, c: [ { type:"btn", src:getBackImage, cb:back }, { type:"v", fillx:1, c: [ - { type:"txt", font:fontMedium, label:msg.artist, pad:2 }, - { type:"txt", font:fontMedium, label:msg.album, pad:2 } + { type:"txt", font:fontMedium, bgCol:g.theme.bg2, label:artistName, pad:2, id:"artist" }, + { type:"txt", font:fontMedium, bgCol:g.theme.bg2, label:albumName, pad:2, id:"album" } ]} ]}, - {type:"txt", font:fontLarge, label:msg.track, fillx:1, filly:1, pad:2 }, + {type:"txt", font:fontLarge, bgCol:g.theme.bg, label:trackName, fillx:1, filly:1, pad:2, id:"track" }, Bangle.musicControl?{type:"h",fillx:1, c: [ {type:"btn", pad:8, label:"\0"+atob("FhgBwAADwAAPwAA/wAD/gAP/gA//gD//gP//g///j///P//////////P//4//+D//gP/4A/+AD/gAP8AA/AADwAAMAAA"), cb:()=>Bangle.musicControl("play")}, // play {type:"btn", pad:8, label:"\0"+atob("EhaBAHgHvwP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP3gHg"), cb:()=>Bangle.musicControl("pause")}, // pause @@ -196,9 +245,18 @@ function showMusicMessage(msg) { ]}); g.clearRect(Bangle.appRect); layout.render(); + + updateLabelsInterval = setInterval(function() { + updateLabels(); + layout.artist.label = artistName; + layout.album.label = albumName; + layout.track.label = trackName; + layout.render(); + }, 400); } function showMessageScroller(msg) { + active = "scroller"; var bodyFont = fontBig; g.setFont(bodyFont); var lines = []; @@ -212,7 +270,9 @@ function showMessageScroller(msg) { // a function to draw a menu item draw : function(idx, r) { // FIXME: in 2v13 onwards, clearRect(r) will work fine. There's a bug in 2v12 - g.setBgColor(idx=lines.length-2) @@ -226,6 +286,7 @@ function showMessageScroller(msg) { } function showMessageSettings(msg) { + active = "settings"; E.showMenu({"":{"title":/*LANG*/"Message"}, "< Back" : () => showMessage(msg.id), /*LANG*/"View Message" : () => { @@ -234,12 +295,12 @@ function showMessageSettings(msg) { /*LANG*/"Delete" : () => { MESSAGES = MESSAGES.filter(m=>m.id!=msg.id); saveMessages(); - checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0}); + checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0}); }, /*LANG*/"Mark Unread" : () => { msg.new = true; saveMessages(); - checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0}); + checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0}); }, /*LANG*/"Delete all messages" : () => { E.showPrompt(/*LANG*/"Are you sure?", {title:/*LANG*/"Delete All Messages"}).then(isYes => { @@ -247,7 +308,7 @@ function showMessageSettings(msg) { MESSAGES = []; saveMessages(); } - checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0}); + checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0}); }); }, }); @@ -255,30 +316,53 @@ function showMessageSettings(msg) { function showMessage(msgid) { var msg = MESSAGES.find(m=>m.id==msgid); - if (!msg) return checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:0}); // go home if no message found - if (msg.src=="Maps") { - cancelReloadTimeout(); // don't auto-reload to clock now - return showMapMessage(msg); + if (updateLabelsInterval) { + clearInterval(updateLabelsInterval); + updateLabelsInterval=undefined; } + if (!msg) return checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:0,openMusic:openMusic}); // go home if no message found if (msg.id=="music") { cancelReloadTimeout(); // don't auto-reload to clock now return showMusicMessage(msg); } + if (msg.src=="Maps") { + cancelReloadTimeout(); // don't auto-reload to clock now + return showMapMessage(msg); + } + active = "message"; // Normal text message display var title=msg.title, titleFont = fontLarge, lines; if (title) { var w = g.getWidth()-48; - if (g.setFont(titleFont).stringWidth(title) > w) - titleFont = fontMedium; + if (g.setFont(titleFont).stringWidth(title) > w) { + titleFont = fontBig; + if (settings.fontSize!=1 && g.setFont(titleFont).stringWidth(title) > w) + titleFont = fontMedium; + } if (g.setFont(titleFont).stringWidth(title) > w) { lines = g.wrapString(title, w); title = (lines.length>2) ? lines.slice(0,2).join("\n")+"..." : lines.join("\n"); } } + // If body of message is only two lines long w/ large font, use large font. + var body=msg.body, bodyFont = fontLarge; + if (body) { + var w = g.getWidth()-10; + if (g.setFont(bodyFont).stringWidth(body) > w * 2) { + bodyFont = fontBig; + if (settings.fontSize!=1 && g.setFont(bodyFont).stringWidth(body) > w * 3) + bodyFont = fontMedium; + } + if (g.setFont(bodyFont).stringWidth(body) > w) { + lines = g.setFont(bodyFont).wrapString(msg.body, w); + var maxLines = Math.floor((g.getHeight()-110) / g.getFontHeight()); + body = (lines.length>maxLines) ? lines.slice(0,maxLines).join("\n")+"..." : lines.join("\n"); + } + } function goBack() { msg.new = false; saveMessages(); // read mail cancelReloadTimeout(); // don't auto-reload to clock now - checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:0}); + checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:0,openMusic:openMusic}); } var buttons = [ {type:"btn", src:getBackImage(), cb:goBack} // back @@ -289,7 +373,7 @@ function showMessage(msgid) { msg.new = false; saveMessages(); cancelReloadTimeout(); // don't auto-reload to clock now Bangle.messageResponse(msg,true); - checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1}); + checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1,openMusic:openMusic}); }}); } if (msg.negative) { @@ -298,30 +382,20 @@ function showMessage(msgid) { msg.new = false; saveMessages(); cancelReloadTimeout(); // don't auto-reload to clock now Bangle.messageResponse(msg,false); - checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1}); + checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1,openMusic:openMusic}); }}); } - // If body of message is only two lines long w/ large font, use large font. - var body=msg.body, bodyFont = fontLarge, lines; - if (body) { - var w = g.getWidth()-48; - if (g.setFont(bodyFont).stringWidth(body) > w * 2) - bodyFont = fontMedium; - if (g.setFont(bodyFont).stringWidth(body) > w) { - lines = g.setFont(bodyFont).wrapString(msg.body, g.getWidth()-10); - body = (lines.length>4) ? lines.slice(0,4).join("\n")+"..." : lines.join("\n"); - } - } + layout = new Layout({ type:"v", c: [ - {type:"h", fillx:1, bgCol:colBg, c: [ + {type:"h", fillx:1, bgCol:g.theme.bg2, col: g.theme.fg2, c: [ { type:"btn", src:getMessageImage(msg), col:getMessageImageCol(msg), pad: 3, cb:()=>{ cancelReloadTimeout(); // don't auto-reload to clock now showMessageSettings(msg); }}, { type:"v", fillx:1, c: [ - {type:"txt", font:fontSmall, label:msg.src||/*LANG*/"Message", bgCol:colBg, fillx:1, pad:2, halign:1 }, - title?{type:"txt", font:titleFont, label:title, bgCol:colBg, fillx:1, pad:2 }:{}, + {type:"txt", font:fontSmall, label:msg.src||/*LANG*/"Message", bgCol:g.theme.bg2, col: g.theme.fg2, fillx:1, pad:2, halign:1 }, + title?{type:"txt", font:titleFont, label:title, bgCol:g.theme.bg2, col: g.theme.fg2, fillx:1, pad:2 }:{}, ]}, ]}, {type:"txt", font:bodyFont, label:body, fillx:1, filly:1, pad:2, cb:()=>{ @@ -357,24 +431,27 @@ function checkMessages(options) { return load(); } // we have >0 messages - var newMessages = MESSAGES.filter(m=>m.new); + 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); + // 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'); // no new messages - go to clock? if (options.clockIfAllRead && newMessages.length==0) return load(); // we don't have to time out of this screen... cancelReloadTimeout(); + active = "main"; // Otherwise show a menu E.showScroller({ h : 48, c : Math.max(MESSAGES.length+1,3), // workaround for 2v10.219 firmware (min 3 not needed for 2v11) draw : function(idx, r) {"ram" var msg = MESSAGES[idx-1]; - if (msg && msg.new) g.setBgColor(colBg); - else g.setBgColor((idx&1) ? colSBg1 : colSBg2); - g.clearRect(r.x,r.y,r.x+r.w-1,r.y+r.h-1).setColor(g.theme.fg); + if (msg && msg.new) g.setBgColor(g.theme.bgH).setColor(g.theme.fgH); + else g.setColor(g.theme.fg); if (idx==0) msg = {id:"back", title:"< Back"}; if (!msg) return; var x = r.x+2, title = msg.title, body = msg.body; @@ -389,18 +466,20 @@ function checkMessages(options) { .setColor(fg); // only color the icon x += 50; } - var m = msg.title+"\n"+msg.body; - if (msg.src) g.setFontAlign(1,1).setFont("6x8").drawString(msg.src, r.x+r.w-2, r.y+r.h-2); + var m = msg.title+"\n"+msg.body, longBody=false; if (title) g.setFontAlign(-1,-1).setFont(fontBig).drawString(title, x,r.y+2); if (body) { g.setFontAlign(-1,-1).setFont("6x8"); - var l = g.wrapString(body, r.w-14); + 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); } + 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 }, select : idx => { if (idx==0) load(); @@ -420,12 +499,14 @@ g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); setTimeout(() => { - var unreadTimeoutSecs = (require('Storage').readJSON("messages.settings.json", true) || {}).unreadTimeout; + var unreadTimeoutSecs = settings.unreadTimeout; if (unreadTimeoutSecs===undefined) unreadTimeoutSecs=60; if (unreadTimeoutSecs) unreadTimeout = setTimeout(function() { print("Message not seen - reloading"); load(); }, unreadTimeoutSecs*1000); - checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:1}); + // only openMusic on launch if music is new + var newMusic = MESSAGES.some(m=>m.id==="music"&&m.new); + checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:1,openMusic:newMusic&&settings.openMusic}); },10); // if checkMessages wants to 'load', do that diff --git a/apps/messages/lib.js b/apps/messages/lib.js index 32dff78ba..0f514a73d 100644 --- a/apps/messages/lib.js +++ b/apps/messages/lib.js @@ -1,3 +1,10 @@ +function openMusic() { + // only read settings file for first music message + if ("undefined"==typeof exports._openMusic) { + exports._openMusic = !!((require('Storage').readJSON("messages.settings.json", true) || {}).openMusic); + } + return exports._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 @@ -26,6 +33,9 @@ exports.pushMessage = function(event) { messages.unshift(event); // add new messages to the beginning } else Object.assign(messages[mIdx], event); + if (event.id=="music" && messages[mIdx].state=="play") { + messages[mIdx].new = true; // new track, or playback (re)started + } } require("Storage").writeJSON("messages.json",messages); // if in app, process immediately @@ -34,8 +44,12 @@ exports.pushMessage = function(event) { if (event.t=="remove" && !messages.some(m=>m.new)) { if (global.WIDGETS && WIDGETS.messages) WIDGETS.messages.hide(); } - // ok, saved now - we only care if it's new - if (event.t!="add") { + // 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; diff --git a/apps/messages/metadata.json b/apps/messages/metadata.json index 6834693ae..a3d3f4179 100644 --- a/apps/messages/metadata.json +++ b/apps/messages/metadata.json @@ -1,8 +1,8 @@ { "id": "messages", "name": "Messages", - "version": "0.21", - "description": "App to display notifications from iOS and Gadgetbridge", + "version": "0.26", + "description": "App to display notifications from iOS and Gadgetbridge/Android", "icon": "app.png", "type": "app", "tags": "tool,system", diff --git a/apps/messages/screenshot.png b/apps/messages/screenshot.png index a95045400..a64416f7d 100644 Binary files a/apps/messages/screenshot.png and b/apps/messages/screenshot.png differ diff --git a/apps/messages/settings.js b/apps/messages/settings.js index c865a37fb..99843602b 100644 --- a/apps/messages/settings.js +++ b/apps/messages/settings.js @@ -4,6 +4,8 @@ if (settings.vibrate===undefined) settings.vibrate="."; if (settings.repeat===undefined) settings.repeat=4; if (settings.unreadTimeout===undefined) settings.unreadTimeout=60; + settings.openMusic=!!settings.openMusic; + settings.maxUnreadTimeout=240; return settings; } function updateSetting(setting, value) { @@ -13,7 +15,6 @@ } var vibPatterns = [/*LANG*/"Off", ".", "-", "--", "-.-", "---"]; - var currentVib = settings().vibrate; var mainmenu = { "" : { "title" : /*LANG*/"Messages" }, "< Back" : back, @@ -27,16 +28,27 @@ }, /*LANG*/'Repeat': { value: settings().repeat, - min: 2, max: 10, - format: v => v+"s", + min: 0, max: 10, + format: v => v?v+"s":/*LANG*/"Off", onchange: v => updateSetting("repeat", v) }, /*LANG*/'Unread timer': { value: settings().unreadTimeout, - min: 0, max: 240, step : 10, + min: 0, max: settings().maxUnreadTimeout, step : 10, format: v => v?v+"s":/*LANG*/"Off", onchange: v => updateSetting("unreadTimeout", v) }, + /*LANG*/'Min Font': { + value: 0|settings().fontSize, + min: 0, max: 1, + format: v => [/*LANG*/"Small",/*LANG*/"Medium"][v], + onchange: v => updateSetting("fontSize", v) + }, + /*LANG*/'Auto-Open Music': { + value: !!settings().openMusic, + format: v => v?/*LANG*/'Yes':/*LANG*/'No', + onchange: v => updateSetting("openMusic", v) + }, }; E.showMenu(mainmenu); }) diff --git a/apps/messages/widget.js b/apps/messages/widget.js index 1239ef262..7abb415c3 100644 --- a/apps/messages/widget.js +++ b/apps/messages/widget.js @@ -1,18 +1,22 @@ -WIDGETS["messages"]={area:"tl", width:0, iconwidth:23, +WIDGETS["messages"]={area:"tl", width:0, iconwidth:24, draw:function() { + // If we had a setTimeout queued from the last time we were called, remove it + if (WIDGETS["messages"].i) { + clearTimeout(WIDGETS["messages"].i); + delete WIDGETS["messages"].i; + } Bangle.removeListener('touch', this.touch); if (!this.width) return; var c = (Date.now()-this.t)/1000; g.reset().clearRect(this.x, this.y, this.x+this.width, this.y+this.iconwidth); g.drawImage((c&1) ? atob("GBiBAAAAAAAAAAAAAAAAAAAAAB//+DAADDAADDAADDwAPD8A/DOBzDDn/DA//DAHvDAPvjAPvjAPvjAPvh///gf/vAAD+AAB8AAAAA==") : atob("GBiBAAAAAAAAAAAAAAAAAAAAAB//+D///D///A//8CP/xDj/HD48DD+B8D/D+D/3vD/vvj/vvj/vvj/vvh/v/gfnvAAD+AAB8AAAAA=="), this.x, this.y); - //if (c<60) Bangle.setLCDPower(1); // keep LCD on for 1 minute let settings = require('Storage').readJSON("messages.settings.json", true) || {}; 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 } - setTimeout(()=>WIDGETS["messages"].draw(), 1000); + WIDGETS["messages"].i=setTimeout(()=>WIDGETS["messages"].draw(), 1000); if (process.env.HWVERSION>1) Bangle.on('touch', this.touch); },show:function(quiet) { WIDGETS["messages"].t=Date.now(); // first time @@ -46,5 +50,5 @@ 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. */ if (global.MESSAGES===undefined) (function() { var messages = require("Storage").readJSON("messages.json",1)||[]; - if (messages.some(m=>m.new)) WIDGETS["messages"].show(true); -})(); \ No newline at end of file + if (messages.some(m=>m.new&&m.id!="music")) WIDGETS["messages"].show(true); +})(); diff --git a/apps/neonx/ChangeLog b/apps/neonx/ChangeLog index af7f83942..2e815a449 100644 --- a/apps/neonx/ChangeLog +++ b/apps/neonx/ChangeLog @@ -1 +1,4 @@ 0.01: Initial release +0.02: Optional fullscreen mode +0.03: Optional show lock status via color +0.04: Ensure that widgets are always hidden in fullscreen mode \ No newline at end of file diff --git a/apps/neonx/README.md b/apps/neonx/README.md index d836dfab3..ffb3c3f2c 100644 --- a/apps/neonx/README.md +++ b/apps/neonx/README.md @@ -4,8 +4,8 @@ |---------------------------------|--------------------------------------| |
Neon X
|
Neon IO X
| -This is a clock based on Pebble's Neon X and Neon IO X watchfaces by Sam Jerichow. -Can be switched between in the Settings menu, which can be accessed through +This is a clock based on Pebble's Neon X and Neon IO X watchfaces by Sam Jerichow. +Can be switched between in the Settings menu, which can be accessed through the app/widget settings menu of the Bangle.js ## Settings @@ -14,7 +14,14 @@ the app/widget settings menu of the Bangle.js Activate the Neon IO X clock look, a bit hard to read until one gets used to it. ### Thickness -The thickness of watch lines, from 1 to 5. +The thickness of watch lines, from 1 to 6. ### Date on touch Shows the current date as DD MM on touch and reverts back to time after 5 seconds or with another touch. + +### Fullscreen +Shows the watchface in fullscreen mode. +Note: In fullscreen mode, widgets are hidden, but still loaded. + +### Show lock status +If enabled, color changes when unlocked to detect the lock state easily. \ No newline at end of file diff --git a/apps/neonx/metadata.json b/apps/neonx/metadata.json index 41b16d11b..840e5b82e 100644 --- a/apps/neonx/metadata.json +++ b/apps/neonx/metadata.json @@ -2,7 +2,7 @@ "id": "neonx", "name": "Neon X & IO X Clock", "shortName": "Neon X Clock", - "version": "0.01", + "version": "0.04", "description": "Pebble Neon X & Neon IO X for Bangle.js", "icon": "neonx.png", "type": "clock", diff --git a/apps/neonx/neonx.app.js b/apps/neonx/neonx.app.js index 967fc8582..4b9231b0e 100644 --- a/apps/neonx/neonx.app.js +++ b/apps/neonx/neonx.app.js @@ -8,6 +8,19 @@ * Created: February 2022 */ +let settings = { + thickness: 4, + io: 0, + showDate: 1, + fullscreen: false, + showLock: false, +}; +let saved_settings = require('Storage').readJSON('neonx.json', 1) || settings; +for (const key in saved_settings) { + settings[key] = saved_settings[key] +} + + const digits = { 0:[[15,15,85,15,85,85,15,85,15,15]], 1:[[85,15,85,85]], @@ -21,6 +34,7 @@ const digits = { 9:[[15,50,15,15,85,15,85,85,15,85]], }; + const colors = { x: [ ["#FF00FF", "#00FFFF"], @@ -31,16 +45,14 @@ const colors = { ["#00FF00", "#00FFFF"] ] }; - const is12hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"]||false; const screenWidth = g.getWidth(); +const screenHeight = g.getHeight(); const halfWidth = screenWidth / 2; const scale = screenWidth / 240; -const REFRESH_RATE = 10E3; - -let interval = 0; let showingDate = false; + function drawLine(poly, thickness){ for (let i = 0; i < poly.length; i = i + 2){ if (poly[i + 2] === undefined) { @@ -58,34 +70,35 @@ function drawLine(poly, thickness){ } } -let settings = require('Storage').readJSON('neonx.json', 1); - -if (!settings) { - settings = { - thickness: 4, - io: 0, - showDate: 1 - }; -} function drawClock(num){ let tx, ty; + if(settings.fullscreen){ + g.clearRect(0,0,screenWidth,screenHeight); + } else { + g.clearRect(0,24,240,240); + } + for (let x = 0; x <= 1; x++) { for (let y = 0; y <= 1; y++) { const current = ((y + 1) * 2 + x - 1); let newScale = scale; - g.setColor(colors[settings.io ? 'io' : 'x'][y][x]); + let xc = settings.showLock && !Bangle.isLocked() ? Math.abs(x-1) : x; + let c = colors[settings.io ? 'io' : 'x'][y][xc]; + g.setColor(c); if (!settings.io) { - tx = (x * 100 + 18) * newScale; - ty = (y * 100 + 32) * newScale; + newScale *= settings.fullscreen ? 1.20 : 1.0; + let dx = settings.fullscreen ? 0 : 18 + tx = (x * 100 + dx) * newScale; + ty = (y * 100 + dx*2) * newScale; } else { - newScale = 0.33 + current * 0.4; + newScale = 0.33 + current * (settings.fullscreen ? 0.48 : 0.4); - tx = (halfWidth - 139) * newScale + halfWidth; - ty = (halfWidth - 139) * newScale + halfWidth + 12; + tx = (halfWidth - 139) * newScale + halfWidth + (settings.fullscreen ? 2 : 0); + ty = (halfWidth - 139) * newScale + halfWidth + (settings.fullscreen ? 2 : 12); } for (let i = 0; i < digits[num[y][x]].length; i++) { @@ -95,59 +108,81 @@ function drawClock(num){ } } + function draw(date){ + queueDraw(); + + // Depending on the settings, we clear all widgets or draw those. + if(settings.fullscreen){ + for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";} + } else { + Bangle.drawWidgets(); + } + + // Now lets draw the time/date let d = new Date(); let l1, l2; showingDate = date; if (date) { - setUpdateInt(0); - l1 = ('0' + (new Date()).getDate()).substr(-2); l2 = ('0' + ((new Date()).getMonth() + 1)).substr(-2); + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + setTimeout(_ => { draw(); - setUpdateInt(1); }, 5000); } else { l1 = ('0' + (d.getHours() % (is12hour ? 12 : 24))).substr(-2); l2 = ('0' + d.getMinutes()).substr(-2); } - g.clearRect(0,24,240,240); - drawClock([l1, l2]); } -function setUpdateInt(set){ - if (interval) { - clearInterval(interval); - } - if (set) { - interval = setInterval(draw, REFRESH_RATE); - } +/* + * Draw watch face + */ +var drawTimeout; +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); } -g.clear(1); - -Bangle.setUI("clock"); - -setUpdateInt(1); -draw(); +/* + * Event handlers + */ if (settings.showDate) { Bangle.on('touch', () => draw(!showingDate)); } Bangle.on('lcdPower', function(on){ - if (on){ + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + + if (on) { draw(); - setUpdateInt(1); - } else setUpdateInt(0); + } }); +Bangle.on('lock', function(isLocked) { + draw(); +}); + + +/* + * Draw first time + */ +g.clear(1); +Bangle.setUI("clock"); Bangle.loadWidgets(); -Bangle.drawWidgets(); + +draw(); \ No newline at end of file diff --git a/apps/neonx/neonx.settings.js b/apps/neonx/neonx.settings.js index 0e205e03b..e01ceb4d3 100644 --- a/apps/neonx/neonx.settings.js +++ b/apps/neonx/neonx.settings.js @@ -7,7 +7,9 @@ neonXSettings = { thickness: 4, io: 0, - showDate: 1 + showDate: 1, + fullscreen: false, + showLock: false, }; updateSettings(); @@ -17,7 +19,7 @@ if (!neonXSettings) resetSettings(); - let thicknesses = [1, 2, 3, 4, 5]; + let thicknesses = [1, 2, 3, 4, 5, 6]; const menu = { "" : { "title":"Neon X & IO"}, @@ -48,7 +50,23 @@ neonXSettings.showDate = v; updateSettings(); } - } + }, + 'Fullscreen': { + value: false | neonXSettings.fullscreen, + format: () => (neonXSettings.fullscreen ? 'Yes' : 'No'), + onchange: () => { + neonXSettings.fullscreen = !neonXSettings.fullscreen; + updateSettings(); + }, + }, + 'Show lock': { + value: false | neonXSettings.showLock, + format: () => (neonXSettings.showLock ? 'Yes' : 'No'), + onchange: () => { + neonXSettings.showLock = !neonXSettings.showLock; + updateSettings(); + }, + }, }; E.showMenu(menu); }) diff --git a/apps/pastel/ChangeLog b/apps/pastel/ChangeLog index d133697b3..a77fa758f 100644 --- a/apps/pastel/ChangeLog +++ b/apps/pastel/ChangeLog @@ -16,3 +16,4 @@ 0.14: incorporated lazybones idle timer, configuration settings to come 0.15: fixed tendancy for mylocation to default to London added setting to enable/disable idle timer warning +0.16: make check_idle boolean setting work properly with new B2 menu diff --git a/apps/pastel/metadata.json b/apps/pastel/metadata.json index da3c18eae..f04a7ae54 100644 --- a/apps/pastel/metadata.json +++ b/apps/pastel/metadata.json @@ -2,7 +2,7 @@ "id": "pastel", "name": "Pastel Clock", "shortName": "Pastel", - "version": "0.15", + "version": "0.16", "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.settings.js b/apps/pastel/pastel.settings.js index 26dafd271..afe461f15 100644 --- a/apps/pastel/pastel.settings.js +++ b/apps/pastel/pastel.settings.js @@ -38,38 +38,28 @@ }, }, 'Show Grid': { - value: s.grid, - format: () => (s.grid ? 'Yes' : 'No'), - onchange: () => { - s.grid = !s.grid; + value: !!s.grid, + format: v => v ? /*LANG*/"Yes":/*LANG*/"No", + onchange: v => { + s.grid = v; save(); }, }, 'Show Weather': { - value: s.weather, - format: () => (s.weather ? 'Yes' : 'No'), - onchange: () => { - s.weather = !s.weather; + value: !!s.weather, + format: v => v ? /*LANG*/"Yes":/*LANG*/"No", + onchange: v => { + s.weather = v; save(); }, }, - // for use when the new menu system goes live - /* 'Idle Warning': { - value: s.idle_check, - onchange : v => { + value: !!s.idle_check, + format: v => v ? /*LANG*/"Yes":/*LANG*/"No", + onchange: v => { s.idle_check = v; save(); }, - }, - */ - 'Idle Warning': { - value: s.idle_check, - format: () => (s.idle_check ? 'Yes' : 'No'), - onchange: () => { - s.idle_check = !s.idle_check; - save(); - }, } }) }) diff --git a/apps/pie/app-icon.js b/apps/pie/app-icon.js new file mode 100644 index 000000000..34a58225c --- /dev/null +++ b/apps/pie/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AH4A/ABl3ABQVJg4WLC/QWMC/4X/U7NVqIXTgtVC4MXC6QWBAAQYLCxQAGqByJIoQAKC8IWMJBIuNGBIXvCxxgIC/4XH1QAO0AX/C/4X/C/4X/C6Q")) diff --git a/apps/pie/app.js b/apps/pie/app.js new file mode 100644 index 000000000..69b67d3bd --- /dev/null +++ b/apps/pie/app.js @@ -0,0 +1,56 @@ +function end(){ + clearInterval(m); + clearWatch(w); + gfx.clear(); + gfx.setColor(1,0,0); + gfx.setFont("Vector30"); + gfx.drawString('Game over!\n Score: '+score+'\nPress BTN1', gfx.getWidth()*0.15,gfx.getHeight()*0.4); + setWatch(function(){init();}, BTN1); +} +function scrollX(){ + gfx.clearRect(0,gfx.getHeight()*(1/4),gfx.getWidth(),0); + gfx.scroll(0,gfx.getHeight()/4); + score++; + if(typeof(m) != undefined && score>0){ + clearInterval(m); + m = setInterval(scrollY,Math.abs(100/score+15-0.1*score));} + gfx.setColor(1,1,1); + gfx.drawString(score,gfx.getWidth()*(4.2/5),gfx.getHeight()*(0.5/5)); + gfx.setColor(Math.random(),Math.random(),Math.random()); + gfx.setColor(col[0],col[1],col[2]); + gfx.fillRect(colm[0],colm[1],colm[2],colm[3]); + col = [Math.random(),Math.random(),Math.random()]; + gfx.setColor(col[0],col[1],col[2]); + block[0] = gfx.getWidth(); +} +function scrollY(){ + block[0] -= 2; + block[2] = block[0]+colm[2]-colm[0]; + gfx.clearRect(block[2], block[1], gfx.getWidth(), block[3]); + gfx.fillRect(block[0],block[1],block[2],block[3]); + if(block[2]block[2] && colm[0] { let width = m[2] == '*' ? null : +m[2]; let c = null, o = 0; lines.forEach((line, l) => { - if (m = /^(<*)(=)([*\d]*)(=*)(>*)$/.exec(line) || /^(<*)(-)(.)(-*)(>*)$/.exec(line)) { + m = /^(<*)(=)([*\d]*)(=*)(>*)$/.exec(line) || /^(<*)(-)(.)(-*)(>*)$/.exec(line); + if (m) { const h = m[2] == '='; if (m[1].length > desc || h && m[1].length != desc) throw new Error('Invalid descender height at ' + l); diff --git a/apps/pooqround/resourcer.js b/apps/pooqround/resourcer.js index 17c35a40d..6b969a102 100644 --- a/apps/pooqround/resourcer.js +++ b/apps/pooqround/resourcer.js @@ -60,7 +60,8 @@ const prepFont = (name, data) => { let width = m[2] == '*' ? null : +m[2]; let c = null, o = 0; lines.forEach((line, l) => { - if (m = /^(<*)(=)([*\d]*)(=*)(>*)$/.exec(line) || /^(<*)(-)(.)(-*)(>*)$/.exec(line)) { + m = /^(<*)(=)([*\d]*)(=*)(>*)$/.exec(line) || /^(<*)(-)(.)(-*)(>*)$/.exec(line); + if (m) { const h = m[2] == '='; if (m[1].length > desc || h && m[1].length != desc) throw new Error('Invalid descender height at ' + l); diff --git a/apps/powermanager/ChangeLog b/apps/powermanager/ChangeLog new file mode 100644 index 000000000..8ccf678de --- /dev/null +++ b/apps/powermanager/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! +0.02: Allow forcing monotonic battery voltage/percentage diff --git a/apps/powermanager/README.md b/apps/powermanager/README.md new file mode 100644 index 000000000..434ec814e --- /dev/null +++ b/apps/powermanager/README.md @@ -0,0 +1,19 @@ +# Power manager + +Manages settings for charging. +Features: +* Warning threshold to be able to disconnect the charger at a given percentage +* Set the battery calibration offset. +* Force monotonic battery percentage or voltage + +## Internals + +Battery calibration offset is set by writing `batFullVoltage` in setting.json + +## TODO + +* Optionally keep battery history and show as graph + +## Creator + +[halemmerich](https://github.com/halemmerich) diff --git a/apps/powermanager/app.png b/apps/powermanager/app.png new file mode 100644 index 000000000..dc51aa01b Binary files /dev/null and b/apps/powermanager/app.png differ diff --git a/apps/powermanager/boot.js b/apps/powermanager/boot.js new file mode 100644 index 000000000..077e24413 --- /dev/null +++ b/apps/powermanager/boot.js @@ -0,0 +1,51 @@ +(function() { + var settings = Object.assign( + require('Storage').readJSON("powermanager.default.json", true) || {}, + require('Storage').readJSON("powermanager.json", true) || {} + ); + + if (settings.warnEnabled){ + print("Charge warning enabled"); + var chargingInterval; + + function handleCharging(charging){ + if (charging){ + if (chargingInterval) clearInterval(chargingInterval); + chargingInterval = setInterval(()=>{ + if (E.getBattery() > settings.warn){ + Bangle.buzz(1000); + } + }, 10000); + } + if (chargingInterval && !charging){ + clearInterval(chargingInterval); + chargingInterval = undefined; + } + } + + Bangle.on("charging",handleCharging); + handleCharging(Bangle.isCharging()); + } + + if (settings.forceMonoPercentage){ + var p = (E.getBattery()+E.getBattery()+E.getBattery()+E.getBattery())/4; + var op = E.getBattery; + E.getBattery = function() { + var current = Math.round((op()+op()+op()+op())/4); + if (Bangle.isCharging() && current > p) p = current; + if (!Bangle.isCharging() && current < p) p = current; + return p; + }; + } + + if (settings.forceMonoVoltage){ + var v = (NRF.getBattery()+NRF.getBattery()+NRF.getBattery()+NRF.getBattery())/4; + var ov = NRF.getBattery; + NRF.getBattery = function() { + var current = (ov()+ov()+ov()+ov())/4; + if (Bangle.isCharging() && current > v) v = current; + if (!Bangle.isCharging() && current < v) v = current; + return v; + }; + } +})(); diff --git a/apps/powermanager/default.json b/apps/powermanager/default.json new file mode 100644 index 000000000..6c929dc38 --- /dev/null +++ b/apps/powermanager/default.json @@ -0,0 +1,6 @@ +{ + "warnEnabled": false, + "warn": 96, + "forceMonoVoltage": false, + "forceMonoPercentage": false +} diff --git a/apps/powermanager/metadata.json b/apps/powermanager/metadata.json new file mode 100644 index 000000000..2bb531099 --- /dev/null +++ b/apps/powermanager/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "powermanager", + "name": "Power Manager", + "shortName": "Power Manager", + "version": "0.02", + "description": "Allow configuration of warnings and thresholds for battery charging and display.", + "icon": "app.png", + "type": "bootloader", + "tags": "tool", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"powermanager.boot.js","url":"boot.js"}, + {"name":"powermanager.settings.js","url":"settings.js"}, + {"name":"powermanager.default.json","url":"default.json"} + ] +} diff --git a/apps/powermanager/settings.js b/apps/powermanager/settings.js new file mode 100644 index 000000000..8af873e5f --- /dev/null +++ b/apps/powermanager/settings.js @@ -0,0 +1,139 @@ +(function(back) { + var systemsettings = require("Storage").readJSON("setting.json") || {}; + + 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("powermanager.default.json", true) || {}, + require('Storage').readJSON(FILE, true) || {} + ); + } + + var FILE = "powermanager.json"; + var settings; + readSettings(); + + var mainmenu = { + '': { + 'title': 'Power Manager' + }, + '< Back': back, + 'Monotonic percentage': { + value: !!settings.forceMonoPercentage, + format: v => settings.forceMonoPercentage ? "On" : "Off", + onchange: v => { + writeSettings("forceMonoPercentage", v); + } + }, + 'Monotonic voltage': { + value: !!settings.forceMonoVoltage, + format: v => settings.forceMonoVoltage ? "On" : "Off", + onchange: v => { + writeSettings("forceMonoVoltage", v); + } + }, + 'Charge warning': function() { + E.showMenu(submenu_chargewarn); + }, + 'Calibrate': function() { + E.showMenu(submenu_calibrate); + } + }; + + + function roundToDigits(number, stepsize) { + return Math.round(number / stepsize) * stepsize; + } + + function getCurrentVoltageDirect() { + return (analogRead(D3) + analogRead(D3) + analogRead(D3) + analogRead(D3)) / 4; + } + + var stepsize = 0.0002; + var full = 0.32; + + function getInitialCalibrationOffset() { + return roundToDigits(systemsettings.batFullVoltage - full, stepsize) || 0; + } + + + var submenu_calibrate = { + '': { + title: "Calibrate" + }, + '< Back': function() { + E.showMenu(mainmenu); + }, + 'Offset': { + min: -0.05, + max: 0.05, + step: stepsize, + value: getInitialCalibrationOffset(), + format: v => roundToDigits(v, stepsize).toFixed((""+stepsize).length - 2), + onchange: v => { + print(typeof v); + systemsettings.batFullVoltage = v + full; + require("Storage").writeJSON("setting.json", systemsettings); + } + }, + 'Auto': function() { + var newVoltage = getCurrentVoltageDirect(); + E.showAlert("Please charge fully before auto setting").then(() => { + E.showPrompt("Set current charge as full").then((r) => { + if (r) { + systemsettings.batFullVoltage = newVoltage; + require("Storage").writeJSON("setting.json", systemsettings); + //reset value shown in menu to the newly set one + submenu_calibrate.Offset.value = getInitialCalibrationOffset(); + E.showMenu(mainmenu); + } + }); + }); + }, + 'Clear': function() { + E.showPrompt("Clear charging offset?").then((r) => { + if (r) { + delete systemsettings.batFullVoltage; + require("Storage").writeJSON("setting.json", systemsettings); + //reset value shown in menu to the newly set one + submenu_calibrate.Offset.value = getInitialCalibrationOffset(); + E.showMenu(mainmenu); + } + }); + } + }; + + var submenu_chargewarn = { + '': { + title: "Charge warning" + }, + '< Back': function() { + E.showMenu(mainmenu); + }, + 'Enabled': { + value: !!settings.warnEnabled, + format: v => settings.warnEnabled ? "On" : "Off", + onchange: v => { + writeSettings("warnEnabled", v); + } + }, + 'Percentage': { + min: 80, + max: 100, + step: 2, + value: settings.warn, + format: v => v + "%", + onchange: v => { + writeSettings("warn", v); + } + } + }; + + E.showMenu(mainmenu); +}) diff --git a/apps/ptlaunch/ChangeLog b/apps/ptlaunch/ChangeLog index 68b7d3e1c..eec3610ed 100644 --- a/apps/ptlaunch/ChangeLog +++ b/apps/ptlaunch/ChangeLog @@ -4,4 +4,5 @@ 0.10: Improve the management of existing patterns: Draw the linked pattern on the left hand side of the app name within a scroller, similar to the default launcher. Slighlty clean up the code to make it less horrible. 0.11: Respect theme colors. Fix: Do not pollute global space with internal variables ans functions in boot.js 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/ \ No newline at end of file +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 diff --git a/apps/ptlaunch/boot.js b/apps/ptlaunch/boot.js index 19a8f16cb..748d564f3 100644 --- a/apps/ptlaunch/boot.js +++ b/apps/ptlaunch/boot.js @@ -76,6 +76,7 @@ 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 = {}; diff --git a/apps/ptlaunch/metadata.json b/apps/ptlaunch/metadata.json index 6c3870d24..0b6dce3d1 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.13", + "version": "0.14", "description": "Directly launch apps from the clock screen with custom patterns.", "icon": "app.png", "screenshots": [{"url":"manage_patterns_light.png"}], diff --git a/apps/qmsched/ChangeLog b/apps/qmsched/ChangeLog index c868b6668..94fcffe1a 100644 --- a/apps/qmsched/ChangeLog +++ b/apps/qmsched/ChangeLog @@ -5,4 +5,5 @@ 0.05: Avoid immediately redrawing widgets on load 0.06: Fix: don't try to redraw widget when widgets not loaded 0.07: Option to switch theme - Changed time selection to 5-minute intervals \ No newline at end of file + Changed time selection to 5-minute intervals +0.08: Support new Bangle.js 2 menu \ No newline at end of file diff --git a/apps/qmsched/app.js b/apps/qmsched/app.js index e05eff6a2..8cd0fa8d9 100644 --- a/apps/qmsched/app.js +++ b/apps/qmsched/app.js @@ -1,8 +1,8 @@ Bangle.loadWidgets(); Bangle.drawWidgets(); -const modeNames = ["Off", "Alarms", "Silent"]; - +const modeNames = [/*LANG*/"Off", /*LANG*/"Alarms", /*LANG*/"Silent"]; +const B2 = process.env.HWVERSION===2; // load global settings let bSettings = require('Storage').readJSON('setting.json',true)||{}; let current = 0|bSettings.quiet; @@ -109,34 +109,26 @@ function setAppQuietMode(mode) { let m; function showMainMenu() { - let menu = { - "": {"title": "Quiet Mode"}, - "< Exit": () => load() - }; - // "Current Mode""Silent" won't fit on Bangle.js 2 - menu["Current"+((process.env.HWVERSION===2) ? "" : " Mode")] = { + let menu = {"": {"title": /*LANG*/"Quiet Mode"},}; + menu[B2 ? /*LANG*/"< Back" : /*LANG*/"< Exit"] = () => {load();}; + menu[/*LANG*/"Current Mode"] = { value: current, min:0, max:2, wrap: true, - format: () => modeNames[current], + format: v => modeNames[v], onchange: require("qmsched").setMode, // library calls setAppMode(), which updates `current` }; scheds.sort((a, b) => (a.hr-b.hr)); scheds.forEach((sched, idx) => { - menu[formatTime(sched.hr)] = { - format: () => modeNames[sched.mode], // abuse format to right-align text - onchange: () => { - m.draw = ()=> {}; // prevent redraw of main menu over edit menu (needed because we abuse format/onchange) - showEditMenu(idx); - } - }; + menu[formatTime(sched.hr)] = () => { showEditMenu(idx); }; + menu[formatTime(sched.hr)].format = () => modeNames[sched.mode]+' >'; // this does nothing :-( }); - menu["Add Schedule"] = () => showEditMenu(-1); - menu["Switch Theme"] = { + menu[/*LANG*/"Add Schedule"] = () => showEditMenu(-1); + menu[/*LANG*/"Switch Theme"] = { value: !!get("switchTheme"), format: v => v ? /*LANG*/"Yes" : /*LANG*/"No", onchange: v => v ? set("switchTheme", v) : unset("switchTheme"), }; - menu["LCD Settings"] = () => showOptionsMenu(); + menu[/*LANG*/"LCD Settings"] = () => showOptionsMenu(); m = E.showMenu(menu); } @@ -150,25 +142,23 @@ function showEditMenu(index) { mins = Math.round((s.hr-hrs)*60); mode = s.mode; } - const menu = { - "": {"title": (isNew ? "Add" : "Edit")+" Schedule"}, - "< Cancel": () => showMainMenu(), - "Hours": { - value: hrs, - min:0, max:23, wrap:true, - onchange: v => {hrs = v;}, - }, - "Minutes": { - value: mins, - min:0, max:55, step:5, wrap:true, - onchange: v => {mins = v;}, - }, - "Switch to": { - value: mode, - min:0, max:2, wrap:true, - format: v => modeNames[v], - onchange: v => {mode = v;}, - }, + let menu = {"": {"title": (isNew ? /*LANG*/"Add Schedule" : /*LANG*/"Edit Schedule")}}; + menu[B2 ? /*LANG*/"< Back" : /*LANG*/"< Cancel"] = () => showMainMenu(); + menu[/*LANG*/"Hours"] = { + value: hrs, + min:0, max:23, wrap:true, + onchange: v => {hrs = v;}, + }; + menu[/*LANG*/"Minutes"] = { + value: mins, + min:0, max:55, step:5, wrap:true, + onchange: v => {mins = v;}, + }; + menu[/*LANG*/"Switch to"] = { + value: mode, + min:0, max:2, wrap:true, + format: v => modeNames[v], + onchange: v => {mode = v;}, }; function getSched() { return { @@ -176,7 +166,7 @@ function showEditMenu(index) { mode: mode, }; } - menu["> Save"] = function() { + menu[B2 ? /*LANG*/"Save" : /*LANG*/"> Save"] = function() { if (isNew) { scheds.push(getSched()); } else { @@ -186,7 +176,7 @@ function showEditMenu(index) { showMainMenu(); }; if (!isNew) { - menu["> Delete"] = function() { + menu[B2 ? /*LANG*/"Delete" : /*LANG*/"> Delete"] = function() { scheds.splice(index, 1); save(); showMainMenu(); @@ -196,7 +186,7 @@ function showEditMenu(index) { } function showOptionsMenu() { - const disabledFormat = v => v ? "Off" : "-"; + const disabledFormat = v => v ? /*LANG*/"Off" : "-"; function toggle(option) { // we disable wakeOn* events by setting them to `false` in options // not disabled = not present in options at all @@ -209,9 +199,9 @@ function showOptionsMenu() { } let resetTimeout; const oMenu = { - "": {"title": "LCD Settings"}, - "< Back": () => showMainMenu(), - "LCD Brightness": { + "": {"title": /*LANG*/"LCD Settings"}, + /*LANG*/"< Back": () => showMainMenu(), + /*LANG*/"LCD Brightness": { value: get("brightness", 0), min: 0, // 0 = use default max: 1, @@ -233,7 +223,7 @@ function showOptionsMenu() { } }, }, - "LCD Timeout": { + /*LANG*/"LCD Timeout": { value: get("timeout", 0), min: 0, // 0 = use default (no constant on for quiet mode) max: 60, @@ -246,17 +236,17 @@ function showOptionsMenu() { }, // we disable wakeOn* events by overwriting them as false in options // not disabled = not present in options at all - "Wake on FaceUp": { + /*LANG*/"Wake on FaceUp": { value: "wakeOnFaceUp" in options, format: disabledFormat, onchange: () => {toggle("wakeOnFaceUp");}, }, - "Wake on Touch": { + /*LANG*/"Wake on Touch": { value: "wakeOnTouch" in options, format: disabledFormat, onchange: () => {toggle("wakeOnTouch");}, }, - "Wake on Twist": { + /*LANG*/"Wake on Twist": { value: "wakeOnTwist" in options, format: disabledFormat, onchange: () => {toggle("wakeOnTwist");}, diff --git a/apps/qmsched/metadata.json b/apps/qmsched/metadata.json index daeaad624..326a8fc4f 100644 --- a/apps/qmsched/metadata.json +++ b/apps/qmsched/metadata.json @@ -2,7 +2,7 @@ "id": "qmsched", "name": "Quiet Mode Schedule and Widget", "shortName": "Quiet Mode", - "version": "0.07", + "version": "0.08", "description": "Automatically turn Quiet Mode on or off at set times, change theme and LCD options while Quiet Mode is active.", "icon": "app.png", "screenshots": [{"url":"screenshot_b1_main.png"},{"url":"screenshot_b1_edit.png"},{"url":"screenshot_b1_lcd.png"}, diff --git a/apps/ratchet_launch/ChangeLog b/apps/ratchet_launch/ChangeLog new file mode 100644 index 000000000..af7f83942 --- /dev/null +++ b/apps/ratchet_launch/ChangeLog @@ -0,0 +1 @@ +0.01: Initial release diff --git a/apps/ratchet_launch/README.md b/apps/ratchet_launch/README.md new file mode 100644 index 000000000..15df463d0 --- /dev/null +++ b/apps/ratchet_launch/README.md @@ -0,0 +1,15 @@ +# Rachet Launcher + +Ratchet Launcher is a fork of the default Launcher with modified user interaction. Instead of free scrolling, apps are selected by swiping up and down, but in discrete "ticks", just like in the settings menus. + +**WARNING: Untested on Bangle.js v1! Please test and give feedback.** + +## Usage +- Choose app: Swipe up/down (top/bottom button on Bangle.js v1) +- Launch app: Tap screen (center button on Bangle.js v1) +- Return to clock: Swipe three ticks beyond first/last app in list + +## Installation +1. Install Ratchet Launcher using App Loader +2. Uninstall default Launcher +3. Reload diff --git a/apps/ratchet_launch/app.js b/apps/ratchet_launch/app.js new file mode 100644 index 000000000..11018080a --- /dev/null +++ b/apps/ratchet_launch/app.js @@ -0,0 +1,149 @@ +var Storage = require("Storage"); +var Layout = require("Layout"); + +var font = g.getFonts().includes("6x15") ? "6x15" : "6x8:2"; +var largeFont = g.getFonts().includes("12x20") ? "12x20" : "6x8:3"; +var currentApp = 0; +var overscroll = 0; +var blankImage = Graphics.createImage(` `); +var rowHeight = g.getHeight()/3; + +// Load apps list +var apps = Storage.list(/\.info$/).map(app=>{ + var a=Storage.readJSON(app,1); + return a&&{ + name:a.name, + type:a.type, + icon:a.icon ? Storage.read(a.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; +}); + +// Uncomment for testing in the emulator without apps: +// apps = [ +// { +// name:"Test", +// type:"app", +// icon:blankImage, +// sortorder:undefined, +// src:"" +// }, +// { +// name:"Test 2", +// type:"app", +// icon:blankImage, +// sortorder:undefined, +// src:"" +// }, +// ]; + +// Initialize layout +var layout = new Layout({ + type:"v", c:[ + // A row for the previous app + { type:"h", height:rowHeight, c:[ + {type: "img", id:"prev_icon", src:blankImage, width:48, height:48, scale:0.8, pad:8}, + {type: "txt", id:"prev_name", label:"", font:font, fillx:1, wrap:1}, + ]}, + // A row for the current app + { type:"h", height:rowHeight, c:[ + {type: "img", id:"cur_icon", src:blankImage, width:48, height:48}, + {type: "txt", id:"cur_name", label:"", font:largeFont, fillx:1, wrap:1}, + ]}, + // A row for the next app + { type:"h", height:rowHeight, c:[ + {type: "img", id:"next_icon", src:blankImage, width:48, height:48, scale:0.8, pad:8}, + {type: "txt", id:"next_name", label:"", font:font, fillx:1, wrap:1}, + ]}, + ] +}); + +// Drawing logic +function render() { + if (!apps.length) { + E.showMessage(/*LANG*/"No apps"); + return load(); + } + + // Previous app + if (currentApp > 0) { + layout.prev_icon.src = apps[currentApp-1].icon; + layout.prev_name.label = apps[currentApp-1].name; + } else { + layout.prev_icon.src = blankImage; + layout.prev_name.label = ""; + } + + // Current app + layout.cur_icon.src = apps[currentApp].icon; + layout.cur_name.label = apps[currentApp].name; + + // Next app + if (currentApp < apps.length-1) { + layout.next_icon.src = apps[currentApp+1].icon; + layout.next_name.label = apps[currentApp+1].name; + } else { + layout.next_icon.src = blankImage; + layout.next_name.label = ""; + } + + g.clear(); + layout.render(); +} + +// Launch the currently selected app +function launch() { + var app = apps[currentApp]; + if (!app) return; + if (!app.src || Storage.read(app.src)===undefined) { + E.showMessage(/*LANG*/"App Source\nNot found"); + setTimeout(render, 2000); + } else { + E.showMessage(/*LANG*/"Loading..."); + load(app.src); + } +} + +// Select previous/next app +function move(step) { + if ((currentApp == 0 && step < 0) || (currentApp >= apps.length-1 && step > 0)) { + // When we hit the end of the list (top or bottom), the step is + // counted towards the overscroll value. When the overscroll + // threshold is exceeded, we return to the clock face. + overscroll += step; + } else { + // This is the default case: the step is countedf towards the currentApp index + currentApp += step; + overscroll = 0; + return render(); + } + + // Overscroll threshold reached, return to clock + if (Math.abs(overscroll) > 3) { + Bangle.buzz(500, 1); + return load(); + } +} + +// Wire up user input +Bangle.setUI('updown', dir => { + if (!dir) launch(); + else { + if (process.env.HWVERSION==2) dir *= -1; // "natural scrolling" on touch screen + move(dir); + } +}); + +render(); diff --git a/apps/ratchet_launch/app.png b/apps/ratchet_launch/app.png new file mode 100644 index 000000000..a27ab48ed Binary files /dev/null and b/apps/ratchet_launch/app.png differ diff --git a/apps/ratchet_launch/metadata.json b/apps/ratchet_launch/metadata.json new file mode 100644 index 000000000..14ffec34a --- /dev/null +++ b/apps/ratchet_launch/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "ratchet_launch", + "name": "Ratchet Launcher", + "shortName": "Ratchet", + "version": "0.01", + "description": "Launcher with discrete scrolling for quicker app selection", + "icon": "app.png", + "type": "launch", + "tags": "tool,system,launcher", + "supports": ["BANGLEJS2","BANGLEJS"], + "storage": [ + {"name":"ratchet_launch.app.js","url":"app.js"} + ], + "sortorder": -10, + "readme":"README.md" +} diff --git a/apps/rebble/ChangeLog b/apps/rebble/ChangeLog index b9c26b4e3..b80dfef94 100644 --- a/apps/rebble/ChangeLog +++ b/apps/rebble/ChangeLog @@ -2,3 +2,4 @@ 0.02: Fix typo to Purple 0.03: Added dependancy on Pedometer Widget 0.04: Fixed icon and png to 48x48 pixels +0.05: added charging icon \ No newline at end of file diff --git a/apps/rebble/metadata.json b/apps/rebble/metadata.json index 212a7b5b3..b26fb6a27 100644 --- a/apps/rebble/metadata.json +++ b/apps/rebble/metadata.json @@ -2,7 +2,7 @@ "id": "rebble", "name": "Rebble Clock", "shortName": "Rebble", - "version": "0.04", + "version": "0.05", "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 d186ea8ec..7c7d57939 100644 --- a/apps/rebble/rebble.app.js +++ b/apps/rebble/rebble.app.js @@ -204,6 +204,14 @@ function drawBattery(x,y,wi,hi) { g.setColor(g.theme.fg); g.fillRect(x+wi-3,y+2+(((hi - 1)/2)-1),x+wi-2,y+2+(((hi - 1)/2)-1)+4); // contact g.fillRect(x+3, y+5, x +4 + E.getBattery()*(wi-12)/100, y+hi-1); // the level + + if( Bangle.isCharging() ) + { + g.setBgColor(settings.bg); + image = ()=> { return require("heatshrink").decompress(atob("j8OwMB/4AD94DC44DCwP//n/gH//EOgE/+AdBh/gAYMH4EAvkDAYP/+/AFAX+FgfzGAnAA=="));} + g.drawImage(image(),x+3,y+4); + } + } function getSteps() { @@ -270,3 +278,14 @@ for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";} loadSettings(); loadLocation(); draw(); // queues the next draw for a minutes time +Bangle.on('charging', function(charging) { + //redraw the sidebar ( with the battery ) + switch(sideBar) { + case 0: + drawSideBar1(); + break; + case 1: + drawSideBar2(); + break; + } +}); \ No newline at end of file diff --git a/apps/recorder/ChangeLog b/apps/recorder/ChangeLog index 60477ae97..90937e160 100644 --- a/apps/recorder/ChangeLog +++ b/apps/recorder/ChangeLog @@ -15,3 +15,5 @@ 0.09: Show correct number for log in overwrite prompt 0.10: Fix broken recorder settings (when launched from settings app) 0.11: Fix KML and GPX export when there is no GPS data +0.12: Fix 'Back' label positioning on track/graph display, make translateable +0.13: Fix for when widget is used before app diff --git a/apps/recorder/app-settings.json b/apps/recorder/app-settings.json index 4a3117a17..7410af213 100644 --- a/apps/recorder/app-settings.json +++ b/apps/recorder/app-settings.json @@ -1,6 +1,6 @@ { "recording":false, - "file":"record.log0.csv", + "file":"recorder.log0.csv", "period":10, "record" : ["gps"] } diff --git a/apps/recorder/app.js b/apps/recorder/app.js index 7075563aa..e5e732fe3 100644 --- a/apps/recorder/app.js +++ b/apps/recorder/app.js @@ -31,7 +31,12 @@ function updateSettings() { } function getTrackNumber(filename) { - return parseInt(filename.match(/^recorder\.log(.*)\.csv$/)[1]||0); + var trackNum = 0; + var matches = filename.match(/^recorder\.log(.*)\.csv$/); + if (matches) { + trackNum = parseInt(matches[1]||0); + } + return trackNum; } function showMainMenu() { @@ -49,11 +54,11 @@ function showMainMenu() { }; } const mainmenu = { - '': { 'title': 'Recorder' }, + '': { 'title': /*LANG*/'Recorder' }, '< Back': ()=>{load();}, - 'RECORD': { + /*LANG*/'RECORD': { value: !!settings.recording, - format: v=>v?"On":"Off", + format: v=>v?/*LANG*/"On":/*LANG*/"Off", onchange: v => { setTimeout(function() { E.showMenu(); @@ -66,7 +71,7 @@ function showMainMenu() { }, 1); } }, - 'File #': { + /*LANG*/'File #': { value: getTrackNumber(settings.file), min: 0, max: 99, @@ -77,8 +82,8 @@ function showMainMenu() { updateSettings(); } }, - 'View Tracks': ()=>{viewTracks();}, - 'Time Period': { + /*LANG*/'View Tracks': ()=>{viewTracks();}, + /*LANG*/'Time Period': { value: settings.period||10, min: 1, max: 120, @@ -103,15 +108,15 @@ function showMainMenu() { function viewTracks() { const menu = { - '': { 'title': 'Tracks' } + '': { 'title': /*LANG*/'Tracks' } }; var found = false; require("Storage").list(/^recorder\.log.*\.csv$/,{sf:true}).forEach(filename=>{ found = true; - menu["Track "+getTrackNumber(filename)] = ()=>viewTrack(filename,false); + menu[/*LANG*/"Track "+getTrackNumber(filename)] = ()=>viewTrack(filename,false); }); if (!found) - menu["No Tracks found"] = function(){}; + menu[/*LANG*/"No Tracks found"] = function(){}; menu['< Back'] = () => { showMainMenu(); }; return E.showMenu(menu); } @@ -175,38 +180,38 @@ function asTime(v){ function viewTrack(filename, info) { if (!info) { - E.showMessage("Loading...","Track "+getTrackNumber(filename)); + E.showMessage(/*LANG*/"Loading...",/*LANG*/"Track "+getTrackNumber(filename)); info = getTrackInfo(filename); } //console.log(info); const menu = { - '': { 'title': 'Track '+info.fn } + '': { 'title': /*LANG*/'Track '+info.fn } }; if (info.time) menu[info.time.toISOString().substr(0,16).replace("T"," ")] = function(){}; menu["Duration"] = { value : asTime(info.duration)}; menu["Records"] = { value : ""+info.records }; if (info.fields.includes("Latitude")) - menu['Plot Map'] = function() { + menu[/*LANG*/'Plot Map'] = function() { info.qOSTM = false; plotTrack(info); }; if (osm && info.fields.includes("Latitude")) - menu['Plot OpenStMap'] = function() { + menu[/*LANG*/'Plot OpenStMap'] = function() { info.qOSTM = true; plotTrack(info); } if (info.fields.includes("Altitude")) - menu['Plot Alt.'] = function() { + menu[/*LANG*/'Plot Alt.'] = function() { plotGraph(info, "Altitude"); }; if (info.fields.includes("Latitude")) - menu['Plot Speed'] = function() { + menu[/*LANG*/'Plot Speed'] = function() { plotGraph(info, "Speed"); }; // TODO: steps, heart rate? - menu['Erase'] = function() { - E.showPrompt("Delete Track?").then(function(v) { + menu[/*LANG*/'Erase'] = function() { + E.showPrompt(/*LANG*/"Delete Track?").then(function(v) { if (v) { settings.recording = false; updateSettings(); @@ -238,7 +243,7 @@ function viewTrack(filename, info) { } E.showMenu(); // remove menu - E.showMessage("Drawing...","Track "+info.fn); + E.showMessage(/*LANG*/"Drawing...",/*LANG*/"Track "+info.fn); g.flip(); // on buffered screens, draw a not saying we're busy g.clear(1); var s = require("Storage"); @@ -305,17 +310,18 @@ function viewTrack(filename, info) { g.drawString(require("locale").distance(dist),g.getWidth() / 2, g.getHeight() - 20); g.setFont("6x8",2); g.setFontAlign(0,0,3); - g.drawString("Back",g.getWidth() - 10, g.getHeight() - 40); + var isBTN3 = "BTN3" in global; + g.drawString(/*LANG*/"Back",g.getWidth() - 10, isBTN3 ? (g.getHeight() - 40) : (g.getHeight()/2)); setWatch(function() { viewTrack(info.fn, info); - }, global.BTN3||BTN1); + }, isBTN3?BTN3:BTN1); Bangle.drawWidgets(); g.flip(); } function plotGraph(info, style) { "ram" E.showMenu(); // remove menu - E.showMessage("Calculating...","Track "+info.fn); + E.showMessage(/*LANG*/"Calculating...",/*LANG*/"Track "+info.fn); var filename = info.filename; var infn = new Float32Array(80); var infc = new Uint16Array(80); @@ -334,7 +340,7 @@ function viewTrack(filename, info) { strt = c[timeIdx]; } if (style=="Altitude") { - title = "Altitude (m)"; + title = /*LANG*/"Altitude (m)"; var altIdx = info.fields.indexOf("Altitude"); while(l!==undefined) { ++nl;c=l.split(",");l = f.readLine(f); @@ -344,7 +350,7 @@ function viewTrack(filename, info) { infc[i]++; } } else if (style=="Speed") { - title = "Speed (m/s)"; + title = /*LANG*/"Speed (m/s)"; var latIdx = info.fields.indexOf("Latitude"); var lonIdx = info.fields.indexOf("Longitude"); // skip until we find our first data @@ -404,10 +410,11 @@ function viewTrack(filename, info) { }); g.setFont("6x8",2); g.setFontAlign(0,0,3); - g.drawString("Back",g.getWidth() - 10, g.getHeight() - 40); + var isBTN3 = "BTN3" in global; + g.drawString(/*LANG*/"Back",g.getWidth() - 10, isBTN3 ? (g.getHeight() - 40) : (g.getHeight()/2)); setWatch(function() { viewTrack(info.filename, info); - }, global.BTN3||BTN1); + }, isBTN3?BTN3:BTN1); g.flip(); } diff --git a/apps/recorder/metadata.json b/apps/recorder/metadata.json index 56865e885..e2400603d 100644 --- a/apps/recorder/metadata.json +++ b/apps/recorder/metadata.json @@ -2,7 +2,7 @@ "id": "recorder", "name": "Recorder", "shortName": "Recorder", - "version": "0.11", + "version": "0.13", "description": "Record GPS position, heart rate and more in the background, then download to your PC.", "icon": "app.png", "tags": "tool,outdoors,gps,widget", @@ -15,5 +15,5 @@ {"name":"recorder.wid.js","url":"widget.js"}, {"name":"recorder.settings.js","url":"settings.js"} ], - "data": [{"name":"recorder.json"},{"wildcard":"recorder.log?.csv","storageFile":true}] + "data": [{"name":"recorder.json","url":"app-settings.json"},{"wildcard":"recorder.log?.csv","storageFile":true}] } diff --git a/apps/recorder/widget.js b/apps/recorder/widget.js index e10c99c0c..221bc6c1a 100644 --- a/apps/recorder/widget.js +++ b/apps/recorder/widget.js @@ -11,7 +11,7 @@ settings.recording = false; return settings; } - + function updateSettings(settings) { require("Storage").writeJSON("recorder.json", settings); if (WIDGETS["recorder"]) WIDGETS["recorder"].reload(); @@ -233,7 +233,9 @@ Bangle.drawWidgets(); // relayout all widgets },setRecording:function(isOn) { var settings = loadSettings(); - if (isOn && !settings.recording && require("Storage").list(settings.file).length){ + if (isOn && !settings.recording && !settings.file) { + settings.file = "recorder.log0.csv"; + } else if (isOn && !settings.recording && require("Storage").list(settings.file).length){ var logfiles=require("Storage").list(/recorder.log.*/); var maxNumber=0; for (var c of logfiles){ @@ -247,9 +249,11 @@ var buttons={Yes:"yes",No:"no"}; if (newFileName) buttons["New"] = "new"; var prompt = E.showPrompt("Overwrite\nLog " + settings.file.match(/\d+/)[0] + "?",{title:"Recorder",buttons:buttons}).then(selection=>{ - if (selection=="no") return false; // just cancel - if (selection=="yes") require("Storage").open(settings.file,"r").erase(); - if (selection=="new"){ + if (selection==="no") return false; // just cancel + if (selection==="yes") { + require("Storage").open(settings.file,"r").erase(); + } + if (selection==="new"){ settings.file = newFileName; updateSettings(settings); } diff --git a/apps/rolex/ChangeLog b/apps/rolex/ChangeLog index 3bfed4a4e..a1e2cee0a 100644 --- a/apps/rolex/ChangeLog +++ b/apps/rolex/ChangeLog @@ -1,3 +1,4 @@ 0.01: Initial Release 0.02: Minor tweaks for light theme -0.03: Made images 2 bit and fixed theme honoring \ No newline at end of file +0.03: Made images 2 bit and fixed theme honoring +0.04: Fixed date font alignment and changed date font to match a real Rolex diff --git a/apps/rolex/README.md b/apps/rolex/README.md index 4c24aad0c..5339f1d0e 100644 --- a/apps/rolex/README.md +++ b/apps/rolex/README.md @@ -1,11 +1,13 @@ # Rolex -![](screenshot.png) +![](screenshot.png) ![](screenshot1.png) Created with the aid of the Espruino documentation and looking through many of the wonderful exising watchfaces that have been made. This has not been tested on a watch yet as I haven't aquired one but has been tested in the emulator. The hands don't rotate dead on center but they're as close as I could get them to. +Colour switches based on watch theme so if you want a white on black change your watch theme to dark, and for black on white change it to light. + Special thanks to: * rozek (for his updated widget draw code for utilization with background images) * Gordon Williams (Bangle.js, watchapps for reference code and documentation) diff --git a/apps/rolex/app.js b/apps/rolex/app.js index e409704fc..124cb6fee 100644 --- a/apps/rolex/app.js +++ b/apps/rolex/app.js @@ -28,6 +28,14 @@ var imgSec = { buffer : E.toArrayBuffer(atob("v/q//r/+v/qv+q/qq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qv+v/6/D/wD8PDw8PwD/w///6v+qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqr+r//v///X/1X/Vf9V/1X/1///+//q/qq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq6qrqg==")) }; +/* use font closer to Rolex */ + +Graphics.prototype.setFontRolexFont = function(scale) { + // Actual height 12 (12 - 1) + this.setFontCustom(atob("AAAABAACAAAAAYAHgA4AOABgAAAAA/gD/gMBgQBAwGA/4A/gAAAAAAIBAQCB/8D/4AAQAAAAAAAAAwEDAYEBQIEgYxA/CA4MAAAAAAAAgIBAhCBCEDOYHvgCOAAAAAAABgANAAyAHEAf/A/+AAgABAAAAAAADBAcCBsECYIEYgIeAAAAAAAHwAfwB5wGggZBAjGBH4CDgAAACAAYAAgABAMCDwE+APgAYAAAAAAABxwH3wJwgRhA3iB54AhgAAAAAAPhA/iBDMCCQGHgH+AHwAAAAAAAQIAgQAAAAAA="), 46, atob("BAUJCQkJCQkJCQkJBQ=="), 17+(scale<<8)+(1<<16)); + return this; +}; + /* Set variables to get screen width, height and center points */ let W = g.getWidth(); @@ -36,10 +44,6 @@ let cx = W/2; let cy = H/2; let Timeout; -/* set font */ - -require("Font4x5Numeric").add(Graphics); - Bangle.loadWidgets(); /* Custom version of Bangle.drawWidgets (does not clear the widget areas) Thanks to rozek */ @@ -106,9 +110,10 @@ function drawHands() { g.drawImage(imgHour,cx-22*hourSin,cy+22*hourCos,{rotate:hourAngle}); g.drawImage(imgMin,cx-34*minSin,cy+34*minCos,{rotate:minAngle}); g.drawImage(imgSec,cx-25*secSin,cy+25*secCos,{rotate:secAngle}); - g.setFont("4x5Numeric:3"); + g.setFontRolexFont(); g.setColor(g.theme.bg); - g.drawString(d.getDate(),157,81); + g.setFontAlign(0,0,0); + g.drawString(d.getDate(),165,89); } function drawBackground() { diff --git a/apps/rolex/metadata.json b/apps/rolex/metadata.json index e24344dad..f307e60c4 100644 --- a/apps/rolex/metadata.json +++ b/apps/rolex/metadata.json @@ -2,8 +2,8 @@ "name": "rolex", "shortName":"rolex", "icon": "rolex.png", - "screenshots": [{"url":"screenshot.png"}], - "version":"0.03", + "screenshots": [{"url":"screenshot1.png"}], + "version":"0.04", "description": "A rolex like watch face", "tags": "clock", "type": "clock", @@ -14,4 +14,4 @@ {"name":"rolex.app.js","url":"app.js"}, {"name":"rolex.img","url":"app-icon.js","evaluate":true} ] - } \ No newline at end of file + } diff --git a/apps/rolex/screenshot1.png b/apps/rolex/screenshot1.png new file mode 100644 index 000000000..f84b0ef5c Binary files /dev/null and b/apps/rolex/screenshot1.png differ diff --git a/apps/run/ChangeLog b/apps/run/ChangeLog index fd28c4414..032ebdc1a 100644 --- a/apps/run/ChangeLog +++ b/apps/run/ChangeLog @@ -5,3 +5,6 @@ 0.04: Use the exstats module, and make what is displayed configurable 0.05: exstats updated so update 'distance' label is updated, option for 'speed' 0.06: Add option to record a run using the recorder app automatically +0.07: Fix crash if an odd number of active boxes are configured (fix #1473) +0.08: Added support for notifications from exstats. Support all stats from exstats +0.09: Fix broken start/stop if recording not enabled (fix #1561) diff --git a/apps/run/README.md b/apps/run/README.md index 17975f92c..89750eb7d 100644 --- a/apps/run/README.md +++ b/apps/run/README.md @@ -13,7 +13,7 @@ the red `STOP` in the bottom right turns to a green `RUN`. the GPS updates your position as it gets more satellites your position changes and the distance shown will increase, even if you are standing still. * `TIME` - the elapsed time for your run -* `PACE` - the number of minutes it takes you to run a kilometer **based on your run so far** +* `PACE` - the number of minutes it takes you to run a given distance, configured in settings (default 1km) **based on your run so far** * `HEART` - Your heart rate * `STEPS` - Steps since you started exercising * `CADENCE` - Steps per second based on your step rate *over the last minute* @@ -24,9 +24,8 @@ so if you have no GPS lock you just need to wait. ## Recording Tracks -`Run` doesn't directly allow you to record your tracks at the moment. -However you can just install the `Recorder` app, turn recording on in -that, and then start the `Run` app. +When the `Recorder` app is installed, `Run` will automatically start and stop tracks +as needed, prompting you to overwrite or begin a new track if necessary. ## Settings @@ -35,19 +34,35 @@ Under `Settings` -> `App` -> `Run` you can change settings for this app. * `Record Run` (only displayed if `Recorder` app installed) should the Run app automatically record GPS/HRM/etc data every time you start a run? * `Pace` is the distance that pace should be shown over - 1km, 1 mile, 1/2 Marathon or 1 Marathon -* `Box 1/2/3/4/5/6` are what should be shown in each of the 6 boxes on the display. From the top left, down. - If you set it to `-` nothing will be displayed, so you can display only 4 boxes of information - if you wish by setting the last 2 boxes to `-`. +* `Boxes` leads to a submenu where you can configure what is shown in each of the 6 boxes on the display. + Available stats are "Time", "Distance", "Steps", "Heart (BPM)", "Pace (avg)", "Pace (curr)", "Speed", and "Cadence". + Any box set to "-" will display no information. + * Box 1 is the top left (defaults to "Distance") + * Box 2 is the top right (defaults to "Time") + * Box 3 is the middle left (defaults to "Pace (avg)") + * Box 4 is the middle right (defaults to "Heart (BPM)") + * Box 5 is the bottom left (defaults to "Steps") + * Box 6 is the bottom right (defaults to "Cadence") +* `Notifications` leads to a submenu where you can configure if the app will notify you after +your distance, steps, or time repeatedly pass your configured thresholds + * `Ntfy Dist`: The distance that you must pass before you are notified. Follows the `Pace` options + * "Off" (default), "1km", "1 mile", "1/2 Marathon", "1 Marathon" + * `Ntfy Steps`: The number of steps that must pass before you are notified. + * "Off" (default), 100, 500, 1000, 5000, 10000 + * `Ntfy Time`: The amount of time that must pass before you are notified. + * "Off" (default), "30 sec", "1 min", "2 min", "5 min", "10 min", "30 min", "1 hour" + * `Dist Pattern`: The vibration pattern to use to notify you about meeting your distance threshold + * `Step Pattern`: The vibration pattern to use to notify you about meeting your step threshold + * `Time Pattern`: The vibration pattern to use to notify you about meeting your time threshold ## TODO -* Allow this app to trigger the `Recorder` app on and off directly. * Keep a log of each run's stats (distance/steps/etc) ## Development -This app uses the [`exstats` module](/modules/exstats.js). When uploaded via the +This app uses the [`exstats` module](https://github.com/espruino/BangleApps/blob/master/modules/exstats.js). When uploaded via the app loader, the module is automatically included in the app's source. However when developing via the IDE the module won't get pulled in by default. -There are some options to fix this easily - please check out the [modules README.md file](/modules/README.md) +There are some options to fix this easily - please check out the [modules README.md file](https://github.com/espruino/BangleApps/blob/master/modules/README.md) diff --git a/apps/run/app.js b/apps/run/app.js index a8515a71a..d066c8b1f 100644 --- a/apps/run/app.js +++ b/apps/run/app.js @@ -1,5 +1,5 @@ var ExStats = require("exstats"); -var B2 = process.env.HWVERSION==2; +var B2 = process.env.HWVERSION===2; var Layout = require("Layout"); var locale = require("locale"); var fontHeading = "6x8:2"; @@ -14,46 +14,75 @@ Bangle.drawWidgets(); // --------------------------- let settings = Object.assign({ - record : true, - B1 : "dist", - B2 : "time", - B3 : "pacea", - B4 : "bpm", - B5 : "step", - B6 : "caden", - paceLength : 1000 + record: true, + B1: "dist", + B2: "time", + B3: "pacea", + B4: "bpm", + B5: "step", + B6: "caden", + paceLength: 1000, + notify: { + dist: { + value: 0, + notifications: [], + }, + step: { + value: 0, + notifications: [], + }, + time: { + value: 0, + notifications: [], + }, + }, }, require("Storage").readJSON("run.json", 1) || {}); -var statIDs = [settings.B1,settings.B2,settings.B3,settings.B4,settings.B5,settings.B6].filter(s=>s!=""); +var statIDs = [settings.B1,settings.B2,settings.B3,settings.B4,settings.B5,settings.B6].filter(s=>s!==""); var exs = ExStats.getStats(statIDs, settings); // --------------------------- // Called to start/stop running function onStartStop() { var running = !exs.state.active; - if (running) { - exs.start(); - } else { - exs.stop(); - } - layout.button.label = running ? "STOP" : "START"; - layout.status.label = running ? "RUN" : "STOP"; - layout.status.bgCol = running ? "#0f0" : "#f00"; - // if stopping running, don't clear state - // so we can at least refer to what we've done - layout.render(); + var prepPromises = []; + // start/stop recording + // Do this first in case recorder needs to prompt for + // an overwrite before we start tracking exstats if (settings.record && WIDGETS["recorder"]) { if (running) { isMenuDisplayed = true; - WIDGETS["recorder"].setRecording(true).then(() => { - isMenuDisplayed = false; - layout.forgetLazyState(); - layout.render(); - }); + prepPromises.push( + WIDGETS["recorder"].setRecording(true).then(() => { + isMenuDisplayed = false; + layout.forgetLazyState(); + layout.render(); + }) + ); } else { - WIDGETS["recorder"].setRecording(false); + prepPromises.push( + WIDGETS["recorder"].setRecording(false) + ); } } + + if (!prepPromises.length) // fix for Promise.all bug in 2v12 + prepPromises.push(Promise.resolve()); + + Promise.all(prepPromises) + .then(() => { + if (running) { + exs.start(); + } else { + exs.stop(); + } + layout.button.label = running ? "STOP" : "START"; + layout.status.label = running ? "RUN" : "STOP"; + layout.status.bgCol = running ? "#0f0" : "#f00"; + // if stopping running, don't clear state + // so we can at least refer to what we've done + layout.render(); + }); } var lc = []; @@ -62,14 +91,14 @@ for (var i=0;ilayout[e.id].label = e.getString()); - sb.on('changed', e=>layout[e.id].label = e.getString()); + if (sa) sa.on('changed', e=>layout[e.id].label = e.getString()); + if (sb) sb.on('changed', e=>layout[e.id].label = e.getString()); } // At the bottom put time/GPS state/etc lc.push({ type:"h", filly:1, c:[ @@ -84,11 +113,27 @@ var layout = new Layout( { delete lc; layout.render(); +function configureNotification(stat) { + stat.on('notify', (e)=>{ + settings.notify[e.id].notifications.reduce(function (promise, buzzPattern) { + return promise.then(function () { + return Bangle.buzz(buzzPattern[0], buzzPattern[1]); + }); + }, Promise.resolve()); + }); +} + +Object.keys(settings.notify).forEach((statType) => { + if (settings.notify[statType].increment > 0) { + configureNotification(exs.stats[statType]); + } +}); + // Handle GPS state change for icon Bangle.on("GPS", function(fix) { layout.gps.bgCol = fix.fix ? "#0f0" : "#f00"; if (!fix.fix) return; // only process actual fixes - if (fixCount++ == 0) { + if (fixCount++ === 0) { Bangle.buzz(); // first fix, does not need to respect quiet mode } }); diff --git a/apps/run/metadata.json b/apps/run/metadata.json index ea68f4734..01a85ed05 100644 --- a/apps/run/metadata.json +++ b/apps/run/metadata.json @@ -1,6 +1,6 @@ { "id": "run", "name": "Run", - "version":"0.06", + "version":"0.09", "description": "Displays distance, time, steps, cadence, pace and more for runners.", "icon": "app.png", "tags": "run,running,fitness,outdoors,gps", diff --git a/apps/run/settings.js b/apps/run/settings.js index 7eb8a8611..29a2f43cc 100644 --- a/apps/run/settings.js +++ b/apps/run/settings.js @@ -9,14 +9,28 @@ // This way saved values are preserved if a new version adds more settings const storage = require('Storage') let settings = Object.assign({ - record : true, - B1 : "dist", - B2 : "time", - B3 : "pacea", - B4 : "bpm", - B5 : "step", - B6 : "caden", - paceLength : 1000 + record: true, + B1: "dist", + B2: "time", + B3: "pacea", + B4: "bpm", + B5: "step", + B6: "caden", + paceLength: 1000, // TODO: Default to either 1km or 1mi based on locale + notify: { + dist: { + increment: 0, + notifications: [], + }, + step: { + increment: 0, + notifications: [], + }, + time: { + increment: 0, + notifications: [], + }, + }, }, storage.readJSON(SETTINGS_FILE, 1) || {}); function saveSettings() { storage.write(SETTINGS_FILE, settings) @@ -24,7 +38,7 @@ function getBoxChooser(boxID) { return { - min :0, max: statsIDs.length-1, + min: 0, max: statsIDs.length-1, value: Math.max(statsIDs.indexOf(settings[boxID]),0), format: v => statsList[v].name, onchange: v => { @@ -34,6 +48,14 @@ } } + function sampleBuzz(buzzPatterns) { + return buzzPatterns.reduce(function (promise, buzzPattern) { + return promise.then(function () { + return Bangle.buzz(buzzPattern[0], buzzPattern[1]); + }); + }, Promise.resolve()); + } + var menu = { '': { 'title': 'Run' }, '< Back': back, @@ -47,8 +69,55 @@ saveSettings(); } }; + var notificationsMenu = { + '< Back': function() { E.showMenu(menu) }, + } + menu[/*LANG*/"Notifications"] = function() { E.showMenu(notificationsMenu)}; ExStats.appendMenuItems(menu, settings, saveSettings); - Object.assign(menu,{ + ExStats.appendNotifyMenuItems(notificationsMenu, settings, saveSettings); + var vibPatterns = [/*LANG*/"Off", ".", "-", "--", "-.-", "---"]; + var vibTimes = [ + [], + [[100, 1]], + [[300, 1]], + [[300, 1], [300, 0], [300, 1]], + [[300, 1],[300, 0], [100, 1], [300, 0], [300, 1]], + [[300, 1],[300, 0],[300, 1],[300, 0],[300, 1]], + ]; + notificationsMenu[/*LANG*/"Dist Pattern"] = { + value: Math.max(0,vibPatterns.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.dist.notifications))), + min: 0, max: vibPatterns.length, + format: v => vibPatterns[v]||"Off", + onchange: v => { + settings.notify.dist.notifications = vibTimes[v]; + sampleBuzz(vibTimes[v]); + saveSettings(); + } + } + notificationsMenu[/*LANG*/"Step Pattern"] = { + value: Math.max(0,vibPatterns.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.step.notifications))), + min: 0, max: vibPatterns.length, + format: v => vibPatterns[v]||"Off", + onchange: v => { + settings.notify.step.notifications = vibTimes[v]; + sampleBuzz(vibTimes[v]); + saveSettings(); + } + } + notificationsMenu[/*LANG*/"Time Pattern"] = { + value: Math.max(0,vibPatterns.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.time.notifications))), + min: 0, max: vibPatterns.length, + format: v => vibPatterns[v]||"Off", + onchange: v => { + settings.notify.time.notifications = vibTimes[v]; + sampleBuzz(vibTimes[v]); + saveSettings(); + } + } + var boxMenu = { + '< Back': function() { E.showMenu(menu) }, + } + Object.assign(boxMenu,{ 'Box 1': getBoxChooser("B1"), 'Box 2': getBoxChooser("B2"), 'Box 3': getBoxChooser("B3"), @@ -56,5 +125,6 @@ 'Box 5': getBoxChooser("B5"), 'Box 6': getBoxChooser("B6"), }); + menu[/*LANG*/"Boxes"] = function() { E.showMenu(boxMenu)}; E.showMenu(menu); }) diff --git a/apps/setting/settings.js b/apps/setting/settings.js index 09d95934f..f74c94db0 100644 --- a/apps/setting/settings.js +++ b/apps/setting/settings.js @@ -226,7 +226,7 @@ function showThemeMenu() { /*LANG*/'Dark BW': ()=>{ upd({ fg:cl("#fff"), bg:cl("#000"), - fg2:cl("#0ff"), bg2:cl("#000"), + fg2:cl("#fff"), bg2:cl("#004"), fgH:cl("#fff"), bgH:cl("#00f"), dark:true }); diff --git a/apps/shortcuts/ChangeLog b/apps/shortcuts/ChangeLog index 2286a7f70..0e4a98065 100644 --- a/apps/shortcuts/ChangeLog +++ b/apps/shortcuts/ChangeLog @@ -1 +1,2 @@ -0.01: New App! \ No newline at end of file +0.01: New App! +0.02: Update setUI to work with new Bangle.js 2v13 menu style diff --git a/apps/shortcuts/boot.js b/apps/shortcuts/boot.js index f71f6d6ca..58ce2d067 100644 --- a/apps/shortcuts/boot.js +++ b/apps/shortcuts/boot.js @@ -1,6 +1,7 @@ (function() { var sui = Bangle.setUI; Bangle.setUI = function(mode, cb) { + if ("object"==typeof mode) mode = mode.mode; if (mode!="clock") return sui(mode,cb); return sui("clockupdown", (dir) => { let settings = require("Storage").readJSON("shortcuts.json", 1)||{}; @@ -12,4 +13,3 @@ }); }; })(); - \ No newline at end of file diff --git a/apps/shortcuts/metadata.json b/apps/shortcuts/metadata.json index 2351a102f..922d5ae11 100644 --- a/apps/shortcuts/metadata.json +++ b/apps/shortcuts/metadata.json @@ -2,7 +2,7 @@ "id": "shortcuts", "name": "Shortcuts", "shortName": "Shortcuts", - "version": "0.01", + "version": "0.02", "description": "Quickly load your favourite apps from (almost) any watch face.", "icon": "app.png", "type": "bootloader", diff --git a/apps/slidingtext/ChangeLog b/apps/slidingtext/ChangeLog index 3bd656cb0..0327ff387 100644 --- a/apps/slidingtext/ChangeLog +++ b/apps/slidingtext/ChangeLog @@ -5,3 +5,4 @@ 0.05: BUGFIX: pedometer widget interfered with the clock Font Alignment 0.06: Use Bangle.setUI for button/launcher handling 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". diff --git a/apps/slidingtext/metadata.json b/apps/slidingtext/metadata.json index 0f380ec8f..2937a618b 100644 --- a/apps/slidingtext/metadata.json +++ b/apps/slidingtext/metadata.json @@ -1,7 +1,7 @@ { "id": "slidingtext", "name": "Sliding Clock", - "version": "0.07", + "version": "0.08", "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", "type": "clock", diff --git a/apps/slidingtext/slidingtext.js b/apps/slidingtext/slidingtext.js index 3be502cfe..7b56af1a1 100644 --- a/apps/slidingtext/slidingtext.js +++ b/apps/slidingtext/slidingtext.js @@ -623,15 +623,6 @@ Bangle.on('lcdPower', (on) => { } }); -Bangle.on('faceUp',function(up){ - //console.log("faceUp: " + up + " LCD: " + Bangle.isLCDOn()); - if (up && !Bangle.isLCDOn()) { - //console.log("faceUp and LCD off"); - clearTimers(); - Bangle.setLCDPower(true); - } -}); - g.clear(); load_settings(); Bangle.loadWidgets(); diff --git a/apps/smclock/ChangeLog b/apps/smclock/ChangeLog new file mode 100644 index 000000000..0300d5ceb --- /dev/null +++ b/apps/smclock/ChangeLog @@ -0,0 +1,4 @@ +0.01: Initial version +0.02: Add battery level +0.03: Fix battery display when full +0.04: Add support for settings diff --git a/apps/smclock/README.md b/apps/smclock/README.md new file mode 100644 index 000000000..635292d0c --- /dev/null +++ b/apps/smclock/README.md @@ -0,0 +1,23 @@ +# Monogram Watch Face + +Just a simple watch face for the Banglejs2. + +It shows battery level in the upper left corner, date information in the upper right, and time information in the bottom. + +![](screenshot.png) + +## Settings + +**Analog Clock:** + +**Human Readable Date:** When the setting is on, the date is shown in a more human-friendly format (e.g. "Oct 2"), otherwise the date is shown in a standard format (e.g. "02/10"). Default is off. + +**Show Week Info:** When the setting is on, the weekday and week number are shown in the upper right box. When the setting is off, the full year is shown instead. Default is off. + +**Vector Font:** When the setting is on, the app uses Espruino's vector font, otherwise it uses the default font. Default is off. + +## Using the app + +Monogram Watch Face can be selected as the default clock or it can be run manually from the launcher. Its settings can be accessed and changed via the relevant menu. + +Tapping on the "Alerts" area will replace the current time display with the time of the most immediate alert. diff --git a/apps/smclock/app-icon.js b/apps/smclock/app-icon.js new file mode 100644 index 000000000..91494a528 --- /dev/null +++ b/apps/smclock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("l0uwkEogAgm4VUod3ugsUu93FigABFyQsCFyQsDFyQsEFyAsFFyAsGFxwsHFxwsIu9yCpVCmQWIkckCxMhkcnFg8yiQsJiEBFw9zkMBFxEhgEAiYuGmUQgAuHFgIWBFwwsBBQQuGBQQuHFgQABFwosDFwwsDFw4KEFwosEFwosFFwgsFFwoKGFwYsGFwYsHEYUzEJAuBoIKHBYMiEJEAilEBRESFhAABLYNAFiUERQQsUFw4sPFwwsPFwosRFwgsRFwYsTFwQsTAANBFigABFigABoQtIFhYuKCpguIFhouICpwuGFh4uGCqAuEFiIuECqQuCFiYuCCqgAMA")) diff --git a/apps/smclock/app.js b/apps/smclock/app.js new file mode 100644 index 000000000..350c0dd07 --- /dev/null +++ b/apps/smclock/app.js @@ -0,0 +1,194 @@ +const SETTINGSFILE = "smclock.json"; +const background = { + width: 176, + height: 176, + bpp: 3, + transparent: 1, + buffer: require("heatshrink").decompress( + atob( + "/4A/AH4ACUb8H9MkyVJAThB/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/INP/AH4A/AAX8Yz4Afn5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/INI=" + ) + ), +}; +const monthName = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]; +const weekday = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + +// dynamic variables +var batLevel = -1; +var batColor = [0, 0, 0]; + +// settings variables +var dateFormat; +var drawInterval; +var pollInterval; +var showAnalogFace; +var showWeekInfo; +var useVectorFont; + +// load settings +function loadSettings() { + // Helper function default setting + function def(value, def) {return value !== undefined ? value : def;} + var settings = require("Storage").readJSON(SETTINGSFILE, true) || {}; + + dateFormat = def(settings.dateFormat, "Short"); + drawInterval = def(settings.drawInterval, 10); + pollInterval = def(settings.pollInterval, 60); + showAnalogFace = def(settings.showAnalogFace, false); + showWeekInfo = def(settings.showWeekInfo, false); + useVectorFont = def(settings.useVectorFont, false); +} + +// copied from: https://gist.github.com/IamSilviu/5899269#gistcomment-3035480 +function ISO8601_week_no(date) { + 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)); + } + return 1 + Math.ceil((firstThursday - tdt) / 604800000); +} + +function d02(value) { + return ("0" + value).substr(-2); +} + +function pollBattery() { + batLevel = E.getBattery(); +} + +function getBatteryColor(level) { + var color; + if (level < 0) { + pollBattery(); + level = batLevel; + } + if (level > 80) { + color = [0, 0, 1]; + } else if (level > 60) { + color = [0, 1, 1]; + } else if (level > 40) { + color = [0, 1, 0]; + } else if (level > 20) { + color = [1, 1, 0]; + } else { + color = [1, 0, 0]; + } + return color; +} + +function draw() { + g.drawImage(background); + + const color = getBatteryColor(batLevel); + var bat = ""; + const d = new Date(); + const day = d.getDate(); + const month = d.getMonth() + 1; + const week = d02(ISO8601_week_no(d)); + var date1 = ""; + var date2 = ""; + const h = d.getHours(); + const m = d.getMinutes(); + const time = d02(h) + ":" + d02(m); + + if (E.getBattery() < 100) { + bat = d02(E.getBattery()) + "%"; + } else { + bat = E.getBattery() + "%"; + } + + g.reset(); + + // draw battery info + g.setColor(1, 1, 1); + if (useVectorFont == true) { + g.setFont("Vector", 16); + g.drawString("Bat:", 12, 22, false); + } else { + g.setFont("4x6", 2); + g.drawString("Bat:", 10, 22, false); + } + g.setColor(color[0], color[1], color[2]); + if (batLevel < 100) { + g.drawString(bat, 52, 22, false); + } else { + g.drawString(bat, 46, 22, false); + } + + // draw date info + g.setColor(0, 0, 0); + if (useVectorFont == true) { + g.setFont("Vector", 20); + } else { + g.setFont("6x8", 2); + } + if (dateFormat == "Short") { + date1 = d02(day) + "/" + d02(month); + g.drawString(date1, 105, 20, false); + } else { + date1 = monthName[month - 1] + d02(day); + g.drawString(date1, 104, 20, false); + } + + // draw week info + if (showWeekInfo == true) { + date2 = weekday[d.getDay()] + " " + d02(week) + if (useVectorFont == true) { + g.setFont("Vector", 18); + } else { + g.setFont("6x8", 2); + } + g.drawString(date2, 105, 55, false); + } else { + date2 = d.getFullYear(); + if (useVectorFont == true) { + g.setFont("Vector", 22); + g.drawString(date2, 105, 55, false); + } else { + g.setFont("4x6", 3); + g.drawString(date2, 108, 55, false); + } + } + + // draw time + g.setColor(1, 1, 1); + if (useVectorFont == true) { + g.setFont("Vector", 60); + g.drawString(time, 10, 108, false); + } else { + g.setFont("6x8", 5); + g.drawString(time, 14, 112, false); + } +} + +loadSettings(); + +g.clear(); + +pollBattery(); +draw(); + +var batInterval = setInterval(pollBattery, pollInterval * 1000); +var actualDrawInterval = setInterval(draw, drawInterval * 1000); + +// Stop updates when LCD is off, restart when on +Bangle.on("lcdPower", (on) => { + if (batInterval) clearInterval(batInterval); + batInterval = undefined; + if (actualDrawInterval) clearInterval(actualDrawInterval); + actualDrawInterval = undefined; + if (on) { + batInterval = setInterval(pollBattery, pollInterval * 1000); + actualDrawInterval = setInterval(draw, drawInterval * 1000); + + pollBattery(); + draw(); + } +}); + +// Show launcher when middle button pressed +Bangle.setUI("clock"); diff --git a/apps/smclock/app.png b/apps/smclock/app.png new file mode 100644 index 000000000..16712c63b Binary files /dev/null and b/apps/smclock/app.png differ diff --git a/apps/smclock/metadata.json b/apps/smclock/metadata.json new file mode 100644 index 000000000..cc995d587 --- /dev/null +++ b/apps/smclock/metadata.json @@ -0,0 +1,20 @@ +{ + "id": "smclock", + "name": "Monogram Watch Face", + "shortName": "MonoClock", + "icon": "app.png", + "screenshots": [{ "url": "screenshot.png" }], + "version": "0.04", + "description": "A simple watchface based on my stylised monogram.", + "type": "clock", + "tags": "clock", + "readme": "README.md", + "supports": ["BANGLEJS", "BANGLEJS2"], + "allow_emulator": true, + "storage": [ + { "name": "smclock.app.js", "url": "app.js" }, + { "name": "smclock.settings.js", "url": "settings.js" }, + { "name": "smclock.img", "url": "app-icon.js", "evaluate": true } + ], + "data": [{ "name": "smclock.json" }] +} diff --git a/apps/smclock/screenshot.png b/apps/smclock/screenshot.png new file mode 100644 index 000000000..c0e0bd0ee Binary files /dev/null and b/apps/smclock/screenshot.png differ diff --git a/apps/smclock/settings.js b/apps/smclock/settings.js new file mode 100644 index 000000000..a6c7d1b98 --- /dev/null +++ b/apps/smclock/settings.js @@ -0,0 +1,94 @@ +// settings menu for Monogram Watch Face +// Anton Clock settings were used as template +// helper functions taken from Anton Clock + +(function (back) { + var FILE = "smclock.json"; + // load settings from the file + // assign default values if it doesn't exist + var settings = Object.assign({ + dateFormat: "Short", + drawInterval: 10, + pollInterval: 60, + showAnalogFace: false, + showWeekInfo: false, + useVectorFont: false, + }, require("Storage").readJSON(FILE, true) || {}); + + // write the new settings to the file + 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) { + return { + value: startvalue === undefined ? 0 : values.indexOf(startvalue), + format: v => values[v], + min: 0, + max: values.length - 1, + wrap: true, + step: 1, + onchange: v => { + writer(values[v]); + writeSettings(); + }, + }; + } + + // 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); + } + + // settings menu + var mainmenu = { + "": {title: "Monogram Clock",}, + "< Back": () => back(), + "Analog Face": { + value: + settings.showAnalogFace !== undefined ? settings.showAnalogFace : false, + format: v => v ? "On" : "Off", + onchange: v => { + settings.showAnalogFace = v; + writeSettings(); + }, + }, + Date: stringInSettings("dateFormat", ["Long", "Short"]), + "Draw Interval": { + value: settings.drawInterval, + onchange: v => { + settings.drawInterval = v; + writeSettings(); + }, + }, + "Poll Interval": { + value: settings.pollInterval, + onchange: v => { + settings.pollInterval = v; + writeSettings(); + }, + }, + "Week Info": { + value: + settings.showWeekInfo !== undefined ? settings.showWeekInfo : false, + format: v => v ? "On" : "Off", + onchange: v => { + settings.showWeekInfo = v; + writeSettings(); + }, + }, + "Vector Font": { + value: + settings.useVectorFont !== undefined ? settings.useVectorFont : false, + format: v => v ? "On" : "Off", + onchange: v => { + settings.useVectorFont = v; + writeSettings(); + }, + }, + }; + + // Actually display the menu + E.showMenu(mainmenu); +}); + +// end of file diff --git a/apps/snek/snek.icon.js b/apps/snek/snek.icon.js index b820ffcf7..c9a17eee3 100644 --- a/apps/snek/snek.icon.js +++ b/apps/snek/snek.icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("oFA4X/AAOJksvr2rmokYgWqB7sq/2AB5krgYPMgW8ioPc1X9i/oLplVqv+1BdK1OV//q9QPMv4PL1eqy/q1SRK3tVu+AgWCFxP96t+Vhn9qoPLgWr/+//wFBSBEq3/qlW+JwJ/I3eXDQIOBB5OrB5sC3xMD1WAH4+r6xsOtSpKLoYPN1fV1bpKTYf+RJAeDytXFxoPOdQYPNPpkCy1VtQPc6wvO62Vu+CbhfVN4P//+q//uMgwPH9QPH3tqqtpqoABv4wHfoOpBoP/6tVUg7uBFwIvB3xlIB4v+OpJsC1WA1fVQpiGCB52+uzlMB58A31XB5sqy4PNlYPfH50rywPN3++BxgPPgW9V5kCZ4L/HBwmq/tX1APM/4PMBwNVvxuKgW/tP/HxUq1X+1eqFxQPRAAKsLB4KqNAFY=")) +require("heatshrink").decompress(atob("mEwwcBkmSpICZqVECJ+SCJ+UxIRP0lIggRNlckxFICJlKrYGCsmJNBfZsmSpdkyIRL7YRBAwIRLAYQyLNAQRPkoGDCJlLBwQmBAoZ6HBYI4Cy3ZCJVZCITpLymSCIhWMF4IRMlMky3JkTRMAYTjNqREBCJ4DCX5gRD2IRO5MlCKAjQRgOSkslBIRrMUoO2fZVSpdkyQ4BBIWRUJNtBIzpHWYYCCpYRJa4RWDEZQCH0oR0yuyCJ+UCKI1QEaOkCKI1PAYQgMyQDDNBwDBxIRLdgOydgQRKqVJloROyVLthWOpQONAUIA=")) diff --git a/apps/speedalt/ChangeLog b/apps/speedalt/ChangeLog index 0550f9b86..78c14594b 100644 --- a/apps/speedalt/ChangeLog +++ b/apps/speedalt/ChangeLog @@ -9,3 +9,4 @@ 0.09: Add third screen mode with large clock and waypoint selection display to ease visibility in bright daylight. 0.10: Add Kalman filter to smooth the speed and altitude values. Can be disabled in settings. 0.11: Now also runs on Bangle.js 2 with basic functionality +0.12: Full functionality on Bangle.js 2: Bangle.js 1 buttons mapped to touch areas. diff --git a/apps/speedalt/README.md b/apps/speedalt/README.md index c21828aff..6f0d4efe5 100644 --- a/apps/speedalt/README.md +++ b/apps/speedalt/README.md @@ -2,23 +2,21 @@ 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. -*Note for **Bangle.js 2:** Currently only the BTN3 functionality is working with the Bangle.js 2 button.* - 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. ## Buttons and Controls -BTN3 : Cycles the modes between Speed+[A]ltitude, Speed+[D]istance and large Time/Waypoint +*(Mapping for **Bangle.js 2**: BTN2 = Touch upper right side; BTN3 = Touch lower right side; BTN4 = Touch left side)* -***Bangle.js 2:** Currently only this button function is working* +BTN3 : Cycles the modes between Speed+[A]ltitude, Speed+[D]istance and large Time/Waypoint ### [A]ltitude mode BTN1 : Short press < 2 secs toggles the displays between showing the current speed/alt values or the maximum speed/alt values recorded. -BTN1 : Long press > 2 secs resets the recorded maximum values. +BTN1 : Long press > 2 secs resets the recorded maximum values. *(Bangle.js 2: Long press > 0.4 secs)* ### [D]istance mode @@ -32,7 +30,7 @@ BTN1 : Select next waypoint. BTN2 : Disables/Restores power saving timeout. Locks the screen on and GPS in SuperE mode to enable reading for longer periods but uses maximum battery drain. Red LED (dot) at top of screen when screen is locked on. Press again to restore power saving timeouts. -BTN3 : Long press exit and return to watch. +BTN3 : Long press exit and return to watch. *(Bangle.js 2: Long press BTN > 2 secs)* BTN4 : Left Display Tap : Swaps which figure is in the large display. You can have either speed or [A]ltitude/[D]istance on the large primary display. diff --git a/apps/speedalt/app.js b/apps/speedalt/app.js index f979762f1..79db932db 100644 --- a/apps/speedalt/app.js +++ b/apps/speedalt/app.js @@ -349,7 +349,7 @@ function drawSecondary(n,u) { s = 30; // Font size if (BANGLEJS2) s *= fontFactorB2; buf.setFontVector(s); - buf.drawString(u,xu - (BANGLEJS2*20),screenH_TwoThirds-25); + buf.drawString(u,xu - (BANGLEJS2*xu/5),screenH_TwoThirds-25); } function drawTime() { @@ -391,7 +391,7 @@ function drawWP() { // from waypoints.json - see README.md buf.setFontAlign(-1,1); //left, bottom if (BANGLEJS2) s *= fontFactorB2; buf.setFontVector(s); - buf.drawString(nm.substring(0,6),72,screenH_TwoThirds-(BANGLEJS2 * 20)); + buf.drawString(nm.substring(0,6),72,screenH_TwoThirds-(BANGLEJS2 * 15)); } if ( cfg.modeA == 2 ) { // clock/large mode @@ -421,7 +421,7 @@ function drawSats(sats) { buf.drawString('A',screenW,140-(BANGLEJS2 * 40)); if ( showMax ) { buf.setFontAlign(0,1); //centre, bottom - buf.drawString('MAX',120,164); + buf.drawString('MAX',screenW_Half,screenH_TwoThirds + 4); } } if ( cfg.modeA == 0 ) buf.drawString('D',screenW,140-(BANGLEJS2 * 40)); @@ -536,22 +536,18 @@ function onGPS(fix) { } -function setButtons(){ -if (!BANGLEJS2) { // Buttons for Bangle.js - // Spd+Dist : Select next waypoint - setWatch(function(e) { - var dur = e.time - e.lastTime; - if ( cfg.modeA == 1 ) { - // Spd+Alt mode - Switch between fix and MAX - if ( dur < 2 ) showMax = !showMax; // Short press toggle fix/max display - else { max.spd = 0; max.alt = 0; } // Long press resets max values. - } - else nxtWp(1); // Spd+Dist or Clock mode - Select next waypoint - onGPS(lf); - }, BTN1, { edge:"falling",repeat:true}); - // Power saving on/off - setWatch(function(e){ +function btn1press(longpress) { + if(emulator) console.log("Btn1, long="+longpress); + if ( cfg.modeA == 1 ) { // Spd+Alt mode - Switch between fix and MAX + if ( !longpress ) showMax = !showMax; // Short press toggle fix/max display + else { max.spd = 0; max.alt = 0; } // Long press resets max values. + } + else nxtWp(1); // Spd+Dist or Clock mode - Select next waypoint + onGPS(lf); + } +function btn2press(){ + if(emulator) console.log("Btn2"); pwrSav=!pwrSav; if ( pwrSav ) { LED1.reset(); @@ -564,52 +560,51 @@ if (!BANGLEJS2) { // Buttons for Bangle.js Bangle.setLCDPower(1); LED1.set(); } - }, BTN2, {repeat:true,edge:"falling"}); - - // Toggle between alt or dist - setWatch(function(e){ - cfg.modeA = cfg.modeA+1; - if ( cfg.modeA > 2 ) cfg.modeA = 0; - savSettings(); - onGPS(lf); - }, BTN3, {repeat:true,edge:"falling"}); - - // Touch left screen to toggle display - setWatch(function(e){ - cfg.primSpd = !cfg.primSpd; - savSettings(); - onGPS(lf); // Update display - }, BTN4, {repeat:true,edge:"falling"}); - -} else { // Buttons for Bangle.js 2 - setWatch(function(e){ // Bangle.js BTN3 + } +function btn3press(){ + if(emulator) console.log("Btn3"); cfg.modeA = cfg.modeA+1; if ( cfg.modeA > 2 ) cfg.modeA = 0; if(emulator)console.log("cfg.modeA="+cfg.modeA); savSettings(); onGPS(lf); - }, BTN1, {repeat:true,edge:"falling"}); - -/* Bangle.on('tap', function(data) { // data - {dir, double, x, y, z} + } +function btn4press(){ + if(emulator) console.log("Btn4"); cfg.primSpd = !cfg.primSpd; - if(emulator)console.log("!cfg.primSpd"); - }); */ + savSettings(); + onGPS(lf); // Update display + } -/* Bangle.on('swipe', function(dir) { - if (dir < 0) { // left: Bangle.js BTN3 - cfg.modeA = cfg.modeA+1; - if ( cfg.modeA > 2 ) cfg.modeA = 0; - if(emulator)console.log("cfg.modeA="+cfg.modeA); - } + +function setButtons(){ +if (!BANGLEJS2) { // Buttons for Bangle.js 1 + setWatch(function(e) { + btn1press(( e.time - e.lastTime) > 2); // > 2 sec. is long press + }, BTN1, { edge:"falling",repeat:true}); + + // Power saving on/off (red dot visible if off) + setWatch(btn2press, BTN2, {repeat:true,edge:"falling"}); + + // Toggle between alt or dist + setWatch(btn3press, BTN3, {repeat:true,edge:"falling"}); + + // Touch left screen to toggle display + setWatch(btn4press, BTN4, {repeat:true,edge:"falling"}); + +} else { // Buttons for Bangle.js 2 + setWatch(function(e) { + btn1press(( e.time - e.lastTime) > 0.4); // > 0.4 sec. is long press + }, BTN1, { edge:"falling",repeat:true}); + + Bangle.on('touch', function(btn_l_r, e) { + if(e.x < screenW_Half) btn4press(); else - { // right: Bangle.js BTN4 - cfg.primSpd = !cfg.primSpd; - if(emulator)console.log("!cfg.primSpd"); - } + if (e.y < screenH_Half) + btn2press(); + else + btn3press(); }); -*/ - savSettings(); - onGPS(lf); } } @@ -700,18 +695,6 @@ Bangle.on('lcdPower',function(on) { else stopDraw(); }); -/* -function onGPSraw(nmea) { - var nofGP = 0, nofBD = 0, nofGL = 0; - if (nmea.slice(3,6) == "GSV") { - // console.log(nmea.slice(1,3) + " " + nmea.slice(11,13)); - if (nmea.slice(0,7) == "$GPGSV,") nofGP = Number(nmea.slice(11,13)); - if (nmea.slice(0,7) == "$BDGSV,") nofBD = Number(nmea.slice(11,13)); - if (nmea.slice(0,7) == "$GLGSV,") nofGL = Number(nmea.slice(11,13)); - SATinView = nofGP + nofBD + nofGL; - } } -if(BANGLEJS2) Bangle.on('GPS-raw', onGPSraw); -*/ var gpssetup; try { diff --git a/apps/speedalt/metadata.json b/apps/speedalt/metadata.json index 617ac4b8e..e03d23c8b 100644 --- a/apps/speedalt/metadata.json +++ b/apps/speedalt/metadata.json @@ -2,7 +2,7 @@ "id": "speedalt", "name": "GPS Adventure Sports", "shortName": "GPS Adv Sport", - "version": "0.11", + "version": "0.12", "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", diff --git a/apps/speedalt2/ChangeLog b/apps/speedalt2/ChangeLog index 602147856..73e9bfc40 100644 --- a/apps/speedalt2/ChangeLog +++ b/apps/speedalt2/ChangeLog @@ -12,4 +12,4 @@ 1.10: Adds Kalman filter. 1.14: Add VMG and coordinates screens 1.43: Adds mirroring of the watch face to an Android device. See README.md -1.48: Droidscript mirroring prog automatically uses last connection address. Auto connects when run. +1.49: Droidscript mirroring prog automatically uses last connection address. Auto connects when run. diff --git a/apps/speedalt2/app.js b/apps/speedalt2/app.js index 73fa3bacb..ed16131a4 100644 --- a/apps/speedalt2/app.js +++ b/apps/speedalt2/app.js @@ -179,7 +179,8 @@ var buf = Graphics.createArrayBuffer(240,160,2,{msb:true}); let LED = // LED as minimal and only definition (as instance / singleton) { isOn: false // status on / off, not needed if you don't need to ask for it , set: function(v) { // turn on w/ no arg or truey, else off - g.setColor((this.isOn=(v===undefined||!!v))?1:0,0,0).fillCircle(120,10,10); } + this.isOn = v===undefined||!!v; + g.setColor(this.isOn?1:0,0,0).fillCircle(120,10,10); } , 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 diff --git a/apps/stopwatch/ChangeLog b/apps/stopwatch/ChangeLog index 9db0e26c5..104fce19d 100644 --- a/apps/stopwatch/ChangeLog +++ b/apps/stopwatch/ChangeLog @@ -1 +1,2 @@ 0.01: first release +0.02: Adjust for touch events outside of screen g dimensions diff --git a/apps/stopwatch/metadata.json b/apps/stopwatch/metadata.json index e72d85af1..cc13ec92f 100644 --- a/apps/stopwatch/metadata.json +++ b/apps/stopwatch/metadata.json @@ -1,7 +1,7 @@ { "id": "stopwatch", "name": "Stopwatch Touch", - "version": "0.01", + "version": "0.02", "description": "A touch based stop watch for Bangle JS 2", "icon": "stopwatch.png", "screenshots": [{"url":"screenshot1.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"}], diff --git a/apps/stopwatch/stopwatch.app.js b/apps/stopwatch/stopwatch.app.js index 48d4f26ea..e2be95451 100644 --- a/apps/stopwatch/stopwatch.app.js +++ b/apps/stopwatch/stopwatch.app.js @@ -185,17 +185,27 @@ resetBtn.setImage(pause_img); Bangle.on('touch', function(button, xy) { + var x = xy.x; + var y = xy.y; + + // adjust for outside the dimension of the screen + // http://forum.espruino.com/conversations/371867/#comment16406025 + if (y > h) y = h; + if (y < 0) y = 0; + if (x > w) x = w; + if (x < 0) x = 0; + // not running, and reset - if (!running && tCurrent == tTotal && bigPlayPauseBtn.check(xy.x, xy.y)) return; + if (!running && tCurrent == tTotal && bigPlayPauseBtn.check(x, y)) return; // paused and hit play - if (!running && tCurrent != tTotal && smallPlayPauseBtn.check(xy.x, xy.y)) return; + if (!running && tCurrent != tTotal && smallPlayPauseBtn.check(x, y)) return; // paused and press reset - if (!running && tCurrent != tTotal && resetBtn.check(xy.x, xy.y)) return; + if (!running && tCurrent != tTotal && resetBtn.check(x, y)) return; // must be running - if (running && bigPlayPauseBtn.check(xy.x, xy.y)) return; + if (running && bigPlayPauseBtn.check(x, y)) return; }); // Stop updates when LCD is off, restart when on diff --git a/apps/swiperclocklaunch/ChangeLog b/apps/swiperclocklaunch/ChangeLog index c1b4a5fbb..244b602b5 100644 --- a/apps/swiperclocklaunch/ChangeLog +++ b/apps/swiperclocklaunch/ChangeLog @@ -1,2 +1,3 @@ 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 diff --git a/apps/swiperclocklaunch/boot.js b/apps/swiperclocklaunch/boot.js index e9b203eee..bb285ea94 100644 --- a/apps/swiperclocklaunch/boot.js +++ b/apps/swiperclocklaunch/boot.js @@ -4,6 +4,7 @@ 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); diff --git a/apps/swiperclocklaunch/metadata.json b/apps/swiperclocklaunch/metadata.json index 733aaa032..5e4a0d648 100644 --- a/apps/swiperclocklaunch/metadata.json +++ b/apps/swiperclocklaunch/metadata.json @@ -1,7 +1,7 @@ { "id": "swiperclocklaunch", "name": "Swiper Clock Launch", - "version": "0.02", + "version": "0.03", "description": "Navigate between clock and launcher with Swipe action", "icon": "swiperclocklaunch.png", "type": "bootloader", diff --git a/apps/tabata/ChangeLog b/apps/tabata/ChangeLog index 5560f00bc..86faf17a5 100644 --- a/apps/tabata/ChangeLog +++ b/apps/tabata/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Fix settings wrapping code for new menu system diff --git a/apps/tabata/metadata.json b/apps/tabata/metadata.json index 14429090f..a840c9c8b 100644 --- a/apps/tabata/metadata.json +++ b/apps/tabata/metadata.json @@ -2,7 +2,7 @@ "id": "tabata", "name": "Tabata", "shortName": "Tabata - Control High-Intensity Interval Training", - "version": "0.01", + "version": "0.02", "description": "Control high-intensity interval training (according to tabata: https://en.wikipedia.org/wiki/Tabata_method).", "icon": "tabata.png", "tags": "workout,health", diff --git a/apps/tabata/tabata.js b/apps/tabata/tabata.js index 603cf96ee..f6addc35b 100644 --- a/apps/tabata/tabata.js +++ b/apps/tabata/tabata.js @@ -30,18 +30,17 @@ function showMainMenu() { }, 'Pause sec.': { value: settings.pause, - onchange: function(v){ - if (v<0)v=MAX_SECONDS; - if (v>MAX_SECONDS)v=0; + min : 0, max : MAX_SECONDS, wrap : true, + onchange: v => { settings.pause=v; - this.value=v; saveSettingsDebounce(); } }, 'Trainig sec.': { value: settings.training, - onchange: function(v){if (v<0)v=MAX_SECONDS;if (v>MAX_SECONDS)v=0;settings.training=v; - this.value=v; + min : 0, max : MAX_SECONDS, wrap : true, + onchange: v => { + settings.training=v; saveSettingsDebounce(); } }, @@ -61,8 +60,8 @@ function startTabata() { g.clear(); Bangle.setLCDMode("doublebuffered"); g.flip(); - var pause = settings.pause, - training = settings.training, + var pause = settings.pause, + training = settings.training, round = 1, active = true, clearBtn1, clearBtn2, clearBtn3, timer; @@ -86,7 +85,7 @@ function startTabata() { exitTraining(); return; } - + if (active) { drawCountDown(round, training, active); training--; @@ -117,7 +116,7 @@ function drawCountDown(round, count, active) { g.setFontAlign(0,0); g.setFont("6x8", 2); - g.drawString("Round " + round + "/" + settings.rounds,120,6); + g.drawString("Round " + round + "/" + settings.rounds,120,6); g.setFont("6x8", 6); g.drawString("" + count,120,80); diff --git a/apps/terminalclock/ChangeLog b/apps/terminalclock/ChangeLog new file mode 100644 index 000000000..6515ab627 --- /dev/null +++ b/apps/terminalclock/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! +0.02: Rename "Activity" in "Motion" and display the true values for it diff --git a/apps/terminalclock/README.md b/apps/terminalclock/README.md new file mode 100644 index 000000000..5a54583d2 --- /dev/null +++ b/apps/terminalclock/README.md @@ -0,0 +1,9 @@ +# Terminal clock + +A clock displayed as a terminal cli. +It can display : +- time +- date +- hrm +- motion +- steps diff --git a/apps/terminalclock/app-icon.js b/apps/terminalclock/app-icon.js new file mode 100644 index 000000000..5e57a4b7c --- /dev/null +++ b/apps/terminalclock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwghC/AHsHu93uAX/C+wACsAaTC/4Xiu1mswXRCYNnDQQxTgwX/C8QABuAaTg4X/C7Z3Xa/4Xds1ms4XUCgV2DYIXUsBdTC/4AEg4VCC61wIiYX/C74A/AGIA==")) diff --git a/apps/terminalclock/app.js b/apps/terminalclock/app.js new file mode 100644 index 000000000..ab83a696f --- /dev/null +++ b/apps/terminalclock/app.js @@ -0,0 +1,135 @@ +var locale = require("locale"); +var fontColor = g.theme.dark ? "#0f0" : "#000"; +var paddingY = 2; +var font6x8At4Size = 32; +var font6x8At2Size = 18; +var heartRate = 0; + + +function setFontSize(pos){ + if(pos == 1) + g.setFont("6x8", 4); + else + g.setFont("6x8", 2); +} + +function clearField(pos){ + var yStartPos = Bangle.appRect.y + + paddingY * (pos - 1) + + font6x8At4Size * Math.min(1, pos-1) + + font6x8At2Size * Math.max(0, pos-2); + var yEndPos = Bangle.appRect.y + + paddingY * (pos - 1) + + font6x8At4Size * Math.min(1, pos) + + font6x8At2Size * Math.max(0, pos-1); + g.clearRect(Bangle.appRect.x, yStartPos, Bangle.appRect.x2, yEndPos); +} + +function clearWatchIfNeeded(now){ + if(now.getMinutes() % 10 == 0) + g.clearRect(Bangle.appRect.x, Bangle.appRect.y, Bangle.appRect.x2, Bangle.appRect.y2); +} + +function drawLine(line, pos){ + setFontSize(pos); + var yPos = Bangle.appRect.y + + paddingY * (pos - 1) + + font6x8At4Size * Math.min(1, pos-1) + + font6x8At2Size * Math.max(0, pos-2); + g.drawString(line, 5, yPos, true); +} + +function drawTime(now, pos){ + var h = now.getHours(); + var m = now.getMinutes(); + var time = ">" + (""+h).substr(-2) + ":" + ("0"+m).substr(-2); + drawLine(time, pos); +} + +function drawDate(now, pos){ + var dow = locale.dow(now, 1); + var date = locale.date(now, 1).substr(0,6) + locale.date(now, 1).substr(-2); + var locale_date = ">" + dow + " " + date; + drawLine(locale_date, pos); +} + +function drawInput(now, pos){ + clearField(pos); + drawLine(">", pos); +} + +function drawStepCount(pos){ + var health = Bangle.getHealthStatus("day"); + var steps_formated = ">Steps: " + health.steps; + drawLine(steps_formated, pos); +} + +function drawHRM(pos){ + clearField(pos); + if(heartRate != 0) + drawLine(">HR: " + parseInt(heartRate), pos); + else + drawLine(">HR: unknown", pos); +} + +function drawActivity(pos){ + clearField(pos); + var health = Bangle.getHealthStatus('last'); + var steps_formated = ">Motion: " + parseInt(health.movement); + drawLine(steps_formated, pos); +} + +function draw(){ + var curPos = 1; + g.reset(); + g.setFontAlign(-1, -1); + g.setColor(fontColor); + var now = new Date(); + clearWatchIfNeeded(now); // mostly to not have issues when changing days + drawTime(now, curPos); + curPos++; + if(settings.showDate){ + drawDate(now, curPos); + curPos++; + } + if(settings.showHRM){ + drawHRM(curPos); + curPos++; + } + if(settings.showActivity){ + drawActivity(curPos); + curPos++; + } + if(settings.showStepCount){ + drawStepCount(curPos); + curPos++; + } + drawInput(now, curPos); +} + +Bangle.on('HRM',function(hrmInfo) { + if(hrmInfo.confidence >= settings.HRMinConfidence) + heartRate = hrmInfo.bpm; +}); + + +// Clear the screen once, at startup +g.clear(); +// load the settings +var settings = Object.assign({ + // default values + HRMinConfidence: 50, + showDate: true, + showHRM: true, + showActivity: true, + showStepCount: true, +}, require('Storage').readJSON("terminalclock.json", true) || {}); +// Show launcher when middle button pressed +Bangle.setUI("clock"); +// Load widgets +Bangle.loadWidgets(); +Bangle.drawWidgets(); +// draw immediately at first +draw(); + +var secondInterval = setInterval(draw, 10000); diff --git a/apps/terminalclock/app.png b/apps/terminalclock/app.png new file mode 100644 index 000000000..5d4507ead Binary files /dev/null and b/apps/terminalclock/app.png differ diff --git a/apps/terminalclock/metadata.json b/apps/terminalclock/metadata.json new file mode 100644 index 000000000..de0244318 --- /dev/null +++ b/apps/terminalclock/metadata.json @@ -0,0 +1,24 @@ +{ + "id": "terminalclock", + "name": "Terminal Clock", + "shortName":"Terminal Clock", + "description": "A terminal cli like clock displaying multiple sensor data", + "version":"0.02", + "icon": "app.png", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name": "terminalclock.app.js","url": "app.js"}, + {"name": "terminalclock.settings.js","url": "settings.js"}, + {"name": "terminalclock.img","url": "app-icon.js","evaluate": true} + ], + "data": [ + {"name": "terminalclock.json"} + ], + "screenshots": [ + {"url": "screenshot1.png"}, + {"url": "screenshot2.png"} + ] +} diff --git a/apps/terminalclock/screenshot1.png b/apps/terminalclock/screenshot1.png new file mode 100644 index 000000000..c0f472102 Binary files /dev/null and b/apps/terminalclock/screenshot1.png differ diff --git a/apps/terminalclock/screenshot2.png b/apps/terminalclock/screenshot2.png new file mode 100644 index 000000000..8bd552a73 Binary files /dev/null and b/apps/terminalclock/screenshot2.png differ diff --git a/apps/terminalclock/settings.js b/apps/terminalclock/settings.js new file mode 100644 index 000000000..77df69b12 --- /dev/null +++ b/apps/terminalclock/settings.js @@ -0,0 +1,61 @@ +(function(back) { + var FILE = "terminalclock.json"; + // Load settings + var settings = Object.assign({ + HRMinConfidence: 50, + showDate: true, + showHRM: true, + showActivity: true, + showStepCount: true, + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + // Show the menu + E.showMenu({ + "" : { "title" : "Terminal Clock" }, + "< Back" : () => back(), + 'HR confidence': { + value: 50|settings.HRMinConfidence, // 0| converts undefined to 0 + min: 0, max: 100, + onchange: v => { + settings.HRMinConfidence = v; + writeSettings(); + } + }, + 'Show date': { + value: !!settings.showDate, + format: v => v?"Yes":"No", + onchange: v => { + settings.showDate = v; + writeSettings(); + } + }, + 'Show HRM': { + value: !!settings.showHRM, + format: v => v?"Yes":"No", + onchange: v => { + settings.showHRM = v; + writeSettings(); + } + }, + 'Show Activity': { + value: !!settings.showActivity, + format: v => v?"Yes":"No", + onchange: v => { + settings.showActivity = v; + writeSettings(); + } + }, + 'Show Steps': { + value: !!settings.showStepCount, + format: v => v?"Yes":"No", + onchange: v => { + settings.showStepCount = v; + writeSettings(); + } + } + }); +}) diff --git a/apps/thering/0p.png b/apps/thering/0p.png new file mode 100644 index 000000000..26b159a17 Binary files /dev/null and b/apps/thering/0p.png differ diff --git a/apps/thering/100p.png b/apps/thering/100p.png new file mode 100644 index 000000000..3e1464b65 Binary files /dev/null and b/apps/thering/100p.png differ diff --git a/apps/thering/10p.png b/apps/thering/10p.png new file mode 100644 index 000000000..e2a80aa23 Binary files /dev/null and b/apps/thering/10p.png differ diff --git a/apps/thering/1circle.png b/apps/thering/1circle.png new file mode 100644 index 000000000..fe50dcb3a Binary files /dev/null and b/apps/thering/1circle.png differ diff --git a/apps/thering/1ring.png b/apps/thering/1ring.png new file mode 100644 index 000000000..8a5e49063 Binary files /dev/null and b/apps/thering/1ring.png differ diff --git a/apps/thering/20p.png b/apps/thering/20p.png new file mode 100644 index 000000000..4b2ce214c Binary files /dev/null and b/apps/thering/20p.png differ diff --git a/apps/thering/2p.png b/apps/thering/2p.png new file mode 100644 index 000000000..aff255ce3 Binary files /dev/null and b/apps/thering/2p.png differ diff --git a/apps/thering/30p.png b/apps/thering/30p.png new file mode 100644 index 000000000..83de9f45d Binary files /dev/null and b/apps/thering/30p.png differ diff --git a/apps/thering/40p.png b/apps/thering/40p.png new file mode 100644 index 000000000..07470987a Binary files /dev/null and b/apps/thering/40p.png differ diff --git a/apps/thering/4p.png b/apps/thering/4p.png new file mode 100644 index 000000000..e8b26942d Binary files /dev/null and b/apps/thering/4p.png differ diff --git a/apps/thering/50p.png b/apps/thering/50p.png new file mode 100644 index 000000000..0c1865f57 Binary files /dev/null and b/apps/thering/50p.png differ diff --git a/apps/thering/60p.png b/apps/thering/60p.png new file mode 100644 index 000000000..18720530a Binary files /dev/null and b/apps/thering/60p.png differ diff --git a/apps/thering/70p.png b/apps/thering/70p.png new file mode 100644 index 000000000..a55a0ffaf Binary files /dev/null and b/apps/thering/70p.png differ diff --git a/apps/thering/7p.png b/apps/thering/7p.png new file mode 100644 index 000000000..5de4ae719 Binary files /dev/null and b/apps/thering/7p.png differ diff --git a/apps/thering/80p.png b/apps/thering/80p.png new file mode 100644 index 000000000..a183084eb Binary files /dev/null and b/apps/thering/80p.png differ diff --git a/apps/thering/90p.png b/apps/thering/90p.png new file mode 100644 index 000000000..ccaaff9ab Binary files /dev/null and b/apps/thering/90p.png differ diff --git a/apps/thering/README.md b/apps/thering/README.md new file mode 100644 index 000000000..382fe853f --- /dev/null +++ b/apps/thering/README.md @@ -0,0 +1,73 @@ +# The Ring + + *A proof of concept clock with large ring guage for steps using pre-set images, acts as a tutorial piece for discussion* + + +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/) + +* The ring is a proof of concept to establish a clean way to draw a +large ring guage with few aliasing issues and artifacts. +* Rather than use grahics commands to draw the ring a series of fixed images are used. +* This allows for better accuracy of the initial image and also does not suffer from performance issues. +* The downside is that more storage and memory is used to hold the + initial images. This is not an issue on a Bangle 2. +* The ring effect is constructed from 14 images that represent a range of different percentages +* The percentages of the images are 0,2,4,7,10,20,30,40,50,60,70,80,90,100% +* The app is not intended to be enhanced further (apart from bug fixes) but rather as code that can be reused in other apps +* The full set of original images are included in the source code to demonstrate the concept +* I will use this code to build a new clock similar to Pastel but + using this ring guage for steps. The new clock will use more + attractive fonts and provide a settings meu to change the primary + color of the ring. + + +## Screenshots + +![](screenshot_thering1.png) + +It is worth looking at a photograph of the clock in action as the +screenshot does not do the final effect justice. + +![](screenshot_thering2.jpg) + +## Production + +1. I first generated a circle on black background using [The +Gimp](https://www.gimp.org/) image editor. I used this [Youtube +video](https://www.youtube.com/watch?v=AoIAznSdLik) to get started. +The initial image is 178x178 pixels. + +![](1circle.png) + +2. I then drew another smaller black circle over the top of the original to make a ring + +![](1ring.png) + +3. From the empty ring image I coloured segments of the ring and saved new images at specific percentages + +4. I used the file `calc_percentages.js` to work out the x and y +coordinates of the end point of each percentage position along the +ring. + +5. The [Image +Converter](https://espruino.github.io/EspruinoWebTools/examples/imageconverter.html) +was used, set to 2-bit optimal, transparency Y, compression Y and +ImageObject Y, to convert each PNG file to code. + +6. NOTE that the generated image object pallete seemed to switch the +order of the colors from 50% onwards. + +7. The greying out of the unused part of the ring is acheived by +using a dithered color. So if the ring colour is green #0f0 then the +greyed out part is done in '#020'. + + +## Stages of The Ring + +Below are some examples of the different stages of the ring + +![](0p.png) +![](7p.png) +![](60p.png) diff --git a/apps/thering/app-icon.js b/apps/thering/app-icon.js new file mode 100644 index 000000000..7d6edf547 --- /dev/null +++ b/apps/thering/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4UA///un/1XEh32DyX/+AKIn/6//ABQ0P/Wq1dcBY3+1QAB64KFgfqBYWVoAtFBQWq2tQBYgtBBYdUERALBq5FE1WuF4dVKoYiB3QLEHgQiC1eq1EAitVuAiEBoOABYsOD4PqCwMA6tVKgU6FwWgAwNVqqFC/WrEQI1CBYY6BFwIiCgILDHQO79QiCBYg6BFwJFBgEFBYc+1YLBOAU1BYf6326FwReCBYXq3wuDLwRfBIwIuEKQR3BgY6CCwRGCBYbsBBYUNBYVAh+r1w6DIwS/BhwLBHQ1VNQILBHQzjBn2q9wLCFwb7Bn2u9wuEEgKCCUoIuEZQaOBIwQiDfQQvBBYRRDKQILEIoQiBIwK+CBYM9BYN9IwTWC1EDNAY6CBYP61AWCBoQuBFYP//oVDFwYLBBQppCAAX/D4V1XgQLDn4YEq4uCHgQYDqtfBQY8C//dCwPwBYgwBAAYiEGAQKC/gKFEgYWHHoYtGABYA=")) diff --git a/apps/thering/app.js b/apps/thering/app.js new file mode 100644 index 000000000..f7cfaa015 --- /dev/null +++ b/apps/thering/app.js @@ -0,0 +1,224 @@ +const h = g.getHeight(); +const w = g.getWidth(); +// palette for 0-40% +const pal1 = new Uint16Array([g.theme.bg, g.toColor("#020"), g.toColor("#0f0"), g.toColor("#00f")]); +// palette for 50-100% +const pal2 = new Uint16Array([g.theme.bg, g.toColor("#0f0"), g.toColor("#020"), g.toColor("#00f")]); +const infoWidth = 50; +const infoHeight = 14; + +var drawingSteps = false; +//var fakeSteps = 0; + +var p0_img = { + width : 176, height : 176, bpp : 2, + transparent : -1, + palette : pal1, + buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVAAVUFUgpDAAdAFMEBFQ4ABqBVnLMQqLLLzWEABLgbVgohEGopYaiofDBihWVHJpYYDgYPbKx1ACJhYZIwT4OcAZWYHyRYUIgQXQH4RqOThCXUYRpCHNyQVVQQTwVQiSZWIQSEQNgSYSIYiEQQSyEUCQLDSOAyCnQiSCYQiSCYQiSCZDaDARObKuBSZwcaVzR0QFYKuZWAYNZWCJJKMoKuaWAahKBhiwTJRSudURorBFTgfMVzqjDO5DaeZ5jaeJhhiKbi4rIbT4hLqoriPI7afUpS5BbTwiKFdZgIADSmHFYIqgbgIrGcgIriEYwzHADZ7HRY4rdaYrjHADcBFYoGBFcgkEGQwAeFYqKHFbzUEcQ4AdiorwiorlEogxFAD59FWoorhoArDqArjgIr/FbYwFAEJSDFf4rXgornqgrDFUkAior/Ff4rGAYYAjKYYr/Ff4r/FbdVFdFAFYNQFcsBFf4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/FbdUFcsFFYUVFdADBFf4r/Ff4rbAYYAjKYYr/Ff4rFoArkqorCgIrnqAr/FbIEFAEBSFFf4rYqgrjgorEiormAocVAogAfEooxFFcB9EFdq1DAD9VFYkBFctQFYoGEADokHFcp8FRQoAdag7iFFb4HFioHGADYjHGY4rcPYyLHADbTHcYNQFT4iIFdZgIADKmJqrcgiorIBIIrhMKIAXUpIrBbjzaBFZAKKbS5MJFcKkJbj4fLBYLcdqorKbjzPMbjxKNMhauTURawdJJorBWDShBFZiRBWDQcOHRyuPOhorBWDIbPWDRzQSYKEYIwLLOHgSEXDIJyPQjD2SQjCCQQjSCRCYY/QN4xDRQiyCSQgjdSCqqECLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5A")) +}; + +var p2_img = { + width : 176, height : 176, bpp : 2, + transparent : -1, + palette: pal1, + buffer : require("heatshrink").decompress(atob("AH4A/ADNUFE8FqtVq2q1AqkFIIrDAAOAFMEBFQYrE1WgKsYrGLL4qFFY2pqDWeFZdUVkAhCAQMKFYdVLDUVFQYMHlWq0oMJKyoOJlQrCLDBWDB5clB5xWOoARMCARYWKwT4OgpYXKwY+SLChECC6A/CNRycIS6jCNIQ5uSCqqCCeCqESTKxCCQiBsCTCRDEQiCCWQigSBYaRwGQU6ESQTCESQTCESQTIbQYCJzZVwKTODjSuaOiArBVzKwDBrKwRJJRlBVzSwDUJQMMWCZKKVzqiNFYIqcD5iudUYZ3IbTzPMbTxMMMRTcXFZDafEJdVFcR5HbT6lKXILaeERQrrMBAAaUw4rBFUDcBFYzkBFcQjGGY4AbPY6LHFbrTFcY4AbgIrFAwIrkEggyGADwrFRQ4reagjiHADsVFeEVFcolEGIoAfPoq1FFcNAFYdQFccBFf4rbGAoAhKQYr/Fa8FFc9UFYYqkgEVFf4r/FYwDDAEZTDFf4r/Ff4rbqorooArBqArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlgorCioroAYIr/Ff4r/FbYDDAEZTDFf4r/FYtAFclVFYUBFc9QFf4rZAgoAgKQor/FbFUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHI")) +}; + +var p4_img = { + width : 176, height : 176, bpp : 2, + transparent : -1, + palette : pal1, + buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFY2loAqjFY1VqDWeFZdUVkAhEhQrDLDcVFQYMHlQrCBhBWVHJpYYDgYPbKx1ACJhYZIwT4OgpYXKwY+SLChECC6A/CNRycIS6jCNIQ5uSCqqCCeCqESTKxCCQiBsCTCRDEQiCCWQigSBYaRwGQU6ESQTCESQTCESQTIbQYCJzZVwKTODjSuaOiArBVzKwDBrKwRJJRlBVzSwDUJQMMWCZKKVzqiNFYIqcD5iudUYZ3IbTzPMbTxMMMRTcXFZDafEJdVFcR5HbT6lKXILaeERQrrMBAAaUw4rBFUDcBFYzkBFcQjGGY4AbPY6LHFbrTFcY4AbgIrFAwIrkEggyGADwrFRQ4reagjiHADsVFeEVFcolEGIoAfPoq1FFcNAFYdQFccBFf4rbGAoAhKQYr/Fa8FFc9UFYYqkgEVFf4r/FYwDDAEZTDFf4r/Ff4rbqorooArBqArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlgorCioroAYIr/Ff4r/FbYDDAEZTDFf4r/FYtAFclVFYUBFc9QFf4rZAgoAgKQor/FbFUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHI")) +}; + +var p7_img = { + width : 176, height : 176, bpp : 2, + transparent : -1, + palette : pal1, + buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFYxZfFQorGLLrWCFZbgbVgtUBQcKLD8VFQYMHlQsDKzoOJFgZYYKwYPLFgWlKzVACJgrCqBWYawgAJcAOlNBhWMCZ8qFYJYUgoqBC6ECFYJqOAApWSS4jCNQQ5uSCqqCCeCqESFQKZUIQSEQNgSYSIYiEQQSyEUCQLDSOAyCnQiSCYQiSCYQiSCZDaDARObKuBSZwcaVzR0QFYKuZWAYNZWCJJKMoKuaWAahKBhiwTJRSudURorBFTgfMVzqjDO5DaeZ5jaeJhhiKbi4rIbT4hLqoriPI7afUpS5BbTwiKFdZgIADSmHFYIqgbgIrGcgIriEYwzHADZ7HRY4rdaYrjHADcBFYoGBFcgkEGQwAeFYqKHFbzUEcQ4AdiorwiorlEogxFAD59FWoorhoArDqArjgIr/FbYwFAEJSDFf4rXgornqgrDFUkAior/Ff4rGAYYAjKYYr/Ff4r/FbdVFdFAFYNQFcsBFf4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/FbdUFcsFFYUVFdADBFf4r/Ff4rbAYYAjKYYr/Ff4rFoArkqorCgIrnqAr/FbIEFAEBSFFf4rYqgrjgorEiormAocVAogAfEooxFFcB9EFdq1DAD9VFYkBFctQFYoGEADokHFcp8FRQoAdag7iFFb4HFioHGADYjHGY4rcPYyLHADbTHcYNQFT4iIFdZgIADKmJqrcgiorIBIIrhMKIAXUpIrBbjzaBFZAKKbS5MJFcKkJbj4fLBYLcdqorKbjzPMbjxKNMhauTURawdJJorBWDShBFZiRBWDQcOHRyuPOhorBWDIbPWDRzQSYKEYIwLLOHgSEXDIJyPQjD2SQjCCQQjSCRCYY/QN4xDRQiyCSQgjdSCqqECLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5A==")) +}; + +var p10_img = { + width : 176, height : 176, bpp : 2, + transparent : -1, + palette : pal1, + buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFYxZfFQorGLLrWCFZbgbVgtUBQcKLD8VFQYMHlQsDKzoOJFgZYYKwYPLFgZWaoARMLDJWCawgAJcAZWYCZ6FCLCkFFQNQCZ8CFYOoFaZWSLAmAQShWQLAiESQQRtTLAOkQSdUFacK1WloCCSCaAAEFYKaQQSyEC0pvQirZTbomlIh6CYZAZFOQTBxDQhyCYOQhoPQS4bQHaBzaVwKTODjSuaOiArBVzKwDBrKwRJJRlBVzSwDUJQMMWCZKKVzqiNFYIqcD5iudUYZ3IbTzPMbTxMMMRTcXFZDafEJdVFcR5HbT6lKXILaeERQrrMBAAaUw4rBFUDcBFYzkBFcQjGGY4AbPY6LHFbrTFcY4AbgIrFAwIrkEggyGADwrFRQ4reagjiHADsVFeEVFcolEGIoAfPoq1FFcNAFYdQFccBFf4rbGAoAhKQYr/Fa8FFc9UFYYqkgEVFf4r/FYwDDAEZTDFf4r/Ff4rbqorooArBqArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlgorCioroAYIr/Ff4r/FbYDDAEZTDFf4r/FYtAFclVFYUBFc9QFf4rZAgoAgKQor/FbFUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHI")) +}; + +var p20_img = { + width : 176, height : 176, bpp : 2, + transparent : -1, + palette : pal1, + buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFYxZfFQorGLLrWCFZbgbVgtUBQcKLD8VFQYMHlQsDKzoOJFgZYYKwYPLFgZWaoARMLDJWCawgAJcAZWYCZ6FCLCkFFQNQCZ8CFYOoFaZWSLAmAQShWQLAiESQQRtTLAKESFQNUFacKQiSCCoArTgCESQSyEUirZTboyCnQiSCYQiSCYQiSCZQgeAVxwqYQgSwMVwNUFbMKWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbhdVFcTcHbT7cDFY0BbT7cD0ArxgtVoArfgGq1ArHFUDcBFY0VFceqFY1UFcMKFY1VFcmAFYtQFcMCFYsBFcugFYtAFcMAFYsFFcuoFYoqigEqFeEVFcuqFYlUFccKFYlVFc2AFYdQFccCFf4AWgNVoAEGAERSDFf4rXgornqgrDFUkAior/Ff4rGAYYAjKYYr/Ff4r/FbdVFdFAFYNQFcsBFf4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/FbdUFcsFFYUVFdADBFf4r/Ff4rbAYYAjKYYr/Ff4rFoArkqorCgIrnqAr/FbIEFAEBSFFf4rYqgrjgorEiormAocVAogAfEooxFFcB9EFdq1DAD9VFYkBFctQFYoGEADokHFcp8FRQoAdag7iFFb4HFioHGADYjHGY4rcPYyLHADbTHcYNQFT4iIFdZgIADKmJqrcgiorIBIIrhMKIAXUpIrBbjzaBFZAKKbS5MJFcKkJbj4fLBYLcdqorKbjzPMbjxKNMhauTURawdJJorBWDShBFZiRBWDQcOHRyuPOhorBWDIbPWDRzQSYKEYIwLLOHgSEXDIJyPQjD2SQjCCQQjSCRCYY/QN4xDRQiyCSQgjdSCqqECLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5A=")) +}; + +var p30_img = { + width : 176, height : 176, bpp : 2, + transparent : -1, + palette : pal1, + buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFYxZfFQorGLLrWCFZbgbVgtUBQcKLD8VFQYMHlQsDKzoOJFgZYYKwYPLFgZWaoARMLDJWCawgAJcAZWYCZ6FCLCkFFQNQCZ8CFYOoFaZWSLAmAQShWQLAiESQQRtTLAKESFQNUFacKQiSCCoArTgCESQSyEUirZTboyCnQiSCYQiSCYQiSCZQgeAVxwqYQgSwMVwNUFbMKWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbhdVFcTcHbT7cDFY0BbT7cD0ArxgtVoArfgGq1ArHFUDcBFY0VFceqFY1UFcMKFY1VFcmAFYtQFcMCFYsBFcugFYtAFcMAFYsFFcuoFYoqigEqFeEVFcuqFYlUFccKFYlVFc2AFYdQFccCFf4rbgNVoArjgGq0Ar/FbMFFc+oFYYqkgEqFf4r/FY0VqgrlhWqFf4r/Ff4rdqorowArBqArlgQr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlhQrCioroAYIr/Ff4r/FbcFqorllWoFf4r/FY9AFcmqFYUBFc+gFf4rZgFVqAqjgWqwAr/FbdUFccFawkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHI")) +}; + +var p40_img = { + width : 176, height : 176, bpp : 2, + transparent : -1, + palette : pal1, + buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFYxZfFQorGLLrWCFZbgbVgtUBQcKLD8VFQYMHlQsDKzoOJFgZYYKwYPLFgZWaoARMLDJWCawgAJcAZWYCZ6FCLCkFFQNQCZ8CFYOoFaZWSLAmAQShWQLAiESQQRtTLAKESFQNUFacKQiSCCoArTgCESQSyEUirZTboyCnQiSCYQiSCYQiSCZQgeAVxwqYQgSwMVwNUFbMKWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbhdVFcTcHbT7cDFY0BbT7cD0ArxgtVoArfgGq1ArHFUDcBFY0VFceqFY1UFcMKFY1VFcmAFYtQFcMCFYsBFcugFYtAFcMAFYsFFcuoFYoqigEqFeEVFcuqFYlUFccKFYlVFc2AFYdQFccCFf4rbgNVoArjgGq0Ar/FbMFFc+oFYYqkgEqFf4r/FY0VqgrlhWqFf4r/Ff4rdqorowArBqArlgQr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlhQrCioroAYIr/Ff4r/FbcFqorllWoFf4r/FY9AFcmqFYUBFc+gFf4rZgFVqAqjgWqwAr/FbdUFccKFYkVFcwFDitVFccqFYkFFcuoFeNAFcWqFYkBFcugFYtQFUMCFYsAFcuAFYtUFcMKFY0VFcgHFitVFcMqFY0FFceoFY9AFcGqFY0BqtQFT8C1WgFeMAqtUFb8K1WAFY7cglQrIioriBI8FqtAFb2q1ArJbjzaBFZEBbj7aB0ALIFcLaHbkLaJFYbcd1QrKbjzaKbkDaLbgSwcVwLaJWD6uLFYawaVwIrMbgKwaVwLaKbgawaVwLaLbgawZQQLaLWDiuOWAaEYQQKuMWAiEXKwKuNQjUBQR6EaiqCPQjVVQSATCqtUFSZvB1WACiSEUY4KCQQgjdSCqqECLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5A")) +}; + +var p50_img = { + width : 176, height : 176, bpp : 2, + transparent : -1, + palette : pal2, + buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESFQOoFacFQiSCCwArTgCESQSyEUlTZTboyCnQiSCYQiSCYQiSCZQgdAVxwqYQgSwMVwOoFbMFWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbheqFcTcHbT7cDFY0CbT7cDqArxhWqwArfgFVqgrHFUDcBFY0qFcdVFY2oFcMFFY2qFclAFYugFcMBFYsCFctQFYuAFcMAFYsKFctUFYoqigEVFeEqFctVFYmoFccFFYmqFc1AFYegFccBFf4rbgWqwArjgFVqAr/FbMKFc9UFYYqkgEVFf4r/FY0q1ArlgtVFf4r/Ff4rd1QrooArB0ArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rb1ArlgorClQroAYIr/Ff4r/FbcK1QrlitUFf4r/FY+AFclVFYUCFc9QFf4rZgGq0AqjgNVoAr/FbeoFccFFYkqFcwFDlWqFccVFYkKFctUFeOAFcVVFYkCFctQFYugFUMBFYsAFctAFYuoFcMFFY0qFcgHFlWqFcMVFY0KFcdUFY+AFcFVFY0C1WgFT8BqtQFeMA1WoFb8FqtAFY7cgiorIlQriBI8K1WAFb1VqgrJbjzaBFZECbj7aBqALIFcLaHbkLaJFYbcdqorKbjzaKbkDaLbgSwcVwLaJWD6uLFYawaVwIrMbgKwaVwLaKbgawaVwLaLbgawZQQLaLWDiuOWAaEYQQKuMWAiEXKwKuNQjSCQQjSCQQjSCRAAIrB1AqTgorBoAUQQiyCSQgjdSbISCRQgZYSKwKCSQghYQKwSCSQghYQKwSCTAAMqFYOoCJsFFQNVFShYEwARMFQRWVLAiFMQIRWWLAosKFQZWXLAosIFQZWYLAzgFawZWbAAMKFgmq1IoEAANUFTQABFZtAFbgsFFYwqeWQorFVjZZJFYhVfcAwrCazoA/AHI")) +}; + +var p60_img = { + width : 176, height : 176, bpp : 2, + transparent : -1, + palette : pal2, + buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESFQOoFacFQiSCCwArTgCESQSyEUlTZTboyCnQiSCYQiSCYQiSCZQgdAVxwqYQgSwMVwOoFbMFWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbheqFcTcHbT7cDFY0CbT7cDqArxhWqwArfgFVqgrHFUDcBFY0qFcdVFY2oFcMFFY2qFclAFYugFcMBFYsCFctQFYuAFcMAFYsKFctUFYoqigEVFeEqFctVFYmoFccFFYmqFc1AFYegFccBFf4rbgWqwArjgFVqAr/FbMKFc9UFYYqkgEVFf4r/FY0q1ArlgtVFf4r/Ff4rd1QrooArB0ArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rb1ArlgorClQroAYIr/Ff4r/FbcK1QrlitUFf4r/FY+AFclVFYUCFc9QFf4rZgGq0AqjgNVoAr/FbeoFccFFYkqFcwFDlWqFccVFYkKFctUFeOAFcVVFYkCFctQFYugFUMBFYsAFctAFYuoFcMFFY0qFcgHFlWqFcMVFY0KFcdUFY+AFcFVFY0C1WgFT8BqtQFeMA1WoFb8FqtAFY7cgiorIlQriBI8K1WAFb1VqgrJbjzaBFZECbj7aBqALIFcLaHbkLaJFYbcdqorKbjzaKbkDaLbgSwcVwLaJWD6uLFYawaVwIrMbgKwaVwLaKbgawaVwLaLbgawZQQLaLWDiuOWAaEYQQKuMWAelNBqCLVxqEC0oRPQS6EC0oSQQSyECFYKEVQSIABFYI/QAAcFFYJDRCgSCmYYjdSCqqYCLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5A")) +}; + +var p70_img = { + width : 176, height : 176, bpp : 2, + transparent : -1, + palette : pal2, + buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESFQOoFacFQiSCCwArTgCESQSyEUlTZTboyCnQiSCYQiSCYQiSCZQgdAVxwqYQgSwMVwOoFbMFWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbheqFcTcHbT7cDFY0CbT7cDqArxhWqwArfgFVqgrHFUDcBFY0qFcdVFY2oFcMFFY2qFclAFYugFcMBFYsCFctQFYuAFcMAFYsKFctUFYoqigEVFeEqFctVFYmoFccFFYmqFc1AFYegFccBFf4rbgWqwArjgFVqAr/FbMKFc9UFYYqkgEVFf4r/FY0q1ArlgtVFf4r/Ff4rd1QrooArB0ArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rb1ArlgorClQroAYIr/Ff4r/FbcK1QrlitUFf4r/FY+AFclVFYUCFc9QFf4rZAgoAggNVoAr/FbdUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHIA=")) +}; + +var p80_img = { + width : 176, height : 176, bpp : 2, + transparent : -1, + palette : pal2, + buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESFQOoFacFQiSCCwArTgCESQSyEUlTZTboyCnQiSCYQiSCYQiSCZQgdAVxwqYQgSwMVwOoFbMFWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbheqFcTcHbT7cDFY0CbT7cDqArxhWqwArfgFVqgrHFUDcBFY0qFcdVFY2oFcMFFY2qFclAFYugFcMBFYsCFctQFYuAFcMAFYsKFctUFYoqigEVFeEqFctVFYmoFccFFYmqFc1AcIdQFccBFf4rbGAoAhKQYr/Fa8FFc9UFYYqkgEVFf4r/FYwDDAEZTDFf4r/Ff4rbqorooArBqArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlgorCioroAYIr/Ff4r/FbYDDAEZTDFf4r/FYtAFclVFYUBFc9QFf4rZAgoAgKQor/FbFUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHIA=")) +}; + +var p90_img = { + width : 176, height : 176, bpp : 2, + transparent : -1, + palette : pal2, + buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESquq1ArTgqESNgOqwArTIYKERH4KCUQigSBbKTdGCKKCVQiTCCFSyERCALBQQjAPBoArXDZ7ARObKuBSZwcaVzR0QFYKuZWAYNZWCJJKMoKuaWAahKBhiwTJRSudURorBFTgfMVzqjDO5DaeZ5jaeJhhiKbi4rIbT4hLqoriPI7afUpS5BbTwiKFdZgIADSmHFYIqgbgIrGcgIriEYwzHADZ7HRY4rdaYrjHADcBFYoGBFcgkEGQwAeFYqKHFbzUEcQ4AdiorwiorlEogxFAD59FWoorhoArDqArjgIr/FbYwFAEJSDFf4rXgornqgrDFUkAior/Ff4rGAYYAjKYYr/Ff4r/FbdVFdFAFYNQFcsBFf4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/FbdUFcsFFYUVFdADBFf4r/Ff4rbAYYAjKYYr/Ff4rFoArkqorCgIrnqAr/FbIEFAEBSFFf4rYqgrjgorEiormAocVAogAfEooxFFcB9EFdq1DAD9VFYkBFctQFYoGEADokHFcp8FRQoAdag7iFFb4HFioHGADYjHGY4rcPYyLHADbTHcYNQFT4iIFdZgIADKmJqrcgiorIBIIrhMKIAXUpIrBbjzaBFZAKKbS5MJFcKkJbj4fLBYLcdqorKbjzPMbjxKNMhauTURawdJJorBWDShBFZiRBWDQcOHRyuPOhorBWDIbPWDRzQSYKEYIwLLOHgSEXDIJyPQjD2SQjCCQQjSCRCYY/QN4xDRQiyCSQgjdSCqqECLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5A")) +}; + +var p100_img = { + width : 176, height : 176, bpp : 2, + transparent : -1, + palette : pal2, + buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVAAVUFUgpDAAdAFMEBFQ4ABqBVnLMQqLFjzWEABLgbVgohEGoqyaiofDBihWVHJpYYDgYPbKxz5NLDJGCfBzgDKzA+SLChECC6A/CNRycIS6jCNIQ5uSCqqCCeCqESTKxCCQiBsCTCRDEQiCCWQigSBYaRwGQU6ESQTCESQTCESQTIbQYCJzZVwKTODjSuaOiArBVzKwDBrKwRJJRlBVzSwDUJQMMWCZKKVzqiNFYIqcD5iudUYZ3IbTzPMbTxMMMRTcXFZDafEJdVFcR5HbT6lKXILaeERQrrMBAAaUw4rBFUDcBFYzkBFcQjGGY4AbPY6LHFbrTFcY4AbgIrFAwIrkEggyGADwrFRQ4reagjiHADsVFeEVFcolEGIoAfPoq1FFcNAFYdQFccBFf4rbGAoAhKQYr/Fa8FFc9UFYYqkgEVFf4r/FYwDDAEZTDFf4r/Ff4rbqorooArBqArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlgorCioroAYIr/Ff4r/FbYDDAEZTDFf4r/FYtAFclVFYUBFc9QFf4rZAgoAgKQor/FbFUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHIA=")) +}; + +function setLargeFont() { + g.setFont('Vector', 48); +} + +function setSmallFont() { + //g.setFont('6x8',1); + g.setFont('Vector', 16); +} + +function getSteps() { + //return fakeSteps; + try { + return Bangle.getHealthStatus("day").steps; + } catch (e) { + if (WIDGETS.wpedom !== undefined) + return WIDGETS.wpedom.getSteps(); + else + return 0; + } +} + +function getGaugeImage(p) { + console.log("p="+p); + if (p < 2) return p0_img; + if (p >= 2 && p < 4) return p2_img; + if (p >= 4 && p < 7) return p4_img; + if (p >= 7 && p < 10) return p7_img; + if (p >= 10 && p < 20) return p10_img; + if (p >= 20 && p < 30) return p20_img; + if (p >= 30 && p < 40) return p30_img; + if (p >= 40 && p < 50) return p40_img; + if (p >= 50 && p < 60) return p50_img; + if (p >= 60 && p < 70) return p60_img; + if (p >= 70 && p < 80) return p70_img; + if (p >= 80 && p < 90) return p80_img; + if (p >= 90 && p < 100) return p90_img; + if (p >= 100) return p100_img; +} + +function draw() { + var date = new Date(); + var timeStr = require("locale").time(date,1); + var da = date.toString().split(" "); + var time = da[4].substr(0,5); + var hh = da[4].substr(0,2); + var mm = da[4].substr(3,2); + var steps = getSteps(); + var p_steps = Math.round(100*(steps/10000)); + console.log("steps="+ steps + " p_steps=" + p_steps); + + g.reset(); + g.setColor(g.theme.bg); + g.fillRect(0, 0, w, h); + g.drawImage(getGaugeImage(p_steps), 0, 0); + setLargeFont(); + + g.setColor('#0f0'); + g.setFontAlign(1,0); // right aligned + g.drawString(hh, (w/2) - 1, h/2); + + g.setColor(g.theme.fg); + g.setFontAlign(-1,0); // left aligned + g.drawString(mm, (w/2) + 1, h/2); + + drawSteps(); + g.drawString('Battery ' + E.getBattery() + '%', w/2, h/4); +} + +function drawSteps() { + if (drawingSteps) return; + drawingSteps = true; + setSmallFont(); + g.setFontAlign(0,0); + var steps = getSteps(); + g.setColor(g.theme.bg); + g.fillRect((w/2) - infoWidth, (3*h/4) - infoHeight, (w/2) + infoWidth, (3*h/4) + infoHeight); + g.setColor(g.theme.fg); + g.drawString('Steps ' + steps, w/2, (3*h/4) - 4); + drawingSteps = false; +} + +Bangle.on('step', s => { + drawSteps(); +}); + +// for testing the segments +/* +function nextSteps() { + fakeSteps += 100; +} + +function prevSteps() { + fakeSteps -= 100; +} + +Bangle.setUI("clockupdown", btn=> { + if (btn<0) prevSteps(); + if (btn>0) nextSteps(); + draw(); +}); +*/ + +g.clear(); +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="";} + +draw(); +setInterval(draw, 60000); diff --git a/apps/thering/app.png b/apps/thering/app.png new file mode 100644 index 000000000..0c68d2246 Binary files /dev/null and b/apps/thering/app.png differ diff --git a/apps/thering/calc_precentages.js b/apps/thering/calc_precentages.js new file mode 100644 index 000000000..b94815c2e --- /dev/null +++ b/apps/thering/calc_precentages.js @@ -0,0 +1,58 @@ +/* + +Used to calculate the x,y coordinates (so that we can manuall create +images) of the outer circumference of a rotating ring guage where +cx,cy is the centre of the screen r1 is the radius of the circle. p +is the % completion or % measure for the guage to take we need to +generate images at percentages 0,2,4,7,10,20....100 + + +the output is below + +p=0 x=88 y=4 +p=2 x=99 y=5 +p=4 x=109 y=7 +p=7 x=124 y=12 +p=10 x=137 y=20 +p=20 x=168 y=62 +p=30 x=168 y=114 +p=40 x=137 y=156 +p=50 x=88 y=172 +p=60 x=39 y=156 +p=70 x=8 y=114 +p=80 x=8 y=62 +p=90 x=39 y=20 + +*/ + +const h = g.getHeight(); +const w = g.getWidth(); + +function radians(a) { + return a*Math.PI/180; +} + +function calc() { + var i = 0; + var r1 = (w/2) - 4; + var startrot = 0 - 180; + var endrot = -360 - 180; + var cx = w/2; + var cy = h/2; + var p = 0; + + // calc coords for each percentage point + for (i = startrot; i > endrot; i -= 3.6) { + x = cx + r1 * Math.sin(radians(i)); + y = cy + r1 * Math.cos(radians(i)); + x = Math.round(x); + y = Math.round(y); + + if (p==2 || p==4 || p==7 || p % 10 == 0) + print('p=' + p + ' x=' + x + ' y=' + y); + p++; + } +} + +g.clear(); +calc(); diff --git a/apps/thering/metadata.json b/apps/thering/metadata.json new file mode 100644 index 000000000..32b1dae4b --- /dev/null +++ b/apps/thering/metadata.json @@ -0,0 +1,14 @@ +{ "id": "thering", + "name": "The Ring", + "version":"0.01", + "description": "A proof of concept clock with large ring guage for steps using pre-set images, acts as a tutorial piece for discussion", + "icon": "app.png", + "tags": "clock", + "supports" : ["BANGLEJS2"], + "screenshots": [{"url":"screenshot_thering3.jpg"}], + "readme": "README.md", + "storage": [ + {"name":"thering.app.js","url":"app.js"}, + {"name":"thering.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/thering/screenshot_thering1.png b/apps/thering/screenshot_thering1.png new file mode 100644 index 000000000..bf7e6cebc Binary files /dev/null and b/apps/thering/screenshot_thering1.png differ diff --git a/apps/thering/screenshot_thering2.jpg b/apps/thering/screenshot_thering2.jpg new file mode 100644 index 000000000..521a61017 Binary files /dev/null and b/apps/thering/screenshot_thering2.jpg differ diff --git a/apps/thering/screenshot_thering3.jpg b/apps/thering/screenshot_thering3.jpg new file mode 100644 index 000000000..da8f140c0 Binary files /dev/null and b/apps/thering/screenshot_thering3.jpg differ diff --git a/apps/timeandlife/metadata.json b/apps/timeandlife/metadata.json index 86800f16f..acd6b0086 100644 --- a/apps/timeandlife/metadata.json +++ b/apps/timeandlife/metadata.json @@ -1,18 +1,26 @@ { "id": "timeandlife", "name": "Time and Life", - "shortName":"Time and Lfie", + "shortName": "Time and Life", "icon": "app.png", - "version":"0.01", + "version": "0.01", "description": "A simple watchface which displays the time when the screen is tapped and decay according to the rules of Conway's game of life.", "type": "clock", "tags": "clock", - "supports": ["BANGLEJS2"], - "allow_emulator":true, + "supports": [ + "BANGLEJS2" + ], + "allow_emulator": true, "readme": "README.md", "storage": [ - {"name":"timeandlife.app.js","url":"app.js"}, - {"name":"timeandlife.img","url":"app-icon.js","evaluate":true} + { + "name": "timeandlife.app.js", + "url": "app.js" + }, + { + "name": "timeandlife.img", + "url": "app-icon.js", + "evaluate": true + } ] -} - +} \ No newline at end of file diff --git a/apps/timecal/ChangeLog b/apps/timecal/ChangeLog new file mode 100644 index 000000000..e48145b4b --- /dev/null +++ b/apps/timecal/ChangeLog @@ -0,0 +1,10 @@ +0.01: Initial creation of the clock face time and calendar +0.02: Feature Request #1154 and some findings... + -> get rendered time from optimisations + -> *BATT SAFE* only update once a minute instead of once a second + -> *RAM optimized* clean code, corrected minute update (timout, no intervall) + -> locale: weekday name (first two characters) from locale + -> added settings to render cal view begin day (-1: today, 0:sunday, 1:monday [default]) +0.03: a lot of more settings for outline, colors and highlights +0.04: finalized README, fixed settings cancel, fixed border-setting +0.05: bugfix: default settings \ No newline at end of file diff --git a/apps/timecal/README.md b/apps/timecal/README.md new file mode 100644 index 000000000..d26f9ba4d --- /dev/null +++ b/apps/timecal/README.md @@ -0,0 +1,22 @@ +# Calendar Clock + +## Features +Shows the +* Date +* Time (hh:mm) - respecting 12/24 (uses locale string) +* 3 weeks calendar view (last,current and next week) + +### The settings menu +Calendar View can be customized +* < Save: Exist and save the current settings +* Show date: Choose if and how the date is displayed: none, locale (default), monthfull or monthshort.yearshort #weeknum with 0 prefixed +* Start wday: Set day of week start. Values: 0=Sunday, 1=Monday,...,6=Saturday or -1=Relative to today (default 0: Sunday) +* Su color: Set Sundays color. Values: none (default), red, green or blue +* Border: show or none (default) +* Submenu Today settings - choose how today is highlighted + * < Back: + * Color: none, red (default), green or blue + * Marker: Outline today graphically. Values: none (default), circle, rect(angle) + * Mrk.Color: Circle/rectangle color: red (default), green or blue + * Mrk.Size: Circle/rectangle thickness in pixel: min:1, max: 10, default:3 +* < Cancel: Exit and no change. Nevertheless missing default settings and superflous settings will be removed and saved. diff --git a/apps/timecal/metadata.json b/apps/timecal/metadata.json index 3237dd08a..f439f4e9c 100644 --- a/apps/timecal/metadata.json +++ b/apps/timecal/metadata.json @@ -1,13 +1,16 @@ { "id": "timecal", "name": "TimeCal", "shortName":"TimeCal", + "version":"0.05", + "description": "TimeCal shows the date/time along with a 3 week calendar", "icon": "icon.png", - "version":"0.01", - "description": "TimeCal shows the Time along with a 3 week calendar", - "tags": "clock", "type": "clock", + "tags": "clock,calendar", "supports":["BANGLEJS2"], + "readme": "README.md", + "allow_emulator":true, "storage": [ - {"name":"timecal.app.js","url":"timecal.app.js"} + {"name":"timecal.app.js","url":"timecal.app.js"}, + {"name":"timecal.settings.js","url":"timecal.settings.js"} ] } diff --git a/apps/timecal/testing/timecal.app.test._js b/apps/timecal/testing/timecal.app.test._js new file mode 100644 index 000000000..e41f3d848 --- /dev/null +++ b/apps/timecal/testing/timecal.app.test._js @@ -0,0 +1,798 @@ +//Clock renders date, time and pre,current,next week calender view +class TimeCalClock{ + DATE_FONT_SIZE(){ return 20; } + TIME_FONT_SIZE(){ return 40; } + + /** + * @param{Date} date optional the date (e.g. for testing) + * @param{Settings} settings optional settings to use e.g. for testing + */ + constructor(date, settings){ + if (date) + this.date=date; + + if (settings) + this._settings = settings; + else + this._settings = require("Storage").readJSON("timecal.settings.json", 1) || {}; + + const defaults = { + shwDate:1, //0:none, 1:locale, 2:month, 3:monthshort.year #week + + wdStrt:0, //identical to getDay() 0->Su, 1->Mo, ... //Issue #1154: weekstart So/Mo, -1 for use today + + tdyNumClr:3, //0:fg, 1:red=#E00, 2:green=#0E0, 3:blue=#00E + tdyMrkr:0, //0:none, 1:circle, 2:rectangle, 3:filled + tdyMrkClr:2, //1:red=#E00, 2:green=#0E0, 3:blue=#00E + tdyMrkPxl:3, //px + + suClr:1, //0:fg, 1:red=#E00, 2:green=#0E0, 3:blue=#00E + //phColor:"#E00", //public holiday + + calBrdr:false + }; + for (const k in this._settings) if (!defaults.hasOwnProperty(k)) delete this._settings[k]; //remove invalid settings + for (const k in defaults) if(!this._settings.hasOwnProperty(k)) this._settings[k] = defaults[k]; //assign missing defaults + + g.clear(); + Bangle.setUI("clock"); + Bangle.loadWidgets(); + Bangle.drawWidgets(); + + this.centerX = Bangle.appRect.w/2; + this.nrgb = [g.theme.fg, "#E00", "#0E0", "#00E"]; //fg, r ,g , b + + this.ABR_DAY=[]; + if (require("locale") && require("locale").dow) + for (let d=0; d<=6; d++) { + var refDay=new Date(); + refDay.setFullYear(1972); + refDay.setMonth(0); + refDay.setDate(2+d); + this.ABR_DAY.push(require("locale").dow(refDay)); + + } + else + this.ABR_DAY=["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + } + + /** + * @returns {Object} current settings object + */ + settings(){ + return this._settings; + } + + + /* + * Run forest run + **/ + draw(){ + this.drawTime(); + + if (this.TZOffset===undefined || this.TZOffset!==d.getTimezoneOffset()) + this.drawDateAndCal(); + } + + /** + * draw given or current time from date + * overwatch timezone changes + * schedules itself to update + */ + drawTime(){ + d=this.date ? this.date : new Date(); + const Y=Bangle.appRect.y+this.DATE_FONT_SIZE()+10; + + d=d?d :new Date(); + + g.setFontAlign(0, -1).setFont("Vector", this.TIME_FONT_SIZE()).setColor(g.theme.fg) + .clearRect(Bangle.appRect.x, Y, Bangle.appRect.x2, Y+this.TIME_FONT_SIZE()-7) + .drawString(("0" + require("locale").time(d, 1)).slice(-5), this.centerX, Y, true); + //.drawRect(Bangle.appRect.x, Y, Bangle.appRect.x2, Y+this.TIME_FONT_SIZE()-7); //DEV-Option + + setTimeout(this.draw.bind(this), 60000-(d.getSeconds()*1000)-d.getMilliseconds()); + } + + /** + * draws given date and cal + * @param{Date} d provide date or uses today + */ + drawDateAndCal(){ + d=this.date ? this.date : new Date(); + + this.TZOffset=d.getTimezoneOffset(); + this.drawDate(); + this.drawCal(); + + if (this.tOutD) //abort exisiting + clearTimeout(this.tOutD); + this.tOutD=setTimeout(this.drawDateAndCal.bind(this), 86400000-(d.getHours()*24*60*1000)-(d.getMinutes()*60*1000)-d.getSeconds()-d.getMilliseconds()); + } + + /** + * draws given date as defiend in settings + */ + drawDate(){ + d=this.date ? this.date : new Date(); + + const FONT_SIZE=20; + const Y=Bangle.appRect.y; + var render=false; + var dateStr = ""; + if (this.settings().shwDate>0) { //skip if exactly -none + const dateSttngs = ["","l","M","m.Y #W"]; + for (let c of dateSttngs[this.settings().shwDate]) { //add part as configured + switch (c){ + case "l":{ //locale + render=true; + dateStr+=require("locale").date(d,1); + break; + } + case "m":{ //month e.g. Jan. + render=true; + dateStr+=require("locale").month(d,1); + break; + } + case "M":{ //month e.g. January + render=true; + dateStr+=require("locale").month(d,0); + break; + } + case "y":{ //year e.g. 22 + render=true; + dateStr+=d.getFullYear().slice(-2); + break; + } + case "Y":{ //year e.g. 2022 + render=true; + dateStr+=d.getFullYear(); + break; + } + case "w":{ //week e.g. #2 + dateStr+=(this.ISO8601calWeek(d)); + break; + } + case "W":{ //week e.g. #02 + dateStr+=("0"+this.ISO8601calWeek(d)).slice(-2); + break; + } + default: //append c + dateStr+=c; + render=dateStr.length>0; + break; //noop + } + } + } + if (render){ + g.setFont("Vector", FONT_SIZE).setColor(g.theme.fg).setFontAlign(0, -1).clearRect(Bangle.appRect.x, Y, Bangle.appRect.x2, Y+FONT_SIZE-3).drawString(dateStr,this.centerX,Y); + } + //g.drawRect(Bangle.appRect.x, Y, Bangle.appRect.x2, Y+FONT_SIZE-3); //DEV-Option + } + + /** + * draws calender week view (-1,0,1) for given date + */ + drawCal(){ + d=this.date ? this.date : new Date(); + + const DAY_NAME_FONT_SIZE=10; + const CAL_Y=Bangle.appRect.y+this.DATE_FONT_SIZE()+10+this.TIME_FONT_SIZE()+3; + const CAL_AREA_H=Bangle.appRect.h-CAL_Y+24; //+24: top widegtes only + const CELL_W=Bangle.appRect.w/7; //cell width + const CELL_H=(CAL_AREA_H-DAY_NAME_FONT_SIZE)/3; //cell heigth + const DAY_NUM_FONT_SIZE=Math.min(CELL_H-1,15); //size down, max 15 + + g.setFont("Vector", DAY_NAME_FONT_SIZE).setColor(g.theme.fg).setFontAlign(-1, -1).clearRect(Bangle.appRect.x, CAL_Y, Bangle.appRect.x2, CAL_Y+CAL_AREA_H); + + //draw grid & Headline + const dNames = this.ABR_DAY.map((a) => a.length<=2 ? a : a.substr(0, 2)); //force shrt 2 + for(var dNo=0; dNo=0 ? (dNo+this.settings().wdStrt)%7 : (dNo+d.getDay()+4)%7; + const dName=dNames[dIdx]; + if(dNo>0) + g.drawLine(dNo*CELL_W, CAL_Y, dNo*CELL_W, CAL_Y+CAL_AREA_H-1); + + if (dIdx==0) g.setColor(this.nrgb[this.settings().suClr]); //sunday maybe colorize txt + g.drawString(dName, dNo*CELL_W+(CELL_W-g.stringWidth(dName))/2+2, CAL_Y+1).setColor(g.theme.fg); + } + + var nextY=CAL_Y+DAY_NAME_FONT_SIZE; + + for(i=0; i<3; i++){ + const y=nextY+i*CELL_H; + g.drawLine(Bangle.appRect.x, y, Bangle.appRect.x2, y); + } + + g.setFont("Vector", DAY_NUM_FONT_SIZE); + + //write days + const tdyDate=d.getDate(); + const days=this.settings().wdStrt>=0 ? 7+((7+d.getDay()-this.settings().wdStrt)%7) : 10;//start day (week before=7 days + days in this week realtive to week start) or fixed 7+3 days + var rD=new Date(d.getTime()); + rD.setDate(rD.getDate()-days); + var rDate=rD.getDate(); + for(var y=0; y<3; y++){ + for(var x=0; x", + cases: [ + { + value: "required,", + beforeTxt: "optional,", + beforeExpression: "optional,", + afterText: "optional,", + afterExpression: "optional," + } + ], + constructorParams: ["optional: ","|TEST_SETTINGS|","..."], //TEST_SETTINGS will be replcaed with each current {setting: case} + functionNames: ["required, ", "..."], + functionParams: ["optional: ","|TEST_SETTINGS|","..."] + }; + } + + constructor(data){ + + this._validate(data); + + this.setting = data.setting; + this.cases = data.cases.map((entry) => { + return { + value: entry.value, + beforeTxt: entry.beforeTxt||"", + beforeExpression: entry.beforeExpression||true, + afterTxt: entry.afterTxt||"", + afterExpression: entry.afterExpression||true + }; + }); + this.constructorParams = data.constructorParams; + this.functionNames = data.functionNames; + this.functionParams = data.functionParams; + } + + /** + * validates the given data config + */ + _validate(data){ + //validate given config + if (!data.setting) throw new EmptyMandatoryError("setting", data, this.TEST_SETTING_SAMPLE()); + if (!(data.cases instanceof Array) || data.cases.length==0) throw new EmptyMandatoryError("cases", data, this.TEST_SETTING_SAMPLE()); + if (!(data.functionNames instanceof Array) || data.functionNames==0) throw new EmptyMandatoryError("functionNames", data, this.TEST_SETTING_SAMPLE()); + + data.cases.forEach((entry,idx) => { + if (entry.value === undefined) throw new EmptyMandatoryError("cases["+idx+"].value", entry, this.TEST_SETTING_SAMPLE()); + }); + } +} + +/*************************************************************************/ + +/** + * Testing a Bangle object + */ +class BangleTestRunner{ + /** + * create for ObjClass + * @param {Class} objClass + * @param {LogSeverity} minSeverity to Log + */ + constructor(objClass, minSeverity){ + this.TESTCASE_MSG_BEFORE_TIMEOUT = 1000; //5s + this.TESTCASE_RUN_TIMEOUT = 1000; //5s + this.TESTCASE_MSG_AFTER_TIMEOUT = 1000; //5s + + this.oClass = objClass; + this.minSvrty = minSeverity; + this.tests = []; + + this.currentCaseNum = this.currentTestNum = this.currentTest = this.currentCase = undefined; + } + + /** + * add a Setting Test, return instance for chaining + * @param {TestSetting} + */ + addTestSettings(sttngs) { + this.tests.push(new TestSetting(sttngs)); + return this; + } + + /** + * Test execution of all tests + */ + execute() { + this._init(); + while (this._nextTest()) { + this._beforeTest(); + while (this._nextCase()) { + this._beforeCase(); + this._runCase(); + this._afterCase(); + } + this._afterTest(); + this._firstCase(); + } + this._exit(); + } + + /** + * global prepare - before all test + */ + _init() { + console.log(this._nowTime(), ">>init"); + this.currentTestNum=-1; + this.currentCaseNum=-1; + } + + /** + * before each test + */ + _beforeTest() { + console.log(this._nowTime(), ">>test #" + this.currentTestNum); + } + + /** + * befor each testcase + */ + _beforeCase() { + console.log(this.currentTest); + console.log(this._nowTime(), ">>case #" + this.currentTestNum + "." + this.currentCaseNum + "/" + (this.currentTest.cases.length-1)); + if (this.currentTest instanceof TestSetting) + console.log(this.currentTest.setting+"="+this.currentCase.value+"/n"+(this.currentCase.beforeTxt ? "#"+this.currentCase.beforeTxt : "")); + } + + /** + * testcase runner + */ + _runCase() { + console.log(this._nowTime(), ">>running..."); + var returns = []; + this.currentTest.functionNames.forEach((fName) => { + var settings={}; settings[this.currentTest.setting] = this.currentCase.value; + var cParams = this.currentTest.constructorParams||[]; + cParams = cParams.map((v) => (v && v instanceof String && v==="|TEST_SETTINGS|") ? settings : v);//replace settings in call params + var fParams = this.currentTest.functionParams||[]; + fParams = fParams.map((v) => (v && v instanceof String && v==="|TEST_SETTINGS|") ? settings : v);//replace settings in call params + + var creatorFunc = new Function("console.log('Constructor params:', arguments); return new " + this.oClass + "(arguments[0],arguments[1],arguments[2],arguments[3],arguments[4],arguments[5],arguments[6],arguments[7],arguments[8],arguments[9])"); //prepare spwan arguments[0],arguments[1] + let instance = creatorFunc.call(this.oClass, cParams[0], cParams[1], cParams[2], cParams[3], cParams[4], cParams[5], cParams[6], cParams[7], cParams[8], cParams[9]); //spwan + + console.log(">>"+this.oClass+"["+fName+"]()"); + + console.log('Instance:', instance); + console.log('Function params:', fParams); + returns.push(instance[fName](fParams[0], fParams[1], fParams[2], fParams[3], fParams[4], fParams[5], fParams[6], fParams[7], fParams[8], fParams[9])); //run method and store result + g.dump(); + console.log("<<"+this.oClass+"["+fName+"]()"); + }); + console.log(this._nowTime(), "<<...running"); + } + + /** + * after each testcase + */ + _afterCase() { + if (this.currentTest instanceof TestSetting) + if (this.currentCase.afterTxt.length>0) + console.log("++EXPECTED:" + this.currentCase.afterTxt + "EXPECTED++"); + console.log(this._nowTime(), "< setTimeout(resolve, sec)); + } + + _waits(sec) { + this._delay(1).then(); + } + + _log() { + + } + + _nextTest() { + if (this.currentTestNum>=-1 && (this.currentTestNum+1)=-1 && (this.currentCaseNum+1)Su, 1->Mo, ... //Issue #1154: weekstart So/Mo, -1 for use today + + tdyNumClr:3, //0:fg, 1:red=#E00, 2:green=#0E0, 3:blue=#00E + tdyMrkr:0, //0:none, 1:circle, 2:rectangle, 3:filled + tdyMrkClr:2, //1:red=#E00, 2:green=#0E0, 3:blue=#00E + tdyMrkPxl:3, //px + + suClr:1, //0:fg, 1:red=#E00, 2:green=#0E0, 3:blue=#00E + //phColor:"#E00", //public holiday + + calBrdr:false + }; + for (const k in this._settings) if (!defaults.hasOwnProperty(k)) delete this._settings[k]; //remove invalid settings + for (const k in defaults) if(!this._settings.hasOwnProperty(k)) this._settings[k] = defaults[k]; //assign missing defaults + + g.clear(); + Bangle.setUI("clock"); + Bangle.loadWidgets(); + Bangle.drawWidgets(); + + this.centerX = Bangle.appRect.w/2; + this.nrgb = [g.theme.fg, "#E00", "#0E0", "#00E"]; //fg, r ,g , b + + this.ABR_DAY=[]; + if (require("locale") && require("locale").dow) + for (let d=0; d<=6; d++) { + var refDay=new Date(); + refDay.setFullYear(1972); + refDay.setMonth(0); + refDay.setDate(2+d); + this.ABR_DAY.push(require("locale").dow(refDay)); -function drawCal(d){ - var calStart = 101; - var cellSize = g.getWidth() / 7; - var halfSize = cellSize / 2; - g.clearRect(0,calStart,g.getWidth(),g.getHeight()); - g.drawLine(0,calStart,g.getWidth(),calStart); - var days = ["Mo","Tu","We","Th","Fr","Sa","Su"]; - g.setFont("Vector",10); - g.setColor(fontColor); - g.setFontAlign(-1,-1,0); - for(var i = 0; i < days.length;i++){ - g.drawString(days[i],i*cellSize+5,calStart -11); - if(i!=0){ - g.drawLine(i*cellSize,calStart,i*cellSize,g.getHeight()); - } - } - var cellHeight = (g.getHeight() -calStart ) / 3; - for(var i = 0;i < 3;i++){ - var starty = calStart + i * cellHeight; - g.drawLine(0,starty,g.getWidth(),starty); - } - - g.setFont("Vector",15); - - var dayOfWeek = d.getDay(); - var dayRem = d.getDay() - 1; - if(dayRem <0){ - dayRem = 0; - } - - var start = new Date(); - start.setDate(start.getDate()-(7+dayRem)); - g.setFontAlign(0,-1,0); - for (var y = 0;y < 3; y++){ - for(var x = 0;x < 7; x++){ - if(start.getDate() === d.getDate()){ - g.setColor(accentColor); - }else{ - g.setColor(fontColor); } - g.drawString(start.getDate(),x*cellSize +(cellSize / 2) + 2,calStart+(cellHeight*y) + 5); - start.setDate(start.getDate()+1); + else + this.ABR_DAY=["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + } + + /** + * @returns {Object} current settings object + */ + settings(){ + return this._settings; + } + + + /* + * Run forest run + **/ + draw(){ + this.drawTime(); + + if (this.TZOffset===undefined || this.TZOffset!==d.getTimezoneOffset()) + this.drawDateAndCal(); } + + /** + * draw given or current time from date + * overwatch timezone changes + * schedules itself to update + */ + drawTime(){ + d=this.date ? this.date : new Date(); + const Y=Bangle.appRect.y+this.DATE_FONT_SIZE()+10; + + d=d?d :new Date(); + + g.setFontAlign(0, -1).setFont("Vector", this.TIME_FONT_SIZE()).setColor(g.theme.fg) + .clearRect(Bangle.appRect.x, Y, Bangle.appRect.x2, Y+this.TIME_FONT_SIZE()-7) + .drawString(("0" + require("locale").time(d, 1)).slice(-5), this.centerX, Y, true); + //.drawRect(Bangle.appRect.x, Y, Bangle.appRect.x2, Y+this.TIME_FONT_SIZE()-7); //DEV-Option + + setTimeout(this.draw.bind(this), 60000-(d.getSeconds()*1000)-d.getMilliseconds()); + } + + /** + * draws given date and cal + * @param{Date} d provide date or uses today + */ + drawDateAndCal(){ + d=this.date ? this.date : new Date(); + + this.TZOffset=d.getTimezoneOffset(); + this.drawDate(); + this.drawCal(); + + if (this.tOutD) //abort exisiting + clearTimeout(this.tOutD); + this.tOutD=setTimeout(this.drawDateAndCal.bind(this), 86400000-(d.getHours()*24*60*1000)-(d.getMinutes()*60*1000)-d.getSeconds()-d.getMilliseconds()); + } + + /** + * draws given date as defiend in settings + */ + drawDate(){ + d=this.date ? this.date : new Date(); + + const FONT_SIZE=20; + const Y=Bangle.appRect.y; + var render=false; + var dateStr = ""; + if (this.settings().shwDate>0) { //skip if exactly -none + const dateSttngs = ["","l","M","m.Y #W"]; + for (let c of dateSttngs[this.settings().shwDate]) { //add part as configured + switch (c){ + case "l":{ //locale + render=true; + dateStr+=require("locale").date(d,1); + break; + } + case "m":{ //month e.g. Jan. + render=true; + dateStr+=require("locale").month(d,1); + break; + } + case "M":{ //month e.g. January + render=true; + dateStr+=require("locale").month(d,0); + break; + } + case "y":{ //year e.g. 22 + render=true; + dateStr+=d.getFullYear().slice(-2); + break; + } + case "Y":{ //year e.g. 2022 + render=true; + dateStr+=d.getFullYear(); + break; + } + case "w":{ //week e.g. #2 + dateStr+=(this.ISO8601calWeek(d)); + break; + } + case "W":{ //week e.g. #02 + dateStr+=("0"+this.ISO8601calWeek(d)).slice(-2); + break; + } + default: //append c + dateStr+=c; + render=dateStr.length>0; + break; //noop + } + } + } + if (render){ + g.setFont("Vector", FONT_SIZE).setColor(g.theme.fg).setFontAlign(0, -1).clearRect(Bangle.appRect.x, Y, Bangle.appRect.x2, Y+FONT_SIZE-3).drawString(dateStr,this.centerX,Y); + } + //g.drawRect(Bangle.appRect.x, Y, Bangle.appRect.x2, Y+FONT_SIZE-3); //DEV-Option + } + + /** + * draws calender week view (-1,0,1) for given date + */ + drawCal(){ + d=this.date ? this.date : new Date(); + + const DAY_NAME_FONT_SIZE=10; + const CAL_Y=Bangle.appRect.y+this.DATE_FONT_SIZE()+10+this.TIME_FONT_SIZE()+3; + const CAL_AREA_H=Bangle.appRect.h-CAL_Y+24; //+24: top widegtes only + const CELL_W=Bangle.appRect.w/7; //cell width + const CELL_H=(CAL_AREA_H-DAY_NAME_FONT_SIZE)/3; //cell heigth + const DAY_NUM_FONT_SIZE=Math.min(CELL_H-1,15); //size down, max 15 + + g.setFont("Vector", DAY_NAME_FONT_SIZE).setColor(g.theme.fg).setFontAlign(-1, -1).clearRect(Bangle.appRect.x, CAL_Y, Bangle.appRect.x2, CAL_Y+CAL_AREA_H); + + //draw grid & Headline + const dNames = this.ABR_DAY.map((a) => a.length<=2 ? a : a.substr(0, 2)); //force shrt 2 + for(var dNo=0; dNo=0 ? (dNo+this.settings().wdStrt)%7 : (dNo+d.getDay()+4)%7; + const dName=dNames[dIdx]; + if(dNo>0) + g.drawLine(dNo*CELL_W, CAL_Y, dNo*CELL_W, CAL_Y+CAL_AREA_H-1); + + if (dIdx==0) g.setColor(this.nrgb[this.settings().suClr]); //sunday maybe colorize txt + g.drawString(dName, dNo*CELL_W+(CELL_W-g.stringWidth(dName))/2+2, CAL_Y+1).setColor(g.theme.fg); + } + + var nextY=CAL_Y+DAY_NAME_FONT_SIZE; + + for(i=0; i<3; i++){ + const y=nextY+i*CELL_H; + g.drawLine(Bangle.appRect.x, y, Bangle.appRect.x2, y); + } + + g.setFont("Vector", DAY_NUM_FONT_SIZE); + + //write days + const tdyDate=d.getDate(); + const days=this.settings().wdStrt>=0 ? 7+((7+d.getDay()-this.settings().wdStrt)%7) : 10;//start day (week before=7 days + days in this week realtive to week start) or fixed 7+3 days + var rD=new Date(d.getTime()); + rD.setDate(rD.getDate()-days); + var rDate=rD.getDate(); + for(var y=0; y<3; y++){ + for(var x=0; xSu, 1->Mo, ... //Issue #1154: weekstart So/Mo, -1 for use today + + tdyNumClr:3, //0:fg, 1:red=#E00, 2:green=#0E0, 3:blue=#00E + tdyMrkr:0, //0:none, 1:circle, 2:rectangle, 3:filled + tdyMrkClr:2, //1:red=#E00, 2:green=#0E0, 3:blue=#00E + tdyMrkPxl:3, //px + + suClr:1, //0:fg, 1:red=#E00, 2:green=#0E0, 3:blue=#00E + //phColor:"#E00", //public holiday + + calBrdr:false + }; + validSttngs = require("Storage").readJSON(FILE, 1) || {}; + for (const k in validSttngs) if (!DEFAULTS.hasOwnProperty(k)) delete this.validSttngs[k]; //remove invalid settings + for (const k in DEFAULTS) if(!validSttngs.hasOwnProperty(k)) validSttngs[k] = DEFAULTS[k]; //assign missing defaults fixed + + var chngdSttngs = Object.assign({}, validSttngs); + + var saveExitSettings = () => { + require('Storage').writeJSON(FILE, chngdSttngs); + exit(); + }; + + var cancelExitSettings = () => { + require('Storage').writeJSON(FILE, validSttngs); + exit(); + }; + + var showMainMenu = () => { + E.showMenu({ + "": { + "title": "TimeCal "+ /*LANG*/"settings" + }, + /*LANG*/"< Save": () => saveExitSettings(), + /*LANG*/"Show date": { + value: chngdSttngs.shwDate, + min: 0, max: 3, + format: v => [/*LANG*/"none", /*LANG*/"locale", /*LANG*/"M", /*LANG*/"m.Y #W"][v], + onchange: v => chngdSttngs.shwDate = v + }, + /*LANG*/"Start wday": { + value: chngdSttngs.wdStrt, + min: -1, max: 6, + format: v => v>=0 ? ABR_DAY[v] : /*LANG*/"today", + onchange: v => chngdSttngs.wdStrt = v + }, + /*LANG*/"Su color": { + value: chngdSttngs.suClr, + min: 0, max: 3, + format: v => [/*LANG*/"none", /*LANG*/"red", /*LANG*/"green", /*LANG*/"blue"][v], + onchange: v => chngdSttngs.suClr = v + }, + /*LANG*/"Border": { + value: chngdSttngs.calBrdr, + format: v => v ? /*LANG*/"show" : /*LANG*/"none", + onchange: v => chngdSttngs.calBrdr = v + }, + /*LANG*/"Today settings": () => { + showTodayMenu(); + }, + /*LANG*/"< Cancel": () => cancelExitSettings() + }); + }; + + var showTodayMenu = () => { + E.showMenu({ + "": { + "title": /*LANG*/"Today settings" + }, + "< Back": () => showMainMenu(), + /*LANG*/"Color": { + value: chngdSttngs.tdyNumClr, + min: 0, max: 3, + format: v => [/*LANG*/"none", /*LANG*/"red", /*LANG*/"green", /*LANG*/"blue"][v], + onchange: v => chngdSttngs.tdyNumClr = v + }, + /*LANG*/"Marker": { + value: chngdSttngs.tdyMrkr, + min: 0, max: 3, + format: v => [/*LANG*/"none", /*LANG*/"circle", /*LANG*/"rectangle", /*LANG*/"filled"][v], + onchange: v => chngdSttngs.tdyMrkr = v + }, + /*LANG*/"Mrk.Color": { + value: chngdSttngs.tdyMrkClr, + min: 0, max: 2, + format: v => [/*LANG*/"red", /*LANG*/"green", /*LANG*/"blue"][v], + onchange: v => chngdSttngs.tdyMrkClr = v + }, + /*LANG*/"Mrk.Size": { + value: chngdSttngs.tdyMrkPxl, + min: 1, max: 10, + format: v => v+"px", + onchange: v => chngdSttngs.tdyMrkPxl = v + }, + /*LANG*/"< Cancel": () => cancelExitSettings() + }); + }; + + showMainMenu(); +}); diff --git a/apps/todolist/ChangeLog b/apps/todolist/ChangeLog new file mode 100644 index 000000000..2e979ec12 --- /dev/null +++ b/apps/todolist/ChangeLog @@ -0,0 +1 @@ +0.01: Initial release \ No newline at end of file diff --git a/apps/todolist/README.md b/apps/todolist/README.md new file mode 100644 index 000000000..27c7cfb63 --- /dev/null +++ b/apps/todolist/README.md @@ -0,0 +1,40 @@ +Todo List +======== + +This is a simple Todo List application. + +![](screenshot2.png) + +The content is loaded from a JSON file. +You can mark a task as completed. + +JSON file content example: +```javascript +[ + { + name: "Pro", + children: [ + { + name: "Read doc", + done: true, + children: [], + } + ], + }, + { + name: "Pers", + children: [ + { + name: "Grocery", + children: [ + { name: "Milk", done: false, children: [] }, + { name: "Eggs", done: false, children: [] }, + { name: "Cheese", done: false, children: [] }, + ], + }, + { name: "Workout", done: false, children: [] }, + { name: "Learn Rust", done: false, children: [] }, + ], + }, +] +``` \ No newline at end of file diff --git a/apps/todolist/app-icon.js b/apps/todolist/app-icon.js new file mode 100644 index 000000000..229852134 --- /dev/null +++ b/apps/todolist/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwgmjiMRiAWTgIXUCoYZQB4IADC4YHECxkSkIECkQYLEwMSkQQBkcyCAMTmYKEiIuGif/AAIXBmciiUzC4MvBQPyC44LCC4YADBYpIFiM/BYZDBC5EhC4wKCBYKLFEYkxC5UxCwsSBYgXK/5GEmYuDC5oAKC/4XUmK5DC6PziMfC6cimTRB+bbDiSpCC5ItBaIXxbIg2CF5QqBB4IcCAAQvMCYMhdIi//X7P/X6sz+S/CkQADX8gXCif/GQIADMwS/LZ4a//BgkyJBK/ll/zmYADX54FBX9cyB4ZHEO5wPDa/7RJAAshC4xyCABacBC40SGBsxiIWEgEBW4gAKFwowCABwWGACgA==")) \ No newline at end of file diff --git a/apps/todolist/app.js b/apps/todolist/app.js new file mode 100644 index 000000000..58cd3783c --- /dev/null +++ b/apps/todolist/app.js @@ -0,0 +1,129 @@ +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +// Const +let TODOLIST_FILE = "todolist.json"; +let MAX_DESCRIPTION_LEN = 14; + +// Clear todolist file +// require("Storage").erase(TODOLIST_FILE); + +let DEFAULT_TODOLIST = [ + { + name: "Pro", + children: [ + { + name: "Read doc", + done: true, + children: [], + }, + ], + }, + { + name: "Pers", + children: [ + { + name: "Grocery", + children: [ + { name: "Milk", done: false, children: [] }, + { name: "Eggs", done: false, children: [] }, + { name: "Cheese", done: false, children: [] }, + ], + }, + { name: "Workout", done: false, children: [] }, + { name: "Learn Rust", done: false, children: [] }, + ], + }, +]; + +// Load todolist +let todolist = + require("Storage").readJSON(TODOLIST_FILE, true) || DEFAULT_TODOLIST; +let menus = {}; + +function writeData() { + require("Storage").writeJSON(TODOLIST_FILE, todolist); +} + +function getChild(todolist, indexes) { + let childData = todolist; + for (let i = 0; i < indexes.length; i++) { + childData = childData[indexes[i]]; + childData = childData.children; + } + + return childData; +} + +function getName(item) { + let title = item.name.substr(0, MAX_DESCRIPTION_LEN); + return title; +} +function getParentTitle(todolist, indexes) { + let parentIndexes = indexes.slice(0, indexes.length - 1); + let lastIndex = indexes[indexes.length - 1]; + let item = getItem(todolist, parentIndexes, lastIndex); + return getName(item); +} + +function getItem(todolist, parentIndexes, index) { + let childData = getChild(todolist, parentIndexes, index); + return childData[index]; +} + +function toggleableStatus(todolist, indexes, index) { + const reminder = getItem(todolist, indexes, index); + return { + value: !!reminder.done, // !! converts undefined to false + format: (val) => (val ? "[X]" : "[-]"), + onchange: (val) => { + reminder.done = val; + writeData(); + }, + }; +} + +function showSubMenu(key) { + const sub_menu = menus[key]; + return E.showMenu(sub_menu); +} + +function createListItem(todolist, indexes, index) { + let reminder = getItem(todolist, indexes, index); + if (reminder.children.length > 0) { + let childIndexes = []; + for (let i = 0; i < indexes.length; i++) { + childIndexes.push(indexes[i]); + } + childIndexes.push(index); + createMenus(todolist, childIndexes); + return () => showSubMenu(childIndexes); + } else { + return toggleableStatus(todolist, indexes, index); + } +} + +function showMainMenu() { + const mainmenu = menus[""]; + return E.showMenu(mainmenu); +} + +function createMenus(todolist, indexes) { + const menuItem = {}; + if (indexes.length == 0) { + menuItem[""] = { title: "todolist" }; + } else { + menuItem[""] = { title: getParentTitle(todolist, indexes) }; + menuItem["< Back"] = () => + showSubMenu(indexes.slice(0, indexes.length - 1)); + } + for (let i = 0; i < getChild(todolist, indexes).length; i++) { + const item = getItem(todolist, indexes, i); + const name = getName(item); + menuItem[name] = createListItem(todolist, indexes, i); + } + menus[indexes] = menuItem; +} + +createMenus(todolist, []); +showMainMenu(); diff --git a/apps/todolist/app.png b/apps/todolist/app.png new file mode 100644 index 000000000..a93fc14ad Binary files /dev/null and b/apps/todolist/app.png differ diff --git a/apps/todolist/metadata.json b/apps/todolist/metadata.json new file mode 100644 index 000000000..0833a86bd --- /dev/null +++ b/apps/todolist/metadata.json @@ -0,0 +1,23 @@ +{ + "id": "todolist", + "name": "TodoList", + "shortName": "TodoList", + "version": "0.01", + "type": "app", + "description": "Simple Todo List", + "icon": "app.png", + "allow_emulator": true, + "tags": "tool,todo", + "supports": ["BANGLEJS", "BANGLEJS2"], + "readme": "README.md", + "storage": [ + { "name": "todolist.app.js", "url": "app.js" }, + { "name": "todolist.img", "url": "app-icon.js", "evaluate": true } + ], + "data": [{ "name": "todolist.json" }], + "screenshots": [ + { "url": "screenshot1.png" }, + { "url": "screenshot2.png" }, + { "url": "screenshot3.png" } + ] +} diff --git a/apps/todolist/screenshot1.png b/apps/todolist/screenshot1.png new file mode 100644 index 000000000..523d60307 Binary files /dev/null and b/apps/todolist/screenshot1.png differ diff --git a/apps/todolist/screenshot2.png b/apps/todolist/screenshot2.png new file mode 100644 index 000000000..0337f9000 Binary files /dev/null and b/apps/todolist/screenshot2.png differ diff --git a/apps/todolist/screenshot3.png b/apps/todolist/screenshot3.png new file mode 100644 index 000000000..e5a4a85ac Binary files /dev/null and b/apps/todolist/screenshot3.png differ diff --git a/apps/vectorclock/ChangeLog b/apps/vectorclock/ChangeLog index abbfcbb99..02831edde 100644 --- a/apps/vectorclock/ChangeLog +++ b/apps/vectorclock/ChangeLog @@ -5,3 +5,4 @@ 0.05: "Chime the time" (buzz or beep) with up/down swipe added 0.06: Redraw widgets when time is updated 0.07: Fix problem with "Bangle.CLOCK": github.com/espruino/BangleApps/issues/1437 +0.08: Redraw widgets only once per minute diff --git a/apps/vectorclock/app.js b/apps/vectorclock/app.js index 8d2961c4a..663a4c84f 100644 --- a/apps/vectorclock/app.js +++ b/apps/vectorclock/app.js @@ -81,7 +81,7 @@ function draw() { executeCommands(); - Bangle.drawWidgets(); + if (process.env.HWVERSION==2) Bangle.drawWidgets(); } var timeout; diff --git a/apps/vectorclock/metadata.json b/apps/vectorclock/metadata.json index 0f558e3ee..541766fa2 100644 --- a/apps/vectorclock/metadata.json +++ b/apps/vectorclock/metadata.json @@ -1,7 +1,7 @@ { "id": "vectorclock", "name": "Vector Clock", - "version": "0.07", + "version": "0.08", "description": "A digital clock that uses the built-in vector font.", "icon": "app.png", "type": "clock", diff --git a/apps/weatherClock/app.js b/apps/weatherClock/app.js index 1a7f53f05..91d0ab36f 100644 --- a/apps/weatherClock/app.js +++ b/apps/weatherClock/app.js @@ -71,7 +71,6 @@ function chooseIconByCode(code) { case 801: return partSunIcon; default: return cloudIcon; } - break; default: return cloudIcon; } } diff --git a/apps/widChargingStatus/ChangeLog b/apps/widChargingStatus/ChangeLog index 1033c0cd3..5a6db5cb7 100644 --- a/apps/widChargingStatus/ChangeLog +++ b/apps/widChargingStatus/ChangeLog @@ -1 +1,2 @@ 0.01: First release. +0.02: No functional changes, just moved codebase to Typescript. diff --git a/apps/widChargingStatus/metadata.json b/apps/widChargingStatus/metadata.json index f68ccf5b4..573c594e7 100644 --- a/apps/widChargingStatus/metadata.json +++ b/apps/widChargingStatus/metadata.json @@ -2,7 +2,7 @@ "name": "Charging Status", "shortName":"ChargingStatus", "icon": "widget.png", - "version":"0.01", + "version":"0.02", "type": "widget", "description": "A simple widget that shows a yellow lightning icon to indicate whenever the watch is charging. This way one can see the charging status at a glance, no matter which battery widget is being used.", "tags": "widget", diff --git a/apps/widChargingStatus/widget.js b/apps/widChargingStatus/widget.js index 90f9199fa..5d9ea3837 100644 --- a/apps/widChargingStatus/widget.js +++ b/apps/widChargingStatus/widget.js @@ -1,31 +1,33 @@ -(() => { - const icon = require("heatshrink").decompress(atob("ikggMAiEAgYIBmEAg4EB+EAh0AgPggEeCAIEBnwQBAgP+gEP//x///j//8f//k///H//4BYOP/4lBv4bDvwEB4EAvAEBwEAuA7DCAI7BgAQBhEAA")); - const iconWidth = 18; - - function draw() { - g.reset(); - if (Bangle.isCharging()) { - g.setColor("#FD0"); - g.drawImage(icon, this.x + 1, this.y + 1, { - scale: 0.6875 - }); - } - } - - WIDGETS.chargingStatus = { - area: 'tr', - width: Bangle.isCharging() ? iconWidth : 0, - draw: draw, - }; - - Bangle.on('charging', (charging) => { - if (charging) { - Bangle.buzz(); - WIDGETS.chargingStatus.width = iconWidth; - } else { - WIDGETS.chargingStatus.width = 0; - } - Bangle.drawWidgets(); // re-layout widgets - g.flip(); - }); -})(); \ No newline at end of file +"use strict"; +(() => { + const icon = require('heatshrink').decompress(atob('ikggMAiEAgYIBmEAg4EB+EAh0AgPggEeCAIEBnwQBAgP+gEP//x///j//8f//k///H//4BYOP/4lBv4bDvwEB4EAvAEBwEAuA7DCAI7BgAQBhEAA')); + const iconWidth = 18; + function draw() { + g.reset(); + if (Bangle.isCharging()) { + g.setColor('#FD0'); + g.drawImage(icon, this.x + 1, this.y + 1, { + scale: 0.6875, + }); + } + } + WIDGETS.chargingStatus = { + area: 'tr', + width: Bangle.isCharging() ? iconWidth : 0, + draw: draw, + }; + Bangle.on('charging', (charging) => { + const widget = WIDGETS.chargingStatus; + if (widget) { + if (charging) { + Bangle.buzz(); + widget.width = iconWidth; + } + else { + widget.width = 0; + } + Bangle.drawWidgets(); // re-layout widgets + g.flip(); + } + }); +})(); diff --git a/apps/widChargingStatus/widget.ts b/apps/widChargingStatus/widget.ts new file mode 100644 index 000000000..14b4df4a4 --- /dev/null +++ b/apps/widChargingStatus/widget.ts @@ -0,0 +1,38 @@ +(() => { + const icon = require('heatshrink').decompress( + atob( + 'ikggMAiEAgYIBmEAg4EB+EAh0AgPggEeCAIEBnwQBAgP+gEP//x///j//8f//k///H//4BYOP/4lBv4bDvwEB4EAvAEBwEAuA7DCAI7BgAQBhEAA' + ) + ); + const iconWidth = 18; + + function draw(this: { x: number; y: number }) { + g.reset(); + if (Bangle.isCharging()) { + g.setColor('#FD0'); + g.drawImage(icon, this.x + 1, this.y + 1, { + scale: 0.6875, + }); + } + } + + WIDGETS.chargingStatus = { + area: 'tr', + width: Bangle.isCharging() ? iconWidth : 0, + draw: draw, + }; + + Bangle.on('charging', (charging) => { + const widget = WIDGETS.chargingStatus; + if (widget) { + if (charging) { + Bangle.buzz(); + widget.width = iconWidth; + } else { + widget.width = 0; + } + Bangle.drawWidgets(); // re-layout widgets + g.flip(); + } + }); +})(); diff --git a/apps/widadjust/README.md b/apps/widadjust/README.md new file mode 100644 index 000000000..4f89af54b --- /dev/null +++ b/apps/widadjust/README.md @@ -0,0 +1,57 @@ +# Adjust Clock + +Adjusts clock continually in the background to counter clock drift. + +## Usage + +First you need to determine the clock drift of your watch in PPM (parts per million). + +For example if you measure that your watch clock is too fast by 5 seconds in 24 hours, +then PPM is `5 / (24*60*60) * 1000000 = 57.9`. + +Then set PPM in settings and this widget will continually adjust the clock by that amount. + +## Settings + +See **Basic logic** below for more details. + +- **PPM x 10** - change PPM in steps of 10 +- **PPM x 1** - change PPM in steps of 1 +- **PPM x 0.1** - change PPM in steps of 0.1 +- **Update Interval** - How often to update widget and clock error. +- **Threshold** - Threshold for adjusting clock. + When clock error exceeds this threshold, clock is adjusted with `setTime`. +- **Save State** - If `On` clock error state is saved to file when widget exits, if needed. + That is recommended and default setting. + If `Off` clock error state is forgotten and reset to 0 whenever widget is restarted, + for example when going to Launcher. This can cause significant inaccuracy especially + with large **Update Interval** or **Threshold**. +- **Debug Log** - If `On` some debug information is logged to file `widadjust.log`. + +## Display + +Widget shows clock error in milliseconds and PPM. + +## Basic logic + +- When widget starts, clock error state is loaded from file `widadjust.state`. +- While widget is running, widget display and clock error is updated + periodically (**Update Interval**) according to **PPM**. +- When clock error exceeds **Threshold** clock is adjusted with `setTime`. +- When widget exists, clock error state is saved to file `widadjust.state` if needed. + +## Services + +Other apps/widgets can use `WIDGETS.adjust.now()` to request current adjusted time. +To support also case where this widget isn't present, the following code can be used: + +``` +function adjustedNow() { + return WIDGETS.adjust ? WIDGETS.adjust.now() : Date.now(); +} +``` + +## Acknowledgment + +Uses [Clock Settings](https://icons8.com/icon/tQvI71EfIWy3/clock-settings) +icon by [Icons8](https://icons8.com). diff --git a/apps/widadjust/icon.png b/apps/widadjust/icon.png new file mode 100644 index 000000000..f97394618 Binary files /dev/null and b/apps/widadjust/icon.png differ diff --git a/apps/widadjust/metadata.json b/apps/widadjust/metadata.json new file mode 100644 index 000000000..a308072f5 --- /dev/null +++ b/apps/widadjust/metadata.json @@ -0,0 +1,19 @@ +{ + "id": "widadjust", + "name": "Adjust Clock", + "icon": "icon.png", + "version": "0.01", + "description": "Adjusts clock continually in the background to counter clock drift", + "type": "widget", + "tags": "widget", + "supports": [ "BANGLEJS", "BANGLEJS2" ], + "readme": "README.md", + "storage": [ + { "name": "widadjust.wid.js", "url": "widget.js" }, + { "name": "widadjust.settings.js", "url": "settings.js" } + ], + "data": [ + { "name": "widadjust.json" }, + { "name": "widadjust.state" } + ] +} diff --git a/apps/widadjust/settings.js b/apps/widadjust/settings.js new file mode 100644 index 000000000..5791d763b --- /dev/null +++ b/apps/widadjust/settings.js @@ -0,0 +1,120 @@ +(function(back) { + const SETTINGS_FILE = 'widadjust.json'; + const STATE_FILE = 'widadjust.state'; + + const DEFAULT_ADJUST_THRESHOLD = 100; + let thresholdV = [ 10, 25, 50, 100, 250, 500, 1000 ]; + + const DEFAULT_UPDATE_INTERVAL = 60000; + let intervalV = [ 10000, 30000, 60000, 180000, 600000, 1800000, 3600000 ]; + let intervalN = [ "10 s", "30 s", "1 m", "3 m", "10 m", "30 m", "1 h" ]; + + let stateFileErased = false; + + let settings = Object.assign({ + advanced: false, + saveState: true, + debugLog: false, + ppm: 0, + adjustThreshold: DEFAULT_ADJUST_THRESHOLD, + updateInterval: DEFAULT_UPDATE_INTERVAL, + }, require('Storage').readJSON(SETTINGS_FILE, true) || {}); + + if (thresholdV.indexOf(settings.adjustThreshold) == -1) { + settings.adjustThreshold = DEFAULT_ADJUST_THRESHOLD; + } + + if (intervalV.indexOf(settings.updateInterval) == -1) { + settings.updateInterval = DEFAULT_UPDATE_INTERVAL; + } + + function onPpmChange(v) { + settings.ppm = v; + mainMenu['PPM x 10' ].value = v; + mainMenu['PPM x 1' ].value = v; + mainMenu['PPM x 0.1'].value = v; + } + + let mainMenu = { + '': { 'title' : 'Adjust Clock' }, + + '< Back': () => { + require('Storage').writeJSON(SETTINGS_FILE, settings); + back(); + }, + + /* + // NOT FULLY WORKING YET + 'Mode': { + value: settings.advanced, + format: v => v ? 'Advanced' : 'Basic', + onchange: () => { + settings.advanced = !settings.advanced; + } + }, + */ + + 'PPM x 10' : { + value: settings.ppm, + format: v => v.toFixed(1), + step: 10, + onchange : onPpmChange, + }, + + 'PPM x 1' : { + value: settings.ppm, + format: v => v.toFixed(1), + step: 1, + onchange : onPpmChange, + }, + + 'PPM x 0.1' : { + value: settings.ppm, + format: v => v.toFixed(1), + step: 0.1, + onchange : onPpmChange, + }, + + 'Update Interval': { + value: intervalV.indexOf(settings.updateInterval), + min: 0, + max: intervalV.length - 1, + format: v => intervalN[v], + onchange: v => { + settings.updateInterval = intervalV[v]; + }, + }, + + 'Threshold': { + value: thresholdV.indexOf(settings.adjustThreshold), + min: 0, + max: thresholdV.length - 1, + format: v => thresholdV[v] + " ms", + onchange: v => { + settings.adjustThreshold = thresholdV[v]; + }, + }, + + 'Save State': { + value: settings.saveState, + format: v => v ? 'On' : 'Off', + onchange: () => { + settings.saveState = !settings.saveState; + if (!settings.saveState && !stateFileErased) { + stateFileErased = true; + require("Storage").erase(STATE_FILE); + } + }, + }, + + 'Debug Log': { + value: settings.debugLog, + format: v => v ? 'On' : 'Off', + onchange: () => { + settings.debugLog = !settings.debugLog; + }, + }, + }; + + E.showMenu(mainMenu); +}) diff --git a/apps/widadjust/widget.js b/apps/widadjust/widget.js new file mode 100644 index 000000000..138833783 --- /dev/null +++ b/apps/widadjust/widget.js @@ -0,0 +1,244 @@ +(() => { + // ====================================================================== + // CONST + + const DEBUG_LOG_FILE = 'widadjust.log'; + const SETTINGS_FILE = 'widadjust.json'; + const STATE_FILE = 'widadjust.state'; + + const DEFAULT_ADJUST_THRESHOLD = 100; + const DEFAULT_UPDATE_INTERVAL = 60 * 1000; + const MIN_INTERVAL = 10 * 1000; + + const MAX_CLOCK_ERROR_FROM_SAVED_STATE = 2000; + + const SAVE_STATE_CLOCK_ERROR_DELTA_THRESHOLD = 1; + const SAVE_STATE_CLOCK_ERROR_DELTA_IN_PPM_THRESHOLD = 1; + const SAVE_STATE_PPM_DELTA_THRESHOLD = 1; + + // Widget width. + const WIDTH = 22; + + // ====================================================================== + // VARIABLES + + let settings; + let saved; + + let lastClockCheckTime = Date.now(); + let lastClockErrorUpdateTime; + + let clockError; + let currentUpdateInterval; + let lastPpm = null; + + let debugLogFile = null; + + // ====================================================================== + // FUNCTIONS + + function clockCheck() { + let now = Date.now(); + let elapsed = now - lastClockCheckTime; + lastClockCheckTime = now; + + let prevUpdateInterval = currentUpdateInterval; + currentUpdateInterval = settings.updateInterval; + setTimeout(clockCheck, lastClockCheckTime + currentUpdateInterval - Date.now()); + + // If elapsed time differs a lot from expected, + // some other app probably used setTime to change clock significantly. + // -> reset clock error since elapsed time can't be trusted + if (Math.abs(elapsed - prevUpdateInterval) > 10 * 1000) { + // RESET CLOCK ERROR + + clockError = 0; + lastClockErrorUpdateTime = now; + + debug( + 'Looks like some other app used setTime, so reset clockError. (elapsed = ' + + elapsed.toFixed(0) + ')' + ); + WIDGETS.adjust.draw(); + + } else if (!settings.advanced) { + // UPDATE CLOCK ERROR WITHOUT TEMPERATURE COMPENSATION + + updateClockError(settings.ppm); + } else { + // UPDATE CLOCK ERROR WITH TEMPERATURE COMPENSATION + + Bangle.getPressure().then(d => { + let temp = d.temperature; + updateClockError(settings.ppm0 + settings.ppm1 * temp + settings.ppm2 * temp * temp); + }).catch(e => { + WIDGETS.adjust.draw(); + }); + } + } + + function debug(line) { + console.log(line); + if (debugLogFile !== null) { + debugLogFile.write(line + '\n'); + } + } + + function draw() { + g.reset().setFont('6x8').setFontAlign(0, 0); + g.clearRect(this.x, this.y, this.x + WIDTH - 1, this.y + 23); + g.drawString(Math.round(clockError), this.x + WIDTH/2, this.y + 9); + + if (lastPpm !== null) { + g.setFont('4x6').setFontAlign(0, 1); + g.drawString(lastPpm.toFixed(1), this.x + WIDTH/2, this.y + 23); + } + } + + function loadSettings() { + settings = Object.assign({ + advanced: false, + saveState: true, + debugLog: false, + ppm: 0, + ppm0: 0, + ppm1: 0, + ppm2: 0, + adjustThreshold: DEFAULT_ADJUST_THRESHOLD, + updateInterval: DEFAULT_UPDATE_INTERVAL, + }, require('Storage').readJSON(SETTINGS_FILE, true) || {}); + + if (settings.debugLog) { + if (debugLogFile === null) { + debugLogFile = require('Storage').open(DEBUG_LOG_FILE, 'a'); + } + } else { + debugLogFile = null; + } + + settings.updateInterval = Math.max(settings.updateInterval, MIN_INTERVAL); + } + + function onQuit() { + let now = Date.now(); + // WIP + let ppm = (lastPpm !== null) ? lastPpm : settings.ppm; + let updatedClockError = clockError + (now - lastClockErrorUpdateTime) * ppm / 1000000; + let save = false; + + if (! settings.saveState) { + debug(new Date(now).toISOString() + ' QUIT'); + + } else if (saved === undefined) { + save = true; + debug(new Date(now).toISOString() + ' QUIT & SAVE STATE'); + + } else { + let elapsedSaved = now - saved.time; + let estimatedClockError = saved.clockError + elapsedSaved * saved.ppm / 1000000; + + let clockErrorDelta = updatedClockError - estimatedClockError; + let clockErrorDeltaInPpm = clockErrorDelta / elapsedSaved * 1000000; + let ppmDelta = ppm - saved.ppm; + + let debugA = new Date(now).toISOString() + ' QUIT'; + let debugB = + '\n> ' + updatedClockError.toFixed(2) + ' - ' + estimatedClockError.toFixed(2) + ' = ' + + clockErrorDelta.toFixed(2) + ' (' + + clockErrorDeltaInPpm.toFixed(1) + ' PPM) ; ' + + ppm.toFixed(1) + ' - ' + saved.ppm.toFixed(1) + ' = ' + ppmDelta.toFixed(1); + + if ((Math.abs(clockErrorDelta) >= SAVE_STATE_CLOCK_ERROR_DELTA_THRESHOLD + && Math.abs(clockErrorDeltaInPpm) >= SAVE_STATE_CLOCK_ERROR_DELTA_IN_PPM_THRESHOLD + ) || Math.abs(ppmDelta) >= SAVE_STATE_PPM_DELTA_THRESHOLD + ) + { + save = true; + debug(debugA + ' & SAVE STATE' + debugB); + } else { + debug(debugA + debugB); + } + } + + if (save) { + require('Storage').writeJSON(STATE_FILE, { + counter: (saved === undefined) ? 1 : saved.counter + 1, + time: Math.round(now), + clockError: Math.round(updatedClockError * 1000) / 1000, + ppm: Math.round(ppm * 1000) / 1000, + }); + } + } + + function updateClockError(ppm) { + let now = Date.now(); + let elapsed = now - lastClockErrorUpdateTime; + let drift = elapsed * ppm / 1000000; + clockError += drift; + lastClockErrorUpdateTime = now; + lastPpm = ppm; + + if (Math.abs(clockError) >= settings.adjustThreshold) { + let now = Date.now(); + // Shorter variables are faster to look up and this part is time sensitive. + let e = clockError / 1000; + setTime(getTime() - e); + debug( + new Date(now).toISOString() + ' -> ' + ((now / 1000 - e) % 60).toFixed(3) + + ' SET TIME (' + clockError.toFixed(2) + ')' + ); + clockError = 0; + } + + WIDGETS.adjust.draw(); + } + + // ====================================================================== + // MAIN + + loadSettings(); + + WIDGETS.adjust = { + area: 'tr', + draw: draw, + now: () => { + let now = Date.now(); + // WIP + let ppm = (lastPpm !== null) ? lastPpm : settings.ppm; + let updatedClockError = clockError + (now - lastClockErrorUpdateTime) * ppm / 1000000; + return now - updatedClockError; + }, + width: WIDTH, + }; + + if (settings.saveState) { + saved = require('Storage').readJSON(STATE_FILE, true); + } + + let now = Date.now(); + lastClockErrorUpdateTime = now; + if (saved === undefined) { + clockError = 0; + debug(new Date().toISOString() + ' START'); + } else { + clockError = saved.clockError + (now - saved.time) * saved.ppm / 1000000; + + if (Math.abs(clockError) <= MAX_CLOCK_ERROR_FROM_SAVED_STATE) { + debug( + new Date().toISOString() + ' START & LOAD STATE (' + + clockError.toFixed(2) + ')' + ); + } else { + debug( + new Date().toISOString() + ' START & IGNORE STATE (' + + clockError.toFixed(2) + ')' + ); + clockError = 0; + } + } + + clockCheck(); + + E.on('kill', onQuit); + +})() diff --git a/apps/widbaroalarm/ChangeLog b/apps/widbaroalarm/ChangeLog new file mode 100644 index 000000000..c12cc0d65 --- /dev/null +++ b/apps/widbaroalarm/ChangeLog @@ -0,0 +1,2 @@ +0.01: Initial version +0.02: Do not warn multiple times for the same exceedance diff --git a/apps/widbaroalarm/README.md b/apps/widbaroalarm/README.md new file mode 100644 index 000000000..fdc239170 --- /dev/null +++ b/apps/widbaroalarm/README.md @@ -0,0 +1,24 @@ +# Barometer alarm widget + +Get a notification when the pressure reaches defined thresholds. + + +## Settings +* Interval: check interval of sensor data in minutes. 0 to disable automatic check. +* Low alarm: Toggle low alarm + * Low threshold: Warn when pressure drops below this value +* High alarm: Toggle high alarm + * High threshold: Warn when pressure exceeds above this value +* Drop alarm: Warn when pressure drops more than this value in the recent 3 hours (having at least 30 min of data) + 0 to disable this alarm. +* Raise alarm: Warn when pressure raises more than this value in the recent 3 hours (having at least 30 min of data) + 0 to disable this alarm. +* Show widget: Enable/disable widget visibility +* Buzz on alarm: Enable/disable buzzer on alarm + + +## Widget +The widget shows two rows: pressure value of last measurement and pressure average of the the last three hours. + +## Creator +Marco ([myxor](https://github.com/myxor)) diff --git a/apps/widbaroalarm/default.json b/apps/widbaroalarm/default.json new file mode 100644 index 000000000..3d81baa81 --- /dev/null +++ b/apps/widbaroalarm/default.json @@ -0,0 +1,11 @@ +{ + "buzz": true, + "lowalarm": false, + "min": 950, + "highalarm": false, + "max": 1030, + "drop3halarm": 2, + "raise3halarm": 0, + "show": true, + "interval": 15 +} diff --git a/apps/widbaroalarm/metadata.json b/apps/widbaroalarm/metadata.json new file mode 100644 index 000000000..9c58a41ab --- /dev/null +++ b/apps/widbaroalarm/metadata.json @@ -0,0 +1,19 @@ +{ + "id": "widbaroalarm", + "name": "Barometer Alarm Widget", + "shortName": "Barometer Alarm", + "version": "0.02", + "description": "A widget that can alarm on when the pressure reaches defined thresholds.", + "icon": "widget.png", + "type": "widget", + "tags": "tool,barometer", + "supports": ["BANGLEJS2"], + "dependencies": {"notify":"type"}, + "readme": "README.md", + "storage": [ + {"name":"widbaroalarm.wid.js","url":"widget.js"}, + {"name":"widbaroalarm.settings.js","url":"settings.js"}, + {"name":"widbaroalarm.default.json","url":"default.json"} + ], + "data": [{"name":"widbaroalarm.json"}, {"name":"widbaroalarm.log"}] +} diff --git a/apps/widbaroalarm/settings.js b/apps/widbaroalarm/settings.js new file mode 100644 index 000000000..bea6319d1 --- /dev/null +++ b/apps/widbaroalarm/settings.js @@ -0,0 +1,95 @@ +(function(back) { + const SETTINGS_FILE = "widbaroalarm.json"; + const storage = require('Storage'); + let settings = Object.assign( + storage.readJSON("widbaroalarm.default.json", true) || {}, + storage.readJSON(SETTINGS_FILE, true) || {} + ); + + function save(key, value) { + settings[key] = value; + storage.write(SETTINGS_FILE, settings); + } + + function showMainMenu() { + let menu ={ + '': { 'title': 'Barometer alarm widget' }, + /*LANG*/'< Back': back, + "Interval": { + value: settings.interval, + min: 0, + max: 120, + step: 1, + format: x => { + return x != 0 ? x + ' min' : 'off'; + }, + onchange: x => save("interval", x) + }, + "Low alarm": { + value: settings.lowalarm, + format: x => { + return x ? 'Yes' : 'No'; + }, + onchange: x => save("lowalarm", x), + }, + "Low threshold": { + value: settings.min, + min: 600, + max: 1000, + step: 5, + onchange: x => save("min", x), + }, + "High alarm": { + value: settings.highalarm, + format: x => { + return x ? 'Yes' : 'No'; + }, + onchange: x => save("highalarm", x), + }, + "High threshold": { + value: settings.max, + min: 700, + max: 1100, + step: 5, + onchange: x => save("max", x), + }, + "Drop alarm": { + value: settings.drop3halarm, + min: 0, + max: 10, + step: 1, + format: x => { + return x != 0 ? x + ' hPa/3h' : 'off'; + }, + onchange: x => save("drop3halarm", x) + }, + "Raise alarm": { + value: settings.raise3halarm, + min: 0, + max: 10, + step: 1, + format: x => { + return x != 0 ? x + ' hPa/3h' : 'off'; + }, + onchange: x => save("raise3halarm", x) + }, + "Show widget": { + value: settings.show, + format: x => { + return x ? 'Yes' : 'No'; + }, + onchange: x => save('show', x) + }, + "Buzz on alarm": { + value: settings.buzz, + format: x => { + return x ? 'Yes' : 'No'; + }, + onchange: x => save('buzz', x) + }, + }; + E.showMenu(menu); + } + + showMainMenu(); +}); diff --git a/apps/widbaroalarm/widget.js b/apps/widbaroalarm/widget.js new file mode 100644 index 000000000..5d62156eb --- /dev/null +++ b/apps/widbaroalarm/widget.js @@ -0,0 +1,239 @@ +(function() { + let medianPressure; + let threeHourAvrPressure; + let currentPressures = []; + + const LOG_FILE = "widbaroalarm.log.json"; + const SETTINGS_FILE = "widbaroalarm.json"; + const storage = require('Storage'); + + let settings; + + function loadSettings() { + settings = Object.assign( + storage.readJSON("widbaroalarm.default.json", true) || {}, + storage.readJSON(SETTINGS_FILE, true) || {} + ); + } + + loadSettings(); + + + function setting(key) { + return settings[key]; + } + + function saveSetting(key, value) { + settings[key] = value; + storage.write(SETTINGS_FILE, settings); + } + + const interval = setting("interval"); + + let history3 = storage.readJSON(LOG_FILE, true) || []; // history of recent 3 hours + + function showAlarm(body, title) { + if (body == undefined) return; + + require("notify").show({ + title: title || "Pressure", + body: body, + icon: require("heatshrink").decompress(atob("jEY4cA///gH4/++mkK30kiWC4H8x3BGDmSGgYDCgmSoEAg3bsAIDpAIFkmSpMAm3btgIFDQwIGNQpTYkAIJwAHEgMoCA0JgMEyBnBCAW3KoQQDhu3oAIH5JnDBAW24IIBEYm2EYwACBCIACA")) + }); + + if (setting("buzz") && + !(storage.readJSON('setting.json', 1) || {}).quiet) { + Bangle.buzz(); + } + } + + + function didWeAlreadyWarn(key) { + return setting(key) == undefined || setting(key) > 0; + } + + function checkForAlarms(pressure) { + if (pressure == undefined || pressure <= 0) return; + + let alreadyWarned = false; + + const ts = Math.round(Date.now() / 1000); // seconds + const d = { + "ts": ts, + "p": pressure + }; + + // delete entries older than 3h + for (let i = 0; i < history3.length; i++) { + if (history3[i]["ts"] < ts - (3 * 60 * 60)) { + history3.shift(); + } + } + // delete oldest entries until we have max 50 + while (history3.length > 50) { + history3.shift(); + } + + if (setting("lowalarm")) { + // Is below the alarm threshold? + if (pressure <= setting("min")) { + if (!didWeAlreadyWarn("lastLowWarningTs")) { + showAlarm("Pressure low: " + Math.round(pressure) + " hPa"); + saveSetting("lastLowWarningTs", ts); + alreadyWarned = true; + } + } else { + saveSetting("lastLowWarningTs", 0); + } + } else { + saveSetting("lastLowWarningTs", 0); + } + + if (setting("highalarm")) { + // Is above the alarm threshold? + if (pressure >= setting("max")) { + if (!didWeAlreadyWarn("lastHighWarningTs")) { + showAlarm("Pressure high: " + Math.round(pressure) + " hPa"); + saveSetting("lastHighWarningTs", ts); + alreadyWarned = true; + } + } else { + saveSetting("lastHighWarningTs", 0); + } + } else { + saveSetting("lastHighWarningTs", 0); + } + + if (!alreadyWarned) { + // 3h change detection + const drop3halarm = setting("drop3halarm"); + const raise3halarm = setting("raise3halarm"); + if (drop3halarm > 0 || raise3halarm > 0) { + // we need at least 30min of data for reliable detection + if (history3[0]["ts"] > ts - (30 * 60)) { + return; + } + + // Get oldest entry: + const oldestPressure = history3[0]["p"]; + if (oldestPressure != undefined && oldestPressure > 0) { + const diff = oldestPressure - pressure; + + // drop alarm + if (drop3halarm > 0 && oldestPressure > pressure) { + if (Math.abs(diff) > drop3halarm) { + if (!didWeAlreadyWarn("lastDropWarningTs")) { + showAlarm((Math.round(Math.abs(diff) * 10) / 10) + " hPa/3h from " + + Math.round(oldestPressure) + " to " + Math.round(pressure) + " hPa", "Pressure drop"); + saveSetting("lastDropWarningTs", ts); + } + } else { + saveSetting("lastDropWarningTs", 0); + } + } else { + saveSetting("lastDropWarningTs", 0); + } + + // raise alarm + if (raise3halarm > 0 && oldestPressure < pressure) { + if (Math.abs(diff) > raise3halarm) { + if (!didWeAlreadyWarn("lastRaiseWarningTs")) { + showAlarm((Math.round(Math.abs(diff) * 10) / 10) + " hPa/3h from " + + Math.round(oldestPressure) + " to " + Math.round(pressure) + " hPa", "Pressure raise"); + saveSetting("lastRaiseWarningTs", ts); + } + } else { + saveSetting("lastRaiseWarningTs", 0); + } + } else { + saveSetting("lastRaiseWarningTs", 0); + } + } + } + } + + history3.push(d); + // write data to storage + storage.writeJSON(LOG_FILE, history3); + + // calculate 3h average for widget + let sum = 0; + for (let i = 0; i < history3.length; i++) { + sum += history3[i]["p"]; + } + threeHourAvrPressure = sum / history3.length; + } + + + + function baroHandler(data) { + if (data) { + const pressure = Math.round(data.pressure); + if (pressure == undefined || pressure <= 0) return; + currentPressures.push(pressure); + } + } + + /* + turn on barometer power + take 5 measurements + sort the results + take the middle one (median) + turn off barometer power + */ + function check() { + Bangle.setBarometerPower(true, "widbaroalarm"); + setTimeout(function() { + currentPressures = []; + + Bangle.getPressure().then(baroHandler); + Bangle.getPressure().then(baroHandler); + Bangle.getPressure().then(baroHandler); + Bangle.getPressure().then(baroHandler); + Bangle.getPressure().then(baroHandler); + + setTimeout(function() { + Bangle.setBarometerPower(false, "widbaroalarm"); + + currentPressures.sort(); + + // take median value + medianPressure = currentPressures[3]; + checkForAlarms(medianPressure); + }, 1000); + }, 500); + } + + function reload() { + check(); + } + + function draw() { + if (global.WIDGETS != undefined && typeof WIDGETS === "object") { + WIDGETS["baroalarm"] = { + width: setting("show") ? 24 : 0, + reload: reload, + area: "tr", + draw: draw + }; + } + + g.reset(); + if (setting("show") && medianPressure != undefined) { + g.setFont("6x8", 1).setFontAlign(1, 0); + g.drawString(Math.round(medianPressure), this.x + 24, this.y + 6); + if (threeHourAvrPressure != undefined && threeHourAvrPressure > 0) { + g.drawString(Math.round(threeHourAvrPressure), this.x + 24, this.y + 6 + 10); + } + } + } + + // Let's delay the first check a bit + setTimeout(function() { + check(); + if (interval > 0) { + setInterval(check, interval * 60000); + } + }, 1000); + +})(); diff --git a/apps/widbaroalarm/widget.png b/apps/widbaroalarm/widget.png new file mode 100644 index 000000000..5be292143 Binary files /dev/null and b/apps/widbaroalarm/widget.png differ diff --git a/apps/widbaroalarm/widget24.png b/apps/widbaroalarm/widget24.png new file mode 100644 index 000000000..2f0d5e4ce Binary files /dev/null and b/apps/widbaroalarm/widget24.png differ diff --git a/apps/widbars/ChangeLog b/apps/widbars/ChangeLog index 4c21f3ace..61e28e6e4 100644 --- a/apps/widbars/ChangeLog +++ b/apps/widbars/ChangeLog @@ -1 +1,3 @@ 0.01: New Widget! +0.02: Battery bar turns yellow on charge + Memory status bar does not trigger garbage collect diff --git a/apps/widbars/metadata.json b/apps/widbars/metadata.json index e8d52c90a..a9981305c 100644 --- a/apps/widbars/metadata.json +++ b/apps/widbars/metadata.json @@ -1,7 +1,7 @@ { "id": "widbars", "name": "Bars Widget", - "version": "0.01", + "version": "0.02", "description": "Display several measurements as vertical bars.", "icon": "icon.png", "screenshots": [{"url":"screenshot.png"}], diff --git a/apps/widbars/widget.js b/apps/widbars/widget.js index a1134f31f..cceeb0897 100644 --- a/apps/widbars/widget.js +++ b/apps/widbars/widget.js @@ -42,19 +42,25 @@ if (top) g .clearRect(x,y, x+w-1,y+top-1); // erase above bar if (f) g.setColor(col).fillRect(x,y+top, x+w-1,y+h-1); // even for f=0.001 this is still 1 pixel high } + let batColor='#0f0'; function draw() { g.reset(); const x = this.x, y = this.y, - m = process.memory(); + m = process.memory(false); let b=0; // ==HRM== bar(x+(w*b++),y,'#f00'/*red */,bpm/200); // >200 seems very unhealthy; if we have no valid bpm this will just be empty space // ==Temperature== bar(x+(w*b++),y,'#ff0'/*yellow */,E.getTemperature()/50); // you really don't want to wear a watch that's hotter than 50°C bar(x+(w*b++),y,g.theme.dark?'#0ff':'#00f'/*cyan/blue*/,1-(require('Storage').getFree() / process.env.STORAGE)); bar(x+(w*b++),y,'#f0f'/*magenta*/,m.usage/m.total); - bar(x+(w*b++),y,'#0f0'/*green */,E.getBattery()/100); + bar(x+(w*b++),y,batColor,E.getBattery()/100); } let redraw; + Bangle.on('charging', function(charging) { + batColor=charging?'#ff0':'#0f0'; + WIDGETS["bars"].draw(); + }); + Bangle.on('lcdPower', on => { if (redraw) clearInterval(redraw) redraw = undefined; diff --git a/apps/widcw/ChangeLog b/apps/widcw/ChangeLog new file mode 100644 index 000000000..a4bc24d1a --- /dev/null +++ b/apps/widcw/ChangeLog @@ -0,0 +1 @@ +0.01: First version \ No newline at end of file diff --git a/apps/widcw/logo.svg b/apps/widcw/logo.svg new file mode 100644 index 000000000..e093414d5 --- /dev/null +++ b/apps/widcw/logo.svg @@ -0,0 +1,62 @@ + + + + + + + + CW + + diff --git a/apps/widcw/metadata.json b/apps/widcw/metadata.json new file mode 100644 index 000000000..653b093ec --- /dev/null +++ b/apps/widcw/metadata.json @@ -0,0 +1,13 @@ +{ + "id": "widcw", + "name": "Calendar Week Widget", + "version": "0.01", + "description": "Widget which shows the current calendar week", + "icon": "widget.png", + "type": "widget", + "tags": "widget,calendar", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"widcw.wid.js","url":"widget.js"} + ] +} diff --git a/apps/widcw/widget.js b/apps/widcw/widget.js new file mode 100644 index 000000000..ef43a4551 --- /dev/null +++ b/apps/widcw/widget.js @@ -0,0 +1,48 @@ +(function() { + var width = 22; // width of the widget + + function draw() { + const x = this.x, y = this.y, x2 = x+21, y2 = y+23; + + var date = new Date(); + + // Calculate calendar week (https://stackoverflow.com/a/6117889) + getCW= function(date){ + var d=new Date(date.getFullYear(), date.getMonth(), date.getDate()); + var dayNum = d.getDay() || 7; + d.setDate(d.getDate() + 4 - dayNum); + var yearStart = new Date(d.getFullYear(),0,1); + return Math.ceil((((d - yearStart) / 86400000) + 1)/7); + }; + + g.reset().setFontAlign(0, 0) // center all text + // header + .setBgColor("#f00").setColor("#fff") + .clearRect(x, y, x2, y+8).setFont("4x6").drawString("CW", (x+x2)/2+1, y+5) + // date + .setBgColor("#fff").setColor("#000") + .clearRect(x, y+9, x2, y2).setFont("Vector:16").drawString(getCW(date), (x+x2)/2+2, y+17); + + if (!g.theme.dark) { + // black border around date for light themes + g.setColor("#000").drawPoly([ + x, y+9, + x, y2, + x2, y2, + x2, y+9 + ]); + } + + // redraw when date changes + setTimeout(()=>WIDGETS["widcw"].draw(), (86401 - Math.floor(date/1000) % 86400)*1000); + + } + + // add your widget + WIDGETS["widcw"]={ + area:"tl", // tl (top left), tr (top right), bl (bottom left), br (bottom right) + width: width, // how wide is the widget? You can change this and call Bangle.drawWidgets() to re-layout + draw:draw // called to draw the widget + }; + +})(); diff --git a/apps/widcw/widget.png b/apps/widcw/widget.png new file mode 100644 index 000000000..c73d40c5a Binary files /dev/null and b/apps/widcw/widget.png differ diff --git a/apps/widcw/widget.svg b/apps/widcw/widget.svg new file mode 100644 index 000000000..d3e567286 --- /dev/null +++ b/apps/widcw/widget.svg @@ -0,0 +1,55 @@ + + + + + + + + CW + + diff --git a/apps/widgps/ChangeLog b/apps/widgps/ChangeLog index 3d47b8a0c..f68fc701c 100644 --- a/apps/widgps/ChangeLog +++ b/apps/widgps/ChangeLog @@ -2,3 +2,4 @@ 0.02: Don't break if running on 2v08 firmware (just don't display anything) 0.03: Fix positioning 0.04: Show GPS fix status +0.05: Don't poll for GPS status, override setGPSPower handler (fix #1456) diff --git a/apps/widgps/metadata.json b/apps/widgps/metadata.json index 2f59aa82a..39bff2fad 100644 --- a/apps/widgps/metadata.json +++ b/apps/widgps/metadata.json @@ -1,7 +1,7 @@ { "id": "widgps", "name": "GPS Widget", - "version": "0.04", + "version": "0.05", "description": "Tiny widget to show the power and fix status of the GPS", "icon": "widget.png", "type": "widget", diff --git a/apps/widgps/widget.js b/apps/widgps/widget.js index 25df2178a..bfdb89d33 100644 --- a/apps/widgps/widget.js +++ b/apps/widgps/widget.js @@ -1,7 +1,13 @@ (function(){ - if (!Bangle.isGPSOn) return; // old firmware + // 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; + } - function draw() { + WIDGETS.gps={area:"tr",width:24,draw:function() { g.reset(); if (Bangle.isGPSOn()) { const gpsObject = Bangle.getGPSFix(); @@ -14,20 +20,5 @@ g.setColor("#888"); // off = grey } g.drawImage(atob("GBiBAAAAAAAAAAAAAA//8B//+BgYGBgYGBgYGBgYGBgYGBgYGB//+B//+BgYGBgYGBgYGBgYGBgYGBgYGB//+A//8AAAAAAAAAAAAA=="), this.x, 2+this.y); - } - - var timerInterval; - Bangle.on('lcdPower', function(on) { - if (on) { - WIDGETS.gps.draw(); - if (!timerInterval) timerInterval = setInterval(()=>WIDGETS.gps.draw(), 2000); - } else { - if (timerInterval) { - clearInterval(timerInterval); - timerInterval = undefined; - } - } - }); - - WIDGETS.gps={area:"tr",width:24,draw:draw}; + }}; })(); diff --git a/apps/widmnth/ChangeLog b/apps/widmnth/ChangeLog new file mode 100644 index 000000000..370f41e8a --- /dev/null +++ b/apps/widmnth/ChangeLog @@ -0,0 +1 @@ +0.01: Simple new widget! diff --git a/apps/widmnth/README.md b/apps/widmnth/README.md new file mode 100644 index 000000000..ef912c739 --- /dev/null +++ b/apps/widmnth/README.md @@ -0,0 +1,22 @@ +# Widget Name + +The days left in month widget is simple and just prints the number of days left in the month in the top left corner. +The idea is to encourage people to keep track of time and keep goals they may have for the month. + +## Usage + +Hopefully you just have to Install it and it'll work. Customizing the location would just be changing tl to tr. + +## Features + +* Shows days left in month +* Only updates at midnight. + + +## Requests + +Complaints,compliments,problems,suggestions,annoyances,bugs, and all other feedback can be filed at [this repo](https://github.com/N-Onorato/BangleApps) + +## Creator + +Nick diff --git a/apps/widmnth/metadata.json b/apps/widmnth/metadata.json new file mode 100644 index 000000000..25f3a8126 --- /dev/null +++ b/apps/widmnth/metadata.json @@ -0,0 +1,14 @@ +{ "id": "widmnth", + "name": "Days left in month widget", + "shortName":"Month Countdown", + "version":"0.01", + "description": "A simple widget that displays the number of days left in the month.", + "icon": "widget.png", + "type": "widget", + "tags": "widget,date,time,countdown,month", + "supports" : ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"widmnth.wid.js","url":"widget.js"} + ] +} diff --git a/apps/widmnth/widget.js b/apps/widmnth/widget.js new file mode 100644 index 000000000..c4eca155a --- /dev/null +++ b/apps/widmnth/widget.js @@ -0,0 +1,42 @@ + +(() => { + var days_left; + var clearCode; + + function getDaysLeft(day) { + let year = day.getMonth() == 11 ? day.getFullYear() + 1 : day.getFullYear(); // rollover if december. + next_month = new Date(year, (day.getMonth() + 1) % 12, 1, 0, 0, 0); + let days_left = Math.floor((next_month - day) / 86400000); // ms left in month divided by ms in a day + return days_left; + } + + function getTimeTilMidnight(now) { + let midnight = new Date(now.getTime()); + midnight.setHours(23); + midnight.setMinutes(59); + midnight.setSeconds(59); + midnight.setMilliseconds(999); + return (midnight - now) + 1; + } + + function update() { + let now = new Date(); + days_left = getDaysLeft(now); + let ms_til_midnight = getTimeTilMidnight(now); + clearCode = setTimeout(update, ms_til_midnight); + } + + function draw() { + g.reset(); + g.setFont("4x6", 3); + if(!clearCode) update(); // On first run calculate days left and setup interval to update state. + g.drawString(days_left < 10 ? "0" + days_left : days_left.toString(), this.x + 2, this.y + 4); + } + + // add your widget + WIDGETS.widmonthcountdown={ + area:"tl", // tl (top left), tr (top right), bl (bottom left), br (bottom right) + width: 24, + draw:draw + }; +})(); \ No newline at end of file diff --git a/apps/widmnth/widget.png b/apps/widmnth/widget.png new file mode 100644 index 000000000..4a042ec05 Binary files /dev/null and b/apps/widmnth/widget.png differ diff --git a/backup.js b/backup.js new file mode 100644 index 000000000..b0159036d --- /dev/null +++ b/backup.js @@ -0,0 +1,118 @@ +/* Code to handle Backup/Restore functionality */ + +const BACKUP_STORAGEFILE_DIR = "storage-files"; + +function bangleDownload() { + var zip = new JSZip(); + Progress.show({title:"Scanning...",sticky:true}); + var normalFiles, storageFiles; + console.log("Listing normal files..."); + Comms.reset() + .then(() => Comms.showMessage("Backing up...")) + .then(() => Comms.listFiles({sf:false})) + .then(f => { + normalFiles = f; + console.log(" - "+f.join(",")); + console.log("Listing StorageFiles..."); + return Comms.listFiles({sf:true}); + }).then(f => { + storageFiles = f; + console.log(" - "+f.join(",")); + var fileCount = normalFiles.length + storageFiles.length; + var promise = Promise.resolve(); + // Normal files + normalFiles.forEach((filename,n) => { + promise = promise.then(() => { + Progress.hide({sticky: true}); + var percent = n/fileCount; + Progress.show({title:`Download ${filename}`,sticky:true,min:percent,max:percent,percent:0}); + return Comms.readFile(filename).then(data => zip.file(filename,data)); + }); + }); + // Storage files + if (storageFiles.length) { + var zipStorageFiles = zip.folder(BACKUP_STORAGEFILE_DIR); + storageFiles.forEach((filename,n) => { + promise = promise.then(() => { + Progress.hide({sticky: true}); + var percent = (normalFiles.length+n)/fileCount; + Progress.show({title:`Download ${filename}`,sticky:true,min:percent,max:percent,percent:0}); + return Comms.readStorageFile(filename).then(data => zipStorageFiles.file(filename,data)); + }); + }); + } + return promise; + }).then(() => { + return Comms.showMessage(Const.MESSAGE_RELOAD); + }).then(() => { + return zip.generateAsync({type:"binarystring"}); + }).then(content => { + Progress.hide({ sticky: true }); + showToast('Backup complete!', 'success'); + Espruino.Core.Utils.fileSaveDialog(content, "Banglejs backup.zip"); + }).catch(err => { + Progress.hide({ sticky: true }); + showToast('Backup failed, ' + err, 'error'); + }); +} + +function bangleUpload() { + Espruino.Core.Utils.fileOpenDialog({ + id:"backup", + type:"arraybuffer", + mimeType:".zip,application/zip"}, function(data) { + if (data===undefined) return; + var promise = Promise.resolve(); + var zip = new JSZip(); + var cmds = ""; + zip.loadAsync(data).then(function(zip) { + return showPrompt("Restore from ZIP","Are you sure? This will remove all existing apps"); + }).then(()=>{ + Progress.show({title:`Reading ZIP`}); + zip.forEach(function (path, file){ + console.log("path"); + promise = promise + .then(() => file.async("string")) + .then(data => { + console.log("decoded", path); + if (path.startsWith(BACKUP_STORAGEFILE_DIR)) { + path = path.substr(BACKUP_STORAGEFILE_DIR.length+1); + cmds += AppInfo.getStorageFileUploadCommands(path, data)+"\n"; + } else if (!path.includes("/")) { + cmds += AppInfo.getFileUploadCommands(path, data)+"\n"; + } else console.log("Ignoring "+path); + }); + }); + return promise; + }) + .then(() => { + Progress.hide({sticky:true}); + Progress.show({title:`Erasing...`}); + return Comms.removeAllApps(); }) + .then(() => { + Progress.hide({sticky:true}); + Progress.show({title:`Restoring...`, sticky:true}); + return Comms.showMessage(`Restoring...`); }) + .then(() => Comms.write("\x10"+Comms.getProgressCmd()+"\n")) + .then(() => Comms.uploadCommandList(cmds, 0, cmds.length)) + .then(() => Comms.showMessage(Const.MESSAGE_RELOAD)) + .then(() => { + Progress.hide({sticky:true}); + showToast('Restore complete!', 'success'); + }) + .catch(err => { + Progress.hide({sticky:true}); + showToast('Restore failed, ' + err, 'error'); + }); + return promise; + }); +} + +window.addEventListener('load', (event) => { + document.getElementById("downloadallapps").addEventListener("click",event=>{ + bangleDownload(); + }); + document.getElementById("uploadallapps").addEventListener("click",event=>{ + bangleUpload(); + }); +}); diff --git a/core b/core index bf29f5697..6e94cf77a 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit bf29f5697445686255a785476e6b1ed6a13ff697 +Subproject commit 6e94cf77a2b355389dcd5efa55e2249a9b31983c diff --git a/css/main.css b/css/main.css index f4850babe..a986df22e 100644 --- a/css/main.css +++ b/css/main.css @@ -81,7 +81,7 @@ a.btn.btn-link.dropdown-toggle { min-height: 8em; } -.tile-content { position: relative; } +.tile-content { position: relative; word-break: break-all; } .link-github { position:absolute; top: 36px; diff --git a/index.html b/index.html index 6c9a21bf8..a418b48eb 100644 --- a/index.html +++ b/index.html @@ -131,6 +131,8 @@

+

+

Settings