Merge branch 'espruino:master' into sleeplog
|
@ -1,6 +1,11 @@
|
||||||
# Active Pedometer
|
# Active Pedometer
|
||||||
|
|
||||||
Pedometer that filters out arm movement and displays a step goal progress.
|
Pedometer that filters out arm movement and displays a step goal progress.
|
||||||
|
|
||||||
|
**Note:** Since creation of this app, Bangle.js's step counting algorithm has
|
||||||
|
improved significantly - and as a result the algorithm in this app (which
|
||||||
|
runs *on top* of Bangle.js's algorithm) may no longer be accurate.
|
||||||
|
|
||||||
I changed the step counting algorithm completely.
|
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.
|
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.
|
To get in 'active' mode, you have to reach the step threshold before the active timer runs out.
|
||||||
|
@ -9,6 +14,7 @@ When you reach the step threshold, the steps needed to reach the threshold are c
|
||||||
Steps are saved to a datafile every 5 minutes. You can watch a graph using the app.
|
Steps are saved to a datafile every 5 minutes. You can watch a graph using the app.
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
* 600 steps
|
* 600 steps
|
||||||

|

|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"name": "Active Pedometer",
|
"name": "Active Pedometer",
|
||||||
"shortName": "Active Pedometer",
|
"shortName": "Active Pedometer",
|
||||||
"version": "0.09",
|
"version": "0.09",
|
||||||
"description": "Pedometer that filters out arm movement and displays a step goal progress. Steps are saved to a daily file and can be viewed as graph.",
|
"description": "(NOT RECOMMENDED) Pedometer that filters out arm movement and displays a step goal progress. Steps are saved to a daily file and can be viewed as graph. The `Health` app now provides step logging and graphs.",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"tags": "outdoors,widget",
|
"tags": "outdoors,widget",
|
||||||
"supports": ["BANGLEJS"],
|
"supports": ["BANGLEJS"],
|
||||||
|
|
|
@ -5,3 +5,5 @@
|
||||||
0.05: Displaying calendar colour and name
|
0.05: Displaying calendar colour and name
|
||||||
0.06: Added clkinfo for clocks.
|
0.06: Added clkinfo for clocks.
|
||||||
0.07: Clkinfo improvements.
|
0.07: Clkinfo improvements.
|
||||||
|
0.08: Fix error in clkinfo (didn't require Storage & locale)
|
||||||
|
Fix clkinfo icon
|
||||||
|
|
|
@ -1,24 +1,24 @@
|
||||||
(function() {
|
(function() {
|
||||||
var agendaItems = {
|
var agendaItems = {
|
||||||
name: "Agenda",
|
name: "Agenda",
|
||||||
img: atob("GBiBAf////////85z/AAAPAAAPgAAP////AAAPAAAPAAAPAAAOAAAeAAAeAAAcAAA8AAAoAABgAADP//+P//8PAAAPAAAPgAAf///w=="),
|
img: atob("GBiBAAAAAAAAAADGMA///w///wf//wAAAA///w///w///w///x///h///h///j///D///X//+f//8wAABwAADw///w///wf//gAAAA=="),
|
||||||
items: []
|
items: []
|
||||||
};
|
};
|
||||||
|
var locale = require("locale");
|
||||||
var now = new Date();
|
var now = new Date();
|
||||||
var agenda = storage.readJSON("android.calendar.json")
|
var agenda = require("Storage").readJSON("android.calendar.json")
|
||||||
.filter(ev=>ev.timestamp + ev.durationInSeconds > now/1000)
|
.filter(ev=>ev.timestamp + ev.durationInSeconds > now/1000)
|
||||||
.sort((a,b)=>a.timestamp - b.timestamp);
|
.sort((a,b)=>a.timestamp - b.timestamp);
|
||||||
|
|
||||||
agenda.forEach((entry, i) => {
|
agenda.forEach((entry, i) => {
|
||||||
|
|
||||||
var title = entry.title.slice(0,18);
|
var title = entry.title.slice(0,12);
|
||||||
var date = new Date(entry.timestamp*1000);
|
var date = new Date(entry.timestamp*1000);
|
||||||
var dateStr = locale.date(date).replace(/\d\d\d\d/,"");
|
var dateStr = locale.date(date).replace(/\d\d\d\d/,"");
|
||||||
dateStr += entry.durationInSeconds < 86400 ? "/ " + locale.time(date,1) : "";
|
dateStr += entry.durationInSeconds < 86400 ? "/ " + locale.time(date,1) : "";
|
||||||
|
|
||||||
agendaItems.items.push({
|
agendaItems.items.push({
|
||||||
name: null,
|
name: "Agenda "+i,
|
||||||
get: () => ({ text: title + "\n" + dateStr, img: null}),
|
get: () => ({ text: title + "\n" + dateStr, img: null}),
|
||||||
show: function() { agendaItems.items[i].emit("redraw"); },
|
show: function() { agendaItems.items[i].emit("redraw"); },
|
||||||
hide: function () {}
|
hide: function () {}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "agenda",
|
"id": "agenda",
|
||||||
"name": "Agenda",
|
"name": "Agenda",
|
||||||
"version": "0.07",
|
"version": "0.08",
|
||||||
"description": "Simple agenda",
|
"description": "Simple agenda",
|
||||||
"icon": "agenda.png",
|
"icon": "agenda.png",
|
||||||
"screenshots": [{"url":"screenshot_agenda_overview.png"}, {"url":"screenshot_agenda_event1.png"}, {"url":"screenshot_agenda_event2.png"}],
|
"screenshots": [{"url":"screenshot_agenda_overview.png"}, {"url":"screenshot_agenda_event1.png"}, {"url":"screenshot_agenda_event2.png"}],
|
||||||
|
|
|
@ -15,3 +15,5 @@
|
||||||
0.15: Allow method/body/headers to be specified for `http` (needs Gadgetbridge 0.68.0b or later)
|
0.15: Allow method/body/headers to be specified for `http` (needs Gadgetbridge 0.68.0b or later)
|
||||||
0.16: Bangle.http now fails immediately if there is no Bluetooth connection (fix #2152)
|
0.16: Bangle.http now fails immediately if there is no Bluetooth connection (fix #2152)
|
||||||
0.17: Now kick off Calendar sync as soon as connected to Gadgetbridge
|
0.17: Now kick off Calendar sync as soon as connected to Gadgetbridge
|
||||||
|
0.18: Use new message library
|
||||||
|
If connected to Gadgetbridge, allow GPS forwarding from phone (Gadgetbridge code still not merged)
|
|
@ -20,6 +20,8 @@ It contains:
|
||||||
of Gadgetbridge - making your phone make noise so you can find it.
|
of Gadgetbridge - making your phone make noise so you can find it.
|
||||||
* `Keep Msgs` - default is `Off`. When Gadgetbridge disconnects, should Bangle.js
|
* `Keep Msgs` - default is `Off`. When Gadgetbridge disconnects, should Bangle.js
|
||||||
keep any messages it has received, or should it delete them?
|
keep any messages it has received, or should it delete them?
|
||||||
|
* `Overwrite GPS` - when GPS is requested by an app, this doesn't use Bangle.js's GPS
|
||||||
|
but instead asks Gadgetbridge on the phone to use the phone's GPS
|
||||||
* `Messages` - launches the messages app, showing a list of messages
|
* `Messages` - launches the messages app, showing a list of messages
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
|
@ -126,6 +126,18 @@
|
||||||
request.j(event.err); //r = reJect function
|
request.j(event.err); //r = reJect function
|
||||||
else
|
else
|
||||||
request.r(event); //r = resolve function
|
request.r(event); //r = resolve function
|
||||||
|
},
|
||||||
|
"gps": function() {
|
||||||
|
const settings = require("Storage").readJSON("android.settings.json",1)||{};
|
||||||
|
if (!settings.overwriteGps) return;
|
||||||
|
delete event.t;
|
||||||
|
event.satellites = NaN;
|
||||||
|
event.course = NaN;
|
||||||
|
event.fix = 1;
|
||||||
|
Bangle.emit('gps', event);
|
||||||
|
},
|
||||||
|
"is_gps_active": function() {
|
||||||
|
gbSend({ t: "gps_power", status: Bangle._PWR && Bangle._PWR.GPS && Bangle._PWR.GPS.length>0 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var h = HANDLERS[event.t];
|
var h = HANDLERS[event.t];
|
||||||
|
@ -189,6 +201,30 @@
|
||||||
if (isFinite(msg.id)) return gbSend({ t: "notify", n:response?"OPEN":"DISMISS", id: msg.id });
|
if (isFinite(msg.id)) return gbSend({ t: "notify", n:response?"OPEN":"DISMISS", id: msg.id });
|
||||||
// error/warn here?
|
// error/warn here?
|
||||||
};
|
};
|
||||||
|
// GPS overwrite logic
|
||||||
|
if (settings.overwriteGps) { // if the overwrite option is set../
|
||||||
|
// Save current logic
|
||||||
|
const originalSetGpsPower = Bangle.setGPSPower;
|
||||||
|
// Replace set GPS power logic to suppress activation of gps (and instead request it from the phone)
|
||||||
|
Bangle.setGPSPower = (isOn, appID) => {
|
||||||
|
// if not connected, use old logic
|
||||||
|
if (!NRF.getSecurityStatus().connected) return originalSetGpsPower(isOn, appID);
|
||||||
|
// Emulate old GPS power logic
|
||||||
|
if (!Bangle._PWR) Bangle._PWR={};
|
||||||
|
if (!Bangle._PWR.GPS) Bangle._PWR.GPS=[];
|
||||||
|
if (!appID) appID="?";
|
||||||
|
if (isOn && !Bangle._PWR.GPS.includes(appID)) Bangle._PWR.GPS.push(appID);
|
||||||
|
if (!isOn && Bangle._PWR.GPS.includes(appID)) Bangle._PWR.GPS.splice(Bangle._PWR.GPS.indexOf(appID),1);
|
||||||
|
let pwr = Bangle._PWR.GPS.length>0;
|
||||||
|
gbSend({ t: "gps_power", status: pwr });
|
||||||
|
return pwr;
|
||||||
|
}
|
||||||
|
// Replace check if the GPS is on to check the _PWR variable
|
||||||
|
Bangle.isGPSOn = () => {
|
||||||
|
return Bangle._PWR && Bangle._PWR.GPS && Bangle._PWR.GPS.length>0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// remove settings object so it's not taking up RAM
|
// remove settings object so it's not taking up RAM
|
||||||
delete settings;
|
delete settings;
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -2,11 +2,11 @@
|
||||||
"id": "android",
|
"id": "android",
|
||||||
"name": "Android Integration",
|
"name": "Android Integration",
|
||||||
"shortName": "Android",
|
"shortName": "Android",
|
||||||
"version": "0.17",
|
"version": "0.18",
|
||||||
"description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.",
|
"description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"tags": "tool,system,messages,notifications,gadgetbridge",
|
"tags": "tool,system,messages,notifications,gadgetbridge",
|
||||||
"dependencies": {"messages":"app"},
|
"dependencies": {"messages":"module"},
|
||||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||||
"readme": "README.md",
|
"readme": "README.md",
|
||||||
"storage": [
|
"storage": [
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
(function(back) {
|
(function(back) {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function gb(j) {
|
function gb(j) {
|
||||||
Bluetooth.println(JSON.stringify(j));
|
Bluetooth.println(JSON.stringify(j));
|
||||||
}
|
}
|
||||||
|
@ -23,7 +26,17 @@
|
||||||
updateSettings();
|
updateSettings();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
/*LANG*/"Messages" : ()=>load("messages.app.js"),
|
/*LANG*/"Overwrite GPS" : {
|
||||||
|
value : !!settings.overwriteGps,
|
||||||
|
onchange: newValue => {
|
||||||
|
if (newValue) {
|
||||||
|
Bangle.setGPSPower(false, 'android');
|
||||||
|
}
|
||||||
|
settings.overwriteGps = newValue;
|
||||||
|
updateSettings();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/*LANG*/"Messages" : ()=>require("message").openGUI(),
|
||||||
};
|
};
|
||||||
E.showMenu(mainmenu);
|
E.showMenu(mainmenu);
|
||||||
})
|
})
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
0.01: New App!
|
0.01: New App!
|
||||||
|
0.02: Add lightning
|
||||||
|
|
|
@ -46,6 +46,9 @@ var booster = { x : g.getWidth()/4 + Math.random()*g.getWidth()/2,
|
||||||
var exploded = false;
|
var exploded = false;
|
||||||
var nExplosions = 0;
|
var nExplosions = 0;
|
||||||
var landed = false;
|
var landed = false;
|
||||||
|
var lightning = 0;
|
||||||
|
|
||||||
|
var settings = require("Storage").readJSON('f9settings.json', 1) || {};
|
||||||
|
|
||||||
const gravity = 4;
|
const gravity = 4;
|
||||||
const dt = 0.1;
|
const dt = 0.1;
|
||||||
|
@ -61,18 +64,40 @@ function flameImageGen (throttle) {
|
||||||
|
|
||||||
function drawFalcon(x, y, throttle, angle) {
|
function drawFalcon(x, y, throttle, angle) {
|
||||||
g.setColor(1, 1, 1).drawImage(falcon9, x, y, {rotate:angle});
|
g.setColor(1, 1, 1).drawImage(falcon9, x, y, {rotate:angle});
|
||||||
if (throttle>0) {
|
if (throttle>0 || lightning>0) {
|
||||||
var flameImg = flameImageGen(throttle);
|
var flameImg = flameImageGen(throttle);
|
||||||
var r = falcon9.height/2 + flameImg.height/2-1;
|
var r = falcon9.height/2 + flameImg.height/2-1;
|
||||||
var xoffs = -Math.sin(angle)*r;
|
var xoffs = -Math.sin(angle)*r;
|
||||||
var yoffs = Math.cos(angle)*r;
|
var yoffs = Math.cos(angle)*r;
|
||||||
if (Math.random()>0.7) g.setColor(1, 0.5, 0);
|
if (Math.random()>0.7) g.setColor(1, 0.5, 0);
|
||||||
else g.setColor(1, 1, 0);
|
else g.setColor(1, 1, 0);
|
||||||
g.drawImage(flameImg, x+xoffs, y+yoffs, {rotate:angle});
|
if (throttle>0) g.drawImage(flameImg, x+xoffs, y+yoffs, {rotate:angle});
|
||||||
|
if (lightning>1 && lightning<30) {
|
||||||
|
for (var i=0; i<6; ++i) {
|
||||||
|
var r = Math.random()*6;
|
||||||
|
var x = Math.random()*5 - xoffs;
|
||||||
|
var y = Math.random()*5 - yoffs;
|
||||||
|
g.setColor(1, Math.random()*0.5+0.5, 0).fillCircle(booster.x+x, booster.y+y, r);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function drawLightning() {
|
||||||
|
var c = {x:cloudOffs+50, y:30};
|
||||||
|
var dx = c.x-booster.x;
|
||||||
|
var dy = c.y-booster.y;
|
||||||
|
var m1 = {x:booster.x+0.6*dx+Math.random()*20, y:booster.y+0.6*dy+Math.random()*10};
|
||||||
|
var m2 = {x:booster.x+0.4*dx+Math.random()*20, y:booster.y+0.4*dy+Math.random()*10};
|
||||||
|
g.setColor(1, 1, 1).drawLine(c.x, c.y, m1.x, m1.y).drawLine(m1.x, m1.y, m2.x, m2.y).drawLine(m2.x, m2.y, booster.x, booster.y);
|
||||||
|
}
|
||||||
|
|
||||||
function drawBG() {
|
function drawBG() {
|
||||||
|
if (lightning==1) {
|
||||||
|
g.setBgColor(1, 1, 1).clear();
|
||||||
|
Bangle.buzz(200);
|
||||||
|
return;
|
||||||
|
}
|
||||||
g.setBgColor(0.2, 0.2, 1).clear();
|
g.setBgColor(0.2, 0.2, 1).clear();
|
||||||
g.setColor(0, 0, 1).fillRect(0, g.getHeight()-oceanHeight, g.getWidth()-1, g.getHeight()-1);
|
g.setColor(0, 0, 1).fillRect(0, g.getHeight()-oceanHeight, g.getWidth()-1, g.getHeight()-1);
|
||||||
g.setColor(0.5, 0.5, 1).fillCircle(cloudOffs+34, 30, 15).fillCircle(cloudOffs+60, 35, 20).fillCircle(cloudOffs+75, 20, 10);
|
g.setColor(0.5, 0.5, 1).fillCircle(cloudOffs+34, 30, 15).fillCircle(cloudOffs+60, 35, 20).fillCircle(cloudOffs+75, 20, 10);
|
||||||
|
@ -88,6 +113,7 @@ function renderScreen(input) {
|
||||||
drawBG();
|
drawBG();
|
||||||
showFuel();
|
showFuel();
|
||||||
drawFalcon(booster.x, booster.y, Math.floor(input.throttle*12), input.angle);
|
drawFalcon(booster.x, booster.y, Math.floor(input.throttle*12), input.angle);
|
||||||
|
if (lightning>1 && lightning<6) drawLightning();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInputs() {
|
function getInputs() {
|
||||||
|
@ -97,6 +123,7 @@ function getInputs() {
|
||||||
if (t > 1) t = 1;
|
if (t > 1) t = 1;
|
||||||
if (t < 0) t = 0;
|
if (t < 0) t = 0;
|
||||||
if (booster.fuel<=0) t = 0;
|
if (booster.fuel<=0) t = 0;
|
||||||
|
if (lightning>0 && lightning<20) t = 0;
|
||||||
return {throttle: t, angle: a};
|
return {throttle: t, angle: a};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,7 +148,6 @@ function gameStep() {
|
||||||
else {
|
else {
|
||||||
var input = getInputs();
|
var input = getInputs();
|
||||||
if (booster.y >= targetY) {
|
if (booster.y >= targetY) {
|
||||||
// console.log(booster.x + " " + booster.y + " " + booster.vy + " " + droneX + " " + input.angle);
|
|
||||||
if (Math.abs(booster.x-droneX-droneShip.width/2)<droneShip.width/2 && Math.abs(input.angle)<Math.PI/8 && booster.vy<maxV) {
|
if (Math.abs(booster.x-droneX-droneShip.width/2)<droneShip.width/2 && Math.abs(input.angle)<Math.PI/8 && booster.vy<maxV) {
|
||||||
renderScreen({angle:0, throttle:0});
|
renderScreen({angle:0, throttle:0});
|
||||||
epilogue("You landed!");
|
epilogue("You landed!");
|
||||||
|
@ -129,6 +155,8 @@ function gameStep() {
|
||||||
else exploded = true;
|
else exploded = true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
if (lightning) ++lightning;
|
||||||
|
if (settings.lightning && (lightning==0||lightning>40) && Math.random()>0.98) lightning = 1;
|
||||||
booster.x += booster.vx*dt;
|
booster.x += booster.vx*dt;
|
||||||
booster.y += booster.vy*dt;
|
booster.y += booster.vy*dt;
|
||||||
booster.vy += gravity*dt;
|
booster.vy += gravity*dt;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{ "id": "f9lander",
|
{ "id": "f9lander",
|
||||||
"name": "Falcon9 Lander",
|
"name": "Falcon9 Lander",
|
||||||
"shortName":"F9lander",
|
"shortName":"F9lander",
|
||||||
"version":"0.01",
|
"version":"0.02",
|
||||||
"description": "Land a rocket booster",
|
"description": "Land a rocket booster",
|
||||||
"icon": "f9lander.png",
|
"icon": "f9lander.png",
|
||||||
"screenshots" : [ { "url":"f9lander_screenshot1.png" }, { "url":"f9lander_screenshot2.png" }, { "url":"f9lander_screenshot3.png" }],
|
"screenshots" : [ { "url":"f9lander_screenshot1.png" }, { "url":"f9lander_screenshot2.png" }, { "url":"f9lander_screenshot3.png" }],
|
||||||
|
@ -10,6 +10,7 @@
|
||||||
"supports" : ["BANGLEJS", "BANGLEJS2"],
|
"supports" : ["BANGLEJS", "BANGLEJS2"],
|
||||||
"storage": [
|
"storage": [
|
||||||
{"name":"f9lander.app.js","url":"app.js"},
|
{"name":"f9lander.app.js","url":"app.js"},
|
||||||
{"name":"f9lander.img","url":"app-icon.js","evaluate":true}
|
{"name":"f9lander.img","url":"app-icon.js","evaluate":true},
|
||||||
|
{"name":"f9lander.settings.js", "url":"settings.js"}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
// This file should contain exactly one function, which shows the app's settings
|
||||||
|
/**
|
||||||
|
* @param {function} back Use back() to return to settings menu
|
||||||
|
*/
|
||||||
|
const boolFormat = v => v ? /*LANG*/"On" : /*LANG*/"Off";
|
||||||
|
(function(back) {
|
||||||
|
const SETTINGS_FILE = 'f9settings.json'
|
||||||
|
// initialize with default settings...
|
||||||
|
let settings = {
|
||||||
|
'lightning': false,
|
||||||
|
}
|
||||||
|
// ...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) {
|
||||||
|
settings[key] = saved[key];
|
||||||
|
}
|
||||||
|
// creates a function to safe a specific setting, e.g. save('color')(1)
|
||||||
|
function save(key) {
|
||||||
|
return function (value) {
|
||||||
|
settings[key] = value;
|
||||||
|
storage.write(SETTINGS_FILE, settings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const menu = {
|
||||||
|
'': { 'title': 'OpenWind' },
|
||||||
|
'< Back': back,
|
||||||
|
'Lightning': {
|
||||||
|
value: settings.lightning,
|
||||||
|
format: boolFormat,
|
||||||
|
onchange: save('lightning'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
E.showMenu(menu);
|
||||||
|
})
|
|
@ -2,3 +2,4 @@
|
||||||
0.02: Saved settings when switching color scheme
|
0.02: Saved settings when switching color scheme
|
||||||
0.03: Added Button 3 opening messages (if app is installed)
|
0.03: Added Button 3 opening messages (if app is installed)
|
||||||
0.04: Use `messages` library to check for new messages
|
0.04: Use `messages` library to check for new messages
|
||||||
|
0.05: Use `messages` library to open message GUI
|
|
@ -234,7 +234,7 @@ function handleMessages()
|
||||||
{
|
{
|
||||||
if(!hasMessages()) return;
|
if(!hasMessages()) return;
|
||||||
E.showMessage("Loading Messages...");
|
E.showMessage("Loading Messages...");
|
||||||
load("messages.app.js");
|
require("messages").openGUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasMessages()
|
function hasMessages()
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "hcclock",
|
"id": "hcclock",
|
||||||
"name": "Hi-Contrast Clock",
|
"name": "Hi-Contrast Clock",
|
||||||
"version": "0.04",
|
"version": "0.05",
|
||||||
"description": "Hi-Contrast Clock : A simple yet very bold clock that aims to be readable in high luninosity environments. Uses big 10x5 pixel digits. Use BTN 1 to switch background and foreground colors.",
|
"description": "Hi-Contrast Clock : A simple yet very bold clock that aims to be readable in high luninosity environments. Uses big 10x5 pixel digits. Use BTN 1 to switch background and foreground colors.",
|
||||||
"icon": "hcclock-icon.png",
|
"icon": "hcclock-icon.png",
|
||||||
"type": "clock",
|
"type": "clock",
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
# HRM Motion Artifacts removal
|
||||||
|
|
||||||
|
Measurements from the build in PPG-Sensor (Photoplethysmograph) is sensitive to motion and can be corrupted with Motion Artifacts (MA). This module allows to remove these.
|
||||||
|
|
||||||
|
## Settings
|
||||||
|
|
||||||
|
* **MA removal**
|
||||||
|
|
||||||
|
Select the algorithm to Remove Motion artifacts:
|
||||||
|
- None: (default) No Motion Artifact removal.
|
||||||
|
- fft elim: (*experimental*) Remove Motion Artifacts by cutting out the frequencies from the HRM frequency spectrum that are noisy in acceleration spectrum. Under motion this can report a heart rate that is closer to the real one but will fail if motion frequency and heart rate overlap.
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -0,0 +1,40 @@
|
||||||
|
{
|
||||||
|
let bpm_corrected; // result of algorithm
|
||||||
|
|
||||||
|
const updateHrm = (bpm) => {
|
||||||
|
bpm_corrected = bpm;
|
||||||
|
};
|
||||||
|
|
||||||
|
Bangle.on('HRM', (hrm) => {
|
||||||
|
if (bpm_corrected > 0) {
|
||||||
|
// replace bpm data in event
|
||||||
|
hrm.bpm_orig = hrm.bpm;
|
||||||
|
hrm.confidence_orig = hrm.confidence;
|
||||||
|
hrm.bpm = bpm_corrected;
|
||||||
|
hrm.confidence = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let run = () => {
|
||||||
|
const settings = Object.assign({
|
||||||
|
mAremoval: 0
|
||||||
|
}, require("Storage").readJSON("hrmmar.json", true) || {});
|
||||||
|
|
||||||
|
// select motion artifact removal algorithm
|
||||||
|
switch(settings.mAremoval) {
|
||||||
|
case 1:
|
||||||
|
require("hrmfftelim").run(settings, updateHrm);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// override setHRMPower so we can run our code on HRM enable
|
||||||
|
const oldSetHRMPower = Bangle.setHRMPower;
|
||||||
|
Bangle.setHRMPower = function(on, id) {
|
||||||
|
if (on && run !== undefined) {
|
||||||
|
run();
|
||||||
|
run = undefined; // Make sure we run only once
|
||||||
|
}
|
||||||
|
return oldSetHRMPower(on, id);
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,190 @@
|
||||||
|
exports.run = (settings, updateHrm) => {
|
||||||
|
const SAMPLE_RATE = 12.5;
|
||||||
|
const NUM_POINTS = 256; // fft size
|
||||||
|
const ACC_PEAKS = 2; // remove this number of ACC peaks
|
||||||
|
|
||||||
|
// ringbuffers
|
||||||
|
const hrmvalues = new Int16Array(8*SAMPLE_RATE);
|
||||||
|
const accvalues = new Int16Array(8*SAMPLE_RATE);
|
||||||
|
// fft buffers
|
||||||
|
const hrmfftbuf = new Int16Array(NUM_POINTS);
|
||||||
|
const accfftbuf = new Int16Array(NUM_POINTS);
|
||||||
|
let BPM_est_1 = 0;
|
||||||
|
let BPM_est_2 = 0;
|
||||||
|
|
||||||
|
let hrmdata;
|
||||||
|
let idx=0, wraps=0;
|
||||||
|
|
||||||
|
// init settings
|
||||||
|
Bangle.setOptions({hrmPollInterval: 40, powerSave: false}); // hrm=25Hz
|
||||||
|
Bangle.setPollInterval(80); // 12.5Hz
|
||||||
|
|
||||||
|
calcfft = (values, idx, normalize, fftbuf) => {
|
||||||
|
fftbuf.fill(0);
|
||||||
|
let i_out=0;
|
||||||
|
let avg = 0;
|
||||||
|
if (normalize) {
|
||||||
|
const sum = values.reduce((a, b) => a + b, 0);
|
||||||
|
avg = sum/values.length;
|
||||||
|
}
|
||||||
|
// sort ringbuffer to fft buffer
|
||||||
|
for(let i_in=idx; i_in<values.length; i_in++, i_out++) {
|
||||||
|
fftbuf[i_out] = values[i_in]-avg;
|
||||||
|
}
|
||||||
|
for(let i_in=0; i_in<idx; i_in++, i_out++) {
|
||||||
|
fftbuf[i_out] = values[i_in]-avg;
|
||||||
|
}
|
||||||
|
|
||||||
|
E.FFT(fftbuf);
|
||||||
|
return fftbuf;
|
||||||
|
};
|
||||||
|
|
||||||
|
getMax = (values) => {
|
||||||
|
let maxVal = -Number.MAX_VALUE;
|
||||||
|
let maxIdx = 0;
|
||||||
|
|
||||||
|
values.forEach((value,i) => {
|
||||||
|
if (value > maxVal) {
|
||||||
|
maxVal = value;
|
||||||
|
maxIdx = i;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {idx: maxIdx, val: maxVal};
|
||||||
|
};
|
||||||
|
|
||||||
|
getSign = (value) => {
|
||||||
|
return value < 0 ? -1 : 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
// idx in fft buffer to frequency
|
||||||
|
getFftFreq = (idx, rate, size) => {
|
||||||
|
return idx*rate/(size-1);
|
||||||
|
};
|
||||||
|
|
||||||
|
// frequency to idx in fft buffer
|
||||||
|
getFftIdx = (freq, rate, size) => {
|
||||||
|
return Math.round(freq*(size-1)/rate);
|
||||||
|
};
|
||||||
|
|
||||||
|
calc2ndDeriative = (values) => {
|
||||||
|
const result = new Int16Array(values.length-2);
|
||||||
|
for(let i=1; i<values.length-1; i++) {
|
||||||
|
const diff = values[i+1]-2*values[i]+values[i-1];
|
||||||
|
result[i-1] = diff;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const minFreqIdx = getFftIdx(1.0, SAMPLE_RATE, NUM_POINTS); // 60 BPM
|
||||||
|
const maxFreqIdx = getFftIdx(3.0, SAMPLE_RATE, NUM_POINTS); // 180 BPM
|
||||||
|
let rangeIdx = [0, maxFreqIdx-minFreqIdx]; // range of search for the next estimates
|
||||||
|
const freqStep=getFftFreq(1, SAMPLE_RATE, NUM_POINTS)*60;
|
||||||
|
const maxBpmDiffIdxDown = Math.ceil(5/freqStep); // maximum down BPM
|
||||||
|
const maxBpmDiffIdxUp = Math.ceil(10/freqStep); // maximum up BPM
|
||||||
|
|
||||||
|
calculate = (idx) => {
|
||||||
|
// fft
|
||||||
|
const ppg_fft = calcfft(hrmvalues, idx, true, hrmfftbuf).subarray(minFreqIdx, maxFreqIdx+1);
|
||||||
|
const acc_fft = calcfft(accvalues, idx, false, accfftbuf).subarray(minFreqIdx, maxFreqIdx+1);
|
||||||
|
|
||||||
|
// remove spectrum that have peaks in acc fft from ppg fft
|
||||||
|
const accGlobalMax = getMax(acc_fft);
|
||||||
|
const acc2nddiff = calc2ndDeriative(acc_fft); // calculate second derivative
|
||||||
|
for(let iClean=0; iClean < ACC_PEAKS; iClean++) {
|
||||||
|
// get max peak in ACC
|
||||||
|
const accMax = getMax(acc_fft);
|
||||||
|
|
||||||
|
if (accMax.val >= 10 && accMax.val/accGlobalMax.val > 0.75) {
|
||||||
|
// set all values in PPG FFT to zero until second derivative of ACC has zero crossing
|
||||||
|
for (let k = accMax.idx-1; k>=0; k--) {
|
||||||
|
ppg_fft[k] = 0;
|
||||||
|
acc_fft[k] = -Math.abs(acc_fft[k]); // max(acc_fft) should no longer find this
|
||||||
|
if (k-2 > 0 && getSign(acc2nddiff[k-1-2]) != getSign(acc2nddiff[k-2]) && Math.abs(acc_fft[k]) < accMax.val*0.75) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// set all values in PPG FFT to zero until second derivative of ACC has zero crossing
|
||||||
|
for (let k = accMax.idx; k < acc_fft.length-1; k++) {
|
||||||
|
ppg_fft[k] = 0;
|
||||||
|
acc_fft[k] = -Math.abs(acc_fft[k]); // max(acc_fft) should no longer find this
|
||||||
|
if (k-2 >= 0 && getSign(acc2nddiff[k+1-2]) != getSign(acc2nddiff[k-2]) && Math.abs(acc_fft[k]) < accMax.val*0.75) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// bpm result is maximum peak in PPG fft
|
||||||
|
const hrRangeMax = getMax(ppg_fft.subarray(rangeIdx[0], rangeIdx[1]));
|
||||||
|
const hrTotalMax = getMax(ppg_fft);
|
||||||
|
const maxDiff = hrTotalMax.val/hrRangeMax.val;
|
||||||
|
let idxMaxPPG = hrRangeMax.idx+rangeIdx[0]; // offset range limit
|
||||||
|
|
||||||
|
if ((maxDiff > 3 && idxMaxPPG != hrTotalMax.idx) || hrRangeMax.val === 0) { // prevent tracking from loosing the real heart rate by checking the full spectrum
|
||||||
|
if (hrTotalMax.idx > idxMaxPPG) {
|
||||||
|
idxMaxPPG = idxMaxPPG+Math.ceil(6/freqStep); // step 6 BPM up into the direction of max peak
|
||||||
|
} else {
|
||||||
|
idxMaxPPG = idxMaxPPG-Math.ceil(2/freqStep); // step 2 BPM down into the direction of max peak
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
idxMaxPPG = idxMaxPPG + minFreqIdx;
|
||||||
|
const BPM_est_0 = getFftFreq(idxMaxPPG, SAMPLE_RATE, NUM_POINTS)*60;
|
||||||
|
|
||||||
|
// smooth with moving average
|
||||||
|
let BPM_est_res;
|
||||||
|
if (BPM_est_2 > 0) {
|
||||||
|
BPM_est_res = 0.9*BPM_est_0 + 0.05*BPM_est_1 + 0.05*BPM_est_2;
|
||||||
|
} else {
|
||||||
|
BPM_est_res = BPM_est_0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return BPM_est_res.toFixed(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
Bangle.on('HRM-raw', (hrm) => {
|
||||||
|
hrmdata = hrm;
|
||||||
|
});
|
||||||
|
|
||||||
|
Bangle.on('accel', (acc) => {
|
||||||
|
if (hrmdata !== undefined) {
|
||||||
|
hrmvalues[idx] = hrmdata.filt;
|
||||||
|
accvalues[idx] = acc.x*1000 + acc.y*1000 + acc.z*1000;
|
||||||
|
idx++;
|
||||||
|
if (idx >= 8*SAMPLE_RATE) {
|
||||||
|
idx = 0;
|
||||||
|
wraps++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (idx % (SAMPLE_RATE*2) == 0) { // every two seconds
|
||||||
|
if (wraps === 0) { // use rate of firmware until hrmvalues buffer is filled
|
||||||
|
updateHrm(undefined);
|
||||||
|
BPM_est_2 = BPM_est_1;
|
||||||
|
BPM_est_1 = hrmdata.bpm;
|
||||||
|
} else {
|
||||||
|
let bpm_result;
|
||||||
|
if (hrmdata.confidence >= 90) { // display firmware value if good
|
||||||
|
bpm_result = hrmdata.bpm;
|
||||||
|
updateHrm(undefined);
|
||||||
|
} else {
|
||||||
|
bpm_result = calculate(idx);
|
||||||
|
bpm_corrected = bpm_result;
|
||||||
|
updateHrm(bpm_result);
|
||||||
|
}
|
||||||
|
BPM_est_2 = BPM_est_1;
|
||||||
|
BPM_est_1 = bpm_result;
|
||||||
|
|
||||||
|
// set search range of next BPM
|
||||||
|
const est_res_idx = getFftIdx(bpm_result/60, SAMPLE_RATE, NUM_POINTS)-minFreqIdx;
|
||||||
|
rangeIdx = [est_res_idx-maxBpmDiffIdxDown, est_res_idx+maxBpmDiffIdxUp];
|
||||||
|
if (rangeIdx[0] < 0) {
|
||||||
|
rangeIdx[0] = 0;
|
||||||
|
}
|
||||||
|
if (rangeIdx[1] > maxFreqIdx-minFreqIdx) {
|
||||||
|
rangeIdx[1] = maxFreqIdx-minFreqIdx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"id": "hrmmar",
|
||||||
|
"name": "HRM Motion Artifacts removal",
|
||||||
|
"shortName":"HRM MA removal",
|
||||||
|
"icon": "app.png",
|
||||||
|
"version":"0.01",
|
||||||
|
"description": "Removes Motion Artifacts in Bangle.js's heart rate sensor data.",
|
||||||
|
"type": "bootloader",
|
||||||
|
"tags": "health",
|
||||||
|
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||||
|
"readme": "README.md",
|
||||||
|
"storage": [
|
||||||
|
{"name":"hrmmar.boot.js","url":"boot.js"},
|
||||||
|
{"name":"hrmfftelim","url":"fftelim.js"},
|
||||||
|
{"name":"hrmmar.settings.js","url":"settings.js"}
|
||||||
|
],
|
||||||
|
"data": [{"name":"hrmmar.json"}]
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
(function(back) {
|
||||||
|
var FILE = "hrmmar.json";
|
||||||
|
// Load settings
|
||||||
|
var settings = Object.assign({
|
||||||
|
mAremoval: 0,
|
||||||
|
}, require('Storage').readJSON(FILE, true) || {});
|
||||||
|
|
||||||
|
function writeSettings() {
|
||||||
|
require('Storage').writeJSON(FILE, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the menu
|
||||||
|
E.showMenu({
|
||||||
|
"" : { "title" : "HRM MA removal" },
|
||||||
|
"< Back" : () => back(),
|
||||||
|
'MA removal': {
|
||||||
|
value: settings.mAremoval,
|
||||||
|
min: 0, max: 1,
|
||||||
|
format: v => ["None", "fft elim."][v],
|
||||||
|
onchange: v => {
|
||||||
|
settings.mAremoval = v;
|
||||||
|
writeSettings();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})
|
|
@ -9,3 +9,4 @@
|
||||||
0.09: Enable 'ams' on new firmwares (ams/ancs can now be enabled individually) (fix #1365)
|
0.09: Enable 'ams' on new firmwares (ams/ancs can now be enabled individually) (fix #1365)
|
||||||
0.10: Added more bundleIds
|
0.10: Added more bundleIds
|
||||||
0.11: Added letters with caron to unicodeRemap, to properly display messages in Czech language
|
0.11: Added letters with caron to unicodeRemap, to properly display messages in Czech language
|
||||||
|
0.12: Use new message library
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
// Config app not implemented yet
|
// Config app not implemented yet
|
||||||
setTimeout(()=>load("messages.app.js"),10);
|
setTimeout(()=>require("messages").openGUI(),10);
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
{
|
{
|
||||||
"id": "ios",
|
"id": "ios",
|
||||||
"name": "iOS Integration",
|
"name": "iOS Integration",
|
||||||
"version": "0.11",
|
"version": "0.12",
|
||||||
"description": "Display notifications/music/etc from iOS devices",
|
"description": "Display notifications/music/etc from iOS devices",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"tags": "tool,system,ios,apple,messages,notifications",
|
"tags": "tool,system,ios,apple,messages,notifications",
|
||||||
"dependencies": {"messages":"app"},
|
"dependencies": {"messages":"module"},
|
||||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||||
"readme": "README.md",
|
"readme": "README.md",
|
||||||
"storage": [
|
"storage": [
|
||||||
|
|
|
@ -19,3 +19,4 @@
|
||||||
0.17: Don't display 'Loading...' now the watch has its own loading screen
|
0.17: Don't display 'Loading...' now the watch has its own loading screen
|
||||||
0.18: Add 'back' icon in top-left to go back to clock
|
0.18: Add 'back' icon in top-left to go back to clock
|
||||||
0.19: Fix regression after back button added (returnToClock was called twice!)
|
0.19: Fix regression after back button added (returnToClock was called twice!)
|
||||||
|
0.20: Use Bangle.showClock for changing to clock
|
||||||
|
|
|
@ -42,16 +42,6 @@ let apps = launchCache.apps;
|
||||||
if (!settings.fullscreen)
|
if (!settings.fullscreen)
|
||||||
Bangle.loadWidgets();
|
Bangle.loadWidgets();
|
||||||
|
|
||||||
let returnToClock = function() {
|
|
||||||
// unload everything manually
|
|
||||||
// ... or we could just call `load();` but it will be slower
|
|
||||||
Bangle.setUI(); // remove scroller's handling
|
|
||||||
if (lockTimeout) clearTimeout(lockTimeout);
|
|
||||||
Bangle.removeListener("lock", lockHandler);
|
|
||||||
// now load the default clock - just call .bootcde as this has the code already
|
|
||||||
setTimeout(eval,0,s.read(".bootcde"));
|
|
||||||
}
|
|
||||||
|
|
||||||
E.showScroller({
|
E.showScroller({
|
||||||
h : 64*scaleval, c : apps.length,
|
h : 64*scaleval, c : apps.length,
|
||||||
draw : (i, r) => {
|
draw : (i, r) => {
|
||||||
|
@ -74,7 +64,12 @@ E.showScroller({
|
||||||
load(app.src);
|
load(app.src);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
back : returnToClock // button press or tap in top left calls returnToClock now
|
back : Bangle.showClock, // button press or tap in top left shows clock now
|
||||||
|
remove : () => {
|
||||||
|
// cleanup the timeout to not leave anything behind after being removed from ram
|
||||||
|
if (lockTimeout) clearTimeout(lockTimeout);
|
||||||
|
Bangle.removeListener("lock", lockHandler);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
g.flip(); // force a render before widgets have finished drawing
|
g.flip(); // force a render before widgets have finished drawing
|
||||||
|
|
||||||
|
@ -85,7 +80,7 @@ let lockHandler = function(locked) {
|
||||||
if (lockTimeout) clearTimeout(lockTimeout);
|
if (lockTimeout) clearTimeout(lockTimeout);
|
||||||
lockTimeout = undefined;
|
lockTimeout = undefined;
|
||||||
if (locked)
|
if (locked)
|
||||||
lockTimeout = setTimeout(returnToClock, 10000);
|
lockTimeout = setTimeout(Bangle.showClock, 10000);
|
||||||
}
|
}
|
||||||
Bangle.on("lock", lockHandler);
|
Bangle.on("lock", lockHandler);
|
||||||
if (!settings.fullscreen) // finally draw widgets
|
if (!settings.fullscreen) // finally draw widgets
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"id": "launch",
|
"id": "launch",
|
||||||
"name": "Launcher",
|
"name": "Launcher",
|
||||||
"shortName": "Launcher",
|
"shortName": "Launcher",
|
||||||
"version": "0.19",
|
"version": "0.20",
|
||||||
"description": "This is needed to display a menu allowing you to choose your own applications. You can replace this with a customised launcher.",
|
"description": "This is needed to display a menu allowing you to choose your own applications. You can replace this with a customised launcher.",
|
||||||
"readme": "README.md",
|
"readme": "README.md",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
0.01: New App!
|
||||||
|
0.02: Add 'messages' library
|
||||||
|
0.03: Fixes for Bangle.js 1
|
||||||
|
0.04: Add require("messages").clearAll()
|
||||||
|
0.05: Handling of message actions (ok/clear)
|
||||||
|
0.06: New messages now go at the start (fix #898)
|
||||||
|
Answering true/false now exits the messages app if no new messages
|
||||||
|
Back now marks a message as read
|
||||||
|
Clicking top-left opens a menu which allows you to delete a message or mark unread
|
||||||
|
0.07: Added settings menu with option to choose vibrate pattern and frequency (fix #909)
|
||||||
|
0.08: Fix rendering of long messages (fix #969)
|
||||||
|
buzz on new message (fix #999)
|
||||||
|
0.09: Message now disappears after 60s if no action taken and clock loads (fix 922)
|
||||||
|
Fix phone icon (#1014)
|
||||||
|
0.10: Respect the 'new' attribute if it was set from iOS integrations
|
||||||
|
0.11: Open app when touching the widget (Bangle.js 2 only)
|
||||||
|
0.12: Extra app-specific notification icons
|
||||||
|
New animated notification icon (instead of large blinking 'MESSAGES')
|
||||||
|
Added screenshots
|
||||||
|
0.13: Add /*LANG*/ comments for internationalisation
|
||||||
|
Add 'Delete All' option to message options
|
||||||
|
Now update correctly when 'require("messages").clearAll()' is called
|
||||||
|
0.14: Hide widget when all unread notifications are dismissed from phone
|
||||||
|
0.15: Don't buzz when Quiet Mode is active
|
||||||
|
0.16: Fix text wrapping so it fits the screen even if title is big (fix #1147)
|
||||||
|
0.17: Fix: Get dynamic dimensions of notify icon, fixed notification font
|
||||||
|
0.18: Use app-specific icon colors
|
||||||
|
Spread message action buttons out
|
||||||
|
Back button now goes back to list of messages
|
||||||
|
If showMessage called with no message (eg all messages deleted) now return to the clock (fix #1267)
|
||||||
|
0.19: Use a larger font for message text if it'll fit
|
||||||
|
0.20: Allow tapping on the body to show a scrollable view of the message and title in a bigger font (fix #1405, #1031)
|
||||||
|
0.21: Improve list readability on dark theme
|
||||||
|
0.22: Add Home Assistant icon
|
||||||
|
Allow repeat to be switched Off, so there is no buzzing repetition.
|
||||||
|
Also gave the widget a pixel more room to the right
|
||||||
|
0.23: Change message colors to match current theme instead of using green
|
||||||
|
Now attempt to use Large/Big/Medium fonts, and allow minimum font size to be configured
|
||||||
|
0.24: Remove left-over debug statement
|
||||||
|
0.25: Fix widget memory usage issues if message received and watch repeatedly calls Bangle.drawWidgets (fix #1550)
|
||||||
|
0.26: Setting to auto-open music
|
||||||
|
0.27: Add 'mark all read' option to popup menu (fix #1624)
|
||||||
|
0.28: Option to auto-unlock the watch when a new message arrives
|
||||||
|
0.29: Fix message list overwrites on Bangle.js 1 (fix #1642)
|
||||||
|
0.30: Add new Icons (Youtube, Twitch, MS TODO, Teams, Snapchat, Signal, Post & DHL, Nina, Lieferando, Kalender, Discord, Corona Warn, Bibel)
|
||||||
|
0.31: Option to disable icon flashing
|
||||||
|
0.32: Added an option to allow quiet mode to override message auto-open
|
||||||
|
0.33: Timeout from the message list screen if the message being displayed is removed and there is a timer going
|
||||||
|
0.34: Don't buzz for 'map' update messages
|
||||||
|
0.35: Reset graphics colors before rendering a message (possibly fix #1752)
|
||||||
|
0.36: Ensure a new message plus an almost immediate deletion of that message doesn't load the messages app (fix #1362)
|
||||||
|
0.37: Now use the setUI 'back' icon in the top left rather than specific buttons/menu items
|
||||||
|
0.38: Add telegram foss handling
|
||||||
|
0.39: Set default color for message icons according to theme
|
||||||
|
0.40: Use default Bangle formatter for booleans
|
||||||
|
0.41: Add notification icons in the widget
|
||||||
|
0.42: Fix messages ignoring "Vibrate: Off" setting
|
||||||
|
0.43: Add new Icons (Airbnb, warnwetter)
|
||||||
|
0.44: Separate buzz pattern for incoming calls
|
||||||
|
0.45: Added new app colors and icons
|
||||||
|
0.46: Add 'Vibrate Timer' option to set how long to vibrate for, and fix Repeat:off
|
||||||
|
Fix message removal from widget bar (previously caused exception as .hide has been removed)
|
||||||
|
0.47: Add new Icons (Nextbike, Mattermost, etc.)
|
||||||
|
0.48: When getting new message from the clock, only buzz once the messages app is loaded
|
||||||
|
0.49: Change messages icon (to fit within 24px) and ensure widget renders icons centrally
|
||||||
|
0.50: Add `getMessages` and `status` functions to library
|
||||||
|
Option to disable auto-open of messages
|
||||||
|
Option to make message icons monochrome (not colored)
|
||||||
|
messages widget buzz now returns a promise
|
||||||
|
0.51: Emit "message events"
|
||||||
|
Setting to hide widget
|
||||||
|
Add custom event handlers to prevent default app form loading
|
||||||
|
Move WIDGETS.messages.buzz() to require("messages").buzz()
|
||||||
|
0.52: Fix require("messages").buzz() regression
|
||||||
|
Fix background color in messages list after one unread message is shown
|
||||||
|
0.53: Messages now uses Bangle.load() to load messages app faster (if possible)
|
||||||
|
0.54: Move icons out to messageicons module
|
||||||
|
0.55: Rename to messagegui, move global message handling library to message module
|
||||||
|
Move widget to widmessage
|
|
@ -0,0 +1,68 @@
|
||||||
|
# Messages app
|
||||||
|
|
||||||
|
Default app to handle the display of messages and message notifications. It allows
|
||||||
|
them to be listed, viewed, and responded to.
|
||||||
|
It is installed automatically if you install `Android Integration` or `iOS Integration`.
|
||||||
|
|
||||||
|
It is a replacement for the old `notify`/`gadgetbridge` apps.
|
||||||
|
|
||||||
|
|
||||||
|
## Settings
|
||||||
|
|
||||||
|
You can change settings by going to the global `Settings` app, then `App Settings`
|
||||||
|
and `Messages`:
|
||||||
|
|
||||||
|
* `Vibrate` - This is the pattern of buzzes that should be made when a new message is received
|
||||||
|
* `Vibrate for calls` - This is the pattern of buzzes that should be made when an incoming call is received
|
||||||
|
* `Repeat` - How often should buzzes repeat - the default of 4 means the Bangle will buzz every 4 seconds
|
||||||
|
* `Vibrate Timer` - When a new message is received when in a non-clock app, we display the message icon and
|
||||||
|
buzz every `Repeat` seconds. This is how long we continue to do that.
|
||||||
|
* `Unread Timer` - When a new message is received when showing the clock we go into the Messages app.
|
||||||
|
If there is no user input for this amount of time then the app will exit and return
|
||||||
|
to the clock where a ringing bell will be shown in the Widget bar.
|
||||||
|
* `Min Font` - The minimum font size used when displaying messages on the screen. A bigger font
|
||||||
|
is chosen if there isn't much message text, but this specifies the smallest the font should get before
|
||||||
|
it starts getting clipped.
|
||||||
|
* `Auto-Open Music` - Should the app automatically open when the phone starts playing music?
|
||||||
|
* `Unlock Watch` - Should the app unlock the watch when a new message arrives, so you can touch the buttons at the bottom of the app?
|
||||||
|
|
||||||
|
## New Messages
|
||||||
|
|
||||||
|
When a new message is received:
|
||||||
|
|
||||||
|
* If you're in an app, the Bangle will buzz and a message icon appears in the Widget bar. You can tap this icon to view the message.
|
||||||
|
* If you're in a clock, the Messages app will automatically start and show the message
|
||||||
|
|
||||||
|
When a message is shown, you'll see a screen showing the message title and text.
|
||||||
|
|
||||||
|
* The 'back-arrow' button (or physical button on Bangle.js 2) goes back to Messages, marking the current message as read.
|
||||||
|
* The top-left icon shows more options, for instance deleting the message of marking unread
|
||||||
|
* On Bangle.js 2 you can tap on the message body to view a scrollable version of the title and text (or can use the top-left icon + `View Message`)
|
||||||
|
* If shown, the 'tick' button:
|
||||||
|
* **Android** opens the notification on the phone
|
||||||
|
* **iOS** responds positively to the notification (accept call/etc)
|
||||||
|
* If shown, the 'cross' button:
|
||||||
|
* **Android** dismisses the notification on the phone
|
||||||
|
* **iOS** responds negatively to the notification (dismiss call/etc)
|
||||||
|
|
||||||
|
## Images
|
||||||
|
_1. Screenshot of a notification_
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
## Requests
|
||||||
|
|
||||||
|
Please file any issues on https://github.com/espruino/BangleApps/issues/new?title=messages%20app
|
||||||
|
|
||||||
|
## Creator
|
||||||
|
|
||||||
|
Gordon Williams
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
[Jeroen Peters](https://github.com/jeroenpeters1986)
|
||||||
|
|
||||||
|
## Attributions
|
||||||
|
|
||||||
|
Icons used in this app are from https://icons8.com
|
|
@ -1,5 +1,5 @@
|
||||||
/* Called when we have a new message when we're in the clock...
|
/* Called when we have a new message when we're in the clock...
|
||||||
BUZZ_ON_NEW_MESSAGE is set so when messages.app.js loads it knows
|
BUZZ_ON_NEW_MESSAGE is set so when messagegui.app.js loads it knows
|
||||||
that it should buzz */
|
that it should buzz */
|
||||||
global.BUZZ_ON_NEW_MESSAGE = true;
|
global.BUZZ_ON_NEW_MESSAGE = true;
|
||||||
eval(require("Storage").read("messages.app.js"));
|
eval(require("Storage").read("messagegui.app.js"));
|
|
@ -19,7 +19,6 @@ require("messages").pushMessage({"t":"add","id":1,"src":"Maps","title":"0 yd - H
|
||||||
// call
|
// call
|
||||||
require("messages").pushMessage({"t":"add","id":"call","src":"Phone","title":"Bob","body":"12421312",positive:true,negative:true})
|
require("messages").pushMessage({"t":"add","id":"call","src":"Phone","title":"Bob","body":"12421312",positive:true,negative:true})
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var Layout = require("Layout");
|
var Layout = require("Layout");
|
||||||
var settings = require('Storage').readJSON("messages.settings.json", true) || {};
|
var settings = require('Storage').readJSON("messages.settings.json", true) || {};
|
||||||
var fontSmall = "6x8";
|
var fontSmall = "6x8";
|
||||||
|
@ -49,8 +48,11 @@ to the clock. */
|
||||||
var unreadTimeout;
|
var unreadTimeout;
|
||||||
/// List of all our messages
|
/// List of all our messages
|
||||||
var MESSAGES = require("messages").getMessages();
|
var MESSAGES = require("messages").getMessages();
|
||||||
if (!Array.isArray(MESSAGES)) MESSAGES=[];
|
|
||||||
var onMessagesModified = function(msg) {
|
var onMessagesModified = function(type,msg) {
|
||||||
|
if (msg.handled) return;
|
||||||
|
msg.handled = true;
|
||||||
|
require("messages").apply(msg, MESSAGES);
|
||||||
// TODO: if new, show this new one
|
// TODO: if new, show this new one
|
||||||
if (msg && msg.id!=="music" && msg.new && active!="map" &&
|
if (msg && msg.id!=="music" && msg.new && active!="map" &&
|
||||||
!((require('Storage').readJSON('setting.json', 1) || {}).quiet)) {
|
!((require('Storage').readJSON('setting.json', 1) || {}).quiet)) {
|
||||||
|
@ -62,9 +64,15 @@ var onMessagesModified = function(msg) {
|
||||||
}
|
}
|
||||||
showMessage(msg&&msg.id);
|
showMessage(msg&&msg.id);
|
||||||
};
|
};
|
||||||
|
Bangle.on("message", onMessagesModified);
|
||||||
|
|
||||||
function saveMessages() {
|
function saveMessages() {
|
||||||
require("Storage").writeJSON("messages.json",MESSAGES)
|
require("messages").write(MESSAGES.map(m => {
|
||||||
|
delete m.show;
|
||||||
|
return m;
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
E.on("kill", saveMessages);
|
||||||
|
|
||||||
function showMapMessage(msg) {
|
function showMapMessage(msg) {
|
||||||
active = "map";
|
active = "map";
|
||||||
|
@ -355,12 +363,16 @@ function checkMessages(options) {
|
||||||
}
|
}
|
||||||
// we have >0 messages
|
// we have >0 messages
|
||||||
var newMessages = MESSAGES.filter(m=>m.new&&m.id!="music");
|
var newMessages = MESSAGES.filter(m=>m.new&&m.id!="music");
|
||||||
|
var toShow = MESSAGES.find(m=>m.show);
|
||||||
|
if (toShow) {
|
||||||
|
newMessages.unshift(toShow);
|
||||||
|
}
|
||||||
// If we have a new message, show it
|
// If we have a new message, show it
|
||||||
if (options.showMsgIfUnread && newMessages.length) {
|
if ((toShow||options.showMsgIfUnread) && newMessages.length) {
|
||||||
showMessage(newMessages[0].id);
|
showMessage(newMessages[0].id);
|
||||||
// buzz after showMessage, so being busy during layout doesn't affect the buzz pattern
|
// buzz after showMessage, so being busy during layout doesn't affect the buzz pattern
|
||||||
if (global.BUZZ_ON_NEW_MESSAGE) {
|
if (global.BUZZ_ON_NEW_MESSAGE) {
|
||||||
// this is set if we entered the messages app by loading `messages.new.js`
|
// this is set if we entered the messages app by loading `messagegui.new.js`
|
||||||
// ... but only buzz the first time we view a new message
|
// ... but only buzz the first time we view a new message
|
||||||
global.BUZZ_ON_NEW_MESSAGE = false;
|
global.BUZZ_ON_NEW_MESSAGE = false;
|
||||||
// messages.buzz respects quiet mode - no need to check here
|
// messages.buzz respects quiet mode - no need to check here
|
||||||
|
@ -428,6 +440,7 @@ function cancelReloadTimeout() {
|
||||||
g.clear();
|
g.clear();
|
||||||
|
|
||||||
Bangle.loadWidgets();
|
Bangle.loadWidgets();
|
||||||
|
require("messages").toggleWidget(false);
|
||||||
Bangle.drawWidgets();
|
Bangle.drawWidgets();
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
After Width: | Height: | Size: 917 B |
|
@ -0,0 +1,3 @@
|
||||||
|
(function() {
|
||||||
|
Bangle.on("message", (type, msg) => require("messagegui").listener(type, msg));
|
||||||
|
})();
|
|
@ -0,0 +1,60 @@
|
||||||
|
/**
|
||||||
|
* Listener set up in boot.js, calls into here to keep boot.js short
|
||||||
|
*/
|
||||||
|
exports.listener = function(type, msg) {
|
||||||
|
// Default handler: Launch the GUI for all unhandled messages (except music if disabled in settings)
|
||||||
|
if (msg.handled || (global.__FILE__ && __FILE__.startsWith('messagegui.'))) return; // already handled or app open
|
||||||
|
|
||||||
|
// if no new messages now, make sure we don't load the messages app
|
||||||
|
if (exports.messageTimeout && !msg.new && require("messages").status(msg) !== "new") {
|
||||||
|
clearTimeout(exports.messageTimeout);
|
||||||
|
delete exports.messageTimeout;
|
||||||
|
}
|
||||||
|
if (msg.t==="remove") return;
|
||||||
|
|
||||||
|
const appSettings = require("Storage").readJSON("messages.settings.json", 1) || {};
|
||||||
|
let loadMessages = (Bangle.CLOCK || event.important);
|
||||||
|
if (type==="music") {
|
||||||
|
if (Bangle.CLOCK && msg.new && appSettings.openMusic) loadMessages = true;
|
||||||
|
else return;
|
||||||
|
}
|
||||||
|
require("messages").save(msg);
|
||||||
|
msg.handled = true;
|
||||||
|
if (msg.t!=="add" || !msg.new) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const quiet = (require("Storage").readJSON("setting.json", 1) || {}).quiet;
|
||||||
|
const unlockWatch = appSettings.unlockWatch;
|
||||||
|
// don't auto-open messages in quiet mode if quietNoAutOpn is true
|
||||||
|
if ((quiet && appSettings.quietNoAutOpn) || appSettings.noAutOpn)
|
||||||
|
loadMessages = false;
|
||||||
|
|
||||||
|
// after a delay load the app, to ensure we have all the messages
|
||||||
|
if (exports.messageTimeout) clearTimeout(exports.messageTimeout);
|
||||||
|
exports.messageTimeout = setTimeout(function() {
|
||||||
|
delete exports.messageTimeout;
|
||||||
|
if (type!=="music") {
|
||||||
|
if (!loadMessages) return require("messages").buzz(msg.src); // no opening the app, just buzz
|
||||||
|
if (!quiet && unlockWatch) {
|
||||||
|
Bangle.setLocked(false);
|
||||||
|
Bangle.setLCDPower(1); // turn screen on
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.open(msg);
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launch GUI app with given message
|
||||||
|
* @param {object} msg
|
||||||
|
*/
|
||||||
|
exports.open = function(msg) {
|
||||||
|
if (msg && msg.id && !msg.show) {
|
||||||
|
// store which message to load
|
||||||
|
msg.show = 1;
|
||||||
|
require("messages").save(msg, {force: 1});
|
||||||
|
}
|
||||||
|
|
||||||
|
Bangle.load((msg && msg.new && msg.id!=="music") ? "messagegui.new.js" : "messagegui.app.js");
|
||||||
|
};
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"id": "messagegui",
|
||||||
|
"name": "Message UI",
|
||||||
|
"version": "0.55",
|
||||||
|
"description": "Default app to display notifications from iOS and Gadgetbridge/Android",
|
||||||
|
"icon": "app.png",
|
||||||
|
"type": "app",
|
||||||
|
"tags": "tool,system",
|
||||||
|
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||||
|
"dependencies" : { "messageicons":"module" },
|
||||||
|
"provides_modules": ["messagegui"],
|
||||||
|
"readme": "README.md",
|
||||||
|
"storage": [
|
||||||
|
{"name":"messagegui","url":"lib.js"},
|
||||||
|
{"name":"messagegui.app.js","url":"app.js"},
|
||||||
|
{"name":"messagegui.new.js","url":"app-newmessage.js"},
|
||||||
|
{"name":"messagegui.boot.js","url":"boot.js"},
|
||||||
|
{"name":"messagegui.img","url":"app-icon.js","evaluate":true}
|
||||||
|
],
|
||||||
|
"screenshots": [{"url":"screenshot.png"}],
|
||||||
|
"sortorder": -9
|
||||||
|
}
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
@ -1,77 +1 @@
|
||||||
0.01: New App!
|
0.55: Moved messages library into standalone library
|
||||||
0.02: Add 'messages' library
|
|
||||||
0.03: Fixes for Bangle.js 1
|
|
||||||
0.04: Add require("messages").clearAll()
|
|
||||||
0.05: Handling of message actions (ok/clear)
|
|
||||||
0.06: New messages now go at the start (fix #898)
|
|
||||||
Answering true/false now exits the messages app if no new messages
|
|
||||||
Back now marks a message as read
|
|
||||||
Clicking top-left opens a menu which allows you to delete a message or mark unread
|
|
||||||
0.07: Added settings menu with option to choose vibrate pattern and frequency (fix #909)
|
|
||||||
0.08: Fix rendering of long messages (fix #969)
|
|
||||||
buzz on new message (fix #999)
|
|
||||||
0.09: Message now disappears after 60s if no action taken and clock loads (fix 922)
|
|
||||||
Fix phone icon (#1014)
|
|
||||||
0.10: Respect the 'new' attribute if it was set from iOS integrations
|
|
||||||
0.11: Open app when touching the widget (Bangle.js 2 only)
|
|
||||||
0.12: Extra app-specific notification icons
|
|
||||||
New animated notification icon (instead of large blinking 'MESSAGES')
|
|
||||||
Added screenshots
|
|
||||||
0.13: Add /*LANG*/ comments for internationalisation
|
|
||||||
Add 'Delete All' option to message options
|
|
||||||
Now update correctly when 'require("messages").clearAll()' is called
|
|
||||||
0.14: Hide widget when all unread notifications are dismissed from phone
|
|
||||||
0.15: Don't buzz when Quiet Mode is active
|
|
||||||
0.16: Fix text wrapping so it fits the screen even if title is big (fix #1147)
|
|
||||||
0.17: Fix: Get dynamic dimensions of notify icon, fixed notification font
|
|
||||||
0.18: Use app-specific icon colors
|
|
||||||
Spread message action buttons out
|
|
||||||
Back button now goes back to list of messages
|
|
||||||
If showMessage called with no message (eg all messages deleted) now return to the clock (fix #1267)
|
|
||||||
0.19: Use a larger font for message text if it'll fit
|
|
||||||
0.20: Allow tapping on the body to show a scrollable view of the message and title in a bigger font (fix #1405, #1031)
|
|
||||||
0.21: Improve list readability on dark theme
|
|
||||||
0.22: Add Home Assistant icon
|
|
||||||
Allow repeat to be switched Off, so there is no buzzing repetition.
|
|
||||||
Also gave the widget a pixel more room to the right
|
|
||||||
0.23: Change message colors to match current theme instead of using green
|
|
||||||
Now attempt to use Large/Big/Medium fonts, and allow minimum font size to be configured
|
|
||||||
0.24: Remove left-over debug statement
|
|
||||||
0.25: Fix widget memory usage issues if message received and watch repeatedly calls Bangle.drawWidgets (fix #1550)
|
|
||||||
0.26: Setting to auto-open music
|
|
||||||
0.27: Add 'mark all read' option to popup menu (fix #1624)
|
|
||||||
0.28: Option to auto-unlock the watch when a new message arrives
|
|
||||||
0.29: Fix message list overwrites on Bangle.js 1 (fix #1642)
|
|
||||||
0.30: Add new Icons (Youtube, Twitch, MS TODO, Teams, Snapchat, Signal, Post & DHL, Nina, Lieferando, Kalender, Discord, Corona Warn, Bibel)
|
|
||||||
0.31: Option to disable icon flashing
|
|
||||||
0.32: Added an option to allow quiet mode to override message auto-open
|
|
||||||
0.33: Timeout from the message list screen if the message being displayed is removed and there is a timer going
|
|
||||||
0.34: Don't buzz for 'map' update messages
|
|
||||||
0.35: Reset graphics colors before rendering a message (possibly fix #1752)
|
|
||||||
0.36: Ensure a new message plus an almost immediate deletion of that message doesn't load the messages app (fix #1362)
|
|
||||||
0.37: Now use the setUI 'back' icon in the top left rather than specific buttons/menu items
|
|
||||||
0.38: Add telegram foss handling
|
|
||||||
0.39: Set default color for message icons according to theme
|
|
||||||
0.40: Use default Bangle formatter for booleans
|
|
||||||
0.41: Add notification icons in the widget
|
|
||||||
0.42: Fix messages ignoring "Vibrate: Off" setting
|
|
||||||
0.43: Add new Icons (Airbnb, warnwetter)
|
|
||||||
0.44: Separate buzz pattern for incoming calls
|
|
||||||
0.45: Added new app colors and icons
|
|
||||||
0.46: Add 'Vibrate Timer' option to set how long to vibrate for, and fix Repeat:off
|
|
||||||
Fix message removal from widget bar (previously caused exception as .hide has been removed)
|
|
||||||
0.47: Add new Icons (Nextbike, Mattermost, etc.)
|
|
||||||
0.48: When getting new message from the clock, only buzz once the messages app is loaded
|
|
||||||
0.49: Change messages icon (to fit within 24px) and ensure widget renders icons centrally
|
|
||||||
0.50: Add `getMessages` and `status` functions to library
|
|
||||||
Option to disable auto-open of messages
|
|
||||||
Option to make message icons monochrome (not colored)
|
|
||||||
messages widget buzz now returns a promise
|
|
||||||
0.51: Emit "message events"
|
|
||||||
Setting to hide widget
|
|
||||||
Add custom event handlers to prevent default app form loading
|
|
||||||
Move WIDGETS.messages.buzz() to require("messages").buzz()
|
|
||||||
0.52: Fix require("messages").buzz() regression
|
|
||||||
Fix background color in messages list after one unread message is shown
|
|
||||||
0.53: Messages now uses Bangle.load() to load messages app faster (if possible)
|
|
||||||
0.54: Move icons out to messageicons module
|
|
||||||
|
|
|
@ -1,62 +1,25 @@
|
||||||
# Messages app
|
# Messages library
|
||||||
|
|
||||||
This app handles the display of messages and message notifications. It stores
|
This library handles the passing of messages. It can storess a list of messages
|
||||||
a list of currently received messages and allows them to be listed, viewed,
|
and allows them to be retrieved by other apps.
|
||||||
and responded to.
|
|
||||||
|
|
||||||
It is a replacement for the old `notify`/`gadgetbridge` apps.
|
## Example
|
||||||
|
|
||||||
## Settings
|
Assuming you are using GadgetBridge and "overlay notifications":
|
||||||
|
|
||||||
You can change settings by going to the global `Settings` app, then `App Settings`
|
1. Gadgetbridge sends an event to your watch for an incoming message
|
||||||
and `Messages`:
|
2. The `android` app parses the message, and calls `require("messages").pushMessage({/** the message */})`
|
||||||
|
3. `require("messages")` (provided by `messagelib`) calls `Bangle.emit("message", "text", {/** the message */})`
|
||||||
* `Vibrate` - This is the pattern of buzzes that should be made when a new message is received
|
4. Overlay Notifications shows the message in an overlay, and marks it as `handled`
|
||||||
* `Vibrate for calls` - This is the pattern of buzzes that should be made when an incoming call is received
|
5. The default GUI app (`messages`) sees the event is marked as `handled`, so does nothing.
|
||||||
* `Repeat` - How often should buzzes repeat - the default of 4 means the Bangle will buzz every 4 seconds
|
6. The default widget (`widmessages`) does nothing with `handled`, and shows a notification icon.
|
||||||
* `Vibrate Timer` - When a new message is received when in a non-clock app, we display the message icon and
|
7. You tap the notification, in order to open the full GUI Overlay Notifications
|
||||||
buzz every `Repeat` seconds. This is how long we continue to do that.
|
calls `require("messages").openGUI({/** the message */})`
|
||||||
* `Unread Timer` - When a new message is received when showing the clock we go into the Messages app.
|
8. The default GUI app (`messages`) sees the "messageGUI" event, and launches itself
|
||||||
If there is no user input for this amount of time then the app will exit and return
|
|
||||||
to the clock where a ringing bell will be shown in the Widget bar.
|
|
||||||
* `Min Font` - The minimum font size used when displaying messages on the screen. A bigger font
|
|
||||||
is chosen if there isn't much message text, but this specifies the smallest the font should get before
|
|
||||||
it starts getting clipped.
|
|
||||||
* `Auto-Open Music` - Should the app automatically open when the phone starts playing music?
|
|
||||||
* `Unlock Watch` - Should the app unlock the watch when a new message arrives, so you can touch the buttons at the bottom of the app?
|
|
||||||
* `Flash Icon` - Toggle flashing of the widget icon.
|
|
||||||
* `Widget messages` - The maximum amount of message icons to show on the widget, or `Hide` the widget completely.
|
|
||||||
|
|
||||||
## New Messages
|
|
||||||
|
|
||||||
When a new message is received:
|
|
||||||
|
|
||||||
* If you're in an app, the Bangle will buzz and a message icon appears in the Widget bar. You can tap this icon to view the message.
|
|
||||||
* If you're in a clock, the Messages app will automatically start and show the message
|
|
||||||
|
|
||||||
When a message is shown, you'll see a screen showing the message title and text.
|
|
||||||
|
|
||||||
* The 'back-arrow' button (or physical button on Bangle.js 2) goes back to Messages, marking the current message as read.
|
|
||||||
* The top-left icon shows more options, for instance deleting the message of marking unread
|
|
||||||
* On Bangle.js 2 you can tap on the message body to view a scrollable version of the title and text (or can use the top-left icon + `View Message`)
|
|
||||||
* If shown, the 'tick' button:
|
|
||||||
* **Android** opens the notification on the phone
|
|
||||||
* **iOS** responds positively to the notification (accept call/etc)
|
|
||||||
* If shown, the 'cross' button:
|
|
||||||
* **Android** dismisses the notification on the phone
|
|
||||||
* **iOS** responds negatively to the notification (dismiss call/etc)
|
|
||||||
|
|
||||||
## Images
|
|
||||||
_1. Screenshot of a notification_
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
_2. What the notify icon looks like (it's touchable on Bangle.js2!)_
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
## Events (for app/widget developers)
|
|
||||||
|
## Events
|
||||||
|
|
||||||
When a new message arrives, a `"message"` event is emitted, you can listen for
|
When a new message arrives, a `"message"` event is emitted, you can listen for
|
||||||
it like this:
|
it like this:
|
||||||
|
@ -64,9 +27,8 @@ it like this:
|
||||||
```js
|
```js
|
||||||
myMessageListener = Bangle.on("message", (type, message)=>{
|
myMessageListener = Bangle.on("message", (type, message)=>{
|
||||||
if (message.handled) return; // another app already handled this message
|
if (message.handled) return; // another app already handled this message
|
||||||
// <type> is one of "text", "call", "alarm", "map", "music", or "clearAll"
|
// <type> is one of "text", "call", "alarm", "map", or "music"
|
||||||
if (type === "clearAll") return; // not a message
|
// see `messagelib/lib.js` for possible <message> formats
|
||||||
// see `messages/lib.js` for possible <message> formats
|
|
||||||
// message.t could be "add", "modify" or "remove"
|
// message.t could be "add", "modify" or "remove"
|
||||||
E.showMessage(`${message.title}\n${message.body}`, `${message.t} ${type} message`);
|
E.showMessage(`${message.title}\n${message.body}`, `${message.t} ${type} message`);
|
||||||
// You can prevent the default `message` app from loading by setting `message.handled = true`:
|
// You can prevent the default `message` app from loading by setting `message.handled = true`:
|
||||||
|
@ -74,10 +36,23 @@ myMessageListener = Bangle.on("message", (type, message)=>{
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Apps can launch the full GUI by calling `require("messages").openGUI()`, if you
|
||||||
|
want to write your own GUI, it should include boot code that listens for
|
||||||
|
`"messageGUI"` events:
|
||||||
|
|
||||||
|
```js
|
||||||
|
Bangle.on("messageGUI", message=>{
|
||||||
|
if (message.handled) return; // another app already opened it's GUI
|
||||||
|
message.handled = true; // prevent other apps form launching
|
||||||
|
Bangle.load("my_message_gui.app.js");
|
||||||
|
})
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Requests
|
## Requests
|
||||||
|
|
||||||
Please file any issues on https://github.com/espruino/BangleApps/issues/new?title=messages%20app
|
Please file any issues on https://github.com/espruino/BangleApps/issues/new?title=messagelib%library
|
||||||
|
|
||||||
## Creator
|
## Creator
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,16 @@
|
||||||
function openMusic() {
|
exports.music = {};
|
||||||
// only read settings file for first music message
|
/**
|
||||||
if ("undefined"==typeof exports._openMusic) {
|
* Emit "message" event with appropriate type from Bangle
|
||||||
exports._openMusic = !!((require('Storage').readJSON("messages.settings.json", true) || {}).openMusic);
|
* @param {object} msg
|
||||||
}
|
*/
|
||||||
return exports._openMusic;
|
function emit(msg) {
|
||||||
|
let type = "text";
|
||||||
|
if (["call", "music", "map"].includes(msg.id)) type = msg.id;
|
||||||
|
if (type==="music" && msg.t!=="remove" && (!("state" in msg) || (!("track" in msg)))) return; // wait for complete music info
|
||||||
|
if (msg.src && msg.src.toLowerCase().startsWith("alarm")) type = "alarm";
|
||||||
|
Bangle.emit("message", type, msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Push a new message onto messages queue, event is:
|
/* Push a new message onto messages queue, event is:
|
||||||
{t:"add",id:int, src,title,subject,body,sender,tel, important:bool, new:bool}
|
{t:"add",id:int, src,title,subject,body,sender,tel, important:bool, new:bool}
|
||||||
{t:"add",id:int, id:"music", state, artist, track, etc} // add new
|
{t:"add",id:int, id:"music", state, artist, track, etc} // add new
|
||||||
|
@ -12,125 +18,178 @@ function openMusic() {
|
||||||
{t:"modify",id:int, title:string} // modified
|
{t:"modify",id:int, title:string} // modified
|
||||||
*/
|
*/
|
||||||
exports.pushMessage = function(event) {
|
exports.pushMessage = function(event) {
|
||||||
var messages = exports.getMessages();
|
|
||||||
// now modify/delete as appropriate
|
// now modify/delete as appropriate
|
||||||
var mIdx = messages.findIndex(m=>m.id==event.id);
|
if (event.t==="remove") {
|
||||||
if (event.t=="remove") {
|
if (event.id==="music") exports.music = {};
|
||||||
if (mIdx>=0) messages.splice(mIdx, 1); // remove item
|
|
||||||
mIdx=-1;
|
|
||||||
} else { // add/modify
|
} else { // add/modify
|
||||||
if (event.t=="add"){
|
if (event.t==="add") {
|
||||||
if(event.new === undefined ) { // If 'new' has not been set yet, set it
|
if (event.new===undefined) event.new = true; // Assume it should be new
|
||||||
event.new=true; // Assume it should be new
|
} else if (event.t==="modify") {
|
||||||
|
const old = exports.getMessages().find(m => m.id===event.id);
|
||||||
|
if (old) event = Object.assign(old, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// combine musicinfo and musicstate events
|
||||||
|
if (event.id==="music") {
|
||||||
|
if (event.state==="play") event.new = true; // new track, or playback (re)started
|
||||||
|
event = Object.assign(exports.music, event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (mIdx<0) {
|
// reset state (just in case)
|
||||||
mIdx=0;
|
delete event.handled;
|
||||||
messages.unshift(event); // add new messages to the beginning
|
delete event.saved;
|
||||||
|
emit(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a single message to flash
|
||||||
|
* Also sets msg.saved=true
|
||||||
|
*
|
||||||
|
* @param {object} msg
|
||||||
|
* @param {object} [options={}] Options:
|
||||||
|
* {boolean} [force=false] Force save even if msg.saved is already set
|
||||||
|
*/
|
||||||
|
exports.save = function(msg, options) {
|
||||||
|
if (!options) options = {};
|
||||||
|
if (msg.saved && !options.force) return; //already saved
|
||||||
|
let messages = exports.getMessages();
|
||||||
|
exports.apply(msg, messages);
|
||||||
|
exports.write(messages);
|
||||||
|
msg.saved = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply incoming event to array of messages
|
||||||
|
*
|
||||||
|
* @param {object} event Event to apply
|
||||||
|
* @param {array} messages Array of messages, *will be modified in-place*
|
||||||
|
* @return {array} Modified messages array
|
||||||
|
*/
|
||||||
|
exports.apply = function(event, messages) {
|
||||||
|
if (!event || !event.id) return messages;
|
||||||
|
const mIdx = messages.findIndex(m => m.id===event.id);
|
||||||
|
if (event.t==="remove") {
|
||||||
|
if (mIdx<0) return messages; // already gone -> nothing to do
|
||||||
|
messages.splice(mIdx, 1);
|
||||||
|
} else if (event.t==="add") {
|
||||||
|
if (mIdx>=0) messages.splice(mIdx, 1); // duplicate ID! erase previous version
|
||||||
|
messages.unshift(event);
|
||||||
|
} else if (event.t==="modify") {
|
||||||
|
if (mIdx>=0) messages[mIdx] = Object.assign(messages[mIdx], event);
|
||||||
|
else messages.unshift(event);
|
||||||
}
|
}
|
||||||
else Object.assign(messages[mIdx], event);
|
return messages;
|
||||||
if (event.id=="music" && messages[mIdx].state=="play") {
|
};
|
||||||
messages[mIdx].new = true; // new track, or playback (re)started
|
|
||||||
type = 'music';
|
/**
|
||||||
}
|
* Accept a call (or other acceptable event)
|
||||||
}
|
* @param {object} msg
|
||||||
require("Storage").writeJSON("messages.json",messages);
|
*/
|
||||||
var message = mIdx<0 ? {id:event.id, t:'remove'} : messages[mIdx];
|
exports.accept = function(msg) {
|
||||||
// if in app, process immediately
|
if (msg.positive) Bangle.messageResponse(msg, true);
|
||||||
if ("undefined"!=typeof MESSAGES) return onMessagesModified(message);
|
};
|
||||||
// emit message event
|
|
||||||
var type = 'text';
|
/**
|
||||||
if (["call", "music", "map"].includes(message.id)) type = message.id;
|
* Dismiss a message (if applicable), and erase it from flash
|
||||||
if (message.src && message.src.toLowerCase().startsWith("alarm")) type = "alarm";
|
* Emits a "message" event with t="remove", only if message existed
|
||||||
Bangle.emit("message", type, message);
|
*
|
||||||
// update the widget icons shown
|
* @param {object} msg
|
||||||
if (global.WIDGETS && WIDGETS.messages) WIDGETS.messages.update(messages,true);
|
*/
|
||||||
var handleMessage = () => {
|
exports.dismiss = function(msg) {
|
||||||
// if no new messages now, make sure we don't load the messages app
|
if (msg.negative) Bangle.messageResponse(msg, false);
|
||||||
if (event.t=="remove" && exports.messageTimeout && !messages.some(m => m.new)) {
|
let messages = exports.getMessages();
|
||||||
clearTimeout(exports.messageTimeout);
|
const mIdx = messages.findIndex(m=>m.id===msg.id);
|
||||||
delete exports.messageTimeout;
|
if (mIdx<0) return;
|
||||||
}
|
messages.splice(mIdx, 1);
|
||||||
// ok, saved now
|
exports.write(messages);
|
||||||
if (event.id=="music" && Bangle.CLOCK && messages[mIdx].new && openMusic()) {
|
if (msg.t==="remove") return; // already removed, don't re-emit
|
||||||
// just load the app to display music: no buzzing
|
msg.t = "remove";
|
||||||
Bangle.load("messages.app.js");
|
emit(msg); // emit t="remove", so e.g. widgets know to update
|
||||||
} else if (event.t!="add") {
|
};
|
||||||
// we only care if it's new
|
|
||||||
return;
|
/**
|
||||||
} else if (event.new==false) {
|
* Emit a "type=openGUI" event, to open GUI app
|
||||||
return;
|
*
|
||||||
}
|
* @param {object} [msg={}] Message the app should show
|
||||||
// otherwise load messages/show widget
|
*/
|
||||||
var loadMessages = Bangle.CLOCK || event.important;
|
exports.openGUI = function(msg) {
|
||||||
var quiet = (require('Storage').readJSON('setting.json', 1) || {}).quiet;
|
if (!require("Storage").read("messagegui")) return; // "messagegui" module is missing!
|
||||||
var appSettings = require('Storage').readJSON('messages.settings.json', 1) || {};
|
// Mark the event as unhandled for GUI, but leave passed arguments intact
|
||||||
var unlockWatch = appSettings.unlockWatch;
|
let copy = Object.assign({}, msg);
|
||||||
// don't auto-open messages in quiet mode if quietNoAutOpn is true
|
delete copy.handled;
|
||||||
if ((quiet && appSettings.quietNoAutOpn) || appSettings.noAutOpn)
|
require("messagegui").open(copy);
|
||||||
loadMessages = false;
|
};
|
||||||
delete appSettings;
|
|
||||||
// after a delay load the app, to ensure we have all the messages
|
/**
|
||||||
if (exports.messageTimeout) clearTimeout(exports.messageTimeout);
|
* Show/hide the messages widget
|
||||||
exports.messageTimeout = setTimeout(function() {
|
*
|
||||||
exports.messageTimeout = undefined;
|
* @param {boolean} show
|
||||||
// if we're in a clock or it's important, go straight to messages app
|
*/
|
||||||
if (loadMessages) {
|
exports.toggleWidget = function(show) {
|
||||||
if (!quiet && unlockWatch) {
|
if (!require("Storage").read("messagewidget")) return; // "messagewidget" module is missing!
|
||||||
Bangle.setLocked(false);
|
if (show) require("messagewidget").show();
|
||||||
Bangle.setLCDPower(1); // turn screen on
|
else require("messagewidget").hide();
|
||||||
}
|
};
|
||||||
// we will buzz when we enter the messages app
|
|
||||||
return Bangle.load("messages.new.js");
|
/**
|
||||||
}
|
* Replace all stored messages
|
||||||
if (global.WIDGETS && WIDGETS.messages) WIDGETS.messages.update(messages);
|
* @param {array} messages Messages to save
|
||||||
exports.buzz(message.src);
|
*/
|
||||||
}, 500);
|
exports.write = function(messages) {
|
||||||
};
|
require("Storage").writeJSON("messages.json", messages.map(m => {
|
||||||
setTimeout(()=>{
|
// we never want to save saved/handled status to file;
|
||||||
if (!message.handled) handleMessage();
|
delete m.saved;
|
||||||
},0);
|
delete m.handled;
|
||||||
}
|
return m;
|
||||||
/// Remove all messages
|
}));
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Erase all messages
|
||||||
|
*/
|
||||||
exports.clearAll = function() {
|
exports.clearAll = function() {
|
||||||
if ("undefined"!= typeof MESSAGES) { // we're in a messages app, clear that as well
|
exports.write([]);
|
||||||
MESSAGES = [];
|
Bangle.emit("message", "clearAll", {});
|
||||||
}
|
|
||||||
// Clear all messages
|
|
||||||
require("Storage").writeJSON("messages.json", []);
|
|
||||||
// if we have a widget, update it
|
|
||||||
if (global.WIDGETS && WIDGETS.messages)
|
|
||||||
WIDGETS.messages.update([]);
|
|
||||||
// let message listeners know
|
|
||||||
Bangle.emit("message", "clearAll", {}); // guarantee listeners an object as `message`
|
|
||||||
// clearAll cannot be marked as "handled"
|
|
||||||
// update app if in app
|
|
||||||
if ("function"== typeof onMessagesModified) onMessagesModified();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Get saved messages
|
||||||
|
*
|
||||||
|
* Optionally pass in a message to apply to the list, this is for event handlers:
|
||||||
|
* By passing the message from the event, you can make sure the list is up-to-date,
|
||||||
|
* even if the message has not been saved (yet)
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* Bangle.on("message", (type, msg) => {
|
||||||
|
* console.log("All messages:", require("messages").getMessages(msg));
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* @param {object} [withMessage] Apply this event to messages
|
||||||
* @returns {array} All messages
|
* @returns {array} All messages
|
||||||
*/
|
*/
|
||||||
exports.getMessages = function() {
|
exports.getMessages = function(withMessage) {
|
||||||
if ("undefined"!=typeof MESSAGES) return MESSAGES; // loaded/managed by app
|
let messages = require("Storage").readJSON("messages.json", true);
|
||||||
return require("Storage").readJSON("messages.json",1)||[];
|
messages = Array.isArray(messages) ? messages : []; // make sure we always return an array
|
||||||
}
|
if (withMessage && withMessage.id) exports.apply(withMessage, messages);
|
||||||
|
return messages;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if there are any messages
|
* Check if there are any messages
|
||||||
|
*
|
||||||
|
* @param {object} [withMessage] Apply this event to messages, see getMessages
|
||||||
* @returns {string} "new"/"old"/"none"
|
* @returns {string} "new"/"old"/"none"
|
||||||
*/
|
*/
|
||||||
exports.status = function() {
|
exports.status = function(withMessage) {
|
||||||
try {
|
try {
|
||||||
let status= "none";
|
let status = "none";
|
||||||
for(const m of exports.getMessages()) {
|
for(const m of exports.getMessages(withMessage)) {
|
||||||
if (["music", "map"].includes(m.id)) continue;
|
if (["music", "map"].includes(m.id)) continue;
|
||||||
if (m.new) return "new";
|
if (m.new) return "new";
|
||||||
status = "old";
|
status = "old";
|
||||||
}
|
}
|
||||||
return status;
|
return status;
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
return "none"; // don't bother e.g. the widget with errors
|
return "none"; // don't bother callers with errors
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -141,24 +200,24 @@ exports.getMessages = function() {
|
||||||
*/
|
*/
|
||||||
exports.buzz = function(msgSrc) {
|
exports.buzz = function(msgSrc) {
|
||||||
exports.stopBuzz(); // cancel any previous buzz timeouts
|
exports.stopBuzz(); // cancel any previous buzz timeouts
|
||||||
if ((require('Storage').readJSON('setting.json',1)||{}).quiet) return Promise.resolve(); // never buzz during Quiet Mode
|
if ((require("Storage").readJSON("setting.json", 1) || {}).quiet) return Promise.resolve(); // never buzz during Quiet Mode
|
||||||
var msgSettings = require('Storage').readJSON("messages.settings.json", true) || {};
|
const msgSettings = require("Storage").readJSON("messages.settings.json", true) || {};
|
||||||
var pattern;
|
let pattern;
|
||||||
if (msgSrc && msgSrc.toLowerCase() === "phone") {
|
if (msgSrc && msgSrc.toLowerCase()==="phone") {
|
||||||
// special vibration pattern for incoming calls
|
// special vibration pattern for incoming calls
|
||||||
pattern = msgSettings.vibrateCalls;
|
pattern = msgSettings.vibrateCalls;
|
||||||
} else {
|
} else {
|
||||||
pattern = msgSettings.vibrate;
|
pattern = msgSettings.vibrate;
|
||||||
}
|
}
|
||||||
if (pattern === undefined) { pattern = ":"; } // pattern may be "", so we can't use || ":" here
|
if (pattern===undefined) { pattern = ":"; } // pattern may be "", so we can't use || ":" here
|
||||||
if (!pattern) return Promise.resolve();
|
if (!pattern) return Promise.resolve();
|
||||||
|
|
||||||
var repeat = msgSettings.repeat;
|
let repeat = msgSettings.repeat;
|
||||||
if (repeat===undefined) repeat=4; // repeat may be zero
|
if (repeat===undefined) repeat = 4; // repeat may be zero
|
||||||
if (repeat) {
|
if (repeat) {
|
||||||
exports.buzzTimeout = setTimeout(()=>require("buzz").pattern(pattern), repeat*1000);
|
exports.buzzTimeout = setTimeout(() => require("buzz").pattern(pattern), repeat*1000);
|
||||||
var vibrateTimeout = msgSettings.vibrateTimeout;
|
let vibrateTimeout = msgSettings.vibrateTimeout;
|
||||||
if (vibrateTimeout===undefined) vibrateTimeout=60;
|
if (vibrateTimeout===undefined) vibrateTimeout = 60;
|
||||||
if (vibrateTimeout && !exports.stopTimeout) exports.stopTimeout = setTimeout(exports.stopBuzz, vibrateTimeout*1000);
|
if (vibrateTimeout && !exports.stopTimeout) exports.stopTimeout = setTimeout(exports.stopBuzz, vibrateTimeout*1000);
|
||||||
}
|
}
|
||||||
return require("buzz").pattern(pattern);
|
return require("buzz").pattern(pattern);
|
||||||
|
|
|
@ -1,23 +1,18 @@
|
||||||
{
|
{
|
||||||
"id": "messages",
|
"id": "messages",
|
||||||
"name": "Messages",
|
"name": "Messages",
|
||||||
"version": "0.54",
|
"version": "0.55",
|
||||||
"description": "App to display notifications from iOS and Gadgetbridge/Android",
|
"description": "Library to handle, load and store message events received from Android/iOS",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"type": "app",
|
"type": "module",
|
||||||
"tags": "tool,system",
|
"tags": "tool,system",
|
||||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||||
"dependencies" : { "messageicons":"module" },
|
"provides_modules" : ["messages"],
|
||||||
|
"dependencies" : { "messagegui":"module","messagewidget":"module" },
|
||||||
"readme": "README.md",
|
"readme": "README.md",
|
||||||
"storage": [
|
"storage": [
|
||||||
{"name":"messages.app.js","url":"app.js"},
|
{"name":"messages","url":"lib.js"},
|
||||||
{"name":"messages.new.js","url":"app-newmessage.js"},
|
{"name":"messages.settings.js","url":"settings.js"}
|
||||||
{"name":"messages.settings.js","url":"settings.js"},
|
|
||||||
{"name":"messages.img","url":"app-icon.js","evaluate":true},
|
|
||||||
{"name":"messages.wid.js","url":"widget.js"},
|
|
||||||
{"name":"messages","url":"lib.js"}
|
|
||||||
],
|
],
|
||||||
"data": [{"name":"messages.json"},{"name":"messages.settings.json"}],
|
"data": [{"name":"messages.json"},{"name":"messages.settings.json"}]
|
||||||
"screenshots": [{"url":"screenshot.png"},{"url":"screenshot-notify.gif"}],
|
|
||||||
"sortorder": -9
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,56 +0,0 @@
|
||||||
(() => {
|
|
||||||
if ((require('Storage').readJSON("messages.settings.json", true) || {}).maxMessages===0) return;
|
|
||||||
|
|
||||||
function filterMessages(msgs) {
|
|
||||||
return msgs.filter(msg => msg.new && msg.id != "music")
|
|
||||||
.map(m => m.src) // we only need this for icon/color
|
|
||||||
.filter((msg, i, arr) => arr.findIndex(nmsg => msg.src == nmsg.src) == i);
|
|
||||||
}
|
|
||||||
|
|
||||||
WIDGETS["messages"]={area:"tl", width:0, draw:function(recall) {
|
|
||||||
// If we had a setTimeout queued from the last time we were called, remove it
|
|
||||||
if (WIDGETS["messages"].i) {
|
|
||||||
clearTimeout(WIDGETS["messages"].i);
|
|
||||||
delete WIDGETS["messages"].i;
|
|
||||||
}
|
|
||||||
Bangle.removeListener('touch', this.touch);
|
|
||||||
if (!this.width) return;
|
|
||||||
let settings = Object.assign({flash:true, maxMessages:3},require('Storage').readJSON("messages.settings.json", true) || {});
|
|
||||||
if (recall !== true || settings.flash) {
|
|
||||||
var msgsShown = E.clip(this.msgs.length, 0, settings.maxMessages);
|
|
||||||
g.reset().clearRect(this.x, this.y, this.x+this.width, this.y+23);
|
|
||||||
for(let i = 0;i < msgsShown;i++) {
|
|
||||||
const msg = this.msgs[i];
|
|
||||||
const colors = [g.theme.bg,
|
|
||||||
require("messageicons").getColor(msg, {settings:settings})];
|
|
||||||
if (settings.flash && ((Date.now()/1000)&1)) {
|
|
||||||
if (colors[1] == g.theme.fg) {
|
|
||||||
colors.reverse();
|
|
||||||
} else {
|
|
||||||
colors[1] = g.theme.fg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
g.setColor(colors[1]).setBgColor(colors[0]);
|
|
||||||
// draw the icon, or '...' if too many messages
|
|
||||||
g.drawImage(i == (settings.maxMessages - 1) && this.msgs.length > settings.maxMessages ? atob("EASBAGGG88/zz2GG") : require("messageicons").getImage(msg),
|
|
||||||
this.x + 12 + i * 24, this.y + 12, {rotate:0/*force centering*/});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
WIDGETS["messages"].i=setTimeout(()=>WIDGETS["messages"].draw(true), 1000);
|
|
||||||
if (process.env.HWVERSION>1) Bangle.on('touch', this.touch);
|
|
||||||
},update:function(rawMsgs) {
|
|
||||||
const settings = Object.assign({maxMessages:3},require('Storage').readJSON("messages.settings.json", true) || {});
|
|
||||||
this.msgs = filterMessages(rawMsgs);
|
|
||||||
this.width = 24 * E.clip(this.msgs.length, 0, settings.maxMessages);
|
|
||||||
Bangle.drawWidgets();
|
|
||||||
},touch:function(b,c) {
|
|
||||||
var w=WIDGETS["messages"];
|
|
||||||
if (!w||!w.width||c.x<w.x||c.x>w.x+w.width||c.y<w.y||c.y>w.y+24) return;
|
|
||||||
load("messages.app.js");
|
|
||||||
}};
|
|
||||||
|
|
||||||
/* We might have returned here if we were in the Messages app for a
|
|
||||||
message but then the watch was never viewed. */
|
|
||||||
if (global.MESSAGES===undefined)
|
|
||||||
WIDGETS["messages"].update(require("messages").getMessages());
|
|
||||||
})();
|
|
|
@ -1,2 +1,3 @@
|
||||||
0.01: New App!
|
0.01: New App!
|
||||||
0.02: Remove one line of code that didn't do anything other than in some instances hinder the function of the app.
|
0.02: Remove one line of code that didn't do anything other than in some instances hinder the function of the app.
|
||||||
|
0.03: Use the new messages library
|
|
@ -1,15 +1,9 @@
|
||||||
Hacky app that uses Messages app and it's library to push a message that triggers the music controls. It's nearly not an app, and yet it moves.
|
Hacky app that uses Messages app and it's library to push a message that triggers the music controls. It's nearly not an app, and yet it moves.
|
||||||
|
|
||||||
This app require Messages setting 'Auto-open Music' to be 'Yes'. If it isn't, the app will change it to 'Yes' and let it stay that way.
|
|
||||||
|
|
||||||
Making the music controls accessible this way lets one start a music stream on the phone in some situations even though the message app didn't receive a music message from gadgetbridge to begin with. (I think.)
|
Making the music controls accessible this way lets one start a music stream on the phone in some situations even though the message app didn't receive a music message from gadgetbridge to begin with. (I think.)
|
||||||
|
|
||||||
It is suggested to use Messages Music along side the app Quick Launch.
|
It is suggested to use Messages Music along side the app Quick Launch.
|
||||||
|
|
||||||
Messages Music v0.02 has been verified to work with Messages v0.41 on Bangle.js 2 fw2v14.
|
|
||||||
|
|
||||||
Messages Music should work with forks of the original Messages app. At least as long as functions pushMessage() in the library and showMusicMessage() in app.js hasn't been changed too much.
|
|
||||||
|
|
||||||
Messages app is created by Gordon Williams with contributions from [Jeroen Peters](https://github.com/jeroenpeters1986).
|
Messages app is created by Gordon Williams with contributions from [Jeroen Peters](https://github.com/jeroenpeters1986).
|
||||||
|
|
||||||
The icon used for this app is from [https://icons8.com](https://icons8.com).
|
The icon used for this app is from [https://icons8.com](https://icons8.com).
|
||||||
|
|
|
@ -1,14 +1 @@
|
||||||
let showMusic = () => {
|
require('messages').openGUI({"t":"add","artist":" ","album":" ","track":" ","dur":0,"c":-1,"n":-1,"id":"music","title":"Music","state":"play","new":true});
|
||||||
Bangle.CLOCK = 1; // To pass condition in messages library
|
|
||||||
require('messages').pushMessage({"t":"add","artist":" ","album":" ","track":" ","dur":0,"c":-1,"n":-1,"id":"music","title":"Music","state":"play","new":true});
|
|
||||||
};
|
|
||||||
|
|
||||||
var settings = require('Storage').readJSON('messages.settings.json', true) || {}; //read settings if they exist else set to empty dict
|
|
||||||
if (!settings.openMusic) {
|
|
||||||
settings.openMusic = true; // This app/hack works as intended only if this setting is true
|
|
||||||
require('Storage').writeJSON('messages.settings.json', settings);
|
|
||||||
E.showMessage("First run:\n\nMessages setting\n\n 'Auto-Open Music'\n\n set to 'Yes'");
|
|
||||||
setTimeout(()=>{showMusic();}, 5000);
|
|
||||||
} else {
|
|
||||||
showMusic();
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "messagesmusic",
|
"id": "messagesmusic",
|
||||||
"name":"Messages Music",
|
"name":"Messages Music",
|
||||||
"version":"0.02",
|
"version":"0.03",
|
||||||
"description": "Uses Messages library to push a music message which in turn displays Messages app music controls",
|
"description": "Uses Messages library to push a music message which in turn displays Messages app music controls",
|
||||||
"icon":"app.png",
|
"icon":"app.png",
|
||||||
"type": "app",
|
"type": "app",
|
||||||
|
@ -13,6 +13,6 @@
|
||||||
{"name":"messagesmusic.app.js","url":"app.js"},
|
{"name":"messagesmusic.app.js","url":"app.js"},
|
||||||
{"name":"messagesmusic.img","url":"app-icon.js","evaluate":true}
|
{"name":"messagesmusic.img","url":"app-icon.js","evaluate":true}
|
||||||
],
|
],
|
||||||
"dependencies": {"messages":"app"}
|
"dependencies" : { "messagelib":"module" }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,3 +6,4 @@
|
||||||
0.06: renamed source files to match standard
|
0.06: renamed source files to match standard
|
||||||
0.07: Move mylocation app into 'Settings -> Apps'
|
0.07: Move mylocation app into 'Settings -> Apps'
|
||||||
0.08: Allow setting location from webinterface in the AppLoader
|
0.08: Allow setting location from webinterface in the AppLoader
|
||||||
|
0.09: Fix web interface so app can be installed (replaced custom with interface html)
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
# My Location
|
# My Location
|
||||||
|
|
||||||
*Sets and stores GPS lat and lon of your preferred city*
|
*Sets and stores GPS lat and lon of your preferred city*
|
||||||
|
|
||||||
To access, go to `Settings -> Apps -> My Location`
|
To access, you have two options:
|
||||||
|
|
||||||
|
**In the App Loader** once My Location is installed, click on the 'Save' icon
|
||||||
|
next to it - and you can choose your location on a map.
|
||||||
|
|
||||||
|
**On Bangle.js** go to `Settings -> Apps -> My Location`
|
||||||
|
|
||||||
* Select one of the preset Cities, setup through the GPS or use the webinterface from the AppLoader
|
* Select one of the preset Cities, setup through the GPS or use the webinterface from the AppLoader
|
||||||
* Other Apps can read this information to do calculations based on location
|
* Other Apps can read this information to do calculations based on location
|
||||||
|
|
|
@ -33,10 +33,11 @@
|
||||||
<div id="map">
|
<div id="map">
|
||||||
</div>
|
</div>
|
||||||
<div id="controls">
|
<div id="controls">
|
||||||
|
<span id="select-hint">Click the map to select a location</span>
|
||||||
<button id="select" class="btn btn-primary" style="display:none">Save</button><br/>
|
<button id="select" class="btn btn-primary" style="display:none">Save</button><br/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="../../core/lib/customize.js"></script>
|
<script src="../../core/lib/interface.js"></script>
|
||||||
<script src="https://unpkg.com/leaflet@1.0.3/dist/leaflet.js"></script>
|
<script src="https://unpkg.com/leaflet@1.0.3/dist/leaflet.js"></script>
|
||||||
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
|
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
|
||||||
<script src="../../webtools/heatshrink.js"></script>
|
<script src="../../webtools/heatshrink.js"></script>
|
||||||
|
@ -69,14 +70,18 @@
|
||||||
let latlon;
|
let latlon;
|
||||||
var marker;
|
var marker;
|
||||||
|
|
||||||
map.on('click', function(e){
|
function setPosition(ll) {
|
||||||
console.log(e);
|
latlon = ll;
|
||||||
if (map.hasLayer(marker)) {
|
if (map.hasLayer(marker)) {
|
||||||
map.removeLayer(marker);
|
map.removeLayer(marker);
|
||||||
}
|
}
|
||||||
latlon = e.latlng;
|
marker = new L.marker(latlon).addTo(map);
|
||||||
marker = new L.marker(e.latlng).addTo(map);
|
document.getElementById("select-hint").style.display="none";
|
||||||
document.getElementById("select").style.display="";
|
document.getElementById("select").style.display="";
|
||||||
|
}
|
||||||
|
|
||||||
|
map.on('click', function(e){
|
||||||
|
setPosition(e.latlng);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("select").addEventListener("click", function() {
|
document.getElementById("select").addEventListener("click", function() {
|
||||||
|
@ -87,9 +92,23 @@
|
||||||
Util.showModal("Saving...");
|
Util.showModal("Saving...");
|
||||||
Util.writeStorage("mylocation.json", JSON.stringify(settings), ()=>{
|
Util.writeStorage("mylocation.json", JSON.stringify(settings), ()=>{
|
||||||
Util.hideModal();
|
Util.hideModal();
|
||||||
|
Util.close(); // close this window
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function onInit() {
|
||||||
|
// read existing location
|
||||||
|
Util.readStorage("mylocation.json", function(data) {
|
||||||
|
if (data===undefined) return; // no file
|
||||||
|
try {
|
||||||
|
var j = JSON.parse(data);
|
||||||
|
setPosition(j);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -4,12 +4,12 @@
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"type": "settings",
|
"type": "settings",
|
||||||
"screenshots": [{"url":"screenshot_1.png"}],
|
"screenshots": [{"url":"screenshot_1.png"}],
|
||||||
"version":"0.08",
|
"version":"0.09",
|
||||||
"description": "Sets and stores the latitude and longitude of your preferred City. It can be set from GPS or webinterface. `mylocation.json` can be used by other apps that need your main location. See README for details.",
|
"description": "Sets and stores the latitude and longitude of your preferred City. It can be set from GPS or webinterface. `mylocation.json` can be used by other apps that need your main location. See README for details.",
|
||||||
"readme": "README.md",
|
"readme": "README.md",
|
||||||
"tags": "tool,utility",
|
"tags": "tool,utility",
|
||||||
"supports": ["BANGLEJS", "BANGLEJS2"],
|
"supports": ["BANGLEJS", "BANGLEJS2"],
|
||||||
"custom": "custom.html","custom": "custom.html",
|
"interface": "interface.html",
|
||||||
"storage": [
|
"storage": [
|
||||||
{"name":"mylocation.settings.js","url":"settings.js"}
|
{"name":"mylocation.settings.js","url":"settings.js"}
|
||||||
],
|
],
|
||||||
|
|
|
@ -12,3 +12,7 @@
|
||||||
Fix alignment of satellite info text
|
Fix alignment of satellite info text
|
||||||
0.12: switch to using normal OpenStreetMap tiles (opentopomap was too slow)
|
0.12: switch to using normal OpenStreetMap tiles (opentopomap was too slow)
|
||||||
0.13: Use a single image file with 'frames' of data (drastically reduces file count, possibility of >1 map on device)
|
0.13: Use a single image file with 'frames' of data (drastically reduces file count, possibility of >1 map on device)
|
||||||
|
0.14: Added ability to upload multiple sets of map tiles
|
||||||
|
Support for zooming in on map
|
||||||
|
Satellite count moved to widget bar to leave more room for the map
|
||||||
|
0.15: Make track drawing an option (default off)
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
# OpenStreetMap
|
||||||
|
|
||||||
|
This app allows you to upload and use OpenSteetMap map tiles onto your
|
||||||
|
Bangle. There's an uploader, the app, and also a library that
|
||||||
|
allows you to use the maps in your Bangle.js applications.
|
||||||
|
|
||||||
|
## Uploader
|
||||||
|
|
||||||
|
Once you've installed OpenStreepMap on your Bangle, find it
|
||||||
|
in the App Loader and click the Disk icon next to it.
|
||||||
|
|
||||||
|
A window will pop up showing what maps you have loaded.
|
||||||
|
|
||||||
|
To add a map:
|
||||||
|
|
||||||
|
* Click `Add Map`
|
||||||
|
* Scroll and zoom to the area of interest or use the Search button in the top left
|
||||||
|
* Now choose the size you want to upload (Small/Medium/etc)
|
||||||
|
* On Bangle.js 1 you can choose if you want a 3 bits per pixel map (this is lower
|
||||||
|
quality but uploads faster and takes less space). On Bangle.js 2 you only have a 3bpp
|
||||||
|
display so can only use 3bpp.
|
||||||
|
* Click `Get Map`, and a preview will be displayed. If you need to adjust the area you
|
||||||
|
can change settings, move the map around, and click `Get Map` again.
|
||||||
|
* When you're ready, click `Upload`
|
||||||
|
|
||||||
|
## Bangle.js App
|
||||||
|
|
||||||
|
The Bangle.js app allows you to view a map - it also turns the GPS on and marks
|
||||||
|
the path that you've been travelling (if enabled).
|
||||||
|
|
||||||
|
* Drag on the screen to move the map
|
||||||
|
* Press the button to bring up a menu, where you can zoom, go to GPS location
|
||||||
|
, put the map back in its default location, or choose whether to draw the currently
|
||||||
|
recording GPS track (from the `Recorder` app).
|
||||||
|
|
||||||
|
**Note:** If enabled, drawing the currently recorded GPS track can take a second
|
||||||
|
or two (which happens after you've finished scrolling the screen with your finger).
|
||||||
|
|
||||||
|
|
||||||
|
## Library
|
||||||
|
|
||||||
|
See the documentation in the library itself for full usage info:
|
||||||
|
https://github.com/espruino/BangleApps/blob/master/apps/openstmap/openstmap.js
|
||||||
|
|
||||||
|
Or check the app itself: https://github.com/espruino/BangleApps/blob/master/apps/openstmap/app.js
|
||||||
|
|
||||||
|
But in the most simple form:
|
||||||
|
|
||||||
|
```
|
||||||
|
var m = require("openstmap");
|
||||||
|
// m.lat/lon are now the center of the loaded map
|
||||||
|
m.draw(); // draw centered on the middle of the loaded map
|
||||||
|
```
|
|
@ -1,20 +1,31 @@
|
||||||
var m = require("openstmap");
|
var m = require("openstmap");
|
||||||
var HASWIDGETS = true;
|
var HASWIDGETS = true;
|
||||||
var y1,y2;
|
var R;
|
||||||
var fix = {};
|
var fix = {};
|
||||||
|
var mapVisible = false;
|
||||||
|
var hasScrolled = false;
|
||||||
|
var settings = require("Storage").readJSON("openstmap.json",1)||{};
|
||||||
|
|
||||||
|
// Redraw the whole page
|
||||||
function redraw() {
|
function redraw() {
|
||||||
g.setClipRect(0,y1,g.getWidth()-1,y2);
|
g.setClipRect(R.x,R.y,R.x2,R.y2);
|
||||||
m.draw();
|
m.draw();
|
||||||
drawMarker();
|
drawMarker();
|
||||||
if (WIDGETS["gpsrec"] && WIDGETS["gpsrec"].plotTrack) {
|
// if track drawing is enabled...
|
||||||
g.flip(); // force immediate draw on double-buffered screens - track will update later
|
if (settings.drawTrack) {
|
||||||
g.setColor(0.75,0.2,0);
|
if (HASWIDGETS && WIDGETS["gpsrec"] && WIDGETS["gpsrec"].plotTrack) {
|
||||||
|
g.setColor("#f00").flip(); // force immediate draw on double-buffered screens - track will update later
|
||||||
WIDGETS["gpsrec"].plotTrack(m);
|
WIDGETS["gpsrec"].plotTrack(m);
|
||||||
}
|
}
|
||||||
|
if (HASWIDGETS && WIDGETS["recorder"] && WIDGETS["recorder"].plotTrack) {
|
||||||
|
g.setColor("#f00").flip(); // force immediate draw on double-buffered screens - track will update later
|
||||||
|
WIDGETS["recorder"].plotTrack(m);
|
||||||
|
}
|
||||||
|
}
|
||||||
g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
|
g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw the marker for where we are
|
||||||
function drawMarker() {
|
function drawMarker() {
|
||||||
if (!fix.fix) return;
|
if (!fix.fix) return;
|
||||||
var p = m.latLonToXY(fix.lat, fix.lon);
|
var p = m.latLonToXY(fix.lat, fix.lon);
|
||||||
|
@ -22,44 +33,33 @@ function drawMarker() {
|
||||||
g.fillRect(p.x-2, p.y-2, p.x+2, p.y+2);
|
g.fillRect(p.x-2, p.y-2, p.x+2, p.y+2);
|
||||||
}
|
}
|
||||||
|
|
||||||
var fix;
|
|
||||||
Bangle.on('GPS',function(f) {
|
Bangle.on('GPS',function(f) {
|
||||||
fix=f;
|
fix=f;
|
||||||
g.reset().clearRect(0,y1,g.getWidth()-1,y1+8).setFont("6x8").setFontAlign(0,0);
|
if (HASWIDGETS) WIDGETS["sats"].draw(WIDGETS["sats"]);
|
||||||
var txt = fix.satellites+" satellites";
|
if (mapVisible) drawMarker();
|
||||||
if (!fix.fix)
|
|
||||||
txt += " - NO FIX";
|
|
||||||
g.drawString(txt,g.getWidth()/2,y1 + 4);
|
|
||||||
drawMarker();
|
|
||||||
});
|
});
|
||||||
Bangle.setGPSPower(1, "app");
|
Bangle.setGPSPower(1, "app");
|
||||||
|
|
||||||
if (HASWIDGETS) {
|
if (HASWIDGETS) {
|
||||||
Bangle.loadWidgets();
|
Bangle.loadWidgets();
|
||||||
|
WIDGETS["sats"] = { area:"tl", width:48, draw:w=>{
|
||||||
|
var txt = (0|fix.satellites)+" Sats";
|
||||||
|
if (!fix.fix) txt += "\nNO FIX";
|
||||||
|
g.reset().setFont("6x8").setFontAlign(0,0)
|
||||||
|
.drawString(txt,w.x+24,w.y+12);
|
||||||
|
}
|
||||||
|
};
|
||||||
Bangle.drawWidgets();
|
Bangle.drawWidgets();
|
||||||
y1 = 24;
|
|
||||||
var hasBottomRow = Object.keys(WIDGETS).some(w=>WIDGETS[w].area[0]=="b");
|
|
||||||
y2 = g.getHeight() - (hasBottomRow ? 24 : 1);
|
|
||||||
} else {
|
|
||||||
y1=0;
|
|
||||||
y2=g.getHeight()-1;
|
|
||||||
}
|
}
|
||||||
|
R = Bangle.appRect;
|
||||||
|
|
||||||
redraw();
|
function showMap() {
|
||||||
|
mapVisible = true;
|
||||||
function recenter() {
|
g.reset().clearRect(R);
|
||||||
if (!fix.fix) return;
|
|
||||||
m.lat = fix.lat;
|
|
||||||
m.lon = fix.lon;
|
|
||||||
redraw();
|
redraw();
|
||||||
}
|
Bangle.setUI({mode:"custom",drag:e=>{
|
||||||
|
|
||||||
setWatch(recenter, global.BTN2?BTN2:BTN1, {repeat:true});
|
|
||||||
|
|
||||||
var hasScrolled = false;
|
|
||||||
Bangle.on('drag',e=>{
|
|
||||||
if (e.b) {
|
if (e.b) {
|
||||||
g.setClipRect(0,y1,g.getWidth()-1,y2);
|
g.setClipRect(R.x,R.y,R.x2,R.y2);
|
||||||
g.scroll(e.dx,e.dy);
|
g.scroll(e.dx,e.dy);
|
||||||
m.scroll(e.dx,e.dy);
|
m.scroll(e.dx,e.dy);
|
||||||
g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
|
g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
|
||||||
|
@ -68,4 +68,35 @@ Bangle.on('drag',e=>{
|
||||||
hasScrolled = false;
|
hasScrolled = false;
|
||||||
redraw();
|
redraw();
|
||||||
}
|
}
|
||||||
});
|
}, btn: btn=>{
|
||||||
|
mapVisible = false;
|
||||||
|
var menu = {"":{title:"Map"},
|
||||||
|
"< Back": ()=> showMap(),
|
||||||
|
/*LANG*/"Zoom In": () =>{
|
||||||
|
m.scale /= 2;
|
||||||
|
showMap();
|
||||||
|
},
|
||||||
|
/*LANG*/"Zoom Out": () =>{
|
||||||
|
m.scale *= 2;
|
||||||
|
showMap();
|
||||||
|
},
|
||||||
|
/*LANG*/"Draw Track": {
|
||||||
|
value : !!settings.drawTrack,
|
||||||
|
onchange : v => { settings.drawTrack=v; require("Storage").writeJSON("openstmap.json",settings); }
|
||||||
|
},
|
||||||
|
/*LANG*/"Center Map": () =>{
|
||||||
|
m.lat = m.map.lat;
|
||||||
|
m.lon = m.map.lon;
|
||||||
|
m.scale = m.map.scale;
|
||||||
|
showMap();
|
||||||
|
}};
|
||||||
|
if (fix.fix) menu[/*LANG*/"Center GPS"]=() =>{
|
||||||
|
m.lat = fix.lat;
|
||||||
|
m.lon = fix.lon;
|
||||||
|
showMap();
|
||||||
|
};
|
||||||
|
E.showMenu(menu);
|
||||||
|
}});
|
||||||
|
}
|
||||||
|
|
||||||
|
showMap();
|
||||||
|
|
|
@ -9,7 +9,8 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
html, body, #map {
|
html, body, #map, #mapsLoaded, #mapContainer {
|
||||||
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
@ -27,20 +28,40 @@
|
||||||
width: 256px;
|
width: 256px;
|
||||||
height: 256px;
|
height: 256px;
|
||||||
}
|
}
|
||||||
|
.tile-title {
|
||||||
|
font-weight:bold;
|
||||||
|
font-size: 125%;
|
||||||
|
}
|
||||||
|
.tile-map {
|
||||||
|
width: 128px;
|
||||||
|
height: 128px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div id="mapsLoadedContainer">
|
||||||
|
</div>
|
||||||
|
<div id="mapContainer">
|
||||||
<div id="map">
|
<div id="map">
|
||||||
</div>
|
</div>
|
||||||
<div id="controls">
|
<div id="controls">
|
||||||
<div style="display:inline-block;text-align:center;vertical-align: top;" id="3bitdiv"> <input type="checkbox" id="3bit"></input><br/><span>3 bit</span></div>
|
<div style="display:inline-block;text-align:center;vertical-align: top;" id="3bitdiv"> <input type="checkbox" id="3bit"></input><br/><span>3 bit</span></div>
|
||||||
<button id="getmap" class="btn btn-primary">Get Map</button><br/>
|
<div class="form-group" style="display:inline-block;">
|
||||||
|
<select class="form-select" id="mapSize">
|
||||||
|
<option value="4">Small</option>
|
||||||
|
<option value="5" selected>Medium</option>
|
||||||
|
<option value="6">Large</option>
|
||||||
|
<option value="7">XL</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button id="getmap" class="btn btn-primary">Get Map</button><button class="btn" onclick="showLoadedMaps()">Map List</button><br/>
|
||||||
<canvas id="maptiles" style="display:none"></canvas>
|
<canvas id="maptiles" style="display:none"></canvas>
|
||||||
<div id="uploadbuttons" style="display:none"><button id="upload" class="btn btn-primary">Upload</button>
|
<div id="uploadbuttons" style="display:none"><button id="upload" class="btn btn-primary">Upload</button>
|
||||||
<button id="cancel" class="btn">Cancel</button></div>
|
<button id="cancel" class="btn">Cancel</button></div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="../../core/lib/customize.js"></script>
|
<script src="../../core/lib/interface.js"></script>
|
||||||
<script src="https://unpkg.com/leaflet@1.0.3/dist/leaflet.js"></script>
|
<script src="https://unpkg.com/leaflet@1.0.3/dist/leaflet.js"></script>
|
||||||
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
|
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
|
||||||
<script src="../../webtools/heatshrink.js"></script>
|
<script src="../../webtools/heatshrink.js"></script>
|
||||||
|
@ -60,8 +81,6 @@ TODO:
|
||||||
*/
|
*/
|
||||||
var TILESIZE = 96; // Size of our tiles
|
var TILESIZE = 96; // Size of our tiles
|
||||||
var OSMTILESIZE = 256; // Size of openstreetmap tiles
|
var OSMTILESIZE = 256; // Size of openstreetmap tiles
|
||||||
var MAPSIZE = TILESIZE*5; ///< 480 - Size of map we download
|
|
||||||
var OSMTILECOUNT = 3; // how many tiles do we download in each direction (Math.floor(MAPSIZE / OSMTILESIZE)+1)
|
|
||||||
/* Can see possible tiles on http://leaflet-extras.github.io/leaflet-providers/preview/
|
/* Can see possible tiles on http://leaflet-extras.github.io/leaflet-providers/preview/
|
||||||
However some don't allow cross-origin use */
|
However some don't allow cross-origin use */
|
||||||
//var TILELAYER = 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png'; // simple, high contrast, TOO SLOW
|
//var TILELAYER = 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png'; // simple, high contrast, TOO SLOW
|
||||||
|
@ -69,8 +88,8 @@ TODO:
|
||||||
var TILELAYER = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
|
var TILELAYER = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
|
||||||
var PREVIEWTILELAYER = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
|
var PREVIEWTILELAYER = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
|
||||||
|
|
||||||
// Create map and try and set the location to where the browser thinks we are
|
var loadedMaps = [];
|
||||||
var map = L.map('map').locate({setView: true, maxZoom: 16, enableHighAccuracy:true});
|
|
||||||
// Tiles used for Bangle.js itself
|
// Tiles used for Bangle.js itself
|
||||||
var bangleTileLayer = L.tileLayer(TILELAYER, {
|
var bangleTileLayer = L.tileLayer(TILELAYER, {
|
||||||
maxZoom: 18,
|
maxZoom: 18,
|
||||||
|
@ -83,6 +102,10 @@ TODO:
|
||||||
});
|
});
|
||||||
// Could optionally overlay trails: https://wiki.openstreetmap.org/wiki/Tiles
|
// Could optionally overlay trails: https://wiki.openstreetmap.org/wiki/Tiles
|
||||||
|
|
||||||
|
// Create map and try and set the location to where the browser thinks we are
|
||||||
|
var map = L.map('map').locate({setView: true, maxZoom: 16, enableHighAccuracy:true});
|
||||||
|
previewTileLayer.addTo(map);
|
||||||
|
|
||||||
// Search box:
|
// Search box:
|
||||||
const searchProvider = new window.GeoSearch.OpenStreetMapProvider();
|
const searchProvider = new window.GeoSearch.OpenStreetMapProvider();
|
||||||
const searchControl = new GeoSearch.GeoSearchControl({
|
const searchControl = new GeoSearch.GeoSearchControl({
|
||||||
|
@ -96,6 +119,7 @@ TODO:
|
||||||
});
|
});
|
||||||
map.addControl(searchControl);
|
map.addControl(searchControl);
|
||||||
|
|
||||||
|
// ---------------------------------------- Run at startup
|
||||||
function onInit(device) {
|
function onInit(device) {
|
||||||
if (device && device.info && device.info.g) {
|
if (device && device.info && device.info.g) {
|
||||||
// On 3 bit devices, don't even offer the option. 3 bit is the only way
|
// On 3 bit devices, don't even offer the option. 3 bit is the only way
|
||||||
|
@ -104,12 +128,120 @@ TODO:
|
||||||
document.getElementById("3bitdiv").style = "display:none";
|
document.getElementById("3bitdiv").style = "display:none";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showLoadedMaps();
|
||||||
}
|
}
|
||||||
|
|
||||||
var mapFiles = [];
|
function showLoadedMaps() {
|
||||||
previewTileLayer.addTo(map);
|
document.getElementById("mapsLoadedContainer").style.display = "";
|
||||||
|
document.getElementById("mapContainer").style.display = "none";
|
||||||
|
|
||||||
function tilesLoaded(ctx, width, height) {
|
Util.showModal("Loading maps...");
|
||||||
|
let mapsLoadedContainer = document.getElementById("mapsLoadedContainer");
|
||||||
|
mapsLoadedContainer.innerHTML = "";
|
||||||
|
loadedMaps = [];
|
||||||
|
|
||||||
|
Puck.write(`\x10Bluetooth.println(require("Storage").list(/openstmap\\.\\d+\\.json/))\n`,function(files) {
|
||||||
|
console.log("MAPS:",files);
|
||||||
|
let promise = Promise.resolve();
|
||||||
|
files.trim().split(",").forEach(filename => {
|
||||||
|
if (filename=="") return;
|
||||||
|
promise = promise.then(() => new Promise(resolve => {
|
||||||
|
Util.readStorage(filename, fileContents => {
|
||||||
|
console.log(filename + " => " + fileContents);
|
||||||
|
let mapNumber = filename.match(/\d+/)[0]; // figure out what map number we are
|
||||||
|
let mapInfo;
|
||||||
|
try {
|
||||||
|
mapInfo = JSON.parse(fileContents);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
loadedMaps[mapNumber] = mapInfo;
|
||||||
|
if (mapInfo!==undefined) {
|
||||||
|
let latlon = L.latLng(mapInfo.lat, mapInfo.lon);
|
||||||
|
mapsLoadedContainer.innerHTML += `
|
||||||
|
<div class="tile">
|
||||||
|
<div class="tile-icon">
|
||||||
|
<div class="tile-map" id="tile-map-${mapNumber}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tile-content">
|
||||||
|
<p class="tile-title">Map ${mapNumber}</p>
|
||||||
|
<p class="tile-subtitle">${mapInfo.w*mapInfo.h} Tiles (${((mapInfo.imgx*mapInfo.imgy)>>11).toFixed(0)}k)</p>
|
||||||
|
</div>
|
||||||
|
<div class="tile-action">
|
||||||
|
<button class="btn btn-primary" onclick="onMapDelete(${mapNumber})">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
let map = L.map(`tile-map-${mapNumber}`);
|
||||||
|
L.tileLayer(PREVIEWTILELAYER, {
|
||||||
|
maxZoom: 18
|
||||||
|
}).addTo(map);
|
||||||
|
let marker = new L.marker(latlon).addTo(map);
|
||||||
|
map.fitBounds(latlon.toBounds(2000/*meters*/), {animation: false});
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
promise = promise.then(() => new Promise(resolve => {
|
||||||
|
if (!loadedMaps.length) {
|
||||||
|
mapsLoadedContainer.innerHTML += `
|
||||||
|
<div class="tile">
|
||||||
|
<div class="tile-icon">
|
||||||
|
<div class="tile-map">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tile-content">
|
||||||
|
<p class="tile-title">No Maps Loaded</p>
|
||||||
|
</div>
|
||||||
|
<div class="tile-action">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
mapsLoadedContainer.innerHTML += `
|
||||||
|
<div class="tile">
|
||||||
|
<div class="tile-icon">
|
||||||
|
<div class="tile-map">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tile-content">
|
||||||
|
</div>
|
||||||
|
<div class="tile-action">
|
||||||
|
<button class="btn btn-primary" onclick="showMap()">Add Map</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
Util.hideModal();
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMapDelete(mapNumber) {
|
||||||
|
console.log("delete", mapNumber);
|
||||||
|
Util.showModal(`Erasing map ${mapNumber}...`);
|
||||||
|
Util.eraseStorage(`openstmap.${mapNumber}.json`, function() {
|
||||||
|
Util.eraseStorage(`openstmap.${mapNumber}.img`, function() {
|
||||||
|
Util.hideModal();
|
||||||
|
showLoadedMaps();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showMap() {
|
||||||
|
document.getElementById("mapsLoadedContainer").style.display = "none";
|
||||||
|
document.getElementById("mapContainer").style.display = "";
|
||||||
|
document.getElementById("maptiles").style.display="none";
|
||||||
|
document.getElementById("uploadbuttons").style.display="none";
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------
|
||||||
|
var mapFiles = [];
|
||||||
|
|
||||||
|
// convert canvas into an actual tiled image file
|
||||||
|
function tilesLoaded(ctx, width, height, mapImageFile) {
|
||||||
var options = {
|
var options = {
|
||||||
compression:false, output:"raw",
|
compression:false, output:"raw",
|
||||||
mode:"web"
|
mode:"web"
|
||||||
|
@ -118,6 +250,7 @@ TODO:
|
||||||
options = {
|
options = {
|
||||||
compression:false, output:"raw",
|
compression:false, output:"raw",
|
||||||
mode:"3bit",
|
mode:"3bit",
|
||||||
|
diffusion:"bayer2"
|
||||||
};
|
};
|
||||||
/* If in 3 bit mode, go through all the data beforehand and
|
/* If in 3 bit mode, go through all the data beforehand and
|
||||||
turn the saturation up to maximum, so when thresholded it
|
turn the saturation up to maximum, so when thresholded it
|
||||||
|
@ -166,12 +299,17 @@ TODO:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return [{
|
return [{
|
||||||
name:"openstmap.0.img",
|
name:mapImageFile,
|
||||||
content:tiledImage
|
content:tiledImage
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("getmap").addEventListener("click", function() {
|
document.getElementById("getmap").addEventListener("click", function() {
|
||||||
|
|
||||||
|
var MAPTILES = parseInt(document.getElementById("mapSize").value);
|
||||||
|
var MAPSIZE = TILESIZE*MAPTILES; /// Size of map we download to Bangle in pixels
|
||||||
|
var OSMTILECOUNT = (Math.ceil((MAPSIZE+TILESIZE) / OSMTILESIZE)+1); // how many tiles do we download from OSM in each direction
|
||||||
|
|
||||||
var zoom = map.getZoom();
|
var zoom = map.getZoom();
|
||||||
var centerlatlon = map.getBounds().getCenter();
|
var centerlatlon = map.getBounds().getCenter();
|
||||||
var center = map.project(centerlatlon, zoom).divideBy(OSMTILESIZE);
|
var center = map.project(centerlatlon, zoom).divideBy(OSMTILESIZE);
|
||||||
|
@ -242,8 +380,11 @@ TODO:
|
||||||
|
|
||||||
Promise.all(tileGetters).then(() => {
|
Promise.all(tileGetters).then(() => {
|
||||||
document.getElementById("uploadbuttons").style.display="";
|
document.getElementById("uploadbuttons").style.display="";
|
||||||
mapFiles = tilesLoaded(ctx, canvas.width, canvas.height);
|
var mapNumber = 0;
|
||||||
mapFiles.unshift({name:"openstmap.0.json",content:JSON.stringify({
|
while (loadedMaps[mapNumber]) mapNumber++;
|
||||||
|
let mapImageFile = `openstmap.${mapNumber}.img`;
|
||||||
|
mapFiles = tilesLoaded(ctx, canvas.width, canvas.height, mapImageFile);
|
||||||
|
mapFiles.unshift({name:`openstmap.${mapNumber}.json`,content:JSON.stringify({
|
||||||
imgx : canvas.width,
|
imgx : canvas.width,
|
||||||
imgy : canvas.height,
|
imgy : canvas.height,
|
||||||
tilesize : TILESIZE,
|
tilesize : TILESIZE,
|
||||||
|
@ -252,21 +393,31 @@ TODO:
|
||||||
lon : centerlatlon.lng,
|
lon : centerlatlon.lng,
|
||||||
w : Math.round(canvas.width / TILESIZE), // width in tiles
|
w : Math.round(canvas.width / TILESIZE), // width in tiles
|
||||||
h : Math.round(canvas.height / TILESIZE), // height in tiles
|
h : Math.round(canvas.height / TILESIZE), // height in tiles
|
||||||
fn : "openstmap.0.img"
|
fn : mapImageFile
|
||||||
})});
|
})});
|
||||||
console.log(mapFiles);
|
console.log(mapFiles);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("upload").addEventListener("click", function() {
|
document.getElementById("upload").addEventListener("click", function() {
|
||||||
sendCustomizedApp({
|
Util.showModal("Uploading...");
|
||||||
storage:mapFiles
|
let promise = Promise.resolve();
|
||||||
|
mapFiles.forEach(file => {
|
||||||
|
promise = promise.then(function() {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
Util.writeStorage(file.name, file.content, resolve);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
promise.then(function() {
|
||||||
|
Util.hideModal();
|
||||||
|
console.log("Upload Complete");
|
||||||
|
showLoadedMaps();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("cancel").addEventListener("click", function() {
|
document.getElementById("cancel").addEventListener("click", function() {
|
||||||
document.getElementById("maptiles").style.display="none";
|
showMap();
|
||||||
document.getElementById("uploadbuttons").style.display="none";
|
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
|
@ -2,17 +2,21 @@
|
||||||
"id": "openstmap",
|
"id": "openstmap",
|
||||||
"name": "OpenStreetMap",
|
"name": "OpenStreetMap",
|
||||||
"shortName": "OpenStMap",
|
"shortName": "OpenStMap",
|
||||||
"version": "0.13",
|
"version": "0.15",
|
||||||
"description": "Loads map tiles from OpenStreetMap onto your Bangle.js and displays a map of where you are. Once installed this also adds map functionality to `GPS Recorder` and `Recorder` apps",
|
"description": "Loads map tiles from OpenStreetMap onto your Bangle.js and displays a map of where you are. Once installed this also adds map functionality to `GPS Recorder` and `Recorder` apps",
|
||||||
|
"readme": "README.md",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"tags": "outdoors,gps,osm",
|
"tags": "outdoors,gps,osm",
|
||||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||||
"screenshots": [{"url":"screenshot.png"}],
|
"screenshots": [{"url":"screenshot.png"}],
|
||||||
"custom": "custom.html",
|
"interface": "interface.html",
|
||||||
"customConnect": true,
|
|
||||||
"storage": [
|
"storage": [
|
||||||
{"name":"openstmap","url":"openstmap.js"},
|
{"name":"openstmap","url":"openstmap.js"},
|
||||||
{"name":"openstmap.app.js","url":"app.js"},
|
{"name":"openstmap.app.js","url":"app.js"},
|
||||||
{"name":"openstmap.img","url":"app-icon.js","evaluate":true}
|
{"name":"openstmap.img","url":"app-icon.js","evaluate":true}
|
||||||
|
], "data": [
|
||||||
|
{"name":"openstmap.json"},
|
||||||
|
{"wildcard":"openstmap.*.json"},
|
||||||
|
{"wildcard":"openstmap.*.img"}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,32 +20,59 @@ function center() {
|
||||||
m.draw();
|
m.draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// you can even change the scale - eg 'm/scale *= 2'
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var map = require("Storage").readJSON("openstmap.0.json");
|
|
||||||
map.center = Bangle.project({lat:map.lat,lon:map.lon});
|
|
||||||
exports.map = map;
|
|
||||||
exports.lat = map.lat; // actual position of middle of screen
|
|
||||||
exports.lon = map.lon; // actual position of middle of screen
|
|
||||||
var m = exports;
|
var m = exports;
|
||||||
|
m.maps = require("Storage").list(/openstmap\.\d+\.json/).map(f=>{
|
||||||
|
let map = require("Storage").readJSON(f);
|
||||||
|
map.center = Bangle.project({lat:map.lat,lon:map.lon});
|
||||||
|
return map;
|
||||||
|
});
|
||||||
|
// we base our start position on the middle of the first map
|
||||||
|
m.map = m.maps[0];
|
||||||
|
m.scale = m.map.scale; // current scale (based on first map)
|
||||||
|
m.lat = m.map.lat; // position of middle of screen
|
||||||
|
m.lon = m.map.lon; // position of middle of screen
|
||||||
|
|
||||||
exports.draw = function() {
|
exports.draw = function() {
|
||||||
var img = require("Storage").read(map.fn);
|
|
||||||
var cx = g.getWidth()/2;
|
var cx = g.getWidth()/2;
|
||||||
var cy = g.getHeight()/2;
|
var cy = g.getHeight()/2;
|
||||||
var p = Bangle.project({lat:m.lat,lon:m.lon});
|
var p = Bangle.project({lat:m.lat,lon:m.lon});
|
||||||
var ix = (p.x-map.center.x)/map.scale + (map.imgx/2) - cx;
|
m.maps.forEach((map,idx) => {
|
||||||
var iy = (map.center.y-p.y)/map.scale + (map.imgy/2) - cy;
|
var d = map.scale/m.scale;
|
||||||
//console.log(ix,iy);
|
var ix = (p.x-map.center.x)/m.scale + (map.imgx*d/2) - cx;
|
||||||
var tx = 0|(ix/map.tilesize);
|
var iy = (map.center.y-p.y)/m.scale + (map.imgy*d/2) - cy;
|
||||||
var ty = 0|(iy/map.tilesize);
|
var o = {};
|
||||||
var ox = (tx*map.tilesize)-ix;
|
var s = map.tilesize;
|
||||||
var oy = (ty*map.tilesize)-iy;
|
if (d!=1) { // if the two are different, add scaling
|
||||||
for (var x=ox,ttx=tx;x<g.getWidth();x+=map.tilesize,ttx++)
|
s *= d;
|
||||||
for (var y=oy,tty=ty;y<g.getHeight();y+=map.tilesize,tty++) {
|
o.scale = d;
|
||||||
if (ttx>=0 && ttx<map.w && tty>=0 && tty<map.h) g.drawImage(img,x,y,{frame:ttx+(tty*map.w)});
|
|
||||||
else g.clearRect(x,y,x+map.tilesize-1,y+map.tilesize-1).drawLine(x,y,x+map.tilesize-1,y+map.tilesize-1).drawLine(x,y+map.tilesize-1,x+map.tilesize-1,y);
|
|
||||||
}
|
}
|
||||||
|
//console.log(ix,iy);
|
||||||
|
var tx = 0|(ix/s);
|
||||||
|
var ty = 0|(iy/s);
|
||||||
|
var ox = (tx*s)-ix;
|
||||||
|
var oy = (ty*s)-iy;
|
||||||
|
var img = require("Storage").read(map.fn);
|
||||||
|
// fix out of range so we don't have to iterate over them
|
||||||
|
if (tx<0) {
|
||||||
|
ox+=s*-tx;
|
||||||
|
tx=0;
|
||||||
|
}
|
||||||
|
if (ty<0) {
|
||||||
|
oy+=s*-ty;
|
||||||
|
ty=0;
|
||||||
|
}
|
||||||
|
var mx = g.getWidth();
|
||||||
|
var my = g.getHeight();
|
||||||
|
for (var x=ox,ttx=tx; x<mx && ttx<map.w; x+=s,ttx++)
|
||||||
|
for (var y=oy,tty=ty;y<my && tty<map.h;y+=s,tty++) {
|
||||||
|
o.frame = ttx+(tty*map.w);
|
||||||
|
g.drawImage(img,x,y,o);
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Convert lat/lon to pixels on the screen
|
/// Convert lat/lon to pixels on the screen
|
||||||
|
@ -55,15 +82,15 @@ exports.latLonToXY = function(lat, lon) {
|
||||||
var cx = g.getWidth()/2;
|
var cx = g.getWidth()/2;
|
||||||
var cy = g.getHeight()/2;
|
var cy = g.getHeight()/2;
|
||||||
return {
|
return {
|
||||||
x : (q.x-p.x)/map.scale + cx,
|
x : (q.x-p.x)/m.scale + cx,
|
||||||
y : cy - (q.y-p.y)/map.scale
|
y : cy - (q.y-p.y)/m.scale
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Given an amount to scroll in pixels on the screen, adjust the lat/lon of the map to match
|
/// Given an amount to scroll in pixels on the screen, adjust the lat/lon of the map to match
|
||||||
exports.scroll = function(x,y) {
|
exports.scroll = function(x,y) {
|
||||||
var a = Bangle.project({lat:this.lat,lon:this.lon});
|
var a = Bangle.project({lat:m.lat,lon:m.lon});
|
||||||
var b = Bangle.project({lat:this.lat+1,lon:this.lon+1});
|
var b = Bangle.project({lat:m.lat+1,lon:m.lon+1});
|
||||||
this.lon += x * this.map.scale / (a.x-b.x);
|
this.lon += x * m.scale / (a.x-b.x);
|
||||||
this.lat -= y * this.map.scale / (a.y-b.y);
|
this.lat -= y * m.scale / (a.y-b.y);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
0.01: New App!
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Quick Center
|
||||||
|
|
||||||
|
An app with a status bar showing various information and up to six shortcuts for your favorite apps!
|
||||||
|
Designed for use with any kind of quick launcher, such as Quick Launch or Pattern Launcher.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Pin your apps with settings, then launch them with your favorite quick launcher to access them quickly.
|
||||||
|
If you don't have any apps pinned, the settings and about apps will be shown as an example.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
Battery and GPS status display (for now)
|
||||||
|
Up to six shortcuts to your favorite apps
|
||||||
|
|
||||||
|
## Upcoming features
|
||||||
|
- Quick switches for toggleable features such as Bluetooth or HID mode
|
||||||
|
- Customizable status information
|
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("mEw4UB6cA/4ACBYNVAElQHAsFBYZFHCxIYEoALHgILNOxILChWqAAmgBYNUBZMVBYIAIBc0C1WAlWoAgQL/O96D/Qf4LZqoLJqoLMoAKHgILNqALHgoLBGBAKCDA4WDAEQA="))
|
|
@ -0,0 +1,120 @@
|
||||||
|
require("Font8x12").add(Graphics);
|
||||||
|
|
||||||
|
// load pinned apps from config
|
||||||
|
var settings = require("Storage").readJSON("qcenter.json", 1) || {};
|
||||||
|
var pinnedApps = settings.pinnedApps || [];
|
||||||
|
var exitGesture = settings.exitGesture || "swipeup";
|
||||||
|
|
||||||
|
// if empty load a default set of apps as an example
|
||||||
|
if (pinnedApps.length == 0) {
|
||||||
|
pinnedApps = [
|
||||||
|
{ src: "setting.app.js", icon: "setting.img" },
|
||||||
|
{ src: "about.app.js", icon: "about.img" },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// button drawing from Layout.js, edited to have completely custom button size with icon
|
||||||
|
function drawButton(l) {
|
||||||
|
var x = l.x + (0 | l.pad),
|
||||||
|
y = l.y + (0 | l.pad),
|
||||||
|
w = l.w - (l.pad << 1),
|
||||||
|
h = l.h - (l.pad << 1);
|
||||||
|
var poly = [
|
||||||
|
x,
|
||||||
|
y + 4,
|
||||||
|
x + 4,
|
||||||
|
y,
|
||||||
|
x + w - 5,
|
||||||
|
y,
|
||||||
|
x + w - 1,
|
||||||
|
y + 4,
|
||||||
|
x + w - 1,
|
||||||
|
y + h - 5,
|
||||||
|
x + w - 5,
|
||||||
|
y + h - 1,
|
||||||
|
x + 4,
|
||||||
|
y + h - 1,
|
||||||
|
x,
|
||||||
|
y + h - 5,
|
||||||
|
x,
|
||||||
|
y + 4,
|
||||||
|
],
|
||||||
|
bg = l.selected ? g.theme.bgH : g.theme.bg2;
|
||||||
|
g.setColor(bg)
|
||||||
|
.fillPoly(poly)
|
||||||
|
.setColor(l.selected ? g.theme.fgH : g.theme.fg2)
|
||||||
|
.drawPoly(poly);
|
||||||
|
if (l.src)
|
||||||
|
g.setBgColor(bg).drawImage(
|
||||||
|
"function" == typeof l.src ? l.src() : l.src,
|
||||||
|
l.x + l.w / 2,
|
||||||
|
l.y + l.h / 2,
|
||||||
|
{ scale: l.scale || undefined, rotate: Math.PI * 0.5 * (l.r || 0) }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// function to split array into group of 3, for button placement
|
||||||
|
function groupBy3(data) {
|
||||||
|
var result = [];
|
||||||
|
for (var i = 0; i < data.length; i += 3) result.push(data.slice(i, i + 3));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate object with buttons for apps by group of 3
|
||||||
|
var appButtons = groupBy3(pinnedApps).map((appGroup, i) => {
|
||||||
|
return appGroup.map((app, j) => {
|
||||||
|
return {
|
||||||
|
type: "custom",
|
||||||
|
render: drawButton,
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
pad: 5,
|
||||||
|
src: require("Storage").read(app.icon),
|
||||||
|
scale: 0.75,
|
||||||
|
cb: (l) => Bangle.load(app.src),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// create basic layout content with status info and sensor status on top
|
||||||
|
var layoutContent = [
|
||||||
|
{
|
||||||
|
type: "h",
|
||||||
|
pad: 5,
|
||||||
|
fillx: 1,
|
||||||
|
c: [
|
||||||
|
{ type: "txt", font: "8x12", pad: 3, scale: 2, label: E.getBattery() + "%" },
|
||||||
|
{ type: "txt", font: "8x12", pad: 3, scale: 2, label: "GPS: " + (Bangle.isGPSOn() ? "ON" : "OFF") },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// create rows for buttons and add them to layoutContent
|
||||||
|
appButtons.forEach((appGroup) => {
|
||||||
|
layoutContent.push({
|
||||||
|
type: "h",
|
||||||
|
pad: 2,
|
||||||
|
c: appGroup,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// create layout with content
|
||||||
|
|
||||||
|
Bangle.loadWidgets();
|
||||||
|
|
||||||
|
var Layout = require("Layout");
|
||||||
|
var layout = new Layout({
|
||||||
|
type: "v",
|
||||||
|
c: layoutContent,
|
||||||
|
});
|
||||||
|
g.clear();
|
||||||
|
layout.render();
|
||||||
|
Bangle.drawWidgets();
|
||||||
|
|
||||||
|
// swipe event listener for exit gesture
|
||||||
|
Bangle.on("swipe", function (lr, ud) {
|
||||||
|
if(exitGesture == "swipeup" && ud == -1) Bangle.showClock();
|
||||||
|
if(exitGesture == "swipedown" && ud == 1) Bangle.showClock();
|
||||||
|
if(exitGesture == "swipeleft" && lr == -1) Bangle.showClock();
|
||||||
|
if(exitGesture == "swiperight" && lr == 1) Bangle.showClock();
|
||||||
|
});
|
After Width: | Height: | Size: 265 B |
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"id": "qcenter",
|
||||||
|
"name": "Quick Center",
|
||||||
|
"shortName": "QCenter",
|
||||||
|
"version": "0.01",
|
||||||
|
"description": "An app for quickly launching your favourite apps, inspired by the control centres of other watches.",
|
||||||
|
"icon": "app.png",
|
||||||
|
"tags": "",
|
||||||
|
"supports": ["BANGLEJS2"],
|
||||||
|
"readme": "README.md",
|
||||||
|
"screenshots": [{ "url": "screenshot.png" }],
|
||||||
|
"storage": [
|
||||||
|
{ "name": "qcenter.app.js", "url": "app.js" },
|
||||||
|
{ "name": "qcenter.settings.js", "url": "settings.js" },
|
||||||
|
{ "name": "qcenter.img", "url": "app-icon.js", "evaluate": true }
|
||||||
|
]
|
||||||
|
}
|
After Width: | Height: | Size: 3.6 KiB |
|
@ -0,0 +1,141 @@
|
||||||
|
// make sure to enclose the function in parentheses
|
||||||
|
(function (back) {
|
||||||
|
let settings = require("Storage").readJSON("qcenter.json", 1) || {};
|
||||||
|
var apps = require("Storage")
|
||||||
|
.list(/\.info$/)
|
||||||
|
.map((app) => {
|
||||||
|
var a = require("Storage").readJSON(app, 1);
|
||||||
|
return (
|
||||||
|
a && {
|
||||||
|
name: a.name,
|
||||||
|
type: a.type,
|
||||||
|
sortorder: a.sortorder,
|
||||||
|
src: a.src,
|
||||||
|
icon: a.icon,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.filter(
|
||||||
|
(app) =>
|
||||||
|
app &&
|
||||||
|
(app.type == "app" ||
|
||||||
|
app.type == "launch" ||
|
||||||
|
app.type == "clock" ||
|
||||||
|
!app.type)
|
||||||
|
);
|
||||||
|
apps.sort((a, b) => {
|
||||||
|
var n = (0 | a.sortorder) - (0 | b.sortorder);
|
||||||
|
if (n) return n; // do sortorder first
|
||||||
|
if (a.name < b.name) return -1;
|
||||||
|
if (a.name > b.name) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
function save(key, value) {
|
||||||
|
settings[key] = value;
|
||||||
|
require("Storage").write("qcenter.json", settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
var pinnedApps = settings.pinnedApps || [];
|
||||||
|
var exitGesture = settings.exitGesture || "swipeup";
|
||||||
|
|
||||||
|
function showMainMenu() {
|
||||||
|
var mainmenu = {
|
||||||
|
"": { title: "Quick Center" },
|
||||||
|
"< Back": () => {
|
||||||
|
load();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set exit gesture
|
||||||
|
mainmenu["Exit Gesture: " + exitGesture] = function () {
|
||||||
|
E.showMenu(exitGestureMenu);
|
||||||
|
};
|
||||||
|
|
||||||
|
//List all pinned apps, redirecting to menu with options to unpin and reorder
|
||||||
|
pinnedApps.forEach((app, i) => {
|
||||||
|
mainmenu[app.name] = function () {
|
||||||
|
E.showMenu({
|
||||||
|
"": { title: app.name },
|
||||||
|
"< Back": () => {
|
||||||
|
showMainMenu();
|
||||||
|
},
|
||||||
|
"Unpin": () => {
|
||||||
|
pinnedApps.splice(i, 1);
|
||||||
|
save("pinnedApps", pinnedApps);
|
||||||
|
showMainMenu();
|
||||||
|
},
|
||||||
|
"Move Up": () => {
|
||||||
|
if (i > 0) {
|
||||||
|
pinnedApps.splice(i - 1, 0, pinnedApps.splice(i, 1)[0]);
|
||||||
|
save("pinnedApps", pinnedApps);
|
||||||
|
showMainMenu();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Move Down": () => {
|
||||||
|
if (i < pinnedApps.length - 1) {
|
||||||
|
pinnedApps.splice(i + 1, 0, pinnedApps.splice(i, 1)[0]);
|
||||||
|
save("pinnedApps", pinnedApps);
|
||||||
|
showMainMenu();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show pin app menu, or show alert if max amount of apps are pinned
|
||||||
|
mainmenu["Pin App"] = function () {
|
||||||
|
if (pinnedApps.length < 6) {
|
||||||
|
E.showMenu(pinAppMenu);
|
||||||
|
} else {
|
||||||
|
E.showAlert("Max apps pinned").then(showMainMenu);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return E.showMenu(mainmenu);
|
||||||
|
}
|
||||||
|
|
||||||
|
// menu for adding apps to the quick launch menu, listing all apps
|
||||||
|
var pinAppMenu = {
|
||||||
|
"": { title: "Add App" },
|
||||||
|
"< Back": showMainMenu,
|
||||||
|
};
|
||||||
|
apps.forEach((a) => {
|
||||||
|
pinAppMenu[a.name] = function () {
|
||||||
|
// strip unncecessary properties
|
||||||
|
delete a.type;
|
||||||
|
delete a.sortorder;
|
||||||
|
pinnedApps.push(a);
|
||||||
|
save("pinnedApps", pinnedApps);
|
||||||
|
showMainMenu();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// menu for setting exit gesture
|
||||||
|
var exitGestureMenu = {
|
||||||
|
"": { title: "Exit Gesture" },
|
||||||
|
"< Back": showMainMenu,
|
||||||
|
};
|
||||||
|
exitGestureMenu["Swipe Up"] = function () {
|
||||||
|
exitGesture = "swipeup";
|
||||||
|
save("exitGesture", "swipeup");
|
||||||
|
showMainMenu();
|
||||||
|
};
|
||||||
|
exitGestureMenu["Swipe Down"] = function () {
|
||||||
|
exitGesture = "swipedown";
|
||||||
|
save("exitGesture", "swipedown");
|
||||||
|
showMainMenu();
|
||||||
|
};
|
||||||
|
exitGestureMenu["Swipe Left"] = function () {
|
||||||
|
exitGesture = "swipeleft";
|
||||||
|
save("exitGesture", "swipeleft");
|
||||||
|
showMainMenu();
|
||||||
|
};
|
||||||
|
exitGestureMenu["Swipe Right"] = function () {
|
||||||
|
exitGesture = "swiperight";
|
||||||
|
save("exitGesture", "swiperight");
|
||||||
|
showMainMenu();
|
||||||
|
};
|
||||||
|
|
||||||
|
showMainMenu();
|
||||||
|
});
|
|
@ -21,3 +21,5 @@
|
||||||
0.15: Show distance more accurately in conjunction with new locale app (fix #1523)
|
0.15: Show distance more accurately in conjunction with new locale app (fix #1523)
|
||||||
0.16: Ability to append to existing track (fix #1712)
|
0.16: Ability to append to existing track (fix #1712)
|
||||||
0.17: Use default Bangle formatter for booleans
|
0.17: Use default Bangle formatter for booleans
|
||||||
|
0.18: Improve widget load speed, allow currently recording track to be plotted in openstmap
|
||||||
|
0.19: Fix track plotting code
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"id": "recorder",
|
"id": "recorder",
|
||||||
"name": "Recorder",
|
"name": "Recorder",
|
||||||
"shortName": "Recorder",
|
"shortName": "Recorder",
|
||||||
"version": "0.17",
|
"version": "0.19",
|
||||||
"description": "Record GPS position, heart rate and more in the background, then download to your PC.",
|
"description": "Record GPS position, heart rate and more in the background, then download to your PC.",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"tags": "tool,outdoors,gps,widget",
|
"tags": "tool,outdoors,gps,widget",
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
(() => {
|
{
|
||||||
var storageFile; // file for GPS track
|
let storageFile; // file for GPS track
|
||||||
var entriesWritten = 0;
|
let entriesWritten = 0;
|
||||||
var activeRecorders = [];
|
let activeRecorders = [];
|
||||||
var writeInterval;
|
let writeInterval;
|
||||||
|
|
||||||
function loadSettings() {
|
let loadSettings = function() {
|
||||||
var settings = require("Storage").readJSON("recorder.json",1)||{};
|
var settings = require("Storage").readJSON("recorder.json",1)||{};
|
||||||
settings.period = settings.period||10;
|
settings.period = settings.period||10;
|
||||||
if (!settings.file || !settings.file.startsWith("recorder.log"))
|
if (!settings.file || !settings.file.startsWith("recorder.log"))
|
||||||
|
@ -12,12 +12,12 @@
|
||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSettings(settings) {
|
let updateSettings = function(settings) {
|
||||||
require("Storage").writeJSON("recorder.json", settings);
|
require("Storage").writeJSON("recorder.json", settings);
|
||||||
if (WIDGETS["recorder"]) WIDGETS["recorder"].reload();
|
if (WIDGETS["recorder"]) WIDGETS["recorder"].reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRecorders() {
|
let getRecorders = function() {
|
||||||
var recorders = {
|
var recorders = {
|
||||||
gps:function() {
|
gps:function() {
|
||||||
var lat = 0;
|
var lat = 0;
|
||||||
|
@ -159,7 +159,7 @@
|
||||||
return recorders;
|
return recorders;
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeLog() {
|
let writeLog = function() {
|
||||||
entriesWritten++;
|
entriesWritten++;
|
||||||
WIDGETS["recorder"].draw();
|
WIDGETS["recorder"].draw();
|
||||||
try {
|
try {
|
||||||
|
@ -178,7 +178,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called by the GPS app to reload settings and decide what to do
|
// Called by the GPS app to reload settings and decide what to do
|
||||||
function reload() {
|
let reload = function() {
|
||||||
var settings = loadSettings();
|
var settings = loadSettings();
|
||||||
if (writeInterval) clearInterval(writeInterval);
|
if (writeInterval) clearInterval(writeInterval);
|
||||||
writeInterval = undefined;
|
writeInterval = undefined;
|
||||||
|
@ -224,7 +224,7 @@
|
||||||
// add the widget
|
// add the widget
|
||||||
WIDGETS["recorder"]={area:"tl",width:0,draw:function() {
|
WIDGETS["recorder"]={area:"tl",width:0,draw:function() {
|
||||||
if (!writeInterval) return;
|
if (!writeInterval) return;
|
||||||
g.reset(); g.drawImage(atob("DRSBAAGAHgDwAwAAA8B/D/hvx38zzh4w8A+AbgMwGYDMDGBjAA=="),this.x+1,this.y+2);
|
g.reset().drawImage(atob("DRSBAAGAHgDwAwAAA8B/D/hvx38zzh4w8A+AbgMwGYDMDGBjAA=="),this.x+1,this.y+2);
|
||||||
activeRecorders.forEach((recorder,i)=>{
|
activeRecorders.forEach((recorder,i)=>{
|
||||||
recorder.draw(this.x+15+(i>>1)*12, this.y+(i&1)*12);
|
recorder.draw(this.x+15+(i>>1)*12, this.y+(i&1)*12);
|
||||||
});
|
});
|
||||||
|
@ -265,23 +265,39 @@
|
||||||
updateSettings(settings);
|
updateSettings(settings);
|
||||||
WIDGETS["recorder"].reload();
|
WIDGETS["recorder"].reload();
|
||||||
return Promise.resolve(settings.recording);
|
return Promise.resolve(settings.recording);
|
||||||
}/*,plotTrack:function(m) { // m=instance of openstmap module
|
},plotTrack:function(m) { // m=instance of openstmap module
|
||||||
// if we're here, settings was already loaded
|
// Plots the current track in the currently set color
|
||||||
|
if (!activeRecorders.length) return; // not recording
|
||||||
|
var settings = loadSettings();
|
||||||
|
// keep function to draw track in RAM
|
||||||
|
function plot(g) { "ram";
|
||||||
var f = require("Storage").open(settings.file,"r");
|
var f = require("Storage").open(settings.file,"r");
|
||||||
var l = f.readLine(f);
|
var l = f.readLine();
|
||||||
if (l===undefined) return;
|
if (l===undefined) return; // empty file?
|
||||||
var c = l.split(",");
|
var mp, c = l.split(",");
|
||||||
var mp = m.latLonToXY(+c[1], +c[2]);
|
var la=c.indexOf("Latitude"),lo=c.indexOf("Longitude");
|
||||||
g.moveTo(mp.x,mp.y);
|
if (la<0 || lo<0) return; // no GPS!
|
||||||
l = f.readLine(f);
|
l = f.readLine();c=[];
|
||||||
while(l!==undefined) {
|
while (l && !c[la]) {
|
||||||
c = l.split(",");
|
c = l.split(",");
|
||||||
mp = m.latLonToXY(+c[1], +c[2]);
|
|
||||||
g.lineTo(mp.x,mp.y);
|
|
||||||
g.fillCircle(mp.x,mp.y,2); // make the track more visible
|
|
||||||
l = f.readLine(f);
|
l = f.readLine(f);
|
||||||
}
|
}
|
||||||
}*/};
|
if (l===undefined) return; // empty file?
|
||||||
|
mp = m.latLonToXY(+c[la], +c[lo]);
|
||||||
|
g.moveTo(mp.x,mp.y);
|
||||||
|
l = f.readLine(f);
|
||||||
|
var n = 200; // only plot first 200 points to keep things fast(ish)
|
||||||
|
while(l && n--) {
|
||||||
|
c = l.split(",");
|
||||||
|
if (c[la]) {
|
||||||
|
mp = m.latLonToXY(+c[la], +c[lo]);
|
||||||
|
g.lineTo(mp.x,mp.y);
|
||||||
|
}
|
||||||
|
l = f.readLine(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
plot(g);
|
||||||
|
}};
|
||||||
// load settings, set correct widget width
|
// load settings, set correct widget width
|
||||||
reload();
|
reload();
|
||||||
})()
|
}
|
||||||
|
|
|
@ -14,3 +14,4 @@
|
||||||
Improve timer message using formatDuration
|
Improve timer message using formatDuration
|
||||||
Fix wrong fallback for buzz pattern
|
Fix wrong fallback for buzz pattern
|
||||||
0.13: Ask to delete a timer after stopping it
|
0.13: Ask to delete a timer after stopping it
|
||||||
|
0.14: Added clkinfo for alarms and timers
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
(function() {
|
||||||
|
const alarm = require('sched');
|
||||||
|
const iconAlarmOn = atob("GBiBAAAAAAAAAAYAYA4AcBx+ODn/nAP/wAf/4A/n8A/n8B/n+B/n+B/n+B/n+B/h+B/4+A/+8A//8Af/4AP/wAH/gAB+AAAAAAAAAA==");
|
||||||
|
const iconAlarmOff = atob("GBiBAAAAAAAAAAYAYA4AcBx+ODn/nAP/wAf/4A/n8A/n8B/n+B/n+B/nAB/mAB/geB/5/g/5tg/zAwfzhwPzhwHzAwB5tgAB/gAAeA==");
|
||||||
|
const iconTimerOn = atob("GBiBAAAAAAAAAAAAAAf/4Af/4AGBgAGBgAGBgAD/AAD/AAB+AAA8AAA8AAB+AADnAADDAAGBgAGBgAGBgAf/4Af/4AAAAAAAAAAAAA==");
|
||||||
|
const iconTimerOff = atob("GBiBAAAAAAAAAAAAAAf/4Af/4AGBgAGBgAGBgAD/AAD/AAB+AAA8AAA8AAB+AADkeADB/gGBtgGDAwGDhwfzhwfzAwABtgAB/gAAeA==");
|
||||||
|
|
||||||
|
//from 0 to max, the higher the closer to fire (as in a progress bar)
|
||||||
|
function getAlarmValue(a){
|
||||||
|
let min = Math.round(alarm.getTimeToAlarm(a)/(60*1000));
|
||||||
|
if(!min) return 0; //not active or more than a day
|
||||||
|
return getAlarmMax(a)-min;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAlarmMax(a) {
|
||||||
|
if(a.timer)
|
||||||
|
return Math.round(a.timer/(60*1000));
|
||||||
|
//minutes cannot be more than a full day
|
||||||
|
return 1440;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAlarmIcon(a) {
|
||||||
|
if(a.on) {
|
||||||
|
if(a.timer) return iconTimerOn;
|
||||||
|
return iconAlarmOn;
|
||||||
|
} else {
|
||||||
|
if(a.timer) return iconTimerOff;
|
||||||
|
return iconAlarmOff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAlarmText(a){
|
||||||
|
if(a.timer) {
|
||||||
|
if(!a.on) return "off";
|
||||||
|
let time = Math.round(alarm.getTimeToAlarm(a)/(60*1000));
|
||||||
|
if(time > 60)
|
||||||
|
time = Math.round(time / 60) + "h";
|
||||||
|
else
|
||||||
|
time += "m";
|
||||||
|
return time;
|
||||||
|
}
|
||||||
|
return require("time_utils").formatTime(a.t);
|
||||||
|
}
|
||||||
|
|
||||||
|
//workaround for sorting undefined values
|
||||||
|
function getAlarmOrder(a) {
|
||||||
|
let val = alarm.getTimeToAlarm(a);
|
||||||
|
if(typeof val == "undefined") return 86400*1000;
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
var img = iconAlarmOn;
|
||||||
|
//get only alarms not created by other apps
|
||||||
|
var alarmItems = {
|
||||||
|
name: "Alarms",
|
||||||
|
img: img,
|
||||||
|
dynamic: true,
|
||||||
|
items: alarm.getAlarms().filter(a=>!a.appid)
|
||||||
|
//.sort((a,b)=>alarm.getTimeToAlarm(a)-alarm.getTimeToAlarm(b))
|
||||||
|
.sort((a,b)=>getAlarmOrder(a)-getAlarmOrder(b))
|
||||||
|
.map((a, i)=>({
|
||||||
|
name: null,
|
||||||
|
hasRange: true,
|
||||||
|
get: () => ({ text: getAlarmText(a), img: getAlarmIcon(a),
|
||||||
|
v: getAlarmValue(a), min:0, max:getAlarmMax(a)}),
|
||||||
|
show: function() { alarmItems.items[i].emit("redraw"); },
|
||||||
|
hide: function () {},
|
||||||
|
run: function() { }
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
return alarmItems;
|
||||||
|
})
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "sched",
|
"id": "sched",
|
||||||
"name": "Scheduler",
|
"name": "Scheduler",
|
||||||
"version": "0.13",
|
"version": "0.14",
|
||||||
"description": "Scheduling library for alarms and timers",
|
"description": "Scheduling library for alarms and timers",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"type": "scheduler",
|
"type": "scheduler",
|
||||||
|
@ -13,7 +13,8 @@
|
||||||
{"name":"sched.js","url":"sched.js"},
|
{"name":"sched.js","url":"sched.js"},
|
||||||
{"name":"sched.img","url":"app-icon.js","evaluate":true},
|
{"name":"sched.img","url":"app-icon.js","evaluate":true},
|
||||||
{"name":"sched","url":"lib.js"},
|
{"name":"sched","url":"lib.js"},
|
||||||
{"name":"sched.settings.js","url":"settings.js"}
|
{"name":"sched.settings.js","url":"settings.js"},
|
||||||
|
{"name":"sched.clkinfo.js","url":"clkinfo.js"}
|
||||||
],
|
],
|
||||||
"data": [{"name":"sched.json"}, {"name":"sched.settings.json"}]
|
"data": [{"name":"sched.json"}, {"name":"sched.settings.json"}]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
0.01: New App!
|
0.01: New App!
|
||||||
0.02: Reset font to save some memory during remove
|
0.02: Reset font to save some memory during remove
|
||||||
|
0.03: Added support for locale based time
|
||||||
|
|
|
@ -34,8 +34,9 @@ let draw = function() {
|
||||||
x = R.w / 2;
|
x = R.w / 2;
|
||||||
y = R.y + R.h / 2 - 12; // 12 = room for date
|
y = R.y + R.h / 2 - 12; // 12 = room for date
|
||||||
var date = new Date();
|
var date = new Date();
|
||||||
var hourStr = date.getHours();
|
var local_time = require("locale").time(date, 1);
|
||||||
var minStr = date.getMinutes().toString().padStart(2,0);
|
var hourStr = local_time.split(":")[0].trim().padStart(2,'0');
|
||||||
|
var minStr = local_time.split(":")[1].trim().padStart(2, '0');
|
||||||
dateStr = require("locale").dow(date, 1).toUpperCase()+ " "+
|
dateStr = require("locale").dow(date, 1).toUpperCase()+ " "+
|
||||||
require("locale").date(date, 0).toUpperCase();
|
require("locale").date(date, 0).toUpperCase();
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{ "id": "slopeclock",
|
{ "id": "slopeclock",
|
||||||
"name": "Slope Clock",
|
"name": "Slope Clock",
|
||||||
"version":"0.02",
|
"version":"0.03",
|
||||||
"description": "A clock where hours and minutes are divided by a sloping line. When the minute changes, the numbers slide off the screen",
|
"description": "A clock where hours and minutes are divided by a sloping line. When the minute changes, the numbers slide off the screen",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"screenshots": [{"url":"screenshot.png"}],
|
"screenshots": [{"url":"screenshot.png"}],
|
||||||
|
|
|
@ -4,3 +4,5 @@
|
||||||
0.04: Changed to use clock_info for displayed data
|
0.04: Changed to use clock_info for displayed data
|
||||||
Made fonts smaller to avoid overlap when (eg) 22:00
|
Made fonts smaller to avoid overlap when (eg) 22:00
|
||||||
Allowed black/white background (as that can look nice too)
|
Allowed black/white background (as that can look nice too)
|
||||||
|
0.05: Images in clkinfo are optional now
|
||||||
|
0.06: Added support for locale based time
|
||||||
|
|
|
@ -51,8 +51,9 @@ let draw = function() {
|
||||||
x = R.w / 2;
|
x = R.w / 2;
|
||||||
y = R.y + R.h / 2 - 12; // 12 = room for date
|
y = R.y + R.h / 2 - 12; // 12 = room for date
|
||||||
var date = new Date();
|
var date = new Date();
|
||||||
var hourStr = date.getHours();
|
var local_time = require("locale").time(date, 1);
|
||||||
var minStr = date.getMinutes().toString().padStart(2,0);
|
var hourStr = local_time.split(":")[0].trim().padStart(2,'0');
|
||||||
|
var minStr = local_time.split(":")[1].trim().padStart(2, '0');
|
||||||
dateStr = require("locale").dow(date, 1).toUpperCase()+ " "+
|
dateStr = require("locale").dow(date, 1).toUpperCase()+ " "+
|
||||||
require("locale").date(date, 0).toUpperCase();
|
require("locale").date(date, 0).toUpperCase();
|
||||||
|
|
||||||
|
@ -121,18 +122,18 @@ let animate = function(isIn, callback) {
|
||||||
|
|
||||||
// clock info menus (scroll up/down for info)
|
// clock info menus (scroll up/down for info)
|
||||||
let clockInfoDraw = (itm, info, options) => {
|
let clockInfoDraw = (itm, info, options) => {
|
||||||
let texty = options.y+26;
|
let texty = options.y+41;
|
||||||
g.reset().setFont("6x15").setBgColor(options.bg).setColor(options.fg).clearRect(options.x, texty, options.x+options.w-2, texty+15);
|
g.reset().setFont("6x15").setBgColor(options.bg).setColor(options.fg).clearRect(options.x, texty-15, options.x+options.w-2, texty);
|
||||||
|
|
||||||
if (options.focus) g.setColor(options.hl);
|
if (options.focus) g.setColor(options.hl);
|
||||||
if (options.x < g.getWidth()/2) { // left align
|
if (options.x < g.getWidth()/2) { // left align
|
||||||
let x = options.x+2;
|
let x = options.x+2;
|
||||||
g.clearRect(x, options.y, x+23, options.y+23).drawImage(info.img, x, options.y);
|
if (info.img) g.clearRect(x, options.y, x+23, options.y+23).drawImage(info.img, x, options.y);
|
||||||
g.setFontAlign(-1,-1).drawString(info.text, x,texty);
|
g.setFontAlign(-1,1).drawString(info.text, x,texty);
|
||||||
} else { // right align
|
} else { // right align
|
||||||
let x = options.x+options.w-3;
|
let x = options.x+options.w-3;
|
||||||
g.clearRect(x-23, options.y, x, options.y+23).drawImage(info.img, x-23, options.y);
|
if (info.img) g.clearRect(x-23, options.y, x, options.y+23).drawImage(info.img, x-23, options.y);
|
||||||
g.setFontAlign(1,-1).drawString(info.text, x,texty);
|
g.setFontAlign(1,1).drawString(info.text, x,texty);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let clockInfoItems = require("clock_info").load();
|
let clockInfoItems = require("clock_info").load();
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{ "id": "slopeclockpp",
|
{ "id": "slopeclockpp",
|
||||||
"name": "Slope Clock ++",
|
"name": "Slope Clock ++",
|
||||||
"version":"0.04",
|
"version":"0.06",
|
||||||
"description": "A clock where hours and minutes are divided by a sloping line. When the minute changes, the numbers slide off the screen. This is a clone of the original Slope Clock which shows extra information and allows the colors to be selected.",
|
"description": "A clock where hours and minutes are divided by a sloping line. When the minute changes, the numbers slide off the screen. This is a clone of the original Slope Clock which shows extra information and allows the colors to be selected.",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"screenshots": [{"url":"screenshot.png"}],
|
"screenshots": [{"url":"screenshot.png"}],
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
# Tetris
|
||||||
|
|
||||||
|
Bangle version of the classic game of Tetris.
|
||||||
|
|
||||||
|
## Controls
|
||||||
|
|
||||||
|
Tapping the screen rotates the pieces once, swiping left, right or down moves the
|
||||||
|
piece in that direction, if possible.
|
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("mEwxH+If4A/AH4A/AH4A/ABe5AA0jABwvYAIovBgABEFAQHFL7IuEL4QuFA45fcF4YuNL7i/FFwoHHL7QvFFxpfaF4wAOF/4nHF5+0AAy3SXYoHGW4QBDF4MAAIgvRFwwHHdAbqDFIQuDL6ouJL4ovDFwpfUAAoHFL4a/FFwhfTFxZfDF4ouFL6QANFopfDF/4vNjwAGF8ABFF4MAAIgvBX4IBDX4YBDL6TyFFIIuEL4QuEL4QuEL6ovDFwpfFF4YuFL6i/FFwhfEX4ouEL6YvFFwpfDF4ouFL6QvGAAwtFL4Yv/AAonHAB4vHG563CAIbuDA5i/CAIb2DA4hfJEwoHPFApZEGwpfLFyJfFFxJfMAAoHNFAa5GX54uTL4YuLL5QAVFowAIF+4A/AH4A/AH4A/AHY"))
|
|
@ -0,0 +1,14 @@
|
||||||
|
{ "id": "tetris",
|
||||||
|
"name": "Tetris",
|
||||||
|
"shortName":"Tetris",
|
||||||
|
"version":"0.01",
|
||||||
|
"description": "Tetris",
|
||||||
|
"icon": "tetris.png",
|
||||||
|
"readme": "README.md",
|
||||||
|
"tags": "games",
|
||||||
|
"supports" : ["BANGLEJS2"],
|
||||||
|
"storage": [
|
||||||
|
{"name":"tetris.app.js","url":"tetris.app.js"},
|
||||||
|
{"name":"tetris.img","url":"app-icon.js","evaluate":true}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,170 @@
|
||||||
|
const block = Graphics.createImage(`
|
||||||
|
########
|
||||||
|
# # # ##
|
||||||
|
## # ###
|
||||||
|
# # ####
|
||||||
|
## #####
|
||||||
|
# ######
|
||||||
|
########
|
||||||
|
########
|
||||||
|
`);
|
||||||
|
const tcols = [ {r:0, g:0, b:1}, {r:0, g:1, b:0}, {r:0, g:1, b:1}, {r:1, g:0, b:0}, {r:1, g:0, b:1}, {r:1, g:1, b:0}, {r:1, g:0.5, b:0.5} ];
|
||||||
|
const tiles = [
|
||||||
|
[[0, 0, 0, 0],
|
||||||
|
[0, 0, 0, 0],
|
||||||
|
[1, 1, 1, 1],
|
||||||
|
[0, 0, 0, 0]],
|
||||||
|
[[0, 0, 0],
|
||||||
|
[0, 1, 0],
|
||||||
|
[1, 1, 1]],
|
||||||
|
[[0, 0, 0],
|
||||||
|
[1, 0, 0],
|
||||||
|
[1, 1, 1]],
|
||||||
|
[[0, 0, 0],
|
||||||
|
[0, 0, 1],
|
||||||
|
[1, 1, 1]],
|
||||||
|
[[0, 0, 0],
|
||||||
|
[1, 1, 0],
|
||||||
|
[0, 1, 1]],
|
||||||
|
[[0, 0, 0],
|
||||||
|
[0, 1, 1],
|
||||||
|
[1, 1, 0]],
|
||||||
|
[[1, 1],
|
||||||
|
[1, 1]]
|
||||||
|
];
|
||||||
|
|
||||||
|
const ox = 176/2 - 5*8;
|
||||||
|
const oy = 8;
|
||||||
|
|
||||||
|
var pf = Array(23).fill().map(()=>Array(12).fill(0)); // field is really 10x20, but adding a border for collision checks
|
||||||
|
pf[20].fill(1);
|
||||||
|
pf[21].fill(1);
|
||||||
|
pf[22].fill(1);
|
||||||
|
pf.forEach((x,i) => { pf[i][0] = 1; pf[i][11] = 1; });
|
||||||
|
|
||||||
|
function rotateTile(t, r) {
|
||||||
|
var nt = JSON.parse(JSON.stringify(t));
|
||||||
|
if (t.length==2) return nt;
|
||||||
|
var s = t.length;
|
||||||
|
for (m=0; m<r; ++m) {
|
||||||
|
tl = JSON.parse(JSON.stringify(nt));
|
||||||
|
for (i=0; i<s; ++i)
|
||||||
|
for (j=0; j<s; ++j)
|
||||||
|
nt[i][j] = tl[s-1-j][i];
|
||||||
|
}
|
||||||
|
return nt;
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawBoundingBox() {
|
||||||
|
g.setBgColor(0, 0, 0).clear().setColor(1, 1, 1);
|
||||||
|
g.theme.bg = 0;
|
||||||
|
for (i=0; i<4; ++i) g.drawRect(ox-i-1, oy-i-1, ox+10*8+i, oy+20*8+i);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawTile (tile, n, x, y, qClear) {
|
||||||
|
if (qClear) g.setColor(0);
|
||||||
|
else g.setColor(tcols[n].r, tcols[n].g, tcols[n].b);
|
||||||
|
for (i=0; i<tile.length; ++i)
|
||||||
|
for (j=0; j<tile.length; ++j)
|
||||||
|
if (tile[j][i]>0)
|
||||||
|
if (qClear) g.fillRect(x+8*i, y+8*j, x+8*(i+1)-1, y+8*(j+1)-1);
|
||||||
|
else g.drawImage(block, x+8*i, y+8*j);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNext(n, r) {
|
||||||
|
var nt = rotateTile(tiles[n], r);
|
||||||
|
g.setColor(0).fillRect(176-33, 40, 176-33+33, 82);
|
||||||
|
drawTile(nt, ntn, 176-33, 40);
|
||||||
|
}
|
||||||
|
|
||||||
|
var time = Date.now();
|
||||||
|
var px=4, py=0;
|
||||||
|
var ctn = Math.floor(Math.random()*7); // current tile number
|
||||||
|
var ntn = Math.floor(Math.random()*7); // next tile number
|
||||||
|
var ntr = Math.floor(Math.random()*4); // next tile rotation
|
||||||
|
var ct = rotateTile(tiles[ctn], Math.floor(Math.random()*4)); // current tile (rotated)
|
||||||
|
var dropInterval = 450;
|
||||||
|
var nlines = 0;
|
||||||
|
|
||||||
|
function redrawPF(ly) {
|
||||||
|
for (y=0; y<=ly; ++y)
|
||||||
|
for (x=1; x<11; ++x) {
|
||||||
|
c = pf[y][x];
|
||||||
|
if (c>0) g.setColor(tcols[c-1].r, tcols[c-1].g, tcols[c-1].b).drawImage(block, ox+(x-1)*8, oy+y*8);
|
||||||
|
else g.setColor(0, 0, 0).fillRect(ox+(x-1)*8, oy+y*8, ox+x*8-1, oy+(y+1)*8-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertAndCheck() {
|
||||||
|
for (y=0; y<ct.length; ++y)
|
||||||
|
for (x=0; x<ct[y].length; ++x)
|
||||||
|
if (ct[y][x]>0) pf[py+y][px+x+1] = ctn+1;
|
||||||
|
// check for full lines
|
||||||
|
for (y=19; y>0; y--) {
|
||||||
|
var qFull = true;
|
||||||
|
for (x=1; x<11; ++x) qFull &= pf[y][x]>0;
|
||||||
|
if (qFull) {
|
||||||
|
nlines++;
|
||||||
|
dropInterval -= 5;
|
||||||
|
Bangle.buzz(30);
|
||||||
|
for (ny=y; ny>0; ny--) pf[ny] = JSON.parse(JSON.stringify(pf[ny-1]));
|
||||||
|
redrawPF(y);
|
||||||
|
g.setColor(0).fillRect(5, 30, 41, 80).setColor(1, 1, 1).drawString(nlines.toString(), 22, 50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// spawn new tile
|
||||||
|
px = 4; py = 0;
|
||||||
|
ctn = ntn;
|
||||||
|
ntn = Math.floor(Math.random()*7);
|
||||||
|
ct = rotateTile(tiles[ctn], ntr);
|
||||||
|
ntr = Math.floor(Math.random()*4);
|
||||||
|
showNext(ntn, ntr);
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveOk(t, dx, dy) {
|
||||||
|
var ok = true;
|
||||||
|
for (y=0; y<t.length; ++y)
|
||||||
|
for (x=0; x<t[y].length; ++x)
|
||||||
|
if (t[y][x]*pf[py+dy+y][px+dx+x+1] > 0) ok = false;
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
function gameStep() {
|
||||||
|
if (Date.now()-time > dropInterval) { // drop one step
|
||||||
|
time = Date.now();
|
||||||
|
if (moveOk(ct, 0, 1)) {
|
||||||
|
drawTile(ct, ctn, ox+px*8, oy+py*8, true);
|
||||||
|
py++;
|
||||||
|
}
|
||||||
|
else { // reached the bottom
|
||||||
|
insertAndCheck(ct, ctn, px, py);
|
||||||
|
}
|
||||||
|
drawTile(ct, ctn, ox+px*8, oy+py*8, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Bangle.setUI();
|
||||||
|
Bangle.on("touch", (e) => {
|
||||||
|
t = rotateTile(ct, 3);
|
||||||
|
if (moveOk(t, 0, 0)) {
|
||||||
|
drawTile(ct, ctn, ox+px*8, oy+py*8, true);
|
||||||
|
ct = t;
|
||||||
|
drawTile(ct, ctn, ox+px*8, oy+py*8, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Bangle.on("swipe", (x,y) => {
|
||||||
|
if (y<0) y = 0;
|
||||||
|
if (moveOk(ct, x, y)) {
|
||||||
|
drawTile(ct, ctn, ox+px*8, oy+py*8, true);
|
||||||
|
px += x;
|
||||||
|
py += y;
|
||||||
|
drawTile(ct, ctn, ox+px*8, oy+py*8, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
drawBoundingBox();
|
||||||
|
g.setColor(1, 1, 1).setFontAlign(0, 1, 0).setFont("6x15", 1).drawString("Lines", 22, 30).drawString("Next", 176-22, 30);
|
||||||
|
showNext(ntn, ntr);
|
||||||
|
g.setColor(0).fillRect(5, 30, 41, 80).setColor(1, 1, 1).drawString(nlines.toString(), 22, 50);
|
||||||
|
var gi = setInterval(gameStep, 20);
|
After Width: | Height: | Size: 492 B |
|
@ -14,3 +14,4 @@
|
||||||
0.15: Fix widget icon
|
0.15: Fix widget icon
|
||||||
0.16: Don't mark app as clock
|
0.16: Don't mark app as clock
|
||||||
0.17: Added clkinfo for clocks.
|
0.17: Added clkinfo for clocks.
|
||||||
|
0.18: Added hasRange to clkinfo.
|
||||||
|
|
|
@ -5,34 +5,41 @@
|
||||||
wind: "?",
|
wind: "?",
|
||||||
};
|
};
|
||||||
|
|
||||||
var weatherJson = storage.readJSON('weather.json');
|
var weatherJson = require("Storage").readJSON('weather.json');
|
||||||
if(weatherJson !== undefined && weatherJson.weather !== undefined){
|
if(weatherJson !== undefined && weatherJson.weather !== undefined){
|
||||||
weather = weatherJson.weather;
|
weather = weatherJson.weather;
|
||||||
weather.temp = locale.temp(weather.temp-273.15);
|
weather.temp = require("locale").temp(weather.temp-273.15);
|
||||||
weather.hum = weather.hum + "%";
|
weather.hum = weather.hum + "%";
|
||||||
weather.wind = locale.speed(weather.wind).match(/^(\D*\d*)(.*)$/);
|
weather.wind = require("locale").speed(weather.wind).match(/^(\D*\d*)(.*)$/);
|
||||||
weather.wind = Math.round(weather.wind[1]) + "kph";
|
weather.wind = Math.round(weather.wind[1]) + "kph";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//FIXME ranges are somehow arbitrary
|
||||||
var weatherItems = {
|
var weatherItems = {
|
||||||
name: "Weather",
|
name: "Weather",
|
||||||
img: atob("GBiBAf+///u5//n7//8f/9wHP8gDf/gB//AB/7AH/5AcP/AQH/DwD/uAD84AD/4AA/wAAfAAAfAAAfAAAfgAA/////+bP/+zf/+zfw=="),
|
img: atob("GBiBAf+///u5//n7//8f/9wHP8gDf/gB//AB/7AH/5AcP/AQH/DwD/uAD84AD/4AA/wAAfAAAfAAAfAAAfgAA/////+bP/+zf/+zfw=="),
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
name: "temperature",
|
name: "temperature",
|
||||||
get: () => ({ text: weather.temp, img: atob("GBiBAAA8AAB+AADnAADDAADDAADDAADDAADDAADbAADbAADbAADbAADbAADbAAHbgAGZgAM8wAN+wAN+wAM8wAGZgAHDgAD/AAA8AA==")}),
|
hasRange : true,
|
||||||
|
get: () => ({ text: weather.temp, img: atob("GBiBAAA8AAB+AADnAADDAADDAADDAADDAADDAADbAADbAADbAADbAADbAADbAAHbgAGZgAM8wAN+wAN+wAM8wAGZgAHDgAD/AAA8AA=="),
|
||||||
|
v: parseInt(weather.temp), min: -30, max: 55}),
|
||||||
show: function() { weatherItems.items[0].emit("redraw"); },
|
show: function() { weatherItems.items[0].emit("redraw"); },
|
||||||
hide: function () {}
|
hide: function () {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "humidity",
|
name: "humidity",
|
||||||
get: () => ({ text: weather.hum, img: atob("GBiBAAAEAAAMAAAOAAAfAAAfAAA/gAA/gAI/gAY/AAcfAA+AQA+A4B/A4D/B8D/h+D/j+H/n/D/n/D/n/B/H/A+H/AAH/AAD+AAA8A==")}),
|
hasRange : true,
|
||||||
|
get: () => ({ text: weather.hum, img: atob("GBiBAAAEAAAMAAAOAAAfAAAfAAA/gAA/gAI/gAY/AAcfAA+AQA+A4B/A4D/B8D/h+D/j+H/n/D/n/D/n/B/H/A+H/AAH/AAD+AAA8A=="),
|
||||||
|
v: parseInt(weather.hum), min: 0, max: 100}),
|
||||||
show: function() { weatherItems.items[1].emit("redraw"); },
|
show: function() { weatherItems.items[1].emit("redraw"); },
|
||||||
hide: function () {}
|
hide: function () {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "wind",
|
name: "wind",
|
||||||
get: () => ({ text: weather.wind, img: atob("GBiBAAHgAAPwAAYYAAwYAAwMfAAY/gAZh3/xg//hgwAAAwAABg///g//+AAAAAAAAP//wH//4AAAMAAAMAAYMAAYMAAMcAAP4AADwA==")}),
|
hasRange : true,
|
||||||
|
get: () => ({ text: weather.wind, img: atob("GBiBAAHgAAPwAAYYAAwYAAwMfAAY/gAZh3/xg//hgwAAAwAABg///g//+AAAAAAAAP//wH//4AAAMAAAMAAYMAAYMAAMcAAP4AADwA=="),
|
||||||
|
v: parseInt(weather.wind), min: 0, max: 118}),
|
||||||
show: function() { weatherItems.items[2].emit("redraw"); },
|
show: function() { weatherItems.items[2].emit("redraw"); },
|
||||||
hide: function () {}
|
hide: function () {}
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "weather",
|
"id": "weather",
|
||||||
"name": "Weather",
|
"name": "Weather",
|
||||||
"version": "0.17",
|
"version": "0.18",
|
||||||
"description": "Show Gadgetbridge weather report",
|
"description": "Show Gadgetbridge weather report",
|
||||||
"icon": "icon.png",
|
"icon": "icon.png",
|
||||||
"screenshots": [{"url":"screenshot.png"}],
|
"screenshots": [{"url":"screenshot.png"}],
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
0.01: Moved messages widget into standalone widget app
|
|
@ -0,0 +1,30 @@
|
||||||
|
# Messages widget
|
||||||
|
|
||||||
|
The default widget to show icons for new messages
|
||||||
|
It is installed automatically if you install `Android Integration` or `iOS Integration`.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Settings
|
||||||
|
You can change settings by going to the global `Settings` app, then `App Settings`
|
||||||
|
and `Messages`:
|
||||||
|
|
||||||
|
* `Flash icon` Toggle flashing of the widget icons.
|
||||||
|
<!-- * `Show read` - Also show the widget when there are only old messages. -->
|
||||||
|
* `Widget messages` Not used by this widget.
|
||||||
|
|
||||||
|
## Requests
|
||||||
|
|
||||||
|
Please file any issues on https://github.com/espruino/BangleApps/issues/new?title=widmessages%widget
|
||||||
|
|
||||||
|
## Creator
|
||||||
|
|
||||||
|
Gordon Williams
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
[Jeroen Peters](https://github.com/jeroenpeters1986)
|
||||||
|
|
||||||
|
## Attributions
|
||||||
|
|
||||||
|
Icons used in this app are from https://icons8.com
|
After Width: | Height: | Size: 917 B |
|
@ -0,0 +1,8 @@
|
||||||
|
exports.hide = function() {
|
||||||
|
if (!global.WIDGETS||!WIDGETS["messages"]) return;
|
||||||
|
WIDGETS["messages"].hide();
|
||||||
|
}
|
||||||
|
exports.show = function() {
|
||||||
|
if (!global.WIDGETS||!WIDGETS["messages"]) return;
|
||||||
|
WIDGETS["messages"].show();
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"id": "widmessages",
|
||||||
|
"name": "Message Widget",
|
||||||
|
"version": "0.01",
|
||||||
|
"description": "Widget showing new messages",
|
||||||
|
"icon": "app.png",
|
||||||
|
"type": "widget",
|
||||||
|
"tags": "tool,system",
|
||||||
|
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||||
|
"screenshots": [{"url": "screenshot.gif"}],
|
||||||
|
"dependencies" : { "messageicons":"module" },
|
||||||
|
"provides_modules" : ["messagewidget"],
|
||||||
|
"readme": "README.md",
|
||||||
|
"storage": [
|
||||||
|
{"name":"messagewidget","url":"lib.js"},
|
||||||
|
{"name":"widmessages.wid.js","url":"widget.js"}
|
||||||
|
]
|
||||||
|
}
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
@ -0,0 +1,73 @@
|
||||||
|
(() => {
|
||||||
|
if ((require("Storage").readJSON("messages.settings.json", true) || {}).maxMessages===0) return;
|
||||||
|
|
||||||
|
function filterMessages(msgs) {
|
||||||
|
return msgs.filter(msg => msg.new && msg.id != "music")
|
||||||
|
.map(m => m.src) // we only need this for icon/color
|
||||||
|
.filter((msg, i, arr) => arr.findIndex(nmsg => msg.src == nmsg.src) == i);
|
||||||
|
}
|
||||||
|
|
||||||
|
WIDGETS["messages"] = {
|
||||||
|
area: "tl", width: 0, srcs: [], draw: function(recall) {
|
||||||
|
// If we had a setTimeout queued from the last time we were called, remove it
|
||||||
|
if (WIDGETS["messages"].i) {
|
||||||
|
clearTimeout(WIDGETS["messages"].i);
|
||||||
|
delete WIDGETS["messages"].i;
|
||||||
|
}
|
||||||
|
Bangle.removeListener("touch", this.touch);
|
||||||
|
if (!this.width) return;
|
||||||
|
let settings = Object.assign({flash: true, maxMessages: 3}, require("Storage").readJSON("messages.settings.json", true) || {});
|
||||||
|
if (recall!==true || settings.flash) {
|
||||||
|
const msgsShown = E.clip(this.srcs.length, 0, settings.maxMessages),
|
||||||
|
srcs = Object.keys(this.srcs);
|
||||||
|
g.reset().clearRect(this.x, this.y, this.x+this.width, this.y+23);
|
||||||
|
for(let i = 0; i<msgsShown; i++) {
|
||||||
|
const src = srcs[i];
|
||||||
|
const colors = [
|
||||||
|
g.theme.bg,
|
||||||
|
require("messageicons").getColor(src, {settings: settings})
|
||||||
|
];
|
||||||
|
if (settings.flash && ((Date.now()/1000)&1)) {
|
||||||
|
if (colors[1]==g.theme.fg) {
|
||||||
|
colors.reverse();
|
||||||
|
} else {
|
||||||
|
colors[1] = g.theme.fg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.setColor(colors[1]).setBgColor(colors[0]);
|
||||||
|
// draw the icon, or '...' if too many messages
|
||||||
|
g.drawImage(i==(settings.maxMessages-1) && this.srcs.length>settings.maxMessages ? atob("EASBAGGG88/zz2GG") : require("messageicons").getImage(src),
|
||||||
|
this.x+12+i*24, this.y+12, {rotate: 0/*force centering*/});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WIDGETS["messages"].i = setTimeout(() => WIDGETS["messages"].draw(true), 1000);
|
||||||
|
if (process.env.HWVERSION>1) Bangle.on("touch", this.touch);
|
||||||
|
}, onMsg: function(type, msg) {
|
||||||
|
if (this.hidden) return;
|
||||||
|
if (type==="music") return;
|
||||||
|
if (msg.id && !msg.new && msg.t!=="remove") return;
|
||||||
|
this.srcs = filterMessages(require("messages").getMessages(msg));
|
||||||
|
const settings = Object.assign({maxMessages:3},require('Storage').readJSON("messages.settings.json", true) || {});
|
||||||
|
this.width = 24 * E.clip(this.srcs.length, 0, settings.maxMessages);
|
||||||
|
if (type!=="init") Bangle.drawWidgets(); // "init" is not a real message type: see below
|
||||||
|
}, touch: function(b, c) {
|
||||||
|
var w = WIDGETS["messages"];
|
||||||
|
if (!w || !w.width || c.x<w.x || c.x>w.x+w.width || c.y<w.y || c.y>w.y+24) return;
|
||||||
|
require("messages").openGUI();
|
||||||
|
}, hide() {
|
||||||
|
this.hidden=true;
|
||||||
|
if (this.width) {
|
||||||
|
// hide widget
|
||||||
|
this.width = 0;
|
||||||
|
Bangle.drawWidgets();
|
||||||
|
}
|
||||||
|
}, show() {
|
||||||
|
delete this.hidden
|
||||||
|
this.onMsg("show", {}); // reload messages+redraw
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Bangle.on("message", WIDGETS["messages"].onMsg);
|
||||||
|
this.srcs = {};
|
||||||
|
WIDGETS["messages"].onMsg("init", {}); // abuse type="init" to prevent Bangle.drawWidgets();
|
||||||
|
})();
|
|
@ -1,2 +1,3 @@
|
||||||
0.01: New widget!
|
0.01: New widget!
|
||||||
0.02: Adjust to message icons moving to messageicons lib
|
0.02: Adjust to message icons moving to messageicons lib
|
||||||
|
0.03: Use new message library
|
|
@ -20,9 +20,9 @@ You probably want to disable the default widget, to do so:
|
||||||
3. Scroll down to the `Widget messages` entry, and change it to `Hide`
|
3. Scroll down to the `Widget messages` entry, and change it to `Hide`
|
||||||
|
|
||||||
## Settings
|
## Settings
|
||||||
This widget uses the `Widget` settings from the `messages` app:
|
You can change settings by going to the global `Settings` app, then `App Settings`
|
||||||
|
and `Messages`:
|
||||||
|
|
||||||
### Widget
|
|
||||||
* `Flash icon` Toggle flashing of the widget icons.
|
* `Flash icon` Toggle flashing of the widget icons.
|
||||||
<!-- * `Show read` - Also show the widget when there are only old messages. -->
|
<!-- * `Show read` - Also show the widget when there are only old messages. -->
|
||||||
* `Widget messages` Not used by this widget, but you should select `Hide` to hide the default widget.
|
* `Widget messages` Not used by this widget.
|
|
@ -0,0 +1,8 @@
|
||||||
|
exports.hide = function() {
|
||||||
|
if (!global.WIDGETS||!WIDGETS["msggrid"]) return;
|
||||||
|
WIDGETS["msggrid"].hide();
|
||||||
|
}
|
||||||
|
exports.show = function() {
|
||||||
|
if (!global.WIDGETS||!WIDGETS["msggrid"]) return;
|
||||||
|
WIDGETS["msggrid"].show();
|
||||||
|
}
|
|
@ -1,16 +1,17 @@
|
||||||
{
|
{
|
||||||
"id": "widmsggrid",
|
"id": "widmsggrid",
|
||||||
"name": "Messages Grid Widget",
|
"name": "Messages Grid Widget",
|
||||||
"version": "0.02",
|
"version": "0.03",
|
||||||
"description": "Widget that display notification icons in a grid",
|
"description": "Widget that displays notification icons in a grid",
|
||||||
"icon": "widget.png",
|
"icon": "widget.png",
|
||||||
"type": "widget",
|
"type": "widget",
|
||||||
"dependencies": {"messages":"app"},
|
|
||||||
"tags": "tool,system",
|
"tags": "tool,system",
|
||||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||||
"dependencies" : { "messageicons":"module" },
|
"dependencies" : { "messages":"module" },
|
||||||
|
"provides_modules" : ["messagewidget"],
|
||||||
"readme": "README.md",
|
"readme": "README.md",
|
||||||
"storage": [
|
"storage": [
|
||||||
|
{"name":"messagewidget","url":"lib.js"},
|
||||||
{"name":"widmsggrid.wid.js","url":"widget.js"}
|
{"name":"widmsggrid.wid.js","url":"widget.js"}
|
||||||
],
|
],
|
||||||
"screenshots": [{"url":"screenshot.png"}]
|
"screenshots": [{"url":"screenshot.png"}]
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
clearTimeout(w.t);
|
clearTimeout(w.t);
|
||||||
delete w.t;
|
delete w.t;
|
||||||
}
|
}
|
||||||
if (!w.width) return;
|
if (!w.width || this.hidden) return;
|
||||||
const b = w.flash && w.status === "new" && ((Date.now() / 1000) & 1), // Blink(= inverse colors) on this second?
|
const b = w.flash && w.status === "new" && ((Date.now() / 1000) & 1), // Blink(= inverse colors) on this second?
|
||||||
// show multiple icons in a grid, by scaling them down
|
// show multiple icons in a grid, by scaling them down
|
||||||
cols = Math.ceil(Math.sqrt(w.srcs.length - 0.1)); // cols===rows, -0.1 to work around rounding error
|
cols = Math.ceil(Math.sqrt(w.srcs.length - 0.1)); // cols===rows, -0.1 to work around rounding error
|
||||||
|
@ -57,9 +57,10 @@
|
||||||
.drawString(w.total, w.x + w.width - 1, w.y + 24, w.total > 9);
|
.drawString(w.total, w.x + w.width - 1, w.y + 24, w.total > 9);
|
||||||
}
|
}
|
||||||
if (w.flash && w.status === "new") w.t = setTimeout(w.draw, 1000); // schedule redraw while blinking
|
if (w.flash && w.status === "new") w.t = setTimeout(w.draw, 1000); // schedule redraw while blinking
|
||||||
}, show: function () {
|
}, show: function (m) {
|
||||||
|
delete w.hidden;
|
||||||
w.width = 24;
|
w.width = 24;
|
||||||
w.srcs = require("messages").getMessages()
|
w.srcs = require("messages").getMessages(m)
|
||||||
.filter(m => !['call', 'map', 'music'].includes(m.id))
|
.filter(m => !['call', 'map', 'music'].includes(m.id))
|
||||||
.filter(m => m.new || w.showRead)
|
.filter(m => m.new || w.showRead)
|
||||||
.map(m => m.src);
|
.map(m => m.src);
|
||||||
|
@ -68,6 +69,7 @@
|
||||||
Bangle.drawWidgets();
|
Bangle.drawWidgets();
|
||||||
Bangle.setLCDPower(1); // turns screen on
|
Bangle.setLCDPower(1); // turns screen on
|
||||||
}, hide: function () {
|
}, hide: function () {
|
||||||
|
w.hidden = true;
|
||||||
w.width = 0;
|
w.width = 0;
|
||||||
w.srcs = [];
|
w.srcs = [];
|
||||||
w.total = 0;
|
w.total = 0;
|
||||||
|
@ -82,13 +84,16 @@
|
||||||
}
|
}
|
||||||
// Bangle.js 2: open app when touching the widget
|
// Bangle.js 2: open app when touching the widget
|
||||||
else if (c.x < w.x || c.x > w.x + w.width || c.y < w.y || c.y > w.y + 24) return;
|
else if (c.x < w.x || c.x > w.x + w.width || c.y < w.y || c.y > w.y + 24) return;
|
||||||
load("messages.app.js");
|
require("messages").openGUI();
|
||||||
}, listener: function () {
|
}, listener: function (t,m) {
|
||||||
w.status = require("messages").status();
|
if (this.hidden) return;
|
||||||
if (w.status === "new" || (w.status === "old" && w.showRead)) w.show();
|
w.status = require("messages").status(m);
|
||||||
|
if (w.status === "new" || (w.status === "old" && w.showRead)) w.show(m);
|
||||||
else w.hide();
|
else w.hide();
|
||||||
|
delete w.hidden; // always set by w.hide(), but we checked it wasn't there before
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
delete s;
|
delete s;
|
||||||
const w = WIDGETS["msggrid"];
|
const w = WIDGETS["msggrid"];
|
||||||
|
Bangle.on("message", w.listener);
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -92,7 +92,8 @@ const INTERNAL_FILES_IN_APP_TYPE = { // list of app types and files they SHOULD
|
||||||
};
|
};
|
||||||
/* These are warnings we know about but don't want in our output */
|
/* These are warnings we know about but don't want in our output */
|
||||||
var KNOWN_WARNINGS = [
|
var KNOWN_WARNINGS = [
|
||||||
"App gpsrec data file wildcard .gpsrc? does not include app ID"
|
"App gpsrec data file wildcard .gpsrc? does not include app ID",
|
||||||
|
"App widmessages storage file messagewidget is also listed as storage file for app widmsggrid",
|
||||||
];
|
];
|
||||||
|
|
||||||
function globToRegex(pattern) {
|
function globToRegex(pattern) {
|
||||||
|
|
2
core
|
@ -1 +1 @@
|
||||||
Subproject commit db08367e0a2c25040449a4b556eaed459e8f47fc
|
Subproject commit f15e99fbe25b2991719011e6da9bc9c7be401a7e
|