Merge branch 'espruino:master' into sleeplog

pull/2322/head
storm64 2022-11-28 21:29:27 +01:00 committed by GitHub
commit c238a783ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
102 changed files with 2161 additions and 576 deletions

View File

@ -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
![](600.png)

View File

@ -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"],

View File

@ -5,3 +5,5 @@
0.05: Displaying calendar colour and name
0.06: Added clkinfo for clocks.
0.07: Clkinfo improvements.
0.08: Fix error in clkinfo (didn't require Storage & locale)
Fix clkinfo icon

View File

@ -1,24 +1,24 @@
(function() {
var agendaItems = {
name: "Agenda",
img: atob("GBiBAf////////85z/AAAPAAAPgAAP////AAAPAAAPAAAPAAAOAAAeAAAeAAAcAAA8AAAoAABgAADP//+P//8PAAAPAAAPgAAf///w=="),
img: atob("GBiBAAAAAAAAAADGMA///w///wf//wAAAA///w///w///w///x///h///h///j///D///X//+f//8wAABwAADw///w///wf//gAAAA=="),
items: []
};
var locale = require("locale");
var now = new Date();
var agenda = storage.readJSON("android.calendar.json")
var agenda = require("Storage").readJSON("android.calendar.json")
.filter(ev=>ev.timestamp + ev.durationInSeconds > now/1000)
.sort((a,b)=>a.timestamp - b.timestamp);
agenda.forEach((entry, i) => {
var title = entry.title.slice(0,18);
var title = entry.title.slice(0,12);
var date = new Date(entry.timestamp*1000);
var dateStr = locale.date(date).replace(/\d\d\d\d/,"");
dateStr += entry.durationInSeconds < 86400 ? "/ " + locale.time(date,1) : "";
agendaItems.items.push({
name: null,
name: "Agenda "+i,
get: () => ({ text: title + "\n" + dateStr, img: null}),
show: function() { agendaItems.items[i].emit("redraw"); },
hide: function () {}

View File

@ -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"}],

View File

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

View File

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

View File

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

View File

@ -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": [

View File

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

View File

@ -1 +1,2 @@
0.01: New App!
0.02: Add lightning

View File

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

View File

@ -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"}
]
}

36
apps/f9lander/settings.js Normal file
View File

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

View File

@ -2,3 +2,4 @@
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.05: Use `messages` library to open message GUI

View File

@ -234,7 +234,7 @@ function handleMessages()
{
if(!hasMessages()) return;
E.showMessage("Loading Messages...");
load("messages.app.js");
require("messages").openGUI();
}
function hasMessages()

View File

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

11
apps/hrmmar/README.md Normal file
View File

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

BIN
apps/hrmmar/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

40
apps/hrmmar/boot.js Normal file
View File

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

190
apps/hrmmar/fftelim.js Normal file
View File

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

18
apps/hrmmar/metadata.json Normal file
View File

@ -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"}]
}

26
apps/hrmmar/settings.js Normal file
View File

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

View File

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

View File

@ -1,2 +1,2 @@
// Config app not implemented yet
setTimeout(()=>load("messages.app.js"),10);
setTimeout(()=>require("messages").openGUI(),10);

View File

@ -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": [

View File

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

View File

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

View File

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

79
apps/messagegui/ChangeLog Normal file
View File

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

68
apps/messagegui/README.md Normal file
View File

@ -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_
![](screenshot.png)
## 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

View File

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

View File

@ -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(() => {

BIN
apps/messagegui/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

3
apps/messagegui/boot.js Normal file
View File

@ -0,0 +1,3 @@
(function() {
Bangle.on("message", (type, msg) => require("messagegui").listener(type, msg));
})();

60
apps/messagegui/lib.js Normal file
View File

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

View File

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

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

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

View File

@ -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_
![](screenshot.png)
_2. What the notify icon looks like (it's touchable on Bangle.js2!)_
![](screenshot-notify.gif)
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

View File

@ -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);
}
// combine musicinfo and musicstate events
if (event.id==="music") {
if (event.state==="play") event.new = true; // new track, or playback (re)started
event = Object.assign(exports.music, event);
}
}
if (mIdx<0) {
mIdx=0;
messages.unshift(event); // add new messages to the beginning
// 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);
}
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';
}
}
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
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);

View File

@ -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"}]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>
@ -69,14 +70,18 @@
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>

View File

@ -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"}
],

View File

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

53
apps/openstmap/README.md Normal file
View File

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

View File

