diff --git a/apps/backswipe/README.md b/apps/backswipe/README.md index 1611263bb..21aa357b3 100644 --- a/apps/backswipe/README.md +++ b/apps/backswipe/README.md @@ -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. +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 Kedlub diff --git a/apps/messagesoverlay/ChangeLog b/apps/messagesoverlay/ChangeLog new file mode 100644 index 000000000..da98bfbce --- /dev/null +++ b/apps/messagesoverlay/ChangeLog @@ -0,0 +1 @@ +0.01: Initial fork from messages_light \ No newline at end of file diff --git a/apps/messagesoverlay/README.md b/apps/messagesoverlay/README.md new file mode 100644 index 000000000..8ce1cc64d --- /dev/null +++ b/apps/messagesoverlay/README.md @@ -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 + diff --git a/apps/messagesoverlay/app-icon.png b/apps/messagesoverlay/app-icon.png new file mode 100644 index 000000000..c9b4b62ac Binary files /dev/null and b/apps/messagesoverlay/app-icon.png differ diff --git a/apps/messagesoverlay/app.png b/apps/messagesoverlay/app.png new file mode 100644 index 000000000..1f738504d Binary files /dev/null and b/apps/messagesoverlay/app.png differ diff --git a/apps/messagesoverlay/boot.js b/apps/messagesoverlay/boot.js new file mode 100644 index 000000000..7731d608a --- /dev/null +++ b/apps/messagesoverlay/boot.js @@ -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); + diff --git a/apps/messagesoverlay/lib.js b/apps/messagesoverlay/lib.js new file mode 100644 index 000000000..cc6b63176 --- /dev/null +++ b/apps/messagesoverlay/lib.js @@ -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();}; \ No newline at end of file diff --git a/apps/messagesoverlay/metadata.json b/apps/messagesoverlay/metadata.json new file mode 100644 index 000000000..9efe95d26 --- /dev/null +++ b/apps/messagesoverlay/metadata.json @@ -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"} ] +} diff --git a/apps/messagesoverlay/screen_call.png b/apps/messagesoverlay/screen_call.png new file mode 100644 index 000000000..45326b37e Binary files /dev/null and b/apps/messagesoverlay/screen_call.png differ diff --git a/apps/messagesoverlay/screen_message.png b/apps/messagesoverlay/screen_message.png new file mode 100644 index 000000000..f3cc3b6e5 Binary files /dev/null and b/apps/messagesoverlay/screen_message.png differ diff --git a/apps/messagesoverlay/settings.js b/apps/messagesoverlay/settings.js new file mode 100644 index 000000000..b7197c70a --- /dev/null +++ b/apps/messagesoverlay/settings.js @@ -0,0 +1 @@ +eval(require("Storage").read("messages.settings.js")); diff --git a/apps/powermanager/ChangeLog b/apps/powermanager/ChangeLog index a43558c3f..ee31195b2 100644 --- a/apps/powermanager/ChangeLog +++ b/apps/powermanager/ChangeLog @@ -7,3 +7,5 @@ 0.06: Allow logging of some things using power Add widget for live monitoring of power use 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 \ No newline at end of file diff --git a/apps/powermanager/README.md b/apps/powermanager/README.md index 043b5ca8c..804e986e7 100644 --- a/apps/powermanager/README.md +++ b/apps/powermanager/README.md @@ -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. +### 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 Battery calibration offset is set by writing `batFullVoltage` in setting.json diff --git a/apps/powermanager/boot.js b/apps/powermanager/boot.js index c931a243b..f3e3f718f 100644 --- a/apps/powermanager/boot.js +++ b/apps/powermanager/boot.js @@ -77,14 +77,14 @@ let functions = {}; let wrapDeferred = ((o,t) => (a) => { - if (a == eval){ + if (a == eval || typeof a == "string") { return o.apply(this, arguments); } else { let wrapped = a; if (!a.__wrapped){ wrapped = ()=>{ 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 f = a.toString().substring(0,100); if (settings.logDetails) logDeferred(t, end, f); diff --git a/apps/powermanager/metadata.json b/apps/powermanager/metadata.json index ca5a9f00b..5487c2278 100644 --- a/apps/powermanager/metadata.json +++ b/apps/powermanager/metadata.json @@ -2,7 +2,7 @@ "id": "powermanager", "name": "Power Manager", "shortName": "Power Manager", - "version": "0.07", + "version": "0.08", "description": "Allow configuration of warnings and thresholds for battery charging and display.", "icon": "app.png", "type": "bootloader", @@ -20,6 +20,7 @@ "data": [ {"name":"powermanager.hw.json"}, {"name":"powermanager.def.json"}, + {"name":"powermanager.json"}, {"name":"powermanager.log"} ] } diff --git a/apps/powermanager/widget.js b/apps/powermanager/widget.js index 116f1703e..3147c40ac 100644 --- a/apps/powermanager/widget.js +++ b/apps/powermanager/widget.js @@ -24,7 +24,7 @@ currently-running apps */ let brightnessSetting = settings.brightness || 1; Bangle.setLCDBrightness = ((o) => (a) => { brightnessSetting = a; - draw(WIDGETS.powermanager); + WIDGETS.powermanager.draw(WIDGETS.powermanager); return o(a); })(Bangle.setLCDBrightness); diff --git a/apps/runplus/ChangeLog b/apps/runplus/ChangeLog index 454b12b6d..0e19b5559 100644 --- a/apps/runplus/ChangeLog +++ b/apps/runplus/ChangeLog @@ -13,5 +13,7 @@ 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.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) +0.16: Don't clear zone 2b indicator segment when updating HRM reading. + Write to correct settings file, fixing settings not working. diff --git a/apps/runplus/README.md b/apps/runplus/README.md index 7f645b518..659cd964d 100644 --- a/apps/runplus/README.md +++ b/apps/runplus/README.md @@ -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. 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 diff --git a/apps/runplus/app.js b/apps/runplus/app.js index 633e47983..9d7010e6c 100644 --- a/apps/runplus/app.js +++ b/apps/runplus/app.js @@ -2,7 +2,7 @@ let wu = require("widget_utils"); let runInterval; -let karvonnenActive = false; +let karvonenActive = false; // Run interface wrapped in a function let ExStats = require("exstats"); let B2 = process.env.HWVERSION===2; @@ -63,7 +63,6 @@ function setStatus(running) { function onStartStop() { let running = !exs.state.active; let prepPromises = []; - // start/stop recording // Do this first in case recorder needs to prompt for // 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() { wu.show(); layout.lazy = false; @@ -165,35 +164,35 @@ function run() { if (!runInterval){ runInterval = setInterval(function() { layout.clock.label = locale.time(new Date(),1); - if (!isMenuDisplayed && !karvonnenActive) layout.render(); + if (!isMenuDisplayed && !karvonenActive) layout.render(); }, 1000); } } run(); /////////////////////////////////////////////// -// Karvonnen +// Karvonen /////////////////////////////////////////////// function stopRunUI() { // stop updating and drawing the traditional run app UI clearInterval(runInterval); runInterval = undefined; - karvonnenActive = true; + karvonenActive = true; } -function stopKarvonnenUI() { +function stopKarvonenUI() { g.reset().clear(); - clearInterval(karvonnenInterval); - karvonnenInterval = undefined; - karvonnenActive = false; + clearInterval(karvonenInterval); + karvonenInterval = undefined; + karvonenActive = false; } -let karvonnenInterval; +let karvonenInterval; // Define the function to go back and forth between the different UI's function swipeHandler(LR,_) { - if (LR==-1 && karvonnenActive && !isMenuDisplayed) {stopKarvonnenUI(); run();} - if (LR==1 && !karvonnenActive && !isMenuDisplayed) {stopRunUI(); karvonnenInterval = eval(require("Storage").read("runplus_karvonnen"))(settings.HRM, exs.stats.bpm);} + if (LR==-1 && karvonenActive && !isMenuDisplayed) {stopKarvonenUI(); run();} + if (LR==1 && !karvonenActive && !isMenuDisplayed) {stopRunUI(); karvonenInterval = eval(require("Storage").read("runplus_karvonen"))(settings.HRM, exs.stats.bpm);} } // Listen for swipes with the swipeHandler Bangle.on("swipe", swipeHandler); diff --git a/apps/runplus/karvonnen.js b/apps/runplus/karvonen.js similarity index 95% rename from apps/runplus/karvonnen.js rename to apps/runplus/karvonen.js index 7e7c07050..de81494bb 100644 --- a/apps/runplus/karvonnen.js +++ b/apps/runplus/karvonen.js @@ -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+ - //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. let wu = require("widget_utils"); wu.hide(); @@ -56,7 +56,7 @@ let hr = exsHrmStats.getValue(); let hr1 = hr; // 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 let minzone2 = hrr * 0.6 + minhr; let maxzone2 = hrr * 0.7 + minhr; @@ -67,7 +67,7 @@ // HR data: large, readable, in the middle of the screen function drawHR() { 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.setFont("Vector",50); g.drawString(hr, Rdiv(x,11/4), Rdiv(y,2)+4); @@ -207,9 +207,9 @@ initDraw(); // check for updates every second. - karvonnenInterval = setInterval(function() { - if (!isMenuDisplayed && karvonnenActive) updateUI(); + karvonenInterval = setInterval(function() { + if (!isMenuDisplayed && karvonenActive) updateUI(); }, 1000); - return karvonnenInterval; + return karvonenInterval; }) diff --git a/apps/runplus/metadata.json b/apps/runplus/metadata.json index 15492e20c..017a52ac3 100644 --- a/apps/runplus/metadata.json +++ b/apps/runplus/metadata.json @@ -1,10 +1,10 @@ { "id": "runplus", "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.", "icon": "app.png", - "tags": "run,running,fitness,outdoors,gps,karvonnen", + "tags": "run,running,fitness,outdoors,gps,karvonen,karvonnen", "supports": [ "BANGLEJS2" ], @@ -29,8 +29,8 @@ "url": "settings.js" }, { - "name": "runplus_karvonnen", - "url": "karvonnen.js" + "name": "runplus_karvonen", + "url": "karvonen.js" } ], "data": [ diff --git a/apps/runplus/settings.js b/apps/runplus/settings.js index 050eed4b8..cd72022be 100644 --- a/apps/runplus/settings.js +++ b/apps/runplus/settings.js @@ -1,5 +1,5 @@ (function(back) { - const SETTINGS_FILE = "run.json"; + const SETTINGS_FILE = "runplus.json"; var ExStats = require("exstats"); var statsList = ExStats.getList(); statsList.unshift({name:"-",id:""}); // add blank menu item diff --git a/apps/widalarmeta/ChangeLog b/apps/widalarmeta/ChangeLog index 66b9a4b8e..4bcf6ec69 100644 --- a/apps/widalarmeta/ChangeLog +++ b/apps/widalarmeta/ChangeLog @@ -6,3 +6,5 @@ Update to match the default alarm widget, and not show itself when an alarm is hidden. 0.04: Fix check for active alarm 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 diff --git a/apps/widalarmeta/metadata.json b/apps/widalarmeta/metadata.json index 20b54d7c8..a3d2e8adb 100644 --- a/apps/widalarmeta/metadata.json +++ b/apps/widalarmeta/metadata.json @@ -2,7 +2,7 @@ "id": "widalarmeta", "name": "Alarm & Timer 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).", "icon": "widget.png", "type": "widget", diff --git a/apps/widalarmeta/widget.js b/apps/widalarmeta/widget.js index 3ad2e6ad2..750ae5d98 100644 --- a/apps/widalarmeta/widget.js +++ b/apps/widalarmeta/widget.js @@ -1,24 +1,43 @@ (() => { require("Font5x9Numeric7Seg").add(Graphics); - const alarms = require("Storage").readJSON("sched.json",1) || []; const config = Object.assign({ maxhours: 24, drawBell: false, showSeconds: 0, // 0=never, 1=only when display is unlocked, 2=for less than a minute }, require("Storage").readJSON("widalarmeta.json",1) || {}); - function draw() { - const times = alarms - .map(alarm => - alarm.hidden !== true - && require("sched").getTimeToAlarm(alarm) - ) - .filter(a => a !== undefined); - const next = times.length > 0 ? Math.min.apply(null, times) : 0; + function getNextAlarm(date) { + const alarms = (require("Storage").readJSON("sched.json",1) || []).filter(alarm => alarm.on && alarm.hidden !== true); + WIDGETS["widalarmeta"].numActiveAlarms = alarms.length; + const times = alarms.map(alarm => require("sched").getTimeToAlarm(alarm, date) || Number.POSITIVE_INFINITY); + const eta = times.length > 0 ? Math.min.apply(null, times) : 0; + if (eta !== Number.POSITIVE_INFINITY) { + const idx = times.indexOf(eta); + 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 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 minutes = Math.floor(((next-1) % 3600000) / 60000).toString(); const seconds = Math.floor(((next-1) % 60000) / 1000).toString(); @@ -39,10 +58,14 @@ if (drawSeconds) { calcWidth += 3*5; } - } else if (config.drawBell && alarms.some(alarm=>alarm.on&&(alarm.hidden!==true))) { - // next alarm too far in future, draw only widalarm bell - g.reset().drawImage(atob("GBgBAAAAAAAAABgADhhwDDwwGP8YGf+YMf+MM//MM//MA//AA//AA//AA//AA//AA//AB//gD//wD//wAAAAADwAABgAAAAAAAAA"),this.x,this.y); + this.bellVisible = false; + } else if (config.drawBell && this.numActiveAlarms > 0) { 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) { @@ -51,8 +74,8 @@ Bangle.drawWidgets(); } - // redraw next full minute or second - const period = drawSeconds ? 1000 : 60000; + // redraw next hour when no alarm else full minute or second + const period = next === 0 ? 3600000 : (drawSeconds ? 1000 : 60000); let timeout = next > 0 ? next % period : period - (Date.now() % period); if (timeout === 0) { timeout += period; @@ -62,8 +85,8 @@ clearTimeout(this.timeoutId); } this.timeoutId = setTimeout(()=>{ - this.timeoutId = undefined; - this.draw(); + WIDGETS["widalarmeta"].timeoutId = undefined; + WIDGETS["widalarmeta"].draw(true); }, timeout); } /* draw */ diff --git a/apps/widbatpc/ChangeLog b/apps/widbatpc/ChangeLog index 97da2cba6..3592656a9 100644 --- a/apps/widbatpc/ChangeLog +++ b/apps/widbatpc/ChangeLog @@ -15,3 +15,4 @@ 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 Add option to disable vibration when charger connects +0.18: Only redraw when values change diff --git a/apps/widbatpc/metadata.json b/apps/widbatpc/metadata.json index cb47475cb..d361da442 100644 --- a/apps/widbatpc/metadata.json +++ b/apps/widbatpc/metadata.json @@ -2,7 +2,7 @@ "id": "widbatpc", "name": "Battery Level Widget (with percentage)", "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", "icon": "widget.png", "type": "widget", diff --git a/apps/widbatpc/widget.js b/apps/widbatpc/widget.js index 7f483c960..b508cce8b 100644 --- a/apps/widbatpc/widget.js +++ b/apps/widbatpc/widget.js @@ -86,7 +86,7 @@ return changed; } - function draw() { + function draw(fromInterval) { // if hidden, don't draw if (!WIDGETS["batpc"].width) return; // else... @@ -103,6 +103,14 @@ 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); if (Bangle.isCharging() && setting('charger')) { @@ -173,7 +181,7 @@ 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}; setWidth(); diff --git a/typescript/types/clock_info.d.ts b/typescript/types/clock_info.d.ts index 9b664a6dc..06a2d400b 100644 --- a/typescript/types/clock_info.d.ts +++ b/typescript/types/clock_info.d.ts @@ -44,6 +44,7 @@ declare module ClockInfo { w: number, h: number, draw(itm: MenuItem, info: Item, options: InteractiveOptions): void, + app?: string, // used to remember clock_info locations, per app }; type InteractiveOptions = diff --git a/typescript/types/main.d.ts b/typescript/types/main.d.ts index 1505c6f5a..a48ce7565 100644 --- a/typescript/types/main.d.ts +++ b/typescript/types/main.d.ts @@ -8688,6 +8688,15 @@ interface ObjectConstructor { */ 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. * properties are currently unsupported. @@ -8709,6 +8718,15 @@ interface ObjectConstructor { */ 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: * * `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 * - * @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 * @returns {any} The return value of executing this function * @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 * - * @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 * @returns {any} The return value of executing this function * @url http://www.espruino.com/Reference#l_Function_apply */ - apply(this: any, args: any): any; + apply(thisArg: any, args: ArrayLike): any; /** * 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 * @returns {any} The 'bound' function * @url http://www.espruino.com/Reference#l_Function_bind */ - bind(this: any, ...params: any[]): any; + bind(thisArg: any, ...params: any[]): any; } /**