Merge branch 'master' of https://github.com/espruino/BangleApps
|
@ -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
|
||||
|
|
85
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",
|
||||
|
@ -1158,5 +1172,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.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",
|
||||
"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.01",
|
||||
"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}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
After Width: | Height: | Size: 370 B |
After Width: | Height: | Size: 374 B |
After Width: | Height: | Size: 338 B |
|
@ -0,0 +1 @@
|
|||
0.01: New Widget!
|
|
@ -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
|
||||

|
||||
|
||||
* 1600 steps
|
||||

|
||||
|
||||
* 10600 steps
|
||||

|
||||
|
||||
## 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/
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwIGDvAEDgP+ApMD/4FVEZY1FABcP8AFDn/wAod/AocB//4AoUHAokPAokf5/8AocfAoc+j5HDvgFEvEf7+AAoP4AoJCC+E/54qCsE/wYkDn+AAos8AohZDj/AAohrEp4FEs5xEuJfDgF5Aon4GgYFBGgZOBnyJD+EeYgfgj4FEh6VD4AFDh+AAIJMCBoIFFLQQtBgYFCHIIFDjA3BC4I="))
|
After Width: | Height: | Size: 836 B |
|
@ -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);
|
||||
});
|
|
@ -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};
|
||||
|
||||
})();
|
|
@ -0,0 +1,2 @@
|
|||
0.01: New App!
|
||||
0.02: Fixed issue with wrong device informations
|
|
@ -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.
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwgJGhGAEKuIxAXXGCoXBGCoXCDCgXDJKYXDGCYUBhAwUFgQwPEogTCGBwNFFYYYNHwoEGJJQlFCIgKCdR4XHJBQNEI6IOFO6IPEDQYGDahoYEa6BJFxBFPJJIuQGAouRGAoWSGAgXTSIoAEgUgL6cCkQACDJCOFGAYWDAAJFLX4gWFGA4sFC40gJQYuHwBEDAQISCMYowEFgoJDCAwYBAwZYEC45AEgIHERAgXMA4i4FC6bPDC4hXFC5B7FC57CHI54XIawgXRVwS/JC5SuDC4wGGC45HBFAQRCAooXIVwYRBAAoXLLIwAFC5IuDGCIuFDAyQLABphKABgwaC6owB"))
|
|
@ -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();
|
After Width: | Height: | Size: 4.1 KiB |
|
@ -0,0 +1 @@
|
|||
0.01: New App!
|
|
@ -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});
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwhBC/AC8r6/XlYvr64CEF9UrMIIv/R/7vTMwIAmlUklQGDroAFqwHGBRgJBqwMDq+k5nNABAWDC4QZFERAvGBQOBF5I0FCYNW1mImWs6+sDoQsDAYIJEAAeB2eB1mBA4QvF43P6/GF4mB6+BAQYlEro3BAAI3FDAezBYgvE43O64DBF4hbCAAMrGAIiFBYRUEHogaBxA6CF4vXLwPHF4giEDIIkDDgI2BFoI6FBgYWCF5PPF4rSBKwVWI4bAFFgdcYAykBX5HX53NFwfNfwIkDAQYAGBBAKCIIYABd4y9DAAJ9CAD9dF4gAGCIi8BABLXBBRQLEF4vHRwgvEERQ6DHpgvH66PB65fUBpZfJ4/G6wxBMIaPbL5QvB6/WF6hqNF5KPDF6jkGd6JeBF5AAdF4oAGDBeH1mHAAwIBF8esABQvdWQonDX4YvIYAq/GXobvNF4hfKCwwvF43GF5AXGL44vJLwgvE453DMIYuFR5JiHI4yPHRoaREIwpIFF7TvbR5BJCX5IvMADgvcroABF6vG4wvIX46DKBZYvEFwPHGAgZHERALRF4YuBHYIwEFxxfPF5CDDF6ZfLDAyPFFwovFKRYvV47vDAgIvRR5aOFL4orCFwbvHADYvEAA4YLdRYvQ45eBR5C6UF5vHX4LvJF8PGZYXXGAYvnLYYvfZ4xfXd6AvKGAK/RDAKNTF4wAG44="))
|
After Width: | Height: | Size: 10 KiB |
|
@ -7,3 +7,6 @@
|
|||
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.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%)
|
|
@ -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
|
||||
|
|
|
@ -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();
|
|
@ -0,0 +1 @@
|
|||
0.01: New App!
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwgFCiIABiAGFiINJAAUS///CAgGEgMT//zBoYXFmIiCC40fEooXF+QXJn4lCC5ARDC4oFC//xMAoXDJAQXFBgY9DC4wKCC4p2CPA4XDCQQXEOwXxPA4XBEQJICC4p2BmICCC44KBJAIXEiIJBkMvPAwXCWgYXFAgQMBPAoXCBwUxC4jtDeI4XDJAQXDFYXxHAoXGJAYXDLYPykUieIwXDJAYXDG4IAEPAgXCRgJICPYoAEPAgXDZ4TcDmYXGMAgXDUAZiEPwIABCALEBC5BZC+YQCRwRsEC45ID+S5BCAkBEYJ4DC4hID+IbCIAYjCCIYXGEgMxXoJwEgI3CA4JQDAAwaBmQGDFIQ3CC5UzkSLBdwIIDmYXCWY4jBCAJBCPYQ0EC5bXGkLuDC5QtEAAXzPoZMCmZwB+YFCbYkykQFCVoZMDWALnDQwRjDeoZIDZAgJCWwYeBFATWFC5LuHawgXKdwyJDD4YXIOAMzH4gICmIXKEwQXXkQXFKAKQFC85HNO64XDU44XMX48Sa5zvCmJICA4YXLE4fziIACJ4PyM4gXHCAQwBCwI2GC5JADAApGFC5ERmYWFFwwXHDARJCMgYWFB4MTmYiFLgMjCwMyiIuGE4QABNIyPDBQgA=="))
|
|
@ -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 });
|
After Width: | Height: | Size: 2.0 KiB |
|
@ -0,0 +1 @@
|
|||
0.01: New App!
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwhC/ABMBzIADyAJIAAkQBoMZBIoXCBIwADyIkIGAIuKGAQkIBJIwEEKQANC/4XWR58RiIHFWpAXFe4QRFcpAXFewQRFcxAXEFwQwGA4QXKiAXDGAgX/C/4X/C/4X/C7uQCwcBBwYXNBwYuEC54wCFwgXPzMRiIHFC54AHC/4XiCAoXRhIHDyK3GAAwOBJA0QG45VGC4YwCD4YwKFwgABcgIfEAwIAHBwgA/AAgA=="))
|
|
@ -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, 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();
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -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);
|
||||
})
|
|
@ -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
|
|
@ -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');
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: New App!
|
|
@ -0,0 +1,13 @@
|
|||
# Snake
|
||||
|
||||