@ -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);
// 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,44 +33,33 @@ 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();
}
setWatch(recenter, global.BTN2?BTN2:BTN1, {repeat:true});
var hasScrolled = false;
Bangle.on('drag',e=>{
Bangle.setUI({mode:"custom",drag:e=>{
if (e.b) {
g.setClipRect(0,y1,g.getWidth()-1,y2);
g.setClipRect(R.x,R.y,R.x2,R.y2);
g.scroll(e.dx,e.dy);
m.scroll(e.dx,e.dy);
g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
@ -68,4 +68,35 @@ Bangle.on('drag',e=>{
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);
}});
}
showMap();

View File

@ -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="mapsLoadedContainer">
</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>
<button id="getmap" class="btn btn-primary">Get Map</button><br/>
<div class="form-group" style="display:inline-block;">
<select class="form-select" id="mapSize">
<option value="4">Small</option>
<option value="5" selected>Medium</option>
<option value="6">Large</option>
<option value="7">XL</option>
</select>
</div>
<button id="getmap" class="btn btn-primary">Get Map</button><button class="btn" onclick="showLoadedMaps()">Map List</button><br/>
<canvas id="maptiles" style="display:none"></canvas>
<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>

View File

@ -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"}
]
}

View File

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

1
apps/qcenter/ChangeLog Normal file
View File

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

20
apps/qcenter/README.md Normal file
View File

@ -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.
![](screenshot.png)
## 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

1
apps/qcenter/app-icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4UB6cA/4ACBYNVAElQHAsFBYZFHCxIYEoALHgILNOxILChWqAAmgBYNUBZMVBYIAIBc0C1WAlWoAgQL/O96D/Qf4LZqoLJqoLMoAKHgILNqALHgoLBGBAKCDA4WDAEQA="))

120
apps/qcenter/app.js Normal file
View File

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

BIN
apps/qcenter/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 B

View File

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

BIN
apps/qcenter/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

141
apps/qcenter/settings.js Normal file
View File

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

View File

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

View File

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

View File

@ -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
},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(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) {
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(",");
mp = m.latLonToXY(+c[1], +c[2]);
g.lineTo(mp.x,mp.y);
g.fillCircle(mp.x,mp.y,2); // make the track more visible
l = f.readLine(f);
}
}*/};
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();
})()
}

View File

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

73
apps/sched/clkinfo.js Normal file
View File

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

View File

@ -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"}]
}

View File

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

View File

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

View File

@ -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"}],

View File

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

View File

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

View File

@ -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"}],

8
apps/tetris/README.md Normal file
View File

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

1
apps/tetris/app-icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+If4A/AH4A/AH4A/ABe5AA0jABwvYAIovBgABEFAQHFL7IuEL4QuFA45fcF4YuNL7i/FFwoHHL7QvFFxpfaF4wAOF/4nHF5+0AAy3SXYoHGW4QBDF4MAAIgvRFwwHHdAbqDFIQuDL6ouJL4ovDFwpfUAAoHFL4a/FFwhfTFxZfDF4ouFL6QANFopfDF/4vNjwAGF8ABFF4MAAIgvBX4IBDX4YBDL6TyFFIIuEL4QuEL4QuEL6ovDFwpfFF4YuFL6i/FFwhfEX4ouEL6YvFFwpfDF4ouFL6QvGAAwtFL4Yv/AAonHAB4vHG563CAIbuDA5i/CAIb2DA4hfJEwoHPFApZEGwpfLFyJfFFxJfMAAoHNFAa5GX54uTL4YuLL5QAVFowAIF+4A/AH4A/AH4A/AHY"))

14
apps/tetris/metadata.json Normal file
View File

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

170
apps/tetris/tetris.app.js Normal file
View File

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

BIN
apps/tetris/tetris.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 B

View File

@ -14,3 +14,4 @@
0.15: Fix widget icon
0.16: Don't mark app as clock
0.17: Added clkinfo for clocks.
0.18: Added hasRange to clkinfo.

View File

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

View File

@ -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"}],

View File

@ -0,0 +1 @@
0.01: Moved messages widget into standalone widget app

View File

@ -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`.
![screenshot](screenshot.gif)
## 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

BIN
apps/widmessages/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

8
apps/widmessages/lib.js Normal file
View File

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

View File

@ -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"}
]
}

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

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

View File

@ -1,2 +1,3 @@
0.01: New widget!
0.02: Adjust to message icons moving to messageicons lib
0.03: Use new message library

View File

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

8
apps/widmsggrid/lib.js Normal file
View File

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

View File

@ -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"}]

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More