diff --git a/Bangle.js.svg b/Bangle.js.svg new file mode 100644 index 000000000..90c908c9b --- /dev/null +++ b/Bangle.js.svg @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index 2d0b54a7d..877fe5e2c 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@ Bangle.js App Loader (and Apps) * Try the **release version** at [banglejs.com/apps](https://banglejs.com/apps) * Try the **development version** at [github.io](https://espruino.github.io/BangleApps/) +**All software (including apps) in this repository is MIT Licensed - see [LICENSE](LICENSE)** By +submitting code to this repository you confirm that you are happy with it being MIT licensed, +and that it is not licensed in another way that would make this impossible. + ## How does it work? * A list of apps is in `apps.json` @@ -32,6 +36,7 @@ easily distinguish between file types, we use the following: * `stuff.img` is an image * `stuff.app.js` is JS code for applications * `stuff.wid.js` is JS code for widgets +* `stuff.settings.js` is JS code for the settings menu * `stuff.boot.js` is JS code that automatically gets run at boot time * `stuff.json` is used for JSON settings for an app @@ -314,6 +319,48 @@ the data you require from Bangle.js. See [apps/gpsrec/interface.html](the GPS Recorder) for a full example. +### Adding configuration to the "Settings" menu + +Apps (or widgets) can add their own settings to the "Settings" menu under "App/widget settings". +To do so, the app needs to include a `settings.js` file, containing a single function +that handles configuring the app. +When the app settings are opened, this function is called with one +argument, `back`: a callback to return to the settings menu. + +Example `settings.js` +```js +// make sure to enclose the function in parentheses +(function(back) { + let settings = require('Storage').readJSON('app.settings.json',1)||{}; + function save(key, value) { + settings[key] = value; + require('Storage').write('app.settings.json',settings); + } + const appMenu = { + '': {'title': 'App Settings'}, + '< Back': back, + 'Monkeys': { + value: settings.monkeys||12, + onchange: (m) => {save('monkeys', m)} + } + }; + E.showMenu(appMenu) +}) +``` +In this example the app needs to add both `app.settings.js` and +`app.settings.json` to `apps.json`: +```json + { "id": "app", + ... + "storage": [ + ... + {"name":"app.settings.js","url":"settings.js"}, + {"name":"app.settings.json","content":"{}"} + ] + }, +``` +That way removing the app also cleans up `app.settings.json`. + ## Coding hints - use `g.setFont(.., size)` to multiply the font size, eg ("6x8",3) : "18x24" diff --git a/apps.json b/apps.json index 2f057b040..2bbc7dd33 100644 --- a/apps.json +++ b/apps.json @@ -27,7 +27,7 @@ { "id": "daysl", "name": "Days left", "icon": "app.png", - "version":"0.02", + "version":"0.03", "description": "Shows you the days left until a certain date. Date can be set with a settings app and is written to a file.", "tags": "", "allow_emulator":false, @@ -78,24 +78,26 @@ { "id": "welcome", "name": "Welcome", "icon": "app.png", - "version":"0.04", + "version":"0.05", "description": "Appears at first boot and explains how to use Bangle.js", "tags": "start,welcome", "allow_emulator":true, "storage": [ {"name":"welcome.js","url":"welcome.js"}, {"name":"welcome.app.js","url":"app.js"}, + {"name":"welcome.settings.js","url":"settings.js"}, {"name":"welcome.img","url":"app-icon.js","evaluate":true} ] }, { "id": "gbridge", "name": "Gadgetbridge", "icon": "app.png", - "version":"0.06", + "version":"0.07", "description": "The default notification handler for Gadgetbridge notifications from Android", "tags": "tool,system,android,widget", + "type":"widget", "storage": [ - {"name":"gbridge.app.js","url":"app.js"}, + {"name":"gbridge.settings.js","url":"settings.js"}, {"name":"gbridge.img","url":"app-icon.js","evaluate":true}, {"name":"gbridge.wid.js","url":"widget.js"} ] @@ -117,11 +119,12 @@ { "id": "setting", "name": "Settings", "icon": "settings.png", - "version":"0.08", + "version":"0.10", "description": "A menu for setting up Bangle.js", "tags": "tool,system", "storage": [ {"name":"setting.app.js","url":"settings.js"}, + {"name":"setting.boot.js","url":"boot.js"}, {"name":"setting.json","url":"settings-default.json","evaluate":true}, {"name":"setting.img","url":"settings-icon.js","evaluate":true} ], @@ -159,7 +162,7 @@ { "id": "aclock", "name": "Analog Clock", "icon": "clock-analog.png", - "version":"0.10", + "version": "0.11", "description": "An Analog Clock", "tags": "clock", "type":"clock", @@ -336,13 +339,16 @@ }, { "id": "widbatpc", "name": "Battery Level Widget (with percentage)", + "shortName": "Battery Widget", "icon": "widget.png", - "version":"0.06", + "version":"0.08", "description": "Show the current battery level and charging status in the top right of the clock, with charge percentage", "tags": "widget,battery", "type":"widget", "storage": [ - {"name":"widbatpc.wid.js","url":"widget.js"} + {"name":"widbatpc.wid.js","url":"widget.js"}, + {"name":"widbatpc.settings.js","url":"settings.js"}, + {"name":"widbatpc.settings.json","content": "{}"} ] }, { "id": "widbt", @@ -864,6 +870,19 @@ {"name":"torch.img","url":"app-icon.js","evaluate":true} ] }, + { "id": "wohrm", + "name": "Workout HRM", + "icon": "app.png", + "version":"0.06", + "description": "Workout heart rate monitor notifies you with a buzz if your heart rate goes above or below the set limits.", + "tags": "hrm,workout", + "type": "app", + "allow_emulator":true, + "storage": [ + {"name":"wohrm.app.js","url":"app.js"}, + {"name":"wohrm.img","url":"app-icon.js","evaluate":true} + ] + }, { "id": "widid", "name": "Bluetooth ID Widget", "icon": "widget.png", @@ -1014,7 +1033,7 @@ "name": "Touch Launcher", "shortName":"Menu", "icon": "app.png", - "version":"0.02", + "version":"0.04", "description": "Touch enable left to right launcher.", "tags": "tool,system,launcher", "type":"launch", @@ -1060,5 +1079,18 @@ "storage": [ {"name":"widmp.wid.js","url":"widget.js"} ] + }, + { "id": "minionclk", + "name": "Minion clock", + "icon": "minionclk.png", + "version": "0.01", + "description": "Minion themed clock.", + "tags": "clock,minion", + "type": "clock", + "allow_emulator": true, + "storage": [ + {"name":"minionclk.app.js","url":"app.js"}, + {"name":"minionclk.img","url":"app-icon.js","evaluate":true} + ] } ] diff --git a/apps/aclock/ChangeLog b/apps/aclock/ChangeLog index a179800be..98e3da8e7 100644 --- a/apps/aclock/ChangeLog +++ b/apps/aclock/ChangeLog @@ -5,3 +5,4 @@ 0.08: make dots bigger and date more readable 0.09: center date, remove box around it, internal refactor to remove redundant code. 0.10: remove debug, refactor seconds to show elapsed secs each time app is displayed +0.11: shift face down for widget area, maximize face size, 0 pad single digit date, use locale for date diff --git a/apps/aclock/clock-analog.js b/apps/aclock/clock-analog.js index 419ed0933..7b60a728f 100644 --- a/apps/aclock/clock-analog.js +++ b/apps/aclock/clock-analog.js @@ -1,3 +1,4 @@ +// eliminate ide undefined errors let g; let Bangle; @@ -5,15 +6,18 @@ let Bangle; const locale = require('locale'); const p = Math.PI / 2; const pRad = Math.PI / 180; -const faceWidth = 100; // watch face radius +const faceWidth = 100; // watch face radius (240/2 - 24px for widget area) +const widgetHeight=24+1; let timer = null; let currentDate = new Date(); -const centerPx = g.getWidth() / 2; +const centerX = g.getWidth() / 2; +const centerY = (g.getWidth() / 2) + widgetHeight/2; + const seconds = (angle) => { const a = angle * pRad; - const x = centerPx + Math.sin(a) * faceWidth; - const y = centerPx - Math.cos(a) * faceWidth; + const x = centerX + Math.sin(a) * faceWidth; + const y = centerY - Math.cos(a) * faceWidth; // if 15 degrees, make hour marker larger const radius = (angle % 15) ? 2 : 4; @@ -25,14 +29,14 @@ const hand = (angle, r1, r2) => { const r3 = 3; g.fillPoly([ - Math.round(centerPx + Math.sin(a) * r1), - Math.round(centerPx - Math.cos(a) * r1), - Math.round(centerPx + Math.sin(a + p) * r3), - Math.round(centerPx - Math.cos(a + p) * r3), - Math.round(centerPx + Math.sin(a) * r2), - Math.round(centerPx - Math.cos(a) * r2), - Math.round(centerPx + Math.sin(a - p) * r3), - Math.round(centerPx - Math.cos(a - p) * r3) + Math.round(centerX + Math.sin(a) * r1), + Math.round(centerY - Math.cos(a) * r1), + Math.round(centerX + Math.sin(a + p) * r3), + Math.round(centerY - Math.cos(a + p) * r3), + Math.round(centerX + Math.sin(a) * r2), + Math.round(centerY - Math.cos(a) * r2), + Math.round(centerX + Math.sin(a - p) * r3), + Math.round(centerY - Math.cos(a - p) * r3) ]); }; @@ -54,6 +58,7 @@ const drawAll = () => { seconds((360 * i) / 60); } onSecond(); + }; const resetSeconds = () => { @@ -88,8 +93,8 @@ const drawDate = () => { // console.log(`${dayString}|${dateString}`); // center date const l = (g.getWidth() - g.stringWidth(dateDisplay)) / 2; - const t = centerPx + 37; - g.drawString(dateDisplay, l, t); + const t = centerY + 37; + g.drawString(dateDisplay, l, t, true); // console.log(l, t); }; const onMinute = () => { diff --git a/apps/daysl/ChangeLog b/apps/daysl/ChangeLog index 4e0b8e6cf..c3faf0092 100644 --- a/apps/daysl/ChangeLog +++ b/apps/daysl/ChangeLog @@ -1,2 +1,3 @@ 0.01: New Widget! 0.02: Improved calculation, new image for app +0.03: Improved display of number diff --git a/apps/daysl/widget.js b/apps/daysl/widget.js index 6fb755d1e..4a32d5f26 100644 --- a/apps/daysl/widget.js +++ b/apps/daysl/widget.js @@ -1,39 +1,84 @@ const storage = require('Storage'); let settings; +let height = 23; +let width = 34; +var debug = 0; //1 = show debug info + +//write settings to file function updateSettings() { storage.write('daysleft.json', settings); +} + +//Define standard settings +function resetSettings() { + settings = { + day : 17, + month : 6, + year: 2020 + }; + updateSettings(); +} + +settings = storage.readJSON('daysleft.json',1); //read storage +if (!settings) resetSettings(); //if settings file was not found, set to standard + +var dd = settings.day, + mm = settings.month-1, //-1 because month is zero-based + yy = settings.year; + +const oneDay = 24 * 60 * 60 * 1000; // hours*minutes*seconds*milliseconds +const targetDate = new Date(yy, mm, dd); //is 00:00 +const today = new Date(); //includes current time + +const currentYear = today.getFullYear(); +const currentMonth = today.getMonth(); +const currentDay = today.getDate(); +const todayMorning = new Date (currentYear, currentMonth, currentDay, 0, 0, 0); //create date object with today, but 00:00:00 + +const diffDays = (targetDate - todayMorning) / oneDay; //calculate day difference + +function drawWidget() { + if (debug == 1) g.drawRect(this.x,this.y,this.x+width,this.y+height); //draw rectangle around widget area + g.reset(); + + //define font size and string position + //small if number has more than 3 digits (positive number) + if (diffDays >= 1000) { + g.setFont("6x8", 1); + g.drawString(diffDays,this.x+10,this.y+7); } - - function resetSettings() { - settings = { - day : 17, - month : 6, - year: 2020 - }; - updateSettings(); + //large if number has 3 digits (positive number) + if (diffDays <= 999 && diffDays >= 100) { + g.setFont("6x8", 2); + g.drawString(diffDays,this.x,this.y+4); } + //large if number has 2 digits (positive number) + if (diffDays <= 99 && diffDays >= 10) { + g.setFont("6x8", 2); + g.drawString(diffDays,this.x+6,this.y+4); + } + //large if number has 1 digit (positive number) + if (diffDays <= 9 && diffDays >= 0) { + g.setFont("6x8", 2); + g.drawString(diffDays,this.x+13,this.y+4); + } + //large if number has 1 digit (negative number) + if (diffDays <= -1 && diffDays >= -9) { + g.setFont("6x8", 2); + g.drawString(diffDays,this.x+5,this.y+4); + } + //large if number has 2 digits (negative number) + if (diffDays <= -10 && diffDays >= -99) { + g.setFont("6x8", 2); + g.drawString(diffDays,this.x,this.y+4); + } + //large if number has 3 digits or more (negative number) + if (diffDays <= -100) { + g.setFont("6x8", 1); + g.drawString(diffDays,this.x,this.y+7); + } +} - settings = storage.readJSON('daysleft.json',1); - if (!settings) resetSettings(); - - var dd = settings.day, - mm = settings.month-1, //month is zero-based - yy = settings.year; - - const oneDay = 24 * 60 * 60 * 1000; // hours*minutes*seconds*milliseconds - const targetDate = new Date(yy, mm, dd); - const today = new Date(); - - //create date object with today, but 00:00:00 - const currentYear = today.getFullYear(); - const currentMonth = today.getMonth(); - const currentDay = today.getDate(); - const todayMorning = new Date (currentYear, currentMonth, currentDay, 0, 0, 0); - - const diffDays = (targetDate - todayMorning) / oneDay; - -WIDGETS["daysl"]={area:"tl",width:40,draw:function(){ - g.setFont("6x8", 1); - g.drawString(diffDays,this.x+12,this.y+12); -}}; \ No newline at end of file +//draw widget +WIDGETS["daysl"]={area:"tl",width:width,draw:drawWidget}; \ No newline at end of file diff --git a/apps/gbridge/ChangeLog b/apps/gbridge/ChangeLog index 0bcf94e25..e02ef176d 100644 --- a/apps/gbridge/ChangeLog +++ b/apps/gbridge/ChangeLog @@ -5,3 +5,4 @@ 0.05: Show incoming call notification Optimize animation, limit title length 0.06: Gadgetbridge App 'Connected' state is no longer toggleable +0.07: Move configuration to settings menu diff --git a/apps/gbridge/app.js b/apps/gbridge/app.js deleted file mode 100644 index d12f0f768..000000000 --- a/apps/gbridge/app.js +++ /dev/null @@ -1,19 +0,0 @@ -function gb(j) { - Bluetooth.println(JSON.stringify(j)); -} - -var mainmenu = { - "" : { "title" : "Gadgetbridge" }, - "Connected" : { value : NRF.getSecurityStatus().connected?"Yes":"No" }, - "Find Phone" : function() { E.showMenu(findPhone); }, - "Exit" : ()=> {load();}, -}; - -var findPhone = { - "" : { "title" : "-- Find Phone --" }, - "On" : _=>gb({t:"findPhone",n:true}), - "Off" : _=>gb({t:"findPhone",n:false}), - "< Back" : function() { E.showMenu(mainmenu); }, -}; - -E.showMenu(mainmenu); diff --git a/apps/gbridge/settings.js b/apps/gbridge/settings.js new file mode 100644 index 000000000..723c9cae9 --- /dev/null +++ b/apps/gbridge/settings.js @@ -0,0 +1,21 @@ +(function(back) { + function gb(j) { + Bluetooth.println(JSON.stringify(j)); + } + + var mainmenu = { + "" : { "title" : "Gadgetbridge" }, + "Connected" : { value : NRF.getSecurityStatus().connected?"Yes":"No" }, + "Find Phone" : function() { E.showMenu(findPhone); }, + "< Back" : back, + }; + + var findPhone = { + "" : { "title" : "-- Find Phone --" }, + "On" : _=>gb({t:"findPhone",n:true}), + "Off" : _=>gb({t:"findPhone",n:false}), + "< Back" : function() { E.showMenu(mainmenu); }, + }; + + E.showMenu(mainmenu); +}) diff --git a/apps/minionclk/ChangeLog b/apps/minionclk/ChangeLog new file mode 100755 index 000000000..7b83706bf --- /dev/null +++ b/apps/minionclk/ChangeLog @@ -0,0 +1 @@ +0.01: First release diff --git a/apps/minionclk/app-icon.js b/apps/minionclk/app-icon.js new file mode 100755 index 000000000..f78fb9e35 --- /dev/null +++ b/apps/minionclk/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwhAaXlZEolVVvOj0mq1XOv9/qtWFb8rquj1XV5wBDAA2jvMqNLMqwAsCABBhBAIujqpbWvIsCLowBHMg0qFyVVFQJNDK4YoEAYxjFvIuQwHP6ur5+rDgfU5wBDMI2qCYOsC4XV0bFOwIWBAAeBAIOrMYYsF54sCCIWswQDB52AGBcrFIOtF4gAEMoJfDAAOrCYQuDAIowKFwQWIBIeA52pGQPWLYgAFA4YDBGA4uDEwQYFGQvPL4IuKC4wwHFglWAIQYIYoQuFCYwDGqrqFF4YYCYBKeHHwgRLlZhCLowMBKIIubWAtWF4PXBQRdEBAIBHGAoTRCIRfCDQQvBLofXB4NVBIQcDFweAOYdWp9WqwrDGA8ABoRJFAAOswFOqtUwBNFeQYVCwMqp4ABqwbDCowvCAgKOGf4N5aAIwKKIVPpwRBGQOBFw5fCIgZfGwFVlT/BqtVBQQwFUAVWFwMAlYvCL6mBqkqDgNUF4RLGAgVPFwMAklPwD0GX5gOCXoReBJgSvDIwWrAoN6py/Cp58DCYxQBVIYwHqyQBbgL6EX4qQDFwRdFLAifBaoQaEAJIuDCYWrEgoTIGAerWIJfHGRZdECZ5fD0bQBIwJgEIoynEGBJxIAAYPBwHNlUq6owBMIZ4OMKQPC0eiqsr53PX4guOwBhOComk0ejqpfB53OJZAeDU4lVvN5OQQXKBIeA0RfBvNV5wwBMI4uD1oLDFoN4AQJEMBYWl0fOL4NUL4QwCPw4BEwN4AAejvGACZaMBLoRfGAIWr1Z8HwGkFQRIBDgekwAVGFoOAB4QTDqgvBFwQDD1Wk1el1YsBv4oDAIoAB0d/GQOlAIV/CpF51ZfFAIgAEUoOiAJYmEAAoHDvOBFxWqAIWpFxwnDOJABBquJ1QwM5oDCMYYDDAIN/5+r6CABHRKOBmVewIwCYY4sC0bGB678B1ekZYIAB0ulwOlvWkIQLJCMY0yq2sr2sMJYABp96vQnB0tPz4FBBANzAAWj5pdI0dWq0zr2Ir2rMJKQCvNIp9PudIuYDBAIV0FwSMFL4d/LoMzL4WBwIwD6hhH5ujuZXBFAIDDAIOdFwPNL4hdCwBdCq15AgMrAQLDB52pRYYACBIWjvGdK4NJAQOdvIlB5oXB0QwBLoWjqsrAAaSCGANVGAJHBSQjBEAAINBewIDCLYIBCAAJfDv5dCLAIvBvIEDAAJoBwGqDQSUBY4htDBwIBBAoIwDL4WjvNdhAvCAAYFGhDGCY4IAB1QABFwQwDv4BB1V/0eA0mALINdP4IpGMYcAAIQABGAIxDAAIFBruCroAGq1eFALkDmdeD4IjDGQYCCBQYDBCgIqBAJAoBAQIDCmeBCoIDCGgIfBLooADL4YBCJAIiCAANdAIQoCAI4ABAYdWKQQDDMooEBlVPBwJfCGAwABFxABDSAZYGLo1OvFOIgQaBLYZhFAIsyFgYAEFAUqpxUBFYQ=")) \ No newline at end of file diff --git a/apps/minionclk/app.js b/apps/minionclk/app.js new file mode 100755 index 000000000..88fe446ae --- /dev/null +++ b/apps/minionclk/app.js @@ -0,0 +1,68 @@ +const bob = require("heatshrink").decompress(atob("nk8hAaXlYLWAEsqvN/0gBBql5lQ2tquj1XV5wBJ52j0hACPsdP1QsBAQQAGBIIBF51/P8OkN5R1GIxF5HLmAFgoDLPZfOpzmZ6vPFwomCPaA6DAYOjeq2A1YyCdI4HGQJQ8F1T2SJ4Oq1XW1es1mtAQOrPoPUAIh3J54ZHIAR5S62s64cBwIBGQIOqHQK4HKQYVDAAIFC1g+BHh9VHAQAFDwQDDHoJ5E54BB6AaBKQ5YGqo6MwJzGHQ4BDeIj/BR4JxDABY8BvI6OOYgaEHwZADHgQ6BZA42GAIusPJNW64eFqzJDlcrERA8BHQI2FqwaBDYYGBPI45GCoIgCLoVWQ5NWXA2rKhaiGLAwOGEAmADxJPDVA51ElQaMC4ouEWALdEHRg8Dc4woCDJo8EAIYxCHQQIFHiwaRegJ5EcYWsHgbrKbBA8GDSrNDO4wfRKgR3FDSh3CN4UrdwZbSHYZ5DHajMFHYQGCHalWO4jtQDQwABwAGCAAQfTKoK0EHahwCeARdFHakASIZWVZ4Q8CO4YgWO4QbCO6hWGEIKYZKzZ3DLog7UG4I6C1lWDSdWO4bpCO4bwUwKYEHajMDwOAlUkLojTUd4gaTZoRWC0YIB1eJLqo4EWiqRE0mjlcr1QkEeKFWOooBCHiB2CC4WA5wzB52rEQgfPHQwABAYJXOHQ2iO4XO6omFEJh1BEAgBGPJlWDIQbC0ej50qgHV1XPEwohKcwRbEvJ3EBQTrLFomkOwOjlR3C5w8GMAR8ClYuBLIgOCvN4HgIZFDQYbBlaOBR4YNCwA5B0XOpzvBHgWqTw4AFxB1EvQ6BAAI8GDZILEdgQBCqp3DPIRfIEQwABvJ1CvGkvGiwA6IAYoBCv6wCAAVOlQ6DAIWkL5ABEwN40Z1CAYJfCv7zEAJNWOYZ3KAIWq5yYHFYLOBLIrVCAIh7BOIzpECoYDDpw7BHAQDG1WqwGkAIN/CwIABLY4LDAAZ9BwAABvLCBC5IBBvOrO44BEAAmjAIPN0XOAJyHIAAgPEquBHBi4BAId+HwWiHxqJDRpYBDq2I1R3LIAQ6BAAOpPogABGgIDDOomk0nP5pIGd42Aq1ewI8CeZI6CHAJhCEAIDCdIo2B0er1esAQIZBC4Z9JlVW1leeKGp0es5+s6+s6ABB0oFBAIervWr0ulub7OqtWmdexJ4BGxB5G0V6pF6wItB0t6p9PvQABvINBudJudPzwXCHQwBDlY6BO4WIwPPPJbvDvA0BFgNzAAIDGugGCzrtHdYh1DAIOBrzxBPIgAIeIXNHgoBCGwYADzoVB0fNOpMyHQdW5+Bro7BHgQAB6jxJAAOjOYhxCAIukOoPN5ujdZFWqyyD0d6AwUrquBwAuB1I1FHgRJBMoQ9BWg1zzw0BDYI6B0R3DAAJ1BvMyp8rAAV4IQWBIodewAeCHAZ5IAAJoBAAXHHAJWDO4TtEdYQvEHgejAwIKClcqIQRdDXYbzFeoQBIGwIDDOot/VgQ6FAAIGBlgBCAAMzmZPBF4LzDACB1FAAOi1WjvFVr0zGYQ7GAAMAAYpPBwNeAIOIwOrfYOA1eA1WkAIWjAIekv4PBwCVBruBq5eBEYIABlcBF4wCEHw55CHwQABIQQBBABkzAILlCHQR1CFYavEPgsAAAIDEDQNdAwQAaHQNWEwQ0DHAh3KleBLoI7dHQKuFWQo0EAIsISoKdBHbyyHNgwADlVVpwEBDANWro7fd4Q6HO495vF5QgIYCd75eBHYUINAN5lQ3EA")); + +const locale = require("locale"); + +const black = 0x0000; +const white = 0xFFFF; + +let hour; +let minute; +let date; + +function draw() { + const d = new Date(); + + const newHour = ('0' + d.getHours()).substr(-2); + const newMinute = ('0' + d.getMinutes()).substr(-2); + const newDate = locale.date(d).trim(); + + g.setFontAlign(0, 0, 0); + + if (newHour !== hour) { + g.setFontVector(48); + g.setColor(black); + g.drawString(hour, 64, 92); + g.setColor(white); + g.drawString(newHour, 64, 92); + hour = newHour; + } + + if (newMinute !== minute) { + g.setFontVector(48); + g.setColor(black); + g.drawString(minute, 172, 92); + g.setColor(white); + g.drawString(newMinute, 172, 92); + minute = newMinute; + } + + if (newDate !== date) { + g.setFontVector(12); + g.setColor(black); + g.drawString(date, 120, 228); + g.setColor(0xFFFF); + g.drawString(newDate, 120, 228); + date = newDate; + } +} + +function drawAll() { + hour = ''; + minute = ''; + date = ''; + g.drawImage(bob, 0, 0, { scale: 4 }); + draw(); +} + +Bangle.on('lcdPower', function(on) { + if (on) { + drawAll(); + } +}); + +Bangle.loadWidgets(); +Bangle.drawWidgets(); +setInterval(draw, 1000); +drawAll(); + +setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"}); diff --git a/apps/minionclk/minionclk.png b/apps/minionclk/minionclk.png new file mode 100755 index 000000000..77cac31df Binary files /dev/null and b/apps/minionclk/minionclk.png differ diff --git a/apps/setting/ChangeLog b/apps/setting/ChangeLog index 0cfa04bf0..73bbc7bd1 100644 --- a/apps/setting/ChangeLog +++ b/apps/setting/ChangeLog @@ -5,3 +5,6 @@ 0.06: Remove distance setting as there's a separate app for Locale now 0.07: Added vibrate as beep workaround 0.08: Add support for app/widget settings +0.09: Move Welcome into App/widget settings +0.10: Added LCD wake-up settings + Adds LCD brightness setting diff --git a/apps/setting/boot.js b/apps/setting/boot.js new file mode 100644 index 000000000..8bf9df50d --- /dev/null +++ b/apps/setting/boot.js @@ -0,0 +1,6 @@ +(() => { + var settings = require('Storage').readJSON('setting.json', true); + if (settings != undefined) { + Bangle.setOptions(settings.options); + } +})() diff --git a/apps/setting/settings-default.json b/apps/setting/settings-default.json index 0800593cb..c61fd6109 100644 --- a/apps/setting/settings-default.json +++ b/apps/setting/settings-default.json @@ -10,4 +10,16 @@ clock: null, // a string for the default clock's name "12hour" : false, // 12 or 24 hour clock? // welcomed : undefined/true (whether welcome app should show) + brightness: 1, // LCD brightness from 0 to 1 + options: { + wakeOnBTN1: true, + wakeOnBTN2: true, + wakeOnBTN3: true, + wakeOnFaceUp: false, + wakeOnTouch: false, + wakeOnTwist: true, + twistThreshold: 819.2, + twistMaxY: -800, + twistTimeout: 1000 + } } diff --git a/apps/setting/settings.js b/apps/setting/settings.js index dbb03555c..cbd856ec5 100644 --- a/apps/setting/settings.js +++ b/apps/setting/settings.js @@ -9,6 +9,21 @@ function updateSettings() { storage.write('setting.json', settings); } +function updateOptions() { + updateSettings(); + Bangle.setOptions(settings.options) +} + +function gToInternal(g) { + // converts g to Espruino internal unit + return g * 8192; +} + +function internalToG(u) { + // converts Espruino internal unit to g + return u / 8192 +} + function resetSettings() { settings = { ble: true, // Bluetooth enabled by default @@ -18,22 +33,34 @@ function resetSettings() { vibrate: true, // Vibration enabled by default. App must support beep: "vib", // Beep enabled by default. App must support timezone: 0, // Set the timezone for the device - HID : false, // BLE HID mode, off by default + HID: false, // BLE HID mode, off by default clock: null, // a string for the default clock's name "12hour" : false, // 12 or 24 hour clock? + brightness: 1, // LCD brightness from 0 to 1 // welcomed : undefined/true (whether welcome app should show) + options: { + wakeOnBTN1: true, + wakeOnBTN2: true, + wakeOnBTN3: true, + wakeOnFaceUp: false, + wakeOnTouch: false, + wakeOnTwist: true, + twistThreshold: 819.2, + twistMaxY: -800, + twistTimeout: 1000 + } }; updateSettings(); } -settings = storage.readJSON('setting.json',1); +settings = storage.readJSON('setting.json', 1); if (!settings) resetSettings(); const boolFormat = v => v ? "On" : "Off"; function showMainMenu() { - var beepV = [ false,true,"vib" ]; - var beepN = [ "Off","Piezo","Vibrate" ]; + var beepV = [false, true, "vib"]; + var beepN = ["Off", "Piezo", "Vibrate"]; const mainmenu = { '': { 'title': 'Settings' }, 'Make Connectable': makeConnectable, @@ -72,14 +99,25 @@ function showMainMenu() { Bangle.setLCDTimeout(settings.timeout); } }, + 'LCD Brightness': { + value: settings.brightness, + min: 0, + max: 1, + step: 0.1, + onchange: v => { + settings.brightness = v || 1; + updateSettings(); + Bangle.setLCDBrightness(settings.brightness); + } + }, 'Beep': { - value: 0|beepV.indexOf(settings.beep), - min:0,max:2, - format: v=>beepN[v], + value: 0 | beepV.indexOf(settings.beep), + min: 0, max: 2, + format: v => beepN[v], onchange: v => { settings.beep = beepV[v]; - if (v==1) { analogWrite(D18,0.5,{freq:2000});setTimeout(()=>D18.reset(),200) } // piezo - else if (v==2) { analogWrite(D13,0.1,{freq:2000});setTimeout(()=>D13.reset(),200) } // vibrate + if (v==1) { analogWrite(D18,0.5,{freq:2000});setTimeout(()=>D18.reset(),200); } // piezo + else if (v==2) { analogWrite(D13,0.1,{freq:2000});setTimeout(()=>D13.reset(),200); } // vibrate updateSettings(); } }, @@ -91,18 +129,10 @@ function showMainMenu() { updateSettings(); if (settings.vibrate) { VIBRATE.write(1); - setTimeout(()=>VIBRATE.write(0), 10); + setTimeout(() => VIBRATE.write(0), 10); } } }, - 'Welcome App': { - value: !settings.welcomed, - format: boolFormat, - onchange: v => { - settings.welcomed = v?undefined:true; - updateSettings(); - } - }, 'Locale': showLocaleMenu, 'Select Clock': showClockMenu, 'HID': { @@ -114,14 +144,101 @@ function showMainMenu() { } }, 'Set Time': showSetTimeMenu, + 'LCD Wake-Up': showWakeUpMenu, 'App/widget settings': showAppSettingsMenu, 'Reset Settings': showResetMenu, 'Turn Off': Bangle.off, - '< Back': ()=> {load();} + '< Back': () => { load(); } }; return E.showMenu(mainmenu); } +function showWakeUpMenu() { + const wakeUpMenu = { + '': { 'title': 'LCD Wake-Up' }, + '< Back': showMainMenu, + 'Wake On BTN1': { + value: settings.options.wakeOnBTN1, + format: boolFormat, + onchange: () => { + settings.options.wakeOnBTN1 = !settings.options.wakeOnBTN1; + updateOptions(); + } + }, + 'Wake On BTN2': { + value: settings.options.wakeOnBTN2, + format: boolFormat, + onchange: () => { + settings.options.wakeOnBTN2 = !settings.options.wakeOnBTN2; + updateOptions(); + } + }, + 'Wake On BTN3': { + value: settings.options.wakeOnBTN3, + format: boolFormat, + onchange: () => { + settings.options.wakeOnBTN3 = !settings.options.wakeOnBTN3; + updateOptions(); + } + }, + 'Wake on FaceUp': { + value: settings.options.wakeOnFaceUp, + format: boolFormat, + onchange: () => { + settings.options.wakeOnFaceUp = !settings.options.wakeOnFaceUp; + updateOptions(); + } + }, + 'Wake on Touch': { + value: settings.options.wakeOnTouch, + format: boolFormat, + onchange: () => { + settings.options.wakeOnTouch = !settings.options.wakeOnTouch; + updateOptions(); + } + }, + 'Wake On Twist': { + value: settings.options.wakeOnTwist, + format: boolFormat, + onchange: () => { + settings.options.wakeOnTwist = !settings.options.wakeOnTwist; + updateOptions(); + } + }, + 'Twist Threshold': { + value: internalToG(settings.options.twistThreshold), + min: -0.5, + max: 0.5, + step: 0.01, + onchange: v => { + settings.options.twistThreshold = gToInternal(v || 0.1); + updateOptions(); + } + }, + 'Twist Max Y': { + value: settings.options.twistMaxY, + min: -1500, + max: 1500, + step: 100, + onchange: v => { + settings.options.twistMaxY = v || -800; + updateOptions(); + } + }, + 'Twist Timeout': { + value: settings.options.twistTimeout, + min: 0, + max: 2000, + step: 100, + onchange: v => { + settings.options.twistTimeout = v || 1000; + updateOptions(); + } + } + } + return E.showMenu(wakeUpMenu) +} + function showLocaleMenu() { const localemenu = { '': { 'title': 'Locale' }, @@ -138,7 +255,7 @@ function showLocaleMenu() { }, 'Clock Style': { value: !!settings["12hour"], - format : v => v?"12hr":"24hr", + format: v => v ? "12hr" : "24hr", onchange: v => { settings["12hour"] = v; updateSettings(); @@ -166,33 +283,33 @@ function showResetMenu() { } function makeConnectable() { - try { NRF.wake(); } catch(e) {} + try { NRF.wake(); } catch (e) { } Bluetooth.setConsole(1); - var name="Bangle.js "+NRF.getAddress().substr(-5).replace(":",""); - E.showPrompt(name+"\nStay Connectable?",{title:"Connectable"}).then(r=>{ - if (settings.ble!=r) { + var name = "Bangle.js " + NRF.getAddress().substr(-5).replace(":", ""); + E.showPrompt(name + "\nStay Connectable?", { title: "Connectable" }).then(r => { + if (settings.ble != r) { settings.ble = r; updateSettings(); } - if (!r) try { NRF.sleep(); } catch(e) {} + if (!r) try { NRF.sleep(); } catch (e) { } showMainMenu(); }); } function showClockMenu() { - var clockApps = require("Storage").list(/\.info$/).map(app=>{ + var clockApps = require("Storage").list(/\.info$/).map(app => { try { return require("Storage").readJSON(app); } - catch (e) {} - }).filter(app=>app.type=="clock").sort((a, b) => a.sortorder - b.sortorder); + catch (e) { } + }).filter(app => app.type == "clock").sort((a, b) => a.sortorder - b.sortorder); const clockMenu = { '': { 'title': 'Select Clock', }, '< Back': showMainMenu, }; - clockApps.forEach((app,index) => { + clockApps.forEach((app, index) => { var label = app.name; if ((!settings.clock && index === 0) || (settings.clock === app.src)) { - label = "* "+label; + label = "* " + label; } clockMenu[label] = () => { if (settings.clock !== app.src) { @@ -203,7 +320,7 @@ function showClockMenu() { }; }); if (clockApps.length === 0) { - clockMenu["No Clocks Found"] = () => {}; + clockMenu["No Clocks Found"] = () => { }; } return E.showMenu(clockMenu); } @@ -215,7 +332,7 @@ function showSetTimeMenu() { const timemenu = { '': { 'title': 'Set Time', - 'predraw': function() { + 'predraw': function () { d = new Date(); timemenu.Hour.value = d.getHours(); timemenu.Minute.value = d.getMinutes(); @@ -234,7 +351,7 @@ function showSetTimeMenu() { onchange: v => { d = new Date(); d.setHours(v); - setTime(d.getTime()/1000); + setTime(d.getTime() / 1000); } }, 'Minute': { @@ -245,7 +362,7 @@ function showSetTimeMenu() { onchange: v => { d = new Date(); d.setMinutes(v); - setTime(d.getTime()/1000); + setTime(d.getTime() / 1000); } }, 'Second': { @@ -256,7 +373,7 @@ function showSetTimeMenu() { onchange: v => { d = new Date(); d.setSeconds(v); - setTime(d.getTime()/1000); + setTime(d.getTime() / 1000); } }, 'Date': { @@ -267,7 +384,7 @@ function showSetTimeMenu() { onchange: v => { d = new Date(); d.setDate(v); - setTime(d.getTime()/1000); + setTime(d.getTime() / 1000); } }, 'Month': { @@ -278,7 +395,7 @@ function showSetTimeMenu() { onchange: v => { d = new Date(); d.setMonth(v - 1); - setTime(d.getTime()/1000); + setTime(d.getTime() / 1000); } }, 'Year': { @@ -289,16 +406,16 @@ function showSetTimeMenu() { onchange: v => { d = new Date(); d.setFullYear(v); - setTime(d.getTime()/1000); + setTime(d.getTime() / 1000); } } }; return E.showMenu(timemenu); } -function showAppSettingsMenu(){ +function showAppSettingsMenu() { let appmenu = { - '': {'title': 'App Settings'}, + '': { 'title': 'App Settings' }, '< Back': showMainMenu, } const apps = storage.list(/\.info$/) @@ -306,10 +423,10 @@ function showAppSettingsMenu(){ .filter(app => app && app.settings) .sort((a, b) => a.sortorder - b.sortorder) if (apps.length === 0) { - appmenu['No app has settings'] = () => {}; + appmenu['No app has settings'] = () => { }; } apps.forEach(function (app) { - appmenu[app.name] = () => {showAppSettings(app)}; + appmenu[app.name] = () => { showAppSettings(app) }; }) E.showMenu(appmenu) } diff --git a/apps/toucher/ChangeLog b/apps/toucher/ChangeLog index bd3d5d225..c536d1a5b 100644 --- a/apps/toucher/ChangeLog +++ b/apps/toucher/ChangeLog @@ -1,2 +1,4 @@ 0.01: New App! -0.02: Add swipe support and doucle tap to run application \ No newline at end of file +0.02: Add swipe support and doucle tap to run application +0.03: Close launcher when lcd turn off +0.04: Complete rewrite to add animation and loop ( issue #210 ) \ No newline at end of file diff --git a/apps/toucher/app.js b/apps/toucher/app.js index 2b80198c9..5c3703129 100644 --- a/apps/toucher/app.js +++ b/apps/toucher/app.js @@ -1,4 +1,6 @@ +Bangle.setLCDMode("120x120"); g.clear(); +g.flip(); const Storage = require("Storage"); @@ -14,99 +16,144 @@ function getApps(){ }); } -const selected = 0; -const apps = getApps(); +const HEIGHT = g.getHeight(); +const WIDTH = g.getWidth(); +const HALF = WIDTH/2; +const ANIMATION_FRAME = 3; +const ANIMATION_STEP = HALF / ANIMATION_FRAME; -function prev(){ - if (selected>=0) { - selected--; - } - drawMenu(); +function getPosition(index){ + return (index*HALF); } -function next() { - if (selected+1 { + const x = getPosition(i) + HALF - offset; + const y = HALF - (HALF*0.3);//-(HALF*0.7); + let diff = (x - HALF); + if(diff < 0) diff *=-1; + let size = 30; + if((diff*0.5) < size) size -= (diff*0.5); + else size = 0; + + const scale = size / 30; + if(size){ + let c = size / 30 * 2; + c = c -1; + if(c < 0) c = 0; + + if(app.back){ + g.setFont('6x8', 1); + g.setFontAlign(0, -1); + g.setColor(c,c,c); + g.drawString('Back', HALF, HALF); + return; + } + // icon + const icon = app.icon ? Storage.read(app.icon) : null; + if(icon){ + try { + g.drawImage(icon, x-(scale*24), y-(scale*24), { scale: scale }); + } catch(e){ + noIcon(x, y, size); + } + }else{ + noIcon(x, y, size); + } + //text + g.setFont('6x8', 1); + g.setFontAlign(0, -1); + g.setColor(c,c,c); + g.drawString(app.name, HALF, HEIGHT - (HALF*0.7)); + + const type = app.type ? app.type : 'App'; + const version = app.version ? app.version : '0.00'; + const info = type+' v'+version; + g.setFontAlign(0,1); + g.setFont('4x6', 0.25); + g.setColor(c,c,c); + g.drawString(info, HALF, 110, { scale: scale }); + } + }); +} + +function draw(ignoreLoop){ + g.clear(); + drawIcons(slideOffset); + g.flip(); + if(slideOffset == target) return; + if(slideOffset < target) slideOffset+= ANIMATION_STEP; + else if(slideOffset > target) slideOffset -= ANIMATION_STEP; + if(!ignoreLoop) draw(); +} + +function animateTo(index){ + target = getPosition(index); + draw(); +} +function goTo(index){ + current_app = index; + target = getPosition(index); + slideOffset = target; + draw(true); +} + +goTo(1); + +function prev(){ + if(current_app == 0) goTo(apps.length-1); + current_app -= 1; + if(current_app < 0) current_app = 0; + animateTo(current_app); +} + +function next(){ + if(current_app == apps.length-1) goTo(0); + current_app += 1; + if(current_app > apps.length-1) current_app = apps.length-1; + animateTo(current_app); } function run() { - if(selected < 0) return load(); - if (!apps[selected].src) return; - if (Storage.read(apps[selected].src)===undefined) { + const app = apps[current_app]; + if(app.back) return load(); + if (Storage.read(app.src)===undefined) { E.showMessage("App Source\nNot found"); - setTimeout(drawMenu, 2000); + setTimeout(draw, 2000); } else { - E.showMessage("Loading..."); - load(apps[selected].src); - } -} - -function getCurrentApp(){ - return apps[selected]; -} - -function getNextApp(){ - return apps[selected+1]; -} - -function drawFallbackIcon(){ - g.setColor(1,1,1); - g.fillRect(72, 40, 168, 136); - g.setColor(0,0,0); - g.setFont('6x8', 8); - g.drawString('?', 124, 88); -} - -function drawArrow(x, y, size, dir){ - size = size || 10; - dir = dir || 1; - g.moveTo(x, y).lineTo(x+(size*dir), y-size).lineTo(x+(size*dir),y+size).lineTo(x, y); -} - -function drawMenu(){ - - if(selected < 0){ + Bangle.setLCDMode(); g.clear(); - g.setFontAlign(0,0); - g.setFont('6x8', 2); - g.drawString('Back', 120, 120); - drawArrow(220, 120, 10, -1); - return; + g.flip(); + E.showMessage("Loading..."); + load(app.src); } - - const app = getCurrentApp(); - g.clear(); - g.setFontAlign(0,0); - g.setFont('6x8', 2); - if(!app) return g.drawString('???', 120, 120); - g.drawString(app.name, 120, 160); - if (app.icon) icon = Storage.read(app.icon); - if (icon) try {g.drawImage(icon, 120-48, 40, { scale: 2 });} catch(e){ drawFallbackIcon(); } - else drawFallbackIcon(); - - g.setFont('6x8', 1); - - const type = app.type ? app.type : 'App'; - const version = app.version ? app.version : '0.00'; - const info = type+' v'+version; - g.setFontAlign(-1,1); - g.drawString(info, 20, 220); - - const count = (selected+1)+'/'+apps.length; - g.setFontAlign(1,1); - g.drawString(count, 220, 220); - - drawArrow(20, 120, 10, 1); - if(getNextApp()) drawArrow(220, 120, 10, -1); } -drawMenu(); -// Physical buttons -setWatch(prev, BTN1, {repeat:true}); -setWatch(next, BTN3, {repeat:true}); +setWatch(prev, BTN1, { repeat: true }); +setWatch(next, BTN3, { repeat: true }); setWatch(run, BTN2, {repeat:true,edge:"falling"}); // Screen event @@ -127,4 +174,9 @@ Bangle.on('touch', function(button){ Bangle.on('swipe', dir => { if(dir == 1) prev(); else next(); +}); + +// close launcher when lcd is off +Bangle.on('lcdPower', on => { + if(!on) return load(); }); \ No newline at end of file diff --git a/apps/welcome/ChangeLog b/apps/welcome/ChangeLog index d8d647138..34f6e3a82 100644 --- a/apps/welcome/ChangeLog +++ b/apps/welcome/ChangeLog @@ -2,3 +2,4 @@ 0.02: Animate balloon intro 0.03: BTN3 now won't restart when at the end 0.04: Fix regression after tweaks to Storage.readJSON +0.05: Move configuration into App/widget settings diff --git a/apps/welcome/settings.js b/apps/welcome/settings.js new file mode 100644 index 000000000..2fbd585c6 --- /dev/null +++ b/apps/welcome/settings.js @@ -0,0 +1,16 @@ +// The welcome app is special, and gets to use global settings +(function(back) { + let settings = require('Storage').readJSON('setting.json', 1) || {} + E.showMenu({ + '': { 'title': 'Welcome App' }, + 'Run again': { + value: !settings.welcomed, + format: v => v ? 'Yes' : 'No', + onchange: v => { + settings.welcomed = v ? undefined : true + require('Storage').write('setting.json', settings) + }, + }, + '< Back': back, + }) +}) diff --git a/apps/widbatpc/ChangeLog b/apps/widbatpc/ChangeLog index 3988729c3..3627a86d3 100644 --- a/apps/widbatpc/ChangeLog +++ b/apps/widbatpc/ChangeLog @@ -3,3 +3,5 @@ 0.04: Ensure redrawing works with variable size widget system 0.05: Change color depending on battery level, cloned from widbat 0.06: Show battery percentage as text +0.07: Add settings: percentage/color/charger icon +0.08: Draw percentage as inverted on monochrome battery diff --git a/apps/widbatpc/settings.js b/apps/widbatpc/settings.js new file mode 100644 index 000000000..5c0bdbcae --- /dev/null +++ b/apps/widbatpc/settings.js @@ -0,0 +1,58 @@ +// This file should contain exactly one function, which shows the app's settings +/** + * @param {function} back Use back() to return to settings menu + */ +(function(back) { + const SETTINGS_FILE = 'widbatpc.settings.json' + const COLORS = ['By Level', 'Green', 'Monochrome'] + + // initialize with default settings... + let s = { + 'color': COLORS[0], + 'percentage': true, + 'charger': 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') + const saved = storage.readJSON(SETTINGS_FILE, 1) || {} + for (const key in saved) { + s[key] = saved[key] + } + + // creates a function to safe a specific setting, e.g. save('color')(1) + function save(key) { + return function (value) { + s[key] = value + storage.write(SETTINGS_FILE, s) + WIDGETS["batpc"].reload() + } + } + + const onOffFormat = b => (b ? 'on' : 'off') + const menu = { + '': { 'title': 'Battery Widget' }, + '< Back': back, + 'Percentage': { + value: s.percentage, + format: onOffFormat, + onchange: save('percentage'), + }, + 'Charging Icon': { + value: s.charger, + format: onOffFormat, + onchange: save('charger'), + }, + 'Color': { + format: () => s.color, + onchange: function () { + // cycles through options + const oldIndex = COLORS.indexOf(s.color) + const newIndex = (oldIndex + 1) % COLORS.length + s.color = COLORS[newIndex] + save('color')(s.color) + }, + }, + } + E.showMenu(menu) +}) diff --git a/apps/widbatpc/widget.js b/apps/widbatpc/widget.js index 7100dc111..9f88b5c49 100644 --- a/apps/widbatpc/widget.js +++ b/apps/widbatpc/widget.js @@ -1,20 +1,62 @@ (function(){ +const DEFAULTS = { + 'color': 'By Level', + 'percentage': true, + 'charger': true, +} +const COLORS = { + 'white': -1, + 'charging': 0x07E0, // "Green" + 'high': 0x05E0, // slightly darker green + 'ok': 0xFD20, // "Orange" + 'low':0xF800, // "Red" +} +const SETTINGS_FILE = 'widbatpc.settings.json' + +let settings +function loadSettings() { + settings = require('Storage').readJSON(SETTINGS_FILE, 1) || {} +} +function setting(key) { + if (!settings) { loadSettings() } + return (key in settings) ? settings[key] : DEFAULTS[key] +} + const levelColor = (l) => { - if (Bangle.isCharging()) return 0x07E0; // "Green" - if (l >= 50) return 0x05E0; // slightly darker green - if (l >= 15) return 0xFD20; // "Orange" - return 0xF800; // "Red" + // "charging" is very bright -> percentage is hard to read, "high" is ok(ish) + const green = setting('percentage') ? COLORS.high : COLORS.charging + switch (setting('color')) { + case 'Monochrome': return COLORS.white; // no chance of reading the percentage here :-( + case 'Green': return green; + case 'By Level': // fall through + default: + if (setting('charger')) { + // charger icon -> always make percentage readable + if (Bangle.isCharging() || l >= 50) return green; + } else { + // no icon -> brightest green to indicate charging, even when showing percentage + if (Bangle.isCharging()) return COLORS.charging; + if (l >= 50) return COLORS.high; + } + if (l >= 15) return COLORS.ok; + return COLORS.low; + } +} +const chargerColor = () => { + return (setting('color') === 'Monochrome') ? COLORS.white : COLORS.charging } function setWidth() { - WIDGETS["bat"].width = 40 + (Bangle.isCharging()?16:0); + WIDGETS["batpc"].width = 40; + if (Bangle.isCharging() && setting('charger')) { + WIDGETS["batpc"].width += 16; + } } function draw() { var s = 39; var x = this.x, y = this.y; - const l = E.getBattery(), c = levelColor(l); - if (Bangle.isCharging()) { - g.setColor(c).drawImage(atob( + if (Bangle.isCharging() && setting('charger')) { + g.setColor(chargerColor()).drawImage(atob( "DhgBHOBzgc4HOP////////////////////3/4HgB4AeAHgB4AeAHgB4AeAHg"),x,y); x+=16; } @@ -22,18 +64,40 @@ function draw() { g.fillRect(x,y+2,x+s-4,y+21); g.clearRect(x+2,y+4,x+s-6,y+19); g.fillRect(x+s-3,y+10,x+s,y+14); - g.setColor(c).fillRect(x+4,y+6,x+4+l*(s-12)/100,y+17); + const l = E.getBattery(), + c = levelColor(l); + const xl = x+4+l*(s-12)/100 + g.setColor(c).fillRect(x+4,y+6,xl,y+17); g.setColor(-1); - g.setFontAlign(-1,-1); + if (!setting('percentage')) { + return; + } + let gfx = g + if (setting('color') === 'Monochrome') { + // draw text inverted on battery level + gfx = Graphics.createCallback(240, 240, 1, + (x,y) => {g.setPixel(x,y,x<=xl?0:-1)}) + } + gfx.setFontAlign(-1,-1); if (l >= 100) { - g.setFont('4x6', 2); - g.drawString(l, x + 6, y + 7); + gfx.setFont('4x6', 2); + gfx.drawString(l, x + 6, y + 7); } else { if (l < 10) x+=6; - g.setFont('6x8', 2); - g.drawString(l, x + 6, y + 4); + gfx.setFont('6x8', 2); + gfx.drawString(l, x + 6, y + 4); } } +// reload widget, e.g. when settings have changed +function reload() { + loadSettings() + // need to redraw all widgets, because changing the "charger" setting + // can affect the width and mess with the whole widget layout + setWidth() + g.clear(); + Bangle.drawWidgets(); +} + Bangle.on('charging',function(charging) { if(charging) Bangle.buzz(); setWidth(); @@ -43,7 +107,7 @@ Bangle.on('charging',function(charging) { var batteryInterval; Bangle.on('lcdPower', function(on) { if (on) { - WIDGETS["bat"].draw(); + WIDGETS["batpc"].draw(); // refresh once a minute if LCD on if (!batteryInterval) batteryInterval = setInterval(draw, 60000); @@ -54,6 +118,6 @@ Bangle.on('lcdPower', function(on) { } } }); -WIDGETS["bat"]={area:"tr",width:40,draw:draw}; +WIDGETS["batpc"]={area:"tr",width:40,draw:draw,reload:reload}; setWidth(); })() diff --git a/apps/wohrm/ChangeLog b/apps/wohrm/ChangeLog new file mode 100644 index 000000000..f5c64dbee --- /dev/null +++ b/apps/wohrm/ChangeLog @@ -0,0 +1,6 @@ +0.01: Only tested on the emulator. +0.02: Adapted to new App code layout +0.03: Optimized rendering for the background +0.04: Only buzz on high confidence (>85%) +0.05: Improved buzz timing and rendering +0.06: Removed debug outputs, fixed rendering for upper limit, improved rendering for +/- icons, changelog version order fixed diff --git a/apps/wohrm/app-icon.js b/apps/wohrm/app-icon.js new file mode 100644 index 000000000..4a69b16bd --- /dev/null +++ b/apps/wohrm/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AH4AVgnd5tABI3c7oJGAAUs5gAC4gJDpgJD4QWGhoMDAAPQBJYADBgoABBJYAChgJD5oDC4AJEAAfAC4fcBIfUDYYJEEogWCgQJEoYSHAAsgIw3MmYqIn89JAoXFn5DH4f/+YXFWQnE/4GEAAXP///ZgooE4X/ngvMPAQXEBoIXHHIJfDC4ss5nf+f9OosjFwgXF5oTBp8z+gMBMQPTn5dBNIgXCAwPDEQM/mQmCJQNP/8zDIJRDO4SnB6fz7k/poXEJwIJBmanGhvMl//loxC7nE/jUCon/6gzBC4PQC4MDKIJFDn9M4YXB5nUKYbACmAXBgE/+YMBOoMvngXDJIKDB6YvBOwRgDaoINB788p5wDn7HELwQABghWCBoPD/s/YwNN5i+Bc4dAC4bBCC4fyPIPU+Z0BDAZGEJAffYgPC+ZxBG4KkB6f/C4JGEAAQsBcIX/+QEBCgP9A4IXBCwwwB5pxDPYJoDcgIuIGASJH5rvBAwIWIeYQABl5jBAAXDIwLrCABCcC76gDAoP0RgwAFYYJ7DJAcsFxYABaYJ7DAAXECxhJEAAgWOPQgACIpoADUwb1BCyBJERZgYKkAXUglACygA/AH4AFA==")) \ No newline at end of file diff --git a/apps/wohrm/app.js b/apps/wohrm/app.js new file mode 100644 index 000000000..7e0af4219 --- /dev/null +++ b/apps/wohrm/app.js @@ -0,0 +1,330 @@ +/* eslint-disable no-undef */ +const Setter = { + NONE: "none", + UPPER: 'upper', + LOWER: 'lower' +}; + +const shortBuzzTimeInMs = 80; +const longBuzzTimeInMs = 400; + +let upperLimit = 130; +let upperLimitChanged = true; + +let lowerLimit = 100; +let lowerLimitChanged = true; + +let limitSetter = Setter.NONE; + +let currentHeartRate = 0; +let hrConfidence = -1; +let hrChanged = true; +let confidenceChanged = true; + +let setterHighlightTimeout; + +function renderUpperLimitBackground() { + g.setColor(1,0,0); + g.fillRect(125,40, 210, 70); + g.fillRect(180,70, 210, 200); + + //Round top left corner + g.fillEllipse(115,40,135,70); + + //Round top right corner + g.setColor(0,0,0); + g.fillRect(205,40, 210, 45); + g.setColor(1,0,0); + g.fillEllipse(190,40,210,50); + + //Round inner corner + g.fillRect(174,71, 179, 76); + g.setColor(0,0,0); + g.fillEllipse(160,71,179,82); + + //Round bottom + g.setColor(1,0,0); + g.fillEllipse(180,190, 210, 210); +} + +function renderLowerLimitBackground() { + g.setColor(0,0,1); + g.fillRect(10, 180, 100, 210); + g.fillRect(10, 50, 40, 180); + + //Rounded top + g.setColor(0,0,1); + g.fillEllipse(10,40, 40, 60); + + //Round bottom right corner + g.setColor(0,0,1); + g.fillEllipse(90,180,110,210); + + //Round inner corner + g.setColor(0,0,1); + g.fillRect(40,175,45,180); + g.setColor(0,0,0); + g.fillEllipse(41,170,60,179); + + //Round bottom left corner + g.setColor(0,0,0); + g.fillRect(10,205, 15, 210); + g.setColor(0,0,1); + g.fillEllipse(10,200,30,210); +} + +function drawTrainingHeartRate() { + //Only redraw if the display is on + if (Bangle.isLCDOn()) { + renderUpperLimit(); + + renderCurrentHeartRate(); + + renderLowerLimit(); + + renderConfidenceBars(); + } + + buzz(); +} + +function renderUpperLimit() { + if(!upperLimitChanged) { return; } + + g.setColor(1,0,0); + g.fillRect(125,40, 210, 70); + + if(limitSetter === Setter.UPPER){ + g.setColor(255,255, 0); + } else { + g.setColor(255,255,255); + } + g.setFontVector(13); + g.drawString("Upper: " + upperLimit, 125, 50); + + upperLimitChanged = false; +} + +function renderCurrentHeartRate() { + if(!hrChanged) { return; } + + g.setColor(255,255,255); + g.fillRect(55, 110, 165, 150); + + g.setColor(0,0,0); + g.setFontVector(24); + g.setFontAlign(1, -1, 0); + g.drawString(currentHeartRate, 130, 117); + + //Reset alignment to defaults + g.setFontAlign(-1, -1, 0); + + hrChanged = false; +} + +function renderLowerLimit() { + if(!lowerLimitChanged) { return; } + + g.setColor(0,0,1); + g.fillRect(10, 180, 100, 210); + + if(limitSetter === Setter.LOWER){ + g.setColor(255,255, 0); + } else { + g.setColor(255,255,255); + } + g.setFontVector(13); + g.drawString("Lower: " + lowerLimit, 20,190); + + lowerLimitChanged = false; +} + +function renderConfidenceBars(){ + if(!confidenceChanged) { return; } + + if(hrConfidence >= 85){ + g.setColor(0, 255, 0); + } else if (hrConfidence >= 50) { + g.setColor(255, 255, 0); + } else if(hrConfidence >= 0){ + g.setColor(255, 0, 0); + } else { + g.setColor(255, 255, 255); + } + + g.fillRect(45, 110, 55, 150); + g.fillRect(165, 110, 175, 150); + + confidenceChanged = false; +} + +function renderPlusMinusIcons() { + if (limitSetter === Setter.NONE) { + g.setColor(0, 0, 0); + } else { + g.setColor(1, 1, 1); + } + + g.setFontVector(14); + + //+ for Btn1 + g.drawString("+", 222, 50); + + //- for Btn3 + g.drawString("-", 222,165); + + return; +} + +function renderHomeIcon() { + //Home for Btn2 + g.setColor(1, 1, 1); + g.drawLine(220, 118, 227, 110); + g.drawLine(227, 110, 234, 118); + + g.drawPoly([222,117,222,125,232,125,232,117], false); + g.drawRect(226,120,229,125); +} + +function buzz() { + // Do not buzz if not confident + if(hrConfidence < 85) { return; } + + if(currentHeartRate > upperLimit) + { + Bangle.buzz(shortBuzzTimeInMs); + setTimeout(() => { Bangle.buzz(shortBuzzTimeInMs); }, shortBuzzTimeInMs * 2); + } + + if(currentHeartRate < lowerLimit) + { + Bangle.buzz(longBuzzTimeInMs); + } +} + +function onHrm(hrm){ + if(currentHeartRate !== hrm.bpm){ + currentHeartRate = hrm.bpm; + hrChanged = true; + } + + if(hrConfidence !== hrm.confidence) { + hrConfidence = hrm.confidence; + confidenceChanged = true; + } +} + +function setLimitSetterToLower() { + resetHighlightTimeout(); + + limitSetter = Setter.LOWER; + + upperLimitChanged = true; + lowerLimitChanged = true; + + renderUpperLimit(); + renderLowerLimit(); + renderPlusMinusIcons(); +} + +function setLimitSetterToUpper() { + resetHighlightTimeout(); + + limitSetter = Setter.UPPER; + + upperLimitChanged = true; + lowerLimitChanged = true; + + renderLowerLimit(); + renderUpperLimit(); + renderPlusMinusIcons(); +} + +function setLimitSetterToNone() { + limitSetter = Setter.NONE; + + upperLimitChanged = true; + lowerLimitChanged = true; + + renderLowerLimit(); + renderUpperLimit(); + renderPlusMinusIcons(); +} + +function incrementLimit() { + resetHighlightTimeout(); + + if (limitSetter === Setter.UPPER) { + upperLimit++; + renderUpperLimit(); + upperLimitChanged = true; + } else if(limitSetter === Setter.LOWER) { + lowerLimit++; + renderLowerLimit(); + lowerLimitChanged = true; + } +} + +function decrementLimit(){ + resetHighlightTimeout(); + + if (limitSetter === Setter.UPPER) { + upperLimit--; + renderUpperLimit(); + upperLimitChanged = true; + } else if(limitSetter === Setter.LOWER) { + lowerLimit--; + renderLowerLimit(); + lowerLimitChanged = true; + } +} + +function resetHighlightTimeout() { + if (setterHighlightTimeout) { + clearTimeout(setterHighlightTimeout); + } + + setterHighlightTimeout = setTimeout(setLimitSetterToNone, 2000); +} + +// Show launcher when middle button pressed +function switchOffApp(){ + Bangle.setHRMPower(0); + Bangle.showLauncher(); +} + +// special function to handle display switch on +Bangle.on('lcdPower', (on) => { + g.clear(); + if (on) { + Bangle.drawWidgets(); + + renderHomeIcon(); + renderLowerLimitBackground(); + renderUpperLimitBackground(); + lowerLimitChanged = true; + upperLimitChanged = true; + drawTrainingHeartRate(); + } +}); + +Bangle.setHRMPower(1); +Bangle.on('HRM', onHrm); + +setWatch(incrementLimit, BTN1, {edge:"rising", debounce:50, repeat:true}); +setWatch(switchOffApp, BTN2, {edge:"rising", debounce:50, repeat:true}); +setWatch(decrementLimit, BTN3, {edge:"rising", debounce:50, repeat:true}); +setWatch(setLimitSetterToLower, BTN4, {edge:"rising", debounce:50, repeat:true}); +setWatch(setLimitSetterToUpper, BTN5, { edge: "rising", debounce: 50, repeat: true }); + +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +//drawTrainingHeartRate(); + +renderHomeIcon(); +renderLowerLimitBackground(); +renderUpperLimitBackground(); + +// refesh every sec +setInterval(drawTrainingHeartRate, 1000); diff --git a/apps/wohrm/app.png b/apps/wohrm/app.png new file mode 100644 index 000000000..8f9c0ea5d Binary files /dev/null and b/apps/wohrm/app.png differ diff --git a/browserconfig.xml b/browserconfig.xml new file mode 100644 index 000000000..13b6c7911 --- /dev/null +++ b/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #5755d9 + + + diff --git a/css/pwa.css b/css/pwa.css new file mode 100644 index 000000000..8e78581bf --- /dev/null +++ b/css/pwa.css @@ -0,0 +1,24 @@ +.hidden { + display: none !important; +} + +#installContainer { + position: absolute; + bottom: 1em; + display: flex; + justify-content: center; + width: 100%; +} + +#installContainer button { + background-color: inherit; + border: 1px solid white; + color: white; + font-size: 1em; + padding: 0.75em; +} + +.floating { + position: fixed; + +} diff --git a/favicon.ico b/favicon.ico index 24ae65966..8b736ee82 100644 Binary files a/favicon.ico and b/favicon.ico differ diff --git a/img/android-chrome-192x192.png b/img/android-chrome-192x192.png new file mode 100644 index 000000000..a4dff3bb5 Binary files /dev/null and b/img/android-chrome-192x192.png differ diff --git a/img/android-chrome-512x512.png b/img/android-chrome-512x512.png new file mode 100644 index 000000000..f89cbfb31 Binary files /dev/null and b/img/android-chrome-512x512.png differ diff --git a/img/apple-touch-icon.png b/img/apple-touch-icon.png new file mode 100644 index 000000000..2330e0fdf Binary files /dev/null and b/img/apple-touch-icon.png differ diff --git a/img/favicon-16x16.png b/img/favicon-16x16.png new file mode 100644 index 000000000..cb68aa50e Binary files /dev/null and b/img/favicon-16x16.png differ diff --git a/img/favicon-32x32.png b/img/favicon-32x32.png new file mode 100644 index 000000000..cc7b68d98 Binary files /dev/null and b/img/favicon-32x32.png differ diff --git a/img/mstile-150x150.png b/img/mstile-150x150.png new file mode 100644 index 000000000..015d36eae Binary files /dev/null and b/img/mstile-150x150.png differ diff --git a/img/safari-pinned-tab.svg b/img/safari-pinned-tab.svg new file mode 100644 index 000000000..10512424f --- /dev/null +++ b/img/safari-pinned-tab.svg @@ -0,0 +1,100 @@ + + + + +Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + + + + + + + diff --git a/index.html b/index.html index 0c528c5a7..f922c7556 100644 --- a/index.html +++ b/index.html @@ -6,6 +6,16 @@ + + + + + + + + + + Bangle.js App Loader