|
||||
|
||||
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
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4A/ADE3m9hsIusrdhGIM3LtU3g0GAgQxlEwIqBmEAgEGF4QwkF4c3F4MxF4dbF8qLDrYHDre74IABF8QwBLoaPDF8wPKF96/jF/4v/F/4vrrc3AIQsnsIAKF94wiFxgv/R//+m4ABrYALBwIpYFwwAQLC4v/F7gXGF91hACovWFqwwUF4VbF7IwUFzSRVF1gwCF9wwZFyoA/AH4A/AH4A/AGg="))
|
|
@ -0,0 +1,144 @@
|
|||
const H = g.getWidth();
|
||||
const W = g.getHeight();
|
||||
let running = true;
|
||||
let score = 0;
|
||||
let d;
|
||||
|
||||
// game world
|
||||
const gridSize = 40;
|
||||
const tileSize = 6;
|
||||
let nextX = 0;
|
||||
let nextY = 0;
|
||||
|
||||
// snake
|
||||
const defaultTailSize = 3;
|
||||
let tailSize = defaultTailSize;
|
||||
const snakeTrail = [];
|
||||
let snakeX = 10;
|
||||
let snakeY = 10;
|
||||
|
||||
// apple
|
||||
let appleX = Math.floor(Math.random() * gridSize);
|
||||
let appleY = Math.floor(Math.random() * gridSize);
|
||||
|
||||
function gameStart() {
|
||||
running = true;
|
||||
score = 0;
|
||||
}
|
||||
|
||||
function gameStop() {
|
||||
g.clear();
|
||||
g.setColor("#FFFFFF");
|
||||
g.setFont("6x8", 2);
|
||||
g.drawString("GAME OVER!", W / 2, H / 2 - 20);
|
||||
g.drawString("Tap to Restart", W / 2, H / 2 + 20);
|
||||
running = false;
|
||||
tailSize = defaultTailSize;
|
||||
}
|
||||
|
||||
function draw() {
|
||||
if (!running) {
|
||||
return;
|
||||
}
|
||||
|
||||
// move snake in next pos
|
||||
snakeX += nextX;
|
||||
snakeY += nextY;
|
||||
|
||||
// snake over game world?
|
||||
if (snakeX < 0) {
|
||||
snakeX = gridSize - 1;
|
||||
}
|
||||
|
||||
if (snakeX > gridSize - 1) {
|
||||
snakeX = 0;
|
||||
}
|
||||
|
||||
if (snakeY < 0) {
|
||||
snakeY = gridSize - 1;
|
||||
}
|
||||
if (snakeY > gridSize - 1) {
|
||||
snakeY = 0;
|
||||
}
|
||||
|
||||
//snake bite apple?
|
||||
if (snakeX === appleX && snakeY === appleY) {
|
||||
tailSize++;
|
||||
score++;
|
||||
|
||||
appleX = Math.floor(Math.random() * gridSize);
|
||||
appleY = Math.floor(Math.random() * gridSize);
|
||||
}
|
||||
|
||||
//paint background
|
||||
g.setColor("#000000");
|
||||
g.fillRect(0, 0, H, W);
|
||||
|
||||
// paint snake
|
||||
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 === snakeX && snakeTrail[i].y === snakeY && tailSize > defaultTailSize) {
|
||||
gameStop();
|
||||
}
|
||||
}
|
||||
|
||||
// paint apple
|
||||
g.setColor("#FF0000");
|
||||
g.fillRect(appleX * tileSize, appleY * tileSize, appleX * tileSize + tileSize, appleY * tileSize + tileSize);
|
||||
|
||||
// paint score
|
||||
g.setColor("#FFFFFF");
|
||||
g.setFont("6x8");
|
||||
g.setFontAlign(0, 0);
|
||||
g.drawString("Score:" + score, W / 2, 10);
|
||||
|
||||
//set snake trail
|
||||
snakeTrail.push({ x: snakeX, y: snakeY });
|
||||
while (snakeTrail.length > tailSize) {
|
||||
snakeTrail.shift();
|
||||
}
|
||||
}
|
||||
|
||||
// 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 });
|
||||
|
||||
Bangle.on('touch', button => {
|
||||
if (!running) {
|
||||
gameStart();
|
||||
}
|
||||
});
|
||||
|
||||
// render X times per second
|
||||
var x = 5;
|
||||
setInterval(draw, 1000 / x);
|
After Width: | Height: | Size: 2.3 KiB |
|
@ -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}`);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
115
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 = " <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/>
|
||||
|
@ -196,6 +219,7 @@ function refreshLibrary() {
|
|||
<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">
|
||||
<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?"♥":"♡"}</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");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
1
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(`<div class="toast ${style}"></div>`);
|
||||
|
|
13
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);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|