Merge pull request #1 from espruino/master

update 20200415
pull/305/head
ps-igel 2020-04-15 16:45:07 +02:00 committed by GitHub
commit dbdb4a5b76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
74 changed files with 2013 additions and 158 deletions

View File

@ -6,4 +6,7 @@ Changed for individual apps are listed in `apps/appname/ChangeLog`
* `Remove All Apps` now doesn't perform a reset before erase - fixes inability to update firmware if settings are wrong
* 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)
* Fix issue removing an app that was just installed (fix #253)
* Add `Favourite` functionality
* Version number now clickable even when you're at the latest version (fix #291)
* Rewrite 'getInstalledApps' to minimize RAM usage

119
apps.json
View File

@ -78,7 +78,7 @@
{ "id": "welcome",
"name": "Welcome",
"icon": "app.png",
"version":"0.06",
"version":"0.07",
"description": "Appears at first boot and explains how to use Bangle.js",
"tags": "start,welcome",
"allow_emulator":true,
@ -86,13 +86,14 @@
{"name":"welcome.boot.js","url":"boot.js"},
{"name":"welcome.app.js","url":"app.js"},
{"name":"welcome.settings.js","url":"settings.js"},
{"name":"welcome.settings.json","url":"settings-default.json","evaluate":true},
{"name":"welcome.img","url":"app-icon.js","evaluate":true}
]
},
{ "id": "gbridge",
"name": "Gadgetbridge",
"icon": "app.png",
"version":"0.08",
"version":"0.09",
"description": "The default notification handler for Gadgetbridge notifications from Android",
"tags": "tool,system,android,widget",
"type":"widget",
@ -119,7 +120,7 @@
{ "id": "setting",
"name": "Settings",
"icon": "settings.png",
"version":"0.12",
"version":"0.15",
"description": "A menu for setting up Bangle.js",
"tags": "tool,system",
"storage": [
@ -279,7 +280,7 @@
{ "id": "gpsrec",
"name": "GPS Recorder",
"icon": "app.png",
"version":"0.06",
"version":"0.07",
"interface": "interface.html",
"description": "Application that allows you to record a GPS track. Can run in background",
"tags": "tool,outdoors,gps,widget",
@ -329,7 +330,7 @@
{ "id": "widbat",
"name": "Battery Level Widget",
"icon": "widget.png",
"version":"0.04",
"version":"0.05",
"description": "Show the current battery level and charging status in the top right of the clock",
"tags": "widget,battery",
"type":"widget",
@ -341,7 +342,7 @@
"name": "Battery Level Widget (with percentage)",
"shortName": "Battery Widget",
"icon": "widget.png",
"version":"0.08",
"version":"0.09",
"description": "Show the current battery level and charging status in the top right of the clock, with charge percentage",
"tags": "widget,battery",
"type":"widget",
@ -354,7 +355,7 @@
{ "id": "widbt",
"name": "Bluetooth Widget",
"icon": "widget.png",
"version":"0.03",
"version":"0.04",
"description": "Show the current Bluetooth connection status in the top right of the clock",
"tags": "widget,bluetooth",
"type":"widget",
@ -362,6 +363,18 @@
{"name":"widbt.wid.js","url":"widget.js"}
]
},
{ "id": "widram",
"name": "RAM Widget",
"shortName":"RAM Widget",
"icon": "widget.png",
"version":"0.01",
"description": "Display your Bangle's available RAM percentage in a widget",
"tags": "widget",
"type": "widget",
"storage": [
{"name":"widram.wid.js","url":"widget.js"}
]
},
{ "id": "hrm",
"name": "Heart Rate Monitor",
"icon": "heartrate.png",
@ -504,13 +517,14 @@
"id": "ncstart",
"name": "NCEU Startup",
"icon": "start.png",
"version":"0.03",
"version":"0.04",
"description": "NodeConfEU 2019 'First Start' Sequence",
"tags": "start,welcome",
"storage": [
{"name":"ncstart.app.js","url":"start.js"},
{"name":"ncstart.boot.js","url":"boot.js"},
{"name":"ncstart.settings.js","url":"settings.js"},
{"name":"ncstart.settings.json","url":"settings-default.json","evaluate":true},
{"name":"ncstart.img","url":"start-icon.js","evaluate":true},
{"name":"nc-bangle.img","url":"start-bangle.js","evaluate":true},
{"name":"nc-nceu.img","url":"start-nceu.js","evaluate":true},
@ -775,7 +789,7 @@
{ "id": "widclk",
"name": "Digital clock widget",
"icon": "widget.png",
"version":"0.03",
"version":"0.04",
"description": "A simple digital clock widget",
"tags": "widget,clock",
"type":"widget",
@ -915,7 +929,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 +1122,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 +1176,9 @@
},
{ "id": "batchart",
"name": "Battery Chart",
"shortName":"BatChart",
"shortName":"Battery Chart",
"icon": "app.png",
"version":"0.03",
"version":"0.07",
"description": "A widget and an app for recording and visualizing battery percentage over time.",
"tags": "app,widget,battery,time,record,chart,tool",
"storage": [
@ -1158,5 +1186,72 @@
{"name":"batchart.app.js","url":"app.js"},
{"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.02",
"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",
"icon": "bledetect.png",
"version":"0.02",
"description": "Detect BLE devices and show some informations.",
"tags": "app,bluetooth,tool",
"readme": "README.md",
"storage": [
{"name":"bledetect.app.js","url":"bledetect.js"},
{"name":"bledetect.img","url":"bledetect-icon.js","evaluate":true}
]
},
{ "id": "snake",
"name": "Snake",
"shortName":"Snake",
"icon": "snake.png",
"version":"0.02",
"description": "The classic snake game. Eat apples and don't bite your tail.",
"tags": "game,fun",
"readme": "README.md",
"storage": [
{"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}
]
}
]

BIN
apps/activepedom/10600.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 B

BIN
apps/activepedom/1600.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 B

BIN
apps/activepedom/600.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 B

View File

@ -0,0 +1 @@
0.01: New Widget!

View File

@ -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/

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwIGDvAEDgP+ApMD/4FVEZY1FABcP8AFDn/wAod/AocB//4AoUHAokPAokf5/8AocfAoc+j5HDvgFEvEf7+AAoP4AoJCC+E/54qCsE/wYkDn+AAos8AohZDj/AAohrEp4FEs5xEuJfDgF5Aon4GgYFBGgZOBnyJD+EeYgfgj4FEh6VD4AFDh+AAIJMCBoIFFLQQtBgYFCHIIFDjA3BC4I="))

BIN
apps/activepedom/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 836 B

View File

@ -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);
});

180
apps/activepedom/widget.js Normal file
View File

@ -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};
})();

View File

@ -1,3 +1,7 @@
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
0.06: Fixes widget events and charting of component states
0.07: Improve logging and charting of component states and add widget icon

View File

@ -1,20 +1,213 @@
// 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;
const 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);
if(topUpLines) {
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) {
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 (parseInt(dataInfo[switchabelsIndex]) & switchableConsumers.compass) {
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 (parseInt(dataInfo[switchabelsIndex]) & switchableConsumers.gps) {
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 (parseInt(dataInfo[switchabelsIndex]) & switchableConsumers.hrm) {
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 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 renderBatteryChart() {
renderCoordinateSystem();
let data = loadData();
renderData(data);
data = null;
}
// Show launcher when middle button pressed
function switchOffApp(){
Bangle.showLauncher();
}
// special function to handle display switch on
@ -22,13 +215,20 @@ 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();
}
});
setWatch(switchOffApp, BTN2, {edge:"rising", debounce:50, repeat:true});
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
// call your app function here
renderHomeIcon();
renderBatteryChart();

View File

@ -1,5 +1,7 @@
(() => {
var switchableConsumers = {
const Storage = require("Storage");
const switchableConsumers = {
none: 0,
lcd: 1,
compass: 2,
@ -8,59 +10,106 @@
hrm: 16
};
var settings = {};
var batChartFile; // file for battery percentage recording
const recordingInterval10Min = 60 * 10 * 1000;
const recordingInterval1Min = 60*1000; //For testing
const recordingInterval10S = 10*1000; //For testing
const recordingInterval1Min = 60*1000; //For testing
const recordingInterval10S = 10*1000; //For testing
var recordingInterval = null;
var compassEventReceived = false;
var gpsEventReceived = false;
var hrmEventReceived = false;
// draw your widget
function draw() {
let x = this.x;
let y = this.y;
g.setColor(0, 1, 0);
g.fillPoly([x+5, y, x+5, y+4, x+1, y+4, x+1, y+20, x+18, y+20, x+18, y+4, x+13, y+4, x+13, y], true);
g.setColor(0,0,0);
g.drawPoly([x+5, y+6, x+8, y+12, x+13, y+12, x+16, y+18], false);
g.reset();
g.drawString("BC", this.x, this.y);
}
function onMag(){
compassEventReceived = true;
// Stop handling events when no longer necessarry
Bangle.removeListener("mag", onMag);
}
function onGps() {
gpsEventReceived = true;
Bangle.removeListener("GPS", onGps);
}
function onHrm() {
hrmEventReceived = true;
Bangle.removeListener("HRM", onHrm);
}
function getEnabledConsumersValue() {
var enabledConsumers = switchableConsumers.none;
Bangle.on('mag', onMag);
Bangle.on('GPS', onGps);
Bangle.on('HRM', onHrm);
// Wait two seconds, that should be enough for each of the events to get raised once
setTimeout(() => {
Bangle.removeAllListeners();
}, 2000);
if (Bangle.isLCDOn())
enabledConsumers = enabledConsumers | switchableConsumers.lcd;
// Already added in the hope they will be available soon to get more details
// if (Bangle.isCompassOn())
// enabledConsumers = enabledConsumers | switchableConsumers.compass;
// if (Bangle.isBluetoothOn())
if (compassEventReceived)
enabledConsumers = enabledConsumers | switchableConsumers.compass;
if (gpsEventReceived)
enabledConsumers = enabledConsumers | switchableConsumers.gps;
if (hrmEventReceived)
enabledConsumers = enabledConsumers | switchableConsumers.hrm;
//if (Bangle.isBluetoothOn())
// enabledConsumers = enabledConsumers | switchableConsumers.bluetooth;
// if (Bangle.isGpsOn())
// enabledConsumers = enabledConsumers | switchableConsumers.gps;
// if (Bangle.isHrmOn())
// enabledConsumers = enabledConsumers | switchableConsumers.hrm;
return enabledConsumers;
// Reset the event registration vars
compassEventReceived = false;
gpsEventReceived = false;
hrmEventReceived = false;
return enabledConsumers.toString();
}
function logBatteryData() {
const previousWriteLogName = "bcprvday";
const previousWriteDay = require("Storage").read(previousWriteLogName);
const previousWriteDay = parseInt(Storage.open(previousWriteLogName, "r").readLine());
const currentWriteDay = new Date().getDay();
const logFileName = "bclog" + currentWriteDay;
// Change log target on day change
if (previousWriteDay != currentWriteDay) {
if (!isNaN(previousWriteDay)
&& previousWriteDay != currentWriteDay) {
//Remove a log file containing data from a week ago
require("Storage").erase(logFileName);
require("Storage").write(previousWriteLogName, currentWriteDay);
Storage.open(logFileName, "r").erase();
Storage.open(previousWriteLogName, "w").write(parseInt(currentWriteDay));
}
var bcLogFileA = require("Storage").open(logFileName, "a");
var bcLogFileA = Storage.open(logFileName, "a");
if (bcLogFileA) {
console.log([getTime().toFixed(0), E.getBattery(), E.getTemperature(), getEnabledConsumersValue()].join(","));
bcLogFileA.write([[getTime().toFixed(0), E.getBattery(), E.getTemperature(), getEnabledConsumersValue()].join(",")].join(",")+"\n");
let logTime = getTime().toFixed(0);
let logPercent = E.getBattery();
let logTemperature = E.getTemperature();
let logConsumers = getEnabledConsumersValue();
let logString = [logTime, logPercent, logTemperature, logConsumers].join(",");
bcLogFileA.write(logString + "\n");
}
}
// Called by the heart app to reload settings and decide what's
function reload() {
WIDGETS["batchart"].width = 24;
@ -74,6 +123,7 @@
reload();
Bangle.drawWidgets(); // relayout all widgets
}};
// load settings, set correct widget width
reload();
})()

2
apps/bledetect/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: New App!
0.02: Fixed issue with wrong device informations

14
apps/bledetect/README.md Normal file
View File

@ -0,0 +1,14 @@
# BLE Detector
BLE Detector it's an app born for testing purpose that aim to show as informations as possible about near BLE devices.
## Features
BLE Detector shows:
- Device name (if available)
- Received Signal Strength Indication (RSSI)
- Manufacturer
- MAC Address
More informations will coming with future versions.

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwgJGhGAEKuIxAXXGCoXBGCoXCDCgXDJKYXDGCYUBhAwUFgQwPEogTCGBwNFFYYYNHwoEGJJQlFCIgKCdR4XHJBQNEI6IOFO6IPEDQYGDahoYEa6BJFxBFPJJIuQGAouRGAoWSGAgXTSIoAEgUgL6cCkQACDJCOFGAYWDAAJFLX4gWFGA4sFC40gJQYuHwBEDAQISCMYowEFgoJDCAwYBAwZYEC45AEgIHERAgXMA4i4FC6bPDC4hXFC5B7FC57CHI54XIawgXRVwS/JC5SuDC4wGGC45HBFAQRCAooXIVwYRBAAoXLLIwAFC5IuDGCIuFDAyQLABphKABgwaC6owB"))

View File

@ -0,0 +1,59 @@
let menu = {
"": { "title": "BLE Detector" },
"RE-SCAN": () => scan()
};
function showMainMenu() {
menu["< Back"] = () => load();
return E.showMenu(menu);
}
function showDeviceInfo(device){
const deviceMenu = {
"": { "title": "Device Info" },
"name": {
value: device.name
},
"rssi": {
value: device.rssi
},
"manufacturer": {
value: device.manufacturer
}
};
deviceMenu[device.id] = () => {};
deviceMenu["< Back"] = () => showMainMenu();
return E.showMenu(deviceMenu);
}
function scan() {
menu = {
"": { "title": "BLE Detector" },
"RE-SCAN": () => scan()
};
waitMessage();
NRF.findDevices(devices => {
devices.forEach(device =>{
let deviceName = device.id.substring(0,17);
if (device.name) {
deviceName = device.name;
}
menu[deviceName] = () => showDeviceInfo(device);
});
showMainMenu(menu);
}, { active: true });
}
function waitMessage() {
E.showMenu();
E.showMessage("scanning");
}
scan();
waitMessage();

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -0,0 +1 @@
0.01: New App!

352
apps/calculator/app.js Normal file
View File

@ -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});

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwhBC/AC8r6/XlYvr64CEF9UrMIIv/R/7vTMwIAmlUklQGDroAFqwHGBRgJBqwMDq+k5nNABAWDC4QZFERAvGBQOBF5I0FCYNW1mImWs6+sDoQsDAYIJEAAeB2eB1mBA4QvF43P6/GF4mB6+BAQYlEro3BAAI3FDAezBYgvE43O64DBF4hbCAAMrGAIiFBYRUEHogaBxA6CF4vXLwPHF4giEDIIkDDgI2BFoI6FBgYWCF5PPF4rSBKwVWI4bAFFgdcYAykBX5HX53NFwfNfwIkDAQYAGBBAKCIIYABd4y9DAAJ9CAD9dF4gAGCIi8BABLXBBRQLEF4vHRwgvEERQ6DHpgvH66PB65fUBpZfJ4/G6wxBMIaPbL5QvB6/WF6hqNF5KPDF6jkGd6JeBF5AAdF4oAGDBeH1mHAAwIBF8esABQvdWQonDX4YvIYAq/GXobvNF4hfKCwwvF43GF5AXGL44vJLwgvE453DMIYuFR5JiHI4yPHRoaREIwpIFF7TvbR5BJCX5IvMADgvcroABF6vG4wvIX46DKBZYvEFwPHGAgZHERALRF4YuBHYIwEFxxfPF5CDDF6ZfLDAyPFFwovFKRYvV47vDAgIvRR5aOFL4orCFwbvHADYvEAA4YLdRYvQ45eBR5C6UF5vHX4LvJF8PGZYXXGAYvnLYYvfZ4xfXd6AvKGAK/RDAKNTF4wAG44="))

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -7,3 +7,4 @@
0.06: Gadgetbridge App 'Connected' state is no longer toggleable
0.07: Move configuration to settings menu
0.08: Don't turn on LCD at start of every song
0.09: Update Bluetooth connection state automatically

View File

@ -189,8 +189,8 @@
g.flip(); // turns screen on
}
NRF.on("connected", changedConnectionState);
NRF.on("disconnected", changedConnectionState);
NRF.on("connect", changedConnectionState);
NRF.on("disconnect", changedConnectionState);
WIDGETS["gbridgew"] = { area: "tl", width: 24, draw: draw };

