diff --git a/.eslintignore b/.eslintignore index 4af79d129..38bc5b5bf 100644 --- a/.eslintignore +++ b/.eslintignore @@ -4,4 +4,5 @@ apps/schoolCalendar/fullcalendar/main.js apps/authentiwatch/qr_packed.js apps/qrcode/qr-scanner.umd.min.js apps/gipy/pkg/gpconv.js +apps/health/chart.min.js *.test.js diff --git a/.github/ISSUE_TEMPLATE/bangle-bug-report-custom-form.yaml b/.github/ISSUE_TEMPLATE/bangle-bug-report-custom-form.yaml index 484b3ba85..045a6e18a 100644 --- a/.github/ISSUE_TEMPLATE/bangle-bug-report-custom-form.yaml +++ b/.github/ISSUE_TEMPLATE/bangle-bug-report-custom-form.yaml @@ -58,3 +58,7 @@ body: validations: required: true + - type: textarea + id: apps + attributes: + label: Installed apps \ No newline at end of file diff --git a/README.md b/README.md index fed13a358..aa8afdbca 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ This is the best way to test... **Note:** It's a great idea to get a local copy of the repository on your PC, then run `bin/sanitycheck.js` - it'll run through a bunch of common issues -that there might be. +that there might be. To get the project running locally, you have to initialize and update the git submodules first: `git submodule --init && git submodule update`. Be aware of the delay between commits and updates on github.io - it can take a few minutes (and a 'hard refresh' of your browser) for changes to take effect. diff --git a/apps/90sclk/ChangeLog b/apps/90sclk/ChangeLog index 057d6ff73..9718a652d 100644 --- a/apps/90sclk/ChangeLog +++ b/apps/90sclk/ChangeLog @@ -1,3 +1,4 @@ 0.01: New App! 0.02: Fullscreen settings. 0.03: Tell clock widgets to hide. +0.04: Use widget_utils. diff --git a/apps/90sclk/app.js b/apps/90sclk/app.js index 351c235e0..63a48b27a 100644 --- a/apps/90sclk/app.js +++ b/apps/90sclk/app.js @@ -1,6 +1,7 @@ const SETTINGS_FILE = "90sclk.setting.json"; const locale = require('locale'); const storage = require('Storage'); +const widget_utils = require('widget_utils'); /* @@ -109,7 +110,7 @@ function draw() { // Draw widgets if not fullscreen if(settings.fullscreen){ - for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";} + widget_utils.hide(); } else { Bangle.drawWidgets(); } diff --git a/apps/90sclk/metadata.json b/apps/90sclk/metadata.json index 59b627427..bfbb6b080 100644 --- a/apps/90sclk/metadata.json +++ b/apps/90sclk/metadata.json @@ -1,7 +1,7 @@ { "id": "90sclk", "name": "90s Clock", - "version": "0.03", + "version": "0.04", "description": "A 90s style watch-face", "readme": "README.md", "icon": "app.png", diff --git a/apps/a_clock_timer/ChangeLog b/apps/a_clock_timer/ChangeLog index c01ad2077..cfb53c432 100644 --- a/apps/a_clock_timer/ChangeLog +++ b/apps/a_clock_timer/ChangeLog @@ -1 +1,4 @@ 0.01: Beta version for Bangle 2 (2021/11/28) +0.02: Shows night time on the map (2022/12/28) +0.03: Add 1 minute timer with upper taps (2023/01/05) +1.00: Page to set up custom time zones (2023/01/06) \ No newline at end of file diff --git a/apps/a_clock_timer/README.md b/apps/a_clock_timer/README.md index e8e2647a9..4e199344a 100644 --- a/apps/a_clock_timer/README.md +++ b/apps/a_clock_timer/README.md @@ -2,14 +2,17 @@ * Works with Bangle 2 * Timer - * Right tap: start/increase by 10 minutes; Left tap: decrease by 5 minutes + * Top Right tap: increase by 1 minute + * Top Left tap: decrease by 1 minute + * Bottom Right tap: increase by 10 minutes + * Bottom Left tap: decrease by 5 minutes * Short buzz at T-30, T-20, T-10 ; Double buzz at T * Other time zones - * Currently hardcoded to Paris and Tokyo (this will be customizable in a future version) + * Showing Paris and Tokyo by default, but you can customize this using the dedicated configuration page on the app store * World Map - * The yellow line shows the position of the sun + * The map shows day and night on Earth and the position of the Sun (yellow line) -![](screenshot.png) +![](screenshot-1.png) ![](screenshot.png) ## Creator [@alainsaas](https://github.com/alainsaas) diff --git a/apps/a_clock_timer/app.js b/apps/a_clock_timer/app.js index 5f9a3a468..b01cec59b 100644 --- a/apps/a_clock_timer/app.js +++ b/apps/a_clock_timer/app.js @@ -18,19 +18,29 @@ var timervalue = 0; var istimeron = false; var timertick; -Bangle.on('touch',t=>{ - if (t == 1) { +Bangle.on('touch',(touchside, touchdata)=>{ + if (touchside == 1) { Bangle.buzz(30); - if (timervalue < 5*60) { timervalue = 1 ; } - else { timervalue -= 5*60; } + var changevalue = 0; + if(touchdata.y > 88) { + changevalue += 60*5; + } else { + changevalue += 60*1; + } + if (timervalue < changevalue) { timervalue = 1 ; } + else { timervalue -= changevalue; } } - else if (t == 2) { + else if (touchside == 2) { Bangle.buzz(30); if (!istimeron) { istimeron = true; timertick = setInterval(countDown, 1000); } - timervalue += 60*10; + if(touchdata.y > 88) { + timervalue += 60*10; + } else { + timervalue += 60*1; + } } }); @@ -73,12 +83,13 @@ function countDown() { function showWelcomeMessage() { g.reset().clearRect(0, 76, 44+44, g.getHeight()/2+6); g.setFontAlign(0, 0).setFont("6x8"); - g.drawString("Touch right to", 44, 80); + g.drawString("Tap right to", 44, 80); g.drawString("start timer", 44, 88); setTimeout(function(){ g.reset().clearRect(0, 76, 44+44, g.getHeight()/2+6); }, 8000); } // time +var offsets = require("Storage").readJSON("a_clock_timer.settings.json") || [ ["PAR",1], ["TYO",9] ]; var drawTimeout; function getGmt() { @@ -102,20 +113,34 @@ function queueNextDraw() { function draw() { g.reset().clearRect(0,24,g.getWidth(),g.getHeight()-IMAGEHEIGHT); g.drawImage(getImg(),0,g.getHeight()-IMAGEHEIGHT); - - var x_sun = 176 - (getGmt().getHours() / 24 * 176 + 4); + + var gmtHours = getGmt().getHours(); + + var x_sun = 176 - (gmtHours / 24 * 176 + 4); g.setColor('#ff0').drawLine(x_sun, g.getHeight()-IMAGEHEIGHT, x_sun, g.getHeight()); g.reset(); + var x_night_start = (176 - (((gmtHours-6)%24) / 24 * 176 + 4)) % 176; + var x_night_end = 176 - (((gmtHours+6)%24) / 24 * 176 + 4); + g.setColor('#000'); + for (let x = x_night_start; x < (x_night_end < x_night_start ? 176 : x_night_end); x+=2) { + g.drawLine(x, g.getHeight()-IMAGEHEIGHT, x, g.getHeight()); + } + if (x_night_end < x_night_start) { + for (let x = 0; x < x_night_end; x+=2) { + g.drawLine(x, g.getHeight()-IMAGEHEIGHT, x, g.getHeight()); + } + } + var locale = require("locale"); - + var date = new Date(); g.setFontAlign(0,0); g.setFont("Michroma36").drawString(locale.time(date,1), g.getWidth()/2, 46); g.setFont("6x8"); g.drawString(locale.date(new Date(),1), 125, 68); - g.drawString("PAR "+locale.time(getTimeFromTimezone(1),1), 125, 80); - g.drawString("TYO "+locale.time(getTimeFromTimezone(9),1), 125, 88); + g.drawString(offsets[0][0]+" "+locale.time(getTimeFromTimezone(offsets[0][1]),1), 125, 80); + g.drawString(offsets[1][0]+" "+locale.time(getTimeFromTimezone(offsets[1][1]),1), 125, 88); queueNextDraw(); } diff --git a/apps/a_clock_timer/custom.html b/apps/a_clock_timer/custom.html new file mode 100644 index 000000000..b62226340 --- /dev/null +++ b/apps/a_clock_timer/custom.html @@ -0,0 +1,58 @@ + + + + + +

You can set the 2 additional timezones displayed by the clock.

+ + + + + +
NameUTC Offset (Hours)
+

Click

+ + + + diff --git a/apps/a_clock_timer/metadata.json b/apps/a_clock_timer/metadata.json index cc61fc57b..6507857f1 100644 --- a/apps/a_clock_timer/metadata.json +++ b/apps/a_clock_timer/metadata.json @@ -1,17 +1,19 @@ { "id": "a_clock_timer", "name": "A Clock with Timer", - "version": "0.01", + "version": "1.00", "description": "A Clock with Timer, Map and Time Zones", "icon": "app.png", - "screenshots": [{"url":"screenshot.png"}], + "screenshots": [{"url":"screenshot.png"},{"url":"screenshot-1.png"}], "type": "clock", "tags": "clock", "supports": ["BANGLEJS2"], "allow_emulator": true, "readme": "README.md", + "custom": "custom.html", "storage": [ {"name":"a_clock_timer.app.js","url":"app.js"}, {"name":"a_clock_timer.img","url":"app-icon.js","evaluate":true} - ] + ], + "data": [{"name":"a_clock_timer.settings.json"}] } diff --git a/apps/a_clock_timer/screenshot-1.png b/apps/a_clock_timer/screenshot-1.png new file mode 100644 index 000000000..ede6439de Binary files /dev/null and b/apps/a_clock_timer/screenshot-1.png differ diff --git a/apps/a_clock_timer/screenshot.png b/apps/a_clock_timer/screenshot.png index 4fb3dd9f2..893daa9d0 100644 Binary files a/apps/a_clock_timer/screenshot.png and b/apps/a_clock_timer/screenshot.png differ diff --git a/apps/a_speech_timer/ChangeLog b/apps/a_speech_timer/ChangeLog index b3aa9e0dd..73e8a67da 100644 --- a/apps/a_speech_timer/ChangeLog +++ b/apps/a_speech_timer/ChangeLog @@ -1,2 +1,3 @@ 1.00: Release (2021/12/01) 1.01: Grey font when timer is frozen (2021/12/04) +1.02: Force light theme, since the app is not designed for dark theme (2022/12/28) diff --git a/apps/a_speech_timer/app.js b/apps/a_speech_timer/app.js index 440cd92c6..cbed2ac00 100644 --- a/apps/a_speech_timer/app.js +++ b/apps/a_speech_timer/app.js @@ -166,6 +166,7 @@ function draw() { g.drawRect(88+8,138-24, 176-10, 138+22); } +g.setTheme({bg:"#fff",fg:"#000",dark:false}).clear(); require("FontHaxorNarrow7x17").add(Graphics); g.clear(); Bangle.loadWidgets(); diff --git a/apps/a_speech_timer/metadata.json b/apps/a_speech_timer/metadata.json index 6255a6b92..fd8813991 100644 --- a/apps/a_speech_timer/metadata.json +++ b/apps/a_speech_timer/metadata.json @@ -2,7 +2,7 @@ "id":"a_speech_timer", "name":"Speech Timer", "icon": "app.png", -"version":"1.01", +"version":"1.02", "description": "A timer designed to help keeping your speeches and presentations to time.", "tags": "tool,timer", "readme":"README.md", diff --git a/apps/agenda/ChangeLog b/apps/agenda/ChangeLog index cb928213e..b53e657fd 100644 --- a/apps/agenda/ChangeLog +++ b/apps/agenda/ChangeLog @@ -8,3 +8,6 @@ 0.08: Fix error in clkinfo (didn't require Storage & locale) Fix clkinfo icon 0.09: Ensure Agenda supplies an image for clkinfo items +0.10: Update clock_info to avoid a redraw +0.11: Setting to use "Today" and "Yesterday" instead of dates + Added dynamic, short and range fields to clkinfo \ No newline at end of file diff --git a/apps/agenda/agenda.clkinfo.js b/apps/agenda/agenda.clkinfo.js index 54677327b..d203119f4 100644 --- a/apps/agenda/agenda.clkinfo.js +++ b/apps/agenda/agenda.clkinfo.js @@ -1,7 +1,14 @@ (function() { + function getPassedSec(date) { + var now = new Date(); + var passed = (now-date)/1000; + if(passed<0) return 0; + return passed; + } var agendaItems = { name: "Agenda", img: atob("GBiBAAAAAAAAAADGMA///w///wf//wAAAA///w///w///w///x///h///h///j///D///X//+f//8wAABwAADw///w///wf//gAAAA=="), + dynamic: true, items: [] }; var locale = require("locale"); @@ -15,12 +22,16 @@ var title = entry.title.slice(0,12); var date = new Date(entry.timestamp*1000); var dateStr = locale.date(date).replace(/\d\d\d\d/,""); + var shortStr = ((date-now) > 86400000 || entry.allDay) ? dateStr : locale.time(date,1); dateStr += entry.durationInSeconds < 86400 ? "/ " + locale.time(date,1) : ""; agendaItems.items.push({ name: "Agenda "+i, - get: () => ({ text: title + "\n" + dateStr, img: agendaItems.img }), - show: function() { agendaItems.items[i].emit("redraw"); }, + hasRange: true, + get: () => ({ text: title + "\n" + dateStr, + img: agendaItems.img, short: shortStr.trim(), + v: getPassedSec(date), min: 0, max: entry.durationInSeconds}), + show: function() {}, hide: function () {} }); }); diff --git a/apps/agenda/agenda.js b/apps/agenda/agenda.js index 9cffe0265..8afca95a9 100644 --- a/apps/agenda/agenda.js +++ b/apps/agenda/agenda.js @@ -33,16 +33,32 @@ CALENDAR=CALENDAR.sort((a,b)=>a.timestamp - b.timestamp); function getDate(timestamp) { return new Date(timestamp*1000); } +function formatDay(date) { + if (!settings.useToday) { + return Locale.date(date); + } + const dateformatted = date.toISOString().split('T')[0]; // yyyy-mm-dd + const today = new Date(Date.now()).toISOString().split('T')[0]; // yyyy-mm-dd + if (dateformatted == today) { + return /*LANG*/"Today "; + } else { + const tomorrow = new Date(Date.now() + 86400 * 1000).toISOString().split('T')[0]; // yyyy-mm-dd + if (dateformatted == tomorrow) { + return /*LANG*/"Tomorrow "; + } + return Locale.date(date); + } +} function formatDateLong(date, includeDay, allDay) { let shortTime = Locale.time(date,1)+Locale.meridian(date); if(allDay) shortTime = ""; - if(includeDay || allDay) - return Locale.date(date)+" "+shortTime; + if(includeDay || allDay) { + return formatDay(date)+" "+shortTime; + } return shortTime; } function formatDateShort(date, allDay) { - return Locale.date(date).replace(/\d\d\d\d/,"")+(allDay? - "" : Locale.time(date,1)+Locale.meridian(date)); + return formatDay(date).replace(/\d\d\d\d/,"")+(allDay?"":Locale.time(date,1)+Locale.meridian(date)); } var lines = []; diff --git a/apps/agenda/metadata.json b/apps/agenda/metadata.json index 58a5091cd..b5b7c1582 100644 --- a/apps/agenda/metadata.json +++ b/apps/agenda/metadata.json @@ -1,7 +1,7 @@ { "id": "agenda", "name": "Agenda", - "version": "0.09", + "version": "0.11", "description": "Simple agenda", "icon": "agenda.png", "screenshots": [{"url":"screenshot_agenda_overview.png"}, {"url":"screenshot_agenda_event1.png"}, {"url":"screenshot_agenda_event2.png"}], diff --git a/apps/agenda/settings.js b/apps/agenda/settings.js index 4220fcb63..62e0c6dbd 100644 --- a/apps/agenda/settings.js +++ b/apps/agenda/settings.js @@ -43,6 +43,13 @@ updateSettings(); } }, + /*LANG*/"Use 'Today',..." : { + value : !!settings.useToday, + onchange: v => { + settings.useToday = v; + updateSettings(); + } + }, }; E.showMenu(mainmenu); }) diff --git a/apps/agpsdata/ChangeLog b/apps/agpsdata/ChangeLog index 8ada244d7..303fc7583 100644 --- a/apps/agpsdata/ChangeLog +++ b/apps/agpsdata/ChangeLog @@ -3,3 +3,6 @@ 0.03: Do not load AGPS data on boot Increase minimum interval to 6 hours 0.04: Write AGPS data chunks with delay to improve reliability +0.05: Show last success date + Do not start A-GPS update automatically +0.06: Switch off gps after updating \ No newline at end of file diff --git a/apps/agpsdata/app.js b/apps/agpsdata/app.js index 4a6d2ba5c..48714d6d2 100644 --- a/apps/agpsdata/app.js +++ b/apps/agpsdata/app.js @@ -23,12 +23,26 @@ Bangle.drawWidgets(); let waiting = false; -function start() { +function start(restart) { g.reset(); g.clear(); waiting = false; - display("Retry?", "touch to retry"); + if (!restart) { + display("Start?", "touch to start"); + } + else { + display("Retry?", "touch to retry"); + } Bangle.on("touch", () => { updateAgps(); }); + + const file = "agpsdata.json"; + let data = require("Storage").readJSON(file, 1) || {}; + if (data.lastUpdate) { + g.setFont("Vector", 11); + g.drawString("last success:", 5, g.getHeight() - 22); + g.drawString(new Date(data.lastUpdate).toISOString(), 5, g.getHeight() - 11); + } + } function updateAgps() { @@ -36,7 +50,7 @@ function updateAgps() { g.clear(); if (!waiting) { waiting = true; - display("Updating A-GPS...", "takes ~ 10 seconds"); + display("Updating A-GPS...", "takes ~10 seconds"); require("agpsdata").pull(function() { waiting = false; display("A-GPS updated.", "touch to close"); @@ -45,10 +59,10 @@ function updateAgps() { function(error) { waiting = false; E.showAlert(error, "Error") - .then(() => { start(); }); + .then(() => { start(true); }); }); } else { display("Waiting..."); } } -updateAgps(); +start(false); diff --git a/apps/agpsdata/lib.js b/apps/agpsdata/lib.js index 34608a5c6..4610331f6 100644 --- a/apps/agpsdata/lib.js +++ b/apps/agpsdata/lib.js @@ -10,20 +10,24 @@ readSettings(); function setAGPS(b64) { return new Promise(function(resolve, reject) { - var initCommands = "Bangle.setGPSPower(1);\n"; // turn GPS on const gnsstype = settings.gnsstype || 1; // default GPS - initCommands += `Serial1.println("${CASIC_CHECKSUM("$PCAS04," + gnsstype)}")\n`; // set GNSS mode // What about: // NAV-TIMEUTC (0x01 0x10) // NAV-PV (0x01 0x03) // or AGPS.zip uses AID-INI (0x0B 0x01) - - eval(initCommands); + Bangle.setGPSPower(1,"agpsdata"); // turn GPS on + Serial1.println(CASIC_CHECKSUM("$PCAS04," + gnsstype)); // set GNSS mode try { - writeChunks(atob(b64), resolve); + writeChunks(atob(b64), ()=>{ + setTimeout(()=>{ + Bangle.setGPSPower(0,"agpsdata"); + resolve(); + }, 1000); + }); } catch (e) { console.log("error:", e); + Bangle.setGPSPower(0,"agpsdata"); reject(); } }); @@ -36,9 +40,8 @@ function writeChunks(bin, resolve) { setTimeout(function() { if (chunkI < bin.length) { var chunk = bin.substr(chunkI, chunkSize); - js = `Serial1.write(atob("${btoa(chunk)}"))\n`; - eval(js); - + Serial1.write(atob(btoa(chunk))); + chunkI += chunkSize; writeChunks(bin, resolve); } else { diff --git a/apps/agpsdata/metadata.json b/apps/agpsdata/metadata.json index 203a00f72..446661045 100644 --- a/apps/agpsdata/metadata.json +++ b/apps/agpsdata/metadata.json @@ -2,7 +2,7 @@ "name": "A-GPS Data Downloader App", "shortName":"A-GPS Data", "icon": "agpsdata.png", - "version":"0.04", + "version":"0.06", "description": "Once installed, this app allows you to download assisted GPS (A-GPS) data directly to your Bangle.js **via Gadgetbridge on an Android phone** when you run the app. If you just want to upload the latest AGPS data from this app loader, please use the `Assisted GPS Update (AGPS)` app.", "tags": "boot,tool,assisted,gps,agps,http", "allow_emulator":true, diff --git a/apps/aiclock/ChangeLog b/apps/aiclock/ChangeLog index fb5aed3e3..6d6eeb55e 100644 --- a/apps/aiclock/ChangeLog +++ b/apps/aiclock/ChangeLog @@ -2,4 +2,6 @@ 0.02: Design improvements and fixes. 0.03: Indicate battery level through line occurrence. 0.04: Use widget_utils module. -0.05: Support for clkinfo. \ No newline at end of file +0.05: Support for clkinfo. +0.06: ClockInfo Fix: Use .get instead of .show as .show is not implemented for weather etc. +0.07: Use clock_info.addInteractive instead of a custom implementation \ No newline at end of file diff --git a/apps/aiclock/README.md b/apps/aiclock/README.md index 31dd5aa29..521bd2c5e 100644 --- a/apps/aiclock/README.md +++ b/apps/aiclock/README.md @@ -11,8 +11,7 @@ The original output of stable diffusion is shown here: My implementation is shown below. Note that horizontal lines occur randomly, but the probability is correlated with the battery level. So if your screen contains only a few lines its time to charge your bangle again ;) Also note that the upper text -implementes the clkinfo module and can be configured via touch left/right/up/down. -Touch at the center to trigger the selected action. +implements the clkinfo module and can be configured via touch and swipe left/right and up/down. ![](impl.png) diff --git a/apps/aiclock/aiclock.app.js b/apps/aiclock/aiclock.app.js index b5bb30b9d..350832367 100644 --- a/apps/aiclock/aiclock.app.js +++ b/apps/aiclock/aiclock.app.js @@ -1,7 +1,6 @@ /************************************************ * AI Clock */ - const storage = require('Storage'); const clock_info = require("clock_info"); @@ -21,147 +20,14 @@ Graphics.prototype.setFontGochiHand = function(scale) { return this; } -/************************************************ - * Set some important constants such as width, height and center - */ -var W = g.getWidth(),R=W/2; -var H = g.getHeight(); -var cx = W/2; -var cy = H/2; -var drawTimeout; -var lock_input = false; - -/************************************************ - * SETTINGS - */ -const SETTINGS_FILE = "aiclock.setting.json"; -let settings = { - menuPosX: 0, - menuPosY: 0, -}; -let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings; -for (const key in saved_settings) { - settings[key] = saved_settings[key] -} - - -/************************************************ - * Menu - */ -function getDate(){ - var date = new Date(); - return ("0"+date.getDate()).substr(-2) + "/" + ("0"+(date.getMonth()+1)).substr(-2) -} - - -// Custom clockItems menu - therefore, its added here and not in a clkinfo.js file. -var clockItems = { - name: getDate(), - img: null, - items: [ - { name: "Week", - get: () => ({ text: "Week " + weekOfYear(), img: null}), - show: function() { clockItems.items[0].emit("redraw"); }, - hide: function () {} - }, - ] - }; - -function weekOfYear() { - var date = new Date(); - date.setHours(0, 0, 0, 0); - // Thursday in current week decides the year. - date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7); - // January 4 is always in week 1. - var week1 = new Date(date.getFullYear(), 0, 4); - // Adjust to Thursday in week 1 and count number of weeks from date to week1. - return 1 + Math.round(((date.getTime() - week1.getTime()) / 86400000 - - 3 + (week1.getDay() + 6) % 7) / 7); -} - - - -// Load menu -var menu = clock_info.load(); -menu = menu.concat(clockItems); - - - // Ensure that our settings are still in range (e.g. app uninstall). Otherwise reset the position it. - if(settings.menuPosX >= menu.length || settings.menuPosY > menu[settings.menuPosX].items.length ){ - settings.menuPosX = 0; - settings.menuPosY = 0; - } - - // Set draw functions for each item - menu.forEach((menuItm, x) => { - menuItm.items.forEach((item, y) => { - function drawItem() { - // For the clock, we have a special case, as we don't wanna redraw - // immediately when something changes. Instead, we update data each minute - // to save some battery etc. Therefore, we hide (and disable the listener) - // immedeately after redraw... - item.hide(); - - // After drawing the item, we enable inputs again... - lock_input = false; - - var info = item.get(); - drawMenuItem(info.text, info.img); - } - - item.on('redraw', drawItem); - }) - }); - - - function canRunMenuItem(){ - if(settings.menuPosY == 0){ - return false; - } - - var menuEntry = menu[settings.menuPosX]; - var item = menuEntry.items[settings.menuPosY-1]; - return item.run !== undefined; - } - - - function runMenuItem(){ - if(settings.menuPosY == 0){ - return; - } - - var menuEntry = menu[settings.menuPosX]; - var item = menuEntry.items[settings.menuPosY-1]; - try{ - var ret = item.run(); - if(ret){ - Bangle.buzz(300, 0.6); - } - } catch (ex) { - // Simply ignore it... - } - } - - -/* - * Based on the great multi clock from https://github.com/jeffmer/BangleApps/ - */ -Graphics.prototype.drawRotRect = function(w, r1, r2, angle) { - angle = angle % 360; - var w2=w/2, h=r2-r1, theta=angle*Math.PI/180; - return this.fillPoly(this.transformVertices([-w2,0,-w2,-h,w2,-h,w2,0], - {x:cx+r1*Math.sin(theta),y:cy-r1*Math.cos(theta),rotate:theta})); -}; - - -function drawBackground() { +function drawBackground(start, end) { g.setFontAlign(0,0); - g.setColor(g.theme.fg); + g.setColor("#000"); var bat = E.getBattery() / 100.0; - var y = 0; - while(y < H){ + var y = start; + while(y < end){ // Show less lines in case of small battery level. if(Math.random() > bat){ y += 5; @@ -177,6 +43,30 @@ function drawBackground() { } +/************************************************ + * Set some important constants such as width, height and center + */ +var W = g.getWidth(),R=W/2; +var H = g.getHeight(); +var cx = W/2; +var cy = H/2; +var drawTimeout; + +var clkInfoY = 60; + + +/* + * Based on the great multi clock from https://github.com/jeffmer/BangleApps/ + */ +Graphics.prototype.drawRotRect = function(w, r1, r2, angle) { + angle = angle % 360; + var w2=w/2, h=r2-r1, theta=angle*Math.PI/180; + return this.fillPoly(this.transformVertices([-w2,0,-w2,-h,w2,-h,w2,0], + {x:cx+r1*Math.sin(theta),y:cy-r1*Math.cos(theta),rotate:theta})); +}; + + + function drawCircle(isLocked){ g.setColor(g.theme.fg); g.fillCircle(cx, cy, 12); @@ -186,56 +76,6 @@ function drawCircle(isLocked){ g.fillCircle(cx, cy, 6); } -function toAngle(a){ - if (a < 0){ - return 360 + a; - } - - if(a > 360) { - return 360 - a; - } - - return a -} - - -function drawMenuItem(text, image){ - if(text == null){ - drawTime(); - return - } - // image = atob("GBiBAAD+AAH+AAH+AAH+AAH/AAOHAAYBgAwAwBgwYBgwYBgwIBAwOBAwOBgYIBgMYBgAYAwAwAYBgAOHAAH/AAH+AAH+AAH+AAD+AA=="); - - text = String(text); - - g.reset().setBgColor("#fff").setColor("#000"); - g.setFontAlign(0,0); - g.setFont("Vector", 20); - - var imgWidth = image == null ? 0 : 24; - var strWidth = g.stringWidth(text); - var strHeight = text.split('\n').length > 1 ? 40 : Math.max(24, imgWidth+2); - var w = imgWidth + strWidth; - - g.clearRect(cx-w/2-8, 40-strHeight/2-1, cx+w/2+4, 40+strHeight/2) - - // Draw right line as designed by stable diffusion - g.drawLine(cx+w/2+5, 40-strHeight/2-1, cx+w/2+5, 40+strHeight/2); - g.drawLine(cx+w/2+6, 40-strHeight/2-1, cx+w/2+6, 40+strHeight/2); - g.drawLine(cx+w/2+7, 40-strHeight/2-1, cx+w/2+7, 40+strHeight/2); - - // And finally the text - g.drawString(text, cx+imgWidth/2, 42); - g.drawString(text, cx+1+imgWidth/2, 41); - - if(image != null) { - var scale = image.width ? imgWidth / image.width : 1; - g.drawImage(image, W/2 + -strWidth/2-4 - parseInt(imgWidth/2), 41-12, {scale: scale}); - } - - drawTime(); -} - function drawTime(){ // Draw digital time first @@ -292,35 +132,23 @@ function drawDigits(){ } -function drawDate(){ - var menuEntry = menu[settings.menuPosX]; - - // The first entry is the overview... - if(settings.menuPosY == 0){ - drawMenuItem(menuEntry.name, menuEntry.img); - return; - } - - // Draw item if needed - lock_input = true; - var item = menuEntry.items[settings.menuPosY-1]; - item.show(); +function draw(){ + // Note that we force a redraw also of the clock info as + // we want to ensure (for design purpose) that the hands + // are above the clkinfo section. + clockInfoMenu.redraw(); } - - - -function draw(){ +function drawMainClock(){ // Queue draw in one minute queueDraw(); - g.reset(); - g.clearRect(0, 0, g.getWidth(), g.getHeight()); - g.setColor(1,1,1); + g.setColor("#fff"); + g.reset().clearRect(0, clkInfoY, g.getWidth(), g.getHeight()); - drawBackground(); - drawDate(); + drawBackground(clkInfoY, H); + drawTime(); drawCircle(Bangle.isLocked()); } @@ -330,7 +158,7 @@ function draw(){ */ Bangle.on('lcdPower',on=>{ if (on) { - draw(true); + draw(); } else { // stop draw timer if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = undefined; @@ -341,66 +169,10 @@ Bangle.on('lock', function(isLocked) { drawCircle(isLocked); }); -Bangle.on('touch', function(btn, e){ - var left = parseInt(g.getWidth() * 0.22); - var right = g.getWidth() - left; - var upper = parseInt(g.getHeight() * 0.22); - var lower = g.getHeight() - upper; - - var is_upper = e.y < upper; - var is_lower = e.y > lower; - var is_left = e.x < left && !is_upper && !is_lower; - var is_right = e.x > right && !is_upper && !is_lower; - var is_center = !is_upper && !is_lower && !is_left && !is_right; - - if(lock_input){ - return; - } - - if(is_lower){ - Bangle.buzz(40, 0.6); - settings.menuPosY = (settings.menuPosY+1) % (menu[settings.menuPosX].items.length+1); - - draw(); - } - - if(is_upper){ - Bangle.buzz(40, 0.6); - settings.menuPosY = settings.menuPosY-1; - settings.menuPosY = settings.menuPosY < 0 ? menu[settings.menuPosX].items.length : settings.menuPosY; - - draw(); - } - - if(is_right){ - Bangle.buzz(40, 0.6); - settings.menuPosX = (settings.menuPosX+1) % menu.length; - settings.menuPosY = 0; - draw(); - } - - if(is_left){ - Bangle.buzz(40, 0.6); - settings.menuPosY = 0; - settings.menuPosX = settings.menuPosX-1; - settings.menuPosX = settings.menuPosX < 0 ? menu.length-1 : settings.menuPosX; - draw(); - } - - if(is_center){ - if(canRunMenuItem()){ - runMenuItem(); - } - } -}); - E.on("kill", function(){ - try{ - storage.write(SETTINGS_FILE, settings); - } catch(ex){ - // If this fails, we still kill the app... - } + clockInfoMenu.remove(); + delete clockInfoMenu; }); @@ -416,6 +188,55 @@ function queueDraw() { } +/************************************************ + * Clock Info + */ +let clockInfoItems = clock_info.load(); +let clockInfoMenu = clock_info.addInteractive(clockInfoItems, { + x : 0, + y: 0, + w: W, + h: clkInfoY, + draw : (itm, info, options) => { + g.setFontAlign(0,0); + g.setFont("Vector", 20); + + g.setColor("#fff"); + g.fillRect(options.x, options.y, options.x+options.w, options.y+options.h); + drawBackground(0, clkInfoY+2); + + // Set text and font + var image = info.img; + var text = String(info.text); + + var imgWidth = image == null ? 0 : 24; + var strWidth = g.stringWidth(text); + var strHeight = text.split('\n').length > 1 ? 40 : Math.max(24, imgWidth+2); + var w = imgWidth + strWidth; + + // Draw right line as designed by stable diffusion + g.setColor(options.focus ? "#0f0" : "#fff"); + g.fillRect(cx-w/2-8, 40-strHeight/2-1, cx+w/2+4, 40+strHeight/2) + + g.setColor("#000"); + g.drawLine(cx+w/2+5, 40-strHeight/2-1, cx+w/2+5, 40+strHeight/2); + g.drawLine(cx+w/2+6, 40-strHeight/2-1, cx+w/2+6, 40+strHeight/2); + g.drawLine(cx+w/2+7, 40-strHeight/2-1, cx+w/2+7, 40+strHeight/2); + + // Draw text and image + g.drawString(text, cx+imgWidth/2, 42); + g.drawString(text, cx+1+imgWidth/2, 41); + + if(image != null) { + var scale = image.width ? imgWidth / image.width : 1; + g.drawImage(image, W/2 + -strWidth/2-4 - parseInt(imgWidth/2), 41-12, {scale: scale}); + } + + drawMainClock(); + } +}); + + /* * Lets start widgets, listen for btn etc. */ @@ -430,7 +251,7 @@ Bangle.loadWidgets(); require('widget_utils').hide(); // Clear the screen once, at startup and draw clock -g.setTheme({bg:"#fff",fg:"#000",dark:false}).clear(); +g.setTheme({bg:"#fff",fg:"#000",dark:false}); draw(); // After drawing the watch face, we can draw the widgets diff --git a/apps/aiclock/metadata.json b/apps/aiclock/metadata.json index 1dcda427f..4c01ecaa9 100644 --- a/apps/aiclock/metadata.json +++ b/apps/aiclock/metadata.json @@ -3,7 +3,7 @@ "name": "AI Clock", "shortName":"AI Clock", "icon": "aiclock.png", - "version":"0.05", + "version":"0.07", "readme": "README.md", "supports": ["BANGLEJS2"], "description": "A watch face that was designed by an AI (stable diffusion) and implemented by a human.", diff --git a/apps/alarm/ChangeLog b/apps/alarm/ChangeLog index 9994d33d9..bb8a292a0 100644 --- a/apps/alarm/ChangeLog +++ b/apps/alarm/ChangeLog @@ -37,3 +37,4 @@ 0.34: Add "Confirm" option to alarm/timer edit menus 0.35: Add automatic translation of more strings 0.36: alarm widget moved out of app +0.37: add message input and dated Events diff --git a/apps/alarm/README.md b/apps/alarm/README.md index 741946b0c..0298e0836 100644 --- a/apps/alarm/README.md +++ b/apps/alarm/README.md @@ -1,15 +1,18 @@ # Alarms & Timers -This app allows you to add/modify any alarms and timers. +This app allows you to add/modify any alarms, timers and events. + +Optional: When a keyboard app is detected, you can add a message to display when any of these is triggered. It uses the [`sched` library](https://github.com/espruino/BangleApps/blob/master/apps/sched) to handle the alarm scheduling in an efficient way that can work alongside other apps. ## Menu overview - `New...` - - `New Alarm` → Configure a new alarm + - `New Alarm` → Configure a new alarm (triggered based on time and day of week) - `Repeat` → Select when the alarm will fire. You can select a predefined option (_Once_, _Every Day_, _Workdays_ or _Weekends_ or you can configure the days freely) - - `New Timer` → Configure a new timer + - `New Timer` → Configure a new timer (triggered based on amount of time elapsed in hours/minutes/seconds) + - `New Event` → Configure a new event (triggered based on time and date) - `Advanced` - `Scheduler settings` → Open the [Scheduler](https://github.com/espruino/BangleApps/tree/master/apps/sched) settings page, see its [README](https://github.com/espruino/BangleApps/blob/master/apps/sched/README.md) for details - `Enable All` → Enable _all_ disabled alarms & timers diff --git a/apps/alarm/app.js b/apps/alarm/app.js index 1414c0b90..74007d04b 100644 --- a/apps/alarm/app.js +++ b/apps/alarm/app.js @@ -48,9 +48,10 @@ function showMainMenu() { }; alarms.forEach((e, index) => { - var label = e.timer + var label = (e.timer ? require("time_utils").formatDuration(e.timer) - : require("time_utils").formatTime(e.t) + (e.rp ? ` ${decodeDOW(e)}` : ""); + : (e.date ? `${e.date.substring(5,10)} ${require("time_utils").formatTime(e.t)}` : require("time_utils").formatTime(e.t) + (e.rp ? ` ${decodeDOW(e)}` : "")) + ) + (e.msg ? " " + e.msg : ""); menu[label] = { value: e.on ? (e.timer ? iconTimerOn : iconAlarmOn) : (e.timer ? iconTimerOff : iconAlarmOff), onchange: () => setTimeout(e.timer ? showEditTimerMenu : showEditAlarmMenu, 10, e, index) @@ -67,11 +68,12 @@ function showNewMenu() { "": { "title": /*LANG*/"New..." }, "< Back": () => showMainMenu(), /*LANG*/"Alarm": () => showEditAlarmMenu(undefined, undefined), - /*LANG*/"Timer": () => showEditTimerMenu(undefined, undefined) + /*LANG*/"Timer": () => showEditTimerMenu(undefined, undefined), + /*LANG*/"Event": () => showEditAlarmMenu(undefined, undefined, true) }); } -function showEditAlarmMenu(selectedAlarm, alarmIndex) { +function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate) { var isNew = alarmIndex === undefined; var alarm = require("sched").newDefaultAlarm(); @@ -82,11 +84,16 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex) { } var time = require("time_utils").decodeTime(alarm.t); + if (withDate && !alarm.date) alarm.date = new Date().toLocalISOString().slice(0,10); + var date = alarm.date ? new Date(alarm.date) : undefined; + var title = date ? (isNew ? /*LANG*/"New Event" : /*LANG*/"Edit Event") : (isNew ? /*LANG*/"New Alarm" : /*LANG*/"Edit Alarm"); + var keyboard = "textinput"; + try {keyboard = require(keyboard);} catch(e) {keyboard = null;} const menu = { - "": { "title": isNew ? /*LANG*/"New Alarm" : /*LANG*/"Edit Alarm" }, + "": { "title": title }, "< Back": () => { - prepareAlarmForSave(alarm, alarmIndex, time); + prepareAlarmForSave(alarm, alarmIndex, time, date); saveAndReload(); showMainMenu(); }, @@ -106,6 +113,36 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex) { wrap: true, onchange: v => time.m = v }, + /*LANG*/"Day": { + value: date ? date.getDate() : null, + min: 1, + max: 31, + wrap: true, + onchange: v => date.setDate(v) + }, + /*LANG*/"Month": { + value: date ? date.getMonth() + 1 : null, + format: v => require("date_utils").month(v), + onchange: v => date.setMonth((v+11)%12) + }, + /*LANG*/"Year": { + value: date ? date.getFullYear() : null, + min: new Date().getFullYear(), + max: 2100, + onchange: v => date.setFullYear(v) + }, + /*LANG*/"Message": { + value: alarm.msg, + onchange: () => { + setTimeout(() => { + keyboard.input({text:alarm.msg}).then(result => { + alarm.msg = result; + prepareAlarmForSave(alarm, alarmIndex, time, date, true); + setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex, withDate); + }); + }, 100); + } + }, /*LANG*/"Enabled": { value: alarm.on, onchange: v => alarm.on = v @@ -115,8 +152,8 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex) { onchange: () => setTimeout(showEditRepeatMenu, 100, alarm.rp, alarm.dow, (repeat, dow) => { alarm.rp = repeat; alarm.dow = dow; - alarm.t = require("time_utils").encodeTime(time); - setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex); + prepareAlarmForSave(alarm, alarmIndex, time, date, true); + setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex, withDate); }) }, /*LANG*/"Vibrate": require("buzz_menu").pattern(alarm.vibrate, v => alarm.vibrate = v), @@ -136,6 +173,15 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex) { } }; + if (!keyboard) delete menu[/*LANG*/"Message"]; + if (alarm.date || withDate) { + delete menu[/*LANG*/"Repeat"]; + } else { + delete menu[/*LANG*/"Day"]; + delete menu[/*LANG*/"Month"]; + delete menu[/*LANG*/"Year"]; + } + if (!isNew) { menu[/*LANG*/"Delete"] = () => { E.showPrompt(/*LANG*/"Are you sure?", { title: /*LANG*/"Delete Alarm" }).then((confirm) => { @@ -145,7 +191,7 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex) { showMainMenu(); } else { alarm.t = require("time_utils").encodeTime(time); - setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex); + setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex, withDate); } }); }; @@ -154,14 +200,17 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex) { E.showMenu(menu); } -function prepareAlarmForSave(alarm, alarmIndex, time) { +function prepareAlarmForSave(alarm, alarmIndex, time, date, temp) { alarm.t = require("time_utils").encodeTime(time); alarm.last = alarm.t < require("time_utils").getCurrentTimeMillis() ? new Date().getDate() : 0; + if(date) alarm.date = date.toLocalISOString().slice(0,10); - if (alarmIndex === undefined) { - alarms.push(alarm); - } else { - alarms[alarmIndex] = alarm; + if(!temp) { + if (alarmIndex === undefined) { + alarms.push(alarm); + } else { + alarms[alarmIndex] = alarm; + } } } @@ -255,6 +304,8 @@ function showEditTimerMenu(selectedTimer, timerIndex) { } var time = require("time_utils").decodeTime(timer.timer); + var keyboard = "textinput"; + try {keyboard = require(keyboard);} catch(e) {keyboard = null;} const menu = { "": { "title": isNew ? /*LANG*/"New Timer" : /*LANG*/"Edit Timer" }, @@ -285,6 +336,18 @@ function showEditTimerMenu(selectedTimer, timerIndex) { wrap: true, onchange: v => time.s = v }, + /*LANG*/"Message": { + value: timer.msg, + onchange: () => { + setTimeout(() => { + keyboard.input({text:timer.msg}).then(result => { + timer.msg = result; + prepareTimerForSave(timer, timerIndex, time, true); + setTimeout(showEditTimerMenu, 10, timer, timerIndex); + }); + }, 100); + } + }, /*LANG*/"Enabled": { value: timer.on, onchange: v => timer.on = v @@ -306,6 +369,7 @@ function showEditTimerMenu(selectedTimer, timerIndex) { } }; + if (!keyboard) delete menu[/*LANG*/"Message"]; if (!isNew) { menu[/*LANG*/"Delete"] = () => { E.showPrompt(/*LANG*/"Are you sure?", { title: /*LANG*/"Delete Timer" }).then((confirm) => { @@ -324,15 +388,17 @@ function showEditTimerMenu(selectedTimer, timerIndex) { E.showMenu(menu); } -function prepareTimerForSave(timer, timerIndex, time) { +function prepareTimerForSave(timer, timerIndex, time, temp) { timer.timer = require("time_utils").encodeTime(time); timer.t = require("time_utils").getCurrentTimeMillis() + timer.timer; timer.last = 0; - if (timerIndex === undefined) { - alarms.push(timer); - } else { - alarms[timerIndex] = timer; + if (!temp) { + if (timerIndex === undefined) { + alarms.push(timer); + } else { + alarms[timerIndex] = timer; + } } } diff --git a/apps/alarm/metadata.json b/apps/alarm/metadata.json index dbf090774..29e71b3d9 100644 --- a/apps/alarm/metadata.json +++ b/apps/alarm/metadata.json @@ -2,7 +2,7 @@ "id": "alarm", "name": "Alarms & Timers", "shortName": "Alarms", - "version": "0.36", + "version": "0.37", "description": "Set alarms and timers on your Bangle", "icon": "app.png", "tags": "tool,alarm", diff --git a/apps/android/ChangeLog b/apps/android/ChangeLog index 86dbdb649..db5c0b057 100644 --- a/apps/android/ChangeLog +++ b/apps/android/ChangeLog @@ -18,3 +18,5 @@ 0.18: Use new message library If connected to Gadgetbridge, allow GPS forwarding from phone (Gadgetbridge code still not merged) 0.19: Add automatic translation for a couple of strings. +0.20: Fix wrong event used for forwarded GPS data from Gadgetbridge and add mapper to map longitude value correctly. +0.21: Fix broken 'Messages' button in menu diff --git a/apps/android/boot.js b/apps/android/boot.js index e1e5b028b..c5a9dd746 100644 --- a/apps/android/boot.js +++ b/apps/android/boot.js @@ -134,7 +134,11 @@ event.satellites = NaN; event.course = NaN; event.fix = 1; - Bangle.emit('gps', event); + if (event.long!==undefined) { + event.lon = event.long; + delete event.long; + } + Bangle.emit('GPS', event); }, "is_gps_active": function() { gbSend({ t: "gps_power", status: Bangle._PWR && Bangle._PWR.GPS && Bangle._PWR.GPS.length>0 }); @@ -208,7 +212,7 @@ // 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); + 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=[]; diff --git a/apps/android/metadata.json b/apps/android/metadata.json index d5a45edb7..63fd7759a 100644 --- a/apps/android/metadata.json +++ b/apps/android/metadata.json @@ -2,7 +2,7 @@ "id": "android", "name": "Android Integration", "shortName": "Android", - "version": "0.19", + "version": "0.21", "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", diff --git a/apps/android/settings.js b/apps/android/settings.js index 3e04e0f9d..0abb32249 100644 --- a/apps/android/settings.js +++ b/apps/android/settings.js @@ -1,6 +1,6 @@ (function(back) { - + function gb(j) { Bluetooth.println(JSON.stringify(j)); @@ -36,7 +36,7 @@ updateSettings(); } }, - /*LANG*/"Messages" : ()=>require("message").openGUI(), + /*LANG*/"Messages" : ()=>require("messages").openGUI(), }; E.showMenu(mainmenu); }) diff --git a/apps/assistedgps/ChangeLog b/apps/assistedgps/ChangeLog index 739ccf915..ff2de6f67 100644 --- a/apps/assistedgps/ChangeLog +++ b/apps/assistedgps/ChangeLog @@ -1,3 +1,4 @@ 0.01: New App! 0.02: Update to work with Bangle.js 2 0.03: Select GNSS systems to use for Bangle.js 2 +0.04: Now turns GPS off after upload diff --git a/apps/assistedgps/custom.html b/apps/assistedgps/custom.html index 80d68a71f..75a4ecf32 100644 --- a/apps/assistedgps/custom.html +++ b/apps/assistedgps/custom.html @@ -133,7 +133,7 @@ function jsFromBase64(b64) { var bin = atob(b64); var chunkSize = 128; - var js = "\x10Bangle.setGPSPower(1);\n"; // turn GPS on + var js = "\x10Bangle.setGPSPower(1,'agps');\n"; // turn GPS on if (isB1) { // UBLOX //js += `\x10Bangle.on('GPS-raw',function (d) { if (d.startsWith("\\xB5\\x62\\x05\\x01")) Terminal.println("GPS ACK"); else if (d.startsWith("\\xB5\\x62\\x05\\x00")) Terminal.println("GPS NACK"); })\n`; //js += "\x10var t=getTime()+1;while(t>getTime());\n"; // wait 1 sec @@ -158,6 +158,7 @@ var chunk = bin.substr(i,chunkSize); js += `\x10Serial1.write(atob("${btoa(chunk)}"))\n`; } + js = "\x10setTimeout(() => Bangle.setGPSPower(0,'agps'), 1000);\n"; // turn GPS off after a delay return js; } diff --git a/apps/assistedgps/metadata.json b/apps/assistedgps/metadata.json index 4c91dcd35..ac9fe5725 100644 --- a/apps/assistedgps/metadata.json +++ b/apps/assistedgps/metadata.json @@ -1,7 +1,7 @@ { "id": "assistedgps", "name": "Assisted GPS Updater (AGPS)", - "version": "0.03", + "version": "0.04", "description": "Downloads assisted GPS (AGPS) data to Bangle.js for faster GPS startup and more accurate fixes. **No app will be installed**, this just uploads new data to the GPS chip.", "sortorder": -1, "icon": "app.png", diff --git a/apps/barclock/ChangeLog b/apps/barclock/ChangeLog index 88f4eaf00..96ee0141e 100644 --- a/apps/barclock/ChangeLog +++ b/apps/barclock/ChangeLog @@ -14,3 +14,4 @@ 0.14: Use ClockFace_menu.addItems 0.15: Add Power saving option 0.16: Support Fast Loading +0.17: Hide widgets instead of not loading them at all diff --git a/apps/barclock/metadata.json b/apps/barclock/metadata.json index 785c228b0..010852083 100644 --- a/apps/barclock/metadata.json +++ b/apps/barclock/metadata.json @@ -1,7 +1,7 @@ { "id": "barclock", "name": "Bar Clock", - "version": "0.16", + "version": "0.17", "description": "A simple digital clock showing seconds as a bar", "icon": "clock-bar.png", "screenshots": [{"url":"screenshot.png"},{"url":"screenshot_pm.png"}], diff --git a/apps/barclock/settings.js b/apps/barclock/settings.js index 7b88b7021..04f0a38ba 100644 --- a/apps/barclock/settings.js +++ b/apps/barclock/settings.js @@ -1,5 +1,10 @@ (function(back) { let s = require("Storage").readJSON("barclock.settings.json", true) || {}; + // migrate "don't load widgets" to "hide widgets" + if (!("hideWidgets" in s) && ("loadWidgets" in s) && !s.loadWidgets) { + s.hideWidgets = 1; + } + delete s.loadWidgets; function save(key, value) { s[key] = value; @@ -19,7 +24,7 @@ }; let items = { showDate: s.showDate, - loadWidgets: s.loadWidgets, + hideWidgets: s.hideWidgets, }; // Power saving for Bangle.js 1 doesn't make sense (no updates while screen is off anyway) if (process.env.HWVERSION>1) { diff --git a/apps/berlinc/ChangeLog b/apps/berlinc/ChangeLog index 9e9c1a6aa..b6ed1a5b4 100644 --- a/apps/berlinc/ChangeLog +++ b/apps/berlinc/ChangeLog @@ -4,3 +4,4 @@ 0.05: Update *on* the minute rather than every 15 secs Now show widgets Make compatible with themes, and Bangle.js 2 +0.06: Enable fastloading \ No newline at end of file diff --git a/apps/berlinc/berlin-clock.js b/apps/berlinc/berlin-clock.js index 0dd8ff8ee..cd0f12fa3 100644 --- a/apps/berlinc/berlin-clock.js +++ b/apps/berlinc/berlin-clock.js @@ -1,3 +1,4 @@ +{ // Berlin Clock see https://en.wikipedia.org/wiki/Mengenlehreuhr // https://github.com/eska-muc/BangleApps const fields = [4, 4, 11, 4]; @@ -6,18 +7,18 @@ const width = g.getWidth() - 2 * offset; const height = g.getHeight() - 2 * offset; const rowHeight = height / 4; -var show_date = false; -var show_time = false; -var yy = 0; +let show_date = false; +let show_time = false; +let yy = 0; -var rowlights = []; -var time_digit = []; +let rowlights = []; +let time_digit = []; // timeout used to update every minute -var drawTimeout; +let drawTimeout; // schedule a draw for the next minute -function queueDraw() { +let queueDraw = () => { if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = setTimeout(function() { drawTimeout = undefined; @@ -25,7 +26,7 @@ function queueDraw() { }, 60000 - (Date.now() % 60000)); } -function draw() { +let draw = () => { g.reset().clearRect(0,24,g.getWidth(),g.getHeight()); var now = new Date(); @@ -84,28 +85,39 @@ function draw() { queueDraw(); } -function toggleDate() { +let toggleDate = () => { show_date = ! show_date; draw(); } -function toggleTime() { +let toggleTime = () => { show_time = ! show_time; draw(); } -// Stop updates when LCD is off, restart when on -Bangle.on('lcdPower',on=>{ +let clear = () => { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; +} + +let onLcdPower = on => { if (on) { draw(); // draw immediately, queue redraw } else { // stop draw timer - if (drawTimeout) clearTimeout(drawTimeout); - drawTimeout = undefined; + clear(); } -}); +} + +let cleanup = () => { + clear(); + Bangle.removeListener("lcdPower", onLcdPower); +} + +// Stop updates when LCD is off, restart when on +Bangle.on('lcdPower',onLcdPower); // Show launcher when button pressed, handle up/down -Bangle.setUI("clockupdown", dir=> { +Bangle.setUI({mode: "clockupdown", remove: cleanup}, dir=> { if (dir<0) toggleTime(); if (dir>0) toggleDate(); }); @@ -114,3 +126,4 @@ g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); draw(); +} \ No newline at end of file diff --git a/apps/berlinc/metadata.json b/apps/berlinc/metadata.json index 85c42fc47..7f63f96cd 100644 --- a/apps/berlinc/metadata.json +++ b/apps/berlinc/metadata.json @@ -1,7 +1,7 @@ { "id": "berlinc", "name": "Berlin Clock", - "version": "0.05", + "version": "0.06", "description": "Berlin Clock (see https://en.wikipedia.org/wiki/Mengenlehreuhr)", "icon": "berlin-clock.png", "type": "clock", diff --git a/apps/bwclk/ChangeLog b/apps/bwclk/ChangeLog index 546c83894..8b82f6843 100644 --- a/apps/bwclk/ChangeLog +++ b/apps/bwclk/ChangeLog @@ -20,4 +20,13 @@ 0.20: Better handling of async data such as getPressure. 0.21: On the default menu the week of year can be shown. 0.22: Use the new clkinfo module for the menu. -0.23: Feedback of apps after run is now optional and decided by the corresponding clkinfo. \ No newline at end of file +0.23: Feedback of apps after run is now optional and decided by the corresponding clkinfo. +0.24: Update clock_info to avoid a redraw +0.25: Use Bangle.setUI({remove:...}) to allow loading the launcher without a full reset on fw2v16. + ClockInfo Fix: Use .get instead of .show as .show is not implemented for weather etc. +0.26: Use clkinfo.addInteractive instead of a custom implementation +0.27: Clean out some leftovers in the remove function after switching to +clkinfo.addInteractive that would cause ReferenceError. +0.28: Option to show (1) time only and (2) week of year. +0.29: use setItem of clockInfoMenu to change the active item +0.30: Use widget_utils. diff --git a/apps/bwclk/README.md b/apps/bwclk/README.md index d869fa2cf..5e2a7b55f 100644 --- a/apps/bwclk/README.md +++ b/apps/bwclk/README.md @@ -5,16 +5,12 @@ A very minimalistic clock. ## Features The BW clock implements features that are exposed by other apps through the `clkinfo` module. -For example, if you install the HomeAssistant app, this menu item will be shown if you click right -and additionally allows you to send triggers directly from the clock (select triggers via up/down and -send via click center). Here are examples of other apps that are integrated: +For example, if you install the HomeAssistant app, this menu item will be shown if you first +touch the bottom of the screen and then swipe left/right to the home assistant menu. To select +sub-items simply swipe up/down. To run an action (e.g. trigger home assistant), simply select the clkinfo (border) and touch on the item again. See also the screenshot below: -- Bangle data such as steps, heart rate, battery or charging state. -- Show agenda entries. A timer for an agenda entry can also be set by simply clicking in the middle of the screen. This can be used to not forget a meeting etc. Note that only one agenda-timer can be set at a time. *Requirement: Gadgetbridge calendar sync enabled* -- Weather temperature as well as the wind speed can be shown. *Requirement: Weather app* -- HomeAssistant triggers can be executed directly. *Requirement: HomeAssistant app* +![](screenshot_3.png) -Note: If some apps are not installed (e.gt. weather app), then this menu item is hidden. ## Settings - Screen: Normal (widgets shown), Dynamic (widgets shown if unlocked) or Full (widgets are hidden). @@ -22,25 +18,6 @@ Note: If some apps are not installed (e.gt. weather app), then this menu item is - The colon (e.g. 7:35 = 735) can be hidden in the settings for an even larger time font to improve readability further. - Your bangle uses the sys color settings so you can change the color too. -## Menu structure -2D menu allows you to display lots of different data including data from 3rd party apps and it's also possible to control things e.g. to trigger HomeAssistant. - -Simply click left / right to go through the menu entries such as Bangle, Weather etc. -and click up/down to move into this sub-menu. You can then click in the middle of the screen -to e.g. send a trigger via HomeAssistant once you selected it. The actions really depend -on the app that provide this sub-menu through the `clkinfo` module. - -``` - Bangle -- Agenda -- Weather -- HomeAssistant - | | | | - Battery Entry 1 Temperature Trigger1 - | | | | - Steps ... ... ... - | - ... -``` - - ## Thanks to - Thanks to Gordon Williams not only for the great BangleJs, but specifically also for the implementation of `clkinfo` which simplified the BWClock a lot and moved complexety to the apps where it should be located. - Icons created by Flaticon diff --git a/apps/bwclk/app.js b/apps/bwclk/app.js index 7dcca9d75..de7c7d510 100644 --- a/apps/bwclk/app.js +++ b/apps/bwclk/app.js @@ -1,10 +1,12 @@ +{ // must be inside our own scope here so that when we are unloaded everything disappears + /************************************************ * Includes */ const locale = require('locale'); const storage = require('Storage'); const clock_info = require("clock_info"); - +const widget_utils = require("widget_utils"); /************************************************ * Globals @@ -12,8 +14,6 @@ const clock_info = require("clock_info"); const SETTINGS_FILE = "bwclk.setting.json"; const W = g.getWidth(); const H = g.getHeight(); -var lock_input = false; - /************************************************ * Settings @@ -28,7 +28,20 @@ let settings = { let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings; for (const key in saved_settings) { - settings[key] = saved_settings[key] + settings[key] = saved_settings[key]; +} + +let isFullscreen = function() { + var s = settings.screen.toLowerCase(); + if(s == "dynamic"){ + return Bangle.isLocked(); + } else { + return s == "full"; + } +}; + +let getLineY = function(){ + return H/5*2 + (isFullscreen() ? 0 : 8); } /************************************************ @@ -74,32 +87,22 @@ Graphics.prototype.setMiniFont = function(scale) { return this; }; -function imgLock(){ +let imgLock = function() { return { width : 16, height : 16, bpp : 1, transparent : 0, buffer : E.toArrayBuffer(atob("A8AH4A5wDDAYGBgYP/w//D/8Pnw+fD58Pnw//D/8P/w=")) - } -} + }; +}; /************************************************ - * Menu + * Clock Info */ -// Custom bwItems menu - therefore, its added here and not in a clkinfo.js file. -var bwItems = { - name: null, - img: null, - items: [ - { name: "WeekOfYear", - get: () => ({ text: "Week " + weekOfYear(), img: null}), - show: function() { bwItems.items[0].emit("redraw"); }, - hide: function () {} - }, - ] -}; +let clockInfoItems = clock_info.load(); -function weekOfYear() { +// Add some custom clock-infos +let weekOfYear = function() { var date = new Date(); date.setHours(0, 0, 0, 0); // Thursday in current week decides the year. @@ -111,87 +114,98 @@ function weekOfYear() { - 3 + (week1.getDay() + 6) % 7) / 7); } +clockInfoItems[0].items.unshift({ name : "weekofyear", + get : function() { return { text : "Week " + weekOfYear(), + img : null}}, + show : function() {}, + hide : function() {}, +}) -// Load menu -var menu = clock_info.load(); -menu = menu.concat(bwItems); +// Empty for large time +clockInfoItems[0].items.unshift({ name : "nop", + get : function() { return { text : null, + img : null}}, + show : function() {}, + hide : function() {}, +}) -// Ensure that our settings are still in range (e.g. app uninstall). Otherwise reset the position it. -if(settings.menuPosX >= menu.length || settings.menuPosY > menu[settings.menuPosX].items.length ){ - settings.menuPosX = 0; - settings.menuPosY = 0; -} -// Set draw functions for each item -menu.forEach((menuItm, x) => { - menuItm.items.forEach((item, y) => { - function drawItem() { - // For the clock, we have a special case, as we don't wanna redraw - // immediately when something changes. Instead, we update data each minute - // to save some battery etc. Therefore, we hide (and disable the listener) - // immedeately after redraw... - item.hide(); +let clockInfoMenu = clock_info.addInteractive(clockInfoItems, { + x : 0, + y: 135, + w: W, + h: H-135, + draw : (itm, info, options) => { + var hideClkInfo = info.text == null; - // After drawing the item, we enable inputs again... - lock_input = false; + g.setColor(g.theme.fg); + g.fillRect(options.x, options.y, options.x+options.w, options.y+options.h); - var info = item.get(); - drawMenuItem(info.text, info.img); + g.setFontAlign(0,0); + g.setColor(g.theme.bg); + + if (options.focus){ + var y = hideClkInfo ? options.y+20 : options.y+2; + var h = hideClkInfo ? options.h-20 : options.h-2; + g.drawRect(options.x, y, options.x+options.w-2, y+h-1); // show if focused + g.drawRect(options.x+1, y+1, options.x+options.w-3, y+h-2); // show if focused } - item.on('redraw', drawItem); - }) + // In case we hide the clkinfo, we show the time again as the time should + // be drawn larger. + if(hideClkInfo){ + drawTime(); + return; + } + + // Set text and font + var image = info.img; + var text = String(info.text); + if(text.split('\n').length > 1){ + g.setMiniFont(); + } else { + g.setSmallFont(); + } + + // Compute sizes + var strWidth = g.stringWidth(text); + var imgWidth = image == null ? 0 : 24; + var midx = options.x+options.w/2; + + // Draw + if (image) { + var scale = imgWidth / image.width; + g.drawImage(image, midx-parseInt(imgWidth*1.3/2)-parseInt(strWidth/2), options.y+6, {scale: scale}); + } + g.drawString(text, midx+parseInt(imgWidth*1.3/2), options.y+20); + + // In case we are in focus and the focus box changes (fullscreen yes/no) + // we draw the time again. Otherwise it could happen that a while line is + // not cleared correctly. + if(options.focus) drawTime(); + } }); -function canRunMenuItem(){ - if(settings.menuPosY == 0){ - return false; - } - - var menuEntry = menu[settings.menuPosX]; - var item = menuEntry.items[settings.menuPosY-1]; - return item.run !== undefined; -} - - -function runMenuItem(){ - if(settings.menuPosY == 0){ - return; - } - - var menuEntry = menu[settings.menuPosX]; - var item = menuEntry.items[settings.menuPosY-1]; - try{ - var ret = item.run(); - if(ret){ - Bangle.buzz(300, 0.6); - } - } catch (ex) { - // Simply ignore it... - } -} - - /************************************************ * Draw */ -function draw() { +let draw = function() { // Queue draw again queueDraw(); // Draw clock drawDate(); - drawMenuAndTime(); + drawTime(); drawLock(); drawWidgets(); -} +}; -function drawDate(){ +let drawDate = function() { // Draw background - var y = H/5*2 + (isFullscreen() ? 0 : 8); + var y = getLineY() g.reset().clearRect(0,0,W,y); // Draw date @@ -216,17 +230,17 @@ function drawDate(){ g.setMediumFont(); g.setColor(g.theme.fg); g.drawString(dateStr, W/2 - fullDateW / 2, y+2); -} +}; -function drawTime(y, smallText){ +let drawTime = function() { + var hideClkInfo = clockInfoMenu.menuA == 0 && clockInfoMenu.menuB == 0; + // Draw background + var y1 = getLineY(); + var y = y1; var date = new Date(); - // Draw time - g.setColor(g.theme.bg); - g.setFontAlign(0,0); - var hours = String(date.getHours()); var minutes = date.getMinutes(); minutes = minutes < 10 ? String("0") + minutes : minutes; @@ -236,212 +250,93 @@ function drawTime(y, smallText){ // Set y coordinates correctly y += parseInt((H - y)/2) + 5; - // Show large or small time depending on info entry - if(smallText){ + if (hideClkInfo){ + g.setLargeFont(); + } else { y -= 15; g.setMediumFont(); - } else { - g.setLargeFont(); } - g.drawString(timeStr, W/2, y); -} - -function drawMenuItem(text, image){ - // First clear the time region - var y = H/5*2 + (isFullscreen() ? 0 : 8); + // Clear region and draw time g.setColor(g.theme.fg); - g.fillRect(0,y,W,H); + g.fillRect(0,y1,W,y+20 + (hideClkInfo ? 1 : 0) + (isFullscreen() ? 3 : 0)); - // Draw menu text - var hasText = (text != null && text != ""); - if(hasText){ - g.setFontAlign(0,0); - - // For multiline text we show an even smaller font... - text = String(text); - if(text.split('\n').length > 1){ - g.setMiniFont(); - } else { - g.setSmallFont(); - } - - var imgWidth = image == null ? 0 : 24; - var strWidth = g.stringWidth(text); - g.setColor(g.theme.fg).fillRect(0, 149-14, W, H); - g.setColor(g.theme.bg).drawString(text, W/2 + imgWidth/2 + 2, 149+3); - - if(image != null){ - var scale = imgWidth / image.width; - g.drawImage(image, W/2 + -strWidth/2-4 - parseInt(imgWidth/2), 149 - parseInt(imgWidth/2), {scale: scale}); - } - } - - // Draw time - drawTime(y, hasText); -} + g.setColor(g.theme.bg); + g.setFontAlign(0,0); + g.drawString(timeStr, W/2, y); +}; -function drawMenuAndTime(){ - var menuEntry = menu[settings.menuPosX]; - - // The first entry is the overview... - if(settings.menuPosY == 0){ - drawMenuItem(menuEntry.name, menuEntry.img); - return; - } - - // Draw item if needed - lock_input = true; - var item = menuEntry.items[settings.menuPosY-1]; - item.show(); -} - - -function drawLock(){ +let drawLock = function() { if(settings.showLock && Bangle.isLocked()){ g.setColor(g.theme.fg); g.drawImage(imgLock(), W-16, 2); } -} +}; -function drawWidgets(){ +let drawWidgets = function() { if(isFullscreen()){ - for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";} + widget_utils.hide(); } else { Bangle.drawWidgets(); } -} - - -function isFullscreen(){ - var s = settings.screen.toLowerCase(); - if(s == "dynamic"){ - return Bangle.isLocked() - } else { - return s == "full" - } -} - +}; /************************************************ * Listener */ // timeout used to update every minute -var drawTimeout; +let drawTimeout; // schedule a draw for the next minute -function queueDraw() { +let queueDraw = function() { if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = setTimeout(function() { drawTimeout = undefined; draw(); }, 60000 - (Date.now() % 60000)); -} +}; // Stop updates when LCD is off, restart when on -Bangle.on('lcdPower',on=>{ +let lcdListenerBw = function(on) { if (on) { draw(); // draw immediately, queue redraw } else { // stop draw timer if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = undefined; } -}); +}; +Bangle.on('lcdPower', lcdListenerBw); -Bangle.on('lock', function(isLocked) { +let lockListenerBw = function(isLocked) { if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = undefined; if(!isLocked && settings.screen.toLowerCase() == "dynamic"){ // If we have to show the widgets again, we load it from our // cache and not through Bangle.loadWidgets as its much faster! - for (let wd of WIDGETS) {wd.draw=wd._draw;wd.area=wd._area;} + widget_utils.show(); } draw(); -}); - -Bangle.on('charging',function(charging) { - if (drawTimeout) clearTimeout(drawTimeout); - drawTimeout = undefined; +}; +Bangle.on('lock', lockListenerBw); +let charging = function(charging){ // Jump to battery - settings.menuPosX = 0; - settings.menuPosY = 1; - draw(); -}); - -Bangle.on('touch', function(btn, e){ - var widget_size = isFullscreen() ? 0 : 20; // Its not exactly 24px -- empirically it seems that 20 worked better... - var left = parseInt(g.getWidth() * 0.22); - var right = g.getWidth() - left; - var upper = parseInt(g.getHeight() * 0.22) + widget_size; - var lower = g.getHeight() - upper; - - var is_upper = e.y < upper; - var is_lower = e.y > lower; - var is_left = e.x < left && !is_upper && !is_lower; - var is_right = e.x > right && !is_upper && !is_lower; - var is_center = !is_upper && !is_lower && !is_left && !is_right; - - if(lock_input){ - return; - } - - if(is_lower){ - Bangle.buzz(40, 0.6); - settings.menuPosY = (settings.menuPosY+1) % (menu[settings.menuPosX].items.length+1); - - drawMenuAndTime(); - } - - if(is_upper){ - if(e.y < widget_size){ - return; - } - - Bangle.buzz(40, 0.6); - settings.menuPosY = settings.menuPosY-1; - settings.menuPosY = settings.menuPosY < 0 ? menu[settings.menuPosX].items.length : settings.menuPosY; - - drawMenuAndTime(); - } - - if(is_right){ - Bangle.buzz(40, 0.6); - settings.menuPosX = (settings.menuPosX+1) % menu.length; - settings.menuPosY = 0; - drawMenuAndTime(); - } - - if(is_left){ - Bangle.buzz(40, 0.6); - settings.menuPosY = 0; - settings.menuPosX = settings.menuPosX-1; - settings.menuPosX = settings.menuPosX < 0 ? menu.length-1 : settings.menuPosX; - drawMenuAndTime(); - } - - if(is_center){ - if(canRunMenuItem()){ - runMenuItem(); - } - } -}); - - -E.on("kill", function(){ - try{ - storage.write(SETTINGS_FILE, settings); - } catch(ex){ - // If this fails, we still kill the app... - } -}); + clockInfoMenu.setItem(0, 2); + drawTime(); +} +Bangle.on('charging', charging); +let kill = function(){ + clockInfoMenu.remove(); + delete clockInfoMenu; +}; +E.on("kill", kill); /************************************************ * Startup Clock @@ -450,17 +345,31 @@ E.on("kill", function(){ // The upper part is inverse i.e. light if dark and dark if light theme // is enabled. In order to draw the widgets correctly, we invert the // dark/light theme as well as the colors. +let themeBackup = g.theme; g.setTheme({bg:g.theme.fg,fg:g.theme.bg, dark:!g.theme.dark}).clear(); // Show launcher when middle button pressed -Bangle.setUI("clock"); +Bangle.setUI({ + mode : "clock", + remove : function() { + // Called to unload all of the clock app + Bangle.removeListener('lcdPower', lcdListenerBw); + Bangle.removeListener('lock', lockListenerBw); + Bangle.removeListener('charging', charging); + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + // save settings + kill(); + E.removeListener("kill", kill); + g.setTheme(themeBackup); + widget_utils.show(); + } +}); // Load widgets and draw clock the first time Bangle.loadWidgets(); -// Cache draw function for dynamic screen to hide / show widgets -// Bangle.loadWidgets() could also be called later on but its much slower! -for (let wd of WIDGETS) {wd._draw=wd.draw; wd._area=wd.area;} - // Draw first time draw(); + +} // End of app scope diff --git a/apps/bwclk/metadata.json b/apps/bwclk/metadata.json index fa0f7b01f..39106c827 100644 --- a/apps/bwclk/metadata.json +++ b/apps/bwclk/metadata.json @@ -1,11 +1,11 @@ { "id": "bwclk", "name": "BW Clock", - "version": "0.23", - "description": "A very minimalistic clock to mainly show date and time.", + "version": "0.30", + "description": "A very minimalistic clock.", "readme": "README.md", "icon": "app.png", - "screenshots": [{"url":"screenshot.png"}, {"url":"screenshot_2.png"}, {"url":"screenshot_3.png"}, {"url":"screenshot_4.png"}], + "screenshots": [{"url":"screenshot.png"}, {"url":"screenshot_2.png"}, {"url":"screenshot_3.png"}], "type": "clock", "tags": "clock,clkinfo", "supports": ["BANGLEJS2"], diff --git a/apps/bwclk/screenshot.png b/apps/bwclk/screenshot.png index 3a75f13d1..37acf7cc0 100644 Binary files a/apps/bwclk/screenshot.png and b/apps/bwclk/screenshot.png differ diff --git a/apps/bwclk/screenshot_2.png b/apps/bwclk/screenshot_2.png index 31bf6373e..8d2f1717f 100644 Binary files a/apps/bwclk/screenshot_2.png and b/apps/bwclk/screenshot_2.png differ diff --git a/apps/bwclk/screenshot_3.png b/apps/bwclk/screenshot_3.png index 8d982cac4..d52057569 100644 Binary files a/apps/bwclk/screenshot_3.png and b/apps/bwclk/screenshot_3.png differ diff --git a/apps/bwclk/screenshot_4.png b/apps/bwclk/screenshot_4.png deleted file mode 100644 index 83de5c2ce..000000000 Binary files a/apps/bwclk/screenshot_4.png and /dev/null differ diff --git a/apps/choozi/ChangeLog b/apps/choozi/ChangeLog index 03f7ef832..35adc7430 100644 --- a/apps/choozi/ChangeLog +++ b/apps/choozi/ChangeLog @@ -1,3 +1,9 @@ 0.01: New App! 0.02: Support Bangle.js 2 0.03: Fix bug for Bangle.js 2 where g.flip was not being called. +0.04: Combine code for both apps + Better colors for Bangle.js 2 + Fix selection animation for Bangle.js 2 + New icon + Slightly wider arc segments for better visibility + Extract arc drawing code in library diff --git a/apps/choozi/README.md b/apps/choozi/README.md index f1e4255bc..ccaa97a27 100644 --- a/apps/choozi/README.md +++ b/apps/choozi/README.md @@ -11,16 +11,21 @@ the players seated in a circle, set the number of segments equal to the number of players, ensure that each person knows which colour represents them, and then choose a segment. After a short animation, the chosen segment will fill the screen. -You can use Choozi to randomly select an element from any set with 2 to 13 members, +You can use Choozi to randomly select an element from any set with 2 to 15 members, as long as you can define a bijection between members of the set and coloured segments on the Bangle.js display. -## Controls +## Controls Bangle 1 BTN1: increase the number of segments BTN2: choose a segment at random BTN3: decrease the number of segments +## Controls Bangle 2 + +Swipe up/down: increase/decrease the number of segments +BTN1 or tap: choose a segment at random + ## Creator James Stanley diff --git a/apps/choozi/app-icon.js b/apps/choozi/app-icon.js index 51b3bead3..560286098 100644 --- a/apps/choozi/app-icon.js +++ b/apps/choozi/app-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mEwggLIrnM4uqAAIhPgvMAAPFzIABzWgCxkMCweqC4QABDBYtC5QVFDBoWCCo5KLOQIWKDARFICxhJIFwOpC5owFFyAwGUYIuOGAwuRC4guSJAgXBCyIwDIyQXF5IXSzJeVMAReUAAOQhheTMAVcC6yOUC4aOUC7GZUyoXXzWqhQXVxGqC9mYC7OqC9eoxEKC6uBC6uIwAXBPCSmBwEAC6Z2BiAXBJCR2BgEAjQXSlGBC4JgSLwYABJCJGBLwJIDGB+IIwRIDGByNBIwZIDGBhdBRoQwSLoIuFGAYYKCwIuGGAgYI1QWBRgYYJMYmaFoSMEAAyrBAAgVCCxgYGjAWQAAMBC4UILZQA==")) +require("heatshrink").decompress(atob("mEwwcH/4AW/u27dt2wQL/YOBCIXbv4QI+AODAQVsh4RHwEbCI0LCI9gCIOANAXbsFbG437tkDPg1btoRFFoILBgmSpMggECHQO/CAf2CIVJkgRBAQIjC24RFsECCItIgIRFMYMAiQRFpMAlqmDVwPYgAOEAQUggu274RD4BWCCIskCIPbCIPt20ABwwCCwARFgIRJyEWCIVt2EJCJi2BCJmSUgIRCwARNt/7CIIOICI1sWAwCFoFbCOtt8EACJsAgARR8hwBCJlJk4RlgARQAgIRKDwMn/gRBdJgRPyARBn4RBpARLiQRB/4RBgIRJwAREpIRLAYP///ypMgCJMACI0ECI4JCp4RB/wZECIsAAYN/CIP/5JPDCIhjDCIraHTIWTCAX//K7DCI+fCIf/EZA1CCAn//ipCLIsBk4RF/5ZHCIIQG//wPo8vCI//6QRFpYQIAAPpCIeXCBQAC/VfBI4=")) \ No newline at end of file diff --git a/apps/choozi/app.js b/apps/choozi/app.js index 1a5b2f17e..b9f53bc89 100644 --- a/apps/choozi/app.js +++ b/apps/choozi/app.js @@ -4,15 +4,16 @@ * * James Stanley 2021 */ - -var colours = ['#ff0000', '#ff8080', '#00ff00', '#80ff80', '#0000ff', '#8080ff', '#ffff00', '#00ffff', '#ff00ff', '#ff8000', '#ff0080', '#8000ff', '#0080ff']; +const GU = require("graphics_utils"); +var colours = ['#ff0000', '#00ff00', '#0000ff', '#ffff00', '#00ffff', '#ff00ff', '#ffffff']; +var colours2 = ['#808080', '#404040', '#000040', '#004000', '#400000', '#ff8000', '#804000', '#4000c0']; var stepAngle = 0.18; // radians - resolution of polygon var gapAngle = 0.035; // radians - gap between segments -var perimMin = 110; // px - min. radius of perimeter -var perimMax = 120; // px - max. radius of perimeter +var perimMin = g.getWidth()*0.40; // px - min. radius of perimeter +var perimMax = g.getWidth()*0.49; // px - max. radius of perimeter -var segmentMax = 106; // px - max radius of filled-in segment +var segmentMax = g.getWidth()*0.38; // px - max radius of filled-in segment var segmentStep = 5; // px - step size of segment fill animation var circleStep = 4; // px - step size of circle fill animation @@ -22,10 +23,10 @@ var minSpeed = 0.001; // rad/sec var animStartSteps = 300; // how many steps before it can start slowing? var accel = 0.0002; // rad/sec/sec - acc-/deceleration rate var ballSize = 3; // px - ball radius -var ballTrack = 100; // px - radius of ball path +var ballTrack = perimMin - ballSize*2; // px - radius of ball path -var centreX = 120; // px - centre of screen -var centreY = 120; // px - centre of screen +var centreX = g.getWidth()*0.5; // px - centre of screen +var centreY = g.getWidth()*0.5; // px - centre of screen var fontSize = 50; // px @@ -33,7 +34,6 @@ var radians = 2*Math.PI; // radians per circle var defaultN = 3; // default value for N var minN = 2; -var maxN = colours.length; var N; var arclen; @@ -51,42 +51,14 @@ function shuffle (array) { } } -// draw an arc between radii minR and maxR, and between -// angles minAngle and maxAngle -function arc(minR, maxR, minAngle, maxAngle) { - var step = stepAngle; - var angle = minAngle; - var inside = []; - var outside = []; - var c, s; - while (angle < maxAngle) { - c = Math.cos(angle); - s = Math.sin(angle); - inside.push(centreX+c*minR); // x - inside.push(centreY+s*minR); // y - // outside coordinates are built up in reverse order - outside.unshift(centreY+s*maxR); // y - outside.unshift(centreX+c*maxR); // x - angle += step; - } - c = Math.cos(maxAngle); - s = Math.sin(maxAngle); - inside.push(centreX+c*minR); - inside.push(centreY+s*minR); - outside.unshift(centreY+s*maxR); - outside.unshift(centreX+c*maxR); - - var vertices = inside.concat(outside); - g.fillPoly(vertices, true); -} - // draw the arc segments around the perimeter function drawPerimeter() { + g.setBgColor('#000000'); g.clear(); for (var i = 0; i < N; i++) { g.setColor(colours[i%colours.length]); var minAngle = (i/N)*radians; - arc(perimMin,perimMax,minAngle,minAngle+arclen); + GU.fillArc(g, centreX, centreY, perimMin,perimMax,minAngle,minAngle+arclen, stepAngle); } } @@ -131,6 +103,7 @@ function animateChoice(target) { g.fillCircle(x, y, ballSize); oldx=x; oldy=y; + if (process.env.HWVERSION == 2) g.flip(); } } @@ -141,11 +114,15 @@ function choose() { var maxAngle = minAngle + arclen; animateChoice((minAngle+maxAngle)/2); g.setColor(colours[chosen%colours.length]); - for (var i = segmentMax-segmentStep; i >= 0; i -= segmentStep) - arc(i, perimMax, minAngle, maxAngle); - arc(0, perimMax, minAngle, maxAngle); - for (var r = 1; r < segmentMax; r += circleStep) + for (var i = segmentMax-segmentStep; i >= 0; i -= segmentStep){ + GU.fillArc(g, centreX, centreY, i, perimMax, minAngle, maxAngle, stepAngle); + if (process.env.HWVERSION == 2) g.flip(); + } + GU.fillArc(g, centreX, centreY, 0, perimMax, minAngle, maxAngle, stepAngle); + for (var r = 1; r < segmentMax; r += circleStep){ g.fillCircle(centreX,centreY,r); + if (process.env.HWVERSION == 2) g.flip(); + } g.fillCircle(centreX,centreY,segmentMax); } @@ -171,38 +148,47 @@ function setN(n) { drawPerimeter(); } -// save N to choozi.txt +// save N to choozi.save function writeN() { - var file = require("Storage").open("choozi.txt","w"); - file.write(N); + var savedN = read(); + if (savedN != N) require("Storage").write("choozi.save","" + N); } -// load N from choozi.txt +function read(){ + var n = require("Storage").read("choozi.save"); + if (n !== undefined) return parseInt(n); + return defaultN; +} + +// load N from choozi.save function readN() { - var file = require("Storage").open("choozi.txt","r"); - var n = file.readLine(); - if (n !== undefined) setN(parseInt(n)); - else setN(defaultN); + setN(read()); } -shuffle(colours); // is this really best? -Bangle.setLCDMode("direct"); -Bangle.setLCDTimeout(0); // keep screen on +if (process.env.HWVERSION == 1){ + colours=colours.concat(colours2); + shuffle(colours); +} else { + shuffle(colours); + shuffle(colours2); + colours=colours.concat(colours2); +} + +var maxN = colours.length; +if (process.env.HWVERSION == 1){ + Bangle.setLCDMode("direct"); + Bangle.setLCDTimeout(0); // keep screen on +} readN(); drawN(); -setWatch(() => { - setN(N+1); - drawN(); -}, BTN1, {repeat:true}); - -setWatch(() => { - writeN(); - drawPerimeter(); - choose(); -}, BTN2, {repeat:true}); - -setWatch(() => { - setN(N-1); - drawN(); -}, BTN3, {repeat:true}); +Bangle.setUI("updown", (v)=>{ + if (!v){ + writeN(); + drawPerimeter(); + choose(); + } else { + setN(N-v); + drawN(); + } +}); diff --git a/apps/choozi/app.png b/apps/choozi/app.png index 99c9fa07a..50f09f164 100644 Binary files a/apps/choozi/app.png and b/apps/choozi/app.png differ diff --git a/apps/choozi/appb2.js b/apps/choozi/appb2.js deleted file mode 100644 index 5f217f638..000000000 --- a/apps/choozi/appb2.js +++ /dev/null @@ -1,207 +0,0 @@ -/* Choozi - Choose people or things at random using Bangle.js. - * Inspired by the "Chwazi" Android app - * - * James Stanley 2021 - */ - -var colours = ['#ff0000', '#ff8080', '#00ff00', '#80ff80', '#0000ff', '#8080ff', '#ffff00', '#00ffff', '#ff00ff', '#ff8000', '#ff0080', '#8000ff', '#0080ff']; - -var stepAngle = 0.18; // radians - resolution of polygon -var gapAngle = 0.035; // radians - gap between segments -var perimMin = 80; // px - min. radius of perimeter -var perimMax = 87; // px - max. radius of perimeter - -var segmentMax = 70; // px - max radius of filled-in segment -var segmentStep = 5; // px - step size of segment fill animation -var circleStep = 4; // px - step size of circle fill animation - -// rolling ball animation: -var maxSpeed = 0.08; // rad/sec -var minSpeed = 0.001; // rad/sec -var animStartSteps = 300; // how many steps before it can start slowing? -var accel = 0.0002; // rad/sec/sec - acc-/deceleration rate -var ballSize = 3; // px - ball radius -var ballTrack = 75; // px - radius of ball path - -var centreX = 88; // px - centre of screen -var centreY = 88; // px - centre of screen - -var fontSize = 50; // px - -var radians = 2*Math.PI; // radians per circle - -var defaultN = 3; // default value for N -var minN = 2; -var maxN = colours.length; -var N; -var arclen; - -// https://www.frankmitchell.org/2015/01/fisher-yates/ -function shuffle (array) { - var i = 0 - , j = 0 - , temp = null; - - for (i = array.length - 1; i > 0; i -= 1) { - j = Math.floor(Math.random() * (i + 1)); - temp = array[i]; - array[i] = array[j]; - array[j] = temp; - } -} - -// draw an arc between radii minR and maxR, and between -// angles minAngle and maxAngle -function arc(minR, maxR, minAngle, maxAngle) { - var step = stepAngle; - var angle = minAngle; - var inside = []; - var outside = []; - var c, s; - while (angle < maxAngle) { - c = Math.cos(angle); - s = Math.sin(angle); - inside.push(centreX+c*minR); // x - inside.push(centreY+s*minR); // y - // outside coordinates are built up in reverse order - outside.unshift(centreY+s*maxR); // y - outside.unshift(centreX+c*maxR); // x - angle += step; - } - c = Math.cos(maxAngle); - s = Math.sin(maxAngle); - inside.push(centreX+c*minR); - inside.push(centreY+s*minR); - outside.unshift(centreY+s*maxR); - outside.unshift(centreX+c*maxR); - - var vertices = inside.concat(outside); - g.fillPoly(vertices, true); -} - -// draw the arc segments around the perimeter -function drawPerimeter() { - g.clear(); - for (var i = 0; i < N; i++) { - g.setColor(colours[i%colours.length]); - var minAngle = (i/N)*radians; - arc(perimMin,perimMax,minAngle,minAngle+arclen); - } -} - -// animate a ball rolling around and settling at "target" radians -function animateChoice(target) { - var angle = 0; - var speed = 0; - var oldx = -10; - var oldy = -10; - var decelFromAngle = -1; - var allowDecel = false; - for (var i = 0; true; i++) { - angle = angle + speed; - if (angle > radians) angle -= radians; - if (i < animStartSteps || (speed < maxSpeed && !allowDecel)) { - speed = speed + accel; - if (speed > maxSpeed) { - speed = maxSpeed; - /* when we reach max speed, we know how long it takes - * to accelerate, and therefore how long to decelerate, so - * we can work out what angle to start decelerating from */ - if (decelFromAngle < 0) { - decelFromAngle = target-angle; - while (decelFromAngle < 0) decelFromAngle += radians; - while (decelFromAngle > radians) decelFromAngle -= radians; - } - } - } else { - if (!allowDecel && (angle < decelFromAngle) && (angle+speed >= decelFromAngle)) allowDecel = true; - if (allowDecel) speed = speed - accel; - if (speed < minSpeed) speed = minSpeed; - if (speed == minSpeed && angle < target && angle+speed >= target) return; - } - - var r = i/2; - if (r > ballTrack) r = ballTrack; - var x = centreX+Math.cos(angle)*r; - var y = centreY+Math.sin(angle)*r; - g.setColor('#000000'); - g.fillCircle(oldx,oldy,ballSize+1); - g.setColor('#ffffff'); - g.fillCircle(x, y, ballSize); - oldx=x; - oldy=y; - g.flip(); - } -} - -// choose a winning segment and animate its selection -function choose() { - var chosen = Math.floor(Math.random()*N); - var minAngle = (chosen/N)*radians; - var maxAngle = minAngle + arclen; - animateChoice((minAngle+maxAngle)/2); - g.setColor(colours[chosen%colours.length]); - for (var i = segmentMax-segmentStep; i >= 0; i -= segmentStep) - arc(i, perimMax, minAngle, maxAngle); - arc(0, perimMax, minAngle, maxAngle); - for (var r = 1; r < segmentMax; r += circleStep) - g.fillCircle(centreX,centreY,r); - g.fillCircle(centreX,centreY,segmentMax); -} - -// draw the current value of N in the middle of the screen, with -// up/down arrows -function drawN() { - g.setColor(g.theme.fg); - g.setFont("Vector",fontSize); - g.drawString(N,centreX-g.stringWidth(N)/2+4,centreY-fontSize/2); - if (N < maxN) - g.fillPoly([centreX-6,centreY-fontSize/2-7, centreX+6,centreY-fontSize/2-7, centreX, centreY-fontSize/2-14]); - if (N > minN) - g.fillPoly([centreX-6,centreY+fontSize/2+5, centreX+6,centreY+fontSize/2+5, centreX, centreY+fontSize/2+12]); -} - -// update number of segments, with min/max limit, "arclen" update, -// and screen reset -function setN(n) { - N = n; - if (N < minN) N = minN; - if (N > maxN) N = maxN; - arclen = radians/N - gapAngle; - drawPerimeter(); -} - -// save N to choozi.txt -function writeN() { - var file = require("Storage").open("choozi.txt","w"); - file.write(N); -} - -// load N from choozi.txt -function readN() { - var file = require("Storage").open("choozi.txt","r"); - var n = file.readLine(); - if (n !== undefined) setN(parseInt(n)); - else setN(defaultN); -} - -shuffle(colours); // is this really best? -Bangle.setLCDTimeout(0); // keep screen on -readN(); -drawN(); - -setWatch(() => { - writeN(); - drawPerimeter(); - choose(); -}, BTN1, {repeat:true}); - -Bangle.on('touch', function(zone,e) { - if(e.x>+88){ - setN(N-1); - drawN(); - }else{ - setN(N+1); - drawN(); - } -}); diff --git a/apps/choozi/bangle1-choozi-screenshot1.png b/apps/choozi/bangle1-choozi-screenshot1.png index 104024958..ee422ed10 100644 Binary files a/apps/choozi/bangle1-choozi-screenshot1.png and b/apps/choozi/bangle1-choozi-screenshot1.png differ diff --git a/apps/choozi/bangle1-choozi-screenshot2.png b/apps/choozi/bangle1-choozi-screenshot2.png index f3b6868bf..20edf4c78 100644 Binary files a/apps/choozi/bangle1-choozi-screenshot2.png and b/apps/choozi/bangle1-choozi-screenshot2.png differ diff --git a/apps/choozi/metadata.json b/apps/choozi/metadata.json index 79af76fa2..c42abe079 100644 --- a/apps/choozi/metadata.json +++ b/apps/choozi/metadata.json @@ -1,7 +1,7 @@ { "id": "choozi", "name": "Choozi", - "version": "0.03", + "version": "0.04", "description": "Choose people or things at random using Bangle.js.", "icon": "app.png", "tags": "tool", @@ -10,8 +10,10 @@ "allow_emulator": true, "screenshots": [{"url":"bangle1-choozi-screenshot1.png"},{"url":"bangle1-choozi-screenshot2.png"}], "storage": [ - {"name":"choozi.app.js","url":"app.js","supports": ["BANGLEJS"]}, - {"name":"choozi.app.js","url":"appb2.js","supports": ["BANGLEJS2"]}, + {"name":"choozi.app.js","url":"app.js"}, {"name":"choozi.img","url":"app-icon.js","evaluate":true} + ], + "data": [ + {"name":"choozi.save"} ] } diff --git a/apps/circlesclock/ChangeLog b/apps/circlesclock/ChangeLog index 83abde6df..830bc28e8 100644 --- a/apps/circlesclock/ChangeLog +++ b/apps/circlesclock/ChangeLog @@ -39,3 +39,4 @@ 0.21: Remade all icons without a palette for dark theme Now re-adds widgets if they were hidden when fast-loading 0.22: Fixed crash if item has no image and cutting long overflowing text +0.23: Setting circles colours per clkinfo and not position diff --git a/apps/circlesclock/app.js b/apps/circlesclock/app.js index 30d6a48f4..e1fc8d846 100644 --- a/apps/circlesclock/app.js +++ b/apps/circlesclock/app.js @@ -20,24 +20,12 @@ let settings = Object.assign( storage.readJSON("circlesclock.default.json", true) || {}, storage.readJSON(SETTINGS_FILE, true) || {} ); - //TODO deprecate this (and perhaps use in the clkinfo module) -// Load step goal from health app and pedometer widget as fallback -if (settings.stepGoal == undefined) { - let d = storage.readJSON("health.json", true) || {}; - settings.stepGoal = d != undefined && d.settings != undefined ? d.settings.stepGoal : undefined; - - if (settings.stepGoal == undefined) { - d = storage.readJSON("wpedom.json", true) || {}; - settings.stepGoal = d != undefined && d.settings != undefined ? d.settings.goal : 10000; - } -} let drawTimeout; const showWidgets = settings.showWidgets || false; const circleCount = settings.circleCount || 3; const showBigWeather = settings.showBigWeather || false; -let hrtValue; //TODO deprecate this let now = Math.round(new Date().getTime() / 1000); // layout values: @@ -128,8 +116,11 @@ let draw = function() { queueDraw(); } -let getCircleColor = function(index) { - let color = settings["circle" + index + "color"]; +let getCircleColor = function(item, clkmenu) { + let colorKey = clkmenu.name; + if(!clkmenu.dynamic) colorKey += "/"+item.name; + colorKey += "_color"; + let color = settings[colorKey]; if (color && color != "") return color; return g.theme.fg; } @@ -138,7 +129,7 @@ let getGradientColor = function(color, percent) { if (isNaN(percent)) percent = 0; if (percent > 1) percent = 1; let colorList = [ - '#00FF00', '#80FF00', '#FFFF00', '#FF8000', '#FF0000' + '#00ff00', '#80ff00', '#ffff00', '#ff8000', '#ff0000' ]; if (color == "fg") { color = colorFg; @@ -151,6 +142,17 @@ let getGradientColor = function(color, percent) { let colorIndex = colorList.length - Math.round(colorList.length * percent); return colorList[Math.min(colorIndex, colorList.length)] || "#ff0000"; } + colorList = [ + '#0000ff', '#8800ff', '#ff00ff', '#ff0088', '#ff0000' + ]; + if (color == "blue-red") { + let colorIndex = Math.round(colorList.length * percent); + return colorList[Math.min(colorIndex, colorList.length) - 1] || "#0000ff"; + } + if (color == "red-blue") { + let colorIndex = colorList.length - Math.round(colorList.length * percent); + return colorList[Math.min(colorIndex, colorList.length)] || "#ff0000"; + } return color; } @@ -172,10 +174,10 @@ let drawEmpty = function(img, w, color) { .drawImage(img, w - iconOffset, h3 + radiusOuter - iconOffset, {scale: 16/24}); } -let drawCircle = function(index, item, data) { +let drawCircle = function(index, item, data, clkmenu) { var w = circlePosX[index-1]; drawCircleBackground(w); - const color = getCircleColor(index); + const color = getCircleColor(item, clkmenu); //drawEmpty(info? info.img : null, w, color); var img = data.img; var percent = 1; //fill up if no range @@ -338,7 +340,8 @@ Bangle.setUI({ let clockInfoDraw = (itm, info, options) => { //print("Draw",itm.name,options); - drawCircle(options.circlePosition, itm, info); + let clkmenu = clockInfoItems[options.menuA]; + drawCircle(options.circlePosition, itm, info, clkmenu); if (options.focus) g.reset().drawRect(options.x, options.y, options.x+options.w-2, options.y+options.h-1) }; let clockInfoItems = require("clock_info").load(); diff --git a/apps/circlesclock/default.json b/apps/circlesclock/default.json index ad409b992..9d5b3e242 100644 --- a/apps/circlesclock/default.json +++ b/apps/circlesclock/default.json @@ -3,23 +3,21 @@ "showWidgets": false, "weatherCircleData": "humidity", "circleCount": 3, - "circle1color": "green-red", - "circle2color": "#0000ff", - "circle3color": "red-green", - "circle4color": "#ffff00", + "Bangle/Battery_color":"red-green", + "Bangle/Steps_color":"#0000ff", + "Bangle/HRM_color":"green-red", + "Bangle/Altitude_color":"#00ff00", + "Weather/conditionWithTemperature_color":"#ffff00", + "Weather/condition_color":"#00ffff", + "Weather/humidity_color":"#00ffff", + "Weather/wind_color":"fg", + "Weather/temperature_color":"blue-red", + "Alarms_color":"#00ff00", + "Agenda_color":"#ff0000", "circle1colorizeIcon": true, "circle2colorizeIcon": true, "circle3colorizeIcon": true, "circle4colorizeIcon": false, "updateInterval": 60, - "showBigWeather": false, - - "minHR": 40, - "maxHR": 200, - "confidence": 0, - "stepGoal": 10000, - "stepDistanceGoal": 8000, - "stepLength": 0.8, - "hrmValidity": 60 - + "showBigWeather": false } diff --git a/apps/circlesclock/metadata.json b/apps/circlesclock/metadata.json index 1b94c00b3..45b869521 100644 --- a/apps/circlesclock/metadata.json +++ b/apps/circlesclock/metadata.json @@ -1,7 +1,7 @@ { "id": "circlesclock", "name": "Circles clock", "shortName":"Circles clock", - "version":"0.22", + "version":"0.23", "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"}], diff --git a/apps/circlesclock/settings.js b/apps/circlesclock/settings.js index 5c5ea4f27..63a2b0f93 100644 --- a/apps/circlesclock/settings.js +++ b/apps/circlesclock/settings.js @@ -12,12 +12,10 @@ storage.write(SETTINGS_FILE, settings); } - const valuesColors = ["", "#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff", - "#00ffff", "#fff", "#000", "green-red", "red-green", "fg"]; - const namesColors = ["default", "red", "green", "blue", "yellow", "magenta", - "cyan", "white", "black", "green->red", "red->green", "foreground"]; - - const weatherData = ["empty", "humidity", "wind"]; + const valuesColors = ["", "#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff", + "#00ffff", "#fff", "#000", "green-red", "red-green", "blue-red", "red-blue", "fg"]; + const namesColors = ["default", "red", "green", "blue", "yellow", "magenta", + "cyan", "white", "black", "green->red", "red->green", "blue->red", "red->blue", "foreground"]; function showMainMenu() { let menu ={ @@ -30,31 +28,11 @@ step: 1, onchange: x => save('circleCount', x), }, - /*LANG*/'circle 1': ()=>showCircleMenu(1), - /*LANG*/'circle 2': ()=>showCircleMenu(2), - /*LANG*/'circle 3': ()=>showCircleMenu(3), - /*LANG*/'circle 4': ()=>showCircleMenu(4), - /*LANG*/'battery warn': { - value: settings.batteryWarn, - min: 10, - max : 100, - step: 10, - format: x => { - return x + '%'; - }, - onchange: x => save('batteryWarn', x), - }, /*LANG*/'show widgets': { value: !!settings.showWidgets, format: () => (settings.showWidgets ? 'Yes' : 'No'), onchange: x => save('showWidgets', x), }, - /*LANG*/'weather data': { - value: weatherData.indexOf(settings.weatherCircleData), - min: 0, max: 2, - format: v => weatherData[v], - onchange: x => save('weatherCircleData', weatherData[x]), - }, /*LANG*/'update interval': { value: settings.updateInterval, min: 0, @@ -65,41 +43,54 @@ }, 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'), onchange: x => save('showBigWeather', x), - } + }, + /*LANG*/'colorize icons': ()=>showCircleMenus() }; + clock_info.load().forEach(e=>{ + if(e.dynamic) { + const colorKey = e.name + "_color"; + menu[e.name+/*LANG*/' color'] = { + value: valuesColors.indexOf(settings[colorKey]) || 0, + min: 0, max: valuesColors.length - 1, + format: v => namesColors[v], + onchange: x => save(colorKey, valuesColors[x]), + }; + } else { + let values = e.items.map(i=>e.name+"/"+i.name); + let names = e.name=="Bangle" ? e.items.map(i=>i.name) : values; + values.forEach((v,i)=>{ + const colorKey = v + "_color"; + menu[names[i]+/*LANG*/' color'] = { + value: valuesColors.indexOf(settings[colorKey]) || 0, + min: 0, max: valuesColors.length - 1, + format: v => namesColors[v], + onchange: x => save(colorKey, valuesColors[x]), + }; + }); + } + }) E.showMenu(menu); } - function showCircleMenu(circleId) { - const circleName = "circle" + circleId; - const colorKey = circleName + "color"; - const colorizeIconKey = circleName + "colorizeIcon"; - - const menu = { - '': { 'title': /*LANG*/'Circle ' + circleId }, - /*LANG*/'< Back': ()=>showMainMenu(), - /*LANG*/'color': { - value: valuesColors.indexOf(settings[colorKey]) || 0, - min: 0, max: valuesColors.length - 1, - format: v => namesColors[v], - onchange: x => save(colorKey, valuesColors[x]), - }, - /*LANG*/'colorize icon': { + function showCircleMenus() { + const menu = { + '': { 'title': /*LANG*/'Colorize icons'}, + /*LANG*/'< Back': ()=>showMainMenu(), + }; + for(var circleId=1; circleId<=4; ++circleId) { + const circleName = "circle" + circleId; + const colorKey = circleName + "color"; + const colorizeIconKey = circleName + "colorizeIcon"; + menu[/*LANG*/'circle ' + circleId] = { value: settings[colorizeIconKey] || false, - format: () => (settings[colorizeIconKey] ? 'Yes' : 'No'), - onchange: x => save(colorizeIconKey, x), - }, - }; + format: () => (settings[colorizeIconKey]? /*LANG*/'Yes': /*LANG*/'No'), + onchange: x => save(colorizeIconKey, x), + }; + } E.showMenu(menu); } diff --git a/apps/clkinfofw/ChangeLog b/apps/clkinfofw/ChangeLog index 7b83706bf..10810802b 100644 --- a/apps/clkinfofw/ChangeLog +++ b/apps/clkinfofw/ChangeLog @@ -1 +1,2 @@ 0.01: First release +0.02: Update clock_info to avoid a redraw and image allocation diff --git a/apps/clkinfofw/clkinfo.js b/apps/clkinfofw/clkinfo.js index 9815ca87f..2b3cb32ba 100644 --- a/apps/clkinfofw/clkinfo.js +++ b/apps/clkinfofw/clkinfo.js @@ -4,26 +4,13 @@ items: [ { name : "FW", get : () => { - let d = new Date(); - let g = Graphics.createArrayBuffer(24,24,1,{msb:true}); - g.drawImage(atob("GBjC////AADve773VWmmmmlVVW22nnlVVbLL445VVwAAAADVWAAAAAAlrAAAAAA6sAAAAAAOWAAAAAAlrAD//wA6sANVVcAOWANVVcAlrANVVcA6rANVVcA6WANVVcAlsANVVcAOrAD//wA6WAAAAAAlsAAAAAAOrAAAAAA6WAAAAAAlVwAAAADVVbLL445VVW22nnlVVWmmmmlV"),1,0); return { text : process.env.VERSION, - img : g.asImage("string") + img : atob("GBjC////AADve773VWmmmmlVVW22nnlVVbLL445VVwAAAADVWAAAAAAlrAAAAAA6sAAAAAAOWAAAAAAlrAD//wA6sANVVcAOWANVVcAlrANVVcA6rANVVcA6WANVVcAlsANVVcAOrAD//wA6WAAAAAAlsAAAAAAOrAAAAAA6WAAAAAAlVwAAAADVVbLL445VVW22nnlVVWmmmmlV") }; }, - show : function() { - this.interval = setTimeout(()=>{ - this.emit("redraw"); - this.interval = setInterval(()=>{ - this.emit("redraw"); - }, 86400000); - }, 86400000 - (Date.now() % 86400000)); - }, - hide : function() { - clearInterval(this.interval); - this.interval = undefined; - } + show : function() {}, + hide : function() {} } ] }; diff --git a/apps/clkinfofw/metadata.json b/apps/clkinfofw/metadata.json index 924297ca3..720a5baa5 100644 --- a/apps/clkinfofw/metadata.json +++ b/apps/clkinfofw/metadata.json @@ -1,6 +1,6 @@ { "id": "clkinfofw", "name": "Firmware Clockinfo", - "version":"0.01", + "version":"0.02", "description": "For clocks that display 'clockinfo', this displays the firmware version string", "icon": "app.png", "type": "clkinfo", diff --git a/apps/cogclock/ChangeLog b/apps/cogclock/ChangeLog index f4bfe77a5..403cd2258 100644 --- a/apps/cogclock/ChangeLog +++ b/apps/cogclock/ChangeLog @@ -1,3 +1,4 @@ 0.01: New clock 0.02: Use ClockFace library, add settings 0.03: Use ClockFace_menu.addSettingsFile +0.04: Hide widgets instead of not loading them at all diff --git a/apps/cogclock/metadata.json b/apps/cogclock/metadata.json index 29000b589..d404275ee 100644 --- a/apps/cogclock/metadata.json +++ b/apps/cogclock/metadata.json @@ -1,7 +1,7 @@ { "id": "cogclock", "name": "Cog Clock", - "version": "0.03", + "version": "0.04", "description": "A cross-shaped clock inside a cog", "icon": "icon.png", "screenshots": [{"url":"screenshot.png"}], diff --git a/apps/cogclock/settings.js b/apps/cogclock/settings.js index a91b033d0..fb1dd761c 100644 --- a/apps/cogclock/settings.js +++ b/apps/cogclock/settings.js @@ -4,7 +4,7 @@ /*LANG*/"< Back": back, }; require("ClockFace_menu").addSettingsFile(menu, "cogclock.settings.json", [ - "showDate", "loadWidgets" + "showDate", "hideWidgets" ]); E.showMenu(menu); }); diff --git a/apps/daisy/ChangeLog b/apps/daisy/ChangeLog index 61a09a18d..751164c07 100644 --- a/apps/daisy/ChangeLog +++ b/apps/daisy/ChangeLog @@ -7,3 +7,4 @@ 0.07: Use default Bangle formatter for booleans 0.08: fix idle timer always getting set to true 0.09: Use 'modules/suncalc.js' to avoid it being copied 8 times for different apps +0.10: Use widget_utils. diff --git a/apps/daisy/app.js b/apps/daisy/app.js index c99b19228..3b3975105 100644 --- a/apps/daisy/app.js +++ b/apps/daisy/app.js @@ -1,6 +1,7 @@ var SunCalc = require("suncalc"); // from modules folder const storage = require('Storage'); const locale = require("locale"); +const widget_utils = require('widget_utils'); const SETTINGS_FILE = "daisy.json"; const LOCATION_FILE = "mylocation.json"; const h = g.getHeight(); @@ -547,8 +548,6 @@ g.clear(); Bangle.loadWidgets(); /* * we are not drawing the widgets as we are taking over the whole screen - * 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="";} +widget_utils.hide(); draw(); diff --git a/apps/daisy/metadata.json b/apps/daisy/metadata.json index 0bad50151..471f8e56f 100644 --- a/apps/daisy/metadata.json +++ b/apps/daisy/metadata.json @@ -1,6 +1,6 @@ { "id": "daisy", "name": "Daisy", - "version":"0.09", + "version":"0.10", "dependencies": {"mylocation":"app"}, "description": "A beautiful digital clock with large ring guage, idle timer and a cyclic information line that includes, day, date, steps, battery, sunrise and sunset times", "icon": "app.png", diff --git a/apps/dragboard/ChangeLog b/apps/dragboard/ChangeLog index 265094e87..faf3d2d33 100644 --- a/apps/dragboard/ChangeLog +++ b/apps/dragboard/ChangeLog @@ -4,3 +4,4 @@ 0.04: Now displays the opened text string at launch. 0.05: Now scrolls text when string gets longer than screen width. 0.06: The code is now more reliable and the input snappier. Widgets will be drawn if present. +0.07: Settings for display colors diff --git a/apps/dragboard/README.md b/apps/dragboard/README.md index 8960e5749..415be5449 100644 --- a/apps/dragboard/README.md +++ b/apps/dragboard/README.md @@ -12,5 +12,8 @@ Known bugs: - Initially developed for use with dark theme set on Bangle.js 2 - that is still the preferred way to view it although it now works with other themes. - When repeatedly doing 'del' on an empty text-string, the letter case is changed back and forth between upper and lower case. -To do: -- Possibly provide a dragboard.settings.js file +Settings: +- CAPS LOCK: all characters are displayed and typed in uppercase +- ABC Color: color of the characters row +- Num Color: color of the digits and symbols row +- Highlight Color: color of the currently highlighted character diff --git a/apps/dragboard/lib.js b/apps/dragboard/lib.js index 220f075d7..83aae5f14 100644 --- a/apps/dragboard/lib.js +++ b/apps/dragboard/lib.js @@ -2,12 +2,14 @@ exports.input = function(options) { options = options||{}; var text = options.text; if ("string"!=typeof text) text=""; + let settings = require('Storage').readJSON('dragboard.json',1)||{} var R = Bangle.appRect; + const paramToColor = (param) => g.toColor(`#${settings[param].toString(16).padStart(3,0)}`); var BGCOLOR = g.theme.bg; - var HLCOLOR = g.theme.fg; - var ABCCOLOR = g.toColor(1,0,0);//'#FF0000'; - var NUMCOLOR = g.toColor(0,1,0);//'#00FF00'; + var HLCOLOR = settings.Highlight ? paramToColor("Highlight") : g.theme.fg; + var ABCCOLOR = settings.ABC ? paramToColor("ABC") : g.toColor(1,0,0);//'#FF0000'; + var NUMCOLOR = settings.Num ? paramToColor("Num") : g.toColor(0,1,0);//'#00FF00'; var BIGFONT = '6x8:3'; var BIGFONTWIDTH = parseInt(BIGFONT.charAt(0)*parseInt(BIGFONT.charAt(-1))); var SMALLFONT = '6x8:1'; @@ -102,6 +104,7 @@ exports.input = function(options) { //setTimeout(initDraw, 0); // So Bangle.appRect reads the correct environment. It would draw off to the side sometimes otherwise. function changeCase(abcHL) { + if (settings.uppercase) return; g.setColor(BGCOLOR); g.setFontAlign(-1, -1, 0); g.drawString(ABC, ABCPADDING, (R.y+R.h)/2); diff --git a/apps/dragboard/metadata.json b/apps/dragboard/metadata.json index 64b6dbe18..964ace3a7 100644 --- a/apps/dragboard/metadata.json +++ b/apps/dragboard/metadata.json @@ -1,6 +1,6 @@ { "id": "dragboard", "name": "Dragboard", - "version":"0.06", + "version":"0.07", "description": "A library for text input via swiping keyboard", "icon": "app.png", "type":"textinput", @@ -9,6 +9,7 @@ "screenshots": [{"url":"screenshot.png"}], "readme": "README.md", "storage": [ - {"name":"textinput","url":"lib.js"} + {"name":"textinput","url":"lib.js"}, + {"name":"dragboard.settings.js","url":"settings.js"} ] } diff --git a/apps/dragboard/settings.js b/apps/dragboard/settings.js new file mode 100644 index 000000000..a53914869 --- /dev/null +++ b/apps/dragboard/settings.js @@ -0,0 +1,48 @@ +(function(back) { + let settings = require('Storage').readJSON('dragboard.json',1)||{}; + const colors = { + 4095: /*LANG*/"White", + 4080: /*LANG*/"Yellow", + 3840: /*LANG*/"Red", + 3855: /*LANG*/"Magenta", + 255: /*LANG*/"Cyan", + 240: /*LANG*/"Green", + 15: /*LANG*/"Blue", + 0: /*LANG*/"Black", + '-1': /*LANG*/"Default" + }; + + const save = () => require('Storage').write('dragboard.json', settings); + function colorMenu(key) { + let menu = {'': {title: key}, '< Back': () => E.showMenu(appMenu)}; + Object.keys(colors).forEach(color => { + var label = colors[color]; + menu[label] = { + value: settings[key] == color, + onchange: () => { + if (color >= 0) { + settings[key] = color; + } else { + delete settings[key]; + } + save(); + setTimeout(E.showMenu, 10, appMenu); + } + }; + }); + return menu; + } + + const appMenu = { + '': {title: 'Dragboard'}, '< Back': back, + /*LANG*/'CAPS LOCK': { + value: !!settings.uppercase, + onchange: v => {settings.uppercase = v; save();} + }, + /*LANG*/'ABC Color': () => E.showMenu(colorMenu("ABC")), + /*LANG*/'Num Color': () => E.showMenu(colorMenu("Num")), + /*LANG*/'Highlight Color': () => E.showMenu(colorMenu("Highlight")) + }; + + E.showMenu(appMenu); +}); \ No newline at end of file diff --git a/apps/dtlaunch/ChangeLog b/apps/dtlaunch/ChangeLog index 044b8c35f..e0bd76eb0 100644 --- a/apps/dtlaunch/ChangeLog +++ b/apps/dtlaunch/ChangeLog @@ -23,4 +23,8 @@ button to exit is no longer an option. facilitate 'fast switching' of apps where available. 0.20: Bangle 2: Revert use of Bangle.load() to classic load() calls since widgets would still be loaded when they weren't supposed to. +0.21: Bangle 2: Call Bangle.drawWidgets() early on so that the widget field +immediately follows the correct theme. +0.22: Bangle 2: Change to not automatically marking the first app on a page +when moving pages. Add caching for faster startups. diff --git a/apps/dtlaunch/app-b2.js b/apps/dtlaunch/app-b2.js index a7a318c18..2070f1147 100644 --- a/apps/dtlaunch/app-b2.js +++ b/apps/dtlaunch/app-b2.js @@ -13,20 +13,25 @@ }, require('Storage').readJSON("dtlaunch.json", true) || {}); let s = require("Storage"); - var apps = s.list(/\.info$/).map(app=>{ - let a=s.readJSON(app,1); - return a && { - name:a.name, type:a.type, icon:a.icon, sortorder:a.sortorder, src:a.src - };}).filter( - app=>app && (app.type=="app" || (app.type=="clock" && settings.showClocks) || (app.type=="launch" && settings.showLaunchers) || !app.type)); - - apps.sort((a,b)=>{ - let n=(0|a.sortorder)-(0|b.sortorder); - if (n) return n; // do sortorder first - if (a.nameb.name) return 1; - return 0; - }); + // Borrowed caching from Icon Launcher, code by halemmerich. + let launchCache = s.readJSON("launch.cache.json", true)||{}; + let launchHash = require("Storage").hash(/\.info/); + if (launchCache.hash!=launchHash) { + launchCache = { + hash : launchHash, + apps : s.list(/\.info$/) + .map(app=>{var a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src};}) + .filter(app=>app && (app.type=="app" || (app.type=="clock" && settings.showClocks) || !app.type)) + .sort((a,b)=>{ + var n=(0|a.sortorder)-(0|b.sortorder); + if (n) return n; // do sortorder first + if (a.nameb.name) return 1; + return 0; + }) }; + s.writeJSON("launch.cache.json", launchCache); + } + let apps = launchCache.apps; apps.forEach(app=>{ if (app.icon) app.icon = s.read(app.icon); // should just be a link to a memory area @@ -84,12 +89,13 @@ g.flip(); }; + Bangle.drawWidgets(); // To immediately update widget field to follow current theme - remove leftovers if previous app set custom theme. Bangle.loadWidgets(); drawPage(0); let swipeListenerDt = function(dirLeftRight, dirUpDown){ updateTimeoutToClock(); - selected = 0; + selected = -1; oldselected=-1; if(settings.swipeExit && dirLeftRight==1) Bangle.showClock(); if (dirUpDown==-1||dirLeftRight==-1){ @@ -153,3 +159,4 @@ updateTimeoutToClock(); } // end of app scope + diff --git a/apps/dtlaunch/metadata.json b/apps/dtlaunch/metadata.json index b69a1a5e6..b19d59e49 100644 --- a/apps/dtlaunch/metadata.json +++ b/apps/dtlaunch/metadata.json @@ -1,7 +1,7 @@ { "id": "dtlaunch", "name": "Desktop Launcher", - "version": "0.20", + "version": "0.22", "description": "Desktop style App Launcher with six (four for Bangle 2) apps per page - fast access if you have lots of apps installed.", "screenshots": [{"url":"shot1.png"},{"url":"shot2.png"},{"url":"shot3.png"}], "icon": "icon.png", diff --git a/apps/entonclk/ChangeLog b/apps/entonclk/ChangeLog index 62e2d0c20..e16defa54 100644 --- a/apps/entonclk/ChangeLog +++ b/apps/entonclk/ChangeLog @@ -1 +1,2 @@ -0.1: New App! \ No newline at end of file +0.1: New App! +0.2: Now with timer function diff --git a/apps/entonclk/README.md b/apps/entonclk/README.md index 8c788c7a5..c67cc19c8 100644 --- a/apps/entonclk/README.md +++ b/apps/entonclk/README.md @@ -6,4 +6,16 @@ Things I changed: - The main font for the time is now Audiowide - Removed the written out day name and replaced it with steps and bpm -- Changed the date string to a (for me) more readable string \ No newline at end of file +- Changed the date string to a (for me) more readable string + +Timer function: +- Touch the right side, to start the timer +- Initial timer timeout is 300s/5min +- Right touch again, add 300s/5min to timeout +- Left touch, decrease timeout by 60s/1min +- So it is easy, to add timeouts like 7min/3min or 12min +- Special thanks to the maintainer of the a_clock_timer app from which I borrowed the code. + +Todo: +- Make displayed information configurable, after https://github.com/espruino/BangleApps/issues/2226 +- Clean up code diff --git a/apps/entonclk/app.js b/apps/entonclk/app.js index 69fdea479..292030b86 100644 --- a/apps/entonclk/app.js +++ b/apps/entonclk/app.js @@ -1,67 +1,127 @@ +// Fonts Graphics.prototype.setFontAudiowide = function() { - // Actual height 33 (36 - 4) var widths = atob("CiAsESQjJSQkHyQkDA=="); var font = atob("AAAAAAAAAAAAAAAAAAAAAPAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAADgAAAAAAHgAAAAAAfgAAAAAA/gAAAAAD/gAAAAAH/gAAAAAf/AAAAAB/8AAAAAD/4AAAAAP/gAAAAAf/AAAAAB/8AAAAAD/4AAAAAP/gAAAAAf+AAAAAB/8AAAAAH/wAAAAAP/gAAAAA/+AAAAAB/8AAAAAD/wAAAAAD/gAAAAAD+AAAAAAD4AAAAAADwAAAAAADAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAA//+AAAAB///AAAAH///wAAAP///4AAAf///8AAA////+AAA/4AP+AAB/gAD/AAB/AA9/AAD+AB+/gAD+AD+/gAD+AD+/gAD8AH+fgAD8AP8fgAD8AP4fgAD8Af4fgAD8A/wfgAD8A/gfgAD8B/gfgAD8D/AfgAD8D+AfgAD8H+AfgAD8P8AfgAD8P4AfgAD8f4AfgAD8/wAfgAD8/gAfgAD+/gA/gAD+/AA/gAB/eAB/AAB/sAD/AAB/wAH/AAA////+AAAf///8AAAP///4AAAH///wAAAD///gAAAA//+AAAAAP/4AAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD/////gAD/////gAD/////gAD/////gAD/////gAD/////gAD/////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//gAAAAH//gAAAAP//gAD8Af//gAD8A///gAD8B///gAD8B///gAD8B/AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD+D+AfgAD//+AfgAD//+AfgAB//8AfgAA//4AfgAAf/wAfgAAP/gAfgAAB8AAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD+B+A/gAD/////gAB/////AAB/////AAA////+AAAf///8AAAP///4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//4AAAAD//8AAAAD//+AAAAD//+AAAAD//+AAAAD//+AAAAD//+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAD/////gAD/////gAD/////gAD/////gAD/////gAD/////gAD/////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//AAfgAD//wAfgAD//4AfgAD//8AfgAD//8AfgAD//+AfgAD8D+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B/A/gAD8B///gAD8B///gAD8A///AAD8A///AAAAAf/+AAAAAP/4AAAAAD/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB///AAAAH///wAAAf///8AAAf///8AAA////+AAB/////AAB/h+H/AAD/B+B/gAD+B+A/gAD+B+A/gAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B/A/gAD8B///gAD8B///gAD8A///AAAAAf//AAAAAf/+AAAAAH/4AAAAAB/gAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD8AAAAgAD8AAABgAD8AAAHgAD8AAAfgAD8AAA/gAD8AAD/gAD8AAP/gAD8AA//gAD8AB//AAD8AH/8AAD8Af/wAAD8A//AAAD8D/+AAAD8P/4AAAD8f/gAAAD9//AAAAD//8AAAAD//wAAAAD//gAAAAD/+AAAAAD/4AAAAAD/wAAAAAD/AAAAAAD8AAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAH/4AAAAAP/8AAAH+f/+AAAf////AAA/////gAB/////gAB///A/gAD//+AfgAD//+AfgAD+D+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD+D+AfgAD//+AfgAD//+AfgAB///A/gAB/////gAA/////AAAP////AAAD+f/+AAAAAP/8AAAAAH/4AAAAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/AAAAAAf/wAAAAA//4AAAAB//8AAAAB//8AfgAD//+AfgAD/D+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD+B+A/gAD+B+A/gAD/B+B/gAB/////AAB/////AAA////+AAAf///8AAAP///4AAAH///wAAAB///AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAPAAAA/AAfgAAA/AAfgAAA/AAfgAAA/AAfgAAAeAAPAAAAAAAAAAAAAAAAAAAAAAAAAA"); var scale = 1; // size multiplier for this font g.setFontCustom(font, 46, widths, 48+(scale<<8)+(1<<16)); }; +// Globals variables +var timervalue = 0; +var istimeron = false; +var timertick; + +// Functions function getSteps() { - var steps = 0; - try{ - if (WIDGETS.wpedom !== undefined) { - steps = WIDGETS.wpedom.getSteps(); - } else if (WIDGETS.activepedom !== undefined) { - steps = WIDGETS.activepedom.getSteps(); - } else { - steps = Bangle.getHealthStatus("day").steps; - } + var steps = 0; + try{ + if (WIDGETS.wpedom !== undefined) { + steps = WIDGETS.wpedom.getSteps(); + } else if (WIDGETS.activepedom !== undefined) { + steps = WIDGETS.activepedom.getSteps(); + } else { + steps = Bangle.getHealthStatus("day").steps; + } } catch(ex) { - // In case we failed, we can only show 0 steps. - return "?"; + // In case we failed, we can only show 0 steps. + return "?"; } - return Math.round(steps); + return Math.round(steps); } +function timeToString(duration) { + var hrs = ~~(duration / 3600); + var mins = ~~((duration % 3600) / 60); + var secs = ~~duration % 60; + var ret = ""; + if (hrs > 0) { + ret += "" + hrs + ":" + (mins < 10 ? "0" : ""); + } + ret += "" + mins + ":" + (secs < 10 ? "0" : ""); + ret += "" + secs; + return ret; +} + +function countDown() { + timervalue--; + + g.reset().clearRect(0, 40, 44+99, g.getHeight()/2-25); + + g.setFontAlign(0, -1, 0); + g.setFont("6x8", 2).drawString(timeToString(timervalue), 95, g.getHeight()/2-50); + + if (timervalue <= 0) { + istimeron = false; + clearInterval(timertick); + + Bangle.buzz().then(()=>{ + return new Promise(resolve=>setTimeout(resolve, 500)); + }).then(()=>{ + return Bangle.buzz(1000); + }); + } + else + if ((timervalue <= 30) && (timervalue % 10 == 0)) { Bangle.buzz(); } +} + +// Touch +Bangle.on('touch',t => { + if (t == 1) { + // Touch on the left, reduce timervalue about 60s + Bangle.buzz(30); + if (timervalue < 60) { timervalue = 1 ; } + else { timervalue -= 60; } + } + // Touch on the right, raise timervaule about 300s + else if (t == 2) { + Bangle.buzz(30); + if (!istimeron) { + istimeron = true; + timertick = setInterval(countDown, 1000); + } + timervalue += 60*5; + } +}); + { // must be inside our own scope here so that when we are unloaded everything disappears // we also define functions using 'let fn = function() {..}' for the same reason. function decls are global -let drawTimeout; + let drawTimeout; -// Actually draw the watch face -let draw = function() { - var x = g.getWidth() / 2; - var y = g.getHeight() / 2; - g.reset().clearRect(Bangle.appRect); // clear whole background (w/o widgets) - var date = new Date(); - var timeStr = require("locale").time(date, 1); // Hour and minute - g.setFontAlign(0, 0).setFont("Audiowide").drawString(timeStr, x, y); - var dateStr = require("locale").date(date, 1).toUpperCase(); - g.setFontAlign(0, 0).setFont("6x8", 2).drawString(dateStr, x, y+28); - g.setFontAlign(0, 0).setFont("6x8", 2); - g.drawString(getSteps(), 50, y+70); - g.drawString(Math.round(Bangle.getHealthStatus("last").bpm), g.getWidth() -37, y + 70); + // Actually draw the watch face + let draw = function() { + var x = g.getWidth() / 2; + var y = g.getHeight() / 2; + g.reset().clearRect(Bangle.appRect); // clear whole background (w/o widgets) + var date = new Date(); + var timeStr = require("locale").time(date, 1); // Hour and minute + g.setFontAlign(0, 0).setFont("Audiowide").drawString(timeStr, x, y); + var dateStr = require("locale").date(date, 1).toUpperCase(); + g.setFontAlign(0, 0).setFont("6x8", 2).drawString(dateStr, x, y+28); + g.setFontAlign(0, 0).setFont("6x8", 2); + g.drawString(getSteps(), 50, y+70); + g.drawString(Math.round(Bangle.getHealthStatus("last").bpm), g.getWidth() -37, y + 70); - // queue next draw - if (drawTimeout) clearTimeout(drawTimeout); - drawTimeout = setTimeout(function() { - drawTimeout = undefined; - draw(); - }, 60000 - (Date.now() % 60000)); -}; - -// Show launcher when middle button pressed -Bangle.setUI({ - mode : "clock", - remove : function() { - // Called to unload all of the clock app + // queue next draw if (drawTimeout) clearTimeout(drawTimeout); - drawTimeout = undefined; - delete Graphics.prototype.setFontAnton; - }}); -// Load widgets -Bangle.loadWidgets(); -draw(); -setTimeout(Bangle.drawWidgets,0); -} + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); + }; + + // Show launcher when middle button pressed + Bangle.setUI({ + mode : "clock", + remove : function() { + // Called to unload all of the clock app + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + delete Graphics.prototype.setFontAnton; + }}); + // Load widgets + Bangle.loadWidgets(); + draw(); + setTimeout(Bangle.drawWidgets,0); +} \ No newline at end of file diff --git a/apps/entonclk/metadata.json b/apps/entonclk/metadata.json index 7e4947406..4b7174263 100644 --- a/apps/entonclk/metadata.json +++ b/apps/entonclk/metadata.json @@ -1,8 +1,8 @@ { "id": "entonclk", "name": "Enton Clock", - "version": "0.1", - "description": "A simple clock using the Audiowide font. ", + "version": "0.2", + "description": "A simple clock using the Audiowide font with timer. ", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], "type": "clock", diff --git a/apps/geminiclock/ChangeLog b/apps/geminiclock/ChangeLog new file mode 100644 index 000000000..c408647b6 --- /dev/null +++ b/apps/geminiclock/ChangeLog @@ -0,0 +1 @@ +0.01: App created! diff --git a/apps/geminiclock/README.md b/apps/geminiclock/README.md new file mode 100644 index 000000000..d05acca04 --- /dev/null +++ b/apps/geminiclock/README.md @@ -0,0 +1,20 @@ +# Gemini clock + +A simple clock face using the Buro Destruct Geminis font, inspired by their Pebble Watch designs: https://burodestruct.net/work/pebble-watchfaces + +![image](watch-in-use.jpg) + +It is designed for maximum legibility and utility whilst still showing widgets. + +If editing or remixing this code, please retain leading zeroes on the hours, they are an integral part of the design. + +The minutes are not right-aligned deliberately so that the numbers don't jump around too much when they change. + + +## Creator +Created by Giles Booth: +- http://www.suppertime.co.uk/blogmywiki/ +- https://mastodon.social/@blogmywiki +- https://github.com/blogmywiki + + diff --git a/apps/geminiclock/app-icon.js b/apps/geminiclock/app-icon.js new file mode 100644 index 000000000..e571d5b6f --- /dev/null +++ b/apps/geminiclock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4f/AoP//+iiE00u++/nnMooWSyhT/AA8C9u27dtAQNkgEKAwdQCIUD5MkyVJAQNOwG9DIf+oARBgIPDAQOZoHSA4cj0ARIyeA6AIDpnAI4X2FgXcwgRBp0SDQYRCgErKAVt+ARBi4HC3AREAAnMCIIFCgN4CJOuiYRDgFmCKFXhIR/CMSPFCI0reYazCCJEDuzXGUIvoCIXCfY0Brdt284pOfqjLBBwQCCzNArf///9DoMx3gaBEAYTBnmA5wZCiQDB4+QCIeY+3bFgPQFg3QCIeXKwdMCJfOCIcbI4gRLhIhCvARMR4oRPWYIR/CNNnCI+Z9u20AREtCPHzMbtv8wEXv7GB3ARHAQXJoGuA4VCCIjdCCIWTwHQfY8DnIHDAQNACI+AgNvHwIACI4Nd23btoCC3x3BEQsggEKCAm26iIGAH4ATA==")) diff --git a/apps/geminiclock/app.png b/apps/geminiclock/app.png new file mode 100644 index 000000000..2c96e8937 Binary files /dev/null and b/apps/geminiclock/app.png differ diff --git a/apps/geminiclock/gemini-watch-app.js b/apps/geminiclock/gemini-watch-app.js new file mode 100644 index 000000000..716bc37b5 --- /dev/null +++ b/apps/geminiclock/gemini-watch-app.js @@ -0,0 +1,73 @@ +// Clock by Giles Booth for BangleJS2 using Büro Destruct Console Remix font +// based on code in https://www.espruino.com/Bangle.js+Clock+Font + +Graphics.prototype.setFontBDGemini = function() { + // Actual height 79 (84 - 6) + var widths = atob("Gio1KS8zNS8vLzkvGg=="); + var font = atob("AAAAAAAAD///gAAAAAAAAAAAH///gAAAAAAAAAAAH///gAAAAAAAAAAAH///gAAAAAAAAAAAH///gAAAAAAAAAAAH///gAAAAAAAAAAAH///gAAAAAAAAAAAH///gAAAAAAAAAAAH///gAAAAAAAAAAAH///gAAAAAAAAAAAH///gAAAAAAAAAAAH///gAAAAAAAAAAAH///gAAAAAAAAAAAH///gAAAAAAAAAAAH///gAAAAAAAAAAAH///gAAAAAAAAAAAH///gAAAAAAAAAAAH///gAAAAAAAAAAAD///AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH+AAAAAAAAAAAAAAf/AAAAAAAAAAAAAD//AAAAAAAAAAAAAP//AAAAAAAAAAAAA///AAAAAAAAAAAAH///AAAAAAAAAAAAf//+AAAAAAAAAAAD///8AAAAAAAAAAAP///wAAAAAAAAAAB///+AAAAAAAAAAAH///4AAAAAAAAAAA////gAAAAAAAAAAD///8AAAAAAAAAAAP///wAAAAAAAAAAB///+AAAAAAAAAAAH///4AAAAAAAAAAA////AAAAAAAAAAAD///8AAAAAAAAAAAf///gAAAAAAAAAAB///+AAAAAAAAAAAP///wAAAAAAAAAAA////AAAAAAAAAAAH///8AAAAAAAAAAAf///gAAAAAAAAAAB///+AAAAAAAAAAAP///wAAAAAAAAAAA////AAAAAAAAAAAH///4AAAAAAAAAAAP///gAAAAAAAAAAAf//8AAAAAAAAAAAAf//wAAAAAAAAAAAAf/+AAAAAAAAAAAAAf/4AAAAAAAAAAAAAf/gAAAAAAAAAAAAAP8AAAAAAAAAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///////////4AAAf///////////8AAAf///////////8AAA////////////+AAA////////////+AAA////////////+AAA////////////+AAA////////////+AAA//gAAAAB////+AAA/+AAAAAD////+AAA/8AAAAAP////+AAA/4AAAAA/////+AAA/4AAAAD/////+AAA/4AAAAH/////+AAA/4AAAAf/////+AAA/4AAAB//+f//+AAA/4AAAD//8f//+AAA/4AAAP//wf//+AAA/4AAA///Af//+AAA/4AAB//+Af//+AAA/4AAH//4Af//+AAA/4AAf//gAf//+AAA/4AB///AAf//+AAA/4AD//8AAf//+AAA/4AP//wAAf//+AAA/4A///gAAf//+AAA/4B//+AAAf//+AAA/4H//4AAAf//+AAA/4f//wAAAf//+AAA/4///AAAAf//+AAA/7//8AAAAf//+AAA////wAAAAf//+AAA////gAAAAf//+AAA///+AAAAAf//+AAA///4AAAAA///+AAA///wAAAAA///+AAA///AAAAAB///+AAA//+AAAAAP///+AAA////////////+AAA////////////+AAA////////////+AAA////////////+AAA////////////+AAAf///////////8AAAP///////////8AAAH///////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwAAAAAAAB//8AAAf4AAAAAAAD//+AAAf4AAAAAAAH//+AAAf4AAAAAAAP///AAAf8AAAAAAAP///AAAf8AAAAAAAP///AAAf8AAAAAAAP///AAAf8AAAAAAAP///AAAf8AAAAAAAP///AAAf8AAAAAAAf///AAAf+AAAAAAAf///AAAf/AAAAAAA////AAAf/4AAAAAP////AAAf////////////AAAf////////////AAAf////////////AAAf////////////AAAf////////////AAAP////////////AAAP////////////AAAD////////////AAAAAAAAAAAH////AAAAAAAAAAAA////AAAAAAAAAAAAf///AAAAAAAAAAAAP///AAAAAAAAAAAAP///AAAAAAAAAAAAP///AAAAAAAAAAAAP///AAAAAAAAAAAAP///AAAAAAAAAAAAP///AAAAAAAAAAAAP///AAAAAAAAAAAAH//+AAAAAAAAAAAAD//+AAAAAAAAAAAAB//4AAAAAAAAAAAAAP+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfwAAAD//////4AAA/wAAAH//////8AAA/wAAAH//////+AAA/4AAAP//////+AAA/4AAAP//////+AAA/4AAAP//////+AAA/4AAAP//////+AAA/4AAAP//////+AAA/4AAAP//////+AAA/4AAAP//////+AAA/4AAAP//////+AAA/4AAAP//////+AAA/4AAAP//////+AAA/4AAAP//////+AAA/4AAAP//////+AAA/4AAAP//////+AAA/4AAAP//////+AAA/4AAAP//////+AAA/4AAAP//////+AAA/4AAAP//////+AAA/4AAAP//////+AAA/4AAAP/4AAB/+AAA/4AAAP/gAAAf+AAA/4AAAP/AAAAf+AAA/4AAAP+AAAAP+AAA/4AAAP+AAAAP+AAA/4AAAP+AAAAP+AAA/4AAAP+AAAAP+AAA/4AAAP+AAAAP+AAA/8AAAf+AAAAP+AAA/+AAA/+AAAAP+AAA//////+AAAAP+AAA//////+AAAAP+AAA//////+AAAAP+AAA//////+AAAAP+AAA//////+AAAAP+AAA//////+AAAAH+AAAf/////8AAAAH+AAAP/////4AAAAH+AAAH/////wAAAAD8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAPwAAAB+AAAf4AAAAP4AAAD/AAAf4AAAAf8AAAD/AAAf8AAAAf8AAAH/AAAf8AAAAf8AAAH/AAAf8AAAAf8AAAH/AAAf8AAAAf8AAAH/AAAf8AAAAf8AAAH/AAAf8AAAAf8AAAH/AAAf8AAAAf8AAAH/AAAf8AAAAf8AAAH/AAAf8AAAAf8AAAH/AAAf8AAAAf8AAAH/AAAf8AAAAf8AAAH/AAAf8AAAAf8AAAH/AAAf8AAAAf8AAAH/AAAf8AAAAf8AAAH/AAAf8AAAAf8AAAH/AAAf8AAAAf8AAAH/AAAf8AAAAf8AAAH/AAAf8AAAAf8AAAH/AAAf8AAAA/+AAAH/AAAf+AAAB/+AAAP/AAAf/gAAD//gAAf/AAAf////////////AAAf////////////AAAf////////////AAAf////////////AAAf////////////AAAf////////////AAAP////////////AAAH////////////AAAD////////////AAAAAAAAH///////AAAAAAAAD///////AAAAAAAAB///////AAAAAAAAA///////AAAAAAAAA///////AAAAAAAAAf//////AAAAAAAAAf//////AAAAAAAAAf//////AAAAAAAAAP/////+AAAAAAAAAH/////+AAAAAAAAAD/////4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP4AAAAAAAAAAAAAAf8AAAAAAAAAAAAAB/+AAAAAAAAAAAAAD/+AAAAAAAAAAAAAP//AAAAAAAAAAAAAf//AAAAAAAAAAAAB///AAAAAAAAAAAAD///AAAAAAAAAAAAP///AAAAAAAAAAAAf///AAAAAAAAAAAB////AAAAAAAAAAAD////AAAAAAAAAAAP////AAAAAAAAAAA//+P/AAAAAAAAAAB//4H/AAAAAAAAAAH//wH/AAAAAAAAAAP//AH/AAAAAAAAAA//+AH/AAAAAAAAAB//4AH/AAAAAAAAAH//wAH/AAAAAAAAAP//AAH/AAAAAAAAAP/+AAH/AAA//4AAAf/4AAH/AAB//8AAAf/wAAH/AAD//+AAAf/AAAH/AAH///AAAf+AAAH/AAH///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAP/AAf///AAAf8AAAP/AAf///AAAf+AAAf/gB////AAAf////////////AAAf////////////AAAf////////////AAAf////////////AAAf////////////AAAf////////////AAAP////////////AAAH////////////AAAD////////////AAAAAAAAAAAD////AAAAAAAAAAAA////AAAAAAAAAAAAP///AAAAAAAAAAAAP//+AAAAAAAAAAAAD//8AAAAAAAAAAAAB//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/////wAAAAD8AAAf/////4AAAAH+AAAf/////8AAAAH+AAA//////8AAAAH+AAA//////+AAAAP+AAA//////+AAAAP+AAA//////+AAAAP+AAA//////+AAAAP+AAA//wAH/+AAAAP+AAA/+AAA/+AAAAP+AAA/8AAAf+AAAAP+AAA/4AAAP+AAAAP+AAA/4AAAP+AAAAP+AAA/4AAAP+AAAAP+AAA/4AAAP+AAAAP+AAA/4AAAP+AAAAP+AAA/4AAAP/AAAAf+AAA/4AAAP/gAAAf+AAA/4AAAP/wAAB/+AAA/4AAAP//////+AAA/4AAAP//////+AAA/4AAAP//////+AAA/4AAAP//////+AAA/4AAAP//////+AAA/4AAAP//////+AAA/4AAAP//////+AAA/4AAAP//////+AAA/4AAAP//////+AAA/4AAAP//////+AAA/4AAAP//////+AAA/4AAAP//////+AAA/4AAAP//////+AAA/4AAAP//////+AAA/4AAAP//////+AAA/4AAAP//////+AAA/4AAAP//////+AAA/4AAAP//////+AAA/wAAAH//////+AAA/wAAAH//////8AAAfwAAAD//////4AAAPAAAAA//////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH///////////8AAAP///////////+AAAf////////////AAAf////////////AAAf////////////AAAf////////////AAAf////////////AAAf////////////AAAf/AAA//4B////AAAf+AAAP/gAf///AAAf8AAAP/gAf///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAH/gAf///AAAf8AAAH/gAf///AAAf8AAAH/4B////AAAf8AAAH///////AAAf8AAAH///////AAAf8AAAH///////AAAf8AAAH///////AAAf8AAAH///////AAAf8AAAD///////AAAf4AAAD///////AAAf4AAAB///////AAAPwAAAA//////+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/+AAAAAAAAAAAAAf//AAAAAAAAAAAAA///gAAAAAAAAAAAA///gAAAAAAAAAAAA///gAAAAAAAAAAAA///gAAAAAAAAAAAA///gAAAAAAAAAAAA///gAAAAAAAAAAAA///gAAAAAAAAAAAA///gAAAAAAAAAAAA///gAAAAAAAAAAAA///gAAAAAAAAAAAA///gAAAAAAAAAAAA///gAAAAAAAAAAAA///gAAAAAAAAAAAA///gAAAH////4AAA///gAAAP////8AAA///gAAAf////+AAA///gAAA/////+AAA///gAAB/////+AAA///gAAD/////+AAA///gAAH/////+AAA///gAAP/////8AAA///gAAf/////4AAA///gAA//gAAAAAAA///gAB//AAAAAAAA///wAD/+AAAAAAAA///wAH/8AAAAAAAA///wAf/wAAAAAAAA///4A//gAAAAAAAA///8B//AAAAAAAAA//////+AAAAAAAAA//////8AAAAAAAAA//////4AAAAAAAAA//////wAAAAAAAAA//////gAAAAAAAAA//////AAAAAAAAAAf////+AAAAAAAAAAP////8AAAAAAAAAAH////4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/////8AAAAAAAAAf/////+AAAAAAAAA///////AAAAAAAAB///////AAAAAAAAD///////AAAAAAAAH///////AAAAAAAAP///////AAAAAAAA////////AAAAAAAD//4B////AAAD//////wA////AAAP//////gAf///AAAP//////AAP///AAAf//////AAP///AAAf//////AAP///AAAf//////AAP///AAAf//////AAP///AAAf//////AAP///AAAf//////AAP///AAAf/AAAf/AAP///AAAf+AAAP/AAP///AAAf8AAAP/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf+AAAP/AAP///AAAf/AAAf/AAP///AAAf/4AD//AAP///AAAf//////AAP///AAAf//////AAP///AAAf//////AAP///AAAf//////AAP///AAAf//////AAP///AAAP//////AAP///AAAP//////gAf///AAAD//////wA////AAAAf/////4B////AAAAAAAA////////AAAAAAAAP///////AAAAAAAAH///////AAAAAAAAD///////AAAAAAAAB///////AAAAAAAAA///////AAAAAAAAAf/////+AAAAAAAAAP/////8AAAAAAAAAH/////4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/////8AAH//+AAAf/////+AAP///AAAf/////+AAP///AAAf//////AAP///AAAf//////AAP///AAAf//////AAP///AAAf//////AAP///AAAf//////AAP///AAAf/AAA//AAP///AAAf+AAAP/AAP///AAAf8AAAP/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAH/AAP///AAAf8AAAP/gAf///AAAf+AAAP/gAf///AAAf/AAAf/wA////AAAf////////////AAAf////////////AAAf////////////AAAf////////////AAAf////////////AAAf////////////AAAP///////////+AAAH///////////+AAAD///////////4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH///AAD///gAAAAAH///gAH///gAAAAAP///gAH///gAAAAAP///gAH///gAAAAAP///gAH///gAAAAAP///gAH///gAAAAAP///gAH///gAAAAAP///gAH///gAAAAAP///gAH///gAAAAAP///gAH///gAAAAAP///gAH///gAAAAAP///gAH///gAAAAAP///gAH///gAAAAAP///gAH///gAAAAAP///gAH///gAAAAAP///gAH///gAAAAAP///gAH///gAAAAAH///AAH///gAAAAAH///AAD///AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); + var scale = 1; // size multiplier for this font + g.setFontCustom(font, 46, widths, 96+(scale<<8)+(1<<16)); +}; + +// timeout used to update every minute +var drawTimeout; + +// schedule a draw for the next minute +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +} + + +function draw() { + g.reset(); + // work out locale-friendly date/time + var date = new Date(); + var timeStr = require("locale").time(date,1); + var hh = timeStr.substr(0,2); + // Kludge to add leading zeros to hours - if recoding please implement leading zeros: + // Leading zeroes are an integral part of the design. + // If the hour is single digit the first character of the string will be + // space, in which case a zero is added. As there is no space in the font, + // spaces are ignored. + if (hh.substr(0,1) == ' ') { + hh = '0' + hh; + } + var mm = timeStr.substr(-3); + var dayName = require("locale").dow(new Date(), 1); + var longDateStr = date.getDate() + ' ' + require("locale").month(new Date(), 1); + // draw time + g.setFont("BDGemini"); + g.clearRect(0,24,175,175); // clear the background + g.drawString(hh,0,24); + g.drawString(mm,49,98); + // draw date + g.setFont("6x8",2); + g.drawString(dayName,106,35); + g.drawString(longDateStr,106,51); + g.drawString(date.getFullYear(),106,67); + // queue draw in one minte + queueDraw(); +} + +// Clear the screen once, at startup +g.clear(); +// draw immediately at first, queue update +draw(); +// Stop updates when LCD is off, restart when on +Bangle.on('lcdPower',on=>{ + if (on) { + draw(); // draw immediately, queue redraw + } else { // stop draw timer + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } +}); +// Show launcher when middle button pressed +Bangle.setUI("clock"); +// Load widgets +Bangle.loadWidgets(); +Bangle.drawWidgets(); \ No newline at end of file diff --git a/apps/geminiclock/metadata.json b/apps/geminiclock/metadata.json new file mode 100644 index 000000000..cf081ed79 --- /dev/null +++ b/apps/geminiclock/metadata.json @@ -0,0 +1,17 @@ +{ "id": "geminiclock", + "name": "Gemini clock", + "shortName":"Gemini Clock", + "icon": "app.png", + "version":"0.01", + "description": "Watch face using retro Gemini font", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "screenshots": [{"url":"screenshot.png"}], + "readme": "README.md", + "allow_emulator": true, + "storage": [ + {"name":"geminiclock.app.js","url":"gemini-watch-app.js"}, + {"name":"geminiclock.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/geminiclock/screenshot.png b/apps/geminiclock/screenshot.png new file mode 100644 index 000000000..44e882f7d Binary files /dev/null and b/apps/geminiclock/screenshot.png differ diff --git a/apps/geminiclock/watch-in-use.jpg b/apps/geminiclock/watch-in-use.jpg new file mode 100644 index 000000000..825a2b120 Binary files /dev/null and b/apps/geminiclock/watch-in-use.jpg differ diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index 3b0d62009..f913c9e58 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -63,3 +63,13 @@ * Record traveled distance to get a good average speed. * Breaks (low speed) will not count in average speed. * Bugfix in average speed. + +0.16: + * When lost indicates nearest point on path. + * Rescale display if lost and too far. + * New setting to hide points and increase display speed. + * Speed optimisations. + * Estimated time of Arrival/Going back. + * Display current and next segment in red so that you know where to go. + * Avoid angles flickering at low speed at the cost of less refresh. + * Splash screen while waiting for gps signal. diff --git a/apps/gipy/README.md b/apps/gipy/README.md index 6c9b87c23..4ca98dea8 100644 --- a/apps/gipy/README.md +++ b/apps/gipy/README.md @@ -2,12 +2,10 @@ Gipy allows you to follow gpx traces on your watch. -![Screenshot](screenshot1.png) +![Screenshot](splash.png) -It is for now meant for bicycling and not hiking -(it uses your movement to figure out your orientation -and walking is too slow). +It is mainly meant for bicycling but hiking might be fine. It is untested on Banglejs1. If you can try it, you would be welcome. @@ -20,10 +18,10 @@ It provides the following features : - display the path with current position from gps - detects and buzzes if you leave the path - buzzes before sharp turns -- buzzes before nodes with comments +- buzzes before waypoints (for example when you need to turn in https://mapstogpx.com/) - display instant / average speed -- display distance to next node +- display distance to next point - display additional data from openstreetmap : - water points - toilets @@ -54,32 +52,47 @@ Your path will be displayed in svg. ### Starting Gipy -Once you start gipy you will have a menu for selecting your trace (if more than one). -Choose the one you want and here you go : +At start you will have a menu for selecting your trace (if more than one). +Choose the one you want and you will reach the splash screen where you'll wait for the gps signal. +Once you have a signal you will reach the main screen: -![Screenshot](screenshot2.png) +![Screenshot](legend.png) -On your screen you can see : +On your screen you can see: - yourself (the big black dot) - the path (the top of the screen is in front of you) +- on the path, current and next segments are red and other ones are black - if needed a projection of yourself on the path (small black dot) -- extremities of segments as white dots -- turning points as doubled white dots -- some text on the left (from top to bottom) : +- points as white dots +- waypoints as doubled white dots +- some text on the left (from top to bottom): + * time to reach start point at current average speed * current time + * time to reach end point at current average speed * left distance till end of current segment - * distance from start of path / path length + * remaining distance / path length * average speed / instant speed - interest points from openstreetmap as color dots : - * red : bakery - * deep blue : water point - * cyan : toilets (often doubles as water point) - * green : artwork + * red: bakery + * deep blue: water point + * cyan: toilets (often doubles as water point) + * green: artwork - a *turn* indicator on the top right when you reach a turning point - a *gps* indicator (blinking) on the top right if you lose gps signal - a *lost* indicator on the top right if you stray too far away from path -- a black segment extending from you when you are lost, indicating the rough direction of where to go + +### Lost + +If you stray away from path we will rescale the display to continue displaying nearby segments and +display the direction to follow as a black segment. + +Note that while lost, the app will slow down a lot since it will start scanning all possible points to figure out where you +are. On path it just needed to scan a few points ahead and behind. + +![Lost](lost.png) + +The distance to next point displayed corresponds to the length of the black segment. ### Settings @@ -87,6 +100,7 @@ Few settings for now (feel free to suggest me more) : - keep gps alive : if turned off, will try to save battery by turning the gps off on long segments - max speed : used to compute how long to turn the gps off +- display points : display/hide points (not waypoints) ### Caveats diff --git a/apps/gipy/TODO b/apps/gipy/TODO index 53c3530e2..266a1c5c9 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -1,10 +1,5 @@ -* bugs - -- when exactly on turn, distance to next point is still often 50m - -----> it does not buzz very often on turns - -- when going backwards we have a tendencing to get a wrong current_segment ++ use Bangle.project(latlong) * additional features @@ -15,7 +10,6 @@ (and look at more than next point) - display distance to next water/toilet ? -- dynamic map rescale - display scale (100m) - compress path ? diff --git a/apps/gipy/app.js b/apps/gipy/app.js index ae82e5dfb..c9d018cac 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -6,10 +6,29 @@ var settings = Object.assign( { keep_gps_alive: true, max_speed: 35, + display_points: true, }, require("Storage").readJSON("gipy.json", true) || {} ); +let profile_start_times = []; + +let splashscreen = require("heatshrink").decompress( + atob( + "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" + ) +); + +function start_profiling() { + profile_start_times.push(getTime()); +} + +function end_profiling(label) { + let end_time = getTime(); + let elapsed = end_time - profile_start_times.pop(); + console.log("profile:", label, "took", elapsed); +} + let interests_colors = [ 0xf800, // Bakery, red 0x001f, // DrinkingWater, blue @@ -29,9 +48,29 @@ function binary_search(array, x) { return start; } +// return a string containing estimated time of arrival. +// speed is in km/h +// remaining distance in km +// hour, minutes is current time +function compute_eta(hour, minutes, approximate_speed, remaining_distance) { + if (isNaN(approximate_speed) || approximate_speed < 0.1) { + return ""; + } + let time_needed = (remaining_distance * 60) / approximate_speed; // in minutes + let eta_in_minutes = hour * 60 + minutes + time_needed; + let eta_minutes = Math.round(eta_in_minutes % 60); + let eta_hour = Math.round((eta_in_minutes - eta_minutes) / 60) % 24; + if (eta_minutes < 10) { + return eta_hour.toString() + ":0" + eta_minutes; + } else { + return eta_hour.toString() + ":" + eta_minutes; + } +} + class Status { constructor(path) { this.path = path; + this.scale_factor = 40000.0; // multiply geo coordinates by this to get pixels coordinates this.on_path = false; // are we on the path or lost ? this.position = null; // where we are this.adjusted_cos_direction = null; // cos of where we look at @@ -39,8 +78,7 @@ class Status { this.current_segment = null; // which segment is closest this.reaching = null; // which waypoint are we reaching ? this.distance_to_next_point = null; // how far are we from next point ? - this.paused_time = 0.0; // how long did we stop (stops don't count in avg speed) - this.paused_since = getTime(); + this.projected_point = null; let r = [0]; // let's do a reversed prefix computations on all distances: @@ -54,67 +92,51 @@ class Status { previous_point = point; } this.remaining_distances = r; // how much distance remains at start of each segment - this.starting_time = this.paused_since; // time we start + this.starting_time = null; // time we start this.advanced_distance = 0.0; this.gps_coordinates_counter = 0; // how many coordinates did we receive - this.old_points = []; - this.old_times = []; + this.old_points = []; // record previous points but only when enough distance between them + this.old_times = []; // the corresponding times } new_position_reached(position) { // we try to figure out direction by looking at previous points // instead of the gps course which is not very nice. - this.gps_coordinates_counter += 1; + let now = getTime(); + + if (this.old_points.length == 0) { + this.gps_coordinates_counter += 1; + this.old_points.push(position); + this.old_times.push(now); + return null; + } else { + let previous_point = this.old_points[this.old_points.length - 1]; + let distance_to_previous = previous_point.distance(position); + // gps signal is noisy but rarely above 4 meters + if (distance_to_previous < 4) { + return null; + } + } + this.gps_coordinates_counter += 1; this.old_points.push(position); this.old_times.push(now); - if (this.old_points.length == 1) { - return null; - } - - let last_point = this.old_points[this.old_points.length - 1]; let oldest_point = this.old_points[0]; + let distance_to_oldest = oldest_point.distance(position); - // every 7 points we count the distance - if (this.gps_coordinates_counter % 7 == 0) { - let distance = last_point.distance(oldest_point); - if (distance < 150.0) { + // every 3 points we count the distance + if (this.gps_coordinates_counter % 3 == 0) { + if (distance_to_oldest < 150.0) { // to avoid gps glitches - this.advanced_distance += distance; + this.advanced_distance += distance_to_oldest; } } - if (this.old_points.length == 8) { - let p1 = this.old_points[0] - .plus(this.old_points[1]) - .plus(this.old_points[2]) - .plus(this.old_points[3]) - .times(1 / 4); - let p2 = this.old_points[4] - .plus(this.old_points[5]) - .plus(this.old_points[6]) - .plus(this.old_points[7]) - .times(1 / 4); - let t1 = (this.old_times[1] + this.old_times[2]) / 2; - let t2 = (this.old_times[5] + this.old_times[6]) / 2; - this.instant_speed = p1.distance(p2) / (t2 - t1); + this.instant_speed = distance_to_oldest / (now - this.old_times[0]); + + if (this.old_points.length == 4) { this.old_points.shift(); this.old_times.shift(); - } else { - this.instant_speed = - oldest_point.distance(last_point) / (now - this.old_times[0]); - - // update paused time if we are too slow - if (this.instant_speed < 2) { - if (this.paused_since === null) { - this.paused_since = now; - } - } else { - if (this.paused_since !== null) { - this.paused_time += now - this.paused_since; - this.paused_since = null; - } - } } // let's just take angle of segment between newest point and a point a bit before let previous_index = this.old_points.length - 3; @@ -153,6 +175,7 @@ class Status { let next_segment = res[1]; if (this.is_lost(next_segment)) { + // start_profiling(); // it did not work, try anywhere res = this.path.nearest_segment( this.position, @@ -163,6 +186,7 @@ class Status { ); orientation = res[0]; next_segment = res[1]; + // end_profiling("repositioning"); } // now check if we strayed away from path or back to it let lost = this.is_lost(next_segment); @@ -223,16 +247,30 @@ class Status { return this.remaining_distances[0] - remaining_in_correct_orientation; } } + // check if we are lost (too far from segment we think we are on) + // if we are adjust scale so that path will still be displayed. + // we do the scale adjustment here to avoid recomputations later on. is_lost(segment) { - let distance_to_nearest = this.position.distance_to_segment( + let projection = this.position.closest_segment_point( this.path.point(segment), this.path.point(segment + 1) ); - return distance_to_nearest > 50; + this.projected_point = projection; // save this info for display + let distance_to_projection = this.position.distance(projection); + if (distance_to_projection > 50) { + this.scale_factor = + Math.min(88.0 / distance_to_projection, 1.0) * 40000.0; + return true; + } else { + this.scale_factor = 40000.0; + return false; + } } display(orientation) { g.clear(); + // start_profiling(); this.display_map(); + // end_profiling("display_map"); this.display_interest_points(); this.display_stats(orientation); @@ -265,14 +303,17 @@ class Status { let c = interest_point.coordinates( this.position, this.adjusted_cos_direction, - this.adjusted_sin_direction + this.adjusted_sin_direction, + this.scale_factor ); g.setColor(color).fillCircle(c[0], c[1], 5); } } display_stats(orientation) { - let remaining_distance = this.remaining_distance(orientation); - let rounded_distance = Math.round(remaining_distance / 100) / 10; + let remaining_forward_distance = this.remaining_distance(0); + let remaining_backward_distance = this.remaining_distance(1); + let rounded_forward_distance = + Math.round(remaining_forward_distance / 100) / 10; let total = Math.round(this.remaining_distances[0] / 100) / 10; let now = new Date(); let minutes = now.getMinutes().toString(); @@ -280,24 +321,53 @@ class Status { minutes = "0" + minutes; } let hours = now.getHours().toString(); + // now, distance to next point in meters g.setFont("6x8:2") - .setFontAlign(-1, -1, 0) .setColor(g.theme.fg) - .drawString(hours + ":" + minutes, 0, 30); - - g.setFont("6x8:2").drawString( - "" + this.distance_to_next_point + "m", - 0, - g.getHeight() - 49 - ); + .drawString( + "" + this.distance_to_next_point + "m", + 0, + g.getHeight() - 49 + ); let point_time = this.old_times[this.old_times.length - 1]; - let done_in = point_time - this.starting_time - this.paused_time; + let done_in = point_time - this.starting_time; let approximate_speed = Math.round( (this.advanced_distance * 3.6) / done_in ); - let approximate_instant_speed = Math.round(this.instant_speed * 3.6); + let forward_eta = compute_eta( + now.getHours(), + now.getMinutes(), + approximate_speed, + remaining_forward_distance / 1000 + ); + + let backward_eta = compute_eta( + now.getHours(), + now.getMinutes(), + approximate_speed, + remaining_backward_distance / 1000 + ); + + // display backward ETA + g.setFont("6x8:2") + .setFontAlign(-1, -1, 0) + .setColor(g.theme.fg) + .drawString(backward_eta, 0, 30); + // display the clock + g.setFont("6x8:2") + .setFontAlign(-1, -1, 0) + .setColor(g.theme.fg) + .drawString(hours + ":" + minutes, 0, 48); + // now display ETA + g.setFont("6x8:2") + .setFontAlign(-1, -1, 0) + .setColor(g.theme.fg) + .drawString(forward_eta, 0, 66); + + // display speed (avg and instant) + let approximate_instant_speed = Math.round(this.instant_speed * 3.6); g.setFont("6x8:2") .setFontAlign(-1, -1, 0) .drawString( @@ -306,12 +376,14 @@ class Status { g.getHeight() - 15 ); + // display distance on path g.setFont("6x8:2").drawString( - "" + rounded_distance + "/" + total, + "" + rounded_forward_distance + "/" + total, 0, g.getHeight() - 32 ); + // display various indicators if (this.distance_to_next_point <= 100) { if (this.path.is_waypoint(this.reaching)) { g.setColor(0.0, 1.0, 0.0) @@ -343,19 +415,44 @@ class Status { let half_height = g.getHeight() / 2; let previous_x = null; let previous_y = null; + let scale_factor = this.scale_factor; + + // display direction to next point if lost + if (!this.on_path) { + let next_point = this.path.point(this.current_segment + 1); + let previous_point = this.path.point(this.current_segment); + let nearest_point; + if ( + previous_point.fake_distance(this.position) < + next_point.fake_distance(this.position) + ) { + nearest_point = previous_point; + } else { + nearest_point = next_point; + } + let tx = (nearest_point.lon - cx) * scale_factor; + let ty = (nearest_point.lat - cy) * scale_factor; + let rotated_x = tx * cos - ty * sin; + let rotated_y = tx * sin + ty * cos; + let x = half_width - Math.round(rotated_x); // x is inverted + let y = half_height + Math.round(rotated_y); + g.setColor(g.theme.fgH).drawLine(half_width, half_height, x, y); + } + + // now display path for (let i = start; i < end; i++) { - let tx = (points[2 * i] - cx) * 40000.0; - let ty = (points[2 * i + 1] - cy) * 40000.0; + let tx = (points[2 * i] - cx) * scale_factor; + let ty = (points[2 * i + 1] - cy) * scale_factor; let rotated_x = tx * cos - ty * sin; let rotated_y = tx * sin + ty * cos; let x = half_width - Math.round(rotated_x); // x is inverted let y = half_height + Math.round(rotated_y); if (previous_x !== null) { - if (i == this.current_segment + 1) { - g.setColor(0.0, 1.0, 0.0); - } else { - g.setColor(1.0, 0.0, 0.0); + let segment_color = g.theme.fg; + if (i == this.current_segment + 1 || i == this.current_segment + 2) { + segment_color = 0xf800; } + g.setColor(segment_color); g.drawLine(previous_x, previous_y, x, y); if (this.path.is_waypoint(i - 1)) { @@ -364,10 +461,12 @@ class Status { g.setColor(g.theme.bg); g.fillCircle(previous_x, previous_y, 5); } - g.setColor(g.theme.fg); - g.fillCircle(previous_x, previous_y, 4); - g.setColor(g.theme.bg); - g.fillCircle(previous_x, previous_y, 3); + if (settings.display_points) { + g.setColor(g.theme.fg); + g.fillCircle(previous_x, previous_y, 4); + g.setColor(g.theme.bg); + g.fillCircle(previous_x, previous_y, 3); + } } previous_x = x; @@ -389,51 +488,21 @@ class Status { g.setColor(g.theme.fgH); g.fillCircle(half_width, half_height, 5); - // display old points for direction debug - // for (let i = 0; i < this.old_points.length; i++) { - // let tx = (this.old_points[i].lon - cx) * 40000.0; - // let ty = (this.old_points[i].lat - cy) * 40000.0; - // let rotated_x = tx * cos - ty * sin; - // let rotated_y = tx * sin + ty * cos; - // let x = half_width - Math.round(rotated_x); // x is inverted - // let y = half_height + Math.round(rotated_y); - // g.setColor((i + 1) / 4.0, 0.0, 0.0); - // g.fillCircle(x, y, 3); - // } - - // display current-segment's projection for debug - let projection = pos.closest_segment_point( - this.path.point(this.current_segment), - this.path.point(this.current_segment + 1) - ); - - let tx = (projection.lon - cx) * 40000.0; - let ty = (projection.lat - cy) * 40000.0; + // display current-segment's projection + let tx = (this.projected_point.lon - cx) * scale_factor; + let ty = (this.projected_point.lat - cy) * scale_factor; let rotated_x = tx * cos - ty * sin; let rotated_y = tx * sin + ty * cos; let x = half_width - Math.round(rotated_x); // x is inverted let y = half_height + Math.round(rotated_y); - g.setColor(g.theme.fg); + g.setColor(g.theme.fgH); g.fillCircle(x, y, 4); - - // display direction to next point if lost - if (!this.on_path) { - let next_point = this.path.point(this.current_segment + 1); - let diff = next_point.minus(this.position); - let angle = Math.atan2(diff.lat, diff.lon); - let tx = Math.cos(angle) * 50.0; - let ty = Math.sin(angle) * 50.0; - let rotated_x = tx * cos - ty * sin; - let rotated_y = tx * sin + ty * cos; - let x = half_width - Math.round(rotated_x); // x is inverted - let y = half_height + Math.round(rotated_y); - g.setColor(g.theme.fgH).drawLine(half_width, half_height, x, y); - } } } function load_gpc(filename) { let buffer = require("Storage").readArrayBuffer(filename); + let file_size = buffer.length; let offset = 0; // header @@ -475,7 +544,7 @@ function load_gpc(filename) { let interests_starts = Uint16Array(buffer, offset, starts_length); offset += 2 * starts_length; - return [ + let path_data = [ points, waypoints, interests_coordinates, @@ -483,6 +552,18 @@ function load_gpc(filename) { interests_on_path, interests_starts, ]; + + // checksum file size + if (offset != file_size) { + console.log("invalid file size", file_size, "expected", offset); + let msg = "invalid file\nsize " + file_size + "\ninstead of" + offset; + E.showAlert(msg).then(function () { + E.showAlert(); + start_gipy(filename, path_data); + }); + } else { + start_gipy(filename, path_data); + } } class Path { @@ -502,20 +583,6 @@ class Path { return r != 0; } - // execute op on all segments. - // start is index of first wanted segment - // end is 1 after index of last wanted segment - on_segments(op, start, end) { - let previous_point = null; - for (let i = start; i < end + 1; i++) { - let point = new Point(this.points[2 * i], this.points[2 * i + 1]); - if (previous_point !== null) { - op(previous_point, point, i); - } - previous_point = point; - } - } - // return point at given index point(index) { let lon = this.points[2 * index]; @@ -541,39 +608,28 @@ class Path { // we are going to compute two min distances, one for each direction. let indices = [0, 0]; let mins = [Number.MAX_VALUE, Number.MAX_VALUE]; - this.on_segments( - function (p1, p2, i) { - // we use the dot product to figure out if oriented correctly - // let distance = point.fake_distance_to_segment(p1, p2); - let projection = point.closest_segment_point(p1, p2); - let distance = point.fake_distance(projection); + let p1 = new Point(this.points[2 * start], this.points[2 * start + 1]); + for (let i = start + 1; i < end + 1; i++) { + let p2 = new Point(this.points[2 * i], this.points[2 * i + 1]); + + let closest_point = point.closest_segment_point(p1, p2); + let distance = point.length_squared(closest_point); + + let dot = + cos_direction * (p2.lon - p1.lon) + sin_direction * (p2.lat - p1.lat); + let orientation = +(dot < 0); // index 0 is good orientation + if (distance <= mins[orientation]) { + mins[orientation] = distance; + indices[orientation] = i - 1; + } + + p1 = p2; + } - // let d = projection.minus(point).times(40000.0); - // let rotated_x = d.lon * acos - d.lat * asin; - // let rotated_y = d.lon * asin + d.lat * acos; - // let x = g.getWidth() / 2 - Math.round(rotated_x); // x is inverted - // let y = g.getHeight() / 2 + Math.round(rotated_y); - // - let diff = p2.minus(p1); - let dot = cos_direction * diff.lon + sin_direction * diff.lat; - let orientation = +(dot < 0); // index 0 is good orientation - // g.setColor(0.0, 0.0 + orientation, 1.0 - orientation).fillCircle( - // x, - // y, - // 10 - // ); - if (distance <= mins[orientation]) { - mins[orientation] = distance; - indices[orientation] = i - 1; - } - }, - start, - end - ); // by default correct orientation (0) wins // but if other one is really closer, return other one - if (mins[1] < mins[0] / 10.0) { + if (mins[1] < mins[0] / 100.0) { return [1, indices[1]]; } else { return [0, indices[0]]; @@ -589,8 +645,8 @@ class Point { this.lon = lon; this.lat = lat; } - coordinates(current_position, cos_direction, sin_direction) { - let translated = this.minus(current_position).times(40000.0); + coordinates(current_position, cos_direction, sin_direction, scale_factor) { + let translated = this.minus(current_position).times(scale_factor); let rotated_x = translated.lon * cos_direction - translated.lat * sin_direction; let rotated_y = @@ -609,8 +665,9 @@ class Point { return new Point(this.lon + other_point.lon, this.lat + other_point.lat); } length_squared(other_point) { - let d = this.minus(other_point); - return d.lon * d.lon + d.lat * d.lat; + let londiff = this.lon - other_point.lon; + let latdiff = this.lat - other_point.lat; + return londiff * londiff + latdiff * latdiff; } times(scalar) { return new Point(this.lon * scalar, this.lat * scalar); @@ -639,10 +696,15 @@ class Point { fake_distance(other_point) { return Math.sqrt(this.length_squared(other_point)); } + // return closest point from 'this' on [v,w] segment. + // since this function is critical we inline all code here. closest_segment_point(v, w) { // from : https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment // Return minimum distance between line segment vw and point p - let l2 = v.length_squared(w); // i.e. |w-v|^2 - avoid a sqrt + let segment_londiff = w.lon - v.lon; + let segment_latdiff = w.lat - v.lat; + let l2 = + segment_londiff * segment_londiff + segment_latdiff * segment_latdiff; // i.e. |w-v|^2 - avoid a sqrt if (l2 == 0.0) { return v; // v == w case } @@ -650,41 +712,48 @@ class Point { // We find projection of point p onto the line. // It falls where t = [(p-v) . (w-v)] / |w-v|^2 // We clamp t from [0,1] to handle points outside the segment vw. - let t = Math.max(0, Math.min(1, this.minus(v).dot(w.minus(v)) / l2)); - return v.plus(w.minus(v).times(t)); // Projection falls on the segment - } - distance_to_segment(v, w) { - let projection = this.closest_segment_point(v, w); - return this.distance(projection); - } - fake_distance_to_segment(v, w) { - let projection = this.closest_segment_point(v, w); - return this.fake_distance(projection); + + // let t = Math.max(0, Math.min(1, this.minus(v).dot(w.minus(v)) / l2)); //inlined below + let start_londiff = this.lon - v.lon; + let start_latdiff = this.lat - v.lat; + let t = + (start_londiff * segment_londiff + start_latdiff * segment_latdiff) / l2; + if (t < 0) { + t = 0; + } else { + if (t > 1) { + t = 1; + } + } + let lon = v.lon + segment_londiff * t; + let lat = v.lat + segment_latdiff * t; + return new Point(lon, lat); } } -Bangle.loadWidgets(); - let fake_gps_point = 0.0; function simulate_gps(status) { + // let's keep the screen on in simulations + Bangle.setLCDTimeout(0); + Bangle.setLCDPower(1); if (fake_gps_point > status.path.len - 1) { return; } let point_index = Math.floor(fake_gps_point); - if (point_index >= status.path.len) { + if (point_index >= status.path.len / 2 - 1) { return; } - //let p1 = status.path.point(0); - //let n = status.path.len; - //let p2 = status.path.point(n - 1); - let p1 = status.path.point(point_index); - let p2 = status.path.point(point_index + 1); + let p1 = status.path.point(2 * point_index); // use these to approximately follow path + let p2 = status.path.point(2 * (point_index + 1)); + //let p1 = status.path.point(point_index); // use these to strictly follow path + //let p2 = status.path.point(point_index + 1); let alpha = fake_gps_point - point_index; let pos = p1.times(1 - alpha).plus(p2.times(alpha)); let old_pos = status.position; fake_gps_point += 0.05; // advance simulation + // status.update_position(new Point(1, 1), null); // uncomment to be always lost status.update_position(pos, null); } @@ -706,21 +775,28 @@ function start(fn) { E.showMenu(); console.log("loading", fn); - // let path = new Path(load_gpx("test.gpx")); - let path = new Path(load_gpc(fn)); + load_gpc(fn); +} + +function start_gipy(filename, path_data) { + console.log("starting"); + let path = new Path(path_data); let status = new Status(path); if (simulated) { + status.starting_time = getTime(); status.position = new Point(status.path.point(0)); setInterval(simulate_gps, 500, status); } else { - // let's display start while waiting for gps signal - let p1 = status.path.point(0); - let p2 = status.path.point(1); - let diff = p2.minus(p1); - let direction = Math.atan2(diff.lat, diff.lon); + // let's display splash screen while waiting for gps signal + g.clear(); + g.drawImage(splashscreen, 0, 0); + g.setFont("6x8:2") + .setFontAlign(-1, -1, 0) + .setColor(0xf800) + .drawString(filename, 0, g.getHeight() - 30); + Bangle.setLocked(false); - status.update_position(p1, direction); let frame = 0; let set_coordinates = function (data) { @@ -731,6 +807,10 @@ function start(fn) { !isNaN(data.lon) && (data.lat != 0.0 || data.lon != 0.0); if (valid_coordinates) { + if (status.starting_time === null) { + status.starting_time = getTime(); + Bangle.loadWidgets(); // i don't know why i cannot load them at start : they would display on splash screen + } status.update_position(new Point(data.lon, data.lat), null); } let gps_status_color; diff --git a/apps/gipy/interface.html b/apps/gipy/interface.html index a1c405ed7..552e7be17 100644 --- a/apps/gipy/interface.html +++ b/apps/gipy/interface.html @@ -182,12 +182,21 @@ document document .getElementById("upload") .addEventListener('click', function() { + document.getElementById('upload').disabled = true; status.innerHTML = "uploading file"; console.log("uploading"); let gpc_string = vec_to_string(gpc_content); Util.writeStorage(gpc_filename + ".gpc", gpc_string, () => { - status.innerHTML = `${gpc_filename}.gpc uploaded`; - console.log("DONE"); + status.innerHTML = "Checking upload"; + Util.readStorage(gpc_filename + ".gpc", uploaded_content => { + if (uploaded_content == gpc_string) { + status.innerHTML = `${gpc_filename}.gpc uploaded`; + console.log("DONE"); + } else { + status.innerHTML = "Upload FAILED"; + document.getElementById('upload').disabled = false; + } + }); }); }); diff --git a/apps/gipy/legend.png b/apps/gipy/legend.png new file mode 100644 index 000000000..9040f6df8 Binary files /dev/null and b/apps/gipy/legend.png differ diff --git a/apps/gipy/lost.png b/apps/gipy/lost.png new file mode 100644 index 000000000..348eaed8e Binary files /dev/null and b/apps/gipy/lost.png differ diff --git a/apps/gipy/metadata.json b/apps/gipy/metadata.json index 2d06a7c2d..97d18f5fe 100644 --- a/apps/gipy/metadata.json +++ b/apps/gipy/metadata.json @@ -2,13 +2,13 @@ "id": "gipy", "name": "Gipy", "shortName": "Gipy", - "version": "0.15", - "description": "Follow gpx files", + "version": "0.16", + "description": "Follow gpx files using the gps. Don't get lost in your bike trips and hikes.", "allow_emulator":false, "icon": "gipy.png", "type": "app", "tags": "tool,outdoors,gps", - "screenshots": [], + "screenshots": [{"url":"splash.png"}], "supports": ["BANGLEJS2"], "readme": "README.md", "interface": "interface.html", diff --git a/apps/gipy/pkg/gpconv.d.ts b/apps/gipy/pkg/gpconv.d.ts index ecffa7b69..2bb57f651 100644 --- a/apps/gipy/pkg/gpconv.d.ts +++ b/apps/gipy/pkg/gpconv.d.ts @@ -46,11 +46,11 @@ export interface InitOutput { readonly __wbindgen_malloc: (a: number) => number; readonly __wbindgen_realloc: (a: number, b: number, c: number) => number; readonly __wbindgen_export_2: WebAssembly.Table; - readonly _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h0601691a32604cdd: (a: number, b: number, c: number) => void; + readonly _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h317df853f2d4653e: (a: number, b: number, c: number) => void; readonly __wbindgen_add_to_stack_pointer: (a: number) => number; readonly __wbindgen_free: (a: number, b: number) => void; readonly __wbindgen_exn_store: (a: number) => void; - readonly wasm_bindgen__convert__closures__invoke2_mut__h25ed812378167476: (a: number, b: number, c: number, d: number) => void; + readonly wasm_bindgen__convert__closures__invoke2_mut__h573cb80e0bf72240: (a: number, b: number, c: number, d: number) => void; } export type SyncInitInput = BufferSource | WebAssembly.Module; diff --git a/apps/gipy/pkg/gpconv.js b/apps/gipy/pkg/gpconv.js index 97b37e340..b9271ad4b 100644 --- a/apps/gipy/pkg/gpconv.js +++ b/apps/gipy/pkg/gpconv.js @@ -98,14 +98,6 @@ function getInt32Memory0() { return cachedInt32Memory0; } -const cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); - -cachedTextDecoder.decode(); - -function getStringFromWasm0(ptr, len) { - return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); -} - function addHeapObject(obj) { if (heap_next === heap.length) heap.push(heap.length + 1); const idx = heap_next; @@ -115,6 +107,14 @@ function addHeapObject(obj) { return idx; } +const cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); + +cachedTextDecoder.decode(); + +function getStringFromWasm0(ptr, len) { + return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); +} + function debugString(val) { // primitive types const type = typeof val; @@ -205,7 +205,7 @@ function makeMutClosure(arg0, arg1, dtor, f) { return real; } function __wbg_adapter_24(arg0, arg1, arg2) { - wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h0601691a32604cdd(arg0, arg1, addHeapObject(arg2)); + wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h317df853f2d4653e(arg0, arg1, addHeapObject(arg2)); } function _assertClass(instance, klass) { @@ -310,7 +310,7 @@ function handleError(f, args) { } } function __wbg_adapter_69(arg0, arg1, arg2, arg3) { - wasm.wasm_bindgen__convert__closures__invoke2_mut__h25ed812378167476(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3)); + wasm.wasm_bindgen__convert__closures__invoke2_mut__h573cb80e0bf72240(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3)); } /** @@ -371,13 +371,13 @@ async function load(module, imports) { function getImports() { const imports = {}; imports.wbg = {}; - imports.wbg.__wbindgen_object_drop_ref = function(arg0) { - takeObject(arg0); - }; imports.wbg.__wbg_gpcsvg_new = function(arg0) { const ret = GpcSvg.__wrap(arg0); return addHeapObject(ret); }; + imports.wbg.__wbindgen_object_drop_ref = function(arg0) { + takeObject(arg0); + }; imports.wbg.__wbindgen_string_get = function(arg0, arg1) { const obj = getObject(arg1); const ret = typeof(obj) === 'string' ? obj : undefined; @@ -386,15 +386,15 @@ function getImports() { getInt32Memory0()[arg0 / 4 + 1] = len0; getInt32Memory0()[arg0 / 4 + 0] = ptr0; }; - imports.wbg.__wbindgen_string_new = function(arg0, arg1) { - const ret = getStringFromWasm0(arg0, arg1); - return addHeapObject(ret); - }; imports.wbg.__wbindgen_object_clone_ref = function(arg0) { const ret = getObject(arg0); return addHeapObject(ret); }; - imports.wbg.__wbg_fetch_386f87a3ebf5003c = function(arg0) { + imports.wbg.__wbindgen_string_new = function(arg0, arg1) { + const ret = getStringFromWasm0(arg0, arg1); + return addHeapObject(ret); + }; + imports.wbg.__wbg_fetch_3894579f6e2af3be = function(arg0) { const ret = fetch(getObject(arg0)); return addHeapObject(ret); }; @@ -558,10 +558,6 @@ function getImports() { const ret = new Uint8Array(getObject(arg0)); return addHeapObject(ret); }; - imports.wbg.__wbg_stringify_d6471d300ded9b68 = function() { return handleError(function (arg0) { - const ret = JSON.stringify(getObject(arg0)); - return addHeapObject(ret); - }, arguments) }; imports.wbg.__wbg_get_765201544a2b6869 = function() { return handleError(function (arg0, arg1) { const ret = Reflect.get(getObject(arg0), getObject(arg1)); return addHeapObject(ret); @@ -574,6 +570,10 @@ function getImports() { const ret = Reflect.set(getObject(arg0), getObject(arg1), getObject(arg2)); return ret; }, arguments) }; + imports.wbg.__wbg_stringify_d6471d300ded9b68 = function() { return handleError(function (arg0) { + const ret = JSON.stringify(getObject(arg0)); + return addHeapObject(ret); + }, arguments) }; imports.wbg.__wbindgen_debug_string = function(arg0, arg1) { const ret = debugString(getObject(arg1)); const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); @@ -588,8 +588,8 @@ function getImports() { const ret = wasm.memory; return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper947 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 147, __wbg_adapter_24); + imports.wbg.__wbindgen_closure_wrapper929 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 143, __wbg_adapter_24); return addHeapObject(ret); }; diff --git a/apps/gipy/pkg/gpconv_bg.wasm b/apps/gipy/pkg/gpconv_bg.wasm index edeb4eb59..245afc20c 100644 Binary files a/apps/gipy/pkg/gpconv_bg.wasm and b/apps/gipy/pkg/gpconv_bg.wasm differ diff --git a/apps/gipy/pkg/gpconv_bg.wasm.d.ts b/apps/gipy/pkg/gpconv_bg.wasm.d.ts index 6bc5d3719..cb912f3de 100644 --- a/apps/gipy/pkg/gpconv_bg.wasm.d.ts +++ b/apps/gipy/pkg/gpconv_bg.wasm.d.ts @@ -9,8 +9,8 @@ export function convert_gpx_strings(a: number, b: number, c: number, d: number, export function __wbindgen_malloc(a: number): number; export function __wbindgen_realloc(a: number, b: number, c: number): number; export const __wbindgen_export_2: WebAssembly.Table; -export function _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h0601691a32604cdd(a: number, b: number, c: number): void; +export function _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h317df853f2d4653e(a: number, b: number, c: number): void; export function __wbindgen_add_to_stack_pointer(a: number): number; export function __wbindgen_free(a: number, b: number): void; export function __wbindgen_exn_store(a: number): void; -export function wasm_bindgen__convert__closures__invoke2_mut__h25ed812378167476(a: number, b: number, c: number, d: number): void; +export function wasm_bindgen__convert__closures__invoke2_mut__h573cb80e0bf72240(a: number, b: number, c: number, d: number): void; diff --git a/apps/gipy/screenshot1.png b/apps/gipy/screenshot1.png deleted file mode 100644 index c7c45fa3b..000000000 Binary files a/apps/gipy/screenshot1.png and /dev/null differ diff --git a/apps/gipy/screenshot2.png b/apps/gipy/screenshot2.png deleted file mode 100644 index ed61eb795..000000000 Binary files a/apps/gipy/screenshot2.png and /dev/null differ diff --git a/apps/gipy/splash.png b/apps/gipy/splash.png new file mode 100644 index 000000000..56d4de06b Binary files /dev/null and b/apps/gipy/splash.png differ diff --git a/apps/gpsinfo/ChangeLog b/apps/gpsinfo/ChangeLog index 5bb531bc7..827c13cdb 100644 --- a/apps/gpsinfo/ChangeLog +++ b/apps/gpsinfo/ChangeLog @@ -7,3 +7,4 @@ 0.08: Leave GPS power switched on on exit (will switch off after 0.5 seconds anyway) 0.09: Fix FIFO_FULL error 0.10: Show satellites "in view" separated by GNS-system +0.11: Show number of packets received diff --git a/apps/gpsinfo/gps-info.js b/apps/gpsinfo/gps-info.js index a6e21af0d..28cb60d8d 100644 --- a/apps/gpsinfo/gps-info.js +++ b/apps/gpsinfo/gps-info.js @@ -5,7 +5,7 @@ function satelliteImage() { var Layout = require("Layout"); var layout; //Bangle.setGPSPower(1, "app"); -E.showMessage(/*LANG*/"Loading..."); // avoid showing rubbish on screen +E.showMessage(/*LANG*/"Waiting for GNS data..."); // avoid showing rubbish on screen var lastFix = { fix: -1, @@ -19,6 +19,7 @@ var lastFix = { var SATinView = 0, lastSATinView = -1, nofGP = 0, nofBD = 0, nofGL = 0; const leaveNofixLayout = 1; // 0 = stay on initial screen for debugging (default = 1) var listenerGPSraw = 0; +var dataCounter = 0; function formatTime(now) { if (now == undefined) { @@ -80,11 +81,15 @@ function onGPS(fix) { type:"v", c: [ {type:"txt", font:"6x8:2", label:"GPS Info" }, {type:"img", src:satelliteImage, pad:4 }, - {type:"txt", font:"6x8", label:"Waiting for GPS" }, + {type:"txt", font:"6x8", label:"Waiting for GPS fix" }, {type:"h", c: [ {type:"txt", font:"10%", label:fix.satellites, pad:2, id:"sat" }, {type:"txt", font:"6x8", pad:3, label:"Satellites used" } ]}, + {type:"h", c: [ + {type:"txt", font:"10%", label:dataCounter, pad:2, id:"dataCounter" }, + {type:"txt", font:"6x8", pad:3, label:"packets received" } + ]}, {type:"txt", font:"6x8", label:"", fillx:true, id:"progress" } ]},{lazy:false}); } @@ -122,6 +127,9 @@ function onGPS(fix) { layout.progress.label = "in view GP/BD/GL: " + nofGP + " " + nofBD + " " + nofGL; // console.log("in view GP/BD/GL: " + nofGP + " " + nofBD + " " + nofGL); layout.render(layout.progress); + layout.clear(layout.dataCounter); + layout.dataCounter.label = ++dataCounter; + layout.render(layout.dataCounter); } } diff --git a/apps/gpsinfo/metadata.json b/apps/gpsinfo/metadata.json index 002febd86..e426f5740 100644 --- a/apps/gpsinfo/metadata.json +++ b/apps/gpsinfo/metadata.json @@ -1,11 +1,11 @@ { "id": "gpsinfo", "name": "GPS Info", - "version": "0.10", - "description": "An application that displays information about altitude, lat/lon, satellites and time", + "version": "0.11", + "description": "An application that displays information about latitude, longitude, altitude, speed, satellites and time", "icon": "gps-info.png", "type": "app", - "tags": "gps,outdoors", + "tags": "gps,outdoors,tools", "supports": ["BANGLEJS","BANGLEJS2"], "storage": [ {"name":"gpsinfo.app.js","url":"gps-info.js"}, diff --git a/apps/grocery/ChangeLog b/apps/grocery/ChangeLog index 906046782..294dab597 100644 --- a/apps/grocery/ChangeLog +++ b/apps/grocery/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App! 0.02: Refactor code to store grocery list in separate file +0.03: Sort selected items to bottom and enable Widgets diff --git a/apps/grocery/app.js b/apps/grocery/app.js index 481efc3d9..a68f53010 100644 --- a/apps/grocery/app.js +++ b/apps/grocery/app.js @@ -1,5 +1,6 @@ var filename = 'grocery_list.json'; var settings = require("Storage").readJSON(filename,1)|| { products: [] }; +let menu; function updateSettings() { require("Storage").writeJSON(filename, settings); @@ -11,19 +12,32 @@ function twoChat(n){ return ''+n; } -const mainMenu = settings.products.reduce(function(m, p, i){ - const name = twoChat(p.quantity)+' '+p.name; - m[name] = { - value: p.ok, - format: v => v?'[x]':'[ ]', - onchange: v => { - settings.products[i].ok = v; - updateSettings(); - } - }; - return m; -}, { - '': { 'title': 'Grocery list' } -}); +function sortMenu() { + mainMenu.sort((a,b) => { + const byValue = a.value-b.value; + return byValue !== 0 ? byValue : a.index-b.index; + }); + if (menu) { + menu.draw(); + } +} + +const mainMenu = settings.products.map((p,i) => ({ + title: twoChat(p.quantity)+' '+p.name, + value: p.ok, + format: v => v?'[x]':'[ ]', + index: i, + onchange: v => { + settings.products[i].ok = v; + updateSettings(); + sortMenu(); + } +})); +sortMenu(); + +mainMenu[''] = { 'title': 'Grocery list' }; mainMenu['< Back'] = ()=>{load();}; -E.showMenu(mainMenu); + +Bangle.loadWidgets(); +menu = E.showMenu(mainMenu); +Bangle.drawWidgets(); diff --git a/apps/grocery/interface.html b/apps/grocery/interface.html new file mode 100644 index 000000000..65528c8e6 --- /dev/null +++ b/apps/grocery/interface.html @@ -0,0 +1,138 @@ + + + + + + +

List of products

+ + + + + + + + + + + + +
namequantitydoneactions
+

+

Add a new product

+
+
+
+ +
+
+ +
+
+ +
+
+
+

+ + + + + + + + + + diff --git a/apps/grocery/metadata.json b/apps/grocery/metadata.json index 8c0e34dff..ef073a1b2 100644 --- a/apps/grocery/metadata.json +++ b/apps/grocery/metadata.json @@ -1,13 +1,14 @@ { "id": "grocery", "name": "Grocery", - "version": "0.02", + "version": "0.03", "description": "Simple grocery (shopping) list - Display a list of product and track if you already put them in your cart.", "icon": "grocery.png", "type": "app", "tags": "tool,outdoors,shopping,list", "supports": ["BANGLEJS", "BANGLEJS2"], "custom": "grocery.html", + "interface": "interface.html", "allow_emulator": true, "storage": [ {"name":"grocery.app.js","url":"app.js"}, diff --git a/apps/ha/ChangeLog b/apps/ha/ChangeLog index f9ca3c16d..2a911c207 100644 --- a/apps/ha/ChangeLog +++ b/apps/ha/ChangeLog @@ -3,4 +3,7 @@ 0.03: Added clkinfo for clocks. 0.04: Feedback if clkinfo run is called. 0.05: Clkinfo improvements. -0.06: Updated clkinfo icon. \ No newline at end of file +0.06: Updated clkinfo icon. +0.07: Update clock_info to avoid a redraw +0.08: Allow swiping to switch triggers +0.09: Improve web interface, arrows in UI diff --git a/apps/ha/README.md b/apps/ha/README.md index 654a262c8..b0309b040 100644 --- a/apps/ha/README.md +++ b/apps/ha/README.md @@ -1,28 +1,30 @@ # Home Assistant -This app integrates your BangleJs into the HomeAssistant. +This app integrates your Bangle.js into the Home Assistant. # How to use -Click on the left and right side of the screen to select the triggers that you -configured. Click in the middle of the screen to send the trigger to HomeAssistant. +Click on the left or right side of the screen to select the triggers that you configured. +Swiping left or right works as well. + +Click in the middle of the screen to send the trigger to Home Assistant via Gadgetbridge. ![](screenshot.png) # Initial Setup -1.) First of all, make sure that HomeAssistant and the HomeAssistant Android App works. +1.) First of all, make sure that Home Assistant and the Home Assistant Android Companion App work. -2.) Open your BangleJs Gadgetbridge App, click on the Settings icon of your BangleJs and enable "Allow Intent Access" +2.) Open your Bangle.js Gadgetbridge App, click on the Settings icon of your Bangle.js and enable "Allow Intent Access" -3.) Enable sensor in HomeAssistant Andoird App/Configuration/Companion App/Manage Sensors/LastUpdate Trigger +3.) Enable sensor in Home Assistant Android App/Configuration/Companion App/Manage Sensors/LastUpdate Trigger 4.) At the bottom of the same screen click on "Add New Intent" and enter "com.espruino.gadgetbridge.banglejs.HA" -5.) The HomeAssistant Android app must be restarted in order to listen for those actions +5.) The Home Assistant Android app must be restarted in order to listen for those actions -- a "Force Stop" is necessary (through Android App settings) or restart your phone! This setup must be done only once -- now you are ready to configure your BangleJS to -control some devices or entities in your HomeAssistant :) +control some devices or entities in your Home Assistant :) # Setup Trigger @@ -35,7 +37,7 @@ The following icons are currently supported: - fire -2.) Create an "automation" in the HomeAssistant WebUI for each trigger that you created on your BangleJs in order to tell HomeAssistant what you want to control. A sample configuration is shown in the image below -- I use this trigger to open the door: +2.) Create an "automation" in the Home Assistant WebUI for each trigger that you created on your Bangle.js in order to tell Home Assistant what you want to control. A sample configuration is shown in the image below -- I use this trigger to open the door: ![](ha_automation.png) @@ -48,7 +50,7 @@ This app also implements two default trigger that can always be used: - TRIGGER -- Will be sent whenever some trigger is executed. So you could generically listen to that. -# How to use the library (ha.lib.js) in my own app/clk +# How to use the library (ha.lib.js) in my own app/clock This app inlcludes a library that can be used by other apps or clocks to read all configured intents or to send a trigger. Example code: @@ -74,17 +76,20 @@ ha.sendTrigger("MY_CUSTOM_TRIGGER"); # FAQ ## Sometimes the trigger is not executed -While playing and testing a bit I found that it is very important that you allow the android HomeAssistant app, as well as BangleJs Gadgetbridge app to (1) run in background and (2), disable energy optimizations for both apps. -Otherwise, Android could stop one of both apps and the trigger will never be sent to HomeAssistant... +While playing and testing a bit I found that it is very important that you allow the android Home Assistant app, as well as Bangle.js Gadgetbridge app to (1) run in background and (2), disable energy optimizations for both apps. +Otherwise, Android could stop one of both apps and the trigger will never be sent to Home Assistant... If you still have problems, you can try another trick: -Install "MacroDroid" from the Android AppStore and start the HomeAssistant App +Install "MacroDroid" from the Android AppStore and start the Home Assistant App each time the "com.espruino.gadgetbridge.banglejs.HA" intent is send together -with the extra trigger: APP_STARTED. Then whenever you open the app on your BangleJs -it is ensured that HomeAssistant is running... +with the extra trigger: APP_STARTED. Then whenever you open the app on your Bangle.js +it is ensured that Home Assistant is running... ## Thanks to Icons created by Flaticon ## Creator - [David Peer](https://github.com/peerdavid). + +## Contributor +- [myxor](https://github.com/myxor) diff --git a/apps/ha/custom.html b/apps/ha/custom.html deleted file mode 100644 index 49f5a2eb8..000000000 --- a/apps/ha/custom.html +++ /dev/null @@ -1,51 +0,0 @@ - - - - - -

Upload Tigger

-

-

- - - - - - diff --git a/apps/ha/ha.app.js b/apps/ha/ha.app.js index d9199fb0e..14c6dc3be 100644 --- a/apps/ha/ha.app.js +++ b/apps/ha/ha.app.js @@ -17,39 +17,41 @@ function draw() { g.setFontAlign(-1,-1); var icon = trigger.getIcon(); - g.setColor(g.theme.fg).drawImage(icon, 12, H/5-2); - g.drawString("Home", icon.width + 20, H/5); - g.drawString("Assistant", icon.width + 18, H/5+24); + g.setColor(g.theme.fg).drawImage(icon, 12, H/5-2-5); + g.drawString("Home", icon.width + 20, H/5-5); + g.drawString("Assistant", icon.width + 18, H/5+24-5); g.setFontAlign(0,0); - var ypos = H/5*3+20; + var ypos = H/5*3+23; g.drawRect(W/2-w/2-8, ypos-h/2-8, W/2+w/2+5, ypos+h/2+5); g.fillRect(W/2-w/2-6, ypos-h/2-6, W/2+w/2+3, ypos+h/2+3); g.setColor(g.theme.bg).drawString(trigger.display, W/2, ypos); + + // draw arrows + g.setColor(g.theme.fg); + if (position > 0) { + g.drawLine(10, H/2, 20, H/2 - 10); + g.drawLine(10, H/2, 20, H/2 + 10); + } + if (position < triggers.length -1) { + g.drawLine(W - 10, H/2, W - 20, H/2 - 10); + g.drawLine(W - 10, H/2, W - 20, H/2 + 10); + } } - -Bangle.on('touch', function(btn, e){ - var left = parseInt(g.getWidth() * 0.3); - var right = g.getWidth() - left; - var isLeft = e.x < left; - var isRight = e.x > right; - - if(isRight){ - Bangle.buzz(40, 0.6); - position += 1; - position = position >= triggers.length ? 0 : position; - draw(); - } - - if(isLeft){ +function toLeft() { Bangle.buzz(40, 0.6); position -= 1; position = position < 0 ? triggers.length-1 : position; draw(); - } - - if(!isRight && !isLeft){ +} +function toRight() { + Bangle.buzz(40, 0.6); + position += 1; + position = position >= triggers.length ? 0 : position; + draw(); +} +function sendTrigger() { ha.sendTrigger("TRIGGER"); // Now send the selected trigger @@ -59,9 +61,34 @@ Bangle.on('touch', function(btn, e){ Bangle.buzz(80, 0.6); }, 250); }); +} + +Bangle.on('touch', function(btn, e){ + var left = parseInt(g.getWidth() * 0.3); + var right = g.getWidth() - left; + var isLeft = e.x < left; + var isRight = e.x > right; + + if(isLeft){ + toLeft(); + } + if(isRight){ + toRight(); + } + if(!isRight && !isLeft){ + sendTrigger(); } }); +Bangle.on("swipe", (lr,ud) => { + if (lr == -1) { + toLeft(); + } + if (lr == 1) { + toRight(); + } + }); + // Send intent that the we started the app. ha.sendTrigger("APP_STARTED"); diff --git a/apps/ha/ha.clkinfo.js b/apps/ha/ha.clkinfo.js index d6a0f72a0..09724ba45 100644 --- a/apps/ha/ha.clkinfo.js +++ b/apps/ha/ha.clkinfo.js @@ -12,7 +12,7 @@ haItems.items.push({ name: null, get: () => ({ text: trigger.display, img: trigger.getIcon()}), - show: function() { haItems.items[i].emit("redraw"); }, + show: function() {}, hide: function () {}, run: function() { ha.sendTrigger("TRIGGER_BW"); @@ -23,4 +23,4 @@ }); return haItems; -}) \ No newline at end of file +}) diff --git a/apps/ha/interface.html b/apps/ha/interface.html new file mode 100644 index 000000000..3520b217b --- /dev/null +++ b/apps/ha/interface.html @@ -0,0 +1,52 @@ + + + + + +

Home Assistant trigger config

+

+

+ + + + + + diff --git a/apps/ha/metadata.json b/apps/ha/metadata.json index 089450f55..d44b4b6f8 100644 --- a/apps/ha/metadata.json +++ b/apps/ha/metadata.json @@ -1,14 +1,14 @@ { "id": "ha", - "name": "HomeAssistant", - "version": "0.06", - "description": "Integrates your BangleJS into HomeAssistant.", + "name": "Home Assistant", + "version": "0.09", + "description": "Integrates your Bangle.js into Home Assistant.", "icon": "ha.png", "type": "app", - "tags": "tool,clkinfo", + "tags": "tool,clkinfo,bluetooth", "readme": "README.md", "supports": ["BANGLEJS2"], - "custom": "custom.html", + "interface": "interface.html", "screenshots": [ {"url":"screenshot.png"}, {"url":"screenshot_2.png"}, diff --git a/apps/health/ChangeLog b/apps/health/ChangeLog index fc8f2c950..921b2b682 100644 --- a/apps/health/ChangeLog +++ b/apps/health/ChangeLog @@ -16,3 +16,8 @@ 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 +0.18: Show step goal in daily step chart +0.19: Can show notification when daily step goal is reached +0.20: Fix the settings page, it would not update settings correctly. +0.21: Update boot.min.js. +0.22: Fix timeout for heartrate sensor on 3 minute setting (#2435) \ No newline at end of file diff --git a/apps/health/README.md b/apps/health/README.md index 3cc234a3f..960e5565b 100644 --- a/apps/health/README.md +++ b/apps/health/README.md @@ -1,6 +1,6 @@ # Health Tracking -Logs health data to a file every 10 minutes, and provides an app to view it +Logs health data to a file in a defined interval, and provides an app to view it **BETA - requires firmware 2v11 or later** @@ -22,9 +22,11 @@ Stores: * **Heart Rt** - Whether to monitor heart rate or not * **Off** - Don't turn HRM on, but record heart rate if the HRM was turned on by another app/widget + * **3 Min** - Turn HRM on every 3 minutes (for each heath entry) and turn it off after 1 minute, or when a good reading is found * **10 Min** - Turn HRM on every 10 minutes (for each heath entry) and turn it off after 2 minutes, or when a good reading is found * **Always** - Keep HRM on all the time (more accurate recording, but reduces battery life to ~36 hours) -* **Daily Step Goal** - Default 10000, daily step goal for pedometer apps to use +* **Daily Step Goal** - Default 10000, daily step goal for pedometer apps to use and for the step goal notification +* **Step Goal Notification** - True if you want a notification when the daily step goal is reached ## Technical Info @@ -49,3 +51,7 @@ and run `EspruinoDocs/bin/minify.js lib.js lib.min.js` * Yearly view * Heart rate 'zone' graph * .. other + +## License + +The graphs on the web interface use Chart.js, licensed under MIT License. diff --git a/apps/health/app.js b/apps/health/app.js index 844dd7241..bd708207b 100644 --- a/apps/health/app.js +++ b/apps/health/app.js @@ -38,6 +38,7 @@ function menuHRM() { function stepsPerHour() { E.showMessage(/*LANG*/"Loading..."); + current_selection = "stepsPerHour"; var data = new Uint16Array(24); require("health").readDay(new Date(), h=>data[h.hr]+=h.steps); setButton(menuStepCount); @@ -46,14 +47,17 @@ function stepsPerHour() { function stepsPerDay() { E.showMessage(/*LANG*/"Loading..."); + current_selection = "stepsPerDay"; var data = new Uint16Array(31); require("health").readDailySummaries(new Date(), h=>data[h.day]+=h.steps); setButton(menuStepCount); barChart(/*LANG*/"DAY", data); + drawHorizontalLine(settings.stepGoal); } function hrmPerHour() { E.showMessage(/*LANG*/"Loading..."); + current_selection = "hrmPerHour"; var data = new Uint16Array(24); var cnt = new Uint8Array(23); require("health").readDay(new Date(), h=>{ @@ -67,6 +71,7 @@ function hrmPerHour() { function hrmPerDay() { E.showMessage(/*LANG*/"Loading..."); + current_selection = "hrmPerDay"; var data = new Uint16Array(31); var cnt = new Uint8Array(31); require("health").readDailySummaries(new Date(), h=>{ @@ -80,6 +85,7 @@ function hrmPerDay() { function movementPerHour() { E.showMessage(/*LANG*/"Loading..."); + current_selection = "movementPerHour"; var data = new Uint16Array(24); require("health").readDay(new Date(), h=>data[h.hr]+=h.movement); setButton(menuMovement); @@ -88,6 +94,7 @@ function movementPerHour() { function movementPerDay() { E.showMessage(/*LANG*/"Loading..."); + current_selection = "movementPerDay"; var data = new Uint16Array(31); require("health").readDailySummaries(new Date(), h=>data[h.day]+=h.movement); setButton(menuMovement); @@ -97,12 +104,14 @@ function movementPerDay() { // Bar Chart Code const w = g.getWidth(); const h = g.getHeight(); +const bar_bot = 140; var data_len; var chart_index; var chart_max_datum; var chart_label; var chart_data; +var current_selection; // find the max value in the array, using a loop due to array size function max(arr) { @@ -131,7 +140,6 @@ function barChart(label, dt) { } function drawBarChart() { - const bar_bot = 140; const bar_width = (w - 2) / 9; // we want 9 bars, bar 5 in the centre var bar_top; var bar; @@ -157,6 +165,11 @@ function drawBarChart() { } } +function drawHorizontalLine(value) { + const top = bar_bot - 100 * value / chart_max_datum; + g.setColor(g.theme.fg).drawLine(0, top ,g.getWidth(), top); +} + function setButton(fn) { Bangle.setUI({mode:"custom", back:fn, @@ -170,9 +183,13 @@ function setButton(fn) { return fn(); } drawBarChart(); + if (current_selection == "stepsPerDay") { + drawHorizontalLine(settings.stepGoal); + } }}); } Bangle.loadWidgets(); Bangle.drawWidgets(); +var settings = require("Storage").readJSON("health.json",1)||{}; menuMain(); diff --git a/apps/health/boot.js b/apps/health/boot.js index 7b9aa51aa..ae9a7cdc9 100644 --- a/apps/health/boot.js +++ b/apps/health/boot.js @@ -11,7 +11,7 @@ Bangle.setHRMPower(1, "health"); setTimeout(()=>{ Bangle.setHRMPower(0, "health"); - }, (i * 200000) + 60000); + }, 60000); }, (i * 200000)); } } @@ -36,6 +36,10 @@ Bangle.on("health", health => { const DB_HEADER_LEN = 8; const DB_FILE_LEN = DB_HEADER_LEN + DB_RECORDS_PER_MONTH*DB_RECORD_LEN; + if (health && health.steps > 0) { + handleStepGoalNotification(); + } + function getRecordFN(d) { return "health-"+d.getFullYear()+"-"+(d.getMonth()+1)+".raw"; } @@ -92,3 +96,21 @@ Bangle.on("health", health => { health.movement /= health.movCnt; require("Storage").write(fn, getRecordData(health), sumPos, DB_FILE_LEN); }); + +function handleStepGoalNotification() { + var settings = require("Storage").readJSON("health.json",1)||{}; + const steps = Bangle.getHealthStatus("day").steps; + if (settings.stepGoalNotification && settings.stepGoal > 0 && steps >= settings.stepGoal) { + const now = new Date(Date.now()).toISOString().split('T')[0]; // yyyy-mm-dd + if (!settings.stepGoalNotificationDate || settings.stepGoalNotificationDate < now) { // notification not yet shown today? + Bangle.buzz(200, 0.5); + require("notify").show({ + title : settings.stepGoal + /*LANG*/ " steps", + body : /*LANG*/ "You reached your step goal!", + icon : atob("DAyBABmD6BaBMAsA8BCBCBCBCA8AAA==") + }); + settings.stepGoalNotificationDate = now; + require("Storage").writeJSON("health.json", settings); + } + } +} diff --git a/apps/health/boot.min.js b/apps/health/boot.min.js index 00313a1f5..e3e45c400 100644 --- a/apps/health/boot.min.js +++ b/apps/health/boot.min.js @@ -1,4 +1,5 @@ -(function(){var a=0|(require("Storage").readJSON("health.json",1)||{}).hrm;if(1==a||2==a){function f(){Bangle.setHRMPower(1,"health");setTimeout(()=>Bangle.setHRMPower(0,"health"),6E4*a);if(1==a)for(var b=1;2>=b;b++)setTimeout(()=>{Bangle.setHRMPower(1,"health");setTimeout(()=>{Bangle.setHRMPower(0,"health")},2E5*b+6E4)},2E5*b)}Bangle.on("health",f);Bangle.on("HRM",b=>{80{function f(c){return String.fromCharCode(c.steps>>8,c.steps&255,c.bpm,Math.min(c.movement/8,255))}var b=new Date(Date.now()-59E4),e=function(c){return 145*(c.getDate()-1)+6*c.getHours()+(0|6*c.getMinutes()/60)}(b);b=function(c){return"health-"+c.getFullYear()+"-"+(c.getMonth()+1)+".raw"}(b);var g=require("Storage").read(b);if(g){var d=g.substr(8+4*e,4);if("\u00ff\u00ff\u00ff\u00ff"!=d){print("HEALTH ERR: Already written!");return}}else require("Storage").write(b, -"HEALTH1\x00",0,17988);var h=8+4*e;require("Storage").write(b,f(a),h,17988);if(143==e%145)if(e=h+4,"\u00ff\u00ff\u00ff\u00ff"!=g.substr(e,4))print("HEALTH ERR: Daily summary already written!");else{a={steps:0,bpm:0,movement:0,movCnt:0,bpmCnt:0};for(var k=0;144>k;k++)d=g.substr(h,4),"\u00ff\u00ff\u00ff\u00ff"!=d&&(a.steps+=(d.charCodeAt(0)<<8)+d.charCodeAt(1),a.movement+=d.charCodeAt(2),a.movCnt++,d=d.charCodeAt(2),a.bpm+=d,d&&a.bpmCnt++),h-=4;a.bpmCnt&&(a.bpm/=a.bpmCnt);a.movCnt&&(a.movement/=a.movCnt); -require("Storage").write(b,f(a),e,17988)}}) \ No newline at end of file +function l(){var a=require("Storage").readJSON("health.json",1)||{},d=Bangle.getHealthStatus("day").steps;a.stepGoalNotification&&0=a.stepGoal&&(d=(new Date(Date.now())).toISOString().split("T")[0],!a.stepGoalNotificationDate||a.stepGoalNotificationDateBangle.setHRMPower(0,"health"),6E4*a);if(1==a)for(var b=1;2>=b;b++)setTimeout(()=>{Bangle.setHRMPower(1,"health");setTimeout(()=>{Bangle.setHRMPower(0,"health")},6E4)},2E5*b)}Bangle.on("health",d);Bangle.on("HRM",b=>{80{function d(c){return String.fromCharCode(c.steps>>8,c.steps&255,c.bpm,Math.min(c.movement/8,255))}var b=new Date(Date.now()-59E4);a&&0k;k++)e=g.substr(h,4),"\u00ff\u00ff\u00ff\u00ff"!=e&&(a.steps+=(e.charCodeAt(0)<<8)+e.charCodeAt(1),a.movement+=e.charCodeAt(2),a.movCnt++,e=e.charCodeAt(2),a.bpm+=e,e&&a.bpmCnt++),h-=4;a.bpmCnt&&(a.bpm/=a.bpmCnt);a.movCnt&&(a.movement/=a.movCnt); +require("Storage").write(b,d(a),f,17988)}}) \ No newline at end of file diff --git a/apps/health/chart.min.js b/apps/health/chart.min.js new file mode 100644 index 000000000..04752c4a3 --- /dev/null +++ b/apps/health/chart.min.js @@ -0,0 +1,14 @@ +/*! + * Chart.js v4.1.1 + * https://www.chartjs.org + * (c) 2022 Chart.js Contributors + * Released under the MIT License + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Chart=e()}(this,(function(){"use strict";var t=Object.freeze({__proto__:null,get Colors(){return Ho},get Decimation(){return Yo},get Filler(){return la},get Legend(){return ua},get SubTitle(){return ma},get Title(){return ga},get Tooltip(){return Ta}});function e(){}const i=(()=>{let t=0;return()=>t++})();function s(t){return null==t}function n(t){if(Array.isArray&&Array.isArray(t))return!0;const e=Object.prototype.toString.call(t);return"[object"===e.slice(0,7)&&"Array]"===e.slice(-6)}function o(t){return null!==t&&"[object Object]"===Object.prototype.toString.call(t)}function a(t){return("number"==typeof t||t instanceof Number)&&isFinite(+t)}function r(t,e){return a(t)?t:e}function l(t,e){return void 0===t?e:t}const h=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100:+t/e,c=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100*e:+t;function d(t,e,i){if(t&&"function"==typeof t.call)return t.apply(i,e)}function u(t,e,i,s){let a,r,l;if(n(t))if(r=t.length,s)for(a=r-1;a>=0;a--)e.call(i,t[a],a);else for(a=0;at,x:t=>t.x,y:t=>t.y};function v(t){const e=t.split("."),i=[];let s="";for(const t of e)s+=t,s.endsWith("\\")?s=s.slice(0,-1)+".":(i.push(s),s="");return i}function M(t,e){const i=y[e]||(y[e]=function(t){const e=v(t);return t=>{for(const i of e){if(""===i)break;t=t&&t[i]}return t}}(e));return i(t)}function w(t){return t.charAt(0).toUpperCase()+t.slice(1)}const k=t=>void 0!==t,S=t=>"function"==typeof t,P=(t,e)=>{if(t.size!==e.size)return!1;for(const i of t)if(!e.has(i))return!1;return!0};function D(t){return"mouseup"===t.type||"click"===t.type||"contextmenu"===t.type}const C=Math.PI,O=2*C,A=O+C,T=Number.POSITIVE_INFINITY,L=C/180,E=C/2,R=C/4,I=2*C/3,z=Math.log10,F=Math.sign;function V(t,e,i){return Math.abs(t-e)t-e)).pop(),e}function W(t){return!isNaN(parseFloat(t))&&isFinite(t)}function H(t,e){const i=Math.round(t);return i-e<=t&&i+e>=t}function j(t,e,i){let s,n,o;for(s=0,n=t.length;sl&&h=Math.min(e,i)-s&&t<=Math.max(e,i)+s}function et(t,e,i){i=i||(i=>t[i]1;)s=o+n>>1,i(s)?o=s:n=s;return{lo:o,hi:n}}const it=(t,e,i,s)=>et(t,i,s?s=>{const n=t[s][e];return nt[s][e]et(t,i,(s=>t[s][e]>=i));function nt(t,e,i){let s=0,n=t.length;for(;ss&&t[n-1]>i;)n--;return s>0||n{const i="_onData"+w(e),s=t[e];Object.defineProperty(t,e,{configurable:!0,enumerable:!1,value(...e){const n=s.apply(this,e);return t._chartjs.listeners.forEach((t=>{"function"==typeof t[i]&&t[i](...e)})),n}})})))}function rt(t,e){const i=t._chartjs;if(!i)return;const s=i.listeners,n=s.indexOf(e);-1!==n&&s.splice(n,1),s.length>0||(ot.forEach((e=>{delete t[e]})),delete t._chartjs)}function lt(t){const e=new Set;let i,s;for(i=0,s=t.length;i{s=!1,t.apply(e,i)})))}}function dt(t,e){let i;return function(...s){return e?(clearTimeout(i),i=setTimeout(t,e,s)):t.apply(this,s),e}}const ut=t=>"start"===t?"left":"end"===t?"right":"center",ft=(t,e,i)=>"start"===t?e:"end"===t?i:(e+i)/2,gt=(t,e,i,s)=>t===(s?"left":"right")?i:"center"===t?(e+i)/2:e;function pt(t,e,i){const s=e.length;let n=0,o=s;if(t._sorted){const{iScale:a,_parsed:r}=t,l=a.axis,{min:h,max:c,minDefined:d,maxDefined:u}=a.getUserBounds();d&&(n=J(Math.min(it(r,a.axis,h).lo,i?s:it(e,l,a.getPixelForValue(h)).lo),0,s-1)),o=u?J(Math.max(it(r,a.axis,c,!0).hi+1,i?0:it(e,l,a.getPixelForValue(c),!0).hi+1),n,s)-n:s-n}return{start:n,count:o}}function mt(t){const{xScale:e,yScale:i,_scaleRanges:s}=t,n={xmin:e.min,xmax:e.max,ymin:i.min,ymax:i.max};if(!s)return t._scaleRanges=n,!0;const o=s.xmin!==e.min||s.xmax!==e.max||s.ymin!==i.min||s.ymax!==i.max;return Object.assign(s,n),o}class bt{constructor(){this._request=null,this._charts=new Map,this._running=!1,this._lastDate=void 0}_notify(t,e,i,s){const n=e.listeners[s],o=e.duration;n.forEach((s=>s({chart:t,initial:e.initial,numSteps:o,currentStep:Math.min(i-e.start,o)})))}_refresh(){this._request||(this._running=!0,this._request=ht.call(window,(()=>{this._update(),this._request=null,this._running&&this._refresh()})))}_update(t=Date.now()){let e=0;this._charts.forEach(((i,s)=>{if(!i.running||!i.items.length)return;const n=i.items;let o,a=n.length-1,r=!1;for(;a>=0;--a)o=n[a],o._active?(o._total>i.duration&&(i.duration=o._total),o.tick(t),r=!0):(n[a]=n[n.length-1],n.pop());r&&(s.draw(),this._notify(s,i,t,"progress")),n.length||(i.running=!1,this._notify(s,i,t,"complete"),i.initial=!1),e+=n.length})),this._lastDate=t,0===e&&(this._running=!1)}_getAnims(t){const e=this._charts;let i=e.get(t);return i||(i={running:!1,initial:!0,items:[],listeners:{complete:[],progress:[]}},e.set(t,i)),i}listen(t,e,i){this._getAnims(t).listeners[e].push(i)}add(t,e){e&&e.length&&this._getAnims(t).items.push(...e)}has(t){return this._getAnims(t).items.length>0}start(t){const e=this._charts.get(t);e&&(e.running=!0,e.start=Date.now(),e.duration=e.items.reduce(((t,e)=>Math.max(t,e._duration)),0),this._refresh())}running(t){if(!this._running)return!1;const e=this._charts.get(t);return!!(e&&e.running&&e.items.length)}stop(t){const e=this._charts.get(t);if(!e||!e.items.length)return;const i=e.items;let s=i.length-1;for(;s>=0;--s)i[s].cancel();e.items=[],this._notify(t,e,Date.now(),"complete")}remove(t){return this._charts.delete(t)}}var xt=new bt; +/*! + * @kurkle/color v0.3.0 + * https://github.com/kurkle/color#readme + * (c) 2022 Jukka Kurkela + * Released under the MIT License + */function _t(t){return t+.5|0}const yt=(t,e,i)=>Math.max(Math.min(t,i),e);function vt(t){return yt(_t(2.55*t),0,255)}function Mt(t){return yt(_t(255*t),0,255)}function wt(t){return yt(_t(t/2.55)/100,0,1)}function kt(t){return yt(_t(100*t),0,100)}const St={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},Pt=[..."0123456789ABCDEF"],Dt=t=>Pt[15&t],Ct=t=>Pt[(240&t)>>4]+Pt[15&t],Ot=t=>(240&t)>>4==(15&t);function At(t){var e=(t=>Ot(t.r)&&Ot(t.g)&&Ot(t.b)&&Ot(t.a))(t)?Dt:Ct;return t?"#"+e(t.r)+e(t.g)+e(t.b)+((t,e)=>t<255?e(t):"")(t.a,e):void 0}const Tt=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function Lt(t,e,i){const s=e*Math.min(i,1-i),n=(e,n=(e+t/30)%12)=>i-s*Math.max(Math.min(n-3,9-n,1),-1);return[n(0),n(8),n(4)]}function Et(t,e,i){const s=(s,n=(s+t/60)%6)=>i-i*e*Math.max(Math.min(n,4-n,1),0);return[s(5),s(3),s(1)]}function Rt(t,e,i){const s=Lt(t,1,.5);let n;for(e+i>1&&(n=1/(e+i),e*=n,i*=n),n=0;n<3;n++)s[n]*=1-e-i,s[n]+=e;return s}function It(t){const e=t.r/255,i=t.g/255,s=t.b/255,n=Math.max(e,i,s),o=Math.min(e,i,s),a=(n+o)/2;let r,l,h;return n!==o&&(h=n-o,l=a>.5?h/(2-n-o):h/(n+o),r=function(t,e,i,s,n){return t===n?(e-i)/s+(e>16&255,o>>8&255,255&o]}return t}(),Ht.transparent=[0,0,0,0]);const e=Ht[t.toLowerCase()];return e&&{r:e[0],g:e[1],b:e[2],a:4===e.length?e[3]:255}}const $t=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;const Yt=t=>t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055,Ut=t=>t<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4);function Xt(t,e,i){if(t){let s=It(t);s[e]=Math.max(0,Math.min(s[e]+s[e]*i,0===e?360:1)),s=Ft(s),t.r=s[0],t.g=s[1],t.b=s[2]}}function qt(t,e){return t?Object.assign(e||{},t):t}function Kt(t){var e={r:0,g:0,b:0,a:255};return Array.isArray(t)?t.length>=3&&(e={r:t[0],g:t[1],b:t[2],a:255},t.length>3&&(e.a=Mt(t[3]))):(e=qt(t,{r:0,g:0,b:0,a:1})).a=Mt(e.a),e}function Gt(t){return"r"===t.charAt(0)?function(t){const e=$t.exec(t);let i,s,n,o=255;if(e){if(e[7]!==i){const t=+e[7];o=e[8]?vt(t):yt(255*t,0,255)}return i=+e[1],s=+e[3],n=+e[5],i=255&(e[2]?vt(i):yt(i,0,255)),s=255&(e[4]?vt(s):yt(s,0,255)),n=255&(e[6]?vt(n):yt(n,0,255)),{r:i,g:s,b:n,a:o}}}(t):Bt(t)}class Zt{constructor(t){if(t instanceof Zt)return t;const e=typeof t;let i;var s,n,o;"object"===e?i=Kt(t):"string"===e&&(o=(s=t).length,"#"===s[0]&&(4===o||5===o?n={r:255&17*St[s[1]],g:255&17*St[s[2]],b:255&17*St[s[3]],a:5===o?17*St[s[4]]:255}:7!==o&&9!==o||(n={r:St[s[1]]<<4|St[s[2]],g:St[s[3]]<<4|St[s[4]],b:St[s[5]]<<4|St[s[6]],a:9===o?St[s[7]]<<4|St[s[8]]:255})),i=n||jt(t)||Gt(t)),this._rgb=i,this._valid=!!i}get valid(){return this._valid}get rgb(){var t=qt(this._rgb);return t&&(t.a=wt(t.a)),t}set rgb(t){this._rgb=Kt(t)}rgbString(){return this._valid?(t=this._rgb)&&(t.a<255?`rgba(${t.r}, ${t.g}, ${t.b}, ${wt(t.a)})`:`rgb(${t.r}, ${t.g}, ${t.b})`):void 0;var t}hexString(){return this._valid?At(this._rgb):void 0}hslString(){return this._valid?function(t){if(!t)return;const e=It(t),i=e[0],s=kt(e[1]),n=kt(e[2]);return t.a<255?`hsla(${i}, ${s}%, ${n}%, ${wt(t.a)})`:`hsl(${i}, ${s}%, ${n}%)`}(this._rgb):void 0}mix(t,e){if(t){const i=this.rgb,s=t.rgb;let n;const o=e===n?.5:e,a=2*o-1,r=i.a-s.a,l=((a*r==-1?a:(a+r)/(1+a*r))+1)/2;n=1-l,i.r=255&l*i.r+n*s.r+.5,i.g=255&l*i.g+n*s.g+.5,i.b=255&l*i.b+n*s.b+.5,i.a=o*i.a+(1-o)*s.a,this.rgb=i}return this}interpolate(t,e){return t&&(this._rgb=function(t,e,i){const s=Ut(wt(t.r)),n=Ut(wt(t.g)),o=Ut(wt(t.b));return{r:Mt(Yt(s+i*(Ut(wt(e.r))-s))),g:Mt(Yt(n+i*(Ut(wt(e.g))-n))),b:Mt(Yt(o+i*(Ut(wt(e.b))-o))),a:t.a+i*(e.a-t.a)}}(this._rgb,t._rgb,e)),this}clone(){return new Zt(this.rgb)}alpha(t){return this._rgb.a=Mt(t),this}clearer(t){return this._rgb.a*=1-t,this}greyscale(){const t=this._rgb,e=_t(.3*t.r+.59*t.g+.11*t.b);return t.r=t.g=t.b=e,this}opaquer(t){return this._rgb.a*=1+t,this}negate(){const t=this._rgb;return t.r=255-t.r,t.g=255-t.g,t.b=255-t.b,this}lighten(t){return Xt(this._rgb,2,t),this}darken(t){return Xt(this._rgb,2,-t),this}saturate(t){return Xt(this._rgb,1,t),this}desaturate(t){return Xt(this._rgb,1,-t),this}rotate(t){return function(t,e){var i=It(t);i[0]=Vt(i[0]+e),i=Ft(i),t.r=i[0],t.g=i[1],t.b=i[2]}(this._rgb,t),this}}function Jt(t){if(t&&"object"==typeof t){const e=t.toString();return"[object CanvasPattern]"===e||"[object CanvasGradient]"===e}return!1}function Qt(t){return Jt(t)?t:new Zt(t)}function te(t){return Jt(t)?t:new Zt(t).saturate(.5).darken(.1).hexString()}const ee=["x","y","borderWidth","radius","tension"],ie=["color","borderColor","backgroundColor"];const se=new Map;function ne(t,e,i){return function(t,e){e=e||{};const i=t+JSON.stringify(e);let s=se.get(i);return s||(s=new Intl.NumberFormat(t,e),se.set(i,s)),s}(e,i).format(t)}const oe={values:t=>n(t)?t:""+t,numeric(t,e,i){if(0===t)return"0";const s=this.chart.options.locale;let n,o=t;if(i.length>1){const e=Math.max(Math.abs(i[0].value),Math.abs(i[i.length-1].value));(e<1e-4||e>1e15)&&(n="scientific"),o=function(t,e){let i=e.length>3?e[2].value-e[1].value:e[1].value-e[0].value;Math.abs(i)>=1&&t!==Math.floor(t)&&(i=t-Math.floor(t));return i}(t,i)}const a=z(Math.abs(o)),r=Math.max(Math.min(-1*Math.floor(a),20),0),l={notation:n,minimumFractionDigits:r,maximumFractionDigits:r};return Object.assign(l,this.options.ticks.format),ne(t,s,l)},logarithmic(t,e,i){if(0===t)return"0";const s=i[e].significand||t/Math.pow(10,Math.floor(z(t)));return[1,2,3,5,10,15].includes(s)||e>.8*i.length?oe.numeric.call(this,t,e,i):""}};var ae={formatters:oe};const re=Object.create(null),le=Object.create(null);function he(t,e){if(!e)return t;const i=e.split(".");for(let e=0,s=i.length;et.chart.platform.getDevicePixelRatio(),this.elements={},this.events=["mousemove","mouseout","click","touchstart","touchmove"],this.font={family:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",size:12,style:"normal",lineHeight:1.2,weight:null},this.hover={},this.hoverBackgroundColor=(t,e)=>te(e.backgroundColor),this.hoverBorderColor=(t,e)=>te(e.borderColor),this.hoverColor=(t,e)=>te(e.color),this.indexAxis="x",this.interaction={mode:"nearest",intersect:!0,includeInvisible:!1},this.maintainAspectRatio=!0,this.onHover=null,this.onClick=null,this.parsing=!0,this.plugins={},this.responsive=!0,this.scale=void 0,this.scales={},this.showLine=!0,this.drawActiveElementsOnTop=!0,this.describe(t),this.apply(e)}set(t,e){return ce(this,t,e)}get(t){return he(this,t)}describe(t,e){return ce(le,t,e)}override(t,e){return ce(re,t,e)}route(t,e,i,s){const n=he(this,t),a=he(this,i),r="_"+e;Object.defineProperties(n,{[r]:{value:n[e],writable:!0},[e]:{enumerable:!0,get(){const t=this[r],e=a[s];return o(t)?Object.assign({},e,t):l(t,e)},set(t){this[r]=t}}})}apply(t){t.forEach((t=>t(this)))}}var ue=new de({_scriptable:t=>!t.startsWith("on"),_indexable:t=>"events"!==t,hover:{_fallback:"interaction"},interaction:{_scriptable:!1,_indexable:!1}},[function(t){t.set("animation",{delay:void 0,duration:1e3,easing:"easeOutQuart",fn:void 0,from:void 0,loop:void 0,to:void 0,type:void 0}),t.describe("animation",{_fallback:!1,_indexable:!1,_scriptable:t=>"onProgress"!==t&&"onComplete"!==t&&"fn"!==t}),t.set("animations",{colors:{type:"color",properties:ie},numbers:{type:"number",properties:ee}}),t.describe("animations",{_fallback:"animation"}),t.set("transitions",{active:{animation:{duration:400}},resize:{animation:{duration:0}},show:{animations:{colors:{from:"transparent"},visible:{type:"boolean",duration:0}}},hide:{animations:{colors:{to:"transparent"},visible:{type:"boolean",easing:"linear",fn:t=>0|t}}}})},function(t){t.set("layout",{autoPadding:!0,padding:{top:0,right:0,bottom:0,left:0}})},function(t){t.set("scale",{display:!0,offset:!1,reverse:!1,beginAtZero:!1,bounds:"ticks",grace:0,grid:{display:!0,lineWidth:1,drawOnChartArea:!0,drawTicks:!0,tickLength:8,tickWidth:(t,e)=>e.lineWidth,tickColor:(t,e)=>e.color,offset:!1},border:{display:!0,dash:[],dashOffset:0,width:1},title:{display:!1,text:"",padding:{top:4,bottom:4}},ticks:{minRotation:0,maxRotation:50,mirror:!1,textStrokeWidth:0,textStrokeColor:"",padding:3,display:!0,autoSkip:!0,autoSkipPadding:3,labelOffset:0,callback:ae.formatters.values,minor:{},major:{},align:"center",crossAlign:"near",showLabelBackdrop:!1,backdropColor:"rgba(255, 255, 255, 0.75)",backdropPadding:2}}),t.route("scale.ticks","color","","color"),t.route("scale.grid","color","","borderColor"),t.route("scale.border","color","","borderColor"),t.route("scale.title","color","","color"),t.describe("scale",{_fallback:!1,_scriptable:t=>!t.startsWith("before")&&!t.startsWith("after")&&"callback"!==t&&"parser"!==t,_indexable:t=>"borderDash"!==t&&"tickBorderDash"!==t&&"dash"!==t}),t.describe("scales",{_fallback:"scale"}),t.describe("scale.ticks",{_scriptable:t=>"backdropPadding"!==t&&"callback"!==t,_indexable:t=>"backdropPadding"!==t})}]);function fe(){return"undefined"!=typeof window&&"undefined"!=typeof document}function ge(t){let e=t.parentNode;return e&&"[object ShadowRoot]"===e.toString()&&(e=e.host),e}function pe(t,e,i){let s;return"string"==typeof t?(s=parseInt(t,10),-1!==t.indexOf("%")&&(s=s/100*e.parentNode[i])):s=t,s}const me=t=>t.ownerDocument.defaultView.getComputedStyle(t,null);function be(t,e){return me(t).getPropertyValue(e)}const xe=["top","right","bottom","left"];function _e(t,e,i){const s={};i=i?"-"+i:"";for(let n=0;n<4;n++){const o=xe[n];s[o]=parseFloat(t[e+"-"+o+i])||0}return s.width=s.left+s.right,s.height=s.top+s.bottom,s}function ye(t,e){if("native"in t)return t;const{canvas:i,currentDevicePixelRatio:s}=e,n=me(i),o="border-box"===n.boxSizing,a=_e(n,"padding"),r=_e(n,"border","width"),{x:l,y:h,box:c}=function(t,e){const i=t.touches,s=i&&i.length?i[0]:t,{offsetX:n,offsetY:o}=s;let a,r,l=!1;if(((t,e,i)=>(t>0||e>0)&&(!i||!i.shadowRoot))(n,o,t.target))a=n,r=o;else{const t=e.getBoundingClientRect();a=s.clientX-t.left,r=s.clientY-t.top,l=!0}return{x:a,y:r,box:l}}(t,i),d=a.left+(c&&r.left),u=a.top+(c&&r.top);let{width:f,height:g}=e;return o&&(f-=a.width+r.width,g-=a.height+r.height),{x:Math.round((l-d)/f*i.width/s),y:Math.round((h-u)/g*i.height/s)}}const ve=t=>Math.round(10*t)/10;function Me(t,e,i,s){const n=me(t),o=_e(n,"margin"),a=pe(n.maxWidth,t,"clientWidth")||T,r=pe(n.maxHeight,t,"clientHeight")||T,l=function(t,e,i){let s,n;if(void 0===e||void 0===i){const o=ge(t);if(o){const t=o.getBoundingClientRect(),a=me(o),r=_e(a,"border","width"),l=_e(a,"padding");e=t.width-l.width-r.width,i=t.height-l.height-r.height,s=pe(a.maxWidth,o,"clientWidth"),n=pe(a.maxHeight,o,"clientHeight")}else e=t.clientWidth,i=t.clientHeight}return{width:e,height:i,maxWidth:s||T,maxHeight:n||T}}(t,e,i);let{width:h,height:c}=l;if("content-box"===n.boxSizing){const t=_e(n,"border","width"),e=_e(n,"padding");h-=e.width+t.width,c-=e.height+t.height}h=Math.max(0,h-o.width),c=Math.max(0,s?h/s:c-o.height),h=ve(Math.min(h,a,l.maxWidth)),c=ve(Math.min(c,r,l.maxHeight)),h&&!c&&(c=ve(h/2));return(void 0!==e||void 0!==i)&&s&&l.height&&c>l.height&&(c=l.height,h=ve(Math.floor(c*s))),{width:h,height:c}}function we(t,e,i){const s=e||1,n=Math.floor(t.height*s),o=Math.floor(t.width*s);t.height=Math.floor(t.height),t.width=Math.floor(t.width);const a=t.canvas;return a.style&&(i||!a.style.height&&!a.style.width)&&(a.style.height=`${t.height}px`,a.style.width=`${t.width}px`),(t.currentDevicePixelRatio!==s||a.height!==n||a.width!==o)&&(t.currentDevicePixelRatio=s,a.height=n,a.width=o,t.ctx.setTransform(s,0,0,s,0,0),!0)}const ke=function(){let t=!1;try{const e={get passive(){return t=!0,!1}};window.addEventListener("test",null,e),window.removeEventListener("test",null,e)}catch(t){}return t}();function Se(t,e){const i=be(t,e),s=i&&i.match(/^(\d+)(\.\d+)?px$/);return s?+s[1]:void 0}function Pe(t){return!t||s(t.size)||s(t.family)?null:(t.style?t.style+" ":"")+(t.weight?t.weight+" ":"")+t.size+"px "+t.family}function De(t,e,i,s,n){let o=e[n];return o||(o=e[n]=t.measureText(n).width,i.push(n)),o>s&&(s=o),s}function Ce(t,e,i,s){let o=(s=s||{}).data=s.data||{},a=s.garbageCollect=s.garbageCollect||[];s.font!==e&&(o=s.data={},a=s.garbageCollect=[],s.font=e),t.save(),t.font=e;let r=0;const l=i.length;let h,c,d,u,f;for(h=0;hi.length){for(h=0;h0&&t.stroke()}}function Ee(t,e,i){return i=i||.5,!e||t&&t.x>e.left-i&&t.xe.top-i&&t.y0&&""!==r.strokeColor;let c,d;for(t.save(),t.font=a.string,function(t,e){e.translation&&t.translate(e.translation[0],e.translation[1]);s(e.rotation)||t.rotate(e.rotation);e.color&&(t.fillStyle=e.color);e.textAlign&&(t.textAlign=e.textAlign);e.textBaseline&&(t.textBaseline=e.textBaseline)}(t,r),c=0;ct[0])){k(s)||(s=Qe("_fallback",t));const o={[Symbol.toStringTag]:"Object",_cacheable:!0,_scopes:t,_rootScopes:i,_fallback:s,_getTarget:n,override:n=>He([n,...t],e,i,s)};return new Proxy(o,{deleteProperty:(e,i)=>(delete e[i],delete e._keys,delete t[0][i],!0),get:(i,s)=>Xe(i,s,(()=>function(t,e,i,s){let n;for(const o of e)if(n=Qe(Ye(o,t),i),k(n))return Ue(t,n)?Ze(i,s,t,n):n}(s,e,t,i))),getOwnPropertyDescriptor:(t,e)=>Reflect.getOwnPropertyDescriptor(t._scopes[0],e),getPrototypeOf:()=>Reflect.getPrototypeOf(t[0]),has:(t,e)=>ti(t).includes(e),ownKeys:t=>ti(t),set(t,e,i){const s=t._storage||(t._storage=n());return t[e]=s[e]=i,delete t._keys,!0}})}function je(t,e,i,s){const a={_cacheable:!1,_proxy:t,_context:e,_subProxy:i,_stack:new Set,_descriptors:$e(t,s),setContext:e=>je(t,e,i,s),override:n=>je(t.override(n),e,i,s)};return new Proxy(a,{deleteProperty:(e,i)=>(delete e[i],delete t[i],!0),get:(t,e,i)=>Xe(t,e,(()=>function(t,e,i){const{_proxy:s,_context:a,_subProxy:r,_descriptors:l}=t;let h=s[e];S(h)&&l.isScriptable(e)&&(h=function(t,e,i,s){const{_proxy:n,_context:o,_subProxy:a,_stack:r}=i;if(r.has(t))throw new Error("Recursion detected: "+Array.from(r).join("->")+"->"+t);r.add(t),e=e(o,a||s),r.delete(t),Ue(t,e)&&(e=Ze(n._scopes,n,t,e));return e}(e,h,t,i));n(h)&&h.length&&(h=function(t,e,i,s){const{_proxy:n,_context:a,_subProxy:r,_descriptors:l}=i;if(k(a.index)&&s(t))e=e[a.index%e.length];else if(o(e[0])){const i=e,s=n._scopes.filter((t=>t!==i));e=[];for(const o of i){const i=Ze(s,n,t,o);e.push(je(i,a,r&&r[t],l))}}return e}(e,h,t,l.isIndexable));Ue(e,h)&&(h=je(h,a,r&&r[e],l));return h}(t,e,i))),getOwnPropertyDescriptor:(e,i)=>e._descriptors.allKeys?Reflect.has(t,i)?{enumerable:!0,configurable:!0}:void 0:Reflect.getOwnPropertyDescriptor(t,i),getPrototypeOf:()=>Reflect.getPrototypeOf(t),has:(e,i)=>Reflect.has(t,i),ownKeys:()=>Reflect.ownKeys(t),set:(e,i,s)=>(t[i]=s,delete e[i],!0)})}function $e(t,e={scriptable:!0,indexable:!0}){const{_scriptable:i=e.scriptable,_indexable:s=e.indexable,_allKeys:n=e.allKeys}=t;return{allKeys:n,scriptable:i,indexable:s,isScriptable:S(i)?i:()=>i,isIndexable:S(s)?s:()=>s}}const Ye=(t,e)=>t?t+w(e):e,Ue=(t,e)=>o(e)&&"adapters"!==t&&(null===Object.getPrototypeOf(e)||e.constructor===Object);function Xe(t,e,i){if(Object.prototype.hasOwnProperty.call(t,e))return t[e];const s=i();return t[e]=s,s}function qe(t,e,i){return S(t)?t(e,i):t}const Ke=(t,e)=>!0===t?e:"string"==typeof t?M(e,t):void 0;function Ge(t,e,i,s,n){for(const o of e){const e=Ke(i,o);if(e){t.add(e);const o=qe(e._fallback,i,n);if(k(o)&&o!==i&&o!==s)return o}else if(!1===e&&k(s)&&i!==s)return null}return!1}function Ze(t,e,i,s){const a=e._rootScopes,r=qe(e._fallback,i,s),l=[...t,...a],h=new Set;h.add(s);let c=Je(h,l,i,r||i,s);return null!==c&&((!k(r)||r===i||(c=Je(h,l,r,c,s),null!==c))&&He(Array.from(h),[""],a,r,(()=>function(t,e,i){const s=t._getTarget();e in s||(s[e]={});const a=s[e];if(n(a)&&o(i))return i;return a||{}}(e,i,s))))}function Je(t,e,i,s,n){for(;i;)i=Ge(t,e,i,s,n);return i}function Qe(t,e){for(const i of e){if(!i)continue;const e=i[t];if(k(e))return e}}function ti(t){let e=t._keys;return e||(e=t._keys=function(t){const e=new Set;for(const i of t)for(const t of Object.keys(i).filter((t=>!t.startsWith("_"))))e.add(t);return Array.from(e)}(t._scopes)),e}function ei(t,e,i,s){const{iScale:n}=t,{key:o="r"}=this._parsing,a=new Array(s);let r,l,h,c;for(r=0,l=s;re"x"===t?"y":"x";function oi(t,e,i,s){const n=t.skip?e:t,o=e,a=i.skip?e:i,r=q(o,n),l=q(a,o);let h=r/(r+l),c=l/(r+l);h=isNaN(h)?0:h,c=isNaN(c)?0:c;const d=s*h,u=s*c;return{previous:{x:o.x-d*(a.x-n.x),y:o.y-d*(a.y-n.y)},next:{x:o.x+u*(a.x-n.x),y:o.y+u*(a.y-n.y)}}}function ai(t,e="x"){const i=ni(e),s=t.length,n=Array(s).fill(0),o=Array(s);let a,r,l,h=si(t,0);for(a=0;a!t.skip))),"monotone"===e.cubicInterpolationMode)ai(t,n);else{let i=s?t[t.length-1]:t[0];for(o=0,a=t.length;o0===t||1===t,ci=(t,e,i)=>-Math.pow(2,10*(t-=1))*Math.sin((t-e)*O/i),di=(t,e,i)=>Math.pow(2,-10*t)*Math.sin((t-e)*O/i)+1,ui={linear:t=>t,easeInQuad:t=>t*t,easeOutQuad:t=>-t*(t-2),easeInOutQuad:t=>(t/=.5)<1?.5*t*t:-.5*(--t*(t-2)-1),easeInCubic:t=>t*t*t,easeOutCubic:t=>(t-=1)*t*t+1,easeInOutCubic:t=>(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2),easeInQuart:t=>t*t*t*t,easeOutQuart:t=>-((t-=1)*t*t*t-1),easeInOutQuart:t=>(t/=.5)<1?.5*t*t*t*t:-.5*((t-=2)*t*t*t-2),easeInQuint:t=>t*t*t*t*t,easeOutQuint:t=>(t-=1)*t*t*t*t+1,easeInOutQuint:t=>(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2),easeInSine:t=>1-Math.cos(t*E),easeOutSine:t=>Math.sin(t*E),easeInOutSine:t=>-.5*(Math.cos(C*t)-1),easeInExpo:t=>0===t?0:Math.pow(2,10*(t-1)),easeOutExpo:t=>1===t?1:1-Math.pow(2,-10*t),easeInOutExpo:t=>hi(t)?t:t<.5?.5*Math.pow(2,10*(2*t-1)):.5*(2-Math.pow(2,-10*(2*t-1))),easeInCirc:t=>t>=1?t:-(Math.sqrt(1-t*t)-1),easeOutCirc:t=>Math.sqrt(1-(t-=1)*t),easeInOutCirc:t=>(t/=.5)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1),easeInElastic:t=>hi(t)?t:ci(t,.075,.3),easeOutElastic:t=>hi(t)?t:di(t,.075,.3),easeInOutElastic(t){const e=.1125;return hi(t)?t:t<.5?.5*ci(2*t,e,.45):.5+.5*di(2*t-1,e,.45)},easeInBack(t){const e=1.70158;return t*t*((e+1)*t-e)},easeOutBack(t){const e=1.70158;return(t-=1)*t*((e+1)*t+e)+1},easeInOutBack(t){let e=1.70158;return(t/=.5)<1?t*t*((1+(e*=1.525))*t-e)*.5:.5*((t-=2)*t*((1+(e*=1.525))*t+e)+2)},easeInBounce:t=>1-ui.easeOutBounce(1-t),easeOutBounce(t){const e=7.5625,i=2.75;return t<1/i?e*t*t:t<2/i?e*(t-=1.5/i)*t+.75:t<2.5/i?e*(t-=2.25/i)*t+.9375:e*(t-=2.625/i)*t+.984375},easeInOutBounce:t=>t<.5?.5*ui.easeInBounce(2*t):.5*ui.easeOutBounce(2*t-1)+.5};function fi(t,e,i,s){return{x:t.x+i*(e.x-t.x),y:t.y+i*(e.y-t.y)}}function gi(t,e,i,s){return{x:t.x+i*(e.x-t.x),y:"middle"===s?i<.5?t.y:e.y:"after"===s?i<1?t.y:e.y:i>0?e.y:t.y}}function pi(t,e,i,s){const n={x:t.cp2x,y:t.cp2y},o={x:e.cp1x,y:e.cp1y},a=fi(t,n,i),r=fi(n,o,i),l=fi(o,e,i),h=fi(a,r,i),c=fi(r,l,i);return fi(h,c,i)}const mi=/^(normal|(\d+(?:\.\d+)?)(px|em|%)?)$/,bi=/^(normal|italic|initial|inherit|unset|(oblique( -?[0-9]?[0-9]deg)?))$/;function xi(t,e){const i=(""+t).match(mi);if(!i||"normal"===i[1])return 1.2*e;switch(t=+i[2],i[3]){case"px":return t;case"%":t/=100}return e*t}function _i(t,e){const i={},s=o(e),n=s?Object.keys(e):e,a=o(t)?s?i=>l(t[i],t[e[i]]):e=>t[e]:()=>t;for(const t of n)i[t]=+a(t)||0;return i}function yi(t){return _i(t,{top:"y",right:"x",bottom:"y",left:"x"})}function vi(t){return _i(t,["topLeft","topRight","bottomLeft","bottomRight"])}function Mi(t){const e=yi(t);return e.width=e.left+e.right,e.height=e.top+e.bottom,e}function wi(t,e){t=t||{},e=e||ue.font;let i=l(t.size,e.size);"string"==typeof i&&(i=parseInt(i,10));let s=l(t.style,e.style);s&&!(""+s).match(bi)&&(console.warn('Invalid font style specified: "'+s+'"'),s=void 0);const n={family:l(t.family,e.family),lineHeight:xi(l(t.lineHeight,e.lineHeight),i),size:i,style:s,weight:l(t.weight,e.weight),string:""};return n.string=Pe(n),n}function ki(t,e,i,s){let o,a,r,l=!0;for(o=0,a=t.length;oi&&0===t?0:t+e;return{min:a(s,-Math.abs(o)),max:a(n,o)}}function Pi(t,e){return Object.assign(Object.create(t),e)}function Di(t,e,i){return t?function(t,e){return{x:i=>t+t+e-i,setWidth(t){e=t},textAlign:t=>"center"===t?t:"right"===t?"left":"right",xPlus:(t,e)=>t-e,leftForLtr:(t,e)=>t-e}}(e,i):{x:t=>t,setWidth(t){},textAlign:t=>t,xPlus:(t,e)=>t+e,leftForLtr:(t,e)=>t}}function Ci(t,e){let i,s;"ltr"!==e&&"rtl"!==e||(i=t.canvas.style,s=[i.getPropertyValue("direction"),i.getPropertyPriority("direction")],i.setProperty("direction",e,"important"),t.prevTextDirection=s)}function Oi(t,e){void 0!==e&&(delete t.prevTextDirection,t.canvas.style.setProperty("direction",e[0],e[1]))}function Ai(t){return"angle"===t?{between:Z,compare:K,normalize:G}:{between:tt,compare:(t,e)=>t-e,normalize:t=>t}}function Ti({start:t,end:e,count:i,loop:s,style:n}){return{start:t%i,end:e%i,loop:s&&(e-t+1)%i==0,style:n}}function Li(t,e,i){if(!i)return[t];const{property:s,start:n,end:o}=i,a=e.length,{compare:r,between:l,normalize:h}=Ai(s),{start:c,end:d,loop:u,style:f}=function(t,e,i){const{property:s,start:n,end:o}=i,{between:a,normalize:r}=Ai(s),l=e.length;let h,c,{start:d,end:u,loop:f}=t;if(f){for(d+=l,u+=l,h=0,c=l;hx||l(n,b,p)&&0!==r(n,b),v=()=>!x||0===r(o,p)||l(o,b,p);for(let t=c,i=c;t<=d;++t)m=e[t%a],m.skip||(p=h(m[s]),p!==b&&(x=l(p,n,o),null===_&&y()&&(_=0===r(p,n)?t:i),null!==_&&v()&&(g.push(Ti({start:_,end:t,loop:u,count:a,style:f})),_=null),i=t,b=p));return null!==_&&g.push(Ti({start:_,end:d,loop:u,count:a,style:f})),g}function Ei(t,e){const i=[],s=t.segments;for(let n=0;nn&&t[o%e].skip;)o--;return o%=e,{start:n,end:o}}(i,n,o,s);if(!0===s)return Ii(t,[{start:a,end:r,loop:o}],i,e);return Ii(t,function(t,e,i,s){const n=t.length,o=[];let a,r=e,l=t[e];for(a=e+1;a<=i;++a){const i=t[a%n];i.skip||i.stop?l.skip||(s=!1,o.push({start:e%n,end:(a-1)%n,loop:s}),e=r=i.stop?a:null):(r=a,l.skip&&(e=a)),l=i}return null!==r&&o.push({start:e%n,end:r%n,loop:s}),o}(i,a,r{t[a](e[i],n)&&(o.push({element:t,datasetIndex:s,index:l}),r=r||t.inRange(e.x,e.y,n))})),s&&!r?[]:o}var Yi={evaluateInteractionItems:Ni,modes:{index(t,e,i,s){const n=ye(e,t),o=i.axis||"x",a=i.includeInvisible||!1,r=i.intersect?Wi(t,n,o,s,a):ji(t,n,o,!1,s,a),l=[];return r.length?(t.getSortedVisibleDatasetMetas().forEach((t=>{const e=r[0].index,i=t.data[e];i&&!i.skip&&l.push({element:i,datasetIndex:t.index,index:e})})),l):[]},dataset(t,e,i,s){const n=ye(e,t),o=i.axis||"xy",a=i.includeInvisible||!1;let r=i.intersect?Wi(t,n,o,s,a):ji(t,n,o,!1,s,a);if(r.length>0){const e=r[0].datasetIndex,i=t.getDatasetMeta(e).data;r=[];for(let t=0;tWi(t,ye(e,t),i.axis||"xy",s,i.includeInvisible||!1),nearest(t,e,i,s){const n=ye(e,t),o=i.axis||"xy",a=i.includeInvisible||!1;return ji(t,n,o,i.intersect,s,a)},x:(t,e,i,s)=>$i(t,ye(e,t),"x",i.intersect,s),y:(t,e,i,s)=>$i(t,ye(e,t),"y",i.intersect,s)}};const Ui=["left","top","right","bottom"];function Xi(t,e){return t.filter((t=>t.pos===e))}function qi(t,e){return t.filter((t=>-1===Ui.indexOf(t.pos)&&t.box.axis===e))}function Ki(t,e){return t.sort(((t,i)=>{const s=e?i:t,n=e?t:i;return s.weight===n.weight?s.index-n.index:s.weight-n.weight}))}function Gi(t,e){const i=function(t){const e={};for(const i of t){const{stack:t,pos:s,stackWeight:n}=i;if(!t||!Ui.includes(s))continue;const o=e[t]||(e[t]={count:0,placed:0,weight:0,size:0});o.count++,o.weight+=n}return e}(t),{vBoxMaxWidth:s,hBoxMaxHeight:n}=e;let o,a,r;for(o=0,a=t.length;o{s[t]=Math.max(e[t],i[t])})),s}return s(t?["left","right"]:["top","bottom"])}function es(t,e,i,s){const n=[];let o,a,r,l,h,c;for(o=0,a=t.length,h=0;ot.box.fullSize)),!0),s=Ki(Xi(e,"left"),!0),n=Ki(Xi(e,"right")),o=Ki(Xi(e,"top"),!0),a=Ki(Xi(e,"bottom")),r=qi(e,"x"),l=qi(e,"y");return{fullSize:i,leftAndTop:s.concat(o),rightAndBottom:n.concat(l).concat(a).concat(r),chartArea:Xi(e,"chartArea"),vertical:s.concat(n).concat(l),horizontal:o.concat(a).concat(r)}}(t.boxes),l=r.vertical,h=r.horizontal;u(t.boxes,(t=>{"function"==typeof t.beforeLayout&&t.beforeLayout()}));const c=l.reduce(((t,e)=>e.box.options&&!1===e.box.options.display?t:t+1),0)||1,d=Object.freeze({outerWidth:e,outerHeight:i,padding:n,availableWidth:o,availableHeight:a,vBoxMaxWidth:o/2/c,hBoxMaxHeight:a/2}),f=Object.assign({},n);Ji(f,Mi(s));const g=Object.assign({maxPadding:f,w:o,h:a,x:n.left,y:n.top},n),p=Gi(l.concat(h),d);es(r.fullSize,g,d,p),es(l,g,d,p),es(h,g,d,p)&&es(l,g,d,p),function(t){const e=t.maxPadding;function i(i){const s=Math.max(e[i]-t[i],0);return t[i]+=s,s}t.y+=i("top"),t.x+=i("left"),i("right"),i("bottom")}(g),ss(r.leftAndTop,g,d,p),g.x+=g.w,g.y+=g.h,ss(r.rightAndBottom,g,d,p),t.chartArea={left:g.left,top:g.top,right:g.left+g.w,bottom:g.top+g.h,height:g.h,width:g.w},u(r.chartArea,(e=>{const i=e.box;Object.assign(i,t.chartArea),i.update(g.w,g.h,{left:0,top:0,right:0,bottom:0})}))}};class os{acquireContext(t,e){}releaseContext(t){return!1}addEventListener(t,e,i){}removeEventListener(t,e,i){}getDevicePixelRatio(){return 1}getMaximumSize(t,e,i,s){return e=Math.max(0,e||t.width),i=i||t.height,{width:e,height:Math.max(0,s?Math.floor(e/s):i)}}isAttached(t){return!0}updateConfig(t){}}class as extends os{acquireContext(t){return t&&t.getContext&&t.getContext("2d")||null}updateConfig(t){t.options.animation=!1}}const rs={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"},ls=t=>null===t||""===t;const hs=!!ke&&{passive:!0};function cs(t,e,i){t.canvas.removeEventListener(e,i,hs)}function ds(t,e){for(const i of t)if(i===e||i.contains(e))return!0}function us(t,e,i){const s=t.canvas,n=new MutationObserver((t=>{let e=!1;for(const i of t)e=e||ds(i.addedNodes,s),e=e&&!ds(i.removedNodes,s);e&&i()}));return n.observe(document,{childList:!0,subtree:!0}),n}function fs(t,e,i){const s=t.canvas,n=new MutationObserver((t=>{let e=!1;for(const i of t)e=e||ds(i.removedNodes,s),e=e&&!ds(i.addedNodes,s);e&&i()}));return n.observe(document,{childList:!0,subtree:!0}),n}const gs=new Map;let ps=0;function ms(){const t=window.devicePixelRatio;t!==ps&&(ps=t,gs.forEach(((e,i)=>{i.currentDevicePixelRatio!==t&&e()})))}function bs(t,e,i){const s=t.canvas,n=s&&ge(s);if(!n)return;const o=ct(((t,e)=>{const s=n.clientWidth;i(t,e),s{const e=t[0],i=e.contentRect.width,s=e.contentRect.height;0===i&&0===s||o(i,s)}));return a.observe(n),function(t,e){gs.size||window.addEventListener("resize",ms),gs.set(t,e)}(t,o),a}function xs(t,e,i){i&&i.disconnect(),"resize"===e&&function(t){gs.delete(t),gs.size||window.removeEventListener("resize",ms)}(t)}function _s(t,e,i){const s=t.canvas,n=ct((e=>{null!==t.ctx&&i(function(t,e){const i=rs[t.type]||t.type,{x:s,y:n}=ye(t,e);return{type:i,chart:e,native:t,x:void 0!==s?s:null,y:void 0!==n?n:null}}(e,t))}),t);return function(t,e,i){t.addEventListener(e,i,hs)}(s,e,n),n}class ys extends os{acquireContext(t,e){const i=t&&t.getContext&&t.getContext("2d");return i&&i.canvas===t?(function(t,e){const i=t.style,s=t.getAttribute("height"),n=t.getAttribute("width");if(t.$chartjs={initial:{height:s,width:n,style:{display:i.display,height:i.height,width:i.width}}},i.display=i.display||"block",i.boxSizing=i.boxSizing||"border-box",ls(n)){const e=Se(t,"width");void 0!==e&&(t.width=e)}if(ls(s))if(""===t.style.height)t.height=t.width/(e||2);else{const e=Se(t,"height");void 0!==e&&(t.height=e)}}(t,e),i):null}releaseContext(t){const e=t.canvas;if(!e.$chartjs)return!1;const i=e.$chartjs.initial;["height","width"].forEach((t=>{const n=i[t];s(n)?e.removeAttribute(t):e.setAttribute(t,n)}));const n=i.style||{};return Object.keys(n).forEach((t=>{e.style[t]=n[t]})),e.width=e.width,delete e.$chartjs,!0}addEventListener(t,e,i){this.removeEventListener(t,e);const s=t.$proxies||(t.$proxies={}),n={attach:us,detach:fs,resize:bs}[e]||_s;s[e]=n(t,e,i)}removeEventListener(t,e){const i=t.$proxies||(t.$proxies={}),s=i[e];if(!s)return;({attach:xs,detach:xs,resize:xs}[e]||cs)(t,e,s),i[e]=void 0}getDevicePixelRatio(){return window.devicePixelRatio}getMaximumSize(t,e,i,s){return Me(t,e,i,s)}isAttached(t){const e=ge(t);return!(!e||!e.isConnected)}}function vs(t){return!fe()||"undefined"!=typeof OffscreenCanvas&&t instanceof OffscreenCanvas?as:ys}var Ms=Object.freeze({__proto__:null,_detectPlatform:vs,BasePlatform:os,BasicPlatform:as,DomPlatform:ys});const ws="transparent",ks={boolean:(t,e,i)=>i>.5?e:t,color(t,e,i){const s=Qt(t||ws),n=s.valid&&Qt(e||ws);return n&&n.valid?n.mix(s,i).hexString():e},number:(t,e,i)=>t+(e-t)*i};class Ss{constructor(t,e,i,s){const n=e[i];s=ki([t.to,s,n,t.from]);const o=ki([t.from,n,s]);this._active=!0,this._fn=t.fn||ks[t.type||typeof o],this._easing=ui[t.easing]||ui.linear,this._start=Math.floor(Date.now()+(t.delay||0)),this._duration=this._total=Math.floor(t.duration),this._loop=!!t.loop,this._target=e,this._prop=i,this._from=o,this._to=s,this._promises=void 0}active(){return this._active}update(t,e,i){if(this._active){this._notify(!1);const s=this._target[this._prop],n=i-this._start,o=this._duration-n;this._start=i,this._duration=Math.floor(Math.max(o,t.duration)),this._total+=n,this._loop=!!t.loop,this._to=ki([t.to,e,s,t.from]),this._from=ki([t.from,s,e])}}cancel(){this._active&&(this.tick(Date.now()),this._active=!1,this._notify(!1))}tick(t){const e=t-this._start,i=this._duration,s=this._prop,n=this._from,o=this._loop,a=this._to;let r;if(this._active=n!==a&&(o||e1?2-r:r,r=this._easing(Math.min(1,Math.max(0,r))),this._target[s]=this._fn(n,a,r))}wait(){const t=this._promises||(this._promises=[]);return new Promise(((e,i)=>{t.push({res:e,rej:i})}))}_notify(t){const e=t?"res":"rej",i=this._promises||[];for(let t=0;t{const a=t[s];if(!o(a))return;const r={};for(const t of e)r[t]=a[t];(n(a.properties)&&a.properties||[s]).forEach((t=>{t!==s&&i.has(t)||i.set(t,r)}))}))}_animateOptions(t,e){const i=e.options,s=function(t,e){if(!e)return;let i=t.options;if(!i)return void(t.options=e);i.$shared&&(t.options=i=Object.assign({},i,{$shared:!1,$animations:{}}));return i}(t,i);if(!s)return[];const n=this._createAnimations(s,i);return i.$shared&&function(t,e){const i=[],s=Object.keys(e);for(let e=0;e{t.options=i}),(()=>{})),n}_createAnimations(t,e){const i=this._properties,s=[],n=t.$animations||(t.$animations={}),o=Object.keys(e),a=Date.now();let r;for(r=o.length-1;r>=0;--r){const l=o[r];if("$"===l.charAt(0))continue;if("options"===l){s.push(...this._animateOptions(t,e));continue}const h=e[l];let c=n[l];const d=i.get(l);if(c){if(d&&c.active()){c.update(d,h,a);continue}c.cancel()}d&&d.duration?(n[l]=c=new Ss(d,t,l,h),s.push(c)):t[l]=h}return s}update(t,e){if(0===this._properties.size)return void Object.assign(t,e);const i=this._createAnimations(t,e);return i.length?(xt.add(this._chart,i),!0):void 0}}function Ds(t,e){const i=t&&t.options||{},s=i.reverse,n=void 0===i.min?e:0,o=void 0===i.max?e:0;return{start:s?o:n,end:s?n:o}}function Cs(t,e){const i=[],s=t._getSortedDatasetMetas(e);let n,o;for(n=0,o=s.length;n0||!i&&e<0)return n.index}return null}function Es(t,e){const{chart:i,_cachedMeta:s}=t,n=i._stacks||(i._stacks={}),{iScale:o,vScale:a,index:r}=s,l=o.axis,h=a.axis,c=function(t,e,i){return`${t.id}.${e.id}.${i.stack||i.type}`}(o,a,s),d=e.length;let u;for(let t=0;ti[t].axis===e)).shift()}function Is(t,e){const i=t.controller.index,s=t.vScale&&t.vScale.axis;if(s){e=e||t._parsed;for(const t of e){const e=t._stacks;if(!e||void 0===e[s]||void 0===e[s][i])return;delete e[s][i],void 0!==e[s]._visualValues&&void 0!==e[s]._visualValues[i]&&delete e[s]._visualValues[i]}}}const zs=t=>"reset"===t||"none"===t,Fs=(t,e)=>e?t:Object.assign({},t);class Vs{static defaults={};static datasetElementType=null;static dataElementType=null;constructor(t,e){this.chart=t,this._ctx=t.ctx,this.index=e,this._cachedDataOpts={},this._cachedMeta=this.getMeta(),this._type=this._cachedMeta.type,this.options=void 0,this._parsing=!1,this._data=void 0,this._objectData=void 0,this._sharedOptions=void 0,this._drawStart=void 0,this._drawCount=void 0,this.enableOptionSharing=!1,this.supportsDecimation=!1,this.$context=void 0,this._syncList=[],this.datasetElementType=new.target.datasetElementType,this.dataElementType=new.target.dataElementType,this.initialize()}initialize(){const t=this._cachedMeta;this.configure(),this.linkScales(),t._stacked=As(t.vScale,t),this.addElements(),this.options.fill&&!this.chart.isPluginEnabled("filler")&&console.warn("Tried to use the 'fill' option without the 'Filler' plugin enabled. Please import and register the 'Filler' plugin and make sure it is not disabled in the options")}updateIndex(t){this.index!==t&&Is(this._cachedMeta),this.index=t}linkScales(){const t=this.chart,e=this._cachedMeta,i=this.getDataset(),s=(t,e,i,s)=>"x"===t?e:"r"===t?s:i,n=e.xAxisID=l(i.xAxisID,Rs(t,"x")),o=e.yAxisID=l(i.yAxisID,Rs(t,"y")),a=e.rAxisID=l(i.rAxisID,Rs(t,"r")),r=e.indexAxis,h=e.iAxisID=s(r,n,o,a),c=e.vAxisID=s(r,o,n,a);e.xScale=this.getScaleForId(n),e.yScale=this.getScaleForId(o),e.rScale=this.getScaleForId(a),e.iScale=this.getScaleForId(h),e.vScale=this.getScaleForId(c)}getDataset(){return this.chart.data.datasets[this.index]}getMeta(){return this.chart.getDatasetMeta(this.index)}getScaleForId(t){return this.chart.scales[t]}_getOtherScale(t){const e=this._cachedMeta;return t===e.iScale?e.vScale:e.iScale}reset(){this._update("reset")}_destroy(){const t=this._cachedMeta;this._data&&rt(this._data,this),t._stacked&&Is(t)}_dataCheck(){const t=this.getDataset(),e=t.data||(t.data=[]),i=this._data;if(o(e))this._data=function(t){const e=Object.keys(t),i=new Array(e.length);let s,n,o;for(s=0,n=e.length;s0&&i._parsed[t-1];if(!1===this._parsing)i._parsed=s,i._sorted=!0,d=s;else{d=n(s[t])?this.parseArrayData(i,s,t,e):o(s[t])?this.parseObjectData(i,s,t,e):this.parsePrimitiveData(i,s,t,e);const a=()=>null===c[l]||f&&c[l]t&&!e.hidden&&e._stacked&&{keys:Cs(i,!0),values:null})(e,i,this.chart),h={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY},{min:c,max:d}=function(t){const{min:e,max:i,minDefined:s,maxDefined:n}=t.getUserBounds();return{min:s?e:Number.NEGATIVE_INFINITY,max:n?i:Number.POSITIVE_INFINITY}}(r);let u,f;function g(){f=s[u];const e=f[r.axis];return!a(f[t.axis])||c>e||d=0;--u)if(!g()){this.updateRangeFromParsed(h,t,f,l);break}return h}getAllParsedValues(t){const e=this._cachedMeta._parsed,i=[];let s,n,o;for(s=0,n=e.length;s=0&&tthis.getContext(i,s,e)),c);return f.$shared&&(f.$shared=r,n[o]=Object.freeze(Fs(f,r))),f}_resolveAnimations(t,e,i){const s=this.chart,n=this._cachedDataOpts,o=`animation-${e}`,a=n[o];if(a)return a;let r;if(!1!==s.options.animation){const s=this.chart.config,n=s.datasetAnimationScopeKeys(this._type,e),o=s.getOptionScopes(this.getDataset(),n);r=s.createResolver(o,this.getContext(t,i,e))}const l=new Ps(s,r&&r.animations);return r&&r._cacheable&&(n[o]=Object.freeze(l)),l}getSharedOptions(t){if(t.$shared)return this._sharedOptions||(this._sharedOptions=Object.assign({},t))}includeOptions(t,e){return!e||zs(t)||this.chart._animationsDisabled}_getSharedOptions(t,e){const i=this.resolveDataElementOptions(t,e),s=this._sharedOptions,n=this.getSharedOptions(i),o=this.includeOptions(e,n)||n!==s;return this.updateSharedOptions(n,e,i),{sharedOptions:n,includeOptions:o}}updateElement(t,e,i,s){zs(s)?Object.assign(t,i):this._resolveAnimations(e,s).update(t,i)}updateSharedOptions(t,e,i){t&&!zs(e)&&this._resolveAnimations(void 0,e).update(t,i)}_setStyle(t,e,i,s){t.active=s;const n=this.getStyle(e,s);this._resolveAnimations(e,i,s).update(t,{options:!s&&this.getSharedOptions(n)||n})}removeHoverStyle(t,e,i){this._setStyle(t,i,"active",!1)}setHoverStyle(t,e,i){this._setStyle(t,i,"active",!0)}_removeDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!1)}_setDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!0)}_resyncElements(t){const e=this._data,i=this._cachedMeta.data;for(const[t,e,i]of this._syncList)this[t](e,i);this._syncList=[];const s=i.length,n=e.length,o=Math.min(n,s);o&&this.parse(0,o),n>s?this._insertElements(s,n-s,t):n{for(t.length+=e,a=t.length-1;a>=o;a--)t[a]=t[a-e]};for(r(n),a=t;a{s[t]=i[t]&&i[t].active()?i[t]._to:this[t]})),s}}function Ns(t,e){const i=t.options.ticks,n=function(t){const e=t.options.offset,i=t._tickSize(),s=t._length/i+(e?0:1),n=t._maxLength/i;return Math.floor(Math.min(s,n))}(t),o=Math.min(i.maxTicksLimit||n,n),a=i.major.enabled?function(t){const e=[];let i,s;for(i=0,s=t.length;io)return function(t,e,i,s){let n,o=0,a=i[0];for(s=Math.ceil(s),n=0;nn)return e}return Math.max(n,1)}(a,e,o);if(r>0){let t,i;const n=r>1?Math.round((h-l)/(r-1)):null;for(Ws(e,c,d,s(n)?0:l-n,l),t=0,i=r-1;t"top"===e||"left"===e?t[e]+i:t[e]-i;function js(t,e){const i=[],s=t.length/e,n=t.length;let o=0;for(;oa+r)))return h}function Ys(t){return t.drawTicks?t.tickLength:0}function Us(t,e){if(!t.display)return 0;const i=wi(t.font,e),s=Mi(t.padding);return(n(t.text)?t.text.length:1)*i.lineHeight+s.height}function Xs(t,e,i){let s=ut(t);return(i&&"right"!==e||!i&&"right"===e)&&(s=(t=>"left"===t?"right":"right"===t?"left":t)(s)),s}class qs extends Bs{constructor(t){super(),this.id=t.id,this.type=t.type,this.options=void 0,this.ctx=t.ctx,this.chart=t.chart,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this._margins={left:0,right:0,top:0,bottom:0},this.maxWidth=void 0,this.maxHeight=void 0,this.paddingTop=void 0,this.paddingBottom=void 0,this.paddingLeft=void 0,this.paddingRight=void 0,this.axis=void 0,this.labelRotation=void 0,this.min=void 0,this.max=void 0,this._range=void 0,this.ticks=[],this._gridLineItems=null,this._labelItems=null,this._labelSizes=null,this._length=0,this._maxLength=0,this._longestTextCache={},this._startPixel=void 0,this._endPixel=void 0,this._reversePixels=!1,this._userMax=void 0,this._userMin=void 0,this._suggestedMax=void 0,this._suggestedMin=void 0,this._ticksLength=0,this._borderValue=0,this._cache={},this._dataLimitsCached=!1,this.$context=void 0}init(t){this.options=t.setContext(this.getContext()),this.axis=t.axis,this._userMin=this.parse(t.min),this._userMax=this.parse(t.max),this._suggestedMin=this.parse(t.suggestedMin),this._suggestedMax=this.parse(t.suggestedMax)}parse(t,e){return t}getUserBounds(){let{_userMin:t,_userMax:e,_suggestedMin:i,_suggestedMax:s}=this;return t=r(t,Number.POSITIVE_INFINITY),e=r(e,Number.NEGATIVE_INFINITY),i=r(i,Number.POSITIVE_INFINITY),s=r(s,Number.NEGATIVE_INFINITY),{min:r(t,i),max:r(e,s),minDefined:a(t),maxDefined:a(e)}}getMinMax(t){let e,{min:i,max:s,minDefined:n,maxDefined:o}=this.getUserBounds();if(n&&o)return{min:i,max:s};const a=this.getMatchingVisibleMetas();for(let r=0,l=a.length;rs?s:i,s=n&&i>s?i:s,{min:r(i,r(s,i)),max:r(s,r(i,s))}}getPadding(){return{left:this.paddingLeft||0,top:this.paddingTop||0,right:this.paddingRight||0,bottom:this.paddingBottom||0}}getTicks(){return this.ticks}getLabels(){const t=this.chart.data;return this.options.labels||(this.isHorizontal()?t.xLabels:t.yLabels)||t.labels||[]}getLabelItems(t=this.chart.chartArea){return this._labelItems||(this._labelItems=this._computeLabelItems(t))}beforeLayout(){this._cache={},this._dataLimitsCached=!1}beforeUpdate(){d(this.options.beforeUpdate,[this])}update(t,e,i){const{beginAtZero:s,grace:n,ticks:o}=this.options,a=o.sampleSize;this.beforeUpdate(),this.maxWidth=t,this.maxHeight=e,this._margins=i=Object.assign({left:0,right:0,top:0,bottom:0},i),this.ticks=null,this._labelSizes=null,this._gridLineItems=null,this._labelItems=null,this.beforeSetDimensions(),this.setDimensions(),this.afterSetDimensions(),this._maxLength=this.isHorizontal()?this.width+i.left+i.right:this.height+i.top+i.bottom,this._dataLimitsCached||(this.beforeDataLimits(),this.determineDataLimits(),this.afterDataLimits(),this._range=Si(this,n,s),this._dataLimitsCached=!0),this.beforeBuildTicks(),this.ticks=this.buildTicks()||[],this.afterBuildTicks();const r=a=n||i<=1||!this.isHorizontal())return void(this.labelRotation=s);const h=this._getLabelSizes(),c=h.widest.width,d=h.highest.height,u=J(this.chart.width-c,0,this.maxWidth);o=t.offset?this.maxWidth/i:u/(i-1),c+6>o&&(o=u/(i-(t.offset?.5:1)),a=this.maxHeight-Ys(t.grid)-e.padding-Us(t.title,this.chart.options.font),r=Math.sqrt(c*c+d*d),l=Y(Math.min(Math.asin(J((h.highest.height+6)/o,-1,1)),Math.asin(J(a/r,-1,1))-Math.asin(J(d/r,-1,1)))),l=Math.max(s,Math.min(n,l))),this.labelRotation=l}afterCalculateLabelRotation(){d(this.options.afterCalculateLabelRotation,[this])}afterAutoSkip(){}beforeFit(){d(this.options.beforeFit,[this])}fit(){const t={width:0,height:0},{chart:e,options:{ticks:i,title:s,grid:n}}=this,o=this._isVisible(),a=this.isHorizontal();if(o){const o=Us(s,e.options.font);if(a?(t.width=this.maxWidth,t.height=Ys(n)+o):(t.height=this.maxHeight,t.width=Ys(n)+o),i.display&&this.ticks.length){const{first:e,last:s,widest:n,highest:o}=this._getLabelSizes(),r=2*i.padding,l=$(this.labelRotation),h=Math.cos(l),c=Math.sin(l);if(a){const e=i.mirror?0:c*n.width+h*o.height;t.height=Math.min(this.maxHeight,t.height+e+r)}else{const e=i.mirror?0:h*n.width+c*o.height;t.width=Math.min(this.maxWidth,t.width+e+r)}this._calculatePadding(e,s,c,h)}}this._handleMargins(),a?(this.width=this._length=e.width-this._margins.left-this._margins.right,this.height=t.height):(this.width=t.width,this.height=this._length=e.height-this._margins.top-this._margins.bottom)}_calculatePadding(t,e,i,s){const{ticks:{align:n,padding:o},position:a}=this.options,r=0!==this.labelRotation,l="top"!==a&&"x"===this.axis;if(this.isHorizontal()){const a=this.getPixelForTick(0)-this.left,h=this.right-this.getPixelForTick(this.ticks.length-1);let c=0,d=0;r?l?(c=s*t.width,d=i*e.height):(c=i*t.height,d=s*e.width):"start"===n?d=e.width:"end"===n?c=t.width:"inner"!==n&&(c=t.width/2,d=e.width/2),this.paddingLeft=Math.max((c-a+o)*this.width/(this.width-a),0),this.paddingRight=Math.max((d-h+o)*this.width/(this.width-h),0)}else{let i=e.height/2,s=t.height/2;"start"===n?(i=0,s=t.height):"end"===n&&(i=e.height,s=0),this.paddingTop=i+o,this.paddingBottom=s+o}}_handleMargins(){this._margins&&(this._margins.left=Math.max(this.paddingLeft,this._margins.left),this._margins.top=Math.max(this.paddingTop,this._margins.top),this._margins.right=Math.max(this.paddingRight,this._margins.right),this._margins.bottom=Math.max(this.paddingBottom,this._margins.bottom))}afterFit(){d(this.options.afterFit,[this])}isHorizontal(){const{axis:t,position:e}=this.options;return"top"===e||"bottom"===e||"x"===t}isFullSize(){return this.options.fullSize}_convertTicksToLabels(t){let e,i;for(this.beforeTickToLabelConversion(),this.generateTickLabels(t),e=0,i=t.length;e{const i=t.gc,s=i.length/2;let n;if(s>e){for(n=0;n({width:a[t]||0,height:r[t]||0});return{first:k(0),last:k(e-1),widest:k(M),highest:k(w),widths:a,heights:r}}getLabelForValue(t){return t}getPixelForValue(t,e){return NaN}getValueForPixel(t){}getPixelForTick(t){const e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getPixelForDecimal(t){this._reversePixels&&(t=1-t);const e=this._startPixel+t*this._length;return Q(this._alignToPixels?Oe(this.chart,e,0):e)}getDecimalForPixel(t){const e=(t-this._startPixel)/this._length;return this._reversePixels?1-e:e}getBasePixel(){return this.getPixelForValue(this.getBaseValue())}getBaseValue(){const{min:t,max:e}=this;return t<0&&e<0?e:t>0&&e>0?t:0}getContext(t){const e=this.ticks||[];if(t>=0&&ta*s?a/i:r/s:r*s0}_computeGridLineItems(t){const e=this.axis,i=this.chart,s=this.options,{grid:n,position:a,border:r}=s,h=n.offset,c=this.isHorizontal(),d=this.ticks.length+(h?1:0),u=Ys(n),f=[],g=r.setContext(this.getContext()),p=g.display?g.width:0,m=p/2,b=function(t){return Oe(i,t,p)};let x,_,y,v,M,w,k,S,P,D,C,O;if("top"===a)x=b(this.bottom),w=this.bottom-u,S=x-m,D=b(t.top)+m,O=t.bottom;else if("bottom"===a)x=b(this.top),D=t.top,O=b(t.bottom)-m,w=x+m,S=this.top+u;else if("left"===a)x=b(this.right),M=this.right-u,k=x-m,P=b(t.left)+m,C=t.right;else if("right"===a)x=b(this.left),P=t.left,C=b(t.right)-m,M=x+m,k=this.left+u;else if("x"===e){if("center"===a)x=b((t.top+t.bottom)/2+.5);else if(o(a)){const t=Object.keys(a)[0],e=a[t];x=b(this.chart.scales[t].getPixelForValue(e))}D=t.top,O=t.bottom,w=x+m,S=w+u}else if("y"===e){if("center"===a)x=b((t.left+t.right)/2);else if(o(a)){const t=Object.keys(a)[0],e=a[t];x=b(this.chart.scales[t].getPixelForValue(e))}M=x-m,k=M-u,P=t.left,C=t.right}const A=l(s.ticks.maxTicksLimit,d),T=Math.max(1,Math.ceil(d/A));for(_=0;_e.value===t));if(i>=0){return e.setContext(this.getContext(i)).lineWidth}return 0}drawGrid(t){const e=this.options.grid,i=this.ctx,s=this._gridLineItems||(this._gridLineItems=this._computeGridLineItems(t));let n,o;const a=(t,e,s)=>{s.width&&s.color&&(i.save(),i.lineWidth=s.width,i.strokeStyle=s.color,i.setLineDash(s.borderDash||[]),i.lineDashOffset=s.borderDashOffset,i.beginPath(),i.moveTo(t.x,t.y),i.lineTo(e.x,e.y),i.stroke(),i.restore())};if(e.display)for(n=0,o=s.length;n{this.drawBackground(),this.drawGrid(t),this.drawTitle()}},{z:s,draw:()=>{this.drawBorder()}},{z:e,draw:t=>{this.drawLabels(t)}}]:[{z:e,draw:t=>{this.draw(t)}}]}getMatchingVisibleMetas(t){const e=this.chart.getSortedVisibleDatasetMetas(),i=this.axis+"AxisID",s=[];let n,o;for(n=0,o=e.length;n{const s=i.split("."),n=s.pop(),o=[t].concat(s).join("."),a=e[i].split("."),r=a.pop(),l=a.join(".");ue.route(o,n,l,r)}))}(e,t.defaultRoutes);t.descriptors&&ue.describe(e,t.descriptors)}(t,o,i),this.override&&ue.override(t.id,t.overrides)),o}get(t){return this.items[t]}unregister(t){const e=this.items,i=t.id,s=this.scope;i in e&&delete e[i],s&&i in ue[s]&&(delete ue[s][i],this.override&&delete re[i])}}class Gs{constructor(){this.controllers=new Ks(Vs,"datasets",!0),this.elements=new Ks(Bs,"elements"),this.plugins=new Ks(Object,"plugins"),this.scales=new Ks(qs,"scales"),this._typedRegistries=[this.controllers,this.scales,this.elements]}add(...t){this._each("register",t)}remove(...t){this._each("unregister",t)}addControllers(...t){this._each("register",t,this.controllers)}addElements(...t){this._each("register",t,this.elements)}addPlugins(...t){this._each("register",t,this.plugins)}addScales(...t){this._each("register",t,this.scales)}getController(t){return this._get(t,this.controllers,"controller")}getElement(t){return this._get(t,this.elements,"element")}getPlugin(t){return this._get(t,this.plugins,"plugin")}getScale(t){return this._get(t,this.scales,"scale")}removeControllers(...t){this._each("unregister",t,this.controllers)}removeElements(...t){this._each("unregister",t,this.elements)}removePlugins(...t){this._each("unregister",t,this.plugins)}removeScales(...t){this._each("unregister",t,this.scales)}_each(t,e,i){[...e].forEach((e=>{const s=i||this._getRegistryForType(e);i||s.isForType(e)||s===this.plugins&&e.id?this._exec(t,s,e):u(e,(e=>{const s=i||this._getRegistryForType(e);this._exec(t,s,e)}))}))}_exec(t,e,i){const s=w(t);d(i["before"+s],[],i),e[t](i),d(i["after"+s],[],i)}_getRegistryForType(t){for(let e=0;et.filter((t=>!e.some((e=>t.plugin.id===e.plugin.id))));this._notify(s(e,i),t,"stop"),this._notify(s(i,e),t,"start")}}function Qs(t,e){return e||!1!==t?!0===t?{}:t:null}function tn(t,{plugin:e,local:i},s,n){const o=t.pluginScopeKeys(e),a=t.getOptionScopes(s,o);return i&&e.defaults&&a.push(e.defaults),t.createResolver(a,n,[""],{scriptable:!1,indexable:!1,allKeys:!0})}function en(t,e){const i=ue.datasets[t]||{};return((e.datasets||{})[t]||{}).indexAxis||e.indexAxis||i.indexAxis||"x"}function sn(t,e){if("x"===t||"y"===t||"r"===t)return t;var i;if(t=e.axis||("top"===(i=e.position)||"bottom"===i?"x":"left"===i||"right"===i?"y":void 0)||t.length>1&&sn(t[0].toLowerCase(),e))return t;throw new Error(`Cannot determine type of '${name}' axis. Please provide 'axis' or 'position' option.`)}function nn(t){const e=t.options||(t.options={});e.plugins=l(e.plugins,{}),e.scales=function(t,e){const i=re[t.type]||{scales:{}},s=e.scales||{},n=en(t.type,e),a=Object.create(null);return Object.keys(s).forEach((t=>{const e=s[t];if(!o(e))return console.error(`Invalid scale configuration for scale: ${t}`);if(e._proxy)return console.warn(`Ignoring resolver passed as options for scale: ${t}`);const r=sn(t,e),l=function(t,e){return t===e?"_index_":"_value_"}(r,n),h=i.scales||{};a[t]=x(Object.create(null),[{axis:r},e,h[r],h[l]])})),t.data.datasets.forEach((i=>{const n=i.type||t.type,o=i.indexAxis||en(n,e),r=(re[n]||{}).scales||{};Object.keys(r).forEach((t=>{const e=function(t,e){let i=t;return"_index_"===t?i=e:"_value_"===t&&(i="x"===e?"y":"x"),i}(t,o),n=i[e+"AxisID"]||e;a[n]=a[n]||Object.create(null),x(a[n],[{axis:e},s[n],r[t]])}))})),Object.keys(a).forEach((t=>{const e=a[t];x(e,[ue.scales[e.type],ue.scale])})),a}(t,e)}function on(t){return(t=t||{}).datasets=t.datasets||[],t.labels=t.labels||[],t}const an=new Map,rn=new Set;function ln(t,e){let i=an.get(t);return i||(i=e(),an.set(t,i),rn.add(i)),i}const hn=(t,e,i)=>{const s=M(e,i);void 0!==s&&t.add(s)};class cn{constructor(t){this._config=function(t){return(t=t||{}).data=on(t.data),nn(t),t}(t),this._scopeCache=new Map,this._resolverCache=new Map}get platform(){return this._config.platform}get type(){return this._config.type}set type(t){this._config.type=t}get data(){return this._config.data}set data(t){this._config.data=on(t)}get options(){return this._config.options}set options(t){this._config.options=t}get plugins(){return this._config.plugins}update(){const t=this._config;this.clearCache(),nn(t)}clearCache(){this._scopeCache.clear(),this._resolverCache.clear()}datasetScopeKeys(t){return ln(t,(()=>[[`datasets.${t}`,""]]))}datasetAnimationScopeKeys(t,e){return ln(`${t}.transition.${e}`,(()=>[[`datasets.${t}.transitions.${e}`,`transitions.${e}`],[`datasets.${t}`,""]]))}datasetElementScopeKeys(t,e){return ln(`${t}-${e}`,(()=>[[`datasets.${t}.elements.${e}`,`datasets.${t}`,`elements.${e}`,""]]))}pluginScopeKeys(t){const e=t.id;return ln(`${this.type}-plugin-${e}`,(()=>[[`plugins.${e}`,...t.additionalOptionScopes||[]]]))}_cachedScopes(t,e){const i=this._scopeCache;let s=i.get(t);return s&&!e||(s=new Map,i.set(t,s)),s}getOptionScopes(t,e,i){const{options:s,type:n}=this,o=this._cachedScopes(t,i),a=o.get(e);if(a)return a;const r=new Set;e.forEach((e=>{t&&(r.add(t),e.forEach((e=>hn(r,t,e)))),e.forEach((t=>hn(r,s,t))),e.forEach((t=>hn(r,re[n]||{},t))),e.forEach((t=>hn(r,ue,t))),e.forEach((t=>hn(r,le,t)))}));const l=Array.from(r);return 0===l.length&&l.push(Object.create(null)),rn.has(e)&&o.set(e,l),l}chartOptionScopes(){const{options:t,type:e}=this;return[t,re[e]||{},ue.datasets[e]||{},{type:e},ue,le]}resolveNamedOptions(t,e,i,s=[""]){const o={$shared:!0},{resolver:a,subPrefixes:r}=dn(this._resolverCache,t,s);let l=a;if(function(t,e){const{isScriptable:i,isIndexable:s}=$e(t);for(const o of e){const e=i(o),a=s(o),r=(a||e)&&t[o];if(e&&(S(r)||un(r))||a&&n(r))return!0}return!1}(a,e)){o.$shared=!1;l=je(a,i=S(i)?i():i,this.createResolver(t,i,r))}for(const t of e)o[t]=l[t];return o}createResolver(t,e,i=[""],s){const{resolver:n}=dn(this._resolverCache,t,i);return o(e)?je(n,e,void 0,s):n}}function dn(t,e,i){let s=t.get(e);s||(s=new Map,t.set(e,s));const n=i.join();let o=s.get(n);if(!o){o={resolver:He(e,i),subPrefixes:i.filter((t=>!t.toLowerCase().includes("hover")))},s.set(n,o)}return o}const un=t=>o(t)&&Object.getOwnPropertyNames(t).reduce(((e,i)=>e||S(t[i])),!1);const fn=["top","bottom","left","right","chartArea"];function gn(t,e){return"top"===t||"bottom"===t||-1===fn.indexOf(t)&&"x"===e}function pn(t,e){return function(i,s){return i[t]===s[t]?i[e]-s[e]:i[t]-s[t]}}function mn(t){const e=t.chart,i=e.options.animation;e.notifyPlugins("afterRender"),d(i&&i.onComplete,[t],e)}function bn(t){const e=t.chart,i=e.options.animation;d(i&&i.onProgress,[t],e)}function xn(t){return fe()&&"string"==typeof t?t=document.getElementById(t):t&&t.length&&(t=t[0]),t&&t.canvas&&(t=t.canvas),t}const _n={},yn=t=>{const e=xn(t);return Object.values(_n).filter((t=>t.canvas===e)).pop()};function vn(t,e,i){const s=Object.keys(t);for(const n of s){const s=+n;if(s>=e){const o=t[n];delete t[n],(i>0||s>e)&&(t[s+i]=o)}}}class Mn{static defaults=ue;static instances=_n;static overrides=re;static registry=Zs;static version="4.1.1";static getChart=yn;static register(...t){Zs.add(...t),wn()}static unregister(...t){Zs.remove(...t),wn()}constructor(t,e){const s=this.config=new cn(e),n=xn(t),o=yn(n);if(o)throw new Error("Canvas is already in use. Chart with ID '"+o.id+"' must be destroyed before the canvas with ID '"+o.canvas.id+"' can be reused.");const a=s.createResolver(s.chartOptionScopes(),this.getContext());this.platform=new(s.platform||vs(n)),this.platform.updateConfig(s);const r=this.platform.acquireContext(n,a.aspectRatio),l=r&&r.canvas,h=l&&l.height,c=l&&l.width;this.id=i(),this.ctx=r,this.canvas=l,this.width=c,this.height=h,this._options=a,this._aspectRatio=this.aspectRatio,this._layers=[],this._metasets=[],this._stacks=void 0,this.boxes=[],this.currentDevicePixelRatio=void 0,this.chartArea=void 0,this._active=[],this._lastEvent=void 0,this._listeners={},this._responsiveListeners=void 0,this._sortedMetasets=[],this.scales={},this._plugins=new Js,this.$proxies={},this._hiddenIndices={},this.attached=!1,this._animationsDisabled=void 0,this.$context=void 0,this._doResize=dt((t=>this.update(t)),a.resizeDelay||0),this._dataChanges=[],_n[this.id]=this,r&&l?(xt.listen(this,"complete",mn),xt.listen(this,"progress",bn),this._initialize(),this.attached&&this.update()):console.error("Failed to create chart: can't acquire context from the given item")}get aspectRatio(){const{options:{aspectRatio:t,maintainAspectRatio:e},width:i,height:n,_aspectRatio:o}=this;return s(t)?e&&o?o:n?i/n:null:t}get data(){return this.config.data}set data(t){this.config.data=t}get options(){return this._options}set options(t){this.config.options=t}get registry(){return Zs}_initialize(){return this.notifyPlugins("beforeInit"),this.options.responsive?this.resize():we(this,this.options.devicePixelRatio),this.bindEvents(),this.notifyPlugins("afterInit"),this}clear(){return Ae(this.canvas,this.ctx),this}stop(){return xt.stop(this),this}resize(t,e){xt.running(this)?this._resizeBeforeDraw={width:t,height:e}:this._resize(t,e)}_resize(t,e){const i=this.options,s=this.canvas,n=i.maintainAspectRatio&&this.aspectRatio,o=this.platform.getMaximumSize(s,t,e,n),a=i.devicePixelRatio||this.platform.getDevicePixelRatio(),r=this.width?"resize":"attach";this.width=o.width,this.height=o.height,this._aspectRatio=this.aspectRatio,we(this,a,!0)&&(this.notifyPlugins("resize",{size:o}),d(i.onResize,[this,o],this),this.attached&&this._doResize(r)&&this.render())}ensureScalesHaveIDs(){u(this.options.scales||{},((t,e)=>{t.id=e}))}buildOrUpdateScales(){const t=this.options,e=t.scales,i=this.scales,s=Object.keys(i).reduce(((t,e)=>(t[e]=!1,t)),{});let n=[];e&&(n=n.concat(Object.keys(e).map((t=>{const i=e[t],s=sn(t,i),n="r"===s,o="x"===s;return{options:i,dposition:n?"chartArea":o?"bottom":"left",dtype:n?"radialLinear":o?"category":"linear"}})))),u(n,(e=>{const n=e.options,o=n.id,a=sn(o,n),r=l(n.type,e.dtype);void 0!==n.position&&gn(n.position,a)===gn(e.dposition)||(n.position=e.dposition),s[o]=!0;let h=null;if(o in i&&i[o].type===r)h=i[o];else{h=new(Zs.getScale(r))({id:o,type:r,ctx:this.ctx,chart:this}),i[h.id]=h}h.init(n,t)})),u(s,((t,e)=>{t||delete i[e]})),u(i,(t=>{ns.configure(this,t,t.options),ns.addBox(this,t)}))}_updateMetasets(){const t=this._metasets,e=this.data.datasets.length,i=t.length;if(t.sort(((t,e)=>t.index-e.index)),i>e){for(let t=e;te.length&&delete this._stacks,t.forEach(((t,i)=>{0===e.filter((e=>e===t._dataset)).length&&this._destroyDatasetMeta(i)}))}buildOrUpdateControllers(){const t=[],e=this.data.datasets;let i,s;for(this._removeUnreferencedMetasets(),i=0,s=e.length;i{this.getDatasetMeta(e).controller.reset()}),this)}reset(){this._resetElements(),this.notifyPlugins("reset")}update(t){const e=this.config;e.update();const i=this._options=e.createResolver(e.chartOptionScopes(),this.getContext()),s=this._animationsDisabled=!i.animation;if(this._updateScales(),this._checkEventBindings(),this._updateHiddenIndices(),this._plugins.invalidate(),!1===this.notifyPlugins("beforeUpdate",{mode:t,cancelable:!0}))return;const n=this.buildOrUpdateControllers();this.notifyPlugins("beforeElementsUpdate");let o=0;for(let t=0,e=this.data.datasets.length;t{t.reset()})),this._updateDatasets(t),this.notifyPlugins("afterUpdate",{mode:t}),this._layers.sort(pn("z","_idx"));const{_active:a,_lastEvent:r}=this;r?this._eventHandler(r,!0):a.length&&this._updateHoverStyles(a,a,!0),this.render()}_updateScales(){u(this.scales,(t=>{ns.removeBox(this,t)})),this.ensureScalesHaveIDs(),this.buildOrUpdateScales()}_checkEventBindings(){const t=this.options,e=new Set(Object.keys(this._listeners)),i=new Set(t.events);P(e,i)&&!!this._responsiveListeners===t.responsive||(this.unbindEvents(),this.bindEvents())}_updateHiddenIndices(){const{_hiddenIndices:t}=this,e=this._getUniformDataChanges()||[];for(const{method:i,start:s,count:n}of e){vn(t,s,"_removeElements"===i?-n:n)}}_getUniformDataChanges(){const t=this._dataChanges;if(!t||!t.length)return;this._dataChanges=[];const e=this.data.datasets.length,i=e=>new Set(t.filter((t=>t[0]===e)).map(((t,e)=>e+","+t.splice(1).join(",")))),s=i(0);for(let t=1;tt.split(","))).map((t=>({method:t[1],start:+t[2],count:+t[3]})))}_updateLayout(t){if(!1===this.notifyPlugins("beforeLayout",{cancelable:!0}))return;ns.update(this,this.width,this.height,t);const e=this.chartArea,i=e.width<=0||e.height<=0;this._layers=[],u(this.boxes,(t=>{i&&"chartArea"===t.position||(t.configure&&t.configure(),this._layers.push(...t._layers()))}),this),this._layers.forEach(((t,e)=>{t._idx=e})),this.notifyPlugins("afterLayout")}_updateDatasets(t){if(!1!==this.notifyPlugins("beforeDatasetsUpdate",{mode:t,cancelable:!0})){for(let t=0,e=this.data.datasets.length;t=0;--e)this._drawDataset(t[e]);this.notifyPlugins("afterDatasetsDraw")}_drawDataset(t){const e=this.ctx,i=t._clip,s=!i.disabled,n=function(t){const{xScale:e,yScale:i}=t;if(e&&i)return{left:e.left,right:e.right,top:i.top,bottom:i.bottom}}(t)||this.chartArea,o={meta:t,index:t.index,cancelable:!0};!1!==this.notifyPlugins("beforeDatasetDraw",o)&&(s&&Re(e,{left:!1===i.left?0:n.left-i.left,right:!1===i.right?this.width:n.right+i.right,top:!1===i.top?0:n.top-i.top,bottom:!1===i.bottom?this.height:n.bottom+i.bottom}),t.controller.draw(),s&&Ie(e),o.cancelable=!1,this.notifyPlugins("afterDatasetDraw",o))}isPointInArea(t){return Ee(t,this.chartArea,this._minPadding)}getElementsAtEventForMode(t,e,i,s){const n=Yi.modes[e];return"function"==typeof n?n(this,t,i,s):[]}getDatasetMeta(t){const e=this.data.datasets[t],i=this._metasets;let s=i.filter((t=>t&&t._dataset===e)).pop();return s||(s={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null,order:e&&e.order||0,index:t,_dataset:e,_parsed:[],_sorted:!1},i.push(s)),s}getContext(){return this.$context||(this.$context=Pi(null,{chart:this,type:"chart"}))}getVisibleDatasetCount(){return this.getSortedVisibleDatasetMetas().length}isDatasetVisible(t){const e=this.data.datasets[t];if(!e)return!1;const i=this.getDatasetMeta(t);return"boolean"==typeof i.hidden?!i.hidden:!e.hidden}setDatasetVisibility(t,e){this.getDatasetMeta(t).hidden=!e}toggleDataVisibility(t){this._hiddenIndices[t]=!this._hiddenIndices[t]}getDataVisibility(t){return!this._hiddenIndices[t]}_updateVisibility(t,e,i){const s=i?"show":"hide",n=this.getDatasetMeta(t),o=n.controller._resolveAnimations(void 0,s);k(e)?(n.data[e].hidden=!i,this.update()):(this.setDatasetVisibility(t,i),o.update(n,{visible:i}),this.update((e=>e.datasetIndex===t?s:void 0)))}hide(t,e){this._updateVisibility(t,e,!1)}show(t,e){this._updateVisibility(t,e,!0)}_destroyDatasetMeta(t){const e=this._metasets[t];e&&e.controller&&e.controller._destroy(),delete this._metasets[t]}_stop(){let t,e;for(this.stop(),xt.remove(this),t=0,e=this.data.datasets.length;t{e.addEventListener(this,i,s),t[i]=s},s=(t,e,i)=>{t.offsetX=e,t.offsetY=i,this._eventHandler(t)};u(this.options.events,(t=>i(t,s)))}bindResponsiveEvents(){this._responsiveListeners||(this._responsiveListeners={});const t=this._responsiveListeners,e=this.platform,i=(i,s)=>{e.addEventListener(this,i,s),t[i]=s},s=(i,s)=>{t[i]&&(e.removeEventListener(this,i,s),delete t[i])},n=(t,e)=>{this.canvas&&this.resize(t,e)};let o;const a=()=>{s("attach",a),this.attached=!0,this.resize(),i("resize",n),i("detach",o)};o=()=>{this.attached=!1,s("resize",n),this._stop(),this._resize(0,0),i("attach",a)},e.isAttached(this.canvas)?a():o()}unbindEvents(){u(this._listeners,((t,e)=>{this.platform.removeEventListener(this,e,t)})),this._listeners={},u(this._responsiveListeners,((t,e)=>{this.platform.removeEventListener(this,e,t)})),this._responsiveListeners=void 0}updateHoverStyle(t,e,i){const s=i?"set":"remove";let n,o,a,r;for("dataset"===e&&(n=this.getDatasetMeta(t[0].datasetIndex),n.controller["_"+s+"DatasetHoverStyle"]()),a=0,r=t.length;a{const i=this.getDatasetMeta(t);if(!i)throw new Error("No dataset found at index "+t);return{datasetIndex:t,element:i.data[e],index:e}}));!f(i,e)&&(this._active=i,this._lastEvent=null,this._updateHoverStyles(i,e))}notifyPlugins(t,e,i){return this._plugins.notify(this,t,e,i)}isPluginEnabled(t){return 1===this._plugins._cache.filter((e=>e.plugin.id===t)).length}_updateHoverStyles(t,e,i){const s=this.options.hover,n=(t,e)=>t.filter((t=>!e.some((e=>t.datasetIndex===e.datasetIndex&&t.index===e.index)))),o=n(e,t),a=i?t:n(t,e);o.length&&this.updateHoverStyle(o,s.mode,!1),a.length&&s.mode&&this.updateHoverStyle(a,s.mode,!0)}_eventHandler(t,e){const i={event:t,replay:e,cancelable:!0,inChartArea:this.isPointInArea(t)},s=e=>(e.options.events||this.options.events).includes(t.native.type);if(!1===this.notifyPlugins("beforeEvent",i,s))return;const n=this._handleEvent(t,e,i.inChartArea);return i.cancelable=!1,this.notifyPlugins("afterEvent",i,s),(n||i.changed)&&this.render(),this}_handleEvent(t,e,i){const{_active:s=[],options:n}=this,o=e,a=this._getActiveElements(t,s,i,o),r=D(t),l=function(t,e,i,s){return i&&"mouseout"!==t.type?s?e:t:null}(t,this._lastEvent,i,r);i&&(this._lastEvent=null,d(n.onHover,[t,a,this],this),r&&d(n.onClick,[t,a,this],this));const h=!f(a,s);return(h||e)&&(this._active=a,this._updateHoverStyles(a,s,e)),this._lastEvent=l,h}_getActiveElements(t,e,i,s){if("mouseout"===t.type)return[];if(!i)return e;const n=this.options.hover;return this.getElementsAtEventForMode(t,n.mode,n,s)}}function wn(){return u(Mn.instances,(t=>t._plugins.invalidate()))}function kn(){throw new Error("This method is not implemented: Check that a complete date adapter is provided.")}class Sn{static override(t){Object.assign(Sn.prototype,t)}constructor(t){this.options=t||{}}init(){}formats(){return kn()}parse(){return kn()}format(){return kn()}add(){return kn()}diff(){return kn()}startOf(){return kn()}endOf(){return kn()}}var Pn={_date:Sn};function Dn(t){const e=t.iScale,i=function(t,e){if(!t._cache.$bar){const i=t.getMatchingVisibleMetas(e);let s=[];for(let e=0,n=i.length;et-e)))}return t._cache.$bar}(e,t.type);let s,n,o,a,r=e._length;const l=()=>{32767!==o&&-32768!==o&&(k(a)&&(r=Math.min(r,Math.abs(o-a)||r)),a=o)};for(s=0,n=i.length;sMath.abs(r)&&(l=r,h=a),e[i.axis]=h,e._custom={barStart:l,barEnd:h,start:n,end:o,min:a,max:r}}(t,e,i,s):e[i.axis]=i.parse(t,s),e}function On(t,e,i,s){const n=t.iScale,o=t.vScale,a=n.getLabels(),r=n===o,l=[];let h,c,d,u;for(h=i,c=i+s;ht.x,i="left",s="right"):(e=t.base"spacing"!==t,_indexable:t=>"spacing"!==t};static overrides={aspectRatio:1,plugins:{legend:{labels:{generateLabels(t){const e=t.data;if(e.labels.length&&e.datasets.length){const{labels:{pointStyle:i,color:s}}=t.legend.options;return e.labels.map(((e,n)=>{const o=t.getDatasetMeta(0).controller.getStyle(n);return{text:e,fillStyle:o.backgroundColor,strokeStyle:o.borderColor,fontColor:s,lineWidth:o.borderWidth,pointStyle:i,hidden:!t.getDataVisibility(n),index:n}}))}return[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}}}};constructor(t,e){super(t,e),this.enableOptionSharing=!0,this.innerRadius=void 0,this.outerRadius=void 0,this.offsetX=void 0,this.offsetY=void 0}linkScales(){}parse(t,e){const i=this.getDataset().data,s=this._cachedMeta;if(!1===this._parsing)s._parsed=i;else{let n,a,r=t=>+i[t];if(o(i[t])){const{key:t="value"}=this._parsing;r=e=>+M(i[e],t)}for(n=t,a=t+e;nZ(t,r,l,!0)?1:Math.max(e,e*i,s,s*i),g=(t,e,s)=>Z(t,r,l,!0)?-1:Math.min(e,e*i,s,s*i),p=f(0,h,d),m=f(E,c,u),b=g(C,h,d),x=g(C+E,c,u);s=(p-b)/2,n=(m-x)/2,o=-(p+b)/2,a=-(m+x)/2}return{ratioX:s,ratioY:n,offsetX:o,offsetY:a}}(u,d,r),b=(i.width-o)/f,x=(i.height-o)/g,_=Math.max(Math.min(b,x)/2,0),y=c(this.options.radius,_),v=(y-Math.max(y*r,0))/this._getVisibleDatasetWeightTotal();this.offsetX=p*y,this.offsetY=m*y,s.total=this.calculateTotal(),this.outerRadius=y-v*this._getRingWeightOffset(this.index),this.innerRadius=Math.max(this.outerRadius-v*l,0),this.updateElements(n,0,n.length,t)}_circumference(t,e){const i=this.options,s=this._cachedMeta,n=this._getCircumference();return e&&i.animation.animateRotate||!this.chart.getDataVisibility(t)||null===s._parsed[t]||s.data[t].hidden?0:this.calculateCircumference(s._parsed[t]*n/O)}updateElements(t,e,i,s){const n="reset"===s,o=this.chart,a=o.chartArea,r=o.options.animation,l=(a.left+a.right)/2,h=(a.top+a.bottom)/2,c=n&&r.animateScale,d=c?0:this.innerRadius,u=c?0:this.outerRadius,{sharedOptions:f,includeOptions:g}=this._getSharedOptions(e,s);let p,m=this._getRotation();for(p=0;p0&&!isNaN(t)?O*(Math.abs(t)/e):0}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart,s=i.data.labels||[],n=ne(e._parsed[t],i.options.locale);return{label:s[t]||"",value:n}}getMaxBorderWidth(t){let e=0;const i=this.chart;let s,n,o,a,r;if(!t)for(s=0,n=i.data.datasets.length;s{const o=t.getDatasetMeta(0).controller.getStyle(n);return{text:e,fillStyle:o.backgroundColor,strokeStyle:o.borderColor,fontColor:s,lineWidth:o.borderWidth,pointStyle:i,hidden:!t.getDataVisibility(n),index:n}}))}return[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}}},scales:{r:{type:"radialLinear",angleLines:{display:!1},beginAtZero:!0,grid:{circular:!0},pointLabels:{display:!1},startAngle:0}}};constructor(t,e){super(t,e),this.innerRadius=void 0,this.outerRadius=void 0}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart,s=i.data.labels||[],n=ne(e._parsed[t].r,i.options.locale);return{label:s[t]||"",value:n}}parseObjectData(t,e,i,s){return ei.bind(this)(t,e,i,s)}update(t){const e=this._cachedMeta.data;this._updateRadius(),this.updateElements(e,0,e.length,t)}getMinMax(){const t=this._cachedMeta,e={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY};return t.data.forEach(((t,i)=>{const s=this.getParsed(i).r;!isNaN(s)&&this.chart.getDataVisibility(i)&&(se.max&&(e.max=s))})),e}_updateRadius(){const t=this.chart,e=t.chartArea,i=t.options,s=Math.min(e.right-e.left,e.bottom-e.top),n=Math.max(s/2,0),o=(n-Math.max(i.cutoutPercentage?n/100*i.cutoutPercentage:1,0))/t.getVisibleDatasetCount();this.outerRadius=n-o*this.index,this.innerRadius=this.outerRadius-o}updateElements(t,e,i,s){const n="reset"===s,o=this.chart,a=o.options.animation,r=this._cachedMeta.rScale,l=r.xCenter,h=r.yCenter,c=r.getIndexAngle(0)-.5*C;let d,u=c;const f=360/this.countVisibleElements();for(d=0;d{!isNaN(this.getParsed(i).r)&&this.chart.getDataVisibility(i)&&e++})),e}_computeAngle(t,e,i){return this.chart.getDataVisibility(t)?$(this.resolveDataElementOptions(t,e).angle||i):0}}var Fn=Object.freeze({__proto__:null,BarController:class extends Vs{static id="bar";static defaults={datasetElementType:!1,dataElementType:"bar",categoryPercentage:.8,barPercentage:.9,grouped:!0,animations:{numbers:{type:"number",properties:["x","y","base","width","height"]}}};static overrides={scales:{_index_:{type:"category",offset:!0,grid:{offset:!0}},_value_:{type:"linear",beginAtZero:!0}}};parsePrimitiveData(t,e,i,s){return On(t,e,i,s)}parseArrayData(t,e,i,s){return On(t,e,i,s)}parseObjectData(t,e,i,s){const{iScale:n,vScale:o}=t,{xAxisKey:a="x",yAxisKey:r="y"}=this._parsing,l="x"===n.axis?a:r,h="x"===o.axis?a:r,c=[];let d,u,f,g;for(d=i,u=i+s;dt.controller.options.grouped)),o=i.options.stacked,a=[],r=t=>{const i=t.controller.getParsed(e),n=i&&i[t.vScale.axis];if(s(n)||isNaN(n))return!0};for(const i of n)if((void 0===e||!r(i))&&((!1===o||-1===a.indexOf(i.stack)||void 0===o&&void 0===i.stack)&&a.push(i.stack),i.index===t))break;return a.length||a.push(void 0),a}_getStackCount(t){return this._getStacks(void 0,t).length}_getStackIndex(t,e,i){const s=this._getStacks(t,i),n=void 0!==e?s.indexOf(e):-1;return-1===n?s.length-1:n}_getRuler(){const t=this.options,e=this._cachedMeta,i=e.iScale,s=[];let n,o;for(n=0,o=e.data.length;n=i?1:-1)}(u,e,r)*a,f===r&&(b-=u/2);const t=e.getPixelForDecimal(0),s=e.getPixelForDecimal(1),o=Math.min(t,s),h=Math.max(t,s);b=Math.max(Math.min(b,h),o),d=b+u,i&&!c&&(l._stacks[e.axis]._visualValues[n]=e.getValueForPixel(d)-e.getValueForPixel(b))}if(b===e.getPixelForValue(r)){const t=F(u)*e.getLineWidthForValue(r)/2;b+=t,u-=t}return{size:u,base:b,head:d,center:d+u/2}}_calculateBarIndexPixels(t,e){const i=e.scale,n=this.options,o=n.skipNull,a=l(n.maxBarThickness,1/0);let r,h;if(e.grouped){const i=o?this._getStackCount(t):e.stackCount,l="flex"===n.barThickness?function(t,e,i,s){const n=e.pixels,o=n[t];let a=t>0?n[t-1]:null,r=t=0;--i)e=Math.max(e,t[i].size(this.resolveDataElementOptions(i))/2);return e>0&&e}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart.data.labels||[],{xScale:s,yScale:n}=e,o=this.getParsed(t),a=s.getLabelForValue(o.x),r=n.getLabelForValue(o.y),l=o._custom;return{label:i[t]||"",value:"("+a+", "+r+(l?", "+l:"")+")"}}update(t){const e=this._cachedMeta.data;this.updateElements(e,0,e.length,t)}updateElements(t,e,i,s){const n="reset"===s,{iScale:o,vScale:a}=this._cachedMeta,{sharedOptions:r,includeOptions:l}=this._getSharedOptions(e,s),h=o.axis,c=a.axis;for(let d=e;d0&&this.getParsed(e-1);for(let i=0;i<_;++i){const g=t[i],_=b?g:{};if(i=x){_.skip=!0;continue}const v=this.getParsed(i),M=s(v[f]),w=_[u]=a.getPixelForValue(v[u],i),k=_[f]=o||M?r.getBasePixel():r.getPixelForValue(l?this.applyStack(r,v,l):v[f],i);_.skip=isNaN(w)||isNaN(k)||M,_.stop=i>0&&Math.abs(v[u]-y[u])>m,p&&(_.parsed=v,_.raw=h.data[i]),d&&(_.options=c||this.resolveDataElementOptions(i,g.active?"active":n)),b||this.updateElement(g,i,_,n),y=v}}getMaxOverflow(){const t=this._cachedMeta,e=t.dataset,i=e.options&&e.options.borderWidth||0,s=t.data||[];if(!s.length)return i;const n=s[0].size(this.resolveDataElementOptions(0)),o=s[s.length-1].size(this.resolveDataElementOptions(s.length-1));return Math.max(i,n,o)/2}draw(){const t=this._cachedMeta;t.dataset.updateControlPoints(this.chart.chartArea,t.iScale.axis),super.draw()}},PolarAreaController:zn,PieController:class extends In{static id="pie";static defaults={cutout:0,rotation:0,circumference:360,radius:"100%"}},RadarController:class extends Vs{static id="radar";static defaults={datasetElementType:"line",dataElementType:"point",indexAxis:"r",showLine:!0,elements:{line:{fill:"start"}}};static overrides={aspectRatio:1,scales:{r:{type:"radialLinear"}}};getLabelAndValue(t){const e=this._cachedMeta.vScale,i=this.getParsed(t);return{label:e.getLabels()[t],value:""+e.getLabelForValue(i[e.axis])}}parseObjectData(t,e,i,s){return ei.bind(this)(t,e,i,s)}update(t){const e=this._cachedMeta,i=e.dataset,s=e.data||[],n=e.iScale.getLabels();if(i.points=s,"resize"!==t){const e=this.resolveDatasetElementOptions(t);this.options.showLine||(e.borderWidth=0);const o={_loop:!0,_fullLoop:n.length===s.length,options:e};this.updateElement(i,void 0,o,t)}this.updateElements(s,0,s.length,t)}updateElements(t,e,i,s){const n=this._cachedMeta.rScale,o="reset"===s;for(let a=e;a0&&this.getParsed(e-1);for(let c=e;c0&&Math.abs(i[f]-_[f])>b,m&&(p.parsed=i,p.raw=h.data[c]),u&&(p.options=d||this.resolveDataElementOptions(c,e.active?"active":n)),x||this.updateElement(e,c,p,n),_=i}this.updateSharedOptions(d,n,c)}getMaxOverflow(){const t=this._cachedMeta,e=t.data||[];if(!this.options.showLine){let t=0;for(let i=e.length-1;i>=0;--i)t=Math.max(t,e[i].size(this.resolveDataElementOptions(i))/2);return t>0&&t}const i=t.dataset,s=i.options&&i.options.borderWidth||0;if(!e.length)return s;const n=e[0].size(this.resolveDataElementOptions(0)),o=e[e.length-1].size(this.resolveDataElementOptions(e.length-1));return Math.max(s,n,o)/2}}});function Vn(t,e,i,s){const n=_i(t.options.borderRadius,["outerStart","outerEnd","innerStart","innerEnd"]);const o=(i-e)/2,a=Math.min(o,s*e/2),r=t=>{const e=(i-Math.min(o,t))*s/2;return J(t,0,Math.min(o,e))};return{outerStart:r(n.outerStart),outerEnd:r(n.outerEnd),innerStart:J(n.innerStart,0,a),innerEnd:J(n.innerEnd,0,a)}}function Bn(t,e,i,s){return{x:i+t*Math.cos(e),y:s+t*Math.sin(e)}}function Nn(t,e,i,s,n,o){const{x:a,y:r,startAngle:l,pixelMargin:h,innerRadius:c}=e,d=Math.max(e.outerRadius+s+i-h,0),u=c>0?c+s+i+h:0;let f=0;const g=n-l;if(s){const t=((c>0?c-s:0)+(d>0?d-s:0))/2;f=(g-(0!==t?g*t/(t+s):g))/2}const p=(g-Math.max(.001,g*d-i/C)/d)/2,m=l+p+f,b=n-p-f,{outerStart:x,outerEnd:_,innerStart:y,innerEnd:v}=Vn(e,u,d,b-m),M=d-x,w=d-_,k=m+x/M,S=b-_/w,P=u+y,D=u+v,O=m+y/P,A=b-v/D;if(t.beginPath(),o){const e=(k+S)/2;if(t.arc(a,r,d,k,e),t.arc(a,r,d,e,S),_>0){const e=Bn(w,S,a,r);t.arc(e.x,e.y,_,S,b+E)}const i=Bn(D,b,a,r);if(t.lineTo(i.x,i.y),v>0){const e=Bn(D,A,a,r);t.arc(e.x,e.y,v,b+E,A+Math.PI)}const s=(b-v/u+(m+y/u))/2;if(t.arc(a,r,u,b-v/u,s,!0),t.arc(a,r,u,s,m+y/u,!0),y>0){const e=Bn(P,O,a,r);t.arc(e.x,e.y,y,O+Math.PI,m-E)}const n=Bn(M,m,a,r);if(t.lineTo(n.x,n.y),x>0){const e=Bn(M,k,a,r);t.arc(e.x,e.y,x,m-E,k)}}else{t.moveTo(a,r);const e=Math.cos(k)*d+a,i=Math.sin(k)*d+r;t.lineTo(e,i);const s=Math.cos(S)*d+a,n=Math.sin(S)*d+r;t.lineTo(s,n)}t.closePath()}function Wn(t,e,i,s,n){const{fullCircles:o,startAngle:a,circumference:r,options:l}=e,{borderWidth:h,borderJoinStyle:c}=l,d="inner"===l.borderAlign;if(!h)return;d?(t.lineWidth=2*h,t.lineJoin=c||"round"):(t.lineWidth=h,t.lineJoin=c||"bevel");let u=e.endAngle;if(o){Nn(t,e,i,s,u,n);for(let e=0;en?(h=n/l,t.arc(o,a,l,i+h,s-h,!0)):t.arc(o,a,n,i+E,s-E),t.closePath(),t.clip()}(t,e,u),o||(Nn(t,e,i,s,u,n),t.stroke())}function Hn(t,e,i=e){t.lineCap=l(i.borderCapStyle,e.borderCapStyle),t.setLineDash(l(i.borderDash,e.borderDash)),t.lineDashOffset=l(i.borderDashOffset,e.borderDashOffset),t.lineJoin=l(i.borderJoinStyle,e.borderJoinStyle),t.lineWidth=l(i.borderWidth,e.borderWidth),t.strokeStyle=l(i.borderColor,e.borderColor)}function jn(t,e,i){t.lineTo(i.x,i.y)}function $n(t,e,i={}){const s=t.length,{start:n=0,end:o=s-1}=i,{start:a,end:r}=e,l=Math.max(n,a),h=Math.min(o,r),c=nr&&o>r;return{count:s,start:l,loop:e.loop,ilen:h(a+(h?r-t:t))%o,_=()=>{f!==g&&(t.lineTo(m,g),t.lineTo(m,f),t.lineTo(m,p))};for(l&&(d=n[x(0)],t.moveTo(d.x,d.y)),c=0;c<=r;++c){if(d=n[x(c)],d.skip)continue;const e=d.x,i=d.y,s=0|e;s===u?(ig&&(g=i),m=(b*m+e)/++b):(_(),t.lineTo(e,i),u=s,b=0,f=g=i),p=i}_()}function Xn(t){const e=t.options,i=e.borderDash&&e.borderDash.length;return!(t._decimated||t._loop||e.tension||"monotone"===e.cubicInterpolationMode||e.stepped||i)?Un:Yn}const qn="function"==typeof Path2D;function Kn(t,e,i,s){qn&&!e.options.segment?function(t,e,i,s){let n=e._path;n||(n=e._path=new Path2D,e.path(n,i,s)&&n.closePath()),Hn(t,e.options),t.stroke(n)}(t,e,i,s):function(t,e,i,s){const{segments:n,options:o}=e,a=Xn(e);for(const r of n)Hn(t,o,r.style),t.beginPath(),a(t,e,r,{start:i,end:i+s-1})&&t.closePath(),t.stroke()}(t,e,i,s)}class Gn extends Bs{static id="line";static defaults={borderCapStyle:"butt",borderDash:[],borderDashOffset:0,borderJoinStyle:"miter",borderWidth:3,capBezierPoints:!0,cubicInterpolationMode:"default",fill:!1,spanGaps:!1,stepped:!1,tension:0};static defaultRoutes={backgroundColor:"backgroundColor",borderColor:"borderColor"};static descriptors={_scriptable:!0,_indexable:t=>"borderDash"!==t&&"fill"!==t};constructor(t){super(),this.animated=!0,this.options=void 0,this._chart=void 0,this._loop=void 0,this._fullLoop=void 0,this._path=void 0,this._points=void 0,this._segments=void 0,this._decimated=!1,this._pointsUpdated=!1,this._datasetIndex=void 0,t&&Object.assign(this,t)}updateControlPoints(t,e){const i=this.options;if((i.tension||"monotone"===i.cubicInterpolationMode)&&!i.stepped&&!this._pointsUpdated){const s=i.spanGaps?this._loop:this._fullLoop;li(this._points,i,t,s,e),this._pointsUpdated=!0}}set points(t){this._points=t,delete this._segments,delete this._path,this._pointsUpdated=!1}get points(){return this._points}get segments(){return this._segments||(this._segments=Ri(this,this.options.segment))}first(){const t=this.segments,e=this.points;return t.length&&e[t[0].start]}last(){const t=this.segments,e=this.points,i=t.length;return i&&e[t[i-1].end]}interpolate(t,e){const i=this.options,s=t[e],n=this.points,o=Ei(this,{property:e,start:s,end:s});if(!o.length)return;const a=[],r=function(t){return t.stepped?gi:t.tension||"monotone"===t.cubicInterpolationMode?pi:fi}(i);let l,h;for(l=0,h=o.length;l=O||Z(n,a,r),g=tt(o,h+u,c+u);return f&&g}getCenterPoint(t){const{x:e,y:i,startAngle:s,endAngle:n,innerRadius:o,outerRadius:a}=this.getProps(["x","y","startAngle","endAngle","innerRadius","outerRadius","circumference"],t),{offset:r,spacing:l}=this.options,h=(s+n)/2,c=(o+a+l+r)/2;return{x:e+Math.cos(h)*c,y:i+Math.sin(h)*c}}tooltipPosition(t){return this.getCenterPoint(t)}draw(t){const{options:e,circumference:i}=this,s=(e.offset||0)/4,n=(e.spacing||0)/2,o=e.circular;if(this.pixelMargin="inner"===e.borderAlign?.33:0,this.fullCircles=i>O?Math.floor(i/O):0,0===i||this.innerRadius<0||this.outerRadius<0)return;t.save();const a=(this.startAngle+this.endAngle)/2;t.translate(Math.cos(a)*s,Math.sin(a)*s);const r=s*(1-Math.sin(Math.min(C,i||0)));t.fillStyle=e.backgroundColor,t.strokeStyle=e.borderColor,function(t,e,i,s,n){const{fullCircles:o,startAngle:a,circumference:r}=e;let l=e.endAngle;if(o){Nn(t,e,i,s,l,n);for(let e=0;e("string"==typeof e?(i=t.push(e)-1,s.unshift({index:i,label:e})):isNaN(e)&&(i=null),i))(t,e,i,s);return n!==t.lastIndexOf(e)?i:n}function ao(t){const e=this.getLabels();return t>=0&&ts=e?s:t,a=t=>n=i?n:t;if(t){const t=F(s),e=F(n);t<0&&e<0?a(0):t>0&&e>0&&o(0)}if(s===n){let e=0===n?1:Math.abs(.05*n);a(n+e),t||o(s-e)}this.min=s,this.max=n}getTickLimit(){const t=this.options.ticks;let e,{maxTicksLimit:i,stepSize:s}=t;return s?(e=Math.ceil(this.max/s)-Math.floor(this.min/s)+1,e>1e3&&(console.warn(`scales.${this.id}.ticks.stepSize: ${s} would result generating up to ${e} ticks. Limiting to 1000.`),e=1e3)):(e=this.computeTickLimit(),i=i||11),i&&(e=Math.min(i,e)),e}computeTickLimit(){return Number.POSITIVE_INFINITY}buildTicks(){const t=this.options,e=t.ticks;let i=this.getTickLimit();i=Math.max(2,i);const n=function(t,e){const i=[],{bounds:n,step:o,min:a,max:r,precision:l,count:h,maxTicks:c,maxDigits:d,includeBounds:u}=t,f=o||1,g=c-1,{min:p,max:m}=e,b=!s(a),x=!s(r),_=!s(h),y=(m-p)/(d+1);let v,M,w,k,S=B((m-p)/g/f)*f;if(S<1e-14&&!b&&!x)return[{value:p},{value:m}];k=Math.ceil(m/S)-Math.floor(p/S),k>g&&(S=B(k*S/g/f)*f),s(l)||(v=Math.pow(10,l),S=Math.ceil(S*v)/v),"ticks"===n?(M=Math.floor(p/S)*S,w=Math.ceil(m/S)*S):(M=p,w=m),b&&x&&o&&H((r-a)/o,S/1e3)?(k=Math.round(Math.min((r-a)/S,c)),S=(r-a)/k,M=a,w=r):_?(M=b?a:M,w=x?r:w,k=h-1,S=(w-M)/k):(k=(w-M)/S,k=V(k,Math.round(k),S/1e3)?Math.round(k):Math.ceil(k));const P=Math.max(U(S),U(M));v=Math.pow(10,s(l)?P:l),M=Math.round(M*v)/v,w=Math.round(w*v)/v;let D=0;for(b&&(u&&M!==a?(i.push({value:a}),MMath.floor(z(t)),uo=(t,e)=>Math.pow(10,co(t)+e);function fo(t){return 1===t/Math.pow(10,co(t))}function go(t,e,i){const s=Math.pow(10,i),n=Math.floor(t/s);return Math.ceil(e/s)-n}function po(t,{min:e,max:i}){e=r(t.min,e);const s=[],n=co(e);let o=function(t,e){let i=co(e-t);for(;go(t,e,i)>10;)i++;for(;go(t,e,i)<10;)i--;return Math.min(i,co(t))}(e,i),a=o<0?Math.pow(10,Math.abs(o)):1;const l=Math.pow(10,o),h=n>o?Math.pow(10,n):0,c=Math.round((e-h)*a)/a,d=Math.floor((e-h)/l/10)*l*10;let u=Math.floor((c-d)/Math.pow(10,o)),f=r(t.min,Math.round((h+d+u*Math.pow(10,o))*a)/a);for(;f=10?u=u<15?15:20:u++,u>=20&&(o++,u=2,a=o>=0?1:a),f=Math.round((h+d+u*Math.pow(10,o))*a)/a;const g=r(t.max,f);return s.push({value:g,major:fo(g),significand:u}),s}class mo extends qs{static id="logarithmic";static defaults={ticks:{callback:ae.formatters.logarithmic,major:{enabled:!0}}};constructor(t){super(t),this.start=void 0,this.end=void 0,this._startValue=void 0,this._valueRange=0}parse(t,e){const i=lo.prototype.parse.apply(this,[t,e]);if(0!==i)return a(i)&&i>0?i:null;this._zero=!0}determineDataLimits(){const{min:t,max:e}=this.getMinMax(!0);this.min=a(t)?Math.max(0,t):null,this.max=a(e)?Math.max(0,e):null,this.options.beginAtZero&&(this._zero=!0),this._zero&&this.min!==this._suggestedMin&&!a(this._userMin)&&(this.min=t===uo(this.min,0)?uo(this.min,-1):uo(this.min,0)),this.handleTickRangeOptions()}handleTickRangeOptions(){const{minDefined:t,maxDefined:e}=this.getUserBounds();let i=this.min,s=this.max;const n=e=>i=t?i:e,o=t=>s=e?s:t;i===s&&(i<=0?(n(1),o(10)):(n(uo(i,-1)),o(uo(s,1)))),i<=0&&n(uo(s,-1)),s<=0&&o(uo(i,1)),this.min=i,this.max=s}buildTicks(){const t=this.options,e=po({min:this._userMin,max:this._userMax},this);return"ticks"===t.bounds&&j(e,this,"value"),t.reverse?(e.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),e}getLabelForValue(t){return void 0===t?"0":ne(t,this.chart.options.locale,this.options.ticks.format)}configure(){const t=this.min;super.configure(),this._startValue=z(t),this._valueRange=z(this.max)-z(t)}getPixelForValue(t){return void 0!==t&&0!==t||(t=this.min),null===t||isNaN(t)?NaN:this.getPixelForDecimal(t===this.min?0:(z(t)-this._startValue)/this._valueRange)}getValueForPixel(t){const e=this.getDecimalForPixel(t);return Math.pow(10,this._startValue+e*this._valueRange)}}function bo(t){const e=t.ticks;if(e.display&&t.display){const t=Mi(e.backdropPadding);return l(e.font&&e.font.size,ue.font.size)+t.height}return 0}function xo(t,e,i,s,n){return t===s||t===n?{start:e-i/2,end:e+i/2}:tn?{start:e-i,end:e}:{start:e,end:e+i}}function _o(t){const e={l:t.left+t._padding.left,r:t.right-t._padding.right,t:t.top+t._padding.top,b:t.bottom-t._padding.bottom},i=Object.assign({},e),s=[],o=[],a=t._pointLabels.length,r=t.options.pointLabels,l=r.centerPointLabels?C/a:0;for(let u=0;ue.r&&(r=(s.end-e.r)/o,t.r=Math.max(t.r,e.r+r)),n.starte.b&&(l=(n.end-e.b)/a,t.b=Math.max(t.b,e.b+l))}function vo(t){return 0===t||180===t?"center":t<180?"left":"right"}function Mo(t,e,i){return"right"===i?t-=e:"center"===i&&(t-=e/2),t}function wo(t,e,i){return 90===i||270===i?t-=e/2:(i>270||i<90)&&(t-=e),t}function ko(t,e,i,s){const{ctx:n}=t;if(i)n.arc(t.xCenter,t.yCenter,e,0,O);else{let i=t.getPointPosition(0,e);n.moveTo(i.x,i.y);for(let o=1;ot,padding:5,centerPointLabels:!1}};static defaultRoutes={"angleLines.color":"borderColor","pointLabels.color":"color","ticks.color":"color"};static descriptors={angleLines:{_fallback:"grid"}};constructor(t){super(t),this.xCenter=void 0,this.yCenter=void 0,this.drawingArea=void 0,this._pointLabels=[],this._pointLabelItems=[]}setDimensions(){const t=this._padding=Mi(bo(this.options)/2),e=this.width=this.maxWidth-t.width,i=this.height=this.maxHeight-t.height;this.xCenter=Math.floor(this.left+e/2+t.left),this.yCenter=Math.floor(this.top+i/2+t.top),this.drawingArea=Math.floor(Math.min(e,i)/2)}determineDataLimits(){const{min:t,max:e}=this.getMinMax(!1);this.min=a(t)&&!isNaN(t)?t:0,this.max=a(e)&&!isNaN(e)?e:0,this.handleTickRangeOptions()}computeTickLimit(){return Math.ceil(this.drawingArea/bo(this.options))}generateTickLabels(t){lo.prototype.generateTickLabels.call(this,t),this._pointLabels=this.getLabels().map(((t,e)=>{const i=d(this.options.pointLabels.callback,[t,e],this);return i||0===i?i:""})).filter(((t,e)=>this.chart.getDataVisibility(e)))}fit(){const t=this.options;t.display&&t.pointLabels.display?_o(this):this.setCenterPoint(0,0,0,0)}setCenterPoint(t,e,i,s){this.xCenter+=Math.floor((t-e)/2),this.yCenter+=Math.floor((i-s)/2),this.drawingArea-=Math.min(this.drawingArea/2,Math.max(t,e,i,s))}getIndexAngle(t){return G(t*(O/(this._pointLabels.length||1))+$(this.options.startAngle||0))}getDistanceFromCenterForValue(t){if(s(t))return NaN;const e=this.drawingArea/(this.max-this.min);return this.options.reverse?(this.max-t)*e:(t-this.min)*e}getValueForDistanceFromCenter(t){if(s(t))return NaN;const e=t/(this.drawingArea/(this.max-this.min));return this.options.reverse?this.max-e:this.min+e}getPointLabelContext(t){const e=this._pointLabels||[];if(t>=0&&t=0;o--){const e=n.setContext(t.getPointLabelContext(o)),a=wi(e.font),{x:r,y:l,textAlign:h,left:c,top:d,right:u,bottom:f}=t._pointLabelItems[o],{backdropColor:g}=e;if(!s(g)){const t=vi(e.borderRadius),s=Mi(e.backdropPadding);i.fillStyle=g;const n=c-s.left,o=d-s.top,a=u-c+s.width,r=f-d+s.height;Object.values(t).some((t=>0!==t))?(i.beginPath(),We(i,{x:n,y:o,w:a,h:r,radius:t}),i.fill()):i.fillRect(n,o,a,r)}Ve(i,t._pointLabels[o],r,l+a.lineHeight/2,a,{color:e.color,textAlign:h,textBaseline:"middle"})}}(this,a),n.display&&this.ticks.forEach(((t,e)=>{if(0!==e){l=this.getDistanceFromCenterForValue(t.value);const i=this.getContext(e),s=n.setContext(i),r=o.setContext(i);!function(t,e,i,s,n){const o=t.ctx,a=e.circular,{color:r,lineWidth:l}=e;!a&&!s||!r||!l||i<0||(o.save(),o.strokeStyle=r,o.lineWidth=l,o.setLineDash(n.dash),o.lineDashOffset=n.dashOffset,o.beginPath(),ko(t,i,a,s),o.closePath(),o.stroke(),o.restore())}(this,s,l,a,r)}})),i.display){for(t.save(),r=a-1;r>=0;r--){const s=i.setContext(this.getPointLabelContext(r)),{color:n,lineWidth:o}=s;o&&n&&(t.lineWidth=o,t.strokeStyle=n,t.setLineDash(s.borderDash),t.lineDashOffset=s.borderDashOffset,l=this.getDistanceFromCenterForValue(e.ticks.reverse?this.min:this.max),h=this.getPointPosition(r,l),t.beginPath(),t.moveTo(this.xCenter,this.yCenter),t.lineTo(h.x,h.y),t.stroke())}t.restore()}}drawBorder(){}drawLabels(){const t=this.ctx,e=this.options,i=e.ticks;if(!i.display)return;const s=this.getIndexAngle(0);let n,o;t.save(),t.translate(this.xCenter,this.yCenter),t.rotate(s),t.textAlign="center",t.textBaseline="middle",this.ticks.forEach(((s,a)=>{if(0===a&&!e.reverse)return;const r=i.setContext(this.getContext(a)),l=wi(r.font);if(n=this.getDistanceFromCenterForValue(this.ticks[a].value),r.showLabelBackdrop){t.font=l.string,o=t.measureText(s.label).width,t.fillStyle=r.backdropColor;const e=Mi(r.backdropPadding);t.fillRect(-o/2-e.left,-n-l.size/2-e.top,o+e.width,l.size+e.height)}Ve(t,s.label,0,-n,l,{color:r.color})})),t.restore()}drawTitle(){}}const Po={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},Do=Object.keys(Po);function Co(t,e){return t-e}function Oo(t,e){if(s(e))return null;const i=t._adapter,{parser:n,round:o,isoWeekday:r}=t._parseOpts;let l=e;return"function"==typeof n&&(l=n(l)),a(l)||(l="string"==typeof n?i.parse(l,n):i.parse(l)),null===l?null:(o&&(l="week"!==o||!W(r)&&!0!==r?i.startOf(l,o):i.startOf(l,"isoWeek",r)),+l)}function Ao(t,e,i,s){const n=Do.length;for(let o=Do.indexOf(t);o=e?i[s]:i[n]]=!0}}else t[e]=!0}function Lo(t,e,i){const s=[],n={},o=e.length;let a,r;for(a=0;a=0&&(e[l].major=!0);return e}(t,s,n,i):s}class Eo extends qs{static id="time";static defaults={bounds:"data",adapters:{},time:{parser:!1,unit:!1,round:!1,isoWeekday:!1,minUnit:"millisecond",displayFormats:{}},ticks:{source:"auto",callback:!1,major:{enabled:!1}}};constructor(t){super(t),this._cache={data:[],labels:[],all:[]},this._unit="day",this._majorUnit=void 0,this._offsets={},this._normalized=!1,this._parseOpts=void 0}init(t,e={}){const i=t.time||(t.time={}),s=this._adapter=new Pn._date(t.adapters.date);s.init(e),x(i.displayFormats,s.formats()),this._parseOpts={parser:i.parser,round:i.round,isoWeekday:i.isoWeekday},super.init(t),this._normalized=e.normalized}parse(t,e){return void 0===t?null:Oo(this,t)}beforeLayout(){super.beforeLayout(),this._cache={data:[],labels:[],all:[]}}determineDataLimits(){const t=this.options,e=this._adapter,i=t.time.unit||"day";let{min:s,max:n,minDefined:o,maxDefined:r}=this.getUserBounds();function l(t){o||isNaN(t.min)||(s=Math.min(s,t.min)),r||isNaN(t.max)||(n=Math.max(n,t.max))}o&&r||(l(this._getLabelBounds()),"ticks"===t.bounds&&"labels"===t.ticks.source||l(this.getMinMax(!1))),s=a(s)&&!isNaN(s)?s:+e.startOf(Date.now(),i),n=a(n)&&!isNaN(n)?n:+e.endOf(Date.now(),i)+1,this.min=Math.min(s,n-1),this.max=Math.max(s+1,n)}_getLabelBounds(){const t=this.getLabelTimestamps();let e=Number.POSITIVE_INFINITY,i=Number.NEGATIVE_INFINITY;return t.length&&(e=t[0],i=t[t.length-1]),{min:e,max:i}}buildTicks(){const t=this.options,e=t.time,i=t.ticks,s="labels"===i.source?this.getLabelTimestamps():this._generate();"ticks"===t.bounds&&s.length&&(this.min=this._userMin||s[0],this.max=this._userMax||s[s.length-1]);const n=this.min,o=nt(s,n,this.max);return this._unit=e.unit||(i.autoSkip?Ao(e.minUnit,this.min,this.max,this._getLabelCapacity(n)):function(t,e,i,s,n){for(let o=Do.length-1;o>=Do.indexOf(i);o--){const i=Do[o];if(Po[i].common&&t._adapter.diff(n,s,i)>=e-1)return i}return Do[i?Do.indexOf(i):0]}(this,o.length,e.minUnit,this.min,this.max)),this._majorUnit=i.major.enabled&&"year"!==this._unit?function(t){for(let e=Do.indexOf(t)+1,i=Do.length;e+t.value)))}initOffsets(t=[]){let e,i,s=0,n=0;this.options.offset&&t.length&&(e=this.getDecimalForValue(t[0]),s=1===t.length?1-e:(this.getDecimalForValue(t[1])-e)/2,i=this.getDecimalForValue(t[t.length-1]),n=1===t.length?i:(i-this.getDecimalForValue(t[t.length-2]))/2);const o=t.length<3?.5:.25;s=J(s,0,o),n=J(n,0,o),this._offsets={start:s,end:n,factor:1/(s+1+n)}}_generate(){const t=this._adapter,e=this.min,i=this.max,s=this.options,n=s.time,o=n.unit||Ao(n.minUnit,e,i,this._getLabelCapacity(e)),a=l(s.ticks.stepSize,1),r="week"===o&&n.isoWeekday,h=W(r)||!0===r,c={};let d,u,f=e;if(h&&(f=+t.startOf(f,"isoWeek",r)),f=+t.startOf(f,h?"day":o),t.diff(i,e,o)>1e5*a)throw new Error(e+" and "+i+" are too far apart with stepSize of "+a+" "+o);const g="data"===s.ticks.source&&this.getDataTimestamps();for(d=f,u=0;dt-e)).map((t=>+t))}getLabelForValue(t){const e=this._adapter,i=this.options.time;return i.tooltipFormat?e.format(t,i.tooltipFormat):e.format(t,i.displayFormats.datetime)}_tickFormatFunction(t,e,i,s){const n=this.options,o=n.ticks.callback;if(o)return d(o,[t,e,i],this);const a=n.time.displayFormats,r=this._unit,l=this._majorUnit,h=r&&a[r],c=l&&a[l],u=i[e],f=l&&c&&u&&u.major;return this._adapter.format(t,s||(f?c:h))}generateTickLabels(t){let e,i,s;for(e=0,i=t.length;e0?a:1}getDataTimestamps(){let t,e,i=this._cache.data||[];if(i.length)return i;const s=this.getMatchingVisibleMetas();if(this._normalized&&s.length)return this._cache.data=s[0].controller.getAllParsedValues(this);for(t=0,e=s.length;t=t[r].pos&&e<=t[l].pos&&({lo:r,hi:l}=it(t,"pos",e)),({pos:s,time:o}=t[r]),({pos:n,time:a}=t[l])):(e>=t[r].time&&e<=t[l].time&&({lo:r,hi:l}=it(t,"time",e)),({time:s,pos:o}=t[r]),({time:n,pos:a}=t[l]));const h=n-s;return h?o+(a-o)*(e-s)/h:o}var Io=Object.freeze({__proto__:null,CategoryScale:class extends qs{static id="category";static defaults={ticks:{callback:ao}};constructor(t){super(t),this._startValue=void 0,this._valueRange=0,this._addedLabels=[]}init(t){const e=this._addedLabels;if(e.length){const t=this.getLabels();for(const{index:i,label:s}of e)t[i]===s&&t.splice(i,1);this._addedLabels=[]}super.init(t)}parse(t,e){if(s(t))return null;const i=this.getLabels();return((t,e)=>null===t?null:J(Math.round(t),0,e))(e=isFinite(e)&&i[e]===t?e:oo(i,t,l(e,t),this._addedLabels),i.length-1)}determineDataLimits(){const{minDefined:t,maxDefined:e}=this.getUserBounds();let{min:i,max:s}=this.getMinMax(!0);"ticks"===this.options.bounds&&(t||(i=0),e||(s=this.getLabels().length-1)),this.min=i,this.max=s}buildTicks(){const t=this.min,e=this.max,i=this.options.offset,s=[];let n=this.getLabels();n=0===t&&e===n.length-1?n:n.slice(t,e+1),this._valueRange=Math.max(n.length-(i?0:1),1),this._startValue=this.min-(i?.5:0);for(let i=t;i<=e;i++)s.push({value:i});return s}getLabelForValue(t){return ao.call(this,t)}configure(){super.configure(),this.isHorizontal()||(this._reversePixels=!this._reversePixels)}getPixelForValue(t){return"number"!=typeof t&&(t=this.parse(t)),null===t?NaN:this.getPixelForDecimal((t-this._startValue)/this._valueRange)}getPixelForTick(t){const e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getValueForPixel(t){return Math.round(this._startValue+this.getDecimalForPixel(t)*this._valueRange)}getBasePixel(){return this.bottom}},LinearScale:ho,LogarithmicScale:mo,RadialLinearScale:So,TimeScale:Eo,TimeSeriesScale:class extends Eo{static id="timeseries";static defaults=Eo.defaults;constructor(t){super(t),this._table=[],this._minPos=void 0,this._tableRange=void 0}initOffsets(){const t=this._getTimestampsForTable(),e=this._table=this.buildLookupTable(t);this._minPos=Ro(e,this.min),this._tableRange=Ro(e,this.max)-this._minPos,super.initOffsets(t)}buildLookupTable(t){const{min:e,max:i}=this,s=[],n=[];let o,a,r,l,h;for(o=0,a=t.length;o=e&&l<=i&&s.push(l);if(s.length<2)return[{time:e,pos:0},{time:i,pos:1}];for(o=0,a=s.length;ot.replace("rgb(","rgba(").replace(")",", 0.5)")));function Vo(t){return zo[t%zo.length]}function Bo(t){return Fo[t%Fo.length]}function No(t){let e=0;return(i,s)=>{const n=t.getDatasetMeta(s).controller;n instanceof In?e=function(t,e){return t.backgroundColor=t.data.map((()=>Vo(e++))),e}(i,e):n instanceof zn?e=function(t,e){return t.backgroundColor=t.data.map((()=>Bo(e++))),e}(i,e):n&&(e=function(t,e){return t.borderColor=Vo(e),t.backgroundColor=Bo(e),++e}(i,e))}}function Wo(t){let e;for(e in t)if(t[e].borderColor||t[e].backgroundColor)return!0;return!1}var Ho={id:"colors",defaults:{enabled:!0,forceOverride:!1},beforeLayout(t,e,i){if(!i.enabled)return;const{options:{elements:s},data:{datasets:n}}=t.config;if(!i.forceOverride&&(Wo(n)||s&&Wo(s)))return;const o=No(t);n.forEach(o)}};function jo(t){if(t._decimated){const e=t._data;delete t._decimated,delete t._data,Object.defineProperty(t,"data",{value:e})}}function $o(t){t.data.datasets.forEach((t=>{jo(t)}))}var Yo={id:"decimation",defaults:{algorithm:"min-max",enabled:!1},beforeElementsUpdate:(t,e,i)=>{if(!i.enabled)return void $o(t);const n=t.width;t.data.datasets.forEach(((e,o)=>{const{_data:a,indexAxis:r}=e,l=t.getDatasetMeta(o),h=a||e.data;if("y"===ki([r,t.options.indexAxis]))return;if(!l.controller.supportsDecimation)return;const c=t.scales[l.xAxisID];if("linear"!==c.type&&"time"!==c.type)return;if(t.options.parsing)return;let{start:d,count:u}=function(t,e){const i=e.length;let s,n=0;const{iScale:o}=t,{min:a,max:r,minDefined:l,maxDefined:h}=o.getUserBounds();return l&&(n=J(it(e,o.axis,a).lo,0,i-1)),s=h?J(it(e,o.axis,r).hi+1,n,i)-n:i-n,{start:n,count:s}}(l,h);if(u<=(i.threshold||4*n))return void jo(e);let f;switch(s(a)&&(e._data=h,delete e.data,Object.defineProperty(e,"data",{configurable:!0,enumerable:!0,get:function(){return this._decimated},set:function(t){this._data=t}})),i.algorithm){case"lttb":f=function(t,e,i,s,n){const o=n.samples||s;if(o>=i)return t.slice(e,e+i);const a=[],r=(i-2)/(o-2);let l=0;const h=e+i-1;let c,d,u,f,g,p=e;for(a[l++]=t[p],c=0;cu&&(u=f,d=t[s],g=s);a[l++]=d,p=g}return a[l++]=t[h],a}(h,d,u,n,i);break;case"min-max":f=function(t,e,i,n){let o,a,r,l,h,c,d,u,f,g,p=0,m=0;const b=[],x=e+i-1,_=t[e].x,y=t[x].x-_;for(o=e;og&&(g=l,d=o),p=(m*p+a.x)/++m;else{const i=o-1;if(!s(c)&&!s(d)){const e=Math.min(c,d),s=Math.max(c,d);e!==u&&e!==i&&b.push({...t[e],x:p}),s!==u&&s!==i&&b.push({...t[s],x:p})}o>0&&i!==u&&b.push(t[i]),b.push(a),h=e,m=0,f=g=l,c=d=u=o}}return b}(h,d,u,n);break;default:throw new Error(`Unsupported decimation algorithm '${i.algorithm}'`)}e._decimated=f}))},destroy(t){$o(t)}};function Uo(t,e,i,s){if(s)return;let n=e[t],o=i[t];return"angle"===t&&(n=G(n),o=G(o)),{property:t,start:n,end:o}}function Xo(t,e,i){for(;e>t;e--){const t=i[e];if(!isNaN(t.x)&&!isNaN(t.y))break}return e}function qo(t,e,i,s){return t&&e?s(t[i],e[i]):t?t[i]:e?e[i]:0}function Ko(t,e){let i=[],s=!1;return n(t)?(s=!0,i=t):i=function(t,e){const{x:i=null,y:s=null}=t||{},n=e.points,o=[];return e.segments.forEach((({start:t,end:e})=>{e=Xo(t,e,n);const a=n[t],r=n[e];null!==s?(o.push({x:a.x,y:s}),o.push({x:r.x,y:s})):null!==i&&(o.push({x:i,y:a.y}),o.push({x:i,y:r.y}))})),o}(t,e),i.length?new Gn({points:i,options:{tension:0},_loop:s,_fullLoop:s}):null}function Go(t){return t&&!1!==t.fill}function Zo(t,e,i){let s=t[e].fill;const n=[e];let o;if(!i)return s;for(;!1!==s&&-1===n.indexOf(s);){if(!a(s))return s;if(o=t[s],!o)return!1;if(o.visible)return s;n.push(s),s=o.fill}return!1}function Jo(t,e,i){const s=function(t){const e=t.options,i=e.fill;let s=l(i&&i.target,i);void 0===s&&(s=!!e.backgroundColor);if(!1===s||null===s)return!1;if(!0===s)return"origin";return s}(t);if(o(s))return!isNaN(s.value)&&s;let n=parseFloat(s);return a(n)&&Math.floor(n)===n?function(t,e,i,s){"-"!==t&&"+"!==t||(i=e+i);if(i===e||i<0||i>=s)return!1;return i}(s[0],e,n,i):["origin","start","end","stack","shape"].indexOf(s)>=0&&s}function Qo(t,e,i){const s=[];for(let n=0;n=0;--e){const i=n[e].$filler;i&&(i.line.updateControlPoints(o,i.axis),s&&i.fill&&sa(t.ctx,i,o))}},beforeDatasetsDraw(t,e,i){if("beforeDatasetsDraw"!==i.drawTime)return;const s=t.getSortedVisibleDatasetMetas();for(let e=s.length-1;e>=0;--e){const i=s[e].$filler;Go(i)&&sa(t.ctx,i,t.chartArea)}},beforeDatasetDraw(t,e,i){const s=e.meta.$filler;Go(s)&&"beforeDatasetDraw"===i.drawTime&&sa(t.ctx,s,t.chartArea)},defaults:{propagate:!0,drawTime:"beforeDatasetDraw"}};const ha=(t,e)=>{let{boxHeight:i=e,boxWidth:s=e}=t;return t.usePointStyle&&(i=Math.min(i,e),s=t.pointStyleWidth||Math.min(s,e)),{boxWidth:s,boxHeight:i,itemHeight:Math.max(e,i)}};class ca extends Bs{constructor(t){super(),this._added=!1,this.legendHitBoxes=[],this._hoveredItem=null,this.doughnutMode=!1,this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this.legendItems=void 0,this.columnSizes=void 0,this.lineWidths=void 0,this.maxHeight=void 0,this.maxWidth=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.height=void 0,this.width=void 0,this._margins=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e,i){this.maxWidth=t,this.maxHeight=e,this._margins=i,this.setDimensions(),this.buildLabels(),this.fit()}setDimensions(){this.isHorizontal()?(this.width=this.maxWidth,this.left=this._margins.left,this.right=this.width):(this.height=this.maxHeight,this.top=this._margins.top,this.bottom=this.height)}buildLabels(){const t=this.options.labels||{};let e=d(t.generateLabels,[this.chart],this)||[];t.filter&&(e=e.filter((e=>t.filter(e,this.chart.data)))),t.sort&&(e=e.sort(((e,i)=>t.sort(e,i,this.chart.data)))),this.options.reverse&&e.reverse(),this.legendItems=e}fit(){const{options:t,ctx:e}=this;if(!t.display)return void(this.width=this.height=0);const i=t.labels,s=wi(i.font),n=s.size,o=this._computeTitleHeight(),{boxWidth:a,itemHeight:r}=ha(i,n);let l,h;e.font=s.string,this.isHorizontal()?(l=this.maxWidth,h=this._fitRows(o,n,a,r)+10):(h=this.maxHeight,l=this._fitCols(o,s,a,r)+10),this.width=Math.min(l,t.maxWidth||this.maxWidth),this.height=Math.min(h,t.maxHeight||this.maxHeight)}_fitRows(t,e,i,s){const{ctx:n,maxWidth:o,options:{labels:{padding:a}}}=this,r=this.legendHitBoxes=[],l=this.lineWidths=[0],h=s+a;let c=t;n.textAlign="left",n.textBaseline="middle";let d=-1,u=-h;return this.legendItems.forEach(((t,f)=>{const g=i+e/2+n.measureText(t.text).width;(0===f||l[l.length-1]+g+2*a>o)&&(c+=h,l[l.length-(f>0?0:1)]=0,u+=h,d++),r[f]={left:0,top:u,row:d,width:g,height:s},l[l.length-1]+=g+a})),c}_fitCols(t,e,i,s){const{ctx:n,maxHeight:o,options:{labels:{padding:a}}}=this,r=this.legendHitBoxes=[],l=this.columnSizes=[],h=o-t;let c=a,d=0,u=0,f=0,g=0;return this.legendItems.forEach(((t,o)=>{const{itemWidth:p,itemHeight:m}=function(t,e,i,s,n){const o=function(t,e,i,s){let n=t.text;n&&"string"!=typeof n&&(n=n.reduce(((t,e)=>t.length>e.length?t:e)));return e+i.size/2+s.measureText(n).width}(s,t,e,i),a=function(t,e,i){let s=t;"string"!=typeof e.text&&(s=da(e,i));return s}(n,s,e.lineHeight);return{itemWidth:o,itemHeight:a}}(i,e,n,t,s);o>0&&u+m+2*a>h&&(c+=d+a,l.push({width:d,height:u}),f+=d+a,g++,d=u=0),r[o]={left:f,top:u,col:g,width:p,height:m},d=Math.max(d,p),u+=m+a})),c+=d,l.push({width:d,height:u}),c}adjustHitBoxes(){if(!this.options.display)return;const t=this._computeTitleHeight(),{legendHitBoxes:e,options:{align:i,labels:{padding:s},rtl:n}}=this,o=Di(n,this.left,this.width);if(this.isHorizontal()){let n=0,a=ft(i,this.left+s,this.right-this.lineWidths[n]);for(const r of e)n!==r.row&&(n=r.row,a=ft(i,this.left+s,this.right-this.lineWidths[n])),r.top+=this.top+t+s,r.left=o.leftForLtr(o.x(a),r.width),a+=r.width+s}else{let n=0,a=ft(i,this.top+t+s,this.bottom-this.columnSizes[n].height);for(const r of e)r.col!==n&&(n=r.col,a=ft(i,this.top+t+s,this.bottom-this.columnSizes[n].height)),r.top=a,r.left+=this.left+s,r.left=o.leftForLtr(o.x(r.left),r.width),a+=r.height+s}}isHorizontal(){return"top"===this.options.position||"bottom"===this.options.position}draw(){if(this.options.display){const t=this.ctx;Re(t,this),this._draw(),Ie(t)}}_draw(){const{options:t,columnSizes:e,lineWidths:i,ctx:s}=this,{align:n,labels:o}=t,a=ue.color,r=Di(t.rtl,this.left,this.width),h=wi(o.font),{padding:c}=o,d=h.size,u=d/2;let f;this.drawTitle(),s.textAlign=r.textAlign("left"),s.textBaseline="middle",s.lineWidth=.5,s.font=h.string;const{boxWidth:g,boxHeight:p,itemHeight:m}=ha(o,d),b=this.isHorizontal(),x=this._computeTitleHeight();f=b?{x:ft(n,this.left+c,this.right-i[0]),y:this.top+c+x,line:0}:{x:this.left+c,y:ft(n,this.top+x+c,this.bottom-e[0].height),line:0},Ci(this.ctx,t.textDirection);const _=m+c;this.legendItems.forEach(((y,v)=>{s.strokeStyle=y.fontColor,s.fillStyle=y.fontColor;const M=s.measureText(y.text).width,w=r.textAlign(y.textAlign||(y.textAlign=o.textAlign)),k=g+u+M;let S=f.x,P=f.y;r.setWidth(this.width),b?v>0&&S+k+c>this.right&&(P=f.y+=_,f.line++,S=f.x=ft(n,this.left+c,this.right-i[f.line])):v>0&&P+_>this.bottom&&(S=f.x=S+e[f.line].width+c,f.line++,P=f.y=ft(n,this.top+x+c,this.bottom-e[f.line].height));if(function(t,e,i){if(isNaN(g)||g<=0||isNaN(p)||p<0)return;s.save();const n=l(i.lineWidth,1);if(s.fillStyle=l(i.fillStyle,a),s.lineCap=l(i.lineCap,"butt"),s.lineDashOffset=l(i.lineDashOffset,0),s.lineJoin=l(i.lineJoin,"miter"),s.lineWidth=n,s.strokeStyle=l(i.strokeStyle,a),s.setLineDash(l(i.lineDash,[])),o.usePointStyle){const a={radius:p*Math.SQRT2/2,pointStyle:i.pointStyle,rotation:i.rotation,borderWidth:n},l=r.xPlus(t,g/2);Le(s,a,l,e+u,o.pointStyleWidth&&g)}else{const o=e+Math.max((d-p)/2,0),a=r.leftForLtr(t,g),l=vi(i.borderRadius);s.beginPath(),Object.values(l).some((t=>0!==t))?We(s,{x:a,y:o,w:g,h:p,radius:l}):s.rect(a,o,g,p),s.fill(),0!==n&&s.stroke()}s.restore()}(r.x(S),P,y),S=gt(w,S+g+u,b?S+k:this.right,t.rtl),function(t,e,i){Ve(s,i.text,t,e+m/2,h,{strikethrough:i.hidden,textAlign:r.textAlign(i.textAlign)})}(r.x(S),P,y),b)f.x+=k+c;else if("string"!=typeof y.text){const t=h.lineHeight;f.y+=da(y,t)}else f.y+=_})),Oi(this.ctx,t.textDirection)}drawTitle(){const t=this.options,e=t.title,i=wi(e.font),s=Mi(e.padding);if(!e.display)return;const n=Di(t.rtl,this.left,this.width),o=this.ctx,a=e.position,r=i.size/2,l=s.top+r;let h,c=this.left,d=this.width;if(this.isHorizontal())d=Math.max(...this.lineWidths),h=this.top+l,c=ft(t.align,c,this.right-d);else{const e=this.columnSizes.reduce(((t,e)=>Math.max(t,e.height)),0);h=l+ft(t.align,this.top,this.bottom-e-t.labels.padding-this._computeTitleHeight())}const u=ft(a,c,c+d);o.textAlign=n.textAlign(ut(a)),o.textBaseline="middle",o.strokeStyle=e.color,o.fillStyle=e.color,o.font=i.string,Ve(o,e.text,u,h,i)}_computeTitleHeight(){const t=this.options.title,e=wi(t.font),i=Mi(t.padding);return t.display?e.lineHeight+i.height:0}_getLegendItemAt(t,e){let i,s,n;if(tt(t,this.left,this.right)&&tt(e,this.top,this.bottom))for(n=this.legendHitBoxes,i=0;it.chart.options.color,boxWidth:40,padding:10,generateLabels(t){const e=t.data.datasets,{labels:{usePointStyle:i,pointStyle:s,textAlign:n,color:o,useBorderRadius:a,borderRadius:r}}=t.legend.options;return t._getSortedDatasetMetas().map((t=>{const l=t.controller.getStyle(i?0:void 0),h=Mi(l.borderWidth);return{text:e[t.index].label,fillStyle:l.backgroundColor,fontColor:o,hidden:!t.visible,lineCap:l.borderCapStyle,lineDash:l.borderDash,lineDashOffset:l.borderDashOffset,lineJoin:l.borderJoinStyle,lineWidth:(h.width+h.height)/4,strokeStyle:l.borderColor,pointStyle:s||l.pointStyle,rotation:l.rotation,textAlign:n||l.textAlign,borderRadius:a&&(r||l.borderRadius),datasetIndex:t.index}}),this)}},title:{color:t=>t.chart.options.color,display:!1,position:"center",text:""}},descriptors:{_scriptable:t=>!t.startsWith("on"),labels:{_scriptable:t=>!["generateLabels","filter","sort"].includes(t)}}};class fa extends Bs{constructor(t){super(),this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this._padding=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e){const i=this.options;if(this.left=0,this.top=0,!i.display)return void(this.width=this.height=this.right=this.bottom=0);this.width=this.right=t,this.height=this.bottom=e;const s=n(i.text)?i.text.length:1;this._padding=Mi(i.padding);const o=s*wi(i.font).lineHeight+this._padding.height;this.isHorizontal()?this.height=o:this.width=o}isHorizontal(){const t=this.options.position;return"top"===t||"bottom"===t}_drawArgs(t){const{top:e,left:i,bottom:s,right:n,options:o}=this,a=o.align;let r,l,h,c=0;return this.isHorizontal()?(l=ft(a,i,n),h=e+t,r=n-i):("left"===o.position?(l=i+t,h=ft(a,s,e),c=-.5*C):(l=n-t,h=ft(a,e,s),c=.5*C),r=s-e),{titleX:l,titleY:h,maxWidth:r,rotation:c}}draw(){const t=this.ctx,e=this.options;if(!e.display)return;const i=wi(e.font),s=i.lineHeight/2+this._padding.top,{titleX:n,titleY:o,maxWidth:a,rotation:r}=this._drawArgs(s);Ve(t,e.text,0,0,i,{color:e.color,maxWidth:a,rotation:r,textAlign:ut(e.align),textBaseline:"middle",translation:[n,o]})}}var ga={id:"title",_element:fa,start(t,e,i){!function(t,e){const i=new fa({ctx:t.ctx,options:e,chart:t});ns.configure(t,i,e),ns.addBox(t,i),t.titleBlock=i}(t,i)},stop(t){const e=t.titleBlock;ns.removeBox(t,e),delete t.titleBlock},beforeUpdate(t,e,i){const s=t.titleBlock;ns.configure(t,s,i),s.options=i},defaults:{align:"center",display:!1,font:{weight:"bold"},fullSize:!0,padding:10,position:"top",text:"",weight:2e3},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const pa=new WeakMap;var ma={id:"subtitle",start(t,e,i){const s=new fa({ctx:t.ctx,options:i,chart:t});ns.configure(t,s,i),ns.addBox(t,s),pa.set(t,s)},stop(t){ns.removeBox(t,pa.get(t)),pa.delete(t)},beforeUpdate(t,e,i){const s=pa.get(t);ns.configure(t,s,i),s.options=i},defaults:{align:"center",display:!1,font:{weight:"normal"},fullSize:!0,padding:0,position:"top",text:"",weight:1500},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const ba={average(t){if(!t.length)return!1;let e,i,s=0,n=0,o=0;for(e=0,i=t.length;e-1?t.split("\n"):t}function ya(t,e){const{element:i,datasetIndex:s,index:n}=e,o=t.getDatasetMeta(s).controller,{label:a,value:r}=o.getLabelAndValue(n);return{chart:t,label:a,parsed:o.getParsed(n),raw:t.data.datasets[s].data[n],formattedValue:r,dataset:o.getDataset(),dataIndex:n,datasetIndex:s,element:i}}function va(t,e){const i=t.chart.ctx,{body:s,footer:n,title:o}=t,{boxWidth:a,boxHeight:r}=e,l=wi(e.bodyFont),h=wi(e.titleFont),c=wi(e.footerFont),d=o.length,f=n.length,g=s.length,p=Mi(e.padding);let m=p.height,b=0,x=s.reduce(((t,e)=>t+e.before.length+e.lines.length+e.after.length),0);if(x+=t.beforeBody.length+t.afterBody.length,d&&(m+=d*h.lineHeight+(d-1)*e.titleSpacing+e.titleMarginBottom),x){m+=g*(e.displayColors?Math.max(r,l.lineHeight):l.lineHeight)+(x-g)*l.lineHeight+(x-1)*e.bodySpacing}f&&(m+=e.footerMarginTop+f*c.lineHeight+(f-1)*e.footerSpacing);let _=0;const y=function(t){b=Math.max(b,i.measureText(t).width+_)};return i.save(),i.font=h.string,u(t.title,y),i.font=l.string,u(t.beforeBody.concat(t.afterBody),y),_=e.displayColors?a+2+e.boxPadding:0,u(s,(t=>{u(t.before,y),u(t.lines,y),u(t.after,y)})),_=0,i.font=c.string,u(t.footer,y),i.restore(),b+=p.width,{width:b,height:m}}function Ma(t,e,i,s){const{x:n,width:o}=i,{width:a,chartArea:{left:r,right:l}}=t;let h="center";return"center"===s?h=n<=(r+l)/2?"left":"right":n<=o/2?h="left":n>=a-o/2&&(h="right"),function(t,e,i,s){const{x:n,width:o}=s,a=i.caretSize+i.caretPadding;return"left"===t&&n+o+a>e.width||"right"===t&&n-o-a<0||void 0}(h,t,e,i)&&(h="center"),h}function wa(t,e,i){const s=i.yAlign||e.yAlign||function(t,e){const{y:i,height:s}=e;return it.height-s/2?"bottom":"center"}(t,i);return{xAlign:i.xAlign||e.xAlign||Ma(t,e,i,s),yAlign:s}}function ka(t,e,i,s){const{caretSize:n,caretPadding:o,cornerRadius:a}=t,{xAlign:r,yAlign:l}=i,h=n+o,{topLeft:c,topRight:d,bottomLeft:u,bottomRight:f}=vi(a);let g=function(t,e){let{x:i,width:s}=t;return"right"===e?i-=s:"center"===e&&(i-=s/2),i}(e,r);const p=function(t,e,i){let{y:s,height:n}=t;return"top"===e?s+=i:s-="bottom"===e?n+i:n/2,s}(e,l,h);return"center"===l?"left"===r?g+=h:"right"===r&&(g-=h):"left"===r?g-=Math.max(c,u)+n:"right"===r&&(g+=Math.max(d,f)+n),{x:J(g,0,s.width-e.width),y:J(p,0,s.height-e.height)}}function Sa(t,e,i){const s=Mi(i.padding);return"center"===e?t.x+t.width/2:"right"===e?t.x+t.width-s.right:t.x+s.left}function Pa(t){return xa([],_a(t))}function Da(t,e){const i=e&&e.dataset&&e.dataset.tooltip&&e.dataset.tooltip.callbacks;return i?t.override(i):t}const Ca={beforeTitle:e,title(t){if(t.length>0){const e=t[0],i=e.chart.data.labels,s=i?i.length:0;if(this&&this.options&&"dataset"===this.options.mode)return e.dataset.label||"";if(e.label)return e.label;if(s>0&&e.dataIndex{const e={before:[],lines:[],after:[]},n=Da(i,t);xa(e.before,_a(Oa(n,"beforeLabel",this,t))),xa(e.lines,Oa(n,"label",this,t)),xa(e.after,_a(Oa(n,"afterLabel",this,t))),s.push(e)})),s}getAfterBody(t,e){return Pa(Oa(e.callbacks,"afterBody",this,t))}getFooter(t,e){const{callbacks:i}=e,s=Oa(i,"beforeFooter",this,t),n=Oa(i,"footer",this,t),o=Oa(i,"afterFooter",this,t);let a=[];return a=xa(a,_a(s)),a=xa(a,_a(n)),a=xa(a,_a(o)),a}_createItems(t){const e=this._active,i=this.chart.data,s=[],n=[],o=[];let a,r,l=[];for(a=0,r=e.length;at.filter(e,s,n,i)))),t.itemSort&&(l=l.sort(((e,s)=>t.itemSort(e,s,i)))),u(l,(e=>{const i=Da(t.callbacks,e);s.push(Oa(i,"labelColor",this,e)),n.push(Oa(i,"labelPointStyle",this,e)),o.push(Oa(i,"labelTextColor",this,e))})),this.labelColors=s,this.labelPointStyles=n,this.labelTextColors=o,this.dataPoints=l,l}update(t,e){const i=this.options.setContext(this.getContext()),s=this._active;let n,o=[];if(s.length){const t=ba[i.position].call(this,s,this._eventPosition);o=this._createItems(i),this.title=this.getTitle(o,i),this.beforeBody=this.getBeforeBody(o,i),this.body=this.getBody(o,i),this.afterBody=this.getAfterBody(o,i),this.footer=this.getFooter(o,i);const e=this._size=va(this,i),a=Object.assign({},t,e),r=wa(this.chart,i,a),l=ka(i,a,r,this.chart);this.xAlign=r.xAlign,this.yAlign=r.yAlign,n={opacity:1,x:l.x,y:l.y,width:e.width,height:e.height,caretX:t.x,caretY:t.y}}else 0!==this.opacity&&(n={opacity:0});this._tooltipItems=o,this.$context=void 0,n&&this._resolveAnimations().update(this,n),t&&i.external&&i.external.call(this,{chart:this.chart,tooltip:this,replay:e})}drawCaret(t,e,i,s){const n=this.getCaretPosition(t,i,s);e.lineTo(n.x1,n.y1),e.lineTo(n.x2,n.y2),e.lineTo(n.x3,n.y3)}getCaretPosition(t,e,i){const{xAlign:s,yAlign:n}=this,{caretSize:o,cornerRadius:a}=i,{topLeft:r,topRight:l,bottomLeft:h,bottomRight:c}=vi(a),{x:d,y:u}=t,{width:f,height:g}=e;let p,m,b,x,_,y;return"center"===n?(_=u+g/2,"left"===s?(p=d,m=p-o,x=_+o,y=_-o):(p=d+f,m=p+o,x=_-o,y=_+o),b=p):(m="left"===s?d+Math.max(r,h)+o:"right"===s?d+f-Math.max(l,c)-o:this.caretX,"top"===n?(x=u,_=x-o,p=m-o,b=m+o):(x=u+g,_=x+o,p=m+o,b=m-o),y=x),{x1:p,x2:m,x3:b,y1:x,y2:_,y3:y}}drawTitle(t,e,i){const s=this.title,n=s.length;let o,a,r;if(n){const l=Di(i.rtl,this.x,this.width);for(t.x=Sa(this,i.titleAlign,i),e.textAlign=l.textAlign(i.titleAlign),e.textBaseline="middle",o=wi(i.titleFont),a=i.titleSpacing,e.fillStyle=i.titleColor,e.font=o.string,r=0;r0!==t))?(t.beginPath(),t.fillStyle=n.multiKeyBackground,We(t,{x:e,y:p,w:h,h:l,radius:r}),t.fill(),t.stroke(),t.fillStyle=a.backgroundColor,t.beginPath(),We(t,{x:i,y:p+1,w:h-2,h:l-2,radius:r}),t.fill()):(t.fillStyle=n.multiKeyBackground,t.fillRect(e,p,h,l),t.strokeRect(e,p,h,l),t.fillStyle=a.backgroundColor,t.fillRect(i,p+1,h-2,l-2))}t.fillStyle=this.labelTextColors[i]}drawBody(t,e,i){const{body:s}=this,{bodySpacing:n,bodyAlign:o,displayColors:a,boxHeight:r,boxWidth:l,boxPadding:h}=i,c=wi(i.bodyFont);let d=c.lineHeight,f=0;const g=Di(i.rtl,this.x,this.width),p=function(i){e.fillText(i,g.x(t.x+f),t.y+d/2),t.y+=d+n},m=g.textAlign(o);let b,x,_,y,v,M,w;for(e.textAlign=o,e.textBaseline="middle",e.font=c.string,t.x=Sa(this,m,i),e.fillStyle=i.bodyColor,u(this.beforeBody,p),f=a&&"right"!==m?"center"===o?l/2+h:l+2+h:0,y=0,M=s.length;y0&&e.stroke()}_updateAnimationTarget(t){const e=this.chart,i=this.$animations,s=i&&i.x,n=i&&i.y;if(s||n){const i=ba[t.position].call(this,this._active,this._eventPosition);if(!i)return;const o=this._size=va(this,t),a=Object.assign({},i,this._size),r=wa(e,t,a),l=ka(t,a,r,e);s._to===l.x&&n._to===l.y||(this.xAlign=r.xAlign,this.yAlign=r.yAlign,this.width=o.width,this.height=o.height,this.caretX=i.x,this.caretY=i.y,this._resolveAnimations().update(this,l))}}_willRender(){return!!this.opacity}draw(t){const e=this.options.setContext(this.getContext());let i=this.opacity;if(!i)return;this._updateAnimationTarget(e);const s={width:this.width,height:this.height},n={x:this.x,y:this.y};i=Math.abs(i)<.001?0:i;const o=Mi(e.padding),a=this.title.length||this.beforeBody.length||this.body.length||this.afterBody.length||this.footer.length;e.enabled&&a&&(t.save(),t.globalAlpha=i,this.drawBackground(n,t,s,e),Ci(t,e.textDirection),n.y+=o.top,this.drawTitle(n,t,e),this.drawBody(n,t,e),this.drawFooter(n,t,e),Oi(t,e.textDirection),t.restore())}getActiveElements(){return this._active||[]}setActiveElements(t,e){const i=this._active,s=t.map((({datasetIndex:t,index:e})=>{const i=this.chart.getDatasetMeta(t);if(!i)throw new Error("Cannot find a dataset at index "+t);return{datasetIndex:t,element:i.data[e],index:e}})),n=!f(i,s),o=this._positionChanged(s,e);(n||o)&&(this._active=s,this._eventPosition=e,this._ignoreReplayEvents=!0,this.update(!0))}handleEvent(t,e,i=!0){if(e&&this._ignoreReplayEvents)return!1;this._ignoreReplayEvents=!1;const s=this.options,n=this._active||[],o=this._getActiveElements(t,n,e,i),a=this._positionChanged(o,t),r=e||!f(o,n)||a;return r&&(this._active=o,(s.enabled||s.external)&&(this._eventPosition={x:t.x,y:t.y},this.update(!0,e))),r}_getActiveElements(t,e,i,s){const n=this.options;if("mouseout"===t.type)return[];if(!s)return e;const o=this.chart.getElementsAtEventForMode(t,n.mode,n,i);return n.reverse&&o.reverse(),o}_positionChanged(t,e){const{caretX:i,caretY:s,options:n}=this,o=ba[n.position].call(this,t,e);return!1!==o&&(i!==o.x||s!==o.y)}}var Ta={id:"tooltip",_element:Aa,positioners:ba,afterInit(t,e,i){i&&(t.tooltip=new Aa({chart:t,options:i}))},beforeUpdate(t,e,i){t.tooltip&&t.tooltip.initialize(i)},reset(t,e,i){t.tooltip&&t.tooltip.initialize(i)},afterDraw(t){const e=t.tooltip;if(e&&e._willRender()){const i={tooltip:e};if(!1===t.notifyPlugins("beforeTooltipDraw",{...i,cancelable:!0}))return;e.draw(t.ctx),t.notifyPlugins("afterTooltipDraw",i)}},afterEvent(t,e){if(t.tooltip){const i=e.replay;t.tooltip.handleEvent(e.event,i,e.inChartArea)&&(e.changed=!0)}},defaults:{enabled:!0,external:null,position:"average",backgroundColor:"rgba(0,0,0,0.8)",titleColor:"#fff",titleFont:{weight:"bold"},titleSpacing:2,titleMarginBottom:6,titleAlign:"left",bodyColor:"#fff",bodySpacing:2,bodyFont:{},bodyAlign:"left",footerColor:"#fff",footerSpacing:2,footerMarginTop:6,footerFont:{weight:"bold"},footerAlign:"left",padding:6,caretPadding:2,caretSize:5,cornerRadius:6,boxHeight:(t,e)=>e.bodyFont.size,boxWidth:(t,e)=>e.bodyFont.size,multiKeyBackground:"#fff",displayColors:!0,boxPadding:0,borderColor:"rgba(0,0,0,0)",borderWidth:0,animation:{duration:400,easing:"easeOutQuart"},animations:{numbers:{type:"number",properties:["x","y","width","height","caretX","caretY"]},opacity:{easing:"linear",duration:200}},callbacks:Ca},defaultRoutes:{bodyFont:"font",footerFont:"font",titleFont:"font"},descriptors:{_scriptable:t=>"filter"!==t&&"itemSort"!==t&&"external"!==t,_indexable:!1,callbacks:{_scriptable:!1,_indexable:!1},animation:{_fallback:!1},animations:{_fallback:"animation"}},additionalOptionScopes:["interaction"]};return Mn.register(Fn,Io,no,t),Mn.helpers={...Vi},Mn._adapters=Pn,Mn.Animation=Ss,Mn.Animations=Ps,Mn.animator=xt,Mn.controllers=Zs.controllers.items,Mn.DatasetController=Vs,Mn.Element=Bs,Mn.elements=no,Mn.Interaction=Yi,Mn.layouts=ns,Mn.platforms=Ms,Mn.Scale=qs,Mn.Ticks=ae,Object.assign(Mn,Fn,Io,no,t,Ms),Mn.Chart=Mn,"undefined"!=typeof window&&(window.Chart=Mn),Mn})); +//# sourceMappingURL=chart.umd.js.map diff --git a/apps/health/interface.html b/apps/health/interface.html index a708e2645..68c71ff83 100644 --- a/apps/health/interface.html +++ b/apps/health/interface.html @@ -3,9 +3,10 @@ -
+
+ + + + \ No newline at end of file diff --git a/apps/messagesdebug/metadata.json b/apps/messagesdebug/metadata.json new file mode 100644 index 000000000..af3ff0622 --- /dev/null +++ b/apps/messagesdebug/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "messagesdebug", + "name": "Messages Debug", + "version": "0.01", + "description": "Write all messages to a file, for debugging purposes", + "icon": "app.png", + "type": "bootloader", + "tags": "tool,system", + "supports": ["BANGLEJS","BANGLEJS2"], + "interface": "interface.html", + "storage": [ + {"name":"messagesdebug.boot.js","url":"boot.js"} + ], + "data": [{"name":"messagesdebug.log"}] +} diff --git a/apps/messagesmusic/ChangeLog b/apps/messagesmusic/ChangeLog index 8cc3079a3..cd1c49b60 100644 --- a/apps/messagesmusic/ChangeLog +++ b/apps/messagesmusic/ChangeLog @@ -2,4 +2,5 @@ 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 \ No newline at end of file + Fix loading message UI +0.05: Ensure we don't clear artist info diff --git a/apps/messagesmusic/app.js b/apps/messagesmusic/app.js index 26fedecc1..68e88c2d8 100644 --- a/apps/messagesmusic/app.js +++ b/apps/messagesmusic/app.js @@ -1 +1,2 @@ -setTimeout(()=>require('messages').openGUI({"t":"add","artist":" ","album":" ","track":" ","dur":0,"c":-1,"n":-1,"id":"music","title":"Music","state":"play","new":true})); +// don't define artist/etc here so we don't wipe them out of memory if they were stored from before +setTimeout(()=>require('messages').openGUI({"t":"add","id":"music","state":"show","new":true})); diff --git a/apps/messagesmusic/metadata.json b/apps/messagesmusic/metadata.json index 22e0eff52..eef528f55 100644 --- a/apps/messagesmusic/metadata.json +++ b/apps/messagesmusic/metadata.json @@ -1,7 +1,8 @@ { "id": "messagesmusic", "name":"Messages Music", - "version":"0.04", + "shortName": "Music", + "version":"0.05", "description": "Uses Messages library to push a music message which in turn displays Messages app music controls", "icon":"app.png", "type": "app", diff --git a/apps/mitherm/ChangeLog b/apps/mitherm/ChangeLog new file mode 100644 index 000000000..630459c15 --- /dev/null +++ b/apps/mitherm/ChangeLog @@ -0,0 +1 @@ +0.01: Create mitherm app with support for pvvx firmware only diff --git a/apps/mitherm/README.md b/apps/mitherm/README.md new file mode 100644 index 000000000..cdf3daa61 --- /dev/null +++ b/apps/mitherm/README.md @@ -0,0 +1,22 @@ +Reads BLE advertisement data from Xiaomi temperature/humidity sensors running the +`pvvx` custom firmware (https://github.com/pvvx/ATC_MiThermometer). + +## Features + +* Display temperature +* Display humidity +* Display battery state of sensor +* Auto-refresh every 5 minutes +* Manual refresh on demand +* Add aliases for MAC addresses to easily recognise devices + +## Planned features + +* Supprt for other advertising formats: + * atc1441 format + * BTHome + * Xiaomi Mijia format +* Configurable auto-refresh interval +* Configurable scan length (currently 30s) +* Alerts when temperature outside defined limits (with a widget or bootcode to + work when app is inactive) diff --git a/apps/mitherm/app-icon.js b/apps/mitherm/app-icon.js new file mode 100644 index 000000000..2e8737704 --- /dev/null +++ b/apps/mitherm/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AH4Ac5gWVhnM4AWVAAIYTCwQABCywYRIoYADJJwWHDB4RD5sz7hJPFIlP//0MRxFE6f/AAM9JJgWE4gWCAANMDBZcEn4XE+ZiKFwhcBCYPdDYRiEGAoXDLgf97vfMQwXILggXFMQYXHLgoXB6czMQoXHLgQXJMQQXG4YWEI44ABngXGh4XHF4v/+DAGC6DXGC5BHGC509F4IXTdwIABV4gXOIwIABJAoX/C6p3Xa4a/UABAXfgczABswC/4XmAH4A/ABY")) diff --git a/apps/mitherm/app.js b/apps/mitherm/app.js new file mode 100644 index 000000000..b7abdb2fc --- /dev/null +++ b/apps/mitherm/app.js @@ -0,0 +1,172 @@ +var filterTemperature = [{ + serviceData: { + "181a": {} + } +}]; +var results = {}; +var macs = []; + +var aliases = require("Storage").readJSON("mitherm.json", true); +if (!aliases) aliases = {}; + +var lastSeen = {}; +var current = 0; +var scanning = false; +var timeoutDraw; +var timeoutScan; + + +const scan = function() { + if (!scanning) { // Don't start scanning if already doing so. + scanning = true; + if (timeoutScan) clearTimeout(timeoutScan); + timeoutScan = setTimeout(scan, 300000); // Scan again in 5 minutes. + drawScanState(scanning); + NRF.findDevices(function(devices) { + onDevices(devices); + }, { + filters: filterTemperature, + timeout: 30000 // Scan for 30s + }); + } +}; + + +const onDevices = function(devices) { + let now = Date.now(); + for (let i = 0; i < devices.length; i++) { + let device = devices[i]; + + let processedData = extractData(device.data); + console.log({ + rssi: device.rssi, + data: processedData + }); + if (!macs.includes(processedData.MAC)) { + macs.push(processedData.MAC); + } + results[processedData.MAC] = processedData; + lastSeen[processedData.MAC] = now; + } + console.log("Scan complete."); + scanning = false; + writeOutput(); +}; + + +const extractData = function(thedata) { + let data = DataView(thedata); + let MAC = []; + for (let i = 9; i > 3; i--) { + MAC.push(data.getUint8(i, true).toString(16).padStart(2, "0")); + } + out = { + size: data.getUint8(0, true), + uid: data.getUint8(1, true), + UUID: data.getUint16(2, true), + MAC: MAC.join(":"), + temperature: data.getInt16(10, true) * 0.01, + humidity: data.getUint16(12, true) * 0.01, + battery_mv: data.getUint16(14, true), + battery_level: data.getUint8(16, true), + }; + return out; +}; + + +const writeOutput = function() { + let now = Date.now(); + if (timeoutDraw) clearTimeout(timeoutDraw); + timeoutDraw = setTimeout(writeOutput, 60000); // Refresh in 1 minute. + g.clear(true); + Bangle.drawWidgets(); + g.reset(); + drawScanState(scanning); + + if (macs.length == 0) return; + + processedData = results[macs[current]]; + g.setFont12x20(2); + g.drawString(`${processedData.temperature.toFixed(2)}°C`, 10, 30); + g.drawString(`${processedData.humidity.toFixed(2)} %`, 10, 70); + + g.setFont6x15(); + g.drawString(`${((now - lastSeen[macs[current]]) / 60000).toFixed(0)} min ago`, 10, 130); + g.drawString(`${processedData.battery_level} % battery`, 80, 130); + g.drawString(` ${processedData.MAC in aliases ? aliases[processedData.MAC] : processedData.MAC}: ${current + 1} / ${macs.length}`, 10, 150); +}; + + +const scrollDevices = function(directionLR) { + // Swipe left or right to move between devices. + current -= directionLR; // inverted feels a more familiar gesture. + if (current + 1 > macs.length) + current = 0; + if (current < 0) + current = macs.length - 1; + writeOutput(); +}; + +const drawScanState = function(state) { + if (state) + g.fillRect(160, 160, 170, 170); + else + g.clearRect(160, 160, 170, 170); +}; + +const setAlias = function(mac, alias) { + if (alias === "") { + delete aliases[mac]; + } + else { + aliases[mac] = alias; + require("Storage").writeJSON("mitherm.json", aliases); + } +}; + +const changeAlias = function(mac) { + g.clear(); + require("textinput").input((mac in aliases) ? aliases[mac] : "").then(function(text) { + setAlias(mac, text); + setUI(); + writeOutput(); + }); +}; + + +const setUI = function() { + Bangle.setUI({ + mode: "custom", + swipe: scrollDevices, + btn: function() { + E.showMenu(actionsMenu); + } + }); +}; + + +const actionsMenu = { + "": { + "title": "-- Actions --", + "back": function() { + E.showMenu(); + }, + "remove": function() { + setUI(); + writeOutput(); + }, + }, + "Scan now": function() { + scan(); + E.showMenu(); + }, + "Edit alias": function() { + changeAlias(macs[current]); + }, +}; + +setUI(); +Bangle.loadWidgets(); +g.setClipRect(Bangle.appRect); +scan(); +writeOutput(); diff --git a/apps/mitherm/app.png b/apps/mitherm/app.png new file mode 100644 index 000000000..81d6bb24f Binary files /dev/null and b/apps/mitherm/app.png differ diff --git a/apps/mitherm/metadata.json b/apps/mitherm/metadata.json new file mode 100644 index 000000000..a8da6fd26 --- /dev/null +++ b/apps/mitherm/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "mitherm", + "name": "Xiaomi Mijia Temperature and Humidity display", + "shortName": "MiTherm", + "version": "0.01", + "description": "Reads and displays data from Xiaomi temperature/humidity sensors running custom firmware", + "icon": "app.png", + "tags": "xiaomi,mi,ble,bluetooth,thermometer,humidity", + "readme": "README.md", + "supports": ["BANGLEJS", "BANGLEJS2"], + "storage": [ + {"name":"mitherm.app.js","url":"app.js"}, + {"name":"mitherm.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/mixdiganclock/ChangeLog b/apps/mixdiganclock/ChangeLog new file mode 100644 index 000000000..5354e44a7 --- /dev/null +++ b/apps/mixdiganclock/ChangeLog @@ -0,0 +1,3 @@ +0.01: fork from miclock, Added compatib with b widgets, devices(dynamic x,y) and themes(dynamic colors) +0.02: Code refactored, change colors in real time +0.03: Hour point size can be modified on real time. \ No newline at end of file diff --git a/apps/mixdiganclock/README.md b/apps/mixdiganclock/README.md new file mode 100644 index 000000000..aa860b4d4 --- /dev/null +++ b/apps/mixdiganclock/README.md @@ -0,0 +1,58 @@ +# Mix Digital & Analog Clock +A dual and simultaneous Analog and Digital Clock, also shows day, month and year. +Color are automatically set depending on the configured Theme or device, bunt also change on realtime through touching the right side. + +Compatible with BangleJS1,BangleJS2,and EMSCRIPTENx emulators + +## Pictures: + +Bangle JS1 + +![](photo_mixdigan_bjs1.jpg) + +Screenshot emulator BJS2 + +![](ss_mixdigan_ems2.png) + +Screenshot emulator BJS1 + +![](ss_mixdigan_ems.png) + + +SS emulator -color change +![](ss_mixdigan_ems_2.png) + +SS emulator -color change +![](ss_mixdigan_ems2_2.png) + +SS emulator -color change +![](ss_mixdigan_ems2_3.png) + +## Usage + +Open and see + +## Features + +Compatibility with devices +Dynamic Colours and positions +Support for bottom widgets + + +## Controls + +Exit : BTN2 (BJS1) +Exit/launcher : left area +Change Color : right area +Increase Hour Points : swipe right +Decrease Hour Points : swipe left + + +## Coming soon +A better color combination + +## Support + +This app is so basic that probably the easiest is to just edit the code ;) + +Otherwise you can contact me [here](https://github.com/dapgo/my_espruino_smartwatch_things) \ No newline at end of file diff --git a/apps/mixdiganclock/app-icon.js b/apps/mixdiganclock/app-icon.js new file mode 100644 index 000000000..8663b48d0 --- /dev/null +++ b/apps/mixdiganclock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4X/AAN994JB8noH+cFqtVqALHr5IB+oWHKgX/DAwWCDA8BA4IvBB4NABYcH/4KBAAP/6ALDj4WCDAX0BYd/6tVq2VqtX/ouECgILCEgIwCgP9BYt/BYUHFwQLDr48CBZcfHQILEq5ICBZd/LgQLDqpUCBbYAEC4+a1WlBaYjLBZNaIwIBBEYwLNy2WC6KbBC4qnFC4oLDZYILFa4oLJfYYADfYcB/4LF/4LCKgLkCcQRSCJAX1BYdfIwQ8CEgn/HQQwDDAVfFwgABA4IAC+oKEgEFBYdQBYoYDCwwYCF4IWHAFgA=")) \ No newline at end of file diff --git a/apps/mixdiganclock/metadata.json b/apps/mixdiganclock/metadata.json new file mode 100644 index 000000000..8fae2dd8f --- /dev/null +++ b/apps/mixdiganclock/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "mixdiganclock", + "name": "Mix Dig&Anal Clock", + "version": "0.03", + "description": "A dual Analog and Digital Clock, based in Mixed Clock, but with more compatibility, change of colors, thicker clock hands... ", + "icon": "mixdiganclock.png", + "type": "clock", + "tags": "clock", + "screenshots": [{"url":"pic_mixdigan_bjs1.jpg"}], + "supports": ["BANGLEJS","BANGLEJS"], + "readme": "README.md", + "allow_emulator": true, + "storage": [ + {"name":"mixdiganclock.app.js","url":"mixdiganclock.app.js"}, + {"name":"mixdiganclock.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/mixdiganclock/mixdiganclock.app.js b/apps/mixdiganclock/mixdiganclock.app.js new file mode 100644 index 000000000..bd36c0f32 --- /dev/null +++ b/apps/mixdiganclock/mixdiganclock.app.js @@ -0,0 +1,230 @@ +/*fork of miclock, dynamic x,y, colors on realtime, +compatible with BJS1, BJS2 and bottom widgets +*/ +var locale = require("locale"); +var v_mode_debug=0; //, 0=no, 1 min, 2 prone detail +var v_model=process.env.BOARD; +var LastDrawDay; // to notice a change and repaint static texts +//RGB565 0x White , black, 'Orange',blue,red, olive,... +var a_colors= [0xFFFF,0x0000, 0xFD20, 0x001F,0xF800,0x7be0,0x780F,0x07E0]; //new Array(0xFFFF +var Radius= []; //new Array(); +var TxtPosition=[]; +var v_bfont_size; +var v_vfont_size; +var v_color1; +var v_color2; +var v_color3; +var v_color_erase; +var v_count_col; +var rect = Bangle.appRect; +var v_center_x; +var v_center_y; +if (v_mode_debug>0) console.log("a_colors.length "+a_colors.length); + +g.clear(); +//show the exit button +//Bangle.setUI(); +Bangle.setUI("clock"); //implies center button for launcher +/*{ + mode : "custom", + back : Bangle.showLauncher +});*/ + +Bangle.loadWidgets(); + + + +function setVariables() { +// different values depending on loaded widgets or not, so after load widgets + rect = Bangle.appRect; + v_center_x = g.getWidth()/2; + v_center_y = g.getHeight()/2; //vertical middle + //if (v_mode_debug>1) console.log(v_model+" center x, y "+v_center_x+" , "+v_center_y+" Max y,y2"+rect.y+" ,"+rect.y2); + TxtPosition = { + "x1": 3, "x2": g.getWidth()-3, + "y1": rect.y+17, "y2": rect.y2-6, + "x_HH": g.getWidth()/2 ,"y_mm": v_center_y+32 + }; + + //emuls EMSCRIPTEN,EMSCRIPTEN2 + v_count_col=2; //1st=0 1st compatible color (dark/light theme) + v_color_erase=g.getBgColor(); + if (v_model=='BANGLEJS'||v_model=='EMSCRIPTEN') { + Radius = { "center": 7, "hour": 50, "min": 70, "dots": 88,"circleH":6,"circleM":2 }; + v_bfont_size=3; + v_vfont_size=35; + v_color1=2; // orange + v_color2=4; + v_color3=0; //white , for hands PEND replace hardcoded by logic + }else{ + Radius = { "center": 5, "hour": 35, "min": 50, "dots": 60, "circleH":5,"circleM":2 }; + v_bfont_size=2; + v_vfont_size=22; + v_color1=3; // blue + v_color2=1; + v_color3=1; //opposite to bg, for hands PEND replace hardcoded by logic + } + if (v_mode_debug>0) console.log("set vars for: "+v_model); +} + + +function rotatePoint(x, y, d) { + rad = -1 * d / 180 * Math.PI; + var sin = Math.sin(rad); + var cos = Math.cos(rad); + xn = ((v_center_x + x * cos - y * sin) + 0.5) | 0; + yn = ((v_center_y + x * sin - y * cos) + 0.5) | 0; + p = [xn, yn]; + return p; +} + +//no need to repaint +function drawStaticRing(v_colorparam){ + // draw hour and minute dots + if (v_mode_debug>0) console.log("color: "+v_colorparam); + //g.setColor(a_colors[v_color1]); + g.setColor(v_colorparam); + + for (i = 0; i < 60; i++) { + // ? 2 : 4; + radius = (i % 5) ? Radius.circleM : Radius.circleH; + point = rotatePoint(0, Radius.dots, i * 6); + //if (v_mode_debug>1) console.log("point"+point); + g.fillCircle(point[0], point[1], radius); + } +} + +//no need to repaint every min +function drawDailyTxt(){ + var date = new Date(); + var isEn = locale.name.startsWith("en"); + var dateArray = date.toString().split(" "); + LastDrawDay=locale.dow(date,true); + var hour = date.getHours(); + + if (v_mode_debug>1) { + console.log("full date "+date.toString()); + console.log("locale time "+locale.time(date,true)); + console.log("LastDrawDay "+LastDrawDay); + console.log("locale new day "+(locale.dow(date,true))); + } + g.setColor(a_colors[v_color2]); + //small size then bitmap + g.setFont("4x6", v_bfont_size); //6x8 + g.setFontAlign(-1, 0); + g.drawString(locale.dow(date,true) + ' ',TxtPosition.x1 , TxtPosition.y1, true); + g.drawString(isEn?(' ' + dateArray[2]):locale.month(date,true), TxtPosition.x1, TxtPosition.y2, true); + g.setFontAlign(1, 0); + g.drawString(isEn?locale.month(date,true):(' ' + dateArray[2]), TxtPosition.x2, TxtPosition.y1, true); + g.drawString(dateArray[3], TxtPosition.x2, TxtPosition.y2, true); +} + + +function drawMixedClock() { + var date = new Date(); + var dateArray = date.toString().split(" "); + //var isEn = locale.name.startsWith("en"); + var point = []; + var minute = date.getMinutes(); + var hour = date.getHours(); + var radius; + //Call function only after a change of day + if (LastDrawDay!=locale.dow(date,true)) drawDailyTxt(); + //ERASE previous hands + // erase last MINutes hand + g.setColor(v_color_erase); + point = rotatePoint(0, Radius.min, (minute - 1) * 6); + g.drawLine(v_center_x, v_center_y, point[0], point[1]); + //to increase thicknes + g.drawLine(v_center_x+1, v_center_y, point[0]+1, point[1]); + // erase last two HOUR hands ¿2? + g.setColor(v_color_erase); + p = rotatePoint(0, Radius.hour, hour % 12 * 30 + (minute - 2) / 2 | 0); + g.drawLine(v_center_x, v_center_y, p[0], p[1]); + //to increase thicknes + g.drawLine(v_center_x+1, v_center_y, p[0]+1, p[1]); + + point = rotatePoint(0, Radius.hour, hour % 12 * 30 + (minute - 1) / 2 | 0); + g.drawLine(v_center_x, v_center_y, point[0], point[1]); + //to increase thicknes + g.drawLine(v_center_x+1, v_center_y, point[0]+1, point[1]); + + // here time DIGITs are draw under hands + + // draw new MINute hand + point = rotatePoint(0, Radius.min, minute * 6); + g.setColor(a_colors[v_color3]); + g.drawLine(v_center_x, v_center_y, point[0], point[1]); + //to increase thicknes + g.drawLine(v_center_x+1, v_center_y, point[0]+1, point[1]); + // draw new HOUR hand + point = rotatePoint(0, Radius.hour, hour % 12 * 30 + date.getMinutes() / 2 | 0); + g.setColor(a_colors[v_color3]); + g.drawLine(v_center_x, v_center_y, point[0], point[1]); + //to increase thicknes + g.drawLine(v_center_x+1, v_center_y, point[0]+1, point[1]); + + // draw DIGITs of time above hands for better UX + //g.setFont("6x8", 3); 3 bigger size + g.setFontVector(v_vfont_size); + g.setColor(a_colors[v_color2]); + g.setFontAlign(0, 0); + //by default 24H, to use format config 12H 24H read from locale + g.drawString(dateArray[4].substr(0, 5), TxtPosition.x_HH, TxtPosition.y_mm, true); + // the central point requires redrawing because hands draw over it + g.setColor(a_colors[v_color1]); + g.fillCircle(v_center_x, v_center_y, Radius.center); +} +function UserInput(){ + Bangle.on('touch', function(button){ + switch(button){ + case 1: + Bangle.showLauncher(); + break; + case 2: + //testing to improve + if (v_mode_debug>0) console.log("v_count_col/total: "+v_count_col+"/"+a_colors.length); + if (v_count_col0) console.log("paint on color: "+v_count_col); + drawStaticRing(a_colors[v_color1]); + drawDailyTxt(); + break; + case 3: + //console.log("Touch 3 aka 1+2 not for emul");//center 1+2 + break; + } + }); + Bangle.on('swipe', dir => { + if(dir == 1) { + drawStaticRing(v_color_erase); + if (Radius.circleH<13) Radius.circleH++; + if (v_mode_debug>0) console.log("radio: "+Radius.circleH); + drawStaticRing(a_colors[v_color1]); + } + else { + drawStaticRing(v_color_erase); + if (Radius.circleH>1) Radius.circleH--; + if (v_mode_debug>0) console.log("radio: "+Radius.circleH); + drawStaticRing(a_colors[v_color1]); + } + }); +} +Bangle.on('lcdPower', function(on) { + if (on) + drawMixedClock(); +}); + +setVariables(); +Bangle.drawWidgets(); +UserInput(); + +setInterval(drawMixedClock, 5E3); +drawStaticRing(a_colors[v_color1]); +drawDailyTxt(); //1st time +drawMixedClock(); diff --git a/apps/mixdiganclock/mixdiganclock.info b/apps/mixdiganclock/mixdiganclock.info new file mode 100644 index 000000000..f9057ef5a --- /dev/null +++ b/apps/mixdiganclock/mixdiganclock.info @@ -0,0 +1 @@ +{"id":"mixdiganclock","name":"Mix Dig&Anal","type":"clock","src":"mixdiganclock.app.js","icon":"mixdiganclock.img","version":"0.02","tags":"clock","files":"mixdiganclock.info,mixdiganclock.app.js,mixdiganclock.img"} \ No newline at end of file diff --git a/apps/mixdiganclock/mixdiganclock.png b/apps/mixdiganclock/mixdiganclock.png new file mode 100644 index 000000000..9881aed85 Binary files /dev/null and b/apps/mixdiganclock/mixdiganclock.png differ diff --git a/apps/mixdiganclock/pic_mixdigan_bjs1.jpg b/apps/mixdiganclock/pic_mixdigan_bjs1.jpg new file mode 100644 index 000000000..eeb23a8ee Binary files /dev/null and b/apps/mixdiganclock/pic_mixdigan_bjs1.jpg differ diff --git a/apps/mixdiganclock/ss_mixdigan_ems.png b/apps/mixdiganclock/ss_mixdigan_ems.png new file mode 100644 index 000000000..0c4b6fab1 Binary files /dev/null and b/apps/mixdiganclock/ss_mixdigan_ems.png differ diff --git a/apps/mixdiganclock/ss_mixdigan_ems2.png b/apps/mixdiganclock/ss_mixdigan_ems2.png new file mode 100644 index 000000000..fee8b8ef3 Binary files /dev/null and b/apps/mixdiganclock/ss_mixdigan_ems2.png differ diff --git a/apps/mixdiganclock/ss_mixdigan_ems2_2.png b/apps/mixdiganclock/ss_mixdigan_ems2_2.png new file mode 100644 index 000000000..3dc16fa36 Binary files /dev/null and b/apps/mixdiganclock/ss_mixdigan_ems2_2.png differ diff --git a/apps/mixdiganclock/ss_mixdigan_ems2_3.png b/apps/mixdiganclock/ss_mixdigan_ems2_3.png new file mode 100644 index 000000000..c61b67fdd Binary files /dev/null and b/apps/mixdiganclock/ss_mixdigan_ems2_3.png differ diff --git a/apps/mixdiganclock/ss_mixdigan_ems_2.png b/apps/mixdiganclock/ss_mixdigan_ems_2.png new file mode 100644 index 000000000..94c1c5d78 Binary files /dev/null and b/apps/mixdiganclock/ss_mixdigan_ems_2.png differ diff --git a/apps/mosaic/ChangeLog b/apps/mosaic/ChangeLog index 7b83706bf..f26a9df0a 100644 --- a/apps/mosaic/ChangeLog +++ b/apps/mosaic/ChangeLog @@ -1 +1,2 @@ 0.01: First release +0.02: Use locale time diff --git a/apps/mosaic/metadata.json b/apps/mosaic/metadata.json index 267c0de55..92548ce9c 100644 --- a/apps/mosaic/metadata.json +++ b/apps/mosaic/metadata.json @@ -2,7 +2,7 @@ "id":"mosaic", "name":"Mosaic Clock", "shortName": "Mosaic Clock", - "version": "0.01", + "version": "0.02", "description": "A fabulously colourful clock", "readme": "README.md", "icon":"mosaic.png", diff --git a/apps/mosaic/mosaic.app.js b/apps/mosaic/mosaic.app.js index 8b008b848..03eb417fd 100644 --- a/apps/mosaic/mosaic.app.js +++ b/apps/mosaic/mosaic.app.js @@ -58,13 +58,15 @@ function draw() { ); } } - let t = new Date(); + let t = require("locale").time(new Date(), 1); + let hour = parseInt(t.split(":")[0]); + let minute = parseInt(t.split(":")[1]); g.setBgColor(theme.fg); g.setColor(theme.bg); - g.drawImage(digits[Math.floor(t.getHours()/10)], (mid_x-5)*s+o_w, (mid_y-7)*s+o_h, {scale:s}); - g.drawImage(digits[t.getHours() % 10], (mid_x+1)*s+o_w, (mid_y-7)*s+o_h, {scale:s}); - g.drawImage(digits[Math.floor(t.getMinutes()/10)], (mid_x-5)*s+o_w, (mid_y+1)*s+o_h, {scale:s}); - g.drawImage(digits[t.getMinutes() % 10], (mid_x+1)*s+o_w, (mid_y+1)*s+o_h, {scale:s}); + g.drawImage(digits[Math.floor(hour/10)], (mid_x-5)*s+o_w, (mid_y-7)*s+o_h, {scale:s}); + g.drawImage(digits[hour % 10], (mid_x+1)*s+o_w, (mid_y-7)*s+o_h, {scale:s}); + g.drawImage(digits[Math.floor(minute/10)], (mid_x-5)*s+o_w, (mid_y+1)*s+o_h, {scale:s}); + g.drawImage(digits[minute % 10], (mid_x+1)*s+o_w, (mid_y+1)*s+o_h, {scale:s}); queueDraw(timeout); } diff --git a/apps/neonx/ChangeLog b/apps/neonx/ChangeLog index c1a50ecd7..e78686a00 100644 --- a/apps/neonx/ChangeLog +++ b/apps/neonx/ChangeLog @@ -2,4 +2,5 @@ 0.02: Optional fullscreen mode 0.03: Optional show lock status via color 0.04: Ensure that widgets are always hidden in fullscreen mode -0.05: Better lock/unlock animation \ No newline at end of file +0.05: Better lock/unlock animation +0.06: Use widget_utils. diff --git a/apps/neonx/metadata.json b/apps/neonx/metadata.json index ee99f98b8..c273cb05a 100644 --- a/apps/neonx/metadata.json +++ b/apps/neonx/metadata.json @@ -2,7 +2,7 @@ "id": "neonx", "name": "Neon X & IO X Clock", "shortName": "Neon X Clock", - "version": "0.05", + "version": "0.06", "description": "Pebble Neon X & Neon IO X for Bangle.js", "icon": "neonx.png", "type": "clock", diff --git a/apps/neonx/neonx.app.js b/apps/neonx/neonx.app.js index fd30fa30f..7fcf01bde 100644 --- a/apps/neonx/neonx.app.js +++ b/apps/neonx/neonx.app.js @@ -19,6 +19,7 @@ let saved_settings = require('Storage').readJSON('neonx.json', 1) || settings; for (const key in saved_settings) { settings[key] = saved_settings[key] } +let widget_utils = require('widget_utils'); const digits = { @@ -133,7 +134,7 @@ function drawAnimated(){ function _draw(date, xc){ // Depending on the settings, we clear all widgets or draw those. if(settings.fullscreen){ - for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";} + widget_utils.hide(); } else { Bangle.drawWidgets(); } @@ -210,4 +211,4 @@ g.clear(1); Bangle.setUI("clock"); Bangle.loadWidgets(); -draw(); \ No newline at end of file +draw(); diff --git a/apps/notanalog/ChangeLog b/apps/notanalog/ChangeLog index 07430406a..094125f52 100644 --- a/apps/notanalog/ChangeLog +++ b/apps/notanalog/ChangeLog @@ -2,4 +2,5 @@ 0.02: 12k steps are 360 degrees - improves readability of steps. 0.03: Battery improvements through sleep (no minute updates) and partial updates of drawing. 0.04: Use alarm for timer instead of own alarm implementation. -0.05: Use internal step counter if no widget is available. \ No newline at end of file +0.05: Use internal step counter if no widget is available. +0.06: Use widget_utils. diff --git a/apps/notanalog/metadata.json b/apps/notanalog/metadata.json index 81d79f4f2..319d396a9 100644 --- a/apps/notanalog/metadata.json +++ b/apps/notanalog/metadata.json @@ -3,7 +3,7 @@ "name": "Not Analog", "shortName":"Not Analog", "icon": "notanalog.png", - "version":"0.05", + "version":"0.06", "readme": "README.md", "supports": ["BANGLEJS2"], "description": "An analog watch face for people that can not read analog watch faces.", diff --git a/apps/notanalog/notanalog.app.js b/apps/notanalog/notanalog.app.js index 3c01a921e..29fb1730f 100644 --- a/apps/notanalog/notanalog.app.js +++ b/apps/notanalog/notanalog.app.js @@ -4,6 +4,7 @@ const TIMER_IDX = "notanalog"; const locale = require('locale'); const storage = require('Storage') +const widget_utils = require('widget_utils'); const SETTINGS_FILE = "notanalog.setting.json"; let settings = { alarm: -1, @@ -460,10 +461,8 @@ Bangle.setUI("clock"); Bangle.loadWidgets(); /* * we are not drawing the widgets as we are taking over the whole screen - * 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="";} +widget_utils.hide(); // Clear the screen once, at startup and draw clock // g.setTheme({bg:"#fff",fg:"#000",dark:false}).clear(); diff --git a/apps/notify/ChangeLog b/apps/notify/ChangeLog index 8803b82b6..d7b754ff9 100644 --- a/apps/notify/ChangeLog +++ b/apps/notify/ChangeLog @@ -8,3 +8,4 @@ 0.09: Add onHide callback 0.10: Improvements to help notifications work with themes 0.11: Fix regression that caused no notifications and corrupted background +0.12: Add Bangle.js 2 support with Bangle.setLCDOverlay diff --git a/apps/notify/metadata.json b/apps/notify/metadata.json index e92d5e0e4..1cc8f52c1 100644 --- a/apps/notify/metadata.json +++ b/apps/notify/metadata.json @@ -2,14 +2,17 @@ "id": "notify", "name": "Notifications (default)", "shortName": "Notifications", - "version": "0.11", - "description": "Provides the default `notify` module used by applications to display notifications in a bar at the top of the screen. This module is installed by default by client applications such as the Gadgetbridge app. Installing `Fullscreen Notifications` replaces this module with a version that displays the notifications using the full screen", + "version": "0.12", + "description": "Provides the default `notify` module used by applications to display notifications on the screen. This module is installed by default by client applications such as the Gadgetbridge app. Installing `Fullscreen Notifications` replaces this module with a version that displays the notifications using the full screen", "icon": "notify.png", "type": "notify", "tags": "widget", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS","BANGLEJS2"], + "provides_modules" : ["notify"], + "default" : true, "readme": "README.md", "storage": [ - {"name":"notify","url":"notify.js"} + {"name":"notify","url":"notify_bjs1.js", "supports": ["BANGLEJS"]}, + {"name":"notify","url":"notify_bjs2.js", "supports": ["BANGLEJS2"]} ] } diff --git a/apps/notify/notify.js b/apps/notify/notify_bjs1.js similarity index 98% rename from apps/notify/notify.js rename to apps/notify/notify_bjs1.js index 332c301d5..fb56e4bbc 100644 --- a/apps/notify/notify.js +++ b/apps/notify/notify_bjs1.js @@ -96,7 +96,7 @@ exports.show = function(options) { b = y+h-1, r = x+w-1; // bottom,right // clear area g.reset().setClipRect(x,y, r,b); - if (options.bgColor!==undefined) g.setColor(options.bgColor); + if (options.bgColor!==undefined) g.setBgColor(options.bgColor); g.clearRect(x,y, r,b); // bottom border g.setColor("#333").fillRect(0,b-1, r,b); diff --git a/apps/notify/notify_bjs2.js b/apps/notify/notify_bjs2.js new file mode 100644 index 000000000..c202e8c55 --- /dev/null +++ b/apps/notify/notify_bjs2.js @@ -0,0 +1,171 @@ +let pos = 0, size = 0; +let id = null; +let hideCallback = undefined; +let overlayImage = undefined; + +/** + * Fit text into area, trying to insert newlines between words + * Appends "..." if more text was present but didn't fit + * + * @param {string} text + * @param {number} rows Maximum number of rows + * @param {number} width Maximum line length, in characters + */ +function fitWords(text,rows,width) { + // We never need more than rows*width characters anyway, split by any whitespace + const words = text.trim().substr(0,rows*width).split(/\s+/); + let row=1,len=0,limit=width; + let result = ""; + for (let word of words) { + // len==0 means first word of row, after that we also add a space + if ((len?len+1:0)+word.length > limit) { + if (row>=rows) { + result += "..."; + break; + } + result += "\n"; + len=0; + row++; + if (row===rows) limit -= 3; // last row needs space for "..." + } + result += (len?" ":"") + word; + len += (len?1:0) + word.length; + } + return result; +} + +/** + options = { + on : bool // turn screen on, default true + size : int // height of notification, default 80 (max) + title : string // optional title + id // optional notification ID, used with hide() + src : string // optional source name + body : string // optional body text + icon : string // optional icon (image string) + render : function(y) // function callback to render + bgColor : int/string // optional background color (default black) + titleBgColor : int/string // optional background color for title (default black) + onHide : function() // callback when notification is hidden + } +*/ +/* + The screen is 240x240px, but has a 240x320 buffer, used like this: + 0,0: top-left ... 239,0: top-right + [Normal screen contents: lines 0-239] + 239,0: bottom-left ... 239,239: bottom-right + [Usually off-screen: lines 240-319] + 319,0: last line in buffer ... 319,239: last pixel in buffer + + When moving the display area, the buffer wraps around + + So we draw notifications at the end of the buffer, + then shift the display down to show them without touching regular content. + Apps don't know about this, so can just keep updating the usual display area. + + For example, a size 40 notification: + - Draws in bottom 40 buffer lines (279-319) + - Shifts display down by 40px + Display now shows buffer lines 279-319,0-199 + Apps/widgets keep drawing to buffer line 0-239 like nothing happened + */ +exports.show = function(options) { + options = options || {}; + if (options.on===undefined) options.on = true; + id = ("id" in options)?options.id:null; + let w = g.getWidth(); + let text = []; + size = options.size; + if (options.body) { + const bh = (size || 80) - 20, + maxRows=Math.floor((bh-4)/8), // font=6x8 + maxChars=Math.floor(w/6)-2; + text=fitWords(options.body, maxRows, maxChars); + // set size based on newlines + if (!size) size = 28 + (text.match(/\n/g).length+1)*8; + } else size = 20; + if (size>80) size = 80; + + let gg = Graphics.createArrayBuffer(w,size,16); + gg.setBgColor(g.theme.bg); + overlayImage = { width : gg.getWidth(), height : gg.getHeight(), bpp : 16, buffer:gg.buffer }; + + // drawing area + let x = 0, + y = 0, + h = size, + b = y+h-1, r = x+w-1; // bottom,right + // clear area + if (options.bgColor!==undefined) gg.setBgColor(options.bgColor); + gg.clearRect(x,y, r,b); + // title bar + if (options.title || options.src) { + gg.setColor(options.titleBgColor||0x39C7).fillRect(x,y, r,y+20); + const title = options.title||options.src; + gg.setColor(g.theme.fg).setFontAlign(-1, -1, 0).setFont("6x8", 2); + gg.drawString(title.trim().substring(0, 13), x+25,y+3); + if (options.title && options.src) { + gg.setFont("6x8", 1).setFontAlign(1, 1, 0); + gg.drawString(options.src.substring(0, 10), gg.getWidth()-23,y+18); + } + } + y += 20; h -= 20; + if (options.icon) { + let i = options.icon, iw; + gg.drawImage(i, x,y+4); + if ("string"==typeof i) iw = i.charCodeAt(0); + else iw = i[0]; + x += iw;w -= iw; + } + // body text + if (options.body) { + gg.setColor(g.theme.fg).setFont("6x8", 1).setFontAlign(-1, -1, 0).drawString(text, x+6,y+4); + } + + if (options.render) { + options.render({x:x, y:y, w:w, h:h}); + } + + if (options.on && !(require('Storage').readJSON('setting.json',1)||{}).quiet) { + Bangle.setLCDPower(1); // light up + } + + + function anim() { + pos -= 2; + if (pos < -size) { + pos = -size; + } + Bangle.setLCDOverlay(overlayImage,0,-(pos+size)); + if (pos > -size) setTimeout(anim, 15); + } + anim(); + Bangle.on("touch", exports.hide); + if (options.onHide) + hideCallback = options.onHide; +}; + +/** + options = { + id // optional, only hide if current notification has this ID + } +*/ +exports.hide = function(options) { + options = options||{}; + if ("id" in options && options.id!==id) return; + if (hideCallback) hideCallback({id:id}); + hideCallback = undefined; + id = null; + Bangle.removeListener("touch", exports.hide); + function anim() { + pos += 4; + if (pos > 0) { + pos = 0; + overlayImage = undefined; + Bangle.setLCDOverlay(); + } else + Bangle.setLCDOverlay(overlayImage,0,-(pos+size)); + if (pos < 0) setTimeout(anim, 10); + } + anim(); +}; diff --git a/apps/notifyfs/metadata.json b/apps/notifyfs/metadata.json index dea8cb022..003b62429 100644 --- a/apps/notifyfs/metadata.json +++ b/apps/notifyfs/metadata.json @@ -8,6 +8,7 @@ "type": "notify", "tags": "widget", "supports": ["BANGLEJS","BANGLEJS2"], + "provides_modules" : ["notify"], "storage": [ {"name":"notify","url":"notify.js"} ] diff --git a/apps/pomoplus/metadata.json b/apps/pomoplus/metadata.json index 068eeed91..84a82c0e1 100644 --- a/apps/pomoplus/metadata.json +++ b/apps/pomoplus/metadata.json @@ -10,7 +10,6 @@ "BANGLEJS", "BANGLEJS2" ], - "allow_emulator": true, "storage": [ { "name": "pomoplus.app.js", @@ -34,4 +33,4 @@ "url": "settings.js" } ] -} \ No newline at end of file +} diff --git a/apps/recorder/ChangeLog b/apps/recorder/ChangeLog index a4d0b7e88..c4d1fa8c1 100644 --- a/apps/recorder/ChangeLog +++ b/apps/recorder/ChangeLog @@ -24,3 +24,4 @@ 0.18: Improve widget load speed, allow currently recording track to be plotted in openstmap 0.19: Fix track plotting code 0.20: Automatic translation of some more strings. +0.21: Speed report now uses speed units from locale diff --git a/apps/recorder/app.js b/apps/recorder/app.js index 8dcc4c3ed..8ac3ff627 100644 --- a/apps/recorder/app.js +++ b/apps/recorder/app.js @@ -348,7 +348,12 @@ function viewTrack(filename, info) { infc[i]++; } } else if (style=="Speed") { - title = /*LANG*/"Speed (m/s)"; + // use locate to work out units + var localeStr = require("locale").speed(1,5); // get what 1kph equates to + let units = localeStr.replace(/[0-9.]*/,""); + var factor = parseFloat(localeStr)*3.6; // m/sec to whatever out units are + // title + title = /*LANG*/"Speed"+` (${units})`; var latIdx = info.fields.indexOf("Latitude"); var lonIdx = info.fields.indexOf("Longitude"); // skip until we find our first data @@ -381,7 +386,7 @@ function viewTrack(filename, info) { } else throw new Error("Unknown type "+style); var min=100000,max=-100000; for (var i=0;i0) infn[i]/=infc[i]; + if (infc[i]>0) infn[i]=factor*infn[i]/infc[i]; var n = infn[i]; if (n>max) max=n; if (n hour) { //refresh every half an hour + let remain = (msecs+minute) % halfhour; + if(remain < 27 * minute && remain != 0) //first align period (some tolerance) + return [ halfhour, remain ]; + return [ halfhour, msecs - hour ]; + } else { //refresh every minute + //alarms just need the progress bar refreshed, no need every minute + if(!a.timer) return []; + return [ minute, msecs ]; + } + } + + function _doInterval(interval) { + return setTimeout(()=>{ + this.emit("redraw"); + this.interval = setInterval(()=>{ + this.emit("redraw"); + }, interval); + }, interval); + } + function _doSwitchTimeout(a, switchTimeout) { + return setTimeout(()=>{ + this.emit("redraw"); + clearInterval(this.interval); + this.interval = undefined; + var tmp = getRefreshIntervals(a); + var interval = tmp[0]; + var switchTimeout = tmp[1]; + if(!interval) return; + this.interval = _doInterval.call(this, interval); + this.switchTimeout = _doSwitchTimeout.call(this, a, switchTimeout); + }, switchTimeout); + } + var img = iconAlarmOn; //get only alarms not created by other apps var alarmItems = { @@ -63,8 +109,20 @@ 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 () {}, + show: function() { + var tmp = getRefreshIntervals(a); + var interval = tmp[0]; + var switchTimeout = tmp[1]; + if(!interval) return; + this.interval = _doInterval.call(this, interval); + this.switchTimeout = _doSwitchTimeout.call(this, a, switchTimeout); + }, + hide: function() { + clearInterval(this.interval); + clearTimeout(this.switchTimeout); + this.interval = undefined; + this.switchTimeout = undefined; + }, run: function() { } })), }; diff --git a/apps/sched/metadata.json b/apps/sched/metadata.json index 9c1a1d6b0..9ffc28524 100644 --- a/apps/sched/metadata.json +++ b/apps/sched/metadata.json @@ -1,7 +1,7 @@ { "id": "sched", "name": "Scheduler", - "version": "0.16", + "version": "0.19", "description": "Scheduling library for alarms and timers", "icon": "app.png", "type": "scheduler", diff --git a/apps/setting/ChangeLog b/apps/setting/ChangeLog index bf78c50b6..85ccfa1a7 100644 --- a/apps/setting/ChangeLog +++ b/apps/setting/ChangeLog @@ -60,3 +60,5 @@ 0.53: Ensure that when clock is set, clockHasWidgets is set correctly too 0.54: If setting.json is corrupt, ensure it gets re-written 0.55: More strings tagged for automatic translation. +0.56: make System menu items shorter and more consistant, Eg 'Clock', intead +of 'Select Clock' diff --git a/apps/setting/metadata.json b/apps/setting/metadata.json index 80db81f65..92fc75915 100644 --- a/apps/setting/metadata.json +++ b/apps/setting/metadata.json @@ -1,7 +1,7 @@ { "id": "setting", "name": "Settings", - "version": "0.55", + "version": "0.56", "description": "A menu for setting up Bangle.js", "icon": "settings.png", "tags": "tool,system", diff --git a/apps/setting/settings.js b/apps/setting/settings.js index b58615913..a877ec79c 100644 --- a/apps/setting/settings.js +++ b/apps/setting/settings.js @@ -89,8 +89,8 @@ function showSystemMenu() { /*LANG*/'Theme': ()=>showThemeMenu(), /*LANG*/'LCD': ()=>showLCDMenu(), /*LANG*/'Locale': ()=>showLocaleMenu(), - /*LANG*/'Select Clock': ()=>showClockMenu(), - /*LANG*/'Select Launcher': ()=>showLauncherMenu(), + /*LANG*/'Clock': ()=>showClockMenu(), + /*LANG*/'Launcher': ()=>showLauncherMenu(), /*LANG*/'Date & Time': ()=>showSetTimeMenu() }; diff --git a/apps/simplestpp/ChangeLog b/apps/simplestpp/ChangeLog new file mode 100644 index 000000000..4db8559c8 --- /dev/null +++ b/apps/simplestpp/ChangeLog @@ -0,0 +1,2 @@ +0.01: first release +0.02: removed fast load, minimalism is useful for narrowing down on issues diff --git a/apps/simplestpp/README.md b/apps/simplestpp/README.md index 4b459bda1..3cbf9abf1 100644 --- a/apps/simplestpp/README.md +++ b/apps/simplestpp/README.md @@ -1,6 +1,6 @@ # Simplest++ Clock -The simplest working clock, with fast load and clock_info +The simplest working clock, with clock_info ![](screenshot1.png) ![](screenshot2.png) @@ -36,8 +36,6 @@ This provides a working demo of how to use the clock_info modules. ## References -* [What is Fast Load and how does it work](http://www.espruino.com/Bangle.js+Fast+Load) - * [Clock Info Tutorial](http://www.espruino.com/Bangle.js+Clock+Info) * [How to load modules through the IDE](https://github.com/espruino/BangleApps/blob/master/modules/README.md) diff --git a/apps/simplestpp/app.js b/apps/simplestpp/app.js index c07fdbcbb..0dcb883d3 100644 --- a/apps/simplestpp/app.js +++ b/apps/simplestpp/app.js @@ -1,108 +1,95 @@ +// Simplestpp Clock, see comments 'clock_info_support' + +function draw() { + var date = new Date(); + var timeStr = require("locale").time(date,1); + var h = g.getHeight(); + var w = g.getWidth(); + + g.reset(); + g.setColor(g.theme.bg); + g.fillRect(Bangle.appRect); + + g.setFont('Vector', w/3); + g.setFontAlign(0, 0); + g.setColor(g.theme.fg); + g.drawString(timeStr, w/2, h/2); + + clockInfoMenu.redraw(); // clock_info_support + queueDraw(); // queue draw in one minute +} + +// timeout used to update every minute +var drawTimeout; + +// schedule a draw for the next minute +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +} + /** - * - * Simplestpp Clock - * - * The entire clock code is contained within the block below this - * supports 'fast load' - * - * To add support for clock_info_supprt we add the code marked at [1] and [2] + * clock_info_support + * this is the callback function that get invoked by clockInfoMenu.redraw(); + * + * We will display the image and text on the same line and centre the combined + * length of the image+text * */ +function clockInfoDraw(itm, info, options) { -{ - // must be inside our own scope here so that when we are unloaded everything disappears - // we also define functions using 'let fn = function() {..}' for the same reason. function decls are global + g.reset().setFont('Vector',24).setBgColor(options.bg).setColor(options.fg); - let draw = function() { - var date = new Date(); - var timeStr = require("locale").time(date,1); - var h = g.getHeight(); - var w = g.getWidth(); - - g.reset(); - g.setColor(g.theme.bg); - g.fillRect(Bangle.appRect); + //use info.text.toString(), steps does not have length defined + var text_w = g.stringWidth(info.text.toString()); + // gap between image and text + var gap = 10; + // width of the image and text combined + var w = gap + (info.img ? 24 :0) + text_w; + // different fg color if we tapped on the menu + if (options.focus) g.setColor(options.hl); - g.setFont('Vector', w/3); - g.setFontAlign(0, 0); - g.setColor(g.theme.fg); - g.drawString(timeStr, w/2, h/2); - clockInfoMenu.redraw(); // clock_info_support - - // schedule a draw for the next minute - if (drawTimeout) clearTimeout(drawTimeout); - drawTimeout = setTimeout(function() { - drawTimeout = undefined; - draw(); - }, 60000 - (Date.now() % 60000)); - }; - - /** - * clock_info_support - * this is the callback function that get invoked by clockInfoMenu.redraw(); - * - * We will display the image and text on the same line and centre the combined - * length of the image+text - * - * - */ - let clockInfoDraw = (itm, info, options) => { - - g.reset().setFont('Vector',24).setBgColor(options.bg).setColor(options.fg); - - //use info.text.toString(), steps does not have length defined - var text_w = g.stringWidth(info.text.toString()); - // gap between image and text - var gap = 10; - // width of the image and text combined - var w = gap + (info.img ? 24 :0) + text_w; - // different fg color if we tapped on the menu - if (options.focus) g.setColor(options.hl); - - // clear the whole info line - g.clearRect(0, options.y -1, g.getWidth(), options.y+24); - - // draw the image if we have one - if (info.img) { - // image start - var x = (g.getWidth() / 2) - (w/2); - g.drawImage(info.img, x, options.y); - // draw the text to the side of the image (left/centre alignment) - g.setFontAlign(-1,0).drawString(info.text, x + 23 + gap, options.y+12); - } else { - // text only option, not tested yet - g.setFontAlign(0,0).drawString(info.text, g.getWidth() / 2, options.y+12); - } - - }; - - // clock_info_support - // retrieve all the clock_info modules that are installed - let clockInfoItems = require("clock_info").load(); - - // clock_info_support - // setup the way we wish to interact with the menu - // the hl property defines the color the of the info when the menu is selected after tapping on it - let clockInfoMenu = require("clock_info").addInteractive(clockInfoItems, { x:64, y:132, w:50, h:40, draw : clockInfoDraw, bg : g.theme.bg, fg : g.theme.fg, hl : "#0ff"} ); + // clear the whole info line + g.clearRect(0, options.y -1, g.getWidth(), options.y+24); - // timeout used to update every minute - var drawTimeout; - g.clear(); + // draw the image if we have one + if (info.img) { + // image start + var x = (g.getWidth() / 2) - (w/2); + g.drawImage(info.img, x, options.y); + // draw the text to the side of the image (left/centre alignment) + g.setFontAlign(-1,0).drawString(info.text, x + 23 + gap, options.y+12); + } else { + // text only option, not tested yet + g.setFontAlign(0,0).drawString(info.text, g.getWidth() / 2, options.y+12); + } - // Show launcher when middle button pressed, add updown button handlers - Bangle.setUI({ - mode : "clock", - remove : function() { - if (drawTimeout) clearTimeout(drawTimeout); - drawTimeout = undefined; - // remove info menu - clockInfoMenu.remove(); - delete clockInfoMenu; - } - }); +} - // Load widgets - Bangle.loadWidgets(); - draw(); - setTimeout(Bangle.drawWidgets,0); -} // end of clock +/** + * clock_info_support: retrieve all the clock_info modules that are + * installed + * + */ +let clockInfoItems = require("clock_info").load(); + +/** + * clock_info_support: setup the way we wish to interact with the menu + * the hl property defines the color the of the info when the menu is + * selected after tapping on it + * + */ +let clockInfoMenu = require("clock_info").addInteractive(clockInfoItems, { x:64, y:132, w:50, h:40, draw : clockInfoDraw, bg : g.theme.bg, fg : g.theme.fg, hl : "#0ff"} ); + +// Clear the screen once, at startup +g.clear(); + +// Show launcher when middle button pressed +Bangle.setUI("clock"); +// Load widgets +Bangle.loadWidgets(); +Bangle.drawWidgets(); +draw(); diff --git a/apps/simplestpp/metadata.json b/apps/simplestpp/metadata.json index d808b132b..145bf7309 100644 --- a/apps/simplestpp/metadata.json +++ b/apps/simplestpp/metadata.json @@ -2,8 +2,8 @@ "id": "simplestpp", "name": "Simplest++ Clock", "shortName": "Simplest++", - "version": "0.01", - "description": "The simplest working clock, with fast load and clock_info, acts as a tutorial piece", + "version": "0.02", + "description": "The simplest working clock, with clock_info, acts as a tutorial piece", "readme": "README.md", "icon": "app.png", "screenshots": [{"url":"screenshot3.png"}], diff --git a/apps/slopeclock/ChangeLog b/apps/slopeclock/ChangeLog index 2eb04a4f0..da82f6355 100644 --- a/apps/slopeclock/ChangeLog +++ b/apps/slopeclock/ChangeLog @@ -1,3 +1,4 @@ 0.01: New App! 0.02: Reset font to save some memory during remove 0.03: Added support for locale based time +0.04: Stability improvements diff --git a/apps/slopeclock/app.js b/apps/slopeclock/app.js index cc3dce630..2164e7ede 100644 --- a/apps/slopeclock/app.js +++ b/apps/slopeclock/app.js @@ -1,7 +1,12 @@ +{ // must be inside our own scope here so that when we are unloaded everything disappears + // we also define functions using 'let fn = function() {..}' for the same reason. function decls are global + +const fontBitmap = E.toString(require('heatshrink').decompress(atob('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'))); + Graphics.prototype.setFontPaytoneOne = function(scale) { // Actual height 81 (91 - 11) this.setFontCustom( - E.toString(require('heatshrink').decompress(atob('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'))), + fontBitmap, 46, atob("ITZOMzs7SDxHNUdGIQ=="), 113+(scale<<8)+(1<<16) @@ -9,8 +14,6 @@ Graphics.prototype.setFontPaytoneOne = function(scale) { return this; }; -{ // must be inside our own scope here so that when we are unloaded everything disappears - // we also define functions using 'let fn = function() {..}' for the same reason. function decls are global let drawTimeout; let g2 = Graphics.createArrayBuffer(g.getWidth(),90,1,{msb:true}); diff --git a/apps/slopeclock/metadata.json b/apps/slopeclock/metadata.json index 6ee78350f..d9d4d85ca 100644 --- a/apps/slopeclock/metadata.json +++ b/apps/slopeclock/metadata.json @@ -1,12 +1,12 @@ { "id": "slopeclock", "name": "Slope Clock", - "version":"0.03", + "version":"0.04", "description": "A clock where hours and minutes are divided by a sloping line. When the minute changes, the numbers slide off the screen", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], "type": "clock", "tags": "clock", - "supports" : ["BANGLEJS2"], + "supports" : ["BANGLEJS2"], "storage": [ {"name":"slopeclock.app.js","url":"app.js"}, {"name":"slopeclock.img","url":"app-icon.js","evaluate":true} diff --git a/apps/slopeclockpp/ChangeLog b/apps/slopeclockpp/ChangeLog index eef1840ea..58299b236 100644 --- a/apps/slopeclockpp/ChangeLog +++ b/apps/slopeclockpp/ChangeLog @@ -7,3 +7,5 @@ 0.05: Images in clkinfo are optional now 0.06: Added support for locale based time 0.07: README file update as UI interaction was not easy to understand +0.08: Stability improvements - ensure we continue even if a flat string can't be allocated + Stop ClockInfo text drawing outside the allocated area diff --git a/apps/slopeclockpp/app.js b/apps/slopeclockpp/app.js index bf719344e..dca4a84e4 100644 --- a/apps/slopeclockpp/app.js +++ b/apps/slopeclockpp/app.js @@ -1,14 +1,3 @@ -Graphics.prototype.setFontPaytoneOne = function(scale) { - // Actual height 71 (81 - 11) - this.setFontCustom( - E.toString(require('heatshrink').decompress(atob('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'))), - 46, - atob("HTBFLTQ0PzU/Lz8+HQ=="), - 100+(scale<<8)+(1<<16) - ); - return this; -}; - { // must be inside our own scope here so that when we are unloaded everything disappears // we also define functions using 'let fn = function() {..}' for the same reason. function decls are global @@ -16,6 +5,17 @@ let settings = Object.assign( require("Storage").readJSON("slopeclockpp.default.json", true) || {}, require("Storage").readJSON("slopeclockpp.json", true) || {} ); +const fontBitmap = E.toString(require('heatshrink').decompress(atob('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'))); + +Graphics.prototype.setFontPaytoneOne = function(scale) { + // Actual height 71 (81 - 11) + this.setFontCustom(fontBitmap, + 46, + atob("HTBFLTQ0PzU/Lz8+HQ=="), + 100+(scale<<8)+(1<<16) + ); + return this; +}; let drawTimeout; @@ -47,6 +47,15 @@ let bgColor = bgColors[(Math.random()*bgColors.length)|0]||"#000"; // Draw the hour, and the minute into an offscreen buffer let draw = function() { + // queue next draw + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + animate(false, function() { + draw(); + }); + }, 60000 - (Date.now() % 60000)); + // Now draw this one R = Bangle.appRect; x = R.w / 2; y = R.y + R.h / 2 - 12; // 12 = room for date @@ -70,15 +79,6 @@ let draw = function() { g2.setColor(0).fillPoly([0,0, g2.getWidth(),0, 0,slope*2]); // start the animation *in* animate(true); - - // queue next draw - if (drawTimeout) clearTimeout(drawTimeout); - drawTimeout = setTimeout(function() { - drawTimeout = undefined; - animate(false, function() { - draw(); - }); - }, 60000 - (Date.now() % 60000)); }; let isAnimIn = true; @@ -123,7 +123,9 @@ let animate = function(isIn, callback) { // clock info menus (scroll up/down for info) let clockInfoDraw = (itm, info, options) => { let texty = options.y+41; - g.reset().setFont("6x15").setBgColor(options.bg).setColor(options.fg).clearRect(options.x, texty-15, options.x+options.w-2, texty); + // set a cliprect to stop us drawing outside our box + g.reset().setClipRect(options.x, options.y, options.x+options.w-1, options.y+options.h-1); + g.setFont("6x15").setBgColor(options.bg).setColor(options.fg).clearRect(options.x, texty-15, options.x+options.w-2, texty); if (options.focus) g.setColor(options.hl); if (options.x < g.getWidth()/2) { // left align @@ -135,6 +137,8 @@ let clockInfoDraw = (itm, info, options) => { if (info.img) g.clearRect(x-23, options.y, x, options.y+23).drawImage(info.img, x-23, options.y); g.setFontAlign(1,1).drawString(info.text, x,texty); } + // return ClipRect + g.setClipRect(0,0,g.getWidth()-1, g.getHeight()-1); }; let clockInfoItems = require("clock_info").load(); let clockInfoMenu = require("clock_info").addInteractive(clockInfoItems, { x:126, y:24, w:50, h:40, draw : clockInfoDraw, bg : g.theme.bg, fg : g.theme.fg, hl : "#f00"/*red*/ }); diff --git a/apps/slopeclockpp/metadata.json b/apps/slopeclockpp/metadata.json index 3243d389a..fbab02fca 100644 --- a/apps/slopeclockpp/metadata.json +++ b/apps/slopeclockpp/metadata.json @@ -1,6 +1,6 @@ { "id": "slopeclockpp", "name": "Slope Clock ++", - "version":"0.07", + "version":"0.08", "description": "A clock where hours and minutes are divided by a sloping line. When the minute changes, the numbers slide off the screen. This is a clone of the original Slope Clock which shows extra information and allows the colors to be selected.", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], diff --git a/apps/smpltmr/ChangeLog b/apps/smpltmr/ChangeLog index 12b77aacd..61111482e 100644 --- a/apps/smpltmr/ChangeLog +++ b/apps/smpltmr/ChangeLog @@ -4,3 +4,4 @@ 0.04: Improvements of clock infos. 0.05: Updated clkinfo icon. 0.06: Ensure Timer supplies an image for clkinfo items +0.07: Update clock_info to avoid a redraw diff --git a/apps/smpltmr/clkinfo.js b/apps/smpltmr/clkinfo.js index 270a14fc4..ac01cfb59 100644 --- a/apps/smpltmr/clkinfo.js +++ b/apps/smpltmr/clkinfo.js @@ -70,7 +70,7 @@ { name: null, get: () => ({ text: getAlarmMinutesText() + (isAlarmEnabled() ? " min" : ""), img: smpltmrItems.img }), - show: function() { smpltmrItems.items[0].emit("redraw"); }, + show: function() {}, hide: function () {}, run: function() { } }, @@ -82,7 +82,7 @@ smpltmrItems.items = smpltmrItems.items.concat({ name: null, get: () => ({ text: (o > 0 ? "+" : "") + o + " min.", img: smpltmrItems.img }), - show: function() { smpltmrItems.items[i+1].emit("redraw"); }, + show: function() {}, hide: function () {}, run: function() { if(o > 0) increaseAlarm(o); diff --git a/apps/smpltmr/metadata.json b/apps/smpltmr/metadata.json index 71e793cc2..b0d1a34da 100644 --- a/apps/smpltmr/metadata.json +++ b/apps/smpltmr/metadata.json @@ -2,7 +2,7 @@ "id": "smpltmr", "name": "Simple Timer", "shortName": "Simple Timer", - "version": "0.06", + "version": "0.07", "description": "A very simple app to start a timer.", "icon": "app.png", "tags": "tool,alarm,timer,clkinfo", diff --git a/apps/spaceclock/ChangeLog b/apps/spaceclock/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/spaceclock/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/spaceclock/README.md b/apps/spaceclock/README.md new file mode 100644 index 000000000..734418738 --- /dev/null +++ b/apps/spaceclock/README.md @@ -0,0 +1,16 @@ +# Space Clock (Casio) +![Light Theme version](spaceclock_light_big.png) +## Description +This watch face is inspired by the Casio Prototype, which was made by Khryzteen Nakamura from Clockology Fans on Facebook. + +## Features +- Time and Date +- Weekday +- Temperature +- HeartRate +- Battery +- Step Count +- Support of light and dark theme + +## Tips +Click on the heart icon to deactivate the heart rate monitor. diff --git a/apps/spaceclock/app-icon.js b/apps/spaceclock/app-icon.js new file mode 100644 index 000000000..c9522ced1 --- /dev/null +++ b/apps/spaceclock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEewgbYhGIxGAC6wABBAcM5gACC5wYCCwgYLIwYcBhoWFDBQTBAofcC4/AGBIEDCw4wLJARdGAAfQGBYWJJBQwCC5SSLC6wwBC6owBwhgUDASQKC5UMpGIpgXU5koMRIXM4hiJC5gNBMRAXN5hiBDA0IC5oPBPYz+CABAQElAYFWgIYJC4h7BDAZeBHAJINPYQYC6DlCGBENRQwRBhgNCGBSjG4QxBBoYwQGIIrEPJS8GIgYADYZwIDDAgXJABQXXAAY")) diff --git a/apps/spaceclock/app-icon.png b/apps/spaceclock/app-icon.png new file mode 100644 index 000000000..89828d287 Binary files /dev/null and b/apps/spaceclock/app-icon.png differ diff --git a/apps/spaceclock/app.js b/apps/spaceclock/app.js new file mode 100644 index 000000000..a3892e952 --- /dev/null +++ b/apps/spaceclock/app.js @@ -0,0 +1,237 @@ +Graphics.prototype.setFontDigitalNumbersRegular = function(scale) { + // Actual height 32 (31 - 0) + this.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///D5///w+///+Pv///z4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AAAA+AAAAHAAAAD4AAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4cAAAOHAAAHjwAAB48AAA+P4AAP//wA////v////8///8AD/+8AAG+PAAAPjwAAD48AAA+PAAAPj2AAD//8AD///z/////f///wA///AAB/jwAAD48AAA8OAAAPDgAADgwAAA4MAAAAAAAAAAAAAAAAAAAAAAAD//gAAf/wAAT/7AAG/84ABwAeAB///////////////8eAHgAHgB4AP//////////HgB4ABwAe/4YADP/EAAH/wAAD/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAD4AAAA+Af8APgf8AD4f8AAAf8AAAf8AAAf8AAAf8AAAf8AAAf8OAAf8HwAf8B8AP8AfAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//3//P/8//t/+H/7v/J/99/jn8fAB8APwAfAH8AD4D/AAfA/sAH4f3gA+Pj8AH3x/AA/4AAAH+AAAB/AAAAPgAAAH8AAAD/gAAB/4AAA+eAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/9//5//P/7f/h/97/wP+/AAAAfwAAAH8AAAB+AAAAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAwAAAAeAAAAPwAAAH8AAAB/f4B/Pv/B/93/4f/j//P/4//3//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAcAAAA/8AAAP/AAAD7gAAB/sAAA//wAAHf4AAA/8AAAO/AAAD/wAAA/8AAAAwAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAABwAAAAeAAAAHgAAAB4AAAH/wAAD//AAAf/gAAD/wAAAHgAAAB4AAAAeAAAADAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/wAAB/wAAAfwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAHAAAAB4AAAAeAAAAHgAAAB4AAAAeAAAAHgAAAB4AAAAeAAAAHgAAAB4AAAAMAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAPgAAAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/AAAH/AAAH/AAAH/AAAH/AAAH/AAAH/AAAH/AAAH/AAAH/AAAH/AAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//3//P/8//t/+H/zv/A/98AAH8/AAAAfwAAAH8AAAB/AAAAfwAAAH8AAAB/AAAAfwAAAH9/wP++/+H/3f/j/+P/9//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf8D/gP/h/8H/4//j//f/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB//wAAP/7AAJ/84AHP/fAB5/PwAeAH8AHgB/AB4AfwAeAH8AHgB/AB4AfwAeAH8AHgB/f94APv/sAB3/6AAD//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAgADgAcAB8AHgA/AB4AfwAeAH8AHgB/AB4AfwAeAH8AHgB/AB4AfwAeAH9/3v++/83/3f/r/+P/9//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/8AAD//AAAf/oAAD/3AAAAB4AAAAeAAAAHgAAAB4AAAAeAAAAHgAAAB4AAAAeAAAAHgAAf97/gP/t/8H/6//j//f/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/8AAD//AADf/oAA7/3AAfAB4APwAeAH8AHgB/AB4AfwAeAH8AHgB/AB4AfwAeAH8AHgB/AB7/vgAN/9wAC//gAAf/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//3//P/8//t/+n/zv/c/98AHn8/AB4AfwAeAH8AHgB/AB4AfwAeAH8AHgB/AB4AfwAeAH8AHv++AA3/3AAL/+AAB//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAOAAAADwAAAA8AAAAPAAAADwAAAA8AAAAPAAAADwAAAA8AAAAPAAAAD3/A/w7/wf/N/+P/4//3//AAB//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/9//z//P/7f/p/87/3P/fAB5/PwAeAH8AHgB/AB4AfwAeAH8AHgB/AB4AfwAeAH8AHgB/f97/vv/t/93/6//j//f/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//wAAP/8AAN/+gADv/cAA8AHgAPAB4ADwAeAA8AHgAPAB4ADwAeAA8AHgAPAB4ADwAeAA9/3v+O/+3/zf/r/+P/9//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPB4AAHweAAB8HgAAfB4AAHweAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4P4AAfH/gAHx/gAB8fgAAfHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAG4AAABuAAAA7wAAAe+AAAPvgAAD78AAB+/gAA/n8AAPw/AAH4P4AD+B/AA/APwAPgB8AD4AfAA8ADwAOAAcADAAHAAwAAwAIAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYYAAAOOAAADzwAAA88AAAPPAAADzwAAA88AAAPPAAADzwAAA88AAAPPAAADzwAAA88AAAPPAAADjgAAAQQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAEADAADAA4ABwAOAA8ADwAPAA+AHwAPwD8AD8A/AA/gfgAH8P4AA/H8AAH5+AAB+/gAAPvwAAB74AAAO8AAADvAAAAbgAAACwAAAAsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAMAAAADwAAAA8AB/8vAAP/fwAN/n8AHPx/AB4ALwAeAA8AHgAPAB4ADwAeAA7/3AAN/+gAC//wAAP/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/9//z//P/7f/p/87/3P/PAB5/DwAeAA8AHgAPAB4ADwAeAA8AHgAPAB4ADwAeAA8AHgAPf97/jv/t/83/6//j//f/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//3//P/8//t/+n/zv/c/98AHn8/AB4AfwAeAH8AHgB/AB4AfwAeAH8AHgB/AB4AfwAeAH9/3v++/+3/3f/r/+P/9//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//3//n/4//u/8H/33+A/78AAAB/AAAAfwAAAH8AAAB/AAAAfwAAAH8AAAB/AAAAfwAAAH4AAAA8AAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/9//z//P/7f/h/87/wP/fAAB/PwAAAH8AAAB/AAAAfwAAAH8AAAB/AAAAfwAAAH8AAAB/f8D/vv/h/93/4//j//f/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/9//5/+P/7v/N/99/nv+/AB4AfwAeAH8AHgB/AB4AfwAeAH8AHgB/AB4AfwAeAH8AHgB+ABwAPAAIABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//3//n/4//u/+3/z//e/48AHgAPAB4ADwAeAA8AHgAPAB4ADwAeAA8AHgAPAB4ADwAeAA4AHAAMAAgACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/9//z//P/7f/p/87/3P/fAB5/PwAeAH8AHgB/AB4AfwAeAH8AHgB/AB4AfwAeAH8AHgB/AB7/vgAN/9wAC//gAAf/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//f/8//z/+H/6f/A/9z/wAAefwAAHgAAAB4AAAAeAAAAHgAAAB4AAAAeAAAAHgAAAB4AAH/e/4D/7f/B/+v/4//3//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8D/gf/h/8P/8//j//f/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAGAAAADgAAAB4AAAAeAAAAHj/wP+Z/+H/y//z/+P/9//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//3//H/6//g/83/wH+e/4AAHAAAAEyAAADAwAABwOAAA8DwAAfA+AAPwPwAP4B+AH4APwD8AB+B+AAPwfAAB8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//f/8f/j/+j/wf/Yf4D/uAAAAHgAAAB4AAAAeAAAAHgAAAB4AAAAeAAAAHgAAAB4AAAAeAAAADgAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD3/v/++/x//H/8P/h/+B/wPwAAAB+AAAAPgAAAB8AAAAPAAAAAwAAAA8AAAAfAAAAHwAAAD4AAAB8AAAA+AAAAf/gf8P/8P/j//n/89/7//gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/+//x//H/4P/g/8B/wH+AAAAAAH+AAAAf4AAAB/AAAAP8AAAA/wAAAD+AAAAf4AAAB/gAAAH8AAAA/gAAAAAAf8D/wP/h/+H/8//z//v/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//f/8//z/+3/4f/O/8D/3wAAfz8AAAB/AAAAfwAAAH8AAAB/AAAAfwAAAH8AAAB/AAAAf3/A/77/4f/d/+P/4//3//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/9//z//P/7f/p/+7/3P/PAB5/DwAeAA8AHgAPAB4ADwAeAA8AHgAPAB4ADwAeAA8AHgAPf94ADv/MAA3/6AAD//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//3//P/8//t/+H/zv/A/98AAH8/AAAAfwAAAH8AAAH/AAAD/wAAB/8AAA//AAAffwAADn9/wP++/+H/3f/j/+P/9//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//f/8//z/+3/6f/u/9z/zwAefw8AHgAPAB6ADwAewA8AHuAPAB7wDwAe+A8AHvwPAB5+D3/ePw7/zB+N/+gPw//wAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/8AAD//AADf/oAA7/3AAfAB4APwAeAH8AHgB/AB4AfwAeAH8AHgB/AB4AfwAeAH8AHgB/AB7/vgAN/9wAC//gAAf/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAAAA4AAAAPAAAADwAAAA8AAAAPAAAADwAAAA8AAAAPAAAADwAAAA//wP+P/+H/z//z/+//9///AAAADwAAAA8AAAAPAAAADwAAAA8AAAAPAAAADwAAAA4AAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/9//z//P/4f/h/8j/wP/YAAB/OAAAAHgAAAB4AAAAeAAAAHgAAAB4AAAAeAAAAHgAAAB4f8D/uP/h/9n/4//r//f/8AAAAAAAAAAAAAAAAAAAAAAAAAAD/gAAAP+AAAA/4AAAD/gAAAP+AAAA/4AAAD/gAAAP+AAAA/4AAAD/gAAAP+AAAA/wAAAAAAAAAAAAAB/wAAB/wAAB/wAAB/wAAB/wAAB/wAAB/wAAB/wAAB/wAAB/wAAB/wAAA/wAAAAAAAAAAAAAAf/4AAn/+/94//H++H/g//g/wH/wAAAH4AAAB8AAAA+AAAAPAAAAHgAAAAwAAAAeAAAAHwAAAA+AAAAPwAAAB+AAAAPgf+D98P/x/vn/+/97//v/uAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/gAD+H8AB/A/gA/gD+AfwAfwfwAD+P4AAf38AAD/+AAAP/AAAB/AAAA/4AAAf/AAAP/4AAH8/AAD8H8AD+A/gB/AH8A/gA/gfwAH8PwAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP4AAfh/AAP4P4AP4B/AP8AP4H8AB/H8AAP/+AAB/+AAAP+AAAB/AAAB/AAAB/AAAA/gAAA/gAAA/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH8MAAD+TgAB/M8AA/nPgA/jz4Afw8+AP4PPgH4Dz4H8A8+D+APPh/ADz4/AA88/gAHOfwAAzP4AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//3//n/8//t/+H/3v/A/78AAAB/AAAAfwAAAH4AAAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/4AAAD/gAAAP+AAAA/4AAAD/gAAAP+AAAA/4AAAD/gAAAP+AAAA/4AAAD/gAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAMAAAAHgAAAD8AAAB/AAAAf3+Afz7/wf/d/+H/4//z/+P/9//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), 32, atob("GhoaGhoaGhoaGhoaAxoDGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGho="), 32+(scale<<8)+(1<<16)); + return this; +}; +Graphics.prototype.setFontDigitalNumbersSmall = function(scale) { + // Actual height 16 (15 - 0) + this.setFontCustom(atob("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"), 32, atob("DQ0NDQ0NDQ0NDQ0NAg0CDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0="), 16+(scale<<8)+(1<<16)); + return this; +}; + +Graphics.prototype.setFontArkitechLight = function(scale) { + // Actual height 10 (10 - 1) + this.setFontCustom(atob("AAAAAAAAAAAAAAP0AAAAAAMAMAMAAAEgPwPwEgPwEgEgAAAAGEPEJEf+f+JEf+JEJsI4AAAAOAKAKEKMO4HgG8ckQkAkA8AYAAAAG4PsJEJEJEJEJEJEJEJEJ8BABAAAMAAAAAP8f+QCQCAAAAQCQCf+H4AAEAPAfAfAPAAAAABABAH4H4BABAAAAAAGAEAABABABABABABAAAAAAEAEAAAGAcBwHAcAQAAAH4OcIEIEIEIEIEIEIEIEMMH4DwIAIAP8H8AAAAGcO0IkIkIkIkIkJkJEJEJEPEAAAAMMNMJEJEJEJEJEJEJEJEJEP8AAAAAgBgDgGgMgIgAgAgAgAgP8P8AAPYLMJEJEJEJEJEJEJEJEJEJ8AAAAP8JMJEJEJEJEJEJEJEJEJEN8AAAAIEIEIMIIIYIQIgJgJALAOAAAAAP8JEJEJEJEJEJEJEJEJEJEP8AAAAPMJMJEJEJEJEJEJEJEJEJEP8AAAABEAAAABGBEAAAABACgCgEQEQIIAAAACQCQCQCQCQCQAAAAMYEQGwCgDgBAAAOAMAIAI0J0JAJAJAJAPAHAAAP4IIP4MYMYMYMYMYP4IYP4AAAAH8NAJAJAJAJAJAJAJANAH8AAAAP8JEJEJEJEJEJEJEJEJsO4AAAAH4OYIEIEIEIEIEIEIEIEIEAAP8IEIEIEIEIEIEIEIEMMH4AAAAH4FYJEJEJEJEJEJEJEJEJEAAAAH8NAJAJAJAJAJAJAJAJAIAAAH4OYIEIEIEIEJEJEJEBMB4AAAAP8BABABABABABABABAP8P8AAP8AAAAAwAYAEAEAEAEAEAEAMP4PwAAP8BABABABABABABABgO8OcAAAAP4AYAEAEAEAEAEAEAEAEAEAAP8IAMAHABwAcAEAcBwHAMAIAP8AAAAP8IAMAGADABgAwAYAMAEP8AAAAH4OcIEIEIEIEIEIEIEIEIEMMH4BgAAP8IgIgIgIgIgIgIgIgIgPgHAAAH4OcIEIEIEIEIEIEIEIEIEMMH8BkAAP8IgIgIgIgIgIgIgIwI4PsHEAACAPEJEJEJEJEJEJEJEJEJ8A4AAIAIAIAIAIAP8IAIAIAIAIAAAP4AcAEAEAEAEAEAEAMP4PwAAIAMAGADgAwAYAMAcAwBgDAOAIAIAAAP4AcAEAEAEAEAEAMP4P4AMAEAEAEAEAEAMP4AAAAOcGwBABABABABABADgO8McAAPADgAgAgAgA8AgAgBgPAOAAAIcIUI0IkIkJkJEJELEKEOEAAAAf+QCQCAAQAYAGABgAcAGACAAQCf+f+AA"), 32, atob("BQUECQwODgMGBgYIAwgDBw4FDg4NDg4NDg4DBAgIBwwNDQ0MDQ0MDQwDDA0MDw0PDQ8ODAwNDhQMDA0FBwU="), 12+(scale<<8)+(1<<16)); + return this; +}; + +var nasa = require("heatshrink").decompress(atob("jEUwcCpMkyQBBqQFCAQOJAgQIEAgclAYUipdkBoWlkVJiVZkofDiVIyVZkmypMmAwIIDy1JyVEBAmTsmQiQKB/1bvnPlP8yQIB3/Z/0P5t8BAWz9s/yfbnoIC6VLtua5ctBANAIgMsiRcCGoVJkYCBsmUBAVIOAYIBMQNBBAekOgUJBA0gQw6YJVQwA=")); + +var runner = require("heatshrink").decompress(atob("kEgwINKhwDCj4DKB4UH+ADBh/4mAOB/wDBjn/BgM5//gAYPxAYMz+HAgEDAwIfBAYYKBEYIDGjoPCnlwGQU4GQPwAYMP8ADBAAIDXAAo")); + +var calendar = require("heatshrink").decompress(atob("j0ZwMAv////7BQU4AYVgAQMBwADBgwKCjgDCDAIABBw0Yg0xx1gjHGnOAocIowODwkwwlnBwWcscY8wODAYQOB4EBBwIsBmOMBwlwAQMDLIX4A==")); + +var heartImageData = require("heatshrink").decompress(atob("//z/4CD4EPAQgOBAQXggEfAQXwgEDAQX4EIN4AQN8AQMMAQMPGAkDwAGEsAFEjAFEgwFEgIYkjgFEh4SEgZmBDwf8RAoSEv5zBAAU//wFDj6MCFQX/QYUAg6iBGAYFBGQUBAoIyDAoIyDv4FBGQU/AoIyCGALICGAQABGQIwCGQQwCGQQwCGQYFDGQIwCGQQwCGQQwDAAPPAogiD")); + +var colorPaletteActive = new Uint16Array([g.toColor(1,0,0), g.toColor(1,1,1)]); +var colorPaletteInactiveLightBg = new Uint16Array([g.toColor(0,0,0), g.toColor(1,0,1)]); +var colorPaletteInactiveDarkBg = new Uint16Array([g.toColor(1,1,1), g.toColor(1,0,1)]); + +var light = require("heatshrink").decompress(atob("iEQwIEB8EBAIPAgYBBwABBg/gAIILBgOAAIUAgQBCAAY")); + +var planet = require("heatshrink").decompress(atob("5F9wZC/AH4A/AH4AugMkyVJkmAIv4AEI4JKCAQJG/AAUCJQ0kyBJ/JROSpAgVhIcEJUcSJRACBwAdQgIcJJVp7QWRKYjJRlJDZsEDJQCCkBKePBjjNbpQcRJUOSoAaJDJzjgJR7jJI54CCpBLdGCDGGghKSpMgJUkAgLjMhJHREogAbGYxWLVigCFyBKhpAMFPZECJS2SJTbXFaggACIQp7BiRKXOYwAVJRhZFyEEI6r9HJUoABBwWAI64CCkBKaQIhKKbYT0FASyWbEIdIB5UJkEJJTeAJTSGCJRgABI7ICCyBKagQhDCCACZcLZKuoBKahIhCkDxOJW0BEIYPKI7YCCpDhbEIeABpEEJT1JJTY8DyBYMATkgJbYhDJX4AGiRKKgJKgkhKbgQhCoALGhJKhwBLbEIVIJVOQJTY/DKpJK8gIhCkBKoyRKbgAhDBAkCJX8Agi3HJUlAJbhsHiRK/IQohDJUlIJTkAEIxK/AAcJEgToGJX8BEgUgJUxzDADYkDJX4AHEgWQJX4AGiQkCAggChkBKegQkCoAEDJX4ABgwkCpEAJX4AFgIkCgEJJUckJT8AN4ZPDJX4ACggkCwBK/AA0SEgOQgBK/AAsCE4RPDJX4ADE4VIAgYCfwBKhgInCgEJJX4AFE4UgJ4ZK/R4ICBE4cEJX5KDkEAiQnByBPDJX8JEYQqCoECJX5KDSIIDByVIJ4ZK/SIUBFQUgAgZK/SIUEFQUAJX5KDSIMAFQTnBJX5KDSIMCFoUAiRK/EwaRBFoTnBJX4pEgEBc4YLFJXyRBc4hK/FIgIBc4cEJX4pEgESc4YEDJXEBFJDnDgRK/AQgJEUhBK8pEAc4hK/AQgLBAwWQghK/AQkAiTnDAgYCTJMBKLO4LnDgRK/AQkAhIGCkAEDJSeQJcAvKoAMECJZKLDwRKppINBAwR9BJScgCoNIJVZ6BiScDAgZKSNIRKqwEAAwR9BJScENIYAdEQYCJgEBGwYEDJSZpBJVVAUomAChoCDFAL2CyCWeGR6cEG4QXPgQXDJVckGQlIAgZKOgC0EJVbEBHIkJJRtIFAQSDJTrLPgEBTghKRC4cgJVg1BgicEChjZEMQgAbgRKOPIQ8EC5hKEC4eAJVh5BCQkAhL1LYA6tBADZKQFwJFEaAhKMMQZKtZwRRFLhQqIdQoAWZBYCGCgKKEfZBKHFQZKukBFEpAaIwAqGgIbEADIfDAR47BIojoEJRQPEJV2SoAVEbAIOFFZEEK5YASJSbcBGoZRBgDvFABBfEADI0EAR4+BiQGDWYgrKDYZKvY4TbGJRhfDVQIAZJSh8BSAbOCgTsBABMCfggAZZQgCQPgKuEQZz1EADJqDASRjGkBKrD4gCSwAYFAwIAKhJcQABofDAScAgLqGABIREJTQxFASNIDQLqGFRqnMABxKWIYUCcZxKEyBKaghKWIYT8FcZDAFJTTHFASaxHJVLHFJShmGagxKFUhAATJSxAEBgtIE4jvFBYoAWEQoCQRJRXEXoxKcgBKUkAcGDCBKcFyJ8KRgxKnFyACBDhLjHVx4AWhJKPwAcKghKsgDEcWhxKegESFxdADhzjNJT7jLbpYAGJVguJDihKtADpKIkBJ/AAMCJX6YQpBF/AH4A/AH4Au")); + +const APP_NAME = "spaceclock"; +const HEADER_LINE_Y = 25; +const HEADER_TEXT_X = 5; +const HEADER_TEXT_Y = 7; +const MAIN_IMAGE_X = 13; +const MAIN_IMAGE_Y = 70; +const BATTERY_IMAGE_X = 112; +const BATTERY_IMAGE_Y = 4; +const BATTERY_CIRCLE_X = 120; +const BATTERY_CIRCLE_Y = 11; +const BATTERY_CIRCLE_RAD = 10; +const BATTERY_TEXT_X = 134; +const BATTERY_TEXT_Y = 5; +const TIME_X = 0; +const TIME_Y = 33; +const DATE_X = 12; +const DATE_Y = 125; +const WEEKDAY_X = 130; +const WEEKDAY_Y = 30; +const TEMPERATURE_X = 130; +const TEMPERATURE_Y = 50; +const HEART_IMAGE_X = 137; +const HEART_IMAGE_Y = 71; +const HEART_RATE_X = 127; +const HEART_RATE_Y = 99; +const FOOTER_LINE_Y = 150; +const FOOTER_TEXT_LEFT_X = 5; +const FOOTER_TEXT_RIGHT_X = 105; +const FOOTER_TEXT_Y = 160; +const FOOTER_IMAGE_X = 75; +const FOOTER_IMAGE_Y = 154; +const RUNNER_IMAGE_X = 90; +const RUNNER_IMAGE_Y = 117; +const STEPS_TEXT_X = 130; +const STEPS_TEXT_Y = 128; + + +const isDarkBg = g.getBgColor() === 0; +const weekdays = ["SUN","MON","TUE","WED","THU","FRI","SAT"]; + +const drawHeader = () => { + g.setFontArkitechLight(1); + g.drawString("ASTEROID", HEADER_TEXT_X, HEADER_TEXT_Y); + g.drawLine(0,HEADER_LINE_Y, g.getWidth(),HEADER_LINE_Y); +}; + +const drawMainImage = ()=>{ + g.drawImage(planet,MAIN_IMAGE_X, MAIN_IMAGE_Y,{scale:0.4}); +}; + +const drawBattery = ()=>{ + const battery = Math.round(E.getBattery()); + g.drawImage(light,BATTERY_IMAGE_X, BATTERY_IMAGE_Y); + g.drawCircle(BATTERY_CIRCLE_X,BATTERY_CIRCLE_Y,BATTERY_CIRCLE_RAD); + g.setFontDigitalNumbersSmall(1); + g.drawString(battery,BATTERY_TEXT_X,BATTERY_TEXT_Y); +}; + +const drawTime = ()=>{ + const date = new Date(); + const month = date.getMonth()+1; + const day = date.getDate(); + const hour = date.getHours(); + const minute = date.getMinutes(); + const weekdayInt = date.getDay(); + + // Time + g.setFontDigitalNumbersRegular(1); + g.drawString( ("0" + hour).substr(-2), TIME_X, TIME_Y); + g.drawString(":",TIME_X + 43,TIME_Y); + g.drawString(("0" + minute).substr(-2),TIME_X + 60,TIME_Y); + + // Date + g.setFontDigitalNumbersSmall(1); + g.drawString(("0" + day).substr(-2) + "-" + ("0" + month).substr(-2), DATE_X, DATE_Y); + + // Weekday + g.setFontDigitalNumbersSmall(1); + g.drawString(weekdays[weekdayInt], WEEKDAY_X, WEEKDAY_Y); +}; + +const drawHeart = (isHRMOn) =>{ + var palette; + + if (isHRMOn){ + palette = colorPaletteActive; + } else { + palette = isDarkBg ? colorPaletteInactiveDarkBg : colorPaletteInactiveLightBg; + } + + var heart = { + width : 50, height : 43, bpp : 1, + buffer : heartImageData, + palette: palette, + transparent: 1 + }; + + g.drawImage(heart, HEART_IMAGE_X, HEART_IMAGE_Y,{scale:0.5}); + +}; + +const drawSteps = () => { + var steps = Bangle.getHealthStatus("day").steps; + const stepsRaw = steps/1000; + const decimal = stepsRaw >=10 ? 0 : 1; + steps =stepsRaw.toFixed(decimal) + "K"; + console.log(steps); + g.setFontDigitalNumbersSmall(1); + g.drawString(steps, STEPS_TEXT_X, STEPS_TEXT_Y); + g.drawImage(runner, RUNNER_IMAGE_X, RUNNER_IMAGE_Y,{scale:0.9}); + +}; + +const drawFooter = ()=>{ + g.drawLine(0, FOOTER_LINE_Y, g.getWidth(), FOOTER_LINE_Y); + g.setFontArkitechLight(1); + g.drawString("SPACE", FOOTER_TEXT_LEFT_X, FOOTER_TEXT_Y); + g.drawString("RESIST", FOOTER_TEXT_RIGHT_X,FOOTER_TEXT_Y); + g.drawImage(nasa, FOOTER_IMAGE_X, FOOTER_IMAGE_Y); +}; + +const drawTemp = () => { + Bangle.getPressure().then((measure) => { + if (measure){ + g.clearRect(TEMPERATURE_X, TEMPERATURE_Y - 2,TEMPERATURE_X + 40, TEMPERATURE_Y+18); + const temp = Math.round(measure.temperature); + g.setFontDigitalNumbersSmall(1); + g.drawString(temp + "C", TEMPERATURE_X, TEMPERATURE_Y); + }}).catch((reason) => { + console.log("Error in getPressure(): " + reason); + }); +}; + +const drawHeartRate =(hrm, isOn)=>{ + const x = ((hrm < 100) || !hrm) ? HEART_RATE_X + 8 : HEART_RATE_X; + const measure = hrm ? hrm : "--"; + g.clearRect(HEART_RATE_X, HEART_RATE_Y - 5, HEART_RATE_X + 42, HEART_RATE_Y + 18); + g.setFontDigitalNumbersSmall(1); + g.drawString(measure, x, HEART_RATE_Y); +}; + +var drawTimeout; +const queueDraw = () => { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +}; + +const clearIntervals = () => { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; +}; + +const draw = ()=>{ + queueDraw(); + + g.clear(1); + const whiteOrBlack = isDarkBg? 1 : 0; + g.setColor(whiteOrBlack, whiteOrBlack, whiteOrBlack); + + drawHeader(); + drawMainImage(); + drawFooter(); + drawHeart(Bangle.isHRMOn()); + drawHeartRate(null, Bangle.isHRMOn()); + drawTime(); + drawBattery(); + drawSteps(); + drawTemp(); +}; + +Bangle.on('HRM',(hrm)=>{ + drawHeartRate(hrm.bpm, Bangle.isHRMOn()); +}); + +Bangle.on('touch',(button, xy)=>{ + // Toggle Heartrate + if (xy.x > 127 && xy.x < 167 && xy.y >71 && xy.y < 119) { + Bangle.setHRMPower(!Bangle.isHRMOn(),APP_NAME ); + console.log("Setting HRM to: " + Bangle.isHRMOn()); + drawHeart(Bangle.isHRMOn()); + } +}); + +Bangle.on("lcdPower", (on) => { + if (on) { + draw(); + } else { + clearIntervals(); + } +}); + +Bangle.on("lock", (locked) => { + clearIntervals(); + draw(); +}); + +Bangle.setUI("clock"); +Bangle.setBarometerPower(true, APP_NAME); +Bangle.setHRMPower(true,APP_NAME); + +draw(); diff --git a/apps/spaceclock/metadata.json b/apps/spaceclock/metadata.json new file mode 100644 index 000000000..404135679 --- /dev/null +++ b/apps/spaceclock/metadata.json @@ -0,0 +1,15 @@ +{ "id": "spaceclock", + "name": "Space Clock (Casio Style)", + "shortName":"Space Clock", + "version":"0.01", + "description": "Watch face in the style of Casio Prototype Space Resist", + "icon": "app-icon.png", + "type": "clock", + "tags": "clock, casio, retro", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"spaceclock.app.js","url":"app.js"}, + {"name":"spaceclock.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/spaceclock/spaceclock_light_big.png b/apps/spaceclock/spaceclock_light_big.png new file mode 100644 index 000000000..91bc590a4 Binary files /dev/null and b/apps/spaceclock/spaceclock_light_big.png differ diff --git a/apps/stopwatch/ChangeLog b/apps/stopwatch/ChangeLog index 14c84afd5..c4f382aa9 100644 --- a/apps/stopwatch/ChangeLog +++ b/apps/stopwatch/ChangeLog @@ -1,3 +1,4 @@ 0.01: first release 0.02: Adjust for touch events outside of screen g dimensions 0.03: Do not register as watch, manually start clock on button +0.04: Keep running in background by saving state diff --git a/apps/stopwatch/metadata.json b/apps/stopwatch/metadata.json index 7840dd9b5..bbc2dc181 100644 --- a/apps/stopwatch/metadata.json +++ b/apps/stopwatch/metadata.json @@ -1,7 +1,7 @@ { "id": "stopwatch", "name": "Stopwatch Touch", - "version": "0.03", + "version": "0.04", "description": "A touch based stop watch for Bangle JS 2", "icon": "stopwatch.png", "screenshots": [{"url":"screenshot1.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"}], @@ -11,5 +11,8 @@ "storage": [ {"name":"stopwatch.app.js","url":"stopwatch.app.js"}, {"name":"stopwatch.img","url":"stopwatch.icon.js","evaluate":true} + ], + "data": [ + {"name":"stopwatch.json"} ] } diff --git a/apps/stopwatch/stopwatch.app.js b/apps/stopwatch/stopwatch.app.js index 92e7a9977..d98f06cdd 100644 --- a/apps/stopwatch/stopwatch.app.js +++ b/apps/stopwatch/stopwatch.app.js @@ -1,9 +1,21 @@ +const CONFIGFILE = "stopwatch.json"; + +const now = Date.now(); +const config = Object.assign({ + state: { + total: now, + start: now, + current: now, + running: false, + } +}, require("Storage").readJSON(CONFIGFILE,1) || {}); + let w = g.getWidth(); let h = g.getHeight(); -let tTotal = Date.now(); -let tStart = tTotal; -let tCurrent = tTotal; -let running = false; +let tTotal = config.state.total; +let tStart = config.state.start; +let tCurrent = config.state.current; +let running = config.state.running; let timeY = 2*h/5; let displayInterval; let redrawButtons = true; @@ -15,6 +27,14 @@ const pause_img = atob("GBiBAf////////////////wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP/wY const play_img = atob("GBjBAP//AAAAAAAAAAAIAAAOAAAPgAAP4AAP+AAP/AAP/wAP/8AP//AP//gP//gP//AP/8AP/wAP/AAP+AAP4AAPgAAOAAAIAAAAAAAAAAA="); const reset_img = atob("GBiBAf////////////AAD+AAB+f/5+f/5+f/5+cA5+cA5+cA5+cA5+cA5+cA5+cA5+cA5+f/5+f/5+f/5+AAB/AAD////////////w=="); +function saveState() { + config.state.total = tTotal; + config.state.start = tStart; + config.state.current = tCurrent; + config.state.running = running; + require("Storage").writeJSON(CONFIGFILE, config); +} + function log_debug(o) { //console.log(o); } @@ -106,6 +126,7 @@ function stopStart() { } else { draw(); } + saveState(); } function setButtonImages() { @@ -130,6 +151,7 @@ function lapReset() { g.clearRect(0,24,w,h); draw(); } + saveState(); } // simple on screen button class @@ -226,5 +248,10 @@ g.fillRect(0,0,w,h); Bangle.loadWidgets(); Bangle.drawWidgets(); -draw(); +setButtonImages(); +if (running) { + startTimer(); +} else { + draw(); +} setWatch(() => load(), BTN, { repeat: false, edge: "falling" }); diff --git a/apps/tempmonitor/CSV_IDE_view.png b/apps/tempmonitor/CSV_IDE_view.png new file mode 100644 index 000000000..50725455b Binary files /dev/null and b/apps/tempmonitor/CSV_IDE_view.png differ diff --git a/apps/tempmonitor/CSV_excel_view.png b/apps/tempmonitor/CSV_excel_view.png new file mode 100644 index 000000000..b903515e8 Binary files /dev/null and b/apps/tempmonitor/CSV_excel_view.png differ diff --git a/apps/tempmonitor/ChangeLog b/apps/tempmonitor/ChangeLog new file mode 100644 index 000000000..99fe6a77d --- /dev/null +++ b/apps/tempmonitor/ChangeLog @@ -0,0 +1,2 @@ +0.01: 1st version: saves values to csv +0.02: added HTML interface diff --git a/apps/tempmonitor/README.md b/apps/tempmonitor/README.md new file mode 100644 index 000000000..a956f0e0f --- /dev/null +++ b/apps/tempmonitor/README.md @@ -0,0 +1,47 @@ +# Temperature Monitor (with logging) +Temperature monitor that shows temperature on real time but also allows to store in a file for a later process. + +Compatible with BangleJS1,BangleJS2,and EMSCRIPTENx emulators + +## Pictures: + +Bangle JS1 + +![](photo_banglejs1.jpg) + +Screenshot BJS2 + +![](ss_emul_bjs2.png) + +Screenshot BJS1 + +![](ss_emul_bjs1.png) + +Screenshot data file content + +![](file_interface.png) + +![](CSV_IDE_view.png) + +![](CSV_excel_view.png) + + +## Usage + +Open and see a temperature in the screen +Download the CSV file and process in your favourite spreadsheet software + +## Features + +Colours, all inputs , graph, widgets loaded +Counter for Times Display + + +## Controls + +exit: left side + + +## Creator + +Daniel Perez \ No newline at end of file diff --git a/apps/tempmonitor/app-icon.js b/apps/tempmonitor/app-icon.js new file mode 100644 index 000000000..807f58d38 --- /dev/null +++ b/apps/tempmonitor/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4X/AoPzvswnmT54cQgWAhWq1ALGlWoBZOptUKqtoBQsK0ta1QLHlWVqwLHgWpqtVtQLGEQILBrQLGCYIADBYgiDBY8KyoLJIQIAEtQiDHIQADrWALgYLFqyECEQpiDLggHC1QdBrWgEQVqAQOmLAQkBlIUB02lq28gFpKoQLBq2+1NW+2qGwVolQIB20prPaBYVq1ALCuwLB8sC02VBYMAhILB1Nb/uqgWVsBfBBYdZ/wLIEYP//TwBC41a/9v+wLHq1/7/6BYxjBz//9IXHqtpuqWBBY9aOwQjHAAYXHBZSNBAAOpBYsq3//AAPZBYda+wLJrawB34GBgwLEs9l0QLChILFz9i3+qxWrBYuvC4ILB1ALEtdqy2/4WKgALErPVuwXIrV/7QLIqxyB3+f4EoBYUqUgVVC4N60/ZtWolS4BAAO/qv1r/ZrWoV4iDEqtolSjDAAojBBZRIBABIA=")) \ No newline at end of file diff --git a/apps/tempmonitor/app.png b/apps/tempmonitor/app.png new file mode 100644 index 000000000..f1e576134 Binary files /dev/null and b/apps/tempmonitor/app.png differ diff --git a/apps/tempmonitor/file_interface.png b/apps/tempmonitor/file_interface.png new file mode 100644 index 000000000..2179d7d1e Binary files /dev/null and b/apps/tempmonitor/file_interface.png differ diff --git a/apps/tempmonitor/interface.html b/apps/tempmonitor/interface.html new file mode 100644 index 000000000..02d1fed91 --- /dev/null +++ b/apps/tempmonitor/interface.html @@ -0,0 +1,67 @@ + + + + + +
+ + + + + + \ No newline at end of file diff --git a/apps/tempmonitor/metadata.json b/apps/tempmonitor/metadata.json new file mode 100644 index 000000000..dafb70f27 --- /dev/null +++ b/apps/tempmonitor/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "tempmonitor", + "name": "Temperature monitor", + "version": "0.02", + "description": "Displays the current temperature and stores in a CSV file", + "icon": "app.png", + "tags": "tool", + "interface": "interface.html", + "supports": ["BANGLEJS", "BANGLEJS2"], + "readme": "README.md", + "screenshots": [{"url":"ss_emul_bjs2.png"}], + "allow_emulator": true, + "storage": [ + {"name":"tempmonitor.app.js","url":"tempmonitor.app.js"}, + {"name":"tempmonitor.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/tempmonitor/photo_banglejs1.jpg b/apps/tempmonitor/photo_banglejs1.jpg new file mode 100644 index 000000000..7352e619a Binary files /dev/null and b/apps/tempmonitor/photo_banglejs1.jpg differ diff --git a/apps/tempmonitor/ss_emul_bjs1.png b/apps/tempmonitor/ss_emul_bjs1.png new file mode 100644 index 000000000..da7a53bd1 Binary files /dev/null and b/apps/tempmonitor/ss_emul_bjs1.png differ diff --git a/apps/tempmonitor/ss_emul_bjs2.png b/apps/tempmonitor/ss_emul_bjs2.png new file mode 100644 index 000000000..1cc2140f9 Binary files /dev/null and b/apps/tempmonitor/ss_emul_bjs2.png differ diff --git a/apps/tempmonitor/tempmonitor.app.js b/apps/tempmonitor/tempmonitor.app.js new file mode 100644 index 000000000..62a4fee67 --- /dev/null +++ b/apps/tempmonitor/tempmonitor.app.js @@ -0,0 +1,138 @@ +// Temperature monitor that saves a log of measures +// standalone ver for developer, to remove testing lines +// delimiter ; (excel) or , (oldscool) +{ +var v_mode_debug=0; //, 0=no, 1 min, 2 prone detail +//var required for drawing with dynamic screen +var rect = Bangle.appRect; +var history = []; +var readFreq=5000; //ms //PEND add to settings +var saveFreq=60000; //ms 1min +var v_saveToFile= new Boolean(true); //true save //false +//with upload file º is not displayed properly +//with upload RAM º is displayed +var v_t_symbol="";//ºC +var v_saved_entries=0; +var filename ="temphistory.csv"; +var lastMeasure = new String(); +var v_model=process.env.BOARD; + +//EMSCRIPTEN,EMSCRIPTEN2 +if (v_model=='BANGLEJS'||v_model=='EMSCRIPTEN') { + v_font_size1=16; + v_font_size2=60; + //g.setColor("#0ff"); //light color + }else{ + v_font_size1=11; + v_font_size2=40; + //g.setColor("#000"); //black or dark + } + +function onTemperature(v_temp) { + if (v_mode_debug>1) console.log("v_temp in "+v_temp); + ClearBox(); + //g.setFont("6x8",2).setFontAlign(0,0); + g.setFontVector(v_font_size1).setFontAlign(0,0); + var x = (rect.x+rect.x2)/2; + var y = (rect.y+rect.y2)/2 + 20; + g.drawString("Records: "+v_saved_entries, x, rect.y+35); + g.drawString("Temperature:", x, rect.y+37+v_font_size1); + //dynamic font (g.getWidth() > 200 ? 60 : 40) + g.setFontVector(v_font_size2).setFontAlign(0,0); + // Avg of temperature readings + while (history.length>4) history.shift(); + history.push(v_temp); + var avrTemp = E.sum(history) / history.length; + //var t = require('locale').temp(avrTemp); + //.replace("'","°"); + lastMeasure=avrTemp.toString(); + if (lastMeasure.length>4) lastMeasure=lastMeasure.substr(0,4); + //DRAW temperature in the center + g.drawString(" ", x-20, y); + g.drawString(v_temp+v_t_symbol, x-20, y); + g.flip(); +} +// from: BJS2 pressure sensor, BJS1 inbuilt thermistor +function drawTemperature() { + if(v_model.substr(0,10)!='EMSCRIPTEN'){ + if (Bangle.getPressure) { + Bangle.getPressure().then(p =>{if (p) onTemperature(p);}); + } else onTemperature(E.getTemperature()); + } + else onTemperature(11);//fake temp for emulators +} + +function saveToFile() { + //input global vars: lastMeasure + var a=new Date(); + var strlastSaveTime=new String(); + strlastSaveTime=a.toISOString(); + //strlastSaveTime=strlastSaveTime.concat(a.getFullYear(),a.getMonth()+1,a.getDate(),a.getHours(),a.getMinutes());; + if (v_mode_debug==1) console.log("saving="+strlastSaveTime+";"+a.getHours()+":"+a.getMinutes()+";"+lastMeasure); + if (v_saveToFile==true){ + //write(strlastSaveTime+";"+ + require("Storage").open(filename,"a").write((a.getMonth()+1)+";"+a.getDate()+";"+a.getHours()+":"+a.getMinutes()+";"+lastMeasure+"\n"); + //(getTime()+","); + v_saved_entries=v_saved_entries+1; + } +} + +function drawGraph(){ + var img_obj_thermo = { + width : 36, height : 36, bpp : 3, + transparent : 0, + buffer : require("heatshrink").decompress(atob("AEFt2AMKm3bsAMJjdt23ABhEB+/7tgaJ///DRUP//7tuADRP923YDRXbDRfymwaJhu/koaK7eyiwaK3cLDRlWDRY1NKBY1Ztu5kjmJg3cyVI7YMHgdu5Mkyu2fxHkyVJjdgDRFJkmRDRPsDQNbDQ5QBGoONKBJrBoxQIQwO2eRcbtu24AMIFIQLJAH4AMA==")) + }; + g.drawImage(img_obj_thermo,rect.x2-50,rect.y2/2); + g.flip(); +} +function ClearScreen(){ + //avoid widget areas + g.reset(1).clearRect(rect.x, rect.y+24, rect.x2, rect.y2-24); + g.flip(); +} +function ClearBox(){ + //custom boxarea , left space for static graph at right + g.reset(1).clearRect(rect.x, rect.y+24, rect.x2-50, rect.y2-24); + g.flip(); +} +function introPage(){ + //g.setFont("6x8",2).setFontAlign(0,0); + g.setFontVector(v_font_size1).setFontAlign(-1,0); + //x alignment. -1=left (default), 0=center, 1=right + var x=3; + //dynamic positions as height for BJS1 is double than BJS2 + var y = (rect.y+rect.y2)/2 + 10; + g.drawString(" Default values ", x, y - ((v_font_size1*3)+2)); + g.drawString("--------------------", x, y - ((v_font_size1*2)+2)); + g.drawString("Mode debug: "+v_mode_debug, x, y - ((v_font_size1*1)+2)); + g.drawString("Read freq(ms): "+readFreq, x, y ); + g.drawString("Save to file: "+v_saveToFile, x, y+ ((v_font_size1*1)+2) ); + g.drawString("Save freq(ms):"+saveFreq, x, y+((v_font_size1*2)+2) ); + fr=require("Storage").read(filename+"\1");//suffix required + if (fr) g.drawString("Current filesize:"+fr.length.toString()+"kb", x, y+((v_font_size1*3)+2) ); + else g.drawString("File not exist", x, y+((v_font_size1*3)+2)); +} +//MAIN +Bangle.loadWidgets(); +Bangle.setUI({ + mode : "custom", + back : function() {load();} +}); + +ClearScreen(); +introPage(); + +setInterval(function() { + drawTemperature(); +}, readFreq); //ms + +if (v_saveToFile==true) { + setInterval(function() { + saveToFile(); + }, saveFreq); //ms +} +setTimeout(ClearScreen, 3500); +setTimeout(drawGraph,4000); +setTimeout(drawTemperature,4500); +} \ No newline at end of file diff --git a/apps/tempmonitor/tempmonitor.img b/apps/tempmonitor/tempmonitor.img new file mode 100644 index 000000000..275109759 Binary files /dev/null and b/apps/tempmonitor/tempmonitor.img differ diff --git a/apps/tempmonitor/tempmonitor.info b/apps/tempmonitor/tempmonitor.info new file mode 100644 index 000000000..1824c5c86 --- /dev/null +++ b/apps/tempmonitor/tempmonitor.info @@ -0,0 +1 @@ +{"id":"tempmonitor","name":"tempmonitor","src":"tempmonitor.app.js","icon":"tempmonitor.img","version":"0.01","files":"tempmonitor.info,tempmonitor.app.js,tempmonitor.img"} \ No newline at end of file diff --git a/apps/terminalclock/ChangeLog b/apps/terminalclock/ChangeLog index 75d1a760e..268e0427c 100644 --- a/apps/terminalclock/ChangeLog +++ b/apps/terminalclock/ChangeLog @@ -5,3 +5,5 @@ 0.05: Add altitude display (only Bangle.js 2) 0.06: Add power related settings to control the HR and pressure(altitude) sensor from the watchface 0.07: Use ClockFace module and rework the settings to be able to personnalize the order of the lines +0.08: Hide widgets instead of not loading them at all + Use Clockface_menu for widgets and power saving settings diff --git a/apps/terminalclock/app.js b/apps/terminalclock/app.js index b60a32094..515ad8f66 100644 --- a/apps/terminalclock/app.js +++ b/apps/terminalclock/app.js @@ -32,7 +32,8 @@ const clock = new ClockFace({ this.unlock_precision = 1; if (this.HRMinConfidence === undefined) this.HRMinConfidence = 50; if (this.PowerOnInterval === undefined) this.PowerOnInterval = 15; - if (this.powerSaving===undefined) this.powerSaving = true; + if (this.powerSave===undefined) this.powerSave = this.powerSaving; // migrate old setting + if (this.powerSave===undefined) this.powerSave = true; ["L2", "L3", "L4", "L5", "L6", "L7", "L8", "L9"].forEach(k => { if (this[k]===undefined){ if(k == "L2") this[k] = "Date"; @@ -55,7 +56,7 @@ const clock = new ClockFace({ }); // set the services (HRM, pressure sensor, etc....) - if(!this.powerSaving){ + if(!this.powerSave){ turnOnServices(); } else{ setInterval(turnOnServices, this.PowerOnInterval*60000); // every PowerOnInterval min @@ -156,7 +157,7 @@ function turnOnServices(){ if(clock.showAltitude){ Bangle.setBarometerPower(true, "terminalclock"); } - if(clock.powerSaving){ + if(clock.powerSave){ setTimeout(function () { turnOffServices(); }, 45000); @@ -194,7 +195,7 @@ Clock related functions but not in the ClockFace module ---------------------------------------------------- */ function unlock(){ - if(clock.powerSaving){ + if(clock.powerSave){ turnOnServices(); } clock.precision = clock.unlock_precision; diff --git a/apps/terminalclock/metadata.json b/apps/terminalclock/metadata.json index a8682f9a8..8403a3b4d 100644 --- a/apps/terminalclock/metadata.json +++ b/apps/terminalclock/metadata.json @@ -3,7 +3,7 @@ "name": "Terminal Clock", "shortName":"Terminal Clock", "description": "A terminal cli like clock displaying multiple sensor data", - "version":"0.07", + "version":"0.08", "icon": "app.png", "type": "clock", "tags": "clock", diff --git a/apps/terminalclock/settings.js b/apps/terminalclock/settings.js index f347e8ee3..80f6248ed 100644 --- a/apps/terminalclock/settings.js +++ b/apps/terminalclock/settings.js @@ -2,11 +2,8 @@ var FILE = "terminalclock.json"; // Load settings var settings = Object.assign({ - // ClockFace lib - loadWidgets: true, // TerminalClock specific HRMinConfidence: 50, - powerSaving: true, PowerOnInterval: 15, L2: 'Date', L3: 'HR', @@ -17,6 +14,18 @@ L8: 'Empty', L9: 'Empty', }, require('Storage').readJSON(FILE, true) || {}); + // ClockFace lib: migrate "don't load widgets" to "hide widgets" + if (!("hideWidgets" in settings)) { + if (("loadWidgets" in settings) && !settings.loadWidgets) settings.hideWidgets = 1; + else settings.hideWidgets = 0; + } + delete settings.loadWidgets; + // ClockFace lib: migrate `powerSaving` to `powerSave` + if (!("powerSave" in settings)) { + if ("powerSaving" in settings) settings.powerSave = settings.powerSaving; + else settings.powerSave = true; + } + delete settings.powerSaving; function writeSettings() { require('Storage').writeJSON(FILE, settings); @@ -63,25 +72,16 @@ writeSettings(); } }, - 'Show widgets': { - value: settings.loadWidgets, - onchange: v => { - settings.loadWidgets = v; - writeSettings(); - } - }, - 'Power saving': { - value: settings.powerSaving, - onchange: v => { - settings.powerSaving = v; - writeSettings(); - setTimeout(function() { - E.showMenu(getMainMenu()); - },0); - } - } }; - if(settings.powerSaving){ + const save = (key, v) => { + settings[key] = v; + writeSettings(); + }; + require("ClockFace_menu").addItems(mainMenu, save, { + hideWidgets: settings.hideWidgets, + powerSave: settings.powerSave, + }); + if(settings.powerSave){ mainMenu['Power on interval'] = { value: settings.PowerOnInterval, min: 3, max: 60, diff --git a/apps/testuserinput/README.md b/apps/testuserinput/README.md index 47c1779be..7e1160bfb 100644 --- a/apps/testuserinput/README.md +++ b/apps/testuserinput/README.md @@ -43,7 +43,7 @@ Colours, font, user input, image, load widgets - Press center area - Prints Touch3 - Swipe Left - Displays Switch OFF image - Swipe Right - Displays Switch ON image - - BTN1 - Prints Button1 + - BTN1 - Prints Button1, Down (moves selection to next row) - BTN2 - Prints Button2 - BTN3 - Quit to Launcher diff --git a/apps/thering/ChangeLog b/apps/thering/ChangeLog new file mode 100644 index 000000000..25c572560 --- /dev/null +++ b/apps/thering/ChangeLog @@ -0,0 +1,2 @@ +0.01: Initial release. +0.02: Use widget_utils. diff --git a/apps/thering/app.js b/apps/thering/app.js index f7cfaa015..8767941b2 100644 --- a/apps/thering/app.js +++ b/apps/thering/app.js @@ -1,5 +1,6 @@ const h = g.getHeight(); const w = g.getWidth(); +const widget_utils = require('widget_utils'); // palette for 0-40% const pal1 = new Uint16Array([g.theme.bg, g.toColor("#020"), g.toColor("#0f0"), g.toColor("#00f")]); // palette for 50-100% @@ -215,10 +216,8 @@ Bangle.setUI("clock"); Bangle.loadWidgets(); /* * we are not drawing the widgets as we are taking over the whole screen - * 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="";} +widget_utils.hide(); draw(); setInterval(draw, 60000); diff --git a/apps/thering/metadata.json b/apps/thering/metadata.json index 32b1dae4b..6118a90f9 100644 --- a/apps/thering/metadata.json +++ b/apps/thering/metadata.json @@ -1,6 +1,6 @@ { "id": "thering", "name": "The Ring", - "version":"0.01", + "version":"0.02", "description": "A proof of concept clock with large ring guage for steps using pre-set images, acts as a tutorial piece for discussion", "icon": "app.png", "tags": "clock", diff --git a/apps/tictactoe/ChangeLog b/apps/tictactoe/ChangeLog new file mode 100644 index 000000000..1619cf1c2 --- /dev/null +++ b/apps/tictactoe/ChangeLog @@ -0,0 +1 @@ +0.01: Stable Launch diff --git a/apps/tictactoe/app-icon.js b/apps/tictactoe/app-icon.js new file mode 100644 index 000000000..c238afa61 --- /dev/null +++ b/apps/tictactoe/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwIJGv//AAX+oEAwEBgECApuD4IFBocww1hAoXwxkxAoNB8GIiIFC4AFDofgCIYdFoEQFIZBRLLoFBHYkAI4YFBKYYFHCIodFLO8M4RHDh4FCKYMHwQFELIkPBYQdFFIVCLK8AAAg=")) diff --git a/apps/tictactoe/app.js b/apps/tictactoe/app.js new file mode 100644 index 000000000..3f2da4945 --- /dev/null +++ b/apps/tictactoe/app.js @@ -0,0 +1,210 @@ +////////////////////////////// +// Tic - Tac - Toe +// Stable Version 1.0 - 12/31/2022 +// MissionMake +////////////////////////////// + +//////////////////////////// +// TODO: +// Implement Computer Player +// Beginning Screen (pick player to go first, pick one or two player) +//////////////////////////// + + +//create 3x3 array to log plays Xs defined as 1, Os defined as -1, blank is undefined, array is initialized undefined, player is which players turn is active (using 1,-1 definition to match matrix), active is if a game is being played + +var arr1 = new Array(3); +var arr2 = new Array(3); +var arr3 = new Array(3); +var arr = new Array(arr1,arr2,arr3); +var val = 0; +var player; +var active = false; +var select = false; +var next = 0; +var winval =0; +var ex = require("heatshrink").decompress(atob("mEwwI63jACEngCEvwCEv4CB/wCBn+AgP8AoMf4ED/AFBh/gg/wAoIDBA4IFBB4ITBAoIbBD4I8C/wrCGAQuCGAQuCGAQuCGAQuCAo4RFDoopFGohBFJopZFMopxFPoqJFSoqhFVooA0A")); +var oh = require("heatshrink").decompress(atob("mEwwIdah/wAof//4ECgYFB4AFBg4FB8AFBj/wh/4AoM/wEB/gFBvwCB/wCBBAU/AQIUCj8AgIzCh+AgYmCg/AgYyCAYIHBAoXgg+AAoMBApkPLgZKBAtBBRLIprDMoJxFPoqJFSoyhCAQStFXIrFFaIrdFdIwAVA")); + +//calculates sum of rows, colums, and diagonals for a win condition. passes winner to win() and breaks out of calcs +function calcWin(){ + winval = 0; + //sum of row + for(let i = 0; i<3; i++){ + val=0; + for(let j = 0; j<3; j++){ + val = arr[i][j]+val; + } + if (Math.abs(val)==3) { + winval = val; + } + } + + //sum of columns + for(let j = 0; j<3; j++){ + val=0; + for (let i = 0; i<3; i++){ + val = arr[i][j]+val; + } + //if win set winval to val + if (Math.abs(val)==3) { + winval = val; + } + } + + //Sum of ul to lr + val=0; + val = arr[0][0]+arr[1][1]+arr[2][2]; + //if win set winval to val + if (Math.abs(val)==3) { + winval = val; + } + + //sum of ur to ll + val=0; + val = arr[0][2]+arr[1][1]+arr[2][0]; + //if win set winval to val + if (Math.abs(val)==3){ + winval = val; + } + + //draw check + // drawChk is sum absolute value of array, if drawChk = 9 then there is a draw + drawChk = 0; + for(let i = 0; i<3; i++){ + for(let j = 0; j<3; j++){ + drawChk = drawChk + Math.abs(arr[i][j]); + } + } + + //checks for win cases and posts correct message, otherwise play + if (winval == 3){ + active = false; + E.showAlert("Player X Wins").then(start); + } else if (winval == -3){ + active = false; + E.showAlert("Player O Wins").then(start); + } else if (drawChk == 9) { + active = false; + E.showAlert("Draw").then(start); + }else{ + //If no win then play + draw(); + } +} + +function draw(){ + g.clear(); + if (player ==1){ + playerIcon = "X"; + } else if(player == -1){ + playerIcon = "O"; + } + //Banner Displays player turn + E.showMessage("","Player "+ playerIcon); + //drawboard + g.drawLine(62,24,62,176); + g.drawLine(112,24,112,176); + g.drawLine(12,74,164,74); + g.drawLine(12,124,164,124); + + //loop through array and draw markers + for(let i = 0; i<3; i++){ + for(let j = 0; j<3; j++){ + if(arr[j][i] == -1){ + g.drawImage(oh,i*50+12,j*50+24);//, {scale:1.05}); + } else if (arr[j][i] == 1){ + g.drawImage(ex,i*50+12,j*50+24);//, {scale:1.05}); + } else { + //blank spot + } + } + } + select=false; + wait(); +} + +// Square locations +//12,24;62,24,112,24 +//12,74;62,74,112,74 +//12,124;62,124,112,124 + +function placeMarker(){ + ///Determine marker square + if (x <= 62) { + b = 0; + } else if (x <= 112){ + b = 1; + } else { + b = 2; + } + + if (y <= 74) { + a = 0; + } else if (y <= 124){ + a = 1; + } else { + a = 2; + } + + //if empty + if( arr[a][b] == undefined){ + //record in array + arr[a][b] = player; + player=player*-1; + select = false; + calcWin(); + } else{ //if filled + // This could just do nothing + + E.showAlert("SpaceFilled Try again").then(draw); + } +} + + + + + +// Wait loop which is run until a tap is selected +function wait(){ + //Terminal.println("wait"); + if(select == true){ + placeMarker(); + } else { + setTimeout(wait,300); + } +} + + +// Starts new game +// Draws the start pattern, sets first player to x and goes to play +function start(){ + //reset array to undefined + arr1.fill(undefined); + arr2.fill(undefined); + arr3.fill(undefined); + g.clear(); + active =true; + player=1; + draw(); +} + + +//Looks for touch +Bangle.on('touch', function(zone,e) { + x = Object.values(e)[0]; + y = Object.values(e)[1]; + //if game is active + if(active == true){ + g.fillCircle(x, y, 10); + select = true; + } + if(active == false){ + start(); + } + }); + + +start(); + + diff --git a/apps/tictactoe/app.png b/apps/tictactoe/app.png new file mode 100644 index 000000000..1e47fdc7b Binary files /dev/null and b/apps/tictactoe/app.png differ diff --git a/apps/tictactoe/metadata.json b/apps/tictactoe/metadata.json new file mode 100644 index 000000000..11010b156 --- /dev/null +++ b/apps/tictactoe/metadata.json @@ -0,0 +1,17 @@ +{ "id": "tictactoe", + "name": "TicTacToe", + "shortName":"TicTacToe", + "icon": "app.png", + "version":"0.01", + "description": "Tic Tac Toe for two players!", + "tags": "game", + "storage": [ + {"name":"tictactoe.app.js","url":"app.js"}, + {"name":"tictactoe.img","url":"app-icon.js","evaluate":true} + ], + "screenshots" : [ + { "url":"tttscreenshot.png" }, + { "url":"tttscreenshot2.png" } + ], + "supports": ["BANGLEJS2"] +} diff --git a/apps/tictactoe/tttscreenshot.png b/apps/tictactoe/tttscreenshot.png new file mode 100644 index 000000000..a956d2fc0 Binary files /dev/null and b/apps/tictactoe/tttscreenshot.png differ diff --git a/apps/tictactoe/tttscreenshot2.png b/apps/tictactoe/tttscreenshot2.png new file mode 100644 index 000000000..d361f94f9 Binary files /dev/null and b/apps/tictactoe/tttscreenshot2.png differ diff --git a/apps/weather/ChangeLog b/apps/weather/ChangeLog index f1d001c81..4b70d3531 100644 --- a/apps/weather/ChangeLog +++ b/apps/weather/ChangeLog @@ -19,3 +19,4 @@ 0.20: Added weather condition with temperature to clkinfo. 0.21: Updated clkinfo icon. 0.22: Automatic translation of strings, some left untranslated. +0.23: Update clock_info to avoid a redraw diff --git a/apps/weather/clkinfo.js b/apps/weather/clkinfo.js index 3cdd31c59..f40924e06 100644 --- a/apps/weather/clkinfo.js +++ b/apps/weather/clkinfo.js @@ -34,14 +34,14 @@ name: "conditionWithTemperature", get: () => ({ text: weather.temp, img: weatherIcon(weather.code), v: parseInt(weather.temp), min: -30, max: 55}), - show: function() { this.emit("redraw"); }, + show: function() {}, hide: function () {} }, { name: "condition", get: () => ({ text: weather.txt, img: weatherIcon(weather.code), v: weather.code}), - show: function() { this.emit("redraw"); }, + show: function() {}, hide: function () {} }, { @@ -49,7 +49,7 @@ hasRange : true, get: () => ({ text: weather.temp, img: atob("GBiBAAA8AAB+AADnAADDAADDAADDAADDAADDAADbAADbAADbAADbAADbAADbAAHbgAGZgAM8wAN+wAN+wAM8wAGZgAHDgAD/AAA8AA=="), v: parseInt(weather.temp), min: -30, max: 55}), - show: function() { this.emit("redraw"); }, + show: function() {}, hide: function () {} }, { @@ -57,7 +57,7 @@ hasRange : true, get: () => ({ text: weather.hum, img: atob("GBiBAAAEAAAMAAAOAAAfAAAfAAA/gAA/gAI/gAY/AAcfAA+AQA+A4B/A4D/B8D/h+D/j+H/n/D/n/D/n/B/H/A+H/AAH/AAD+AAA8A=="), v: parseInt(weather.hum), min: 0, max: 100}), - show: function() { this.emit("redraw"); }, + show: function() {}, hide: function () {} }, { @@ -65,7 +65,7 @@ hasRange : true, get: () => ({ text: weather.wind, img: atob("GBiBAAHgAAPwAAYYAAwYAAwMfAAY/gAZh3/xg//hgwAAAwAABg///g//+AAAAAAAAP//wH//4AAAMAAAMAAYMAAYMAAMcAAP4AADwA=="), v: parseInt(weather.wind), min: 0, max: 118}), - show: function() { this.emit("redraw"); }, + show: function() {}, hide: function () {} }, ] diff --git a/apps/weather/metadata.json b/apps/weather/metadata.json index 7fefb7685..77ca37721 100644 --- a/apps/weather/metadata.json +++ b/apps/weather/metadata.json @@ -1,7 +1,7 @@ { "id": "weather", "name": "Weather", - "version": "0.22", + "version": "0.23", "description": "Show Gadgetbridge weather report", "icon": "icon.png", "screenshots": [{"url":"screenshot.png"}], diff --git a/apps/widbgjs/ChangeLog b/apps/widbgjs/ChangeLog new file mode 100644 index 000000000..7b83706bf --- /dev/null +++ b/apps/widbgjs/ChangeLog @@ -0,0 +1 @@ +0.01: First release diff --git a/apps/widbgjs/README.md b/apps/widbgjs/README.md new file mode 100644 index 000000000..dbb8cd9e4 --- /dev/null +++ b/apps/widbgjs/README.md @@ -0,0 +1,29 @@ +# Prerequisites +For this widget to work and to get data from the phone, you need: +- An Android phone + - with xDrip and the helper app installed. + - the Gadgetbridge app (bangle version) for the Android phone +- A BangleJS + - With this widget installed + +# Widget + +## How to use it +Make sure you have all the prerequisites from above. + +The watch should automatically start displaying values, if there is an arrow visible behind the value, the value is within the not-expired-yet time range changeable in the settings standard is 15 minutes. (I will probably change this in the future, to strike through the text to make expired values clearer). + +## Settings +In the settings, you can: +- Disable/hide the widget +- Change the unit from mmol/L to mg/dL +- Set a time at which old BG values expire + + +# Developer +Developed by Phil Roggenbuck (phrogg) + + +# Disclaimer +As well as xdrip you should not use this app to make medical decisions! + diff --git a/apps/widbgjs/metadata.json b/apps/widbgjs/metadata.json new file mode 100644 index 000000000..0639b5a51 --- /dev/null +++ b/apps/widbgjs/metadata.json @@ -0,0 +1,22 @@ +{ + "id": "widbgjs", + "name": "Blood Glucose Widget (xdrip)", + "shortName":"BG Widget", + "icon": "screenshot.png", + "screenshots": [{"url":"screenshot.png"}], + "version":"0.01", + "type": "widget", + "supports": ["BANGLEJS", "BANGLEJS2"], + "readme": "README.md", + "allow_emulator":true, + "description": "Displays the current blood glucose received from xdrip and send over via a helper app on the watch.", + "tags": "widget", + "storage": [ + {"name":"widbgjs.wid.js","url":"widget.js"}, + {"name":"widbgjs.settings.js","url":"settings.js"} + ], + "data": [ + {"name":"widbgjs.json"}, + {"name":"widbgjs.settings.json"} + ] + } diff --git a/apps/widbgjs/screenshot.png b/apps/widbgjs/screenshot.png new file mode 100644 index 000000000..b6e092af2 Binary files /dev/null and b/apps/widbgjs/screenshot.png differ diff --git a/apps/widbgjs/settings.js b/apps/widbgjs/settings.js new file mode 100644 index 000000000..f2eb190bc --- /dev/null +++ b/apps/widbgjs/settings.js @@ -0,0 +1,54 @@ +(function (back) { + const SAVEFILE = "wpbgjs.settings.json"; + + // initialize with default settings... + let s = { + 'unitIsMmol': true, + 'expireThreshold': 600000, + 'hide': 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 d = storage.readJSON(SAVEFILE, 1) || {}; + const saved = d.settings || {}; + for (const key in saved) { + s[key] = saved[key]; + } + + function save() { + d.settings = s; + storage.write(SAVEFILE, d); + WIDGETS['widbgjs'].draw(); + } + + E.showMenu({ + '': { 'title': 'BG widget' }, + 'Unit': { + value: s.unitIsMmol, + format: () => (s.unitIsMmol ? 'mmol/L' : 'mg/dL'), + onchange: () => { + s.unitIsMmol = !s.unitIsMmol; + save(); + }, + }, + 'Exp. BG': { + value: s.expireThreshold, + min: 18000, step: 60000, + format: s => (s ? s / 60000 + ' min' : '0'), + onchange: (g) => { + s.expireThreshold = g; + save(); + }, + }, + 'Hide Widget': { + value: s.hide, + format: () => (s.hide ? 'Yes' : 'No'), + onchange: () => { + s.hide = !s.hide; + save(); + }, + }, + '< Back': back, + }); +}); \ No newline at end of file diff --git a/apps/widbgjs/widget.js b/apps/widbgjs/widget.js new file mode 100644 index 000000000..1a1df002d --- /dev/null +++ b/apps/widbgjs/widget.js @@ -0,0 +1,143 @@ +//WIDGETS = {}; // <-- for development only + +(() => { + // persistant vals + let storedData; + let settings; + + function loadSettings() { // stolen from https://github.com/espruino/BangleApps/blob/master/apps/widpedom/widget.js + const d = require('Storage').readJSON("widbgjs.settings.json", 1) || {}; + settings = Object.assign({ + 'unitIsMmol': true, + 'expireThreshold': 600000, + 'reloadInterval': 5 * 60000, + 'hide': false + }, d || {}); + return d; + } + + function loadVals() { + try { + const d = require('Storage').readJSON("widbgjs.json", 1) || {}; + storedData = Object.assign({ + 'bg': null, + 'bgTimeStamp': null, + 'bgDirection': null + }, d || {}); + return d; + } catch(e) { + Bangle.removeFile("widbgjs.json"); + } + return null; + } + + function calculateRotation(bgDirection) { + var a = 90; + // get the arrow right (https://github.com/StephenBlackWasAlreadyTaken/NightWatch/blob/6de1d3775c6e447177c12f387f647628cc8e24ce/mobile/src/main/java/com/dexdrip/stephenblack/nightwatch/Bg.java) + switch (bgDirection) { + case ("DoubleDown"): + g.setColor("#f00"); + a = 180; + break; + case ("SingleDown"): + a = 180; + break; + case ("DoubleUp"): + g.setColor("#f00"); + a = 0; + break; + case ("SingleUp"): + a = 0; + break; + case ("FortyFiveUp"): + a = 45; + break; + case ("FortyFiveDown"): + a = 135; + break; + case ("Flat"): + a = 90; + break; + } + // turn the arrow thanks to (https://forum.espruino.com/conversations/344607/) + const p180 = Math.PI / 180; + // a is defined above + var r = 21; + var x = r * Math.sin(a * p180); + var y = r * Math.cos(a * p180); + + return a * p180; + } + + function getBG(bg) { + var tmp = null; + + try { + if (storedData.bg !== null) { + tmp = bg; + + if (settings.unitIsMmol) { + tmp /= 18; + tmp = tmp.toFixed(1); + } + } + + } catch (e) { } + return tmp; + } + + function isBgTooOld(bgTimeStamp) { + var currTimeInMilli = new Date().valueOf(); + + try { + if (bgTimeStamp === null) { + return true; + } + + if (currTimeInMilli - settings.expireThreshold <= bgTimeStamp) { + return false; + } + } catch (e) { } + return true; + } + + function draw() { + loadSettings(); + try { + if (settings.hide) return; + } catch (e) { } + loadVals(); + + outpt = getBG(storedData.bg); + + if (outpt === null) { // this means no value has been received yet + outpt = "BG"; + bgTimeStamp = "0"; + } + + // prepare to write on the screen + g.reset().clearRect(this.x, this.y, this.x + this.width, this.y + 23); // erase background + g.setFont('Vector', 22); + g.setColor(g.theme.fg); + + // check if the value is too old + if (!isBgTooOld(storedData.bgTimeStamp)) { + g.drawImage(atob("FBQBAGAADwAB+AA/wAduAGZgAGAABgAAYAAGAABgAAYAAGAABgAAYAAGAABgAAYAAGAABgA="), this.x + 60, this.y + 9, { rotate: calculateRotation(storedData.bgDirection) }); + } + g.setColor(g.theme.fg).drawString(outpt, this.x + 5, this.y); + } + + setInterval(function () { + WIDGETS["widbgjs"].draw(WIDGETS["widbgjs"]); + }, 5 * 60000); // update every 5 minutes (%* 60000 + + + // add your widget + WIDGETS["widbgjs"] = { + area: "tl", + width: 72, + draw: draw + }; +})(); + +//Bangle.drawWidgets(); // <-- for development only diff --git a/apps/widclk/widget.js b/apps/widclk/widget.js index 7c281f761..a31bd4772 100644 --- a/apps/widclk/widget.js +++ b/apps/widclk/widget.js @@ -6,7 +6,7 @@ WIDGETS["wdclk"]={area:"tl",width:Bangle.CLOCK?0:52/* g.stringWidth("00:00") */, this.width = Bangle.CLOCK?0:52; return setTimeout(Bangle.drawWidgets,1); // widget changed size - redraw } - if (!this.width) return; // if size not right, return + if (!this.width) return; // if not visible, return g.reset().setFontCustom(atob("AAAAAAAAAAIAAAQCAQAAAd0BgMBdwAAAAAAAdwAB0RiMRcAAAERiMRdwAcAQCAQdwAcERiMRBwAd0RiMRBwAAEAgEAdwAd0RiMRdwAcERiMRdwAFAAd0QiEQdwAdwRCIRBwAd0BgMBAAABwRCIRdwAd0RiMRAAAd0QiEQAAAAAAAAAA="), 32, atob("BgAAAAAAAAAAAAAAAAYCAAYGBgYGBgYGBgYCAAAAAAAABgYGBgYG"), 512+9); var time = require("locale").time(new Date(),1); g.drawString(time, this.x, this.y+3, true); // 5 * 6*2 = 60 diff --git a/apps/widclkbttm/ChangeLog b/apps/widclkbttm/ChangeLog index 9dc8f8d2c..373337378 100644 --- a/apps/widclkbttm/ChangeLog +++ b/apps/widclkbttm/ChangeLog @@ -2,3 +2,5 @@ 0.02: Modification for bottom widget area and text color 0.03: based in widclk v0.05 compatible at same time, bottom area and color 0.04: refactored to use less memory, and allow turning on/off when quick-switching apps +0.05: Remove cyan color, use theme foreground instead + diff --git a/apps/widclkbttm/README.md b/apps/widclkbttm/README.md index 5e386a757..a8379e288 100644 --- a/apps/widclkbttm/README.md +++ b/apps/widclkbttm/README.md @@ -1,5 +1,9 @@ # Digital clock widget (bottom widget area) -This very basic widget clock allows to test the unfrequently used widget bottom area. +This very basic widget clock shows time inside apps that respect the bottom widget area, also allows to test this unfrequently used area. + +Note that it will not be displayed when a clock app is been shown + +Compatible with BangleJS1,BangleJS2,and EMSCRIPTENx emulators forked from https://github.com/espruino/BangleApps/tree/master/apps/widclk @@ -10,14 +14,17 @@ Example of usage ![](widTextBottom_ss1.jpg) +Screenshot emulator bangle.js2 + +![](ss_bjs2.jpg) + ## Usage Upload the widget file -Open an app that supports displaying widgets - +Open an app (not a clock/watchface) that supports displaying widgets (included the bottom one) @@ -25,4 +32,4 @@ Open an app that supports displaying widgets This app is so basic that probably the easiest is to just edit the code ;) -Otherwise you can contact me [here](https://github.com/dapgo) \ No newline at end of file +Otherwise you can contact me [here](https://github.com/dapgo/my_espruino_smartwatch_things) \ No newline at end of file diff --git a/apps/widclkbttm/metadata.json b/apps/widclkbttm/metadata.json index 7c5fe4b63..4b14ef9c6 100644 --- a/apps/widclkbttm/metadata.json +++ b/apps/widclkbttm/metadata.json @@ -2,12 +2,13 @@ "id": "widclkbttm", "name": "Digital clock (Bottom) widget", "shortName": "Digital clock Bottom Widget", - "version": "0.04", - "description": "Displays time in the bottom of the screen (may not be compatible with some apps)", + "version": "0.05", + "description": "Displays time HH:mm in the bottom of the screen (may not be compatible with some apps)", "icon": "widclkbttm.png", "type": "widget", "tags": "widget", "supports": ["BANGLEJS","BANGLEJS2"], + "screenshots": [{"url":"ss_bjs1.png"}], "readme": "README.md", "storage": [ {"name":"widclkbttm.wid.js","url":"widclkbttm.wid.js"} diff --git a/apps/widclkbttm/ss_bjs1.png b/apps/widclkbttm/ss_bjs1.png new file mode 100644 index 000000000..b1b2f553a Binary files /dev/null and b/apps/widclkbttm/ss_bjs1.png differ diff --git a/apps/widclkbttm/ss_bjs2.png b/apps/widclkbttm/ss_bjs2.png new file mode 100644 index 000000000..a7c557c53 Binary files /dev/null and b/apps/widclkbttm/ss_bjs2.png differ diff --git a/apps/widclkbttm/widclkbttm.wid.js b/apps/widclkbttm/widclkbttm.wid.js index c5e85318c..50142a5b9 100644 --- a/apps/widclkbttm/widclkbttm.wid.js +++ b/apps/widclkbttm/widclkbttm.wid.js @@ -3,8 +3,8 @@ WIDGETS["wdclkbttm"]={area:"br",width:Bangle.CLOCK?0:60,draw:function() { this.width = Bangle.CLOCK?0:60; return setTimeout(Bangle.drawWidgets,1); // widget changed size - redraw } - if (!this.width) return; // if size not right, return - g.reset().setFont("6x8", 2).setFontAlign(-1, 0).setColor("#0ff"); // cyan + if (!this.width) return; // if not visible, return + g.reset().setFont("6x8", 2).setFontAlign(-1, 0); var time = require("locale").time(new Date(),1); g.drawString(time, this.x, this.y+11, true); // 5 * 6*2 = 60 // queue draw in one minute diff --git a/apps/widhwbttm/ChangeLog b/apps/widhwbttm/ChangeLog new file mode 100644 index 000000000..7d3aafc41 --- /dev/null +++ b/apps/widhwbttm/ChangeLog @@ -0,0 +1,3 @@ +0.01: 1st ver, inspired in some code from widclkbttm (Digital clock bttom widget) +0.02: Correction, intervals, dynamic color and font size depending on device +0.03: minor corrections, and color depending on theme diff --git a/apps/widhwbttm/README.md b/apps/widhwbttm/README.md new file mode 100644 index 000000000..85f7af47f --- /dev/null +++ b/apps/widhwbttm/README.md @@ -0,0 +1,33 @@ +# hw stats bttom widget (bottom widget area) +A basic HW/performance monitor widget that shows on real time some technical info, such as free mem, free storage, trash mem, files, FW version. Also allows to test the unfrequently used widget bottom area. + +Compatible with BangleJS1,BangleJS2,and EMSCRIPTENx emulators +Dynamic Color dependant on Theme color bg + +forked from my widclkbttm (Digital clock bttom widget) + + +## Photo + +Example of usage + +![](widhwbttm.ss1.jpg) +![](widhwbttm.ss2.jpg) + +Screenshot emulator +![](screenshot_ems2.png) + + + +## Usage + +Upload the widget file +Open an app (not a clock/watchface) that supports displaying widgets (included the bottom one) +Different info is refreshed following a predefined frequency and sequence. + + +## Support + +This app is so basic that probably the easiest is to just edit the code ;) + +Otherwise you can contact me [here](https://github.com/dapgo/my_espruino_smartwatch_things) \ No newline at end of file diff --git a/apps/widhwbttm/metadata.json b/apps/widhwbttm/metadata.json new file mode 100644 index 000000000..8a6957a46 --- /dev/null +++ b/apps/widhwbttm/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "widhwbttm", + "name": "HW stats (Bottom) widget", + "shortName": "Digital clock Bottom Widget", + "version": "0.03", + "description": "Displays technical info, such as model, ver, temperatura or mem stats in the bottom of the screen (may not be compatible with some apps)", + "icon": "widhwbttm.png", + "type": "widget", + "tags": "widget", + "supports": ["BANGLEJS","BANGLEJS2"], + "screenshots": [{"url":"screenshot.png"}], + "readme": "README.md", + "storage": [ + {"name":"widhwbttm.wid.js","url":"widhwbttm.wid.js"} + ] +} diff --git a/apps/widhwbttm/screenshot.png b/apps/widhwbttm/screenshot.png new file mode 100644 index 000000000..de648d399 Binary files /dev/null and b/apps/widhwbttm/screenshot.png differ diff --git a/apps/widhwbttm/screenshot_ems2.png b/apps/widhwbttm/screenshot_ems2.png new file mode 100644 index 000000000..d0c753946 Binary files /dev/null and b/apps/widhwbttm/screenshot_ems2.png differ diff --git a/apps/widhwbttm/widhwbttm.png b/apps/widhwbttm/widhwbttm.png new file mode 100644 index 000000000..82ad43795 Binary files /dev/null and b/apps/widhwbttm/widhwbttm.png differ diff --git a/apps/widhwbttm/widhwbttm.ss1.jpg b/apps/widhwbttm/widhwbttm.ss1.jpg new file mode 100644 index 000000000..bfca08f06 Binary files /dev/null and b/apps/widhwbttm/widhwbttm.ss1.jpg differ diff --git a/apps/widhwbttm/widhwbttm.ss2.jpg b/apps/widhwbttm/widhwbttm.ss2.jpg new file mode 100644 index 000000000..17942d0be Binary files /dev/null and b/apps/widhwbttm/widhwbttm.ss2.jpg differ diff --git a/apps/widhwbttm/widhwbttm.ss3.jpg b/apps/widhwbttm/widhwbttm.ss3.jpg new file mode 100644 index 000000000..90fc5eecb Binary files /dev/null and b/apps/widhwbttm/widhwbttm.ss3.jpg differ diff --git a/apps/widhwbttm/widhwbttm.wid.js b/apps/widhwbttm/widhwbttm.wid.js new file mode 100644 index 000000000..551e2005b --- /dev/null +++ b/apps/widhwbttm/widhwbttm.wid.js @@ -0,0 +1,59 @@ +(function() { + let intervalRef = null; + var v_count; // show stats + var v_str_hw=new String(); + //if (process.env.BOARD=='BANGLEJS'||process.env.BOARD=='EMSCRIPTEN') var v_bfont_size=2; + var v_bfont_size=2; + if (g.theme.dark==true) var v_color=0xFFFF; //white + else var v_color=0x0000; //black + if (v_count == null || v_count == '') v_count=0; + + function draw(){ + // if (Bangle.CLOCK) return; //to remove from a clock + if (v_count==0) { + v_str_hw=process.env.VERSION.substr(0,6); + v_count++; + } else if (v_count==1) { + v_str_hw=process.env.BOARD.substr(0,3)+".."+process.env.BOARD.substr(process.env.BOARD.length-3,3); + v_count++; + } else if (v_count==2) { + v_str_hw="Bat "+E.getBattery()+"%"; + v_count++; + } + else if (v_count==3 && process.env.BOARD.substr(0,6)=='BANGLE') { + v_str_hw="Tmp "+E.getTemperature(); + v_count++; + } + else { + // text prefix has to be 4char + stor=require("Storage").getStats(); + if (v_count==4) { + v_str_hw="Fre "+process.memory().free; + //+"/"+process.memory().total; + v_count++; + } + else if (v_count==5) { + v_str_hw="Sto "+stor.freeBytes; + v_count++; + } else if (v_count==6) { + v_str_hw="Tra "+stor.trashBytes; + v_count++; + } else if (v_count==7) { + v_str_hw="Fil "+stor.fileCount; + v_count=0; + } + // 4 char are prefix + if (v_str_hw.length>7) { + //replace 3 digits by k + v_str_hw=v_str_hw.substr(0,v_str_hw.length-3)+"k"; + } + } //end else storage + g.reset().setColor(v_color).setFont("6x8",v_bfont_size).setFontAlign(-1, 0); + //clean a longer previous string, care with br widgets + g.drawString(" ", this.x, this.y+11, true); + g.drawString(v_str_hw, this.x, this.y+11, true); + } //end draw + +WIDGETS["wdhwbttm"]={area:"bl",width:100,draw:draw}; +if (Bangle.isLCDOn) intervalRef = setInterval(()=>WIDGETS["wdhwbttm"].draw(), 10*1000); +})() diff --git a/apps/widlock/ChangeLog b/apps/widlock/ChangeLog index 665750150..abceba48e 100644 --- a/apps/widlock/ChangeLog +++ b/apps/widlock/ChangeLog @@ -5,3 +5,4 @@ 0.05: Set sortorder to -10 so that others can take -1 etc 0.06: Set sortorder to -10 in widget code 0.07: Remove check for .isLocked (extremely old firmwares), speed up widget loading +0.08: Don't completely remove the lock widget when screen unlocked (use 1px) to ensure appRect/drawWidgets still thinks there are widgets diff --git a/apps/widlock/metadata.json b/apps/widlock/metadata.json index db532851a..509a5b7a5 100644 --- a/apps/widlock/metadata.json +++ b/apps/widlock/metadata.json @@ -1,7 +1,7 @@ { "id": "widlock", "name": "Lock Widget", - "version": "0.07", + "version": "0.08", "description": "On devices with always-on display (Bangle.js 2) this displays lock icon whenever the display is locked", "icon": "widget.png", "type": "widget", diff --git a/apps/widlock/widget.js b/apps/widlock/widget.js index abcedde20..b5787e09d 100644 --- a/apps/widlock/widget.js +++ b/apps/widlock/widget.js @@ -1,8 +1,8 @@ Bangle.on("lock", function() { - WIDGETS["lock"].width = Bangle.isLocked()?16:0; + WIDGETS["lock"].width = Bangle.isLocked()?16:1; Bangle.drawWidgets(); }); -WIDGETS["lock"]={area:"tl",sortorder:10,width:Bangle.isLocked()?16:0,draw:function(w) { +WIDGETS["lock"]={area:"tl",sortorder:10,width:Bangle.isLocked()?16:1,draw:function(w) { if (Bangle.isLocked()) g.reset().drawImage(atob("DhABH+D/wwMMDDAwwMf/v//4f+H/h/8//P/z///f/g=="), w.x+1, w.y+4); }}; diff --git a/apps/widmessages/ChangeLog b/apps/widmessages/ChangeLog index 598068920..348d49528 100644 --- a/apps/widmessages/ChangeLog +++ b/apps/widmessages/ChangeLog @@ -1,3 +1,5 @@ 0.01: Moved messages widget into standalone widget app 0.02: Fix 'srcs' being defined in global scope Remove library stub +0.03: Fix messages not showing if UI auto-open is disabled +0.04: Now shows message icons again (#2416) diff --git a/apps/widmessages/metadata.json b/apps/widmessages/metadata.json index a8a23df19..0e399f71f 100644 --- a/apps/widmessages/metadata.json +++ b/apps/widmessages/metadata.json @@ -1,7 +1,7 @@ { "id": "widmessages", "name": "Message Widget", - "version": "0.02", + "version": "0.04", "description": "Widget showing new messages", "icon": "app.png", "type": "widget", diff --git a/apps/widmessages/widget.js b/apps/widmessages/widget.js index 316957e29..44f525ec8 100644 --- a/apps/widmessages/widget.js +++ b/apps/widmessages/widget.js @@ -22,10 +22,9 @@ let settings = Object.assign({flash: true, maxMessages: 3}, require("Storage").readJSON("messages.settings.json", true) || {}); if (recall!==true || settings.flash) { const msgsShown = E.clip(this.srcs.length, 0, settings.maxMessages); - srcs = Object.keys(this.srcs); g.reset().clearRect(this.x, this.y, this.x+this.width, this.y+23); for(let i = 0; i{ function getWidth() { - return E.getBattery() <= 30 ? 40 : 0; + return E.getBattery() <= 30 || Bangle.isCharging() ? 40 : 0; } WIDGETS.minbat={area:"tr",width:getWidth(),draw:function() { if(this.width < 40) return; @@ -11,7 +11,7 @@ g.clearRect(x,y,x+s,y+24); g.setColor(g.theme.fg).fillRect(x,y+2,x+s-4,y+21).clearRect(x+2,y+4,x+s-6,y+19).fillRect(x+s-3,y+10,x+s,y+14); var barWidth = bat*(s-12)/100; - var color = bat < 15 ? "#f00" : "#f80"; + var color = bat < 15 ? "#f00" : (bat <= 30 ? "#f80" : "#0f0"); g.setColor(color).fillRect(x+4,y+6,x+4+barWidth,y+17); },update: function() { var newWidth = getWidth(); @@ -26,4 +26,5 @@ var widget = WIDGETS.minbat; if(widget) {widget.update();} }, 10*60*1000); + Bangle.on('charging', () => WIDGETS.minbat.update()); })(); diff --git a/apps/widviz/ChangeLog b/apps/widviz/ChangeLog index 9785f4d84..5df89e719 100644 --- a/apps/widviz/ChangeLog +++ b/apps/widviz/ChangeLog @@ -1,3 +1,4 @@ 0.01: New Widget 0.02: swipe left,right update 0.03: Fix widget visibility code to the top bar isn't cleared by drawWidgets +0.04: Use widget_utils. diff --git a/apps/widviz/metadata.json b/apps/widviz/metadata.json index ba9cf793b..4d88c4d5f 100644 --- a/apps/widviz/metadata.json +++ b/apps/widviz/metadata.json @@ -2,7 +2,7 @@ "id": "widviz", "name": "Widget Visibility Widget", "shortName": "Viz Widget", - "version": "0.03", + "version": "0.04", "description": "Swipe left to hide top bar widgets, swipe right to redisplay.", "icon": "eye.png", "type": "widget", diff --git a/apps/widviz/widget.js b/apps/widviz/widget.js index 1490cf11a..7b9f55053 100644 --- a/apps/widviz/widget.js +++ b/apps/widviz/widget.js @@ -1,28 +1,20 @@ (() => { + let widget_utils = require('widget_utils'); - var saved = null; + var saved = false; function hide(){ if (!Bangle.isLCDOn() || saved) return; - saved = []; - for (var wd of WIDGETS) { - saved.push({d:wd.draw,a:wd.area}); - wd.draw=()=>{}; - wd.area=""; - } + saved = true; + widget_utils.hide(); g.setColor(0,0,0); g.fillRect(0,0,g.getWidth(),23); } function reveal(){ if (!Bangle.isLCDOn() || !saved) return; - for (var wd of WIDGETS) { - var o = saved.shift(); - wd.draw = o.d; - wd.area = o.a; - } - Bangle.drawWidgets(); - saved=null; + widget_utils.show(); + saved = false; } function draw(){ diff --git a/apps/widviztime/ChangeLog b/apps/widviztime/ChangeLog index 287061d0c..be95ed81c 100644 --- a/apps/widviztime/ChangeLog +++ b/apps/widviztime/ChangeLog @@ -1 +1,2 @@ 0.01: New Widget, forked from widviz +0.02: Use widget_utils. diff --git a/apps/widviztime/metadata.json b/apps/widviztime/metadata.json index b364bbd74..669c8fc12 100644 --- a/apps/widviztime/metadata.json +++ b/apps/widviztime/metadata.json @@ -2,7 +2,7 @@ "id": "widviztime", "name": "Widget Autohide Widget", "shortName": "Viz Time Widget", - "version": "0.01", + "version": "0.02", "description": "The widgets will be shown for four seconds after the device is unlocked.", "icon": "eye.png", "type": "widget", diff --git a/apps/widviztime/widget.js b/apps/widviztime/widget.js index 5e81af611..8422a3842 100644 --- a/apps/widviztime/widget.js +++ b/apps/widviztime/widget.js @@ -1,32 +1,21 @@ (() => { + let widget_utils = require('widget_utils'); - var saved = null; + var saved = false; function hide() { if (!Bangle.isLCDOn() || saved) return; - saved = []; - for (var wd of WIDGETS) { - saved.push({ - d: wd.draw, - a: wd.area - }); - wd.draw = () => {}; - wd.area = ""; - } + saved = true; + widget_utils.hide(); g.setColor(0, 0, 0); g.fillRect(0, 0, g.getWidth(), 23); } function reveal() { if (!Bangle.isLCDOn() || !saved) return; - for (var wd of WIDGETS) { - var o = saved.shift(); - wd.draw = o.d; - wd.area = o.a; - } - Bangle.drawWidgets(); - saved = null; + widget_utils.show(); + saved = false; } function draw() { diff --git a/bin/apploader.js b/bin/apploader.js index 26c4c1f09..d4a5f828e 100755 --- a/bin/apploader.js +++ b/bin/apploader.js @@ -14,7 +14,6 @@ for Noble. var SETTINGS = { pretokenise : true }; -var APPSDIR = __dirname+"/../apps/"; var noble; ["@abandonware/noble", "noble"].forEach(module => { if (!noble) try { @@ -37,36 +36,18 @@ function ERROR(msg) { process.exit(1); } -//eval(require("fs").readFileSync(__dirname+"../core/js/utils.js")); -var AppInfo = require("../core/js/appinfo.js"); -global.Const = { - /* Are we only putting a single app on a device? If so - apps should all be saved as .bootcde and we write info - about the current app into app.info */ - SINGLE_APP_ONLY : false, -}; var deviceId = "BANGLEJS2"; -var apps = []; -var dirs = require("fs").readdirSync(APPSDIR, {withFileTypes: true}); -dirs.forEach(dir => { - var appsFile; - if (dir.name.startsWith("_example") || !dir.isDirectory()) - return; - try { - appsFile = require("fs").readFileSync(APPSDIR+dir.name+"/metadata.json").toString(); - } catch (e) { - ERROR(dir.name+"/metadata.json does not exist"); - return; - } - apps.push(JSON.parse(appsFile)); -}); +var apploader = require("./lib/apploader.js"); var args = process.argv; var bangleParam = args.findIndex(arg => /-b\d/.test(arg)); if (bangleParam!==-1) { deviceId = "BANGLEJS"+args.splice(bangleParam, 1)[0][2]; } +apploader.init({ + DEVICEID : deviceId +}); if (args.length==3 && args[2]=="list") cmdListApps(); else if (args.length==3 && args[2]=="devices") cmdListDevices(); else if (args.length==4 && args[2]=="install") cmdInstallApp(args[3]); @@ -90,7 +71,7 @@ process.exit(0); } function cmdListApps() { - console.log(apps.map(a=>a.id).join("\n")); + console.log(apploader.apps.map(a=>a.id).join("\n")); } function cmdListDevices() { var foundDevices = []; @@ -113,19 +94,10 @@ function cmdListDevices() { } function cmdInstallApp(appId, deviceAddress) { - var app = apps.find(a=>a.id==appId); + var app = apploader.apps.find(a=>a.id==appId); if (!app) ERROR(`App ${JSON.stringify(appId)} not found`); if (app.custom) ERROR(`App ${JSON.stringify(appId)} requires HTML customisation`); - return AppInfo.getFiles(app, { - fileGetter:function(url) { - console.log(__dirname+"/"+url); - return Promise.resolve(require("fs").readFileSync(__dirname+"/../"+url).toString("binary")); - }, - settings : SETTINGS, - device : { id : deviceId } - }).then(files => { - //console.log(files); - var command = files.map(f=>f.cmd).join("\n")+"\n"; + return apploader.getAppFilesString(app).then(command => { bangleSend(command, deviceAddress).then(() => process.exit(0)); }); } diff --git a/bin/firmwaremaker.js b/bin/firmwaremaker.js index 1dc5ec073..4535c4a5e 100755 --- a/bin/firmwaremaker.js +++ b/bin/firmwaremaker.js @@ -1,17 +1,12 @@ -#!/usr/bin/env nodejs +#!/usr/bin/env node /* Mashes together a bunch of different apps to make a single firmware JS file which can be uploaded. */ -var SETTINGS = { - pretokenise : true -}; - var path = require('path'); var ROOTDIR = path.join(__dirname, '..'); -var APPDIR = ROOTDIR+'/apps'; var OUTFILE = ROOTDIR+'/firmware.js'; -var DEVICE = "BANGLEJS"; +var DEVICEID = "BANGLEJS"; var APPS = [ // IDs of apps to install "boot","launch","mclock","setting", "about","alarm","widbat","widbt","welcome" @@ -19,53 +14,17 @@ var APPS = [ // IDs of apps to install var MINIFY = true; var fs = require("fs"); -global.Const = { - /* Are we only putting a single app on a device? If so - apps should all be saved as .bootcde and we write info - about the current app into app.info */ - SINGLE_APP_ONLY : false, -}; +var apploader = require("./lib/apploader.js"); +apploader.init({ + DEVICEID : DEVICEID +}); -var AppInfo = require(ROOTDIR+"/core/js/appinfo.js"); var appfiles = []; -function fileGetter(url) { - console.log("Loading "+url) - if (MINIFY) { - /*if (url.endsWith(".js")) { - var f = url.slice(0,-3); - console.log("MINIFYING "+f); - const execSync = require('child_process').execSync; - // --config PRETOKENISE=true - // --minify - code = execSync(`espruino --config SET_TIME_ON_WRITE=false --minify --board BANGLEJS ${f}.js -o ${f}.min.js`); - console.log(code.toString()); - url = f+".min.js"; - }*/ - if (url.endsWith(".json")) { - var f = url.slice(0,-5); - console.log("MINIFYING JSON "+f); - var j = eval("("+fs.readFileSync(url).toString("binary")+")"); - var code = JSON.stringify(j); - //console.log(code); - url = f+".min.json"; - fs.writeFileSync(url, code); - } - } - return Promise.resolve(fs.readFileSync(url).toString("binary")); -} - Promise.all(APPS.map(appid => { - try { - var app = JSON.parse(fs.readFileSync(APPDIR + "/" + appid + "/metadata.json").toString()); - } catch (e) { - throw new Error(`App ${appid} not found`); - } - return AppInfo.getFiles(app, { - fileGetter : fileGetter, - settings : SETTINGS, - device : { id : DEVICE } - }).then(files => { + var app = apploader.apps.find(a => a.id==appid); + if (!app) throw new Error(`App ${appid} not found`); + return apploader.getAppFiles(app).then(files => { appfiles = appfiles.concat(files); }); })).then(() => { diff --git a/bin/firmwaremaker_c.js b/bin/firmwaremaker_c.js index 08fc6fe83..54b63d5d9 100755 --- a/bin/firmwaremaker_c.js +++ b/bin/firmwaremaker_c.js @@ -7,25 +7,20 @@ to populate Storage initially. Bangle.js 1 doesn't really have anough flash space for this, but we have enough on v2. */ -var SETTINGS = { - pretokenise : true -}; - -var DEVICE = process.argv[2]; +var DEVICEID = process.argv[2]; var path = require('path'); +var fs = require("fs"); var ROOTDIR = path.join(__dirname, '..'); -var APPDIR = ROOTDIR+'/apps'; -var MINIFY = true; var OUTFILE, APPS; -if (DEVICE=="BANGLEJS") { +if (DEVICEID=="BANGLEJS") { var OUTFILE = path.join(ROOTDIR, '../Espruino/libs/banglejs/banglejs1_storage_default.c'); var APPS = [ // IDs of apps to install "boot","launch","mclock","setting", "about","alarm","sched","widbat","widbt","welcome" ]; -} else if (DEVICE=="BANGLEJS2") { +} else if (DEVICEID=="BANGLEJS2") { var OUTFILE = path.join(ROOTDIR, '../Espruino/libs/banglejs/banglejs2_storage_default.c'); var APPS = [ // IDs of apps to install "boot","launch","antonclk","setting", @@ -37,16 +32,12 @@ if (DEVICE=="BANGLEJS") { console.log(" bin/firmwaremaker_c.js BANGLEJS2"); process.exit(1); } -console.log("Device = ",DEVICE); +console.log("Device = ",DEVICEID); - -var fs = require("fs"); -global.Const = { - /* Are we only putting a single app on a device? If so - apps should all be saved as .bootcde and we write info - about the current app into app.info */ - SINGLE_APP_ONLY : false, -}; +var apploader = require("./lib/apploader.js"); +apploader.init({ + DEVICEID : DEVICEID +}); function atob(input) { @@ -84,31 +75,8 @@ function atob(input) { return new Uint8Array(output); } -var AppInfo = require(ROOTDIR+"/core/js/appinfo.js"); var appfiles = []; -function fileGetter(url) { - console.log("Loading "+url) - if (MINIFY) { - if (url.endsWith(".json")) { - var f = url.slice(0,-5); - console.log("MINIFYING JSON "+f); - var j = eval("("+fs.readFileSync(url).toString("binary")+")"); - var code = JSON.stringify(j); - //console.log(code); - url = f+".min.json"; - fs.writeFileSync(url, code); - } - } - var blob = fs.readFileSync(url); - var data; - if (url.endsWith(".js") || url.endsWith(".json")) - data = blob.toString(); // allow JS/etc to be written in UTF-8 - else - data = blob.toString("binary") - return Promise.resolve(data); -} - // If file should be evaluated, try and do it... function evaluateFile(file) { var hsStart = 'require("heatshrink").decompress(atob("'; @@ -132,16 +100,9 @@ function evaluateFile(file) { } Promise.all(APPS.map(appid => { - try { - var app = JSON.parse(fs.readFileSync(APPDIR + "/" + appid + "/metadata.json").toString()); - } catch (e) { - throw new Error(`App ${appid} not found`); - } - return AppInfo.getFiles(app, { - fileGetter : fileGetter, - settings : SETTINGS, - device : { id : DEVICE } - }).then(files => { + var app = apploader.apps.find(a => a.id==appid); + if (!app) throw new Error(`App ${appid} not found`); + return apploader.getAppFiles(app).then(files => { appfiles = appfiles.concat(files); }); })).then(() => { diff --git a/bin/lib/apploader.js b/bin/lib/apploader.js new file mode 100644 index 000000000..329ece7b7 --- /dev/null +++ b/bin/lib/apploader.js @@ -0,0 +1,101 @@ +/* Node.js library with utilities to handle using the app loader from node.js */ + +var DEVICEID = "BANGLEJS2"; +var MINIFY = true; // minify JSON? +var BASE_DIR = __dirname + "/../.."; +var APPSDIR = BASE_DIR+"/apps/"; + +//eval(require("fs").readFileSync(__dirname+"../core/js/utils.js")); +var Espruino = require(__dirname + "/../../core/lib/espruinotools.js"); +//eval(require("fs").readFileSync(__dirname + "/../../core/lib/espruinotools.js").toString()); +//eval(require("fs").readFileSync(__dirname + "/../../core/js/utils.js").toString()); +var AppInfo = require(__dirname+"/../../core/js/appinfo.js"); + +var SETTINGS = { + pretokenise : true +}; +global.Const = { + /* Are we only putting a single app on a device? If so + apps should all be saved as .bootcde and we write info + about the current app into app.info */ + SINGLE_APP_ONLY : false, +}; + +var apps = []; +var device = { id : DEVICEID, appsInstalled : [] }; + +// call with {DEVICEID:"BANGLEJS/BANGLEJS2"} +exports.init = function(options) { + if (options.DEVICEID) + DEVICEID = options.DEVICEID; + // Load app metadata + var dirs = require("fs").readdirSync(APPSDIR, {withFileTypes: true}); + dirs.forEach(dir => { + var appsFile; + if (dir.name.startsWith("_example") || !dir.isDirectory()) + return; + try { + appsFile = require("fs").readFileSync(APPSDIR+dir.name+"/metadata.json").toString(); + } catch (e) { + ERROR(dir.name+"/metadata.json does not exist"); + return; + } + apps.push(JSON.parse(appsFile)); + }); +}; + +exports.AppInfo = AppInfo; +exports.apps = apps; + +// used by getAppFiles +function fileGetter(url) { + url = BASE_DIR+"/"+url; + console.log("Loading "+url) + var data; + if (MINIFY && url.endsWith(".json")) { + var f = url.slice(0,-5); + console.log("MINIFYING JSON "+f); + var j = eval("("+require("fs").readFileSync(url).toString("binary")+")"); + data = JSON.stringify(j); + } else { + var blob = require("fs").readFileSync(url); + if (url.endsWith(".js") || url.endsWith(".json")) + data = blob.toString(); // allow JS/etc to be written in UTF-8 + else + data = blob.toString("binary") + } + return Promise.resolve(data); +} + +exports.getAppFiles = function(app) { + var allFiles = []; + var uploadOptions = { + apps : apps, + needsApp : app => { + if (app.provides_modules) { + if (!app.files) app.files=""; + app.files = app.files.split(",").concat(app.provides_modules).join(","); + } + return AppInfo.getFiles(app, { + fileGetter:fileGetter, + settings : SETTINGS, + device : { id : DEVICEID } + }).then(files => { allFiles = allFiles.concat(files); return app; }); + } + }; + return AppInfo.checkDependencies(app, device, uploadOptions).then(() => AppInfo.getFiles(app, { + fileGetter:fileGetter, + settings : SETTINGS, + device : device + })).then(files => { + allFiles = allFiles.concat(files); + return allFiles; + }); +}; + +// Get all the files for this app as a string of Storage.write commands +exports.getAppFilesString = function(app) { + return exports.getAppFiles(app).then(files => { + return files.map(f=>f.cmd).join("\n")+"\n" + }) +}; diff --git a/bin/lib/emulator.js b/bin/lib/emulator.js new file mode 100644 index 000000000..f7c82ec3c --- /dev/null +++ b/bin/lib/emulator.js @@ -0,0 +1,115 @@ +/* Node.js library with utilities to handle using the emulator from node.js */ + +var EMULATOR = "banglejs2"; +var DEVICEID = "BANGLEJS2"; + +var BASE_DIR = __dirname + "/../.."; +var DIR_IDE = BASE_DIR + "/../EspruinoWebIDE"; + +/* we factory reset ONCE, get this, then we can use it to reset +state quickly for each new app */ +var factoryFlashMemory; + +// Log of messages from app +var appLog = ""; +var lastOutputLine = ""; + +function onConsoleOutput(txt) { + appLog += txt + "\n"; + lastOutputLine = txt; +} + +exports.init = function(options) { + if (options.EMULATOR) + EMULATOR = options.EMULATOR; + if (options.DEVICEID) + DEVICEID = options.DEVICEID; + + eval(require("fs").readFileSync(DIR_IDE + "/emu/emulator_"+EMULATOR+".js").toString()); + eval(require("fs").readFileSync(DIR_IDE + "/emu/emu_"+EMULATOR+".js").toString()); + eval(require("fs").readFileSync(DIR_IDE + "/emu/common.js").toString()/*.replace('console.log("EMSCRIPTEN:"', '//console.log("EMSCRIPTEN:"')*/); + + jsRXCallback = function() {}; + jsUpdateGfx = function() {}; + + factoryFlashMemory = new Uint8Array(FLASH_SIZE); + factoryFlashMemory.fill(255); + + exports.flashMemory = flashMemory; + exports.GFX_WIDTH = GFX_WIDTH; + exports.GFX_HEIGHT = GFX_HEIGHT; + exports.tx = jsTransmitString; + exports.idle = jsIdle; + exports.stopIdle = jsStopIdle; + exports.getGfxContents = jsGetGfxContents; + + return new Promise(resolve => { + setTimeout(function() { + console.log("Emulator Loaded..."); + jsInit(); + jsIdle(); + console.log("Emulator Factory reset"); + exports.tx("Bangle.factoryReset()\n"); + factoryFlashMemory.set(flashMemory); + console.log("Emulator Ready!"); + + resolve(); + },0); + }); +}; + +// Factory reset +exports.factoryReset = function() { + exports.flashMemory.set(factoryFlashMemory); + exports.tx("reset()\n"); + appLog=""; +}; + +// Transmit a string +exports.tx = function() {}; // placeholder +exports.idle = function() {}; // placeholder +exports.stopIdle = function() {}; // placeholder +exports.getGfxContents = function() {}; // placeholder + +exports.flashMemory = undefined; // placeholder +exports.GFX_WIDTH = undefined; // placeholder +exports.GFX_HEIGHT = undefined; // placeholder + +// Get last line sent to console +exports.getLastLine = function() { + return lastOutputLine; +}; + +// Gets the screenshot as RGBA Uint32Array +exports.getScreenshot = function() { + var rgba = new Uint8Array(exports.GFX_WIDTH*exports.GFX_HEIGHT*4); + exports.getGfxContents(rgba); + var rgba32 = new Uint32Array(rgba.buffer); + return rgba32; +} + +// Write the screenshot to a file options={errorIfBlank} +exports.writeScreenshot = function(imageFn, options) { + options = options||{}; + return new Promise((resolve,reject) => { + var rgba32 = exports.getScreenshot(); + + if (options.errorIfBlank) { + var firstPixel = rgba32[0]; + var blankImage = rgba32.every(col=>col==firstPixel); + if (blankImage) reject("Image is blank"); + } + + var Jimp = require("jimp"); + let image = new Jimp(exports.GFX_WIDTH, exports.GFX_HEIGHT, function (err, image) { + if (err) throw err; + let buffer = image.bitmap.data; + buffer.set(new Uint8Array(rgba32.buffer)); + image.write(imageFn, (err) => { + if (err) return reject(err); + console.log("Image written as "+imageFn); + resolve(); + }); + }); + }); +} diff --git a/bin/runapptests.js b/bin/runapptests.js new file mode 100755 index 000000000..b50a7e15c --- /dev/null +++ b/bin/runapptests.js @@ -0,0 +1,175 @@ +#!/usr/bin/node +/* + +This allows us to test apps using the Bangle.js emulator + +IT IS UNFINISHED + +It searches for `test.json` in each app's directory and will +run them in sequence. + +TODO: + +* more code to test with +* run tests that we have found and loaded (currently we just use TEST) +* documentation +* actual tests +* detecting 'Uncaught Error' +* logging of success/fail +* ... + +*/ + +// A simpletest +/*var TEST = { + app : "android", + tests : [ { + steps : [ + {t:"load", fn:"messagesgui.app.js"}, + {t:"gb", "obj":{"t":"notify","id":1234,"src":"Twitter","title":"A Name","body":"message contents"}}, + {t:"cmd", "js":"X='hello';"}, + {t:"eval", "js":"X", eq:"hello"} + ] + }] +};*/ +var TEST = { + app : "antonclk", + tests : [ { + steps : [ + {t:"cmd", "js": "Bangle.loadWidgets()"}, + {t:"cmd", "js": "eval(require('Storage').read('antonclk.app.js'))"}, + {t:"cmd", "js":"Bangle.setUI()"}, // load and free + {t:"saveMemoryUsage"}, + {t:"cmd", "js": "eval(require('Storage').read('antonclk.app.js'))"}, + {t:"cmd", "js":"Bangle.setUI()"}, // load and free + {t:"checkMemoryUsage"}, // check memory usage is the same + ] + }] +}; + +var EMULATOR = "banglejs2"; +var DEVICEID = "BANGLEJS2"; + +var BASE_DIR = __dirname + "/.."; +var APP_DIR = BASE_DIR + "/apps"; +var DIR_IDE = BASE_DIR + "/../EspruinoWebIDE"; + + +if (!require("fs").existsSync(DIR_IDE)) { + console.log("You need to:"); + console.log(" git clone https://github.com/espruino/EspruinoWebIDE"); + console.log("At the same level as this project"); + process.exit(1); +} + +var apploader = require(BASE_DIR+"/bin/lib/apploader.js"); +apploader.init({ + DEVICEID : DEVICEID +}); +var emu = require(BASE_DIR+"/bin/lib/emulator.js"); + +// Last set of text received +var lastTxt; + +function ERROR(s) { + console.error(s); + process.exit(1); +} + +function runTest(test) { + var app = apploader.apps.find(a=>a.id==test.app); + if (!app) ERROR(`App ${JSON.stringify(test.app)} not found`); + if (app.custom) ERROR(`App ${JSON.stringify(appId)} requires HTML customisation`); + return apploader.getAppFilesString(app).then(command => { + // What about dependencies?? + test.tests.forEach((subtest,subtestIdx) => { + console.log(`==============================`); + console.log(`"${test.app}" Test ${subtestIdx}`); + console.log(`==============================`); + emu.factoryReset(); + console.log("> Sending app "+test.app); + emu.tx(command); + console.log("> Sent app"); + emu.tx("reset()\n"); + console.log("> Reset."); + var ok = true; + subtest.steps.forEach(step => { + if (ok) switch(step.t) { + case "load" : + console.log(`> Loading file "${step.fn}"`); + emu.tx(`load(${JSON.stringify(step.fn)})\n`); + break; + case "cmd" : + console.log(`> Sending JS "${step.js}"`); + emu.tx(`${step.js}\n`); + break; + case "gb" : emu.tx(`GB(${JSON.stringify(step.obj)})\n`); break; + case "tap" : emu.tx(`Bangle.emit(...)\n`); break; + case "eval" : + console.log(`> Evaluate "${step.js}"`); + emu.tx(`\x10print(JSON.stringify(${step.js}))\n`); + var result = emu.getLastLine(); + var expected = JSON.stringify(step.eq); + console.log("> GOT "+result); + if (result!=expected) { + console.log("> FAIL: EXPECTED "+expected); + ok = false; + } + break; + // tap/touch/drag/button press + // delay X milliseconds? + case "screenshot" : + console.log(`> Compare screenshots - UNIMPLEMENTED`); + break; + case "saveMemoryUsage" : + emu.tx(`\x10print(process.memory().usage)\n`); + subtest.memUsage = parseInt( emu.getLastLine()); + console.log("> CURRENT MEMORY USAGE", subtest.memUsage); + break; + case "checkMemoryUsage" : + emu.tx(`\x10print(process.memory().usage)\n`); + var memUsage = emu.getLastLine(); + console.log("> CURRENT MEMORY USAGE", memUsage); + if (subtest.memUsage != memUsage ) { + console.log("> FAIL: EXPECTED MEMORY USAGE OF "+subtest.memUsage); + ok = false; + } + break; + default: ERROR("Unknown step type "+step.t); + } + emu.idle(); + }); + }); + emu.stopIdle(); + }); +} + + +emu.init({ + EMULATOR : EMULATOR, + DEVICEID : DEVICEID +}).then(function() { + // Emulator is now loaded + console.log("Loading tests"); + var tests = []; + apploader.apps.forEach(app => { + var testFile = APP_DIR+"/"+app.id+"/test.json"; + if (!require("fs").existsSync(testFile)) return; + var test = JSON.parse(require("fs").readFileSync(testFile).toString()); + test.app = app.id; + tests.push(test); + }); + // Running tests + runTest(TEST); +}); +/* + if (erroredApps.length) { + erroredApps.forEach(app => { + console.log(`::error file=${app.id}::${app.id}`); + console.log("::group::Log"); + app.log.split("\n").forEach(line => console.log(`\u001b[38;2;255;0;0m${line}`)); + console.log("::endgroup::"); + }); + process.exit(1); + } +*/ diff --git a/bin/sanitycheck.js b/bin/sanitycheck.js old mode 100644 new mode 100755 index 838f99895..82b2896b8 --- a/bin/sanitycheck.js +++ b/bin/sanitycheck.js @@ -94,6 +94,7 @@ const INTERNAL_FILES_IN_APP_TYPE = { // list of app types and files they SHOULD var KNOWN_WARNINGS = [ "App gpsrec data file wildcard .gpsrc? does not include app ID", "App owmweather data file weather.json is also listed as data file for app weather", + "App messagegui storage file messagegui is also listed as storage file for app messagelist", ]; function globToRegex(pattern) { diff --git a/bin/thumbnailer.js b/bin/thumbnailer.js index 0895098e9..22cdc27a5 100755 --- a/bin/thumbnailer.js +++ b/bin/thumbnailer.js @@ -6,6 +6,10 @@ var DEVICEID = "BANGLEJS2"; */ var EMULATOR = "banglejs1"; var DEVICEID = "BANGLEJS"; +var SCREENSHOT_DIR = __dirname+"/../screenshots/"; + +var emu = require("./lib/emulator.js"); +var apploader = require("./lib/apploader.js"); var singleAppId; @@ -20,130 +24,66 @@ if (process.argv.length!=3 && process.argv.length!=2) { if (process.argv.length==3) singleAppId = process.argv[2]; -if (!require("fs").existsSync(__dirname + "/../../EspruinoWebIDE")) { - console.log("You need to:"); - console.log(" git clone https://github.com/espruino/EspruinoWebIDE"); - console.log("At the same level as this project"); - process.exit(1); -} - -eval(require("fs").readFileSync(__dirname + "/../../EspruinoWebIDE/emu/emulator_"+EMULATOR+".js").toString()); -eval(require("fs").readFileSync(__dirname + "/../../EspruinoWebIDE/emu/emu_"+EMULATOR+".js").toString()); -eval(require("fs").readFileSync(__dirname + "/../../EspruinoWebIDE/emu/common.js").toString()); - -var SETTINGS = { - pretokenise : true -}; -var Const = { -}; -module = undefined; -var Espruino = require(__dirname + "/../core/lib/espruinotools.js"); -//eval(require("fs").readFileSync(__dirname + "/../core/lib/espruinotools.js").toString()); -eval(require("fs").readFileSync(__dirname + "/../core/js/utils.js").toString()); -eval(require("fs").readFileSync(__dirname + "/../core/js/appinfo.js").toString()); -var apps = JSON.parse(require("fs").readFileSync(__dirname+"/../apps.json")); - -/* we factory reset ONCE, get this, then we can use it to reset -state quickly for each new app */ -var factoryFlashMemory = new Uint8Array(FLASH_SIZE); -// Log of messages from app -var appLog = ""; // List of apps that errored var erroredApps = []; -jsRXCallback = function() {}; -jsUpdateGfx = function() {}; - function ERROR(s) { console.error(s); process.exit(1); } -function onConsoleOutput(txt) { - appLog += txt + "\n"; -} - function getThumbnail(appId, imageFn) { console.log("Thumbnail for "+appId); - var app = apps.find(a=>a.id==appId); + var app = apploader.apps.find(a=>a.id==appId); if (!app) ERROR(`App ${JSON.stringify(appId)} not found`); if (app.custom) ERROR(`App ${JSON.stringify(appId)} requires HTML customisation`); - return new Promise(resolve => { - AppInfo.getFiles(app, { - fileGetter:function(url) { - console.log(__dirname+"/"+url); - return Promise.resolve(require("fs").readFileSync(__dirname+"/../"+url).toString("binary")); - }, - settings : SETTINGS, - device : { id : DEVICEID } - }).then(files => { - console.log(`AppInfo returned for ${appId}`);//, files); - flashMemory.set(factoryFlashMemory); - jsTransmitString("reset()\n"); - console.log("Uploading..."); - jsTransmitString("g.clear()\n"); - var command = files.map(f=>f.cmd).join("\n")+"\n"; - command += `load("${appId}.app.js")\n`; - appLog = ""; - jsTransmitString(command); - console.log("Done."); - jsTransmitString("Bangle.setLCDMode();clearInterval();clearTimeout();\n"); - jsStopIdle(); - - var rgba = new Uint8Array(GFX_WIDTH*GFX_HEIGHT*4); - jsGetGfxContents(rgba); - var rgba32 = new Uint32Array(rgba.buffer); - var firstPixel = rgba32[0]; - var blankImage = rgba32.every(col=>col==firstPixel) - - if (appLog.replace("Uncaught Storage Updated!", "").indexOf("Uncaught")>=0) - erroredApps.push( { id : app.id, log : appLog } ); - - if (!blankImage) { - var Jimp = require("jimp"); - let image = new Jimp(GFX_WIDTH, GFX_HEIGHT, function (err, image) { - if (err) throw err; - let buffer = image.bitmap.data; - buffer.set(rgba); - image.write(imageFn, (err) => { - if (err) throw err; - console.log("Image written as "+imageFn); - resolve(true); - }); - }); - } else { - console.log("Image is empty"); - resolve(false); - } + return apploader.getAppFilesString(app).then(command => { + console.log(`AppInfo returned for ${appId}`);//, files); + emu.factoryReset(); + console.log("Uploading..."); + emu.tx("g.clear()\n"); + command += `load("${appId}.app.js")\n`; + appLog = ""; + emu.tx(command); + console.log("Done."); + emu.tx("Bangle.setLCDMode();clearInterval();clearTimeout();\n"); + emu.stopIdle(); + return emu.writeScreenshot(imageFn, { errorIfBlank : true }).then(() => console.log("X")).catch( err => { + console.log("Error", err); }); }); } var screenshots = []; +apploader.init({ + EMULATOR : EMULATOR, + DEVICEID : DEVICEID +}); // wait until loaded... -setTimeout(function() { - console.log("Loaded..."); - jsInit(); - jsIdle(); - console.log("Factory reset"); - jsTransmitString("Bangle.factoryReset()\n"); - factoryFlashMemory.set(flashMemory); - console.log("Ready!"); - +emu.init({ + EMULATOR : EMULATOR, + DEVICEID : DEVICEID +}).then(function() { if (singleAppId) { - getThumbnail(singleAppId, "screenshots/"+singleAppId+"-"+EMULATOR+".png"); + console.log("Single Screenshot"); + getThumbnail(singleAppId, SCREENSHOT_DIR+singleAppId+"-"+EMULATOR+".png"); return; } - var appList = apps.filter(app => (!app.type || app.type=="clock") && !app.custom); + console.log("Screenshot ALL"); + var appList = apploader.apps.filter(app => (!app.type || app.type=="clock") && !app.custom); appList = appList.filter(app => !app.screenshots && app.supports.includes(DEVICEID)); var promise = Promise.resolve(); appList.forEach(app => { + if (!app.supports.includes(DEVICEID)) { + console.log(`App ${app.id} isn't designed for ${DEVICEID}`); + return; + } promise = promise.then(() => { var imageFile = "screenshots/"+app.id+"-"+EMULATOR+".png"; return getThumbnail(app.id, imageFile).then(ok => { diff --git a/core b/core index 376824068..893c2dbbe 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 376824068d90986c245b46970fd80ccdca44e431 +Subproject commit 893c2dbbe5a93fbb80d035a695663b4f4cca8875 diff --git a/index.html b/index.html index cb00d87ab..cc69781ee 100644 --- a/index.html +++ b/index.html @@ -136,7 +136,8 @@ -

+ +

Settings

diff --git a/loader.js b/loader.js index c525fd963..a6e51192e 100644 --- a/loader.js +++ b/loader.js @@ -49,7 +49,7 @@ function onFoundDeviceInfo(deviceId, deviceVersion) { if (deviceId != "BANGLEJS" && deviceId != "BANGLEJS2") { showToast(`You're using ${deviceId}, not a Bangle.js. Did you want espruino.com/apps instead?` ,"warning", 20000); } else if (versionLess(deviceVersion, RECOMMENDED_VERSION)) { - showToast(`You're using an old Bangle.js firmware (${deviceVersion}) and ${RECOMMENDED_VERSION} is available (see changes). You can update ${fwExtraText}with the instructions here` ,"warning", 20000); + showToast(`You're using an old Bangle.js firmware (${deviceVersion}) and ${RECOMMENDED_VERSION} is available (see changes). You can update ${fwExtraText}with the instructions here` ,"warning", 20000); } // check against features shown? filterAppsForDevice(deviceId); diff --git a/modules/ClockFace.js b/modules/ClockFace.js index bf64d418a..10dfb9e43 100644 --- a/modules/ClockFace.js +++ b/modules/ClockFace.js @@ -41,17 +41,12 @@ function ClockFace(options) { this[k] = settings[k]; }); } - // these default to true - ["showDate", "loadWidgets"].forEach(k => { - if (this[k]===undefined) this[k] = true; - }); + // showDate defaults to true + if (this.showDate===undefined) this.showDate = true; + // if (old) setting was to not load widgets, default to hiding them + if (this.hideWidgets===undefined && this.loadWidgets===false) this.hideWidgets = 1; + let s = require("Storage").readJSON("setting.json",1)||{}; - if ((global.__FILE__===undefined || global.__FILE__===s.clock) - && s.clockHasWidgets!==this.loadWidgets) { - // save whether we can Fast Load - s.clockHasWidgets = this.loadWidgets; - require("Storage").writeJSON("setting.json", s); - } // use global 24/12-hour setting if not set by clock-settings if (!('is12Hour' in this)) this.is12Hour = !!(s["12hour"]); } @@ -92,7 +87,9 @@ ClockFace.prototype.start = function() { .CLOCK is set by Bangle.setUI('clock') but we want to load widgets so we can check appRect and *then* call setUI. see #1864 */ Bangle.CLOCK = 1; - if (this.loadWidgets) Bangle.loadWidgets(); + Bangle.loadWidgets(); + const widget_util = ["show", "hide", "swipeOn"][this.hideWidgets|0]; + require("widget_utils")[widget_util](); if (this.init) this.init.apply(this); const uiRemove = this._remove ? () => this.remove() : undefined; if (this._upDown) { @@ -133,6 +130,7 @@ ClockFace.prototype.resume = function() { }; ClockFace.prototype.remove = function() { this._removed = true; + require("widget_utils").show(); if (this._timeout) clearTimeout(this._timeout); Bangle.removeListener("lcdPower", this._onLcd); if (this._remove) this._remove.apply(this); diff --git a/modules/ClockFace.md b/modules/ClockFace.md index f123d38c0..36452cf85 100644 --- a/modules/ClockFace.md +++ b/modules/ClockFace.md @@ -140,7 +140,7 @@ For example: // now clock.showDate === false; clock.foo === 123; - clock.loadWidgets === true; // default when not in settings file + clock.hideWidgets === 0; // default when not in settings file clock.is12Hour === ??; // not in settings file: uses global setting clock.start(); @@ -152,13 +152,14 @@ The following properties are automatically set on the clock: * `is12Hour`: `true` if the "Time Format" setting is set to "12h", `false` for "24h". * `paused`: `true` while the clock is paused. (You don't need to check this inside your `draw()` code) * `showDate`: `true` (if not overridden through the settings file.) -* `loadWidgets`: `true` (if not overridden through the settings file.) - If set to `false` before calling `start()`, the clock won't call `Bangle.loadWidgets();` for you. - Best is to add a setting for this, but if you never want to load widgets, you could do this: +* `hideWidgets`: `0` (if not overridden through the settings file.) + If set to `1` before calling `start()`, the clock calls `require("widget_utils")hide();` for you. + (Bangle.js 2 only: `2` for swipe-down) + Best is to add a setting for this, but if you never want to show widgets, you could do this: ```js var ClockFace = require("ClockFace"); var clock = new ClockFace({draw: function(){/*...*/}}); - clock.loadWidgets = false; // prevent loading of widgets + clock.hideWidgets = 1; // hide widgets clock.start(); ``` @@ -200,7 +201,7 @@ let menu = { }; require("ClockFace_menu").addItems(menu, save, { showDate: settings.showDate, - loadWidgets: settings.loadWidgets, + hideWidgets: settings.hideWidgets, }); E.showMenu(menu); @@ -213,7 +214,7 @@ let menu = { /*LANG*/"< Back": back, }; require("ClockFace_menu").addSettingsFile(menu, ".settings.json", [ - "showDate", "loadWidgets", "powerSave", + "showDate", "hideWidgets", "powerSave", ]); E.showMenu(menu); diff --git a/modules/ClockFace_menu.js b/modules/ClockFace_menu.js index a1dd76fee..e78246f43 100644 --- a/modules/ClockFace_menu.js +++ b/modules/ClockFace_menu.js @@ -10,13 +10,12 @@ exports.addItems = function(menu, callback, items) { let value = items[key]; const label = { showDate:/*LANG*/"Show date", - loadWidgets:/*LANG*/"Load widgets", + hideWidgets:/*LANG*/"Widgets", powerSave:/*LANG*/"Power saving", }[key]; switch(key) { // boolean options which default to true case "showDate": - case "loadWidgets": if (value===undefined) value = true; // fall through case "powerSave": @@ -25,6 +24,17 @@ exports.addItems = function(menu, callback, items) { value: !!value, onchange: v => callback(key, v), }; + break; + + case "hideWidgets": + let options = [/*LANG*/"Show",/*LANG*/"Hide"]; + if (process.env.HWVERSION===2) options.push(/*LANG*/"Swipe"); + menu[label] = { + value: value|0, + min: 0, max: options.length-1, + format: v => options[v|0], + onchange: v => callback(key, v), + }; } }); }; @@ -39,6 +49,12 @@ exports.addItems = function(menu, callback, items) { exports.addSettingsFile = function(menu, settingsFile, items) { let s = require("Storage").readJSON(settingsFile, true) || {}; + // migrate "don't load widgets" to "hide widgets" + if (!("hideWidgets" in s) && ("loadWidgets" in s) && !s.loadWidgets) { + s.hideWidgets = 1; + } + delete s.loadWidgets; + function save(key, value) { s[key] = value; require("Storage").writeJSON(settingsFile, s); diff --git a/modules/Layout.js b/modules/Layout.js index f8e27b66b..4cf8752a3 100644 --- a/modules/Layout.js +++ b/modules/Layout.js @@ -267,7 +267,7 @@ Layout.prototype.layout = function (l) { }); } }; - cb[l.type](l); + if (cb[l.type]) cb[l.type](l); }; Layout.prototype.debug = function(l,c) { if (!l) l = this._l; diff --git a/modules/Layout.min.js b/modules/Layout.min.js index 19e60f7a0..959657228 100644 --- a/modules/Layout.min.js +++ b/modules/Layout.min.js @@ -1,14 +1,14 @@ -function p(d,h){function b(e){"ram";e.id&&(a[e.id]=e);e.type||(e.type="");e.c&&e.c.forEach(b)}this._l=this.l=d;this.options=h||{};this.lazy=this.options.lazy||!1;this.physBtns=1;let f;if(2!=process.env.HWVERSION){this.physBtns=3;f=[];function e(l){"ram";"btn"==l.type&&f.push(l);l.c&&l.c.forEach(e)}e(d);f.length&&(this.physBtns=0,this.buttons=f,this.selectedButton=-1)}if(this.options.btns)if(d=this.options.btns,this.physBtns>=d.length){this.b=d;let e=Math.floor(Bangle.appRect.h/this.physBtns); -for(2d.length;)d.push({label:""});this._l.width=g.getWidth()-8;this._l={type:"h",filly:1,c:[this._l,{type:"v",pad:1,filly:1,c:d.map(l=>(l.type="txt",l.font="6x8",l.height=e,l.r=1,l))}]}}else this._l.width=g.getWidth()-32,this._l={type:"h",c:[this._l,{type:"v",c:d.map(e=>(e.type="btn",e.filly=1,e.width=32,e.r=1,e))}]},f&&f.push.apply(f,this._l.c[1].c);this.setUI();var a=this;b(this._l);this.updateNeeded=!0}function t(d,h,b,f,a){var e= -null==d.bgCol?a:g.toColor(d.bgCol);if(e!=a||"txt"==d.type||"btn"==d.type||"img"==d.type||"custom"==d.type){var l=d.c;delete d.c;var k="H"+E.CRC32(E.toJS(d));l&&(d.c=l);delete h[k]||((f[k]=[d.x,d.y,d.x+d.w-1,d.y+d.h-1]).bg=null==a?g.theme.bg:a,b&&(b.push(d),b=null))}if(d.c)for(var c of d.c)t(c,h,b,f,e)}p.prototype.setUI=function(){Bangle.setUI();let d;this.buttons&&(Bangle.setUI({mode:"updown",back:this.options.back,remove:this.options.remove},h=>{var b=this.selectedButton,f=this.buttons.length;if(void 0=== -h&&this.buttons[b])return this.buttons[b].cb();this.buttons[b]&&(delete this.buttons[b].selected,this.render(this.buttons[b]));b=(b+f+h)%f;this.buttons[b]&&(this.buttons[b].selected=1,this.render(this.buttons[b]));this.selectedButton=b}),d=!0);!this.options.back&&!this.options.remove||d||Bangle.setUI({mode:"custom",back:this.options.back,remove:this.options.remove});if(this.b){function h(b,f){.75=b.x&&f.y>=b.y&&f.x<=b.x+b.w&&f.y<=b.y+b.h&&(2==f.type&&b.cbl?b.cbl(f):b.cb&&b.cb(f));b.c&&b.c.forEach(a=>h(a,f))}Bangle.touchHandler=(b,f)=>h(this._l,f);Bangle.on("touch", -Bangle.touchHandler)}};p.prototype.render=function(d){function h(c){"ram";b.reset();void 0!==c.col&&b.setColor(c.col);void 0!==c.bgCol&&b.setBgColor(c.bgCol).clearRect(c.x,c.y,c.x+c.w-1,c.y+c.h-1);f[c.type](c)}d||(d=this._l);this.updateNeeded&&this.update();var b=g,f={"":function(){},txt:function(c){"ram";if(c.wrap){var m=b.setFont(c.font).setFontAlign(0,-1).wrapString(c.label,c.w),n=c.y+(c.h-b.getFontHeight()*m.length>>1);b.drawString(m.join("\n"),c.x+(c.w>>1),n)}else b.setFont(c.font).setFontAlign(0, -0,c.r).drawString(c.label,c.x+(c.w>>1),c.y+(c.h>>1))},btn:function(c){"ram";var m=c.x+(0|c.pad),n=c.y+(0|c.pad),q=c.w-(c.pad<<1),r=c.h-(c.pad<<1);m=[m,n+4,m+4,n,m+q-5,n,m+q-1,n+4,m+q-1,n+r-5,m+q-5,n+r-1,m+4,n+r-1,m,n+r-5,m,n+4];n=c.selected?b.theme.bgH:b.theme.bg2;b.setColor(n).fillPoly(m).setColor(c.selected?b.theme.fgH:b.theme.fg2).drawPoly(m);void 0!==c.col&&b.setColor(c.col);c.src?b.setBgColor(n).drawImage("function"==typeof c.src?c.src():c.src,c.x+c.w/2,c.y+c.h/2,{scale:c.scale||void 0,rotate:.5* -Math.PI*(c.r||0)}):b.setFont(c.font||"6x8:2").setFontAlign(0,0,c.r).drawString(c.label,c.x+c.w/2,c.y+c.h/2)},img:function(c){"ram";b.drawImage("function"==typeof c.src?c.src():c.src,c.x+c.w/2,c.y+c.h/2,{scale:c.scale||void 0,rotate:.5*Math.PI*(c.r||0)})},custom:function(c){"ram";c.render(c)},h:function(c){"ram";c.c.forEach(h)},v:function(c){"ram";c.c.forEach(h)}};if(this.lazy){this.rects||(this.rects={});var a=this.rects.clone(),e=[];t(d,a,e,this.rects,null);for(var l in a)delete this.rects[l];d= -Object.keys(a).map(c=>a[c]).reverse();for(var k of d)b.setBgColor(k.bg).clearRect.apply(g,k);e.forEach(h)}else h(d)};p.prototype.forgetLazyState=function(){this.rects={}};p.prototype.layout=function(d){var h={h:function(b){"ram";var f=b.x+(0|b.pad),a=0,e=b.c&&b.c.reduce((k,c)=>k+(0|c.fillx),0);e||(f+=b.w-b._w>>1,e=1);var l=f;b.c.forEach(k=>{k.x=0|l;f+=k._w;a+=0|k.fillx;l=f+Math.floor(a*(b.w-b._w)/e);k.w=0|l-k.x;k.h=0|(k.filly?b.h-(b.pad<<1):k._h);k.y=0|b.y+(0|b.pad)+((1+(0|k.valign))*(b.h-(b.pad<< -1)-k.h)>>1);if(k.c)h[k.type](k)})},v:function(b){"ram";var f=b.y+(0|b.pad),a=0,e=b.c&&b.c.reduce((k,c)=>k+(0|c.filly),0);e||(f+=b.h-b._h>>1,e=1);var l=f;b.c.forEach(k=>{k.y=0|l;f+=k._h;a+=0|k.filly;l=f+Math.floor(a*(b.h-b._h)/e);k.h=0|l-k.y;k.w=0|(k.fillx?b.w-(b.pad<<1):k._w);k.x=0|b.x+(0|b.pad)+((1+(0|k.halign))*(b.w-(b.pad<<1)-k.w)>>1);if(k.c)h[k.type](k)})}};h[d.type](d)};p.prototype.debug=function(d,h){d||(d=this._l);h=h||1;g.setColor(h&1,h&2,h&4).drawRect(d.x+h-1,d.y+h-1,d.x+d.w-h,d.y+d.h-h); -d.pad&&g.drawRect(d.x+d.pad-1,d.y+d.pad-1,d.x+d.w-d.pad,d.y+d.h-d.pad);h++;d.c&&d.c.forEach(b=>this.debug(b,h))};p.prototype.update=function(){function d(a){"ram";b[a.type](a);if(a.r&1){var e=a._w;a._w=a._h;a._h=e}a._w=Math.max(a._w+(a.pad<<1),0|a.width);a._h=Math.max(a._h+(a.pad<<1),0|a.height)}delete this.updateNeeded;var h=g,b={txt:function(a){"ram";a.font.endsWith("%")&&(a.font="Vector"+Math.round(h.getHeight()*a.font.slice(0,-1)/100));if(a.wrap)a._h=a._w=0;else{var e=g.setFont(a.font).stringMetrics(a.label); -a._w=e.width;a._h=e.height}},btn:function(a){"ram";a.font&&a.font.endsWith("%")&&(a.font="Vector"+Math.round(h.getHeight()*a.font.slice(0,-1)/100));var e=a.src?h.imageMetrics("function"==typeof a.src?a.src():a.src):h.setFont(a.font||"6x8:2").stringMetrics(a.label);a._h=16+e.height;a._w=20+e.width},img:function(a){"ram";var e=h.imageMetrics("function"==typeof a.src?a.src():a.src),l=a.scale||1;a._w=e.width*l;a._h=e.height*l},"":function(a){"ram";a._w=0;a._h=0},custom:function(a){"ram";a._w=0;a._h=0}, -h:function(a){"ram";a.c.forEach(d);a._h=a.c.reduce((e,l)=>Math.max(e,l._h),0);a._w=a.c.reduce((e,l)=>e+l._w,0);null==a.fillx&&a.c.some(e=>e.fillx)&&(a.fillx=1);null==a.filly&&a.c.some(e=>e.filly)&&(a.filly=1)},v:function(a){"ram";a.c.forEach(d);a._h=a.c.reduce((e,l)=>e+l._h,0);a._w=a.c.reduce((e,l)=>Math.max(e,l._w),0);null==a.fillx&&a.c.some(e=>e.fillx)&&(a.fillx=1);null==a.filly&&a.c.some(e=>e.filly)&&(a.filly=1)}},f=this._l;d(f);delete b;f.fillx||f.filly?(f.w=Bangle.appRect.w,f.h=Bangle.appRect.h, -f.x=Bangle.appRect.x,f.y=Bangle.appRect.y):(f.w=f._w,f.h=f._h,f.x=Bangle.appRect.w-f.w>>1,f.y=Bangle.appRect.y+(Bangle.appRect.h-f.h>>1));this.layout(f)};p.prototype.clear=function(d){d||(d=this._l);g.reset();void 0!==d.bgCol&&g.setBgColor(d.bgCol);g.clearRect(d.x,d.y,d.x+d.w-1,d.y+d.h-1)};exports=p \ No newline at end of file +function p(d,h){function b(e){"ram";e.id&&(a[e.id]=e);e.type||(e.type="");e.c&&e.c.forEach(b)}this._l=this.l=d;this.options=h||{};this.lazy=this.options.lazy||!1;this.physBtns=1;let f;if(2!=process.env.HWVERSION){this.physBtns=3;f=[];function e(l){"ram";"btn"==l.type&&f.push(l);l.c&&l.c.forEach(e)}e(d);f.length&&(this.physBtns=0,this.buttons=f,this.selectedButton=-1)}if(this.options.btns)if(d=this.options.btns,this.physBtns>=d.length){this.b=d;let e=Math.floor(Bangle.appRect.h/ +this.physBtns);for(2d.length;)d.push({label:""});this._l.width=g.getWidth()-8;this._l={type:"h",filly:1,c:[this._l,{type:"v",pad:1,filly:1,c:d.map(l=>(l.type="txt",l.font="6x8",l.height=e,l.r=1,l))}]}}else this._l.width=g.getWidth()-32,this._l={type:"h",c:[this._l,{type:"v",c:d.map(e=>(e.type="btn",e.filly=1,e.width=32,e.r=1,e))}]},f&&f.push.apply(f,this._l.c[1].c);this.setUI();var a=this;b(this._l);this.updateNeeded=!0}function t(d, +h,b,f,a){var e=null==d.bgCol?a:g.toColor(d.bgCol);if(e!=a||"txt"==d.type||"btn"==d.type||"img"==d.type||"custom"==d.type){var l=d.c;delete d.c;var k="H"+E.CRC32(E.toJS(d));l&&(d.c=l);delete h[k]||((f[k]=[d.x,d.y,d.x+d.w-1,d.y+d.h-1]).bg=null==a?g.theme.bg:a,b&&(b.push(d),b=null))}if(d.c)for(var c of d.c)t(c,h,b,f,e)}p.prototype.setUI=function(){Bangle.setUI();let d;this.buttons&&(Bangle.setUI({mode:"updown",back:this.options.back,remove:this.options.remove},h=>{var b=this.selectedButton,f=this.buttons.length; +if(void 0===h&&this.buttons[b])return this.buttons[b].cb();this.buttons[b]&&(delete this.buttons[b].selected,this.render(this.buttons[b]));b=(b+f+h)%f;this.buttons[b]&&(this.buttons[b].selected=1,this.render(this.buttons[b]));this.selectedButton=b}),d=!0);!this.options.back&&!this.options.remove||d||Bangle.setUI({mode:"custom",back:this.options.back,remove:this.options.remove});if(this.b){function h(b,f){.75=b.x&&f.y>=b.y&&f.x<=b.x+b.w&&f.y<=b.y+b.h&&(2==f.type&&b.cbl?b.cbl(f):b.cb&&b.cb(f));b.c&&b.c.forEach(a=>h(a,f))}Bangle.touchHandler= +(b,f)=>h(this._l,f);Bangle.on("touch",Bangle.touchHandler)}};p.prototype.render=function(d){function h(c){"ram";b.reset();void 0!==c.col&&b.setColor(c.col);void 0!==c.bgCol&&b.setBgColor(c.bgCol).clearRect(c.x,c.y,c.x+c.w-1,c.y+c.h-1);f[c.type](c)}d||(d=this._l);this.updateNeeded&&this.update();var b=g,f={"":function(){},txt:function(c){"ram";if(c.wrap){var m=b.setFont(c.font).setFontAlign(0,-1).wrapString(c.label,c.w),n=c.y+(c.h-b.getFontHeight()*m.length>>1);b.drawString(m.join("\n"),c.x+(c.w>> +1),n)}else b.setFont(c.font).setFontAlign(0,0,c.r).drawString(c.label,c.x+(c.w>>1),c.y+(c.h>>1))},btn:function(c){"ram";var m=c.x+(0|c.pad),n=c.y+(0|c.pad),q=c.w-(c.pad<<1),r=c.h-(c.pad<<1);m=[m,n+4,m+4,n,m+q-5,n,m+q-1,n+4,m+q-1,n+r-5,m+q-5,n+r-1,m+4,n+r-1,m,n+r-5,m,n+4];n=c.selected?b.theme.bgH:b.theme.bg2;b.setColor(n).fillPoly(m).setColor(c.selected?b.theme.fgH:b.theme.fg2).drawPoly(m);void 0!==c.col&&b.setColor(c.col);c.src?b.setBgColor(n).drawImage("function"==typeof c.src?c.src():c.src,c.x+ +c.w/2,c.y+c.h/2,{scale:c.scale||void 0,rotate:.5*Math.PI*(c.r||0)}):b.setFont(c.font||"6x8:2").setFontAlign(0,0,c.r).drawString(c.label,c.x+c.w/2,c.y+c.h/2)},img:function(c){"ram";b.drawImage("function"==typeof c.src?c.src():c.src,c.x+c.w/2,c.y+c.h/2,{scale:c.scale||void 0,rotate:.5*Math.PI*(c.r||0)})},custom:function(c){"ram";c.render(c)},h:function(c){"ram";c.c.forEach(h)},v:function(c){"ram";c.c.forEach(h)}};if(this.lazy){this.rects||(this.rects={});var a=this.rects.clone(),e=[];t(d,a,e,this.rects, +null);for(var l in a)delete this.rects[l];d=Object.keys(a).map(c=>a[c]).reverse();for(var k of d)b.setBgColor(k.bg).clearRect.apply(g,k);e.forEach(h)}else h(d)};p.prototype.forgetLazyState=function(){this.rects={}};p.prototype.layout=function(d){var h={h:function(b){"ram";var f=b.x+(0|b.pad),a=0,e=b.c&&b.c.reduce((k,c)=>k+(0|c.fillx),0);e||(f+=b.w-b._w>>1,e=1);var l=f;b.c.forEach(k=>{k.x=0|l;f+=k._w;a+=0|k.fillx;l=f+Math.floor(a*(b.w-b._w)/e);k.w=0|l-k.x;k.h=0|(k.filly?b.h-(b.pad<<1):k._h);k.y=0| +b.y+(0|b.pad)+((1+(0|k.valign))*(b.h-(b.pad<<1)-k.h)>>1);if(k.c)h[k.type](k)})},v:function(b){"ram";var f=b.y+(0|b.pad),a=0,e=b.c&&b.c.reduce((k,c)=>k+(0|c.filly),0);e||(f+=b.h-b._h>>1,e=1);var l=f;b.c.forEach(k=>{k.y=0|l;f+=k._h;a+=0|k.filly;l=f+Math.floor(a*(b.h-b._h)/e);k.h=0|l-k.y;k.w=0|(k.fillx?b.w-(b.pad<<1):k._w);k.x=0|b.x+(0|b.pad)+((1+(0|k.halign))*(b.w-(b.pad<<1)-k.w)>>1);if(k.c)h[k.type](k)})}};if(h[d.type])h[d.type](d)};p.prototype.debug=function(d,h){d||(d=this._l);h=h||1;g.setColor(h& +1,h&2,h&4).drawRect(d.x+h-1,d.y+h-1,d.x+d.w-h,d.y+d.h-h);d.pad&&g.drawRect(d.x+d.pad-1,d.y+d.pad-1,d.x+d.w-d.pad,d.y+d.h-d.pad);h++;d.c&&d.c.forEach(b=>this.debug(b,h))};p.prototype.update=function(){function d(a){"ram";b[a.type](a);if(a.r&1){var e=a._w;a._w=a._h;a._h=e}a._w=Math.max(a._w+(a.pad<<1),0|a.width);a._h=Math.max(a._h+(a.pad<<1),0|a.height)}delete this.updateNeeded;var h=g,b={txt:function(a){"ram";a.font.endsWith("%")&&(a.font="Vector"+Math.round(h.getHeight()*a.font.slice(0,-1)/100)); +if(a.wrap)a._h=a._w=0;else{var e=g.setFont(a.font).stringMetrics(a.label);a._w=e.width;a._h=e.height}},btn:function(a){"ram";a.font&&a.font.endsWith("%")&&(a.font="Vector"+Math.round(h.getHeight()*a.font.slice(0,-1)/100));var e=a.src?h.imageMetrics("function"==typeof a.src?a.src():a.src):h.setFont(a.font||"6x8:2").stringMetrics(a.label);a._h=16+e.height;a._w=20+e.width},img:function(a){"ram";var e=h.imageMetrics("function"==typeof a.src?a.src():a.src),l=a.scale||1;a._w=e.width*l;a._h=e.height*l}, +"":function(a){"ram";a._w=0;a._h=0},custom:function(a){"ram";a._w=0;a._h=0},h:function(a){"ram";a.c.forEach(d);a._h=a.c.reduce((e,l)=>Math.max(e,l._h),0);a._w=a.c.reduce((e,l)=>e+l._w,0);null==a.fillx&&a.c.some(e=>e.fillx)&&(a.fillx=1);null==a.filly&&a.c.some(e=>e.filly)&&(a.filly=1)},v:function(a){"ram";a.c.forEach(d);a._h=a.c.reduce((e,l)=>e+l._h,0);a._w=a.c.reduce((e,l)=>Math.max(e,l._w),0);null==a.fillx&&a.c.some(e=>e.fillx)&&(a.fillx=1);null==a.filly&&a.c.some(e=>e.filly)&&(a.filly=1)}},f=this._l; +d(f);delete b;f.fillx||f.filly?(f.w=Bangle.appRect.w,f.h=Bangle.appRect.h,f.x=Bangle.appRect.x,f.y=Bangle.appRect.y):(f.w=f._w,f.h=f._h,f.x=Bangle.appRect.w-f.w>>1,f.y=Bangle.appRect.y+(Bangle.appRect.h-f.h>>1));this.layout(f)};p.prototype.clear=function(d){d||(d=this._l);g.reset();void 0!==d.bgCol&&g.setBgColor(d.bgCol);g.clearRect(d.x,d.y,d.x+d.w-1,d.y+d.h-1)};exports=p \ No newline at end of file diff --git a/modules/clock_info.js b/modules/clock_info.js index 6f37e5d3d..b28271e9c 100644 --- a/modules/clock_info.js +++ b/modules/clock_info.js @@ -71,11 +71,15 @@ exports.load = function() { bangleItems[2].emit("redraw"); } function altUpdateHandler() { - Bangle.getPressure().then(data=>{ - if (!data) return; - alt = Math.round(data.altitude) + "m"; - bangleItems[3].emit("redraw"); - }); + try { + Bangle.getPressure().then(data=>{ + if (!data) return; + alt = Math.round(data.altitude) + "m"; + bangleItems[3].emit("redraw"); + }); + } catch (e) { + print("Caught "+e+"\n in function altUpdateHandler in module clock_info"); + bangleItems[3].emit('redraw');} } // actual menu var menu = [{ @@ -239,10 +243,15 @@ exports.addInteractive = function(menu, options) { } else if (lr) { if (menu.length==1) return; // 1 item - can't move oldMenuItem = menu[options.menuA].items[options.menuB]; - options.menuA += lr; - if (options.menuA<0) options.menuA = menu.length-1; - if (options.menuA>=menu.length) options.menuA = 0; - options.menuB = 0; + do { + options.menuA += lr; + if (options.menuA<0) options.menuA = menu.length-1; + if (options.menuA>=menu.length) options.menuA = 0; + options.menuB = 0; + //get the next one if the menu is empty + //can happen for dynamic ones (alarms, events) + //in the worst case we come back to 0 + } while(menu[options.menuA].items.length==0); } if (oldMenuItem) { menuHideItem(oldMenuItem); @@ -269,9 +278,12 @@ exports.addInteractive = function(menu, options) { if (!options.focus) { options.focus=true; // if not focussed, set focus options.redraw(); - } else if (menu[options.menuA].items[options.menuB].run) + } else if (menu[options.menuA].items[options.menuB].run) { + Bangle.buzz(100, 0.7); menu[options.menuA].items[options.menuB].run(); // allow tap on an item to run it (eg home assistant) - else options.focus=true; + } else { + options.focus=true; + } }; Bangle.on("touch",touchHandler); } @@ -287,6 +299,23 @@ exports.addInteractive = function(menu, options) { options.redraw = function() { drawItem(menu[options.menuA].items[options.menuB]); }; + options.setItem = function (menuA, menuB) { + if (!menu[menuA] || !menu[menuA].items[menuB] || (options.menuA == menuA && options.menuB == menuB)) { + // menuA or menuB did not exist or did not change + return false; + } + + const oldMenuItem = menu[options.menuA].items[options.menuB]; + if (oldMenuItem) { + menuHideItem(oldMenuItem); + oldMenuItem.removeAllListeners("draw"); + } + options.menuA = menuA; + options.menuB = menuB; + menuShowItem(menu[options.menuA].items[options.menuB]); + + return true; + } return options; }; diff --git a/modules/graphics_utils.js b/modules/graphics_utils.js new file mode 100644 index 000000000..5c08188bc --- /dev/null +++ b/modules/graphics_utils.js @@ -0,0 +1,35 @@ +// draw an arc between radii minR and maxR, and between angles minAngle and maxAngle centered at X,Y. All angles are radians. +exports.fillArc = function(graphics, X, Y, minR, maxR, minAngle, maxAngle, stepAngle) { + var step = stepAngle || 0.2; + var angle = minAngle; + var inside = []; + var outside = []; + var c, s; + while (angle < maxAngle) { + c = Math.cos(angle); + s = Math.sin(angle); + inside.push(X+c*minR); // x + inside.push(Y+s*minR); // y + // outside coordinates are built up in reverse order + outside.unshift(Y+s*maxR); // y + outside.unshift(X+c*maxR); // x + angle += step; + } + c = Math.cos(maxAngle); + s = Math.sin(maxAngle); + inside.push(X+c*minR); + inside.push(Y+s*minR); + outside.unshift(Y+s*maxR); + outside.unshift(X+c*maxR); + + var vertices = inside.concat(outside); + graphics.fillPoly(vertices, true); +} + +exports.degreesToRadians = function(degrees){ + return Math.PI/180 * degrees; +} + +exports.radiansToDegrees = function(radians){ + return 180/Math.PI * degrees; +} \ No newline at end of file diff --git a/modules/widget_utils.js b/modules/widget_utils.js index 33fd303f9..154a95f68 100644 --- a/modules/widget_utils.js +++ b/modules/widget_utils.js @@ -53,8 +53,14 @@ exports.cleanup = function() { back onscreen with a downwards swipe. Use .show to undo. First parameter controls automatic hiding time, 0 equals not hiding at all. Default value is 2000ms until hiding. -Bangle.js 2 only at the moment. */ +Bangle.js 2 only at the moment. On Bangle.js 1 widgets will be hidden permanently. + +Note: On Bangle.js 1 is is possible to draw widgets in an offscreen area of the LCD +and use Bangle.setLCDOffset. However we can't detect a downward swipe so how to +actually make this work needs some thought. +*/ exports.swipeOn = function(autohide) { + if (process.env.HWVERSION!==2) return exports.hide(); exports.cleanup(); if (!global.WIDGETS) return; exports.autohide=autohide===undefined?2000:autohide; diff --git a/package-lock.json b/package-lock.json index e981abdb8..5e531b4a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -987,9 +987,9 @@ "dev": true }, "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "requires": { "minimist": "^1.2.0"