From ed9d4d47c50878e0aea5497d0c934bd750ed1e5b Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Mon, 29 May 2023 13:32:25 +0100 Subject: [PATCH 01/54] sched: interface.html, show timers & non-date alarms too --- apps/sched/interface.html | 74 +++++++++++++++++++++++++++++++-------- 1 file changed, 60 insertions(+), 14 deletions(-) diff --git a/apps/sched/interface.html b/apps/sched/interface.html index f1ace7d0c..0b3c0d986 100644 --- a/apps/sched/interface.html +++ b/apps/sched/interface.html @@ -93,24 +93,53 @@ function upload() { } function renderAlarm(alarm, exists) { - const localDate = dateFromAlarm(alarm); + const localDate = alarm.date ? dateFromAlarm(alarm) : null; const tr = document.createElement('tr'); tr.classList.add('event-row'); tr.dataset.uid = alarm.id; - const tdTime = document.createElement('td'); - tr.appendChild(tdTime); + const tdType = document.createElement('td'); + tdType.type = "text"; + tdType.classList.add('event-summary'); + tr.appendChild(tdType); const inputTime = document.createElement('input'); - inputTime.type = "datetime-local"; + if (localDate) { + tdType.textContent = "Alarm"; + inputTime.type = "datetime-local"; + inputTime.value = localDate.toISOString().slice(0,16); + inputTime.onchange = (e => { + const date = new Date(inputTime.value); + alarm.t = dateToMsSinceMidnight(date); + alarm.date = formatDate(date); + }); + } else { + const [hours, mins, secs] = msToHMS(alarm.timer || alarm.t); + + inputTime.type = "time"; + inputTime.step = 1; // display seconds + inputTime.value = `${hours}:${mins}:${secs}`; + + if (alarm.timer) { + tdType.textContent = "Timer"; + inputTime.onchange = e => { + alarm.timer = hmsToMs(inputTime.value); + const now = new Date(); + const currentTime = (now.getHours()*3600000)+(now.getMinutes()*60000)+(now.getSeconds()*1000); + alarm.t = currentTime + alarm.timer; + }; + } else { + tdType.textContent = "Alarm"; + inputTime.onchange = e => { + alarm.t = hmsToMs(inputTime.value); + }; + } + } + if (!exists) tdType.textContent = "* " + tdType.textContent; inputTime.classList.add('event-date'); inputTime.classList.add('form-input'); inputTime.dataset.uid = alarm.id; - inputTime.value = localDate.toISOString().slice(0,16); - inputTime.onchange = (e => { - const date = new Date(inputTime.value); - alarm.t = dateToMsSinceMidnight(date); - alarm.date = formatDate(date); - }); + const tdTime = document.createElement('td'); + tr.appendChild(tdTime); tdTime.appendChild(inputTime); const tdSummary = document.createElement('td'); @@ -150,6 +179,24 @@ function renderAlarm(alarm, exists) { document.getElementById('upload').disabled = false; } +function msToHMS(ms) { + let secs = Math.floor(ms / 1000) % 60; + let mins = Math.floor(ms / 1000 / 60) % 60; + let hours = Math.floor(ms / 1000 / 60 / 60); + if (secs < 10) secs = "0" + secs; + if (mins < 10) mins = "0" + mins; + if (hours < 10) hours = "0" + hours; + return [hours, mins, secs]; +} + +function hmsToMs(hms) { + let [hours, mins, secs] = hms.split(":"); + hours = Number(hours); + mins = Number(mins); + secs = Number(secs); + return ((hours * 60 + mins) * 60 + secs) * 1000; +} + function addAlarm() { const alarm = getAlarmDefaults(); renderAlarm(alarm); @@ -165,9 +212,7 @@ function getData() { schedSettings = JSON.parse(data || "{}") || {}; Util.hideModal(); alarms.forEach(alarm => { - if (alarm.date) { - renderAlarm(alarm, true); - } + renderAlarm(alarm, true); }); }); }); @@ -191,7 +236,8 @@ function onInit() { - + + From 138a607a1b0f76428fa3120b8d0425eaf278beda Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Mon, 29 May 2023 13:32:39 +0100 Subject: [PATCH 02/54] sched: interface.html, sort by type, then time --- apps/sched/interface.html | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/apps/sched/interface.html b/apps/sched/interface.html index 0b3c0d986..0896fbd4e 100644 --- a/apps/sched/interface.html +++ b/apps/sched/interface.html @@ -211,6 +211,17 @@ function getData() { Util.readStorage('sched.settings.json',data=>{ schedSettings = JSON.parse(data || "{}") || {}; Util.hideModal(); + alarms.sort((a, b) => { + let x; + + x = !!b.date - !!a.date; + if(x) return x; + + x = !!a.timer - !!b.timer; + if(x) return x; + + return a.t - b.t; + }); alarms.forEach(alarm => { renderAlarm(alarm, true); }); From bd416eb0df6b59a85b2602f7a381e54db78308b6 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Mon, 29 May 2023 13:49:33 +0100 Subject: [PATCH 03/54] sched: interface.html can create timers & daily alarms --- apps/sched/interface.html | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/apps/sched/interface.html b/apps/sched/interface.html index 0896fbd4e..ee5303bf0 100644 --- a/apps/sched/interface.html +++ b/apps/sched/interface.html @@ -203,6 +203,29 @@ function addAlarm() { alarms.push(alarm); } +function addDailyAlarm() { + const alarm = getAlarmDefaults(); + delete alarm.date; + renderAlarm(alarm); + alarms.push(alarm); +} + +function addTimer() { + const alarmDefaults = getAlarmDefaults(); + const timer = { + timer: hmsToMs("00:00:30"), + on: false, + t: 0, + dow: alarmDefaults.dow, + last: alarmDefaults.last, + rp: alarmDefaults.rp, + vibrate: alarmDefaults.vibrate, + as: alarmDefaults.as, + };; + renderAlarm(timer); + alarms.push(timer); +} + function getData() { Util.showModal("Loading..."); Util.readStorage('sched.json',data=>{ @@ -241,6 +264,15 @@ function onInit() {
+ +
From 64a8f50b76348cc27be80a1c784a95dad44b44c4 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Mon, 29 May 2023 13:58:06 +0100 Subject: [PATCH 04/54] sched: interface.html reloads watch to apply alarms --- apps/sched/interface.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/sched/interface.html b/apps/sched/interface.html index ee5303bf0..69d1a8bc9 100644 --- a/apps/sched/interface.html +++ b/apps/sched/interface.html @@ -88,7 +88,9 @@ function eventToAlarm(event, offsetMs) { function upload() { Util.showModal("Saving..."); Util.writeStorage("sched.json", JSON.stringify(alarms), () => { - location.reload(); // reload so we see current data + Puck.write(`\x10load()\n`, () => { // reload watch to load alarms + location.reload(); // reload so we see current data + }); }); } From 4d01587c107c1af6acd6d86154a4c2c35f0f2e04 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Mon, 29 May 2023 14:09:23 +0100 Subject: [PATCH 05/54] sched: interface.html can turn alarms on/off --- apps/sched/interface.html | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/apps/sched/interface.html b/apps/sched/interface.html index 69d1a8bc9..0e3dad8d3 100644 --- a/apps/sched/interface.html +++ b/apps/sched/interface.html @@ -164,10 +164,24 @@ function renderAlarm(alarm, exists) { const tdInfo = document.createElement('td'); tr.appendChild(tdInfo); + const onOffCheck = document.createElement('input'); + onOffCheck.type = 'checkbox'; + onOffCheck.checked = alarm.on; + onOffCheck.onchange = e => { + alarm.on = !alarm.on; + }; + const onOffIcon = document.createElement('i'); + onOffIcon.classList.add('form-icon'); + const onOff = document.createElement('label'); + onOff.classList.add('form-switch'); + onOff.appendChild(onOffCheck); + onOff.appendChild(onOffIcon); + tdInfo.appendChild(onOff); + const buttonDelete = document.createElement('button'); buttonDelete.classList.add('btn'); buttonDelete.classList.add('btn-action'); - tdInfo.prepend(buttonDelete); + tdInfo.appendChild(buttonDelete); const iconDelete = document.createElement('i'); iconDelete.classList.add('icon'); iconDelete.classList.add('icon-delete'); From 81e810bcd7d84199635c49030d819831d8cb7f8c Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Mon, 29 May 2023 14:18:01 +0100 Subject: [PATCH 06/54] sched: separate on/off button from delete --- apps/sched/interface.html | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/sched/interface.html b/apps/sched/interface.html index 0e3dad8d3..c5db6806a 100644 --- a/apps/sched/interface.html +++ b/apps/sched/interface.html @@ -161,8 +161,8 @@ function renderAlarm(alarm, exists) { tdSummary.appendChild(inputSummary); inputSummary.onchange(); - const tdInfo = document.createElement('td'); - tr.appendChild(tdInfo); + const tdOptions = document.createElement('td'); + tr.appendChild(tdOptions); const onOffCheck = document.createElement('input'); onOffCheck.type = 'checkbox'; @@ -176,7 +176,10 @@ function renderAlarm(alarm, exists) { onOff.classList.add('form-switch'); onOff.appendChild(onOffCheck); onOff.appendChild(onOffIcon); - tdInfo.appendChild(onOff); + tdOptions.appendChild(onOff); + + const tdInfo = document.createElement('td'); + tr.appendChild(tdInfo); const buttonDelete = document.createElement('button'); buttonDelete.classList.add('btn'); @@ -299,6 +302,7 @@ function onInit() {
+ From f8d1de16ce904b974c2f1831ad670bd3c74167f6 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Mon, 29 May 2023 15:53:07 +0100 Subject: [PATCH 07/54] sched: wait before reloading to avoid `load()` output --- apps/sched/interface.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/sched/interface.html b/apps/sched/interface.html index c5db6806a..8bfeeed13 100644 --- a/apps/sched/interface.html +++ b/apps/sched/interface.html @@ -89,7 +89,9 @@ function upload() { Util.showModal("Saving..."); Util.writeStorage("sched.json", JSON.stringify(alarms), () => { Puck.write(`\x10load()\n`, () => { // reload watch to load alarms - location.reload(); // reload so we see current data + setTimeout(() => { + location.reload(); // reload so we see current data + }, 500); }); }); } From 4fdef7bd8c66c4dc1091251348b3b700c8cd61da Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Mon, 29 May 2023 20:50:38 +0100 Subject: [PATCH 08/54] sched: add th for active --- apps/sched/interface.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sched/interface.html b/apps/sched/interface.html index 8bfeeed13..edfb8e0b3 100644 --- a/apps/sched/interface.html +++ b/apps/sched/interface.html @@ -303,7 +303,7 @@ function onInit() { - + From b6829b65cc04e6b050b6e9710d5e992ed3267746 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Mon, 29 May 2023 20:51:01 +0100 Subject: [PATCH 09/54] sched: default timers to on --- apps/sched/interface.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sched/interface.html b/apps/sched/interface.html index edfb8e0b3..9ee1fb649 100644 --- a/apps/sched/interface.html +++ b/apps/sched/interface.html @@ -235,8 +235,8 @@ function addTimer() { const alarmDefaults = getAlarmDefaults(); const timer = { timer: hmsToMs("00:00:30"), - on: false, t: 0, + on: alarmDefaults.on, dow: alarmDefaults.dow, last: alarmDefaults.last, rp: alarmDefaults.rp, From bbd93f530c15cbf713e3fadf5ab191dc7ee8d5cf Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Mon, 29 May 2023 20:51:23 +0100 Subject: [PATCH 10/54] sched: rename dated-alarms to events --- apps/sched/interface.html | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/sched/interface.html b/apps/sched/interface.html index 9ee1fb649..0560a524b 100644 --- a/apps/sched/interface.html +++ b/apps/sched/interface.html @@ -108,7 +108,7 @@ function renderAlarm(alarm, exists) { tr.appendChild(tdType); const inputTime = document.createElement('input'); if (localDate) { - tdType.textContent = "Alarm"; + tdType.textContent = "Event"; inputTime.type = "datetime-local"; inputTime.value = localDate.toISOString().slice(0,16); inputTime.onchange = (e => { @@ -218,13 +218,13 @@ function hmsToMs(hms) { return ((hours * 60 + mins) * 60 + secs) * 1000; } -function addAlarm() { - const alarm = getAlarmDefaults(); - renderAlarm(alarm); - alarms.push(alarm); +function addEvent() { + const event = getAlarmDefaults(); + renderAlarm(event); + alarms.push(event); } -function addDailyAlarm() { +function addAlarm() { const alarm = getAlarmDefaults(); delete alarm.date; renderAlarm(alarm); @@ -283,11 +283,11 @@ function onInit() {

Manage dated events

- - From 1749b4c2f1668a5a11f9d3b21ed30aabc20d8d4c Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Mon, 29 May 2023 21:01:17 +0100 Subject: [PATCH 11/54] sched: superscript asterisk (for space) --- apps/sched/interface.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/sched/interface.html b/apps/sched/interface.html index 0560a524b..7fe04b7c1 100644 --- a/apps/sched/interface.html +++ b/apps/sched/interface.html @@ -138,7 +138,11 @@ function renderAlarm(alarm, exists) { }; } } - if (!exists) tdType.textContent = "* " + tdType.textContent; + if (!exists) { + const asterisk = document.createElement('sup'); + asterisk.textContent = '*'; + tdType.appendChild(asterisk); + } inputTime.classList.add('event-date'); inputTime.classList.add('form-input'); inputTime.dataset.uid = alarm.id; From 4616d533b4bd0ce9b9ac97fdeac1f399f5e15b5c Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Mon, 5 Jun 2023 11:02:25 +0100 Subject: [PATCH 12/54] sched: don't force reload on alarm save --- apps/sched/interface.html | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/sched/interface.html b/apps/sched/interface.html index 7fe04b7c1..a16cecc38 100644 --- a/apps/sched/interface.html +++ b/apps/sched/interface.html @@ -88,10 +88,8 @@ function eventToAlarm(event, offsetMs) { function upload() { Util.showModal("Saving..."); Util.writeStorage("sched.json", JSON.stringify(alarms), () => { - Puck.write(`\x10load()\n`, () => { // reload watch to load alarms - setTimeout(() => { - location.reload(); // reload so we see current data - }, 500); + Puck.write(`\x10E.showMessage("Hold button to\\nreload alarms")\n`, () => { + location.reload(); // reload so we see current data }); }); } From aa8ecc7e2c4e3fc345ae3b1ba29ca8fabfd03bf0 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Wed, 7 Jun 2023 10:06:39 +0100 Subject: [PATCH 13/54] sched: fix enabling an alarm that's sounded today --- apps/sched/interface.html | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/sched/interface.html b/apps/sched/interface.html index a16cecc38..3a8d99849 100644 --- a/apps/sched/interface.html +++ b/apps/sched/interface.html @@ -173,6 +173,7 @@ function renderAlarm(alarm, exists) { onOffCheck.checked = alarm.on; onOffCheck.onchange = e => { alarm.on = !alarm.on; + if (alarm.on) delete alarm.last; }; const onOffIcon = document.createElement('i'); onOffIcon.classList.add('form-icon'); From 4a48511fe933b2cb1e5c70f539eb89401ed6ab87 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Wed, 7 Jun 2023 10:10:10 +0100 Subject: [PATCH 14/54] sched: reload after uploading sched.json --- apps/sched/interface.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sched/interface.html b/apps/sched/interface.html index 3a8d99849..5730b5741 100644 --- a/apps/sched/interface.html +++ b/apps/sched/interface.html @@ -88,7 +88,7 @@ function eventToAlarm(event, offsetMs) { function upload() { Util.showModal("Saving..."); Util.writeStorage("sched.json", JSON.stringify(alarms), () => { - Puck.write(`\x10E.showMessage("Hold button to\\nreload alarms")\n`, () => { + Puck.write(`\x10require("sched").reload();\n`, () => { location.reload(); // reload so we see current data }); }); From b7cbdf269d2b5fd88d1e3527b91ed402ad350cdf Mon Sep 17 00:00:00 2001 From: stweedo <108593831+stweedo@users.noreply.github.com> Date: Fri, 9 Jun 2023 02:19:35 -0500 Subject: [PATCH 15/54] [widbt] - fixes widget not showing on blue background --- apps/widbt/widget.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/widbt/widget.js b/apps/widbt/widget.js index c7ef8c0ad..31b8e12d8 100644 --- a/apps/widbt/widget.js +++ b/apps/widbt/widget.js @@ -1,9 +1,14 @@ WIDGETS["bluetooth"]={area:"tr",width:15,draw:function() { g.reset(); - if (NRF.getSecurityStatus().connected) - g.setColor((g.getBPP()>8) ? "#07f" : (g.theme.dark ? "#0ff" : "#00f")); - else + if (NRF.getSecurityStatus().connected) { + if (g.getBgColor() === 31) { // If background color is blue use cyan instead + g.setColor("#0ff"); + } else { + g.setColor((g.getBPP()>8) ? "#07f" : (g.theme.dark ? "#0ff" : "#00f")); + } + } else { g.setColor(g.theme.dark ? "#666" : "#999"); + } g.drawImage(atob("CxQBBgDgFgJgR4jZMawfAcA4D4NYybEYIwTAsBwDAA=="),2+this.x,2+this.y); },changed:function() { WIDGETS["bluetooth"].draw(); From 465097b54b8c7e23db5dc5b1e1f2718b57bc1398 Mon Sep 17 00:00:00 2001 From: stweedo <108593831+stweedo@users.noreply.github.com> Date: Fri, 9 Jun 2023 02:27:49 -0500 Subject: [PATCH 16/54] Update ChangeLog --- apps/widbt/ChangeLog | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/widbt/ChangeLog b/apps/widbt/ChangeLog index 4c2132122..292d1347e 100644 --- a/apps/widbt/ChangeLog +++ b/apps/widbt/ChangeLog @@ -5,3 +5,4 @@ 0.06: Tweaking colors for dark/light themes and low bpp screens 0.07: Memory usage improvements 0.08: Disable LCD on, on bluetooth status change +0.09: Fixes widget not showing on blue background From bed497a3bfa724027a885f98da0c002d7ac0f5bf Mon Sep 17 00:00:00 2001 From: stweedo <108593831+stweedo@users.noreply.github.com> Date: Fri, 9 Jun 2023 02:28:13 -0500 Subject: [PATCH 17/54] Update ChangeLog --- apps/widbt/ChangeLog | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/widbt/ChangeLog b/apps/widbt/ChangeLog index 292d1347e..74d31ada6 100644 --- a/apps/widbt/ChangeLog +++ b/apps/widbt/ChangeLog @@ -5,4 +5,4 @@ 0.06: Tweaking colors for dark/light themes and low bpp screens 0.07: Memory usage improvements 0.08: Disable LCD on, on bluetooth status change -0.09: Fixes widget not showing on blue background +0.09: Fix widget not showing on blue background From 030279dc317a7e63b2b30f7c90e22f930863e62e Mon Sep 17 00:00:00 2001 From: stweedo <108593831+stweedo@users.noreply.github.com> Date: Fri, 9 Jun 2023 02:29:16 -0500 Subject: [PATCH 18/54] Update metadata.json --- apps/widbt/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/widbt/metadata.json b/apps/widbt/metadata.json index 1623db7a1..ec03fb353 100644 --- a/apps/widbt/metadata.json +++ b/apps/widbt/metadata.json @@ -1,7 +1,7 @@ { "id": "widbt", "name": "Bluetooth Widget", - "version": "0.08", + "version": "0.09", "description": "Show the current Bluetooth connection status in the top right of the clock", "icon": "widget.png", "type": "widget", From f1ae6c92157f71ebd24495495339aa6e180f11cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20B=C3=BCsgen?= Date: Thu, 8 Jun 2023 20:50:56 +0200 Subject: [PATCH 19/54] feat: :sparkles: add gatthrm bootloader code --- apps/bootgatthrm/ChangeLog | 1 + apps/bootgatthrm/README.md | 16 ++++++++++++ apps/bootgatthrm/bluetooth.png | Bin 0 -> 1119 bytes apps/bootgatthrm/boot.js | 43 +++++++++++++++++++++++++++++++++ apps/bootgatthrm/metadata.json | 15 ++++++++++++ 5 files changed, 75 insertions(+) create mode 100644 apps/bootgatthrm/ChangeLog create mode 100644 apps/bootgatthrm/README.md create mode 100644 apps/bootgatthrm/bluetooth.png create mode 100644 apps/bootgatthrm/boot.js create mode 100644 apps/bootgatthrm/metadata.json diff --git a/apps/bootgatthrm/ChangeLog b/apps/bootgatthrm/ChangeLog new file mode 100644 index 000000000..2a37193a3 --- /dev/null +++ b/apps/bootgatthrm/ChangeLog @@ -0,0 +1 @@ +0.01: Initial release. diff --git a/apps/bootgatthrm/README.md b/apps/bootgatthrm/README.md new file mode 100644 index 000000000..3e559c0a5 --- /dev/null +++ b/apps/bootgatthrm/README.md @@ -0,0 +1,16 @@ +# BLE GATT Battery Service + +Adds the GATT Battery Service to advertise the percentage of battery currently remaining over Bluetooth. + +## Usage + +This boot code runs in the background and has no user interface. + +## Requests + +If you have any suggestions or ideas please post in [this forum thread](http://forum.espruino.com/conversations/351959/), +or [@jjok](https://github.com/jjok) in your Github issue. + +## Creator + +[Jonathan Jefferies](https://github.com/jjok) diff --git a/apps/bootgatthrm/bluetooth.png b/apps/bootgatthrm/bluetooth.png new file mode 100644 index 0000000000000000000000000000000000000000..1a884a62c1029ae43243f8f58ef9dc3a3327e437 GIT binary patch literal 1119 zcmV-l1fctgP)cE5(396`@Dz$wLqIPzYppJ*B(brZX|I zHChYW;Hm1lL~9feUi^283bm!!Ka0DG`v(zZvk8h?W%2@c?XT zES*ZPJU3a{7h{cB0RRrPTh=dGr*a^!0&xQX?DMczGEQwQ4)Y`c0EQJR_Oa?r)W%5x z0HhI_x1HK2pc0j7k^sKW*iQXQ=2YX6D9n-q_%qO+(!1;R%(3!ggBm9SkZ!j|fYm^E zR%O?(qZ5_=gLo$b@WZ#`wXJ`4=)@t~Y>Yv)Y!7y;OB zeO8rs?l*_RK!7NCKLhRU0}||esEhzCyx)O;I=Y5XtC(@B$NTljy45^tT?SHJ10ruX zOS$(<@@!=?P@`0+S)vnkL!=bB)DOg{Q*}L+GO)XAK;&$g@DSo22n#XlR9!)?07D(! zDxz;SOSunBbNCAVm!5U2c~9jVx@WU3=u3)pJ!ur3cu;sm-)NQ!pM}i;0{{TnZpA^Z ztASvff%b#?JdoF$<=hv8)Q159pyx{LBoBD4SyIzhb(HlW;>}!J+1=AqCDO_A6&XN}=e)0!pcibn z_HtD9d_@BAknp}zDCb9=>MK#y^fmCZ_8GoYkv>KTT7e$nHy?0mXP*UpX*>1PgVgRc z3#Fdn#d~2}5mATky^{qxZ#%U!Ve5AonQN!;&C-!_@cJGbKmk6^xYakqWbkDSU>e?6 zF9=onXw<2mHO=A62q0{DUp*iYB(+@Ja7a-n2aQU$C-1do)^M)j_l7Z`m-DHf;N2WNgeIsEr6@kFmJ z8_YbwGn3bL?QPY+LOBr_TDRcEMmfJ|;s=HR0IQ!ry9xAti1(G5Y&@#1^&${_&Hfi+ z9c`2jUpMuHhg{we{KebpwXs3NLqaRiAr)b6sg!$n>kZxDN)lj0k<-mm?qZatNdeqJ z)Lky+8&Ml40dUjvB)_tlzY&Ld+&A&{bh~wEWib~^c!+ZagoS&X-t=l^d_A@r#J2*U lKABp3ezkHW*6{xc{R@3@kib`1LqGrk002ovPDHLkV1l684mkh- literal 0 HcmV?d00001 diff --git a/apps/bootgatthrm/boot.js b/apps/bootgatthrm/boot.js new file mode 100644 index 000000000..9f1ec1584 --- /dev/null +++ b/apps/bootgatthrm/boot.js @@ -0,0 +1,43 @@ +(() => { + function setupHRMAdvertising() { + /* + * This function preparse BLE heart rate Advertisement. + */ + NRF.setServices({ + 0x180D: { // heart_rate + 0x2A37: { // heart_rate_measurement + notify: true, + value: [0x06, 0], + } + } + }, { advertise: ['180D'] }); + + } + + function updateBLEHeartRate(hrm) { + /* + * Send updated heart rate measurement via BLE + */ + if (hrm === undefined) return; + try { + NRF.updateServices({ + '180d': { + '2a37': { + value: [ + 0x06, // + hrm + ], + notify: true + } + } + }); + } catch (error) { + // After setupHRMAdvertising() BLE needs to restart. + // We force a disconnect if the Bangle was connected while setting HRM + NRF.disconnect(); + } + } + + setupHRMAdvertising(); + Bangle.on("HRM", function (hrm) { updateBLEHeartRate(hrm.bpm); }); +})(); diff --git a/apps/bootgatthrm/metadata.json b/apps/bootgatthrm/metadata.json new file mode 100644 index 000000000..e7bfb0762 --- /dev/null +++ b/apps/bootgatthrm/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "bootgatthrm", + "name": "BLE GATT HRM Service", + "shortName": "BLE HRM Service", + "version": "0.01", + "description": "Adds the GATT HRM Service to advertise the measured HRM over Bluetooth.\n", + "icon": "bluetooth.png", + "type": "bootloader", + "tags": "hrm,health,ble,bluetooth,gatt", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"gatthrm.boot.js","url":"boot.js"} + ] +} From dbeb391e6965d1d9990b035054c47bcde5847df0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20B=C3=BCsgen?= Date: Fri, 9 Jun 2023 10:43:50 +0200 Subject: [PATCH 20/54] feat: :sparkles: Added new bootloader "bootgatthrm" which exposes HRM data via BLE --- apps/bootgatthrm/boot.js | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/apps/bootgatthrm/boot.js b/apps/bootgatthrm/boot.js index 9f1ec1584..391f245b9 100644 --- a/apps/bootgatthrm/boot.js +++ b/apps/bootgatthrm/boot.js @@ -1,8 +1,9 @@ (() => { function setupHRMAdvertising() { /* - * This function preparse BLE heart rate Advertisement. + * This function prepares BLE heart rate Advertisement. */ + NRF.setServices({ 0x180D: { // heart_rate 0x2A37: { // heart_rate_measurement @@ -10,34 +11,44 @@ value: [0x06, 0], } } - }, { advertise: ['180D'] }); + }, { advertise: [0x180d] }); } - function updateBLEHeartRate(hrm) { /* * Send updated heart rate measurement via BLE */ - if (hrm === undefined) return; + if (hrm === undefined || hrm.confidence < 50) return; try { NRF.updateServices({ - '180d': { - '2a37': { + 0x180D: { + 0x2A37: { value: [ 0x06, // - hrm + hrm.bpm ], notify: true } } }); } catch (error) { - // After setupHRMAdvertising() BLE needs to restart. - // We force a disconnect if the Bangle was connected while setting HRM - NRF.disconnect(); + if (error.message.includes("BLE restart")) { + /* + * BLE has to restart after service setup. + */ + NRF.disconnect(); + } + else if (error.message.includes("UUID 0x2a37")) { + /* + * Setup service if it wasn't setup correctly for some reason + */ + setupHRMAdvertising(); + } else { + console.log("[bootgatthrm]: Unexpected error occured while updating HRM over BLE! Error: " + error.message); + } } } setupHRMAdvertising(); - Bangle.on("HRM", function (hrm) { updateBLEHeartRate(hrm.bpm); }); + Bangle.on("HRM", function (hrm) { updateBLEHeartRate(hrm); }); })(); From b424e0cb27fc33770a2ab5079e61879590eb888c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20B=C3=BCsgen?= Date: Fri, 9 Jun 2023 11:03:44 +0200 Subject: [PATCH 21/54] docs: :memo: Update Readme --- apps/bootgatthrm/README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/bootgatthrm/README.md b/apps/bootgatthrm/README.md index 3e559c0a5..15bb2b670 100644 --- a/apps/bootgatthrm/README.md +++ b/apps/bootgatthrm/README.md @@ -1,16 +1,16 @@ -# BLE GATT Battery Service +# BLE GATT HRM Service -Adds the GATT Battery Service to advertise the percentage of battery currently remaining over Bluetooth. +Adds the GATT HRM Service to advertise the current HRM over Bluetooth. ## Usage This boot code runs in the background and has no user interface. -## Requests - -If you have any suggestions or ideas please post in [this forum thread](http://forum.espruino.com/conversations/351959/), -or [@jjok](https://github.com/jjok) in your Github issue. - ## Creator -[Jonathan Jefferies](https://github.com/jjok) +[Another Stranger](https://github.com/anotherstranger) + +## Aknowledgements + +Special thanks to [Jonathan Jefferies](https://github.com/jjok) for creating the +bootgattbat app, which was the inspiration for this App! From 7be8b4638d839e7eec323305e5fd939c63591e2f Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Fri, 9 Jun 2023 10:39:47 +0100 Subject: [PATCH 22/54] Improved nav icons for roundabouts (not bothering with a version bump for this) --- apps/messagegui/app.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/messagegui/app.js b/apps/messagegui/app.js index d88fba6df..1f61743ef 100644 --- a/apps/messagegui/app.js +++ b/apps/messagegui/app.js @@ -98,9 +98,9 @@ function showMapMessage(msg) { case "left_slight": img = "ERgB//B/+D/8H4AP4Af4A74Bz4Dj4HD4OD4cD4AD4ADwADwADgAHgAPAAOAAcAA4ABwADgAH";break; case "right_slight": img = "ERgBB/+D/8H/4APwA/gD/APuA+cD44Phw+Dj4HPgAeAB4ADgAPAAeAA4ABwADgAHAAOAAcAA";break; case "finish": img = "HhsBAcAAAD/AAAH/wAAPB4AAeA4AAcAcAAYIcAA4cMAA48MAA4cMAAYAcAAcAcAAcA4AAOA4AAOBxjwHBzjwHjj/4Dnn/4B3P/4B+Pj4A8fj8Acfj8AI//8AA//+AA/j+AB/j+AB/j/A";break; - case "roundabout_left": img = "HhcCAAAAAAAAAAAADwAAAEAAAAP8AAGqkAAA/8ABqqqAAD/wAGqqqgAP/AAKpAakA/8AAakAGoD/////QACpP/////gABpP/////gABpD/////wACpA/8AAv4AGoAP/AAf/QakAD/wAL//qgAA/8AC//qAAAP8AAf/kAAADwAAB/AAAAAAAAB/AAAAAAAAB/AAAAAAAAB/AAAAAAAAB/AAAAAAAAB/AAAAAAAAB/AAA=";break; - case "roundabout_right": img = "HhcCAAAAAAAAAAAAZAAADwAAAf/9AAP8AAB///gAP/AAH///4AD/wAP/Rv9AA/8Af8AH/AAP/AvwAD/////w/wAC/////8/wAC/////8vwAB/////wf8AGpAAP/AP/QaoAA/8AH//qkAD/wAB//6QAP/AAAf/0AAP8AAAA/QAADwAAAA/QAAAAAAAA/QAAAAAAAA/QAAAAAAAA/QAAAAAAAA/QAAAAAAAA/QAAAAAAA=";break; - case "roundabout_straight": img = "EhwCAABQAAAAH0AAAAf9AAAB//QAAH//0AAf//9AB////QH/v+/0P+P8v8L4P8L4CQP8BgAC/+QAAP/+kAA//+pAC/4GqQD/AAqgH+AAagL8AAKkL8AAKkH+AAagD/AAqgC/4GqQA//+pAAP/+kAAC/+QAAAP8AAAAP8AAAAP8AA";break; + case "roundabout_left": img = "HBaCAAADwAAAAAAAD/AAAVUAAD/wABVVUAD/wABVVVQD/wAAVABUD/wAAVAAFT/////wABX/////8AAF//////AABT/////wABUP/AAD/AAVA/8AA/8AVAD/wAD//VQAP/AAP/1QAA/wAA/9AAADwAAD/AAAAAAAA/wAAAAAAAP8AAAAAAAD/AAAAAAAA/wAAAAAAAP8AAAAAAAD/AA=";break; + case "roundabout_right": img = "HRaCAAAAAAAA8AAAP/8AAP8AAD///AA/8AA////AA/8AP/A/8AA/8A/wAP8AA/8P8AA/////8/wAD///////AAD//////8AAP////8P8ABUAAP/A/8AVQAD/wA//1UAA/8AA//VAAP/AAA/9AAA/wAAAPwAAA8AAAA/AAAAAAAAD8AAAAAAAAPwAAAAAAAA/AAAAAAAAD8AAAAAAAAPwAAAAAAA=";break; + case "roundabout_straight": img = "EBuCAAADwAAAD/AAAD/8AAD//wAD///AD///8D/P8/z/D/D//A/wPzAP8AwA//UAA//1QA//9VA/8AFUP8AAVD8AAFQ/AABUPwAAVD8AAFQ/wABUP/ABVA//9VAD//VAAP/1AAAP8AAAD/AAAA/wAA==";break; } //FIXME: what about countries where we drive on the right? How will we know to flip the icons? @@ -208,7 +208,7 @@ function showMessageScroller(msg) { var bodyFont = fontBig; g.setFont(bodyFont); var lines = []; - if (msg.title) lines = g.wrapString(msg.title, g.getWidth()-10) + if (msg.title) lines = g.wrapString(msg.title, g.getWidth()-10); var titleCnt = lines.length; if (titleCnt) lines.push(""); // add blank line after title lines = lines.concat(g.wrapString(msg.body, g.getWidth()-10),["",/*LANG*/"< Back"]); From 6bdb7dc0ad5312c0214f0ea7f058b46a007cc2c2 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Fri, 9 Jun 2023 11:04:06 +0100 Subject: [PATCH 23/54] Fix widgets that clear too low, and make widget swipeOn have a 2px border at the bottom --- apps/widbatv/ChangeLog | 1 + apps/widbatv/metadata.json | 2 +- apps/widbatv/widget.js | 2 +- apps/widclkinfo/ChangeLog | 3 ++- apps/widclkinfo/metadata.json | 2 +- apps/widclkinfo/widget.js | 2 +- modules/widget_utils.js | 6 +++--- 7 files changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/widbatv/ChangeLog b/apps/widbatv/ChangeLog index f7ced965b..1282b846f 100644 --- a/apps/widbatv/ChangeLog +++ b/apps/widbatv/ChangeLog @@ -1,2 +1,3 @@ 0.01: New widget 0.02: Make color depend on level +0.03: Stop battery widget clearing too far down \ No newline at end of file diff --git a/apps/widbatv/metadata.json b/apps/widbatv/metadata.json index 74e374601..d4cbf46ac 100644 --- a/apps/widbatv/metadata.json +++ b/apps/widbatv/metadata.json @@ -1,7 +1,7 @@ { "id": "widbatv", "name": "Battery Level Widget (Vertical)", - "version": "0.02", + "version": "0.03", "description": "Slim, vertical battery widget that only takes up 14px", "icon": "widget.png", "type": "widget", diff --git a/apps/widbatv/widget.js b/apps/widbatv/widget.js index efc42fdad..f26733648 100644 --- a/apps/widbatv/widget.js +++ b/apps/widbatv/widget.js @@ -12,7 +12,7 @@ WIDGETS["batv"]={area:"tr",width:14,draw:function() { if (Bangle.isCharging()) { g.setColor("#0f0").drawImage(atob("DhgBHOBzgc4HOP////////////////////3/4HgB4AeAHgB4AeAHgB4AeAHg"),x,y); } else { - g.clearRect(x,y,x+14,y+24); + g.clearRect(x,y,x+14,y+23); g.setColor(g.theme.fg).fillRect(x+2,y+2,x+12,y+22).clearRect(x+4,y+4,x+10,y+20).fillRect(x+5,y+1,x+9,y+2); var battery = E.getBattery(); if (battery < 20) {g.setColor("#f00");} diff --git a/apps/widclkinfo/ChangeLog b/apps/widclkinfo/ChangeLog index 3ca502120..6c3b85b00 100644 --- a/apps/widclkinfo/ChangeLog +++ b/apps/widclkinfo/ChangeLog @@ -1,2 +1,3 @@ 0.01: New Widget! -0.02: Now use an app ID (to avoid conflicts with clocks that also use ClockInfo) \ No newline at end of file +0.02: Now use an app ID (to avoid conflicts with clocks that also use ClockInfo) +0.03: Fix widget clearing too far down \ No newline at end of file diff --git a/apps/widclkinfo/metadata.json b/apps/widclkinfo/metadata.json index aca046d90..4ba9b2444 100644 --- a/apps/widclkinfo/metadata.json +++ b/apps/widclkinfo/metadata.json @@ -1,6 +1,6 @@ { "id": "widclkinfo", "name": "Clock Info Widget", - "version":"0.02", + "version":"0.03", "description": "Use 'Clock Info' in the Widget bar. Tap on the widget to select, then drag up/down/left/right to choose what information is displayed.", "icon": "widget.png", "screenshots" : [ { "url":"screenshot.png" }], diff --git a/apps/widclkinfo/widget.js b/apps/widclkinfo/widget.js index 95bc9a111..a802ba872 100644 --- a/apps/widclkinfo/widget.js +++ b/apps/widclkinfo/widget.js @@ -32,7 +32,7 @@ if (!require("clock_info").loadCount) { // don't load if a clock_info was alread // indicate focus - make background reddish //if (clockInfoMenu.focus) g.setBgColor(g.blendColor(g.theme.bg, "#f00", 0.25)); if (clockInfoMenu.focus) g.setColor("#f00"); - g.clearRect(o.x, o.y, o.x+o.w-1, o.y+o.h); + g.clearRect(o.x, o.y, o.x+o.w-1, o.y+o.h-1); if (clockInfoInfo) { var x = o.x; if (clockInfoInfo.img) { diff --git a/modules/widget_utils.js b/modules/widget_utils.js index e83555729..7124ac8c8 100644 --- a/modules/widget_utils.js +++ b/modules/widget_utils.js @@ -70,13 +70,13 @@ exports.swipeOn = function(autohide) { // force app rect to be fullscreen Bangle.appRect = { x: 0, y: 0, w: g.getWidth(), h: g.getHeight(), x2: g.getWidth()-1, y2: g.getHeight()-1 }; // setup offscreen graphics for widgets - let og = Graphics.createArrayBuffer(g.getWidth(),24,16,{msb:true}); + let og = Graphics.createArrayBuffer(g.getWidth(),26,16,{msb:true}); og.theme = g.theme; og._reset = og.reset; og.reset = function() { return this._reset().setColor(g.theme.fg).setBgColor(g.theme.bg); }; - og.reset().clearRect(0,0,og.getWidth(),og.getHeight()); + og.reset().clearRect(0,0,og.getWidth(),23).fillRect(0,24,og.getWidth(),25); let _g = g; let offset = -24; // where on the screen are we? -24=hidden, 0=full visible @@ -146,4 +146,4 @@ exports.swipeOn = function(autohide) { }; Bangle.on("swipe", exports.swipeHandler); Bangle.drawWidgets(); -}; +}; \ No newline at end of file From 38e3fe40e51519d3c0b9966b3079a555c153a2f1 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Fri, 9 Jun 2023 14:10:02 +0100 Subject: [PATCH 24/54] More navigation icons (keep/uturn left/right)Added icon for uturn --- apps/messagegui/ChangeLog | 3 ++- apps/messagegui/app.js | 5 +++++ apps/messagegui/metadata.json | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/messagegui/ChangeLog b/apps/messagegui/ChangeLog index 4231a9f26..ace1d3091 100644 --- a/apps/messagegui/ChangeLog +++ b/apps/messagegui/ChangeLog @@ -91,4 +91,5 @@ 0.66: Updated Navigation handling to work with new Gadgetbridge release 0.67: Support for 'Ignore' for messages from Gadgetbridge Message view is now taller, and we use swipe left/right to dismiss messages rather than buttons -0.68: More navigation icons (for roundabouts) \ No newline at end of file +0.68: More navigation icons (for roundabouts) +0.69: More navigation icons (keep/uturn left/right) \ No newline at end of file diff --git a/apps/messagegui/app.js b/apps/messagegui/app.js index 1f61743ef..614552371 100644 --- a/apps/messagegui/app.js +++ b/apps/messagegui/app.js @@ -97,10 +97,15 @@ function showMapMessage(msg) { case "right": img = "GhcBAABgAAA8AAAPgAAB8AAAPgAAB8D///j///9///+/AAPPAAHjgAD44AB8OAA+DgAPA4ABAOAAADgAAA4AAAOAAADgAAA4AAAOAAAA";break; case "left_slight": img = "ERgB//B/+D/8H4AP4Af4A74Bz4Dj4HD4OD4cD4AD4ADwADwADgAHgAPAAOAAcAA4ABwADgAH";break; case "right_slight": img = "ERgBB/+D/8H/4APwA/gD/APuA+cD44Phw+Dj4HPgAeAB4ADgAPAAeAA4ABwADgAHAAOAAcAA";break; + case "keep_left": img = "ERmBAACAAOAB+AD+AP+B/+H3+PO+8c8w4wBwADgAHgAPAAfAAfAAfAAfAAeAAeAAcAA8AA4ABwADgA==";break; + case "keep_right": img = "ERmBAACAAOAA/AD+AP+A//D/fPueeceY4YBwADgAPAAeAB8AHwAfAB8ADwAPAAcAB4ADgAHAAOAAAA==";break; + case "uturn_left": img = "GRiBAAAH4AAP/AAP/wAPj8APAfAPAHgHgB4DgA8BwAOA4AHAcADsOMB/HPA7zvgd9/gOf/gHH/gDh/gBwfgA4DgAcBgAOAAAHAAADgAABw==";break; + case "uturn_right": img = "GRiBAAPwAAf+AAf/gAfj4AfAeAPAHgPADwHgA4DgAcBwAOA4AHAcBjhuB5x/A+57gP99wD/84A/8cAP8OAD8HAA4DgAMBwAAA4AAAcAAAA==";break; case "finish": img = "HhsBAcAAAD/AAAH/wAAPB4AAeA4AAcAcAAYIcAA4cMAA48MAA4cMAAYAcAAcAcAAcA4AAOA4AAOBxjwHBzjwHjj/4Dnn/4B3P/4B+Pj4A8fj8Acfj8AI//8AA//+AA/j+AB/j+AB/j/A";break; case "roundabout_left": img = "HBaCAAADwAAAAAAAD/AAAVUAAD/wABVVUAD/wABVVVQD/wAAVABUD/wAAVAAFT/////wABX/////8AAF//////AABT/////wABUP/AAD/AAVA/8AA/8AVAD/wAD//VQAP/AAP/1QAA/wAA/9AAADwAAD/AAAAAAAA/wAAAAAAAP8AAAAAAAD/AAAAAAAA/wAAAAAAAP8AAAAAAAD/AA=";break; case "roundabout_right": img = "HRaCAAAAAAAA8AAAP/8AAP8AAD///AA/8AA////AA/8AP/A/8AA/8A/wAP8AA/8P8AA/////8/wAD///////AAD//////8AAP////8P8ABUAAP/A/8AVQAD/wA//1UAA/8AA//VAAP/AAA/9AAA/wAAAPwAAA8AAAA/AAAAAAAAD8AAAAAAAAPwAAAAAAAA/AAAAAAAAD8AAAAAAAAPwAAAAAAA=";break; case "roundabout_straight": img = "EBuCAAADwAAAD/AAAD/8AAD//wAD///AD///8D/P8/z/D/D//A/wPzAP8AwA//UAA//1QA//9VA/8AFUP8AAVD8AAFQ/AABUPwAAVD8AAFQ/wABUP/ABVA//9VAD//VAAP/1AAAP8AAAD/AAAA/wAA==";break; + case "roundabout_uturn": img = "ICCBAAAAAAAAAAAAAAAAAAAP4AAAH/AAAD/4AAB4fAAA8DwAAPAcAADgHgAA4B4AAPAcAADwPAAAeHwAADz4AAAc8AAABPAAAADwAAAY8YAAPPPAAD73gAAf/4AAD/8AABf8AAAb+AAAHfAAABzwAAAcYAAAAAAAAAAAAAAAAAAAAAAA";break; } //FIXME: what about countries where we drive on the right? How will we know to flip the icons? diff --git a/apps/messagegui/metadata.json b/apps/messagegui/metadata.json index f18f39096..dffff0c58 100644 --- a/apps/messagegui/metadata.json +++ b/apps/messagegui/metadata.json @@ -2,7 +2,7 @@ "id": "messagegui", "name": "Message UI", "shortName": "Messages", - "version": "0.68", + "version": "0.69", "description": "Default app to display notifications from iOS and Gadgetbridge/Android", "icon": "app.png", "type": "app", From 0f431d53cfe0a8269c9752b68e99237e70abc4d8 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Fri, 9 Jun 2023 14:16:27 +0100 Subject: [PATCH 25/54] Nav messages with '/' now get split on newlines --- apps/messagegui/ChangeLog | 3 ++- apps/messagegui/app.js | 8 +++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/messagegui/ChangeLog b/apps/messagegui/ChangeLog index ace1d3091..2e8f9bb3c 100644 --- a/apps/messagegui/ChangeLog +++ b/apps/messagegui/ChangeLog @@ -92,4 +92,5 @@ 0.67: Support for 'Ignore' for messages from Gadgetbridge Message view is now taller, and we use swipe left/right to dismiss messages rather than buttons 0.68: More navigation icons (for roundabouts) -0.69: More navigation icons (keep/uturn left/right) \ No newline at end of file +0.69: More navigation icons (keep/uturn left/right) + Nav messages with '/' now get split on newlines \ No newline at end of file diff --git a/apps/messagegui/app.js b/apps/messagegui/app.js index 614552371..97396496f 100644 --- a/apps/messagegui/app.js +++ b/apps/messagegui/app.js @@ -17,6 +17,7 @@ require("messages").pushMessage({"t":"add","id":1575479849,"src":"Skype","title" // maps GB({t:"nav",src:"maps",title:"Navigation",instr:"High St towards Tollgate Rd",distance:966,action:"continue",eta:"08:39"}) GB({t:"nav",src:"maps",title:"Navigation",instr:"High St",distance:12345,action:"left_slight",eta:"08:39"}) +GB({t:"nav",src:"maps",title:"Navigation",instr:"Main St / I-29 ALT / Centerpoint Dr",distance:12345,action:"left_slight",eta:"08:39"}) // call require("messages").pushMessage({"t":"add","id":"call","src":"Phone","title":"Bob","body":"12421312",positive:true,negative:true}) */ @@ -84,12 +85,13 @@ function showMapMessage(msg) { if (msg.distance!==undefined) distance = require("locale").distance(msg.distance); if (msg.instr) { - if (msg.instr.includes("towards") || msg.instr.includes("toward")) { - m = msg.instr.split(/towards|toward/); + var instr = msg.instr.replace(/\s*\/\s*/g," \/\n"); // convert slashes to newlines + if (instr.includes("towards") || instr.includes("toward")) { + m = instr.split(/towards|toward/); target = m[0].trim(); street = m[1].trim(); }else - target = msg.instr; + target = instr; } switch (msg.action) { case "continue": img = "EBgBAIABwAPgD/Af+D/8f/773/PPY8cDwAPAA8ADwAPAA8AAAAPAA8ADwAAAA8ADwAPA";break; From f0fef28fca008b4e6f6c762e66eecea0ab788104 Mon Sep 17 00:00:00 2001 From: pancake Date: Fri, 9 Jun 2023 14:33:17 +0200 Subject: [PATCH 26/54] Initial import of the sunrise watchface --- apps/sunrise/ChangeLog | 1 + apps/sunrise/README.md | 27 + apps/sunrise/app-icon.js | 1 + apps/sunrise/app.js | 321 ++++++ apps/sunrise/app.png | Bin 0 -> 1563 bytes apps/sunrise/metadata.json | 31 + apps/sunrise/screenshot.png | Bin 0 -> 2592 bytes package-lock.json | 2181 ++++++++++++++++++++++++++++++++++- 8 files changed, 2551 insertions(+), 11 deletions(-) create mode 100644 apps/sunrise/ChangeLog create mode 100644 apps/sunrise/README.md create mode 100644 apps/sunrise/app-icon.js create mode 100644 apps/sunrise/app.js create mode 100644 apps/sunrise/app.png create mode 100644 apps/sunrise/metadata.json create mode 100644 apps/sunrise/screenshot.png diff --git a/apps/sunrise/ChangeLog b/apps/sunrise/ChangeLog new file mode 100644 index 000000000..7b83706bf --- /dev/null +++ b/apps/sunrise/ChangeLog @@ -0,0 +1 @@ +0.01: First release diff --git a/apps/sunrise/README.md b/apps/sunrise/README.md new file mode 100644 index 000000000..7a60b681d --- /dev/null +++ b/apps/sunrise/README.md @@ -0,0 +1,27 @@ +# sunrise watchface + +This app mimics the Apple Watch watchface that shows the sunrise and sunset time. + +This is a work-in-progress app, so you may expect missfeatures, bugs and heavy +battery draining. There's still a lot of things to optimize and improve, so take +this into account before complaining :-) + +* Lat/Lon hardcoded to Barcelona, but it's configurable if you install the "mylocation app" +* Shows sea level and make the sun/moon glow depending on the x position +* The sinus is fixed, so the sea level is curved to match the sunrise/sunset positions) + +## TODO + +* Access GPS on first start to cache lat+lon to compute sunrise/sunset times +* Cache constant values once +* Show month and day too? +* Improved gradients +* Faster rendering, by reducing sinus stepsize, only refreshing whats needed, etc + +## Author + +Written by pancake in 2023 + +## Screenshots + +![sunrise](screenshot.png) diff --git a/apps/sunrise/app-icon.js b/apps/sunrise/app-icon.js new file mode 100644 index 000000000..79e0fded7 --- /dev/null +++ b/apps/sunrise/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkEIf4A/AH4A/AH4ADgMiAAMgCyoABiAXQiUjkQBCkIXQFYMzAAIEBIyMjn8z+cyMKECFwM+93uGAJIPgUzn4WBC4IwBIx0jC4njmQvOkUSkQXTicQL4JHSgSFBgUyO6QkCU4YWBF4KnLFwTXGIwMQRZQ7EC4IxBAYRHKgQjEVIIXCkcgiQwJQQxhBAAJQCGBBdEABIwIfJwmHFxwnFAYQuOFAuIcAMwC54pE1WpWgURMKUxhUKzWqDYLOKVQh1FGoOTnQaKdAR1HhWqzWKkUykK7GkKkM1WZyRsCGAikPhWZ1EzGoKHBaZ5rEGoQWRNgoXVAH4A5")) diff --git a/apps/sunrise/app.js b/apps/sunrise/app.js new file mode 100644 index 000000000..b6dd71702 --- /dev/null +++ b/apps/sunrise/app.js @@ -0,0 +1,321 @@ +// banglejs app made by pancake +// sunrise/sunset script by Matt Kane from https://github.com/Triggertrap/sun-js + +const LOCATION_FILE = 'mylocation.json'; +let location; + +// requires the myLocation app +function loadLocation () { + try { + return require('Storage').readJSON(LOCATION_FILE, 1) || {}; + } catch (e) { + return { }; + } +} + +const latlon = loadLocation(); +const lat = latlon.lat || 41.38; +const lon = latlon.lon || 2.168; + +/** + * Sunrise/sunset script. By Matt Kane. + * + * Based loosely and indirectly on Kevin Boone's SunTimes Java implementation + * of the US Naval Observatory's algorithm. + * + * Copyright © 2012 Triggertrap Ltd. All rights reserved. + * + * This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General + * Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA, + * or connect to: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html + */ + +Date.prototype.sunrise = function (latitude, longitude, zenith) { + return this.sunriseSet(latitude, longitude, true, zenith); +}; + +Date.prototype.sunset = function (latitude, longitude, zenith) { + return this.sunriseSet(latitude, longitude, false, zenith); +}; + +Date.prototype.sunriseSet = function (latitude, longitude, sunrise, zenith) { + if (!zenith) { + zenith = 90.8333; + } + + const hoursFromMeridian = longitude / Date.DEGREES_PER_HOUR; + const dayOfYear = this.getDayOfYear(); + let approxTimeOfEventInDays; + let sunMeanAnomaly; + let sunTrueLongitude; + let ascension; + let rightAscension; + let lQuadrant; + let raQuadrant; + let sinDec; + let cosDec; + let localHourAngle; + let localHour; + let localMeanTime; + let time; + + if (sunrise) { + approxTimeOfEventInDays = dayOfYear + ((6 - hoursFromMeridian) / 24); + } else { + approxTimeOfEventInDays = dayOfYear + ((18.0 - hoursFromMeridian) / 24); + } + + sunMeanAnomaly = (0.9856 * approxTimeOfEventInDays) - 3.289; + + sunTrueLongitude = sunMeanAnomaly + (1.916 * Math.sinDeg(sunMeanAnomaly)) + (0.020 * Math.sinDeg(2 * sunMeanAnomaly)) + 282.634; + sunTrueLongitude = Math.mod(sunTrueLongitude, 360); + + ascension = 0.91764 * Math.tanDeg(sunTrueLongitude); + rightAscension = 360 / (2 * Math.PI) * Math.atan(ascension); + rightAscension = Math.mod(rightAscension, 360); + + lQuadrant = Math.floor(sunTrueLongitude / 90) * 90; + raQuadrant = Math.floor(rightAscension / 90) * 90; + rightAscension = rightAscension + (lQuadrant - raQuadrant); + rightAscension /= Date.DEGREES_PER_HOUR; + + sinDec = 0.39782 * Math.sinDeg(sunTrueLongitude); + cosDec = Math.cosDeg(Math.asinDeg(sinDec)); + cosLocalHourAngle = ((Math.cosDeg(zenith)) - (sinDec * (Math.sinDeg(latitude)))) / (cosDec * (Math.cosDeg(latitude))); + + localHourAngle = Math.acosDeg(cosLocalHourAngle); + + if (sunrise) { + localHourAngle = 360 - localHourAngle; + } + + localHour = localHourAngle / Date.DEGREES_PER_HOUR; + + localMeanTime = localHour + rightAscension - (0.06571 * approxTimeOfEventInDays) - 6.622; + + time = localMeanTime - (longitude / Date.DEGREES_PER_HOUR); + time = Math.mod(time, 24); + + const midnight = new Date(0); + // midnight.setUTCFullYear(this.getUTCFullYear()); + // midnight.setUTCMonth(this.getUTCMonth()); + // midnight.setUTCDate(this.getUTCDate()); + + const milli = midnight.getTime() + (time * 60 * 60 * 1000); + + return new Date(milli); +}; + +Date.DEGREES_PER_HOUR = 360 / 24; + +// Utility functions + +Date.prototype.getDayOfYear = function () { + const onejan = new Date(this.getFullYear(), 0, 1); + return Math.ceil((this - onejan) / 86400000); +}; + +Math.degToRad = function (num) { + return num * Math.PI / 180; +}; + +Math.radToDeg = function (radians) { + return radians * 180.0 / Math.PI; +}; + +Math.sinDeg = function (deg) { + return Math.sin(deg * 2.0 * Math.PI / 360.0); +}; + +Math.acosDeg = function (x) { + return Math.acos(x) * 360.0 / (2 * Math.PI); +}; + +Math.asinDeg = function (x) { + return Math.asin(x) * 360.0 / (2 * Math.PI); +}; + +Math.tanDeg = function (deg) { + return Math.tan(deg * 2.0 * Math.PI / 360.0); +}; + +Math.cosDeg = function (deg) { + return Math.cos(deg * 2.0 * Math.PI / 360.0); +}; + +Math.mod = function (a, b) { + let result = a % b; + if (result < 0) { + result += b; + } + return result; +}; + +const delta = 2; +const sunrise = new Date().sunrise(lat, lon); +const sr = sunrise.getHours() + ':' + sunrise.getMinutes(); +console.log('sunrise', sunrise); +const sunset = new Date().sunset(lat, lon); +const ss = sunset.getHours() + ':' + sunset.getMinutes(); +console.log('sunset', sunset); + +const w = g.getWidth(); +const h = g.getHeight(); +const oy = h / 1.7; + +function ypos (x) { + const pc = (x * 100 / w); + return oy + (32 * Math.sin(1.7 + (pc / 16))); +} + +let sunRiseX = 0; +let sunSetX = 0; + +function drawSinuses () { + let x = 0; + + g.setColor(0, 0, 0); + // g.fillRect(0,oy,w, h); + g.setColor(1, 1, 1); + let y = oy; + for (i = 0; i < w; i++) { + x = i; + x2 = x + 6; + y2 = ypos(i); + if (x == 0) { + y = y2; + } + g.drawLine(x, y, x2, y2); + y = y2; + i += 5; // no need to draw all steps + } + + // sea level line + const sl0 = seaLevel(sunrise.getHours()); + const sl1 = seaLevel(sunset.getHours()); + sunRiseX = xfromTime(sunrise.getHours()); + sunSetX = xfromTime(sunset.getHours()); + g.setColor(0.5, 0.5, 1); + g.drawLine(0, sl0, w, sl1); + g.setColor(0, 0, 1); + g.drawLine(0, sl0 + 1, w, sl1 + 1); + g.setColor(0, 0, 0.5); + g.drawLine(0, sl0 + 2, w, sl1 + 2); +} + +function drawTimes () { + g.setColor(1, 1, 1); + g.setFont('Vector', 20); + g.drawString(sr, 10, h - 20); + g.drawString(ss, w - 60, h - 20); +} + +let pos = 0; +let realTime = true; +const r = 10; + +function drawGlow () { + const now = new Date(); + if (realTime) { + pos = xfromTime(now.getHours()); + } + const x = pos; + const y = ypos(x - (r / 2)); + const r2 = (x > sunRiseX && x < sunSetX) ? r + 20 : r + 10; + + g.setColor(0.3, 0.3, 0.3); + g.fillCircle(x, y, r2); + g.setColor(0.5, 0.5, 0.5); + g.fillCircle(x, y, r + 3); +} + +function seaLevel (hour) { + // hour goes from 0 to 24 + // to get the X we divide the screen in 24 + return ypos(xfromTime(hour)); +} + +function xfromTime (t) { + return (w / 24) * t; +} + +function drawBall () { + let x = pos; + const now = new Date(); + if (realTime) { + x = xfromTime(now.getHours()); + pos = x; + } + const y = ypos(x - (r / 2)); + + // glow + g.setColor(1, 1, 0); + if (x < sunRiseX) { + g.setColor(0.2, 0.2, 0); + } else if (x > sunSetX) { + g.setColor(0.2, 0.2, 0); + } + g.fillCircle(x, y, r); + g.setColor(1, 1, 1); + g.drawCircle(x, y, r); + let curTime = ''; + let fhours = 0.0; + let fmins = 0.0; + if (realTime) { + fhours = now.getHours(); + fmins = now.getMinutes(); + } else { + fhours = 24 * (pos / w); + if (fhours > 23) { + fhours = 0; + } + fmins = (24 - fhours) % 60; + if (fmins < 0) { + fmins = 0; + } + } + const hours = ((fhours < 10) ? '0' : '') + (0 | fhours); + const mins = ((fmins < 10) ? '0' : '') + (0 | fmins); + curTime = hours + ':' + mins; + g.setFont('Vector', 30); + g.setColor(1, 1, 1); + g.drawString(curTime, w / 1.9, 32); +} + +function renderScreen () { + g.setBgColor(0, 0, 0); + g.clear(); + if (realTime) { + Bangle.loadWidgets(); + Bangle.drawWidgets(); + } + drawGlow(); + drawSinuses(); + drawTimes(); + drawBall(); +} + +Bangle.on('drag', function (tap, top) { + pos = tap.x; + realTime = false; + renderScreen(); +}); + +Bangle.on('lock', () => { + realTime = Bangle.isLocked(); + renderScreen(); +}); +Bangle.on('tap', () => { + realTime = true; + renderScreen(); +}); +renderScreen(); + +setInterval(renderScreen, 60 * 1000); diff --git a/apps/sunrise/app.png b/apps/sunrise/app.png new file mode 100644 index 0000000000000000000000000000000000000000..a170dfa35c8ace11d424b8a6fd0c3dd74d7c5087 GIT binary patch literal 1563 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.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", + "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-includes": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.5.tgz", + "integrity": "sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5", + "get-intrinsic": "^1.1.1", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz", + "integrity": "sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.2", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/confusing-browser-globals": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", + "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/define-properties": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "dev": true, + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/es-abstract": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.1.tgz", + "integrity": "sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.1", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.4", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "regexp.prototype.flags": "^1.4.3", + "string.prototype.trimend": "^1.0.5", + "string.prototype.trimstart": "^1.0.5", + "unbox-primitive": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", + "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.17.0.tgz", + "integrity": "sha512-gq0m0BTJfci60Fz4nczYxNAlED+sMcihltndR8t9t1evnU/azx53x3t2UHXC/uRjcbvRw/XctpaNygSTcQD+Iw==", + "dev": true, + "dependencies": { + "@eslint/eslintrc": "^1.3.0", + "@humanwhocodes/config-array": "^0.9.2", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.2", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^6.0.1", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-airbnb-base": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", + "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==", + "dev": true, + "dependencies": { + "confusing-browser-globals": "^1.0.10", + "object.assign": "^4.1.2", + "object.entries": "^1.1.5", + "semver": "^6.3.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "peerDependencies": { + "eslint": "^7.32.0 || ^8.2.0", + "eslint-plugin-import": "^2.25.2" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", + "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "resolve": "^1.20.0" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz", + "integrity": "sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "find-up": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.26.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz", + "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.4", + "array.prototype.flat": "^1.2.5", + "debug": "^2.6.9", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-module-utils": "^2.7.3", + "has": "^1.0.3", + "is-core-module": "^2.8.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.values": "^1.1.5", + "resolve": "^1.22.0", + "tsconfig-paths": "^3.14.1" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/espree": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz", + "integrity": "sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==", + "dev": true, + "dependencies": { + "acorn": "^8.7.1", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/espree/node_modules/acorn": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", + "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", + "dev": true, + "dependencies": { + "locate-path": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", + "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz", + "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.15.0.tgz", + "integrity": "sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", + "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", + "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", + "dev": true, + "dependencies": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/nodemon": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz", + "integrity": "sha512-Km2mWHKKY5GzRg6i1j5OxOHQtuvVsgskLfigG25yTtbyfRGn/GNvIbRyOf1PSCKJ2aT/58TiuUsuOU5UToVViw==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^3.2.7", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^5.7.1", + "simple-update-notifier": "^1.0.7", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "dev": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-watch": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/npm-watch/-/npm-watch-0.11.0.tgz", + "integrity": "sha512-wAOd0moNX2kSA2FNvt8+7ORwYaJpQ1ZoWjUYdb1bBCxq4nkWuU0IiJa9VpVxrj5Ks+FGXQd62OC/Bjk0aSr+dg==", + "dev": true, + "dependencies": { + "nodemon": "^2.0.7", + "through2": "^4.0.2" + }, + "bin": { + "npm-watch": "cli.js" + } + }, + "node_modules/object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz", + "integrity": "sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", + "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "dependencies": { + "p-try": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", + "dev": true, + "dependencies": { + "p-limit": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/resolve": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", + "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.8.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-update-notifier": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.0.7.tgz", + "integrity": "sha512-BBKgR84BJQJm6WjWFMHgLVuo61FBDSj1z/xSFUIozqO6wO7ii0JxCqlIud7Enr/+LhlbNI0whErq96P2qHNWew==", + "dev": true, + "dependencies": { + "semver": "~7.0.0" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", + "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", + "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "dev": true, + "dependencies": { + "readable-stream": "3" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "dependencies": { + "nopt": "~1.0.10" + }, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", + "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + } + }, "dependencies": { "@eslint/eslintrc": { "version": "1.3.0", @@ -59,7 +2217,8 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true + "dev": true, + "requires": {} }, "ajv": { "version": "6.12.6", @@ -1391,6 +3550,15 @@ } } }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + } + }, "string.prototype.trimend": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", @@ -1413,15 +3581,6 @@ "es-abstract": "^1.19.5" } }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "requires": { - "safe-buffer": "~5.2.0" - } - }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", From de3fbe6578f93342e7355a4efb6ab24946938f62 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Fri, 9 Jun 2023 14:55:11 +0100 Subject: [PATCH 27/54] Update app.js as per https://github.com/espruino/BangleApps/commit/f0fef28fca008b4e6f6c762e66eecea0ab788104#r117262688 --- apps/sunrise/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sunrise/app.js b/apps/sunrise/app.js index b6dd71702..30b344c56 100644 --- a/apps/sunrise/app.js +++ b/apps/sunrise/app.js @@ -7,7 +7,7 @@ let location; // requires the myLocation app function loadLocation () { try { - return require('Storage').readJSON(LOCATION_FILE, 1) || {}; + return require('Storage').readJSON(LOCATION_FILE, 1); } catch (e) { return { }; } From c62ccf27e15bf0485b9fa68a5b5b40ee49a18293 Mon Sep 17 00:00:00 2001 From: pancake Date: Fri, 9 Jun 2023 16:41:11 +0200 Subject: [PATCH 28/54] Release sunrise 0.02, faster sinus and fix menu button --- apps/sunrise/ChangeLog | 1 + apps/sunrise/app.js | 6 ++++-- apps/sunrise/metadata.json | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/sunrise/ChangeLog b/apps/sunrise/ChangeLog index 7b83706bf..19e3bb33b 100644 --- a/apps/sunrise/ChangeLog +++ b/apps/sunrise/ChangeLog @@ -1 +1,2 @@ 0.01: First release +0.02: Faster sinus line and fix button to open menu diff --git a/apps/sunrise/app.js b/apps/sunrise/app.js index 30b344c56..2fa2a7502 100644 --- a/apps/sunrise/app.js +++ b/apps/sunrise/app.js @@ -177,6 +177,7 @@ function ypos (x) { let sunRiseX = 0; let sunSetX = 0; +const sinStep = 12; function drawSinuses () { let x = 0; @@ -187,14 +188,14 @@ function drawSinuses () { let y = oy; for (i = 0; i < w; i++) { x = i; - x2 = x + 6; + x2 = x + sinStep + 1; y2 = ypos(i); if (x == 0) { y = y2; } g.drawLine(x, y, x2, y2); y = y2; - i += 5; // no need to draw all steps + i += sinStep; // no need to draw all steps } // sea level line @@ -318,4 +319,5 @@ Bangle.on('tap', () => { }); renderScreen(); +Bangle.setUI("clock"); setInterval(renderScreen, 60 * 1000); diff --git a/apps/sunrise/metadata.json b/apps/sunrise/metadata.json index 61fe45520..f3012144a 100644 --- a/apps/sunrise/metadata.json +++ b/apps/sunrise/metadata.json @@ -2,7 +2,7 @@ "id": "sunrise", "name": "Sunrise", "shortName": "Sunrise", - "version": "0.01", + "version": "0.02", "type": "clock", "description": "Show sunrise and sunset times", "icon": "app.png", From 84bde38d89414868963b72d3e26b6ab227a0a62d Mon Sep 17 00:00:00 2001 From: pancake Date: Fri, 9 Jun 2023 16:58:51 +0200 Subject: [PATCH 29/54] Call setUI before loadWidgets --- apps/sunrise/app.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sunrise/app.js b/apps/sunrise/app.js index 2fa2a7502..57fb1c2dc 100644 --- a/apps/sunrise/app.js +++ b/apps/sunrise/app.js @@ -13,6 +13,8 @@ function loadLocation () { } } +Bangle.setUI("clock"); +Bangle.loadWidgets(); const latlon = loadLocation(); const lat = latlon.lat || 41.38; const lon = latlon.lon || 2.168; @@ -294,7 +296,6 @@ function renderScreen () { g.setBgColor(0, 0, 0); g.clear(); if (realTime) { - Bangle.loadWidgets(); Bangle.drawWidgets(); } drawGlow(); @@ -319,5 +320,4 @@ Bangle.on('tap', () => { }); renderScreen(); -Bangle.setUI("clock"); setInterval(renderScreen, 60 * 1000); From f8ccc2f59659b26eb4078deff6809ef1009c051f Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Fri, 9 Jun 2023 16:58:00 +0100 Subject: [PATCH 30/54] 0.27: Fix first ever recorded filename being log0 (now all are dated) --- apps/recorder/ChangeLog | 1 + apps/recorder/metadata.json | 2 +- apps/recorder/widget.js | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/recorder/ChangeLog b/apps/recorder/ChangeLog index 46ea05918..94e2f28c2 100644 --- a/apps/recorder/ChangeLog +++ b/apps/recorder/ChangeLog @@ -32,3 +32,4 @@ 0.24: Can now specify `setRecording(true, {force:...` to not show a menu 0.25: Widget now has `isRecording()` for retrieving recording status. 0.26: Now record filename based on date +0.27: Fix first ever recorded filename being log0 (now all are dated) \ No newline at end of file diff --git a/apps/recorder/metadata.json b/apps/recorder/metadata.json index 598318e29..beb5648c8 100644 --- a/apps/recorder/metadata.json +++ b/apps/recorder/metadata.json @@ -2,7 +2,7 @@ "id": "recorder", "name": "Recorder", "shortName": "Recorder", - "version": "0.26", + "version": "0.27", "description": "Record GPS position, heart rate and more in the background, then download to your PC.", "icon": "app.png", "tags": "tool,outdoors,gps,widget", diff --git a/apps/recorder/widget.js b/apps/recorder/widget.js index 061859f9c..c2e04fd09 100644 --- a/apps/recorder/widget.js +++ b/apps/recorder/widget.js @@ -240,8 +240,9 @@ var settings = loadSettings(); options = options||{}; if (isOn && !settings.recording) { + var date=(new Date()).toISOString().substr(0,10).replace(/-/g,""), trackNo=10; if (!settings.file) { // if no filename set - settings.file = "recorder.log0.csv"; + settings.file = "recorder.log" + date + trackNo.toString(36) + ".csv";; } else if (require("Storage").list(settings.file).length){ // if file exists if (!options.force) { // if not forced, ask the question g.reset(); // work around bug in 2v17 and earlier where bg color wasn't reset @@ -263,7 +264,6 @@ require("Storage").open(settings.file,"r").erase(); } else if (options.force=="new") { // new file - use the current date - var date=(new Date()).toISOString().substr(0,10).replace(/-/g,""), trackNo=10; var newFileName; do { // while a file exists, add one to the letter after the date newFileName = "recorder.log" + date + trackNo.toString(36) + ".csv"; From d620ff25be2566d88ae1b0346f57dacc455b46ab Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Fri, 9 Jun 2023 16:59:37 +0100 Subject: [PATCH 31/54] oops --- apps/recorder/widget.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/recorder/widget.js b/apps/recorder/widget.js index c2e04fd09..3c9afcf70 100644 --- a/apps/recorder/widget.js +++ b/apps/recorder/widget.js @@ -242,7 +242,7 @@ if (isOn && !settings.recording) { var date=(new Date()).toISOString().substr(0,10).replace(/-/g,""), trackNo=10; if (!settings.file) { // if no filename set - settings.file = "recorder.log" + date + trackNo.toString(36) + ".csv";; + settings.file = "recorder.log" + date + trackNo.toString(36) + ".csv"; } else if (require("Storage").list(settings.file).length){ // if file exists if (!options.force) { // if not forced, ask the question g.reset(); // work around bug in 2v17 and earlier where bg color wasn't reset From 239c2777b1af7b8b26f1191c9079c4fd6a545d7d Mon Sep 17 00:00:00 2001 From: Luc Tielen Date: Fri, 9 Jun 2023 20:36:43 +0200 Subject: [PATCH 32/54] Update the sunrise watchface --- apps/sunrise/ChangeLog | 1 + apps/sunrise/README.md | 8 +- apps/sunrise/app.js | 147 ++++++++++++++++++++++++++----------- apps/sunrise/metadata.json | 2 +- 4 files changed, 111 insertions(+), 47 deletions(-) diff --git a/apps/sunrise/ChangeLog b/apps/sunrise/ChangeLog index 19e3bb33b..b2c5a2ae1 100644 --- a/apps/sunrise/ChangeLog +++ b/apps/sunrise/ChangeLog @@ -1,2 +1,3 @@ 0.01: First release 0.02: Faster sinus line and fix button to open menu +0.03: Show day/month, add animations, fix !mylocation and text glitch diff --git a/apps/sunrise/README.md b/apps/sunrise/README.md index 7a60b681d..bafe9f76c 100644 --- a/apps/sunrise/README.md +++ b/apps/sunrise/README.md @@ -6,17 +6,15 @@ This is a work-in-progress app, so you may expect missfeatures, bugs and heavy battery draining. There's still a lot of things to optimize and improve, so take this into account before complaining :-) -* Lat/Lon hardcoded to Barcelona, but it's configurable if you install the "mylocation app" +* Requires to configure the location in Settings -> Apps -> My Location * Shows sea level and make the sun/moon glow depending on the x position * The sinus is fixed, so the sea level is curved to match the sunrise/sunset positions) ## TODO -* Access GPS on first start to cache lat+lon to compute sunrise/sunset times -* Cache constant values once -* Show month and day too? -* Improved gradients +* Improved gradients and add support for banglejs1 * Faster rendering, by reducing sinus stepsize, only refreshing whats needed, etc +* Show red vertical lines or dots inside the sinus if there are alarms ## Author diff --git a/apps/sunrise/app.js b/apps/sunrise/app.js index 57fb1c2dc..fd6274129 100644 --- a/apps/sunrise/app.js +++ b/apps/sunrise/app.js @@ -4,18 +4,20 @@ const LOCATION_FILE = 'mylocation.json'; let location; +Bangle.setUI('clock'); +Bangle.loadWidgets(); // requires the myLocation app function loadLocation () { try { return require('Storage').readJSON(LOCATION_FILE, 1); } catch (e) { - return { }; + return { lat: 41.38, lon: 2.168 }; } } - -Bangle.setUI("clock"); -Bangle.loadWidgets(); -const latlon = loadLocation(); +let frames = 0; // amount of pending frames to render (0 if none) +let curPos = 0; // x position of the sun +let realPos = 0; // x position of the sun depending on currentime +const latlon = loadLocation() || {}; const lat = latlon.lat || 41.38; const lon = latlon.lon || 2.168; @@ -172,11 +174,6 @@ const w = g.getWidth(); const h = g.getHeight(); const oy = h / 1.7; -function ypos (x) { - const pc = (x * 100 / w); - return oy + (32 * Math.sin(1.7 + (pc / 16))); -} - let sunRiseX = 0; let sunSetX = 0; const sinStep = 12; @@ -201,21 +198,27 @@ function drawSinuses () { } // sea level line - const sl0 = seaLevel(sunrise.getHours()); - const sl1 = seaLevel(sunset.getHours()); - sunRiseX = xfromTime(sunrise.getHours()); - sunSetX = xfromTime(sunset.getHours()); - g.setColor(0.5, 0.5, 1); + const hh0 = sunrise.getHours(); + const hh1 = sunset.getHours(); + const sl0 = seaLevel(hh0); + const sl1 = seaLevel(hh1); + sunRiseX = xfromTime(hh0) + (r / 2); + sunSetX = xfromTime(hh1) + (r / 2); + g.setColor(0, 0, 1); g.drawLine(0, sl0, w, sl1); g.setColor(0, 0, 1); g.drawLine(0, sl0 + 1, w, sl1 + 1); + /* + g.setColor(0, 0, 1); + g.drawLine(0, sl0 + 1, w, sl1 + 1); g.setColor(0, 0, 0.5); g.drawLine(0, sl0 + 2, w, sl1 + 2); + */ } function drawTimes () { g.setColor(1, 1, 1); - g.setFont('Vector', 20); + g.setFont('6x8', 2); g.drawString(sr, 10, h - 20); g.drawString(ss, w - 60, h - 20); } @@ -226,17 +229,23 @@ const r = 10; function drawGlow () { const now = new Date(); - if (realTime) { + if (frames < 1 && realTime) { pos = xfromTime(now.getHours()); } + const rh = r / 2; const x = pos; - const y = ypos(x - (r / 2)); - const r2 = (x > sunRiseX && x < sunSetX) ? r + 20 : r + 10; - - g.setColor(0.3, 0.3, 0.3); - g.fillCircle(x, y, r2); - g.setColor(0.5, 0.5, 0.5); - g.fillCircle(x, y, r + 3); + const y = ypos(x - r); + const r2 = 0; + if (x > sunRiseX && x < sunSetX) { + g.setColor(0.2, 0.2, 0); + g.fillCircle(x, y, r + 20); + g.setColor(0.5, 0.5, 0); + // wide glow + } else { + g.setColor(0.2, 0.2, 0); + } + // smol glow + g.fillCircle(x, y, r + 8); } function seaLevel (hour) { @@ -245,6 +254,11 @@ function seaLevel (hour) { return ypos(xfromTime(hour)); } +function ypos (x) { + const pc = (x * 100 / w); + return oy + (32 * Math.sin(1.7 + (pc / 16))); +} + function xfromTime (t) { return (w / 24) * t; } @@ -252,47 +266,66 @@ function xfromTime (t) { function drawBall () { let x = pos; const now = new Date(); - if (realTime) { + if (frames < 1 && realTime) { x = xfromTime(now.getHours()); - pos = x; } - const y = ypos(x - (r / 2)); + const y = ypos(x - r); // glow - g.setColor(1, 1, 0); - if (x < sunRiseX) { - g.setColor(0.2, 0.2, 0); - } else if (x > sunSetX) { - g.setColor(0.2, 0.2, 0); + if (x < sunRiseX || x > sunSetX) { + g.setColor(0.5, 0.5, 0); + } else { + g.setColor(1, 1, 1); } + const rh = r / 2; g.fillCircle(x, y, r); - g.setColor(1, 1, 1); + g.setColor(1, 1, 0); g.drawCircle(x, y, r); +} +function drawClock () { + const now = new Date(); + let curTime = ''; let fhours = 0.0; let fmins = 0.0; + let ypos = 32; if (realTime) { fhours = now.getHours(); fmins = now.getMinutes(); } else { + ypos = 32; fhours = 24 * (pos / w); if (fhours > 23) { fhours = 0; } - fmins = (24 - fhours) % 60; + const nexth = 24 * 60 * (pos / w); + fmins = 59 - ((24 * 60) - nexth) % 60; if (fmins < 0) { fmins = 0; } } + if (fmins > 59) { + fmins = 59; + } const hours = ((fhours < 10) ? '0' : '') + (0 | fhours); const mins = ((fmins < 10) ? '0' : '') + (0 | fmins); curTime = hours + ':' + mins; g.setFont('Vector', 30); g.setColor(1, 1, 1); - g.drawString(curTime, w / 1.9, 32); + g.drawString(curTime, w / 1.9, ypos); + // day-month + if (realTime) { + const mo = now.getMonth() + 1; + const da = now.getDate(); + const daymonth = '' + da + '/' + mo; + g.setFont('6x8', 2); + g.drawString(daymonth, 5, 30); + } } function renderScreen () { + realPos = xfromTime((new Date()).getHours()); + g.setFontAlign(-1, -1, 0); g.setBgColor(0, 0, 0); g.clear(); if (realTime) { @@ -301,23 +334,55 @@ function renderScreen () { drawGlow(); drawSinuses(); drawTimes(); + drawClock(); drawBall(); } Bangle.on('drag', function (tap, top) { - pos = tap.x; - realTime = false; + if (tap.y < h / 3) { + curPos = pos; + initialAnimation(); + } else { + pos = tap.x - 5; + realTime = false; + } renderScreen(); }); Bangle.on('lock', () => { + // TODO: render animation here realTime = Bangle.isLocked(); renderScreen(); }); -Bangle.on('tap', () => { - realTime = true; - renderScreen(); -}); + renderScreen(); +realPos = xfromTime((new Date()).getHours()); + +function initialAnimationFrame () { + curPos += (realPos - curPos) / 3; + pos = curPos; + renderScreen(); + if (curPos >= realPos) { + frame = 0; + } + frames--; + if (frames-- > 0) { + setTimeout(initialAnimationFrame, 50); + } else { + realTime = true; + renderScreen(); + } +} + +function initialAnimation () { + const distance = Math.abs(realPos - pos); + frames = distance / 4; + realTime = false; + initialAnimationFrame(); +} + setInterval(renderScreen, 60 * 1000); + +pos = 0; +initialAnimation(); diff --git a/apps/sunrise/metadata.json b/apps/sunrise/metadata.json index f3012144a..d9d7533b6 100644 --- a/apps/sunrise/metadata.json +++ b/apps/sunrise/metadata.json @@ -2,7 +2,7 @@ "id": "sunrise", "name": "Sunrise", "shortName": "Sunrise", - "version": "0.02", + "version": "0.03", "type": "clock", "description": "Show sunrise and sunset times", "icon": "app.png", From 0535229f53adf2c312d96285ad375d77f8ad135d Mon Sep 17 00:00:00 2001 From: Luc Tielen Date: Fri, 9 Jun 2023 20:45:16 +0200 Subject: [PATCH 33/54] sunrise 0.04 --- apps/sunrise/ChangeLog | 1 + apps/sunrise/app.js | 37 +++++++++++++++++++++++++------------ apps/sunrise/metadata.json | 2 +- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/apps/sunrise/ChangeLog b/apps/sunrise/ChangeLog index b2c5a2ae1..490992812 100644 --- a/apps/sunrise/ChangeLog +++ b/apps/sunrise/ChangeLog @@ -1,3 +1,4 @@ 0.01: First release 0.02: Faster sinus line and fix button to open menu 0.03: Show day/month, add animations, fix !mylocation and text glitch +0.04: Always show the widgets, swifter animations and lighter sea line diff --git a/apps/sunrise/app.js b/apps/sunrise/app.js index fd6274129..3feb4dfd4 100644 --- a/apps/sunrise/app.js +++ b/apps/sunrise/app.js @@ -204,9 +204,9 @@ function drawSinuses () { const sl1 = seaLevel(hh1); sunRiseX = xfromTime(hh0) + (r / 2); sunSetX = xfromTime(hh1) + (r / 2); - g.setColor(0, 0, 1); + g.setColor(0, 0.5, 1); g.drawLine(0, sl0, w, sl1); - g.setColor(0, 0, 1); + g.setColor(0, 0.5, 1); g.drawLine(0, sl0 + 1, w, sl1 + 1); /* g.setColor(0, 0, 1); @@ -311,7 +311,11 @@ function drawClock () { const mins = ((fmins < 10) ? '0' : '') + (0 | fmins); curTime = hours + ':' + mins; g.setFont('Vector', 30); - g.setColor(1, 1, 1); + if (realTime) { + g.setColor(1, 1, 1); + } else { + g.setColor(0, 1, 1); + } g.drawString(curTime, w / 1.9, ypos); // day-month if (realTime) { @@ -324,13 +328,13 @@ function drawClock () { } function renderScreen () { + g.setColor(0, 0, 0); + g.fillRect(0, 30, w, h); realPos = xfromTime((new Date()).getHours()); g.setFontAlign(-1, -1, 0); - g.setBgColor(0, 0, 0); - g.clear(); - if (realTime) { - Bangle.drawWidgets(); - } + + Bangle.drawWidgets(); + drawGlow(); drawSinuses(); drawTimes(); @@ -360,7 +364,11 @@ renderScreen(); realPos = xfromTime((new Date()).getHours()); function initialAnimationFrame () { - curPos += (realPos - curPos) / 3; + let distance = (realPos - curPos) / 4; + if (distance > 20) { + distance = 20; + } + curPos += distance; pos = curPos; renderScreen(); if (curPos >= realPos) { @@ -382,7 +390,12 @@ function initialAnimation () { initialAnimationFrame(); } -setInterval(renderScreen, 60 * 1000); +function main () { + g.setBgColor(0, 0, 0); + g.clear(); + setInterval(renderScreen, 60 * 1000); + pos = 0; + initialAnimation(); +} -pos = 0; -initialAnimation(); +main(); diff --git a/apps/sunrise/metadata.json b/apps/sunrise/metadata.json index d9d7533b6..051ff99bd 100644 --- a/apps/sunrise/metadata.json +++ b/apps/sunrise/metadata.json @@ -2,7 +2,7 @@ "id": "sunrise", "name": "Sunrise", "shortName": "Sunrise", - "version": "0.03", + "version": "0.04", "type": "clock", "description": "Show sunrise and sunset times", "icon": "app.png", From 8271b7472dab8dd26b20f6c6d67924d6b6a71273 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20B=C3=BCsgen?= Date: Fri, 9 Jun 2023 20:50:51 +0200 Subject: [PATCH 34/54] feat: :sparkles: tune service advertisement so apps like OpenTracks can find the hrm service --- apps/bootgatthrm/boot.js | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/apps/bootgatthrm/boot.js b/apps/bootgatthrm/boot.js index 391f245b9..aad210f6f 100644 --- a/apps/bootgatthrm/boot.js +++ b/apps/bootgatthrm/boot.js @@ -4,14 +4,30 @@ * This function prepares BLE heart rate Advertisement. */ + NRF.setAdvertising( + { + 0x180d: undefined + }, + { + // We need custom Advertisement settings for Apps like OpenTracks + connectable: true, + discoverable: true, + scannable: true, + whenConnected: true, + } + ); + NRF.setServices({ 0x180D: { // heart_rate 0x2A37: { // heart_rate_measurement notify: true, value: [0x06, 0], + }, + 0x2A38: { // Sensor Location: Wrist + value: 0x02, } } - }, { advertise: [0x180d] }); + }); } function updateBLEHeartRate(hrm) { @@ -23,11 +39,11 @@ NRF.updateServices({ 0x180D: { 0x2A37: { - value: [ - 0x06, // - hrm.bpm - ], + value: [0x06, hrm.bpm], notify: true + }, + 0x2A38: { + value: 0x02, } } }); From ef197a5133ea37ba7a881b34587d5ec2a1dee1e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20B=C3=BCsgen?= Date: Sat, 10 Jun 2023 12:36:59 +0200 Subject: [PATCH 35/54] docs: :bookmark: Increment version number and add ChangeLog --- apps/bootgatthrm/ChangeLog | 1 + apps/bootgatthrm/metadata.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/bootgatthrm/ChangeLog b/apps/bootgatthrm/ChangeLog index 2a37193a3..1e772af29 100644 --- a/apps/bootgatthrm/ChangeLog +++ b/apps/bootgatthrm/ChangeLog @@ -1 +1,2 @@ 0.01: Initial release. +0.02: Added compatibility to OpenTracks and added HRM Location \ No newline at end of file diff --git a/apps/bootgatthrm/metadata.json b/apps/bootgatthrm/metadata.json index e7bfb0762..450066622 100644 --- a/apps/bootgatthrm/metadata.json +++ b/apps/bootgatthrm/metadata.json @@ -2,7 +2,7 @@ "id": "bootgatthrm", "name": "BLE GATT HRM Service", "shortName": "BLE HRM Service", - "version": "0.01", + "version": "0.02", "description": "Adds the GATT HRM Service to advertise the measured HRM over Bluetooth.\n", "icon": "bluetooth.png", "type": "bootloader", From 238542669911d81b55706a4176bc47cd2786b0c6 Mon Sep 17 00:00:00 2001 From: Philip Andresen Date: Sat, 10 Jun 2023 11:14:04 -0400 Subject: [PATCH 36/54] Implement Matryoshka Keyboard --- apps/kbmatry/ChangeLog | 7 + apps/kbmatry/README.md | 119 +++++++++ apps/kbmatry/app-icon.js | 1 + apps/kbmatry/help.png | Bin 0 -> 5578 bytes apps/kbmatry/icon.png | Bin 0 -> 852 bytes apps/kbmatry/lib.js | 489 +++++++++++++++++++++++++++++++++++ apps/kbmatry/metadata.json | 14 + apps/kbmatry/screenshot.png | Bin 0 -> 3562 bytes apps/kbmatry/screenshot2.png | Bin 0 -> 2977 bytes apps/kbmatry/screenshot3.png | Bin 0 -> 3359 bytes apps/kbmatry/screenshot4.png | Bin 0 -> 2989 bytes apps/kbmatry/screenshot5.png | Bin 0 -> 3458 bytes apps/kbmatry/screenshot6.png | Bin 0 -> 3576 bytes 13 files changed, 630 insertions(+) create mode 100644 apps/kbmatry/ChangeLog create mode 100644 apps/kbmatry/README.md create mode 100644 apps/kbmatry/app-icon.js create mode 100644 apps/kbmatry/help.png create mode 100644 apps/kbmatry/icon.png create mode 100644 apps/kbmatry/lib.js create mode 100644 apps/kbmatry/metadata.json create mode 100644 apps/kbmatry/screenshot.png create mode 100644 apps/kbmatry/screenshot2.png create mode 100644 apps/kbmatry/screenshot3.png create mode 100644 apps/kbmatry/screenshot4.png create mode 100644 apps/kbmatry/screenshot5.png create mode 100644 apps/kbmatry/screenshot6.png diff --git a/apps/kbmatry/ChangeLog b/apps/kbmatry/ChangeLog new file mode 100644 index 000000000..98d0187ab --- /dev/null +++ b/apps/kbmatry/ChangeLog @@ -0,0 +1,7 @@ +1.00: New keyboard +1.01: Change swipe interface to taps, speed up responses (efficiency tweaks). +1.02: Generalize drawing and letter scaling. Allow custom and auto-generated character sets. Improve documentation. +1.03: Attempt to improve keyboard load time. +1.04: Make code asynchronous and improve load time. +1.05: Fix layout issue and rename library +1.06: Touch up readme, prep for IPO, add screenshots \ No newline at end of file diff --git a/apps/kbmatry/README.md b/apps/kbmatry/README.md new file mode 100644 index 000000000..73767828d --- /dev/null +++ b/apps/kbmatry/README.md @@ -0,0 +1,119 @@ +# Matryoshka Keyboard + +![icon](icon.png) + +![screenshot](screenshot.png) ![screenshot](screenshot6.png) + +![screenshot](screenshot5.png) ![screenshot](screenshot2.png) +![screenshot](screenshot3.png) ![screenshot](screenshot4.png) + +Nested key input utility. + +## How to type + +Press your finger down on the letter group that contains the character you would like to type, then tap the letter you +want to enter. Once you are touching the letter you want, release your +finger. + +![help](help.png) + +Press "shft" or "caps" to access alternative characters, including upper case letters, punctuation, and special +characters. +Pressing "shft" also reveals a cancel button if you would like to terminate input without saving. + +Press "ok" to finish typing and send your text to whatever app called this keyboard. + +Press "del" to delete the leftmost character. + +## Themes and Colors + +This keyboard will attempt to use whatever theme or colorscheme is being used by your Bangle device. + +## How to use in a program + +This was developed to match the interface implemented for kbtouch, kbswipe, etc. + +In your app's metadata, add: + +```json + "dependencies": {"textinput": "type"} +``` + +From inside your app, call: + +```js +const textInput = require("textinput"); + +textInput.input({text: ""}) + .then(result => { + console.log("The user entered: ", result); + }); +``` + +Alternatively, if you want to improve the load time of the keyboard, you can pre-generate the data the keyboard needs +to function and render like so: + +```js +const textInput = require("textinput"); + +const defaultKeyboard = textInput.generateKeyboard(textInput.defaultCharSet); +const defaultShiftKeyboard = textInput.generateKeyboard(textInput.defaultCharSetShift); +// ... +textInput.input({text: "", keyboardMain: defaultKeyboard, keyboardShift: defaultShiftKeyboard}) + .then(result => { + console.log("The user entered: ", result); + // And it was faster! + }); +``` + +This isn't required, but if you are using a large character set, and the user is interacting with the keyboard a lot, +it can really smooth the experience. + +The default keyboard has a full set of alphanumeric characters as well as special characters and buttons in a +pre-defined layout. If your application needs something different, or you want to have a custom layout, you can do so: + +```js +const textInput = require("textinput"); + +const customKeyboard = textInput.generateKeyboard([ + ["1", "2", "3", "4"], ["5", "6", "7", "8"], ["9", "0", ".", "-"], "ok", "del", "cncl" +]); +// ... +textInput.input({text: "", keyboardMain: customKeyboard}) + .then(result => { + console.log("The user entered: ", result); + // And they could only enter numbers, periods, and dashes! + }); +``` + +This will give you a keyboard with six buttons. The first three buttons will open up a 2x2 keyboard. The second three +buttons are special keys for submitting, deleting, and cancelling respectively. + +Finally if you are like, super lazy, or have a dynamic set of keys you want to be using at any given time, you can +generate keysets from strings like so: + +```js +const textInput = require("textinput"); + +const customKeyboard = textInput.generateKeyboard(createCharSet("ABCDEFGHIJKLMNOP", ["ok", "shft", "cncl"])); +const customShiftKeyboard = textInput.generateKeyboard(createCharSet("abcdefghijklmnop", ["ok", "shft", "cncl"])); +// ... +textInput.input({text: "", keyboardMain: customKeyboard, keyboardShift: customShiftKeyboard}) + .then(result => { + console.log("The user entered: ", result); + // And the keyboard was automatically generated to include "ABCDEFGHIJKLMNOP" plus an OK button, a shift button, and a cancel button! + }); +``` + +The promise resolves when the user hits "ok" on the input or if they cancel. If the user cancels, undefined is +returned, although the user can hit "OK" with an empty string as well. If you define a custom character set and +do not include the "ok" button your user will be soft-locked by the keyboard. Fair warning! + +At some point I may add swipe-for-space and swipe-for-delete as well as swipe-for-submit and swipe-for-cancel +however I want to have a good strategy for the touch screen +[affordance](https://careerfoundry.com/en/blog/ux-design/affordances-ux-design/). + +## Secret features + +If you long press a key with characters on it, that will enable "Shift" mode. + diff --git a/apps/kbmatry/app-icon.js b/apps/kbmatry/app-icon.js new file mode 100644 index 000000000..a4b0ecc16 --- /dev/null +++ b/apps/kbmatry/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwcBkmSpICVz//ABARGCBIRByA/Dk+AAgUH8AECgP4kmRCwX4n+PAoXH8YEC+IRC4HguE4/+P/EfCIXwgARHn4RG+P/j4RDJwgRBGQIRIEYNxCIRECGpV/CIXAgY1P4/8v41JOgeOn4RDGo4jER5Y1FCJWQg4RDYpeSNIQAMkmTCBwRBz4IG9YRIyA8COgJHBhMgI4+QyVJAYJrC9Mkw5rHwFAkEQCImSCJvAhIRBpazFGo3HEYVJkIjGCIIUCAQu/CKGSGo4jPLIhHMNayPLYo6zBYozpH9MvdI+TfaGSv4KHCI+Qg4GDI4IABg5HGyIYENYIAB45rGyPACKIIDx/4gF/CIPx/8fCIY1F4H8CJPA8BtCa4I1DCJFxCIYXBCILXBGpXHGplwn5HPuE4NaH4n6PLyC6CgEnYpeSpICDdJYRFz4RQARQ")) \ No newline at end of file diff --git a/apps/kbmatry/help.png b/apps/kbmatry/help.png new file mode 100644 index 0000000000000000000000000000000000000000..6eef5694b06410b6cf8af39f59702b5b5bb441e6 GIT binary patch literal 5578 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 literal 0 HcmV?d00001 diff --git a/apps/kbmatry/icon.png b/apps/kbmatry/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..058df4487310ce3148fd3b3af424639896ea742b GIT binary patch literal 852 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!phSslL`iUdT1k0gQ7S`0VrE{6US4X6f{C7? zo@t7E1kf6$qn<8~AsNnBrzI9W4&Z36o@ML%{>mMjwH;5n7NtM_TD4K9e#flks&y^$xi7jCm+d>ct7T>G#G04}g_~C|?f&7rezxMj>s%w4!srKy65vw$yKkMO#^wA z3p*a!E&IP}Lh-(fJga^M_+Ah=>Uf4x^qa%t8J8@#hOcz;t9{{dLBv(~@|y&Hm;S2I z6TLNB-^1irb!G)zv~b#6+E;!4!@^k$6uX2jZ)yD&es#{dD^f{Ixh%gfRCWv&nLeY< z_Qridu(Ou(B^S3FcbA0LzJ8=Ki96@bqI-P7HD4JHE!%%CTAx2|)1T+(Gp5f!c$x9n z_G^l~#-$C+9nZekC{H>s`{DiScD~0B>b4I2H$Pj3#z*}N{2#kcxcJ63rFRUQo)%xq zSMob;|EicTLB60nT;pq_){`jqk_(CN7*6ThIMw?7@;&nU5sRnZ@f)=-vfK{&E}!CyUdwZf2Dr?`vZxu9h#$Me%jvec(R&(gN1PHovY_RlpGLskJ)Ikk)7fC zAMwhlJAc=f2IjYG+LbqSot0fx?p-=@-KzZ9IRz0YVFQdZ4MX4P4f7xIEIM@iXUu$1 zT!b`t9TR-j`|#=7L)A}$-~I%}4kRL$Y!rSaJHL80|5X;=K$#;l0g3z}$+gZ_$G>W6 z?YY{XwMfeCk$@{w5WUk zRJ8r^it;x7X< num - mandatoryExtraKeys); + let keyIndex = 0, charIndex = 0; + let keySpace = preferredNumChars[charIndex] * preferredNumKeys[keyIndex]; + while (keySpace < text.length) { + const numKeys = preferredNumKeys[keyIndex]; + const numChars = preferredNumChars[charIndex]; + const nextNumKeys = preferredNumKeys[keyIndex]; + const nextNumChars = preferredNumChars[charIndex]; + if (numChars <= numKeys) { + charIndex++; + } else if ((text.length / nextNumChars) < nextNumKeys) { + charIndex++; + } else { + keyIndex++; + } + keySpace = preferredNumChars[charIndex] * preferredNumKeys[keyIndex]; + } + const charsPerKey = preferredNumChars[charIndex]; + let charSet = []; + for (let i = 0; i < text.length; i += charsPerKey) { + charSet.push(text.slice(i, i + charsPerKey) + .split("")); + } + charSet = charSet.concat(specials); + return charSet; +} + +/** + * Given the width, height, padding (between chars) and number of characters that need to fit horizontally / + * vertically, this function attempts to select the largest font it can that will still fit within the bounds when + * drawing a grid of characters. Does not handle multi-letter entries well, assumes we are laying out a grid of + * single characters. + * @param width The total width available for letters (px) + * @param height The total height available for letters (px) + * @param padding The amount of space required between characters (px) + * @param gridWidth The number of characters wide the rendering is going to be + * @param gridHeight The number of characters high the rendering is going to be + * @returns {{w: number, h: number, font: string}} + */ +function getBestFont(width, height, padding, gridWidth, gridHeight) { + let font = "4x6"; + let w = 4; + let h = 6; + const charMaxWidth = width / gridWidth - padding * gridWidth; + const charMaxHeight = height / gridHeight - padding * gridHeight; + if (charMaxWidth >= 6 && charMaxHeight >= 8) { + w = 6; + h = 8; + font = "6x8"; + } + if (charMaxWidth >= 12 && charMaxHeight >= 16) { + w = 12; + h = 16; + font = "6x8:2"; + } + if (charMaxWidth >= 12 && charMaxHeight >= 20) { + w = 12; + h = 20; + font = "12x20"; + } + if (charMaxWidth >= 20 && charMaxHeight >= 20) { + font = "Vector" + Math.floor(Math.min(charMaxWidth, charMaxHeight)); + const dims = g.setFont(font) + .stringMetrics("W"); + w = dims.width + h = dims.height; + } + return {w, h, font}; +} + + +/** + * Generate a set of key objects given an array of arrays of characters to make available for typing. + * @param characterArrays + * @returns {Promise} + */ +function getKeys(characterArrays) { + if (Array.isArray(characterArrays)) { + return Promise.all(characterArrays.map((chars, i) => generateKeyFromChars(characterArrays, i))); + } else { + return generateKeyFromChars(characterArrays, 0); + } +} + +function generateKeyFromChars(chars, i) { + return new Promise((resolve, reject) => { + let special; + if (!Array.isArray(chars[i]) && chars[i].length > 1) { + // If it's not an array we assume it's a string. Fingers crossed I guess, lol. Be nice to my functions! + special = chars[i]; + } + const key = getKeyByIndex(chars, i, special); + if (!special) { + key.chars = chars[i]; + } + if (key.chars.length > 1) { + key.pendingSubKeys = true; + key.getSubKeys = () => getKeys(key.chars); + resolve(key) + } else { + resolve(key); + } + }) +} + + +/** + * Given a set of characters (or sets of characters) get the position and dimensions of the i'th key in that set. + * @param charSet An array where each element represents a key on the hypothetical keyboard. + * @param i The index of the key in the set you want to get dimensions for. + * @param special The special property of the key - for example "del" for a key used for deleting characters. + * @returns {{special, bord: number, pad: number, w: number, x: number, h: number, y: number, chars: *[]}} + */ +function getKeyByIndex(charSet, i, special) { + // Key dimensions + const keyboardOffsetY = 40; + const margin = 3; + const padding = 4; + const border = 2; + const gridWidth = Math.ceil(Math.sqrt(charSet.length)); + const gridHeight = Math.ceil(charSet.length / gridWidth); + const keyWidth = Math.floor((g.getWidth()) / gridWidth) - margin; + const keyHeight = Math.floor((g.getHeight() - keyboardOffsetY) / gridHeight) - margin; + const gridx = i % gridWidth; + const gridy = Math.floor(i / gridWidth) % gridWidth; + const x = gridx * (keyWidth + margin); + const y = gridy * (keyHeight + margin) + keyboardOffsetY; + const w = keyWidth; + const h = keyHeight; + // internal Character spacing + const numChars = charSet[i].length; + const subGridWidth = Math.ceil(Math.sqrt(numChars)); + const subGridHeight = Math.ceil(numChars / subGridWidth); + const bestFont = getBestFont(w - padding, h - padding, 0, subGridWidth, subGridHeight); + const letterWidth = bestFont.w; + const letterHeight = bestFont.h; + const totalWidth = (subGridWidth - 1) * (w / subGridWidth) + padding + letterWidth + 1; + const totalHeight = (subGridHeight - 1) * (h / subGridHeight) + padding + letterHeight + 1; + const extraPadH = (w - totalWidth) / 2; + const extraPadV = (h - totalHeight) / 2; + return { + x, + y, + w, + h, + pad : padding, + bord : border, + chars: [], + special, + subGridWidth, + subGridHeight, + extraPadH, + extraPadV, + font : bestFont.font + }; +} + + +/** + * This is probably the most intense part of this keyboard library. If you don't do it ahead of time, it will happen + * when you call the keyboard, and it can take up to 1.5 seconds for a full keyboard. Not a super great user experience + * SO if you have a tiny keyset, don't worry about it so much, but if you want to maximize performance, generate + * a keyboard ahead of time and pass it into the "keyboard" argument of the object in the "input" function. + * @param charSets + * @returns {Promise} + */ +function generateKeyboard(charSets) { + if (!Array.isArray(charSets)) { + // User passed a string. We will divvy it up into a real set of subdivided characters. + charSets = createCharSet(charSets, ["ok", "del", "shft"]); + } + return getKeys(charSets); +} + +const defaultCharSet = [ + ["a", "b", "c", "d", "e", "f", "g", "h", "i"], + ["j", "k", "l", "m", "n", "o", "p", "q", "r"], + ["s", "t", "u", "v", "w", "x", "y", "z", "0"], + ["1", "2", "3", "4", "5", "6", "7", "8", "9"], + [" ", "`", "-", "=", "[", "]", "\\", ";", "'"], + [",", ".", "/"], + "ok", + "shft", + "del" +]; + +const defaultCharSetShift = [ + ["A", "B", "C", "D", "E", "F", "G", "H", "I"], + ["J", "K", "L", "M", "N", "O", "P", "Q", "R"], + ["S", "T", "U", "V", "W", "X", "Y", "Z", ")"], + ["!", "@", "#", "$", "%", "^", "&", "*", "("], + ["~", "_", "+", "{", "}", "|", ":", "\"", "<"], + [">", "?"], + "ok", + "shft", + "del" +]; + +/** + * Given initial options, allow the user to type a set of characters and return their entry in a promise. If you do not + * submit your own character set, a default alphanumeric keyboard will display. + * @param options The object containing initial options for the keyboard. + * @param {string} options.text The initial text to display / edit in the keyboard + * @param {array[]|string[]} [options.keyboardMain] The primary keyboard generated with generateKeyboard() + * @param {array[]|string[]} [options.keyboardShift] Like keyboardMain, but displayed when shift / capslock is pressed. + * @returns {Promise} + */ +function input(options) { + options = options || {}; + let typed = options.text || ""; + let resolveFunction = () => {}; + let shift = false; + let caps = false; + let activeKeySet; + + const offsetX = 0; + const offsetY = 40; + + E.showMessage("Loading..."); + let keyboardPromise; + if (options.keyboardMain) { + keyboardPromise = Promise.all([options.keyboardMain, options.keyboardShift || Promise.resolve([])]); + } else { + keyboardPromise = Promise.all([generateKeyboard(defaultCharSet), generateKeyboard(defaultCharSetShift)]) + } + + let mainKeys; + let mainKeysShift; + + /** + * Draw an individual keyboard key - handles special formatting and the rectangle pad, followed by the character + * rendering. Returns a promise so that you can do many of these in a loop without blocking the thread. + * @param key + */ + function drawKey(key) { + let bgColor = g.theme.bg; + if (key.special) { + if (key.special === "ok") bgColor = "#0F0"; + if (key.special === "cncl") bgColor = "#F00"; + if (key.special === "del") bgColor = g.theme.bg2; + if (key.special === "spc") bgColor = g.theme.bg2; + if (key.special === "shft") { + bgColor = shift ? g.theme.bgH : g.theme.bg2; + } + if (key.special === "caps") { + bgColor = caps ? g.theme.bgH : g.theme.bg2; + } + g.setColor(bgColor) + .fillRect({x: key.x, y: key.y, w: key.w, h: key.h}); + } + g.setColor(g.theme.fg) + .drawRect({x: key.x, y: key.y, w: key.w, h: key.h}); + drawChars(key); + } + + /** + * Draw the characters for a given key - this handles the layout of all characters needed for the key, whether the + * key has 12 characters, 1, or if it represents a special key. + * @param key + */ + function drawChars(key) { + const numChars = key.chars.length; + if (key.special) { + g.setColor(g.theme.fg) + .setFont("12x20") + .setFontAlign(-1, -1) + .drawString(key.special, key.x + key.w / 2 - g.stringWidth(key.special) / 2, key.y + key.h / 2 - 10, false); + } else { + g.setColor(g.theme.fg) + .setFont(key.font) + .setFontAlign(-1, -1); + for (let i = 0; i < numChars; i++) { + const gridX = i % key.subGridWidth; + const gridY = Math.floor(i / key.subGridWidth) % key.subGridWidth; + const charOffsetX = gridX * (key.w / key.subGridWidth); + const charOffsetY = gridY * (key.h / key.subGridHeight); + const posX = key.x + key.pad + charOffsetX + key.extraPadH; + const posY = key.y + key.pad + charOffsetY + key.extraPadV; + g.drawString(key.chars[i], posX, posY, false); + } + } + } + + /** + * Get the key set corresponding to the indicated shift state. Allows easy switching between capital letters and + * lower case by just switching the boolean passed here. + * @param shift + * @returns {*[]} + */ + function getMainKeySet(shift) { + return shift ? mainKeysShift : mainKeys; + } + + /** + * Draw all the given keys on the screen. + * @param keys + */ + function drawKeys(keys) { + keys.forEach(key => { + drawKey(key); + }); + } + + /** + * Draw the text that the user has typed so far, includes a cursor and automatic truncation when the string is too + * long. + * @param text + * @param cursorChar + */ + function drawTyped(text, cursorChar) { + let visibleText = text; + let ellipsis = false; + const maxWidth = 176 - 40; + while (g.setFont("12x20") + .stringWidth(visibleText) > maxWidth) { + ellipsis = true; + visibleText = visibleText.slice(1); + } + if (ellipsis) { + visibleText = "..." + visibleText; + } + g.setColor(g.theme.bg2) + .fillRect(5, 5, 171, 30); + g.setColor(g.theme.fg2) + .setFont("12x20") + .drawString(visibleText + cursorChar, 15, 10, false); + } + + /** + * Clear the space on the screen that the keyboard occupies (not the text the user has written). + */ + function clearKeySpace() { + g.setColor(g.theme.bg) + .fillRect(offsetX, offsetY, 176, 176); + } + + /** + * Based on a touch even, determine which key was pressed by the user. + * @param touchEvent + * @param keys + * @returns {*} + */ + function getTouchedKey(touchEvent, keys) { + return keys.find((key) => { + let relX = touchEvent.x - key.x; + let relY = touchEvent.y - key.y; + return relX > 0 && relX < key.w && relY > 0 && relY < key.h; + }) + } + + /** + * On a touch event, determine whether a key is touched and take appropriate action if it is. + * @param button + * @param touchEvent + */ + function keyTouch(button, touchEvent) { + const pressedKey = getTouchedKey(touchEvent, activeKeySet); + if (pressedKey == null) { + // User tapped empty space. + swapKeySet(getMainKeySet(shift !== caps)); + return; + } + if (pressedKey.pendingSubKeys) { + // We have to generate the subkeys for this key still, but we decided to wait until we needed it! + pressedKey.pendingSubKeys = false; + pressedKey.getSubKeys() + .then(subkeys => { + pressedKey.subKeys = subkeys; + keyTouch(undefined, touchEvent); + }) + return; + } + // Haptic feedback + Bangle.buzz(25, 1); + if (pressedKey.subKeys) { + // Hold press for "shift!" + if (touchEvent.type > 1) { + shift = !shift; + swapKeySet(getMainKeySet(shift !== caps)); + } else { + swapKeySet(pressedKey.subKeys); + } + } else { + if (pressedKey.special) { + evaluateSpecialFunctions(pressedKey); + } else { + typed = typed + pressedKey.chars; + shift = false; + drawTyped(typed, ""); + swapKeySet(getMainKeySet(shift !== caps)); + } + } + } + + /** + * Manage setting, generating, and rendering new keys when a key set is changed. + * @param newKeys + */ + function swapKeySet(newKeys) { + activeKeySet = newKeys; + clearKeySpace(); + drawKeys(activeKeySet); + } + + /** + * Determine if the key contains any of the special strings that have their own special behaviour when pressed. + * @param key + */ + function evaluateSpecialFunctions(key) { + switch (key.special) { + case "ok": + setTimeout(() => resolveFunction(typed), 50); + break; + case "del": + typed = typed.slice(0, -1); + drawTyped(typed, ""); + break; + case "shft": + shift = !shift; + swapKeySet(getMainKeySet(shift !== caps)); + break; + case "caps": + caps = !caps; + swapKeySet(getMainKeySet(shift !== caps)); + break; + case "cncl": + setTimeout(() => resolveFunction(), 50); + break; + case "spc": + typed = typed + " "; + break; + } + } + + let isCursorVisible = true; + + const blinkInterval = setInterval(() => { + if (!activeKeySet) return; + isCursorVisible = !isCursorVisible; + if (isCursorVisible) { + drawTyped(typed, "_"); + } else { + drawTyped(typed, ""); + } + }, 200); + + + /** + * We return a promise but the resolve function is assigned to a variable in the higher function scope. That allows + * us to return the promise and resolve it after we are done typing without having to return the entire scope of the + * application within the promise. + */ + return new Promise((resolve, reject) => { + g.clear(true); + resolveFunction = resolve; + keyboardPromise.then((result) => { + mainKeys = result[0]; + mainKeysShift = result[1]; + swapKeySet(getMainKeySet(shift !== caps)); + Bangle.setUI({ + mode: "custom", touch: keyTouch + }); + Bangle.setLocked(false); + }) + }).then((result) => { + g.clearRect(Bangle.appRect); + clearInterval(blinkInterval); + Bangle.setUI(); + return result; + }); +} + +exports.input = input; +exports.generateKeyboard = generateKeyboard; +exports.defaultCharSet = defaultCharSet; +exports.defaultCharSetShift = defaultCharSetShift; +exports.createCharSet = createCharSet; \ No newline at end of file diff --git a/apps/kbmatry/metadata.json b/apps/kbmatry/metadata.json new file mode 100644 index 000000000..793286180 --- /dev/null +++ b/apps/kbmatry/metadata.json @@ -0,0 +1,14 @@ +{ "id": "kbmatry", + "name": "Matryoshka Keyboard", + "version":"1.06", + "description": "A library for text input via onscreen keyboard. Easily enter characters with nested keyboards.", + "icon": "icon.png", + "type":"textinput", + "tags": "keyboard", + "supports" : ["BANGLEJS2"], + "screenshots": [{"url":"screenshot.png"},{"url":"screenshot6.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"},{"url":"screenshot4.png"},{"url":"screenshot5.png"},{"url": "help.png"}], + "readme": "README.md", + "storage": [ + {"name":"textinput","url":"lib.js"} + ] +} diff --git a/apps/kbmatry/screenshot.png b/apps/kbmatry/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..08bb366e48d3f7b5b1f35d063fba50e2dc97e11a GIT binary patch literal 3562 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 S%sYGD03!o4RJ9)V$$tR#q^i*X literal 0 HcmV?d00001 diff --git a/apps/kbmatry/screenshot2.png b/apps/kbmatry/screenshot2.png new file mode 100644 index 0000000000000000000000000000000000000000..21874244d9b17efb3469606b6423eaa34a070dce GIT binary patch literal 2977 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 literal 0 HcmV?d00001 diff --git a/apps/kbmatry/screenshot3.png b/apps/kbmatry/screenshot3.png new file mode 100644 index 0000000000000000000000000000000000000000..1f0c732657e405ed8e4b0c8264c32f76e272f086 GIT binary patch literal 3359 zcmXw6c{tST`~J*k88t*SYV4(uEp@UDvW@MKFoQERl`TRjYZxN?(qgI5qE5;TW1W$0 zDv`pNFt#z~n2vob%gEMG*Z2JXxS!{_uJ?Wac<%Rp@8^2c&f8=5!w{n{~_vq@RGxe?LanGn2Se7Fha+7n$J>5c+t8on;FKMTYDXSx{bN5$jdC^sC&5WaFjGC0=vn)jJL+d zYjfDq`S;Wb_sW7e%t8~xw!^(GEJb$euwMRPCn=mY=QlMnCKtWUjuZ{! zUePaKyxKro(;pcQ=cma2>WQ>P+#=OFW)1KDvQu~WcW?+ypAVjtXBk@ZkjLugtyt2} z#u_Xa=E7N3S0Vy)LVTV=Fl@~U)rTVuSx*)ip`vO*Uh~7nAGX+b5&^RL2wc?8R|MYt zhrY#}E_2FoK?zYr_V#6vSI67CvqgC?h8g)q^K>40m9JdNk!^kUW2biX4SzWGh+Ci% z!tl`mDJO3i7p`(qV4dn7&gJFPYpT_!rpG#>QB}<^Q&*jej!u|kw>Q_U@JrjcP5!se zu(_2Jf{O$1&;^vDQqRV_^$U*}hDV~>{S89(7(G|UtQkG1W?%J=y2iy6; z*8>6&o>TRx!P$;d{%{}x!EVf&)}x&H=fiOIEB>&-?fkGoUU0`y<9?M+Ik}#e2>IYf zW}uBst8B-KprvBkx=c57oYCyD%n^~08A^1jv<20I&kMG6z}G8Q`To8O2TNKiF!9T2&*ee;o95fUuSI?>RGanexvz zGc{yE^NvR20s_n0(4%cNfg%kHtgmhvr)u)tfhi|Gzq4z@aHnP>db55+h%1*|m~nYf z9V&7fix;tH%&8DZy>ygmqX`T!a(KzAX259Zr-w9OoZc)F?JQl@DByFnGc2nfj;n;e z2_vR5dfV@hmq4hugt>nwnW)nhTU;PYK>3zGLs4i$Oxqpl0Es-R8-0%>{G4c(^9v4? zP9~-6=xfD6qb?xt(Q^0Zbc*VDT9 z%Lg7%@ytvE3AG~kbplD z*-@qRC5$`r-7m&IEjFPGaj*UPCvt*p1}B7WLbY%NP}e}jMxLUjhjq#4V;@8aq3)!^ zh3p;P?=l_szYVx?8&28Ms?_Ch&Bx6bi@LkRMA?m)uGx>X4v3=e)FnMCBt|?FI^6dJ zSU>i&ll&AI2bDRVaucycba@zEBKkfV_X09++;r$nTyy{~qPV9Fmwb;k^$o{5e4cdx zVsuIZsPlAiq(;-4J^P=H%#XJ--8xL$1BYMNEiX=L5pxiC?>BxWci6aH0e-JXnQ5$xhM;~6ej-6U-$;-4MjA<%O&R?sk);fPB8DO(c2?=4T z=$tE2_vEtr>I)z_5r2C=6!SP}3-HsgLz||L>=#WU!NLUSXONEaqz~BU#}Dcs7V+Ej zwBKvdb&#hdIz6jMsb_7)LlNEzi>eB<$vI6nW^5@}5hVjpBt5e&rbuF7NdiTi&I_r2 z$WkI67-2}I9B(;75ViAP!6i5fha4xnW{ie%P7G&nFw}L! zdsW9j0Dl#XVE83-q5*}ny68YsoMptTPf@F!Ps`bJstd9piXcaEL$&SqS`+XoVaf(A z&ShdixPQ>4No8B+4Z&$Dg6ktzO~s8#L(O3H;_Ow4yGh!Z0s7s2ZKH6mO-(T{m?7FM z)@)FCsK9ZCR^699iRwOw*=JfgtpHH~)9_5MEth9Z;RY<8@vB(TtJE8*K z@Dh|&PdtgrOn!$ySO64;B#uY%n2k1H!2MvoHwl=ai&VbutuY^|ur)$Dr`TRiA=*?6 z#96a!U;InRXDov~>z1)~0pQ~a1Mdul*K5;g$PsVDGs|!6n}2Fkw_T10A8M-?e?NEr zM44fTVIb!K*U_yCEEgRT9KmIFn=IMJshPT|J@{vx6c|c{@a4@CJF*8BQ6*!Yu_8>{ zA6;XH#f%a@t^8HiV1o3`?I0pX+3B>&ze$psPK_xbpNt#%5MvnEGYuA zai{&;q>PqI-bG=03eH@Gg4r{3jPIK11#Y>3^2fM2imC!_=2vTyE?|kb4@-%uoYrp( zS$P?7x}xQ+LC}6z9~j4tUUfigw?R3qZlUNcyt9l(bsS2`J^PQ$zSAqKl<}pCy{Dar>L8i!;_R<%<5`?*DOvtaC0F1|{!_d9~onAMd zBt74BYi}Sl1*h^Ctr1<_PxcrnkKtx*zFkb{1pXf{$nY| zUsyp<`8Z$Z{g;FWw|q$0S{@|0AD5#0A8l58TDXgP%a*bK(xFv*aKxc#5!Dv#{)pMte4gHS^}D>Q<2Vqfw}wDL;>AXQstg4 z{%&H$rE%|WnpYA20-+9`9Ks{^;s_qVNM_U>+Vh0>d!FE0-3tNh9>q0P0oegkQKkT< z-=)%bOyZoN;_}fkJj_jjKQkFjaVePX^ zib12~$-w>SuD<_i<#QR%jn^MBLAtgWi{5wLqg*!Lvhs40Q@uALw1^2Ip$bKAzzK`F zC6YUK#3wJk;{N~_7I}lZBwc~9wd8oE+lK7X}VXz#@Cph5pt|3)VrF!TT)< zmAwC#1j0`2uRYYkRRsTgT&P0%`aR61Lv)O&^ynD<&3a8Ue^A%?wX>6?FdFp<&f;tjSm#0{KJNTfqyWnPCy8kj zd-qNYjE%?2^gQ`D3SE{YAD}5X3EvhZ1Vt=KH@8^EIKXH%yR8C7Ht--^x&|x~-UQWh fz*|hJEz2qAeHE27kuuo(@c`Bq_U09)gv9>?)dX^_ literal 0 HcmV?d00001 diff --git a/apps/kbmatry/screenshot4.png b/apps/kbmatry/screenshot4.png new file mode 100644 index 0000000000000000000000000000000000000000..de2f90bee69987f37b68a5b629cb0d4cc8f15b96 GIT binary patch literal 2989 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

=)K0Lp-la>Tmr)hS0c^U1GDq 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 VzP=*r(cV%85X{b+mKu|y{twp1VzmGO literal 0 HcmV?d00001 diff --git a/apps/kbmatry/screenshot6.png b/apps/kbmatry/screenshot6.png new file mode 100644 index 0000000000000000000000000000000000000000..20de7ddc1c44ec14693caf9dfa5316eb7ebad0f7 GIT binary patch literal 3576 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 y@?+nz!eHyM4De&$vHY_2U|N~u0000 Date: Sat, 10 Jun 2023 11:47:11 -0400 Subject: [PATCH 37/54] kbmatry: Fix jsdoc inaccuracies, add comments --- apps/kbmatry/lib.js | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/apps/kbmatry/lib.js b/apps/kbmatry/lib.js index 6c32e5a81..a7a434dad 100644 --- a/apps/kbmatry/lib.js +++ b/apps/kbmatry/lib.js @@ -94,6 +94,14 @@ function getKeys(characterArrays) { } } +/** + * Given a set of characters, determine whether or not this needs to be a matryoshka key, a basic key, or a special key. + * Then generate that key. If the key is a matryoshka key, we queue up the generation of its sub-keys for later to + * improve load times. + * @param chars + * @param i + * @returns {Promise} + */ function generateKeyFromChars(chars, i) { return new Promise((resolve, reject) => { let special; @@ -170,9 +178,11 @@ function getKeyByIndex(charSet, i, special) { /** * This is probably the most intense part of this keyboard library. If you don't do it ahead of time, it will happen - * when you call the keyboard, and it can take up to 1.5 seconds for a full keyboard. Not a super great user experience - * SO if you have a tiny keyset, don't worry about it so much, but if you want to maximize performance, generate - * a keyboard ahead of time and pass it into the "keyboard" argument of the object in the "input" function. + * when you call the keyboard, and it can take up to 0.5 seconds for a full alphanumeric keyboard. Depending on what + * is an acceptable user experience for you, and how many keys you are actually generating, you may choose to do this + * ahead of time and pass the result to the "input" function of this library. NOTE: This function would need to be + * called once per key set - so if you have a keyboard with a "shift" key you'd need to run it once for your base + * keyset and once for your shift keyset. * @param charSets * @returns {Promise} */ @@ -184,6 +194,7 @@ function generateKeyboard(charSets) { return getKeys(charSets); } +// Default layout const defaultCharSet = [ ["a", "b", "c", "d", "e", "f", "g", "h", "i"], ["j", "k", "l", "m", "n", "o", "p", "q", "r"], @@ -196,6 +207,7 @@ const defaultCharSet = [ "del" ]; +// Default layout with shift pressed const defaultCharSetShift = [ ["A", "B", "C", "D", "E", "F", "G", "H", "I"], ["J", "K", "L", "M", "N", "O", "P", "Q", "R"], @@ -241,7 +253,7 @@ function input(options) { /** * Draw an individual keyboard key - handles special formatting and the rectangle pad, followed by the character - * rendering. Returns a promise so that you can do many of these in a loop without blocking the thread. + * rendering. * @param key */ function drawKey(key) { @@ -347,7 +359,7 @@ function input(options) { } /** - * Based on a touch even, determine which key was pressed by the user. + * Based on a touch event, determine which key was pressed by the user. * @param touchEvent * @param keys * @returns {*} From c1d7a4ed735c3c2ec91c1e07ced8d271149e6fc2 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Sat, 10 Jun 2023 21:30:40 +0100 Subject: [PATCH 38/54] sched: kick off timers on upload --- apps/sched/interface.html | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/apps/sched/interface.html b/apps/sched/interface.html index 5730b5741..b67029fa2 100644 --- a/apps/sched/interface.html +++ b/apps/sched/interface.html @@ -86,6 +86,16 @@ function eventToAlarm(event, offsetMs) { } function upload() { + // kick off all the (active) timers + const now = new Date(); + const currentTime = now.getHours()*3600000 + + now.getMinutes()*60000 + + now.getSeconds()*1000; + + for (const alarm of alarms) + if (alarm.timer != undefined && alarm.on) + alarm.t = currentTime + alarm.timer; + Util.showModal("Saving..."); Util.writeStorage("sched.json", JSON.stringify(alarms), () => { Puck.write(`\x10require("sched").reload();\n`, () => { @@ -125,9 +135,7 @@ function renderAlarm(alarm, exists) { tdType.textContent = "Timer"; inputTime.onchange = e => { alarm.timer = hmsToMs(inputTime.value); - const now = new Date(); - const currentTime = (now.getHours()*3600000)+(now.getMinutes()*60000)+(now.getSeconds()*1000); - alarm.t = currentTime + alarm.timer; + // alarm.t is set on upload }; } else { tdType.textContent = "Alarm"; From a3042f206aaca3dab54eb3df3ed515f37a332b8f Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Sun, 11 Jun 2023 12:40:11 +0100 Subject: [PATCH 39/54] hwid-batt: whitespace --- apps/hwid_a_battery_widget/widget.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/hwid_a_battery_widget/widget.js b/apps/hwid_a_battery_widget/widget.js index e42c15355..11997a2bd 100644 --- a/apps/hwid_a_battery_widget/widget.js +++ b/apps/hwid_a_battery_widget/widget.js @@ -22,8 +22,8 @@ }; function draw() { - if (typeof old_x === 'undefined') old_x = this.x; - if (typeof old_y === 'undefined') old_y = this.y; + if (typeof old_x === 'undefined') old_x = this.x; + if (typeof old_y === 'undefined') old_y = this.y; var s = 29; var x = this.x; var y = this.y; @@ -38,10 +38,10 @@ g.fillRect(x,y,xl+4,y+16+3); //Clear g.setFontAlign(0,0); g.setFont('Vector',16); - //g.fillRect(old_x,old_y,old_x+4+l*(s-12)/100,old_y+16+3); // clear (lazy) + //g.fillRect(old_x,old_y,old_x+4+l*(s-12)/100,old_y+16+3); // clear (lazy) g.drawString(old_l, old_x + 14, old_y + 10); g.fillRect(x+4,y+14+3,xl_old,y+16+3); // charging bar - + } old_l = l; //console.log(old_x); @@ -54,11 +54,11 @@ g.setFontAlign(0,0); g.setFont('Vector',16); g.drawString(l, x + 14, y + 10); - + } old_x = this.x; - old_y = this.y; - + old_y = this.y; + if (Bangle.isCharging()) changeInterval(id, intervalHigh); else changeInterval(id, intervalLow); } From 60287b725209f1ca520c1f01187c8a25d9c10407 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Sun, 11 Jun 2023 12:40:36 +0100 Subject: [PATCH 40/54] hwid-batt: factor width --- apps/hwid_a_battery_widget/widget.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/hwid_a_battery_widget/widget.js b/apps/hwid_a_battery_widget/widget.js index 11997a2bd..cad782df2 100644 --- a/apps/hwid_a_battery_widget/widget.js +++ b/apps/hwid_a_battery_widget/widget.js @@ -24,7 +24,7 @@ function draw() { if (typeof old_x === 'undefined') old_x = this.x; if (typeof old_y === 'undefined') old_y = this.y; - var s = 29; + var s = width - 1; var x = this.x; var y = this.y; if ((typeof x === 'undefined') || (typeof y === 'undefined')) { @@ -65,6 +65,7 @@ Bangle.on('charging',function(charging) { draw(); }); var id = setInterval(()=>WIDGETS["hwid_a_battery_widget"].draw(), intervalLow); + var width = 30; - WIDGETS["hwid_a_battery_widget"]={area:"tr",width:30,draw:draw}; + WIDGETS["hwid_a_battery_widget"]={area:"tr",width,draw:draw}; })(); From 00c29eb6f67d683c8f5eb4dd13e05768606d569d Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Sun, 11 Jun 2023 12:41:24 +0100 Subject: [PATCH 41/54] hwid-batt: option to show batt high mark --- apps/hwid_a_battery_widget/metadata.json | 6 ++++-- apps/hwid_a_battery_widget/settings.js | 22 ++++++++++++++++++++++ apps/hwid_a_battery_widget/widget.js | 8 +++++++- 3 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 apps/hwid_a_battery_widget/settings.js diff --git a/apps/hwid_a_battery_widget/metadata.json b/apps/hwid_a_battery_widget/metadata.json index 981b81079..32697fac7 100644 --- a/apps/hwid_a_battery_widget/metadata.json +++ b/apps/hwid_a_battery_widget/metadata.json @@ -11,6 +11,8 @@ "tags": "widget,battery", "provides_widgets" : ["battery"], "storage": [ - {"name":"hwid_a_battery_widget.wid.js","url":"widget.js"} - ] + {"name":"hwid_a_battery_widget.wid.js","url":"widget.js"}, + {"name":"hwid_a_battery_widget.setting.js","url":"settings.js"} + ], + "data": [{"name":"hwid_a_battery_widget.settings.json"}] } diff --git a/apps/hwid_a_battery_widget/settings.js b/apps/hwid_a_battery_widget/settings.js new file mode 100644 index 000000000..588ccae91 --- /dev/null +++ b/apps/hwid_a_battery_widget/settings.js @@ -0,0 +1,22 @@ +(back => { + const S = require('Storage'); + + const SETTINGS_FILE = "hwid_a_battery_widget.settings.json"; + const settings = S.readJSON(SETTINGS_FILE, 1) || { + showHighMark: true, + }; + + const save = () => S.write(SETTINGS_FILE, settings); + + E.showMenu({ + '': { 'title': 'Battery Widget (hank mod)' }, + '< Back': back, + 'Show high mark': { + value: settings.showHighMark, + onchange: v => { + settings.showHighMark = v; + save(); + }, + }, + }); +}) diff --git a/apps/hwid_a_battery_widget/widget.js b/apps/hwid_a_battery_widget/widget.js index cad782df2..db2a664ba 100644 --- a/apps/hwid_a_battery_widget/widget.js +++ b/apps/hwid_a_battery_widget/widget.js @@ -1,4 +1,9 @@ (function(){ + const showHighMark = ( + require("Storage").readJSON("hwid_a_battery_widget.settings.json",1) || { + showHighMark: true, + }).showHighMark; + const intervalLow = 60000; // update time when not charging const intervalHigh = 2000; // update time when charging var old_l; @@ -48,7 +53,8 @@ g.setColor(levelColor(l)); g.fillRect(x+4,y+14+3,xl,y+16+3); // charging bar - g.fillRect((x+4+100*(s-12)/100)-1,y+14+3,x+4+100*(s-12)/100,y+16+3); // charging bar "full mark" + if (showHighMark) + g.fillRect((x+4+100*(s-12)/100)-1,y+14+3,x+4+100*(s-12)/100,y+16+3); // charging bar "full mark" // Show percentage g.setColor(COLORS.black); g.setFontAlign(0,0); From d5146903684a10d11cebe577d6d3c2dfda0d9cca Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Sun, 11 Jun 2023 12:42:37 +0100 Subject: [PATCH 42/54] hwid-batt: bump version --- apps/hwid_a_battery_widget/ChangeLog | 3 ++- apps/hwid_a_battery_widget/metadata.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/hwid_a_battery_widget/ChangeLog b/apps/hwid_a_battery_widget/ChangeLog index cbdccfecf..6c57f97a8 100644 --- a/apps/hwid_a_battery_widget/ChangeLog +++ b/apps/hwid_a_battery_widget/ChangeLog @@ -5,4 +5,5 @@ 0.05: Deleting Background - making Font larger 0.06: Fixing refresh issues 0.07: Fixed position after unlocking -0.08: Handling exceptions \ No newline at end of file +0.08: Handling exceptions +0.09: Add option for showing battery high mark diff --git a/apps/hwid_a_battery_widget/metadata.json b/apps/hwid_a_battery_widget/metadata.json index 32697fac7..14590e49a 100644 --- a/apps/hwid_a_battery_widget/metadata.json +++ b/apps/hwid_a_battery_widget/metadata.json @@ -3,7 +3,7 @@ "name": "A Battery Widget (with percentage) - Hanks Mod", "shortName":"H Battery Widget", "icon": "widget.png", - "version":"0.08", + "version":"0.09", "type": "widget", "supports": ["BANGLEJS", "BANGLEJS2"], "readme": "README.md", From 9a3f13507f9f75e083722d4ed22e03071e14fb4a Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Sun, 11 Jun 2023 13:01:26 +0100 Subject: [PATCH 43/54] hwid-batt: shorten setting filename --- apps/hwid_a_battery_widget/metadata.json | 2 +- apps/hwid_a_battery_widget/settings.js | 2 +- apps/hwid_a_battery_widget/widget.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/hwid_a_battery_widget/metadata.json b/apps/hwid_a_battery_widget/metadata.json index 14590e49a..958c01c76 100644 --- a/apps/hwid_a_battery_widget/metadata.json +++ b/apps/hwid_a_battery_widget/metadata.json @@ -14,5 +14,5 @@ {"name":"hwid_a_battery_widget.wid.js","url":"widget.js"}, {"name":"hwid_a_battery_widget.setting.js","url":"settings.js"} ], - "data": [{"name":"hwid_a_battery_widget.settings.json"}] + "data": [{"name":"hwid_batt.settings.json"}] } diff --git a/apps/hwid_a_battery_widget/settings.js b/apps/hwid_a_battery_widget/settings.js index 588ccae91..910fb1452 100644 --- a/apps/hwid_a_battery_widget/settings.js +++ b/apps/hwid_a_battery_widget/settings.js @@ -1,7 +1,7 @@ (back => { const S = require('Storage'); - const SETTINGS_FILE = "hwid_a_battery_widget.settings.json"; + const SETTINGS_FILE = "hwid_batt.settings.json"; const settings = S.readJSON(SETTINGS_FILE, 1) || { showHighMark: true, }; diff --git a/apps/hwid_a_battery_widget/widget.js b/apps/hwid_a_battery_widget/widget.js index db2a664ba..0054e7b1e 100644 --- a/apps/hwid_a_battery_widget/widget.js +++ b/apps/hwid_a_battery_widget/widget.js @@ -1,6 +1,6 @@ (function(){ const showHighMark = ( - require("Storage").readJSON("hwid_a_battery_widget.settings.json",1) || { + require("Storage").readJSON("hwid_batt.settings.json",1) || { showHighMark: true, }).showHighMark; From 0a894f15f0e258422f002f5e6d5f355a8e5427f5 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Sun, 11 Jun 2023 13:08:02 +0100 Subject: [PATCH 44/54] hwid-batt: note high-mark in readme --- apps/hwid_a_battery_widget/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/hwid_a_battery_widget/README.md b/apps/hwid_a_battery_widget/README.md index db105635a..fd7bbec67 100644 --- a/apps/hwid_a_battery_widget/README.md +++ b/apps/hwid_a_battery_widget/README.md @@ -8,6 +8,8 @@ Show the current battery level and charging status in the top right of the clock * Blue when charging * 40 pixels wide +The high-level marker (a little bar at the 100% point) can be toggled in settings. + ![](a_battery_widget-pic.jpg) ## Creator From ff9b3448f6874bd32563024b7b81794759cc24cf Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Sun, 11 Jun 2023 13:23:23 +0100 Subject: [PATCH 45/54] hwid-batt: handle filename length limit --- apps/hwid_a_battery_widget/metadata.json | 4 ++-- apps/hwid_a_battery_widget/settings.js | 2 +- apps/hwid_a_battery_widget/widget.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/hwid_a_battery_widget/metadata.json b/apps/hwid_a_battery_widget/metadata.json index 958c01c76..c06bff4cc 100644 --- a/apps/hwid_a_battery_widget/metadata.json +++ b/apps/hwid_a_battery_widget/metadata.json @@ -12,7 +12,7 @@ "provides_widgets" : ["battery"], "storage": [ {"name":"hwid_a_battery_widget.wid.js","url":"widget.js"}, - {"name":"hwid_a_battery_widget.setting.js","url":"settings.js"} + {"name":"hwid_battwid.setting.js","url":"settings.js"} ], - "data": [{"name":"hwid_batt.settings.json"}] + "data": [{"name":"hwid_battwid.settings.json"}] } diff --git a/apps/hwid_a_battery_widget/settings.js b/apps/hwid_a_battery_widget/settings.js index 910fb1452..a7623dc65 100644 --- a/apps/hwid_a_battery_widget/settings.js +++ b/apps/hwid_a_battery_widget/settings.js @@ -1,7 +1,7 @@ (back => { const S = require('Storage'); - const SETTINGS_FILE = "hwid_batt.settings.json"; + const SETTINGS_FILE = "hwid_battwid.settings.json"; const settings = S.readJSON(SETTINGS_FILE, 1) || { showHighMark: true, }; diff --git a/apps/hwid_a_battery_widget/widget.js b/apps/hwid_a_battery_widget/widget.js index 0054e7b1e..d23c98099 100644 --- a/apps/hwid_a_battery_widget/widget.js +++ b/apps/hwid_a_battery_widget/widget.js @@ -1,6 +1,6 @@ (function(){ const showHighMark = ( - require("Storage").readJSON("hwid_batt.settings.json",1) || { + require("Storage").readJSON("hwid_battwid.settings.json",1) || { showHighMark: true, }).showHighMark; From 5ca4db69d7636ee9578ab1366911ea886f754fb8 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Sun, 11 Jun 2023 14:36:40 +0100 Subject: [PATCH 46/54] swiperclocklaunch: ensure we clear old listeners too Previously we wouldn't clear old listeners, which meant that they'd remain hanging around, potentially interfering with the currently shown app/clock. We now remember to clear them too, so setUI behaves as a proper reset. See #2809 for more details. --- apps/swiperclocklaunch/boot.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/swiperclocklaunch/boot.js b/apps/swiperclocklaunch/boot.js index ea00a6735..11abb84c9 100644 --- a/apps/swiperclocklaunch/boot.js +++ b/apps/swiperclocklaunch/boot.js @@ -1,7 +1,13 @@ (function() { var sui = Bangle.setUI; + var oldSwipe; + Bangle.setUI = function(mode, cb) { + if (oldSwipe && oldSwipe !== Bangle.swipeHandler) + Bangle.removeListener("swipe", oldSwipe); sui(mode,cb); + oldSwipe = Bangle.swipeHandler; + if(!mode) return; if ("object"==typeof mode) mode = mode.mode; if (mode.startsWith("clock")) { From fc273a81c811477a4d85c51aae0ba9221fe0e891 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Sun, 11 Jun 2023 14:41:14 +0100 Subject: [PATCH 47/54] swiperclocklaunch: bump version --- apps/swiperclocklaunch/ChangeLog | 1 + apps/swiperclocklaunch/metadata.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/swiperclocklaunch/ChangeLog b/apps/swiperclocklaunch/ChangeLog index e7ad4555c..f62e10940 100644 --- a/apps/swiperclocklaunch/ChangeLog +++ b/apps/swiperclocklaunch/ChangeLog @@ -2,3 +2,4 @@ 0.02: Fix issue with mode being undefined 0.03: Update setUI to work with new Bangle.js 2v13 menu style 0.04: Update to work with new 'fast switch' clock->launcher functionality +0.05: Keep track of event listeners we "overwrite", and remove them at the start of setUI diff --git a/apps/swiperclocklaunch/metadata.json b/apps/swiperclocklaunch/metadata.json index 4f27da528..d46c56693 100644 --- a/apps/swiperclocklaunch/metadata.json +++ b/apps/swiperclocklaunch/metadata.json @@ -1,7 +1,7 @@ { "id": "swiperclocklaunch", "name": "Swiper Clock Launch", - "version": "0.04", + "version": "0.05", "description": "Navigate between clock and launcher with Swipe action", "icon": "swiperclocklaunch.png", "type": "bootloader", From f2674671173b5bd905078047cfb7d8bacd292d29 Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Fri, 12 May 2023 10:49:14 +0200 Subject: [PATCH 48/54] sensortools - Format README and add emulated routes as gpx --- apps/sensortools/README.md | 64 ++++++++------ apps/sensortools/square.gpx | 33 +++++++ apps/sensortools/squareFuzzy.gpx | 144 +++++++++++++++++++++++++++++++ 3 files changed, 217 insertions(+), 24 deletions(-) create mode 100644 apps/sensortools/square.gpx create mode 100644 apps/sensortools/squareFuzzy.gpx diff --git a/apps/sensortools/README.md b/apps/sensortools/README.md index 8b89add7c..f44a89090 100644 --- a/apps/sensortools/README.md +++ b/apps/sensortools/README.md @@ -5,40 +5,56 @@ This allows to simulate sensor behaviour for development purposes ## Per Sensor settings: -enabled: - true or false -mode: - emulate: Completely craft events for this sensor - modify: Take existing events from real sensor and modify their data -name: - name of the emulation or modification mode -power: - emulate: Simulate Bangle._PWR changes, but do not call real power function - nop: Do nothing, ignore all power calls for this sensor but return true - passthrough: Just pass all power calls unmodified - on: Do not allow switching the sensor off, all calls are switching the real sensor on +Enabled: +* **true** +* **false** + +Mode: +* **emulate**: Completely craft events for this sensor +* **modify**: Take existing events from real sensor and modify their data + +Name: +* name of the emulation or modification mode + +Power: +* **emulate**: Simulate Bangle._PWR changes, but do not call real power function +* **nop**: Do nothing, ignore all power calls for this sensor but return true +* **passthrough**: Just pass all power calls unmodified +* **on**: Do not allow switching the sensor off, all calls are switching the real sensor on ### HRM -Modes: modify, emulate +Modes: +* **modify**: Modify the original events from this sensor +* **emulate**: Create events simulating sensor activity + Modification: - bpmtrippled: Multiply the bpm value of the original HRM values with 3 +* **bpmtrippled**: Multiply the bpm value of the original HRM values with 3 + Emulation: - sin: Calculate bpm changes by using sin +* **sin**: Calculate bpm changes by using sin ### GPS -Modes: emulate +Modes: +* **emulate** + Emulation: - staticfix: static complete fix with all values - route: A square route starting in the SW corner and moving SW->NW->NO->SW... - routeFuzzy: Roughly the same square as route, but with 100m seqments with some variaton in course - nofix: All values NaN but time,sattelites,fix and fix == 0 - changingfix: A fix with randomly changing values +* **staticfix**: static complete fix with all values +* **route**: A square route starting in the SW corner and moving SW->NW->NO->SW... [Download as gpx](square.gpx) +* **routeFuzzy**: Roughly the same square as route, but with 100m seqments with some variaton in course [Download as gpx](squareFuzzy.gpx) +* **nofix**: All values NaN but time,sattelites,fix and fix == 0 +* **changingfix**: A fix with randomly changing values ### Compass -Modes: emulate +Modes: +* **emulate** + Emulation: - static: All values but heading are 1, heading == 0 - rotate: All values but heading are 1, heading rotates 360° +* **static**: All values but heading are 1, heading == 0 +* **rotate**: All values but heading are 1, heading rotates 360° + +# Creator + +[halemmerich](https://github.com/halemmerich) \ No newline at end of file diff --git a/apps/sensortools/square.gpx b/apps/sensortools/square.gpx new file mode 100644 index 000000000..0220b4261 --- /dev/null +++ b/apps/sensortools/square.gpx @@ -0,0 +1,33 @@ + + + + 1kmsquare + Export from GpsPrune + + + 1kmsquare + 1 + + + 2273 + Lower left + + + 2166 + Top left + + + 2245 + Top right + + + 1994 + Lower right + + + 2273 + Destination + + + + diff --git a/apps/sensortools/squareFuzzy.gpx b/apps/sensortools/squareFuzzy.gpx new file mode 100644 index 000000000..8f73cc72b --- /dev/null +++ b/apps/sensortools/squareFuzzy.gpx @@ -0,0 +1,144 @@ + + + + 1kmsquare100 + Export from GpsPrune + + + 1kmsquare100 + 1 + + + 2265 + Lower left + + + 2256 + + + 2230 + + + 2219 + + + 2199 + + + 2182 + + + 2189 + + + 2180 + + + 2191 + + + 2185 + + + 2186 + + + 2166 + Top left + + + 2171 + + + 2194 + + + 2207 + + + 2212 + + + 2262 + + + 2278 + + + 2297 + + + 2303 + + + 2251 + + + 2245 + Top right + + + 2195 + + + 2163 + + + 2131 + + + 2095 + + + 2066 + + + 2037 + + + 1993 + + + 1967 + + + 1949 + + + 1972 + + + 2011 + Lower right + + + 2044 + + + 2061 + + + 2065 + + + 2071 + + + 2102 + + + 2147 + + + 2197 + + + 2228 + + + 2265 + Destination + + + + From 560e39b900b55c6c67d9a4e5b463a8a1d98a323b Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Thu, 25 May 2023 22:38:21 +0200 Subject: [PATCH 49/54] sensortools - Fix interpolation and speed for route simulation --- apps/sensortools/lib.js | 135 +++++++++++++++++++--------------------- 1 file changed, 64 insertions(+), 71 deletions(-) diff --git a/apps/sensortools/lib.js b/apps/sensortools/lib.js index 7dfc6307d..85ed3be60 100644 --- a/apps/sensortools/lib.js +++ b/apps/sensortools/lib.js @@ -138,63 +138,63 @@ exports.enable = () => { function interpolate(a,b,progress){ return { - lat: a.lat * progress + b.lat * (1-progress), - lon: a.lon * progress + b.lon * (1-progress), - ele: a.ele * progress + b.ele * (1-progress) + lat: b.lat * progress + a.lat * (1-progress), + lon: b.lon * progress + a.lon * (1-progress), + alt: b.alt * progress + a.alt * (1-progress) } } function getSquareRoute(){ return [ - {lat:"47.2577411",lon:"11.9927442",ele:2273}, - {lat:"47.266761",lon:"11.9926673",ele:2166}, - {lat:"47.2667605",lon:"12.0059511",ele:2245}, - {lat:"47.2577516",lon:"12.0059925",ele:1994} + {lat:47.2577411,lon:11.9927442,alt:2273}, + {lat:47.266761,lon:11.9926673,alt:2166}, + {lat:47.2667605,lon:12.0059511,alt:2245}, + {lat:47.2577516,lon:12.0059925,alt:1994} ]; } function getSquareRouteFuzzy(){ return [ - {lat:"47.2578455",lon:"11.9929891",ele:2265}, - {lat:"47.258592",lon:"11.9923341",ele:2256}, - {lat:"47.2594506",lon:"11.9927412",ele:2230}, - {lat:"47.2603323",lon:"11.9924949",ele:2219}, - {lat:"47.2612056",lon:"11.9928175",ele:2199}, - {lat:"47.2621002",lon:"11.9929817",ele:2182}, - {lat:"47.2629025",lon:"11.9923915",ele:2189}, - {lat:"47.2637828",lon:"11.9926486",ele:2180}, - {lat:"47.2646733",lon:"11.9928167",ele:2191}, - {lat:"47.2655617",lon:"11.9930357",ele:2185}, - {lat:"47.2662862",lon:"11.992252",ele:2186}, - {lat:"47.2669305",lon:"11.993173",ele:2166}, - {lat:"47.266666",lon:"11.9944419",ele:2171}, - {lat:"47.2667579",lon:"11.99576",ele:2194}, - {lat:"47.2669409",lon:"11.9970579",ele:2207}, - {lat:"47.2666562",lon:"11.9983128",ele:2212}, - {lat:"47.2666027",lon:"11.9996335",ele:2262}, - {lat:"47.2667245",lon:"12.0009395",ele:2278}, - {lat:"47.2668457",lon:"12.002256",ele:2297}, - {lat:"47.2666126",lon:"12.0035373",ele:2303}, - {lat:"47.2664554",lon:"12.004841",ele:2251}, - {lat:"47.2669461",lon:"12.005948",ele:2245}, - {lat:"47.2660877",lon:"12.006323",ele:2195}, - {lat:"47.2652729",lon:"12.0057552",ele:2163}, - {lat:"47.2643926",lon:"12.0060123",ele:2131}, - {lat:"47.2634978",lon:"12.0058302",ele:2095}, - {lat:"47.2626129",lon:"12.0060759",ele:2066}, - {lat:"47.2617325",lon:"12.0058188",ele:2037}, - {lat:"47.2608668",lon:"12.0061784",ele:1993}, - {lat:"47.2600155",lon:"12.0057392",ele:1967}, - {lat:"47.2591203",lon:"12.0058233",ele:1949}, - {lat:"47.2582307",lon:"12.0059718",ele:1972}, - {lat:"47.2578014",lon:"12.004804",ele:2011}, - {lat:"47.2577232",lon:"12.0034834",ele:2044}, - {lat:"47.257745",lon:"12.0021656",ele:2061}, - {lat:"47.2578682",lon:"12.0008597",ele:2065}, - {lat:"47.2577082",lon:"11.9995526",ele:2071}, - {lat:"47.2575917",lon:"11.9982348",ele:2102}, - {lat:"47.2577401",lon:"11.996924",ele:2147}, - {lat:"47.257715",lon:"11.9956061",ele:2197}, - {lat:"47.2578996",lon:"11.9943081",ele:2228} + {lat:47.2578455,lon:11.9929891,alt:2265}, + {lat:47.258592,lon:11.9923341,alt:2256}, + {lat:47.2594506,lon:11.9927412,alt:2230}, + {lat:47.2603323,lon:11.9924949,alt:2219}, + {lat:47.2612056,lon:11.9928175,alt:2199}, + {lat:47.2621002,lon:11.9929817,alt:2182}, + {lat:47.2629025,lon:11.9923915,alt:2189}, + {lat:47.2637828,lon:11.9926486,alt:2180}, + {lat:47.2646733,lon:11.9928167,alt:2191}, + {lat:47.2655617,lon:11.9930357,alt:2185}, + {lat:47.2662862,lon:11.992252,alt:2186}, + {lat:47.2669305,lon:11.993173,alt:2166}, + {lat:47.266666,lon:11.9944419,alt:2171}, + {lat:47.2667579,lon:11.99576,alt:2194}, + {lat:47.2669409,lon:11.9970579,alt:2207}, + {lat:47.2666562,lon:11.9983128,alt:2212}, + {lat:47.2666027,lon:11.9996335,alt:2262}, + {lat:47.2667245,lon:12.0009395,alt:2278}, + {lat:47.2668457,lon:12.002256,alt:2297}, + {lat:47.2666126,lon:12.0035373,alt:2303}, + {lat:47.2664554,lon:12.004841,alt:2251}, + {lat:47.2669461,lon:12.005948,alt:2245}, + {lat:47.2660877,lon:12.006323,alt:2195}, + {lat:47.2652729,lon:12.0057552,alt:2163}, + {lat:47.2643926,lon:12.0060123,alt:2131}, + {lat:47.2634978,lon:12.0058302,alt:2095}, + {lat:47.2626129,lon:12.0060759,alt:2066}, + {lat:47.2617325,lon:12.0058188,alt:2037}, + {lat:47.2608668,lon:12.0061784,alt:1993}, + {lat:47.2600155,lon:12.0057392,alt:1967}, + {lat:47.2591203,lon:12.0058233,alt:1949}, + {lat:47.2582307,lon:12.0059718,alt:1972}, + {lat:47.2578014,lon:12.004804,alt:2011}, + {lat:47.2577232,lon:12.0034834,alt:2044}, + {lat:47.257745,lon:12.0021656,alt:2061}, + {lat:47.2578682,lon:12.0008597,alt:2065}, + {lat:47.2577082,lon:11.9995526,alt:2071}, + {lat:47.2575917,lon:11.9982348,alt:2102}, + {lat:47.2577401,lon:11.996924,alt:2147}, + {lat:47.257715,lon:11.9956061,alt:2197}, + {lat:47.2578996,lon:11.9943081,alt:2228} ]; } @@ -215,51 +215,43 @@ exports.enable = () => { let interpSteps; if (settings.gps.name == "routeFuzzy"){ route = getSquareRouteFuzzy(); - interpSteps = 5; + interpSteps = 74; } else { route = getSquareRoute(); - interpSteps = 50; + interpSteps = 740; } let step = 0; let routeIndex = 0; modGps(() => { let newIndex = (routeIndex + 1)%route.length; - + let followingIndex = (routeIndex + 2)%route.length; + let result = { - "speed": Math.random() * 3 + 2, + "speed": Math.random()*1 + 4.5, "time": new Date(), "satellites": Math.floor(Math.random()*5)+3, "fix": 1, "hdop": Math.floor(Math.random(30)+1) }; - + let oldPos = route[routeIndex]; - if (step != 0){ - oldPos = interpolate(route[routeIndex], route[newIndex], E.clip(0,1,step/interpSteps)); - } let newPos = route[newIndex]; - if (step < interpSteps - 1){ - newPos = interpolate(route[routeIndex], route[newIndex], E.clip(0,1,(step+1)%interpSteps/interpSteps)); + let followingPos = route[followingIndex]; + let interpPos = interpolate(oldPos, newPos, E.clip(0,1,step/interpSteps)); + + if (step > 0.5* interpSteps) { + result.course = bearing(interpPos, interpolate(newPos, followingPos, E.clip(0,1,(step-0.5*interpSteps)/interpSteps))); + } else { + result.course = bearing(oldPos, newPos); } - if (step == interpSteps - 1){ - let followingIndex = (routeIndex + 2)%route.length; - newPos = interpolate(route[newIndex], route[followingIndex], E.clip(0,1,1/interpSteps)); - } - - result.lat = oldPos.lat; - result.lon = oldPos.lon; - result.alt = oldPos.ele; - - result.course = bearing(oldPos,newPos); - step++; if (step == interpSteps){ routeIndex = (routeIndex + 1) % route.length; step = 0; } - return result; + return Object.assign(result, interpPos); }); } else if (settings.gps.name == "nofix") { modGps(() => { return { @@ -281,7 +273,8 @@ exports.enable = () => { let currentDir=1000; let currentAlt=500; let currentSats=5; - modGps(() => { + + (() => { currentLat += 0.01; if (currentLat > 50) currentLat = 20; currentLon += 0.01; From d884e21a8ec6d0ed55b67ef63198a00a8d428da6 Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Sun, 28 May 2023 22:13:30 +0200 Subject: [PATCH 50/54] sensortools - Add setting for log --- apps/sensortools/default.json | 1 + apps/sensortools/lib.js | 1 + apps/sensortools/settings.js | 6 ++++++ 3 files changed, 8 insertions(+) diff --git a/apps/sensortools/default.json b/apps/sensortools/default.json index a85e1ddeb..0e0d0a9af 100644 --- a/apps/sensortools/default.json +++ b/apps/sensortools/default.json @@ -1,5 +1,6 @@ { "enabled": false, + "log": false, "mag": { "enabled": false, "mode": "emulate", diff --git a/apps/sensortools/lib.js b/apps/sensortools/lib.js index 85ed3be60..f0155d289 100644 --- a/apps/sensortools/lib.js +++ b/apps/sensortools/lib.js @@ -5,6 +5,7 @@ exports.enable = () => { ); let log = function(text, param) { + if (!settings.log) return; let logline = new Date().toISOString() + " - " + "Sensortools - " + text; if (param) logline += ": " + JSON.stringify(param); print(logline); diff --git a/apps/sensortools/settings.js b/apps/sensortools/settings.js index 231ab8467..ae631e60c 100644 --- a/apps/sensortools/settings.js +++ b/apps/sensortools/settings.js @@ -88,6 +88,12 @@ writeSettings("enabled",v); }, }, + 'Log': { + value: !!settings.log, + onchange: v => { + writeSettings("log",v); + }, + }, 'GPS': ()=>{showSubMenu("GPS","gps",["nop", "staticfix", "nofix", "changingfix", "route", "routeFuzzy"],[]);}, 'Compass': ()=>{showSubMenu("Compass","mag",["nop", "static", "rotate"],[]);}, 'HRM': ()=>{showSubMenu("HRM","hrm",["nop", "static"],["bpmtrippled"],["sin"]);} From 9642b546e28fced5728ac7e330ad3798175f2316 Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Sun, 11 Jun 2023 20:35:12 +0200 Subject: [PATCH 51/54] sensortools - Bump version --- apps/sensortools/ChangeLog | 2 ++ apps/sensortools/metadata.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/sensortools/ChangeLog b/apps/sensortools/ChangeLog index 7d9bdd6a8..6d2f5d2b4 100644 --- a/apps/sensortools/ChangeLog +++ b/apps/sensortools/ChangeLog @@ -2,3 +2,5 @@ 0.02: Less time used during boot if disabled 0.03: Fixed some test data 0.04: Correct type of time attribute in gps to Date +0.05: Fix gps emulation interpolation + Add setting for log output diff --git a/apps/sensortools/metadata.json b/apps/sensortools/metadata.json index f5bace383..48b146617 100644 --- a/apps/sensortools/metadata.json +++ b/apps/sensortools/metadata.json @@ -2,7 +2,7 @@ "id": "sensortools", "name": "Sensor tools", "shortName": "Sensor tools", - "version": "0.04", + "version": "0.05", "description": "Tools for testing and debugging apps that use sensor input", "icon": "icon.png", "type": "bootloader", From c8070a7d1446ddead245162032b9b286d43af40c Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Sun, 11 Jun 2023 20:43:56 +0200 Subject: [PATCH 52/54] sensortools - Fix changingfix emulation --- apps/sensortools/lib.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sensortools/lib.js b/apps/sensortools/lib.js index f0155d289..5e1c199c2 100644 --- a/apps/sensortools/lib.js +++ b/apps/sensortools/lib.js @@ -275,7 +275,7 @@ exports.enable = () => { let currentAlt=500; let currentSats=5; - (() => { + modGps(() => { currentLat += 0.01; if (currentLat > 50) currentLat = 20; currentLon += 0.01; From 786b8db814a1e19086357945125c4ae4c9d4c39c Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Sun, 11 Jun 2023 21:57:41 +0100 Subject: [PATCH 53/54] hwid-batt: drop high mark altogether --- apps/hwid_a_battery_widget/metadata.json | 6 ++---- apps/hwid_a_battery_widget/settings.js | 22 ---------------------- apps/hwid_a_battery_widget/widget.js | 7 ------- 3 files changed, 2 insertions(+), 33 deletions(-) delete mode 100644 apps/hwid_a_battery_widget/settings.js diff --git a/apps/hwid_a_battery_widget/metadata.json b/apps/hwid_a_battery_widget/metadata.json index c06bff4cc..73dfc7c92 100644 --- a/apps/hwid_a_battery_widget/metadata.json +++ b/apps/hwid_a_battery_widget/metadata.json @@ -11,8 +11,6 @@ "tags": "widget,battery", "provides_widgets" : ["battery"], "storage": [ - {"name":"hwid_a_battery_widget.wid.js","url":"widget.js"}, - {"name":"hwid_battwid.setting.js","url":"settings.js"} - ], - "data": [{"name":"hwid_battwid.settings.json"}] + {"name":"hwid_a_battery_widget.wid.js","url":"widget.js"} + ] } diff --git a/apps/hwid_a_battery_widget/settings.js b/apps/hwid_a_battery_widget/settings.js deleted file mode 100644 index a7623dc65..000000000 --- a/apps/hwid_a_battery_widget/settings.js +++ /dev/null @@ -1,22 +0,0 @@ -(back => { - const S = require('Storage'); - - const SETTINGS_FILE = "hwid_battwid.settings.json"; - const settings = S.readJSON(SETTINGS_FILE, 1) || { - showHighMark: true, - }; - - const save = () => S.write(SETTINGS_FILE, settings); - - E.showMenu({ - '': { 'title': 'Battery Widget (hank mod)' }, - '< Back': back, - 'Show high mark': { - value: settings.showHighMark, - onchange: v => { - settings.showHighMark = v; - save(); - }, - }, - }); -}) diff --git a/apps/hwid_a_battery_widget/widget.js b/apps/hwid_a_battery_widget/widget.js index d23c98099..2c7bd2568 100644 --- a/apps/hwid_a_battery_widget/widget.js +++ b/apps/hwid_a_battery_widget/widget.js @@ -1,9 +1,4 @@ (function(){ - const showHighMark = ( - require("Storage").readJSON("hwid_battwid.settings.json",1) || { - showHighMark: true, - }).showHighMark; - const intervalLow = 60000; // update time when not charging const intervalHigh = 2000; // update time when charging var old_l; @@ -53,8 +48,6 @@ g.setColor(levelColor(l)); g.fillRect(x+4,y+14+3,xl,y+16+3); // charging bar - if (showHighMark) - g.fillRect((x+4+100*(s-12)/100)-1,y+14+3,x+4+100*(s-12)/100,y+16+3); // charging bar "full mark" // Show percentage g.setColor(COLORS.black); g.setFontAlign(0,0); From 37236b254e88f06742d82b16da7351d9a70c70d4 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Sun, 11 Jun 2023 22:05:18 +0100 Subject: [PATCH 54/54] hwid-batt: simplify clearing old widget --- apps/hwid_a_battery_widget/widget.js | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/apps/hwid_a_battery_widget/widget.js b/apps/hwid_a_battery_widget/widget.js index 2c7bd2568..027535051 100644 --- a/apps/hwid_a_battery_widget/widget.js +++ b/apps/hwid_a_battery_widget/widget.js @@ -1,7 +1,6 @@ (function(){ const intervalLow = 60000; // update time when not charging const intervalHigh = 2000; // update time when charging - var old_l; var old_x = this.x; var old_y = this.y; @@ -22,29 +21,15 @@ }; function draw() { - if (typeof old_x === 'undefined') old_x = this.x; - if (typeof old_y === 'undefined') old_y = this.y; var s = width - 1; var x = this.x; var y = this.y; if ((typeof x === 'undefined') || (typeof y === 'undefined')) { } else { + g.clearRect(old_x, old_y, old_x + width, old_y + height); + const l = E.getBattery(); // debug: Math.floor(Math.random() * 101); let xl = x+4+l*(s-12)/100; - if ((l != old_l) && (typeof old_l != 'undefined') ){ // Delete the old value from screen - let xl_old = x+4+old_l*(s-12)/100; - g.setColor(COLORS.white); - // g.fillRect(x+2,y+5,x+s-6,y+18); - g.fillRect(x,y,xl+4,y+16+3); //Clear - g.setFontAlign(0,0); - g.setFont('Vector',16); - //g.fillRect(old_x,old_y,old_x+4+l*(s-12)/100,old_y+16+3); // clear (lazy) - g.drawString(old_l, old_x + 14, old_y + 10); - g.fillRect(x+4,y+14+3,xl_old,y+16+3); // charging bar - - } - old_l = l; - //console.log(old_x); g.setColor(levelColor(l)); g.fillRect(x+4,y+14+3,xl,y+16+3); // charging bar @@ -65,6 +50,7 @@ Bangle.on('charging',function(charging) { draw(); }); var id = setInterval(()=>WIDGETS["hwid_a_battery_widget"].draw(), intervalLow); var width = 30; + var height = 19; WIDGETS["hwid_a_battery_widget"]={area:"tr",width,draw:draw}; })();

DateTypeDate/Time Summary
Date/Time Summary
Type Date/Time SummaryOn?