Merge remote-tracking branch 'upstream/master'
|
@ -11,3 +11,4 @@
|
|||
0.11: Bangle.js2: New pixels, btn1 to exit
|
||||
0.12: Actual pixels as of 29th Nov 2021
|
||||
0.13: Bangle.js 2: Use setUI to add software back button
|
||||
0.14: Add automatic translation of more strings
|
||||
|
|
|
@ -11,8 +11,8 @@ g.drawString("BANGLEJS.COM",120,y-4);
|
|||
} else {
|
||||
y=-(4+h); // small screen, start right at top
|
||||
}
|
||||
g.drawString("Powered by Espruino",0,y+=4+h);
|
||||
g.drawString("Version "+ENV.VERSION,0,y+=h);
|
||||
g.drawString(/*LANG*/"Powered by Espruino",0,y+=4+h);
|
||||
g.drawString(/*LANG*/"Version "+ENV.VERSION,0,y+=h);
|
||||
g.drawString("Commit "+ENV.GIT_COMMIT,0,y+=h);
|
||||
function getVersion(name,file) {
|
||||
var j = s.readJSON(file,1);
|
||||
|
@ -24,9 +24,9 @@ getVersion("Launcher","launch.info");
|
|||
getVersion("Settings","setting.info");
|
||||
|
||||
y+=h;
|
||||
g.drawString(MEM.total+" JS Variables available",0,y+=h);
|
||||
g.drawString("Storage: "+(require("Storage").getFree()>>10)+"k free",0,y+=h);
|
||||
if (ENV.STORAGE) g.drawString(" "+(ENV.STORAGE>>10)+"k total",0,y+=h);
|
||||
g.drawString(MEM.total+/*LANG*/" JS Variables available",0,y+=h);
|
||||
g.drawString("Storage: "+(require("Storage").getFree()>>10)+/*LANG*/"k free",0,y+=h);
|
||||
if (ENV.STORAGE) g.drawString(" "+(ENV.STORAGE>>10)+/*LANG*/"k total",0,y+=h);
|
||||
if (ENV.SPIFLASH) g.drawString("SPI Flash: "+(ENV.SPIFLASH>>10)+"k",0,y+=h);
|
||||
g.setFontAlign(0,-1);
|
||||
g.flip();
|
||||
|
|
|
@ -35,17 +35,17 @@ function drawInfo() {
|
|||
g.setFont("4x6").setFontAlign(0,0).drawString("BANGLEJS.COM",W-30,56);
|
||||
var h=8, y = 24-h;
|
||||
g.setFont("6x8").setFontAlign(-1,-1);
|
||||
g.drawString("Powered by Espruino",0,y+=4+h);
|
||||
g.drawString("Version "+ENV.VERSION,0,y+=h);
|
||||
g.drawString(/*LANG*/"Powered by Espruino",0,y+=4+h);
|
||||
g.drawString(/*LANG*/"Version "+ENV.VERSION,0,y+=h);
|
||||
g.drawString("Commit "+ENV.GIT_COMMIT,0,y+=h);
|
||||
|
||||
getVersion("Bootloader","boot.info");
|
||||
getVersion("Launcher","launch.info");
|
||||
getVersion("Settings","setting.info");
|
||||
|
||||
g.drawString(MEM.total+" JS Vars",0,y+=h);
|
||||
g.drawString("Storage: "+(require("Storage").getFree()>>10)+"k free",0,y+=h);
|
||||
if (ENV.STORAGE) g.drawString(" "+(ENV.STORAGE>>10)+"k total",0,y+=h);
|
||||
g.drawString(MEM.total+/*LANG*/" JS Vars",0,y+=h);
|
||||
g.drawString("Storage: "+(require("Storage").getFree()>>10)+/*LANG*/"k free",0,y+=h);
|
||||
if (ENV.STORAGE) g.drawString(" "+(ENV.STORAGE>>10)+/*LANG*/"k total",0,y+=h);
|
||||
if (ENV.SPIFLASH) g.drawString("SPI Flash: "+(ENV.SPIFLASH>>10)+"k",0,y+=h);
|
||||
imageTop = y+h;
|
||||
imgScroll = imgHeight-imageTop;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "about",
|
||||
"name": "About",
|
||||
"version": "0.13",
|
||||
"version": "0.14",
|
||||
"description": "Bangle.js About page - showing software version, stats, and a collaborative mural from the Bangle.js KickStarter backers",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,system",
|
||||
|
|
|
@ -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
|
||||

|
||||
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
0.01: New app!
|
||||
0.02: Design improvements and fixes.
|
||||
0.03: Indicate battery level through line occurrence.
|
||||
0.04: Use widget_utils module.
|
||||
|
|
|
@ -215,8 +215,7 @@ Bangle.loadWidgets();
|
|||
* so we will blank out the draw() functions of each widget and change the
|
||||
* area to the top bar doesn't get cleared.
|
||||
*/
|
||||
for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
|
||||
|
||||
require('widget_utils').hide();
|
||||
// Clear the screen once, at startup and draw clock
|
||||
g.setTheme({bg:"#fff",fg:"#000",dark:false}).clear();
|
||||
draw();
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"name": "AI Clock",
|
||||
"shortName":"AI Clock",
|
||||
"icon": "aiclock.png",
|
||||
"version":"0.03",
|
||||
"version":"0.04",
|
||||
"readme": "README.md",
|
||||
"supports": ["BANGLEJS2"],
|
||||
"description": "A watch face that was designed by an AI (stable diffusion) and implemented by a human.",
|
||||
|
|
|
@ -34,4 +34,6 @@
|
|||
0.32: Fix wrong hidden filter
|
||||
Add option for auto-delete a timer after it expires
|
||||
0.33: Allow hiding timers&alarms
|
||||
0.34: Add "Confirm" option to alarm/timer edit menus
|
||||
0.35: Add automatic translation of more strings
|
||||
|
||||
|
|
|
@ -128,7 +128,12 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex) {
|
|||
value: alarm.hidden || false,
|
||||
onchange: v => alarm.hidden = v
|
||||
},
|
||||
/*LANG*/"Cancel": () => showMainMenu()
|
||||
/*LANG*/"Cancel": () => showMainMenu(),
|
||||
/*LANG*/"Confirm": () => {
|
||||
prepareAlarmForSave(alarm, alarmIndex, time);
|
||||
saveAndReload();
|
||||
showMainMenu();
|
||||
}
|
||||
};
|
||||
|
||||
if (!isNew) {
|
||||
|
@ -178,7 +183,7 @@ function decodeDOW(alarm) {
|
|||
.map((day, index) => alarm.dow & (1 << (index + firstDayOfWeek)) ? day : "_")
|
||||
.join("")
|
||||
.toLowerCase()
|
||||
: "Once"
|
||||
: /*LANG*/"Once"
|
||||
}
|
||||
|
||||
function showEditRepeatMenu(repeat, dow, dowChangeCallback) {
|
||||
|
@ -293,7 +298,12 @@ function showEditTimerMenu(selectedTimer, timerIndex) {
|
|||
onchange: v => timer.hidden = v
|
||||
},
|
||||
/*LANG*/"Vibrate": require("buzz_menu").pattern(timer.vibrate, v => timer.vibrate = v),
|
||||
/*LANG*/"Cancel": () => showMainMenu()
|
||||
/*LANG*/"Cancel": () => showMainMenu(),
|
||||
/*LANG*/"Confirm": () => {
|
||||
prepareTimerForSave(timer, timerIndex, time);
|
||||
saveAndReload();
|
||||
showMainMenu();
|
||||
}
|
||||
};
|
||||
|
||||
if (!isNew) {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "alarm",
|
||||
"name": "Alarms & Timers",
|
||||
"shortName": "Alarms",
|
||||
"version": "0.33",
|
||||
"version": "0.35",
|
||||
"description": "Set alarms and timers on your Bangle",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,alarm,widget",
|
||||
|
|
|
@ -15,3 +15,5 @@
|
|||
0.15: Allow method/body/headers to be specified for `http` (needs Gadgetbridge 0.68.0b or later)
|
||||
0.16: Bangle.http now fails immediately if there is no Bluetooth connection (fix #2152)
|
||||
0.17: Now kick off Calendar sync as soon as connected to Gadgetbridge
|
||||
0.18: Use new message library
|
||||
If connected to Gadgetbridge, allow GPS forwarding from phone (Gadgetbridge code still not merged)
|
|
@ -20,6 +20,8 @@ It contains:
|
|||
of Gadgetbridge - making your phone make noise so you can find it.
|
||||
* `Keep Msgs` - default is `Off`. When Gadgetbridge disconnects, should Bangle.js
|
||||
keep any messages it has received, or should it delete them?
|
||||
* `Overwrite GPS` - when GPS is requested by an app, this doesn't use Bangle.js's GPS
|
||||
but instead asks Gadgetbridge on the phone to use the phone's GPS
|
||||
* `Messages` - launches the messages app, showing a list of messages
|
||||
|
||||
## How it works
|
||||
|
|
|
@ -126,6 +126,18 @@
|
|||
request.j(event.err); //r = reJect function
|
||||
else
|
||||
request.r(event); //r = resolve function
|
||||
},
|
||||
"gps": function() {
|
||||
const settings = require("Storage").readJSON("android.settings.json",1)||{};
|
||||
if (!settings.overwriteGps) return;
|
||||
delete event.t;
|
||||
event.satellites = NaN;
|
||||
event.course = NaN;
|
||||
event.fix = 1;
|
||||
Bangle.emit('gps', event);
|
||||
},
|
||||
"is_gps_active": function() {
|
||||
gbSend({ t: "gps_power", status: Bangle._PWR && Bangle._PWR.GPS && Bangle._PWR.GPS.length>0 });
|
||||
}
|
||||
};
|
||||
var h = HANDLERS[event.t];
|
||||
|
@ -189,6 +201,30 @@
|
|||
if (isFinite(msg.id)) return gbSend({ t: "notify", n:response?"OPEN":"DISMISS", id: msg.id });
|
||||
// error/warn here?
|
||||
};
|
||||
// GPS overwrite logic
|
||||
if (settings.overwriteGps) { // if the overwrite option is set../
|
||||
// Save current logic
|
||||
const originalSetGpsPower = Bangle.setGPSPower;
|
||||
// Replace set GPS power logic to suppress activation of gps (and instead request it from the phone)
|
||||
Bangle.setGPSPower = (isOn, appID) => {
|
||||
// if not connected, use old logic
|
||||
if (!NRF.getSecurityStatus().connected) return originalSetGpsPower(isOn, appID);
|
||||
// Emulate old GPS power logic
|
||||
if (!Bangle._PWR) Bangle._PWR={};
|
||||
if (!Bangle._PWR.GPS) Bangle._PWR.GPS=[];
|
||||
if (!appID) appID="?";
|
||||
if (isOn && !Bangle._PWR.GPS.includes(appID)) Bangle._PWR.GPS.push(appID);
|
||||
if (!isOn && Bangle._PWR.GPS.includes(appID)) Bangle._PWR.GPS.splice(Bangle._PWR.GPS.indexOf(appID),1);
|
||||
let pwr = Bangle._PWR.GPS.length>0;
|
||||
gbSend({ t: "gps_power", status: pwr });
|
||||
return pwr;
|
||||
}
|
||||
// Replace check if the GPS is on to check the _PWR variable
|
||||
Bangle.isGPSOn = () => {
|
||||
return Bangle._PWR && Bangle._PWR.GPS && Bangle._PWR.GPS.length>0;
|
||||
}
|
||||
}
|
||||
|
||||
// remove settings object so it's not taking up RAM
|
||||
delete settings;
|
||||
})();
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
"id": "android",
|
||||
"name": "Android Integration",
|
||||
"shortName": "Android",
|
||||
"version": "0.17",
|
||||
"version": "0.18",
|
||||
"description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,system,messages,notifications,gadgetbridge",
|
||||
"dependencies": {"messages":"app"},
|
||||
"dependencies": {"messages":"module"},
|
||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
(function(back) {
|
||||
|
||||
|
||||
|
||||
function gb(j) {
|
||||
Bluetooth.println(JSON.stringify(j));
|
||||
}
|
||||
|
@ -23,7 +26,17 @@
|
|||
updateSettings();
|
||||
}
|
||||
},
|
||||
/*LANG*/"Messages" : ()=>load("messages.app.js"),
|
||||
/*LANG*/"Overwrite GPS" : {
|
||||
value : !!settings.overwriteGps,
|
||||
onchange: newValue => {
|
||||
if (newValue) {
|
||||
Bangle.setGPSPower(false, 'android');
|
||||
}
|
||||
settings.overwriteGps = newValue;
|
||||
updateSettings();
|
||||
}
|
||||
},
|
||||
/*LANG*/"Messages" : ()=>require("message").openGUI(),
|
||||
};
|
||||
E.showMenu(mainmenu);
|
||||
})
|
||||
|
|
|
@ -13,3 +13,4 @@
|
|||
0.13: Add font setting
|
||||
0.14: Use ClockFace_menu.addItems
|
||||
0.15: Add Power saving option
|
||||
0.16: Support Fast Loading
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
/* jshint esversion: 6 */
|
||||
/**
|
||||
{
|
||||
/**
|
||||
* A simple digital clock showing seconds as a bar
|
||||
**/
|
||||
// Check settings for what type our clock should be
|
||||
let locale = require("locale");
|
||||
{ // add some more info to locale
|
||||
let locale = require("locale");
|
||||
{ // add some more info to locale
|
||||
let date = new Date();
|
||||
date.setFullYear(1111);
|
||||
date.setMonth(1, 3); // februari: months are zero-indexed
|
||||
const localized = locale.date(date, true);
|
||||
locale.dayFirst = /3.*2/.test(localized);
|
||||
locale.hasMeridian = (locale.meridian(date)!=="");
|
||||
}
|
||||
}
|
||||
|
||||
let barW = 0, prevX = 0;
|
||||
function renderBar(l) {
|
||||
let barW = 0, prevX = 0;
|
||||
const renderBar = function (l) {
|
||||
"ram";
|
||||
if (l) prevX = 0; // called from Layout: drawing area was cleared
|
||||
else l = clock.layout.bar;
|
||||
|
@ -25,9 +26,9 @@ function renderBar(l) {
|
|||
if (x2<Math.max(0, prevX)) g.setBgColor(l.bgCol || g.theme.bg).clearRect(x2+1, l.y, prevX, l.y2);
|
||||
else g.setColor(l.col || g.theme.fg).fillRect(prevX+1, l.y, x2, l.y2);
|
||||
prevX = x2;
|
||||
}
|
||||
}
|
||||
|
||||
function timeText(date) {
|
||||
const timeText = function(date) {
|
||||
if (!clock.is12Hour) {
|
||||
return locale.time(date, true);
|
||||
}
|
||||
|
@ -39,19 +40,17 @@ function timeText(date) {
|
|||
date12.setHours(hours-12);
|
||||
}
|
||||
return locale.time(date12, true);
|
||||
}
|
||||
function ampmText(date) {
|
||||
return (clock.is12Hour && locale.hasMeridian) ? locale.meridian(date) : "";
|
||||
}
|
||||
function dateText(date) {
|
||||
}
|
||||
const ampmText = date => (clock.is12Hour && locale.hasMeridian) ? locale.meridian(date) : "";
|
||||
const dateText = date => {
|
||||
const dayName = locale.dow(date, true),
|
||||
month = locale.month(date, true),
|
||||
day = date.getDate();
|
||||
const dayMonth = locale.dayFirst ? `${day} ${month}` : `${month} ${day}`;
|
||||
return `${dayName} ${dayMonth}`;
|
||||
}
|
||||
};
|
||||
|
||||
const ClockFace = require("ClockFace"),
|
||||
const ClockFace = require("ClockFace"),
|
||||
clock = new ClockFace({
|
||||
precision: 1,
|
||||
settingsFile: "barclock.settings.json",
|
||||
|
@ -110,15 +109,20 @@ const ClockFace = require("ClockFace"),
|
|||
prevX = 0; // force redraw of bar
|
||||
this.layout.forgetLazyState();
|
||||
},
|
||||
remove: function() {
|
||||
if (this.onLock) Bangle.removeListener("lock", this.onLock);
|
||||
},
|
||||
});
|
||||
|
||||
// power saving: only update once a minute while locked, hide bar
|
||||
if (clock.powerSave) {
|
||||
Bangle.on("lock", lock => {
|
||||
// power saving: only update once a minute while locked, hide bar
|
||||
if (clock.powerSave) {
|
||||
clock.onLock = lock => {
|
||||
clock.precision = lock ? 60 : 1;
|
||||
clock.tick();
|
||||
renderBar(); // hide/redraw bar right away
|
||||
});
|
||||
}
|
||||
}
|
||||
Bangle.on("lock", clock.onLock);
|
||||
}
|
||||
|
||||
clock.start();
|
||||
clock.start();
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "barclock",
|
||||
"name": "Bar Clock",
|
||||
"version": "0.15",
|
||||
"version": "0.16",
|
||||
"description": "A simple digital clock showing seconds as a bar",
|
||||
"icon": "clock-bar.png",
|
||||
"screenshots": [{"url":"screenshot.png"},{"url":"screenshot_pm.png"}],
|
||||
|
|
|
@ -2,3 +2,4 @@
|
|||
0.02: More compact rendering & app icon
|
||||
0.03: Tell clock widgets to hide.
|
||||
0.04: Improve current time readability in light theme.
|
||||
0.05: Show calendar colors & improved all day events.
|
||||
|
|
|
@ -20,41 +20,55 @@ function zp(str) {
|
|||
}
|
||||
|
||||
function drawEventHeader(event, y) {
|
||||
g.setFont("Vector", 24);
|
||||
|
||||
var x = 0;
|
||||
var time = isActive(event) ? new Date() : new Date(event.timestamp * 1000);
|
||||
|
||||
//Don't need to know what time the event is at if its all day
|
||||
if (isActive(event) || !event.allDay) {
|
||||
g.setFont("Vector", 24);
|
||||
var timeStr = zp(time.getHours()) + ":" + zp(time.getMinutes());
|
||||
g.drawString(timeStr, 5, y);
|
||||
y += 24;
|
||||
g.drawString(timeStr, 0, y);
|
||||
y += 3;
|
||||
x = 13*timeStr.length+5;
|
||||
}
|
||||
|
||||
g.setFont("12x20", 1);
|
||||
|
||||
if (isActive(event)) {
|
||||
g.drawString(zp(time.getDate())+". " + require("locale").month(time,1),15*timeStr.length,y-21);
|
||||
g.drawString(zp(time.getDate())+". " + require("locale").month(time,1),x,y);
|
||||
} else {
|
||||
var offset = 0-time.getTimezoneOffset()/1440;
|
||||
var days = Math.floor((time.getTime()/1000)/86400+offset)-Math.floor(getTime()/86400+offset);
|
||||
if(days > 0) {
|
||||
if(days > 0 || event.allDay) {
|
||||
var daysStr = days===1?/*LANG*/"tomorrow":/*LANG*/"in "+days+/*LANG*/" days";
|
||||
g.drawString(daysStr,15*timeStr.length,y-21);
|
||||
g.drawString(daysStr,x,y);
|
||||
}
|
||||
}
|
||||
y += 21;
|
||||
return y;
|
||||
}
|
||||
|
||||
function drawEventBody(event, y) {
|
||||
g.setFont("12x20", 1);
|
||||
var lines = g.wrapString(event.title, g.getWidth()-10);
|
||||
var lines = g.wrapString(event.title, g.getWidth()-15);
|
||||
var yStart = y;
|
||||
if (lines.length > 2) {
|
||||
lines = lines.slice(0,2);
|
||||
lines[1] = lines[1].slice(0,-3)+"...";
|
||||
}
|
||||
g.drawString(lines.join('\n'), 5, y);
|
||||
g.drawString(lines.join('\n'),10,y);
|
||||
y+=20 * lines.length;
|
||||
if(event.location) {
|
||||
g.drawImage(atob("DBSBAA8D/H/nDuB+B+B+B3Dn/j/B+A8A8AYAYAYAAAAAAA=="),5,y);
|
||||
g.drawString(event.location, 20, y);
|
||||
g.drawImage(atob("DBSBAA8D/H/nDuB+B+B+B3Dn/j/B+A8A8AYAYAYAAAAAAA=="),10,y);
|
||||
g.drawString(event.location,25,y);
|
||||
y+=20;
|
||||
}
|
||||
if (event.color) {
|
||||
var oldColor = g.getColor();
|
||||
g.setColor("#"+(0x1000000+Number(event.color)).toString(16).padStart(6,"0"));
|
||||
g.fillRect(0,yStart,5,y-3);
|
||||
g.setColor(oldColor);
|
||||
}
|
||||
y+=5;
|
||||
return y;
|
||||
}
|
||||
|
@ -68,19 +82,19 @@ function drawEvent(event, y) {
|
|||
var curEventHeight = 0;
|
||||
|
||||
function drawCurrentEvents(y) {
|
||||
g.setColor(g.theme.dark ? "#0ff" : "#0000ff");
|
||||
g.clearRect(5, y, g.getWidth() - 5, y + curEventHeight);
|
||||
g.setColor(g.theme.dark ? "#0ff" : "#00f");
|
||||
g.clearRect(0,y,g.getWidth()-5,y+curEventHeight);
|
||||
curEventHeight = y;
|
||||
|
||||
if(current.length === 0) {
|
||||
y = drawEvent({timestamp: getTime(), durationInSeconds: 100}, y);
|
||||
} else {
|
||||
y = drawEventHeader(current[0], y);
|
||||
y = drawEventHeader(current[0],y);
|
||||
for (var e of current) {
|
||||
y = drawEventBody(e, y);
|
||||
y = drawEventBody(e,y);
|
||||
}
|
||||
}
|
||||
curEventHeight = y - curEventHeight;
|
||||
curEventHeight = y-curEventHeight;
|
||||
return y;
|
||||
}
|
||||
|
||||
|
@ -94,7 +108,7 @@ function drawFutureEvents(y) {
|
|||
}
|
||||
|
||||
function fullRedraw() {
|
||||
g.clearRect(5,24,g.getWidth()-5,g.getHeight());
|
||||
g.clearRect(0,24,g.getWidth()-5,g.getHeight());
|
||||
updateCalendar();
|
||||
var y = 30;
|
||||
y = drawCurrentEvents(y);
|
||||
|
@ -117,3 +131,4 @@ var minuteInterval = setInterval(redraw, 60 * 1000);
|
|||
Bangle.setUI("clock");
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "calclock",
|
||||
"name": "Calendar Clock",
|
||||
"shortName": "CalClock",
|
||||
"version": "0.04",
|
||||
"version": "0.05",
|
||||
"description": "Show the current and upcoming events synchronized from Gadgetbridge",
|
||||
"icon": "calclock.png",
|
||||
"type": "clock",
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
diff --git a/apps/calclock/calclock.js b/apps/calclock/calclock.js
|
||||
index cb8c6100e..2092c1a4e 100644
|
||||
--- a/apps/calclock/calclock.js
|
||||
+++ b/apps/calclock/calclock.js
|
||||
@@ -3,9 +3,24 @@ var current = [];
|
||||
var next = [];
|
||||
|
||||
function updateCalendar() {
|
||||
- calendar = require("Storage").readJSON("android.calendar.json",true)||[];
|
||||
- calendar = calendar.filter(e => isActive(e) || getTime() <= e.timestamp);
|
||||
- calendar.sort((a,b) => a.timestamp - b.timestamp);
|
||||
+ calendar = [
|
||||
+ {
|
||||
+ t: "calendar",
|
||||
+ id: 2, type: 0, timestamp: getTime(), durationInSeconds: 200,
|
||||
+ title: "Capture Screenshot",
|
||||
+ description: "Capture Screenshot",
|
||||
+ location: "",
|
||||
+ calName: "",
|
||||
+ color: -7151168, allDay: true },
|
||||
+ {
|
||||
+ t: "calendar",
|
||||
+ id: 7186, type: 0, timestamp: getTime() + 2000, durationInSeconds: 100,
|
||||
+ title: "Upload to BangleApps",
|
||||
+ description: "",
|
||||
+ location: "",
|
||||
+ calName: "",
|
||||
+ color: -509406, allDay: false }
|
||||
+ ];
|
||||
|
||||
current = calendar.filter(isActive);
|
||||
next = calendar.filter(e=>!isActive(e));
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 3.1 KiB |
|
@ -31,3 +31,4 @@
|
|||
0.16: Fix const error
|
||||
Use widget_utils if available
|
||||
0.17: Load circles from clkinfo
|
||||
0.18: Improved clkinfo handling and using it for the weather circle
|
||||
|
|
|
@ -106,6 +106,10 @@ let circleItemNum = [
|
|||
2, // circle3
|
||||
3, // circle4
|
||||
];
|
||||
let weatherCircleNum = 0;
|
||||
let weatherCircleDataNum = 0;
|
||||
let weatherCircleCondNum = 0;
|
||||
let weatherCircleTempNum = 0;
|
||||
|
||||
function hideWidgets() {
|
||||
/*
|
||||
|
@ -323,6 +327,55 @@ function getImage(graphic, color) {
|
|||
}
|
||||
|
||||
function drawWeather(w) {
|
||||
if (!w) w = getCircleXPosition("weather");
|
||||
let weatherInfo = menu[weatherCircleNum];
|
||||
let weatherCond = weatherCircleCondNum >= 0? weatherInfo.items[weatherCircleCondNum]: undefined;
|
||||
let weatherData = weatherCircleDataNum >= 0? weatherInfo.items[weatherCircleDataNum]: undefined;
|
||||
let weatherTemp = weatherCircleTempNum >= 0? weatherInfo.items[weatherCircleTempNum]: undefined;
|
||||
let color = getCircleColor("weather");
|
||||
let percent = 0;
|
||||
let data = settings.weatherCircleData;
|
||||
let tempString = "?", icon = undefined;
|
||||
let scale = 16/24; //our icons are 16x16 while clkinfo's are 24x24
|
||||
|
||||
if(weatherCond) {
|
||||
weatherCond.show()
|
||||
weatherCond.hide()
|
||||
let data = weatherCond.get()
|
||||
if(settings.legacyWeatherIcons) { //may disappear in future
|
||||
icon = getWeatherIconByCode(data.v);
|
||||
scale = 1;
|
||||
} else
|
||||
icon = data.img;
|
||||
}
|
||||
if(weatherTemp) {
|
||||
weatherTemp.show()
|
||||
weatherTemp.hide()
|
||||
tempString = weatherTemp.get().text;
|
||||
}
|
||||
|
||||
drawCircleBackground(w);
|
||||
|
||||
if(weatherData) {
|
||||
weatherData.show();
|
||||
weatherData.hide();
|
||||
let data = weatherData.get();
|
||||
if(weatherData.hasRange) percent = (data.v-data.min) / (data.max-data.min);
|
||||
drawGauge(w, h3, percent, color);
|
||||
}
|
||||
|
||||
drawInnerCircleAndTriangle(w);
|
||||
|
||||
writeCircleText(w, tempString);
|
||||
|
||||
if(icon) {
|
||||
g.setColor(getCircleIconColor("weather", color, percent))
|
||||
.drawImage(icon, w - iconOffset, h3 + radiusOuter - iconOffset, {scale: scale});
|
||||
} else {
|
||||
g.drawString("?", w, h3 + radiusOuter);
|
||||
}
|
||||
}
|
||||
function drawWeatherOld(w) {
|
||||
if (!w) w = getCircleXPosition("weather");
|
||||
let weather = getWeather();
|
||||
let tempString = weather ? locale.temp(weather.temp - 273.15) : undefined;
|
||||
|
@ -659,12 +712,16 @@ function reloadMenu() {
|
|||
let parts = settings['circle'+i].split("/");
|
||||
let infoName = parts[0], itemName = parts[1];
|
||||
let infoNum = menu.findIndex(e=>e.name==infoName);
|
||||
let itemNum = 0;
|
||||
//suppose unnamed are varying (like timers or events), pick the first
|
||||
if(itemName)
|
||||
let itemNum = 0; //get first if dynamic
|
||||
if(!menu[infoNum].dynamic)
|
||||
itemNum = menu[infoNum].items.findIndex(it=>it.name==itemName);
|
||||
circleInfoNum[i-1] = infoNum;
|
||||
circleItemNum[i-1] = itemNum;
|
||||
} else if(settings['circle'+i] == "weather") {
|
||||
weatherCircleNum = menu.findIndex(e=>e.name.toLowerCase() == "weather");
|
||||
weatherCircleDataNum = menu[weatherCircleNum].items.findIndex(it=>it.name==settings.weatherCircleData);
|
||||
weatherCircleCondNum = menu[weatherCircleNum].items.findIndex(it=>it.name=="condition");
|
||||
weatherCircleTempNum = menu[weatherCircleNum].items.findIndex(it=>it.name=="temperature");
|
||||
}
|
||||
}
|
||||
//reload periodically for changes?
|
||||
|
@ -685,22 +742,23 @@ function drawClkInfo(index, w) {
|
|||
if (!w) w = getCircleXPosition(type);
|
||||
drawCircleBackground(w);
|
||||
const color = getCircleColor(type);
|
||||
if(!info || !info.items.length) {
|
||||
var item = info.items[circleItemNum[index-1]];
|
||||
if(!info || !item) {
|
||||
drawEmpty(info? info.img : null, w, color);
|
||||
return;
|
||||
}
|
||||
var item = info.items[circleItemNum[index-1]];
|
||||
//TODO do hide()+get() here
|
||||
item.show();
|
||||
item.hide();
|
||||
item=item.get();
|
||||
var img = item.img;
|
||||
var data=item.get();
|
||||
var img = data.img;
|
||||
var percent = 1; //fill up if no range
|
||||
var txt = data.text;
|
||||
if(!img) img = info.img;
|
||||
let percent = (item.v-item.min) / item.max;
|
||||
if(isNaN(percent)) percent = 1; //fill it up
|
||||
if(item.hasRange) percent = (data.v-data.min) / (data.max-data.min);
|
||||
if(data.short) txt = data.short;
|
||||
drawGauge(w, h3, percent, color);
|
||||
drawInnerCircleAndTriangle(w);
|
||||
writeCircleText(w, item.text);
|
||||
writeCircleText(w, txt);
|
||||
g.setColor(getCircleIconColor(type, color, percent))
|
||||
.drawImage(img, w - iconOffset, h3 + radiusOuter - iconOffset, {scale: 16/24});
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{ "id": "circlesclock",
|
||||
"name": "Circles clock",
|
||||
"shortName":"Circles clock",
|
||||
"version":"0.17",
|
||||
"version":"0.18",
|
||||
"description": "A clock with three or four circles for different data at the bottom in a probably familiar style",
|
||||
"icon": "app.png",
|
||||
"screenshots": [{"url":"screenshot-dark.png"}, {"url":"screenshot-light.png"}, {"url":"screenshot-dark-4.png"}, {"url":"screenshot-light-4.png"}],
|
||||
|
|
|
@ -17,10 +17,7 @@
|
|||
var valuesCircleTypes = ["empty","weather", "sunprogress"];
|
||||
var namesCircleTypes = ["empty","weather", "sun"];
|
||||
clock_info.load().forEach(e=>{
|
||||
//TODO filter for hasRange and other
|
||||
if(!e.items.length || !e.items[0].name) {
|
||||
//suppose unnamed are varying (like timers or events), pick the first
|
||||
item = e.items[0];
|
||||
if(e.dynamic) {
|
||||
valuesCircleTypes = valuesCircleTypes.concat([e.name+"/"]);
|
||||
namesCircleTypes = namesCircleTypes.concat([e.name]);
|
||||
} else {
|
||||
|
@ -85,6 +82,12 @@
|
|||
},
|
||||
onchange: x => save('updateInterval', x),
|
||||
},
|
||||
//TODO deprecated local icons, may disappear in future
|
||||
/*LANG*/'legacy weather icons': {
|
||||
value: !!settings.legacyWeatherIcons,
|
||||
format: () => (settings.legacyWeatherIcons ? 'Yes' : 'No'),
|
||||
onchange: x => save('legacyWeatherIcons', x),
|
||||
},
|
||||
/*LANG*/'show big weather': {
|
||||
value: !!settings.showBigWeather,
|
||||
format: () => (settings.showBigWeather ? 'Yes' : 'No'),
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
0.01: New App!
|
||||
0.02: Add lightning
|
||||
|
|
|
@ -46,6 +46,9 @@ var booster = { x : g.getWidth()/4 + Math.random()*g.getWidth()/2,
|
|||
var exploded = false;
|
||||
var nExplosions = 0;
|
||||
var landed = false;
|
||||
var lightning = 0;
|
||||
|
||||
var settings = require("Storage").readJSON('f9settings.json', 1) || {};
|
||||
|
||||
const gravity = 4;
|
||||
const dt = 0.1;
|
||||
|
@ -61,18 +64,40 @@ function flameImageGen (throttle) {
|
|||
|
||||
function drawFalcon(x, y, throttle, angle) {
|
||||
g.setColor(1, 1, 1).drawImage(falcon9, x, y, {rotate:angle});
|
||||
if (throttle>0) {
|
||||
if (throttle>0 || lightning>0) {
|
||||
var flameImg = flameImageGen(throttle);
|
||||
var r = falcon9.height/2 + flameImg.height/2-1;
|
||||
var xoffs = -Math.sin(angle)*r;
|
||||
var yoffs = Math.cos(angle)*r;
|
||||
if (Math.random()>0.7) g.setColor(1, 0.5, 0);
|
||||
else g.setColor(1, 1, 0);
|
||||
g.drawImage(flameImg, x+xoffs, y+yoffs, {rotate:angle});
|
||||
if (throttle>0) g.drawImage(flameImg, x+xoffs, y+yoffs, {rotate:angle});
|
||||
if (lightning>1 && lightning<30) {
|
||||
for (var i=0; i<6; ++i) {
|
||||
var r = Math.random()*6;
|
||||
var x = Math.random()*5 - xoffs;
|
||||
var y = Math.random()*5 - yoffs;
|
||||
g.setColor(1, Math.random()*0.5+0.5, 0).fillCircle(booster.x+x, booster.y+y, r);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawLightning() {
|
||||
var c = {x:cloudOffs+50, y:30};
|
||||
var dx = c.x-booster.x;
|
||||
var dy = c.y-booster.y;
|
||||
var m1 = {x:booster.x+0.6*dx+Math.random()*20, y:booster.y+0.6*dy+Math.random()*10};
|
||||
var m2 = {x:booster.x+0.4*dx+Math.random()*20, y:booster.y+0.4*dy+Math.random()*10};
|
||||
g.setColor(1, 1, 1).drawLine(c.x, c.y, m1.x, m1.y).drawLine(m1.x, m1.y, m2.x, m2.y).drawLine(m2.x, m2.y, booster.x, booster.y);
|
||||
}
|
||||
|
||||
function drawBG() {
|
||||
if (lightning==1) {
|
||||
g.setBgColor(1, 1, 1).clear();
|
||||
Bangle.buzz(200);
|
||||
return;
|
||||
}
|
||||
g.setBgColor(0.2, 0.2, 1).clear();
|
||||
g.setColor(0, 0, 1).fillRect(0, g.getHeight()-oceanHeight, g.getWidth()-1, g.getHeight()-1);
|
||||
g.setColor(0.5, 0.5, 1).fillCircle(cloudOffs+34, 30, 15).fillCircle(cloudOffs+60, 35, 20).fillCircle(cloudOffs+75, 20, 10);
|
||||
|
@ -88,6 +113,7 @@ function renderScreen(input) {
|
|||
drawBG();
|
||||
showFuel();
|
||||
drawFalcon(booster.x, booster.y, Math.floor(input.throttle*12), input.angle);
|
||||
if (lightning>1 && lightning<6) drawLightning();
|
||||
}
|
||||
|
||||
function getInputs() {
|
||||
|
@ -97,6 +123,7 @@ function getInputs() {
|
|||
if (t > 1) t = 1;
|
||||
if (t < 0) t = 0;
|
||||
if (booster.fuel<=0) t = 0;
|
||||
if (lightning>0 && lightning<20) t = 0;
|
||||
return {throttle: t, angle: a};
|
||||
}
|
||||
|
||||
|
@ -121,7 +148,6 @@ function gameStep() {
|
|||
else {
|
||||
var input = getInputs();
|
||||
if (booster.y >= targetY) {
|
||||
// console.log(booster.x + " " + booster.y + " " + booster.vy + " " + droneX + " " + input.angle);
|
||||
if (Math.abs(booster.x-droneX-droneShip.width/2)<droneShip.width/2 && Math.abs(input.angle)<Math.PI/8 && booster.vy<maxV) {
|
||||
renderScreen({angle:0, throttle:0});
|
||||
epilogue("You landed!");
|
||||
|
@ -129,6 +155,8 @@ function gameStep() {
|
|||
else exploded = true;
|
||||
}
|
||||
else {
|
||||
if (lightning) ++lightning;
|
||||
if (settings.lightning && (lightning==0||lightning>40) && Math.random()>0.98) lightning = 1;
|
||||
booster.x += booster.vx*dt;
|
||||
booster.y += booster.vy*dt;
|
||||
booster.vy += gravity*dt;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{ "id": "f9lander",
|
||||
"name": "Falcon9 Lander",
|
||||
"shortName":"F9lander",
|
||||
"version":"0.01",
|
||||
"version":"0.02",
|
||||
"description": "Land a rocket booster",
|
||||
"icon": "f9lander.png",
|
||||
"screenshots" : [ { "url":"f9lander_screenshot1.png" }, { "url":"f9lander_screenshot2.png" }, { "url":"f9lander_screenshot3.png" }],
|
||||
|
@ -10,6 +10,7 @@
|
|||
"supports" : ["BANGLEJS", "BANGLEJS2"],
|
||||
"storage": [
|
||||
{"name":"f9lander.app.js","url":"app.js"},
|
||||
{"name":"f9lander.img","url":"app-icon.js","evaluate":true}
|
||||
{"name":"f9lander.img","url":"app-icon.js","evaluate":true},
|
||||
{"name":"f9lander.settings.js", "url":"settings.js"}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
// This file should contain exactly one function, which shows the app's settings
|
||||
/**
|
||||
* @param {function} back Use back() to return to settings menu
|
||||
*/
|
||||
const boolFormat = v => v ? /*LANG*/"On" : /*LANG*/"Off";
|
||||
(function(back) {
|
||||
const SETTINGS_FILE = 'f9settings.json'
|
||||
// initialize with default settings...
|
||||
let settings = {
|
||||
'lightning': false,
|
||||
}
|
||||
// ...and overwrite them with any saved values
|
||||
// This way saved values are preserved if a new version adds more settings
|
||||
const storage = require('Storage')
|
||||
const saved = storage.readJSON(SETTINGS_FILE, 1) || {}
|
||||
for (const key in saved) {
|
||||
settings[key] = saved[key];
|
||||
}
|
||||
// creates a function to safe a specific setting, e.g. save('color')(1)
|
||||
function save(key) {
|
||||
return function (value) {
|
||||
settings[key] = value;
|
||||
storage.write(SETTINGS_FILE, settings);
|
||||
}
|
||||
}
|
||||
const menu = {
|
||||
'': { 'title': 'OpenWind' },
|
||||
'< Back': back,
|
||||
'Lightning': {
|
||||
value: settings.lightning,
|
||||
format: boolFormat,
|
||||
onchange: save('lightning'),
|
||||
}
|
||||
}
|
||||
E.showMenu(menu);
|
||||
})
|
|
@ -0,0 +1,3 @@
|
|||
0.01: New App!
|
||||
0.02: Allow redirection of loads to the launcher
|
||||
0.03: Allow hiding the fastloading info screen
|
|
@ -0,0 +1,21 @@
|
|||
# Fastload Utils
|
||||
|
||||
*EXPERIMENTAL* Use this with caution. When you find something misbehaving please check if the problem actually persists when removing this app.
|
||||
|
||||
This allows fast loading of all apps with two conditions:
|
||||
* Loaded app contains `Bangle.loadWidgets`. This is needed to prevent problems with apps not expecting widgets to be already loaded.
|
||||
* Current app can be removed completely from RAM.
|
||||
|
||||
## Settings
|
||||
|
||||
* Allows to redirect all loads usually loading the clock to the launcher instead
|
||||
* The "Fastloading..." screen can be switched off
|
||||
|
||||
## Technical infos
|
||||
|
||||
This is still experimental but it uses the same mechanism as `.bootcde` does.
|
||||
It checks the app to be loaded for widget use and stores the result of that and a hash of the js in a cache.
|
||||
|
||||
# Creator
|
||||
|
||||
[halemmerich](https://github.com/halemmerich)
|
|
@ -0,0 +1,66 @@
|
|||
{
|
||||
const SETTINGS = require("Storage").readJSON("fastload.json") || {};
|
||||
|
||||
let loadingScreen = function(){
|
||||
g.reset();
|
||||
|
||||
let x = g.getWidth()/2;
|
||||
let y = g.getHeight()/2;
|
||||
g.setColor(g.theme.bg);
|
||||
g.fillRect(x-49, y-19, x+49, y+19);
|
||||
g.setColor(g.theme.fg);
|
||||
g.drawRect(x-50, y-20, x+50, y+20);
|
||||
g.setFont("6x8");
|
||||
g.setFontAlign(0,0);
|
||||
g.drawString("Fastloading...", x, y);
|
||||
g.flip(true);
|
||||
};
|
||||
|
||||
let cache = require("Storage").readJSON("fastload.cache") || {};
|
||||
|
||||
let checkApp = function(n){
|
||||
// no widgets, no problem
|
||||
if (!global.WIDGETS) return true;
|
||||
let app = require("Storage").read(n);
|
||||
if (cache[n] && E.CRC32(app) == cache[n].crc)
|
||||
return cache[n].fast
|
||||
cache[n] = {};
|
||||
cache[n].fast = app.includes("Bangle.loadWidgets");
|
||||
cache[n].crc = E.CRC32(app);
|
||||
require("Storage").writeJSON("fastload.cache", cache);
|
||||
return cache[n].fast;
|
||||
}
|
||||
|
||||
global._load = load;
|
||||
|
||||
let slowload = function(n){
|
||||
global._load(n);
|
||||
}
|
||||
|
||||
let fastload = function(n){
|
||||
if (!n || checkApp(n)){
|
||||
// Bangle.load can call load, to prevent recursion this must be the system load
|
||||
global.load = slowload;
|
||||
Bangle.load(n);
|
||||
// if fastloading worked, we need to set load back to this method
|
||||
global.load = fastload;
|
||||
}
|
||||
else
|
||||
slowload(n);
|
||||
};
|
||||
global.load = fastload;
|
||||
|
||||
Bangle.load = (o => (name) => {
|
||||
if (Bangle.uiRemove && !SETTINGS.hideLoading) loadingScreen();
|
||||
if (SETTINGS.autoloadLauncher && !name){
|
||||
let orig = Bangle.load;
|
||||
Bangle.load = (n)=>{
|
||||
Bangle.load = orig;
|
||||
fastload(n);
|
||||
}
|
||||
Bangle.showLauncher();
|
||||
Bangle.load = orig;
|
||||
} else
|
||||
o(name);
|
||||
})(Bangle.load);
|
||||
}
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -0,0 +1,16 @@
|
|||
{ "id": "fastload",
|
||||
"name": "Fastload Utils",
|
||||
"shortName" : "Fastload Utils",
|
||||
"version": "0.03",
|
||||
"icon": "icon.png",
|
||||
"description": "Enable experimental fastloading for more apps",
|
||||
"type":"bootloader",
|
||||
"tags": "system",
|
||||
"supports": ["BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name":"fastload.5.boot.js","url":"boot.js"},
|
||||
{"name":"fastload.settings.js","url":"settings.js"}
|
||||
],
|
||||
"data": [{"name":"fastload.json"}]
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
(function(back) {
|
||||
var FILE="fastload.json";
|
||||
var settings;
|
||||
|
||||
function writeSettings(key, value) {
|
||||
var s = require('Storage').readJSON(FILE, true) || {};
|
||||
s[key] = value;
|
||||
require('Storage').writeJSON(FILE, s);
|
||||
readSettings();
|
||||
}
|
||||
|
||||
function readSettings(){
|
||||
settings = require('Storage').readJSON(FILE, true) || {};
|
||||
}
|
||||
|
||||
readSettings();
|
||||
|
||||
function buildMainMenu(){
|
||||
var mainmenu = {
|
||||
'': { 'title': 'Fastload', back: back },
|
||||
'Force load to launcher': {
|
||||
value: !!settings.autoloadLauncher,
|
||||
onchange: v => {
|
||||
writeSettings("autoloadLauncher",v);
|
||||
}
|
||||
},
|
||||
'Hide "Fastloading..."': {
|
||||
value: !!settings.hideLoading,
|
||||
onchange: v => {
|
||||
writeSettings("hideLoading",v);
|
||||
}
|
||||
}
|
||||
};
|
||||
return mainmenu;
|
||||
}
|
||||
|
||||
E.showMenu(buildMainMenu());
|
||||
})
|
|
@ -8,6 +8,8 @@ Upon opening the gallery app, you will be presented with a list of images that y
|
|||
|
||||
## Adding images
|
||||
|
||||
Once this app is installed you can manage images by pressing the Disk icon next to it or by following the manual steps below:
|
||||
|
||||
1. The gallery app does not perform any scaling, and does not support panning. Therefore, you should use your favorite image editor to produce an image of the appropriate size for your watch. (240x240 for Bangle 1 or 176x176 for Bangle 2.) How you achieve this is up to you. If on a Bangle 2, I recommend adjusting the colors here to comply with the color restrictions.
|
||||
|
||||
2. Upload your image to the [Espruino image converter](https://www.espruino.com/Image+Converter). I recommend enabling compression and choosing one of the following color settings:
|
||||
|
|
|
@ -0,0 +1,165 @@
|
|||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="../../css/spectre.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<script src="../../webtools/heatshrink.js"></script>
|
||||
<script src="../../webtools/imageconverter.js"></script>
|
||||
<script src="../../core/lib/interface.js"></script>
|
||||
|
||||
<h4>Existing Images</h4>
|
||||
<ul id="imagelist">
|
||||
</ul>
|
||||
|
||||
<h4>Convert & Upload Images</h4>
|
||||
|
||||
<input type="file" id="fileLoader"/><br/>
|
||||
<input type="checkbox" id="compression" onchange="imageLoaded()"> Use Compression?</input><br/>
|
||||
<input type="checkbox" id="alphaToColor" onchange="imageLoaded()"> Transparency to Color</input><br/>
|
||||
<input type="checkbox" id="transparent" onchange="imageLoaded()" checked> Transparency?</input><br/>
|
||||
<input type="checkbox" id="inverted" onchange="imageLoaded()"> Inverted?</input><br/>
|
||||
<input type="checkbox" id="autoCrop" onchange="imageLoaded()"> Crop?</input><br/>
|
||||
Diffusion: <select id="diffusion" onchange="imageLoaded()"></select><br/>
|
||||
|
||||
Brightness: <span id="brightnessv"></span>
|
||||
<input type="range" id="brightness" min="-127" max="127" value="0" onchange="imageLoaded()"></input><br/>
|
||||
Contrast: <span id="contrastv"></span>
|
||||
<input type="range" id="contrast" min="-255" max="255" value="0" onchange="imageLoaded()"></input><br/>
|
||||
Colours: <select id="colorStyle" onchange="imageLoaded()"></select><br/>
|
||||
|
||||
<canvas id="canvas" style="display:none;"></canvas>
|
||||
|
||||
<button class="btn btn-default" id="btnUpload" disabled="disabled">Upload</button>
|
||||
|
||||
<script>
|
||||
// load available colour formats and diffusion...
|
||||
imageconverter.setFormatOptions(document.getElementById("colorStyle"));
|
||||
imageconverter.setDiffusionOptions(document.getElementById("diffusion"));
|
||||
|
||||
let img;
|
||||
let screenSize;
|
||||
let imgstr;
|
||||
const uploadBtn = document.getElementById("btnUpload");
|
||||
uploadBtn.addEventListener("click", function() {
|
||||
const filename = document.getElementById("fileLoader").value.split(/(\\|\/)/g).pop();
|
||||
const filenameWithoutExt = filename.replace(/\.[^/.]+$/, "");
|
||||
Util.showModal("Uploading...");
|
||||
Util.writeStorage("gal-" + filenameWithoutExt.substring(0, 12) + ".img", imgstr, () => {
|
||||
Util.hideModal();
|
||||
updateFileList();
|
||||
});
|
||||
});
|
||||
|
||||
function imageLoaded() {
|
||||
if (img === undefined) return;
|
||||
if (screenSize !== img.width + "x" + img.height) {
|
||||
alert("Image must be " + screenSize);
|
||||
return;
|
||||
}
|
||||
let options = {};
|
||||
let diffusionSelect = document.getElementById("diffusion");
|
||||
options.diffusion = diffusionSelect.options[diffusionSelect.selectedIndex].value;
|
||||
options.compression = document.getElementById("compression").checked;
|
||||
options.alphaToColor = document.getElementById("alphaToColor").checked;
|
||||
options.transparent = document.getElementById("transparent").checked;
|
||||
options.inverted = document.getElementById("inverted").checked;
|
||||
options.autoCrop = document.getElementById("autoCrop").checked;
|
||||
options.brightness = 0|document.getElementById("brightness").value;
|
||||
document.getElementById("brightnessv").innerText = options.brightness;
|
||||
options.contrast = 0|document.getElementById("contrast").value;
|
||||
document.getElementById("contrastv").innerText = options.contrast;
|
||||
let colorSelect = document.getElementById("colorStyle");
|
||||
options.mode = colorSelect.options[colorSelect.selectedIndex].value;
|
||||
|
||||
options.output = "string";
|
||||
|
||||
let canvas = document.getElementById("canvas")
|
||||
canvas.width = img.width*2;
|
||||
canvas.height = img.height;
|
||||
canvas.style = "display:block;border:1px solid black;margin:8px;"
|
||||
let ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(img,0,0);
|
||||
|
||||
let imageData1 = ctx.getImageData(0, 0, img.width, img.height);
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.fillRect(options.width, 0, img.width, img.height);
|
||||
let rgba = imageData1.data;
|
||||
options.rgbaOut = rgba;
|
||||
options.width = img.width;
|
||||
options.height = img.height;
|
||||
imgstr = imageconverter.RGBAtoString(rgba, options);
|
||||
let outputImageData = new ImageData(options.rgbaOut, options.width, options.height);
|
||||
ctx.putImageData(outputImageData,img.width,0);
|
||||
|
||||
// checkerboard for transparency on original image
|
||||
let imageData2 = ctx.getImageData(0, 0, img.width, img.height);
|
||||
imageconverter.RGBAtoCheckerboard(imageData2.data, {width:img.width,height:img.height});
|
||||
ctx.putImageData(imageData2,0,0);
|
||||
|
||||
uploadBtn.disabled=false;
|
||||
}
|
||||
function handleFileSelect(event) {
|
||||
if (event.target.files.length != 1) return;
|
||||
let reader = new FileReader();
|
||||
reader.onload = function(event) {
|
||||
img = new Image();
|
||||
img.onload = imageLoaded;
|
||||
img.src = event.target.result;
|
||||
};
|
||||
reader.readAsDataURL(event.target.files[0]);
|
||||
};
|
||||
document.getElementById('fileLoader').addEventListener('change', handleFileSelect, false);
|
||||
|
||||
function updateFileList() {
|
||||
Puck.write(`\x10(function() {
|
||||
Bluetooth.print(JSON.stringify(require("Storage").list(/^gal-.*\.img/).sort()));
|
||||
})()\n`, contents => {
|
||||
let fileNames = JSON.parse(contents);
|
||||
|
||||
const imagelist = document.getElementById("imagelist");
|
||||
imagelist.innerHTML=""; // remove all children
|
||||
// add a list of existing files
|
||||
if (fileNames.length === 0) {
|
||||
const span = document.createElement("span");
|
||||
span.textContent = "No existing images";
|
||||
imagelist.appendChild(span);
|
||||
}
|
||||
fileNames.forEach(fileName => {
|
||||
const li = document.createElement("li");
|
||||
const span = document.createElement("span");
|
||||
span.classList.add("label");
|
||||
span.textContent = fileName.substr(4, fileName.length - 8);
|
||||
li.appendChild(span);
|
||||
|
||||
const buttonDelete = document.createElement("button");
|
||||
buttonDelete.classList.add('btn');
|
||||
buttonDelete.classList.add('btn-link');
|
||||
buttonDelete.textContent = "Delete";
|
||||
buttonDelete.onclick = () => {
|
||||
Util.showModal(`Erasing ${fileName}...`);
|
||||
Util.eraseStorage(fileName, () => {
|
||||
Util.hideModal();
|
||||
updateFileList();
|
||||
});
|
||||
}
|
||||
li.appendChild(buttonDelete);
|
||||
imagelist.appendChild(li);
|
||||
});
|
||||
Util.hideModal(); // Loading modal
|
||||
});
|
||||
}
|
||||
Util.showModal("Loading...");
|
||||
|
||||
// Called when app starts
|
||||
function onInit() {
|
||||
// Read BangleJS screen size
|
||||
Puck.write(`\x10(function() {
|
||||
Bluetooth.print(g.getWidth() + "x" + g.getHeight());
|
||||
})()\n`, contents => {
|
||||
screenSize = contents;
|
||||
updateFileList();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -12,6 +12,7 @@
|
|||
"BANGLEJS"
|
||||
],
|
||||
"allow_emulator": true,
|
||||
"interface": "interface.html",
|
||||
"storage": [
|
||||
{
|
||||
"name": "gallery.app.js",
|
||||
|
|
|
@ -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
|
|
@ -234,7 +234,7 @@ function handleMessages()
|
|||
{
|
||||
if(!hasMessages()) return;
|
||||
E.showMessage("Loading Messages...");
|
||||
load("messages.app.js");
|
||||
require("messages").openGUI();
|
||||
}
|
||||
|
||||
function hasMessages()
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "hcclock",
|
||||
"name": "Hi-Contrast Clock",
|
||||
"version": "0.04",
|
||||
"version": "0.05",
|
||||
"description": "Hi-Contrast Clock : A simple yet very bold clock that aims to be readable in high luninosity environments. Uses big 10x5 pixel digits. Use BTN 1 to switch background and foreground colors.",
|
||||
"icon": "hcclock-icon.png",
|
||||
"type": "clock",
|
||||
|
|
|
@ -14,3 +14,5 @@
|
|||
0.13: Add support for internationalization
|
||||
0.14: Move settings
|
||||
0.15: Fix charts (fix #1366)
|
||||
0.16: Code tidyup, add back button in top left of health app graphs
|
||||
0.17: Add automatic translation of bar chart labels
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
function menuMain() {
|
||||
swipe_enabled = false;
|
||||
clearButton();
|
||||
E.showMenu({
|
||||
"": { title: /*LANG*/"Health Tracking" },
|
||||
/*LANG*/"< Back": () => load(),
|
||||
|
@ -12,8 +10,6 @@ function menuMain() {
|
|||
}
|
||||
|
||||
function menuStepCount() {
|
||||
swipe_enabled = false;
|
||||
clearButton();
|
||||
E.showMenu({
|
||||
"": { title:/*LANG*/"Steps" },
|
||||
/*LANG*/"< Back": () => menuMain(),
|
||||
|
@ -23,8 +19,6 @@ function menuStepCount() {
|
|||
}
|
||||
|
||||
function menuMovement() {
|
||||
swipe_enabled = false;
|
||||
clearButton();
|
||||
E.showMenu({
|
||||
"": { title:/*LANG*/"Movement" },
|
||||
/*LANG*/"< Back": () => menuMain(),
|
||||
|
@ -34,8 +28,6 @@ function menuMovement() {
|
|||
}
|
||||
|
||||
function menuHRM() {
|
||||
swipe_enabled = false;
|
||||
clearButton();
|
||||
E.showMenu({
|
||||
"": { title:/*LANG*/"Heart Rate" },
|
||||
/*LANG*/"< Back": () => menuMain(),
|
||||
|
@ -48,22 +40,16 @@ function stepsPerHour() {
|
|||
E.showMessage(/*LANG*/"Loading...");
|
||||
var data = new Uint16Array(24);
|
||||
require("health").readDay(new Date(), h=>data[h.hr]+=h.steps);
|
||||
g.clear(1);
|
||||
Bangle.drawWidgets();
|
||||
g.reset();
|
||||
setButton(menuStepCount);
|
||||
barChart("HOUR", data);
|
||||
barChart(/*LANG*/"HOUR", data);
|
||||
}
|
||||
|
||||
function stepsPerDay() {
|
||||
E.showMessage(/*LANG*/"Loading...");
|
||||
var data = new Uint16Array(31);
|
||||
require("health").readDailySummaries(new Date(), h=>data[h.day]+=h.steps);
|
||||
g.clear(1);
|
||||
Bangle.drawWidgets();
|
||||
g.reset();
|
||||
setButton(menuStepCount);
|
||||
barChart("DAY", data);
|
||||
barChart(/*LANG*/"DAY", data);
|
||||
}
|
||||
|
||||
function hrmPerHour() {
|
||||
|
@ -75,11 +61,8 @@ function hrmPerHour() {
|
|||
if (h.bpm) cnt[h.hr]++;
|
||||
});
|
||||
data.forEach((d,i)=>data[i] = d/cnt[i]);
|
||||
g.clear(1);
|
||||
Bangle.drawWidgets();
|
||||
g.reset();
|
||||
setButton(menuHRM);
|
||||
barChart("HOUR", data);
|
||||
barChart(/*LANG*/"HOUR", data);
|
||||
}
|
||||
|
||||
function hrmPerDay() {
|
||||
|
@ -91,37 +74,27 @@ function hrmPerDay() {
|
|||
if (h.bpm) cnt[h.day]++;
|
||||
});
|
||||
data.forEach((d,i)=>data[i] = d/cnt[i]);
|
||||
g.clear(1);
|
||||
Bangle.drawWidgets();
|
||||
g.reset();
|
||||
setButton(menuHRM);
|
||||
barChart("DAY", data);
|
||||
barChart(/*LANG*/"DAY", data);
|
||||
}
|
||||
|
||||
function movementPerHour() {
|
||||
E.showMessage(/*LANG*/"Loading...");
|
||||
var data = new Uint16Array(24);
|
||||
require("health").readDay(new Date(), h=>data[h.hr]+=h.movement);
|
||||
g.clear(1);
|
||||
Bangle.drawWidgets();
|
||||
g.reset();
|
||||
setButton(menuMovement);
|
||||
barChart("HOUR", data);
|
||||
barChart(/*LANG*/"HOUR", data);
|
||||
}
|
||||
|
||||
function movementPerDay() {
|
||||
E.showMessage(/*LANG*/"Loading...");
|
||||
var data = new Uint16Array(31);
|
||||
require("health").readDailySummaries(new Date(), h=>data[h.day]+=h.movement);
|
||||
g.clear(1);
|
||||
Bangle.drawWidgets();
|
||||
g.reset();
|
||||
setButton(menuMovement);
|
||||
barChart("DAY", data);
|
||||
barChart(/*LANG*/"DAY", data);
|
||||
}
|
||||
|
||||
// Bar Chart Code
|
||||
|
||||
const w = g.getWidth();
|
||||
const h = g.getHeight();
|
||||
|
||||
|
@ -130,13 +103,10 @@ var chart_index;
|
|||
var chart_max_datum;
|
||||
var chart_label;
|
||||
var chart_data;
|
||||
var swipe_enabled = false;
|
||||
var btn;
|
||||
|
||||
// find the max value in the array, using a loop due to array size
|
||||
function max(arr) {
|
||||
var m = -Infinity;
|
||||
|
||||
for(var i=0; i< arr.length; i++)
|
||||
if(arr[i] > m) m = arr[i];
|
||||
return m;
|
||||
|
@ -145,10 +115,8 @@ function max(arr) {
|
|||
// find the end of the data, the array might be for 31 days but only have 2 days of data in it
|
||||
function get_data_length(arr) {
|
||||
var nlen = arr.length;
|
||||
|
||||
for(var i = arr.length - 1; i > 0 && arr[i] == 0; i--)
|
||||
nlen--;
|
||||
|
||||
return nlen;
|
||||
}
|
||||
|
||||
|
@ -167,15 +135,11 @@ function drawBarChart() {
|
|||
const bar_width = (w - 2) / 9; // we want 9 bars, bar 5 in the centre
|
||||
var bar_top;
|
||||
var bar;
|
||||
|
||||
g.setColor(g.theme.bg);
|
||||
g.fillRect(0,24,w,h);
|
||||
g.reset().clearRect(0,24,w,h);
|
||||
|
||||
for (bar = 1; bar < 10; bar++) {
|
||||
if (bar == 5) {
|
||||
g.setFont('6x8', 2);
|
||||
g.setFontAlign(0,-1);
|
||||
g.setColor(g.theme.fg);
|
||||
g.setFont('6x8', 2).setFontAlign(0,-1).setColor(g.theme.fg);
|
||||
g.drawString(chart_label + " " + (chart_index + bar -1) + " " + chart_data[chart_index + bar - 1], g.getWidth()/2, 150);
|
||||
g.setColor("#00f");
|
||||
} else {
|
||||
|
@ -189,45 +153,26 @@ function drawBarChart() {
|
|||
bar_top = bar_bot;
|
||||
|
||||
g.fillRect( 1 + (bar - 1)* bar_width, bar_bot, 1 + bar*bar_width, bar_top);
|
||||
g.setColor(g.theme.fg);
|
||||
g.drawRect( 1 + (bar - 1)* bar_width, bar_bot, 1 + bar*bar_width, bar_top);
|
||||
g.setColor(g.theme.fg).drawRect( 1 + (bar - 1)* bar_width, bar_bot, 1 + bar*bar_width, bar_top);
|
||||
}
|
||||
}
|
||||
|
||||
function next_bar() {
|
||||
chart_index = Math.min(data_len - 5, chart_index + 1);
|
||||
}
|
||||
|
||||
function prev_bar() {
|
||||
// HOUR data starts at index 0, DAY data starts at index 1
|
||||
chart_index = Math.max((chart_label == "DAY") ? -3 : -4, chart_index - 1);
|
||||
}
|
||||
|
||||
Bangle.on('swipe', dir => {
|
||||
if (!swipe_enabled) return;
|
||||
if (dir == 1) prev_bar(); else next_bar();
|
||||
drawBarChart();
|
||||
});
|
||||
|
||||
// use setWatch() as Bangle.setUI("updown",..) interacts with swipes
|
||||
function setButton(fn) {
|
||||
// cancel callback, otherwise a slight up down movement will show the E.showMenu()
|
||||
Bangle.setUI("updown", undefined);
|
||||
|
||||
if (process.env.HWVERSION == 1)
|
||||
btn = setWatch(fn, BTN2);
|
||||
else
|
||||
btn = setWatch(fn, BTN1);
|
||||
}
|
||||
|
||||
function clearButton() {
|
||||
if (btn !== undefined) {
|
||||
clearWatch(btn);
|
||||
btn = undefined;
|
||||
Bangle.setUI({mode:"custom",
|
||||
back:fn,
|
||||
swipe:(lr,ud) => {
|
||||
if (lr == 1) {
|
||||
// HOUR data starts at index 0, DAY data starts at index 1
|
||||
chart_index = Math.max((chart_label == /*LANG*/"DAY") ? -3 : -4, chart_index - 1);
|
||||
} else if (lr<0) {
|
||||
chart_index = Math.min(data_len - 5, chart_index + 1);
|
||||
} else {
|
||||
return fn();
|
||||
}
|
||||
drawBarChart();
|
||||
}});
|
||||
}
|
||||
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
|
||||
menuMain();
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "health",
|
||||
"name": "Health Tracking",
|
||||
"version": "0.15",
|
||||
"version": "0.17",
|
||||
"description": "Logs health data and provides an app to view it",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,system,health",
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
# HRM Motion Artifacts removal
|
||||
|
||||
Measurements from the build in PPG-Sensor (Photoplethysmograph) is sensitive to motion and can be corrupted with Motion Artifacts (MA). This module allows to remove these.
|
||||
|
||||
## Settings
|
||||
|
||||
* **MA removal**
|
||||
|
||||
Select the algorithm to Remove Motion artifacts:
|
||||
- None: (default) No Motion Artifact removal.
|
||||
- fft elim: (*experimental*) Remove Motion Artifacts by cutting out the frequencies from the HRM frequency spectrum that are noisy in acceleration spectrum. Under motion this can report a heart rate that is closer to the real one but will fail if motion frequency and heart rate overlap.
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
let bpm_corrected; // result of algorithm
|
||||
|
||||
const updateHrm = (bpm) => {
|
||||
bpm_corrected = bpm;
|
||||
};
|
||||
|
||||
Bangle.on('HRM', (hrm) => {
|
||||
if (bpm_corrected > 0) {
|
||||
// replace bpm data in event
|
||||
hrm.bpm_orig = hrm.bpm;
|
||||
hrm.confidence_orig = hrm.confidence;
|
||||
hrm.bpm = bpm_corrected;
|
||||
hrm.confidence = 0;
|
||||
}
|
||||
});
|
||||
|
||||
let run = () => {
|
||||
const settings = Object.assign({
|
||||
mAremoval: 0
|
||||
}, require("Storage").readJSON("hrmmar.json", true) || {});
|
||||
|
||||
// select motion artifact removal algorithm
|
||||
switch(settings.mAremoval) {
|
||||
case 1:
|
||||
require("hrmfftelim").run(settings, updateHrm);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// override setHRMPower so we can run our code on HRM enable
|
||||
const oldSetHRMPower = Bangle.setHRMPower;
|
||||
Bangle.setHRMPower = function(on, id) {
|
||||
if (on && run !== undefined) {
|
||||
run();
|
||||
run = undefined; // Make sure we run only once
|
||||
}
|
||||
return oldSetHRMPower(on, id);
|
||||
};
|
||||
}
|
|
@ -0,0 +1,190 @@
|
|||
exports.run = (settings, updateHrm) => {
|
||||
const SAMPLE_RATE = 12.5;
|
||||
const NUM_POINTS = 256; // fft size
|
||||
const ACC_PEAKS = 2; // remove this number of ACC peaks
|
||||
|
||||
// ringbuffers
|
||||
const hrmvalues = new Int16Array(8*SAMPLE_RATE);
|
||||
const accvalues = new Int16Array(8*SAMPLE_RATE);
|
||||
// fft buffers
|
||||
const hrmfftbuf = new Int16Array(NUM_POINTS);
|
||||
const accfftbuf = new Int16Array(NUM_POINTS);
|
||||
let BPM_est_1 = 0;
|
||||
let BPM_est_2 = 0;
|
||||
|
||||
let hrmdata;
|
||||
let idx=0, wraps=0;
|
||||
|
||||
// init settings
|
||||
Bangle.setOptions({hrmPollInterval: 40, powerSave: false}); // hrm=25Hz
|
||||
Bangle.setPollInterval(80); // 12.5Hz
|
||||
|
||||
calcfft = (values, idx, normalize, fftbuf) => {
|
||||
fftbuf.fill(0);
|
||||
let i_out=0;
|
||||
let avg = 0;
|
||||
if (normalize) {
|
||||
const sum = values.reduce((a, b) => a + b, 0);
|
||||
avg = sum/values.length;
|
||||
}
|
||||
// sort ringbuffer to fft buffer
|
||||
for(let i_in=idx; i_in<values.length; i_in++, i_out++) {
|
||||
fftbuf[i_out] = values[i_in]-avg;
|
||||
}
|
||||
for(let i_in=0; i_in<idx; i_in++, i_out++) {
|
||||
fftbuf[i_out] = values[i_in]-avg;
|
||||
}
|
||||
|
||||
E.FFT(fftbuf);
|
||||
return fftbuf;
|
||||
};
|
||||
|
||||
getMax = (values) => {
|
||||
let maxVal = -Number.MAX_VALUE;
|
||||
let maxIdx = 0;
|
||||
|
||||
values.forEach((value,i) => {
|
||||
if (value > maxVal) {
|
||||
maxVal = value;
|
||||
maxIdx = i;
|
||||
}
|
||||
});
|
||||
return {idx: maxIdx, val: maxVal};
|
||||
};
|
||||
|
||||
getSign = (value) => {
|
||||
return value < 0 ? -1 : 1;
|
||||
};
|
||||
|
||||
// idx in fft buffer to frequency
|
||||
getFftFreq = (idx, rate, size) => {
|
||||
return idx*rate/(size-1);
|
||||
};
|
||||
|
||||
// frequency to idx in fft buffer
|
||||
getFftIdx = (freq, rate, size) => {
|
||||
return Math.round(freq*(size-1)/rate);
|
||||
};
|
||||
|
||||
calc2ndDeriative = (values) => {
|
||||
const result = new Int16Array(values.length-2);
|
||||
for(let i=1; i<values.length-1; i++) {
|
||||
const diff = values[i+1]-2*values[i]+values[i-1];
|
||||
result[i-1] = diff;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const minFreqIdx = getFftIdx(1.0, SAMPLE_RATE, NUM_POINTS); // 60 BPM
|
||||
const maxFreqIdx = getFftIdx(3.0, SAMPLE_RATE, NUM_POINTS); // 180 BPM
|
||||
let rangeIdx = [0, maxFreqIdx-minFreqIdx]; // range of search for the next estimates
|
||||
const freqStep=getFftFreq(1, SAMPLE_RATE, NUM_POINTS)*60;
|
||||
const maxBpmDiffIdxDown = Math.ceil(5/freqStep); // maximum down BPM
|
||||
const maxBpmDiffIdxUp = Math.ceil(10/freqStep); // maximum up BPM
|
||||
|
||||
calculate = (idx) => {
|
||||
// fft
|
||||
const ppg_fft = calcfft(hrmvalues, idx, true, hrmfftbuf).subarray(minFreqIdx, maxFreqIdx+1);
|
||||
const acc_fft = calcfft(accvalues, idx, false, accfftbuf).subarray(minFreqIdx, maxFreqIdx+1);
|
||||
|
||||
// remove spectrum that have peaks in acc fft from ppg fft
|
||||
const accGlobalMax = getMax(acc_fft);
|
||||
const acc2nddiff = calc2ndDeriative(acc_fft); // calculate second derivative
|
||||
for(let iClean=0; iClean < ACC_PEAKS; iClean++) {
|
||||
// get max peak in ACC
|
||||
const accMax = getMax(acc_fft);
|
||||
|
||||
if (accMax.val >= 10 && accMax.val/accGlobalMax.val > 0.75) {
|
||||
// set all values in PPG FFT to zero until second derivative of ACC has zero crossing
|
||||
for (let k = accMax.idx-1; k>=0; k--) {
|
||||
ppg_fft[k] = 0;
|
||||
acc_fft[k] = -Math.abs(acc_fft[k]); // max(acc_fft) should no longer find this
|
||||
if (k-2 > 0 && getSign(acc2nddiff[k-1-2]) != getSign(acc2nddiff[k-2]) && Math.abs(acc_fft[k]) < accMax.val*0.75) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// set all values in PPG FFT to zero until second derivative of ACC has zero crossing
|
||||
for (let k = accMax.idx; k < acc_fft.length-1; k++) {
|
||||
ppg_fft[k] = 0;
|
||||
acc_fft[k] = -Math.abs(acc_fft[k]); // max(acc_fft) should no longer find this
|
||||
if (k-2 >= 0 && getSign(acc2nddiff[k+1-2]) != getSign(acc2nddiff[k-2]) && Math.abs(acc_fft[k]) < accMax.val*0.75) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// bpm result is maximum peak in PPG fft
|
||||
const hrRangeMax = getMax(ppg_fft.subarray(rangeIdx[0], rangeIdx[1]));
|
||||
const hrTotalMax = getMax(ppg_fft);
|
||||
const maxDiff = hrTotalMax.val/hrRangeMax.val;
|
||||
let idxMaxPPG = hrRangeMax.idx+rangeIdx[0]; // offset range limit
|
||||
|
||||
if ((maxDiff > 3 && idxMaxPPG != hrTotalMax.idx) || hrRangeMax.val === 0) { // prevent tracking from loosing the real heart rate by checking the full spectrum
|
||||
if (hrTotalMax.idx > idxMaxPPG) {
|
||||
idxMaxPPG = idxMaxPPG+Math.ceil(6/freqStep); // step 6 BPM up into the direction of max peak
|
||||
} else {
|
||||
idxMaxPPG = idxMaxPPG-Math.ceil(2/freqStep); // step 2 BPM down into the direction of max peak
|
||||
}
|
||||
}
|
||||
|
||||
idxMaxPPG = idxMaxPPG + minFreqIdx;
|
||||
const BPM_est_0 = getFftFreq(idxMaxPPG, SAMPLE_RATE, NUM_POINTS)*60;
|
||||
|
||||
// smooth with moving average
|
||||
let BPM_est_res;
|
||||
if (BPM_est_2 > 0) {
|
||||
BPM_est_res = 0.9*BPM_est_0 + 0.05*BPM_est_1 + 0.05*BPM_est_2;
|
||||
} else {
|
||||
BPM_est_res = BPM_est_0;
|
||||
}
|
||||
|
||||
return BPM_est_res.toFixed(1);
|
||||
};
|
||||
|
||||
Bangle.on('HRM-raw', (hrm) => {
|
||||
hrmdata = hrm;
|
||||
});
|
||||
|
||||
Bangle.on('accel', (acc) => {
|
||||
if (hrmdata !== undefined) {
|
||||
hrmvalues[idx] = hrmdata.filt;
|
||||
accvalues[idx] = acc.x*1000 + acc.y*1000 + acc.z*1000;
|
||||
idx++;
|
||||
if (idx >= 8*SAMPLE_RATE) {
|
||||
idx = 0;
|
||||
wraps++;
|
||||
}
|
||||
|
||||
if (idx % (SAMPLE_RATE*2) == 0) { // every two seconds
|
||||
if (wraps === 0) { // use rate of firmware until hrmvalues buffer is filled
|
||||
updateHrm(undefined);
|
||||
BPM_est_2 = BPM_est_1;
|
||||
BPM_est_1 = hrmdata.bpm;
|
||||
} else {
|
||||
let bpm_result;
|
||||
if (hrmdata.confidence >= 90) { // display firmware value if good
|
||||
bpm_result = hrmdata.bpm;
|
||||
updateHrm(undefined);
|
||||
} else {
|
||||
bpm_result = calculate(idx);
|
||||
bpm_corrected = bpm_result;
|
||||
updateHrm(bpm_result);
|
||||
}
|
||||
BPM_est_2 = BPM_est_1;
|
||||
BPM_est_1 = bpm_result;
|
||||
|
||||
// set search range of next BPM
|
||||
const est_res_idx = getFftIdx(bpm_result/60, SAMPLE_RATE, NUM_POINTS)-minFreqIdx;
|
||||
rangeIdx = [est_res_idx-maxBpmDiffIdxDown, est_res_idx+maxBpmDiffIdxUp];
|
||||
if (rangeIdx[0] < 0) {
|
||||
rangeIdx[0] = 0;
|
||||
}
|
||||
if (rangeIdx[1] > maxFreqIdx-minFreqIdx) {
|
||||
rangeIdx[1] = maxFreqIdx-minFreqIdx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"id": "hrmmar",
|
||||
"name": "HRM Motion Artifacts removal",
|
||||
"shortName":"HRM MA removal",
|
||||
"icon": "app.png",
|
||||
"version":"0.01",
|
||||
"description": "Removes Motion Artifacts in Bangle.js's heart rate sensor data.",
|
||||
"type": "bootloader",
|
||||
"tags": "health",
|
||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name":"hrmmar.boot.js","url":"boot.js"},
|
||||
{"name":"hrmfftelim","url":"fftelim.js"},
|
||||
{"name":"hrmmar.settings.js","url":"settings.js"}
|
||||
],
|
||||
"data": [{"name":"hrmmar.json"}]
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
(function(back) {
|
||||
var FILE = "hrmmar.json";
|
||||
// Load settings
|
||||
var settings = Object.assign({
|
||||
mAremoval: 0,
|
||||
}, require('Storage').readJSON(FILE, true) || {});
|
||||
|
||||
function writeSettings() {
|
||||
require('Storage').writeJSON(FILE, settings);
|
||||
}
|
||||
|
||||
// Show the menu
|
||||
E.showMenu({
|
||||
"" : { "title" : "HRM MA removal" },
|
||||
"< Back" : () => back(),
|
||||
'MA removal': {
|
||||
value: settings.mAremoval,
|
||||
min: 0, max: 1,
|
||||
format: v => ["None", "fft elim."][v],
|
||||
onchange: v => {
|
||||
settings.mAremoval = v;
|
||||
writeSettings();
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
|
@ -9,3 +9,4 @@
|
|||
0.09: Enable 'ams' on new firmwares (ams/ancs can now be enabled individually) (fix #1365)
|
||||
0.10: Added more bundleIds
|
||||
0.11: Added letters with caron to unicodeRemap, to properly display messages in Czech language
|
||||
0.12: Use new message library
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
// Config app not implemented yet
|
||||
setTimeout(()=>load("messages.app.js"),10);
|
||||
setTimeout(()=>require("messages").openGUI(),10);
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"id": "ios",
|
||||
"name": "iOS Integration",
|
||||
"version": "0.11",
|
||||
"version": "0.12",
|
||||
"description": "Display notifications/music/etc from iOS devices",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,system,ios,apple,messages,notifications",
|
||||
"dependencies": {"messages":"app"},
|
||||
"dependencies": {"messages":"module"},
|
||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
|
|
|
@ -19,3 +19,4 @@
|
|||
0.17: Don't display 'Loading...' now the watch has its own loading screen
|
||||
0.18: Add 'back' icon in top-left to go back to clock
|
||||
0.19: Fix regression after back button added (returnToClock was called twice!)
|
||||
0.20: Use Bangle.showClock for changing to clock
|
||||
|
|
|
@ -42,16 +42,6 @@ let apps = launchCache.apps;
|
|||
if (!settings.fullscreen)
|
||||
Bangle.loadWidgets();
|
||||
|
||||
let returnToClock = function() {
|
||||
// unload everything manually
|
||||
// ... or we could just call `load();` but it will be slower
|
||||
Bangle.setUI(); // remove scroller's handling
|
||||
if (lockTimeout) clearTimeout(lockTimeout);
|
||||
Bangle.removeListener("lock", lockHandler);
|
||||
// now load the default clock - just call .bootcde as this has the code already
|
||||
setTimeout(eval,0,s.read(".bootcde"));
|
||||
}
|
||||
|
||||
E.showScroller({
|
||||
h : 64*scaleval, c : apps.length,
|
||||
draw : (i, r) => {
|
||||
|
@ -74,7 +64,12 @@ E.showScroller({
|
|||
load(app.src);
|
||||
}
|
||||
},
|
||||
back : returnToClock // button press or tap in top left calls returnToClock now
|
||||
back : Bangle.showClock, // button press or tap in top left shows clock now
|
||||
remove : () => {
|
||||
// cleanup the timeout to not leave anything behind after being removed from ram
|
||||
if (lockTimeout) clearTimeout(lockTimeout);
|
||||
Bangle.removeListener("lock", lockHandler);
|
||||
}
|
||||
});
|
||||
g.flip(); // force a render before widgets have finished drawing
|
||||
|
||||
|
@ -85,7 +80,7 @@ let lockHandler = function(locked) {
|
|||
if (lockTimeout) clearTimeout(lockTimeout);
|
||||
lockTimeout = undefined;
|
||||
if (locked)
|
||||
lockTimeout = setTimeout(returnToClock, 10000);
|
||||
lockTimeout = setTimeout(Bangle.showClock, 10000);
|
||||
}
|
||||
Bangle.on("lock", lockHandler);
|
||||
if (!settings.fullscreen) // finally draw widgets
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "launch",
|
||||
"name": "Launcher",
|
||||
"shortName": "Launcher",
|
||||
"version": "0.19",
|
||||
"version": "0.20",
|
||||
"description": "This is needed to display a menu allowing you to choose your own applications. You can replace this with a customised launcher.",
|
||||
"readme": "README.md",
|
||||
"icon": "app.png",
|
||||
|
|
|
@ -3,3 +3,4 @@
|
|||
0.03: Settings page now uses built-in min/max/wrap (fix #1607)
|
||||
0.04: Add masking widget input to other apps (using espruino/Espruino#2151), add a oversize option to increase the touch area.
|
||||
0.05: Prevent drawing into app area.
|
||||
0.06: Fix issue where .draw was being called by reference (not allowing widgets to be hidden)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "lightswitch",
|
||||
"name": "Light Switch Widget",
|
||||
"shortName": "Light Switch",
|
||||
"version": "0.05",
|
||||
"version": "0.06",
|
||||
"description": "A fast way to switch LCD backlight on/off, change the brightness and show the lock status. All in one widget.",
|
||||
"icon": "images/app.png",
|
||||
"screenshots": [
|
||||
|
|
|
@ -224,28 +224,20 @@
|
|||
|
||||
// main widget function //
|
||||
// display and setup/reset function
|
||||
draw: function(locked) {
|
||||
draw: function() {
|
||||
// setup shortcut to this widget
|
||||
var w = WIDGETS.lightswitch;
|
||||
|
||||
// set lcd brightness on unlocking
|
||||
// all other cases are catched by the boot file
|
||||
if (locked === false) Bangle.setLCDBrightness(w.isOn ? w.value : 0);
|
||||
|
||||
// read lock status
|
||||
locked = Bangle.isLocked();
|
||||
var locked = Bangle.isLocked();
|
||||
|
||||
// remove listeners to prevent uncertainties
|
||||
Bangle.removeListener("lock", w.draw);
|
||||
Bangle.removeListener("touch", w.touchListener);
|
||||
Bangle.removeListener("tap", require("lightswitch.js").tapListener);
|
||||
|
||||
// draw widget icon
|
||||
w.drawIcon(locked);
|
||||
|
||||
// add lock listener
|
||||
Bangle.on("lock", w.draw);
|
||||
|
||||
// add touch listener to control the light depending on settings at first position
|
||||
if (w.touchOn === "always" || !global.__FILE__ ||
|
||||
w.touchOn.includes(__FILE__) ||
|
||||
|
@ -260,6 +252,14 @@
|
|||
}
|
||||
});
|
||||
|
||||
Bangle.on("lock", locked => {
|
||||
var w = WIDGETS.lightswitch;
|
||||
// set lcd brightness on unlocking
|
||||
// all other cases are catched by the boot file
|
||||
if (locked === false) Bangle.setLCDBrightness(w.isOn ? w.value : 0);
|
||||
w.draw()
|
||||
});
|
||||
|
||||
// clear variable
|
||||
settings = undefined;
|
||||
delete settings;
|
||||
})()
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
0.01: New App!
|
||||
0.02: Add 'messages' library
|
||||
0.03: Fixes for Bangle.js 1
|
||||
0.04: Add require("messages").clearAll()
|
||||
0.05: Handling of message actions (ok/clear)
|
||||
0.06: New messages now go at the start (fix #898)
|
||||
Answering true/false now exits the messages app if no new messages
|
||||
Back now marks a message as read
|
||||
Clicking top-left opens a menu which allows you to delete a message or mark unread
|
||||
0.07: Added settings menu with option to choose vibrate pattern and frequency (fix #909)
|
||||
0.08: Fix rendering of long messages (fix #969)
|
||||
buzz on new message (fix #999)
|
||||
0.09: Message now disappears after 60s if no action taken and clock loads (fix 922)
|
||||
Fix phone icon (#1014)
|
||||
0.10: Respect the 'new' attribute if it was set from iOS integrations
|
||||
0.11: Open app when touching the widget (Bangle.js 2 only)
|
||||
0.12: Extra app-specific notification icons
|
||||
New animated notification icon (instead of large blinking 'MESSAGES')
|
||||
Added screenshots
|
||||
0.13: Add /*LANG*/ comments for internationalisation
|
||||
Add 'Delete All' option to message options
|
||||
Now update correctly when 'require("messages").clearAll()' is called
|
||||
0.14: Hide widget when all unread notifications are dismissed from phone
|
||||
0.15: Don't buzz when Quiet Mode is active
|
||||
0.16: Fix text wrapping so it fits the screen even if title is big (fix #1147)
|
||||
0.17: Fix: Get dynamic dimensions of notify icon, fixed notification font
|
||||
0.18: Use app-specific icon colors
|
||||
Spread message action buttons out
|
||||
Back button now goes back to list of messages
|
||||
If showMessage called with no message (eg all messages deleted) now return to the clock (fix #1267)
|
||||
0.19: Use a larger font for message text if it'll fit
|
||||
0.20: Allow tapping on the body to show a scrollable view of the message and title in a bigger font (fix #1405, #1031)
|
||||
0.21: Improve list readability on dark theme
|
||||
0.22: Add Home Assistant icon
|
||||
Allow repeat to be switched Off, so there is no buzzing repetition.
|
||||
Also gave the widget a pixel more room to the right
|
||||
0.23: Change message colors to match current theme instead of using green
|
||||
Now attempt to use Large/Big/Medium fonts, and allow minimum font size to be configured
|
||||
0.24: Remove left-over debug statement
|
||||
0.25: Fix widget memory usage issues if message received and watch repeatedly calls Bangle.drawWidgets (fix #1550)
|
||||
0.26: Setting to auto-open music
|
||||
0.27: Add 'mark all read' option to popup menu (fix #1624)
|
||||
0.28: Option to auto-unlock the watch when a new message arrives
|
||||
0.29: Fix message list overwrites on Bangle.js 1 (fix #1642)
|
||||
0.30: Add new Icons (Youtube, Twitch, MS TODO, Teams, Snapchat, Signal, Post & DHL, Nina, Lieferando, Kalender, Discord, Corona Warn, Bibel)
|
||||
0.31: Option to disable icon flashing
|
||||
0.32: Added an option to allow quiet mode to override message auto-open
|
||||
0.33: Timeout from the message list screen if the message being displayed is removed and there is a timer going
|
||||
0.34: Don't buzz for 'map' update messages
|
||||
0.35: Reset graphics colors before rendering a message (possibly fix #1752)
|
||||
0.36: Ensure a new message plus an almost immediate deletion of that message doesn't load the messages app (fix #1362)
|
||||
0.37: Now use the setUI 'back' icon in the top left rather than specific buttons/menu items
|
||||
0.38: Add telegram foss handling
|
||||
0.39: Set default color for message icons according to theme
|
||||
0.40: Use default Bangle formatter for booleans
|
||||
0.41: Add notification icons in the widget
|
||||
0.42: Fix messages ignoring "Vibrate: Off" setting
|
||||
0.43: Add new Icons (Airbnb, warnwetter)
|
||||
0.44: Separate buzz pattern for incoming calls
|
||||
0.45: Added new app colors and icons
|
||||
0.46: Add 'Vibrate Timer' option to set how long to vibrate for, and fix Repeat:off
|
||||
Fix message removal from widget bar (previously caused exception as .hide has been removed)
|
||||
0.47: Add new Icons (Nextbike, Mattermost, etc.)
|
||||
0.48: When getting new message from the clock, only buzz once the messages app is loaded
|
||||
0.49: Change messages icon (to fit within 24px) and ensure widget renders icons centrally
|
||||
0.50: Add `getMessages` and `status` functions to library
|
||||
Option to disable auto-open of messages
|
||||
Option to make message icons monochrome (not colored)
|
||||
messages widget buzz now returns a promise
|
||||
0.51: Emit "message events"
|
||||
Setting to hide widget
|
||||
Add custom event handlers to prevent default app form loading
|
||||
Move WIDGETS.messages.buzz() to require("messages").buzz()
|
||||
0.52: Fix require("messages").buzz() regression
|
||||
Fix background color in messages list after one unread message is shown
|
||||
0.53: Messages now uses Bangle.load() to load messages app faster (if possible)
|
||||
0.54: Move icons out to messageicons module
|
||||
0.55: Rename to messagegui, move global message handling library to message module
|
||||
Move widget to widmessage
|
||||
0.56: Fix handling of music messages
|
||||
0.57: Fix "unread Timeout" = off (previously defaulted to 60s)
|
|
@ -0,0 +1,68 @@
|
|||
# Messages app
|
||||
|
||||
Default app to handle the display of messages and message notifications. It allows
|
||||
them to be listed, viewed, and responded to.
|
||||
It is installed automatically if you install `Android Integration` or `iOS Integration`.
|
||||
|
||||
It is a replacement for the old `notify`/`gadgetbridge` apps.
|
||||
|
||||
|
||||
## Settings
|
||||
|
||||
You can change settings by going to the global `Settings` app, then `App Settings`
|
||||
and `Messages`:
|
||||
|
||||
* `Vibrate` - This is the pattern of buzzes that should be made when a new message is received
|
||||
* `Vibrate for calls` - This is the pattern of buzzes that should be made when an incoming call is received
|
||||
* `Repeat` - How often should buzzes repeat - the default of 4 means the Bangle will buzz every 4 seconds
|
||||
* `Vibrate Timer` - When a new message is received when in a non-clock app, we display the message icon and
|
||||
buzz every `Repeat` seconds. This is how long we continue to do that.
|
||||
* `Unread Timer` - When a new message is received when showing the clock we go into the Messages app.
|
||||
If there is no user input for this amount of time then the app will exit and return
|
||||
to the clock where a ringing bell will be shown in the Widget bar.
|
||||
* `Min Font` - The minimum font size used when displaying messages on the screen. A bigger font
|
||||
is chosen if there isn't much message text, but this specifies the smallest the font should get before
|
||||
it starts getting clipped.
|
||||
* `Auto-Open Music` - Should the app automatically open when the phone starts playing music?
|
||||
* `Unlock Watch` - Should the app unlock the watch when a new message arrives, so you can touch the buttons at the bottom of the app?
|
||||
|
||||
## New Messages
|
||||
|
||||
When a new message is received:
|
||||
|
||||
* If you're in an app, the Bangle will buzz and a message icon appears in the Widget bar. You can tap this icon to view the message.
|
||||
* If you're in a clock, the Messages app will automatically start and show the message
|
||||
|
||||
When a message is shown, you'll see a screen showing the message title and text.
|
||||
|
||||
* The 'back-arrow' button (or physical button on Bangle.js 2) goes back to Messages, marking the current message as read.
|
||||
* The top-left icon shows more options, for instance deleting the message of marking unread
|
||||
* On Bangle.js 2 you can tap on the message body to view a scrollable version of the title and text (or can use the top-left icon + `View Message`)
|
||||
* If shown, the 'tick' button:
|
||||
* **Android** opens the notification on the phone
|
||||
* **iOS** responds positively to the notification (accept call/etc)
|
||||
* If shown, the 'cross' button:
|
||||
* **Android** dismisses the notification on the phone
|
||||
* **iOS** responds negatively to the notification (dismiss call/etc)
|
||||
|
||||
## Images
|
||||
_1. Screenshot of a notification_
|
||||
|
||||

|
||||
|
||||
|
||||
## Requests
|
||||
|
||||
Please file any issues on https://github.com/espruino/BangleApps/issues/new?title=messages%20app
|
||||
|
||||
## Creator
|
||||
|
||||
Gordon Williams
|
||||
|
||||
## Contributors
|
||||
|
||||
[Jeroen Peters](https://github.com/jeroenpeters1986)
|
||||
|
||||
## Attributions
|
||||
|
||||
Icons used in this app are from https://icons8.com
|
|
@ -1,5 +1,5 @@
|
|||
/* Called when we have a new message when we're in the clock...
|
||||
BUZZ_ON_NEW_MESSAGE is set so when messages.app.js loads it knows
|
||||
BUZZ_ON_NEW_MESSAGE is set so when messagegui.app.js loads it knows
|
||||
that it should buzz */
|
||||
global.BUZZ_ON_NEW_MESSAGE = true;
|
||||
eval(require("Storage").read("messages.app.js"));
|
||||
eval(require("Storage").read("messagegui.app.js"));
|
|
@ -19,7 +19,6 @@ require("messages").pushMessage({"t":"add","id":1,"src":"Maps","title":"0 yd - H
|
|||
// call
|
||||
require("messages").pushMessage({"t":"add","id":"call","src":"Phone","title":"Bob","body":"12421312",positive:true,negative:true})
|
||||
*/
|
||||
|
||||
var Layout = require("Layout");
|
||||
var settings = require('Storage').readJSON("messages.settings.json", true) || {};
|
||||
var fontSmall = "6x8";
|
||||
|
@ -49,8 +48,11 @@ to the clock. */
|
|||
var unreadTimeout;
|
||||
/// List of all our messages
|
||||
var MESSAGES = require("messages").getMessages();
|
||||
if (!Array.isArray(MESSAGES)) MESSAGES=[];
|
||||
var onMessagesModified = function(msg) {
|
||||
|
||||
var onMessagesModified = function(type,msg) {
|
||||
if (msg.handled) return;
|
||||
msg.handled = true;
|
||||
require("messages").apply(msg, MESSAGES);
|
||||
// TODO: if new, show this new one
|
||||
if (msg && msg.id!=="music" && msg.new && active!="map" &&
|
||||
!((require('Storage').readJSON('setting.json', 1) || {}).quiet)) {
|
||||
|
@ -62,9 +64,15 @@ var onMessagesModified = function(msg) {
|
|||
}
|
||||
showMessage(msg&&msg.id);
|
||||
};
|
||||
Bangle.on("message", onMessagesModified);
|
||||
|
||||
function saveMessages() {
|
||||
require("Storage").writeJSON("messages.json",MESSAGES)
|
||||
require("messages").write(MESSAGES.map(m => {
|
||||
delete m.show;
|
||||
return m;
|
||||
}));
|
||||
}
|
||||
E.on("kill", saveMessages);
|
||||
|
||||
function showMapMessage(msg) {
|
||||
active = "map";
|
||||
|
@ -104,9 +112,11 @@ function showMapMessage(msg) {
|
|||
Bangle.setUI({mode:"updown", back: back}, back); // any input takes us back
|
||||
}
|
||||
|
||||
var updateLabelsInterval;
|
||||
let updateLabelsInterval,
|
||||
music = {artist: "", album: "", title: ""}; // defaults, so e.g. msg.title.length doesn't error
|
||||
function showMusicMessage(msg) {
|
||||
active = "music";
|
||||
msg = Object.assign(music, msg); // combine+remember "musicinfo" and "musicstate" messages
|
||||
openMusic = msg.state=="play";
|
||||
var trackScrollOffset = 0;
|
||||
var artistScrollOffset = 0;
|
||||
|
@ -355,12 +365,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,13 +442,13 @@ function cancelReloadTimeout() {
|
|||
g.clear();
|
||||
|
||||
Bangle.loadWidgets();
|
||||
require("messages").toggleWidget(false);
|
||||
Bangle.drawWidgets();
|
||||
|
||||
setTimeout(() => {
|
||||
var unreadTimeoutMillis = (settings.unreadTimeout || 60) * 1000;
|
||||
if (unreadTimeoutMillis) {
|
||||
unreadTimeout = setTimeout(load, unreadTimeoutMillis);
|
||||
}
|
||||
if (!isFinite(settings.unreadTimeout)) settings.unreadTimeout=60;
|
||||
if (settings.unreadTimeout)
|
||||
unreadTimeout = setTimeout(load, settings.unreadTimeout*1000);
|
||||
// only openMusic on launch if music is new
|
||||
var newMusic = MESSAGES.some(m => m.id === "music" && m.new);
|
||||
checkMessages({ clockIfNoMsg: 0, clockIfAllRead: 0, showMsgIfUnread: 1, openMusic: newMusic && settings.openMusic });
|
After Width: | Height: | Size: 917 B |
|
@ -0,0 +1,3 @@
|
|||
(function() {
|
||||
Bangle.on("message", (type, msg) => require("messagegui").listener(type, msg));
|
||||
})();
|
|
@ -0,0 +1,60 @@
|
|||
/**
|
||||
* Listener set up in boot.js, calls into here to keep boot.js short
|
||||
*/
|
||||
exports.listener = function(type, msg) {
|
||||
// Default handler: Launch the GUI for all unhandled messages (except music if disabled in settings)
|
||||
if (msg.handled || (global.__FILE__ && __FILE__.startsWith('messagegui.'))) return; // already handled or app open
|
||||
|
||||
// if no new messages now, make sure we don't load the messages app
|
||||
if (exports.messageTimeout && !msg.new && require("messages").status(msg) !== "new") {
|
||||
clearTimeout(exports.messageTimeout);
|
||||
delete exports.messageTimeout;
|
||||
}
|
||||
if (msg.t==="remove") return;
|
||||
|
||||
const appSettings = require("Storage").readJSON("messages.settings.json", 1) || {};
|
||||
let loadMessages = (Bangle.CLOCK || event.important);
|
||||
if (type==="music") {
|
||||
if (Bangle.CLOCK && msg.state && msg.title && appSettings.openMusic) loadMessages = true;
|
||||
else return;
|
||||
}
|
||||
require("messages").save(msg);
|
||||
msg.handled = true;
|
||||
if ((msg.t!=="add" || !msg.new) && (type!=="music")) { // music always has t:"modify"
|
||||
return;
|
||||
}
|
||||
|
||||
const quiet = (require("Storage").readJSON("setting.json", 1) || {}).quiet;
|
||||
const unlockWatch = appSettings.unlockWatch;
|
||||
// don't auto-open messages in quiet mode if quietNoAutOpn is true
|
||||
if ((quiet && appSettings.quietNoAutOpn) || appSettings.noAutOpn)
|
||||
loadMessages = false;
|
||||
|
||||
// after a delay load the app, to ensure we have all the messages
|
||||
if (exports.messageTimeout) clearTimeout(exports.messageTimeout);
|
||||
exports.messageTimeout = setTimeout(function() {
|
||||
delete exports.messageTimeout;
|
||||
if (type!=="music") {
|
||||
if (!loadMessages) return require("messages").buzz(msg.src); // no opening the app, just buzz
|
||||
if (!quiet && unlockWatch) {
|
||||
Bangle.setLocked(false);
|
||||
Bangle.setLCDPower(1); // turn screen on
|
||||
}
|
||||
}
|
||||
exports.open(msg);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
/**
|
||||
* Launch GUI app with given message
|
||||
* @param {object} msg
|
||||
*/
|
||||
exports.open = function(msg) {
|
||||
if (msg && msg.id && !msg.show) {
|
||||
// store which message to load
|
||||
msg.show = 1;
|
||||
require("messages").save(msg, {force: 1});
|
||||
}
|
||||
|
||||
Bangle.load((msg && msg.new && msg.id!=="music") ? "messagegui.new.js" : "messagegui.app.js");
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"id": "messagegui",
|
||||
"name": "Message UI",
|
||||
"version": "0.57",
|
||||
"description": "Default app to display notifications from iOS and Gadgetbridge/Android",
|
||||
"icon": "app.png",
|
||||
"type": "app",
|
||||
"tags": "tool,system",
|
||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||
"dependencies" : { "messageicons":"module" },
|
||||
"provides_modules": ["messagegui"],
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name":"messagegui","url":"lib.js"},
|
||||
{"name":"messagegui.app.js","url":"app.js"},
|
||||
{"name":"messagegui.new.js","url":"app-newmessage.js"},
|
||||
{"name":"messagegui.boot.js","url":"boot.js"},
|
||||
{"name":"messagegui.img","url":"app-icon.js","evaluate":true}
|
||||
],
|
||||
"screenshots": [{"url":"screenshot.png"}],
|
||||
"sortorder": -9
|
||||
}
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
@ -5,44 +5,73 @@ exports.getImage = function(msg) {
|
|||
*/
|
||||
if (msg.img) return atob(msg.img);
|
||||
const s = (("string"=== typeof msg) ? msg : (msg.src || "")).toLowerCase();
|
||||
if (s=="airbnb") return atob("GBgBAAAAAAAAAAAAADwAAH4AAGYAAMMAAIEAAYGAAYGAAzzAA2bABmZgBmZgDGYwDDwwCDwQCBgQDDwwB+fgA8PAAAAAAAAAAAAA");
|
||||
if (s=="alarm" || s =="alarmclockreceiver") return atob("GBjBAP////8AAAAAAAACAEAHAOAefng5/5wTgcgHAOAOGHAMGDAYGBgYGBgYGBgYGBgYDhgYBxgMATAOAHAHAOADgcAB/4AAfgAAAAAAAAA=");
|
||||
if (s=="airbnb") return atob("GBgBAAAAADwAAH4AAMMAAIMAAYGAAQGAAwDAAwDABjxgBn5gDMMwDMMwGMMYGMMYMGYMMGYMIDwEIBgEIDwEMH4MHee4D4HwAAAA"); // icons/airbnb.png
|
||||
if (s=="alarm" || s =="alarmclockreceiver") return atob("GBgBAAAAAAAAAgBABwDgHn54Of+cE8PIBwDgDhhwDBgwHBg4GBgYGBgYGBgYGA4YHAc4DAEwDgBwBwDgA8PAAf+AAH4AAAAAAAAA"); // icons/alarm.png
|
||||
if (s=="amazon shopping") return atob("GBgBAAAAAP8AAf+AA//AA+fAA8PAAIPAAD/AAP/AA//AA+PAB8PAB8fAB8fgB//gA//gA/3AAPCecAAeOAAeDwH0B//kAf+AAAAA"); // icons/amazon.png
|
||||
if (s=="bibel") return atob("GBgBAAAAA//wD//4D//4H//4H/f4H/f4H+P4H4D4H4D4H/f4H/f4H/f4H/f4H/f4H//4H//4H//4GAAAEAAAEAAACAAAB//4AAAA");
|
||||
if (s=="bitwarden" || s=="1password" || s=="lastpass" || s=="dashlane") return atob("GBgBAAAAABgAAP8AA//AD4/wHg/4GA/4GA/4GA/4GA/4GA/4GA/4H/AYH/AYH/A4D/AwD/BwB/BgB/DgA/HAAfeAAP8AADwAAAAA"); // icons/security.png
|
||||
if (s=="bring") return atob("GBgBAAAAAAAAAAAAAAAAAHwAAFoAAf+AA/+AA/+AA/+AA/eAA+eAA0+AAx+AA7+AA/+AA//AA/+AAf8AAAIAAAAAAAAAAAAAAAAA");
|
||||
if (s=="calendar" || s=="etar") return atob("GBiBAAAAAAAAAAAAAA//8B//+BgAGBgAGBgAGB//+B//+B//+B9m2B//+B//+Btm2B//+B//+Btm+B//+B//+A//8AAAAAAAAAAAAA==");
|
||||
if (s=="corona-warn") return atob("GBgBAAAAABwAAP+AAf/gA//wB/PwD/PgDzvAHzuAP8EAP8AAPAAAPMAAP8AAH8AAHzsADzuAB/PAB/PgA//wAP/gAH+AAAwAAAAA");
|
||||
if (s=="discord") return atob("GBgBAAAAAAAAAAAAAIEABwDgDP8wH//4H//4P//8P//8P//8Pjx8fhh+fzz+f//+f//+e//ePH48HwD4AgBAAAAAAAAAAAAAAAAA");
|
||||
if (s=="facebook" || s=="messenger") return atob("GBiBAAAAAAAAAAAYAAD/AAP/wAf/4A/48A/g8B/g+B/j+B/n+D/n/D8A/B8A+B+B+B/n+A/n8A/n8Afn4APnwADnAAAAAAAAAAAAAA==");
|
||||
if (s=="chat") return atob("GBgBAAAAAf/8A//+A//+A//+OAB+e/8+e/++e/++e/++e/++e/++e/++ef+8fAAAf//Af//Af//Af//Af/+AcAAAYAAAQAAAAAAA"); // icons/google chat.png
|
||||
if (s=="chrome") return atob("GBgBAAAAAAAAAP8AA//AB+fgDwDwHgB4HAA4Pj/8OmYcO8McMYEMMYEMOMMcOGccOD4cHAw4Hgx4DxjwB//gA//AAP8AAAAAAAAA"); // icons/chrome.png
|
||||
if (s=="corona-warn") return atob("GBgBAAAAAAAAABgAABgABhhgDn5wD//wA8PAA+fAB2bgBgBgPpl8Ppl8BgBgB2bgA+fAA8PAD//wDn5wBhhgABgAABgAAAAAAAAA"); // icons/coronavirus.png
|
||||
if (s=="bmo" || s=="desjardins" || s=="rbc mobile" || s=="nbc" || s=="rabobank" || s=="scotiabank" || s=="td (canada)") return atob("GBgBAAAAADgAAP4AAe8AB4PAHgDwP//4P//4AAAAAAAADjjgDjjgDjjgDjjgDjjgDjjgDjjgAAAAAAAAP//4P//4AAAAAAAAAAAA"); // icons/bank.png
|
||||
if (s=="discord") return atob("GBgBAAAAAAAAAAAAAAAAA4HAD//wH//4H//4P//8P//8P//8fn5+fDw+fDw+fn5+f//+f//+ff++PgB8DgBwAAAAAAAAAAAAAAAA"); // icons/discord.png
|
||||
if (s=="drive") return atob("GBgBAAAAAAAAAH8AAH8AAT+AA7/AA9/AB8/gB+/gD+fwD+fwH8P4P8P8P4H8fwAAf3/+Pn/8Pv/8HP/4Df/wC//wAAAAAAAAAAAA"); // icons/google drive.png
|
||||
if (s=="element") return atob("GBgBAAAAAHwAAH4AAH8AAAeAAePAB+HAD+DgHgDgPADuOADucAAOcAAOdwAcdwA8BwB4BwfwA4fgA8eAAeAAAP4AAH4AAD4AAAAA"); // icons/matrix element.png
|
||||
if (s=="facebook") return atob("GBgBAAAAAAAAAH4AAf+AB//gD//wD/DwH+D4H+P4P+f8P+f8P+f8PwD8PwD8PwD8H+f4H+f4D+fwD+fwB+fgAeeAAOcAAAAAAAAA"); // icons/facebook.png
|
||||
if (s=="messenger") return atob("GBgBAAAAAAAAAP8AA//AB//gD//wH//4H//4P//8P9+8P458PwB8PgD8PnH8Pfv8H//4H//4D//wB//gB//AB/8AAwAAAAAAAAAA"); // icons/facebook messenger.png
|
||||
if (s=="firefox" || s=="firefox beta" || s=="firefox nightly") return atob("GBgBAAAAAAAAAAMAAAcAAAeABA/ADY/gH4P4H4H4H8H8P/H8P+D8PwD8PwD8PwD8H4H4H8P4H//4D//wB//gA//AAP8AAAAAAAAA"); // icons/firefox.png
|
||||
if (s=="f-droid" || s=="neo store" || s=="aurora droid") return atob("GBgBAAAAQAACYAAGP//8H//4H//4HH44HH44H//4AAAAH//4H8P4H734H374HsN4Hvl4Hv14Hvl4HsN4H374H734H8P4D//wAAAA"); // icons/security.png
|
||||
if (s=="github") return atob("GBgBAAAAAAAAAH4AAf+AB//gD//wDv9wHgB4HgB4PAA8PAA8PAA8PAA8PAA8PgB8HwD4G8P4DcPwDgPwB4PgAcOAAAAAAAAAAAAA"); // icons/github.png
|
||||
if (s=="gitlab") return atob("GBgBAAAABAAgDAAwDAAwHgB4HgB4PgB8PwD8P//8f//+f//+f//+f//+f//+f//+P//8H//4D//wA//AAf+AAP8AADwAABgAAAAA"); // icons/gitlab.png
|
||||
if (s=="gmx") return atob("GBgBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEJmfmd8Zuc85v847/88Z9s8fttmHIHiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
|
||||
if (s=="google") return atob("GBiBAAAAAAD/AAP/wAf/4A/D4B8AwDwAADwAAHgAAHgAAHAAAHAH/nAH/nAH/ngH/ngAHjwAPDwAfB8A+A/D8Af/4AP/wAD/AAAAAA==");
|
||||
if (s=="google home") return atob("GBiCAAAAAAAAAAAAAAAAAAAAAoAAAAAACqAAAAAAKqwAAAAAqroAAAACquqAAAAKq+qgAAAqr/qoAACqv/6qAAKq//+qgA6r///qsAqr///6sAqv///6sAqv///6sAqv///6sA6v///6sA6v///qsA6qqqqqsA6qqqqqsA6qqqqqsAP7///vwAAAAAAAAAAAAAAAAA=="); // 2 bit unpaletted
|
||||
if (s=="google") return atob("GBgBAAAAAP8AA//AB//gD//gH+fAP4CAPwAAPgAAfAAAfA/+fA/+fA/+fA/+fAA+PgA+PwB8P4D8H+f4D//4B//wA//AAP8AAAAA"); // icons/google.png
|
||||
if (s=="google home") return atob("GBgBAAAAABgAADwAAH4AAf4AA/zAB/vgD/fwH+f4P4H8fwD+fgB+fAA+eAA+cAA+bAA+HAA+PAA+ff++ff++ff++ff++Pf+8AAAA"); // icons/google home.png
|
||||
if (s=="google play store") return atob("GBgBAAAAAAAAAH4AAP8AAMMAAMMAP//8P//8MAAMMAAMMGAMMHgMMH4MMH8MMH4MMHgMMGAMMAAMMAAMP//8H//4AAAAAAAAAAAA"); // icons/google play store.png
|
||||
if (s=="home assistant") return atob("FhaBAAAAAADAAAeAAD8AAf4AD/3AfP8D7fwft/D/P8ec572zbzbNsOEhw+AfD8D8P4fw/z/D/P8P8/w/z/AAAAA=");
|
||||
if (s=="instagram") return atob("GBiBAAAAAAAAAAAAAAAAAAP/wAYAYAwAMAgAkAh+EAjDEAiBEAiBEAiBEAiBEAjDEAh+EAgAEAwAMAYAYAP/wAAAAAAAAAAAAAAAAA==");
|
||||
if (s=="instagram") return atob("GBgBAAAAD//wH//4OAAccAAOYABmYDxmYP8GYeeGYYGGY4HGYwDGYwDGY4HGYYGGYeeGYP8GYDwGYAAGcAAOOAAcH//4D//wAAAA"); // icons/instagram.png
|
||||
if (s=="kalender") return atob("GBgBBgBgBQCgff++RQCiRgBiQAACf//+QAACQAACR//iRJkiRIEiR//iRNsiRIEiRJkiR//iRIEiRIEiR//iQAACQAACf//+AAAA");
|
||||
if (s=="lieferando") return atob("GBgBABgAAH5wAP9wAf/4A//4B//4D//4H//4P/88fV8+fV4//V4//Vw/HVw4HVw4HBg4HBg4HBg4HDg4Hjw4Hj84Hj44Hj44Hj44");
|
||||
if (s=="mattermost") return atob("GBgBAAAAAPAAA+EAB4MADgcYHAcYOA8MOB8OeD8GcD8GcH8GcD8HcD8HeBwHeAAOfAAOfgAePwA8P8D8H//4D//wB//gAf/AAH4A");
|
||||
if (s=="keep notes") return atob("GBgBAAAAAAAAH//4P//8P8P8Pzz8P378Pv98Pv98Pv98Pv98P378Pzz8P738P4H8P738P738P4GMP8OYP/+wP//gH//AAAAAAAAA"); // icons/google keep.png
|
||||
if (s=="lieferando") return atob("GBgBAAAAADwAAH4AAP/gAf/wA//wB//wD//wH//4H/98Pt58ft5+Ptx8DtxwDtxwDhxwDhhwDhhwDzhwD75wD75wD75wB77gAAAA"); // icons/lieferando.png
|
||||
if (s=="linkedin") return atob("GBgBAAAAf//+f//+f//+ef/+cf/+cf/+f//+f//+ccw+ccAeccAecccOcceOcceOcceOcceOcceOcceOec+ef//+f//+f//+AAAA"); // icons/linkedin.png
|
||||
if (s=="maps" || s=="organic maps" || s=="osmand") return atob("GBgBAAAAAAAAAAAAAeAYD/z4H//4GMeYGMMYGMMYGMMYGMMYGMMYGMMYGMMYGMMYGMMYGMMYGeMYH//4Hz/wGAeAAAAAAAAAAAAA"); // icons/map.png
|
||||
if (s=="mastodon" || s=="fedilab" || s=="tooot" || s=="tusky") return atob("GBgBAAAAB//gD//4H//4P//8PBg8PAA8fOMeeOeeeOeeOOeeOOecOP+cOP+cP//8P//4P//4P//gHwAAH4AAD+cAB/8AAf4AAAAA"); // icons/mastodon.png
|
||||
if (s=="mattermost") return atob("GBgBAAAAAPAAA+EAB4GADgOQHAeYOA+cOB+MeB+OcD+GcD+GcD+GeD8OeB4OeAAOfAAePgA8P4B8H/f4D//wB//gA//AAP8AAAAA"); // icons/mattermost.png
|
||||
if (s=="n26") return atob("GBgBAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAOIAAOIAAPIAANoAANoAAM4AAMYAAMYAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAA");
|
||||
if (s=="netflix") return atob("GBgBAAAAA8PAA+PAAePAAePAAfPAAvPAA/PAAvvAAn/AA/nAA3/AA/7AA5/AA/5AA99AA8/AA89AA8+AA8eAA8eAA8fAA8PAAAAA"); // icons/netflix.png
|
||||
if (s=="news" || s=="cbc news" || s=="rc info" || s=="reuters" || s=="ap news" || s=="la presse" || s=="nbc news") return atob("GBgBAAAAAAAAAAAALaW0P//8P//8P//8P//8MAAMMAAMMAAMP//8P//8MBwcMBwcMB/8MB/8MBwcMBwcP//8P//8AAAAAAAAAAAA"); // icons/news.png
|
||||
if (s=="nextbike") return atob("GBgBAAAAAAAAAAAAAAAAAAAAAACAfgDAPwDAP4HAH4N4H8f8D82GMd8CMDsDMGMDMGGGGMHOD4D8AAAAAAAAAAAAAAAAAAAAAAAA");
|
||||
if (s=="nina") return atob("GBgBAAAABAAQCAAICAAIEAAEEgAkJAgSJBwSKRxKSj4pUn8lVP+VVP+VUgAlSgApKQBKJAASJAASEgAkEAAECAAICAAIBAAQAAAA");
|
||||
if (s=="outlook mail") return atob("HBwBAAAAAAAAAAAIAAAfwAAP/gAB/+AAP/5/A//v/D/+/8P/7/g+Pv8Dye/gPd74w5znHDnOB8Oc4Pw8nv/Dwe/8Pj7/w//v/D/+/8P/7/gf/gAA/+AAAfwAAACAAAAAAAAAAAA=");
|
||||
if (s=="paypal") return atob("GBgBAAAAAAAAAAAAAf+AAf/AAf/gA//gA//gA//wA//wA//wA//wB//wB//wB//gB/+AB/gAB/gAB/gAAPgAAPgAAAAAAAAAAAAA");
|
||||
if (s=="phone") return atob("FxeBABgAAPgAAfAAB/AAD+AAH+AAP8AAP4AAfgAA/AAA+AAA+AAA+AAB+AAB+AAB+OAB//AB//gB//gA//AA/8AAf4AAPAA=");
|
||||
if (s=="post & dhl") return atob("GBgBAPgAE/5wMwZ8NgN8NgP4NgP4HgP4HgPwDwfgD//AB/+AAf8AAAAABs7AHcdgG4MwAAAAGESAFESAEkSAEnyAEkSAFESAGETw");
|
||||
if (s=="signal") return atob("GBgBAAAAAGwAAQGAAhggCP8QE//AB//oJ//kL//wD//0D//wT//wD//wL//0J//kB//oA//ICf8ABfxgBYBAADoABMAABAAAAAAA");
|
||||
if (s=="skype") return atob("GhoBB8AAB//AA//+Af//wH//+D///w/8D+P8Afz/DD8/j4/H4fP5/A/+f4B/n/gP5//B+fj8fj4/H8+DB/PwA/x/A/8P///B///gP//4B//8AD/+AAA+AA==");
|
||||
if (s=="slack") return atob("GBiBAAAAAAAAAABAAAHvAAHvAADvAAAPAB/PMB/veD/veB/mcAAAABzH8B3v+B3v+B3n8AHgAAHuAAHvAAHvAADGAAAAAAAAAAAAAA==");
|
||||
if (s=="snapchat") return atob("GBgBAAAAAAAAAH4AAf+AAf+AA//AA//AA//AA//AA//AH//4D//wB//gA//AB//gD//wH//4f//+P//8D//wAf+AAH4AAAAAAAAA");
|
||||
if (s=="steam") return atob("GBgBAAAAAAAAAAAAAAAAAAAAAAfgAAwwAAvQABvQABvQADvQgDww4H/g+f8A/zwAf9gAH9AAB8AAACAAAcAAAAAAAAAAAAAAAAAA");
|
||||
if (s=="teams") return atob("GBgBAAAAAAAAAAQAAB4AAD8IAA8cP/M+f/scf/gIeDgAfvvefvvffvvffvvffvvff/vff/veP/PeAA/cAH/AAD+AAD8AAAQAAAAA");
|
||||
if (s=="telegram" || s=="telegram foss") return atob("GBiBAAAAAAAAAAAAAAAAAwAAHwAA/wAD/wAf3gD/Pgf+fh/4/v/z/P/H/D8P/Acf/AM//AF/+AF/+AH/+ADz+ADh+ADAcAAAMAAAAA==");
|
||||
if (s=="threema") return atob("GBjB/4Yx//8AAAAAAAAAAAAAfgAB/4AD/8AH/+AH/+AP//AP2/APw/APw/AHw+AH/+AH/8AH/4AH/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=");
|
||||
if (s=="to do" || s=="opentasks") return atob("GBgBAAAAAAAAAAAwAAB4AAD8AAH+AAP/DAf/Hg//Px/+f7/8///4///wf//gP//AH/+AD/8AB/4AA/wAAfgAAPAAAGAAAAAAAAAA");
|
||||
if (s=="twitch") return atob("GBgBH//+P//+P//+eAAGeAAGeAAGeDGGeDOGeDOGeDOGeDOGeDOGeDOGeAAOeAAOeAAcf4/4f5/wf7/gf//Af/+AA/AAA+AAAcAA");
|
||||
if (s=="twitter") return atob("GhYBAABgAAB+JgA/8cAf/ngH/5+B/8P8f+D///h///4f//+D///g///wD//8B//+AP//gD//wAP/8AB/+AB/+AH//AAf/AAAYAAA");
|
||||
if (s=="outlook mail") return atob("GBgBAAAAAAAAAP/8AP/8AP/8AJjMf/jMf//8f//8cHjMd3jMZz/+Zz/+d3jecHj+f//mf/eGf/PGAwDmAwA+A//+Af/+AAAAAAAA"); // icons/outlook.png
|
||||
if (s=="paypal") return atob("GBgBAAAAA/+AA//gA//wB//wB//wB//wB//wB//wB//gD//gD//ID/+ID/wwD4BwD5/gD74AH7gAHzAAHzAAHzAAAHAAAHAAAAAA"); // icons/paypal.png
|
||||
if (s=="phone") return atob("GBgBAAAAAAAAH4AAP8AAP8AAP8AAH8AAH8AAH8AAH4AADwAADwAABwAAA4AAA8HwAeP8AP/8AH/8AD/8AA/8AAP8AAB4AAAAAAAA"); // icons/phone.png
|
||||
if (s=="plex") return atob("GBgBAAAAB/gAB/gAA/wAAf4AAf4AAP8AAH+AAH+AAD/AAB/gAB/gAB/gAB/gAD/AAH+AAH+AAP8AAf4AAf4AA/wAB/gAB/gAAAAA"); // icons/plex.png
|
||||
if (s=="pocket") return atob("GBgBAAAAAAAAP//8f//+f//+f//+f//+f//+fP8+eH4efDw+fhh+fwD+f4H+P8P8P+f8H//4H//4D//wB//gAf+AAH4AAAAAAAAA"); // icons/pocket.png
|
||||
if (s=="post & dhl") return atob("GBgBAAAAAAAAAAAAAAAAP/+Af/+AYAGAYAGAYAHwYAH4YAGMYAGGYAH+YAH+bwH+f//+ef+eGYGYH4H4DwDwAAAAAAAAAAAAAAAA"); // icons/delivery.png
|
||||
if (s=="proton mail") return atob("GBgBAAAAAAAAAAAAQAACYAAGcAAOeAAePABeXgDebwHed4Pee/fefe/efh/ef//ef//ef//ef//ef//ef//eP//cAAAAAAAAAAAA"); // icons/protonmail.png
|
||||
if (s=="reddit" || s=="sync pro" || s=="sync dev" || s=="boost" || s=="infinity" || s=="slide") return atob("GBgBAAAAAAAAAAYwAAX4AAh4AAgwAAgAAAgAAH4AAf+AN//sf//+fn5+PDw8HDw4Hn54H//4H//4DzzwB4HgAf+AAH4AAAAAAAAA"); // icons/reddit.png
|
||||
if (s=="signal") return atob("GBgBAAAAAL0AAYGABH4gCf+QE//IB//gL//0b//2H//4X//6X//6X//6X//6H//4b//2L//0D//gL//ID/+QYH4gVYGAcL0AAAAA"); // icons/signal.png
|
||||
if (s=="skype") return atob("GBgBAAAAB8AAH/8AP//AP//gf8fwfwD4fgB4fjx8fj/8Pg/8PwH8P4B8P/h8Pnx+Pjx+Hhh+HwD+D8P+B//8A//8AP/4AAPgAAAA"); // icons/skype.png
|
||||
if (s=="slack") return atob("GBgBAAAAAOcAAeeAAeeAAeeAAGeAAAeAP+ecf+eef+e+f+e+AAAAAAAAfef+fef+eef+Oef8AeAAAeYAAeeAAeeAAeeAAOcAAAAA"); // icons/slack.png
|
||||
if (s=="snapchat") return atob("GBgBAAAAAAAAAAAAAH4AAf+AAYGAAwDAAwDAAwDADwDwDwDwDgBwBwDgBwDgDgBwHAA4OAAcHAA4D4HwB//gAH4AAAAAAAAAAAAA"); // icons/snapchat.png
|
||||
if (s=="starbucks") return atob("GBgBAAAAAAAAAAAAD//4D//8DADMDADMDADMDAD8DAD4DADADADADADADADADgHAB/+AA/8AAAAAAAAAP//wP//wAAAAAAAAAAAA"); // icons/cafe.png
|
||||
if (s=="steam") return atob("GBgBAAAAAAAAAf+AA//AD//wD//wH/g4P/OcP/RcP+RcP+ReH8OeB4A+AAH+AMP8IC/8OS/8HN/4Dj/wD//wA//AAf+AAAAAAAAA"); // icons/steam.png
|
||||
if (s=="teams") return atob("GBgBAAAAAAgAAD4AADcYAGM8AGNmP/dmP/48MDAYMD/+PP/+PPBmPPBmPPBmPPBmP/BmP/BmH+B+AYD4AMDAAOOAAH8AABwAAAAA"); // icons/teams.png
|
||||
if (s=="telegram" || s=="telegram foss") return atob("GBgBAAAAAAAAAAAAAAAeAAB+AAP+AA/+AD/+Af9+B/z+H/n8f+P8f8f8Dw/8AB/8AB/8AB/4AAf4AAP4AAD4AABwAAAAAAAAAAAA"); // icons/telegram.png
|
||||
if (s=="threema") return atob("GBgBAAAAAP8AA//AB//gD//wH8P4H9v4H734P5n8P4H8P4H8H4H4H4H4D//wD//gD//AH/8AHDwAAAAAAAAABhhgDzzwBhhgAAAA"); // icons/threema.png
|
||||
if (s=="tiktok") return atob("GBgBAAAAAAAAAAcAAAcAAAeAAAfAAAfwAAf4AAf4AMd4A8cAB8cAD8cADwcAHgcAHgcAHg8ADw8AD/4AB/4AA/wAAfAAAAAAAAAA"); // icons/tiktok.png
|
||||
if (s=="to do" || s=="opentasks" || s=="tasks") return atob("GBgBAAAAAHwAAf+AA//ID4GcHwA8HAB4PADwOAHgcAPGcAeOcY8Oc94OcfwOcPgOOHAcOCAcHAA4DgB4D4HwB//gAf+AAH4AAAAA"); // icons/task.png
|
||||
if (s=="transit") return atob("GBgBAAAAD//wP//8P//8f//+f/j+ffA+eOA+eOMef+cefef+eOe+fecef+e+eOf+eOcefAcefA++fx/+f//+P//8P//8D//wAAAA"); // icons/transit.png
|
||||
if (s=="twitch") return atob("GBgBAAAAA//8B//8DgAMHgAMPhjMPhjMPhjMPhjMPhjMPgAMPgAMPgAYPgAwP+fgP+/AP/+AP/8AP/4AAeAAAcAAAYAAAQAAAAAA"); // icons/twitch.png
|
||||
if (s=="twitter") return atob("GBgBAAAAAAAAAAAAAAPAIAf8MA/4PA/8Pg/4H//4H//4P//4P//wH//wD//wD//gD//AA//AAf+AB/8AP/wAD/AAAAAAAAAAAAAA"); // icons/twitter.png
|
||||
if (s=="uber" || s=="lyft") return atob("GBgBAAAAAAAAAAAAAH4AAH4AB//gB//gDgBwDAAwDAAwH//4H//4GAAYG4HYG4HYG4HYGAAYH//4H//4HAA4HAA4AAAAAAAAAAAA"); // icons/taxi.png
|
||||
if (s=="vlc") return atob("GBgBAAAAABgAABgAADwAADwAAAAAAAAAAAAAAAAAAIEAAP8AAP8AAf+AAP8AAAAADAAwDAAwHAA4HwD4H//4P//8P//8P//8AAAA"); // icons/vlc.png
|
||||
if (s=="warnapp") return atob("GBgBAAAAAAAAAAAAAH4AAP8AA//AA//AD//gP//gf//4f//+/+P+/8H//8n//4n/fxh/fzg+Pj88Dn44AA4AAAwAAAwAAAgAAAAA");
|
||||
if (s=="whatsapp") return atob("GBiBAAB+AAP/wAf/4A//8B//+D///H9//n5//nw//vw///x///5///4///8e//+EP3/APn/wPn/+/j///H//+H//8H//4H//wMB+AA==");
|
||||
if (s=="whatsapp") return atob("GBgBAAAAAP8AA//AB4HwDgB4HAA4OAAcMYAMc8AOc8AGY8AGYcAGYeAGYPOGcH/OcD/OMA+MOAAcMAA4MgBwf8Pgf//AcP8AAAAA"); // icons/whatsapp.png
|
||||
if (s=="wordfeud") return atob("GBgCWqqqqqqlf//////9v//////+v/////++v/////++v8///Lu+v8///L++v8///P/+v8v//P/+v9v//P/+v+fx/P/+v+Pk+P/+v/PN+f/+v/POuv/+v/Ofdv/+v/NvM//+v/I/Y//+v/k/k//+v/i/w//+v/7/6//+v//////+v//////+f//////9Wqqqqqql");
|
||||
if (s=="youtube" || s=="newpipe") return atob("GBgBAAAAAAAAAAAAAAAAAf8AH//4P//4P//8P//8P5/8P4/8f4P8f4P8P4/8P5/8P//8P//8P//4H//4Af8AAAAAAAAAAAAAAAAA");
|
||||
if (s=="youtube" || s=="newpipe") return atob("GBgBAAAAAAAAAAAAAAAAAAAAH//4P//8P//8f//+f8/+f8P+f8D+f8D+f8P+f8/+f//+P//8P//8H//4AAAAAAAAAAAAAAAAAAAA"); // icons/youtube.png
|
||||
if (s=="zoom" || s=="meet") return atob("GBgBAAAAAAAAAAAAP/+Af//Af//AcADicADmcADucAD+cAD+cAD+cAD+cAD+cAD+cADucADmcADif//Af//AP/+AAAAAAAAAAAAA"); // icons/videoconf.png
|
||||
if (msg.id=="music") return atob("FhaBAH//+/////////////h/+AH/4Af/gB/+H3/7/f/v9/+/3/7+f/vB/w8H+Dwf4PD/x/////////////3//+A=");
|
||||
// if (s=="sms message" || s=="mail" || s=="gmail") // .. default icon (below)
|
||||
return atob("FhKBAH//+P//yf/+c//z5/+fz/z/n+f/Pz/+ef/8D///////////////////////f//4///A");
|
||||
|
@ -57,7 +86,7 @@ exports.getColor = function(msg,options) {
|
|||
return {
|
||||
// generic colors, using B2-safe colors
|
||||
// DO NOT USE BLACK OR WHITE HERE, just leave the declaration out and then the theme's fg color will be used
|
||||
"airbnb": "#f00",
|
||||
"airbnb": "#ff385c", // https://news.airbnb.com/media-assets/category/brand/
|
||||
"mail": "#ff0",
|
||||
"music": "#f0f",
|
||||
"phone": "#0f0",
|
||||
|
@ -66,39 +95,44 @@ exports.getColor = function(msg,options) {
|
|||
// all dithered on B2, but we only use the color for the icons. (Could maybe pick the closest 3-bit color for B2?)
|
||||
"bibel": "#54342c",
|
||||
"bring": "#455a64",
|
||||
"discord": "#738adb",
|
||||
"discord": "#5865f2", // https://discord.com/branding
|
||||
"etar": "#36a18b",
|
||||
"facebook": "#4267b2",
|
||||
"facebook": "#1877f2", // https://www.facebook.com/brand/resources/facebookapp/logo
|
||||
"gmail": "#ea4335",
|
||||
"gmx": "#1c449b",
|
||||
"google": "#4285F4",
|
||||
"google home": "#fbbc05",
|
||||
// "home assistant": "#41bdf5", // ha-blue is #41bdf5, but that's the background
|
||||
"instagram": "#dd2a7b",
|
||||
"lieferando": "#ee5c00",
|
||||
"instagram": "#ff0069", // https://about.instagram.com/brand/gradient
|
||||
"lieferando": "#ff8000",
|
||||
"linkedin": "#0a66c2", // https://brand.linkedin.com/
|
||||
"messenger": "#0078ff",
|
||||
"mastodon": "#563acc", // https://www.joinmastodon.org/branding
|
||||
"mattermost": "#00f",
|
||||
"n26": "#36a18b",
|
||||
"nextbike": "#00f",
|
||||
"newpipe": "#f00",
|
||||
"nina": "#e57004",
|
||||
"opentasks": "#409f8f",
|
||||
"outlook mail": "#0072c6",
|
||||
"outlook mail": "#0078d4", // https://developer.microsoft.com/en-us/fluentui#/styles/web/colors/products
|
||||
"paypal": "#003087",
|
||||
"pocket": "#ef4154f", // https://blog.getpocket.com/press/
|
||||
"post & dhl": "#f2c101",
|
||||
"signal": "#00f",
|
||||
"skype": "#00aff0",
|
||||
"reddit": "#ff4500", // https://www.redditinc.com/brand
|
||||
"signal": "#3a76f0", // https://github.com/signalapp/Signal-Desktop/blob/main/images/signal-logo.svg
|
||||
"skype": "#0078d4", // https://developer.microsoft.com/en-us/fluentui#/styles/web/colors/products
|
||||
"slack": "#e51670",
|
||||
"snapchat": "#ff0",
|
||||
"steam": "#171a21",
|
||||
"teams": "#464eb8",
|
||||
"teams": "#6264a7", // https://developer.microsoft.com/en-us/fluentui#/styles/web/colors/products
|
||||
"telegram": "#0088cc",
|
||||
"telegram foss": "#0088cc",
|
||||
"to do": "#3999e5",
|
||||
"twitch": "#6441A4",
|
||||
"twitter": "#1da1f2",
|
||||
"twitch": "#9146ff", // https://brand.twitch.tv/
|
||||
"twitter": "#1d9bf0", // https://about.twitter.com/en/who-we-are/brand-toolkit
|
||||
"vlc": "#ff8800",
|
||||
"whatsapp": "#4fce5d",
|
||||
"wordfeud": "#e7d3c7",
|
||||
"youtube": "#f00",
|
||||
"youtube": "#f00", // https://www.youtube.com/howyoutubeworks/resources/brand-resources/#logos-icons-and-colors
|
||||
}[s]||options.default;
|
||||
};
|
||||
|
|
|
@ -1,77 +1,2 @@
|
|||
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
|
||||
0.56: Fix handling of music messages
|
||||
|
|
|
@ -1,62 +1,26 @@
|
|||
# Messages app
|
||||
# Messages library
|
||||
|
||||
This app handles the display of messages and message notifications. It stores
|
||||
a list of currently received messages and allows them to be listed, viewed,
|
||||
and responded to.
|
||||
This library handles the passing of messages. It can storess a list of messages
|
||||
and allows them to be retrieved by other apps.
|
||||
|
||||
It is a replacement for the old `notify`/`gadgetbridge` apps.
|
||||
## Example
|
||||
|
||||
## Settings
|
||||
Assuming you are using GadgetBridge and "overlay notifications":
|
||||
|
||||
You can change settings by going to the global `Settings` app, then `App Settings`
|
||||
and `Messages`:
|
||||
|
||||
* `Vibrate` - This is the pattern of buzzes that should be made when a new message is received
|
||||
* `Vibrate for calls` - This is the pattern of buzzes that should be made when an incoming call is received
|
||||
* `Repeat` - How often should buzzes repeat - the default of 4 means the Bangle will buzz every 4 seconds
|
||||
* `Vibrate Timer` - When a new message is received when in a non-clock app, we display the message icon and
|
||||
buzz every `Repeat` seconds. This is how long we continue to do that.
|
||||
* `Unread Timer` - When a new message is received when showing the clock we go into the Messages app.
|
||||
If there is no user input for this amount of time then the app will exit and return
|
||||
to the clock where a ringing bell will be shown in the Widget bar.
|
||||
* `Min Font` - The minimum font size used when displaying messages on the screen. A bigger font
|
||||
is chosen if there isn't much message text, but this specifies the smallest the font should get before
|
||||
it starts getting clipped.
|
||||
* `Auto-Open Music` - Should the app automatically open when the phone starts playing music?
|
||||
* `Unlock Watch` - Should the app unlock the watch when a new message arrives, so you can touch the buttons at the bottom of the app?
|
||||
* `Flash Icon` - Toggle flashing of the widget icon.
|
||||
* `Widget messages` - The maximum amount of message icons to show on the widget, or `Hide` the widget completely.
|
||||
|
||||
## New Messages
|
||||
|
||||
When a new message is received:
|
||||
|
||||
* If you're in an app, the Bangle will buzz and a message icon appears in the Widget bar. You can tap this icon to view the message.
|
||||
* If you're in a clock, the Messages app will automatically start and show the message
|
||||
|
||||
When a message is shown, you'll see a screen showing the message title and text.
|
||||
|
||||
* The 'back-arrow' button (or physical button on Bangle.js 2) goes back to Messages, marking the current message as read.
|
||||
* The top-left icon shows more options, for instance deleting the message of marking unread
|
||||
* On Bangle.js 2 you can tap on the message body to view a scrollable version of the title and text (or can use the top-left icon + `View Message`)
|
||||
* If shown, the 'tick' button:
|
||||
* **Android** opens the notification on the phone
|
||||
* **iOS** responds positively to the notification (accept call/etc)
|
||||
* If shown, the 'cross' button:
|
||||
* **Android** dismisses the notification on the phone
|
||||
* **iOS** responds negatively to the notification (dismiss call/etc)
|
||||
|
||||
## Images
|
||||
_1. Screenshot of a notification_
|
||||
|
||||

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

|
||||
1. Gadgetbridge sends an event to your watch for an incoming message
|
||||
2. The `android` app parses the message, and calls `require("messages").pushMessage({/** the message */})`
|
||||
3. `require("messages")` calls `Bangle.emit("message", "text", {/** the message */})`
|
||||
4. Overlay Notifications shows the message in an overlay, and marks it as `handled`
|
||||
5. The default UI app (Message UI, `messagegui`) 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. `openGUI` calls `require("messagegui").open(/** copy of the message */)`.
|
||||
9. The `messagegui` library loads the Message UI app.
|
||||
|
||||
|
||||
## Events (for app/widget developers)
|
||||
|
||||
## Events
|
||||
|
||||
When a new message arrives, a `"message"` event is emitted, you can listen for
|
||||
it like this:
|
||||
|
@ -64,8 +28,7 @@ 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
|
||||
// <type> is one of "text", "call", "alarm", "map", or "music"
|
||||
// see `messages/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`);
|
||||
|
@ -74,10 +37,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=[messages]%20library
|
||||
|
||||
## Creator
|
||||
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
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 (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 +17,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 +199,24 @@ exports.getMessages = function() {
|
|||
*/
|
||||
exports.buzz = function(msgSrc) {
|
||||
exports.stopBuzz(); // cancel any previous buzz timeouts
|
||||
if ((require('Storage').readJSON('setting.json',1)||{}).quiet) return Promise.resolve(); // never buzz during Quiet Mode
|
||||
var msgSettings = require('Storage').readJSON("messages.settings.json", true) || {};
|
||||
var pattern;
|
||||
if (msgSrc && msgSrc.toLowerCase() === "phone") {
|
||||
if ((require("Storage").readJSON("setting.json", 1) || {}).quiet) return Promise.resolve(); // never buzz during Quiet Mode
|
||||
const msgSettings = require("Storage").readJSON("messages.settings.json", true) || {};
|
||||
let pattern;
|
||||
if (msgSrc && msgSrc.toLowerCase()==="phone") {
|
||||
// special vibration pattern for incoming calls
|
||||
pattern = msgSettings.vibrateCalls;
|
||||
} else {
|
||||
pattern = msgSettings.vibrate;
|
||||
}
|
||||
if (pattern === undefined) { pattern = ":"; } // pattern may be "", so we can't use || ":" here
|
||||
if (pattern===undefined) { pattern = ":"; } // pattern may be "", so we can't use || ":" here
|
||||
if (!pattern) return Promise.resolve();
|
||||
|
||||
var repeat = msgSettings.repeat;
|
||||
if (repeat===undefined) repeat=4; // repeat may be zero
|
||||
let repeat = msgSettings.repeat;
|
||||
if (repeat===undefined) repeat = 4; // repeat may be zero
|
||||
if (repeat) {
|
||||
exports.buzzTimeout = setTimeout(()=>require("buzz").pattern(pattern), repeat*1000);
|
||||
var vibrateTimeout = msgSettings.vibrateTimeout;
|
||||
if (vibrateTimeout===undefined) vibrateTimeout=60;
|
||||
exports.buzzTimeout = setTimeout(() => require("buzz").pattern(pattern), repeat*1000);
|
||||
let vibrateTimeout = msgSettings.vibrateTimeout;
|
||||
if (vibrateTimeout===undefined) vibrateTimeout = 60;
|
||||
if (vibrateTimeout && !exports.stopTimeout) exports.stopTimeout = setTimeout(exports.stopBuzz, vibrateTimeout*1000);
|
||||
}
|
||||
return require("buzz").pattern(pattern);
|
||||
|
|
|
@ -1,23 +1,18 @@
|
|||
{
|
||||
"id": "messages",
|
||||
"name": "Messages",
|
||||
"version": "0.54",
|
||||
"description": "App to display notifications from iOS and Gadgetbridge/Android",
|
||||
"version": "0.56",
|
||||
"description": "Library to handle, load and store message events received from Android/iOS",
|
||||
"icon": "app.png",
|
||||
"type": "app",
|
||||
"type": "module",
|
||||
"tags": "tool,system",
|
||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||
"dependencies" : { "messageicons":"module" },
|
||||
"provides_modules" : ["messages"],
|
||||
"dependencies" : { "messagegui":"module","messagewidget":"module" },
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name":"messages.app.js","url":"app.js"},
|
||||
{"name":"messages.new.js","url":"app-newmessage.js"},
|
||||
{"name":"messages.settings.js","url":"settings.js"},
|
||||
{"name":"messages.img","url":"app-icon.js","evaluate":true},
|
||||
{"name":"messages.wid.js","url":"widget.js"},
|
||||
{"name":"messages","url":"lib.js"}
|
||||
{"name":"messages","url":"lib.js"},
|
||||
{"name":"messages.settings.js","url":"settings.js"}
|
||||
],
|
||||
"data": [{"name":"messages.json"},{"name":"messages.settings.json"}],
|
||||
"screenshots": [{"url":"screenshot.png"},{"url":"screenshot-notify.gif"}],
|
||||
"sortorder": -9
|
||||
"data": [{"name":"messages.json"},{"name":"messages.settings.json"}]
|
||||
}
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
(() => {
|
||||
if ((require('Storage').readJSON("messages.settings.json", true) || {}).maxMessages===0) return;
|
||||
|
||||
function filterMessages(msgs) {
|
||||
return msgs.filter(msg => msg.new && msg.id != "music")
|
||||
.map(m => m.src) // we only need this for icon/color
|
||||
.filter((msg, i, arr) => arr.findIndex(nmsg => msg.src == nmsg.src) == i);
|
||||
}
|
||||
|
||||
WIDGETS["messages"]={area:"tl", width:0, draw:function(recall) {
|
||||
// If we had a setTimeout queued from the last time we were called, remove it
|
||||
if (WIDGETS["messages"].i) {
|
||||
clearTimeout(WIDGETS["messages"].i);
|
||||
delete WIDGETS["messages"].i;
|
||||
}
|
||||
Bangle.removeListener('touch', this.touch);
|
||||
if (!this.width) return;
|
||||
let settings = Object.assign({flash:true, maxMessages:3},require('Storage').readJSON("messages.settings.json", true) || {});
|
||||
if (recall !== true || settings.flash) {
|
||||
var msgsShown = E.clip(this.msgs.length, 0, settings.maxMessages);
|
||||
g.reset().clearRect(this.x, this.y, this.x+this.width, this.y+23);
|
||||
for(let i = 0;i < msgsShown;i++) {
|
||||
const msg = this.msgs[i];
|
||||
const colors = [g.theme.bg,
|
||||
require("messageicons").getColor(msg, {settings:settings})];
|
||||
if (settings.flash && ((Date.now()/1000)&1)) {
|
||||
if (colors[1] == g.theme.fg) {
|
||||
colors.reverse();
|
||||
} else {
|
||||
colors[1] = g.theme.fg;
|
||||
}
|
||||
}
|
||||
g.setColor(colors[1]).setBgColor(colors[0]);
|
||||
// draw the icon, or '...' if too many messages
|
||||
g.drawImage(i == (settings.maxMessages - 1) && this.msgs.length > settings.maxMessages ? atob("EASBAGGG88/zz2GG") : require("messageicons").getImage(msg),
|
||||
this.x + 12 + i * 24, this.y + 12, {rotate:0/*force centering*/});
|
||||
}
|
||||
}
|
||||
WIDGETS["messages"].i=setTimeout(()=>WIDGETS["messages"].draw(true), 1000);
|
||||
if (process.env.HWVERSION>1) Bangle.on('touch', this.touch);
|
||||
},update:function(rawMsgs) {
|
||||
const settings = Object.assign({maxMessages:3},require('Storage').readJSON("messages.settings.json", true) || {});
|
||||
this.msgs = filterMessages(rawMsgs);
|
||||
this.width = 24 * E.clip(this.msgs.length, 0, settings.maxMessages);
|
||||
Bangle.drawWidgets();
|
||||
},touch:function(b,c) {
|
||||
var w=WIDGETS["messages"];
|
||||
if (!w||!w.width||c.x<w.x||c.x>w.x+w.width||c.y<w.y||c.y>w.y+24) return;
|
||||
load("messages.app.js");
|
||||
}};
|
||||
|
||||
/* We might have returned here if we were in the Messages app for a
|
||||
message but then the watch was never viewed. */
|
||||
if (global.MESSAGES===undefined)
|
||||
WIDGETS["messages"].update(require("messages").getMessages());
|
||||
})();
|
|
@ -1,2 +1,5 @@
|
|||
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
|
||||
0.04: Fix dependency on messages library
|
||||
Fix loading message UI
|
|
@ -1,15 +1,9 @@
|
|||
Hacky app that uses Messages app and it's library to push a message that triggers the music controls. It's nearly not an app, and yet it moves.
|
||||
|
||||
This app require Messages setting 'Auto-open Music' to be 'Yes'. If it isn't, the app will change it to 'Yes' and let it stay that way.
|
||||
|
||||
Making the music controls accessible this way lets one start a music stream on the phone in some situations even though the message app didn't receive a music message from gadgetbridge to begin with. (I think.)
|
||||
|
||||
It is suggested to use Messages Music along side the app Quick Launch.
|
||||
|
||||
Messages Music v0.02 has been verified to work with Messages v0.41 on Bangle.js 2 fw2v14.
|
||||
|
||||
Messages Music should work with forks of the original Messages app. At least as long as functions pushMessage() in the library and showMusicMessage() in app.js hasn't been changed too much.
|
||||
|
||||
Messages app is created by Gordon Williams with contributions from [Jeroen Peters](https://github.com/jeroenpeters1986).
|
||||
|
||||
The icon used for this app is from [https://icons8.com](https://icons8.com).
|
||||
|
|
|
@ -1,14 +1 @@
|
|||
let showMusic = () => {
|
||||
Bangle.CLOCK = 1; // To pass condition in messages library
|
||||
require('messages').pushMessage({"t":"add","artist":" ","album":" ","track":" ","dur":0,"c":-1,"n":-1,"id":"music","title":"Music","state":"play","new":true});
|
||||
};
|
||||
|
||||
var settings = require('Storage').readJSON('messages.settings.json', true) || {}; //read settings if they exist else set to empty dict
|
||||
if (!settings.openMusic) {
|
||||
settings.openMusic = true; // This app/hack works as intended only if this setting is true
|
||||
require('Storage').writeJSON('messages.settings.json', settings);
|
||||
E.showMessage("First run:\n\nMessages setting\n\n 'Auto-Open Music'\n\n set to 'Yes'");
|
||||
setTimeout(()=>{showMusic();}, 5000);
|
||||
} else {
|
||||
showMusic();
|
||||
}
|
||||
setTimeout(()=>require('messages').openGUI({"t":"add","artist":" ","album":" ","track":" ","dur":0,"c":-1,"n":-1,"id":"music","title":"Music","state":"play","new":true}));
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "messagesmusic",
|
||||
"name":"Messages Music",
|
||||
"version":"0.02",
|
||||
"version":"0.04",
|
||||
"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,5 @@
|
|||
{"name":"messagesmusic.app.js","url":"app.js"},
|
||||
{"name":"messagesmusic.img","url":"app-icon.js","evaluate":true}
|
||||
],
|
||||
"dependencies": {"messages":"app"}
|
||||
|
||||
"dependencies":{"messages":"module"}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,11 @@
|
|||
var notesElement = document.getElementById("notes");
|
||||
var notes = {};
|
||||
|
||||
function disableFormInput() {
|
||||
document.querySelectorAll(".form-input").forEach(el => el.disabled = true);
|
||||
document.querySelectorAll(".btn").forEach(el => el.disabled = true);
|
||||
}
|
||||
|
||||
function getData() {
|
||||
// show loading window
|
||||
Util.showModal("Loading...");
|
||||
|
@ -53,8 +58,10 @@ function getData() {
|
|||
buttonSave.classList.add('btn-default');
|
||||
buttonSave.onclick = function() {
|
||||
notes[i].note = textarea.value;
|
||||
Util.writeStorage("noteify.json", JSON.stringify(notes));
|
||||
location.reload();
|
||||
disableFormInput();
|
||||
Util.writeStorage("noteify.json", JSON.stringify(notes), () => {
|
||||
location.reload(); // reload so we see current data
|
||||
});
|
||||
}
|
||||
divColumn2.appendChild(buttonSave);
|
||||
|
||||
|
@ -64,8 +71,10 @@ function getData() {
|
|||
buttonDelete.onclick = function() {
|
||||
notes[i].note = textarea.value;
|
||||
notes.splice(i, 1);
|
||||
Util.writeStorage("noteify.json", JSON.stringify(notes));
|
||||
disableFormInput();
|
||||
Util.writeStorage("noteify.json", JSON.stringify(notes), () => {
|
||||
location.reload(); // reload so we see current data
|
||||
});
|
||||
}
|
||||
divColumn2.appendChild(buttonDelete);
|
||||
divColumn.appendChild(divColumn2);
|
||||
|
@ -77,10 +86,12 @@ function getData() {
|
|||
document.getElementById("btnAdd").addEventListener("click", function() {
|
||||
const note = document.getElementById("note-new").value;
|
||||
notes.push({"note": note});
|
||||
Util.writeStorage("noteify.json", JSON.stringify(notes));
|
||||
disableFormInput();
|
||||
Util.writeStorage("noteify.json", JSON.stringify(notes), () => {
|
||||
location.reload(); // reload so we see current data
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Called when app starts
|
||||
|
|
|
@ -15,3 +15,4 @@
|
|||
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)
|
||||
|
|
|
@ -26,11 +26,16 @@ can change settings, move the map around, and click `Get Map` again.
|
|||
## 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.
|
||||
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
|
||||
or put the map back in its default 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
|
||||
|
||||
|
|
|
@ -4,12 +4,15 @@ 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(R.x,R.y,R.x2,R.y2);
|
||||
m.draw();
|
||||
drawMarker();
|
||||
// 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);
|
||||
|
@ -18,6 +21,7 @@ function redraw() {
|
|||
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);
|
||||
}
|
||||
|
||||
|
@ -76,6 +80,10 @@ function showMap() {
|
|||
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;
|
||||
|
|
|
@ -48,16 +48,18 @@
|
|||
<div style="display:inline-block;text-align:center;vertical-align: top;" id="3bitdiv"> <input type="checkbox" id="3bit"></input><br/><span>3 bit</span></div>
|
||||
<div class="form-group" style="display:inline-block;">
|
||||
<select class="form-select" id="mapSize">
|
||||
<option value="4">Small</option>
|
||||
<option value="5" selected>Medium</option>
|
||||
<option value="6">Large</option>
|
||||
<option value="7">XL</option>
|
||||
<option value="4">Small (4x4)</option>
|
||||
<option value="5" selected>Medium (5x5)</option>
|
||||
<option value="7">Large (7x7)</option>
|
||||
<option value="10">XL (10x10)</option>
|
||||
<option value="15">XXL (15x15)</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>
|
||||
<button id="cancel" class="btn">Cancel</button>
|
||||
<span id="mapstats"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -174,12 +176,14 @@ TODO:
|
|||
</div>
|
||||
</div>
|
||||
`;
|
||||
setTimeout(function() {
|
||||
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});
|
||||
}, 100);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
|
@ -312,11 +316,13 @@ TODO:
|
|||
|
||||
var zoom = map.getZoom();
|
||||
var centerlatlon = map.getBounds().getCenter();
|
||||
var center = map.project(centerlatlon, zoom).divideBy(OSMTILESIZE);
|
||||
// Reason for 16px adjustment below not 100% known, but it seems to
|
||||
// align everything perfectly: https://github.com/espruino/BangleApps/issues/984
|
||||
var ox = Math.round((center.x - Math.floor(center.x)) * OSMTILESIZE) + 16;
|
||||
var oy = Math.round((center.y - Math.floor(center.y)) * OSMTILESIZE) + 16;
|
||||
var center = map.project(centerlatlon, zoom).divideBy(OSMTILESIZE); // the center of our map
|
||||
// ox/oy = offset in pixels
|
||||
var ox = Math.round((center.x - Math.floor(center.x)) * OSMTILESIZE);
|
||||
var oy = Math.round((center.y - Math.floor(center.y)) * OSMTILESIZE);
|
||||
// adjust offset because we want to center our map
|
||||
ox -= MAPTILES * TILESIZE / 2;
|
||||
oy -= MAPTILES * TILESIZE / 2;
|
||||
center = center.floor(); // make sure we're in the middle of a tile
|
||||
// JS version of Bangle.js's projection
|
||||
function bproject(lat, lon) {
|
||||
|
@ -353,10 +359,12 @@ TODO:
|
|||
var ctx = canvas.getContext('2d');
|
||||
canvas.width = MAPSIZE;
|
||||
canvas.height = MAPSIZE;
|
||||
for (var i = 0; i < OSMTILECOUNT; i++) {
|
||||
for (var j = 0; j < OSMTILECOUNT; j++) {
|
||||
var tileMin = Math.round(-OSMTILECOUNT/2);
|
||||
var tileMax = Math.round(OSMTILECOUNT/2);
|
||||
for (var i = tileMin; i <= tileMax; i++) {
|
||||
for (var j = tileMin; j <= tileMax; j++) {
|
||||
(function(i,j){
|
||||
var coords = new L.Point(center.x+i-1, center.y+j-1);
|
||||
var coords = new L.Point(center.x+i, center.y+j);
|
||||
coords.z = zoom;
|
||||
var img = new Image();
|
||||
img.crossOrigin = "Anonymous";
|
||||
|
@ -368,6 +376,8 @@ TODO:
|
|||
ctx.fillRect(testPt.x-1, testPt.y-5, 3,10);
|
||||
ctx.fillRect(testPt.x-5, testPt.y-1, 10,3);
|
||||
}*/
|
||||
/*ctx.fillStyle="black";
|
||||
ctx.fillRect(i*OSMTILESIZE - ox, j*OSMTILESIZE - oy, 6,6);*/
|
||||
resolve();
|
||||
};
|
||||
}));
|
||||
|
@ -395,6 +405,8 @@ TODO:
|
|||
h : Math.round(canvas.height / TILESIZE), // height in tiles
|
||||
fn : mapImageFile
|
||||
})});
|
||||
var mapSizeInK = Math.round(mapFiles.reduce((r,m)=>m.content.length+r,0)/1000);
|
||||
document.getElementById("mapstats").innerText = "Size : "+ (mapSizeInK+"kb");
|
||||
console.log(mapFiles);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "openstmap",
|
||||
"name": "OpenStreetMap",
|
||||
"shortName": "OpenStMap",
|
||||
"version": "0.14",
|
||||
"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",
|
||||
|
@ -15,6 +15,7 @@
|
|||
{"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"}
|
||||
]
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
0.01: New App!
|
||||
0.02: Fix fast loading on swipe to clock
|
|
@ -0,0 +1,20 @@
|
|||
# Quick Center
|
||||
|
||||
An app with a status bar showing various information and up to six shortcuts for your favorite apps!
|
||||
Designed for use with any kind of quick launcher, such as Quick Launch or Pattern Launcher.
|
||||
|
||||

|
||||
|
||||
## Usage
|
||||
|
||||
Pin your apps with settings, then launch them with your favorite quick launcher to access them quickly.
|
||||
If you don't have any apps pinned, the settings and about apps will be shown as an example.
|
||||
|
||||
## Features
|
||||
|
||||
Battery and GPS status display (for now)
|
||||
Up to six shortcuts to your favorite apps
|
||||
|
||||
## Upcoming features
|
||||
- Quick switches for toggleable features such as Bluetooth or HID mode
|
||||
- Customizable status information
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEw4UB6cA/4ACBYNVAElQHAsFBYZFHCxIYEoALHgILNOxILChWqAAmgBYNUBZMVBYIAIBc0C1WAlWoAgQL/O96D/Qf4LZqoLJqoLMoAKHgILNqALHgoLBGBAKCDA4WDAEQA="))
|
|
@ -0,0 +1,129 @@
|
|||
{
|
||||
require("Font8x12").add(Graphics);
|
||||
|
||||
// load pinned apps from config
|
||||
let settings = require("Storage").readJSON("qcenter.json", 1) || {};
|
||||
let pinnedApps = settings.pinnedApps || [];
|
||||
let 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
|
||||
let drawButton = function(l) {
|
||||
let x = l.x + (0 | l.pad),
|
||||
y = l.y + (0 | l.pad),
|
||||
w = l.w - (l.pad << 1),
|
||||
h = l.h - (l.pad << 1);
|
||||
let 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
|
||||
let groupBy3 = function(data) {
|
||||
let result = [];
|
||||
for (let 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
|
||||
let 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) => load(app.src),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// create basic layout content with status info and sensor status on top
|
||||
let 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();
|
||||
|
||||
let Layout = require("Layout");
|
||||
let layout = new Layout({
|
||||
type: "v",
|
||||
c: layoutContent
|
||||
}, {
|
||||
remove: ()=>{
|
||||
Bangle.removeListener("swipe", onSwipe);
|
||||
delete Graphics.prototype.setFont8x12;
|
||||
}
|
||||
});
|
||||
g.clear();
|
||||
layout.render();
|
||||
Bangle.drawWidgets();
|
||||
|
||||
// swipe event listener for exit gesture
|
||||
let onSwipe = 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();
|
||||
}
|
||||
|
||||
Bangle.on("swipe", onSwipe);
|
||||
}
|
After Width: | Height: | Size: 265 B |
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"id": "qcenter",
|
||||
"name": "Quick Center",
|
||||
"shortName": "QCenter",
|
||||
"version": "0.02",
|
||||
"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 }
|
||||
],
|
||||
"data": [{"name":"qcenter.json"}]
|
||||
}
|
After Width: | Height: | Size: 3.6 KiB |
|
@ -0,0 +1,133 @@
|
|||
// 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: back},
|
||||
};
|
||||
|
||||
// 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();
|
||||
});
|
|
@ -14,3 +14,5 @@
|
|||
Improve timer message using formatDuration
|
||||
Fix wrong fallback for buzz pattern
|
||||
0.13: Ask to delete a timer after stopping it
|
||||
0.14: Added clkinfo for alarms and timers
|
||||
0.15: Automatic translation of some string in clkinfo
|
||||
|
|
|
@ -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 /*LANG*/"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: /*LANG*/"Alarms",
|
||||
img: img,
|
||||
dynamic: true,
|
||||
items: alarm.getAlarms().filter(a=>!a.appid)
|
||||
//.sort((a,b)=>alarm.getTimeToAlarm(a)-alarm.getTimeToAlarm(b))
|
||||
.sort((a,b)=>getAlarmOrder(a)-getAlarmOrder(b))
|
||||
.map((a, i)=>({
|
||||
name: null,
|
||||
hasRange: true,
|
||||
get: () => ({ text: getAlarmText(a), img: getAlarmIcon(a),
|
||||
v: getAlarmValue(a), min:0, max:getAlarmMax(a)}),
|
||||
show: function() { alarmItems.items[i].emit("redraw"); },
|
||||
hide: function () {},
|
||||
run: function() { }
|
||||
})),
|
||||
};
|
||||
|
||||
return alarmItems;
|
||||
})
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "sched",
|
||||
"name": "Scheduler",
|
||||
"version": "0.13",
|
||||
"version": "0.15",
|
||||
"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"}]
|
||||
}
|
||||
|
|