Merge branch 'espruino:master' into sleeplog
|
@ -1,6 +1,11 @@
|
|||
# Active Pedometer
|
||||
|
||||
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.
|
||||
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.
|
||||
|
@ -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.
|
||||
|
||||
## Screenshots
|
||||
|
||||
* 600 steps
|
||||

|
||||
|
||||
|
@ -70,4 +76,4 @@ Steps are saved to a datafile every 5 minutes. You can watch a graph using the a
|
|||
|
||||
## Requests
|
||||
|
||||
If you have any feature requests, please post in this forum thread: http://forum.espruino.com/conversations/345754/
|
||||
If you have any feature requests, please post in this forum thread: http://forum.espruino.com/conversations/345754/
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"name": "Active Pedometer",
|
||||
"shortName": "Active Pedometer",
|
||||
"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",
|
||||
"tags": "outdoors,widget",
|
||||
"supports": ["BANGLEJS"],
|
||||
|
|
|
@ -4,4 +4,6 @@
|
|||
0.04: Added awareness of allDay field
|
||||
0.05: Displaying calendar colour and name
|
||||
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,29 +1,29 @@
|
|||
(function() {
|
||||
var agendaItems = {
|
||||
name: "Agenda",
|
||||
img: atob("GBiBAf////////85z/AAAPAAAPgAAP////AAAPAAAPAAAPAAAOAAAeAAAeAAAcAAA8AAAoAABgAADP//+P//8PAAAPAAAPgAAf///w=="),
|
||||
items: []
|
||||
};
|
||||
var agendaItems = {
|
||||
name: "Agenda",
|
||||
img: atob("GBiBAAAAAAAAAADGMA///w///wf//wAAAA///w///w///w///x///h///h///j///D///X//+f//8wAABwAADw///w///wf//gAAAA=="),
|
||||
items: []
|
||||
};
|
||||
var locale = require("locale");
|
||||
var now = new Date();
|
||||
var agenda = require("Storage").readJSON("android.calendar.json")
|
||||
.filter(ev=>ev.timestamp + ev.durationInSeconds > now/1000)
|
||||
.sort((a,b)=>a.timestamp - b.timestamp);
|
||||
|
||||
var now = new Date();
|
||||
var agenda = storage.readJSON("android.calendar.json")
|
||||
.filter(ev=>ev.timestamp + ev.durationInSeconds > now/1000)
|
||||
.sort((a,b)=>a.timestamp - b.timestamp);
|
||||
agenda.forEach((entry, i) => {
|
||||
|
||||
agenda.forEach((entry, i) => {
|
||||
var title = entry.title.slice(0,12);
|
||||
var date = new Date(entry.timestamp*1000);
|
||||
var dateStr = locale.date(date).replace(/\d\d\d\d/,"");
|
||||
dateStr += entry.durationInSeconds < 86400 ? "/ " + locale.time(date,1) : "";
|
||||
|
||||
var title = entry.title.slice(0,18);
|
||||
var date = new Date(entry.timestamp*1000);
|
||||
var dateStr = locale.date(date).replace(/\d\d\d\d/,"");
|
||||
dateStr += entry.durationInSeconds < 86400 ? "/ " + locale.time(date,1) : "";
|
||||
agendaItems.items.push({
|
||||
name: "Agenda "+i,
|
||||
get: () => ({ text: title + "\n" + dateStr, img: null}),
|
||||
show: function() { agendaItems.items[i].emit("redraw"); },
|
||||
hide: function () {}
|
||||
});
|
||||
});
|
||||
|
||||
agendaItems.items.push({
|
||||
name: null,
|
||||
get: () => ({ text: title + "\n" + dateStr, img: null}),
|
||||
show: function() { agendaItems.items[i].emit("redraw"); },
|
||||
hide: function () {}
|
||||
});
|
||||
});
|
||||
|
||||
return agendaItems;
|
||||
})
|
||||
return agendaItems;
|
||||
})
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "agenda",
|
||||
"name": "Agenda",
|
||||
"version": "0.07",
|
||||
"version": "0.08",
|
||||
"description": "Simple agenda",
|
||||
"icon": "agenda.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.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.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.
|
||||
* `Keep Msgs` - default is `Off`. When Gadgetbridge disconnects, should Bangle.js
|
||||
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
|
||||
|
||||
## How it works
|
||||
|
|
|
@ -126,6 +126,18 @@
|
|||
request.j(event.err); //r = reJect function
|
||||
else
|
||||
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];
|
||||
|
@ -189,6 +201,30 @@
|
|||
if (isFinite(msg.id)) return gbSend({ t: "notify", n:response?"OPEN":"DISMISS", id: msg.id });
|
||||
// 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
|
||||
delete settings;
|
||||
})();
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
"id": "android",
|
||||
"name": "Android Integration",
|
||||
"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.",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,system,messages,notifications,gadgetbridge",
|
||||
"dependencies": {"messages":"app"},
|
||||
"dependencies": {"messages":"module"},
|
||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
(function(back) {
|
||||
|
||||
|
||||
|
||||
function gb(j) {
|
||||
Bluetooth.println(JSON.stringify(j));
|
||||
}
|
||||
|
@ -23,7 +26,17 @@
|
|||
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);
|
||||
})
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
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 nExplosions = 0;
|
||||
var landed = false;
|
||||
var lightning = 0;
|
||||
|
||||
var settings = require("Storage").readJSON('f9settings.json', 1) || {};
|
||||
|
||||
const gravity = 4;
|
||||
const dt = 0.1;
|
||||
|
@ -61,18 +64,40 @@ function flameImageGen (throttle) {
|
|||
|
||||
function drawFalcon(x, y, throttle, 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 r = falcon9.height/2 + flameImg.height/2-1;
|
||||
var xoffs = -Math.sin(angle)*r;
|
||||
var yoffs = Math.cos(angle)*r;
|
||||
if (Math.random()>0.7) g.setColor(1, 0.5, 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() {
|
||||
if (lightning==1) {
|
||||
g.setBgColor(1, 1, 1).clear();
|
||||
Bangle.buzz(200);
|
||||
return;
|
||||
}
|
||||
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.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();
|
||||
showFuel();
|
||||
drawFalcon(booster.x, booster.y, Math.floor(input.throttle*12), input.angle);
|
||||
if (lightning>1 && lightning<6) drawLightning();
|
||||
}
|
||||
|
||||
function getInputs() {
|
||||
|
@ -97,6 +123,7 @@ function getInputs() {
|
|||
if (t > 1) t = 1;
|
||||
if (t < 0) t = 0;
|
||||
if (booster.fuel<=0) t = 0;
|
||||
if (lightning>0 && lightning<20) t = 0;
|
||||
return {throttle: t, angle: a};
|
||||
}
|
||||
|
||||
|
@ -121,7 +148,6 @@ function gameStep() {
|
|||
else {
|
||||
var input = getInputs();
|
||||
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) {
|
||||
renderScreen({angle:0, throttle:0});
|
||||
epilogue("You landed!");
|
||||
|
@ -129,6 +155,8 @@ function gameStep() {
|
|||
else exploded = true;
|
||||
}
|
||||
else {
|
||||
if (lightning) ++lightning;
|
||||
if (settings.lightning && (lightning==0||lightning>40) && Math.random()>0.98) lightning = 1;
|
||||
booster.x += booster.vx*dt;
|
||||
booster.y += booster.vy*dt;
|
||||
booster.vy += gravity*dt;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{ "id": "f9lander",
|
||||
"name": "Falcon9 Lander",
|
||||
"shortName":"F9lander",
|
||||
"version":"0.01",
|
||||
"version":"0.02",
|
||||
"description": "Land a rocket booster",
|
||||
"icon": "f9lander.png",
|
||||
"screenshots" : [ { "url":"f9lander_screenshot1.png" }, { "url":"f9lander_screenshot2.png" }, { "url":"f9lander_screenshot3.png" }],
|
||||
|
@ -10,6 +10,7 @@
|
|||
"supports" : ["BANGLEJS", "BANGLEJS2"],
|
||||
"storage": [
|
||||
{"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);
|
||||
})
|
|
@ -1,4 +1,5 @@
|
|||
0.01: Base code
|
||||
0.02: Saved settings when switching color scheme
|
||||
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;
|
||||
E.showMessage("Loading Messages...");
|
||||
load("messages.app.js");
|
||||
require("messages").openGUI();
|
||||
}
|
||||
|
||||
function hasMessages()
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "hcclock",
|
||||
"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.",
|
||||
"icon": "hcclock-icon.png",
|
||||
"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.10: Added more bundleIds
|
||||
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
|
||||
setTimeout(()=>load("messages.app.js"),10);
|
||||
setTimeout(()=>require("messages").openGUI(),10);
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"id": "ios",
|
||||
"name": "iOS Integration",
|
||||
"version": "0.11",
|
||||
"version": "0.12",
|
||||
"description": "Display notifications/music/etc from iOS devices",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,system,ios,apple,messages,notifications",
|
||||
"dependencies": {"messages":"app"},
|
||||
"dependencies": {"messages":"module"},
|
||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
|
|
|
@ -19,3 +19,4 @@
|
|||
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.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)
|
||||
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({
|
||||
h : 64*scaleval, c : apps.length,
|
||||
draw : (i, r) => {
|
||||
|
@ -74,7 +64,12 @@ E.showScroller({
|
|||
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
|
||||
|
||||
|
@ -85,7 +80,7 @@ let lockHandler = function(locked) {
|
|||
if (lockTimeout) clearTimeout(lockTimeout);
|
||||
lockTimeout = undefined;
|
||||
if (locked)
|
||||
lockTimeout = setTimeout(returnToClock, 10000);
|
||||
lockTimeout = setTimeout(Bangle.showClock, 10000);
|
||||
}
|
||||
Bangle.on("lock", lockHandler);
|
||||
if (!settings.fullscreen) // finally draw widgets
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "launch",
|
||||
"name": "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.",
|
||||
"readme": "README.md",
|
||||
"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...
|
||||
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 */
|
||||
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
|
||||
require("messages").pushMessage({"t":"add","id":"call","src":"Phone","title":"Bob","body":"12421312",positive:true,negative:true})
|
||||
*/
|
||||
|
||||
var Layout = require("Layout");
|
||||
var settings = require('Storage').readJSON("messages.settings.json", true) || {};
|
||||
var fontSmall = "6x8";
|
||||
|
@ -49,8 +48,11 @@ to the clock. */
|
|||
var unreadTimeout;
|
||||
/// List of all our messages
|
||||
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
|
||||
if (msg && msg.id!=="music" && msg.new && active!="map" &&
|
||||
!((require('Storage').readJSON('setting.json', 1) || {}).quiet)) {
|
||||
|
@ -62,9 +64,15 @@ var onMessagesModified = function(msg) {
|
|||
}
|
||||
showMessage(msg&&msg.id);
|
||||
};
|
||||
Bangle.on("message", onMessagesModified);
|
||||
|
||||
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) {
|
||||
active = "map";
|
||||
|
@ -355,12 +363,16 @@ function checkMessages(options) {
|
|||
}
|
||||
// we have >0 messages
|
||||
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 (options.showMsgIfUnread && newMessages.length) {
|
||||
if ((toShow||options.showMsgIfUnread) && newMessages.length) {
|
||||
showMessage(newMessages[0].id);
|
||||
// buzz after showMessage, so being busy during layout doesn't affect the buzz pattern
|
||||
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
|
||||
global.BUZZ_ON_NEW_MESSAGE = false;
|
||||
// messages.buzz respects quiet mode - no need to check here
|
||||
|
@ -428,6 +440,7 @@ function cancelReloadTimeout() {
|
|||
g.clear();
|
||||
|
||||
Bangle.loadWidgets();
|
||||
require("messages").toggleWidget(false);
|
||||
Bangle.drawWidgets();
|
||||
|
||||
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.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: Moved messages library into standalone library
|
||||
|
|
|
@ -1,62 +1,25 @@
|
|||
# Messages app
|
||||
# Messages library
|
||||
|
||||
This app handles the display of messages and message notifications. It stores
|
||||
a list of currently received messages and allows them to be listed, viewed,
|
||||
and responded to.
|
||||
This library handles the passing of messages. It can storess a list of messages
|
||||
and allows them to be retrieved by other apps.
|
||||
|
||||
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`
|
||||
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?
|
||||
* `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!)_
|
||||
|
||||

|
||||
1. Gadgetbridge sends an event to your watch for an incoming message
|
||||
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 */})`
|
||||
4. Overlay Notifications shows the message in an overlay, and marks it as `handled`
|
||||
5. The default GUI app (`messages`) sees the event is marked as `handled`, so does nothing.
|
||||
6. The default widget (`widmessages`) does nothing with `handled`, and shows a notification icon.
|
||||
7. You tap the notification, in order to open the full GUI Overlay Notifications
|
||||
calls `require("messages").openGUI({/** the message */})`
|
||||
8. The default GUI app (`messages`) sees the "messageGUI" event, and launches itself
|
||||
|
||||
|
||||
## Events (for app/widget developers)
|
||||
|
||||
## Events
|
||||
|
||||
When a new message arrives, a `"message"` event is emitted, you can listen for
|
||||
it like this:
|
||||
|
@ -64,9 +27,8 @@ it like this:
|
|||
```js
|
||||
myMessageListener = Bangle.on("message", (type, message)=>{
|
||||
if (message.handled) return; // another app already handled this message
|
||||
// <type> is one of "text", "call", "alarm", "map", "music", or "clearAll"
|
||||
if (type === "clearAll") return; // not a message
|
||||
// see `messages/lib.js` for possible <message> formats
|
||||
// <type> is one of "text", "call", "alarm", "map", or "music"
|
||||
// see `messagelib/lib.js` for possible <message> formats
|
||||
// message.t could be "add", "modify" or "remove"
|
||||
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`:
|
||||
|
@ -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
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
function openMusic() {
|
||||
// only read settings file for first music message
|
||||
if ("undefined"==typeof exports._openMusic) {
|
||||
exports._openMusic = !!((require('Storage').readJSON("messages.settings.json", true) || {}).openMusic);
|
||||
}
|
||||
return exports._openMusic;
|
||||
exports.music = {};
|
||||
/**
|
||||
* Emit "message" event with appropriate type from Bangle
|
||||
* @param {object} msg
|
||||
*/
|
||||
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:
|
||||
{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
|
||||
|
@ -12,125 +18,178 @@ function openMusic() {
|
|||
{t:"modify",id:int, title:string} // modified
|
||||
*/
|
||||
exports.pushMessage = function(event) {
|
||||
var messages = exports.getMessages();
|
||||
// now modify/delete as appropriate
|
||||
var mIdx = messages.findIndex(m=>m.id==event.id);
|
||||
if (event.t=="remove") {
|
||||
if (mIdx>=0) messages.splice(mIdx, 1); // remove item
|
||||
mIdx=-1;
|
||||
if (event.t==="remove") {
|
||||
if (event.id==="music") exports.music = {};
|
||||
} else { // add/modify
|
||||
if (event.t=="add"){
|
||||
if(event.new === undefined ) { // If 'new' has not been set yet, set it
|
||||
event.new=true; // Assume it should be new
|
||||
}
|
||||
if (event.t==="add") {
|
||||
if (event.new===undefined) 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);
|
||||
}
|
||||
if (mIdx<0) {
|
||||
mIdx=0;
|
||||
messages.unshift(event); // add new messages to the beginning
|
||||
}
|
||||
else Object.assign(messages[mIdx], event);
|
||||
if (event.id=="music" && messages[mIdx].state=="play") {
|
||||
messages[mIdx].new = true; // new track, or playback (re)started
|
||||
type = 'music';
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
require("Storage").writeJSON("messages.json",messages);
|
||||
var message = mIdx<0 ? {id:event.id, t:'remove'} : messages[mIdx];
|
||||
// if in app, process immediately
|
||||
if ("undefined"!=typeof MESSAGES) return onMessagesModified(message);
|
||||
// emit message event
|
||||
var type = 'text';
|
||||
if (["call", "music", "map"].includes(message.id)) type = message.id;
|
||||
if (message.src && message.src.toLowerCase().startsWith("alarm")) type = "alarm";
|
||||
Bangle.emit("message", type, message);
|
||||
// update the widget icons shown
|
||||
if (global.WIDGETS && WIDGETS.messages) WIDGETS.messages.update(messages,true);
|
||||
var handleMessage = () => {
|
||||
// if no new messages now, make sure we don't load the messages app
|
||||
if (event.t=="remove" && exports.messageTimeout && !messages.some(m => m.new)) {
|
||||
clearTimeout(exports.messageTimeout);
|
||||
delete exports.messageTimeout;
|
||||
}
|
||||
// ok, saved now
|
||||
if (event.id=="music" && Bangle.CLOCK && messages[mIdx].new && openMusic()) {
|
||||
// just load the app to display music: no buzzing
|
||||
Bangle.load("messages.app.js");
|
||||
} else if (event.t!="add") {
|
||||
// we only care if it's new
|
||||
return;
|
||||
} else if (event.new==false) {
|
||||
return;
|
||||
}
|
||||
// otherwise load messages/show widget
|
||||
var loadMessages = Bangle.CLOCK || event.important;
|
||||
var quiet = (require('Storage').readJSON('setting.json', 1) || {}).quiet;
|
||||
var appSettings = require('Storage').readJSON('messages.settings.json', 1) || {};
|
||||
var unlockWatch = appSettings.unlockWatch;
|
||||
// don't auto-open messages in quiet mode if quietNoAutOpn is true
|
||||
if ((quiet && appSettings.quietNoAutOpn) || appSettings.noAutOpn)
|
||||
loadMessages = false;
|
||||
delete appSettings;
|
||||
// after a delay load the app, to ensure we have all the messages
|
||||
if (exports.messageTimeout) clearTimeout(exports.messageTimeout);
|
||||
exports.messageTimeout = setTimeout(function() {
|
||||
exports.messageTimeout = undefined;
|
||||
// if we're in a clock or it's important, go straight to messages app
|
||||
if (loadMessages) {
|
||||
if (!quiet && unlockWatch) {
|
||||
Bangle.setLocked(false);
|
||||
Bangle.setLCDPower(1); // turn screen on
|
||||
}
|
||||
// we will buzz when we enter the messages app
|
||||
return Bangle.load("messages.new.js");
|
||||
}
|
||||
if (global.WIDGETS && WIDGETS.messages) WIDGETS.messages.update(messages);
|
||||
exports.buzz(message.src);
|
||||
}, 500);
|
||||
};
|
||||
setTimeout(()=>{
|
||||
if (!message.handled) handleMessage();
|
||||
},0);
|
||||
}
|
||||
/// Remove all messages
|
||||
// reset state (just in case)
|
||||
delete event.handled;
|
||||
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);
|
||||
}
|
||||
return messages;
|
||||
};
|
||||
|
||||
/**
|
||||
* Accept a call (or other acceptable event)
|
||||
* @param {object} msg
|
||||
*/
|
||||
exports.accept = function(msg) {
|
||||
if (msg.positive) Bangle.messageResponse(msg, true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Dismiss a message (if applicable), and erase it from flash
|
||||
* Emits a "message" event with t="remove", only if message existed
|
||||
*
|
||||
* @param {object} msg
|
||||
*/
|
||||
exports.dismiss = function(msg) {
|
||||
if (msg.negative) Bangle.messageResponse(msg, false);
|
||||
let messages = exports.getMessages();
|
||||
const mIdx = messages.findIndex(m=>m.id===msg.id);
|
||||
if (mIdx<0) return;
|
||||
messages.splice(mIdx, 1);
|
||||
exports.write(messages);
|
||||
if (msg.t==="remove") return; // already removed, don't re-emit
|
||||
msg.t = "remove";
|
||||
emit(msg); // emit t="remove", so e.g. widgets know to update
|
||||
};
|
||||
|
||||
/**
|
||||
* Emit a "type=openGUI" event, to open GUI app
|
||||
*
|
||||
* @param {object} [msg={}] Message the app should show
|
||||
*/
|
||||
exports.openGUI = function(msg) {
|
||||
if (!require("Storage").read("messagegui")) return; // "messagegui" module is missing!
|
||||
// Mark the event as unhandled for GUI, but leave passed arguments intact
|
||||
let copy = Object.assign({}, msg);
|
||||
delete copy.handled;
|
||||
require("messagegui").open(copy);
|
||||
};
|
||||
|
||||
/**
|
||||
* Show/hide the messages widget
|
||||
*
|
||||
* @param {boolean} show
|
||||
*/
|
||||
exports.toggleWidget = function(show) {
|
||||
if (!require("Storage").read("messagewidget")) return; // "messagewidget" module is missing!
|
||||
if (show) require("messagewidget").show();
|
||||
else require("messagewidget").hide();
|
||||
};
|
||||
|
||||
/**
|
||||
* Replace all stored messages
|
||||
* @param {array} messages Messages to save
|
||||
*/
|
||||
exports.write = function(messages) {
|
||||
require("Storage").writeJSON("messages.json", messages.map(m => {
|
||||
// we never want to save saved/handled status to file;
|
||||
delete m.saved;
|
||||
delete m.handled;
|
||||
return m;
|
||||
}));
|
||||
};
|
||||
/**
|
||||
* Erase all messages
|
||||
*/
|
||||
exports.clearAll = function() {
|
||||
if ("undefined"!= typeof MESSAGES) { // we're in a messages app, clear that as well
|
||||
MESSAGES = [];
|
||||
}
|
||||
// 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();
|
||||
exports.write([]);
|
||||
Bangle.emit("message", "clearAll", {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
exports.getMessages = function() {
|
||||
if ("undefined"!=typeof MESSAGES) return MESSAGES; // loaded/managed by app
|
||||
return require("Storage").readJSON("messages.json",1)||[];
|
||||
}
|
||||
exports.getMessages = function(withMessage) {
|
||||
let messages = require("Storage").readJSON("messages.json", true);
|
||||
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
|
||||
*
|
||||
* @param {object} [withMessage] Apply this event to messages, see getMessages
|
||||
* @returns {string} "new"/"old"/"none"
|
||||
*/
|
||||
exports.status = function() {
|
||||
exports.status = function(withMessage) {
|
||||
try {
|
||||
let status= "none";
|
||||
for(const m of exports.getMessages()) {
|
||||
let status = "none";
|
||||
for(const m of exports.getMessages(withMessage)) {
|
||||
if (["music", "map"].includes(m.id)) continue;
|
||||
if (m.new) return "new";
|
||||
status = "old";
|
||||
}
|
||||
return status;
|
||||
} 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.stopBuzz(); // cancel any previous buzz timeouts
|
||||
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) || {};
|
||||
var pattern;
|
||||
if (msgSrc && msgSrc.toLowerCase() === "phone") {
|
||||
if ((require("Storage").readJSON("setting.json", 1) || {}).quiet) return Promise.resolve(); // never buzz during Quiet Mode
|
||||
const msgSettings = require("Storage").readJSON("messages.settings.json", true) || {};
|
||||
let pattern;
|
||||
if (msgSrc && msgSrc.toLowerCase()==="phone") {
|
||||
// special vibration pattern for incoming calls
|
||||
pattern = msgSettings.vibrateCalls;
|
||||
} else {
|
||||
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();
|
||||
|
||||
var repeat = msgSettings.repeat;
|
||||
if (repeat===undefined) repeat=4; // repeat may be zero
|
||||
let repeat = msgSettings.repeat;
|
||||
if (repeat===undefined) repeat = 4; // repeat may be zero
|
||||
if (repeat) {
|
||||
exports.buzzTimeout = setTimeout(()=>require("buzz").pattern(pattern), repeat*1000);
|
||||
var vibrateTimeout = msgSettings.vibrateTimeout;
|
||||
if (vibrateTimeout===undefined) vibrateTimeout=60;
|
||||
exports.buzzTimeout = setTimeout(() => require("buzz").pattern(pattern), repeat*1000);
|
||||
let vibrateTimeout = msgSettings.vibrateTimeout;
|
||||
if (vibrateTimeout===undefined) vibrateTimeout = 60;
|
||||
if (vibrateTimeout && !exports.stopTimeout) exports.stopTimeout = setTimeout(exports.stopBuzz, vibrateTimeout*1000);
|
||||
}
|
||||
return require("buzz").pattern(pattern);
|
||||
|
|
|
@ -1,23 +1,18 @@
|
|||
{
|
||||
"id": "messages",
|
||||
"name": "Messages",
|
||||
"version": "0.54",
|
||||
"description": "App to display notifications from iOS and Gadgetbridge/Android",
|
||||
"version": "0.55",
|
||||
"description": "Library to handle, load and store message events received from Android/iOS",
|
||||
"icon": "app.png",
|
||||
"type": "app",
|
||||
"type": "module",
|
||||
"tags": "tool,system",
|
||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||
"dependencies" : { "messageicons":"module" },
|
||||
"provides_modules" : ["messages"],
|
||||
"dependencies" : { "messagegui":"module","messagewidget":"module" },
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name":"messages.app.js","url":"app.js"},
|
||||
{"name":"messages.new.js","url":"app-newmessage.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"}
|
||||
{"name":"messages","url":"lib.js"},
|
||||
{"name":"messages.settings.js","url":"settings.js"}
|
||||
],
|
||||
"data": [{"name":"messages.json"},{"name":"messages.settings.json"}],
|
||||
"screenshots": [{"url":"screenshot.png"},{"url":"screenshot-notify.gif"}],
|
||||
"sortorder": -9
|
||||
"data": [{"name":"messages.json"},{"name":"messages.settings.json"}]
|
||||
}
|
||||
|
|
|
@ -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.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.
|
||||
|
||||
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.)
|
||||
|
||||
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).
|
||||
|
||||
The icon used for this app is from [https://icons8.com](https://icons8.com).
|
||||
|
|
|
@ -1,14 +1 @@
|
|||
let showMusic = () => {
|
||||
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();
|
||||
}
|
||||
require('messages').openGUI({"t":"add","artist":" ","album":" ","track":" ","dur":0,"c":-1,"n":-1,"id":"music","title":"Music","state":"play","new":true});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "messagesmusic",
|
||||
"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",
|
||||
"icon":"app.png",
|
||||
"type": "app",
|
||||
|
@ -13,6 +13,6 @@
|
|||
{"name":"messagesmusic.app.js","url":"app.js"},
|
||||
{"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.07: Move mylocation app into 'Settings -> Apps'
|
||||
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
|
||||
|
||||
*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
|
||||
* Other Apps can read this information to do calculations based on location
|
||||
|
|
|
@ -33,10 +33,11 @@
|
|||
<div id="map">
|
||||
</div>
|
||||
<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/>
|
||||
</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://code.jquery.com/jquery-3.2.1.min.js"></script>
|
||||
<script src="../../webtools/heatshrink.js"></script>
|
||||
|
@ -68,15 +69,19 @@
|
|||
|
||||
let latlon;
|
||||
var marker;
|
||||
|
||||
map.on('click', function(e){
|
||||
console.log(e);
|
||||
|
||||
function setPosition(ll) {
|
||||
latlon = ll;
|
||||
if (map.hasLayer(marker)) {
|
||||
map.removeLayer(marker);
|
||||
}
|
||||
latlon = e.latlng;
|
||||
marker = new L.marker(e.latlng).addTo(map);
|
||||
marker = new L.marker(latlon).addTo(map);
|
||||
document.getElementById("select-hint").style.display="none";
|
||||
document.getElementById("select").style.display="";
|
||||
}
|
||||
|
||||
map.on('click', function(e){
|
||||
setPosition(e.latlng);
|
||||
});
|
||||
|
||||
document.getElementById("select").addEventListener("click", function() {
|
||||
|
@ -87,9 +92,23 @@
|
|||
Util.showModal("Saving...");
|
||||
Util.writeStorage("mylocation.json", JSON.stringify(settings), ()=>{
|
||||
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>
|
||||
</body>
|
||||
</html>
|
|
@ -4,12 +4,12 @@
|
|||
"icon": "app.png",
|
||||
"type": "settings",
|
||||
"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.",
|
||||
"readme": "README.md",
|
||||
"tags": "tool,utility",
|
||||
"supports": ["BANGLEJS", "BANGLEJS2"],
|
||||
"custom": "custom.html","custom": "custom.html",
|
||||
"interface": "interface.html",
|
||||
"storage": [
|
||||
{"name":"mylocation.settings.js","url":"settings.js"}
|
||||
],
|
||||
|
|
|
@ -12,3 +12,7 @@
|
|||
Fix alignment of satellite info text
|
||||
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.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 HASWIDGETS = true;
|
||||
var y1,y2;
|
||||
var R;
|
||||
var fix = {};
|
||||
var mapVisible = false;
|
||||
var hasScrolled = false;
|
||||
var settings = require("Storage").readJSON("openstmap.json",1)||{};
|
||||
|
||||
// Redraw the whole page
|
||||
function redraw() {
|
||||
g.setClipRect(0,y1,g.getWidth()-1,y2);
|
||||
g.setClipRect(R.x,R.y,R.x2,R.y2);
|
||||
m.draw();
|
||||
drawMarker();
|
||||
if (WIDGETS["gpsrec"] && WIDGETS["gpsrec"].plotTrack) {
|
||||
g.flip(); // force immediate draw on double-buffered screens - track will update later
|
||||
g.setColor(0.75,0.2,0);
|
||||
WIDGETS["gpsrec"].plotTrack(m);
|
||||
// if track drawing is enabled...
|
||||
if (settings.drawTrack) {
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
// Draw the marker for where we are
|
||||
function drawMarker() {
|
||||
if (!fix.fix) return;
|
||||
var p = m.latLonToXY(fix.lat, fix.lon);
|
||||
|
@ -22,50 +33,70 @@ function drawMarker() {
|
|||
g.fillRect(p.x-2, p.y-2, p.x+2, p.y+2);
|
||||
}
|
||||
|
||||
var fix;
|
||||
Bangle.on('GPS',function(f) {
|
||||
fix=f;
|
||||
g.reset().clearRect(0,y1,g.getWidth()-1,y1+8).setFont("6x8").setFontAlign(0,0);
|
||||
var txt = fix.satellites+" satellites";
|
||||
if (!fix.fix)
|
||||
txt += " - NO FIX";
|
||||
g.drawString(txt,g.getWidth()/2,y1 + 4);
|
||||
drawMarker();
|
||||
if (HASWIDGETS) WIDGETS["sats"].draw(WIDGETS["sats"]);
|
||||
if (mapVisible) drawMarker();
|
||||
});
|
||||
Bangle.setGPSPower(1, "app");
|
||||
|
||||
if (HASWIDGETS) {
|
||||
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();
|
||||
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 recenter() {
|
||||
if (!fix.fix) return;
|
||||
m.lat = fix.lat;
|
||||
m.lon = fix.lon;
|
||||
function showMap() {
|
||||
mapVisible = true;
|
||||
g.reset().clearRect(R);
|
||||
redraw();
|
||||
Bangle.setUI({mode:"custom",drag:e=>{
|
||||
if (e.b) {
|
||||
g.setClipRect(R.x,R.y,R.x2,R.y2);
|
||||
g.scroll(e.dx,e.dy);
|
||||
m.scroll(e.dx,e.dy);
|
||||
g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
|
||||
hasScrolled = true;
|
||||
} else if (hasScrolled) {
|
||||
hasScrolled = false;
|
||||
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);
|
||||
}});
|
||||
}
|
||||
|
||||
setWatch(recenter, global.BTN2?BTN2:BTN1, {repeat:true});
|
||||
|
||||
var hasScrolled = false;
|
||||
Bangle.on('drag',e=>{
|
||||
if (e.b) {
|
||||
g.setClipRect(0,y1,g.getWidth()-1,y2);
|
||||
g.scroll(e.dx,e.dy);
|
||||
m.scroll(e.dx,e.dy);
|
||||
g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
|
||||
hasScrolled = true;
|
||||
} else if (hasScrolled) {
|
||||
hasScrolled = false;
|
||||
redraw();
|
||||
}
|
||||
});
|
||||
showMap();
|
||||
|
|
|
@ -9,7 +9,8 @@
|
|||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
html, body, #map {
|
||||
html, body, #map, #mapsLoaded, #mapContainer {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
@ -27,20 +28,40 @@
|
|||
width: 256px;
|
||||
height: 256px;
|
||||
}
|
||||
.tile-title {
|
||||
font-weight:bold;
|
||||
font-size: 125%;
|
||||
}
|
||||
.tile-map {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="map">
|
||||
<div id="mapsLoadedContainer">
|
||||
</div>
|
||||
<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>
|
||||
<button id="getmap" class="btn btn-primary">Get Map</button><br/>
|
||||
<canvas id="maptiles" style="display:none"></canvas>
|
||||
<div id="uploadbuttons" style="display:none"><button id="upload" class="btn btn-primary">Upload</button>
|
||||
<button id="cancel" class="btn">Cancel</button></div>
|
||||
<div id="mapContainer">
|
||||
<div id="map">
|
||||
</div>
|
||||
<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 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>
|
||||
<div id="uploadbuttons" style="display:none"><button id="upload" class="btn btn-primary">Upload</button>
|
||||
<button id="cancel" class="btn">Cancel</button></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://code.jquery.com/jquery-3.2.1.min.js"></script>
|
||||
<script src="../../webtools/heatshrink.js"></script>
|
||||
|
@ -60,8 +81,6 @@ TODO:
|
|||
*/
|
||||
var TILESIZE = 96; // Size of our 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/
|
||||
However some don't allow cross-origin use */
|
||||
//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 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 map = L.map('map').locate({setView: true, maxZoom: 16, enableHighAccuracy:true});
|
||||
var loadedMaps = [];
|
||||
|
||||
// Tiles used for Bangle.js itself
|
||||
var bangleTileLayer = L.tileLayer(TILELAYER, {
|
||||
maxZoom: 18,
|
||||
|
@ -83,6 +102,10 @@ TODO:
|
|||
});
|
||||
// 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:
|
||||
const searchProvider = new window.GeoSearch.OpenStreetMapProvider();
|
||||
const searchControl = new GeoSearch.GeoSearchControl({
|
||||
|
@ -96,6 +119,7 @@ TODO:
|
|||
});
|
||||
map.addControl(searchControl);
|
||||
|
||||
// ---------------------------------------- Run at startup
|
||||
function onInit(device) {
|
||||
if (device && device.info && device.info.g) {
|
||||
// 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";
|
||||
}
|
||||
}
|
||||
|
||||
showLoadedMaps();
|
||||
}
|
||||
|
||||
var mapFiles = [];
|
||||
previewTileLayer.addTo(map);
|
||||
function showLoadedMaps() {
|
||||
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 = {
|
||||
compression:false, output:"raw",
|
||||
mode:"web"
|
||||
|
@ -118,6 +250,7 @@ TODO:
|
|||
options = {
|
||||
compression:false, output:"raw",
|
||||
mode:"3bit",
|
||||
diffusion:"bayer2"
|
||||
};
|
||||
/* If in 3 bit mode, go through all the data beforehand and
|
||||
turn the saturation up to maximum, so when thresholded it
|
||||
|
@ -166,12 +299,17 @@ TODO:
|
|||
}
|
||||
}
|
||||
return [{
|
||||
name:"openstmap.0.img",
|
||||
name:mapImageFile,
|
||||
content:tiledImage
|
||||
}];
|
||||
}
|
||||
|
||||
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 centerlatlon = map.getBounds().getCenter();
|
||||
var center = map.project(centerlatlon, zoom).divideBy(OSMTILESIZE);
|
||||
|
@ -242,8 +380,11 @@ TODO:
|
|||
|
||||
Promise.all(tileGetters).then(() => {
|
||||
document.getElementById("uploadbuttons").style.display="";
|
||||
mapFiles = tilesLoaded(ctx, canvas.width, canvas.height);
|
||||
mapFiles.unshift({name:"openstmap.0.json",content:JSON.stringify({
|
||||
var mapNumber = 0;
|
||||
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,
|
||||
imgy : canvas.height,
|
||||
tilesize : TILESIZE,
|
||||
|
@ -252,21 +393,31 @@ TODO:
|
|||
lon : centerlatlon.lng,
|
||||
w : Math.round(canvas.width / TILESIZE), // width in tiles
|
||||
h : Math.round(canvas.height / TILESIZE), // height in tiles
|
||||
fn : "openstmap.0.img"
|
||||
fn : mapImageFile
|
||||
})});
|
||||
console.log(mapFiles);
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById("upload").addEventListener("click", function() {
|
||||
sendCustomizedApp({
|
||||
storage:mapFiles
|
||||
Util.showModal("Uploading...");
|
||||
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("maptiles").style.display="none";
|
||||
document.getElementById("uploadbuttons").style.display="none";
|
||||
showMap();
|
||||
});
|
||||
|
||||
</script>
|
|
@ -2,17 +2,21 @@
|
|||
"id": "openstmap",
|
||||
"name": "OpenStreetMap",
|
||||
"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",
|
||||
"readme": "README.md",
|
||||
"icon": "app.png",
|
||||
"tags": "outdoors,gps,osm",
|
||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||
"screenshots": [{"url":"screenshot.png"}],
|
||||
"custom": "custom.html",
|
||||
"customConnect": true,
|
||||
"interface": "interface.html",
|
||||
"storage": [
|
||||
{"name":"openstmap","url":"openstmap.js"},
|
||||
{"name":"openstmap.app.js","url":"app.js"},
|
||||
{"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();
|
||||
}
|
||||
|
||||
// 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;
|
||||
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() {
|
||||
var img = require("Storage").read(map.fn);
|
||||
var cx = g.getWidth()/2;
|
||||
var cy = g.getHeight()/2;
|
||||
var p = Bangle.project({lat:m.lat,lon:m.lon});
|
||||
var ix = (p.x-map.center.x)/map.scale + (map.imgx/2) - cx;
|
||||
var iy = (map.center.y-p.y)/map.scale + (map.imgy/2) - cy;
|
||||
//console.log(ix,iy);
|
||||
var tx = 0|(ix/map.tilesize);
|
||||
var ty = 0|(iy/map.tilesize);
|
||||
var ox = (tx*map.tilesize)-ix;
|
||||
var oy = (ty*map.tilesize)-iy;
|
||||
for (var x=ox,ttx=tx;x<g.getWidth();x+=map.tilesize,ttx++)
|
||||
for (var y=oy,tty=ty;y<g.getHeight();y+=map.tilesize,tty++) {
|
||||
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);
|
||||
m.maps.forEach((map,idx) => {
|
||||
var d = map.scale/m.scale;
|
||||
var ix = (p.x-map.center.x)/m.scale + (map.imgx*d/2) - cx;
|
||||
var iy = (map.center.y-p.y)/m.scale + (map.imgy*d/2) - cy;
|
||||
var o = {};
|
||||
var s = map.tilesize;
|
||||
if (d!=1) { // if the two are different, add scaling
|
||||
s *= d;
|
||||
o.scale = d;
|
||||
}
|
||||
//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
|
||||
|
@ -55,15 +82,15 @@ exports.latLonToXY = function(lat, lon) {
|
|||
var cx = g.getWidth()/2;
|
||||
var cy = g.getHeight()/2;
|
||||
return {
|
||||
x : (q.x-p.x)/map.scale + cx,
|
||||
y : cy - (q.y-p.y)/map.scale
|
||||
x : (q.x-p.x)/m.scale + cx,
|
||||
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
|
||||
exports.scroll = function(x,y) {
|
||||
var a = Bangle.project({lat:this.lat,lon:this.lon});
|
||||
var b = Bangle.project({lat:this.lat+1,lon:this.lon+1});
|
||||
this.lon += x * this.map.scale / (a.x-b.x);
|
||||
this.lat -= y * this.map.scale / (a.y-b.y);
|
||||
var a = Bangle.project({lat:m.lat,lon:m.lon});
|
||||
var b = Bangle.project({lat:m.lat+1,lon:m.lon+1});
|
||||
this.lon += x * m.scale / (a.x-b.x);
|
||||
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.16: Ability to append to existing track (fix #1712)
|
||||
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",
|
||||
"name": "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.",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,outdoors,gps,widget",
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
(() => {
|
||||
var storageFile; // file for GPS track
|
||||
var entriesWritten = 0;
|
||||
var activeRecorders = [];
|
||||
var writeInterval;
|
||||
{
|
||||
let storageFile; // file for GPS track
|
||||
let entriesWritten = 0;
|
||||
let activeRecorders = [];
|
||||
let writeInterval;
|
||||
|
||||
function loadSettings() {
|
||||
let loadSettings = function() {
|
||||
var settings = require("Storage").readJSON("recorder.json",1)||{};
|
||||
settings.period = settings.period||10;
|
||||
if (!settings.file || !settings.file.startsWith("recorder.log"))
|
||||
|
@ -12,12 +12,12 @@
|
|||
return settings;
|
||||
}
|
||||
|
||||
function updateSettings(settings) {
|
||||
let updateSettings = function(settings) {
|
||||
require("Storage").writeJSON("recorder.json", settings);
|
||||
if (WIDGETS["recorder"]) WIDGETS["recorder"].reload();
|
||||
}
|
||||
|
||||
function getRecorders() {
|
||||
let getRecorders = function() {
|
||||
var recorders = {
|
||||
gps:function() {
|
||||
var lat = 0;
|
||||
|
@ -159,7 +159,7 @@
|
|||
return recorders;
|
||||
}
|
||||
|
||||
function writeLog() {
|
||||
let writeLog = function() {
|
||||
entriesWritten++;
|
||||
WIDGETS["recorder"].draw();
|
||||
try {
|
||||
|
@ -178,7 +178,7 @@
|
|||
}
|
||||
|
||||
// Called by the GPS app to reload settings and decide what to do
|
||||
function reload() {
|
||||
let reload = function() {
|
||||
var settings = loadSettings();
|
||||
if (writeInterval) clearInterval(writeInterval);
|
||||
writeInterval = undefined;
|
||||
|
@ -224,7 +224,7 @@
|
|||
// add the widget
|
||||
WIDGETS["recorder"]={area:"tl",width:0,draw:function() {
|
||||
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)=>{
|
||||
recorder.draw(this.x+15+(i>>1)*12, this.y+(i&1)*12);
|
||||
});
|
||||
|
@ -265,23 +265,39 @@
|
|||
updateSettings(settings);
|
||||
WIDGETS["recorder"].reload();
|
||||
return Promise.resolve(settings.recording);
|
||||
}/*,plotTrack:function(m) { // m=instance of openstmap module
|
||||
// if we're here, settings was already loaded
|
||||
var f = require("Storage").open(settings.file,"r");
|
||||
var l = f.readLine(f);
|
||||
if (l===undefined) return;
|
||||
var c = l.split(",");
|
||||
var mp = m.latLonToXY(+c[1], +c[2]);
|
||||
g.moveTo(mp.x,mp.y);
|
||||
l = f.readLine(f);
|
||||
while(l!==undefined) {
|
||||
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
|
||||
},plotTrack:function(m) { // m=instance of openstmap module
|
||||
// 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 l = f.readLine();
|
||||
if (l===undefined) return; // empty file?
|
||||
var mp, c = l.split(",");
|
||||
var la=c.indexOf("Latitude"),lo=c.indexOf("Longitude");
|
||||
if (la<0 || lo<0) return; // no GPS!
|
||||
l = f.readLine();c=[];
|
||||
while (l && !c[la]) {
|
||||
c = l.split(",");
|
||||
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
|
||||
reload();
|
||||
})()
|
||||
}
|
||||
|
|
|
@ -14,3 +14,4 @@
|
|||
Improve timer message using formatDuration
|
||||
Fix wrong fallback for buzz pattern
|
||||
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",
|
||||
"name": "Scheduler",
|
||||
"version": "0.13",
|
||||
"version": "0.14",
|
||||
"description": "Scheduling library for alarms and timers",
|
||||
"icon": "app.png",
|
||||
"type": "scheduler",
|
||||
|
@ -13,7 +13,8 @@
|
|||
{"name":"sched.js","url":"sched.js"},
|
||||
{"name":"sched.img","url":"app-icon.js","evaluate":true},
|
||||
{"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"}]
|
||||
}
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
0.01: New App!
|
||||
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;
|
||||
y = R.y + R.h / 2 - 12; // 12 = room for date
|
||||
var date = new Date();
|
||||
var hourStr = date.getHours();
|
||||
var minStr = date.getMinutes().toString().padStart(2,0);
|
||||
var local_time = require("locale").time(date, 1);
|
||||
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()+ " "+
|
||||
require("locale").date(date, 0).toUpperCase();
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{ "id": "slopeclock",
|
||||
"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",
|
||||
"icon": "app.png",
|
||||
"screenshots": [{"url":"screenshot.png"}],
|
||||
|
|
|
@ -4,3 +4,5 @@
|
|||
0.04: Changed to use clock_info for displayed data
|
||||
Made fonts smaller to avoid overlap when (eg) 22:00
|
||||
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;
|
||||
y = R.y + R.h / 2 - 12; // 12 = room for date
|
||||
var date = new Date();
|
||||
var hourStr = date.getHours();
|
||||
var minStr = date.getMinutes().toString().padStart(2,0);
|
||||
var local_time = require("locale").time(date, 1);
|
||||
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()+ " "+
|
||||
require("locale").date(date, 0).toUpperCase();
|
||||
|
||||
|
@ -121,18 +122,18 @@ let animate = function(isIn, callback) {
|
|||
|
||||
// clock info menus (scroll up/down for info)
|
||||
let clockInfoDraw = (itm, info, options) => {
|
||||
let texty = options.y+26;
|
||||
g.reset().setFont("6x15").setBgColor(options.bg).setColor(options.fg).clearRect(options.x, texty, options.x+options.w-2, texty+15);
|
||||
let texty = options.y+41;
|
||||
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.x < g.getWidth()/2) { // left align
|
||||
let x = options.x+2;
|
||||
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);
|
||||
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);
|
||||
} else { // right align
|
||||
let x = options.x+options.w-3;
|
||||
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);
|
||||
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);
|
||||
}
|
||||
};
|
||||
let clockInfoItems = require("clock_info").load();
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{ "id": "slopeclockpp",
|
||||
"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.",
|
||||
"icon": "app.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 |
|
@ -13,4 +13,5 @@
|
|||
0.14: Use weather condition code for icon selection
|
||||
0.15: Fix widget icon
|
||||
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: "?",
|
||||
};
|
||||
|
||||
var weatherJson = storage.readJSON('weather.json');
|
||||
var weatherJson = require("Storage").readJSON('weather.json');
|
||||
if(weatherJson !== undefined && weatherJson.weather !== undefined){
|
||||
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.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";
|
||||
}
|
||||
|
||||
//FIXME ranges are somehow arbitrary
|
||||
var weatherItems = {
|
||||
name: "Weather",
|
||||
img: atob("GBiBAf+///u5//n7//8f/9wHP8gDf/gB//AB/7AH/5AcP/AQH/DwD/uAD84AD/4AA/wAAfAAAfAAAfAAAfgAA/////+bP/+zf/+zfw=="),
|
||||
items: [
|
||||
{
|
||||
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"); },
|
||||
hide: function () {}
|
||||
},
|
||||
{
|
||||
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"); },
|
||||
hide: function () {}
|
||||
},
|
||||
{
|
||||
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"); },
|
||||
hide: function () {}
|
||||
},
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "weather",
|
||||
"name": "Weather",
|
||||
"version": "0.17",
|
||||
"version": "0.18",
|
||||
"description": "Show Gadgetbridge weather report",
|
||||
"icon": "icon.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.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`
|
||||
|
||||
## 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.
|
||||
<!-- * `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",
|
||||
"name": "Messages Grid Widget",
|
||||
"version": "0.02",
|
||||
"description": "Widget that display notification icons in a grid",
|
||||
"version": "0.03",
|
||||
"description": "Widget that displays notification icons in a grid",
|
||||
"icon": "widget.png",
|
||||
"type": "widget",
|
||||
"dependencies": {"messages":"app"},
|
||||
"tags": "tool,system",
|
||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||
"dependencies" : { "messageicons":"module" },
|
||||
"dependencies" : { "messages":"module" },
|
||||
"provides_modules" : ["messagewidget"],
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name":"messagewidget","url":"lib.js"},
|
||||
{"name":"widmsggrid.wid.js","url":"widget.js"}
|
||||
],
|
||||
"screenshots": [{"url":"screenshot.png"}]
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
clearTimeout(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?
|
||||
// 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
|
||||
|
@ -57,9 +57,10 @@
|
|||
.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
|
||||
}, show: function () {
|
||||
}, show: function (m) {
|
||||
delete w.hidden;
|
||||
w.width = 24;
|
||||
w.srcs = require("messages").getMessages()
|
||||
w.srcs = require("messages").getMessages(m)
|
||||
.filter(m => !['call', 'map', 'music'].includes(m.id))
|
||||
.filter(m => m.new || w.showRead)
|
||||
.map(m => m.src);
|
||||
|
@ -68,6 +69,7 @@
|
|||
Bangle.drawWidgets();
|
||||
Bangle.setLCDPower(1); // turns screen on
|
||||
}, hide: function () {
|
||||
w.hidden = true;
|
||||
w.width = 0;
|
||||
w.srcs = [];
|
||||
w.total = 0;
|
||||
|
@ -82,13 +84,16 @@
|
|||
}
|
||||
// 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;
|
||||
load("messages.app.js");
|
||||
}, listener: function () {
|
||||
w.status = require("messages").status();
|
||||
if (w.status === "new" || (w.status === "old" && w.showRead)) w.show();
|
||||
require("messages").openGUI();
|
||||
}, listener: function (t,m) {
|
||||
if (this.hidden) return;
|
||||
w.status = require("messages").status(m);
|
||||
if (w.status === "new" || (w.status === "old" && w.showRead)) w.show(m);
|
||||
else w.hide();
|
||||
delete w.hidden; // always set by w.hide(), but we checked it wasn't there before
|
||||
}
|
||||
};
|
||||
delete s;
|
||||
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 */
|
||||
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) {
|
||||
|
|
2
core
|
@ -1 +1 @@
|
|||
Subproject commit db08367e0a2c25040449a4b556eaed459e8f47fc
|
||||
Subproject commit f15e99fbe25b2991719011e6da9bc9c7be401a7e
|