diff --git a/CHANGELOG.md b/CHANGELOG.md index a1cd3d803..6368c2c46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,3 +7,4 @@ Changed for individual apps are listed in `apps/appname/ChangeLog` * Added optional `README.md` file for apps * Remove 2v04 version warning, add links in About to official/developer versions * Fix issue removing an app that was just installed (Fix #253) +* Add `Favourite` functionality diff --git a/apps.json b/apps.json index dc9d47c2c..47618e1a8 100644 --- a/apps.json +++ b/apps.json @@ -119,7 +119,7 @@ { "id": "setting", "name": "Settings", "icon": "settings.png", - "version":"0.12", + "version":"0.13", "description": "A menu for setting up Bangle.js", "tags": "tool,system", "storage": [ @@ -915,7 +915,7 @@ { "id": "marioclock", "name": "Mario Clock", "icon": "marioclock.png", - "version":"0.09", + "version":"0.12", "description": "Animated retro Mario clock, with Gameboy style 8-bit grey-scale graphics.", "tags": "clock,mario,retro", "type": "clock", @@ -1108,6 +1108,20 @@ {"name":"openstmap.app.js","url":"app.js"}, {"name":"openstmap.img","url":"app-icon.js","evaluate":true} ] + }, + { "id": "activepedom", + "name": "Active Pedometer", + "shortName":"Active Pedometer", + "icon": "app.png", + "version":"0.01", + "description": "Pedometer that filters out arm movement and displays a step goal progress.", + "tags": "outdoors,widget", + "type":"widget", + "storage": [ + {"name":"activepedom.wid.js","url":"widget.js"}, + {"name":"activepedom.settings.js","url":"settings.js"}, + {"name":"activepedom.img","url":"app-icon.js","evaluate":true} + ] }, { "id": "tabata", "name": "Tabata", @@ -1148,9 +1162,9 @@ }, { "id": "batchart", "name": "Battery Chart", - "shortName":"BatChart", + "shortName":"Battery Chart", "icon": "app.png", - "version":"0.03", + "version":"0.05", "description": "A widget and an app for recording and visualizing battery percentage over time.", "tags": "app,widget,battery,time,record,chart,tool", "storage": [ @@ -1159,6 +1173,35 @@ {"name":"batchart.img","url":"app-icon.js","evaluate":true} ] }, + { "id": "nato", + "name": "NATO Alphabet", + "shortName" : "NATOAlphabet", + "icon": "nato.png", + "version":"0.01", + "type": "app", + "description": "Learn the NATO Phonetic alphabet plus some numbers.", + "tags": "app,learn,visual", + "allow_emulator":true, + "storage": [ + {"name":"nato.app.js","url":"nato.js"}, + {"name":"nato.img","url":"nato-icon.js","evaluate":true} + ] + }, + { "id": "numerals", + "name": "Numerals Clock", + "shortName": "Numerals Clock", + "icon": "numerals.png", + "version":"0.01", + "description": "A simple big numerals clock", + "tags": "numerals,clock", + "type":"clock", + "allow_emulator":true, + "storage": [ + {"name":"numerals.app.js","url":"numerals.app.js"}, + {"name":"numerals.img","url":"numerals-icon.js","evaluate":true}, + {"name":"numerals.settings.js","url":"numerals.settings.js"} + ] + }, { "id": "bledetect", "name": "BLE Detector", "shortName":"BLE Detector", @@ -1184,5 +1227,17 @@ {"name":"snake.app.js","url":"snake.js"}, {"name":"snake.img","url":"snake-icon.js","evaluate":true} ] + }, + { "id": "calculator", + "name": "Calculator", + "shortName":"Calculator", + "icon": "calculator.png", + "version":"0.01", + "description": "Basic calculator reminiscent of MacOs's one. Handy for small calculus. Push button1 and 3 to navigate up/down, tap right or left to navigate the sides, push button 2 to select.", + "tags": "app,tool", + "storage": [ + {"name":"calculator.app.js","url":"app.js"}, + {"name":"calculator.img","url":"calculator-icon.js","evaluate":true} + ] } ] diff --git a/apps/activepedom/10600.png b/apps/activepedom/10600.png new file mode 100644 index 000000000..36de436df Binary files /dev/null and b/apps/activepedom/10600.png differ diff --git a/apps/activepedom/1600.png b/apps/activepedom/1600.png new file mode 100644 index 000000000..fb11f999a Binary files /dev/null and b/apps/activepedom/1600.png differ diff --git a/apps/activepedom/600.png b/apps/activepedom/600.png new file mode 100644 index 000000000..4d2c625c7 Binary files /dev/null and b/apps/activepedom/600.png differ diff --git a/apps/activepedom/ChangeLog b/apps/activepedom/ChangeLog new file mode 100644 index 000000000..4c21f3ace --- /dev/null +++ b/apps/activepedom/ChangeLog @@ -0,0 +1 @@ +0.01: New Widget! diff --git a/apps/activepedom/README.md b/apps/activepedom/README.md new file mode 100644 index 000000000..8a10727cd --- /dev/null +++ b/apps/activepedom/README.md @@ -0,0 +1,38 @@ +# Improved pedometer +Pedometer that filters out arm movement and displays a step goal progress. + +I changed the step counting algorithm completely. +Now every step is counted when in status 'active', if the time difference between two steps is not too short or too long. +To get in 'active' mode, you have to reach the step threshold before the active timer runs out. +When you reach the step threshold, the steps needed to reach the threshold are counted as well. + +## Screenshots +* 600 steps +![](600.png) + +* 1600 steps +![](1600.png) + +* 10600 steps +![](10600.png) + +## Features + +* Two line display +* Large number for good readability +* Small number with the exact steps counted +* Large number is displayed in green when status is 'active' +* Progress bar for step goal +* Counts steps only if they are reached in a certain time +* Filters out steps where time between two steps is too long or too short +* Step detection sensitivity from firmware can be configured +* Steps are saved to a file and read-in at start (to not lose step progress) +* Settings can be changed in Settings - App/widget settings - Active Pedometer + +## Development version + +* https://github.com/Purple-Tentacle/BangleAppsDev/tree/master/apps/pedometer + +## Requests + +If you have any feature requests, please post in this forum thread: http://forum.espruino.com/conversations/345754/ \ No newline at end of file diff --git a/apps/activepedom/app-icon.js b/apps/activepedom/app-icon.js new file mode 100644 index 000000000..82e786c7f --- /dev/null +++ b/apps/activepedom/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwIGDvAEDgP+ApMD/4FVEZY1FABcP8AFDn/wAod/AocB//4AoUHAokPAokf5/8AocfAoc+j5HDvgFEvEf7+AAoP4AoJCC+E/54qCsE/wYkDn+AAos8AohZDj/AAohrEp4FEs5xEuJfDgF5Aon4GgYFBGgZOBnyJD+EeYgfgj4FEh6VD4AFDh+AAIJMCBoIFFLQQtBgYFCHIIFDjA3BC4I=")) \ No newline at end of file diff --git a/apps/activepedom/app.png b/apps/activepedom/app.png new file mode 100644 index 000000000..6fccf4308 Binary files /dev/null and b/apps/activepedom/app.png differ diff --git a/apps/activepedom/settings.js b/apps/activepedom/settings.js new file mode 100644 index 000000000..43764a164 --- /dev/null +++ b/apps/activepedom/settings.js @@ -0,0 +1,81 @@ +// 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 = 'activepedom.settings.json'; + + // initialize with default settings... + let s = { + 'cMaxTime' : 1100, + 'cMinTime' : 240, + 'stepThreshold' : 30, + 'intervalResetActive' : 30000, + 'stepSensitivity' : 80, + 'stepGoal' : 10000, + }; + // ...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["activepedom"].draw(); + }; + } + + const menu = { + '': { 'title': 'Active Pedometer' }, + '< Back': back, + 'Max time (ms)': { + value: s.cMaxTime, + min: 0, + max: 10000, + step: 100, + onchange: save('cMaxTime'), + }, + 'Min time (ms)': { + value: s.cMinTime, + min: 0, + max: 500, + step: 10, + onchange: save('cMinTime'), + }, + 'Step threshold': { + value: s.stepThreshold, + min: 0, + max: 100, + step: 1, + onchange: save('stepThreshold'), + }, + 'Act.Res. (ms)': { + value: s.intervalResetActive, + min: 100, + max: 100000, + step: 1000, + onchange: save('intervalResetActive'), + }, + 'Step sens.': { + value: s.stepSensitivity, + min: 0, + max: 1000, + step: 10, + onchange: save('stepSensitivity'), + }, + 'Step goal': { + value: s.stepGoal, + min: 1000, + max: 100000, + step: 1000, + onchange: save('stepGoal'), + }, + }; + E.showMenu(menu); +}); \ No newline at end of file diff --git a/apps/activepedom/widget.js b/apps/activepedom/widget.js new file mode 100644 index 000000000..0c8b2438d --- /dev/null +++ b/apps/activepedom/widget.js @@ -0,0 +1,180 @@ +(() => { + var stepTimeDiff = 9999; //Time difference between two steps + var startTimeStep = new Date(); //set start time + var stopTimeStep = 0; //Time after one step + var timerResetActive = 0; //timer to reset active + var steps = 0; //steps taken + var stepsCounted = 0; //active steps counted + var active = 0; //x steps in y seconds achieved + var stepGoalPercent = 0; //percentage of step goal + var stepGoalBarLength = 0; //length og progress bar + var lastUpdate = new Date(); + var width = 45; + + var stepsTooShort = 0; + var stepsTooLong = 0; + var stepsOutsideTime = 0; + + //define default settings + const DEFAULTS = { + 'cMaxTime' : 1100, + 'cMinTime' : 240, + 'stepThreshold' : 30, + 'intervalResetActive' : 30000, + 'stepSensitivity' : 80, + 'stepGoal' : 10000, + }; + const SETTINGS_FILE = 'activepedom.settings.json'; + const PEDOMFILE = "activepedom.steps.json"; + + let settings; + //load settings + function loadSettings() { + settings = require('Storage').readJSON(SETTINGS_FILE, 1) || {}; + } + //return setting + function setting(key) { + if (!settings) { loadSettings(); } + return (key in settings) ? settings[key] : DEFAULTS[key]; + } + + function setStepSensitivity(s) { + function sqr(x) { return x*x; } + var X=sqr(8192-s); + var Y=sqr(8192+s); + Bangle.setOptions({stepCounterThresholdLow:X,stepCounterThresholdHigh:Y}); + } + + //format number to make them shorter + function kFormatter(num) { + if (num <= 999) return num; //smaller 1.000, return 600 as 600 + if (num >= 1000 && num < 10000) { //between 1.000 and 10.000 + num = Math.floor(num/100)*100; + return (num / 1000).toFixed(1).replace(/\.0$/, '') + 'k'; //return 1600 as 1.6k + } + if (num >= 10000) { //greater 10.000 + num = Math.floor(num/1000)*1000; + return (num / 1000).toFixed(1).replace(/\.0$/, '') + 'k'; //return 10.600 as 10k + } + } + + //Set Active to 0 + function resetActive() { + active = 0; + steps = 0; + if (Bangle.isLCDOn()) WIDGETS["activepedom"].draw(); + } + + function calcSteps() { + stopTimeStep = new Date(); //stop time after each step + stepTimeDiff = stopTimeStep - startTimeStep; //time between steps in milliseconds + startTimeStep = new Date(); //start time again + + //Remove step if time between first and second step is too long + if (stepTimeDiff >= setting('cMaxTime')) { //milliseconds + stepsTooLong++; //count steps which are note counted, because time too long + steps--; + } + + //Remove step if time between first and second step is too short + if (stepTimeDiff <= setting('cMinTime')) { //milliseconds + stepsTooShort++; //count steps which are note counted, because time too short + steps--; + } + + if (steps >= setting('stepThreshold')) { + if (active == 0) { + stepsCounted = stepsCounted + (setting('stepThreshold') -1) ; //count steps needed to reach active status, last step is counted anyway, so treshold -1 + stepsOutsideTime = stepsOutsideTime - 10; //substract steps needed to reac active status + } + active = 1; + clearInterval(timerResetActive); //stop timer which resets active + timerResetActive = setInterval(resetActive, setting('intervalResetActive')); //reset active after timer runs out + steps = 0; + } + + if (active == 1) { + stepsCounted++; //count steps + } + else { + stepsOutsideTime++; + } + } + + function draw() { + var height = 23; //width is deined globally + var stepsDisplayLarge = kFormatter(stepsCounted); + + //Check if same day + let date = new Date(); + if (lastUpdate.getDate() == date.getDate()){ //if same day + } + else { + stepsCounted = 1; //set stepcount to 1 + } + lastUpdate = date; + + g.reset(); + g.clearRect(this.x, this.y, this.x+width, this.y+height); + + //draw numbers + if (active == 1) g.setColor(0x07E0); //green + else g.setColor(0xFFFF); //white + g.setFont("6x8", 2); + g.drawString(stepsDisplayLarge,this.x+1,this.y); //first line, big number + g.setFont("6x8", 1); + g.setColor(0xFFFF); //white + g.drawString(stepsCounted,this.x+1,this.y+14); //second line, small number + + //draw step goal bar + stepGoalPercent = (stepsCounted / setting('stepGoal')) * 100; + stepGoalBarLength = width / 100 * stepGoalPercent; + if (stepGoalBarLength > width) stepGoalBarLength = width; //do not draw across width of widget + g.setColor(0x7BEF); //grey + g.fillRect(this.x, this.y+height, this.x+width, this.y+height); // draw background bar + g.setColor(0xFFFF); //white + g.fillRect(this.x, this.y+height, this.x+1, this.y+height-1); //draw start of bar + g.fillRect(this.x+width, this.y+height, this.x+width-1, this.y+height-1); //draw end of bar + g.fillRect(this.x, this.y+height, this.x+stepGoalBarLength, this.y+height); // draw progress bar + } + + //This event is called just before the device shuts down for commands such as reset(), load(), save(), E.reboot() or Bangle.off() + E.on('kill', () => { + let d = { //define array to write to file + lastUpdate : lastUpdate.toISOString(), + stepsToday : stepsCounted, + stepsTooShort : stepsTooShort, + stepsTooLong : stepsTooLong, + stepsOutsideTime : stepsOutsideTime + }; + require("Storage").write(PEDOMFILE,d); //write array to file + }); + + //When Step is registered by firmware + Bangle.on('step', (up) => { + steps++; //increase step count + calcSteps(); + if (Bangle.isLCDOn()) WIDGETS["activepedom"].draw(); + }); + + // redraw when the LCD turns on + Bangle.on('lcdPower', function(on) { + if (on) WIDGETS["activepedom"].draw(); + }); + + //Read data from file and set variables + let pedomData = require("Storage").readJSON(PEDOMFILE,1); + if (pedomData) { + if (pedomData.lastUpdate) lastUpdate = new Date(pedomData.lastUpdate); + stepsCounted = pedomData.stepsToday|0; + stepsTooShort = pedomData.stepsTooShort; + stepsTooLong = pedomData.stepsTooLong; + stepsOutsideTime = pedomData.stepsOutsideTime; + } + + setStepSensitivity(setting('stepSensitivity')); //set step sensitivity (80 is standard, 400 is muss less sensitive) + + //Add widget + WIDGETS["activepedom"]={area:"tl",width:width,draw:draw}; + +})(); \ No newline at end of file diff --git a/apps/batchart/ChangeLog b/apps/batchart/ChangeLog index 1b77ff82f..f57805b6a 100644 --- a/apps/batchart/ChangeLog +++ b/apps/batchart/ChangeLog @@ -1,3 +1,5 @@ 0.01: New app and widget 0.02: Widget stores data to file (1 dataset/10min) 0.03: Rotate log files once a week. +0.04: chart in the app is now active. +0.05: Display temperature and LCD state in chart \ No newline at end of file diff --git a/apps/batchart/app.js b/apps/batchart/app.js index 684f9a88d..4fb919354 100644 --- a/apps/batchart/app.js +++ b/apps/batchart/app.js @@ -1,20 +1,195 @@ -// place your const, vars, functions or classes here +const GraphXZero = 40; +const GraphYZero = 180; +const GraphY100 = 80; -function renderBatteryChart(){ - g.drawString("t", 215, 175); - g.drawLine(40,190,40,80); +const GraphMarkerOffset = 5; +const MaxValueCount = 144; +const GraphXMax = GraphXZero + MaxValueCount; + +const GraphLcdY = GraphYZero + 10; +// const GraphCompassY = GraphYZero + 16; +// const GraphBluetoothY = GraphYZero + 22; +// const GraphGpsY = GraphYZero + 28; +// const GraphHrmY = GraphYZero + 34; + +var Storage = require("Storage"); + +function renderCoordinateSystem() { + g.setFont("6x8", 1); - g.drawString("%", 39, 70); - g.drawString("100", 15, 75); - g.drawLine(35,80,40,80); + // Left Y axis (Battery) + g.setColor(1, 1, 0); + g.drawLine(GraphXZero, GraphYZero + GraphMarkerOffset, GraphXZero, GraphY100); + g.drawString("%", 39, GraphY100 - 10); - g.drawString("50", 20,125); - g.drawLine(35,130,40,130); + g.setFontAlign(1, -1, 0); + g.drawString("100", 30, GraphY100 - GraphMarkerOffset); + g.drawLine(GraphXZero - GraphMarkerOffset, GraphY100, GraphXZero, GraphY100); - g.drawString("0", 25, 175); - g.drawLine(35,180,210,180); + g.drawString("50", 30, GraphYZero - 50 - GraphMarkerOffset); + g.drawLine(GraphXZero - GraphMarkerOffset, 130, GraphXZero, 130); - g.drawString("Chart not yet functional", 60, 125); + g.drawString("0", 30, GraphYZero - GraphMarkerOffset); + + g.setColor(1,1,1); + g.setFontAlign(1, -1, 0); + g.drawLine(GraphXZero - GraphMarkerOffset, GraphYZero, GraphXMax + GraphMarkerOffset, GraphYZero); + + // Right Y axis (Temperature) + g.setColor(0.4, 0.4, 1); + g.drawLine(GraphXMax, GraphYZero + GraphMarkerOffset, GraphXMax, GraphY100); + g.drawString("°C", GraphXMax + GraphMarkerOffset, GraphY100 - 10); + g.setFontAlign(-1, -1, 0); + g.drawString("20", GraphXMax + 2 * GraphMarkerOffset, GraphYZero - GraphMarkerOffset); + + g.drawLine(GraphXMax + GraphMarkerOffset, 130, GraphXMax, 130); + g.drawString("30", GraphXMax + 2 * GraphMarkerOffset, GraphYZero - 50 - GraphMarkerOffset); + + g.drawLine(GraphXMax + GraphMarkerOffset, 80, GraphXMax, 80); + g.drawString("40", GraphXMax + 2 * GraphMarkerOffset, GraphY100 - GraphMarkerOffset); + + g.setColor(1,1,1); +} + +function decrementDay(dayToDecrement) { + return dayToDecrement === 0 ? 6 : dayToDecrement-1; +} + +function loadData() { + const startingDay = new Date().getDay(); + + // Load data for the current day + let logFileName = "bclog" + startingDay; + + let dataLines = loadLinesFromFile(MaxValueCount, logFileName); + + // Top up to MaxValueCount from previous days as required + let previousDay = decrementDay(startingDay); + while (dataLines.length < MaxValueCount + && previousDay !== startingDay) { + + let topUpLogFileName = "bclog" + previousDay; + let remainingLines = MaxValueCount - dataLines.length; + let topUpLines = loadLinesFromFile(remainingLines, topUpLogFileName); + dataLines = topUpLines.concat(dataLines); + + previousDay = decrementDay(previousDay); + } + + return dataLines; +} + +function loadLinesFromFile(requestedLineCount, fileName) { + let allLines = []; + let returnLines = []; + + var readFile = Storage.open(fileName, "r"); + + while ((nextLine = readFile.readLine())) { + if(nextLine) { + allLines.push(nextLine); + } + } + + readFile = null; + + if (allLines.length <= 0) return; + + let linesToReadCount = Math.min(requestedLineCount, allLines.length); + let startingLineIndex = Math.max(0, allLines.length - requestedLineCount - 1); + + for (let i = startingLineIndex; i < linesToReadCount + startingLineIndex; i++) { + if(allLines[i]) { + returnLines.push(allLines[i]); + } + } + + allLines = null; + + return returnLines; +} + +function renderData(dataArray) { + const switchableConsumers = { + none: 0, + lcd: 1, + compass: 2, + bluetooth: 4, + gps: 8, + hrm: 16 + }; + + //const timestampIndex = 0; + const batteryIndex = 1; + const temperatureIndex = 2; + const switchabelsIndex = 3; + + var allConsumers = switchableConsumers.none | switchableConsumers.lcd | switchableConsumers.compass | switchableConsumers.bluetooth | switchableConsumers.gps | switchableConsumers.hrm; + + for (let i = 0; i < dataArray.length; i++) { + const element = dataArray[i]; + + var dataInfo = element.split(","); + + // Battery percentage + g.setColor(1, 1, 0); + g.setPixel(GraphXZero + i, GraphYZero - parseInt(dataInfo[batteryIndex])); + + // Temperature + g.setColor(0.4, 0.4, 1); + let scaledTemp = Math.floor(((parseFloat(dataInfo[temperatureIndex]) * 100) - 2000)/20) + ((((parseFloat(dataInfo[temperatureIndex]) * 100) - 2000) % 100)/25); + + g.setPixel(GraphXZero + i, GraphYZero - scaledTemp); + + // LCD state + if (parseInt(dataInfo[switchabelsIndex]) & switchableConsumers.lcd == switchableConsumers.lcd) { + g.setColor(1, 1, 1); + g.setFontAlign(1, -1, 0); + g.drawString("LCD", GraphXZero - GraphMarkerOffset, GraphLcdY - 2, true); + g.drawLine(GraphXZero + i, GraphLcdY, GraphXZero + i, GraphLcdY + 1); + } + + // // Compass state + // if (switchables & switchableConsumers.lcd == switchableConsumers.lcd) { + // g.setColor(0, 1, 0); + // g.setFontAlign(-1, -1, 0); + // g.drawString("Compass", GraphXMax + GraphMarkerOffset, GraphCompassY - 2, true); + // g.drawLine(GraphXZero + i, GraphCompassY, GraphXZero + i, GraphCompassY + 1); + // } + + // // Bluetooth state + // if (switchables & switchableConsumers.lcd == switchableConsumers.lcd) { + // g.setColor(0, 0, 1); + // g.setFontAlign(1, -1, 0); + // g.drawString("BLE", GraphXZero - GraphMarkerOffset, GraphBluetoothY - 2, true); + // g.drawLine(GraphXZero + i, GraphBluetoothY, GraphXZero + i, GraphBluetoothY + 1); + // } + + // // Gps state + // if (switchables & switchableConsumers.lcd == switchableConsumers.lcd) { + // g.setColor(0.8, 0.5, 0.24); + // g.setFontAlign(-1, -1, 0); + // g.drawString("GPS", GraphXMax + GraphMarkerOffset, GraphGpsY - 2, true); + // g.drawLine(GraphXZero + i, GraphGpsY, GraphXZero + i, GraphGpsY + 1); + // } + + // // Hrm state + // if (switchables & switchableConsumers.lcd == switchableConsumers.lcd) { + // g.setColor(1, 0, 0); + // g.setFontAlign(1, -1, 0); + // g.drawString("HRM", GraphXZero - GraphMarkerOffset, GraphHrmY - 2, true); + // g.drawLine(GraphXZero + i, GraphHrmY, GraphXZero + i, GraphHrmY + 1); + // } + } + + dataArray = null; +} + +function renderBatteryChart() { + renderCoordinateSystem(); + let data = loadData(); + renderData(data); + data = null; } // special function to handle display switch on @@ -22,6 +197,9 @@ Bangle.on('lcdPower', (on) => { if (on) { // call your app function here // If you clear the screen, do Bangle.drawWidgets(); + g.clear() + Bangle.loadWidgets(); + Bangle.drawWidgets(); renderBatteryChart(); } }); diff --git a/apps/batchart/batchart.dat b/apps/batchart/batchart.dat deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/batchart/widget.js b/apps/batchart/widget.js index 2e2f43cdf..de7ce230d 100644 --- a/apps/batchart/widget.js +++ b/apps/batchart/widget.js @@ -60,7 +60,6 @@ } } - // Called by the heart app to reload settings and decide what's function reload() { WIDGETS["batchart"].width = 24; diff --git a/apps/calculator/ChangeLog b/apps/calculator/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/calculator/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/calculator/app.js b/apps/calculator/app.js new file mode 100644 index 000000000..91dd7c49d --- /dev/null +++ b/apps/calculator/app.js @@ -0,0 +1,352 @@ +/** + * BangleJS Calculator + * + * Original Author: Frederic Rousseau https://github.com/fredericrous + * Created: April 2020 + */ + +g.clear(); +Graphics.prototype.setFont7x11Numeric7Seg = function() { + this.setFontCustom(atob("ACAB70AYAwBgC94AAAAAAAAAAB7wAAPQhhDCGELwAAAAhDCGEMIXvAAeACAEAIAQPeAA8CEMIYQwhA8AB70IYQwhhCB4AAAIAQAgBAB7wAHvQhhDCGEL3gAPAhDCGEMIXvAAe9CCEEIIQPeAA94EIIQQghA8AB70AYAwBgCAAAAHgQghBCCF7wAHvQhhDCGEIAAAPehBCCEEIAAAAA=="), 46, atob("AgAHBwcHBwcHBwcHAAAAAAAAAAcHBwcHBw=="), 11); +}; + +var DEFAULT_SELECTION = '5'; +var BOTTOM_MARGIN = 10; +var RIGHT_MARGIN = 20; +var COLORS = { + // [normal, selected] + DEFAULT: ['#7F8183', '#A6A6A7'], + OPERATOR: ['#F99D1C', '#CA7F2A'], + SPECIAL: ['#65686C', '#7F8183'] +}; + +var keys = { + '0': { + xy: [0, 200, 120, 240], + trbl: '2.00' + }, + '.': { + xy: [120, 200, 180, 240], + trbl: '3=.0' + }, + '=': { + xy: [181, 200, 240, 240], + trbl: '+==.', + color: COLORS.OPERATOR + }, + '1': { + xy: [0, 160, 60, 200], + trbl: '4201' + }, + '2': { + xy: [60, 160, 120, 200], + trbl: '5301' + }, + '3': { + xy: [120, 160, 180, 200], + trbl: '6+.2' + }, + '+': { + xy: [181, 160, 240, 200], + trbl: '-+=3', + color: COLORS.OPERATOR + }, + '4': { + xy: [0, 120, 60, 160], + trbl: '7514' + }, + '5': { + xy: [60, 120, 120, 160], + trbl: '8624' + }, + '6': { + xy: [120, 120, 180, 160], + trbl: '9-35' + }, + '-': { + xy: [181, 120, 240, 160], + trbl: '*-+6', + color: COLORS.OPERATOR + }, + '7': { + xy: [0, 80, 60, 120], + trbl: 'R847' + }, + '8': { + xy: [60, 80, 120, 120], + trbl: 'N957' + }, + '9': { + xy: [120, 80, 180, 120], + trbl: '%*68' + }, + '*': { + xy: [181, 80, 240, 120], + trbl: '/*-9', + color: COLORS.OPERATOR + }, + 'R': { + xy: [0, 40, 60, 79], + trbl: 'RN7R', + color: COLORS.SPECIAL, + val: 'AC' + }, + 'N': { + xy: [60, 40, 120, 79], + trbl: 'N%8R', + color: COLORS.SPECIAL, + val: '+/-' + }, + '%': { + xy: [120, 40, 180, 79], + trbl: '%/9N', + color: COLORS.SPECIAL + }, + '/': { + xy: [181, 40, 240, 79], + trbl: '//*%', + color: COLORS.OPERATOR + } +}; + +var selected = DEFAULT_SELECTION; +var prevSelected = DEFAULT_SELECTION; +var prevNumber = null; +var currNumber = null; +var operator = null; +var results = null; +var isDecimal = false; +var hasPressedEquals = false; + +function drawKey(name, k, selected) { + var rMargin = 0; + var bMargin = 0; + var color = k.color || COLORS.DEFAULT; + g.setColor(color[selected ? 1 : 0]); + g.setFont('Vector', 20); + g.fillRect(k.xy[0], k.xy[1], k.xy[2], k.xy[3]); + g.setColor(-1); + // correct margins to center the texts + if (name == '0') { + rMargin = (RIGHT_MARGIN * 2) - 7; + } else if (name === '/') { + rMargin = 5; + } else if (name === '*') { + bMargin = 5; + rMargin = 3; + } else if (name === '-') { + rMargin = 3; + } else if (name === 'R' || name === 'N') { + rMargin = k.val === 'C' ? 0 : -9; + } else if (name === '%') { + rMargin = -3; + } + g.drawString(k.val || name, k.xy[0] + RIGHT_MARGIN + rMargin, k.xy[1] + BOTTOM_MARGIN + bMargin); +} + +function doMath(x, y, operator) { + // might not be a number due to display of dot "." algo + x = Number(x); + y = Number(y); + switch (operator) { + case '/': + return x / y; + case '*': + return x * y; + case '+': + return x + y; + case '-': + return x - y; + } +} + +function displayOutput(num) { + var len; + var minusMarge = 0; + g.setColor(0); + g.fillRect(0, 0, 240, 39); + g.setColor(-1); + if (num === Infinity || num === -Infinity || isNaN(num)) { + // handle division by 0 + if (num === Infinity) { + num = 'INFINITY'; + } else if (num === -Infinity) { + num = '-INFINITY'; + } else { + num = 'NOT A NUMBER'; + minusMarge = -25; + } + len = (num + '').length; + currNumber = null; + results = null; + isDecimal = false; + hasPressedEquals = false; + prevNumber = null; + operator = null; + keys.R.val = 'AC'; + drawKey('R', keys.R); + g.setFont('Vector', 22); + } else { + // might not be a number due to display of dot "." + var numNumeric = Number(num); + + if (typeof num === 'string') { + if (num.indexOf('.') !== -1) { + // display a 0 before a lonely dot + if (numNumeric == 0) { + num = '0.'; + } + } else { + // remove preceding 0 + while (num.length > 1 && num[0] === '0') + num = num.substr(1); + } + } + + len = (num + '').length; + if (numNumeric < 0) { + // minus is not available in font 7x11Numeric7Seg, we use Vector + g.setFont('Vector', 20); + g.drawString('-', 220 - (len * 15), 10); + minusMarge = 15; + } + g.setFont('7x11Numeric7Seg', 2); + } + g.drawString(num, 220 - (len * 15) + minusMarge, 10); +} + +function calculatorLogic(x) { + if (hasPressedEquals) { + currNumber = results; + prevNumber = null; + operator = null; + results = null; + isDecimal = null; + displayOutput(currNumber); + hasPressedEquals = false; + } + if (prevNumber != null && currNumber != null && operator != null) { + // we execute the calculus only when there was a previous number entered before and an operator + results = doMath(prevNumber, currNumber, operator); + operator = x; + prevNumber = results; + currNumber = null; + displayOutput(results); + } else if (prevNumber == null && currNumber != null && operator == null) { + // no operator yet, save the current number for later use when an operator is pressed + operator = x; + prevNumber = currNumber; + currNumber = null; + displayOutput(prevNumber); + } else if (prevNumber == null && currNumber == null && operator == null) { + displayOutput(0); + } +} + +function buttonPress(val) { + switch (val) { + case 'R': + currNumber = null; + results = null; + isDecimal = false; + hasPressedEquals = false; + if (keys.R.val == 'AC') { + prevNumber = null; + operator = null; + } else { + keys.R.val = 'AC'; + drawKey('R', keys.R); + } + displayOutput(0); + break; + case '%': + if (results != null) { + displayOutput(results /= 100); + } else if (currNumber != null) { + displayOutput(currNumber /= 100); + } + break; + case 'N': + if (results != null) { + displayOutput(results *= -1); + } else if (currNumber != null) { + displayOutput(currNumber *= -1); + } + break; + case '/': + case '*': + case '-': + case '+': + calculatorLogic(val); + break; + case '.': + keys.R.val = 'C'; + drawKey('R', keys.R); + isDecimal = true; + displayOutput(currNumber == null ? 0 + '.' : currNumber + '.'); + break; + case '=': + if (prevNumber != null && currNumber != null && operator != null) { + results = doMath(prevNumber, currNumber, operator); + prevNumber = results; + displayOutput(results); + hasPressedEquals = true; + } + break; + default: + keys.R.val = 'C'; + drawKey('R', keys.R); + if (isDecimal) { + currNumber = currNumber == null ? 0 + '.' + val : currNumber + '.' + val; + isDecimal = false; + } else { + currNumber = currNumber == null ? val : currNumber + val; + } + displayOutput(currNumber); + break; + } +} + +for (var k in keys) { + if (keys.hasOwnProperty(k)) { + drawKey(k, keys[k], k == '5'); + } +} +g.setFont('7x11Numeric7Seg', 2.8); +g.drawString('0', 205, 10); + + +setWatch(function() { + drawKey(selected, keys[selected]); + // key 0 is 2 keys wide, go up to 1 if it was previously selected + if (selected == '0' && prevSelected === '1') { + prevSelected = selected; + selected = '1'; + } else { + prevSelected = selected; + selected = keys[selected].trbl[0]; + } + drawKey(selected, keys[selected], true); +}, BTN1, {repeat: true, debounce: 100}); + +setWatch(function() { + drawKey(selected, keys[selected]); + prevSelected = selected; + selected = keys[selected].trbl[2]; + drawKey(selected, keys[selected], true); +}, BTN3, {repeat: true, debounce: 100}); + +Bangle.on('touch', function(direction) { + drawKey(selected, keys[selected]); + prevSelected = selected; + if (direction == 1) { + selected = keys[selected].trbl[3]; + } else if (direction == 2) { + selected = keys[selected].trbl[1]; + } + drawKey(selected, keys[selected], true); +}); + +setWatch(function() { + buttonPress(selected); +}, BTN2, {repeat: true, debounce: 100}); diff --git a/apps/calculator/calculator-icon.js b/apps/calculator/calculator-icon.js new file mode 100644 index 000000000..94158e7d2 --- /dev/null +++ b/apps/calculator/calculator-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwhBC/AC8r6/XlYvr64CEF9UrMIIv/R/7vTMwIAmlUklQGDroAFqwHGBRgJBqwMDq+k5nNABAWDC4QZFERAvGBQOBF5I0FCYNW1mImWs6+sDoQsDAYIJEAAeB2eB1mBA4QvF43P6/GF4mB6+BAQYlEro3BAAI3FDAezBYgvE43O64DBF4hbCAAMrGAIiFBYRUEHogaBxA6CF4vXLwPHF4giEDIIkDDgI2BFoI6FBgYWCF5PPF4rSBKwVWI4bAFFgdcYAykBX5HX53NFwfNfwIkDAQYAGBBAKCIIYABd4y9DAAJ9CAD9dF4gAGCIi8BABLXBBRQLEF4vHRwgvEERQ6DHpgvH66PB65fUBpZfJ4/G6wxBMIaPbL5QvB6/WF6hqNF5KPDF6jkGd6JeBF5AAdF4oAGDBeH1mHAAwIBF8esABQvdWQonDX4YvIYAq/GXobvNF4hfKCwwvF43GF5AXGL44vJLwgvE453DMIYuFR5JiHI4yPHRoaREIwpIFF7TvbR5BJCX5IvMADgvcroABF6vG4wvIX46DKBZYvEFwPHGAgZHERALRF4YuBHYIwEFxxfPF5CDDF6ZfLDAyPFFwovFKRYvV47vDAgIvRR5aOFL4orCFwbvHADYvEAA4YLdRYvQ45eBR5C6UF5vHX4LvJF8PGZYXXGAYvnLYYvfZ4xfXd6AvKGAK/RDAKNTF4wAG44=")) diff --git a/apps/calculator/calculator.png b/apps/calculator/calculator.png new file mode 100644 index 000000000..8362c9200 Binary files /dev/null and b/apps/calculator/calculator.png differ diff --git a/apps/marioclock/ChangeLog b/apps/marioclock/ChangeLog index acce6a7ed..69a3ccc7b 100644 --- a/apps/marioclock/ChangeLog +++ b/apps/marioclock/ChangeLog @@ -6,4 +6,7 @@ 0.06: Performance refactor, and enhanced graphics! 0.07: Swipe right to change between Mario and Toad characters, swipe left to toggle night mode 0.08: Update date panel to be info panel toggling between Date, Battery and Temperature. Add Princes Daisy -0.09: Add GadgetBridge functionality. Mario shows message type in speach bubble, while message scrolls in info panel \ No newline at end of file +0.09: Add GadgetBridge functionality. Mario shows message type in speach bubble, while message scrolls in info panel +0.10: Swiping left to enable night-mode now also reduces LCD brightness through 3 levels before returning to day-mode. +0.11: User settings persisted and read to file. +0.12: Add info banner message when phone (dis)connects. Display low-battery warning (<=10%) \ No newline at end of file diff --git a/apps/marioclock/README.md b/apps/marioclock/README.md index e6aeaa1bb..25276a351 100644 --- a/apps/marioclock/README.md +++ b/apps/marioclock/README.md @@ -8,7 +8,7 @@ Enjoy watching Mario, or one of the other game characters run through a level wh ## Features * Multiple characters - swipe the screen right to change the character between `Mario`, `Toad`, and `Daisy` -* Night and Day modes - swipe left to toggle mode +* Night and Day modes - swipe left to enter night mode, with 3 levels of darkness before returning to day mode. * Smooth animation * Awesome 8-bit style grey-scale graphics * Mario jumps to change the time, every minute diff --git a/apps/marioclock/marioclock-app.js b/apps/marioclock/marioclock-app.js index 529f1c95b..7601b89ba 100644 --- a/apps/marioclock/marioclock-app.js +++ b/apps/marioclock/marioclock-app.js @@ -16,6 +16,8 @@ const is12Hour = settings["12hour"] || false; // Screen dimensions let W, H; +// Screen brightness +let brightness = 1; let intervalRef, displayTimeoutRef = null; @@ -79,6 +81,16 @@ const phone = { messageType: null, }; +const SETTINGS_FILE = "marioclock.json"; + +function readSettings() { + return require('Storage').readJSON(SETTINGS_FILE, 1) || {}; +} + +function writeSettings(newSettings) { + require("Storage").writeJSON(SETTINGS_FILE, newSettings); +} + function phoneOutbound(msg) { Bluetooth.println(JSON.stringify(msg)); } @@ -164,7 +176,17 @@ function switchCharacter() { } function toggleNightMode() { - nightMode = !nightMode; + if (!nightMode) { + nightMode = true; + return; + } + + brightness -= 0.30; + if (brightness <= 0) { + brightness = 1; + nightMode = false; + } + Bangle.setLCDBrightness(brightness); } function incrementTimer() { @@ -324,16 +346,20 @@ function drawToadFrame(idx, x, y) { function drawNotice(x, y) { if (phone.message === null) return; + let img; switch (phone.messageType) { case "call": - const callImg = require("heatshrink").decompress(atob("h8PxH+AAMHABIND6wAJB4INEw9cAAIPFBxAPEBw/WBxYACDrQ7QLI53OSpApDBoQAHB4INLByANNAwo=")); - g.drawImage(callImg, characterSprite.x, characterSprite.y - 16); + img = require("heatshrink").decompress(atob("h8PxH+AAMHABIND6wAJB4INEw9cAAIPFBxAPEBw/WBxYACDrQ7QLI53OSpApDBoQAHB4INLByANNAwo=")); break; case "notify": - const msgImg = require("heatshrink").decompress(atob("h8PxH+AAMHABIND6wAJB4INCrgAHB4QOEDQgOIAIQFGBwovDA4gOGFooOVLJR3OSpApDBoQAHB4INLByANNAwoA=")); - g.drawImage(msgImg, characterSprite.x, characterSprite.y - 16); + img = require("heatshrink").decompress(atob("h8PxH+AAMHABIND6wAJB4INCrgAHB4QOEDQgOIAIQFGBwovDA4gOGFooOVLJR3OSpApDBoQAHB4INLByANNAwoA=")); + break; + case "lowBatt": + img = require("heatshrink").decompress(atob("h8PxH+AAMHABIND6wAJB4INFrgABB4oOEBoQPFBwwDGB0uHAAIOLJRB3OSpApDBoQAHB4INLByANNAwo")); break; } + + if (img) g.drawImage(img, characterSprite.x, characterSprite.y - 16); } function drawCharacter(date, character) { @@ -555,8 +581,39 @@ function startTimers(){ redraw(); } +function loadSettings() { + const settings = readSettings(); + if (!settings) return; + + if (settings.character) characterSprite.character = settings.character; + if (settings.nightMode) nightMode = settings.nightMode; + if (settings.brightness) { + brightness = settings.brightness; + Bangle.setLCDBrightness(brightness); + } +} + +function updateSettings() { + const newSettings = { + character: characterSprite.character, + nightMode: nightMode, + brightness: brightness, + }; + writeSettings(newSettings); +} + +function checkBatteryLevel() { + if (Bangle.isCharging()) return; + if (E.getBattery() > 10) return; + if (phone.message !== null) return; + + phoneNewMessage("lowBatt", "Warning, battery is low"); +} + // Main function init() { + loadSettings(); + clearInterval(); // Initialise display @@ -606,23 +663,31 @@ function init() { default: toggleNightMode(); } + + updateSettings(); }); // Phone connectivity try { NRF.wake(); } catch (e) {} - NRF.on('disconnect', () => Bangle.buzz()); + NRF.on('disconnect', () => { + phoneNewMessage(null, "Phone disconnected"); + }); + NRF.on('connect', () => { setTimeout(() => { phoneOutbound({ t: "status", bat: E.getBattery() }); }, ONE_SECOND * 2); - Bangle.buzz(); + phoneNewMessage(null, "Phone connected"); }); GB = (evt) => phoneInbound(evt); startTimers(); + + setInterval(checkBatteryLevel, ONE_SECOND * 60 * 10); + checkBatteryLevel(); } // Initialise! -init() \ No newline at end of file +init(); \ No newline at end of file diff --git a/apps/nato/changelog.txt b/apps/nato/changelog.txt new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/nato/changelog.txt @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/nato/nato-icon.js b/apps/nato/nato-icon.js new file mode 100644 index 000000000..ae38c0274 --- /dev/null +++ b/apps/nato/nato-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwgFCiIABiAGFiINJAAUS///CAgGEgMT//zBoYXFmIiCC40fEooXF+QXJn4lCC5ARDC4oFC//xMAoXDJAQXFBgY9DC4wKCC4p2CPA4XDCQQXEOwXxPA4XBEQJICC4p2BmICCC44KBJAIXEiIJBkMvPAwXCWgYXFAgQMBPAoXCBwUxC4jtDeI4XDJAQXDFYXxHAoXGJAYXDLYPykUieIwXDJAYXDG4IAEPAgXCRgJICPYoAEPAgXDZ4TcDmYXGMAgXDUAZiEPwIABCALEBC5BZC+YQCRwRsEC45ID+S5BCAkBEYJ4DC4hID+IbCIAYjCCIYXGEgMxXoJwEgI3CA4JQDAAwaBmQGDFIQ3CC5UzkSLBdwIIDmYXCWY4jBCAJBCPYQ0EC5bXGkLuDC5QtEAAXzPoZMCmZwB+YFCbYkykQFCVoZMDWALnDQwRjDeoZIDZAgJCWwYeBFATWFC5LuHawgXKdwyJDD4YXIOAMzH4gICmIXKEwQXXkQXFKAKQFC85HNO64XDU44XMX48Sa5zvCmJICA4YXLE4fziIACJ4PyM4gXHCAQwBCwI2GC5JADAApGFC5ERmYWFFwwXHDARJCMgYWFB4MTmYiFLgMjCwMyiIuGE4QABNIyPDBQgA==")) diff --git a/apps/nato/nato.js b/apps/nato/nato.js new file mode 100644 index 000000000..f4301b83f --- /dev/null +++ b/apps/nato/nato.js @@ -0,0 +1,106 @@ +// Teach a user the NATO Phonetic Alphabet + numbers +// Based on the Morse Code app + +const FONT_NAME = 'Vector12'; +const FONT_SIZE = 80; +const SCREEN_PIXELS = 240; +const UNIT = 100; +const NATO_MAP = { + A: 'ALFA', + B: 'BRAVO', + C: 'CHARLIE', + D: 'DELTA', + E: 'ECHO', + F: 'FOXTROT', + G: 'GOLF', + H: 'HOTEL', + I: 'INDIA', + J: 'JULIETT', + K: 'KILO', + L: 'LIMA', + M: 'MIKE', + N: 'NOVEMBER', + O: 'OSCAR', + P: 'PAPA', + Q: 'QUEBEC', + R: 'ROMEO', + S: 'SIERRA', + T: 'TANGO', + U: 'UNIFORM', + V: 'VICTOR', + W: 'WHISKEY', + X: 'X-RAY', + Y: 'YANKEE', + Z: 'ZULU', + '0': 'ZE-RO', + '1': 'WUN', + '2': 'TOO', + '3': 'TREE', + '4': 'FOW-ER', + '5': 'FIFE', + '6': 'SIX', + '7': 'SEV-EN', + '8': 'AIT', + '9': 'NIN-ER', +}; + +let INDEX = 0; +let showLetter = true; + +const writeText = (txt) => { + g.clear(); + g.setFont(FONT_NAME, FONT_SIZE); + + var width = g.stringWidth(txt); + + // Fit text to screen + var fontFix = FONT_SIZE; + while(width > SCREEN_PIXELS-10){ + fontFix--; + g.setFont(FONT_NAME, fontFix); + width = g.stringWidth(txt); + } + g.drawString(txt, (SCREEN_PIXELS / 2) - (width / 2), SCREEN_PIXELS / 2); +}; +const writeLetter = () => { + writeText(Object.keys(NATO_MAP)[INDEX]); +}; +const writeCode = () => { + writeText(NATO_MAP[Object.keys(NATO_MAP)[INDEX]]); +}; +const toggle = () => { + showLetter = !showLetter; + if(showLetter){ + writeLetter(); + }else { + writeCode(); + } +}; + +// Bootstrapping + +g.clear(); +g.setFont(FONT_NAME, FONT_SIZE); +g.setColor(0, 1, 0); +g.setFontAlign(-1, 0, 0); + + +const step = (positive) => () => { + if (positive) { + INDEX = INDEX + 1; + if (INDEX > Object.keys(NATO_MAP).length - 1) INDEX = 0; + } else { + INDEX = INDEX - 1; + if (INDEX < 0) INDEX = Object.keys(NATO_MAP).length - 1; + } + showLetter = true; // for toggle() + writeLetter(); +}; + +writeLetter(); + +// Press the middle button to see the NATO Phonetic wording +setWatch(toggle, BTN2, { repeat: true }); +// Allow user to switch between letters +setWatch(step(true), BTN1, { repeat: true }); +setWatch(step(false), BTN3, { repeat: true }); diff --git a/apps/nato/nato.png b/apps/nato/nato.png new file mode 100644 index 000000000..bd4678c11 Binary files /dev/null and b/apps/nato/nato.png differ diff --git a/apps/numerals/ChangeLog b/apps/numerals/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/numerals/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/numerals/README.md b/apps/numerals/README.md new file mode 100644 index 000000000..01d784ef8 --- /dev/null +++ b/apps/numerals/README.md @@ -0,0 +1,17 @@ +# Numerals Clock + +This is a simple big numerals clock. +Settings can be accessed through the app/widget settings menu of the Bangle.js + +## Settings available + +### color: +* rnd - shows numerals in different color combinations every time the watches wakes +* r/g - red/green +* y/w - yellow/white +* o/c - orange/cyan +* b/y - blue/yellow'ish + +### draw mode +* fill - fill numerals +* frame - only shows outline of numerals diff --git a/apps/numerals/numerals-icon.js b/apps/numerals/numerals-icon.js new file mode 100644 index 000000000..7e471fb0d --- /dev/null +++ b/apps/numerals/numerals-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/ABMBzIADyAJIAAkQBoMZBIoXCBIwADyIkIGAIuKGAQkIBJIwEEKQANC/4XWR58RiIHFWpAXFe4QRFcpAXFewQRFcxAXEFwQwGA4QXKiAXDGAgX/C/4X/C/4X/C7uQCwcBBwYXNBwYuEC54wCFwgXPzMRiIHFC54AHC/4XiCAoXRhIHDyK3GAAwOBJA0QG45VGC4YwCD4YwKFwgABcgIfEAwIAHBwgA/AAgA==")) \ No newline at end of file diff --git a/apps/numerals/numerals.app.js b/apps/numerals/numerals.app.js new file mode 100644 index 000000000..648a1005a --- /dev/null +++ b/apps/numerals/numerals.app.js @@ -0,0 +1,93 @@ +/** + * Bangle.js Numerals Clock + * + * + Original Author: Raik M. https://github.com/ps-igel + * + Created: April 2020 + * + see README.md for details + */ + +var numerals = { + 0:[[9,1,82,1,90,9,90,82,82,90,9,90,1,82,1,9,9,1],[30,21,61,21,69,29,69,61,61,69,30,69,22,61,22,29,30,21]], + 1:[[59,1,82,1,90,9,90,82,82,90,73,90,65,82,65,27,59,27,51,19,51,9,59,1]], + 2:[[9,1,82,1,90,9,90,47,82,55,21,55,21,64,82,64,90,72,90,82,82,90,9,90,1,82,1,43,9,35,70,35,70,25,9,25,1,17,1,9,9,1]], + 3:[[9,1,82,1,90,9,90,82,82,90,9,90,1,82,1,74,9,66,70,66,70,57,9,57,1,49,1,41,9,33,70,33,70,25,9,25,1,17,1,9,9,1]], + 4:[[9,1,14,1,22,9,22,34,69,34,69,9,77,1,82,1,90,9,90,82,82,90,78,90,70,82,70,55,9,55,1,47,1,9,9,1]], + 5:[[9,1,82,1,90,9,90,17,82,25,21,25,21,35,82,35,90,43,90,82,82,90,9,90,1,82,1,72,9,64,71,64,71,55,9,55,1,47,1,9,9,1]], + 6:[[9,1,82,1,90,9,90,14,82,22,22,22,22,36,82,36,90,44,90,82,82,90,9,90,1,82,1,9,9,1],[22,55,69,55,69,69,22,69,22,55]], + 7:[[9,1,82,1,90,9,90,15,15,90,9,90,1,82,1,76,54,23,9,23,1,15,1,9,9,1]], + 8:[[9,1,82,1,90,9,90,82,82,90,9,90,1,82,1,9,9,1],[22,22,69,22,69,36,22,36,22,22],[22,55,69,55,69,69,22,69,22,55]], + 9:[[9,1,82,1,90,9,90,82,82,90,9,90,1,82,1,77,9,69,69,69,69,55,9,55,1,47,1,9,9,1],[22,22,69,22,69,36,22,36,22,22]], +}; +var _hCol = ["#ff5555","#ffff00","#FF9901","#2F00FF"]; +var _mCol = ["#55ff55","#ffffff","#00EFEF","#FFBF00"]; +var _rCol = 0; +var interval = 0; +const REFRESH_RATE = 10E3; + +function translate(tx, ty, p) { + return p.map((x, i)=> x+((i%2)?ty:tx)); +} + +function fill(poly){ + return g.fillPoly(poly); +} + +function frame(poly){ + return g.drawPoly(poly); +} + +let settings = require('Storage').readJSON('numerals.json',1); +if (!settings) { + settings = { + color: 0, + drawMode: "fill" + }; +} + +function drawNum(num,col,x,y,func){ + g.setColor(col); + let tx = x*100+35; + let ty = y*100+35; + for (let i=0;i0) g.setColor((func==fill)?"#000000":col); + func(translate(tx, ty,numerals[num][i])); + } +} + +function draw(drawMode){ + let d = new Date(); + let h1 = Math.floor(d.getHours()/10); + let h2 = d.getHours()%10; + let m1 = Math.floor(d.getMinutes()/10); + let m2 = d.getMinutes()%10; + g.clearRect(0,24,240,240); + drawNum(h1,_hCol[_rCol],0,0,eval(drawMode)); + drawNum(h2,_hCol[_rCol],1,0,eval(drawMode)); + drawNum(m1,_mCol[_rCol],0,1,eval(drawMode)); + drawNum(m2,_mCol[_rCol],1,1,eval(drawMode)); +} + +Bangle.setLCDMode(); + +clearWatch(); +setWatch(Bangle.showLauncher, BTN1, {repeat:false,edge:"falling"}); + +g.clear(); +clearInterval(); +if (settings.color>0) _rCol=settings.color-1; +interval=setInterval(draw, REFRESH_RATE, settings.drawMode); +draw(settings.drawMode); + +Bangle.on('lcdPower', function(on) { + if (on) { + if (settings.color==0) _rCol = Math.floor(Math.random()*_hCol.length); + draw(settings.drawMode); + interval=setInterval(draw, REFRESH_RATE, settings.drawMode); + }else + { + clearInterval(interval); + } +}); + +Bangle.loadWidgets(); +Bangle.drawWidgets(); \ No newline at end of file diff --git a/apps/numerals/numerals.png b/apps/numerals/numerals.png new file mode 100644 index 000000000..c181e2e0d Binary files /dev/null and b/apps/numerals/numerals.png differ diff --git a/apps/numerals/numerals.settings.js b/apps/numerals/numerals.settings.js new file mode 100644 index 000000000..f9c417da6 --- /dev/null +++ b/apps/numerals/numerals.settings.js @@ -0,0 +1,33 @@ +(function(back) { + function updateSettings() { + storage.write('numerals.json', numeralsSettings); + }; + function resetSettings() { + numeralsSettings = { + color: 0, + drawMode: "fill" + }; + updateSettings(); + } + let numeralsSettings = storage.readJSON('numerals.json',1); + if (!numeralsSettings) resetSettings(); + let dm = ["fill","frame"]; + let col = ["rnd","r/g","y/w","o/c","b/y"] + var menu={ + "" : { "title":"Numerals"}, + "Colors": { + value: 0|numeralsSettings.color, + min:0,max:4, + format: v=>col[v], + onchange: v=> { numeralsSettings.color=v; updateSettings();} + }, + "Draw mode": { + value: 0|dm.indexOf(numeralsSettings.drawMode), + min:0,max:1, + format: v=>dm[v], + onchange: v=> { numeralsSettings.drawMode=dm[v]; updateSettings();} + }, + "< back": back + }; + E.showMenu(menu); +}) \ No newline at end of file diff --git a/apps/setting/ChangeLog b/apps/setting/ChangeLog index 22277968c..5c5a26c61 100644 --- a/apps/setting/ChangeLog +++ b/apps/setting/ChangeLog @@ -12,3 +12,6 @@ 0.12: Fix memory leak (#206) Bring App settings nearer the top Move LCD Timeout to wakeup menu +0.13: Fix memory leak for App settings + Make capitalization more consistent + Move LCD Brightness menu into more general LCD menu \ No newline at end of file diff --git a/apps/setting/settings.js b/apps/setting/settings.js index ac7692610..71a6a181e 100644 --- a/apps/setting/settings.js +++ b/apps/setting/settings.js @@ -64,7 +64,7 @@ function showMainMenu() { const mainmenu = { '': { 'title': 'Settings' }, 'Make Connectable': ()=>makeConnectable(), - 'App/widget settings': ()=>showAppSettingsMenu(), + 'App/Widget Settings': ()=>showAppSettingsMenu(), 'BLE': { value: settings.ble, format: boolFormat, @@ -81,7 +81,7 @@ function showMainMenu() { updateSettings(); } }, - 'Debug info': { + 'Debug Info': { value: settings.log, format: v => v ? "Show" : "Hide", onchange: () => { @@ -89,17 +89,6 @@ function showMainMenu() { updateSettings(); } }, - 'LCD Brightness': { - value: settings.brightness, - min: 0.1, - 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, @@ -134,7 +123,7 @@ function showMainMenu() { } }, 'Set Time': ()=>showSetTimeMenu(), - 'LCD Wake-Up': ()=>showWakeUpMenu(), + 'LCD': ()=>showLCDMenu(), 'Reset Settings': ()=>showResetMenu(), 'Turn Off': ()=>Bangle.off(), '< Back': ()=>load() @@ -142,10 +131,21 @@ function showMainMenu() { return E.showMenu(mainmenu); } -function showWakeUpMenu() { - const wakeUpMenu = { - '': { 'title': 'LCD Wake-Up' }, +function showLCDMenu() { + const lcdMenu = { + '': { 'title': 'LCD' }, '< Back': ()=>showMainMenu(), + 'LCD Brightness': { + value: settings.brightness, + min: 0.1, + max: 1, + step: 0.1, + onchange: v => { + settings.brightness = v || 1; + updateSettings(); + Bangle.setLCDBrightness(settings.brightness); + } + }, 'LCD Timeout': { value: settings.timeout, min: 0, @@ -157,7 +157,7 @@ function showWakeUpMenu() { Bangle.setLCDTimeout(settings.timeout); } }, - 'Wake On BTN1': { + 'Wake on BTN1': { value: settings.options.wakeOnBTN1, format: boolFormat, onchange: () => { @@ -165,7 +165,7 @@ function showWakeUpMenu() { updateOptions(); } }, - 'Wake On BTN2': { + 'Wake on BTN2': { value: settings.options.wakeOnBTN2, format: boolFormat, onchange: () => { @@ -173,7 +173,7 @@ function showWakeUpMenu() { updateOptions(); } }, - 'Wake On BTN3': { + 'Wake on BTN3': { value: settings.options.wakeOnBTN3, format: boolFormat, onchange: () => { @@ -197,7 +197,7 @@ function showWakeUpMenu() { updateOptions(); } }, - 'Wake On Twist': { + 'Wake on Twist': { value: settings.options.wakeOnTwist, format: boolFormat, onchange: () => { @@ -236,7 +236,7 @@ function showWakeUpMenu() { } } } - return E.showMenu(wakeUpMenu) + return E.showMenu(lcdMenu) } function showLocaleMenu() { @@ -450,7 +450,7 @@ function showAppSettings(app) { } try { // pass showAppSettingsMenu as "back" argument - appSettings(showAppSettingsMenu); + appSettings(()=>showAppSettingsMenu()); } catch (e) { console.log(`${app.name} settings error:`, e) return showError('Error in settings'); diff --git a/bin/sanitycheck.js b/bin/sanitycheck.js index a2c9dee9a..62b111ae0 100755 --- a/bin/sanitycheck.js +++ b/bin/sanitycheck.js @@ -37,7 +37,13 @@ try{ ERROR("apps.json not valid JSON"); } -apps.forEach((app,addIdx) => { +const APP_KEYS = [ + 'id', 'name', 'shortName', 'version', 'icon', 'description', 'tags', 'type', + 'sortorder', 'readme', 'custom', 'interface', 'storage', 'allow_emulator', +]; +const STORAGE_KEYS = ['name', 'url', 'content', 'evaluate']; + +apps.forEach((app,appIdx) => { if (!app.id) ERROR(`App ${appIdx} has no id`); //console.log(`Checking ${app.id}...`); var appDir = APPSDIR+app.id+"/"; @@ -105,9 +111,15 @@ apps.forEach((app,addIdx) => { ERROR(`App ${app.id}'s ${file.name} is a JS file but isn't valid JS`); } } + for (const key in file) { + if (!STORAGE_KEYS.includes(key)) ERROR(`App ${app.id}'s ${file.name} has unknown key ${key}`); + } }); //console.log(fileNames); if (isApp && !fileNames.includes(app.id+".app.js")) ERROR(`App ${app.id} has no entrypoint`); if (isApp && !fileNames.includes(app.id+".img")) ERROR(`App ${app.id} has no JS icon`); if (app.type=="widget" && !fileNames.includes(app.id+".wid.js")) ERROR(`Widget ${app.id} has no entrypoint`); + for (const key in app) { + if (!APP_KEYS.includes(key)) ERROR(`App ${app.id} has unknown key ${key}`); + } }); diff --git a/index.html b/index.html index 0d5c17251..f016ffb49 100644 --- a/index.html +++ b/index.html @@ -96,6 +96,7 @@ +
@@ -134,7 +135,8 @@

Utilities

-

+ +

diff --git a/js/index.js b/js/index.js index d2c6d698b..ef9bcb4f1 100644 --- a/js/index.js +++ b/js/index.js @@ -1,6 +1,8 @@ var appJSON = []; // List of apps and info from apps.json var appsInstalled = []; // list of app JSON var files = []; // list of files on Bangle +var favourites = []; // list of user favourite app +const FAVOURITE = "favouriteapps.json"; httpGet("apps.json").then(apps=>{ try { @@ -18,7 +20,7 @@ httpGet("apps.json").then(apps=>{ function showChangeLog(appid) { var app = appNameToApp(appid); function show(contents) { - showPrompt(app.name+" Change Log",contents,{ok:true}).catch(()=>{});; + showPrompt(app.name+" Change Log",contents,{ok:true}).catch(()=>{}); } httpGet(`apps/${appid}/ChangeLog`). then(show).catch(()=>show("No Change Log available")); @@ -142,6 +144,20 @@ function handleAppInterface(app) { }); } +function handleAppFavourite(favourite, app){ + if (favourite) { + favourites = favourites.concat([app.id]); + } else { + if ([ "boot","setting"].includes(app.id)) { + showToast(app.name + ' is required, can\'t remove it' , 'warning'); + }else { + favourites = favourites.filter(e => e != app.id); + } + } + localStorage.setItem("favouriteapps.json", JSON.stringify(favourites)); + refreshLibrary(); +} + // =========================================== Top Navigation function showTab(tabname) { htmlToArray(document.querySelectorAll("#tab-navigate .tab-item")).forEach(tab => { @@ -156,7 +172,7 @@ function showTab(tabname) { // =========================================== Library -var chips = Array.from(document.querySelectorAll('.chip')).map(chip => chip.attributes.filterid.value) +var chips = Array.from(document.querySelectorAll('.chip')).map(chip => chip.attributes.filterid.value); var hash = window.location.hash ? window.location.hash.slice(1) : ''; var activeFilter = !!~chips.indexOf(hash) ? hash : ''; @@ -165,27 +181,34 @@ var currentSearch = ''; function refreshFilter(){ var filtersContainer = document.querySelector("#librarycontainer .filter-nav"); filtersContainer.querySelector('.active').classList.remove('active'); - if(activeFilter) filtersContainer.querySelector('.chip[filterid="'+activeFilter+'"]').classList.add('active') - else filtersContainer.querySelector('.chip[filterid]').classList.add('active') + if(activeFilter) filtersContainer.querySelector('.chip[filterid="'+activeFilter+'"]').classList.add('active'); + else filtersContainer.querySelector('.chip[filterid]').classList.add('active'); } function refreshLibrary() { var panelbody = document.querySelector("#librarycontainer .panel-body"); var visibleApps = appJSON; if (activeFilter) { - visibleApps = visibleApps.filter(app => app.tags && app.tags.split(',').includes(activeFilter)); + if ( activeFilter == "favourites" ) { + visibleApps = visibleApps.filter(app => app.id && (favourites.filter( e => e == app.id).length)); + }else{ + visibleApps = visibleApps.filter(app => app.tags && app.tags.split(',').includes(activeFilter)); + } } if (currentSearch) { visibleApps = visibleApps.filter(app => app.name.toLowerCase().includes(currentSearch) || app.tags.includes(currentSearch)); } + favourites = (localStorage.getItem(FAVOURITE)) === null ? JSON.parse('["boot","launch","setting"]') : JSON.parse(localStorage.getItem("favouriteapps.json")); + panelbody.innerHTML = visibleApps.map((app,idx) => { var appInstalled = appsInstalled.find(a=>a.id==app.id); var version = getVersionInfo(app, appInstalled); var versionInfo = version.text; if (versionInfo) versionInfo = " ("+versionInfo+")"; var readme = `Read more...`; + var favourite = favourites.find(e => e == app.id); return `
${escapeHtml(app.name)}

@@ -195,7 +218,8 @@ function refreshLibrary() {

${escapeHtml(app.description)}${app.readme?`
${readme}`:""}

See the code on GitHub
-
+
+ @@ -232,7 +256,7 @@ function refreshLibrary() { // upload icon.classList.remove("icon-upload"); icon.classList.add("loading"); - uploadApp(app) + uploadApp(app); } else if (icon.classList.contains("icon-menu")) { // custom HTML update icon.classList.remove("icon-menu"); @@ -250,6 +274,10 @@ function refreshLibrary() { updateApp(app); } else if (icon.classList.contains("icon-download")) { handleAppInterface(app); + } else if ( button.innerText == String.fromCharCode(0x2661)) { + handleAppFavourite(true, app); + } else if ( button.innerText == String.fromCharCode(0x2665) ) { + handleAppFavourite(false, app); } }); }); @@ -262,17 +290,17 @@ refreshLibrary(); function uploadApp(app) { return getInstalledApps().then(()=>{ if (appsInstalled.some(i => i.id === app.id)) { - return updateApp(app) + return updateApp(app); } Comms.uploadApp(app).then((appJSON) => { - Progress.hide({ sticky: true }) + Progress.hide({ sticky: true }); if (appJSON) { - appsInstalled.push(appJSON) + appsInstalled.push(appJSON); } - showToast(app.name + ' Uploaded!', 'success') + showToast(app.name + ' Uploaded!', 'success'); }).catch(err => { - Progress.hide({ sticky: true }) - showToast('Upload failed, ' + err, 'error') + Progress.hide({ sticky: true }); + showToast('Upload failed, ' + err, 'error'); }).finally(()=>{ refreshMyApps(); refreshLibrary(); @@ -286,8 +314,8 @@ function removeApp(app) { return showPrompt("Delete","Really remove '"+app.name+"'?").then(() => { return getInstalledApps().then(()=>{ // a = from appid.info, app = from apps.json - return Comms.removeApp(appsInstalled.find(a => a.id === app.id)) - }) + return Comms.removeApp(appsInstalled.find(a => a.id === app.id)); + }); }).then(()=>{ appsInstalled = appsInstalled.filter(a=>a.id!=app.id); showToast(app.name+" removed successfully","success"); @@ -315,13 +343,13 @@ function updateApp(app) { if (app.custom) return customApp(app); return getInstalledApps().then(() => { // a = from appid.info, app = from apps.json - let remove = appsInstalled.find(a => a.id === app.id) + let remove = appsInstalled.find(a => a.id === app.id); // no need to remove files which will be overwritten anyway remove.files = remove.files.split(',') .filter(f => f !== app.id + '.info') .filter(f => !app.storage.some(s => s.name === f)) - .join(',') - return Comms.removeApp(remove) + .join(','); + return Comms.removeApp(remove); }).then(()=>{ showToast(`Updating ${app.name}...`); appsInstalled = appsInstalled.filter(a=>a.id!=app.id); @@ -397,7 +425,7 @@ return `
// check icon to figure out what we should do if (icon.classList.contains("icon-delete")) removeApp(app); if (icon.classList.contains("icon-refresh")) updateApp(app); - if (icon.classList.contains("icon-download")) handleAppInterface(app) + if (icon.classList.contains("icon-download")) handleAppInterface(app); }); }); } @@ -405,7 +433,7 @@ return `
let haveInstalledApps = false; function getInstalledApps(refresh) { if (haveInstalledApps && !refresh) { - return Promise.resolve(appsInstalled) + return Promise.resolve(appsInstalled); } showLoadingIndicator("myappscontainer"); // Get apps and files @@ -453,7 +481,7 @@ filtersContainer.addEventListener('click', ({ target }) => { activeFilter = target.getAttribute('filterid') || ''; refreshFilter(); refreshLibrary(); - window.location.hash = activeFilter + window.location.hash = activeFilter; }); var librarySearchInput = document.querySelector("#searchform input"); @@ -526,7 +554,7 @@ document.getElementById("installdefault").addEventListener("click",event=>{ upload(); }).catch(function() { Progress.hide({sticky:true}); - reject() + reject(); }); } upload(); @@ -541,3 +569,48 @@ document.getElementById("installdefault").addEventListener("click",event=>{ showToast("App Install failed, "+err,"error"); }); }); + +// Install all favoutrie apps in one go +document.getElementById("installfavourite").addEventListener("click",event=>{ + var defaultApps, appCount; + asyncLocalStorage.getItem(FAVOURITE).then(json=>{ + defaultApps = JSON.parse(json); + defaultApps = defaultApps.map( appid => appJSON.find(app=>app.id==appid) ); + if (defaultApps.some(x=>x===undefined)) + throw "Not all apps found"; + appCount = defaultApps.length; + return showPrompt("Install Defaults","Remove everything and install favourite apps?"); + }).then(() => { + return Comms.removeAllApps(); + }).then(()=>{ + Progress.hide({sticky:true}); + appsInstalled = []; + showToast(`Existing apps removed. Installing ${appCount} apps...`); + return new Promise((resolve,reject) => { + function upload() { + var app = defaultApps.shift(); + if (app===undefined) return resolve(); + Progress.show({title:`${app.name} (${appCount-defaultApps.length}/${appCount})`,sticky:true}); + Comms.uploadApp(app,"skip_reset").then((appJSON) => { + Progress.hide({sticky:true}); + if (appJSON) appsInstalled.push(appJSON); + showToast(`(${appCount-defaultApps.length}/${appCount}) ${app.name} Uploaded`); + upload(); + }).catch(function() { + Progress.hide({sticky:true}); + reject(); + }); + } + upload(); + }); + }).then(()=>{ + return Comms.setTime(); + }).then(()=>{ + showToast("Favourites apps successfully installed!","success"); + return getInstalledApps(true); + }).catch(err=>{ + Progress.hide({sticky:true}); + showToast("App Install failed, "+err,"error"); + }); +}); + diff --git a/js/ui.js b/js/ui.js index 616a92555..ea6885eac 100644 --- a/js/ui.js +++ b/js/ui.js @@ -86,6 +86,7 @@ function showToast(message, type) { var style = "toast-primary"; if (type=="success") style = "toast-success"; else if (type=="error") style = "toast-error"; + else if (type=="warning") style = "toast-warning"; else if (type!==undefined) console.log("showToast: unknown toast "+type); var toastcontainer = document.getElementById("toastcontainer"); var msgDiv = htmlElement(`
`); diff --git a/js/utils.js b/js/utils.js index 85b6eb0a1..4913c7129 100644 --- a/js/utils.js +++ b/js/utils.js @@ -67,3 +67,16 @@ function getVersionInfo(appListing, appInstalled) { canUpdate : canUpdate } } + +const asyncLocalStorage = { + setItem: function (key, value) { + return Promise.resolve().then(function () { + localStorage.setItem(key, value); + }); + }, + getItem: function (key) { + return Promise.resolve().then(function () { + return localStorage.getItem(key); + }); + } +};