View File

@ -4,3 +4,4 @@
0.04: Properly Fix GPS time display in gpsrec app
0.05: Tweaks for variable size widget system
0.06: Ensure widget update itself (fix #118) and change to using icons
0.07: Added @jeffmer's awesome track viewer

View File

@ -70,27 +70,65 @@ function viewTracks() {
return E.showMenu(menu);
}
function getTrackInfo(fn) {
var filename = getFN(fn);
var minLat = 90;
var maxLat = -90;
var minLong = 180;
var maxLong = -180;
var starttime, duration=0;
var f = require("Storage").open(filename,"r");
if (f===undefined) return;
var l = f.readLine(f);
var nl = 0, c, n;
if (l!==undefined) {
c = l.split(",");
starttime = parseInt(c[0]);
}
// pushed this loop together to try and bump loading speed a little
while(l!==undefined) {
++nl;c=l.split(",");
n = parseFloat(c[1]);if(n>maxLat)maxLat=n;if(n<minLat)minLat=n;
n = parseFloat(c[2]);if(n>maxLong)maxLong=n;if(n<minLong)minLong=n;
l = f.readLine(f);
}
if (c) duration = parseInt(c[0]) - starttime;
var lfactor = Math.cos(minLat*Math.PI/180);
var ylen = (maxLat-minLat);
var xlen = (maxLong-minLong)* lfactor;
var scale = xlen>ylen ? 200/xlen : 200/ylen;
return {
fn : fn,
filename : filename,
time : new Date(starttime),
records : nl,
minLat : minLat, maxLat : maxLat,
minLong : minLong, maxLong : maxLong,
lfactor : lfactor,
scale : scale,
duration : Math.round(duration/1000)
};
}
function asTime(v){
var mins = Math.floor(v/60);
var secs = v-mins*60;
return ""+mins.toString()+"m "+secs.toString()+"s";
}
function viewTrack(n) {
E.showMessage("Loading...","GPS Track "+n);
var info = getTrackInfo(n);
const menu = {
'': { 'title': 'GPS Track '+n }
};
var trackCount = 0;
var trackTime;
var f = require("Storage").open(getFN(n),"r");
var l = f.readLine();
if (l!==undefined) {
var c = l.split(",");
trackTime = new Date(parseInt(c[0]));
}
while (l!==undefined) {
trackCount++;
// TODO: min/max/length of track?
l = f.readLine();
}
if (trackTime)
menu[" "+trackTime.toISOString().substr(0,16).replace("T"," ")] = function(){};
menu[trackCount+" records"] = function(){};
// TODO: option to draw it? Just scan through, project using min/max
if (info.time)
menu[info.time.toISOString().substr(0,16).replace("T"," ")] = function(){};
menu["Duration"] = { value : asTime(info.duration)};
menu["Records"] = { value : ""+info.records };
menu['Plot'] = function() {
plotTrack(info);
};
menu['Erase'] = function() {
E.showPrompt("Delete Track?").then(function(v) {
if (v) {
@ -107,4 +145,80 @@ function viewTrack(n) {
return E.showMenu(menu);
}
function plotTrack(info) {
function xcoord(long){
return 30 + Math.round((long-info.minLong)*info.lfactor*info.scale);
}
function ycoord(lat){
return 210 - Math.round((lat - info.minLat)*info.scale);
}
function radians(a) {
return a*Math.PI/180;
}
function distance(lat1,long1,lat2,long2){
var x = radians(long1-long2) * Math.cos(radians((lat1+lat2)/2));
var y = radians(lat2-lat1);
return Math.sqrt(x*x + y*y) * 6371000;
}
E.showMenu(); // remove menu
g.setColor(1,0.5,0.5);
g.setFont("Vector",16);
g.fillRect(9,80,11,120);
g.fillPoly([9,60,19,80,0,80]);
g.setColor(1,1,1);
g.drawString("N",2,40);
g.drawString("Track"+info.fn.toString()+" - Loading",10,220);
g.setColor(0,0,0);
g.fillRect(0,220,239,239);
g.setColor(1,1,1);
g.drawString(asTime(info.duration),10,220);
var f = require("Storage").open(info.filename,"r");
if (f===undefined) return;
var l = f.readLine(f);
var ox=0;
var oy=0;
var olat,olong,dist=0;
var first = true;
var i=0;
while(l!==undefined) {
var c = l.split(",");
var lat = parseFloat(c[1]);
var long = parseFloat(c[2]);
var x = xcoord(long);
var y = ycoord(lat);
if (first) {
g.moveTo(x,y);
g.setColor(0,1,0);
g.fillCircle(x,y,5);
g.setColor(1,1,1);
first = false;
} else if (x!=ox || y!=oy) {
g.lineTo(x,y);
}
if (!first) {
var d = distance(olat,olong,lat,long);
if (!isNaN(d)) dist+=d;
}
olat = lat;
olong = long;
ox = x;
oy = y;
l = f.readLine(f);
}
g.setColor(1,0,0);
g.fillCircle(ox,oy,5);
g.setColor(1,1,1);
g.drawString(require("locale").distance(dist),120,220);
g.setFont("6x8",2);
g.setFontAlign(0,0,3);
g.drawString("Back",230,200);
setWatch(function() {
viewTrack(info.fn);
}, BTN3);
}
showMainMenu();

View File

@ -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
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%)

View File

@ -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

View File

@ -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()
init();

1
apps/nato/changelog.txt Normal file
View File

@ -0,0 +1 @@
0.01: New App!

1
apps/nato/nato-icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwgFCiIABiAGFiINJAAUS///CAgGEgMT//zBoYXFmIiCC40fEooXF+QXJn4lCC5ARDC4oFC//xMAoXDJAQXFBgY9DC4wKCC4p2CPA4XDCQQXEOwXxPA4XBEQJICC4p2BmICCC44KBJAIXEiIJBkMvPAwXCWgYXFAgQMBPAoXCBwUxC4jtDeI4XDJAQXDFYXxHAoXGJAYXDLYPykUieIwXDJAYXDG4IAEPAgXCRgJICPYoAEPAgXDZ4TcDmYXGMAgXDUAZiEPwIABCALEBC5BZC+YQCRwRsEC45ID+S5BCAkBEYJ4DC4hID+IbCIAYjCCIYXGEgMxXoJwEgI3CA4JQDAAwaBmQGDFIQ3CC5UzkSLBdwIIDmYXCWY4jBCAJBCPYQ0EC5bXGkLuDC5QtEAAXzPoZMCmZwB+YFCbYkykQFCVoZMDWALnDQwRjDeoZIDZAgJCWwYeBFATWFC5LuHawgXKdwyJDD4YXIOAMzH4gICmIXKEwQXXkQXFKAKQFC85HNO64XDU44XMX48Sa5zvCmJICA4YXLE4fziIACJ4PyM4gXHCAQwBCwI2GC5JADAApGFC5ERmYWFFwwXHDARJCMgYWFB4MTmYiFLgMjCwMyiIuGE4QABNIyPDBQgA=="))

106
apps/nato/nato.js Normal file
View File

@ -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 });

BIN
apps/nato/nato.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -2,3 +2,6 @@
Renamed as nodeconf-specific
0.03: Move configuration into App/widget settings
Move loader into welcome.boot.js
0.04: Run again when updated
Don't run again when settings app is updated (or absent)
Add "Run Now" option to settings

View File

@ -1,9 +1,11 @@
(function() {
let s = require('Storage').readJSON('setting.json', 1) || {}
let s = require('Storage').readJSON('ncstart.settings.json', 1)
|| require('Storage').readJSON('setting.json', 1)
|| {welcomed: true} // do NOT run if global settings are also absent
if (!s.welcomed && require('Storage').read('ncstart.app.js')) {
setTimeout(() => {
s.welcomed = true
require('Storage').write('setting.json', s)
require('Storage').write('ncstart.settings.json', s)
load('ncstart.app.js')
})
}

View File

@ -0,0 +1,3 @@
{
"welcomed": false
}

View File

@ -1,16 +1,15 @@
// The welcome app is special, and gets to use global settings
(function(back) {
let settings = require('Storage').readJSON('setting.json', 1) || {}
let settings = require('Storage').readJSON('ncstart.settings.json', 1)
|| require('Storage').readJSON('setting.json', 1) || {}
E.showMenu({
'': { 'title': 'NCEU Startup' },
'Run again': {
'Run on Next Boot': {
value: !settings.welcomed,
format: v => v ? 'Yes' : 'No',
onchange: v => {
settings.welcomed = v ? undefined : true
require('Storage').write('setting.json', settings)
},
format: v => v ? 'OK' : 'No',
onchange: v => require('Storage').write('ncstart.settings.json', {welcomed: !v}),
},
'Run Now': () => load('ncstart.app.js'),
'< Back': back,
})
})

2
apps/numerals/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: New App!
0.02: Use BTN2 for settings menu like other clocks

17
apps/numerals/README.md Normal file
View File

@ -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

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwhC/ABMBzIADyAJIAAkQBoMZBIoXCBIwADyIkIGAIuKGAQkIBJIwEEKQANC/4XWR58RiIHFWpAXFe4QRFcpAXFewQRFcxAXEFwQwGA4QXKiAXDGAgX/C/4X/C/4X/C7uQCwcBBwYXNBwYuEC54wCFwgXPzMRiIHFC54AHC/4XiCAoXRhIHDyK3GAAwOBJA0QG45VGC4YwCD4YwKFwgABcgIfEAwIAHBwgA/AAgA=="))

View File

@ -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;i<numerals[num].length;i++){
if (i>0) 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, BTN2, {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();

BIN
apps/numerals/numerals.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -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);
})

View File

@ -12,3 +12,8 @@
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
0.14: Reduce memory usage when running app settings page
0.15: Reduce memory usage when running default clock chooser (#294)

View File

@ -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() {
@ -296,10 +296,10 @@ function makeConnectable() {
});
}
function showClockMenu() {
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);
var clockApps = require("Storage").list(/\.info$/)
.map(app => {var a=storage.readJSON(app, 1);return (a&&a.type == "clock")?a:undefined})
.filter(app => app) // filter out any undefined apps
.sort((a, b) => a.sortorder - b.sortorder);
const clockMenu = {
'': {
'title': 'Select Clock',
@ -325,8 +325,6 @@ function showClockMenu() {
return E.showMenu(clockMenu);
}
function showSetTimeMenu() {
d = new Date();
const timemenu = {
@ -419,8 +417,8 @@ function showAppSettingsMenu() {
'< Back': ()=>showMainMenu(),
}
const apps = storage.list(/\.info$/)
.map(app => storage.readJSON(app, 1))
.filter(app => app && app.settings)
.map(app => {var a=storage.readJSON(app, 1);return (a&&a.settings)?a:undefined})
.filter(app => app) // filter out any undefined apps
.sort((a, b) => a.sortorder - b.sortorder)
if (apps.length === 0) {
appmenu['No app has settings'] = () => { };
@ -450,7 +448,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');

2
apps/snake/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: New App!
0.02: Performance and graphic improvements, game pause, beep and buzz

14
apps/snake/README.md Normal file
View File

@ -0,0 +1,14 @@
# Snake
![Screenshot](https://i.imgur.com/bXQjxhB.png)
The legentary classic game is now available on Bangle.js!
Eat apples and don't bite your tail.
## Controls
- UP: BTN1
- DOWN: BTN3
- LEFT: BTN4
- RIGHT: BTN5
- PAUSE: BTN2

1
apps/snake/snake-icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4A/ADE3m9hsIusrdhGIM3LtU3g0GAgQxlEwIqBmEAgEGF4QwkF4c3F4MxF4dbF8qLDrYHDre74IABF8QwBLoaPDF8wPKF96/jF/4v/F/4vrrc3AIQsnsIAKF94wiFxgv/R//+m4ABrYALBwIpYFwwAQLC4v/F7gXGF91hACovWFqwwUF4VbF7IwUFzSRVF1gwCF9wwZFyoA/AH4A/AH4A/AGg="))

155
apps/snake/snake.js Normal file
View File

@ -0,0 +1,155 @@
Bangle.setLCDMode("120x120");
const H = g.getWidth();
const W = g.getHeight();
let running = true;
let score = 0;
let d;
const gridSize = 20;
const tileSize = 6;
let nextX = 0;
let nextY = 0;
const defaultTailSize = 3;
let tailSize = defaultTailSize;
const snakeTrail = [];
const snake = { x: 10, y: 10 };
const apple = { x: Math.floor(Math.random() * gridSize), y: Math.floor(Math.random() * gridSize) };
function drawBackground(){
g.setColor("#000000");
g.fillRect(0, 0, H, W);
}
function drawApple(){
g.setColor("#FF0000");
g.fillCircle((apple.x * tileSize) + tileSize/2, (apple.y * tileSize) + tileSize/2, tileSize/2);
}
function drawSnake(){
g.setColor("#008000");
for (let i = 0; i < snakeTrail.length; i++) {
g.fillRect(snakeTrail[i].x * tileSize, snakeTrail[i].y * tileSize, snakeTrail[i].x * tileSize + tileSize, snakeTrail[i].y * tileSize + tileSize);
//snake bites it's tail
if (snakeTrail[i].x === snake.x && snakeTrail[i].y === snake.y && tailSize > defaultTailSize) {
Bangle.buzz(1000);
gameOver();
}
}
}
function drawScore(){
g.setColor("#FFFFFF");
g.setFont("6x8");
g.setFontAlign(0, 0);
g.drawString("Score:" + score, W / 2, 10);
}
function gameStart() {
running = true;
score = 0;
}
function gameOver() {
g.clear();
g.setColor("#FFFFFF");
g.setFont("6x8");
g.drawString("GAME OVER!", W / 2, H / 2 - 10);
g.drawString("Tap to Restart", W / 2, H / 2 + 10);
running = false;
tailSize = defaultTailSize;
}
function draw() {
if (!running) {
return;
}
g.clear();
// move snake in next pos
snake.x += nextX;
snake.y += nextY;
// snake over game world
if (snake.x < 0) {
snake.x = gridSize - 1;
}
if (snake.x > gridSize - 1) {
snake.x = 0;
}
if (snake.y < 0) {
snake.y = gridSize - 1;
}
if (snake.y > gridSize - 1) {
snake.y = 0;
}
//snake bite apple
if (snake.x === apple.x && snake.y === apple.y) {
Bangle.beep(20);
tailSize++;
score++;
apple.x = Math.floor(Math.random() * gridSize);
apple.y = Math.floor(Math.random() * gridSize);
drawApple();
}
drawBackground();
drawApple();
drawSnake();
drawScore();
//set snake trail
snakeTrail.push({ x: snake.x, y: snake.y });
while (snakeTrail.length > tailSize) {
snakeTrail.shift();
}
g.flip();
}
// input
setWatch(() => {// Up
if (d !== 'd') {
nextX = 0;
nextY = -1;
d = 'u';
}
}, BTN1, { repeat: true });
setWatch(() => {// Down
if (d !== 'u') {
nextX = 0;
nextY = 1;
d = 'd';
}
}, BTN3, { repeat: true });
setWatch(() => {// Left
if (d !== 'r') {
nextX = -1;
nextY = 0;
d = 'l';
}
}, BTN4, { repeat: true });
setWatch(() => {// Right
if (d !== 'l') {
nextX = 1;
nextY = 0;
d = 'r';
}
}, BTN5, { repeat: true });
setWatch(() => {// Pause
running = !running;
}, BTN2, { repeat: true });
Bangle.on('touch', button => {
if (!running) {
gameStart();
}
});
// render X times per second
const x = 5;
setInterval(draw, 1000 / x);

BIN
apps/snake/snake.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -4,3 +4,6 @@
0.04: Fix regression after tweaks to Storage.readJSON
0.05: Move configuration into App/widget settings
0.06: Move loader into welcome.boot.js
0.07: Run again when updated
Don't run again when settings app is updated (or absent)
Add "Run Now" option to settings

View File

@ -288,6 +288,13 @@ setWatch(()=>{
}, BTN2, {repeat:true,edge:"rising"});
setWatch(()=>move(-1), BTN1, {repeat:true});
(function migrateSettings(){
let global_settings = require('Storage').readJSON('setting.json', 1)
if (global_settings) {
delete global_settings.welcomed
require('Storage').write('setting.json', global_settings)
}
})()
Bangle.setLCDTimeout(0);
Bangle.setLCDPower(1);

View File

@ -1,9 +1,11 @@
(function() {
let s = require('Storage').readJSON('setting.json', 1) || {}
let s = require('Storage').readJSON('welcome.settings.json', 1)
|| require('Storage').readJSON('setting.json', 1)
|| {welcomed: true} // do NOT run if global settings are also absent
if (!s.welcomed && require('Storage').read('welcome.app.js')) {
setTimeout(() => {
s.welcomed = true
require('Storage').write('setting.json', s)
require('Storage').write('welcome.settings.json', {welcomed: "yes"})
load('welcome.app.js')
})
}

View File

@ -0,0 +1,3 @@
{
"welcomed": false
}

View File

@ -1,16 +1,15 @@
// The welcome app is special, and gets to use global settings
(function(back) {
let settings = require('Storage').readJSON('setting.json', 1) || {}
let settings = require('Storage').readJSON('welcome.settings.json', 1)
|| require('Storage').readJSON('setting.json', 1) || {}
E.showMenu({
'': { 'title': 'Welcome App' },
'Run again': {
'Run on Next Boot': {
value: !settings.welcomed,
format: v => v ? 'Yes' : 'No',
onchange: v => {
settings.welcomed = v ? undefined : true
require('Storage').write('setting.json', settings)
},
format: v => v ? 'OK' : 'No',
onchange: v => require('Storage').write('welcome.settings.json', {welcomed: !v}),
},
'Run Now': () => load('welcome.app.js'),
'< Back': back,
})
})

View File

@ -1,3 +1,4 @@
0.02: Now refresh battery monitor every minute if LCD on
0.03: Tweaks for variable size widget system
0.04: Ensure redrawing works with variable size widget system
0.05: Fix regression stopping correct widget updates

View File

@ -30,7 +30,7 @@ Bangle.on('lcdPower', function(on) {
WIDGETS["bat"].draw();
// refresh once a minute if LCD on
if (!batteryInterval)
batteryInterval = setInterval(draw, 60000);
batteryInterval = setInterval(()=>WIDGETS["bat"].draw(), 60000);
} else {
if (batteryInterval) {
clearInterval(batteryInterval);

View File

@ -5,3 +5,4 @@
0.06: Show battery percentage as text
0.07: Add settings: percentage/color/charger icon
0.08: Draw percentage as inverted on monochrome battery
0.09: Fix regression stopping correct widget updates

View File

@ -110,7 +110,7 @@ Bangle.on('lcdPower', function(on) {
WIDGETS["batpc"].draw();
// refresh once a minute if LCD on
if (!batteryInterval)
batteryInterval = setInterval(draw, 60000);
batteryInterval = setInterval(()=>WIDGETS["batpc"].draw(), 60000);
} else {
if (batteryInterval) {
clearInterval(batteryInterval);

View File

@ -1,2 +1,3 @@
0.02: Tweaks for variable size widget system
0.03: Ensure redrawing works with variable size widget system
0.04: Fix automatic update of Bluetooth connection status

View File

@ -13,7 +13,7 @@ function changed() {
WIDGETS["bluetooth"].draw();
g.flip();// turns screen on
}
NRF.on('connected',changed);
NRF.on('disconnected',changed);
NRF.on('connect',changed);
NRF.on('disconnect',changed);
WIDGETS["bluetooth"]={area:"tr",width:24,draw:draw};
})()

View File

@ -1,2 +1,3 @@
0.02: Now refresh battery monitor every minute if LCD on
0.03: Ensure redrawing works with variable size widget system
0.04: Fix regression stopping correct widget updates

View File

@ -14,7 +14,7 @@
}
}
function startTimers(){
intervalRef = setInterval(draw, 60*1000);
intervalRef = setInterval(()=>WIDGETS["wdclk"].draw(), 60*1000);
WIDGETS["wdclk"].draw();
}
Bangle.on('lcdPower', (on) => {
@ -23,5 +23,5 @@
});
WIDGETS["wdclk"]={area:"tr",width:width,draw:draw};
if (Bangle.isLCDOn) intervalRef = setInterval(draw, 60*1000);
if (Bangle.isLCDOn) intervalRef = setInterval(()=>WIDGETS["wdclk"].draw(), 60*1000);
})()

1
apps/widram/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: New Widget!

23
apps/widram/widget.js Normal file
View File

@ -0,0 +1,23 @@
(() => {
function draw() {
g.reset();
var m = process.memory();
var pc = Math.round(m.usage*100/m.total);
g.drawImage(atob("BwgBqgP////AVQ=="), this.x+(24-7)/2, this.y+4);
g.setColor(pc>70 ? "#ff0000" : (pc>50 ? "#ffff00" : "#ffffff"));
g.setFont("6x8").setFontAlign(0,0).drawString(pc+"%", this.x+12, this.y+20, true/*solid*/);
}
var ramInterval;
Bangle.on('lcdPower', function(on) {
if (on) {
WIDGETS["ram"].draw();
if (!ramInterval) ramInterval = setInterval(()=>WIDGETS["ram"].draw(), 10000);
} else {
if (ramInterval) {
clearInterval(ramInterval);
ramInterval = undefined;
}
}
});
WIDGETS["ram"]={area:"tl",width: 24,draw:draw};
})()

BIN
apps/widram/widget.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 B

View File

@ -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}`);
}
});

View File

@ -96,6 +96,7 @@
<label class="chip" filterid="widget">Widgets</label>
<label class="chip" filterid="bluetooth">Bluetooth</label>
<label class="chip" filterid="outdoors">Outdoors</label>
<label class="chip" filterid="favourites">Favourites</label>
</div>
<div class="panel">
<div class="panel-header">
@ -134,7 +135,8 @@
<h3>Utilities</h3>
<p><button class="btn" id="settime">Set Bangle.js Time</button>
<button class="btn" id="removeall">Remove all Apps</button>
<button class="btn" id="installdefault">Install default apps</button></p>
<button class="btn" id="installdefault">Install default apps</button>
<button class="btn" id="installfavourite">Install favourite apps</button></p>
</div>
</div>

View File

@ -40,7 +40,7 @@ uploadApp : (app,skipReset) => { // expects an apps.json structure (i.e. with `s
currentBytes += f.content.length;
// Chould check CRC here if needed instead of returning 'OK'...
// E.CRC32(require("Storage").read(${JSON.stringify(app.name)}))
Puck.write(`\x10${f.cmd};Bluetooth.println("OK")\n`,(result) => {
Puck.write(`${f.cmd};Bluetooth.println("OK")\n`,(result) => {
if (!result || result.trim()!="OK") {
Progress.hide({sticky:true});
return reject("Unexpected response "+(result||""));
@ -75,12 +75,21 @@ getInstalledApps : () => {
Progress.hide({sticky:true});
return reject("");
}
Puck.eval('require("Storage").list(/\.info$/).map(f=>{var j=require("Storage").readJSON(f,1)||{};j.id=f.slice(0,-5);return j})', (appList,err) => {
Puck.write('\x10Bluetooth.print("[");require("Storage").list(/\.info$/).forEach(f=>{var j=require("Storage").readJSON(f,1)||{};j.id=f.slice(0,-5);Bluetooth.print(JSON.stringify(j)+",")});Bluetooth.println("0]")\n', (appList,err) => {
Progress.hide({sticky:true});
try {
appList = JSON.parse(appList);
// remove last element since we added a final '0'
// to make things easy on the Bangle.js side
appList = appList.slice(0,-1);
} catch (e) {
appList = null;
err = e.toString();
}
if (appList===null) return reject(err || "");
console.log("getInstalledApps", appList);
resolve(appList);
});
}, true /* callback on newline */);
});
});
},

View File

@ -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 = " <small>("+versionInfo+")</small>";
var readme = `<a href="#" onclick="showReadme('${app.id}')">Read more...</a>`;
var favourite = favourites.find(e => e == app.id);
return `<div class="tile column col-6 col-sm-12 col-xs-12">
<div class="tile-icon">
<figure class="avatar"><img src="apps/${app.icon?`${app.id}/${app.icon}`:"unknown.png"}" alt="${escapeHtml(app.name)}"></figure><br/>
@ -195,7 +218,8 @@ function refreshLibrary() {
<p class="tile-subtitle">${escapeHtml(app.description)}${app.readme?`<br/>${readme}`:""}</p>
<a href="https://github.com/espruino/BangleApps/tree/master/apps/${app.id}" target="_blank" class="link-github"><img src="img/github-icon-sml.png" alt="See the code on GitHub"/></a>
</div>
<div class="tile-action">
<div class="tile-action">
<button class="btn btn-link btn-action btn-lg ${!app.custom?"text-error":"d-hide"}" appid="${app.id}" title="Favorite"><i class="icon"></i>${favourite?"&#x2665;":"&#x2661;"}</button>
<button class="btn btn-link btn-action btn-lg ${(appInstalled&&app.interface)?"":"d-hide"}" appid="${app.id}" title="Download data from app"><i class="icon icon-download"></i></button>
<button class="btn btn-link btn-action btn-lg ${app.allow_emulator?"":"d-hide"}" appid="${app.id}" title="Try in Emulator"><i class="icon icon-share"></i></button>
<button class="btn btn-link btn-action btn-lg ${version.canUpdate?"":"d-hide"}" appid="${app.id}" title="Update App"><i class="icon icon-refresh"></i></button>
@ -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 `<div class="tile column col-6 col-sm-12 col-xs-12">
// 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 `<div class="tile column col-6 col-sm-12 col-xs-12">
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");
});
});

View File

@ -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(`<div class="toast ${style}"></div>`);

View File

@ -56,7 +56,7 @@ function getVersionInfo(appListing, appInstalled) {
if (appListing.version)
versionText = clicky("v"+appListing.version);
} else {
versionText = (appInstalled.version ? ("v"+appInstalled.version) : "Unknown version");
versionText = (appInstalled.version ? (clicky("v"+appInstalled.version)) : "Unknown version");
if (appListing.version != appInstalled.version) {
if (appListing.version) versionText += ", latest "+clicky("v"+appListing.version);
canUpdate = true;
@ -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);
});
}
};