Merge branch 'master' of github.com:espruino/BangleApps

pull/2613/head
Gordon Williams 2023-02-27 13:17:57 +00:00
commit 625e52514d
30 changed files with 681 additions and 57 deletions

View File

@ -10,6 +10,12 @@ Standard # of drag handlers: 0-10 (Default: 0, must be changed for backswipe to
Standard # of handlers settings are used to fine tune when backswipe should trigger the back function. E.g. when using a keyboard that works on drags, we don't want the backswipe to trigger when we just wanted to select a letter. This might not be able to cover all cases however. Standard # of handlers settings are used to fine tune when backswipe should trigger the back function. E.g. when using a keyboard that works on drags, we don't want the backswipe to trigger when we just wanted to select a letter. This might not be able to cover all cases however.
To get an indication for standard # of handlers `Bangle["#onswipe"]` and `Bangle["#ondrag"]` can be entered in the [Espruino Web IDE](https://www.espruino.com/ide) console field. They return `undefined` if no handler is active, a function if one is active, or a list of functions if multiple are active. Calling this on the clock app is a good start.
## TODO
- Possibly add option to tweak standard # of handlers on per app basis.
## Creator ## Creator
Kedlub Kedlub

View File

@ -0,0 +1 @@
0.01: Initial fork from messages_light

View File

@ -0,0 +1,24 @@
# Messages overlay app
This app handles the display of messages and message notifications as an overlay pop up.
It is a GUI replacement for the `messages` apps.
Messages are ephemeral and not stored on the Bangle.
## Usage
Close app by tapping the X and scroll by swiping. The border of the pop up changes color if the Bangle is locked. The color depends on your currently active theme.
## Firmware hint
Current stable firmware draws incorrect colors for emojis. Nightly firmware builds correct this.
## Low memory mode
If free memory is below 2000 blocks, the overlay automatically only uses 1 bit depth. Default uses roundabout 1300 blocks, while low memory mode uses about 600.
## Creator
[halemmerich](https://github.com/halemmerich)
Forked from messages_light by Rarder44

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,7 @@
//override require to filter require("message")
global.require_real=global.require;
global.require = (_require => file => {
if (file==="messages") file = "messagesoverlay";
return _require(file);
})(require);

495
apps/messagesoverlay/lib.js Normal file
View File

@ -0,0 +1,495 @@
/* MESSAGES is a list of:
{id:int,
src,
title,
subject,
body,
sender,
tel:string,
new:true // not read yet
}
*/
const ovrx = 10;
const ovry = 10;
const ovrw = g.getWidth()-2*ovrx;
const ovrh = g.getHeight()-2*ovry;
let _g = g;
let lockListener;
let quiet;
let LOG = function() {
//print.apply(null, arguments);
};
let isQuiet = function(){
if (quiet == undefined) quiet = (require('Storage').readJSON('setting.json', 1) || {}).quiet;
return quiet;
};
let settings = {
fontSmall:"6x8",
fontMedium:"Vector:14",
fontBig:"Vector:20",
fontLarge:"Vector:30",
};
let eventQueue = [];
let callInProgress = false;
let show = function(ovr){
let img = ovr;
if (ovr.getBPP() == 1) {
img = ovr.asImage();
img.palette = new Uint16Array([_g.theme.fg,_g.theme.bg]);
}
Bangle.setLCDOverlay(img, ovrx, ovry);
};
let manageEvent = function(ovr, event) {
event.new = true;
LOG("manageEvent");
if (event.id == "call") {
showCall(ovr, event);
return;
}
switch (event.t) {
case "add":
eventQueue.unshift(event);
if (!callInProgress)
showMessage(ovr, event);
break;
case "modify":
let find = false;
eventQueue.forEach(element => {
if (element.id == event.id) {
find = true;
Object.assign(element, event);
}
});
if (!find)
eventQueue.unshift(event);
if (!callInProgress)
showMessage(ovr, event);
break;
case "remove":
if (eventQueue.length == 0 && !callInProgress)
next(ovr);
if (!callInProgress && eventQueue[0] !== undefined && eventQueue[0].id == event.id)
next(ovr);
else {
eventQueue.length = 0; // empty existing queue
eventQueue.forEach(element => {
if (element.id != event.id)
neweventQueue.push(element);
});
}
break;
case "musicstate":
case "musicinfo":
break;
}
};
let roundedRect = function(ovr, x,y,w,h,filled){
var poly = [
x,y+4,
x+4,y,
x+w-5,y,
x+w-1,y+4,
x+w-1,y+h-5,
x+w-5,y+h-1,
x+4,y+h-1,
x,y+h-5,
x,y+4
];
ovr.drawPoly(poly,true);
if (filled) ovr.fillPoly(poly,true);
};
let drawScreen = function(ovr, title, titleFont, src, iconcolor, icon){
ovr.setBgColor(ovr.theme.bg2);
ovr.clearRect(2,2,ovr.getWidth()-3,37);
ovr.setColor(ovr.theme.fg2);
ovr.setFont(settings.fontSmall);
ovr.setFontAlign(0,-1);
let textCenter = (ovr.getWidth()+35-26)/2;
if (src) {
let shortened = src;
while (ovr.stringWidth(shortened) > ovr.getWidth()-80) shortened = shortened.substring(0,shortened.length-2);
if (shortened.length != src.length) shortened += "...";
ovr.drawString(shortened, textCenter, 2);
}
ovr.setFontAlign(0,0);
ovr.setFont(titleFont);
if (title) ovr.drawString(title, textCenter, 38/2 + 5);
ovr.setColor(ovr.theme.fg2);
ovr.setFont(settings.fontMedium);
roundedRect(ovr, ovr.getWidth()-26,5,22,30,false);
ovr.setFont("Vector:16");
ovr.drawString("X",ovr.getWidth()-14,21);
ovr.setColor("#888");
roundedRect(ovr, 5,5,30,30,true);
ovr.setColor(ovr.getBPP() != 1 ? iconcolor : ovr.theme.bg2);
ovr.drawImage(icon,8,8);
};
let showMessage = function(ovr, msg) {
LOG("showMessage");
ovr.setBgColor(ovr.theme.bg);
if (typeof msg.CanscrollDown === "undefined")
msg.CanscrollDown = false;
if (typeof msg.CanscrollUp === "undefined")
msg.CanscrollUp = false;
// Normal text message display
let title = msg.title,
titleFont = settings.fontLarge,
lines;
if (title) {
let w = ovr.getWidth() - 35 - 26;
if (ovr.setFont(titleFont).stringWidth(title) > w)
titleFont = settings.fontMedium;
if (ovr.setFont(titleFont).stringWidth(title) > w) {
lines = ovr.wrapString(title, w);
title = (lines.length > 2) ? lines.slice(0, 2).join("\n") + "..." : lines.join("\n");
}
}
drawScreen(ovr, title, titleFont, msg.src || /*LANG*/ "Message", require("messageicons").getColor(msg), require("messageicons").getImage(msg));
if (!isQuiet() && msg.new) {
msg.new = false;
Bangle.buzz();
}
drawMessage(ovr, msg);
};
let drawBorder = function(ovr) {
if (Bangle.isLocked())
ovr.setColor(ovr.theme.fgH);
else
ovr.setColor(ovr.theme.fg);
ovr.drawRect(0,0,ovr.getWidth()-1,ovr.getHeight()-1);
ovr.drawRect(1,1,ovr.getWidth()-2,ovr.getHeight()-2);
show(ovr);
if (!isQuiet()) Bangle.setLCDPower(1);
};
let showCall = function(ovr, msg) {
LOG("showCall");
LOG(msg);
if (msg.t == "remove") {
LOG("hide call screen");
next(ovr); //dont shift
return;
}
callInProgress = true;
let title = msg.title,
titleFont = settings.fontLarge,
lines;
if (title) {
let w = ovr.getWidth() - 35 -26;
if (ovr.setFont(titleFont).stringWidth(title) > w)
titleFont = settings.fontMedium;
if (ovr.setFont(titleFont).stringWidth(title) > w) {
lines = ovr.wrapString(title, w);
title = (lines.length > 2) ? lines.slice(0, 2).join("\n") + "..." : lines.join("\n");
}
}
drawScreen(ovr, title, titleFont, msg.src || /*LANG*/ "Message", require("messageicons").getColor(msg), require("messageicons").getImage(msg));
stopCallBuzz();
if (!isQuiet()) {
if (msg.new) {
msg.new = false;
if (callBuzzTimer) clearInterval(callBuzzTimer);
callBuzzTimer = setInterval(function() {
Bangle.buzz(500);
}, 1000);
Bangle.buzz(500);
}
}
drawMessage(ovr, msg);
};
let next = function(ovr) {
LOG("next");
stopCallBuzz();
if (!callInProgress)
eventQueue.shift();
callInProgress = false;
if (eventQueue.length == 0) {
LOG("no element in queue - closing");
cleanup();
return;
}
showMessage(ovr, eventQueue[0]);
};
let showMapMessage = function(ovr, msg) {
ovr.clearRect(2,2,ovr.getWidth()-3,ovr.getHeight()-3);
drawMessage(ovr, {
body: "Not implemented!"
});
};
let callBuzzTimer = null;
let stopCallBuzz = function() {
if (callBuzzTimer) {
clearInterval(callBuzzTimer);
callBuzzTimer = undefined;
}
};
let drawTriangleUp = function(ovr) {
ovr.reset();
ovr.fillPoly([ovr.getWidth()-9, 46,ovr.getWidth()-14, 56,ovr.getWidth()-4, 56]);
};
let drawTriangleDown = function(ovr) {
ovr.reset();
ovr.fillPoly([ovr.getWidth()-9, ovr.getHeight()-6, ovr.getWidth()-14, ovr.getHeight()-16, ovr.getWidth()-4, ovr.getHeight()-16]);
};
let scrollUp = function(ovr) {
msg = eventQueue[0];
LOG("up", msg);
if (typeof msg.FirstLine === "undefined")
msg.FirstLine = 0;
if (typeof msg.CanscrollUp === "undefined")
msg.CanscrollUp = false;
if (!msg.CanscrollUp) return;
msg.FirstLine = msg.FirstLine > 0 ? msg.FirstLine - 1 : 0;
drawMessage(ovr, msg);
};
let scrollDown = function(ovr) {
msg = eventQueue[0];
LOG("down", msg);
if (typeof msg.FirstLine === "undefined")
msg.FirstLine = 0;
if (typeof msg.CanscrollDown === "undefined")
msg.CanscrollDown = false;
if (!msg.CanscrollDown) return;
msg.FirstLine = msg.FirstLine + 1;
drawMessage(ovr, msg);
};
let drawMessage = function(ovr, msg) {
let MyWrapString = function(str, maxWidth) {
str = str.replace("\r\n", "\n").replace("\r", "\n");
return ovr.wrapString(str, maxWidth);
};
if (typeof msg.FirstLine === "undefined") msg.FirstLine = 0;
let bodyFont = typeof msg.bodyFont === "undefined" ? settings.fontMedium : msg.bodyFont;
let Padding = 3;
if (typeof msg.lines === "undefined") {
ovr.setFont(bodyFont);
msg.lines = MyWrapString(msg.body, ovr.getWidth() - (Padding * 2));
if (msg.lines.length <= 2) {
bodyFont = ovr.getFonts().includes("Vector") ? "Vector:20" : "6x8:3";
ovr.setFont(bodyFont);
msg.lines = MyWrapString(msg.body, ovr.getWidth() - (Padding * 2));
msg.bodyFont = bodyFont;
}
}
let NumLines = 7;
let linesToPrint = (msg.lines.length > NumLines) ? msg.lines.slice(msg.FirstLine, msg.FirstLine + NumLines) : msg.lines;
let yText = 40;
ovr.setBgColor(ovr.theme.bg);
ovr.setColor(ovr.theme.fg);
ovr.clearRect(2, yText, ovrw-3, ovrh-3);
let xText = Padding;
yText += Padding;
ovr.setFont(bodyFont);
let HText = ovr.getFontHeight();
yText = ((ovrh - yText) / 2) - (linesToPrint.length * HText / 2) + yText;
if (linesToPrint.length <= 3) {
ovr.setFontAlign(0, -1);
xText = ovr.getWidth() / 2;
} else
ovr.setFontAlign(-1, -1);
linesToPrint.forEach((line, i) => {
ovr.drawString(line, xText, yText + HText * i);
});
if (msg.FirstLine != 0) {
msg.CanscrollUp = true;
drawTriangleUp(ovr);
} else
msg.CanscrollUp = false;
if (msg.FirstLine + linesToPrint.length < msg.lines.length) {
msg.CanscrollDown = true;
drawTriangleDown(ovr);
} else
msg.CanscrollDown = false;
show(ovr);
if (!isQuiet()) Bangle.setLCDPower(1);
};
let getSwipeHandler = function(ovr){
return (lr, ud) => {
if (ud == 1) {
scrollUp(ovr);
} else if (ud == -1){
scrollDown(ovr);
}
};
};
let getTouchHandler = function(ovr){
return (_, xy) => {
if (xy.y < ovry + 40){
next(ovr);
}
};
};
let touchHandler;
let swipeHandler;
let restoreHandler = function(event){
if (backup[event]){
Bangle["#on" + event]=backup[event];
backup[event] = undefined;
}
};
let backupHandler = function(event){
if (eventQueue.length > 1 && ovr) return; // do not backup, overlay is already up
backup[event] = Bangle["#on" + event];
Bangle.removeAllListeners(event);
};
let cleanup = function(){
if (lockListener) {
Bangle.removeListener("lock", lockListener);
lockListener = undefined;
}
restoreHandler("touch");
restoreHandler("swipe");
restoreHandler("drag");
if (touchHandler) {
Bangle.removeListener("touch", touchHandler);
touchHandler = undefined;
}
if (swipeHandler) {
Bangle.removeListener("swipe", swipeHandler);
swipeHandler = undefined;
}
Bangle.setLCDOverlay();
ovr = undefined;
quiet = undefined;
};
let backup = {};
let main = function(ovr, event) {
LOG("Main", event, settings);
if (!lockListener) {
lockListener = function (){
drawBorder(ovr);
};
Bangle.on('lock', lockListener);
}
backupHandler("touch");
backupHandler("swipe");
backupHandler("drag");
if (touchHandler) Bangle.removeListener("touch",touchHandler);
if (swipeHandler) Bangle.removeListener("swipe",swipeHandler);
touchHandler = getTouchHandler(ovr);
swipeHandler = getSwipeHandler(ovr);
Bangle.on('touch', touchHandler);
Bangle.on('swipe', swipeHandler);
if (event !== undefined){
drawBorder(ovr);
manageEvent(ovr, event);
} else {
LOG("No event given");
cleanup();
}
};
let ovr;
exports.pushMessage = function(event) {
if( event.id=="music") return require_real("messages").pushMessage(event);
bpp = 4;
if (process.memory().free < 2000) bpp = 1;
if (!ovr) {
ovr = Graphics.createArrayBuffer(ovrw, ovrh, bpp, {
msb: true
});
} else {
ovr.clear();
}
g = ovr;
if (bpp == 4)
ovr.theme = g.theme;
else
ovr.theme = { fg:0, bg:1, fg2:1, bg2:0, fgH:1, bgH:0 };
main(ovr, event);
g = _g;
};
//Call original message library
exports.clearAll = function() { return require_real("messages").clearAll();};
exports.getMessages = function() { return require_real("messages").getMessages();};
exports.status = function() { return require_real("messages").status();};
exports.buzz = function() { return require_real("messages").buzz(msgSrc);};
exports.stopBuzz = function() { return require_real("messages").stopBuzz();};

View File

@ -0,0 +1,18 @@
{
"id": "messagesoverlay",
"name": "Messages Overlay",
"version": "0.01",
"description": "An overlay based implementation of a messages UI (display notifications from iOS and Gadgetbridge/Android)",
"icon": "app.png",
"type": "bootloader",
"tags": "tool,system",
"supports": ["BANGLEJS2"],
"dependencies" : { "messageicons":"module","messages":"app" },
"readme": "README.md",
"storage": [
{"name":"messagesoverlay.settings.js","url":"settings.js"},
{"name":"messagesoverlay","url":"lib.js"},
{"name":"messagesoverlay.boot.js","url":"boot.js"}
],
"screenshots": [{"url":"screen_call.png"} ,{"url":"screen_message.png"} ]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -0,0 +1 @@
eval(require("Storage").read("messages.settings.js"));

View File

@ -7,3 +7,5 @@
0.06: Allow logging of some things using power 0.06: Allow logging of some things using power
Add widget for live monitoring of power use Add widget for live monitoring of power use
0.07: Convert Yes/No On/Off in settings to checkboxes 0.07: Convert Yes/No On/Off in settings to checkboxes
0.08: Fix the wrapping of intervals/timeouts with parameters
Fix the widget drawing if widgets are hidden and Bangle.setLCDBrightness is called

View File

@ -19,6 +19,14 @@ You can switch on logging in the options to diagnose unexpected power use. Curre
Do not use trace logging for extended time, it uses a lot of storage and can fill up the flash quite quick. Do not use trace logging for extended time, it uses a lot of storage and can fill up the flash quite quick.
### TODO
* Wrap functions given as strings to setTimeout/setInterval
* Handle eval in setTimeout/setInterval
* Track functions executed as event handlers
* Track buzzer
* Modify browser interface to estimate power use like widget does
## Internals ## Internals
Battery calibration offset is set by writing `batFullVoltage` in setting.json Battery calibration offset is set by writing `batFullVoltage` in setting.json

View File

@ -77,14 +77,14 @@
let functions = {}; let functions = {};
let wrapDeferred = ((o,t) => (a) => { let wrapDeferred = ((o,t) => (a) => {
if (a == eval){ if (a == eval || typeof a == "string") {
return o.apply(this, arguments); return o.apply(this, arguments);
} else { } else {
let wrapped = a; let wrapped = a;
if (!a.__wrapped){ if (!a.__wrapped){
wrapped = ()=>{ wrapped = ()=>{
let start = Date.now(); let start = Date.now();
let result = a.apply(undefined, arguments.slice(1)); let result = a.apply(undefined, arguments.slice(2)); // function arguments for deferred calls start at index 2, first is function, second is time
let end = Date.now()-start; let end = Date.now()-start;
let f = a.toString().substring(0,100); let f = a.toString().substring(0,100);
if (settings.logDetails) logDeferred(t, end, f); if (settings.logDetails) logDeferred(t, end, f);

View File

@ -2,7 +2,7 @@
"id": "powermanager", "id": "powermanager",
"name": "Power Manager", "name": "Power Manager",
"shortName": "Power Manager", "shortName": "Power Manager",
"version": "0.07", "version": "0.08",
"description": "Allow configuration of warnings and thresholds for battery charging and display.", "description": "Allow configuration of warnings and thresholds for battery charging and display.",
"icon": "app.png", "icon": "app.png",
"type": "bootloader", "type": "bootloader",
@ -20,6 +20,7 @@
"data": [ "data": [
{"name":"powermanager.hw.json"}, {"name":"powermanager.hw.json"},
{"name":"powermanager.def.json"}, {"name":"powermanager.def.json"},
{"name":"powermanager.json"},
{"name":"powermanager.log"} {"name":"powermanager.log"}
] ]
} }

View File

@ -24,7 +24,7 @@ currently-running apps */
let brightnessSetting = settings.brightness || 1; let brightnessSetting = settings.brightness || 1;
Bangle.setLCDBrightness = ((o) => (a) => { Bangle.setLCDBrightness = ((o) => (a) => {
brightnessSetting = a; brightnessSetting = a;
draw(WIDGETS.powermanager); WIDGETS.powermanager.draw(WIDGETS.powermanager);
return o(a); return o(a);
})(Bangle.setLCDBrightness); })(Bangle.setLCDBrightness);

View File

@ -13,5 +13,7 @@
0.12: Fix for recorder not stopping at end of run. Bug introduced in 0.11 0.12: Fix for recorder not stopping at end of run. Bug introduced in 0.11
0.13: Revert #1578 (stop duplicate entries) as with 2v12 menus it causes other boxes to be wiped (fix #1643) 0.13: Revert #1578 (stop duplicate entries) as with 2v12 menus it causes other boxes to be wiped (fix #1643)
0.14: Fix Bangle.js 1 issue where after the 'overwrite track' menu, the start/stop button stopped working 0.14: Fix Bangle.js 1 issue where after the 'overwrite track' menu, the start/stop button stopped working
0.15: Diverge from the standard "Run" app. Swipe to intensity interface a la Karvonnen (curtesy of FTeacher at https://github.com/f-teacher) 0.15: Diverge from the standard "Run" app. Swipe to intensity interface a la Karvonen (curtesy of FTeacher at https://github.com/f-teacher)
Keep run state between runs (allowing you to exit and restart the app) Keep run state between runs (allowing you to exit and restart the app)
0.16: Don't clear zone 2b indicator segment when updating HRM reading.
Write to correct settings file, fixing settings not working.

View File

@ -67,3 +67,10 @@ app loader, the module is automatically included in the app's source. However
when developing via the IDE the module won't get pulled in by default. when developing via the IDE the module won't get pulled in by default.
There are some options to fix this easily - please check out the [modules README.md file](https://github.com/espruino/BangleApps/blob/master/modules/README.md) There are some options to fix this easily - please check out the [modules README.md file](https://github.com/espruino/BangleApps/blob/master/modules/README.md)
## Contributors (Run and Run+)
gfwilliams
hughbarney
GrandVizierOlaf
BartS23
f-teacher
thyttan

View File

@ -2,7 +2,7 @@
let wu = require("widget_utils"); let wu = require("widget_utils");
let runInterval; let runInterval;
let karvonnenActive = false; let karvonenActive = false;
// Run interface wrapped in a function // Run interface wrapped in a function
let ExStats = require("exstats"); let ExStats = require("exstats");
let B2 = process.env.HWVERSION===2; let B2 = process.env.HWVERSION===2;
@ -63,7 +63,6 @@ function setStatus(running) {
function onStartStop() { function onStartStop() {
let running = !exs.state.active; let running = !exs.state.active;
let prepPromises = []; let prepPromises = [];
// start/stop recording // start/stop recording
// Do this first in case recorder needs to prompt for // Do this first in case recorder needs to prompt for
// an overwrite before we start tracking exstats // an overwrite before we start tracking exstats
@ -155,7 +154,7 @@ Bangle.on("GPS", function(fix) {
} }
}); });
// run() function used to switch between traditional run UI and karvonnen UI // run() function used to switch between traditional run UI and karvonen UI
function run() { function run() {
wu.show(); wu.show();
layout.lazy = false; layout.lazy = false;
@ -165,35 +164,35 @@ function run() {
if (!runInterval){ if (!runInterval){
runInterval = setInterval(function() { runInterval = setInterval(function() {
layout.clock.label = locale.time(new Date(),1); layout.clock.label = locale.time(new Date(),1);
if (!isMenuDisplayed && !karvonnenActive) layout.render(); if (!isMenuDisplayed && !karvonenActive) layout.render();
}, 1000); }, 1000);
} }
} }
run(); run();
/////////////////////////////////////////////// ///////////////////////////////////////////////
// Karvonnen // Karvonen
/////////////////////////////////////////////// ///////////////////////////////////////////////
function stopRunUI() { function stopRunUI() {
// stop updating and drawing the traditional run app UI // stop updating and drawing the traditional run app UI
clearInterval(runInterval); clearInterval(runInterval);
runInterval = undefined; runInterval = undefined;
karvonnenActive = true; karvonenActive = true;
} }
function stopKarvonnenUI() { function stopKarvonenUI() {
g.reset().clear(); g.reset().clear();
clearInterval(karvonnenInterval); clearInterval(karvonenInterval);
karvonnenInterval = undefined; karvonenInterval = undefined;
karvonnenActive = false; karvonenActive = false;
} }
let karvonnenInterval; let karvonenInterval;
// Define the function to go back and forth between the different UI's // Define the function to go back and forth between the different UI's
function swipeHandler(LR,_) { function swipeHandler(LR,_) {
if (LR==-1 && karvonnenActive && !isMenuDisplayed) {stopKarvonnenUI(); run();} if (LR==-1 && karvonenActive && !isMenuDisplayed) {stopKarvonenUI(); run();}
if (LR==1 && !karvonnenActive && !isMenuDisplayed) {stopRunUI(); karvonnenInterval = eval(require("Storage").read("runplus_karvonnen"))(settings.HRM, exs.stats.bpm);} if (LR==1 && !karvonenActive && !isMenuDisplayed) {stopRunUI(); karvonenInterval = eval(require("Storage").read("runplus_karvonen"))(settings.HRM, exs.stats.bpm);}
} }
// Listen for swipes with the swipeHandler // Listen for swipes with the swipeHandler
Bangle.on("swipe", swipeHandler); Bangle.on("swipe", swipeHandler);

View File

@ -1,6 +1,6 @@
(function karvonnen(hrmSettings, exsHrmStats) { (function karvonen(hrmSettings, exsHrmStats) {
//This app is an extra feature implementation for the Run.app of the bangle.js. It's called run+ //This app is an extra feature implementation for the Run.app of the bangle.js. It's called run+
//The calculation of the Heart Rate Zones is based on the Karvonnen method. It requires to know maximum and minimum heart rates. More precise calculation methods require a lab. //The calculation of the Heart Rate Zones is based on the Karvonen method. It requires to know maximum and minimum heart rates. More precise calculation methods require a lab.
//Other methods are even more approximative. //Other methods are even more approximative.
let wu = require("widget_utils"); let wu = require("widget_utils");
wu.hide(); wu.hide();
@ -56,7 +56,7 @@
let hr = exsHrmStats.getValue(); let hr = exsHrmStats.getValue();
let hr1 = hr; let hr1 = hr;
// These letiables display next and previous HR zone. // These letiables display next and previous HR zone.
//get the hrzones right. The calculation of the Heart rate zones here is based on the Karvonnen method //get the hrzones right. The calculation of the Heart rate zones here is based on the Karvonen method
//60-70% of HRR+minHR = zone2. //70-80% of HRR+minHR = zone3. //80-90% of HRR+minHR = zone4. //90-99% of HRR+minHR = zone5. //=>99% of HRR+minHR = serious risk of heart attack //60-70% of HRR+minHR = zone2. //70-80% of HRR+minHR = zone3. //80-90% of HRR+minHR = zone4. //90-99% of HRR+minHR = zone5. //=>99% of HRR+minHR = serious risk of heart attack
let minzone2 = hrr * 0.6 + minhr; let minzone2 = hrr * 0.6 + minhr;
let maxzone2 = hrr * 0.7 + minhr; let maxzone2 = hrr * 0.7 + minhr;
@ -67,7 +67,7 @@
// HR data: large, readable, in the middle of the screen // HR data: large, readable, in the middle of the screen
function drawHR() { function drawHR() {
g.setFontAlign(-1,0,0); g.setFontAlign(-1,0,0);
g.clearRect(Rdiv(x,11/4),Rdiv(y,2)-25,Rdiv(x,11/4)+50*2,Rdiv(y,2)+25); g.clearRect(Rdiv(x,11/4),Rdiv(y,2)-25,Rdiv(x,11/4)+50*2-14,Rdiv(y,2)+25);
g.setColor(g.theme.fg); g.setColor(g.theme.fg);
g.setFont("Vector",50); g.setFont("Vector",50);
g.drawString(hr, Rdiv(x,11/4), Rdiv(y,2)+4); g.drawString(hr, Rdiv(x,11/4), Rdiv(y,2)+4);
@ -207,9 +207,9 @@
initDraw(); initDraw();
// check for updates every second. // check for updates every second.
karvonnenInterval = setInterval(function() { karvonenInterval = setInterval(function() {
if (!isMenuDisplayed && karvonnenActive) updateUI(); if (!isMenuDisplayed && karvonenActive) updateUI();
}, 1000); }, 1000);
return karvonnenInterval; return karvonenInterval;
}) })

View File

@ -1,10 +1,10 @@
{ {
"id": "runplus", "id": "runplus",
"name": "Run+", "name": "Run+",
"version": "0.15", "version": "0.16",
"description": "Displays distance, time, steps, cadence, pace and more for runners. Based on the Run app, but extended with additional screen for heart rate interval training.", "description": "Displays distance, time, steps, cadence, pace and more for runners. Based on the Run app, but extended with additional screen for heart rate interval training.",
"icon": "app.png", "icon": "app.png",
"tags": "run,running,fitness,outdoors,gps,karvonnen", "tags": "run,running,fitness,outdoors,gps,karvonen,karvonnen",
"supports": [ "supports": [
"BANGLEJS2" "BANGLEJS2"
], ],
@ -29,8 +29,8 @@
"url": "settings.js" "url": "settings.js"
}, },
{ {
"name": "runplus_karvonnen", "name": "runplus_karvonen",
"url": "karvonnen.js" "url": "karvonen.js"
} }
], ],
"data": [ "data": [

View File

@ -1,5 +1,5 @@
(function(back) { (function(back) {
const SETTINGS_FILE = "run.json"; const SETTINGS_FILE = "runplus.json";
var ExStats = require("exstats"); var ExStats = require("exstats");
var statsList = ExStats.getList(); var statsList = ExStats.getList();
statsList.unshift({name:"-",id:""}); // add blank menu item statsList.unshift({name:"-",id:""}); // add blank menu item

View File

@ -6,3 +6,5 @@
Update to match the default alarm widget, and not show itself when an alarm is hidden. Update to match the default alarm widget, and not show itself when an alarm is hidden.
0.04: Fix check for active alarm 0.04: Fix check for active alarm
0.05: Convert Yes/No On/Off in settings to checkboxes 0.05: Convert Yes/No On/Off in settings to checkboxes
0.06: Remember next alarm to reduce calculation amount
Redraw only every hour when no alarm in next 24h

View File

@ -2,7 +2,7 @@
"id": "widalarmeta", "id": "widalarmeta",
"name": "Alarm & Timer ETA", "name": "Alarm & Timer ETA",
"shortName": "Alarm ETA", "shortName": "Alarm ETA",
"version": "0.05", "version": "0.06",
"description": "A widget that displays the time to the next Alarm or Timer in hours and minutes, maximum 24h (configurable).", "description": "A widget that displays the time to the next Alarm or Timer in hours and minutes, maximum 24h (configurable).",
"icon": "widget.png", "icon": "widget.png",
"type": "widget", "type": "widget",

View File

@ -1,24 +1,43 @@
(() => { (() => {
require("Font5x9Numeric7Seg").add(Graphics); require("Font5x9Numeric7Seg").add(Graphics);
const alarms = require("Storage").readJSON("sched.json",1) || [];
const config = Object.assign({ const config = Object.assign({
maxhours: 24, maxhours: 24,
drawBell: false, drawBell: false,
showSeconds: 0, // 0=never, 1=only when display is unlocked, 2=for less than a minute showSeconds: 0, // 0=never, 1=only when display is unlocked, 2=for less than a minute
}, require("Storage").readJSON("widalarmeta.json",1) || {}); }, require("Storage").readJSON("widalarmeta.json",1) || {});
function draw() { function getNextAlarm(date) {
const times = alarms const alarms = (require("Storage").readJSON("sched.json",1) || []).filter(alarm => alarm.on && alarm.hidden !== true);
.map(alarm => WIDGETS["widalarmeta"].numActiveAlarms = alarms.length;
alarm.hidden !== true const times = alarms.map(alarm => require("sched").getTimeToAlarm(alarm, date) || Number.POSITIVE_INFINITY);
&& require("sched").getTimeToAlarm(alarm) const eta = times.length > 0 ? Math.min.apply(null, times) : 0;
) if (eta !== Number.POSITIVE_INFINITY) {
.filter(a => a !== undefined); const idx = times.indexOf(eta);
const next = times.length > 0 ? Math.min.apply(null, times) : 0; const alarm = alarms[idx];
delete alarm.msg; delete alarm.id; delete alarm.data; // free some memory
return alarm;
}
} // getNextAlarm
function draw(fromInterval) {
if (this.nextAlarm === undefined) {
let alarm = getNextAlarm();
if (alarm === undefined) {
// try again with next hour
const nextHour = new Date();
nextHour.setHours(nextHour.getHours()+1);
alarm = getNextAlarm(nextHour);
}
if (alarm !== undefined) {
this.nextAlarm = alarm;
}
}
const next = this.nextAlarm !== undefined ? require("sched").getTimeToAlarm(this.nextAlarm) : 0;
let calcWidth = 0; let calcWidth = 0;
let drawSeconds = false; let drawSeconds = false;
if (next > 0 && next < config.maxhours*60*60*1000) { if (next > 0 && next <= config.maxhours*60*60*1000) {
const hours = Math.floor((next-1) / 3600000).toString(); const hours = Math.floor((next-1) / 3600000).toString();
const minutes = Math.floor(((next-1) % 3600000) / 60000).toString(); const minutes = Math.floor(((next-1) % 3600000) / 60000).toString();
const seconds = Math.floor(((next-1) % 60000) / 1000).toString(); const seconds = Math.floor(((next-1) % 60000) / 1000).toString();
@ -39,10 +58,14 @@
if (drawSeconds) { if (drawSeconds) {
calcWidth += 3*5; calcWidth += 3*5;
} }
} else if (config.drawBell && alarms.some(alarm=>alarm.on&&(alarm.hidden!==true))) { this.bellVisible = false;
// next alarm too far in future, draw only widalarm bell } else if (config.drawBell && this.numActiveAlarms > 0) {
g.reset().drawImage(atob("GBgBAAAAAAAAABgADhhwDDwwGP8YGf+YMf+MM//MM//MA//AA//AA//AA//AA//AA//AB//gD//wD//wAAAAADwAABgAAAAAAAAA"),this.x,this.y);
calcWidth = 24; calcWidth = 24;
// next alarm too far in future, draw only widalarm bell
if (this.bellVisible !== true || fromInterval !== true) {
g.reset().drawImage(atob("GBgBAAAAAAAAABgADhhwDDwwGP8YGf+YMf+MM//MM//MA//AA//AA//AA//AA//AA//AB//gD//wD//wAAAAADwAABgAAAAAAAAA"),this.x,this.y);
this.bellVisible = true;
}
} }
if (this.width !== calcWidth) { if (this.width !== calcWidth) {
@ -51,8 +74,8 @@
Bangle.drawWidgets(); Bangle.drawWidgets();
} }
// redraw next full minute or second // redraw next hour when no alarm else full minute or second
const period = drawSeconds ? 1000 : 60000; const period = next === 0 ? 3600000 : (drawSeconds ? 1000 : 60000);
let timeout = next > 0 ? next % period : period - (Date.now() % period); let timeout = next > 0 ? next % period : period - (Date.now() % period);
if (timeout === 0) { if (timeout === 0) {
timeout += period; timeout += period;
@ -62,8 +85,8 @@
clearTimeout(this.timeoutId); clearTimeout(this.timeoutId);
} }
this.timeoutId = setTimeout(()=>{ this.timeoutId = setTimeout(()=>{
this.timeoutId = undefined; WIDGETS["widalarmeta"].timeoutId = undefined;
this.draw(); WIDGETS["widalarmeta"].draw(true);
}, timeout); }, timeout);
} /* draw */ } /* draw */

View File

@ -15,3 +15,4 @@
0.16: Increase screen update rate when charging 0.16: Increase screen update rate when charging
0.17: Add option 'Remove Jitter'='Drop only' to prevent percentage from getting up again when not charging 0.17: Add option 'Remove Jitter'='Drop only' to prevent percentage from getting up again when not charging
Add option to disable vibration when charger connects Add option to disable vibration when charger connects
0.18: Only redraw when values change

View File

@ -2,7 +2,7 @@
"id": "widbatpc", "id": "widbatpc",
"name": "Battery Level Widget (with percentage)", "name": "Battery Level Widget (with percentage)",
"shortName": "Battery Widget", "shortName": "Battery Widget",
"version": "0.17", "version": "0.18",
"description": "Show the current battery level and charging status in the top right of the clock, with charge percentage", "description": "Show the current battery level and charging status in the top right of the clock, with charge percentage",
"icon": "widget.png", "icon": "widget.png",
"type": "widget", "type": "widget",

View File

@ -86,7 +86,7 @@
return changed; return changed;
} }
function draw() { function draw(fromInterval) {
// if hidden, don't draw // if hidden, don't draw
if (!WIDGETS["batpc"].width) return; if (!WIDGETS["batpc"].width) return;
// else... // else...
@ -103,6 +103,14 @@
l = prevMin; l = prevMin;
} }
} }
if (fromInterval === true && this.prevLevel === l && this.prevCharging === Bangle.isCharging()) {
return; // unchanged, do nothing
}
this.prevLevel = l;
this.prevCharging = Bangle.isCharging();
const c = levelColor(l); const c = levelColor(l);
if (Bangle.isCharging() && setting('charger')) { if (Bangle.isCharging() && setting('charger')) {
@ -173,7 +181,7 @@
if (on) update(); if (on) update();
}); });
var id = setInterval(()=>WIDGETS["batpc"].draw(), intervalLow); var id = setInterval(()=>WIDGETS["batpc"].draw(true), intervalLow);
WIDGETS["batpc"]={area:"tr",width:40,draw:draw,reload:reload}; WIDGETS["batpc"]={area:"tr",width:40,draw:draw,reload:reload};
setWidth(); setWidth();

View File

@ -44,6 +44,7 @@ declare module ClockInfo {
w: number, w: number,
h: number, h: number,
draw(itm: MenuItem, info: Item, options: InteractiveOptions): void, draw(itm: MenuItem, info: Item, options: InteractiveOptions): void,
app?: string, // used to remember clock_info locations, per app
}; };
type InteractiveOptions = type InteractiveOptions =

View File

@ -8688,6 +8688,15 @@ interface ObjectConstructor {
*/ */
entries(object: any): Array<[string, any]>; entries(object: any): Array<[string, any]>;
/**
* Transforms an array of key-value pairs into an object
*
* @param {any} entries - An array of `[key,value]` pairs to be used to create an object
* @returns {any} An object containing all the specified pairs
* @url http://www.espruino.com/Reference#l_Object_fromEntries
*/
fromEntries(entries: any): any;
/** /**
* Creates a new object with the specified prototype object and properties. * Creates a new object with the specified prototype object and properties.
* properties are currently unsupported. * properties are currently unsupported.
@ -8709,6 +8718,15 @@ interface ObjectConstructor {
*/ */
getOwnPropertyDescriptor(obj: any, name: any): any; getOwnPropertyDescriptor(obj: any, name: any): any;
/**
* Get information on all properties in the object (from `Object.getOwnPropertyDescriptor`), or just `{}` if no properties
*
* @param {any} obj - The object
* @returns {any} An object containing all the property descriptors of an object
* @url http://www.espruino.com/Reference#l_Object_getOwnPropertyDescriptors
*/
getOwnPropertyDescriptors(obj: any): any;
/** /**
* Add a new property to the Object. 'Desc' is an object with the following fields: * Add a new property to the Object. 'Desc' is an object with the following fields:
* * `configurable` (bool = false) - can this property be changed/deleted (not * * `configurable` (bool = false) - can this property be changed/deleted (not
@ -8945,32 +8963,32 @@ interface Function {
/** /**
* This executes the function with the supplied 'this' argument and parameters * This executes the function with the supplied 'this' argument and parameters
* *
* @param {any} this - The value to use as the 'this' argument when executing the function * @param {any} thisArg - The value to use as the 'this' argument when executing the function
* @param {any} params - Optional Parameters * @param {any} params - Optional Parameters
* @returns {any} The return value of executing this function * @returns {any} The return value of executing this function
* @url http://www.espruino.com/Reference#l_Function_call * @url http://www.espruino.com/Reference#l_Function_call
*/ */
call(this: any, ...params: any[]): any; call(thisArg: any, ...params: any[]): any;
/** /**
* This executes the function with the supplied 'this' argument and parameters * This executes the function with the supplied 'this' argument and parameters
* *
* @param {any} this - The value to use as the 'this' argument when executing the function * @param {any} thisArg - The value to use as the 'this' argument when executing the function
* @param {any} args - Optional Array of Arguments * @param {any} args - Optional Array of Arguments
* @returns {any} The return value of executing this function * @returns {any} The return value of executing this function
* @url http://www.espruino.com/Reference#l_Function_apply * @url http://www.espruino.com/Reference#l_Function_apply
*/ */
apply(this: any, args: any): any; apply(thisArg: any, args: ArrayLike<any>): any;
/** /**
* This executes the function with the supplied 'this' argument and parameters * This executes the function with the supplied 'this' argument and parameters
* *
* @param {any} this - The value to use as the 'this' argument when executing the function * @param {any} thisArg - The value to use as the 'this' argument when executing the function
* @param {any} params - Optional Default parameters that are prepended to the call * @param {any} params - Optional Default parameters that are prepended to the call
* @returns {any} The 'bound' function * @returns {any} The 'bound' function
* @url http://www.espruino.com/Reference#l_Function_bind * @url http://www.espruino.com/Reference#l_Function_bind
*/ */
bind(this: any, ...params: any[]): any; bind(thisArg: any, ...params: any[]): any;
} }
/** /**