From 4301342292d4ae2a278bfff56d9d3cfac2aca2b6 Mon Sep 17 00:00:00 2001 From: Marco Heiming Date: Wed, 8 Apr 2020 12:58:39 +0200 Subject: [PATCH 001/302] Reload apps after successful upload to get a fresh list of apps and files --- js/comms.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/js/comms.js b/js/comms.js index eb453871d..305dd26d3 100644 --- a/js/comms.js +++ b/js/comms.js @@ -23,6 +23,8 @@ uploadApp : (app,skipReset) => { // expects an apps.json structure (i.e. with `s Puck.write(`\x10E.showMessage('Hold BTN3\\nto reload')\n`,(result) => { Progress.hide({sticky:true}); if (result===null) return reject(""); + // Reload apps to get a fresh list of apps and files + this.getInstalledApps(); resolve(app); }); return; From dfad1218d630bf0acb684e91f879924b6a0763c5 Mon Sep 17 00:00:00 2001 From: Marco Heiming Date: Wed, 8 Apr 2020 13:02:36 +0200 Subject: [PATCH 002/302] Force reload --- js/comms.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/comms.js b/js/comms.js index 305dd26d3..7010660f4 100644 --- a/js/comms.js +++ b/js/comms.js @@ -24,7 +24,7 @@ uploadApp : (app,skipReset) => { // expects an apps.json structure (i.e. with `s Progress.hide({sticky:true}); if (result===null) return reject(""); // Reload apps to get a fresh list of apps and files - this.getInstalledApps(); + this.getInstalledApps(true); resolve(app); }); return; From 0aad2a3082f47554b2094bbd3f2c4f7fd486cab1 Mon Sep 17 00:00:00 2001 From: unmotivagedgene Date: Thu, 9 Apr 2020 18:29:49 -0500 Subject: [PATCH 003/302] Added NATO Alphabet app files --- apps/nato/appsManifestEntry.txt | 13 ++++ apps/nato/nato-icon.js | 1 + apps/nato/nato.js | 116 ++++++++++++++++++++++++++++++++ apps/nato/nato.png | Bin 0 -> 2023 bytes 4 files changed, 130 insertions(+) create mode 100644 apps/nato/appsManifestEntry.txt create mode 100644 apps/nato/nato-icon.js create mode 100644 apps/nato/nato.js create mode 100644 apps/nato/nato.png diff --git a/apps/nato/appsManifestEntry.txt b/apps/nato/appsManifestEntry.txt new file mode 100644 index 000000000..2e8d840e0 --- /dev/null +++ b/apps/nato/appsManifestEntry.txt @@ -0,0 +1,13 @@ + { "id": "nato", + "name": "NATO Alphabet", + "shortName" : "NATOAlphabet", + "icon": "nato.png", + "version":"0.01", + "type": "app", + "description": "Learn the NATO Phonetic alphabet plus some numbers.", + "tags": "app,learn,visual", + "storage": [ + {"name":"nato.app.js","url":"nato.js"}, + {"name":"nato.img","url":"nato-icon.js","evaluate":true} + ], + }, \ No newline at end of file diff --git a/apps/nato/nato-icon.js b/apps/nato/nato-icon.js new file mode 100644 index 000000000..b1a6e0947 --- /dev/null +++ b/apps/nato/nato-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwgFCiIABiAGFiINJAAUS///CAgGEgMT//zBoYXFmIiCC40fEooXF+QXJn4lCC5ARDC4oFC//xMAoXDJAQXFBgY9DC4wKCC4p2CPA4XDCQQXEOwXxPA4XBEQJICC4p2BmICCC44KBJAIXEiIJBkMvPAwXCWgYXFAgQMBPAoXCBwUxC4jtDeI4XDJAQXDFYXxHAoXGJAYXDLYPykUieIwXDJAYXDG4IAEPAgXCRgJICPYoAEPAgXDZ4TcDmYXGMAgXDUAZiEPwIABCALEBC5BZC+YQCRwRsEC45ID+S5BCAkBEYJ4DC4hID+IbCIAYjCCIYXGEgMxXoJwEgI3CA4JQDAAwaBmQGDFIQ3CC5UzkSLBdwIIDmYXCWY4jBCAJBCPYQ0EC5bXGkLuDC5QtEAAXzPoZMCmZwB+YFCbYkykQFCVoZMDWALnDQwRjDeoZIDZAgJCWwYeBFATWFC5LuHawgXKdwyJDD4YXIOAMzH4gICmIXKEwQXXkQXFKAKQFC85HNO64XDU44XMX48Sa5zvCmJICA4YXLE4fziIACJ4PyM4gXHCAQwBCwI2GC5JADAApGFC5ERmYWFFwwXHDARJCMgYWFB4MTmYiFLgMjCwMyiIuGE4QABNIyPDBQgA==")) \ No newline at end of file diff --git a/apps/nato/nato.js b/apps/nato/nato.js new file mode 100644 index 000000000..7e1e06db7 --- /dev/null +++ b/apps/nato/nato.js @@ -0,0 +1,116 @@ +/** + * Teach a user the NATO Phonetic Alphabet + numbers +*/ + +/** + * Constants +*/ +const FONT_NAME = 'Vector12'; +const FONT_SIZE = 80; +const SCREEN_PIXELS = 240; +const UNIT = 100; +const NATO_MAP = { + A: 'ALFA', + B: 'BRAVO', + C: 'CHARLIE', + D: 'DELTA', + E: 'ECHO', + F: 'FOXTROT', + G: 'GOLF', + H: 'HOTEL', + I: 'INDIA', + J: 'JULIETT', + K: 'KILO', + L: 'LIMA', + M: 'MIKE', + N: 'NOVEMBER', + O: 'OSCAR', + P: 'PAPA', + Q: 'QUEBEC', + R: 'ROMEO', + S: 'SIERRA', + T: 'TANGO', + U: 'UNIFORM', + V: 'VICTOR', + W: 'WHISKEY', + X: 'X-RAY', + Y: 'YANKEE', + Z: 'ZULU', + '0': 'ZE-RO', + '1': 'WUN', + '2': 'TOO', + '3': 'TREE', + '4': 'FOW-ER', + '5': 'FIFE', + '6': 'SIX', + '7': 'SEV-EN', + '8': 'AIT', + '9': 'NIN-ER', +}; + +/** + * Set the local state +*/ +let INDEX = 0; +let showLetter = true; + +/** + * Utility functions for writing text, changing state +*/ +const writeText = (txt) => { + g.clear(); + g.setFont(FONT_NAME, FONT_SIZE); + + var width = g.stringWidth(txt); + + var fontFix = FONT_SIZE; + while(width > SCREEN_PIXELS-10){ + fontFix--; + g.setFont(FONT_NAME, fontFix); + width = g.stringWidth(txt); + } + g.drawString(txt, (SCREEN_PIXELS / 2) - (width / 2), SCREEN_PIXELS / 2); +}; +const writeLetter = () => { + writeText(Object.keys(NATO_MAP)[INDEX]); +}; +const writeCode = () => { + writeText(NATO_MAP[Object.keys(NATO_MAP)[INDEX]]); +}; +const toggle = () => { + showLetter = !showLetter; + if(showLetter){ + writeLetter(); + }else { + writeCode(); + } +}; + +/** + * Bootstrapping +*/ +g.clear(); +g.setFont(FONT_NAME, FONT_SIZE); +g.setColor(0, 1, 0); +g.setFontAlign(-1, 0, 0); + + +const step = (positive) => () => { + if (positive) { + INDEX = INDEX + 1; + if (INDEX > Object.keys(NATO_MAP).length - 1) INDEX = 0; + } else { + INDEX = INDEX - 1; + if (INDEX < 0) INDEX = Object.keys(NATO_MAP).length - 1; + } + showLetter = true; + writeLetter(); +}; + +writeLetter(); + +// Press the middle button to see the NATO Phonetic wording +setWatch(toggle, BTN2, { repeat: true }); +// Allow user to switch between letters +setWatch(step(true), BTN1, { repeat: true }); +setWatch(step(false), BTN3, { repeat: true }); diff --git a/apps/nato/nato.png b/apps/nato/nato.png new file mode 100644 index 0000000000000000000000000000000000000000..bd4678c11f489649f765c47e73d04cd94cbe0a03 GIT binary patch literal 2023 zcmV^5HKn!Rq~;Y;-8uZY{ZHnw;Kf|H|4jzx6ID%-rbsu?R>i1*_nC2 zdB6AOy*DdJX^}|eH6g@r>NEAX5aQ!lEcUHezhM~CZ=95})3U73^!{)-ToDR|eonDo zNh}uo#jhXFg%^Ov#zxw)V~5imJOoSdA(M~@!;sZYuPZr!@&bltmmk5;c< zO@08P(P)d5vZ=4HkCrW4=8PK|8KI3EHA9AR)7iAtRyosyHY%O2U%yUORaLNq4$HFYvj)J6N|^#! zuwX&>@#Du9gn_q-L?X6amSs^;0x_Rz4R1Bs7L$R1wh+ zUQ|+rQ2_ufn(Bs?9reWE6)Wx8vxm-{IRh%aZdumvfi=JxStJta@S_q-bLrA0s;Q|V zFF`nTNVogCmX!#=Hs83S=c}_^>UgU+E+h6i6#^79cCb35@JdO=^+= zU@{N^!muXpd>r5iVH*pFZmM6nF@L%0X(5b$@o)dgKjKKR203_Qv zIXN$TNhGEG(5nc=0C1qFHv#-lQT zt9LpL)!YP;an=F&(G58*8Y*pm0h|heHL26+KLY^nqv81HWF(af^UDr7E91+L7p8%1 z0nl%K{;m2RNr*tQ*A)4D)`ZeZ*aVv0UQC}5@j6#FO2ZFqN2i) zPxV4Vh?ZC^)||=)xCpH0#C-L{b<(T@m^W`;0SdT!5`v(Ny-=dw?}P(75T=h;BEuy{ zzWmEN00@)n{!8KT)lew(iGB|Z;e`-i3L)OmcVDRsoit5ziz^mN zGp~FY{hX91);uAE7{_lZWib)Ghu=B{@hoj+E*r5B=1@ZczgpG z0HUD*7n9_ftfZnZE;q0N3BymiC!0|K34^w;R9Q$IOl#JxamD#w0FXBf4i0iEiB}r) z^76h)c}JGc2K00@kz648PPq|YU!}mnar0SNSm;`IY;4T_b^k^)6j%TN002ovPDHLk FV1jvc#zp`D literal 0 HcmV?d00001 From 866b6ca6355565ce51fcfb523a629064bdf88528 Mon Sep 17 00:00:00 2001 From: unmotivagedgene Date: Thu, 9 Apr 2020 18:46:32 -0500 Subject: [PATCH 004/302] Added NATO Alphabet to manifest --- apps.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/apps.json b/apps.json index d85739aad..d432b861b 100644 --- a/apps.json +++ b/apps.json @@ -1159,4 +1159,17 @@ {"name":"batchart.img","url":"app-icon.js","evaluate":true} ] } + { "id": "nato", + "name": "NATO Alphabet", + "shortName" : "NATOAlphabet", + "icon": "nato.png", + "version":"0.01", + "type": "app", + "description": "Learn the NATO Phonetic alphabet plus some numbers.", + "tags": "app,learn,visual", + "storage": [ + {"name":"nato.app.js","url":"nato.js"}, + {"name":"nato.img","url":"nato-icon.js","evaluate":true} + ], + }, ] From b833c39ac2d2fdfca70bdb41bb8d214e97b6bde4 Mon Sep 17 00:00:00 2001 From: unmotivagedgene Date: Thu, 9 Apr 2020 18:48:07 -0500 Subject: [PATCH 005/302] ...missed a comma --- apps.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps.json b/apps.json index d432b861b..b18488bbc 100644 --- a/apps.json +++ b/apps.json @@ -1158,7 +1158,7 @@ {"name":"batchart.app.js","url":"app.js"}, {"name":"batchart.img","url":"app-icon.js","evaluate":true} ] - } + }, { "id": "nato", "name": "NATO Alphabet", "shortName" : "NATOAlphabet", From a9c5b7341715de222f901909844ab5fe54fcb9e3 Mon Sep 17 00:00:00 2001 From: unmotivagedgene Date: Thu, 9 Apr 2020 18:49:00 -0500 Subject: [PATCH 006/302] more commas --- apps.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps.json b/apps.json index b18488bbc..c2377adcf 100644 --- a/apps.json +++ b/apps.json @@ -1171,5 +1171,5 @@ {"name":"nato.app.js","url":"nato.js"}, {"name":"nato.img","url":"nato-icon.js","evaluate":true} ], - }, + } ] From 6126a34bfa043fba7b1f606e2bc8346bb2b24a81 Mon Sep 17 00:00:00 2001 From: unmotivagedgene Date: Thu, 9 Apr 2020 18:50:19 -0500 Subject: [PATCH 007/302] , --- apps.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps.json b/apps.json index c2377adcf..2216f6671 100644 --- a/apps.json +++ b/apps.json @@ -1170,6 +1170,6 @@ "storage": [ {"name":"nato.app.js","url":"nato.js"}, {"name":"nato.img","url":"nato-icon.js","evaluate":true} - ], + ] } ] From 6d31f60055211a3179d3a7e8841c667c5730b79d Mon Sep 17 00:00:00 2001 From: unmotivagedgene Date: Fri, 10 Apr 2020 07:30:38 -0500 Subject: [PATCH 008/302] allow emulator for NATO --- apps.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apps.json b/apps.json index 2216f6671..20ef55e38 100644 --- a/apps.json +++ b/apps.json @@ -1167,6 +1167,7 @@ "type": "app", "description": "Learn the NATO Phonetic alphabet plus some numbers.", "tags": "app,learn,visual", + "allow_emulator":true, "storage": [ {"name":"nato.app.js","url":"nato.js"}, {"name":"nato.img","url":"nato-icon.js","evaluate":true} From 4072bb68697923bf3a9a7e86bf5c8ced381e9919 Mon Sep 17 00:00:00 2001 From: unmotivagedgene Date: Fri, 10 Apr 2020 07:36:40 -0500 Subject: [PATCH 009/302] Removing app manifest txt This has been added to the main manifest. --- apps/nato/appsManifestEntry.txt | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 apps/nato/appsManifestEntry.txt diff --git a/apps/nato/appsManifestEntry.txt b/apps/nato/appsManifestEntry.txt deleted file mode 100644 index 2e8d840e0..000000000 --- a/apps/nato/appsManifestEntry.txt +++ /dev/null @@ -1,13 +0,0 @@ - { "id": "nato", - "name": "NATO Alphabet", - "shortName" : "NATOAlphabet", - "icon": "nato.png", - "version":"0.01", - "type": "app", - "description": "Learn the NATO Phonetic alphabet plus some numbers.", - "tags": "app,learn,visual", - "storage": [ - {"name":"nato.app.js","url":"nato.js"}, - {"name":"nato.img","url":"nato-icon.js","evaluate":true} - ], - }, \ No newline at end of file From 987be3201b580cf852aa8f9b86631689ef89d614 Mon Sep 17 00:00:00 2001 From: unmotivagedgene Date: Fri, 10 Apr 2020 07:40:29 -0500 Subject: [PATCH 010/302] Cleanup and comments --- apps/nato/nato.js | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/apps/nato/nato.js b/apps/nato/nato.js index 7e1e06db7..f4301b83f 100644 --- a/apps/nato/nato.js +++ b/apps/nato/nato.js @@ -1,10 +1,6 @@ -/** - * Teach a user the NATO Phonetic Alphabet + numbers -*/ +// Teach a user the NATO Phonetic Alphabet + numbers +// Based on the Morse Code app -/** - * Constants -*/ const FONT_NAME = 'Vector12'; const FONT_SIZE = 80; const SCREEN_PIXELS = 240; @@ -48,21 +44,16 @@ const NATO_MAP = { '9': 'NIN-ER', }; -/** - * Set the local state -*/ let INDEX = 0; let showLetter = true; -/** - * Utility functions for writing text, changing state -*/ const writeText = (txt) => { g.clear(); g.setFont(FONT_NAME, FONT_SIZE); var width = g.stringWidth(txt); + // Fit text to screen var fontFix = FONT_SIZE; while(width > SCREEN_PIXELS-10){ fontFix--; @@ -86,9 +77,8 @@ const toggle = () => { } }; -/** - * Bootstrapping -*/ +// Bootstrapping + g.clear(); g.setFont(FONT_NAME, FONT_SIZE); g.setColor(0, 1, 0); @@ -103,7 +93,7 @@ const step = (positive) => () => { INDEX = INDEX - 1; if (INDEX < 0) INDEX = Object.keys(NATO_MAP).length - 1; } - showLetter = true; + showLetter = true; // for toggle() writeLetter(); }; From ada96912f78107e00272e6897f75c78d6b160e9c Mon Sep 17 00:00:00 2001 From: unmotivagedgene Date: Fri, 10 Apr 2020 07:46:44 -0500 Subject: [PATCH 011/302] Added newline to the end of apps/nato/nato-icon.js --- apps/nato/nato-icon.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/nato/nato-icon.js b/apps/nato/nato-icon.js index b1a6e0947..ae38c0274 100644 --- a/apps/nato/nato-icon.js +++ b/apps/nato/nato-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mEwwgFCiIABiAGFiINJAAUS///CAgGEgMT//zBoYXFmIiCC40fEooXF+QXJn4lCC5ARDC4oFC//xMAoXDJAQXFBgY9DC4wKCC4p2CPA4XDCQQXEOwXxPA4XBEQJICC4p2BmICCC44KBJAIXEiIJBkMvPAwXCWgYXFAgQMBPAoXCBwUxC4jtDeI4XDJAQXDFYXxHAoXGJAYXDLYPykUieIwXDJAYXDG4IAEPAgXCRgJICPYoAEPAgXDZ4TcDmYXGMAgXDUAZiEPwIABCALEBC5BZC+YQCRwRsEC45ID+S5BCAkBEYJ4DC4hID+IbCIAYjCCIYXGEgMxXoJwEgI3CA4JQDAAwaBmQGDFIQ3CC5UzkSLBdwIIDmYXCWY4jBCAJBCPYQ0EC5bXGkLuDC5QtEAAXzPoZMCmZwB+YFCbYkykQFCVoZMDWALnDQwRjDeoZIDZAgJCWwYeBFATWFC5LuHawgXKdwyJDD4YXIOAMzH4gICmIXKEwQXXkQXFKAKQFC85HNO64XDU44XMX48Sa5zvCmJICA4YXLE4fziIACJ4PyM4gXHCAQwBCwI2GC5JADAApGFC5ERmYWFFwwXHDARJCMgYWFB4MTmYiFLgMjCwMyiIuGE4QABNIyPDBQgA==")) \ No newline at end of file +require("heatshrink").decompress(atob("mEwwgFCiIABiAGFiINJAAUS///CAgGEgMT//zBoYXFmIiCC40fEooXF+QXJn4lCC5ARDC4oFC//xMAoXDJAQXFBgY9DC4wKCC4p2CPA4XDCQQXEOwXxPA4XBEQJICC4p2BmICCC44KBJAIXEiIJBkMvPAwXCWgYXFAgQMBPAoXCBwUxC4jtDeI4XDJAQXDFYXxHAoXGJAYXDLYPykUieIwXDJAYXDG4IAEPAgXCRgJICPYoAEPAgXDZ4TcDmYXGMAgXDUAZiEPwIABCALEBC5BZC+YQCRwRsEC45ID+S5BCAkBEYJ4DC4hID+IbCIAYjCCIYXGEgMxXoJwEgI3CA4JQDAAwaBmQGDFIQ3CC5UzkSLBdwIIDmYXCWY4jBCAJBCPYQ0EC5bXGkLuDC5QtEAAXzPoZMCmZwB+YFCbYkykQFCVoZMDWALnDQwRjDeoZIDZAgJCWwYeBFATWFC5LuHawgXKdwyJDD4YXIOAMzH4gICmIXKEwQXXkQXFKAKQFC85HNO64XDU44XMX48Sa5zvCmJICA4YXLE4fziIACJ4PyM4gXHCAQwBCwI2GC5JADAApGFC5ERmYWFFwwXHDARJCMgYWFB4MTmYiFLgMjCwMyiIuGE4QABNIyPDBQgA==")) From d9a9bae5eea9450acccae6f9a65b502311d478e9 Mon Sep 17 00:00:00 2001 From: Marco Heiming Date: Fri, 10 Apr 2020 17:42:14 +0200 Subject: [PATCH 012/302] Move LCD Brightness menu into more general LCD menu & unify writings --- apps.json | 2 +- apps/setting/ChangeLog | 1 + apps/setting/settings.js | 40 ++++++++++++++++++++-------------------- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/apps.json b/apps.json index d85739aad..a8390b927 100644 --- a/apps.json +++ b/apps.json @@ -119,7 +119,7 @@ { "id": "setting", "name": "Settings", "icon": "settings.png", - "version":"0.12", + "version":"0.13", "description": "A menu for setting up Bangle.js", "tags": "tool,system", "storage": [ diff --git a/apps/setting/ChangeLog b/apps/setting/ChangeLog index 22277968c..c3109dda6 100644 --- a/apps/setting/ChangeLog +++ b/apps/setting/ChangeLog @@ -12,3 +12,4 @@ 0.12: Fix memory leak (#206) Bring App settings nearer the top Move LCD Timeout to wakeup menu +0.13: Move LCD Brightness menu into more general LCD menu diff --git a/apps/setting/settings.js b/apps/setting/settings.js index ac7692610..c6be52191 100644 --- a/apps/setting/settings.js +++ b/apps/setting/settings.js @@ -89,17 +89,6 @@ function showMainMenu() { updateSettings(); } }, - 'LCD Brightness': { - value: settings.brightness, - min: 0.1, - max: 1, - step: 0.1, - onchange: v => { - settings.brightness = v || 1; - updateSettings(); - Bangle.setLCDBrightness(settings.brightness); - } - }, 'Beep': { value: 0 | beepV.indexOf(settings.beep), min: 0, max: 2, @@ -134,7 +123,7 @@ function showMainMenu() { } }, 'Set Time': ()=>showSetTimeMenu(), - 'LCD Wake-Up': ()=>showWakeUpMenu(), + 'LCD': ()=>showLCDMenu(), 'Reset Settings': ()=>showResetMenu(), 'Turn Off': ()=>Bangle.off(), '< Back': ()=>load() @@ -142,10 +131,21 @@ function showMainMenu() { return E.showMenu(mainmenu); } -function showWakeUpMenu() { - const wakeUpMenu = { - '': { 'title': 'LCD Wake-Up' }, +function showLCDMenu() { + const lcdMenu = { + '': { 'title': 'LCD' }, '< Back': ()=>showMainMenu(), + 'LCD Brightness': { + value: settings.brightness, + min: 0.1, + max: 1, + step: 0.1, + onchange: v => { + settings.brightness = v || 1; + updateSettings(); + Bangle.setLCDBrightness(settings.brightness); + } + }, 'LCD Timeout': { value: settings.timeout, min: 0, @@ -157,7 +157,7 @@ function showWakeUpMenu() { Bangle.setLCDTimeout(settings.timeout); } }, - 'Wake On BTN1': { + 'Wake on BTN1': { value: settings.options.wakeOnBTN1, format: boolFormat, onchange: () => { @@ -165,7 +165,7 @@ function showWakeUpMenu() { updateOptions(); } }, - 'Wake On BTN2': { + 'Wake on BTN2': { value: settings.options.wakeOnBTN2, format: boolFormat, onchange: () => { @@ -173,7 +173,7 @@ function showWakeUpMenu() { updateOptions(); } }, - 'Wake On BTN3': { + 'Wake on BTN3': { value: settings.options.wakeOnBTN3, format: boolFormat, onchange: () => { @@ -197,7 +197,7 @@ function showWakeUpMenu() { updateOptions(); } }, - 'Wake On Twist': { + 'Wake on Twist': { value: settings.options.wakeOnTwist, format: boolFormat, onchange: () => { @@ -236,7 +236,7 @@ function showWakeUpMenu() { } } } - return E.showMenu(wakeUpMenu) + return E.showMenu(lcdMenu) } function showLocaleMenu() { From 5e5fb570db0ff446275d45bfa658f6c59ad9c918 Mon Sep 17 00:00:00 2001 From: Fabio Date: Fri, 10 Apr 2020 19:20:49 +0200 Subject: [PATCH 013/302] New bledetect app --- apps.json | 12 ++++++ apps/bledetect/ChangeLog | 1 + apps/bledetect/bledetect-icon.js | 1 + apps/bledetect/bledetect.js | 67 +++++++++++++++++++++++++++++++ apps/bledetect/bledetect.png | Bin 0 -> 4163 bytes 5 files changed, 81 insertions(+) create mode 100644 apps/bledetect/ChangeLog create mode 100644 apps/bledetect/bledetect-icon.js create mode 100644 apps/bledetect/bledetect.js create mode 100644 apps/bledetect/bledetect.png diff --git a/apps.json b/apps.json index d85739aad..8e4e2555a 100644 --- a/apps.json +++ b/apps.json @@ -1158,5 +1158,17 @@ {"name":"batchart.app.js","url":"app.js"}, {"name":"batchart.img","url":"app-icon.js","evaluate":true} ] + }, + { "id": "bledetect", + "name": "BLE Detector", + "shortName":"BLEDetector", + "icon": "bledetect.png", + "version":"0.01", + "description": "Detect BLE devices and show some informations.", + "tags": "app,bluetooth,tool", + "storage": [ + {"name":"bledetect.app.js","url":"bledetect.js"}, + {"name":"bledetect.img","url":"bledetect-icon.js","evaluate":true} + ] } ] diff --git a/apps/bledetect/ChangeLog b/apps/bledetect/ChangeLog new file mode 100644 index 000000000..9352c7b96 --- /dev/null +++ b/apps/bledetect/ChangeLog @@ -0,0 +1 @@ +0.01: Initial Release \ No newline at end of file diff --git a/apps/bledetect/bledetect-icon.js b/apps/bledetect/bledetect-icon.js new file mode 100644 index 000000000..a15e13307 --- /dev/null +++ b/apps/bledetect/bledetect-icon.js @@ -0,0 +1 @@ +E.toArrayBuffer(atob("AAAAAAAACIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiIiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiIiIAAAAAAAAAAAAAAAAAAAAAAAAAAAAiIiIiAAAAAAAAAAAAAAAAAAAAAAAAAAAiIiIiIAAAAAAAAAAAAAAAAAAAAAAAAAAiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAiIgIiIiAAAAAAAAAAAAAAAAAAAAAAAAAiIgAiIiIAAAAAAAAAAAAAAAAAIAAAAAAiIgACIiIgAAAAAAAAAAAAAAAiIiAAAAAiIgAAAiIiIAAAAAAAAAAAAAAiIiIAAAAiIgAAACIiIgAAAAAAAAAAAAACIiIgAAAiIgAAAAIiIgAAAAAAAAAAAAAAIiIiAAAiIgAAAAIiIiAAAAAAAAAAAAAAAiIiIAAiIgAAACIiIgAAAAAAAAAAAAAAACIiIgAiIgAAAiIiIAAAAAAAAAAAAAAAAAIiIiIiIgACIiIiAAAAAAAAAAAAAAAAAAACIiIiIgAiIiIAAAAAAAAAAAAAAAAAAAAAIiIiIgIiIiAAAAAAAAAAAAAAAAAAAAAAAiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAACIiIiIiIAAAAAAAAAAAAAAAAAAAAAAAAAIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAIiIiIiAAAAAAAAAIgAAAAAAAAAAAAAACIiIiIiIAAAAACIiIiIiAAAAAAAAAAAIiIiIiIiIgAAAAiIiIiIiIAAAAAAAAACIiIiIgIiIiAACIiIiIiIiIgAAAAAAAAiIiIiIgAiIiAACIiIiACIiIiAAAAAAAIiIiAiIgAAIgAAiIiAAAAAiIiIAAAAACIiIgAiIgAAAAAIiIgAAAAAAIiIAAAAAiIiIAAiIgAAAAAIiIAAAAAAAEiIgAAAIiIiAAAiIgAAAAAIiIAAAAAAAAiIgAAiIiIgAAAiIgAAAAAIiIAAAAAAAAiIgAAiIiAAAAAiIgAAAACIiAAAAAAAAAiIgAAiIgAAAAAiIgAAAACIiAAAAAAAAAiIgAAAAAAAAAAiIgAAAAAIiIAAAAAAAAiIgAAAAAAAAAAiIgAiIAAIiIAAAAAAAAiIgAAAAAAAAAAiIgIiIgAIiIgAAAAAAIiIgAAAAAAAAAAiIiIiIgAIiIgAAAAACIiIAAAAAAAAAAAiIiIiIAAAiIiIAAAAiIiIAAAAAAAAAAAiIiIgAAAACIiIiIiIiIiIAAAAAAAAAAAiIiIAAAAAAIiIiIiIiIiIgAAAAAAAAAAiIiAAAAAAAAiIiIiIiIiIiAAAAAAAAAAiIgAAAAAAAAAIiIiIgACIiIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiIiAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIi")) \ No newline at end of file diff --git a/apps/bledetect/bledetect.js b/apps/bledetect/bledetect.js new file mode 100644 index 000000000..dde3ee9eb --- /dev/null +++ b/apps/bledetect/bledetect.js @@ -0,0 +1,67 @@ +let menu = { + "": { "title": "BLE Detector" }, + "RE-SCAN": () => scan() +}; + +function showMainMenu() { + menu["< Back"] = () => load(); + return E.showMenu(menu); +} + +function showDeviceInfo(device){ + console.log(device); + const deviceMenu = { + "": { "title": "Device Info" }, + "name": { + value: device.name + }, + "rssi": { + value: device.rssi + }, + "manufacturer": { + value: device.manufacturer + } + }; + + deviceMenu[device.id] = () => {}; + deviceMenu["< Back"] = () => scan(); + + /*for(let key in device){ + deviceMenu[key.substring(0,17)] = { + value: device[key.substring(0,17)] + }; + }*/ + + return E.showMenu(deviceMenu); +} + +function scan() { + menu = { + "": { "title": "BLE Detector" }, + "RE-SCAN": () => scan() + }; + + waitMessage(); + + NRF.findDevices(devices => { + for (let device of devices) { + let deviceName = device.id.substring(0,17); + + if (device.name) { + deviceName = device.name; + } + + menu[deviceName] = () => showDeviceInfo(device); + } + showMainMenu(menu); + }, { active: true }); +} + +function waitMessage() { + E.showMenu(); + E.showMessage("scanning"); +} + +scan(); +waitMessage(); + diff --git a/apps/bledetect/bledetect.png b/apps/bledetect/bledetect.png new file mode 100644 index 0000000000000000000000000000000000000000..59d6a26cee07bc61899bc7ec129c426784e5f716 GIT binary patch literal 4163 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 literal 0 HcmV?d00001 From 558952c5408833fe0efaa6adf3a2a909b672de94 Mon Sep 17 00:00:00 2001 From: Fabio Date: Fri, 10 Apr 2020 19:24:34 +0200 Subject: [PATCH 014/302] Fix bledetect icon --- apps/bledetect/bledetect-icon.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/bledetect/bledetect-icon.js b/apps/bledetect/bledetect-icon.js index a15e13307..70b90cd42 100644 --- a/apps/bledetect/bledetect-icon.js +++ b/apps/bledetect/bledetect-icon.js @@ -1 +1 @@ -E.toArrayBuffer(atob("AAAAAAAACIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiIiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiIiIAAAAAAAAAAAAAAAAAAAAAAAAAAAAiIiIiAAAAAAAAAAAAAAAAAAAAAAAAAAAiIiIiIAAAAAAAAAAAAAAAAAAAAAAAAAAiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAiIgIiIiAAAAAAAAAAAAAAAAAAAAAAAAAiIgAiIiIAAAAAAAAAAAAAAAAAIAAAAAAiIgACIiIgAAAAAAAAAAAAAAAiIiAAAAAiIgAAAiIiIAAAAAAAAAAAAAAiIiIAAAAiIgAAACIiIgAAAAAAAAAAAAACIiIgAAAiIgAAAAIiIgAAAAAAAAAAAAAAIiIiAAAiIgAAAAIiIiAAAAAAAAAAAAAAAiIiIAAiIgAAACIiIgAAAAAAAAAAAAAAACIiIgAiIgAAAiIiIAAAAAAAAAAAAAAAAAIiIiIiIgACIiIiAAAAAAAAAAAAAAAAAAACIiIiIgAiIiIAAAAAAAAAAAAAAAAAAAAAIiIiIgIiIiAAAAAAAAAAAAAAAAAAAAAAAiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAACIiIiIiIAAAAAAAAAAAAAAAAAAAAAAAAAIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAIiIiIiAAAAAAAAAIgAAAAAAAAAAAAAACIiIiIiIAAAAACIiIiIiAAAAAAAAAAAIiIiIiIiIgAAAAiIiIiIiIAAAAAAAAACIiIiIgIiIiAACIiIiIiIiIgAAAAAAAAiIiIiIgAiIiAACIiIiACIiIiAAAAAAAIiIiAiIgAAIgAAiIiAAAAAiIiIAAAAACIiIgAiIgAAAAAIiIgAAAAAAIiIAAAAAiIiIAAiIgAAAAAIiIAAAAAAAEiIgAAAIiIiAAAiIgAAAAAIiIAAAAAAAAiIgAAiIiIgAAAiIgAAAAAIiIAAAAAAAAiIgAAiIiAAAAAiIgAAAACIiAAAAAAAAAiIgAAiIgAAAAAiIgAAAACIiAAAAAAAAAiIgAAAAAAAAAAiIgAAAAAIiIAAAAAAAAiIgAAAAAAAAAAiIgAiIAAIiIAAAAAAAAiIgAAAAAAAAAAiIgIiIgAIiIgAAAAAAIiIgAAAAAAAAAAiIiIiIgAIiIgAAAAACIiIAAAAAAAAAAAiIiIiIAAAiIiIAAAAiIiIAAAAAAAAAAAiIiIgAAAACIiIiIiIiIiIAAAAAAAAAAAiIiIAAAAAAIiIiIiIiIiIgAAAAAAAAAAiIiAAAAAAAAiIiIiIiIiIiAAAAAAAAAAiIgAAAAAAAAAIiIiIgACIiIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiIiAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIi")) \ No newline at end of file +require("heatshrink").decompress(atob("MDCEAAAAAAAAAAiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIiAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAIiIiIiAAAAAAAAAAAAAAAAAAAAAAAAAAIiIiIiIAAAAAAAAAAAAAAAAAAAAAAAAAIiICIiIgAAAAAAAAAAAAAAAAAAAAAAAAIiIAIiIiAAAAAAAAAAAAAAAAACAAAAAAIiIAAiIiIAAAAAAAAAAAAAAAIiIgAAAAIiIAAAIiIiAAAAAAAAAAAAAAIiIiAAAAIiIAAAAiIiIAAAAAAAAAAAAAAiIiIAAAIiIAAAACIiIAAAAAAAAAAAAAACIiIgAAIiIAAAACIiIgAAAAAAAAAAAAAAIiIiAAIiIAAAAiIiIAAAAAAAAAAAAAAAAiIiIAIiIAAAIiIiAAAAAAAAAAAAAAAAACIiIiIiIAAiIiIgAAAAAAAAAAAAAAAAAAAiIiIiIAIiIiAAAAAAAAAAAAAAAAAAAAACIiIiICIiIgAAAAAAAAAAAAAAAAAAAAAAIiIiIiIiIAAAAAAAAAAAAAAAAAAAAAAAAiIiIiIiAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIiIAAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIgAAAAAAAACIAAAAAAAAAAAAAAAiIiIiIiAAAAAAiIiIiIgAAAAAAAAAACIiIiIiIiIAAAAIiIiIiIiAAAAAAAAAAiIiIiICIiIgAAiIiIiIiIiIAAAAAAAAIiIiIiIAIiIgAAiIiIgAiIiIgAAAAAACIiIgIiIAACIAAIiIgAAAAIiIiAAAAAAiIiIAIiIAAAAACIiIAAAAAACIiAAAAAIiIiAAIiIAAAAACIiAAAAAAABIiIAAACIiIgAAIiIAAAAACIiAAAAAAAAIiIAAIiIiIAAAIiIAAAAACIiAAAAAAAAIiIAAIiIgAAAAIiIAAAAAiIgAAAAAAAAIiIAAIiIAAAAAIiIAAAAAiIgAAAAAAAAIiIAAAAAAAAAAIiIAAAAACIiAAAAAAAAIiIAAAAAAAAAAIiIAIiAACIiAAAAAAAAIiIAAAAAAAAAAIiICIiIACIiIAAAAAACIiIAAAAAAAAAAIiIiIiIACIiIAAAAAAiIiAAAAAAAAAAAIiIiIiAAAIiIiAAAAIiIiAAAAAAAAAAAIiIiIAAAAAiIiIiIiIiIiAAAAAAAAAAAIiIiAAAAAACIiIiIiIiIiIAAAAAAAAAAIiIgAAAAAAAIiIiIiIiIiIgAAAAAAAAAIiIAAAAAAAAACIiIiIAAiIiAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIiIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIg==")) From 8d8befd5ec33d95f5333232c098ddd96bca74999 Mon Sep 17 00:00:00 2001 From: Fabio Date: Fri, 10 Apr 2020 19:35:34 +0200 Subject: [PATCH 015/302] Finally fixed icon for bledetect --- apps.json | 2 +- apps/bledetect/bledetect-icon.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps.json b/apps.json index 8e4e2555a..9071c269f 100644 --- a/apps.json +++ b/apps.json @@ -1161,7 +1161,7 @@ }, { "id": "bledetect", "name": "BLE Detector", - "shortName":"BLEDetector", + "shortName":"BLE Detector", "icon": "bledetect.png", "version":"0.01", "description": "Detect BLE devices and show some informations.", diff --git a/apps/bledetect/bledetect-icon.js b/apps/bledetect/bledetect-icon.js index 70b90cd42..8c605889a 100644 --- a/apps/bledetect/bledetect-icon.js +++ b/apps/bledetect/bledetect-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("MDCEAAAAAAAAAAiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIiAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAIiIiIiAAAAAAAAAAAAAAAAAAAAAAAAAAIiIiIiIAAAAAAAAAAAAAAAAAAAAAAAAAIiICIiIgAAAAAAAAAAAAAAAAAAAAAAAAIiIAIiIiAAAAAAAAAAAAAAAAACAAAAAAIiIAAiIiIAAAAAAAAAAAAAAAIiIgAAAAIiIAAAIiIiAAAAAAAAAAAAAAIiIiAAAAIiIAAAAiIiIAAAAAAAAAAAAAAiIiIAAAIiIAAAACIiIAAAAAAAAAAAAAACIiIgAAIiIAAAACIiIgAAAAAAAAAAAAAAIiIiAAIiIAAAAiIiIAAAAAAAAAAAAAAAAiIiIAIiIAAAIiIiAAAAAAAAAAAAAAAAACIiIiIiIAAiIiIgAAAAAAAAAAAAAAAAAAAiIiIiIAIiIiAAAAAAAAAAAAAAAAAAAAACIiIiICIiIgAAAAAAAAAAAAAAAAAAAAAAIiIiIiIiIAAAAAAAAAAAAAAAAAAAAAAAAiIiIiIiAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIiIAAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIgAAAAAAAACIAAAAAAAAAAAAAAAiIiIiIiAAAAAAiIiIiIgAAAAAAAAAACIiIiIiIiIAAAAIiIiIiIiAAAAAAAAAAiIiIiICIiIgAAiIiIiIiIiIAAAAAAAAIiIiIiIAIiIgAAiIiIgAiIiIgAAAAAACIiIgIiIAACIAAIiIgAAAAIiIiAAAAAAiIiIAIiIAAAAACIiIAAAAAACIiAAAAAIiIiAAIiIAAAAACIiAAAAAAABIiIAAACIiIgAAIiIAAAAACIiAAAAAAAAIiIAAIiIiIAAAIiIAAAAACIiAAAAAAAAIiIAAIiIgAAAAIiIAAAAAiIgAAAAAAAAIiIAAIiIAAAAAIiIAAAAAiIgAAAAAAAAIiIAAAAAAAAAAIiIAAAAACIiAAAAAAAAIiIAAAAAAAAAAIiIAIiAACIiAAAAAAAAIiIAAAAAAAAAAIiICIiIACIiIAAAAAACIiIAAAAAAAAAAIiIiIiIACIiIAAAAAAiIiAAAAAAAAAAAIiIiIiAAAIiIiAAAAIiIiAAAAAAAAAAAIiIiIAAAAAiIiIiIiIiIiAAAAAAAAAAAIiIiAAAAAACIiIiIiIiIiIAAAAAAAAAAIiIgAAAAAAAIiIiIiIiIiIgAAAAAAAAAIiIAAAAAAAAACIiIiIAAiIiAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIiIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIg==")) +require("heatshrink").decompress(atob("oFAwgLIhGIFbuIxGAD7xAdD4RAcD4QgcD4ZhbD4hAaD4hAaD4pAZDYRAcD4UIEDZbDMKY0BD5IgES6IfJMIZAOCI4GFICIRHD4pARCI5GIICwfYIAxfXGQr7DT6ozFCgLfXKg4YDFIoARCwZjFHyZhGDIJdVIBJdWIBA+YIBIeWIA4eXEAxdXD44eZQAw+dMBEiAAUgX7IeDAAT/XDwxBLDYpAFgQfHIBI+FwAGDHxJAJThAICHwpFGLpQECPYQDCDAYUDA4ZAFHwYXBbg4WIEAQIFSwofKKwwJGHwofHGpAfIHwofSBQQ+JD5T1HBQoeGD6pKCLoofbW4ofXDAINFP64AHD4ZqCX6AfKZQT/SD5LKECpIfPAAYVKJJQfLCxAoCD6DCCD4QXEAwReLD4jhEDAYAGH553HABAfNHwhAXHw5AJA4b/MBQ4gFYJ0IDxAhEE47CLACDCOACBAjD7hACD7hABkAA=")) \ No newline at end of file From a95a396ac1cd20630848ef92baaa9db5a520a29a Mon Sep 17 00:00:00 2001 From: Fabio Date: Fri, 10 Apr 2020 19:43:10 +0200 Subject: [PATCH 016/302] Resized icon bledetect --- apps/bledetect/bledetect-icon.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/bledetect/bledetect-icon.js b/apps/bledetect/bledetect-icon.js index 8c605889a..2e49b3d0a 100644 --- a/apps/bledetect/bledetect-icon.js +++ b/apps/bledetect/bledetect-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("oFAwgLIhGIFbuIxGAD7xAdD4RAcD4QgcD4ZhbD4hAaD4hAaD4pAZDYRAcD4UIEDZbDMKY0BD5IgES6IfJMIZAOCI4GFICIRHD4pARCI5GIICwfYIAxfXGQr7DT6ozFCgLfXKg4YDFIoARCwZjFHyZhGDIJdVIBJdWIBA+YIBIeWIA4eXEAxdXD44eZQAw+dMBEiAAUgX7IeDAAT/XDwxBLDYpAFgQfHIBI+FwAGDHxJAJThAICHwpFGLpQECPYQDCDAYUDA4ZAFHwYXBbg4WIEAQIFSwofKKwwJGHwofHGpAfIHwofSBQQ+JD5T1HBQoeGD6pKCLoofbW4ofXDAINFP64AHD4ZqCX6AfKZQT/SD5LKECpIfPAAYVKJJQfLCxAoCD6DCCD4QXEAwReLD4jhEDAYAGH553HABAfNHwhAXHw5AJA4b/MBQ4gFYJ0IDxAhEE47CLACDCOACBAjD7hACD7hABkAA=")) \ No newline at end of file +require("heatshrink").decompress(atob("mEwwgJGhGAEKuIxAXXGCoXBGCoXCDCgXDJKYXDGCYUBhAwUFgQwPEogTCGBwNFFYYYNHwoEGJJQlFCIgKCdR4XHJBQNEI6IOFO6IPEDQYGDahoYEa6BJFxBFPJJIuQGAouRGAoWSGAgXTSIoAEgUgL6cCkQACDJCOFGAYWDAAJFLX4gWFGA4sFC40gJQYuHwBEDAQISCMYowEFgoJDCAwYBAwZYEC45AEgIHERAgXMA4i4FC6bPDC4hXFC5B7FC57CHI54XIawgXRVwS/JC5SuDC4wGGC45HBFAQRCAooXIVwYRBAAoXLLIwAFC5IuDGCIuFDAyQLABphKABgwaC6owB")) \ No newline at end of file From d843210ed09194875e64cc2c3b138f9b9ee5f1e2 Mon Sep 17 00:00:00 2001 From: ps-igel <60899838+ps-igel@users.noreply.github.com> Date: Fri, 10 Apr 2020 22:32:09 +0200 Subject: [PATCH 017/302] add numerals clock --- apps.json | 15 +++++ apps/numerals/README.md | 17 ++++++ apps/numerals/numerals-icon.js | 1 + apps/numerals/numerals.app.js | 93 +++++++++++++++++++++++++++++ apps/numerals/numerals.png | Bin 0 -> 1173 bytes apps/numerals/numerals.settings.js | 33 ++++++++++ 6 files changed, 159 insertions(+) create mode 100644 apps/numerals/README.md create mode 100644 apps/numerals/numerals-icon.js create mode 100644 apps/numerals/numerals.app.js create mode 100644 apps/numerals/numerals.png create mode 100644 apps/numerals/numerals.settings.js diff --git a/apps.json b/apps.json index d85739aad..1f05142f5 100644 --- a/apps.json +++ b/apps.json @@ -1158,5 +1158,20 @@ {"name":"batchart.app.js","url":"app.js"}, {"name":"batchart.img","url":"app-icon.js","evaluate":true} ] + }, + { "id": "numerals", + "name": "Numerals Clock", + "shortName": "Numerals Clock", + "icon": "numerals.png", + "version":"0.01", + "description": "A simple big numerals clock", + "tags": "numerals,clock", + "type":"clock", + "allow_emulator":true, + "storage": [ + {"name":"numerals.app.js","url":"numerals.app.js"}, + {"name":"numerals.img","url":"numerals-icon.js","evaluate":true}, + {"name":"numerals.settings.js","url":"numerals.settings.js"} + ] } ] diff --git a/apps/numerals/README.md b/apps/numerals/README.md new file mode 100644 index 000000000..01d784ef8 --- /dev/null +++ b/apps/numerals/README.md @@ -0,0 +1,17 @@ +# Numerals Clock + +This is a simple big numerals clock. +Settings can be accessed through the app/widget settings menu of the Bangle.js + +## Settings available + +### color: +* rnd - shows numerals in different color combinations every time the watches wakes +* r/g - red/green +* y/w - yellow/white +* o/c - orange/cyan +* b/y - blue/yellow'ish + +### draw mode +* fill - fill numerals +* frame - only shows outline of numerals diff --git a/apps/numerals/numerals-icon.js b/apps/numerals/numerals-icon.js new file mode 100644 index 000000000..7e471fb0d --- /dev/null +++ b/apps/numerals/numerals-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/ABMBzIADyAJIAAkQBoMZBIoXCBIwADyIkIGAIuKGAQkIBJIwEEKQANC/4XWR58RiIHFWpAXFe4QRFcpAXFewQRFcxAXEFwQwGA4QXKiAXDGAgX/C/4X/C/4X/C7uQCwcBBwYXNBwYuEC54wCFwgXPzMRiIHFC54AHC/4XiCAoXRhIHDyK3GAAwOBJA0QG45VGC4YwCD4YwKFwgABcgIfEAwIAHBwgA/AAgA==")) \ No newline at end of file diff --git a/apps/numerals/numerals.app.js b/apps/numerals/numerals.app.js new file mode 100644 index 000000000..648a1005a --- /dev/null +++ b/apps/numerals/numerals.app.js @@ -0,0 +1,93 @@ +/** + * Bangle.js Numerals Clock + * + * + Original Author: Raik M. https://github.com/ps-igel + * + Created: April 2020 + * + see README.md for details + */ + +var numerals = { + 0:[[9,1,82,1,90,9,90,82,82,90,9,90,1,82,1,9,9,1],[30,21,61,21,69,29,69,61,61,69,30,69,22,61,22,29,30,21]], + 1:[[59,1,82,1,90,9,90,82,82,90,73,90,65,82,65,27,59,27,51,19,51,9,59,1]], + 2:[[9,1,82,1,90,9,90,47,82,55,21,55,21,64,82,64,90,72,90,82,82,90,9,90,1,82,1,43,9,35,70,35,70,25,9,25,1,17,1,9,9,1]], + 3:[[9,1,82,1,90,9,90,82,82,90,9,90,1,82,1,74,9,66,70,66,70,57,9,57,1,49,1,41,9,33,70,33,70,25,9,25,1,17,1,9,9,1]], + 4:[[9,1,14,1,22,9,22,34,69,34,69,9,77,1,82,1,90,9,90,82,82,90,78,90,70,82,70,55,9,55,1,47,1,9,9,1]], + 5:[[9,1,82,1,90,9,90,17,82,25,21,25,21,35,82,35,90,43,90,82,82,90,9,90,1,82,1,72,9,64,71,64,71,55,9,55,1,47,1,9,9,1]], + 6:[[9,1,82,1,90,9,90,14,82,22,22,22,22,36,82,36,90,44,90,82,82,90,9,90,1,82,1,9,9,1],[22,55,69,55,69,69,22,69,22,55]], + 7:[[9,1,82,1,90,9,90,15,15,90,9,90,1,82,1,76,54,23,9,23,1,15,1,9,9,1]], + 8:[[9,1,82,1,90,9,90,82,82,90,9,90,1,82,1,9,9,1],[22,22,69,22,69,36,22,36,22,22],[22,55,69,55,69,69,22,69,22,55]], + 9:[[9,1,82,1,90,9,90,82,82,90,9,90,1,82,1,77,9,69,69,69,69,55,9,55,1,47,1,9,9,1],[22,22,69,22,69,36,22,36,22,22]], +}; +var _hCol = ["#ff5555","#ffff00","#FF9901","#2F00FF"]; +var _mCol = ["#55ff55","#ffffff","#00EFEF","#FFBF00"]; +var _rCol = 0; +var interval = 0; +const REFRESH_RATE = 10E3; + +function translate(tx, ty, p) { + return p.map((x, i)=> x+((i%2)?ty:tx)); +} + +function fill(poly){ + return g.fillPoly(poly); +} + +function frame(poly){ + return g.drawPoly(poly); +} + +let settings = require('Storage').readJSON('numerals.json',1); +if (!settings) { + settings = { + color: 0, + drawMode: "fill" + }; +} + +function drawNum(num,col,x,y,func){ + g.setColor(col); + let tx = x*100+35; + let ty = y*100+35; + for (let i=0;i0) g.setColor((func==fill)?"#000000":col); + func(translate(tx, ty,numerals[num][i])); + } +} + +function draw(drawMode){ + let d = new Date(); + let h1 = Math.floor(d.getHours()/10); + let h2 = d.getHours()%10; + let m1 = Math.floor(d.getMinutes()/10); + let m2 = d.getMinutes()%10; + g.clearRect(0,24,240,240); + drawNum(h1,_hCol[_rCol],0,0,eval(drawMode)); + drawNum(h2,_hCol[_rCol],1,0,eval(drawMode)); + drawNum(m1,_mCol[_rCol],0,1,eval(drawMode)); + drawNum(m2,_mCol[_rCol],1,1,eval(drawMode)); +} + +Bangle.setLCDMode(); + +clearWatch(); +setWatch(Bangle.showLauncher, BTN1, {repeat:false,edge:"falling"}); + +g.clear(); +clearInterval(); +if (settings.color>0) _rCol=settings.color-1; +interval=setInterval(draw, REFRESH_RATE, settings.drawMode); +draw(settings.drawMode); + +Bangle.on('lcdPower', function(on) { + if (on) { + if (settings.color==0) _rCol = Math.floor(Math.random()*_hCol.length); + draw(settings.drawMode); + interval=setInterval(draw, REFRESH_RATE, settings.drawMode); + }else + { + clearInterval(interval); + } +}); + +Bangle.loadWidgets(); +Bangle.drawWidgets(); \ No newline at end of file diff --git a/apps/numerals/numerals.png b/apps/numerals/numerals.png new file mode 100644 index 0000000000000000000000000000000000000000..c181e2e0dfc32596174d6c94480a6f92a101e20d GIT binary patch literal 1173 zcmV;G1Zw+EX>4Tx04R}tkv&MmKpe$i(@Iq;4t5Z6$WUFhAS&W0RV;#q(pG5I!Q|2}Xws0R zxHt-~1qVMCs}3&Cx;nTDg5U>;lcSTOi5bWxZD8-pLEHP9LY~pC=`JAGy0|+FmMa>uDQLn_Hp_EWT>m<8{ps& z7%fuvy2rb_JA3>0Osl^iO2u-QrwyT@00006VoOIv0RI600RN!9r;`8x010qNS#tmY zE+YT{E+YYWr9XB600N~+L_t(&-tF1TYZO@+#^I;Rj;%XUaA*DnM_qYCM8{!7W-&}7 zZq!9gyrOFb2bDONC<@aHr-KW3O0PtM7jU5nf=X0GH|oL&qM&XyLptT6PfQy-rlQ@Q zIMfSGS5chryuYfi&iU#bGEJ?b+LNSEn0$b8dE9GX6K}G8Mq@hLhfWNqi1QE|#{&rQ zW}^@D8{|Dk2`66^%cB6Hv)%Y-k^%d$VJt+5m&Z*kYQjUO9GNd<$j&CpzKjS=pMJ1%o%_f_NQk0X#=xso-U$8bD?`VPf5%tCm z3|0yML#-S}W8b;{7Xa_SXMh}*hEZeI6!d=@SL(b=CCp6=a1YCIBMgD9Qc0p~@N9Jw ztW@~(D2E>v;5Pa(Z^9G`vuHn7v@o4Y+I3m!6Z1z7eXMf^uo$mXy~H&TD{%~e<9^f9 zCefvx!Chs;!*n01njcfF4PXi0Oq4>kjqc!1C=X+pX(k~VPp5`T2puQ-!JVE|ik zVcHI(QACn_v22dAi7W%i05X6KAOpw%GJp(VngUcR5urp4MXHe|EkoujRdzj}yc ziZ4H!zdMonsRV$7Vl(oa3YVxZWfwg^SWb{!hC2lv^XH^-a*PL)MzS_Ct{jd^FDpg+Y<{TeBj n$x|xAHL;ift}V-ysR8-`u%*0+fO-J!00000NkvXXu0mjfBikB4 literal 0 HcmV?d00001 diff --git a/apps/numerals/numerals.settings.js b/apps/numerals/numerals.settings.js new file mode 100644 index 000000000..f9c417da6 --- /dev/null +++ b/apps/numerals/numerals.settings.js @@ -0,0 +1,33 @@ +(function(back) { + function updateSettings() { + storage.write('numerals.json', numeralsSettings); + }; + function resetSettings() { + numeralsSettings = { + color: 0, + drawMode: "fill" + }; + updateSettings(); + } + let numeralsSettings = storage.readJSON('numerals.json',1); + if (!numeralsSettings) resetSettings(); + let dm = ["fill","frame"]; + let col = ["rnd","r/g","y/w","o/c","b/y"] + var menu={ + "" : { "title":"Numerals"}, + "Colors": { + value: 0|numeralsSettings.color, + min:0,max:4, + format: v=>col[v], + onchange: v=> { numeralsSettings.color=v; updateSettings();} + }, + "Draw mode": { + value: 0|dm.indexOf(numeralsSettings.drawMode), + min:0,max:1, + format: v=>dm[v], + onchange: v=> { numeralsSettings.drawMode=dm[v]; updateSettings();} + }, + "< back": back + }; + E.showMenu(menu); +}) \ No newline at end of file From 20ad2efbd4cf5b66b368e792826d0f7b631bf128 Mon Sep 17 00:00:00 2001 From: Paul Cockrell Date: Sat, 11 Apr 2020 01:52:27 +0100 Subject: [PATCH 018/302] In night mode, further left swipes reduce the screen brightness (3 levels) until returning to day-mode --- apps.json | 2 +- apps/marioclock/ChangeLog | 3 ++- apps/marioclock/README.md | 2 +- apps/marioclock/marioclock-app.js | 16 ++++++++++++++-- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/apps.json b/apps.json index 0271c244d..293a709c7 100644 --- a/apps.json +++ b/apps.json @@ -914,7 +914,7 @@ { "id": "marioclock", "name": "Mario Clock", "icon": "marioclock.png", - "version":"0.09", + "version":"0.10", "description": "Animated retro Mario clock, with Gameboy style 8-bit grey-scale graphics.", "tags": "clock,mario,retro", "type": "clock", diff --git a/apps/marioclock/ChangeLog b/apps/marioclock/ChangeLog index acce6a7ed..b4a4c7af9 100644 --- a/apps/marioclock/ChangeLog +++ b/apps/marioclock/ChangeLog @@ -6,4 +6,5 @@ 0.06: Performance refactor, and enhanced graphics! 0.07: Swipe right to change between Mario and Toad characters, swipe left to toggle night mode 0.08: Update date panel to be info panel toggling between Date, Battery and Temperature. Add Princes Daisy -0.09: Add GadgetBridge functionality. Mario shows message type in speach bubble, while message scrolls in info panel \ No newline at end of file +0.09: Add GadgetBridge functionality. Mario shows message type in speach bubble, while message scrolls in info panel +0.10: Swiping left to enable night-mode now also reduces LCD brightness through 3 levels before returning to day-mode. \ No newline at end of file diff --git a/apps/marioclock/README.md b/apps/marioclock/README.md index e6aeaa1bb..25276a351 100644 --- a/apps/marioclock/README.md +++ b/apps/marioclock/README.md @@ -8,7 +8,7 @@ Enjoy watching Mario, or one of the other game characters run through a level wh ## Features * Multiple characters - swipe the screen right to change the character between `Mario`, `Toad`, and `Daisy` -* Night and Day modes - swipe left to toggle mode +* Night and Day modes - swipe left to enter night mode, with 3 levels of darkness before returning to day mode. * Smooth animation * Awesome 8-bit style grey-scale graphics * Mario jumps to change the time, every minute diff --git a/apps/marioclock/marioclock-app.js b/apps/marioclock/marioclock-app.js index 529f1c95b..e213c2498 100644 --- a/apps/marioclock/marioclock-app.js +++ b/apps/marioclock/marioclock-app.js @@ -16,6 +16,8 @@ const is12Hour = settings["12hour"] || false; // Screen dimensions let W, H; +// Screen brightness +let brightness = 1; let intervalRef, displayTimeoutRef = null; @@ -164,7 +166,17 @@ function switchCharacter() { } function toggleNightMode() { - nightMode = !nightMode; + if (!nightMode) { + nightMode = true; + return; + } + + brightness -= 0.30; + if (brightness <= 0) { + brightness = 1; + nightMode = false; + } + Bangle.setLCDBrightness(brightness); } function incrementTimer() { @@ -625,4 +637,4 @@ function init() { } // Initialise! -init() \ No newline at end of file +init(); \ No newline at end of file From cab228f47e79ec1872653dc6a29ac6ed5c4bbe8b Mon Sep 17 00:00:00 2001 From: Paul Cockrell Date: Sat, 11 Apr 2020 10:47:01 +0100 Subject: [PATCH 019/302] Persist user settings --- apps.json | 2 +- apps/marioclock/ChangeLog | 3 ++- apps/marioclock/marioclock-app.js | 35 +++++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/apps.json b/apps.json index 293a709c7..73a093a2c 100644 --- a/apps.json +++ b/apps.json @@ -914,7 +914,7 @@ { "id": "marioclock", "name": "Mario Clock", "icon": "marioclock.png", - "version":"0.10", + "version":"0.11", "description": "Animated retro Mario clock, with Gameboy style 8-bit grey-scale graphics.", "tags": "clock,mario,retro", "type": "clock", diff --git a/apps/marioclock/ChangeLog b/apps/marioclock/ChangeLog index b4a4c7af9..6f3c79504 100644 --- a/apps/marioclock/ChangeLog +++ b/apps/marioclock/ChangeLog @@ -7,4 +7,5 @@ 0.07: Swipe right to change between Mario and Toad characters, swipe left to toggle night mode 0.08: Update date panel to be info panel toggling between Date, Battery and Temperature. Add Princes Daisy 0.09: Add GadgetBridge functionality. Mario shows message type in speach bubble, while message scrolls in info panel -0.10: Swiping left to enable night-mode now also reduces LCD brightness through 3 levels before returning to day-mode. \ No newline at end of file +0.10: Swiping left to enable night-mode now also reduces LCD brightness through 3 levels before returning to day-mode. +0.11: User settings persisted and read to file. \ No newline at end of file diff --git a/apps/marioclock/marioclock-app.js b/apps/marioclock/marioclock-app.js index e213c2498..eb9632c59 100644 --- a/apps/marioclock/marioclock-app.js +++ b/apps/marioclock/marioclock-app.js @@ -81,6 +81,16 @@ const phone = { messageType: null, }; +const SETTINGS_FILE = "marioclock.json"; + +function readSettings() { + return require('Storage').readJSON(SETTINGS_FILE, 1) || {}; +} + +function writeSettings(newSettings) { + require("Storage").writeJSON(SETTINGS_FILE, newSettings); +} + function phoneOutbound(msg) { Bluetooth.println(JSON.stringify(msg)); } @@ -567,8 +577,31 @@ function startTimers(){ redraw(); } +function loadSettings() { + const settings = readSettings(); + if (!settings) return; + + if (settings.character) characterSprite.character = settings.character; + if (settings.nightMode) nightMode = settings.nightMode; + if (settings.brightness) { + brightness = settings.brightness; + Bangle.setLCDBrightness(brightness); + } +} + +function updateSettings() { + const newSettings = { + character: characterSprite.character, + nightMode: nightMode, + brightness: brightness, + }; + writeSettings(newSettings); +} + // Main function init() { + loadSettings(); + clearInterval(); // Initialise display @@ -618,6 +651,8 @@ function init() { default: toggleNightMode(); } + + updateSettings(); }); // Phone connectivity From cf58c0eb5f93c27966a98e4c7cd97040e4ddf751 Mon Sep 17 00:00:00 2001 From: Fabio Date: Sat, 11 Apr 2020 13:03:45 +0200 Subject: [PATCH 020/302] bledetect fixed issue with wrong device informations --- apps/bledetect/ChangeLog | 3 ++- apps/bledetect/bledetect.js | 16 ++++------------ 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/apps/bledetect/ChangeLog b/apps/bledetect/ChangeLog index 9352c7b96..cd5ce5845 100644 --- a/apps/bledetect/ChangeLog +++ b/apps/bledetect/ChangeLog @@ -1 +1,2 @@ -0.01: Initial Release \ No newline at end of file +0.01: Initial Release +0.02: Fixed issue with wrong device informations \ No newline at end of file diff --git a/apps/bledetect/bledetect.js b/apps/bledetect/bledetect.js index dde3ee9eb..6f5f5fa30 100644 --- a/apps/bledetect/bledetect.js +++ b/apps/bledetect/bledetect.js @@ -9,7 +9,6 @@ function showMainMenu() { } function showDeviceInfo(device){ - console.log(device); const deviceMenu = { "": { "title": "Device Info" }, "name": { @@ -24,13 +23,7 @@ function showDeviceInfo(device){ }; deviceMenu[device.id] = () => {}; - deviceMenu["< Back"] = () => scan(); - - /*for(let key in device){ - deviceMenu[key.substring(0,17)] = { - value: device[key.substring(0,17)] - }; - }*/ + deviceMenu["< Back"] = () => showMainMenu(); return E.showMenu(deviceMenu); } @@ -44,7 +37,7 @@ function scan() { waitMessage(); NRF.findDevices(devices => { - for (let device of devices) { + devices.forEach(device =>{ let deviceName = device.id.substring(0,17); if (device.name) { @@ -52,7 +45,7 @@ function scan() { } menu[deviceName] = () => showDeviceInfo(device); - } + }); showMainMenu(menu); }, { active: true }); } @@ -63,5 +56,4 @@ function waitMessage() { } scan(); -waitMessage(); - +waitMessage(); \ No newline at end of file From c368bcdb89f63925a85bb236c1c5f82152586e5f Mon Sep 17 00:00:00 2001 From: Fabio Di Stasio Date: Sat, 11 Apr 2020 13:26:02 +0200 Subject: [PATCH 021/302] Update app.json with bledetect new version. --- apps.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps.json b/apps.json index 9071c269f..fc60cb276 100644 --- a/apps.json +++ b/apps.json @@ -1163,7 +1163,7 @@ "name": "BLE Detector", "shortName":"BLE Detector", "icon": "bledetect.png", - "version":"0.01", + "version":"0.02", "description": "Detect BLE devices and show some informations.", "tags": "app,bluetooth,tool", "storage": [ From cffe04a54e41fce6be2fbcc11573769f2e03a2e9 Mon Sep 17 00:00:00 2001 From: ps-igel <60899838+ps-igel@users.noreply.github.com> Date: Sat, 11 Apr 2020 15:37:40 +0200 Subject: [PATCH 022/302] add ChangeLog --- apps/numerals/ChangeLog | 1 + 1 file changed, 1 insertion(+) create mode 100644 apps/numerals/ChangeLog diff --git a/apps/numerals/ChangeLog b/apps/numerals/ChangeLog new file mode 100644 index 000000000..1dfcf61a6 --- /dev/null +++ b/apps/numerals/ChangeLog @@ -0,0 +1 @@ +0.01: new awesome clock \ No newline at end of file From fc801b29de7f1a0eb0d86cdc9d350f1b8841d38e Mon Sep 17 00:00:00 2001 From: ps-igel <60899838+ps-igel@users.noreply.github.com> Date: Sat, 11 Apr 2020 18:22:48 +0200 Subject: [PATCH 023/302] update --- apps/numerals/ChangeLog | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/numerals/ChangeLog b/apps/numerals/ChangeLog index 1dfcf61a6..5560f00bc 100644 --- a/apps/numerals/ChangeLog +++ b/apps/numerals/ChangeLog @@ -1 +1 @@ -0.01: new awesome clock \ No newline at end of file +0.01: New App! From 3f08eee070e36cdfcd8552f75ac55293d1700324 Mon Sep 17 00:00:00 2001 From: Fabio Date: Sat, 11 Apr 2020 21:04:16 +0200 Subject: [PATCH 024/302] Changelog update and README.md --- apps.json | 1 + apps/bledetect/ChangeLog | 2 +- apps/bledetect/README.md | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 apps/bledetect/README.md diff --git a/apps.json b/apps.json index fc60cb276..56a6b69d3 100644 --- a/apps.json +++ b/apps.json @@ -1166,6 +1166,7 @@ "version":"0.02", "description": "Detect BLE devices and show some informations.", "tags": "app,bluetooth,tool", + "readme": "README.md", "storage": [ {"name":"bledetect.app.js","url":"bledetect.js"}, {"name":"bledetect.img","url":"bledetect-icon.js","evaluate":true} diff --git a/apps/bledetect/ChangeLog b/apps/bledetect/ChangeLog index cd5ce5845..520ccfa2f 100644 --- a/apps/bledetect/ChangeLog +++ b/apps/bledetect/ChangeLog @@ -1,2 +1,2 @@ -0.01: Initial Release +0.01: New App! 0.02: Fixed issue with wrong device informations \ No newline at end of file diff --git a/apps/bledetect/README.md b/apps/bledetect/README.md new file mode 100644 index 000000000..1f0c0a7a4 --- /dev/null +++ b/apps/bledetect/README.md @@ -0,0 +1,14 @@ +# BLE Detector + +BLE Detector it's an app born for testing purpose that aim to show as informations as possible about near BLE devices. + +## Features + +BLE Detector shows: + +- Device name (if available) +- Received Signal Strength Indication (RSSI) +- Manufacturer +- MAC Address + +More informations will coming with future versions. From 437b94d166c52a14e1cf748c4e3c0dbb0b888573 Mon Sep 17 00:00:00 2001 From: unmotivagedgene Date: Sat, 11 Apr 2020 14:53:52 -0500 Subject: [PATCH 025/302] Create changelog.txt --- apps/nato/changelog.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 apps/nato/changelog.txt diff --git a/apps/nato/changelog.txt b/apps/nato/changelog.txt new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/nato/changelog.txt @@ -0,0 +1 @@ +0.01: New App! From da2c2dba593be534d3ef0c8c691746c02c99ae86 Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Sun, 12 Apr 2020 01:11:47 +0200 Subject: [PATCH 026/302] Sanity check: fix warning about app without id, check for unknown keys --- bin/sanitycheck.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/bin/sanitycheck.js b/bin/sanitycheck.js index a2c9dee9a..62b111ae0 100755 --- a/bin/sanitycheck.js +++ b/bin/sanitycheck.js @@ -37,7 +37,13 @@ try{ ERROR("apps.json not valid JSON"); } -apps.forEach((app,addIdx) => { +const APP_KEYS = [ + 'id', 'name', 'shortName', 'version', 'icon', 'description', 'tags', 'type', + 'sortorder', 'readme', 'custom', 'interface', 'storage', 'allow_emulator', +]; +const STORAGE_KEYS = ['name', 'url', 'content', 'evaluate']; + +apps.forEach((app,appIdx) => { if (!app.id) ERROR(`App ${appIdx} has no id`); //console.log(`Checking ${app.id}...`); var appDir = APPSDIR+app.id+"/"; @@ -105,9 +111,15 @@ apps.forEach((app,addIdx) => { ERROR(`App ${app.id}'s ${file.name} is a JS file but isn't valid JS`); } } + for (const key in file) { + if (!STORAGE_KEYS.includes(key)) ERROR(`App ${app.id}'s ${file.name} has unknown key ${key}`); + } }); //console.log(fileNames); if (isApp && !fileNames.includes(app.id+".app.js")) ERROR(`App ${app.id} has no entrypoint`); if (isApp && !fileNames.includes(app.id+".img")) ERROR(`App ${app.id} has no JS icon`); if (app.type=="widget" && !fileNames.includes(app.id+".wid.js")) ERROR(`Widget ${app.id} has no entrypoint`); + for (const key in app) { + if (!APP_KEYS.includes(key)) ERROR(`App ${app.id} has unknown key ${key}`); + } }); From f5246a4212dff6a7c18c640539ff261846ac2ca7 Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Sun, 12 Apr 2020 01:29:57 +0200 Subject: [PATCH 027/302] Some minor settings fixes --- apps.json | 2 +- apps/setting/ChangeLog | 2 ++ apps/setting/settings.js | 14 +++++++------- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/apps.json b/apps.json index d85739aad..a8390b927 100644 --- a/apps.json +++ b/apps.json @@ -119,7 +119,7 @@ { "id": "setting", "name": "Settings", "icon": "settings.png", - "version":"0.12", + "version":"0.13", "description": "A menu for setting up Bangle.js", "tags": "tool,system", "storage": [ diff --git a/apps/setting/ChangeLog b/apps/setting/ChangeLog index 22277968c..3ca9dc3ec 100644 --- a/apps/setting/ChangeLog +++ b/apps/setting/ChangeLog @@ -12,3 +12,5 @@ 0.12: Fix memory leak (#206) Bring App settings nearer the top Move LCD Timeout to wakeup menu +0.13: Fix memory leak for App settings + Make capitalization more consistent diff --git a/apps/setting/settings.js b/apps/setting/settings.js index ac7692610..f1dc81ca9 100644 --- a/apps/setting/settings.js +++ b/apps/setting/settings.js @@ -64,7 +64,7 @@ function showMainMenu() { const mainmenu = { '': { 'title': 'Settings' }, 'Make Connectable': ()=>makeConnectable(), - 'App/widget settings': ()=>showAppSettingsMenu(), + 'App/Widget Settings': ()=>showAppSettingsMenu(), 'BLE': { value: settings.ble, format: boolFormat, @@ -81,7 +81,7 @@ function showMainMenu() { updateSettings(); } }, - 'Debug info': { + 'Debug Info': { value: settings.log, format: v => v ? "Show" : "Hide", onchange: () => { @@ -157,7 +157,7 @@ function showWakeUpMenu() { Bangle.setLCDTimeout(settings.timeout); } }, - 'Wake On BTN1': { + 'Wake on BTN1': { value: settings.options.wakeOnBTN1, format: boolFormat, onchange: () => { @@ -165,7 +165,7 @@ function showWakeUpMenu() { updateOptions(); } }, - 'Wake On BTN2': { + 'Wake on BTN2': { value: settings.options.wakeOnBTN2, format: boolFormat, onchange: () => { @@ -173,7 +173,7 @@ function showWakeUpMenu() { updateOptions(); } }, - 'Wake On BTN3': { + 'Wake on BTN3': { value: settings.options.wakeOnBTN3, format: boolFormat, onchange: () => { @@ -197,7 +197,7 @@ function showWakeUpMenu() { updateOptions(); } }, - 'Wake On Twist': { + 'Wake on Twist': { value: settings.options.wakeOnTwist, format: boolFormat, onchange: () => { @@ -450,7 +450,7 @@ function showAppSettings(app) { } try { // pass showAppSettingsMenu as "back" argument - appSettings(showAppSettingsMenu); + appSettings(()=>showAppSettingsMenu()); } catch (e) { console.log(`${app.name} settings error:`, e) return showError('Error in settings'); From eebc49470412d9cf93792df73156ee9ef12c4c3e Mon Sep 17 00:00:00 2001 From: msdeibel Date: Sun, 12 Apr 2020 13:32:25 +0200 Subject: [PATCH 028/302] Enables chart in the app --- apps/batchart/app.js | 107 ++++++++++++++++++++++++++++++++----- apps/batchart/batchart.dat | 0 apps/batchart/widget.js | 1 - 3 files changed, 95 insertions(+), 13 deletions(-) delete mode 100644 apps/batchart/batchart.dat diff --git a/apps/batchart/app.js b/apps/batchart/app.js index 684f9a88d..e8b30ba69 100644 --- a/apps/batchart/app.js +++ b/apps/batchart/app.js @@ -1,20 +1,100 @@ -// place your const, vars, functions or classes here +const GraphXZero = 40; +const GraphYZero = 180; +const GraphY100 = 80; +const GraphMarkerOffset = 5; +const MaxValueCount = 144; +const GraphXMax = GraphXZero + MaxValueCount; +var Storage = require("Storage"); -function renderBatteryChart(){ - g.drawString("t", 215, 175); - g.drawLine(40,190,40,80); +function renderCoordinateSystem() { + g.setFont("6x8", 1); + g.drawString("t", GraphXMax + GraphMarkerOffset, GraphYZero - GraphMarkerOffset); + g.drawLine(GraphXZero, GraphYZero + GraphMarkerOffset, GraphXZero, GraphY100); g.drawString("%", 39, 70); - g.drawString("100", 15, 75); - g.drawLine(35,80,40,80); - g.drawString("50", 20,125); - g.drawLine(35,130,40,130); + g.setFontAlign(1, -1, 0); + g.drawString("100", 30, GraphY100 - GraphMarkerOffset); + g.drawLine(GraphXZero - GraphMarkerOffset, GraphY100, GraphXZero, GraphY100); - g.drawString("0", 25, 175); - g.drawLine(35,180,210,180); + g.drawString("50", 30, GraphYZero - 50 - GraphMarkerOffset); + g.drawLine(GraphXZero - GraphMarkerOffset, 130, GraphXZero, 130); - g.drawString("Chart not yet functional", 60, 125); + g.drawString("0", 30, GraphYZero - GraphMarkerOffset); + g.drawLine(GraphXZero - GraphMarkerOffset, GraphYZero, GraphXMax, GraphYZero); +} + +function decrementDay(dayToDecrement) { + return dayToDecrement === 0 ? 6 : dayToDecrement-1; +} +function loadData() { + const MaxValueCount = 144; + const startingDay = new Date().getDay(); + + // Load data for the current day + var logFileName = "bclog" + startingDay; + + var dataLines = loadLinesFromFile(MaxValueCount, logFileName); + + // Top up to MaxValueCount from previous days as required + var previousDay = decrementDay(startingDay); + while (dataLines.length < MaxValueCount + && previousDay !== startingDay) { + + var topUpLogFileName = "bclog" + previousDay; + var remainingLines = MaxValueCount - dataLines.length; + var topUpLines = loadLinesFromFile(remainingLines, topUpLogFileName); + dataLines = topUpLines.concat(dataLines); + + previousDay = decrementDay(previousDay); + } + + return dataLines; +} + +function loadLinesFromFile(requestedLineCount, fileName) { + var allLines = []; + var returnLines = []; + + var readFile = Storage.open(fileName, "r"); + + while ((nextLine = readFile.readLine())) { + if(nextLine) { + allLines.push(nextLine); + } + } + + if (allLines.length <= 0) return; + + linesToReadCount = Math.min(requestedLineCount, allLines.length); + startingLineIndex = Math.max(0, allLines.length - requestedLineCount - 1); + + for (let i = startingLineIndex; i < linesToReadCount + startingLineIndex; i++) { + if(allLines[i]) { + returnLines.push(allLines[i]); + } + } + + allLines = null; + + return returnLines; +} + +function renderData(dataArray) { + g.setColor(1, 1, 0); + for (let i = 0; i < dataArray.length; i++) { + const element = dataArray[i]; + var dataInfo = element.split(","); + var batteryPercentage = parseInt(dataInfo[1]); + + g.setPixel(GraphXZero + i, GraphYZero - batteryPercentage); + } +} + +function renderBatteryChart() { + renderCoordinateSystem(); + var data = loadData(); + renderData(data); } // special function to handle display switch on @@ -22,7 +102,10 @@ Bangle.on('lcdPower', (on) => { if (on) { // call your app function here // If you clear the screen, do Bangle.drawWidgets(); - renderBatteryChart(); + //g.clear() + Bangle.loadWidgets(); + Bangle.drawWidgets(); + //renderBatteryChart(); } }); diff --git a/apps/batchart/batchart.dat b/apps/batchart/batchart.dat deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/batchart/widget.js b/apps/batchart/widget.js index 2e2f43cdf..de7ce230d 100644 --- a/apps/batchart/widget.js +++ b/apps/batchart/widget.js @@ -60,7 +60,6 @@ } } - // Called by the heart app to reload settings and decide what's function reload() { WIDGETS["batchart"].width = 24; From 1a8a0d0a63b304fa0722447f73495d1ff6625731 Mon Sep 17 00:00:00 2001 From: Markus Date: Sun, 12 Apr 2020 14:06:12 +0200 Subject: [PATCH 029/302] Version for BatChart app updated --- apps.json | 2 +- apps/batchart/ChangeLog | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps.json b/apps.json index d85739aad..027abcb7c 100644 --- a/apps.json +++ b/apps.json @@ -1150,7 +1150,7 @@ "name": "Battery Chart", "shortName":"BatChart", "icon": "app.png", - "version":"0.03", + "version":"0.04", "description": "A widget and an app for recording and visualizing battery percentage over time.", "tags": "app,widget,battery,time,record,chart,tool", "storage": [ diff --git a/apps/batchart/ChangeLog b/apps/batchart/ChangeLog index 1b77ff82f..dcbd1687b 100644 --- a/apps/batchart/ChangeLog +++ b/apps/batchart/ChangeLog @@ -1,3 +1,4 @@ 0.01: New app and widget 0.02: Widget stores data to file (1 dataset/10min) 0.03: Rotate log files once a week. +0.04: chart in the app is now active. From 1c68ea0d1bbd6e16e12662e53876e7e723935190 Mon Sep 17 00:00:00 2001 From: Paul Cockrell Date: Sun, 12 Apr 2020 14:43:52 +0100 Subject: [PATCH 030/302] Display info message when phone (dis)connectes and battery level <= 10% --- apps.json | 2 +- apps/marioclock/ChangeLog | 3 ++- apps/marioclock/marioclock-app.js | 30 +++++++++++++++++++++++++----- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/apps.json b/apps.json index 73a093a2c..6c7364a03 100644 --- a/apps.json +++ b/apps.json @@ -914,7 +914,7 @@ { "id": "marioclock", "name": "Mario Clock", "icon": "marioclock.png", - "version":"0.11", + "version":"0.12", "description": "Animated retro Mario clock, with Gameboy style 8-bit grey-scale graphics.", "tags": "clock,mario,retro", "type": "clock", diff --git a/apps/marioclock/ChangeLog b/apps/marioclock/ChangeLog index 6f3c79504..69a3ccc7b 100644 --- a/apps/marioclock/ChangeLog +++ b/apps/marioclock/ChangeLog @@ -8,4 +8,5 @@ 0.08: Update date panel to be info panel toggling between Date, Battery and Temperature. Add Princes Daisy 0.09: Add GadgetBridge functionality. Mario shows message type in speach bubble, while message scrolls in info panel 0.10: Swiping left to enable night-mode now also reduces LCD brightness through 3 levels before returning to day-mode. -0.11: User settings persisted and read to file. \ No newline at end of file +0.11: User settings persisted and read to file. +0.12: Add info banner message when phone (dis)connects. Display low-battery warning (<=10%) \ No newline at end of file diff --git a/apps/marioclock/marioclock-app.js b/apps/marioclock/marioclock-app.js index eb9632c59..4acbf384b 100644 --- a/apps/marioclock/marioclock-app.js +++ b/apps/marioclock/marioclock-app.js @@ -346,16 +346,20 @@ function drawToadFrame(idx, x, y) { function drawNotice(x, y) { if (phone.message === null) return; + let img; switch (phone.messageType) { case "call": - const callImg = require("heatshrink").decompress(atob("h8PxH+AAMHABIND6wAJB4INEw9cAAIPFBxAPEBw/WBxYACDrQ7QLI53OSpApDBoQAHB4INLByANNAwo=")); - g.drawImage(callImg, characterSprite.x, characterSprite.y - 16); + img = require("heatshrink").decompress(atob("h8PxH+AAMHABIND6wAJB4INEw9cAAIPFBxAPEBw/WBxYACDrQ7QLI53OSpApDBoQAHB4INLByANNAwo=")); break; case "notify": - const msgImg = require("heatshrink").decompress(atob("h8PxH+AAMHABIND6wAJB4INCrgAHB4QOEDQgOIAIQFGBwovDA4gOGFooOVLJR3OSpApDBoQAHB4INLByANNAwoA=")); - g.drawImage(msgImg, characterSprite.x, characterSprite.y - 16); + img = require("heatshrink").decompress(atob("h8PxH+AAMHABIND6wAJB4INCrgAHB4QOEDQgOIAIQFGBwovDA4gOGFooOVLJR3OSpApDBoQAHB4INLByANNAwoA=")); + break; + case "lowBatt": + img = require("heatshrink").decompress(atob("h8PxH+AAMHABIND6wAJB4INFrgABB4oOEBoQPFBwwDGB0uHAAIOLJRB3OSpApDBoQAHB4INLByANNAwo")); break; } + + g.drawImage(img, characterSprite.x, characterSprite.y - 16); } function drawCharacter(date, character) { @@ -598,6 +602,14 @@ function updateSettings() { writeSettings(newSettings); } +function checkBatteryLevel() { + if (Bangle.isCharging()) return; + if (E.getBattery() > 10) return; + if (phone.message !== null) return; + + phoneNewMessage("lowBatt", "Warning, battery is low"); +} + // Main function init() { loadSettings(); @@ -658,17 +670,25 @@ function init() { // Phone connectivity try { NRF.wake(); } catch (e) {} - NRF.on('disconnect', () => Bangle.buzz()); + NRF.on('disconnect', () => { + Bangle.buzz(); + phoneNewMessage(null, "Phone disconnected"); + }); + NRF.on('connect', () => { setTimeout(() => { phoneOutbound({ t: "status", bat: E.getBattery() }); }, ONE_SECOND * 2); Bangle.buzz(); + phoneNewMessage(null, "Phone connected"); }); GB = (evt) => phoneInbound(evt); startTimers(); + + setInterval(checkBatteryLevel, ONE_SECOND * 60 * 10); + checkBatteryLevel(); } // Initialise! From 900b820474625dbd6437abed026203146e808a04 Mon Sep 17 00:00:00 2001 From: msdeibel Date: Mon, 13 Apr 2020 11:14:50 +0200 Subject: [PATCH 031/302] Display temperature and LCD state in chart --- apps.json | 4 +- apps/batchart/ChangeLog | 1 + apps/batchart/app.js | 137 ++++++++++++++++++++++++++++++++++------ 3 files changed, 119 insertions(+), 23 deletions(-) diff --git a/apps.json b/apps.json index d85739aad..2fa94fa18 100644 --- a/apps.json +++ b/apps.json @@ -1148,9 +1148,9 @@ }, { "id": "batchart", "name": "Battery Chart", - "shortName":"BatChart", + "shortName":"Battery Chart", "icon": "app.png", - "version":"0.03", + "version":"0.04", "description": "A widget and an app for recording and visualizing battery percentage over time.", "tags": "app,widget,battery,time,record,chart,tool", "storage": [ diff --git a/apps/batchart/ChangeLog b/apps/batchart/ChangeLog index 1b77ff82f..8c1e8f995 100644 --- a/apps/batchart/ChangeLog +++ b/apps/batchart/ChangeLog @@ -1,3 +1,4 @@ 0.01: New app and widget 0.02: Widget stores data to file (1 dataset/10min) 0.03: Rotate log files once a week. +0.04: Display temperature and LCD state in chart diff --git a/apps/batchart/app.js b/apps/batchart/app.js index e8b30ba69..4fb919354 100644 --- a/apps/batchart/app.js +++ b/apps/batchart/app.js @@ -1,17 +1,26 @@ const GraphXZero = 40; const GraphYZero = 180; const GraphY100 = 80; + const GraphMarkerOffset = 5; const MaxValueCount = 144; const GraphXMax = GraphXZero + MaxValueCount; + +const GraphLcdY = GraphYZero + 10; +// const GraphCompassY = GraphYZero + 16; +// const GraphBluetoothY = GraphYZero + 22; +// const GraphGpsY = GraphYZero + 28; +// const GraphHrmY = GraphYZero + 34; + var Storage = require("Storage"); function renderCoordinateSystem() { g.setFont("6x8", 1); - g.drawString("t", GraphXMax + GraphMarkerOffset, GraphYZero - GraphMarkerOffset); - g.drawLine(GraphXZero, GraphYZero + GraphMarkerOffset, GraphXZero, GraphY100); - g.drawString("%", 39, 70); + // Left Y axis (Battery) + g.setColor(1, 1, 0); + g.drawLine(GraphXZero, GraphYZero + GraphMarkerOffset, GraphXZero, GraphY100); + g.drawString("%", 39, GraphY100 - 10); g.setFontAlign(1, -1, 0); g.drawString("100", 30, GraphY100 - GraphMarkerOffset); @@ -21,29 +30,47 @@ function renderCoordinateSystem() { g.drawLine(GraphXZero - GraphMarkerOffset, 130, GraphXZero, 130); g.drawString("0", 30, GraphYZero - GraphMarkerOffset); - g.drawLine(GraphXZero - GraphMarkerOffset, GraphYZero, GraphXMax, GraphYZero); + + g.setColor(1,1,1); + g.setFontAlign(1, -1, 0); + g.drawLine(GraphXZero - GraphMarkerOffset, GraphYZero, GraphXMax + GraphMarkerOffset, GraphYZero); + + // Right Y axis (Temperature) + g.setColor(0.4, 0.4, 1); + g.drawLine(GraphXMax, GraphYZero + GraphMarkerOffset, GraphXMax, GraphY100); + g.drawString("°C", GraphXMax + GraphMarkerOffset, GraphY100 - 10); + g.setFontAlign(-1, -1, 0); + g.drawString("20", GraphXMax + 2 * GraphMarkerOffset, GraphYZero - GraphMarkerOffset); + + g.drawLine(GraphXMax + GraphMarkerOffset, 130, GraphXMax, 130); + g.drawString("30", GraphXMax + 2 * GraphMarkerOffset, GraphYZero - 50 - GraphMarkerOffset); + + g.drawLine(GraphXMax + GraphMarkerOffset, 80, GraphXMax, 80); + g.drawString("40", GraphXMax + 2 * GraphMarkerOffset, GraphY100 - GraphMarkerOffset); + + g.setColor(1,1,1); } function decrementDay(dayToDecrement) { return dayToDecrement === 0 ? 6 : dayToDecrement-1; } + function loadData() { - const MaxValueCount = 144; const startingDay = new Date().getDay(); // Load data for the current day - var logFileName = "bclog" + startingDay; + let logFileName = "bclog" + startingDay; - var dataLines = loadLinesFromFile(MaxValueCount, logFileName); + let dataLines = loadLinesFromFile(MaxValueCount, logFileName); // Top up to MaxValueCount from previous days as required - var previousDay = decrementDay(startingDay); + let previousDay = decrementDay(startingDay); while (dataLines.length < MaxValueCount && previousDay !== startingDay) { - var topUpLogFileName = "bclog" + previousDay; - var remainingLines = MaxValueCount - dataLines.length; - var topUpLines = loadLinesFromFile(remainingLines, topUpLogFileName); + let topUpLogFileName = "bclog" + previousDay; + let remainingLines = MaxValueCount - dataLines.length; + let topUpLines = loadLinesFromFile(remainingLines, topUpLogFileName); dataLines = topUpLines.concat(dataLines); previousDay = decrementDay(previousDay); @@ -53,8 +80,8 @@ function loadData() { } function loadLinesFromFile(requestedLineCount, fileName) { - var allLines = []; - var returnLines = []; + let allLines = []; + let returnLines = []; var readFile = Storage.open(fileName, "r"); @@ -63,11 +90,13 @@ function loadLinesFromFile(requestedLineCount, fileName) { allLines.push(nextLine); } } + + readFile = null; if (allLines.length <= 0) return; - linesToReadCount = Math.min(requestedLineCount, allLines.length); - startingLineIndex = Math.max(0, allLines.length - requestedLineCount - 1); + let linesToReadCount = Math.min(requestedLineCount, allLines.length); + let startingLineIndex = Math.max(0, allLines.length - requestedLineCount - 1); for (let i = startingLineIndex; i < linesToReadCount + startingLineIndex; i++) { if(allLines[i]) { @@ -81,20 +110,86 @@ function loadLinesFromFile(requestedLineCount, fileName) { } function renderData(dataArray) { - g.setColor(1, 1, 0); + const switchableConsumers = { + none: 0, + lcd: 1, + compass: 2, + bluetooth: 4, + gps: 8, + hrm: 16 + }; + + //const timestampIndex = 0; + const batteryIndex = 1; + const temperatureIndex = 2; + const switchabelsIndex = 3; + + var allConsumers = switchableConsumers.none | switchableConsumers.lcd | switchableConsumers.compass | switchableConsumers.bluetooth | switchableConsumers.gps | switchableConsumers.hrm; + for (let i = 0; i < dataArray.length; i++) { const element = dataArray[i]; + var dataInfo = element.split(","); - var batteryPercentage = parseInt(dataInfo[1]); - g.setPixel(GraphXZero + i, GraphYZero - batteryPercentage); + // Battery percentage + g.setColor(1, 1, 0); + g.setPixel(GraphXZero + i, GraphYZero - parseInt(dataInfo[batteryIndex])); + + // Temperature + g.setColor(0.4, 0.4, 1); + let scaledTemp = Math.floor(((parseFloat(dataInfo[temperatureIndex]) * 100) - 2000)/20) + ((((parseFloat(dataInfo[temperatureIndex]) * 100) - 2000) % 100)/25); + + g.setPixel(GraphXZero + i, GraphYZero - scaledTemp); + + // LCD state + if (parseInt(dataInfo[switchabelsIndex]) & switchableConsumers.lcd == switchableConsumers.lcd) { + g.setColor(1, 1, 1); + g.setFontAlign(1, -1, 0); + g.drawString("LCD", GraphXZero - GraphMarkerOffset, GraphLcdY - 2, true); + g.drawLine(GraphXZero + i, GraphLcdY, GraphXZero + i, GraphLcdY + 1); + } + + // // Compass state + // if (switchables & switchableConsumers.lcd == switchableConsumers.lcd) { + // g.setColor(0, 1, 0); + // g.setFontAlign(-1, -1, 0); + // g.drawString("Compass", GraphXMax + GraphMarkerOffset, GraphCompassY - 2, true); + // g.drawLine(GraphXZero + i, GraphCompassY, GraphXZero + i, GraphCompassY + 1); + // } + + // // Bluetooth state + // if (switchables & switchableConsumers.lcd == switchableConsumers.lcd) { + // g.setColor(0, 0, 1); + // g.setFontAlign(1, -1, 0); + // g.drawString("BLE", GraphXZero - GraphMarkerOffset, GraphBluetoothY - 2, true); + // g.drawLine(GraphXZero + i, GraphBluetoothY, GraphXZero + i, GraphBluetoothY + 1); + // } + + // // Gps state + // if (switchables & switchableConsumers.lcd == switchableConsumers.lcd) { + // g.setColor(0.8, 0.5, 0.24); + // g.setFontAlign(-1, -1, 0); + // g.drawString("GPS", GraphXMax + GraphMarkerOffset, GraphGpsY - 2, true); + // g.drawLine(GraphXZero + i, GraphGpsY, GraphXZero + i, GraphGpsY + 1); + // } + + // // Hrm state + // if (switchables & switchableConsumers.lcd == switchableConsumers.lcd) { + // g.setColor(1, 0, 0); + // g.setFontAlign(1, -1, 0); + // g.drawString("HRM", GraphXZero - GraphMarkerOffset, GraphHrmY - 2, true); + // g.drawLine(GraphXZero + i, GraphHrmY, GraphXZero + i, GraphHrmY + 1); + // } } + + dataArray = null; } function renderBatteryChart() { renderCoordinateSystem(); - var data = loadData(); + let data = loadData(); renderData(data); + data = null; } // special function to handle display switch on @@ -102,10 +197,10 @@ Bangle.on('lcdPower', (on) => { if (on) { // call your app function here // If you clear the screen, do Bangle.drawWidgets(); - //g.clear() + g.clear() Bangle.loadWidgets(); Bangle.drawWidgets(); - //renderBatteryChart(); + renderBatteryChart(); } }); From 65f5a46dd95d5c63b27a0a8ee958d432f082ea84 Mon Sep 17 00:00:00 2001 From: Purple-Tentacle <59914607+Purple-Tentacle@users.noreply.github.com> Date: Mon, 13 Apr 2020 12:53:38 +0200 Subject: [PATCH 032/302] Create ChangeLog --- apps/activepedom/ChangeLog | 1 + 1 file changed, 1 insertion(+) create mode 100644 apps/activepedom/ChangeLog diff --git a/apps/activepedom/ChangeLog b/apps/activepedom/ChangeLog new file mode 100644 index 000000000..4c21f3ace --- /dev/null +++ b/apps/activepedom/ChangeLog @@ -0,0 +1 @@ +0.01: New Widget! From 0cee31908614af8b8e02f7c9fd1f12073c95a2ab Mon Sep 17 00:00:00 2001 From: Purple-Tentacle <59914607+Purple-Tentacle@users.noreply.github.com> Date: Mon, 13 Apr 2020 13:04:18 +0200 Subject: [PATCH 033/302] Initial transfer --- apps/activepedom/10600.png | Bin 0 -> 370 bytes apps/activepedom/1600.png | Bin 0 -> 374 bytes apps/activepedom/600.png | Bin 0 -> 338 bytes apps/activepedom/README.md | 38 ++++++++ apps/activepedom/app-icon.js | 1 + apps/activepedom/app.png | Bin 0 -> 836 bytes apps/activepedom/settings.js | 81 ++++++++++++++++ apps/activepedom/widget.js | 180 +++++++++++++++++++++++++++++++++++ 8 files changed, 300 insertions(+) create mode 100644 apps/activepedom/10600.png create mode 100644 apps/activepedom/1600.png create mode 100644 apps/activepedom/600.png create mode 100644 apps/activepedom/README.md create mode 100644 apps/activepedom/app-icon.js create mode 100644 apps/activepedom/app.png create mode 100644 apps/activepedom/settings.js create mode 100644 apps/activepedom/widget.js diff --git a/apps/activepedom/10600.png b/apps/activepedom/10600.png new file mode 100644 index 0000000000000000000000000000000000000000..36de436df0021f74082396aab83d959120af1fc5 GIT binary patch literal 370 zcmV-&0ge8NP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGqB>(^xB>_oNB=7(L0S8G$K~z{r?Um6E zgCGn=+2Vu$|Fu4tY}8Rx8ZJsPW;C9pK??|$J0qJFSh zwJEg-q^^!TJ6|J?E1{J|${}S@{81&0m2!AHkrHI2M57uT*7(kK9ah4qL^G^>X9XCC zm9WyPD34>~cq1+Px#1ZP1_K>z@;j|==^1poj532;bRa{vGqB>(^xB>_oNB=7(L0Sie)K~z{r?UmaO z!ypJi-O`7Cwx8XHCYvmhDGKUlt<|1{*uvx@mOYeGFC#~PSMJ9Pc#?w$3f$8@rAr5`=*eiI>q^;c6_G@ z`AqshRSKLece?)oopD8oGuiY@!QE-x&)E?!4&9gX7qL$|{VoVw)V6PGT~_<{1@wSx zLmiN#ZvhnLiAm-oF}Xc U&O83Y0{{R307*qoM6N<$f?Z6Z`Tzg` literal 0 HcmV?d00001 diff --git a/apps/activepedom/600.png b/apps/activepedom/600.png new file mode 100644 index 0000000000000000000000000000000000000000..4d2c625c709002faa5e3e5207e0caa12782fe1d9 GIT binary patch literal 338 zcmV-Y0j>UtP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGqB>(^xB>_oNB=7(L0Ov_WK~z{r-IdV} z!ypJn-O`7Cc0aohO*(13hEj2oY^`zBSl+OwdPlF{`^8AX;833oOi^Eb=Ix< z4Y=t@{U4EN>is*v0p}eVkm5nsZ9V)v&4(utBU<85KnKYd3p?#)jMms|@h9wbM`9t_ zW6hyhNp&rLCzs1k1;I*(>6PYJtLJVIm&?w(zzej-@5asZn@5Y3MF(QJU*xBe^AR;z`8dFFL zUsbR>Fo;!9EEKUY+GwFz*~Jh=379B|Xkj6W8t)RLCYKaGh{f!Zz1f-B$?ap3KYZk| z|M|Y(cQZRXdyu~JzyMJ8;xUlRpl%|%2+V4t$03)M^626HSkN;uKy(BB&gd*OMRWs` zaf{T(O%PMFaTBE3zzYk{aEkR~D@XJaI^c}XQa$M5FF53ymvV^RX3C3x7QrXK=v4Oj831Y|-y`(Ee+f6AiE4qTQt>jdQS#LQW9S>WA^klD>`fmZ&M06`DSVq0 zzF=$sN{asv0f$KO-9#=rs3~^b0RPGJ^cpD^$2Dk^Frv>r7kZGn$ zKu3}dv?I$NL4Kg zfMBeS7{3Pb3@IXJr8g~5lU|~4;Iw6qdSH1fbQU^{zJXuBdRJgJs`q{t&^K}437Mt( zfK}e_1@uj{xqypO9pY#)g1DJM-^546%g}<-4(0$~F*g(4sGGR!fYGQER07(}L}%ud z23;czxIJ*aptwE(G-OI?&@h#7PZS>2ayph3ph>$BFX=(BBO31=$@e@9K$BJ?j=({v zabT&&znkzZw5E@#*F6Q$(TFi4lK*O&fF^YjhQDbs?xZy4Ck}4rR>H^MHZ@bSDW;E(H;iWU*PcZo8_9K(;h&mO8`>M<}B<0EEX8tM_eM|K( zA01MzViJH+8VwYjA>JO|0DKSR8;wn8p*-*zF%wCg>)U{D2>5)pN&W+*T-arpW9jGs O0000 { + var stepTimeDiff = 9999; //Time difference between two steps + var startTimeStep = new Date(); //set start time + var stopTimeStep = 0; //Time after one step + var timerResetActive = 0; //timer to reset active + var steps = 0; //steps taken + var stepsCounted = 0; //active steps counted + var active = 0; //x steps in y seconds achieved + var stepGoalPercent = 0; //percentage of step goal + var stepGoalBarLength = 0; //length og progress bar + var lastUpdate = new Date(); + var width = 45; + + var stepsTooShort = 0; + var stepsTooLong = 0; + var stepsOutsideTime = 0; + + //define default settings + const DEFAULTS = { + 'cMaxTime' : 1100, + 'cMinTime' : 240, + 'stepThreshold' : 30, + 'intervalResetActive' : 30000, + 'stepSensitivity' : 80, + 'stepGoal' : 10000, + }; + const SETTINGS_FILE = 'activepedom.settings.json'; + const PEDOMFILE = "activepedom.steps.json"; + + let settings; + //load settings + function loadSettings() { + settings = require('Storage').readJSON(SETTINGS_FILE, 1) || {}; + } + //return setting + function setting(key) { + if (!settings) { loadSettings(); } + return (key in settings) ? settings[key] : DEFAULTS[key]; + } + + function setStepSensitivity(s) { + function sqr(x) { return x*x; } + var X=sqr(8192-s); + var Y=sqr(8192+s); + Bangle.setOptions({stepCounterThresholdLow:X,stepCounterThresholdHigh:Y}); + } + + //format number to make them shorter + function kFormatter(num) { + if (num <= 999) return num; //smaller 1.000, return 600 as 600 + if (num >= 1000 && num < 10000) { //between 1.000 and 10.000 + num = Math.floor(num/100)*100; + return (num / 1000).toFixed(1).replace(/\.0$/, '') + 'k'; //return 1600 as 1.6k + } + if (num >= 10000) { //greater 10.000 + num = Math.floor(num/1000)*1000; + return (num / 1000).toFixed(1).replace(/\.0$/, '') + 'k'; //return 10.600 as 10k + } + } + + //Set Active to 0 + function resetActive() { + active = 0; + steps = 0; + if (Bangle.isLCDOn()) WIDGETS["activepedom"].draw(); + } + + function calcSteps() { + stopTimeStep = new Date(); //stop time after each step + stepTimeDiff = stopTimeStep - startTimeStep; //time between steps in milliseconds + startTimeStep = new Date(); //start time again + + //Remove step if time between first and second step is too long + if (stepTimeDiff >= setting('cMaxTime')) { //milliseconds + stepsTooLong++; //count steps which are note counted, because time too long + steps--; + } + + //Remove step if time between first and second step is too short + if (stepTimeDiff <= setting('cMinTime')) { //milliseconds + stepsTooShort++; //count steps which are note counted, because time too short + steps--; + } + + if (steps >= setting('stepThreshold')) { + if (active == 0) { + stepsCounted = stepsCounted + (stepThreshold -1) ; //count steps needed to reach active status, last step is counted anyway, so treshold -1 + stepsOutsideTime = stepsOutsideTime - 10; //substract steps needed to reac active status + } + active = 1; + clearInterval(timerResetActive); //stop timer which resets active + timerResetActive = setInterval(resetActive, setting('intervalResetActive')); //reset active after timer runs out + steps = 0; + } + + if (active == 1) { + stepsCounted++; //count steps + } + else { + stepsOutsideTime++; + } + } + + function draw() { + var height = 23; //width is deined globally + var stepsDisplayLarge = kFormatter(stepsCounted); + + //Check if same day + let date = new Date(); + if (lastUpdate.getDate() == date.getDate()){ //if same day + } + else { + stepsCounted = 1; //set stepcount to 1 + } + lastUpdate = date; + + g.reset(); + g.clearRect(this.x, this.y, this.x+width, this.y+height); + + //draw numbers + if (active == 1) g.setColor(0x07E0); //green + else g.setColor(0xFFFF); //white + g.setFont("6x8", 2); + g.drawString(stepsDisplayLarge,this.x+1,this.y); //first line, big number + g.setFont("6x8", 1); + g.setColor(0xFFFF); //white + g.drawString(stepsCounted,this.x+1,this.y+14); //second line, small number + + //draw step goal bar + stepGoalPercent = (stepsCounted / setting('stepGoal')) * 100; + stepGoalBarLength = width / 100 * stepGoalPercent; + if (stepGoalBarLength > width) stepGoalBarLength = width; //do not draw across width of widget + g.setColor(0x7BEF); //grey + g.fillRect(this.x, this.y+height, this.x+width, this.y+height); // draw background bar + g.setColor(0xFFFF); //white + g.fillRect(this.x, this.y+height, this.x+1, this.y+height-1); //draw start of bar + g.fillRect(this.x+width, this.y+height, this.x+width-1, this.y+height-1); //draw end of bar + g.fillRect(this.x, this.y+height, this.x+stepGoalBarLength, this.y+height); // draw progress bar + } + + //This event is called just before the device shuts down for commands such as reset(), load(), save(), E.reboot() or Bangle.off() + E.on('kill', () => { + let d = { //define array to write to file + lastUpdate : lastUpdate.toISOString(), + stepsToday : stepsCounted, + stepsTooShort : stepsTooShort, + stepsTooLong : stepsTooLong, + stepsOutsideTime : stepsOutsideTime + }; + require("Storage").write(PEDOMFILE,d); //write array to file + }); + + //When Step is registered by firmware + Bangle.on('step', (up) => { + steps++; //increase step count + calcSteps(); + if (Bangle.isLCDOn()) WIDGETS["activepedom"].draw(); + }); + + // redraw when the LCD turns on + Bangle.on('lcdPower', function(on) { + if (on) WIDGETS["activepedom"].draw(); + }); + + //Read data from file and set variables + let pedomData = require("Storage").readJSON(PEDOMFILE,1); + if (pedomData) { + if (pedomData.lastUpdate) lastUpdate = new Date(pedomData.lastUpdate); + stepsCounted = pedomData.stepsToday|0; + stepsTooShort = pedomData.stepsTooShort; + stepsTooLong = pedomData.stepsTooLong; + stepsOutsideTime = pedomData.stepsOutsideTime; + } + + setStepSensitivity(setting('stepSensitivity')); //set step sensitivity (80 is standard, 400 is muss less sensitive) + + //Add widget + WIDGETS["activepedom"]={area:"tl",width:width,draw:draw}; + +})(); \ No newline at end of file From 43d0f455a7d730632f6b8b00cc464cd959fd7025 Mon Sep 17 00:00:00 2001 From: Purple-Tentacle <59914607+Purple-Tentacle@users.noreply.github.com> Date: Mon, 13 Apr 2020 13:09:30 +0200 Subject: [PATCH 034/302] Active Pedometer --- apps.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/apps.json b/apps.json index e9ba66804..49e2bad6a 100644 --- a/apps.json +++ b/apps.json @@ -1107,6 +1107,19 @@ {"name":"openstmap.app.js","url":"app.js"}, {"name":"openstmap.img","url":"app-icon.js","evaluate":true} ] + }, + { "id": "activepedom", + "name": "Active Pedometer", + "shortName":"activepedom", + "icon": "app.png", + "version":"0.01", + "description": "Pedometer that filters out arm movement and displays a step goal progress.", + "tags": "outdoors,widget", + "storage": [ + {"name":"activepedom.wid.js","url":"widget.js"}, + {"name":"activepedom.settings.js","url":"settings.js"}, + {"name":"activepedom.img","url":"app-icon.js","evaluate":true} + ] }, { "id": "tabata", "name": "Tabata", From 1bb6af5ad50833b8cd6a2ae29c34b3061e5911c8 Mon Sep 17 00:00:00 2001 From: Purple-Tentacle <59914607+Purple-Tentacle@users.noreply.github.com> Date: Mon, 13 Apr 2020 13:19:45 +0200 Subject: [PATCH 035/302] added type widget --- apps.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apps.json b/apps.json index 49e2bad6a..a0b1a56aa 100644 --- a/apps.json +++ b/apps.json @@ -1115,6 +1115,7 @@ "version":"0.01", "description": "Pedometer that filters out arm movement and displays a step goal progress.", "tags": "outdoors,widget", + "type":"widget", "storage": [ {"name":"activepedom.wid.js","url":"widget.js"}, {"name":"activepedom.settings.js","url":"settings.js"}, From 477e6f07609a6a80a69cc9265732699d544a3c53 Mon Sep 17 00:00:00 2001 From: fredericrous Date: Mon, 13 Apr 2020 03:10:18 +0100 Subject: [PATCH 036/302] Add App Calculator --- apps.json | 12 + apps/calculator/ChangeLog | 1 + apps/calculator/app.js | 352 +++++++++++++++++++++++++++++ apps/calculator/calculator-icon.js | 1 + apps/calculator/calculator.info | 1 + apps/calculator/calculator.png | Bin 0 -> 10312 bytes 6 files changed, 367 insertions(+) create mode 100644 apps/calculator/ChangeLog create mode 100644 apps/calculator/app.js create mode 100644 apps/calculator/calculator-icon.js create mode 100644 apps/calculator/calculator.info create mode 100644 apps/calculator/calculator.png diff --git a/apps.json b/apps.json index d85739aad..77239e39e 100644 --- a/apps.json +++ b/apps.json @@ -1158,5 +1158,17 @@ {"name":"batchart.app.js","url":"app.js"}, {"name":"batchart.img","url":"app-icon.js","evaluate":true} ] + }, + { "id": "calculator", + "name": "Calculator", + "shortName":"Calculator", + "icon": "calculator.png", + "version":"0.01", + "description": "Basic calculator reminiscent of MacOs's one. Handy for small calculus. Push button1 and 3 to navigate up/down, tap right or left to navigate the sides, push button 2 to select.", + "tags": "app,tool", + "storage": [ + {"name":"calculator.app.js","url":"app.js"}, + {"name":"calculator.img","url":"calculator-icon.js","evaluate":true} + ] } ] diff --git a/apps/calculator/ChangeLog b/apps/calculator/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/calculator/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/calculator/app.js b/apps/calculator/app.js new file mode 100644 index 000000000..91dd7c49d --- /dev/null +++ b/apps/calculator/app.js @@ -0,0 +1,352 @@ +/** + * BangleJS Calculator + * + * Original Author: Frederic Rousseau https://github.com/fredericrous + * Created: April 2020 + */ + +g.clear(); +Graphics.prototype.setFont7x11Numeric7Seg = function() { + this.setFontCustom(atob("ACAB70AYAwBgC94AAAAAAAAAAB7wAAPQhhDCGELwAAAAhDCGEMIXvAAeACAEAIAQPeAA8CEMIYQwhA8AB70IYQwhhCB4AAAIAQAgBAB7wAHvQhhDCGEL3gAPAhDCGEMIXvAAe9CCEEIIQPeAA94EIIQQghA8AB70AYAwBgCAAAAHgQghBCCF7wAHvQhhDCGEIAAAPehBCCEEIAAAAA=="), 46, atob("AgAHBwcHBwcHBwcHAAAAAAAAAAcHBwcHBw=="), 11); +}; + +var DEFAULT_SELECTION = '5'; +var BOTTOM_MARGIN = 10; +var RIGHT_MARGIN = 20; +var COLORS = { + // [normal, selected] + DEFAULT: ['#7F8183', '#A6A6A7'], + OPERATOR: ['#F99D1C', '#CA7F2A'], + SPECIAL: ['#65686C', '#7F8183'] +}; + +var keys = { + '0': { + xy: [0, 200, 120, 240], + trbl: '2.00' + }, + '.': { + xy: [120, 200, 180, 240], + trbl: '3=.0' + }, + '=': { + xy: [181, 200, 240, 240], + trbl: '+==.', + color: COLORS.OPERATOR + }, + '1': { + xy: [0, 160, 60, 200], + trbl: '4201' + }, + '2': { + xy: [60, 160, 120, 200], + trbl: '5301' + }, + '3': { + xy: [120, 160, 180, 200], + trbl: '6+.2' + }, + '+': { + xy: [181, 160, 240, 200], + trbl: '-+=3', + color: COLORS.OPERATOR + }, + '4': { + xy: [0, 120, 60, 160], + trbl: '7514' + }, + '5': { + xy: [60, 120, 120, 160], + trbl: '8624' + }, + '6': { + xy: [120, 120, 180, 160], + trbl: '9-35' + }, + '-': { + xy: [181, 120, 240, 160], + trbl: '*-+6', + color: COLORS.OPERATOR + }, + '7': { + xy: [0, 80, 60, 120], + trbl: 'R847' + }, + '8': { + xy: [60, 80, 120, 120], + trbl: 'N957' + }, + '9': { + xy: [120, 80, 180, 120], + trbl: '%*68' + }, + '*': { + xy: [181, 80, 240, 120], + trbl: '/*-9', + color: COLORS.OPERATOR + }, + 'R': { + xy: [0, 40, 60, 79], + trbl: 'RN7R', + color: COLORS.SPECIAL, + val: 'AC' + }, + 'N': { + xy: [60, 40, 120, 79], + trbl: 'N%8R', + color: COLORS.SPECIAL, + val: '+/-' + }, + '%': { + xy: [120, 40, 180, 79], + trbl: '%/9N', + color: COLORS.SPECIAL + }, + '/': { + xy: [181, 40, 240, 79], + trbl: '//*%', + color: COLORS.OPERATOR + } +}; + +var selected = DEFAULT_SELECTION; +var prevSelected = DEFAULT_SELECTION; +var prevNumber = null; +var currNumber = null; +var operator = null; +var results = null; +var isDecimal = false; +var hasPressedEquals = false; + +function drawKey(name, k, selected) { + var rMargin = 0; + var bMargin = 0; + var color = k.color || COLORS.DEFAULT; + g.setColor(color[selected ? 1 : 0]); + g.setFont('Vector', 20); + g.fillRect(k.xy[0], k.xy[1], k.xy[2], k.xy[3]); + g.setColor(-1); + // correct margins to center the texts + if (name == '0') { + rMargin = (RIGHT_MARGIN * 2) - 7; + } else if (name === '/') { + rMargin = 5; + } else if (name === '*') { + bMargin = 5; + rMargin = 3; + } else if (name === '-') { + rMargin = 3; + } else if (name === 'R' || name === 'N') { + rMargin = k.val === 'C' ? 0 : -9; + } else if (name === '%') { + rMargin = -3; + } + g.drawString(k.val || name, k.xy[0] + RIGHT_MARGIN + rMargin, k.xy[1] + BOTTOM_MARGIN + bMargin); +} + +function doMath(x, y, operator) { + // might not be a number due to display of dot "." algo + x = Number(x); + y = Number(y); + switch (operator) { + case '/': + return x / y; + case '*': + return x * y; + case '+': + return x + y; + case '-': + return x - y; + } +} + +function displayOutput(num) { + var len; + var minusMarge = 0; + g.setColor(0); + g.fillRect(0, 0, 240, 39); + g.setColor(-1); + if (num === Infinity || num === -Infinity || isNaN(num)) { + // handle division by 0 + if (num === Infinity) { + num = 'INFINITY'; + } else if (num === -Infinity) { + num = '-INFINITY'; + } else { + num = 'NOT A NUMBER'; + minusMarge = -25; + } + len = (num + '').length; + currNumber = null; + results = null; + isDecimal = false; + hasPressedEquals = false; + prevNumber = null; + operator = null; + keys.R.val = 'AC'; + drawKey('R', keys.R); + g.setFont('Vector', 22); + } else { + // might not be a number due to display of dot "." + var numNumeric = Number(num); + + if (typeof num === 'string') { + if (num.indexOf('.') !== -1) { + // display a 0 before a lonely dot + if (numNumeric == 0) { + num = '0.'; + } + } else { + // remove preceding 0 + while (num.length > 1 && num[0] === '0') + num = num.substr(1); + } + } + + len = (num + '').length; + if (numNumeric < 0) { + // minus is not available in font 7x11Numeric7Seg, we use Vector + g.setFont('Vector', 20); + g.drawString('-', 220 - (len * 15), 10); + minusMarge = 15; + } + g.setFont('7x11Numeric7Seg', 2); + } + g.drawString(num, 220 - (len * 15) + minusMarge, 10); +} + +function calculatorLogic(x) { + if (hasPressedEquals) { + currNumber = results; + prevNumber = null; + operator = null; + results = null; + isDecimal = null; + displayOutput(currNumber); + hasPressedEquals = false; + } + if (prevNumber != null && currNumber != null && operator != null) { + // we execute the calculus only when there was a previous number entered before and an operator + results = doMath(prevNumber, currNumber, operator); + operator = x; + prevNumber = results; + currNumber = null; + displayOutput(results); + } else if (prevNumber == null && currNumber != null && operator == null) { + // no operator yet, save the current number for later use when an operator is pressed + operator = x; + prevNumber = currNumber; + currNumber = null; + displayOutput(prevNumber); + } else if (prevNumber == null && currNumber == null && operator == null) { + displayOutput(0); + } +} + +function buttonPress(val) { + switch (val) { + case 'R': + currNumber = null; + results = null; + isDecimal = false; + hasPressedEquals = false; + if (keys.R.val == 'AC') { + prevNumber = null; + operator = null; + } else { + keys.R.val = 'AC'; + drawKey('R', keys.R); + } + displayOutput(0); + break; + case '%': + if (results != null) { + displayOutput(results /= 100); + } else if (currNumber != null) { + displayOutput(currNumber /= 100); + } + break; + case 'N': + if (results != null) { + displayOutput(results *= -1); + } else if (currNumber != null) { + displayOutput(currNumber *= -1); + } + break; + case '/': + case '*': + case '-': + case '+': + calculatorLogic(val); + break; + case '.': + keys.R.val = 'C'; + drawKey('R', keys.R); + isDecimal = true; + displayOutput(currNumber == null ? 0 + '.' : currNumber + '.'); + break; + case '=': + if (prevNumber != null && currNumber != null && operator != null) { + results = doMath(prevNumber, currNumber, operator); + prevNumber = results; + displayOutput(results); + hasPressedEquals = true; + } + break; + default: + keys.R.val = 'C'; + drawKey('R', keys.R); + if (isDecimal) { + currNumber = currNumber == null ? 0 + '.' + val : currNumber + '.' + val; + isDecimal = false; + } else { + currNumber = currNumber == null ? val : currNumber + val; + } + displayOutput(currNumber); + break; + } +} + +for (var k in keys) { + if (keys.hasOwnProperty(k)) { + drawKey(k, keys[k], k == '5'); + } +} +g.setFont('7x11Numeric7Seg', 2.8); +g.drawString('0', 205, 10); + + +setWatch(function() { + drawKey(selected, keys[selected]); + // key 0 is 2 keys wide, go up to 1 if it was previously selected + if (selected == '0' && prevSelected === '1') { + prevSelected = selected; + selected = '1'; + } else { + prevSelected = selected; + selected = keys[selected].trbl[0]; + } + drawKey(selected, keys[selected], true); +}, BTN1, {repeat: true, debounce: 100}); + +setWatch(function() { + drawKey(selected, keys[selected]); + prevSelected = selected; + selected = keys[selected].trbl[2]; + drawKey(selected, keys[selected], true); +}, BTN3, {repeat: true, debounce: 100}); + +Bangle.on('touch', function(direction) { + drawKey(selected, keys[selected]); + prevSelected = selected; + if (direction == 1) { + selected = keys[selected].trbl[3]; + } else if (direction == 2) { + selected = keys[selected].trbl[1]; + } + drawKey(selected, keys[selected], true); +}); + +setWatch(function() { + buttonPress(selected); +}, BTN2, {repeat: true, debounce: 100}); diff --git a/apps/calculator/calculator-icon.js b/apps/calculator/calculator-icon.js new file mode 100644 index 000000000..94158e7d2 --- /dev/null +++ b/apps/calculator/calculator-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwhBC/AC8r6/XlYvr64CEF9UrMIIv/R/7vTMwIAmlUklQGDroAFqwHGBRgJBqwMDq+k5nNABAWDC4QZFERAvGBQOBF5I0FCYNW1mImWs6+sDoQsDAYIJEAAeB2eB1mBA4QvF43P6/GF4mB6+BAQYlEro3BAAI3FDAezBYgvE43O64DBF4hbCAAMrGAIiFBYRUEHogaBxA6CF4vXLwPHF4giEDIIkDDgI2BFoI6FBgYWCF5PPF4rSBKwVWI4bAFFgdcYAykBX5HX53NFwfNfwIkDAQYAGBBAKCIIYABd4y9DAAJ9CAD9dF4gAGCIi8BABLXBBRQLEF4vHRwgvEERQ6DHpgvH66PB65fUBpZfJ4/G6wxBMIaPbL5QvB6/WF6hqNF5KPDF6jkGd6JeBF5AAdF4oAGDBeH1mHAAwIBF8esABQvdWQonDX4YvIYAq/GXobvNF4hfKCwwvF43GF5AXGL44vJLwgvE453DMIYuFR5JiHI4yPHRoaREIwpIFF7TvbR5BJCX5IvMADgvcroABF6vG4wvIX46DKBZYvEFwPHGAgZHERALRF4YuBHYIwEFxxfPF5CDDF6ZfLDAyPFFwovFKRYvV47vDAgIvRR5aOFL4orCFwbvHADYvEAA4YLdRYvQ45eBR5C6UF5vHX4LvJF8PGZYXXGAYvnLYYvfZ4xfXd6AvKGAK/RDAKNTF4wAG44=")) diff --git a/apps/calculator/calculator.info b/apps/calculator/calculator.info new file mode 100644 index 000000000..e8a760024 --- /dev/null +++ b/apps/calculator/calculator.info @@ -0,0 +1 @@ +{"name":"Calculator","src":"calculator.app.js","icon":"calculator.img"} diff --git a/apps/calculator/calculator.png b/apps/calculator/calculator.png new file mode 100644 index 0000000000000000000000000000000000000000..8362c9200facdad594bf07376e12eaf6d0c3550c GIT binary patch literal 10312 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 Date: Mon, 13 Apr 2020 14:26:53 +0200 Subject: [PATCH 037/302] Active Pedometer rename --- apps.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps.json b/apps.json index a0b1a56aa..3dbccb9b1 100644 --- a/apps.json +++ b/apps.json @@ -1110,7 +1110,7 @@ }, { "id": "activepedom", "name": "Active Pedometer", - "shortName":"activepedom", + "shortName":"Active Pedometer", "icon": "app.png", "version":"0.01", "description": "Pedometer that filters out arm movement and displays a step goal progress.", From 5bbb6bfc894c88922719801426c93c6ec0509f2a Mon Sep 17 00:00:00 2001 From: Purple-Tentacle <59914607+Purple-Tentacle@users.noreply.github.com> Date: Mon, 13 Apr 2020 14:57:47 +0200 Subject: [PATCH 038/302] Bugfix --- apps/activepedom/widget.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/activepedom/widget.js b/apps/activepedom/widget.js index ac07856fd..0c8b2438d 100644 --- a/apps/activepedom/widget.js +++ b/apps/activepedom/widget.js @@ -84,7 +84,7 @@ if (steps >= setting('stepThreshold')) { if (active == 0) { - stepsCounted = stepsCounted + (stepThreshold -1) ; //count steps needed to reach active status, last step is counted anyway, so treshold -1 + stepsCounted = stepsCounted + (setting('stepThreshold') -1) ; //count steps needed to reach active status, last step is counted anyway, so treshold -1 stepsOutsideTime = stepsOutsideTime - 10; //substract steps needed to reac active status } active = 1; From be10466ed14082f58923f80dfe1c55f2d6f3d6b2 Mon Sep 17 00:00:00 2001 From: Paul Cockrell Date: Mon, 13 Apr 2020 14:33:22 +0100 Subject: [PATCH 039/302] Don't double-buzz, it throws an error!, Also only call drawImage if we have an image to draw --- apps/marioclock/marioclock-app.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/marioclock/marioclock-app.js b/apps/marioclock/marioclock-app.js index 4acbf384b..7601b89ba 100644 --- a/apps/marioclock/marioclock-app.js +++ b/apps/marioclock/marioclock-app.js @@ -359,7 +359,7 @@ function drawNotice(x, y) { break; } - g.drawImage(img, characterSprite.x, characterSprite.y - 16); + if (img) g.drawImage(img, characterSprite.x, characterSprite.y - 16); } function drawCharacter(date, character) { @@ -671,7 +671,6 @@ function init() { try { NRF.wake(); } catch (e) {} NRF.on('disconnect', () => { - Bangle.buzz(); phoneNewMessage(null, "Phone disconnected"); }); @@ -679,7 +678,6 @@ function init() { setTimeout(() => { phoneOutbound({ t: "status", bat: E.getBattery() }); }, ONE_SECOND * 2); - Bangle.buzz(); phoneNewMessage(null, "Phone connected"); }); From c64d7bf57e04070b691b26a43a237c9e9ca9448a Mon Sep 17 00:00:00 2001 From: msdeibel Date: Tue, 14 Apr 2020 07:48:07 +0200 Subject: [PATCH 040/302] Experimental changes for hrm, gps an compass --- apps/batchart/app.js | 50 +++++++++++++++++----------------- apps/batchart/widget.js | 60 ++++++++++++++++++++++++++++++++--------- 2 files changed, 73 insertions(+), 37 deletions(-) diff --git a/apps/batchart/app.js b/apps/batchart/app.js index 4fb919354..569fecfd1 100644 --- a/apps/batchart/app.js +++ b/apps/batchart/app.js @@ -7,12 +7,12 @@ const MaxValueCount = 144; const GraphXMax = GraphXZero + MaxValueCount; const GraphLcdY = GraphYZero + 10; -// const GraphCompassY = GraphYZero + 16; +const GraphCompassY = GraphYZero + 16; // const GraphBluetoothY = GraphYZero + 22; -// const GraphGpsY = GraphYZero + 28; -// const GraphHrmY = GraphYZero + 34; +const GraphGpsY = GraphYZero + 28; +const GraphHrmY = GraphYZero + 34; -var Storage = require("Storage"); +const Storage = require("Storage"); function renderCoordinateSystem() { g.setFont("6x8", 1); @@ -149,13 +149,13 @@ function renderData(dataArray) { g.drawLine(GraphXZero + i, GraphLcdY, GraphXZero + i, GraphLcdY + 1); } - // // Compass state - // if (switchables & switchableConsumers.lcd == switchableConsumers.lcd) { - // g.setColor(0, 1, 0); - // g.setFontAlign(-1, -1, 0); - // g.drawString("Compass", GraphXMax + GraphMarkerOffset, GraphCompassY - 2, true); - // g.drawLine(GraphXZero + i, GraphCompassY, GraphXZero + i, GraphCompassY + 1); - // } + // Compass state + if (switchables & switchableConsumers.lcd == switchableConsumers.lcd) { + g.setColor(0, 1, 0); + g.setFontAlign(-1, -1, 0); + g.drawString("Compass", GraphXMax + GraphMarkerOffset, GraphCompassY - 2, true); + g.drawLine(GraphXZero + i, GraphCompassY, GraphXZero + i, GraphCompassY + 1); + } // // Bluetooth state // if (switchables & switchableConsumers.lcd == switchableConsumers.lcd) { @@ -165,21 +165,21 @@ function renderData(dataArray) { // g.drawLine(GraphXZero + i, GraphBluetoothY, GraphXZero + i, GraphBluetoothY + 1); // } - // // Gps state - // if (switchables & switchableConsumers.lcd == switchableConsumers.lcd) { - // g.setColor(0.8, 0.5, 0.24); - // g.setFontAlign(-1, -1, 0); - // g.drawString("GPS", GraphXMax + GraphMarkerOffset, GraphGpsY - 2, true); - // g.drawLine(GraphXZero + i, GraphGpsY, GraphXZero + i, GraphGpsY + 1); - // } + // Gps state + if (switchables & switchableConsumers.lcd == switchableConsumers.lcd) { + g.setColor(0.8, 0.5, 0.24); + g.setFontAlign(-1, -1, 0); + g.drawString("GPS", GraphXMax + GraphMarkerOffset, GraphGpsY - 2, true); + g.drawLine(GraphXZero + i, GraphGpsY, GraphXZero + i, GraphGpsY + 1); + } - // // Hrm state - // if (switchables & switchableConsumers.lcd == switchableConsumers.lcd) { - // g.setColor(1, 0, 0); - // g.setFontAlign(1, -1, 0); - // g.drawString("HRM", GraphXZero - GraphMarkerOffset, GraphHrmY - 2, true); - // g.drawLine(GraphXZero + i, GraphHrmY, GraphXZero + i, GraphHrmY + 1); - // } + // Hrm state + if (switchables & switchableConsumers.lcd == switchableConsumers.lcd) { + g.setColor(1, 0, 0); + g.setFontAlign(1, -1, 0); + g.drawString("HRM", GraphXZero - GraphMarkerOffset, GraphHrmY - 2, true); + g.drawLine(GraphXZero + i, GraphHrmY, GraphXZero + i, GraphHrmY + 1); + } } dataArray = null; diff --git a/apps/batchart/widget.js b/apps/batchart/widget.js index de7ce230d..6ec330511 100644 --- a/apps/batchart/widget.js +++ b/apps/batchart/widget.js @@ -15,6 +15,12 @@ const recordingInterval10S = 10*1000; //For testing var recordingInterval = null; + var compassEventReceived = false; + var gpsEventReceived = false; + var hrmEventReceived = false; + + const Storage = require("Storage"); + // draw your widget function draw() { g.reset(); @@ -24,24 +30,54 @@ function getEnabledConsumersValue() { var enabledConsumers = switchableConsumers.none; + Bangle.on('mag', (() => { + console.log("mag received"); + compassEventReceived = true; + Bangle.on("mag", () => {}); + })); + + Bangle.on('GPS', (() => { + console.log("GPS received"); + gpsEventReceived = true; + })); + + Bangle.on('HRM', (() => { + console.log("HRM received"); + hrmEventReceived = true; + })); + + // Wait two seconds, that should be enough for each of the events to get raised once + setTimeout(() => { + Bangle.on('mag', () => {}); + Bangle.on('GPS', () => {}); + Bangle.on('HRM', () => {}); + }, 2000); + if (Bangle.isLCDOn()) enabledConsumers = enabledConsumers | switchableConsumers.lcd; // Already added in the hope they will be available soon to get more details - // if (Bangle.isCompassOn()) - // enabledConsumers = enabledConsumers | switchableConsumers.compass; - // if (Bangle.isBluetoothOn()) + if (compassEventReceived) + enabledConsumers = enabledConsumers | switchableConsumers.compass; + if (gpsEventReceived) + enabledConsumers = enabledConsumers | switchableConsumers.gps; + if (hrmEventReceived) + enabledConsumers = enabledConsumers | switchableConsumers.hrm; + //if (Bangle.isBluetoothOn()) // enabledConsumers = enabledConsumers | switchableConsumers.bluetooth; - // if (Bangle.isGpsOn()) - // enabledConsumers = enabledConsumers | switchableConsumers.gps; - // if (Bangle.isHrmOn()) - // enabledConsumers = enabledConsumers | switchableConsumers.hrm; + + // Reset the event registration vars + compassEventReceived = false; + gpsEventReceived = false; + hrmEventReceived = false; + + console.log("Enabled: " + enabledConsumers); return enabledConsumers; } function logBatteryData() { const previousWriteLogName = "bcprvday"; - const previousWriteDay = require("Storage").read(previousWriteLogName); + const previousWriteDay = Storage.read(previousWriteLogName); const currentWriteDay = new Date().getDay(); const logFileName = "bclog" + currentWriteDay; @@ -49,11 +85,11 @@ // Change log target on day change if (previousWriteDay != currentWriteDay) { //Remove a log file containing data from a week ago - require("Storage").erase(logFileName); - require("Storage").write(previousWriteLogName, currentWriteDay); + Storage.erase(logFileName); + Storage.write(previousWriteLogName, currentWriteDay); } - var bcLogFileA = require("Storage").open(logFileName, "a"); + var bcLogFileA = Storage.open(logFileName, "a"); if (bcLogFileA) { console.log([getTime().toFixed(0), E.getBattery(), E.getTemperature(), getEnabledConsumersValue()].join(",")); bcLogFileA.write([[getTime().toFixed(0), E.getBattery(), E.getTemperature(), getEnabledConsumersValue()].join(",")].join(",")+"\n"); @@ -63,7 +99,7 @@ function reload() { WIDGETS["batchart"].width = 24; - recordingInterval = setInterval(logBatteryData, recordingInterval10Min); + recordingInterval = setInterval(logBatteryData, recordingInterval10S); logBatteryData(); } From 0fa4b44990a9b8f63827b4185f318ced1ee2eb92 Mon Sep 17 00:00:00 2001 From: MaBecker Date: Tue, 14 Apr 2020 08:52:04 +0200 Subject: [PATCH 041/302] add favourite functionality * library select/unselect apps/widgets * as library section * as upload * deny unselect for boot and setting --- CHANGELOG.md | 1 + index.html | 4 +- js/index.js | 117 +++++++++++++++++++++++++++++++++++++++++---------- js/ui.js | 1 + js/utils.js | 13 ++++++ 5 files changed, 113 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1cd3d803..6368c2c46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,3 +7,4 @@ Changed for individual apps are listed in `apps/appname/ChangeLog` * Added optional `README.md` file for apps * Remove 2v04 version warning, add links in About to official/developer versions * Fix issue removing an app that was just installed (Fix #253) +* Add `Favourite` functionality diff --git a/index.html b/index.html index 0d5c17251..f016ffb49 100644 --- a/index.html +++ b/index.html @@ -96,6 +96,7 @@ +
@@ -134,7 +135,8 @@

Utilities

-

+ +

diff --git a/js/index.js b/js/index.js index d2c6d698b..ef9bcb4f1 100644 --- a/js/index.js +++ b/js/index.js @@ -1,6 +1,8 @@ var appJSON = []; // List of apps and info from apps.json var appsInstalled = []; // list of app JSON var files = []; // list of files on Bangle +var favourites = []; // list of user favourite app +const FAVOURITE = "favouriteapps.json"; httpGet("apps.json").then(apps=>{ try { @@ -18,7 +20,7 @@ httpGet("apps.json").then(apps=>{ function showChangeLog(appid) { var app = appNameToApp(appid); function show(contents) { - showPrompt(app.name+" Change Log",contents,{ok:true}).catch(()=>{});; + showPrompt(app.name+" Change Log",contents,{ok:true}).catch(()=>{}); } httpGet(`apps/${appid}/ChangeLog`). then(show).catch(()=>show("No Change Log available")); @@ -142,6 +144,20 @@ function handleAppInterface(app) { }); } +function handleAppFavourite(favourite, app){ + if (favourite) { + favourites = favourites.concat([app.id]); + } else { + if ([ "boot","setting"].includes(app.id)) { + showToast(app.name + ' is required, can\'t remove it' , 'warning'); + }else { + favourites = favourites.filter(e => e != app.id); + } + } + localStorage.setItem("favouriteapps.json", JSON.stringify(favourites)); + refreshLibrary(); +} + // =========================================== Top Navigation function showTab(tabname) { htmlToArray(document.querySelectorAll("#tab-navigate .tab-item")).forEach(tab => { @@ -156,7 +172,7 @@ function showTab(tabname) { // =========================================== Library -var chips = Array.from(document.querySelectorAll('.chip')).map(chip => chip.attributes.filterid.value) +var chips = Array.from(document.querySelectorAll('.chip')).map(chip => chip.attributes.filterid.value); var hash = window.location.hash ? window.location.hash.slice(1) : ''; var activeFilter = !!~chips.indexOf(hash) ? hash : ''; @@ -165,27 +181,34 @@ var currentSearch = ''; function refreshFilter(){ var filtersContainer = document.querySelector("#librarycontainer .filter-nav"); filtersContainer.querySelector('.active').classList.remove('active'); - if(activeFilter) filtersContainer.querySelector('.chip[filterid="'+activeFilter+'"]').classList.add('active') - else filtersContainer.querySelector('.chip[filterid]').classList.add('active') + if(activeFilter) filtersContainer.querySelector('.chip[filterid="'+activeFilter+'"]').classList.add('active'); + else filtersContainer.querySelector('.chip[filterid]').classList.add('active'); } function refreshLibrary() { var panelbody = document.querySelector("#librarycontainer .panel-body"); var visibleApps = appJSON; if (activeFilter) { - visibleApps = visibleApps.filter(app => app.tags && app.tags.split(',').includes(activeFilter)); + if ( activeFilter == "favourites" ) { + visibleApps = visibleApps.filter(app => app.id && (favourites.filter( e => e == app.id).length)); + }else{ + visibleApps = visibleApps.filter(app => app.tags && app.tags.split(',').includes(activeFilter)); + } } if (currentSearch) { visibleApps = visibleApps.filter(app => app.name.toLowerCase().includes(currentSearch) || app.tags.includes(currentSearch)); } + favourites = (localStorage.getItem(FAVOURITE)) === null ? JSON.parse('["boot","launch","setting"]') : JSON.parse(localStorage.getItem("favouriteapps.json")); + panelbody.innerHTML = visibleApps.map((app,idx) => { var appInstalled = appsInstalled.find(a=>a.id==app.id); var version = getVersionInfo(app, appInstalled); var versionInfo = version.text; if (versionInfo) versionInfo = " ("+versionInfo+")"; var readme = `
Read more...`; + var favourite = favourites.find(e => e == app.id); return `
${escapeHtml(app.name)}

@@ -195,7 +218,8 @@ function refreshLibrary() {

${escapeHtml(app.description)}${app.readme?`
${readme}`:""}

See the code on GitHub
-
+
+ @@ -232,7 +256,7 @@ function refreshLibrary() { // upload icon.classList.remove("icon-upload"); icon.classList.add("loading"); - uploadApp(app) + uploadApp(app); } else if (icon.classList.contains("icon-menu")) { // custom HTML update icon.classList.remove("icon-menu"); @@ -250,6 +274,10 @@ function refreshLibrary() { updateApp(app); } else if (icon.classList.contains("icon-download")) { handleAppInterface(app); + } else if ( button.innerText == String.fromCharCode(0x2661)) { + handleAppFavourite(true, app); + } else if ( button.innerText == String.fromCharCode(0x2665) ) { + handleAppFavourite(false, app); } }); }); @@ -262,17 +290,17 @@ refreshLibrary(); function uploadApp(app) { return getInstalledApps().then(()=>{ if (appsInstalled.some(i => i.id === app.id)) { - return updateApp(app) + return updateApp(app); } Comms.uploadApp(app).then((appJSON) => { - Progress.hide({ sticky: true }) + Progress.hide({ sticky: true }); if (appJSON) { - appsInstalled.push(appJSON) + appsInstalled.push(appJSON); } - showToast(app.name + ' Uploaded!', 'success') + showToast(app.name + ' Uploaded!', 'success'); }).catch(err => { - Progress.hide({ sticky: true }) - showToast('Upload failed, ' + err, 'error') + Progress.hide({ sticky: true }); + showToast('Upload failed, ' + err, 'error'); }).finally(()=>{ refreshMyApps(); refreshLibrary(); @@ -286,8 +314,8 @@ function removeApp(app) { return showPrompt("Delete","Really remove '"+app.name+"'?").then(() => { return getInstalledApps().then(()=>{ // a = from appid.info, app = from apps.json - return Comms.removeApp(appsInstalled.find(a => a.id === app.id)) - }) + return Comms.removeApp(appsInstalled.find(a => a.id === app.id)); + }); }).then(()=>{ appsInstalled = appsInstalled.filter(a=>a.id!=app.id); showToast(app.name+" removed successfully","success"); @@ -315,13 +343,13 @@ function updateApp(app) { if (app.custom) return customApp(app); return getInstalledApps().then(() => { // a = from appid.info, app = from apps.json - let remove = appsInstalled.find(a => a.id === app.id) + let remove = appsInstalled.find(a => a.id === app.id); // no need to remove files which will be overwritten anyway remove.files = remove.files.split(',') .filter(f => f !== app.id + '.info') .filter(f => !app.storage.some(s => s.name === f)) - .join(',') - return Comms.removeApp(remove) + .join(','); + return Comms.removeApp(remove); }).then(()=>{ showToast(`Updating ${app.name}...`); appsInstalled = appsInstalled.filter(a=>a.id!=app.id); @@ -397,7 +425,7 @@ return `
// check icon to figure out what we should do if (icon.classList.contains("icon-delete")) removeApp(app); if (icon.classList.contains("icon-refresh")) updateApp(app); - if (icon.classList.contains("icon-download")) handleAppInterface(app) + if (icon.classList.contains("icon-download")) handleAppInterface(app); }); }); } @@ -405,7 +433,7 @@ return `
let haveInstalledApps = false; function getInstalledApps(refresh) { if (haveInstalledApps && !refresh) { - return Promise.resolve(appsInstalled) + return Promise.resolve(appsInstalled); } showLoadingIndicator("myappscontainer"); // Get apps and files @@ -453,7 +481,7 @@ filtersContainer.addEventListener('click', ({ target }) => { activeFilter = target.getAttribute('filterid') || ''; refreshFilter(); refreshLibrary(); - window.location.hash = activeFilter + window.location.hash = activeFilter; }); var librarySearchInput = document.querySelector("#searchform input"); @@ -526,7 +554,7 @@ document.getElementById("installdefault").addEventListener("click",event=>{ upload(); }).catch(function() { Progress.hide({sticky:true}); - reject() + reject(); }); } upload(); @@ -541,3 +569,48 @@ document.getElementById("installdefault").addEventListener("click",event=>{ showToast("App Install failed, "+err,"error"); }); }); + +// Install all favoutrie apps in one go +document.getElementById("installfavourite").addEventListener("click",event=>{ + var defaultApps, appCount; + asyncLocalStorage.getItem(FAVOURITE).then(json=>{ + defaultApps = JSON.parse(json); + defaultApps = defaultApps.map( appid => appJSON.find(app=>app.id==appid) ); + if (defaultApps.some(x=>x===undefined)) + throw "Not all apps found"; + appCount = defaultApps.length; + return showPrompt("Install Defaults","Remove everything and install favourite apps?"); + }).then(() => { + return Comms.removeAllApps(); + }).then(()=>{ + Progress.hide({sticky:true}); + appsInstalled = []; + showToast(`Existing apps removed. Installing ${appCount} apps...`); + return new Promise((resolve,reject) => { + function upload() { + var app = defaultApps.shift(); + if (app===undefined) return resolve(); + Progress.show({title:`${app.name} (${appCount-defaultApps.length}/${appCount})`,sticky:true}); + Comms.uploadApp(app,"skip_reset").then((appJSON) => { + Progress.hide({sticky:true}); + if (appJSON) appsInstalled.push(appJSON); + showToast(`(${appCount-defaultApps.length}/${appCount}) ${app.name} Uploaded`); + upload(); + }).catch(function() { + Progress.hide({sticky:true}); + reject(); + }); + } + upload(); + }); + }).then(()=>{ + return Comms.setTime(); + }).then(()=>{ + showToast("Favourites apps successfully installed!","success"); + return getInstalledApps(true); + }).catch(err=>{ + Progress.hide({sticky:true}); + showToast("App Install failed, "+err,"error"); + }); +}); + diff --git a/js/ui.js b/js/ui.js index 616a92555..ea6885eac 100644 --- a/js/ui.js +++ b/js/ui.js @@ -86,6 +86,7 @@ function showToast(message, type) { var style = "toast-primary"; if (type=="success") style = "toast-success"; else if (type=="error") style = "toast-error"; + else if (type=="warning") style = "toast-warning"; else if (type!==undefined) console.log("showToast: unknown toast "+type); var toastcontainer = document.getElementById("toastcontainer"); var msgDiv = htmlElement(`
`); diff --git a/js/utils.js b/js/utils.js index 85b6eb0a1..4913c7129 100644 --- a/js/utils.js +++ b/js/utils.js @@ -67,3 +67,16 @@ function getVersionInfo(appListing, appInstalled) { canUpdate : canUpdate } } + +const asyncLocalStorage = { + setItem: function (key, value) { + return Promise.resolve().then(function () { + localStorage.setItem(key, value); + }); + }, + getItem: function (key) { + return Promise.resolve().then(function () { + return localStorage.getItem(key); + }); + } +}; From b84d0446a87f86ffc2c1f7eee5a695a3b863f19d Mon Sep 17 00:00:00 2001 From: Fabio Date: Tue, 14 Apr 2020 09:08:26 +0200 Subject: [PATCH 042/302] New game Snake! --- apps.json | 13 ++++ apps/snake/ChangeLog | 1 + apps/snake/README.md | 13 ++++ apps/snake/snake-icon.js | 1 + apps/snake/snake.js | 144 +++++++++++++++++++++++++++++++++++++++ apps/snake/snake.png | Bin 0 -> 2337 bytes 6 files changed, 172 insertions(+) create mode 100644 apps/snake/ChangeLog create mode 100644 apps/snake/README.md create mode 100644 apps/snake/snake-icon.js create mode 100644 apps/snake/snake.js create mode 100644 apps/snake/snake.png diff --git a/apps.json b/apps.json index 56a6b69d3..dc9d47c2c 100644 --- a/apps.json +++ b/apps.json @@ -1171,5 +1171,18 @@ {"name":"bledetect.app.js","url":"bledetect.js"}, {"name":"bledetect.img","url":"bledetect-icon.js","evaluate":true} ] + }, + { "id": "snake", + "name": "Snake", + "shortName":"Snake", + "icon": "snake.png", + "version":"0.01", + "description": "The classic snake game. Eat apples and don't bite your tail:", + "tags": "game,fun", + "readme": "README.md", + "storage": [ + {"name":"snake.app.js","url":"snake.js"}, + {"name":"snake.img","url":"snake-icon.js","evaluate":true} + ] } ] diff --git a/apps/snake/ChangeLog b/apps/snake/ChangeLog new file mode 100644 index 000000000..2286a7f70 --- /dev/null +++ b/apps/snake/ChangeLog @@ -0,0 +1 @@ +0.01: New App! \ No newline at end of file diff --git a/apps/snake/README.md b/apps/snake/README.md new file mode 100644 index 000000000..7860dbd88 --- /dev/null +++ b/apps/snake/README.md @@ -0,0 +1,13 @@ +# Snake + +![Screenshot](https://i.ibb.co/XzWrvPL/screenshot.png) + +The legentary classic game is now available on Bangle.js! +Eat apples and don't bite your tail. + +## Controls + +- UP: BTN1 +- DOWN: BTN3 +- LEFT: BTN4 +- RIGHT: BTN5 diff --git a/apps/snake/snake-icon.js b/apps/snake/snake-icon.js new file mode 100644 index 000000000..305061003 --- /dev/null +++ b/apps/snake/snake-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4A/ADE3m9hsIusrdhGIM3LtU3g0GAgQxlEwIqBmEAgEGF4QwkF4c3F4MxF4dbF8qLDrYHDre74IABF8QwBLoaPDF8wPKF96/jF/4v/F/4vrrc3AIQsnsIAKF94wiFxgv/R//+m4ABrYALBwIpYFwwAQLC4v/F7gXGF91hACovWFqwwUF4VbF7IwUFzSRVF1gwCF9wwZFyoA/AH4A/AH4A/AGg=")) \ No newline at end of file diff --git a/apps/snake/snake.js b/apps/snake/snake.js new file mode 100644 index 000000000..37b461596 --- /dev/null +++ b/apps/snake/snake.js @@ -0,0 +1,144 @@ +const H = g.getWidth(); +const W = g.getHeight(); +let running = true; +let score = 0; +let d; + +// game world +const gridSize = 40; +const tileSize = 6; +let nextX = 0; +let nextY = 0; + +// snake +const defaultTailSize = 3; +let tailSize = defaultTailSize; +const snakeTrail = []; +let snakeX = 10; +let snakeY = 10; + +// apple +let appleX = Math.floor(Math.random() * gridSize); +let appleY = Math.floor(Math.random() * gridSize); + +function gameStart() { + running = true; + score = 0; +} + +function gameStop() { + g.clear(); + g.setColor("#FFFFFF"); + g.setFont("6x8", 2); + g.drawString("GAME OVER!", W / 2, H / 2 - 20); + g.drawString("Tap to Restart", W / 2, H / 2 + 20); + running = false; + tailSize = defaultTailSize; +} + +function draw() { + if (!running) { + return; + } + + // move snake in next pos + snakeX += nextX; + snakeY += nextY; + + // snake over game world? + if (snakeX < 0) { + snakeX = gridSize - 1; + } + + if (snakeX > gridSize - 1) { + snakeX = 0; + } + + if (snakeY < 0) { + snakeY = gridSize - 1; + } + if (snakeY > gridSize - 1) { + snakeY = 0; + } + + //snake bite apple? + if (snakeX === appleX && snakeY === appleY) { + tailSize++; + score++; + + appleX = Math.floor(Math.random() * gridSize); + appleY = Math.floor(Math.random() * gridSize); + } + + //paint background + g.setColor("#000000"); + g.fillRect(0, 0, H, W); + + // paint snake + g.setColor("#008000"); + + for (let i = 0; i < snakeTrail.length; i++) { + g.fillRect(snakeTrail[i].x * tileSize, snakeTrail[i].y * tileSize, snakeTrail[i].x * tileSize + tileSize, snakeTrail[i].y * tileSize + tileSize); + + //snake bites it's tail? + if (snakeTrail[i].x === snakeX && snakeTrail[i].y === snakeY && tailSize > defaultTailSize) { + gameStop(); + } + } + + // paint apple + g.setColor("#FF0000"); + g.fillRect(appleX * tileSize, appleY * tileSize, appleX * tileSize + tileSize, appleY * tileSize + tileSize); + + // paint score + g.setColor("#FFFFFF"); + g.setFont("6x8"); + g.setFontAlign(0, 0); + g.drawString("Score:" + score, W / 2, 10); + + //set snake trail + snakeTrail.push({ x: snakeX, y: snakeY }); + while (snakeTrail.length > tailSize) { + snakeTrail.shift(); + } +} + +// input +setWatch(() => {// Up + if (d !== 'd') { + nextX = 0; + nextY = -1; + d = 'u'; + } +}, BTN1, { repeat: true }); +setWatch(() => {// Down + if (d !== 'u') { + nextX = 0; + nextY = 1; + d = 'd'; + } +}, BTN3, { repeat: true }); +setWatch(() => {// Left + if (d !== 'r') { + nextX = -1; + nextY = 0; + d = 'l'; + } +}, BTN4, { repeat: true }); +setWatch(() => {// Right + if (d !== 'l') { + nextX = 1; + nextY = 0; + d = 'r'; + } +}, BTN5, { repeat: true }); + +Bangle.on('touch', button => { + if (!running) { + gameStart(); + } +}); + +// render X times per second +var x = 5; +setInterval(draw, 1000 / x); \ No newline at end of file diff --git a/apps/snake/snake.png b/apps/snake/snake.png new file mode 100644 index 0000000000000000000000000000000000000000..04564a8f77dad377b2dd2dd0a50561ca582bc6fb GIT binary patch literal 2337 zcmaJ@dpuNWAD`rwUb~B2QaBxEcE)AKV1}6n;ml;b#$_m8DaOpfe3*-wgFzRhTq@Sa zOB-^J-dI`6t?W{5Yg1ApOBX4ds3;*~N2PuLcze%t&Ur4M@Avz={_&)Hd2Cl#(^o^G zQ0h!qh%KKjS3Xr``QJ}rK2<(#MCksAk0=z0;LFx_5a9FdK|d19k|lP5 z!}+f964)o+!gS*q7}hX{*o<-HdGRsYGn(2dO*1~2``k& zh8Ul0$#?c(ID&|&1VU_VEI!s6FOq~2h!hHCMZ?O z`#*)HzHwrhz=oxwXbD$dxKQI2Q(~%%1m++jiLXc$xtc|cVg#Su?C4Tgb_lNTfSaY)A~q$=TV)#@d-q zX4tNBArUuP01J^-F7IzH{fpd{RuG8gmLXWekAZm%iAVr^2${n<(toq4-4hpE|Ir2*+9Jsg;L68LQcN2!HIwE3J;{~ zEj=BInVkszDbI#qUW3_@u6xF}tpN}isj6-ci?>p%-uv#0~Qd2!is z*7siT5k{9y$1>8*DX6C9fx?};K6e(ikYzTKLdfA#R$Wa;(T`7M>f4G!T| zh1{1+b;43Hw_Fe}3YeOoda7!J+qF)m`DVflS-oho^W`wuC<%KZglNE%`hwU*l^=)5b&HH6?|!yCmjl4NoGp3Jrz zK3&=R|L*fBXnLjlBC2Pp4ysT*rLyLVl$N2S zcLqYoz03Q{AKebT@(8@K>v&?t&thaic^6IL(qrTM+1ncI-2@_x?T+j&Z7JP!Bdu$G z++4Hv3FzDvmQW9pG`!vBBCN1J)2AIvr#4r&Wj(@IXy}$`esi~!NoU!u2yX|Sp%0a`%MFS(NLxL2BkB#T__BFwmVICPxsE5 zZ=beodpv-J&SxswVk&lVH*kc=guZjDjZ|swM%CPS ztF#fSh)=E5bKIIa9saDNo*O_;F<3jQqaC31jCyvQt#Gd>vtZFK=sPC{=JoM>Kmlw` zvUtG=P3!X8?|;gH>b*cx8?v8oXT;~1ny0PV$W0x47l!yadQ65@o3;BZ#%S)z=Q=$= zPvoJKqABR4f#%#scQ~RNXL@sMdA5vnIQeXvPS3RSd~3==+9Cn-?e9lMA`ci}YQ&6p zjF%2OtBJ~O63WSr$wLJyTk2R?&pA5Qh^be8HlsH@{nmD9^h8NvVqj2Tss3Qg_w^gr zv1^pKh&?AM0LC3T|I78u*_*Cu|MO7wgS5+q9IU6*^Ok}UtLBEQad+eGAbe6^qo$o8 z)Lkz<4LkUii62JPZ#>wt{&``gs@Sk1_kBQLu9nBWxYQO2N24mWSLfX|o%%ak4R;n& zvM@iobzgMaegE9?y~?|O^BS?5@a~9@;^-%$TZnz7Y!Yb&TvS8FRdLpEsvaP*JALrMJqvYUZGj;WIy+%C0f9NL#dDXo@ z%*STaf8tK_FlOCXw4%b^#SBzj&Vxd#XgdczPTuVfCEtKZEvn0v2ADRqM{!a}`^ Date: Tue, 14 Apr 2020 10:47:39 +0100 Subject: [PATCH 043/302] remove un-needed file --- apps/calculator/calculator.info | 1 - 1 file changed, 1 deletion(-) delete mode 100644 apps/calculator/calculator.info diff --git a/apps/calculator/calculator.info b/apps/calculator/calculator.info deleted file mode 100644 index e8a760024..000000000 --- a/apps/calculator/calculator.info +++ /dev/null @@ -1 +0,0 @@ -{"name":"Calculator","src":"calculator.app.js","icon":"calculator.img"} From bf1eed04ae4d423fef022189a93ac763b2fec0ca Mon Sep 17 00:00:00 2001 From: msdeibel Date: Tue, 14 Apr 2020 13:55:36 +0200 Subject: [PATCH 044/302] Remove unused settings variable --- apps/batchart/widget.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/batchart/widget.js b/apps/batchart/widget.js index de7ce230d..19cbd054c 100644 --- a/apps/batchart/widget.js +++ b/apps/batchart/widget.js @@ -1,5 +1,7 @@ (() => { - var switchableConsumers = { + const Storage = require("Storage"); + + const switchableConsumers = { none: 0, lcd: 1, compass: 2, @@ -8,7 +10,6 @@ hrm: 16 }; - var settings = {}; var batChartFile; // file for battery percentage recording const recordingInterval10Min = 60 * 10 * 1000; const recordingInterval1Min = 60*1000; //For testing @@ -49,11 +50,11 @@ // Change log target on day change if (previousWriteDay != currentWriteDay) { //Remove a log file containing data from a week ago - require("Storage").erase(logFileName); - require("Storage").write(previousWriteLogName, currentWriteDay); + Storage.open(logFileName, "r")­.erase(); + Storage.write(previousWriteLogName, currentWriteDay); } - var bcLogFileA = require("Storage").open(logFileName, "a"); + var bcLogFileA = Storage.open(logFileName, "a"); if (bcLogFileA) { console.log([getTime().toFixed(0), E.getBattery(), E.getTemperature(), getEnabledConsumersValue()].join(",")); bcLogFileA.write([[getTime().toFixed(0), E.getBattery(), E.getTemperature(), getEnabledConsumersValue()].join(",")].join(",")+"\n"); From 4644fe4baf2005dacf792bfa9a56780f3748209d Mon Sep 17 00:00:00 2001 From: msdeibel Date: Tue, 14 Apr 2020 14:02:03 +0200 Subject: [PATCH 045/302] Fixes removal of listeners --- apps/batchart/widget.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/apps/batchart/widget.js b/apps/batchart/widget.js index 1fdbfe1c9..c45515fe5 100644 --- a/apps/batchart/widget.js +++ b/apps/batchart/widget.js @@ -20,8 +20,6 @@ var gpsEventReceived = false; var hrmEventReceived = false; - const Storage = require("Storage"); - // draw your widget function draw() { g.reset(); @@ -34,7 +32,6 @@ Bangle.on('mag', (() => { console.log("mag received"); compassEventReceived = true; - Bangle.on("mag", () => {}); })); Bangle.on('GPS', (() => { @@ -49,9 +46,7 @@ // Wait two seconds, that should be enough for each of the events to get raised once setTimeout(() => { - Bangle.on('mag', () => {}); - Bangle.on('GPS', () => {}); - Bangle.on('HRM', () => {}); + Bangle.removeAllListeners; }, 2000); if (Bangle.isLCDOn()) From 22a005203febe6c7d9bff8a10f06627360474598 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Tue, 14 Apr 2020 15:56:12 +0100 Subject: [PATCH 046/302] Use BTN2 for settings menu like other clocks --- apps.json | 6 +++--- apps/numerals/ChangeLog | 1 + apps/numerals/numerals.app.js | 14 +++++++------- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/apps.json b/apps.json index 47618e1a8..e278df965 100644 --- a/apps.json +++ b/apps.json @@ -1191,7 +1191,7 @@ "name": "Numerals Clock", "shortName": "Numerals Clock", "icon": "numerals.png", - "version":"0.01", + "version":"0.02", "description": "A simple big numerals clock", "tags": "numerals,clock", "type":"clock", @@ -1209,7 +1209,7 @@ "version":"0.02", "description": "Detect BLE devices and show some informations.", "tags": "app,bluetooth,tool", - "readme": "README.md", + "readme": "README.md", "storage": [ {"name":"bledetect.app.js","url":"bledetect.js"}, {"name":"bledetect.img","url":"bledetect-icon.js","evaluate":true} @@ -1222,7 +1222,7 @@ "version":"0.01", "description": "The classic snake game. Eat apples and don't bite your tail:", "tags": "game,fun", - "readme": "README.md", + "readme": "README.md", "storage": [ {"name":"snake.app.js","url":"snake.js"}, {"name":"snake.img","url":"snake-icon.js","evaluate":true} diff --git a/apps/numerals/ChangeLog b/apps/numerals/ChangeLog index 5560f00bc..a8396e26b 100644 --- a/apps/numerals/ChangeLog +++ b/apps/numerals/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Use BTN2 for settings menu like other clocks diff --git a/apps/numerals/numerals.app.js b/apps/numerals/numerals.app.js index 648a1005a..fbfe5b9ed 100644 --- a/apps/numerals/numerals.app.js +++ b/apps/numerals/numerals.app.js @@ -11,7 +11,7 @@ var numerals = { 1:[[59,1,82,1,90,9,90,82,82,90,73,90,65,82,65,27,59,27,51,19,51,9,59,1]], 2:[[9,1,82,1,90,9,90,47,82,55,21,55,21,64,82,64,90,72,90,82,82,90,9,90,1,82,1,43,9,35,70,35,70,25,9,25,1,17,1,9,9,1]], 3:[[9,1,82,1,90,9,90,82,82,90,9,90,1,82,1,74,9,66,70,66,70,57,9,57,1,49,1,41,9,33,70,33,70,25,9,25,1,17,1,9,9,1]], - 4:[[9,1,14,1,22,9,22,34,69,34,69,9,77,1,82,1,90,9,90,82,82,90,78,90,70,82,70,55,9,55,1,47,1,9,9,1]], + 4:[[9,1,14,1,22,9,22,34,69,34,69,9,77,1,82,1,90,9,90,82,82,90,78,90,70,82,70,55,9,55,1,47,1,9,9,1]], 5:[[9,1,82,1,90,9,90,17,82,25,21,25,21,35,82,35,90,43,90,82,82,90,9,90,1,82,1,72,9,64,71,64,71,55,9,55,1,47,1,9,9,1]], 6:[[9,1,82,1,90,9,90,14,82,22,22,22,22,36,82,36,90,44,90,82,82,90,9,90,1,82,1,9,9,1],[22,55,69,55,69,69,22,69,22,55]], 7:[[9,1,82,1,90,9,90,15,15,90,9,90,1,82,1,76,54,23,9,23,1,15,1,9,9,1]], @@ -47,7 +47,7 @@ if (!settings) { function drawNum(num,col,x,y,func){ g.setColor(col); let tx = x*100+35; - let ty = y*100+35; + let ty = y*100+35; for (let i=0;i0) g.setColor((func==fill)?"#000000":col); func(translate(tx, ty,numerals[num][i])); @@ -57,7 +57,7 @@ function drawNum(num,col,x,y,func){ function draw(drawMode){ let d = new Date(); let h1 = Math.floor(d.getHours()/10); - let h2 = d.getHours()%10; + let h2 = d.getHours()%10; let m1 = Math.floor(d.getMinutes()/10); let m2 = d.getMinutes()%10; g.clearRect(0,24,240,240); @@ -70,9 +70,9 @@ function draw(drawMode){ Bangle.setLCDMode(); clearWatch(); -setWatch(Bangle.showLauncher, BTN1, {repeat:false,edge:"falling"}); +setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"}); -g.clear(); +g.clear(); clearInterval(); if (settings.color>0) _rCol=settings.color-1; interval=setInterval(draw, REFRESH_RATE, settings.drawMode); @@ -80,7 +80,7 @@ draw(settings.drawMode); Bangle.on('lcdPower', function(on) { if (on) { - if (settings.color==0) _rCol = Math.floor(Math.random()*_hCol.length); + if (settings.color==0) _rCol = Math.floor(Math.random()*_hCol.length); draw(settings.drawMode); interval=setInterval(draw, REFRESH_RATE, settings.drawMode); }else @@ -90,4 +90,4 @@ Bangle.on('lcdPower', function(on) { }); Bangle.loadWidgets(); -Bangle.drawWidgets(); \ No newline at end of file +Bangle.drawWidgets(); From d54c0c7dea9dcf2d944f1ecd10a64fa7d50984f9 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Tue, 14 Apr 2020 16:11:29 +0100 Subject: [PATCH 047/302] 0.14: Reduce memory usage when running app settings page --- apps.json | 2 +- apps/setting/ChangeLog | 3 ++- apps/setting/settings.js | 6 ++---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/apps.json b/apps.json index e278df965..21fdf09b2 100644 --- a/apps.json +++ b/apps.json @@ -119,7 +119,7 @@ { "id": "setting", "name": "Settings", "icon": "settings.png", - "version":"0.13", + "version":"0.14", "description": "A menu for setting up Bangle.js", "tags": "tool,system", "storage": [ diff --git a/apps/setting/ChangeLog b/apps/setting/ChangeLog index 5c5a26c61..6c4c19230 100644 --- a/apps/setting/ChangeLog +++ b/apps/setting/ChangeLog @@ -14,4 +14,5 @@ Move LCD Timeout to wakeup menu 0.13: Fix memory leak for App settings Make capitalization more consistent - Move LCD Brightness menu into more general LCD menu \ No newline at end of file + Move LCD Brightness menu into more general LCD menu +0.14: Reduce memory usage when running app settings page diff --git a/apps/setting/settings.js b/apps/setting/settings.js index 71a6a181e..d0d6578dc 100644 --- a/apps/setting/settings.js +++ b/apps/setting/settings.js @@ -325,8 +325,6 @@ function showClockMenu() { return E.showMenu(clockMenu); } - - function showSetTimeMenu() { d = new Date(); const timemenu = { @@ -419,8 +417,8 @@ function showAppSettingsMenu() { '< Back': ()=>showMainMenu(), } const apps = storage.list(/\.info$/) - .map(app => storage.readJSON(app, 1)) - .filter(app => app && app.settings) + .map(app => {var a=storage.readJSON(app, 1);return (a&&a.settings)?a:undefined}) + .filter(app => app) // filter out any undefined apps .sort((a, b) => a.sortorder - b.sortorder) if (apps.length === 0) { appmenu['No app has settings'] = () => { }; From 1d371db91622e6a29ab3d8e8b7c5b8f48bb3088e Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Tue, 14 Apr 2020 16:11:43 +0100 Subject: [PATCH 048/302] Don't send double char code 16 when uploading --- js/comms.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/comms.js b/js/comms.js index 12989e089..604ef19ed 100644 --- a/js/comms.js +++ b/js/comms.js @@ -40,7 +40,7 @@ uploadApp : (app,skipReset) => { // expects an apps.json structure (i.e. with `s currentBytes += f.content.length; // Chould check CRC here if needed instead of returning 'OK'... // E.CRC32(require("Storage").read(${JSON.stringify(app.name)})) - Puck.write(`\x10${f.cmd};Bluetooth.println("OK")\n`,(result) => { + Puck.write(`${f.cmd};Bluetooth.println("OK")\n`,(result) => { if (!result || result.trim()!="OK") { Progress.hide({sticky:true}); return reject("Unexpected response "+(result||"")); From 9f8747a797a07cc9002af8289d376e29a9250240 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Tue, 14 Apr 2020 16:15:43 +0100 Subject: [PATCH 049/302] Version number now clickable even when you're at the latest version (fix #291) --- CHANGELOG.md | 5 +++-- js/utils.js | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6368c2c46..9480f2ace 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,5 +6,6 @@ Changed for individual apps are listed in `apps/appname/ChangeLog` * `Remove All Apps` now doesn't perform a reset before erase - fixes inability to update firmware if settings are wrong * Added optional `README.md` file for apps * Remove 2v04 version warning, add links in About to official/developer versions -* Fix issue removing an app that was just installed (Fix #253) -* Add `Favourite` functionality +* Fix issue removing an app that was just installed (fix #253) +* Add `Favourite` functionality +* Version number now clickable even when you're at the latest version (fix #291) diff --git a/js/utils.js b/js/utils.js index 4913c7129..d8c1b8063 100644 --- a/js/utils.js +++ b/js/utils.js @@ -56,7 +56,7 @@ function getVersionInfo(appListing, appInstalled) { if (appListing.version) versionText = clicky("v"+appListing.version); } else { - versionText = (appInstalled.version ? ("v"+appInstalled.version) : "Unknown version"); + versionText = (appInstalled.version ? (clicky("v"+appInstalled.version)) : "Unknown version"); if (appListing.version != appInstalled.version) { if (appListing.version) versionText += ", latest "+clicky("v"+appListing.version); canUpdate = true; From f88f8756949b2b2742f2930dd6085989a8aeb394 Mon Sep 17 00:00:00 2001 From: Markus Date: Tue, 14 Apr 2020 17:29:24 +0200 Subject: [PATCH 050/302] Fixes eventhandlers for batchart widget --- apps/batchart/widget.js | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/apps/batchart/widget.js b/apps/batchart/widget.js index c45515fe5..9b5a53c4e 100644 --- a/apps/batchart/widget.js +++ b/apps/batchart/widget.js @@ -26,23 +26,28 @@ g.drawString("BC", this.x, this.y); } + function onMag(){ + compassEventReceived = true; + // Stop handling events when no longer necessarry + Bangle.removeListener("mag", onMag); + } + + function onGps() { + gpsEventReceived = true; + Bangle.removeListener("GPS", onGps); + } + + function onHrm() { + gpsEventReceived = true; + Bangle.removeListener("HRM", onHrm); + } + function getEnabledConsumersValue() { var enabledConsumers = switchableConsumers.none; - Bangle.on('mag', (() => { - console.log("mag received"); - compassEventReceived = true; - })); - - Bangle.on('GPS', (() => { - console.log("GPS received"); - gpsEventReceived = true; - })); - - Bangle.on('HRM', (() => { - console.log("HRM received"); - hrmEventReceived = true; - })); + Bangle.on('mag', onMag); + Bangle.on('GPS', onGps); + Bangle.on('HRM', onHrm); // Wait two seconds, that should be enough for each of the events to get raised once setTimeout(() => { @@ -66,8 +71,6 @@ gpsEventReceived = false; hrmEventReceived = false; - console.log("Enabled: " + enabledConsumers); - return enabledConsumers; } From 9daf178fc1c5e95a99a3ae9c871e9848a54419c0 Mon Sep 17 00:00:00 2001 From: Markus Date: Tue, 14 Apr 2020 17:30:10 +0200 Subject: [PATCH 051/302] Fixes consumer indicators and adds home button --- apps/batchart/app.js | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/apps/batchart/app.js b/apps/batchart/app.js index 569fecfd1..b81c38790 100644 --- a/apps/batchart/app.js +++ b/apps/batchart/app.js @@ -142,7 +142,7 @@ function renderData(dataArray) { g.setPixel(GraphXZero + i, GraphYZero - scaledTemp); // LCD state - if (parseInt(dataInfo[switchabelsIndex]) & switchableConsumers.lcd == switchableConsumers.lcd) { + if (parseInt(dataInfo[switchabelsIndex]) & switchableConsumers.lcd) { g.setColor(1, 1, 1); g.setFontAlign(1, -1, 0); g.drawString("LCD", GraphXZero - GraphMarkerOffset, GraphLcdY - 2, true); @@ -150,7 +150,7 @@ function renderData(dataArray) { } // Compass state - if (switchables & switchableConsumers.lcd == switchableConsumers.lcd) { + if (parseInt(dataInfo[switchabelsIndex]) & switchableConsumers.compass) { g.setColor(0, 1, 0); g.setFontAlign(-1, -1, 0); g.drawString("Compass", GraphXMax + GraphMarkerOffset, GraphCompassY - 2, true); @@ -166,7 +166,7 @@ function renderData(dataArray) { // } // Gps state - if (switchables & switchableConsumers.lcd == switchableConsumers.lcd) { + if (parseInt(dataInfo[switchabelsIndex]) & switchableConsumers.gps) { g.setColor(0.8, 0.5, 0.24); g.setFontAlign(-1, -1, 0); g.drawString("GPS", GraphXMax + GraphMarkerOffset, GraphGpsY - 2, true); @@ -174,7 +174,7 @@ function renderData(dataArray) { } // Hrm state - if (switchables & switchableConsumers.lcd == switchableConsumers.lcd) { + if (parseInt(dataInfo[switchabelsIndex]) & switchableConsumers.hrm) { g.setColor(1, 0, 0); g.setFontAlign(1, -1, 0); g.drawString("HRM", GraphXZero - GraphMarkerOffset, GraphHrmY - 2, true); @@ -185,6 +185,16 @@ function renderData(dataArray) { dataArray = null; } +function renderHomeIcon() { + //Home for Btn2 + g.setColor(1, 1, 1); + g.drawLine(220, 118, 227, 110); + g.drawLine(227, 110, 234, 118); + + g.drawPoly([222,117,222,125,232,125,232,117], false); + g.drawRect(226,120,229,125); +} + function renderBatteryChart() { renderCoordinateSystem(); let data = loadData(); @@ -192,21 +202,30 @@ function renderBatteryChart() { data = null; } +// Show launcher when middle button pressed +function switchOffApp(){ + Bangle.showLauncher(); +} + // special function to handle display switch on Bangle.on('lcdPower', (on) => { if (on) { // call your app function here // If you clear the screen, do Bangle.drawWidgets(); - g.clear() + g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); renderBatteryChart(); } }); +setWatch(switchOffApp, BTN2, {edge:"rising", debounce:50, repeat:true}); + g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); // call your app function here +renderHomeIcon(); + renderBatteryChart(); From 63dbed0e3c11e9a52106bfe4ce675720f1a9feb0 Mon Sep 17 00:00:00 2001 From: Markus Date: Tue, 14 Apr 2020 17:31:37 +0200 Subject: [PATCH 052/302] Updates BatChart version --- apps.json | 2 +- apps/batchart/ChangeLog | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps.json b/apps.json index 47618e1a8..22a6f5500 100644 --- a/apps.json +++ b/apps.json @@ -1164,7 +1164,7 @@ "name": "Battery Chart", "shortName":"Battery Chart", "icon": "app.png", - "version":"0.05", + "version":"0.06", "description": "A widget and an app for recording and visualizing battery percentage over time.", "tags": "app,widget,battery,time,record,chart,tool", "storage": [ diff --git a/apps/batchart/ChangeLog b/apps/batchart/ChangeLog index f57805b6a..7a0ce429b 100644 --- a/apps/batchart/ChangeLog +++ b/apps/batchart/ChangeLog @@ -2,4 +2,5 @@ 0.02: Widget stores data to file (1 dataset/10min) 0.03: Rotate log files once a week. 0.04: chart in the app is now active. -0.05: Display temperature and LCD state in chart \ No newline at end of file +0.05: Display temperature and LCD state in chart +0.06: Fixes widget events and charting of component states \ No newline at end of file From 24cfc840e0f2d3d4fbed3d3b27b3eddda0eb2015 Mon Sep 17 00:00:00 2001 From: Fabio Date: Tue, 14 Apr 2020 17:43:12 +0200 Subject: [PATCH 053/302] Improvements of snake game --- apps.json | 2 +- apps/snake/ChangeLog | 3 +- apps/snake/README.md | 3 +- apps/snake/snake.js | 129 +++++++++++++++++++++++-------------------- 4 files changed, 75 insertions(+), 62 deletions(-) diff --git a/apps.json b/apps.json index 47618e1a8..1f168e771 100644 --- a/apps.json +++ b/apps.json @@ -1219,7 +1219,7 @@ "name": "Snake", "shortName":"Snake", "icon": "snake.png", - "version":"0.01", + "version":"0.02", "description": "The classic snake game. Eat apples and don't bite your tail:", "tags": "game,fun", "readme": "README.md", diff --git a/apps/snake/ChangeLog b/apps/snake/ChangeLog index 2286a7f70..428718739 100644 --- a/apps/snake/ChangeLog +++ b/apps/snake/ChangeLog @@ -1 +1,2 @@ -0.01: New App! \ No newline at end of file +0.01: New App! +0.02: Performance and graphic improvements, game pause, beep and buzz \ No newline at end of file diff --git a/apps/snake/README.md b/apps/snake/README.md index 7860dbd88..483eae7a9 100644 --- a/apps/snake/README.md +++ b/apps/snake/README.md @@ -1,6 +1,6 @@ # Snake -![Screenshot](https://i.ibb.co/XzWrvPL/screenshot.png) +![Screenshot](https://i.imgur.com/bXQjxhB.png) The legentary classic game is now available on Bangle.js! Eat apples and don't bite your tail. @@ -11,3 +11,4 @@ Eat apples and don't bite your tail. - DOWN: BTN3 - LEFT: BTN4 - RIGHT: BTN5 +- PAUSE: BTN2 diff --git a/apps/snake/snake.js b/apps/snake/snake.js index 37b461596..4532e0113 100644 --- a/apps/snake/snake.js +++ b/apps/snake/snake.js @@ -1,37 +1,61 @@ +Bangle.setLCDMode("120x120"); + const H = g.getWidth(); const W = g.getHeight(); let running = true; let score = 0; let d; - -// game world -const gridSize = 40; +const gridSize = 20; const tileSize = 6; let nextX = 0; let nextY = 0; - -// snake const defaultTailSize = 3; let tailSize = defaultTailSize; const snakeTrail = []; -let snakeX = 10; -let snakeY = 10; +const snake = { x: 10, y: 10 }; +const apple = { x: Math.floor(Math.random() * gridSize), y: Math.floor(Math.random() * gridSize) }; -// apple -let appleX = Math.floor(Math.random() * gridSize); -let appleY = Math.floor(Math.random() * gridSize); +function drawBackground(){ + g.setColor("#000000"); + g.fillRect(0, 0, H, W); +} + +function drawApple(){ + g.setColor("#FF0000"); + g.fillCircle((apple.x * tileSize) + tileSize/2, (apple.y * tileSize) + tileSize/2, tileSize/2); +} + +function drawSnake(){ + g.setColor("#008000"); + for (let i = 0; i < snakeTrail.length; i++) { + g.fillRect(snakeTrail[i].x * tileSize, snakeTrail[i].y * tileSize, snakeTrail[i].x * tileSize + tileSize, snakeTrail[i].y * tileSize + tileSize); + + //snake bites it's tail + if (snakeTrail[i].x === snake.x && snakeTrail[i].y === snake.y && tailSize > defaultTailSize) { + Bangle.buzz(1000); + gameOver(); + } + } +} + +function drawScore(){ + g.setColor("#FFFFFF"); + g.setFont("6x8"); + g.setFontAlign(0, 0); + g.drawString("Score:" + score, W / 2, 10); +} function gameStart() { running = true; score = 0; } -function gameStop() { +function gameOver() { g.clear(); g.setColor("#FFFFFF"); - g.setFont("6x8", 2); - g.drawString("GAME OVER!", W / 2, H / 2 - 20); - g.drawString("Tap to Restart", W / 2, H / 2 + 20); + g.setFont("6x8"); + g.drawString("GAME OVER!", W / 2, H / 2 - 10); + g.drawString("Tap to Restart", W / 2, H / 2 + 10); running = false; tailSize = defaultTailSize; } @@ -41,66 +65,50 @@ function draw() { return; } + g.clear(); + // move snake in next pos - snakeX += nextX; - snakeY += nextY; + snake.x += nextX; + snake.y += nextY; - // snake over game world? - if (snakeX < 0) { - snakeX = gridSize - 1; + // snake over game world + if (snake.x < 0) { + snake.x = gridSize - 1; + } + if (snake.x > gridSize - 1) { + snake.x = 0; } - if (snakeX > gridSize - 1) { - snakeX = 0; + if (snake.y < 0) { + snake.y = gridSize - 1; + } + if (snake.y > gridSize - 1) { + snake.y = 0; } - if (snakeY < 0) { - snakeY = gridSize - 1; - } - if (snakeY > gridSize - 1) { - snakeY = 0; - } - - //snake bite apple? - if (snakeX === appleX && snakeY === appleY) { + //snake bite apple + if (snake.x === apple.x && snake.y === apple.y) { + Bangle.beep(20); tailSize++; score++; - appleX = Math.floor(Math.random() * gridSize); - appleY = Math.floor(Math.random() * gridSize); + apple.x = Math.floor(Math.random() * gridSize); + apple.y = Math.floor(Math.random() * gridSize); + drawApple(); } - //paint background - g.setColor("#000000"); - g.fillRect(0, 0, H, W); - - // paint snake - g.setColor("#008000"); - - for (let i = 0; i < snakeTrail.length; i++) { - g.fillRect(snakeTrail[i].x * tileSize, snakeTrail[i].y * tileSize, snakeTrail[i].x * tileSize + tileSize, snakeTrail[i].y * tileSize + tileSize); - - //snake bites it's tail? - if (snakeTrail[i].x === snakeX && snakeTrail[i].y === snakeY && tailSize > defaultTailSize) { - gameStop(); - } - } - - // paint apple - g.setColor("#FF0000"); - g.fillRect(appleX * tileSize, appleY * tileSize, appleX * tileSize + tileSize, appleY * tileSize + tileSize); - - // paint score - g.setColor("#FFFFFF"); - g.setFont("6x8"); - g.setFontAlign(0, 0); - g.drawString("Score:" + score, W / 2, 10); + drawBackground(); + drawApple(); + drawSnake(); + drawScore(); //set snake trail - snakeTrail.push({ x: snakeX, y: snakeY }); + snakeTrail.push({ x: snake.x, y: snake.y }); while (snakeTrail.length > tailSize) { snakeTrail.shift(); } + + g.flip(); } // input @@ -132,6 +140,9 @@ setWatch(() => {// Right d = 'r'; } }, BTN5, { repeat: true }); +setWatch(() => {// Pause + running = !running; +}, BTN2, { repeat: true }); Bangle.on('touch', button => { if (!running) { @@ -140,5 +151,5 @@ Bangle.on('touch', button => { }); // render X times per second -var x = 5; +const x = 5; setInterval(draw, 1000 / x); \ No newline at end of file From e1342857284da2d6cc33b94c0fb8b22c4d9a9d9d Mon Sep 17 00:00:00 2001 From: Markus Date: Tue, 14 Apr 2020 17:46:59 +0200 Subject: [PATCH 054/302] Fixes . error --- apps/batchart/widget.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/batchart/widget.js b/apps/batchart/widget.js index 9b5a53c4e..610da4017 100644 --- a/apps/batchart/widget.js +++ b/apps/batchart/widget.js @@ -84,7 +84,7 @@ // Change log target on day change if (previousWriteDay != currentWriteDay) { //Remove a log file containing data from a week ago - Storage.open(logFileName, "r")­.erase(); + Storage.open(logFileName, "r").erase(); Storage.write(previousWriteLogName, currentWriteDay); } From 758b2e447e7e47b39bbf914a0f609d7adc6e0952 Mon Sep 17 00:00:00 2001 From: Markus Date: Tue, 14 Apr 2020 18:04:02 +0200 Subject: [PATCH 055/302] Fixes event registration --- apps/batchart/widget.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/batchart/widget.js b/apps/batchart/widget.js index 610da4017..7c4762649 100644 --- a/apps/batchart/widget.js +++ b/apps/batchart/widget.js @@ -38,7 +38,7 @@ } function onHrm() { - gpsEventReceived = true; + hrmEventReceived = true; Bangle.removeListener("HRM", onHrm); } From 3cc63e2a3e385cc32b4c12f6f114ef4bcb7f7a88 Mon Sep 17 00:00:00 2001 From: Markus Date: Tue, 14 Apr 2020 18:33:56 +0200 Subject: [PATCH 056/302] Fix data recording file retrieval --- apps/batchart/widget.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/batchart/widget.js b/apps/batchart/widget.js index 7c4762649..0565f4160 100644 --- a/apps/batchart/widget.js +++ b/apps/batchart/widget.js @@ -1,3 +1,5 @@ +WIDGETS = {}; + (() => { const Storage = require("Storage"); @@ -76,7 +78,7 @@ function logBatteryData() { const previousWriteLogName = "bcprvday"; - const previousWriteDay = Storage.read(previousWriteLogName); + const previousWriteDay = parseInt(Storage.open(previousWriteLogName, "r").readLine()); const currentWriteDay = new Date().getDay(); const logFileName = "bclog" + currentWriteDay; @@ -85,7 +87,7 @@ if (previousWriteDay != currentWriteDay) { //Remove a log file containing data from a week ago Storage.open(logFileName, "r").erase(); - Storage.write(previousWriteLogName, currentWriteDay); + Storage.open(previousWriteLogName, "w").write(parseInt(currentWriteDay)); } var bcLogFileA = Storage.open(logFileName, "a"); From f43b1e6ee394f5f49a21ae7261928d96c69ed8be Mon Sep 17 00:00:00 2001 From: Markus Date: Tue, 14 Apr 2020 21:02:39 +0200 Subject: [PATCH 057/302] PrevDay lgging fixed --- apps/batchart/widget.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/batchart/widget.js b/apps/batchart/widget.js index 0565f4160..2ef54e997 100644 --- a/apps/batchart/widget.js +++ b/apps/batchart/widget.js @@ -14,8 +14,8 @@ WIDGETS = {}; var batChartFile; // file for battery percentage recording const recordingInterval10Min = 60 * 10 * 1000; - const recordingInterval1Min = 60*1000; //For testing - const recordingInterval10S = 10*1000; //For testing + const recordingInterval1Min = 60*1000; //For testing + const recordingInterval10S = 10*1000; //For testing var recordingInterval = null; var compassEventReceived = false; @@ -67,7 +67,7 @@ WIDGETS = {}; enabledConsumers = enabledConsumers | switchableConsumers.hrm; //if (Bangle.isBluetoothOn()) // enabledConsumers = enabledConsumers | switchableConsumers.bluetooth; - + // Reset the event registration vars compassEventReceived = false; gpsEventReceived = false; @@ -84,7 +84,8 @@ WIDGETS = {}; const logFileName = "bclog" + currentWriteDay; // Change log target on day change - if (previousWriteDay != currentWriteDay) { + if (previousWriteDay != NaN + && previousWriteDay != currentWriteDay) { //Remove a log file containing data from a week ago Storage.open(logFileName, "r").erase(); Storage.open(previousWriteLogName, "w").write(parseInt(currentWriteDay)); @@ -110,6 +111,7 @@ WIDGETS = {}; reload(); Bangle.drawWidgets(); // relayout all widgets }}; + // load settings, set correct widget width reload(); })() \ No newline at end of file From 2299e283169f1668ebbaa179a721f194443a6310 Mon Sep 17 00:00:00 2001 From: Markus Date: Tue, 14 Apr 2020 21:11:52 +0200 Subject: [PATCH 058/302] Fix undefined topUpLines --- apps/batchart/app.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/batchart/app.js b/apps/batchart/app.js index b81c38790..deb406d8b 100644 --- a/apps/batchart/app.js +++ b/apps/batchart/app.js @@ -71,8 +71,11 @@ function loadData() { let topUpLogFileName = "bclog" + previousDay; let remainingLines = MaxValueCount - dataLines.length; let topUpLines = loadLinesFromFile(remainingLines, topUpLogFileName); - dataLines = topUpLines.concat(dataLines); - + + if(topUpLines) { + dataLines = topUpLines.concat(dataLines); + } + previousDay = decrementDay(previousDay); } From 935241d7c5aa5529d1211ba1cca2a3c5a480c199 Mon Sep 17 00:00:00 2001 From: Markus Date: Tue, 14 Apr 2020 21:20:09 +0200 Subject: [PATCH 059/302] Logstring changed --- apps/batchart/widget.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/batchart/widget.js b/apps/batchart/widget.js index 2ef54e997..892c163fc 100644 --- a/apps/batchart/widget.js +++ b/apps/batchart/widget.js @@ -93,8 +93,10 @@ WIDGETS = {}; var bcLogFileA = Storage.open(logFileName, "a"); if (bcLogFileA) { - console.log([getTime().toFixed(0), E.getBattery(), E.getTemperature(), getEnabledConsumersValue()].join(",")); - bcLogFileA.write([[getTime().toFixed(0), E.getBattery(), E.getTemperature(), getEnabledConsumersValue()].join(",")].join(",")+"\n"); + let logString = [getTime().toFixed(0), E.getBattery(), E.getTemperature(), getEnabledConsumersValue()].join(","); + + console.log(logString); + bcLogFileA.write(logString + "\n"); } } From a798bb9dc664153243948f56ab9820ba8f007040 Mon Sep 17 00:00:00 2001 From: Markus Date: Tue, 14 Apr 2020 22:15:15 +0200 Subject: [PATCH 060/302] Improve logging and charting of component states and add widget icon --- apps.json | 2 +- apps/batchart/ChangeLog | 3 ++- apps/batchart/widget.js | 28 +++++++++++++++++++--------- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/apps.json b/apps.json index 22a6f5500..a191375cc 100644 --- a/apps.json +++ b/apps.json @@ -1164,7 +1164,7 @@ "name": "Battery Chart", "shortName":"Battery Chart", "icon": "app.png", - "version":"0.06", + "version":"0.07", "description": "A widget and an app for recording and visualizing battery percentage over time.", "tags": "app,widget,battery,time,record,chart,tool", "storage": [ diff --git a/apps/batchart/ChangeLog b/apps/batchart/ChangeLog index 7a0ce429b..ba9e4e847 100644 --- a/apps/batchart/ChangeLog +++ b/apps/batchart/ChangeLog @@ -3,4 +3,5 @@ 0.03: Rotate log files once a week. 0.04: chart in the app is now active. 0.05: Display temperature and LCD state in chart -0.06: Fixes widget events and charting of component states \ No newline at end of file +0.06: Fixes widget events and charting of component states +0.07: Improve logging and charting of component states and add widget icon \ No newline at end of file diff --git a/apps/batchart/widget.js b/apps/batchart/widget.js index 892c163fc..53f8b3549 100644 --- a/apps/batchart/widget.js +++ b/apps/batchart/widget.js @@ -1,5 +1,3 @@ -WIDGETS = {}; - (() => { const Storage = require("Storage"); @@ -24,8 +22,16 @@ WIDGETS = {}; // draw your widget function draw() { + let x = this.x; + let y = this.y; + + g.setColor(0, 1, 0); + g.fillPoly([x+5, y, x+5, y+4, x+1, y+4, x+1, y+20, x+18, y+20, x+18, y+4, x+13, y+4, x+13, y], true); + + g.setColor(0,0,0); + g.drawPoly([x+5, y+6, x+8, y+12, x+13, y+12, x+16, y+18], false); + g.reset(); - g.drawString("BC", this.x, this.y); } function onMag(){ @@ -53,7 +59,7 @@ WIDGETS = {}; // Wait two seconds, that should be enough for each of the events to get raised once setTimeout(() => { - Bangle.removeAllListeners; + Bangle.removeAllListeners(); }, 2000); if (Bangle.isLCDOn()) @@ -73,7 +79,7 @@ WIDGETS = {}; gpsEventReceived = false; hrmEventReceived = false; - return enabledConsumers; + return enabledConsumers.toString(); } function logBatteryData() { @@ -84,7 +90,7 @@ WIDGETS = {}; const logFileName = "bclog" + currentWriteDay; // Change log target on day change - if (previousWriteDay != NaN + if (!isNaN(previousWriteDay) && previousWriteDay != currentWriteDay) { //Remove a log file containing data from a week ago Storage.open(logFileName, "r").erase(); @@ -93,9 +99,13 @@ WIDGETS = {}; var bcLogFileA = Storage.open(logFileName, "a"); if (bcLogFileA) { - let logString = [getTime().toFixed(0), E.getBattery(), E.getTemperature(), getEnabledConsumersValue()].join(","); + let logTime = getTime().toFixed(0); + let logPercent = E.getBattery(); + let logTemperature = E.getTemperature(); + let logConsumers = getEnabledConsumersValue(); + + let logString = [logTime, logPercent, logTemperature, logConsumers].join(","); - console.log(logString); bcLogFileA.write(logString + "\n"); } } @@ -103,7 +113,7 @@ WIDGETS = {}; function reload() { WIDGETS["batchart"].width = 24; - recordingInterval = setInterval(logBatteryData, recordingInterval10S); + recordingInterval = setInterval(logBatteryData, recordingInterval10Min); logBatteryData(); } From 50d5ca7f7b70c892158335d3e26096a735898212 Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Wed, 15 Apr 2020 01:04:45 +0200 Subject: [PATCH 061/302] welcome: don't run when settings are absent/updated Fixes #298 --- apps.json | 3 ++- apps/welcome/ChangeLog | 3 +++ apps/welcome/app.js | 7 +++++++ apps/welcome/boot.js | 6 ++++-- apps/welcome/settings-default.json | 3 +++ apps/welcome/settings.js | 13 ++++++------- 6 files changed, 25 insertions(+), 10 deletions(-) create mode 100644 apps/welcome/settings-default.json diff --git a/apps.json b/apps.json index 21fdf09b2..a2ebea7b5 100644 --- a/apps.json +++ b/apps.json @@ -78,7 +78,7 @@ { "id": "welcome", "name": "Welcome", "icon": "app.png", - "version":"0.06", + "version":"0.07", "description": "Appears at first boot and explains how to use Bangle.js", "tags": "start,welcome", "allow_emulator":true, @@ -86,6 +86,7 @@ {"name":"welcome.boot.js","url":"boot.js"}, {"name":"welcome.app.js","url":"app.js"}, {"name":"welcome.settings.js","url":"settings.js"}, + {"name":"welcome.settings.json","url":"settings-default.json","evaluate":true}, {"name":"welcome.img","url":"app-icon.js","evaluate":true} ] }, diff --git a/apps/welcome/ChangeLog b/apps/welcome/ChangeLog index 89f3ab2c9..b8786af6a 100644 --- a/apps/welcome/ChangeLog +++ b/apps/welcome/ChangeLog @@ -4,3 +4,6 @@ 0.04: Fix regression after tweaks to Storage.readJSON 0.05: Move configuration into App/widget settings 0.06: Move loader into welcome.boot.js +0.07: Run again when updated + Don't run again when settings app is updated (or absent) + Add "Run Now" option to settings diff --git a/apps/welcome/app.js b/apps/welcome/app.js index 93a4234d8..a32a6e56f 100644 --- a/apps/welcome/app.js +++ b/apps/welcome/app.js @@ -288,6 +288,13 @@ setWatch(()=>{ }, BTN2, {repeat:true,edge:"rising"}); setWatch(()=>move(-1), BTN1, {repeat:true}); +(function migrateSettings(){ + let global_settings = require('Storage').readJSON('setting.json', 1) + if (global_settings) { + delete global_settings.welcomed + require('Storage').write('setting.json', global_settings) + } +})() Bangle.setLCDTimeout(0); Bangle.setLCDPower(1); diff --git a/apps/welcome/boot.js b/apps/welcome/boot.js index ecf98b555..bc5afcc66 100644 --- a/apps/welcome/boot.js +++ b/apps/welcome/boot.js @@ -1,9 +1,11 @@ (function() { - let s = require('Storage').readJSON('setting.json', 1) || {} + let s = require('Storage').readJSON('welcome.settings.json', 1) + || require('Storage').readJSON('setting.json', 1) + || {welcomed: true} // do NOT run if global settings are also absent if (!s.welcomed && require('Storage').read('welcome.app.js')) { setTimeout(() => { s.welcomed = true - require('Storage').write('setting.json', s) + require('Storage').write('welcome.settings.json', {welcomed: "yes"}) load('welcome.app.js') }) } diff --git a/apps/welcome/settings-default.json b/apps/welcome/settings-default.json new file mode 100644 index 000000000..d250efff5 --- /dev/null +++ b/apps/welcome/settings-default.json @@ -0,0 +1,3 @@ +{ + "welcomed": false +} diff --git a/apps/welcome/settings.js b/apps/welcome/settings.js index 2fbd585c6..b11921646 100644 --- a/apps/welcome/settings.js +++ b/apps/welcome/settings.js @@ -1,16 +1,15 @@ // The welcome app is special, and gets to use global settings (function(back) { - let settings = require('Storage').readJSON('setting.json', 1) || {} + let settings = require('Storage').readJSON('welcome.settings.json', 1) + || require('Storage').readJSON('setting.json', 1) || {} E.showMenu({ '': { 'title': 'Welcome App' }, - 'Run again': { + 'Run on Next Boot': { value: !settings.welcomed, - format: v => v ? 'Yes' : 'No', - onchange: v => { - settings.welcomed = v ? undefined : true - require('Storage').write('setting.json', settings) - }, + format: v => v ? 'OK' : 'No', + onchange: v => require('Storage').write('welcome.settings.json', {welcomed: !v}), }, + 'Run Now': () => load('welcome.app.js'), '< Back': back, }) }) From 69a1e0038491a41e53a71892648f65fb3e88918a Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Wed, 15 Apr 2020 01:10:36 +0200 Subject: [PATCH 062/302] ncstart: don't run when settings are absent/updated Fixes #298 --- apps.json | 3 ++- apps/ncstart/ChangeLog | 3 +++ apps/ncstart/boot.js | 6 ++++-- apps/ncstart/settings-default.json | 3 +++ apps/ncstart/settings.js | 13 ++++++------- 5 files changed, 18 insertions(+), 10 deletions(-) create mode 100644 apps/ncstart/settings-default.json diff --git a/apps.json b/apps.json index a2ebea7b5..6a4ce496f 100644 --- a/apps.json +++ b/apps.json @@ -505,13 +505,14 @@ "id": "ncstart", "name": "NCEU Startup", "icon": "start.png", - "version":"0.03", + "version":"0.04", "description": "NodeConfEU 2019 'First Start' Sequence", "tags": "start,welcome", "storage": [ {"name":"ncstart.app.js","url":"start.js"}, {"name":"ncstart.boot.js","url":"boot.js"}, {"name":"ncstart.settings.js","url":"settings.js"}, + {"name":"ncstart.settings.json","url":"settings-default.json","evaluate":true}, {"name":"ncstart.img","url":"start-icon.js","evaluate":true}, {"name":"nc-bangle.img","url":"start-bangle.js","evaluate":true}, {"name":"nc-nceu.img","url":"start-nceu.js","evaluate":true}, diff --git a/apps/ncstart/ChangeLog b/apps/ncstart/ChangeLog index 553f7388a..f4418827e 100644 --- a/apps/ncstart/ChangeLog +++ b/apps/ncstart/ChangeLog @@ -2,3 +2,6 @@ Renamed as nodeconf-specific 0.03: Move configuration into App/widget settings Move loader into welcome.boot.js +0.04: Run again when updated + Don't run again when settings app is updated (or absent) + Add "Run Now" option to settings diff --git a/apps/ncstart/boot.js b/apps/ncstart/boot.js index dbb70d213..e3f514f5b 100644 --- a/apps/ncstart/boot.js +++ b/apps/ncstart/boot.js @@ -1,9 +1,11 @@ (function() { - let s = require('Storage').readJSON('setting.json', 1) || {} + let s = require('Storage').readJSON('ncstart.settings.json', 1) + || require('Storage').readJSON('setting.json', 1) + || {welcomed: true} // do NOT run if global settings are also absent if (!s.welcomed && require('Storage').read('ncstart.app.js')) { setTimeout(() => { s.welcomed = true - require('Storage').write('setting.json', s) + require('Storage').write('ncstart.settings.json', s) load('ncstart.app.js') }) } diff --git a/apps/ncstart/settings-default.json b/apps/ncstart/settings-default.json new file mode 100644 index 000000000..d250efff5 --- /dev/null +++ b/apps/ncstart/settings-default.json @@ -0,0 +1,3 @@ +{ + "welcomed": false +} diff --git a/apps/ncstart/settings.js b/apps/ncstart/settings.js index 284262634..2b24095cf 100644 --- a/apps/ncstart/settings.js +++ b/apps/ncstart/settings.js @@ -1,16 +1,15 @@ // The welcome app is special, and gets to use global settings (function(back) { - let settings = require('Storage').readJSON('setting.json', 1) || {} + let settings = require('Storage').readJSON('ncstart.settings.json', 1) + || require('Storage').readJSON('setting.json', 1) || {} E.showMenu({ '': { 'title': 'NCEU Startup' }, - 'Run again': { + 'Run on Next Boot': { value: !settings.welcomed, - format: v => v ? 'Yes' : 'No', - onchange: v => { - settings.welcomed = v ? undefined : true - require('Storage').write('setting.json', settings) - }, + format: v => v ? 'OK' : 'No', + onchange: v => require('Storage').write('ncstart.settings.json', {welcomed: !v}), }, + 'Run Now': () => load('ncstart.app.js'), '< Back': back, }) }) From 6068eb078654d05011a63fe0d2c976ba358c6242 Mon Sep 17 00:00:00 2001 From: Fabio Date: Wed, 15 Apr 2020 09:16:19 +0200 Subject: [PATCH 063/302] Minor text fix in app.json description --- apps.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps.json b/apps.json index 1f168e771..91bfe19af 100644 --- a/apps.json +++ b/apps.json @@ -1220,7 +1220,7 @@ "shortName":"Snake", "icon": "snake.png", "version":"0.02", - "description": "The classic snake game. Eat apples and don't bite your tail:", + "description": "The classic snake game. Eat apples and don't bite your tail.", "tags": "game,fun", "readme": "README.md", "storage": [ From 78214a267e6b68662b542deef2fd38314437b578 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Wed, 15 Apr 2020 10:35:29 +0100 Subject: [PATCH 064/302] 0.15: Reduce memory usage when running default clock chooser (#294) --- apps.json | 2 +- apps/setting/ChangeLog | 1 + apps/setting/settings.js | 8 ++++---- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/apps.json b/apps.json index 96e695237..60c9ca5f3 100644 --- a/apps.json +++ b/apps.json @@ -120,7 +120,7 @@ { "id": "setting", "name": "Settings", "icon": "settings.png", - "version":"0.14", + "version":"0.15", "description": "A menu for setting up Bangle.js", "tags": "tool,system", "storage": [ diff --git a/apps/setting/ChangeLog b/apps/setting/ChangeLog index 6c4c19230..3d82be9c0 100644 --- a/apps/setting/ChangeLog +++ b/apps/setting/ChangeLog @@ -16,3 +16,4 @@ Make capitalization more consistent Move LCD Brightness menu into more general LCD menu 0.14: Reduce memory usage when running app settings page +0.15: Reduce memory usage when running default clock chooser (#294) diff --git a/apps/setting/settings.js b/apps/setting/settings.js index d0d6578dc..9e343a68e 100644 --- a/apps/setting/settings.js +++ b/apps/setting/settings.js @@ -296,10 +296,10 @@ function makeConnectable() { }); } function showClockMenu() { - var clockApps = require("Storage").list(/\.info$/).map(app => { - try { return require("Storage").readJSON(app); } - catch (e) { } - }).filter(app => app.type == "clock").sort((a, b) => a.sortorder - b.sortorder); + var clockApps = require("Storage").list(/\.info$/) + .map(app => {var a=storage.readJSON(app, 1);return (a&&a.type == "clock")?a:undefined}) + .filter(app => app) // filter out any undefined apps + .sort((a, b) => a.sortorder - b.sortorder); const clockMenu = { '': { 'title': 'Select Clock', From 02dcc1970960cd648ca0c42a48e428d6191f2946 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Wed, 15 Apr 2020 10:50:07 +0100 Subject: [PATCH 065/302] widbt: Fix automatic update of Bluetooth connection status --- apps.json | 2 +- apps/widbt/ChangeLog | 1 + apps/widbt/widget.js | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps.json b/apps.json index 60c9ca5f3..69a41c52f 100644 --- a/apps.json +++ b/apps.json @@ -355,7 +355,7 @@ { "id": "widbt", "name": "Bluetooth Widget", "icon": "widget.png", - "version":"0.03", + "version":"0.04", "description": "Show the current Bluetooth connection status in the top right of the clock", "tags": "widget,bluetooth", "type":"widget", diff --git a/apps/widbt/ChangeLog b/apps/widbt/ChangeLog index c268d6df0..59dc603a9 100644 --- a/apps/widbt/ChangeLog +++ b/apps/widbt/ChangeLog @@ -1,2 +1,3 @@ 0.02: Tweaks for variable size widget system 0.03: Ensure redrawing works with variable size widget system +0.04: Fix automatic update of Bluetooth connection status diff --git a/apps/widbt/widget.js b/apps/widbt/widget.js index 8e96a395d..c3254c791 100644 --- a/apps/widbt/widget.js +++ b/apps/widbt/widget.js @@ -13,7 +13,7 @@ function changed() { WIDGETS["bluetooth"].draw(); g.flip();// turns screen on } -NRF.on('connected',changed); -NRF.on('disconnected',changed); +NRF.on('connect',changed); +NRF.on('disconnect',changed); WIDGETS["bluetooth"]={area:"tr",width:24,draw:draw}; })() From f890227b619c433047fc8784328002cc6f7da4de Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Wed, 15 Apr 2020 10:53:03 +0100 Subject: [PATCH 066/302] gbridge: 0.09: Update Bluetooth connection state automatically --- apps.json | 2 +- apps/gbridge/ChangeLog | 1 + apps/gbridge/widget.js | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps.json b/apps.json index 69a41c52f..0643ad6de 100644 --- a/apps.json +++ b/apps.json @@ -93,7 +93,7 @@ { "id": "gbridge", "name": "Gadgetbridge", "icon": "app.png", - "version":"0.08", + "version":"0.09", "description": "The default notification handler for Gadgetbridge notifications from Android", "tags": "tool,system,android,widget", "type":"widget", diff --git a/apps/gbridge/ChangeLog b/apps/gbridge/ChangeLog index d1f9c6a62..53f8a1b4c 100644 --- a/apps/gbridge/ChangeLog +++ b/apps/gbridge/ChangeLog @@ -7,3 +7,4 @@ 0.06: Gadgetbridge App 'Connected' state is no longer toggleable 0.07: Move configuration to settings menu 0.08: Don't turn on LCD at start of every song +0.09: Update Bluetooth connection state automatically diff --git a/apps/gbridge/widget.js b/apps/gbridge/widget.js index fa44757fc..03c622443 100644 --- a/apps/gbridge/widget.js +++ b/apps/gbridge/widget.js @@ -189,8 +189,8 @@ g.flip(); // turns screen on } - NRF.on("connected", changedConnectionState); - NRF.on("disconnected", changedConnectionState); + NRF.on("connect", changedConnectionState); + NRF.on("disconnect", changedConnectionState); WIDGETS["gbridgew"] = { area: "tl", width: 24, draw: draw }; From 24d35eca2cf4e676832362803a378e453871e2db Mon Sep 17 00:00:00 2001 From: Red-The-Hunter <62763030+Red-The-Hunter@users.noreply.github.com> Date: Wed, 15 Apr 2020 18:54:29 +0800 Subject: [PATCH 067/302] Update stopwatch.js --- apps/swatch/stopwatch.js | 67 ++++++++++++++++++++++++++++------------ 1 file changed, 47 insertions(+), 20 deletions(-) diff --git a/apps/swatch/stopwatch.js b/apps/swatch/stopwatch.js index 6f8ad9e34..478c22619 100644 --- a/apps/swatch/stopwatch.js +++ b/apps/swatch/stopwatch.js @@ -1,8 +1,11 @@ +var tTotal = Date.now(); var tStart = Date.now(); var tCurrent = Date.now(); var started = false; -var timeY = 60; +var timeY = 45; var hsXPos = 0; +var TtimeY = 75; +var ThsXPos = 0; var lapTimes = []; var displayInterval; @@ -12,6 +15,7 @@ function timeToText(t) { var hs = Math.floor(t/10)%100; return mins+":"+("0"+secs).substr(-2)+"."+("0"+hs).substr(-2); } + function updateLabels() { g.reset(1); g.clearRect(0,23,g.getWidth()-1,g.getHeight()-24); @@ -24,35 +28,53 @@ function updateLabels() { g.setFontAlign(-1,-1); for (var i in lapTimes) { if (i<16) - {g.drawString(lapTimes.length-i+": "+timeToText(lapTimes[i]),35,timeY + 30 + i*8);} + {g.drawString(lapTimes.length-i+": "+timeToText(lapTimes[i]),35,timeY + 40 + i*8);} else if (i<32) - {g.drawString(lapTimes.length-i+": "+timeToText(lapTimes[i]),125,timeY + 30 + (i-16)*8);} + {g.drawString(lapTimes.length-i+": "+timeToText(lapTimes[i]),125,timeY + 40 + (i-16)*8);} } drawsecs(); } + function drawsecs() { var t = tCurrent-tStart; - g.reset(1); - g.setFont("Vector",48); - g.setFontAlign(0,0); + var Tt = tCurrent-tTotal; var secs = Math.floor(t/1000)%60; var mins = Math.floor(t/60000); var txt = mins+":"+("0"+secs).substr(-2); + var Tsecs = Math.floor(Tt/1000)%60; + var Tmins = Math.floor(Tt/60000); + var Ttxt = Tmins+":"+("0"+Tsecs).substr(-2); var x = 100; - g.clearRect(0,timeY-26,200,timeY+26); - g.drawString(txt,x,timeY); + var Tx = 125; + g.reset(1); + g.setFont("Vector",38); + g.setFontAlign(0,0); + g.clearRect(0,timeY-21,200,timeY+21); + g.drawString(Ttxt,x,timeY); hsXPos = 5+x+g.stringWidth(txt)/2; + g.setFont("6x8",2); + g.clearRect(0,TtimeY-7,200,TtimeY+7); + g.drawString(txt,Tx,TtimeY); + ThsXPos = 5+Tx+g.stringWidth(Ttxt)/2; drawms(); } + function drawms() { var t = tCurrent-tStart; var hs = Math.floor(t/10)%100; + var Tt = tCurrent-tTotal; + var Ths = Math.floor(Tt/10)%100; g.setFontAlign(-1,0); g.setFont("6x8",2); g.clearRect(hsXPos,timeY,220,timeY+20); - g.drawString("."+("0"+hs).substr(-2),hsXPos,timeY+10); + g.drawString("."+("0"+Ths).substr(-2),hsXPos-5,timeY+14); + g.setFont("6x8",1); + g.clearRect(ThsXPos,TtimeY,220,TtimeY+5); + g.drawString("."+("0"+hs).substr(-2),ThsXPos-5,TtimeY+3); } + function getLapTimesArray() { + lapTimes.push(tCurrent-tTotal); return lapTimes.map(timeToText).reverse(); } @@ -61,7 +83,8 @@ setWatch(function() { // Start/stop Bangle.beep(); if (started) tStart = Date.now()+tStart-tCurrent; - tCurrent = Date.now(); + tTotal = Date.now()+tTotal-tCurrent; + tCurrent = Date.now(); if (displayInterval) { clearInterval(displayInterval); displayInterval = undefined; @@ -69,37 +92,41 @@ setWatch(function() { // Start/stop updateLabels(); if (started) displayInterval = setInterval(function() { - var last = tCurrent; - if (started) tCurrent = Date.now(); - if (Math.floor(last/1000)!=Math.floor(tCurrent/1000)) - drawsecs(); - else - drawms(); + var last = tCurrent; + if (started) tCurrent = Date.now(); + if (Math.floor(last/1000)!=Math.floor(tCurrent/1000)) + drawsecs(); + else + drawms(); }, 20); }, BTN2, {repeat:true}); + setWatch(function() { // Lap Bangle.beep(); if (started) { tCurrent = Date.now(); lapTimes.unshift(tCurrent-tStart); } - tStart = tCurrent; if (!started) { // save var timenow= Date(); var filename = "swatch-"+(new Date()).toISOString().substr(0,16).replace("T","_")+".json"; // this maxes out the 28 char maximum + lapTimes.unshift(tCurrent-tStart); require("Storage").writeJSON(filename, getLapTimesArray()); + tStart = tCurrent = tTotal = Date.now(); + lapTimes = []; E.showMessage("Laps Saved","Stopwatch"); setTimeout(updateLabels, 1000); } else { + tStart = tCurrent; updateLabels(); } }, BTN1, {repeat:true}); setWatch(function() { // Reset if (!started) { - Bangle.beep(); - tStart = tCurrent = Date.now(); - lapTimes = []; + Bangle.beep(); + tStart = tCurrent = tTotal = Date.now(); + lapTimes = []; } updateLabels(); }, BTN3, {repeat:true}); From 9a03c9bcce110c428d7334a11b01e4b48019ce81 Mon Sep 17 00:00:00 2001 From: Red-The-Hunter <62763030+Red-The-Hunter@users.noreply.github.com> Date: Wed, 15 Apr 2020 18:57:52 +0800 Subject: [PATCH 068/302] Update ChangeLog --- apps/swatch/ChangeLog | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/swatch/ChangeLog b/apps/swatch/ChangeLog index 3246eeced..9a037fa41 100644 --- a/apps/swatch/ChangeLog +++ b/apps/swatch/ChangeLog @@ -5,3 +5,4 @@ Fixed bug from 0.01 where BN1 (reset) could clear the lap log when timer is running 0.04: Changed save file filename, add interface.html to allow laps to be loaded 0.05: Added widgets +0.06: Added total running time, moved lap time to smaller display, total run time now appends as first entry in array, saving now saves last lap as well From 5d588d6d696d00133e28269e4d125595faa8d89e Mon Sep 17 00:00:00 2001 From: Red-The-Hunter <62763030+Red-The-Hunter@users.noreply.github.com> Date: Wed, 15 Apr 2020 19:11:54 +0800 Subject: [PATCH 069/302] Update interface.html --- apps/swatch/interface.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/swatch/interface.html b/apps/swatch/interface.html index 928c5fe39..bfa7f8193 100644 --- a/apps/swatch/interface.html +++ b/apps/swatch/interface.html @@ -20,7 +20,7 @@ function getLapTimes() { html += `
-
${lap.date}
+
${lap.date} Elapsed Time: ${lap.d.1}
${lap.d.length} Laps
@@ -32,7 +32,7 @@ function getLapTimes() { - ${ lap.d.map((d,n)=>`${n+1}${d}`).join("\n") } + ${ lap.d.map((d+1,n)=>`${n+1}${d+1}`).join("\n") }
From 70e9c23c54f5898a83cdf1abb61b17e5304fa66d Mon Sep 17 00:00:00 2001 From: Red-The-Hunter <62763030+Red-The-Hunter@users.noreply.github.com> Date: Wed, 15 Apr 2020 19:17:03 +0800 Subject: [PATCH 070/302] Update interface.html --- apps/swatch/interface.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/swatch/interface.html b/apps/swatch/interface.html index bfa7f8193..735f643a8 100644 --- a/apps/swatch/interface.html +++ b/apps/swatch/interface.html @@ -20,7 +20,7 @@ function getLapTimes() { html += `
-
${lap.date} Elapsed Time: ${lap.d.1}
+
${lap.date} Elapsed Time: ${lap[1]}
${lap.d.length} Laps
From 02aaf2dd166ba350019c3eb44eba757ba835dfef Mon Sep 17 00:00:00 2001 From: Red-The-Hunter <62763030+Red-The-Hunter@users.noreply.github.com> Date: Wed, 15 Apr 2020 19:22:07 +0800 Subject: [PATCH 071/302] Update interface.html --- apps/swatch/interface.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/swatch/interface.html b/apps/swatch/interface.html index 735f643a8..53f0dd503 100644 --- a/apps/swatch/interface.html +++ b/apps/swatch/interface.html @@ -20,7 +20,7 @@ function getLapTimes() { html += `
-
${lap.date} Elapsed Time: ${lap[1]}
+
${lap.date}
${lap.d.length} Laps
From 0f20176280e15875b8c9c3ef06619b887c3c28c3 Mon Sep 17 00:00:00 2001 From: Red-The-Hunter <62763030+Red-The-Hunter@users.noreply.github.com> Date: Wed, 15 Apr 2020 19:22:51 +0800 Subject: [PATCH 072/302] Update interface.html --- apps/swatch/interface.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/swatch/interface.html b/apps/swatch/interface.html index 53f0dd503..928c5fe39 100644 --- a/apps/swatch/interface.html +++ b/apps/swatch/interface.html @@ -32,7 +32,7 @@ function getLapTimes() { - ${ lap.d.map((d+1,n)=>`${n+1}${d+1}`).join("\n") } + ${ lap.d.map((d,n)=>`${n+1}${d}`).join("\n") }
From 32e811b2312ab99bf7ba1bcf466621c0d0a30bc2 Mon Sep 17 00:00:00 2001 From: Red-The-Hunter <62763030+Red-The-Hunter@users.noreply.github.com> Date: Wed, 15 Apr 2020 19:32:17 +0800 Subject: [PATCH 074/302] Update apps.json --- apps.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps.json b/apps.json index 0643ad6de..5fae8f482 100644 --- a/apps.json +++ b/apps.json @@ -399,7 +399,7 @@ { "id": "swatch", "name": "Stopwatch", "icon": "stopwatch.png", - "version":"0.05", + "version":"0.06", "interface": "interface.html", "description": "Simple stopwatch with Lap Time logging to a JSON file", "tags": "health", From b8226cce8fbfaa0a86c5f13a288a6d920be74bd1 Mon Sep 17 00:00:00 2001 From: OmegaRogue Date: Wed, 15 Apr 2020 14:00:01 +0200 Subject: [PATCH 075/302] Added DANE Signed-off-by: OmegaRogue --- apps.json | 22 +++++ apps/dane/ChangeLog | 4 + apps/dane/add_to_apps.json | 22 +++++ apps/dane/app-icon.js | 1 + apps/dane/app.js | 163 +++++++++++++++++++++++++++++++++++++ apps/dane/app.png | Bin 0 -> 15535 bytes site.webmanifest | 4 +- 7 files changed, 214 insertions(+), 2 deletions(-) create mode 100644 apps/dane/ChangeLog create mode 100644 apps/dane/add_to_apps.json create mode 100644 apps/dane/app-icon.js create mode 100644 apps/dane/app.js create mode 100644 apps/dane/app.png diff --git a/apps.json b/apps.json index 0643ad6de..91d2d249b 100644 --- a/apps.json +++ b/apps.json @@ -1241,5 +1241,27 @@ {"name":"calculator.app.js","url":"app.js"}, {"name":"calculator.img","url":"calculator-icon.js","evaluate":true} ] + }, + { + "id": "dane", + "name": "Digital Assistant, not EDITH", + "shortName": "DANE", + "icon": "app.png", + "version": "0.06", + "description": "A detailed description of my great app", + "tags": "clock", + "type": "clock", + "allow_emulator": true, + "storage": [ + { + "name": "dane.app.js", + "url": "app.js" + }, + { + "name": "dane.img", + "url": "app-icon.js", + "evaluate": true + } + ] } ] diff --git a/apps/dane/ChangeLog b/apps/dane/ChangeLog new file mode 100644 index 000000000..607d0adf5 --- /dev/null +++ b/apps/dane/ChangeLog @@ -0,0 +1,4 @@ +0.01: New App! +0.04: Added Icon to watchface +0.05: bugfix +0.06: moved and resized icon \ No newline at end of file diff --git a/apps/dane/add_to_apps.json b/apps/dane/add_to_apps.json new file mode 100644 index 000000000..6efb3ec85 --- /dev/null +++ b/apps/dane/add_to_apps.json @@ -0,0 +1,22 @@ +{ + "id": "dane", + "name": "Digital Assistant, not EDITH", + "shortName": "DANE", + "icon": "app.png", + "version": "0.06", + "description": "A detailed description of my great app", + "tags": "clock", + "type": "clock", + "allow_emulator": true, + "storage": [ + { + "name": "dane.app.js", + "url": "app.js" + }, + { + "name": "dane.img", + "url": "app-icon.js", + "evaluate": true + } + ] +} \ No newline at end of file diff --git a/apps/dane/app-icon.js b/apps/dane/app-icon.js new file mode 100644 index 000000000..4deb12640 --- /dev/null +++ b/apps/dane/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("l8wxH+AH4A/AH4A/AFUvl8Cu4AEgUCBQIrfFQMRAAe/Aw4xbDYIlBiUS7AjCAAY5BBYMSiJkBGC4sCicTiRQJHoUSCAIwBF6sv30SikUiRMMMIISD7AvTl/YiYtPF40TF6R4BicVFqAWDF4MViaPRIwQWTF4O/IwiKRCoMRUiZHEDJ5cXJAxeOOQuQhQuShWQJIe/JJkviIuC74tTFwORRqKLD+3cmVLpsLFZtNAANKhXeDYKNOu4uEmdlDwVNBoNlsoDDmoKBhYQChcyFycVFwOTFwJcBpomBhYjCmouBAwYMCmZdBa4d3FyonBKoIoCAwIECLooucEIIjCRIYuFms1Lqq7CFwS7DLQQsDhYrBHIZdHXZkCdQpQDXoIQDFwIDBeoQQCpYuSl8RFwMT70KCRYAIhUSFwMTiMvFxm/CQUSFyp5Did3Fxi8DOBwuLDSEv7ETfoRCNDI13DIMT34ZPIYSgOaxJ3SIgZeTC7COBdgMCC58vOoakWiQvQFoQTBFqgvEiURF5gRDOKIdIDwMRiO/axMCBoMRLQItXF4Z9B7F3BxF37BZBAAQnRIYobDMAKqIl5aDAA5zJFwaCBAA6PBFxQQEAAYKBFxjSCU4IECA4YuJCAoAEFx0UikTAAIEBAwQuKCIoADFxsCI5RdiUAoAEVgIVJABRDHAH4A/AH4A/ADAA=")) \ No newline at end of file diff --git a/apps/dane/app.js b/apps/dane/app.js new file mode 100644 index 000000000..dc6262c58 --- /dev/null +++ b/apps/dane/app.js @@ -0,0 +1,163 @@ +const font = "6x8"; +const timeFontSize = 4; +const dateFontSize = 3; +const smallFontSize = 2; +const yOffset = 23; +const xyCenter = g.getWidth()/2; +const cornerSize = 14; +const cornerOffset = 3; +const borderWidth = 1; +const yposTime = 27+yOffset; +const yposDate = 65+yOffset; + +const mainColor = "#26dafd"; +const mainColorDark = "#029dbb"; +const mainColorLight = "#8bebfe"; + +const secondaryColor = "#df9527"; +const secondaryColorDark = "#8b5c15"; +const secondaryColorLight = "#ecc180"; + +const success = "#00ff00"; +const successDark = "#000900"; +const successLight = "#060f06"; + +const alert = "#ff0000"; +const alertDark = "#090000"; +const alertLight = "#0f0606"; + +var img = { + width : 120, height : 120, bpp : 8, + transparent : 254, + buffer : require("heatshrink").decompress(atob("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")) +} + + +function drawTopLeftCorner(x,y) { + g.setColor(mainColor); + var x1 = x-cornerOffset; + var y1 = y-cornerOffset; + g.fillRect(x1,y1,x1+cornerSize,y1+cornerSize); + g.setColor("#000000"); + g.fillRect(x,y,x+cornerSize-cornerOffset,y+cornerSize-cornerOffset); +} +function drawTopRightCorner(x,y) { + g.setColor(mainColor); + var x1 = x+cornerOffset; + var y1 = y-cornerOffset; + g.fillRect(x1,y1,x1-cornerSize,y1+cornerSize); + g.setColor("#000000"); + g.fillRect(x,y,x-cornerSize-cornerOffset,y+cornerSize-cornerOffset); +} +function drawBottomLeftCorner(x,y) { + g.setColor(mainColor); + var x1 = x-cornerOffset; + var y1 = y+cornerOffset; + g.fillRect(x1,y1,x1+cornerSize,y1-cornerSize); + g.setColor("#000000"); + g.fillRect(x,y,x+cornerSize-cornerOffset,y-cornerSize+cornerOffset); +} +function drawBottomRightCorner(x,y) { + g.setColor(mainColor); + var x1 = x+cornerOffset; + var y1 = y+cornerOffset; + g.fillRect(x1,y1,x1-cornerSize,y1-cornerSize); + g.setColor("#000000"); + g.fillRect(x,y,x-cornerSize+cornerOffset,y-cornerSize+cornerOffset); +} + +function drawFrame(x1,y1,x2,y2) { + drawTopLeftCorner(x1,y1); + drawTopRightCorner(x2,y1); + drawBottomLeftCorner(x1,y2); + drawBottomRightCorner(x2,y2); + g.setColor(mainColorDark); + g.drawRect(x1,y1,x2,y2); + g.setColor("#000000"); + g.fillRect(x1+borderWidth,y1+borderWidth,x2-borderWidth,y2-borderWidth); +} +function drawTopFrame(x1,y1,x2,y2) { + + drawBottomLeftCorner(x1,y2); + drawBottomRightCorner(x2,y2); + g.setColor(mainColorDark); + g.drawRect(x1,y1,x2,y2); + g.setColor("#000000"); + g.fillRect(x1+borderWidth,y1+borderWidth,x2-borderWidth,y2-borderWidth); +} +function drawBottomFrame(x1,y1,x2,y2) { + drawTopLeftCorner(x1,y1); + drawTopRightCorner(x2,y1); + g.setColor(mainColorDark); + g.drawRect(x1,y1,x2,y2); + g.setColor("#000000"); + g.fillRect(x1+borderWidth,y1+borderWidth,x2-borderWidth,y2-borderWidth); +} + +function getUTCTime(d) { + return d.toUTCString().split(' ')[4].split(':').map(function(d){return Number(d);}); +} + + + + + +function drawTimeText() { + g.setFontAlign(0, 0); + var d = new Date(); + var da = d.toString().split(" "); + var dutc = getUTCTime(d); + + var time = da[4].split(":"); + var hours = time[0], + minutes = time[1], + seconds = time[2]; + g.setColor(mainColor); + g.setFont(font, timeFontSize); + g.drawString(`${hours}:${minutes}:${seconds}`, xyCenter, yposTime, true); + g.setFont(font, smallFontSize); +} +function drawDateText() { + g.setFontAlign(0, 0); + var d = new Date(); + g.setFont(font, dateFontSize); + g.drawString(`${d.getDate()}.${d.getMonth()+1}.${d.getFullYear()}`, xyCenter, yposDate, true); +} + + + +function drawClock() { + // main frame + drawFrame(3,10+yOffset,g.getWidth()-3,g.getHeight()-3); + // time frame + drawTopFrame(20,10+yOffset,220,46+yOffset); + // date frame + drawTopFrame(28,46+yOffset,212,46+yOffset+35); + + // texts + drawTimeText(); + drawDateText(); + g.drawImage(img,g.getWidth()/2-(img.width/2),g.getHeight()/2); +} +function updateClock() { + drawTimeText(); + drawDateText(); +} + + +Bangle.on('lcdPower', function(on) { + if (on) drawClock(); +}); +g.clear(); + +Bangle.loadWidgets(); +Bangle.drawWidgets(); + + +drawClock(); + + +setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"}); + +// refesh every 100 milliseconds +setInterval(updateClock, 100); diff --git a/apps/dane/app.png b/apps/dane/app.png new file mode 100644 index 0000000000000000000000000000000000000000..ee4f8403a82262e37b2750c9d7b245668e6cc46e GIT binary patch literal 15535 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 literal 0 HcmV?d00001 diff --git a/site.webmanifest b/site.webmanifest index 0d1fb4679..0565f159c 100644 --- a/site.webmanifest +++ b/site.webmanifest @@ -17,6 +17,6 @@ "theme_color": "#5755d9", "background_color": "#5755d9", "display": "standalone", - "start_url": "https://banglejs.com/apps/", - "scope": "https://banglejs.com/apps/" + "start_url": "https://omegavoid.codes/BangleApps", + "scope": "https://omegavoid.codes/BangleApps" } From 40f291730e8fbee478c6783008eb0ea620651143 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Wed, 15 Apr 2020 13:39:50 +0100 Subject: [PATCH 076/302] 0.07: Added @jeffmer's awesome track viewer (fix #273) --- apps.json | 2 +- apps/gpsrec/ChangeLog | 1 + apps/gpsrec/app.js | 148 +++++++++++++++++++++++++++++++++++++----- 3 files changed, 133 insertions(+), 18 deletions(-) diff --git a/apps.json b/apps.json index 0643ad6de..04061a71d 100644 --- a/apps.json +++ b/apps.json @@ -280,7 +280,7 @@ { "id": "gpsrec", "name": "GPS Recorder", "icon": "app.png", - "version":"0.06", + "version":"0.07", "interface": "interface.html", "description": "Application that allows you to record a GPS track. Can run in background", "tags": "tool,outdoors,gps,widget", diff --git a/apps/gpsrec/ChangeLog b/apps/gpsrec/ChangeLog index 9a47bdd9a..8f1c575a1 100644 --- a/apps/gpsrec/ChangeLog +++ b/apps/gpsrec/ChangeLog @@ -4,3 +4,4 @@ 0.04: Properly Fix GPS time display in gpsrec app 0.05: Tweaks for variable size widget system 0.06: Ensure widget update itself (fix #118) and change to using icons +0.07: Added @jeffmer's awesome track viewer diff --git a/apps/gpsrec/app.js b/apps/gpsrec/app.js index 58b4295a6..63f3840ff 100644 --- a/apps/gpsrec/app.js +++ b/apps/gpsrec/app.js @@ -70,27 +70,65 @@ function viewTracks() { return E.showMenu(menu); } +function getTrackInfo(fn) { + var filename = getFN(fn); + var minLat = 90; + var maxLat = -90; + var minLong = 180; + var maxLong = -180; + var starttime, duration=0; + var f = require("Storage").open(filename,"r"); + if (f===undefined) return; + var l = f.readLine(f); + var nl = 0, c, n; + if (l!==undefined) { + c = l.split(","); + starttime = parseInt(c[0]); + } + // pushed this loop together to try and bump loading speed a little + while(l!==undefined) { + ++nl;c=l.split(","); + n = parseFloat(c[1]);if(n>maxLat)maxLat=n;if(nmaxLong)maxLong=n;if(nylen ? 200/xlen : 200/ylen; + return { + fn : fn, + filename : filename, + time : new Date(starttime), + records : nl, + minLat : minLat, maxLat : maxLat, + minLong : minLong, maxLong : maxLong, + lfactor : lfactor, + scale : scale, + duration : Math.round(duration/1000) + }; +} + +function asTime(v){ + var mins = Math.floor(v/60); + var secs = v-mins*60; + return ""+mins.toString()+"m "+secs.toString()+"s"; +} + function viewTrack(n) { + E.showMessage("Loading...","GPS Track "+n); + var info = getTrackInfo(n); const menu = { '': { 'title': 'GPS Track '+n } }; - var trackCount = 0; - var trackTime; - var f = require("Storage").open(getFN(n),"r"); - var l = f.readLine(); - if (l!==undefined) { - var c = l.split(","); - trackTime = new Date(parseInt(c[0])); - } - while (l!==undefined) { - trackCount++; - // TODO: min/max/length of track? - l = f.readLine(); - } - if (trackTime) - menu[" "+trackTime.toISOString().substr(0,16).replace("T"," ")] = function(){}; - menu[trackCount+" records"] = function(){}; - // TODO: option to draw it? Just scan through, project using min/max + if (info.time) + menu[info.time.toISOString().substr(0,16).replace("T"," ")] = function(){}; + menu["Duration"] = { value : asTime(info.duration)}; + menu["Records"] = { value : ""+info.records }; + menu['Plot'] = function() { + plotTrack(info); + }; menu['Erase'] = function() { E.showPrompt("Delete Track?").then(function(v) { if (v) { @@ -107,4 +145,80 @@ function viewTrack(n) { return E.showMenu(menu); } +function plotTrack(info) { + function xcoord(long){ + return 30 + Math.round((long-info.minLong)*info.lfactor*info.scale); + } + + function ycoord(lat){ + return 210 - Math.round((lat - info.minLat)*info.scale); + } + + function radians(a) { + return a*Math.PI/180; + } + + function distance(lat1,long1,lat2,long2){ + var x = radians(long1-long2) * Math.cos(radians((lat1+lat2)/2)); + var y = radians(lat2-lat1); + return Math.sqrt(x*x + y*y) * 6371000; + } + + E.showMenu(); // remove menu + g.setColor(1,0.5,0.5); + g.setFont("Vector",16); + g.fillRect(9,80,11,120); + g.fillPoly([9,60,19,80,0,80]); + g.setColor(1,1,1); + g.drawString("N",2,40); + g.drawString("Track"+info.fn.toString()+" - Loading",10,220); + g.setColor(0,0,0); + g.fillRect(0,220,239,239); + g.setColor(1,1,1); + g.drawString(asTime(info.duration),10,220); + var f = require("Storage").open(info.filename,"r"); + if (f===undefined) return; + var l = f.readLine(f); + var ox=0; + var oy=0; + var olat,olong,dist=0; + var first = true; + var i=0; + while(l!==undefined) { + var c = l.split(","); + var lat = parseFloat(c[1]); + var long = parseFloat(c[2]); + var x = xcoord(long); + var y = ycoord(lat); + if (first) { + g.moveTo(x,y); + g.setColor(0,1,0); + g.fillCircle(x,y,5); + g.setColor(1,1,1); + first = false; + } else if (x!=ox || y!=oy) { + g.lineTo(x,y); + } + if (!first) { + var d = distance(olat,olong,lat,long); + if (!isNaN(d)) dist+=d; + } + olat = lat; + olong = long; + ox = x; + oy = y; + l = f.readLine(f); + } + g.setColor(1,0,0); + g.fillCircle(ox,oy,5); + g.setColor(1,1,1); + g.drawString(require("locale").distance(dist),120,220); + g.setFont("6x8",2); + g.setFontAlign(0,0,3); + g.drawString("Back",230,200); + setWatch(function() { + viewTrack(info.fn); + }, BTN3); +} + showMainMenu(); From a56a9792f15499ed3ee6f74a1fe6c70a5f22ab3d Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Wed, 15 Apr 2020 13:58:05 +0100 Subject: [PATCH 077/302] Rewrite 'getInstalledApps' to minimize RAM usage --- CHANGELOG.md | 1 + js/comms.js | 13 +++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9480f2ace..95e973e0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,3 +9,4 @@ Changed for individual apps are listed in `apps/appname/ChangeLog` * Fix issue removing an app that was just installed (fix #253) * Add `Favourite` functionality * Version number now clickable even when you're at the latest version (fix #291) +* Rewrite 'getInstalledApps' to minimize RAM usage diff --git a/js/comms.js b/js/comms.js index 604ef19ed..1f840ada7 100644 --- a/js/comms.js +++ b/js/comms.js @@ -75,12 +75,21 @@ getInstalledApps : () => { Progress.hide({sticky:true}); return reject(""); } - Puck.eval('require("Storage").list(/\.info$/).map(f=>{var j=require("Storage").readJSON(f,1)||{};j.id=f.slice(0,-5);return j})', (appList,err) => { + Puck.write('\x10Bluetooth.print("[");require("Storage").list(/\.info$/).forEach(f=>{var j=require("Storage").readJSON(f,1)||{};j.id=f.slice(0,-5);Bluetooth.print(JSON.stringify(j)+",")});Bluetooth.println("0]")\n', (appList,err) => { Progress.hide({sticky:true}); + try { + appList = JSON.parse(appList); + // remove last element since we added a final '0' + // to make things easy on the Bangle.js side + appList = appList.slice(0,-1); + } catch (e) { + appList = null; + err = e.toString(); + } if (appList===null) return reject(err || ""); console.log("getInstalledApps", appList); resolve(appList); - }); + }, true /* callback on newline */); }); }); }, From a8f1aabbee9ac286d012b16fbfa63f7efa8632a1 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Wed, 15 Apr 2020 14:30:51 +0100 Subject: [PATCH 078/302] Fix regression stopping correct widget updates --- apps.json | 6 +++--- apps/widbat/ChangeLog | 1 + apps/widbat/widget.js | 2 +- apps/widbatpc/ChangeLog | 1 + apps/widbatpc/widget.js | 2 +- apps/widclk/ChangeLog | 1 + apps/widclk/widget.js | 4 ++-- 7 files changed, 10 insertions(+), 7 deletions(-) diff --git a/apps.json b/apps.json index 04061a71d..0a65897b5 100644 --- a/apps.json +++ b/apps.json @@ -330,7 +330,7 @@ { "id": "widbat", "name": "Battery Level Widget", "icon": "widget.png", - "version":"0.04", + "version":"0.05", "description": "Show the current battery level and charging status in the top right of the clock", "tags": "widget,battery", "type":"widget", @@ -342,7 +342,7 @@ "name": "Battery Level Widget (with percentage)", "shortName": "Battery Widget", "icon": "widget.png", - "version":"0.08", + "version":"0.09", "description": "Show the current battery level and charging status in the top right of the clock, with charge percentage", "tags": "widget,battery", "type":"widget", @@ -777,7 +777,7 @@ { "id": "widclk", "name": "Digital clock widget", "icon": "widget.png", - "version":"0.03", + "version":"0.04", "description": "A simple digital clock widget", "tags": "widget,clock", "type":"widget", diff --git a/apps/widbat/ChangeLog b/apps/widbat/ChangeLog index cd9993c02..b9d50ab8b 100644 --- a/apps/widbat/ChangeLog +++ b/apps/widbat/ChangeLog @@ -1,3 +1,4 @@ 0.02: Now refresh battery monitor every minute if LCD on 0.03: Tweaks for variable size widget system 0.04: Ensure redrawing works with variable size widget system +0.05: Fix regression stopping correct widget updates diff --git a/apps/widbat/widget.js b/apps/widbat/widget.js index 2f1f29802..dd6774d4c 100644 --- a/apps/widbat/widget.js +++ b/apps/widbat/widget.js @@ -30,7 +30,7 @@ Bangle.on('lcdPower', function(on) { WIDGETS["bat"].draw(); // refresh once a minute if LCD on if (!batteryInterval) - batteryInterval = setInterval(draw, 60000); + batteryInterval = setInterval(()=>WIDGETS["bat"].draw(), 60000); } else { if (batteryInterval) { clearInterval(batteryInterval); diff --git a/apps/widbatpc/ChangeLog b/apps/widbatpc/ChangeLog index 3627a86d3..129707320 100644 --- a/apps/widbatpc/ChangeLog +++ b/apps/widbatpc/ChangeLog @@ -5,3 +5,4 @@ 0.06: Show battery percentage as text 0.07: Add settings: percentage/color/charger icon 0.08: Draw percentage as inverted on monochrome battery +0.09: Fix regression stopping correct widget updates diff --git a/apps/widbatpc/widget.js b/apps/widbatpc/widget.js index 9f88b5c49..aca690ce0 100644 --- a/apps/widbatpc/widget.js +++ b/apps/widbatpc/widget.js @@ -110,7 +110,7 @@ Bangle.on('lcdPower', function(on) { WIDGETS["batpc"].draw(); // refresh once a minute if LCD on if (!batteryInterval) - batteryInterval = setInterval(draw, 60000); + batteryInterval = setInterval(()=>WIDGETS["batpc"].draw(), 60000); } else { if (batteryInterval) { clearInterval(batteryInterval); diff --git a/apps/widclk/ChangeLog b/apps/widclk/ChangeLog index 5370129cc..6fda78a08 100644 --- a/apps/widclk/ChangeLog +++ b/apps/widclk/ChangeLog @@ -1,2 +1,3 @@ 0.02: Now refresh battery monitor every minute if LCD on 0.03: Ensure redrawing works with variable size widget system +0.04: Fix regression stopping correct widget updates diff --git a/apps/widclk/widget.js b/apps/widclk/widget.js index 1d5df36b2..ff22bb4d1 100644 --- a/apps/widclk/widget.js +++ b/apps/widclk/widget.js @@ -14,7 +14,7 @@ } } function startTimers(){ - intervalRef = setInterval(draw, 60*1000); + intervalRef = setInterval(()=>WIDGETS["wdclk"].draw(), 60*1000); WIDGETS["wdclk"].draw(); } Bangle.on('lcdPower', (on) => { @@ -23,5 +23,5 @@ }); WIDGETS["wdclk"]={area:"tr",width:width,draw:draw}; - if (Bangle.isLCDOn) intervalRef = setInterval(draw, 60*1000); + if (Bangle.isLCDOn) intervalRef = setInterval(()=>WIDGETS["wdclk"].draw(), 60*1000); })() From 9ccba226940906b154ac6c529d15912a6136dec4 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Wed, 15 Apr 2020 14:31:45 +0100 Subject: [PATCH 079/302] RAM widget --- apps.json | 12 ++++++++++++ apps/widram/ChangeLog | 1 + apps/widram/widget.js | 23 +++++++++++++++++++++++ apps/widram/widget.png | Bin 0 -> 403 bytes 4 files changed, 36 insertions(+) create mode 100644 apps/widram/ChangeLog create mode 100644 apps/widram/widget.js create mode 100644 apps/widram/widget.png diff --git a/apps.json b/apps.json index 0a65897b5..7449a233b 100644 --- a/apps.json +++ b/apps.json @@ -363,6 +363,18 @@ {"name":"widbt.wid.js","url":"widget.js"} ] }, + { "id": "widram", + "name": "RAM Widget", + "shortName":"RAM Widget", + "icon": "widget.png", + "version":"0.01", + "description": "Display your Bangle's available RAM percentage in a widget", + "tags": "widget", + "type": "widget", + "storage": [ + {"name":"widram.wid.js","url":"widget.js"} + ] + }, { "id": "hrm", "name": "Heart Rate Monitor", "icon": "heartrate.png", diff --git a/apps/widram/ChangeLog b/apps/widram/ChangeLog new file mode 100644 index 000000000..4c21f3ace --- /dev/null +++ b/apps/widram/ChangeLog @@ -0,0 +1 @@ +0.01: New Widget! diff --git a/apps/widram/widget.js b/apps/widram/widget.js new file mode 100644 index 000000000..08710b726 --- /dev/null +++ b/apps/widram/widget.js @@ -0,0 +1,23 @@ +(() => { + function draw() { + g.reset(); + var m = process.memory(); + var pc = Math.round(m.usage*100/m.total); + g.drawImage(atob("BwgBqgP////AVQ=="), this.x+(24-7)/2, this.y+4); + g.setColor(pc>70 ? "#ff0000" : (pc>50 ? "#ffff00" : "#ffffff")); + g.setFont("6x8").setFontAlign(0,0).drawString(pc+"%", this.x+12, this.y+20, true/*solid*/); + } + var ramInterval; + Bangle.on('lcdPower', function(on) { + if (on) { + WIDGETS["ram"].draw(); + if (!ramInterval) ramInterval = setInterval(()=>WIDGETS["ram"].draw(), 10000); + } else { + if (ramInterval) { + clearInterval(ramInterval); + ramInterval = undefined; + } + } + }); + WIDGETS["ram"]={area:"tl",width: 24,draw:draw}; +})() diff --git a/apps/widram/widget.png b/apps/widram/widget.png new file mode 100644 index 0000000000000000000000000000000000000000..c1cbf2e1aea7e91968c357811e99f33cc62ffacb GIT binary patch literal 403 zcmV;E0c`$>P)1O4+bVCO4?^37mlMvtzIpiNAgg@-GVfPF0=OmtT$2E< zNdVWZUt-&r@W1CT^L_;+fNSO*u>BR*mG>)<8(`cSV%!-P?x&RQ5M#Hj`+diHjQ+*s z>S6)_t!TfpXJl7kR@qa^_of?}0CeM`l0pb0-82C>pdNr%Ck6e^R5AB-#{{^$96YGC x#WsUaQP1r(1K~`8TesKePp*p*LI@#B@d1JOVd<$MIgtPW002ovPDHLkV1j3etDpb? literal 0 HcmV?d00001 From af427a3105d54a30dd3921e43dbcf2c562e8870a Mon Sep 17 00:00:00 2001 From: ps-igel <60899838+ps-igel@users.noreply.github.com> Date: Wed, 15 Apr 2020 21:36:29 +0200 Subject: [PATCH 080/302] update v0.03 --- apps/numerals/ChangeLog | 1 + apps/numerals/numerals-default.json | 5 +++ apps/numerals/numerals-icon.js | 2 +- apps/numerals/numerals.app.js | 50 +++++++++++++++-------------- apps/numerals/numerals.settings.js | 14 ++++++-- 5 files changed, 44 insertions(+), 28 deletions(-) create mode 100644 apps/numerals/numerals-default.json diff --git a/apps/numerals/ChangeLog b/apps/numerals/ChangeLog index a8396e26b..ec465a83f 100644 --- a/apps/numerals/ChangeLog +++ b/apps/numerals/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App! 0.02: Use BTN2 for settings menu like other clocks +0.03: maximize numerals, make menu button configurable, change icon to mac palette, add default settings file, respect 12hour setting \ No newline at end of file diff --git a/apps/numerals/numerals-default.json b/apps/numerals/numerals-default.json new file mode 100644 index 000000000..aa6a25047 --- /dev/null +++ b/apps/numerals/numerals-default.json @@ -0,0 +1,5 @@ +{ + color:0, + drawMode:"fill", + menuButton:22 +} \ No newline at end of file diff --git a/apps/numerals/numerals-icon.js b/apps/numerals/numerals-icon.js index 7e471fb0d..7ef609874 100644 --- a/apps/numerals/numerals-icon.js +++ b/apps/numerals/numerals-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mEwwhC/ABMBzIADyAJIAAkQBoMZBIoXCBIwADyIkIGAIuKGAQkIBJIwEEKQANC/4XWR58RiIHFWpAXFe4QRFcpAXFewQRFcxAXEFwQwGA4QXKiAXDGAgX/C/4X/C/4X/C7uQCwcBBwYXNBwYuEC54wCFwgXPzMRiIHFC54AHC/4XiCAoXRhIHDyK3GAAwOBJA0QG45VGC4YwCD4YwKFwgABcgIfEAwIAHBwgA/AAgA==")) \ No newline at end of file +require("heatshrink").decompress(atob("mEwwhC/ABXdAAfQBJAAEBgUNBJ4mGBKAmFEhAuLEwQhSABoX/C6yPPYw61IB4r3DHxoIFCwQIHC5YuDCIo3HC4oWEBI4X/C/4X/C/4X/C7XQC4gOEC5gwEBA4XLGAYOFC5oPCA44XNAA4X/C8SAGC6q4CCxb4EG5guICAgfIFxQA/ADg")) \ No newline at end of file diff --git a/apps/numerals/numerals.app.js b/apps/numerals/numerals.app.js index fbfe5b9ed..b24e8bc5e 100644 --- a/apps/numerals/numerals.app.js +++ b/apps/numerals/numerals.app.js @@ -7,57 +7,59 @@ */ var numerals = { - 0:[[9,1,82,1,90,9,90,82,82,90,9,90,1,82,1,9,9,1],[30,21,61,21,69,29,69,61,61,69,30,69,22,61,22,29,30,21]], - 1:[[59,1,82,1,90,9,90,82,82,90,73,90,65,82,65,27,59,27,51,19,51,9,59,1]], - 2:[[9,1,82,1,90,9,90,47,82,55,21,55,21,64,82,64,90,72,90,82,82,90,9,90,1,82,1,43,9,35,70,35,70,25,9,25,1,17,1,9,9,1]], - 3:[[9,1,82,1,90,9,90,82,82,90,9,90,1,82,1,74,9,66,70,66,70,57,9,57,1,49,1,41,9,33,70,33,70,25,9,25,1,17,1,9,9,1]], - 4:[[9,1,14,1,22,9,22,34,69,34,69,9,77,1,82,1,90,9,90,82,82,90,78,90,70,82,70,55,9,55,1,47,1,9,9,1]], - 5:[[9,1,82,1,90,9,90,17,82,25,21,25,21,35,82,35,90,43,90,82,82,90,9,90,1,82,1,72,9,64,71,64,71,55,9,55,1,47,1,9,9,1]], - 6:[[9,1,82,1,90,9,90,14,82,22,22,22,22,36,82,36,90,44,90,82,82,90,9,90,1,82,1,9,9,1],[22,55,69,55,69,69,22,69,22,55]], - 7:[[9,1,82,1,90,9,90,15,15,90,9,90,1,82,1,76,54,23,9,23,1,15,1,9,9,1]], - 8:[[9,1,82,1,90,9,90,82,82,90,9,90,1,82,1,9,9,1],[22,22,69,22,69,36,22,36,22,22],[22,55,69,55,69,69,22,69,22,55]], - 9:[[9,1,82,1,90,9,90,82,82,90,9,90,1,82,1,77,9,69,69,69,69,55,9,55,1,47,1,9,9,1],[22,22,69,22,69,36,22,36,22,22]], + 0:[[9,1,82,1,90,9,90,92,82,100,9,100,1,92,1,9],[30,25,61,25,69,33,69,67,61,75,30,75,22,67,22,33]], + 1:[[59,1,82,1,90,9,90,92,82,100,73,100,65,92,65,27,59,27,51,19,51,9]], + 2:[[9,1,82,1,90,9,90,53,82,61,21,61,21,74,82,74,90,82,90,92,82,100,9,100,1,92,1,48,9,40,70,40,70,27,9,27,1,19,1,9]], + 3:[[9,1,82,1,90,9,90,92,82,100,9,100,1,92,1,82,9,74,70,74,70,61,9,61,1,53,1,48,9,40,70,40,70,27,9,27,1,19,1,9]], + 4:[[9,1,14,1,22,9,22,36,69,36,69,9,77,1,82,1,90,9,90,92,82,100,78,100,70,92,70,61,9,61,1,53,1,9]], + 5:[[9,1,82,1,90,9,90,19,82,27,21,27,21,40,82,40,90,48,90,92,82,100,9,100,1,92,1,82,9,74,71,74,71,61,9,61,1,53,1,9]], + 6:[[9,1,82,1,90,9,90,19,82,27,22,27,22,40,82,40,90,48,90,92,82,100,9,100,1,92,1,9],[22,60,69,60,69,74,22,74]], + 7:[[9,1,82,1,90,9,90,15,20,98,9,98,1,90,1,86,56,22,9,22,1,14,1,9]], + 8:[[9,1,82,1,90,9,90,92,82,100,9,100,1,92,1,9],[22,27,69,27,69,43,22,43],[22,58,69,58,69,74,22,74]], + 9:[[9,1,82,1,90,9,90,92,82,100,9,100,1,92,1,82,9,74,69,74,69,61,9,61,1,53,1,9],[22,27,69,27,69,41,22,41]], }; +var _12hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"]||false; var _hCol = ["#ff5555","#ffff00","#FF9901","#2F00FF"]; var _mCol = ["#55ff55","#ffffff","#00EFEF","#FFBF00"]; var _rCol = 0; var interval = 0; const REFRESH_RATE = 10E3; -function translate(tx, ty, p) { +function translate(tx, ty, p){ return p.map((x, i)=> x+((i%2)?ty:tx)); } function fill(poly){ - return g.fillPoly(poly); + return g.fillPoly(poly,true); } function frame(poly){ - return g.drawPoly(poly); + return g.drawPoly(poly,true); } let settings = require('Storage').readJSON('numerals.json',1); if (!settings) { settings = { - color: 0, - drawMode: "fill" + color:0, + drawMode:"fill", + menuButton:24 }; } function drawNum(num,col,x,y,func){ g.setColor(col); - let tx = x*100+35; - let ty = y*100+35; + let tx = x*100+25; + let ty = y*104+32; for (let i=0;i0) g.setColor((func==fill)?"#000000":col); - func(translate(tx, ty,numerals[num][i])); + func(translate(tx,ty,numerals[num][i])); } } function draw(drawMode){ let d = new Date(); - let h1 = Math.floor(d.getHours()/10); - let h2 = d.getHours()%10; + let h1 = Math.floor((_12hour?d.getHours()%12:d.getHours())/10); + let h2 = (_12hour?d.getHours()%12:d.getHours())%10; let m1 = Math.floor(d.getMinutes()/10); let m2 = d.getMinutes()%10; g.clearRect(0,24,240,240); @@ -70,7 +72,7 @@ function draw(drawMode){ Bangle.setLCDMode(); clearWatch(); -setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"}); +setWatch(Bangle.showLauncher, settings.menuButton, {repeat:false,edge:"falling"}); g.clear(); clearInterval(); @@ -78,8 +80,8 @@ if (settings.color>0) _rCol=settings.color-1; interval=setInterval(draw, REFRESH_RATE, settings.drawMode); draw(settings.drawMode); -Bangle.on('lcdPower', function(on) { - if (on) { +Bangle.on('lcdPower', function(on){ + if (on){ if (settings.color==0) _rCol = Math.floor(Math.random()*_hCol.length); draw(settings.drawMode); interval=setInterval(draw, REFRESH_RATE, settings.drawMode); @@ -90,4 +92,4 @@ Bangle.on('lcdPower', function(on) { }); Bangle.loadWidgets(); -Bangle.drawWidgets(); +Bangle.drawWidgets(); \ No newline at end of file diff --git a/apps/numerals/numerals.settings.js b/apps/numerals/numerals.settings.js index f9c417da6..2d388525c 100644 --- a/apps/numerals/numerals.settings.js +++ b/apps/numerals/numerals.settings.js @@ -4,15 +4,17 @@ }; function resetSettings() { numeralsSettings = { - color: 0, - drawMode: "fill" + color:0, + drawMode:"fill", + menuButton:22 }; updateSettings(); } let numeralsSettings = storage.readJSON('numerals.json',1); if (!numeralsSettings) resetSettings(); let dm = ["fill","frame"]; - let col = ["rnd","r/g","y/w","o/c","b/y"] + let col = ["rnd","r/g","y/w","o/c","b/y"]; + let btn = [[24,"BTN1"],[22,"BTN2"],[23,"BTN3"],[11,"BTN4"],[16,"BTN5"]]; var menu={ "" : { "title":"Numerals"}, "Colors": { @@ -27,6 +29,12 @@ format: v=>dm[v], onchange: v=> { numeralsSettings.drawMode=dm[v]; updateSettings();} }, + "Menu button": { + value: 1|btn[numeralsSettings.menuButton], + min:0,max:4, + format: v=>btn[v][1], + onchange: v=> { numeralsSettings.menuButton=btn[v][0]; updateSettings();} + }, "< back": back }; E.showMenu(menu); From b08715cad4ca3531b6d82344923685faa37bf20b Mon Sep 17 00:00:00 2001 From: ps-igel <60899838+ps-igel@users.noreply.github.com> Date: Wed, 15 Apr 2020 21:39:29 +0200 Subject: [PATCH 081/302] update numerals app v0.03 --- apps.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps.json b/apps.json index 7449a233b..6327a653c 100644 --- a/apps.json +++ b/apps.json @@ -1205,7 +1205,7 @@ "name": "Numerals Clock", "shortName": "Numerals Clock", "icon": "numerals.png", - "version":"0.02", + "version":"0.03", "description": "A simple big numerals clock", "tags": "numerals,clock", "type":"clock", @@ -1213,7 +1213,8 @@ "storage": [ {"name":"numerals.app.js","url":"numerals.app.js"}, {"name":"numerals.img","url":"numerals-icon.js","evaluate":true}, - {"name":"numerals.settings.js","url":"numerals.settings.js"} + {"name":"numerals.settings.js","url":"numerals.settings.js"}, + {"name":"numerals.json","url":"numerals-default.json","evaluate":true} ] }, { "id": "bledetect", From 7e300068b1598e8f8b7da702ba6e05de52c6887f Mon Sep 17 00:00:00 2001 From: ps-igel <60899838+ps-igel@users.noreply.github.com> Date: Wed, 15 Apr 2020 21:43:03 +0200 Subject: [PATCH 082/302] update v0.03 --- apps/numerals/README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/numerals/README.md b/apps/numerals/README.md index 01d784ef8..52e84c76d 100644 --- a/apps/numerals/README.md +++ b/apps/numerals/README.md @@ -5,13 +5,16 @@ Settings can be accessed through the app/widget settings menu of the Bangle.js ## Settings available -### color: +### Color: * rnd - shows numerals in different color combinations every time the watches wakes * r/g - red/green * y/w - yellow/white * o/c - orange/cyan * b/y - blue/yellow'ish -### draw mode +### Draw mode * fill - fill numerals * frame - only shows outline of numerals + +### Menu button +* choose button to start launcher menu with \ No newline at end of file From 0ef33e12dce47fe91106b7b03b5bc7880dc68c33 Mon Sep 17 00:00:00 2001 From: Purple-Tentacle <59914607+Purple-Tentacle@users.noreply.github.com> Date: Thu, 16 Apr 2020 08:21:27 +0200 Subject: [PATCH 083/302] Distance calc and display --- apps/activepedom/ChangeLog | 3 +- apps/activepedom/README.md | 23 +++++++++++--- apps/activepedom/settings.js | 33 +++++++++++++++++++- apps/activepedom/widget.js | 58 +++++++++++++++++++++++++----------- 4 files changed, 93 insertions(+), 24 deletions(-) diff --git a/apps/activepedom/ChangeLog b/apps/activepedom/ChangeLog index 4c21f3ace..fb0bc78e5 100644 --- a/apps/activepedom/ChangeLog +++ b/apps/activepedom/ChangeLog @@ -1 +1,2 @@ -0.01: New Widget! +0.01: New Widget! +0.02: Distance calculation and display \ No newline at end of file diff --git a/apps/activepedom/README.md b/apps/activepedom/README.md index 8a10727cd..055a91f56 100644 --- a/apps/activepedom/README.md +++ b/apps/activepedom/README.md @@ -1,4 +1,4 @@ -# Improved pedometer +# Active Pedometer Pedometer that filters out arm movement and displays a step goal progress. I changed the step counting algorithm completely. @@ -19,8 +19,9 @@ When you reach the step threshold, the steps needed to reach the threshold are c ## Features * Two line display +* Can display distance (in km) or steps in each line * Large number for good readability -* Small number with the exact steps counted +* Small number with the exact steps counted or more exact distance * Large number is displayed in green when status is 'active' * Progress bar for step goal * Counts steps only if they are reached in a certain time @@ -29,9 +30,23 @@ When you reach the step threshold, the steps needed to reach the threshold are c * Steps are saved to a file and read-in at start (to not lose step progress) * Settings can be changed in Settings - App/widget settings - Active Pedometer -## Development version +## Settings -* https://github.com/Purple-Tentacle/BangleAppsDev/tree/master/apps/pedometer +* Max time (ms): Maximum time between two steps in milliseconds, steps will not be counted if exceeded. Standard: 1100 +* Min time (ms): Minimum time between two steps in milliseconds, steps will not be counted if fallen below. Standard: 240 +* Step threshold: How many steps are needed to reach 'active' mode. If you do not reach the threshold in the 'Active Reset' time, the steps are not counted. Standard: 30 +* Act.Res. (ms): Active Reset. After how many miliseconds will the 'active mode' reset. You have to reach the step threshold in this time, otherwise the steps are not counted. Standard: 30000 +* Step sens.: Step Sensitivity. How sensitive should the sted detection be? This changes sensitivity in step detection in the firmware. Standard in firmware: 80 +* Step goal: This is your daily step goal. Standard: 10000 +* Step length: Length of one step in cm. Standard: 75 +* Line One: What to display in line one, steps or distance. Standard: steps +* Line Two: What to display in line two, steps or distance. Standard: distance + +## Releases + +* Offifical app loader: https://github.com/espruino/BangleApps/tree/master/apps/activepedom (https://banglejs.com/apps) +* Forked app loader: https://github.com/Purple-Tentacle/BangleApps/tree/master/apps/activepedom (https://purple-tentacle.github.io/BangleApps/#widget) +* Development: https://github.com/Purple-Tentacle/BangleAppsDev/tree/master/apps/pedometer ## Requests diff --git a/apps/activepedom/settings.js b/apps/activepedom/settings.js index 43764a164..94ae435d2 100644 --- a/apps/activepedom/settings.js +++ b/apps/activepedom/settings.js @@ -4,6 +4,7 @@ */ (function(back) { const SETTINGS_FILE = 'activepedom.settings.json'; + const LINES = ['Steps', 'Distance']; // initialize with default settings... let s = { @@ -13,6 +14,9 @@ 'intervalResetActive' : 30000, 'stepSensitivity' : 80, 'stepGoal' : 10000, + 'stepLength' : 75, + 'lineOne': LINES[0], + 'lineTwo': LINES[1], }; // ...and overwrite them with any saved values // This way saved values are preserved if a new version adds more settings @@ -27,7 +31,7 @@ return function (value) { s[key] = value; storage.write(SETTINGS_FILE, s); - WIDGETS["activepedom"].draw(); + //WIDGETS["activepedom"].draw(); }; } @@ -76,6 +80,33 @@ step: 1000, onchange: save('stepGoal'), }, + 'Step length (cm)': { + value: s.stepLength, + min: 1, + max: 150, + step: 1, + onchange: save('stepLength'), + }, + 'Line One': { + format: () => s.lineOne, + onchange: function () { + // cycles through options + const oldIndex = LINES.indexOf(s.lineOne) + const newIndex = (oldIndex + 1) % LINES.length + s.lineOne = LINES[newIndex] + save('lineOne')(s.lineOne) + }, + }, + 'Line Two': { + format: () => s.lineTwo, + onchange: function () { + // cycles through options + const oldIndex = LINES.indexOf(s.lineTwo) + const newIndex = (oldIndex + 1) % LINES.length + s.lineTwo = LINES[newIndex] + save('lineTwo')(s.lineTwo) + }, + }, }; E.showMenu(menu); }); \ No newline at end of file diff --git a/apps/activepedom/widget.js b/apps/activepedom/widget.js index 0c8b2438d..d569716ec 100644 --- a/apps/activepedom/widget.js +++ b/apps/activepedom/widget.js @@ -8,22 +8,16 @@ var active = 0; //x steps in y seconds achieved var stepGoalPercent = 0; //percentage of step goal var stepGoalBarLength = 0; //length og progress bar - var lastUpdate = new Date(); - var width = 45; + var lastUpdate = new Date(); //used to reset counted steps on new day + var width = 45; //width of widget - var stepsTooShort = 0; + //used for statistics and debugging + var stepsTooShort = 0; var stepsTooLong = 0; var stepsOutsideTime = 0; - //define default settings - const DEFAULTS = { - 'cMaxTime' : 1100, - 'cMinTime' : 240, - 'stepThreshold' : 30, - 'intervalResetActive' : 30000, - 'stepSensitivity' : 80, - 'stepGoal' : 10000, - }; + var distance = 0; //distance travelled + const SETTINGS_FILE = 'activepedom.settings.json'; const PEDOMFILE = "activepedom.steps.json"; @@ -32,10 +26,21 @@ function loadSettings() { settings = require('Storage').readJSON(SETTINGS_FILE, 1) || {}; } + //return setting function setting(key) { - if (!settings) { loadSettings(); } - return (key in settings) ? settings[key] : DEFAULTS[key]; + //define default settings + const DEFAULTS = { + 'cMaxTime' : 1100, + 'cMinTime' : 240, + 'stepThreshold' : 30, + 'intervalResetActive' : 30000, + 'stepSensitivity' : 80, + 'stepGoal' : 10000, + 'stepLength' : 75, + }; + if (!settings) { loadSettings(); } + return (key in settings) ? settings[key] : DEFAULTS[key]; } function setStepSensitivity(s) { @@ -46,7 +51,7 @@ } //format number to make them shorter - function kFormatter(num) { + function kFormatterSteps(num) { if (num <= 999) return num; //smaller 1.000, return 600 as 600 if (num >= 1000 && num < 10000) { //between 1.000 and 10.000 num = Math.floor(num/100)*100; @@ -99,11 +104,12 @@ else { stepsOutsideTime++; } + settings = 0; //reset settings to save memory } function draw() { var height = 23; //width is deined globally - var stepsDisplayLarge = kFormatter(stepsCounted); + distance = (stepsCounted * setting('stepLength')) / 100 /1000 //distance in km //Check if same day let date = new Date(); @@ -121,10 +127,21 @@ if (active == 1) g.setColor(0x07E0); //green else g.setColor(0xFFFF); //white g.setFont("6x8", 2); - g.drawString(stepsDisplayLarge,this.x+1,this.y); //first line, big number + + if (setting('lineOne') == 'Steps') { + g.drawString(kFormatterSteps(stepsCounted),this.x+1,this.y); //first line, big number, steps + } + if (setting('lineOne') == 'Distance') { + g.drawString(distance.toFixed(2),this.x+1,this.y); //first line, big number, distance + } g.setFont("6x8", 1); g.setColor(0xFFFF); //white - g.drawString(stepsCounted,this.x+1,this.y+14); //second line, small number + if (setting('lineTwo') == 'Steps') { + g.drawString(stepsCounted,this.x+1,this.y+14); //second line, small number, steps + } + if (setting('lineTwo') == 'Distance') { + g.drawString(distance.toFixed(3) + "km",this.x+1,this.y+14); //second line, small number, distance + } //draw step goal bar stepGoalPercent = (stepsCounted / setting('stepGoal')) * 100; @@ -136,6 +153,8 @@ g.fillRect(this.x, this.y+height, this.x+1, this.y+height-1); //draw start of bar g.fillRect(this.x+width, this.y+height, this.x+width-1, this.y+height-1); //draw end of bar g.fillRect(this.x, this.y+height, this.x+stepGoalBarLength, this.y+height); // draw progress bar + + settings = 0; //reset settings to save memory } //This event is called just before the device shuts down for commands such as reset(), load(), save(), E.reboot() or Bangle.off() @@ -164,6 +183,7 @@ //Read data from file and set variables let pedomData = require("Storage").readJSON(PEDOMFILE,1); + if (pedomData) { if (pedomData.lastUpdate) lastUpdate = new Date(pedomData.lastUpdate); stepsCounted = pedomData.stepsToday|0; @@ -172,6 +192,8 @@ stepsOutsideTime = pedomData.stepsOutsideTime; } + pedomdata = 0; //reset pedomdata to save memory + setStepSensitivity(setting('stepSensitivity')); //set step sensitivity (80 is standard, 400 is muss less sensitive) //Add widget From 505730cd50cb9ab08b2100bd6064399c907be3a3 Mon Sep 17 00:00:00 2001 From: Purple-Tentacle <59914607+Purple-Tentacle@users.noreply.github.com> Date: Thu, 16 Apr 2020 08:23:38 +0200 Subject: [PATCH 084/302] active pedometer: added readme --- apps.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apps.json b/apps.json index 3dbccb9b1..ec211f060 100644 --- a/apps.json +++ b/apps.json @@ -1116,6 +1116,7 @@ "description": "Pedometer that filters out arm movement and displays a step goal progress.", "tags": "outdoors,widget", "type":"widget", + "readme": "README.md", "storage": [ {"name":"activepedom.wid.js","url":"widget.js"}, {"name":"activepedom.settings.js","url":"settings.js"}, From d74eb43fb6477e5958dee98c623cb43dc3213e3d Mon Sep 17 00:00:00 2001 From: Purple-Tentacle <59914607+Purple-Tentacle@users.noreply.github.com> Date: Thu, 16 Apr 2020 08:27:13 +0200 Subject: [PATCH 085/302] active pedometer: version 0.02 --- apps.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps.json b/apps.json index ec211f060..ab8af0f37 100644 --- a/apps.json +++ b/apps.json @@ -1112,7 +1112,7 @@ "name": "Active Pedometer", "shortName":"Active Pedometer", "icon": "app.png", - "version":"0.01", + "version":"0.02", "description": "Pedometer that filters out arm movement and displays a step goal progress.", "tags": "outdoors,widget", "type":"widget", From 078a3e6d5f31363a7ab7042859870f02cf3a1126 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Thu, 16 Apr 2020 08:16:27 +0100 Subject: [PATCH 086/302] change default temperature for israel as per #248 --- apps/locale/locales.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/locale/locales.js b/apps/locale/locales.js index ebf28e53d..508c92015 100644 --- a/apps/locale/locales.js +++ b/apps/locale/locales.js @@ -220,7 +220,7 @@ var locales = { int_curr_symbol: "ILS", speed: "kmh", distance: { 0: "m", 1: "km" }, - temperature: "°F", + temperature: "°C", ampm: { 0: "am", 1: "pm" }, timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" }, datePattern: { 0: "%A, %B %d, %Y", "1": "%d/%m/%Y" }, // Sunday, 1 March 2020 // 01/03/2020 From 944693eaf966fe4b54cbfeaef5148f5b1efc444d Mon Sep 17 00:00:00 2001 From: Purple-Tentacle <59914607+Purple-Tentacle@users.noreply.github.com> Date: Thu, 16 Apr 2020 10:06:44 +0200 Subject: [PATCH 087/302] Chronowid initial --- apps/chronowid/ChangeLog | 1 + 1 file changed, 1 insertion(+) create mode 100644 apps/chronowid/ChangeLog diff --git a/apps/chronowid/ChangeLog b/apps/chronowid/ChangeLog new file mode 100644 index 000000000..a6f342f01 --- /dev/null +++ b/apps/chronowid/ChangeLog @@ -0,0 +1 @@ +0.01: New widget and app! From 1a935951b03efdeca39d61da79302ff83c73ea18 Mon Sep 17 00:00:00 2001 From: Purple-Tentacle <59914607+Purple-Tentacle@users.noreply.github.com> Date: Thu, 16 Apr 2020 10:07:39 +0200 Subject: [PATCH 088/302] Chronowid initial --- apps/chronowid/README.md | 35 ++++++++++++++ apps/chronowid/app-icon.js | 1 + apps/chronowid/app.js | 91 ++++++++++++++++++++++++++++++++++++ apps/chronowid/app.png | Bin 0 -> 1060 bytes apps/chronowid/widget.js | 93 +++++++++++++++++++++++++++++++++++++ 5 files changed, 220 insertions(+) create mode 100644 apps/chronowid/README.md create mode 100644 apps/chronowid/app-icon.js create mode 100644 apps/chronowid/app.js create mode 100644 apps/chronowid/app.png create mode 100644 apps/chronowid/widget.js diff --git a/apps/chronowid/README.md b/apps/chronowid/README.md new file mode 100644 index 000000000..b6325d94f --- /dev/null +++ b/apps/chronowid/README.md @@ -0,0 +1,35 @@ +# Chronometer Widget + +Chronometer (timer) that runs as a widget. +The advantage is, that you can still see your normal watchface and other widgets when the timer is running. +The widget is always active, but only shown when the timer is on. +Hours, minutes, seconds and timer status can be set with an app. + +## Screenshots + +TBD + +## Features + +* Using other apps does not interrupt the timer, no need to keep the widget open (BUT: there will be no buzz when the time is up, for that the widget has to be loaded) +* Target time is saved to a file and timer picks up again when widget is loaded again. + +## Settings + +There are no settings section in the settings app, timer can be set using an app. + +* Hours: Set the hours for the timer +* Minutes: Set the minutes for the timer +* Seconds: Set the seconds for the timer +* Timer on: Starts the timer and displays the widget when set to 'On'. You have to leave the app. The widget is always there, but only visible when timer is on. + + +## Releases + +* Offifical app loader: Not yet published. +* Forked app loader: Not yet published. +* Development: https://github.com/Purple-Tentacle/BangleAppsDev/tree/master/apps/chronowid + +## Requests + +If you have any feature requests, please contact me on the Espruino forum: http://forum.espruino.com/profiles/155005/ \ No newline at end of file diff --git a/apps/chronowid/app-icon.js b/apps/chronowid/app-icon.js new file mode 100644 index 000000000..db2010218 --- /dev/null +++ b/apps/chronowid/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwIFCn/8BYYFRABcD4AFFgIFCh/wgeAAoP//8HCYMDAoPD8EAg4FB8PwgEf+EP/H4HQOAgP8uEAvwfBv0ggBFCn4CB/EBwEfgEB+AFBh+AgfgAoI1BIoQJB4AHBAoXgg4uBAIIFCCYQFGh5rDJQJUBK4IFCNYIFVDoopDGoJiBHYYFKVYRZBWIYDBA4IFBNIQzBG4IbBToKkBAQKVFUIYICVoQUCXIQmCYoIsCaITqDAoLvDNYUAA=")) \ No newline at end of file diff --git a/apps/chronowid/app.js b/apps/chronowid/app.js new file mode 100644 index 000000000..cb99268af --- /dev/null +++ b/apps/chronowid/app.js @@ -0,0 +1,91 @@ +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +const storage = require('Storage'); +const boolFormat = v => v ? "On" : "Off"; +let settingsChronowid; + +function updateSettings() { + var now = new Date(); + const goal = new Date(now.getFullYear(), now.getMonth(), now.getDate(), + now.getHours() + settingsChronowid.hours, now.getMinutes() + settingsChronowid.minutes, now.getSeconds() + settingsChronowid.seconds); + settingsChronowid.goal = goal.getTime(); + storage.writeJSON('chronowid.json', settingsChronowid); +} + +function resetSettings() { + settingsChronowid = { + hours : 0, + minutes : 0, + seconds : 0, + started : false, + counter : 0, + goal : 0, + }; + updateSettings(); +} + +settingsChronowid = storage.readJSON('chronowid.json',1); +if (!settingsChronowid.started) resetSettings(); + +E.on('kill', () => { + print("-KILL-"); + updateSettings(); +}); + +function showMenu() { + const timerMenu = { + '': { + 'title': 'Set timer', + 'predraw': function() { + timerMenu.hours.value = settingsChronowid.hours; + timerMenu.minutes.value = settingsChronowid.minutes; + timerMenu.seconds.value = settingsChronowid.seconds; + timerMenu.started.value = settingsChronowid.started; + } + }, + 'Hours': { + value: settingsChronowid.hours, + min: 0, + max: 24, + step: 1, + onchange: v => { + settingsChronowid.hours = v; + updateSettings(); + } + }, + 'Minutes': { + value: settingsChronowid.minutes, + min: 0, + max: 59, + step: 1, + onchange: v => { + settingsChronowid.minutes = v; + updateSettings(); + } + }, + 'Seconds': { + value: settingsChronowid.seconds, + min: 0, + max: 59, + step: 1, + onchange: v => { + settingsChronowid.seconds = v; + updateSettings(); + } + }, + 'Timer on': { + value: settingsChronowid.started, + format: boolFormat, + onchange: v => { + settingsChronowid.started = v; + updateSettings(); + } + }, + }; + timerMenu['-Exit-'] = ()=>{load();}; + return E.showMenu(timerMenu); +} + +showMenu(); \ No newline at end of file diff --git a/apps/chronowid/app.png b/apps/chronowid/app.png new file mode 100644 index 0000000000000000000000000000000000000000..5ac7a480c5d4533c9851ba6264b74aae48035eb0 GIT binary patch literal 1060 zcmV+<1l#+GP)*2?CL* z=jdIsdaVqLAjyY*ycLl}Syc2;L97)RfoN7x+ost=DzrV^$!5>oot--~TQ7d$;EsFG z_k7Qtx%ZxXXA1}qX~XsfVFXm;x<_9I=$uURJ1ZseBV6R)F#Xg92 zC;;k!X6tX2rp*iB9q=U=45ym<6tfq%{NOz;R#zF~ zJ#TM22egDKJPw=zW_$>jHUpdnVmAI3OLSh6Xa!Os2$%L1*ptsBqQH3<0V&k;dH^*J zV!%>h8S0MJS>N^N=zw!Tp-U3u_?VrR0C%$lOrU0pB8}e`%Or|`Hefgl=b%lbJ&8}K z0d9mSa7Pnl3h2bW9NBGIHbj6fi%XY;{Z5Sg5f5-dL;ez|^x%8cvRtS_-ANgz&(_A* zqSmeY`$fNzK2BYIbgjB!6`wIzK6`b8=ix@{cYKKzS;Nui}lHqvFZQ(WIb6|yu`hkz&m1I8mjlf19JVWAIYSZF1nYXRZFlZI3veZ?Zx{Zz- z8U)9%7bjUM#@w4ba1E#UKX%2CD=z$#UYuryQ`9i1TdC|xfDJ{$-!T?-V<2r9M8a*K z9dr-w(56e^hqzvD804PIcY}spvhJGp;v`o<(?MK={xM1d>kPT%AWp~zx;ro;uD2Qn z9RX@lgZaBNBK9n5lVt>Xh&6?IE#n8Z zsI|x*yVnoVDxC9q0(E+jFT`XVq)WAoT2$kM2z58s3?hyzbG@mhlt>&`<*F<;=^ zP>ZwY)2MUOz$gT6YsjZjrw2i1rwDKu=QE!MZt?eMF)&KeCy_?gPVK0L$193a^SqoY z?cz&A(mszu+>h5Mfy=-_)QTBL?Ioht=LYM$0h}wWd~8DNc^%r&ZyAGHk`M;0SHw|0 z71k@JUh*@uTUMOHnq3)qg@)Rd!MF@c(8CV;o7*RA(a euUUTu4g4F8cKpwc4bA8P0000 { + const storage = require('Storage'); + settingsChronowid = storage.readJSON("chronowid.json",1)||{}; //read settingsChronowid from file + var height = 23; + var width = 58; + var interval = 0; //used for the 1 second interval timer + var now = new Date(); + + var time = 0; + var diff = settingsChronowid.goal - now; + + //Convert ms to time + function getTime(t) { + var milliseconds = parseInt((t % 1000) / 100), + seconds = Math.floor((t / 1000) % 60), + minutes = Math.floor((t / (1000 * 60)) % 60), + hours = Math.floor((t / (1000 * 60 * 60)) % 24); + + hours = (hours < 10) ? "0" + hours : hours; + minutes = (minutes < 10) ? "0" + minutes : minutes; + seconds = (seconds < 10) ? "0" + seconds : seconds; + + return hours + ":" + minutes + ":" + seconds; + } + + function printDebug() { + print ("Nowtime: " + getTime(now)); + print ("Now: " + now); + print ("Goaltime: " + getTime(settingsChronowid.goal)); + print ("Goal: " + settingsChronowid.goal); + print("Difftime: " + getTime(diff)); + print("Diff: " + diff); + print ("Started: " + settingsChronowid.started); + print ("----"); + } + + //counts down, calculates and displays + function countDown() { + //printDebug(); + now = new Date(); + diff = settingsChronowid.goal - now; //calculate difference + WIDGETS["chronowid"].draw(); + //time is up + if (settingsChronowid.started && diff <= 0) { + Bangle.buzz(1500); + //write timer off to file + settingsChronowid.started = false; + storage.writeJSON('chronowid.json', settingsChronowid); + clearInterval(interval); //stop interval + //printDebug(); + } + } + + // draw your widget + function draw() { + if (!settingsChronowid.started) { + width = 0; + return; //do not draw anything if timer is not started + } + g.reset(); + if (diff >= 0) { + if (diff < 600000) { //less than 1 hour left + width = 58; + g.clearRect(this.x,this.y,this.x+width,this.y+height); + g.setFont("6x8", 2); + g.drawString(getTime(diff).substring(3), this.x+1, this.y+5); //remove hour part 00:00:00 -> 00:00 + } + if (diff >= 600000) { //one hour or more left + width = 48; + g.clearRect(this.x,this.y,this.x+width,this.y+height); + g.setFont("6x8", 1); + g.drawString(getTime(diff), this.x+1, this.y+((height/2)-4)); //display hour 00:00:00 + } + } + else { + width = 58; + g.clearRect(this.x,this.y,this.x+width,this.y+height); + g.setFont("6x8", 2); + g.drawString("END", this.x+15, this.y+5); + } + } + + if (settingsChronowid.started) interval = setInterval(countDown, 1000); //start countdown each second + + // add the widget + WIDGETS["chronowid"]={area:"bl",width:width,draw:draw,reload:function() { + reload(); + Bangle.drawWidgets(); // relayout all widgets + }}; + + //printDebug(); + countDown(); +})(); \ No newline at end of file From 6fc326bb49ae2e004c6c210e84f68c3c76906764 Mon Sep 17 00:00:00 2001 From: Purple-Tentacle <59914607+Purple-Tentacle@users.noreply.github.com> Date: Thu, 16 Apr 2020 10:10:57 +0200 Subject: [PATCH 089/302] Chronowid initial --- apps.json | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/apps.json b/apps.json index ab8af0f37..0d6cdd214 100644 --- a/apps.json +++ b/apps.json @@ -1108,7 +1108,7 @@ {"name":"openstmap.img","url":"app-icon.js","evaluate":true} ] }, - { "id": "activepedom", + { "id": "activepedom", "name": "Active Pedometer", "shortName":"Active Pedometer", "icon": "app.png", @@ -1123,6 +1123,21 @@ {"name":"activepedom.img","url":"app-icon.js","evaluate":true} ] }, + { "id": "chronowidget", + "name": "Chrono Widget", + "shortName":"Chrono Widget", + "icon": "app.png", + "version":"0.01", + "description": "Chronometer (timer) which runs as widget.", + "tags": "tools,widget", + "type":"widget", + "readme": "README.md", + "storage": [ + {"name":"chronowidget.wid.js","url":"widget.js"}, + {"name":"chronowidget.app.js","url":"app.js"}, + {"name":"chronowidget.img","url":"app-icon.js","evaluate":true} + ] + }, { "id": "tabata", "name": "Tabata", "shortName": "Tabata - Control High-Intensity Interval Training", From e2657a62a592ab6e4b3018bfb8f8216d0ff3412b Mon Sep 17 00:00:00 2001 From: Purple-Tentacle <59914607+Purple-Tentacle@users.noreply.github.com> Date: Thu, 16 Apr 2020 10:13:23 +0200 Subject: [PATCH 090/302] chronowid typo --- apps.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps.json b/apps.json index 0d6cdd214..591ddad28 100644 --- a/apps.json +++ b/apps.json @@ -1123,7 +1123,7 @@ {"name":"activepedom.img","url":"app-icon.js","evaluate":true} ] }, - { "id": "chronowidget", + { "id": "chronowid", "name": "Chrono Widget", "shortName":"Chrono Widget", "icon": "app.png", @@ -1133,9 +1133,9 @@ "type":"widget", "readme": "README.md", "storage": [ - {"name":"chronowidget.wid.js","url":"widget.js"}, - {"name":"chronowidget.app.js","url":"app.js"}, - {"name":"chronowidget.img","url":"app-icon.js","evaluate":true} + {"name":"chronowid.wid.js","url":"widget.js"}, + {"name":"chronowid.app.js","url":"app.js"}, + {"name":"chronowid.img","url":"app-icon.js","evaluate":true} ] }, { "id": "tabata", From e0a3b9ae3e15274d77d1a4af5ad7c8c90b737316 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Thu, 16 Apr 2020 09:16:37 +0100 Subject: [PATCH 091/302] Launchers: Only store relevant app data (saves RAM when many apps) --- apps.json | 4 ++-- apps/launch/ChangeLog | 2 ++ apps/launch/app.js | 2 +- apps/toucher/ChangeLog | 3 ++- apps/toucher/app.js | 8 ++++---- 5 files changed, 11 insertions(+), 8 deletions(-) create mode 100644 apps/launch/ChangeLog diff --git a/apps.json b/apps.json index d3dd95b26..167df46f6 100644 --- a/apps.json +++ b/apps.json @@ -41,7 +41,7 @@ "name": "Default Launcher", "shortName":"Launcher", "icon": "app.png", - "version":"0.01", + "version":"0.02", "description": "This is needed by Bangle.js to display a menu allowing you to choose your own applications. You can replace this with a customised launcher.", "tags": "tool,system,launcher", "type":"launch", @@ -1050,7 +1050,7 @@ "name": "Touch Launcher", "shortName":"Menu", "icon": "app.png", - "version":"0.05", + "version":"0.06", "description": "Touch enable left to right launcher.", "tags": "tool,system,launcher", "type":"launch", diff --git a/apps/launch/ChangeLog b/apps/launch/ChangeLog new file mode 100644 index 000000000..9e4a1eaf3 --- /dev/null +++ b/apps/launch/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! +0.02: Only store relevant app data (saves RAM when many apps) diff --git a/apps/launch/app.js b/apps/launch/app.js index 682122f82..a256b6909 100644 --- a/apps/launch/app.js +++ b/apps/launch/app.js @@ -1,5 +1,5 @@ var s = require("Storage"); -var apps = s.list(/\.info$/).map(app=>s.readJSON(app,1)||{name:"DEAD: "+app.substr(1)}).filter(app=>app.type=="app" || app.type=="clock" || !app.type); +var apps = s.list(/\.info$/).map(app=>{var a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src}}).filter(app=>app && (app.type=="app" || app.type=="clock" || !app.type)); apps.sort((a,b)=>{ var n=(0|a.sortorder)-(0|b.sortorder); if (n) return n; // do sortorder first diff --git a/apps/toucher/ChangeLog b/apps/toucher/ChangeLog index a2a709ee3..0c97d9e13 100644 --- a/apps/toucher/ChangeLog +++ b/apps/toucher/ChangeLog @@ -2,4 +2,5 @@ 0.02: Add swipe support and doucle tap to run application 0.03: Close launcher when lcd turn off 0.04: Complete rewrite to add animation and loop ( issue #210 ) -0.05: Improve perf \ No newline at end of file +0.05: Improve perf +0.06: Only store relevant app data (saves RAM when many apps) diff --git a/apps/toucher/app.js b/apps/toucher/app.js index b67e5b26c..cf7d5333b 100644 --- a/apps/toucher/app.js +++ b/apps/toucher/app.js @@ -5,8 +5,8 @@ g.flip(); const Storage = require("Storage"); function getApps(){ - return Storage.list(/\.info$/).filter(app => app.endsWith('.info')).map(app => Storage.readJSON(app,1) || { name: "DEAD: "+app.substr(1) }) - .filter(app=>app.type=="app" || app.type=="clock" || !app.type) + return Storage.list(/\.info$/).map(app=>{var a=Storage.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src,version:a.version}}) + .filter(app=>app && (app.type=="app" || app.type=="clock" || !app.type)) .sort((a,b)=>{ var n=(0|a.sortorder)-(0|b.sortorder); if (n) return n; // do sortorder first @@ -19,7 +19,7 @@ function getApps(){ const HEIGHT = g.getHeight(); const WIDTH = g.getWidth(); const HALF = WIDTH/2; -const ANIMATION_FRAME = 4; +const ANIMATION_FRAME = 4; const ANIMATION_STEP = HALF / ANIMATION_FRAME; function getPosition(index){ @@ -192,4 +192,4 @@ Bangle.on('swipe', dir => { // close launcher when lcd is off Bangle.on('lcdPower', on => { if(!on) return load(); -}); \ No newline at end of file +}); From 8c2decfa7b1d8a2d059129622207bbe810645d6a Mon Sep 17 00:00:00 2001 From: Purple-Tentacle <59914607+Purple-Tentacle@users.noreply.github.com> Date: Thu, 16 Apr 2020 10:17:14 +0200 Subject: [PATCH 092/302] readme update --- apps/chronowid/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/chronowid/README.md b/apps/chronowid/README.md index b6325d94f..f31c24c7b 100644 --- a/apps/chronowid/README.md +++ b/apps/chronowid/README.md @@ -27,7 +27,7 @@ There are no settings section in the settings app, timer can be set using an app ## Releases * Offifical app loader: Not yet published. -* Forked app loader: Not yet published. +* Forked app loader: https://github.com/Purple-Tentacle/BangleApps/tree/master/apps/chronowid (https://purple-tentacle.github.io/BangleApps/index.html#) * Development: https://github.com/Purple-Tentacle/BangleAppsDev/tree/master/apps/chronowid ## Requests From 9704522813daaf13354e63f79ba0f655546161b4 Mon Sep 17 00:00:00 2001 From: Purple-Tentacle <59914607+Purple-Tentacle@users.noreply.github.com> Date: Thu, 16 Apr 2020 10:26:32 +0200 Subject: [PATCH 093/302] removed type widget --- apps.json | 1 - 1 file changed, 1 deletion(-) diff --git a/apps.json b/apps.json index 591ddad28..0a894353b 100644 --- a/apps.json +++ b/apps.json @@ -1130,7 +1130,6 @@ "version":"0.01", "description": "Chronometer (timer) which runs as widget.", "tags": "tools,widget", - "type":"widget", "readme": "README.md", "storage": [ {"name":"chronowid.wid.js","url":"widget.js"}, From 8c6b78ebe19b29e7513d708a8f9bfe34d1f51cdf Mon Sep 17 00:00:00 2001 From: Purple-Tentacle <59914607+Purple-Tentacle@users.noreply.github.com> Date: Thu, 16 Apr 2020 10:32:41 +0200 Subject: [PATCH 094/302] Bugfix json creation --- apps/chronowid/app.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/chronowid/app.js b/apps/chronowid/app.js index cb99268af..48401a7bb 100644 --- a/apps/chronowid/app.js +++ b/apps/chronowid/app.js @@ -27,7 +27,7 @@ function resetSettings() { } settingsChronowid = storage.readJSON('chronowid.json',1); -if (!settingsChronowid.started) resetSettings(); +if (!settingsChronowid) resetSettings(); E.on('kill', () => { print("-KILL-"); @@ -88,4 +88,4 @@ function showMenu() { return E.showMenu(timerMenu); } -showMenu(); \ No newline at end of file +showMenu(); From 304e23f53ed8715d640b797e94b7d2c041f9c990 Mon Sep 17 00:00:00 2001 From: msdeibel Date: Thu, 16 Apr 2020 11:23:19 +0200 Subject: [PATCH 095/302] Fix home button and add min/max for chart --- apps/batchart/app.js | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/apps/batchart/app.js b/apps/batchart/app.js index deb406d8b..2d0d8e585 100644 --- a/apps/batchart/app.js +++ b/apps/batchart/app.js @@ -65,9 +65,7 @@ function loadData() { // Top up to MaxValueCount from previous days as required let previousDay = decrementDay(startingDay); - while (dataLines.length < MaxValueCount - && previousDay !== startingDay) { - + while (dataLines.length < MaxValueCount && previousDay !== startingDay) { let topUpLogFileName = "bclog" + previousDay; let remainingLines = MaxValueCount - dataLines.length; let topUpLines = loadLinesFromFile(remainingLines, topUpLogFileName); @@ -126,6 +124,12 @@ function renderData(dataArray) { const batteryIndex = 1; const temperatureIndex = 2; const switchabelsIndex = 3; + + const minTemperature = 20; + const maxTemparature = 40; + + const belowMinIndicatorValue = minTemperature - 1; + const aboveMaxIndicatorValue = maxTemparature + 1; var allConsumers = switchableConsumers.none | switchableConsumers.lcd | switchableConsumers.compass | switchableConsumers.bluetooth | switchableConsumers.gps | switchableConsumers.hrm; @@ -140,8 +144,19 @@ function renderData(dataArray) { // Temperature g.setColor(0.4, 0.4, 1); - let scaledTemp = Math.floor(((parseFloat(dataInfo[temperatureIndex]) * 100) - 2000)/20) + ((((parseFloat(dataInfo[temperatureIndex]) * 100) - 2000) % 100)/25); + + let datapointTemp = parseFloat(dataInfo[temperatureIndex]); + if (datapointTemp < minTemperature) { + datapointTemp = belowMinIndicatorValue; + } + if (datapointTemp > maxTemparature) { + datapointTemp = aboveMaxIndicatorValue; + } + + // Scale down the range of 20 - 40°C to a 100px y-axis, where 1px = .25° + let scaledTemp = Math.floor(((datapointTemp * 100) - 2000) / 20) + ((((datapointTemp * 100) - 2000) % 100) / 25); + g.setPixel(GraphXZero + i, GraphYZero - scaledTemp); // LCD state @@ -213,8 +228,6 @@ function switchOffApp(){ // special function to handle display switch on Bangle.on('lcdPower', (on) => { if (on) { - // call your app function here - // If you clear the screen, do Bangle.drawWidgets(); g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); @@ -222,12 +235,11 @@ Bangle.on('lcdPower', (on) => { } }); -setWatch(switchOffApp, BTN2, {edge:"rising", debounce:50, repeat:true}); +setWatch(switchOffApp, BTN2, {edge:"falling", debounce:50, repeat:true}); g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); -// call your app function here renderHomeIcon(); From 24eb0f33fef2431bb589d59f95c342005b08a03a Mon Sep 17 00:00:00 2001 From: msdeibel Date: Thu, 16 Apr 2020 11:24:17 +0200 Subject: [PATCH 096/302] Minor changes based on JSLint hints --- apps/batchart/widget.js | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/apps/batchart/widget.js b/apps/batchart/widget.js index 53f8b3549..1b8ce79ba 100644 --- a/apps/batchart/widget.js +++ b/apps/batchart/widget.js @@ -12,8 +12,8 @@ var batChartFile; // file for battery percentage recording const recordingInterval10Min = 60 * 10 * 1000; - const recordingInterval1Min = 60*1000; //For testing - const recordingInterval10S = 10*1000; //For testing + const recordingInterval1Min = 60 * 1000; //For testing + const recordingInterval10S = 10 * 1000; //For testing var recordingInterval = null; var compassEventReceived = false; @@ -26,15 +26,15 @@ let y = this.y; g.setColor(0, 1, 0); - g.fillPoly([x+5, y, x+5, y+4, x+1, y+4, x+1, y+20, x+18, y+20, x+18, y+4, x+13, y+4, x+13, y], true); + g.fillPoly([x + 5, y, x + 5, y + 4, x + 1, y + 4, x + 1, y + 20, x + 18, y + 20, x + 18, y + 4, x + 13, y + 4, x + 13, y], true); - g.setColor(0,0,0); - g.drawPoly([x+5, y+6, x+8, y+12, x+13, y+12, x+16, y+18], false); + g.setColor(0, 0, 0); + g.drawPoly([x + 5, y + 6, x + 8, y + 12, x + 13, y + 12, x + 16, y + 18], false); g.reset(); } - function onMag(){ + function onMag() { compassEventReceived = true; // Stop handling events when no longer necessarry Bangle.removeListener("mag", onMag); @@ -51,6 +51,7 @@ } function getEnabledConsumersValue() { + // Wait for an event from each of the devices to see if they are switched on var enabledConsumers = switchableConsumers.none; Bangle.on('mag', onMag); @@ -58,13 +59,12 @@ Bangle.on('HRM', onHrm); // Wait two seconds, that should be enough for each of the events to get raised once - setTimeout(() => { + setTimeout(() => { Bangle.removeAllListeners(); }, 2000); if (Bangle.isLCDOn()) enabledConsumers = enabledConsumers | switchableConsumers.lcd; - // Already added in the hope they will be available soon to get more details if (compassEventReceived) enabledConsumers = enabledConsumers | switchableConsumers.compass; if (gpsEventReceived) @@ -90,8 +90,7 @@ const logFileName = "bclog" + currentWriteDay; // Change log target on day change - if (!isNaN(previousWriteDay) - && previousWriteDay != currentWriteDay) { + if (!isNaN(previousWriteDay) && previousWriteDay != currentWriteDay) { //Remove a log file containing data from a week ago Storage.open(logFileName, "r").erase(); Storage.open(previousWriteLogName, "w").write(parseInt(currentWriteDay)); @@ -111,7 +110,7 @@ } function reload() { - WIDGETS["batchart"].width = 24; + WIDGETS.batchart.width = 24; recordingInterval = setInterval(logBatteryData, recordingInterval10Min); @@ -119,11 +118,12 @@ } // add the widget - WIDGETS["batchart"]={area:"tl",width:24,draw:draw,reload:function() { - reload(); - Bangle.drawWidgets(); // relayout all widgets - }}; + WIDGETS.batchart = { + area: "tl", width: 24, draw: draw, reload: function () { + reload(); + Bangle.drawWidgets(); + } + }; - // load settings, set correct widget width reload(); -})() \ No newline at end of file +})(); \ No newline at end of file From bb42044aa92f25f5375112194488c82ce76f0a37 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Thu, 16 Apr 2020 10:25:13 +0100 Subject: [PATCH 097/302] Show elapsed time, and tweak stopwatch so lap times don't overlap the bottom widget area --- apps/swatch/interface.html | 3 ++- apps/swatch/stopwatch.js | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/apps/swatch/interface.html b/apps/swatch/interface.html index 928c5fe39..45391fb6e 100644 --- a/apps/swatch/interface.html +++ b/apps/swatch/interface.html @@ -17,11 +17,12 @@ function getLapTimes() {
\n`; lapData.forEach((lap,lapIndex) => { lap.date = lap.n.substr(7,16).replace("_"," "); + lap.elapsed = lap.d.shift(); // remove first item html += `
${lap.date}
-
${lap.d.length} Laps
+
${lap.d.length} Laps, total time ${lap.elapsed}
diff --git a/apps/swatch/stopwatch.js b/apps/swatch/stopwatch.js index 478c22619..659f0606d 100644 --- a/apps/swatch/stopwatch.js +++ b/apps/swatch/stopwatch.js @@ -27,10 +27,10 @@ function updateLabels() { g.setFont("6x8",1); g.setFontAlign(-1,-1); for (var i in lapTimes) { - if (i<16) + if (i<15) {g.drawString(lapTimes.length-i+": "+timeToText(lapTimes[i]),35,timeY + 40 + i*8);} - else if (i<32) - {g.drawString(lapTimes.length-i+": "+timeToText(lapTimes[i]),125,timeY + 40 + (i-16)*8);} + else if (i<30) + {g.drawString(lapTimes.length-i+": "+timeToText(lapTimes[i]),125,timeY + 40 + (i-15)*8);} } drawsecs(); } @@ -92,12 +92,12 @@ setWatch(function() { // Start/stop updateLabels(); if (started) displayInterval = setInterval(function() { - var last = tCurrent; - if (started) tCurrent = Date.now(); - if (Math.floor(last/1000)!=Math.floor(tCurrent/1000)) - drawsecs(); - else - drawms(); + var last = tCurrent; + if (started) tCurrent = Date.now(); + if (Math.floor(last/1000)!=Math.floor(tCurrent/1000)) + drawsecs(); + else + drawms(); }, 20); }, BTN2, {repeat:true}); @@ -108,10 +108,10 @@ setWatch(function() { // Lap lapTimes.unshift(tCurrent-tStart); } if (!started) { // save - var timenow= Date(); var filename = "swatch-"+(new Date()).toISOString().substr(0,16).replace("T","_")+".json"; + if (tCurrent!=tStart) + lapTimes.unshift(tCurrent-tStart); // this maxes out the 28 char maximum - lapTimes.unshift(tCurrent-tStart); require("Storage").writeJSON(filename, getLapTimesArray()); tStart = tCurrent = tTotal = Date.now(); lapTimes = []; From de2dc3a2cc23010af7d1cdb454048401ece9cf33 Mon Sep 17 00:00:00 2001 From: msdeibel Date: Thu, 16 Apr 2020 11:26:57 +0200 Subject: [PATCH 098/302] Add readme and increment version --- apps.json | 3 +- apps/batchart/ChangeLog | 3 +- apps/batchart/README.md | 67 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 apps/batchart/README.md diff --git a/apps.json b/apps.json index 7449a233b..7985c6776 100644 --- a/apps.json +++ b/apps.json @@ -1178,7 +1178,8 @@ "name": "Battery Chart", "shortName":"Battery Chart", "icon": "app.png", - "version":"0.07", + "version":"0.08", + "readme": "README.md", "description": "A widget and an app for recording and visualizing battery percentage over time.", "tags": "app,widget,battery,time,record,chart,tool", "storage": [ diff --git a/apps/batchart/ChangeLog b/apps/batchart/ChangeLog index ba9e4e847..439d877be 100644 --- a/apps/batchart/ChangeLog +++ b/apps/batchart/ChangeLog @@ -4,4 +4,5 @@ 0.04: chart in the app is now active. 0.05: Display temperature and LCD state in chart 0.06: Fixes widget events and charting of component states -0.07: Improve logging and charting of component states and add widget icon \ No newline at end of file +0.07: Improve logging and charting of component states and add widget icon +0.08: Fix for Home button in the app and README added. \ No newline at end of file diff --git a/apps/batchart/README.md b/apps/batchart/README.md new file mode 100644 index 000000000..0ce2c646d --- /dev/null +++ b/apps/batchart/README.md @@ -0,0 +1,67 @@ +# Summary + +Battery Chart contains a widget that records the battery usage as well as information that might influence this usage. + +The app that comes with provides a graph that accumulates this information in a single screen. + +## How the widget works + +The widget records data in a fixed interval of ten minutes. + +When this timespan has passed, it saves the following information to a file called `bclogx` where `x` is + +the current day retrieved by `new Date().getDay()`: + +- Battery percentage +- Temperature (of the die) +- LCD state +- Compass state +- HRM state +- GPS state + +After seven days the logging rolls over and the previous data is overwritten. + +To properly handle the roll-over, the day of the previous logging operation is stored in `bcprvday`. + +The value is changed with the first recording operation of the new day. + +## How the App works + +### Events + +The app charts the last 144 (6/h * 24h) datapoints that have been recorded. + +If for the current day the 144 events have not been reached the list is padded with + +events from the previous `bclog` file(s). + +### Graph + +The graph then contains the battery percentage (left y-axis) and the temperature (right y-axis). + +In case the recorded temperature is outside the limits of the graph, the value is set to a minimum of 19 or a maximum of 41 and thus should be clearly visible outside of the graph's boundaries for the y-axis. + +The states of the various SoC devices are depicted below the graph. If at the time of recording the device was enabled a marker in the respective color is set, if not the pixels for this point in time stay black. + +If a device was not enabled during the 144 selected events, the name is not displayed. + +## File schema + +You can download the `bclog` files for your own analysis. They are `CSV` files without header rows and contain + +``` +timestamp,batteryPercentage,temperatureInDegreeC,deviceStates +``` + +with the `deviceStates` resembling a flag set consisting of + +``` +const switchableConsumers = { + none: 0, + lcd: 1, + compass: 2, + bluetooth: 4, + gps: 8, + hrm: 16 +}; +``` From a4ec9965ba5dae7768b20f25aadec9e6459411b4 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Thu, 16 Apr 2020 10:31:59 +0100 Subject: [PATCH 099/302] Make widget play well with other Gadgetbridge widgets/apps --- apps.json | 2 +- apps/gbridge/ChangeLog | 1 + apps/gbridge/widget.js | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/apps.json b/apps.json index e6fb7aaac..447d45154 100644 --- a/apps.json +++ b/apps.json @@ -93,7 +93,7 @@ { "id": "gbridge", "name": "Gadgetbridge", "icon": "app.png", - "version":"0.09", + "version":"0.10", "description": "The default notification handler for Gadgetbridge notifications from Android", "tags": "tool,system,android,widget", "type":"widget", diff --git a/apps/gbridge/ChangeLog b/apps/gbridge/ChangeLog index 53f8a1b4c..f23a4eb6d 100644 --- a/apps/gbridge/ChangeLog +++ b/apps/gbridge/ChangeLog @@ -8,3 +8,4 @@ 0.07: Move configuration to settings menu 0.08: Don't turn on LCD at start of every song 0.09: Update Bluetooth connection state automatically +0.10: Make widget play well with other Gadgetbridge widgets/apps diff --git a/apps/gbridge/widget.js b/apps/gbridge/widget.js index 03c622443..a87b9d1ec 100644 --- a/apps/gbridge/widget.js +++ b/apps/gbridge/widget.js @@ -145,6 +145,7 @@ } } + var _GB = global.GB; global.GB = (event) => { switch (event.t) { case "notify": @@ -160,6 +161,7 @@ handleCallEvent(event); break; } + if(_GB)setTimeout(_GB,0,event); }; // Touch control From eb4ac302ccd3464bd3d97b6f18b5fa635fcbe436 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Thu, 16 Apr 2020 11:33:36 +0100 Subject: [PATCH 100/302] Reduce memory usage further when running app settings page --- apps.json | 2 +- apps/setting/ChangeLog | 1 + apps/setting/settings.js | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps.json b/apps.json index 447d45154..0adf7de0b 100644 --- a/apps.json +++ b/apps.json @@ -120,7 +120,7 @@ { "id": "setting", "name": "Settings", "icon": "settings.png", - "version":"0.15", + "version":"0.16", "description": "A menu for setting up Bangle.js", "tags": "tool,system", "storage": [ diff --git a/apps/setting/ChangeLog b/apps/setting/ChangeLog index 3d82be9c0..3acfb5fb0 100644 --- a/apps/setting/ChangeLog +++ b/apps/setting/ChangeLog @@ -17,3 +17,4 @@ Move LCD Brightness menu into more general LCD menu 0.14: Reduce memory usage when running app settings page 0.15: Reduce memory usage when running default clock chooser (#294) +0.16: Reduce memory usage further when running app settings page diff --git a/apps/setting/settings.js b/apps/setting/settings.js index 9e343a68e..d0d88ce20 100644 --- a/apps/setting/settings.js +++ b/apps/setting/settings.js @@ -417,7 +417,7 @@ function showAppSettingsMenu() { '< Back': ()=>showMainMenu(), } const apps = storage.list(/\.info$/) - .map(app => {var a=storage.readJSON(app, 1);return (a&&a.settings)?a:undefined}) + .map(app => {var a=storage.readJSON(app, 1);return (a&&a.settings)?{sortorder:a.sortorder,name:a.name,settings:a.settings}:undefined}) .filter(app => app) // filter out any undefined apps .sort((a, b) => a.sortorder - b.sortorder) if (apps.length === 0) { From 92c24e2cfc5f5d5f1b391b61a57674641c20becd Mon Sep 17 00:00:00 2001 From: OmegaRogue Date: Thu, 16 Apr 2020 12:35:43 +0200 Subject: [PATCH 101/302] Fix URLs in Webmanifest Signed-off-by: OmegaRogue --- site.webmanifest | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site.webmanifest b/site.webmanifest index 0565f159c..0d1fb4679 100644 --- a/site.webmanifest +++ b/site.webmanifest @@ -17,6 +17,6 @@ "theme_color": "#5755d9", "background_color": "#5755d9", "display": "standalone", - "start_url": "https://omegavoid.codes/BangleApps", - "scope": "https://omegavoid.codes/BangleApps" + "start_url": "https://banglejs.com/apps/", + "scope": "https://banglejs.com/apps/" } From b02af77e514aa4bbcb41a8ab2039bb81762a1b03 Mon Sep 17 00:00:00 2001 From: OmegaRogue Date: Thu, 16 Apr 2020 12:48:35 +0200 Subject: [PATCH 102/302] Add Description, Remove add_to_apps.json Add Description Remove add_to_apps.json Signed-off-by: OmegaRogue --- apps.json | 4 ++-- apps/dane/ChangeLog | 3 ++- apps/dane/add_to_apps.json | 22 ---------------------- 3 files changed, 4 insertions(+), 25 deletions(-) delete mode 100644 apps/dane/add_to_apps.json diff --git a/apps.json b/apps.json index 91d2d249b..c29bf8018 100644 --- a/apps.json +++ b/apps.json @@ -1247,8 +1247,8 @@ "name": "Digital Assistant, not EDITH", "shortName": "DANE", "icon": "app.png", - "version": "0.06", - "description": "A detailed description of my great app", + "version": "0.07", + "description": "A Watchface inspired by Tony Stark's EDITH", "tags": "clock", "type": "clock", "allow_emulator": true, diff --git a/apps/dane/ChangeLog b/apps/dane/ChangeLog index 607d0adf5..419109ec1 100644 --- a/apps/dane/ChangeLog +++ b/apps/dane/ChangeLog @@ -1,4 +1,5 @@ 0.01: New App! 0.04: Added Icon to watchface 0.05: bugfix -0.06: moved and resized icon \ No newline at end of file +0.06: moved and resized icon +0.07: Added Description \ No newline at end of file diff --git a/apps/dane/add_to_apps.json b/apps/dane/add_to_apps.json deleted file mode 100644 index 6efb3ec85..000000000 --- a/apps/dane/add_to_apps.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "id": "dane", - "name": "Digital Assistant, not EDITH", - "shortName": "DANE", - "icon": "app.png", - "version": "0.06", - "description": "A detailed description of my great app", - "tags": "clock", - "type": "clock", - "allow_emulator": true, - "storage": [ - { - "name": "dane.app.js", - "url": "app.js" - }, - { - "name": "dane.img", - "url": "app-icon.js", - "evaluate": true - } - ] -} \ No newline at end of file From 217d19815456966c0fd268e31153d6bf8ac57e63 Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Sun, 12 Apr 2020 00:26:08 +0200 Subject: [PATCH 103/302] Add a "data" section to apps.json, with data files to clean on uninstall These are added to `appid.info` as "dataFiles" or "storageFiles", and can contain wildcards. --- README.md | 25 ++++++++++++++++++++----- bin/sanitycheck.js | 33 +++++++++++++++++++++++++++++++++ js/appinfo.js | 13 +++++++++++++ js/comms.js | 24 +++++++++++++++++++++--- js/index.js | 13 +++++++++++++ js/utils.js | 12 ++++++++++++ 6 files changed, 112 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index ca874ad2f..7a96ad335 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,13 @@ and which gives information about the app for the Launcher. "files:"file1,file2,file3", // added by BangleApps loader on upload - lists all files // that belong to the app so it can be deleted + "dataFiles":"appid.data.json,appid.data?.json" + // added by BangleApps loader on upload - lists files that + // the app might write, so they can be deleted on uninstall + // typically these files are not uploaded, but created by the app + // these can include '*' or '?' wildcards + "storageFiles":" + // same as "dataFiles", except the app handles these as storageFile } ``` @@ -240,16 +247,27 @@ and which gives information about the app for the Launcher. "evaluate":true // if supplied, data isn't quoted into a String before upload // (eg it's evaluated as JS) }, + ] + "data": [ // list of files the app writes to + {"name":"appid.data.json", // filename used in storage + "storageFile":true // if supplied, file is treated as storageFile + }, + {"wildcard":"appid.data.*" // wildcard of filenames used in storage + }, // this is mutually exclusive with using "name" + ], "sortorder" : 0, // optional - choose where in the list this goes. // this should only really be used to put system // stuff at the top - ] } ``` * name, icon and description present the app in the app loader. * tags is used for grouping apps in the library, separate multiple entries by comma. Known tags are `tool`, `system`, `clock`, `game`, `sound`, `gps`, `widget`, `launcher` or empty. * storage is used to identify the app files and how to handle them +* data is used to clean up files when the app is uninstalled + (If the app has settings but no data section, it is assumed settings are + stored in `appid.settings.json`, so there is no need to add a data section + containing only that file) ### `apps.json`: `custom` element @@ -351,19 +369,16 @@ Example `settings.js` E.showMenu(appMenu) }) ``` -In this example the app needs to add both `app.settings.js` and -`app.settings.json` to `apps.json`: +In this example the app needs to add `app.settings.js` to `apps.json`: ```json { "id": "app", ... "storage": [ ... {"name":"app.settings.js","url":"settings.js"}, - {"name":"app.settings.json","content":"{}"} ] }, ``` -That way removing the app also cleans up `app.settings.json`. ## Coding hints diff --git a/bin/sanitycheck.js b/bin/sanitycheck.js index 62b111ae0..fdf15a26b 100755 --- a/bin/sanitycheck.js +++ b/bin/sanitycheck.js @@ -74,6 +74,8 @@ apps.forEach((app,appIdx) => { var fileNames = []; app.storage.forEach((file) => { if (!file.name) ERROR(`App ${app.id} has a file with no name`); + if (file.name.includes('?') || file.name.includes('*')) + ERROR(`App ${app.id} storage file ${file.name} contains wildcards`); if (fileNames.includes(file.name)) ERROR(`App ${app.id} file ${file.name} is a duplicate`); fileNames.push(file.name); @@ -115,6 +117,37 @@ apps.forEach((app,appIdx) => { if (!STORAGE_KEYS.includes(key)) ERROR(`App ${app.id}'s ${file.name} has unknown key ${key}`); } }); + let dataNames = []; + (app.data||[]).forEach((data)=>{ + if (!data.name && !data.wildcard) ERROR(`App ${app.id} has a data file with no name`); + if (dataNames.includes(data.name||data.wildcard)) + ERROR(`App ${app.id} data file ${data.name||data.wildcard} is a duplicate`); + dataNames.push(data.name||data.wildcard) + if ('name' in data && 'wildcard' in data) + ERROR(`App ${app.id} data file ${data.name} has both name and wildcard`); + if (data.name) { + if (data.name.includes('?') || data.name.includes('*')) + ERROR(`App ${app.id} data file name ${data.name} contains wildcards`); + } + if (data.wildcard) { + if (!data.wildcard.includes('?') && !data.wildcard.includes('*')) + ERROR(`App ${app.id} data file wildcard ${data.wildcard} does not actually contains wildcard`); + if (data.wildcard.replace(/\?|\*/g,'') === '') + ERROR(`App ${app.id} data file wildcard ${data.wildcard} does not contain regular characters`); + else if (data.wildcard.replace(/\?|\*/g,'').length < 3) + WARN(`App ${app.id} data file wildcard ${data.wildcard} is very broad`); + else if (!data.wildcard.includes(app.id)) + WARN(`App ${app.id} data file wildcard ${data.wildcard} does not include app ID`); + } + if ('storageFile' in data && typeof data.storageFile !== 'boolean') + ERROR(`App ${app.id} data file ${data.name||data.wildcard} has non-boolean value for "storageFile"`); + for (const key in data) { + if (!['name','wildcard','storageFile'].includes(key)) + ERROR(`App ${app.id} data file ${data.name||data.wildcard} has unknown property "${key}"`); + } + }); + if (fileNames.includes(app.id+".settings.js") && dataNames.length===1 && dataNames[0] === app.id+'.settings.json') + WARN(`App ${app.id} has settings, so does not need to declare data file ${app.id+'.settings.json'}`) //console.log(fileNames); if (isApp && !fileNames.includes(app.id+".app.js")) ERROR(`App ${app.id} has no entrypoint`); if (isApp && !fileNames.includes(app.id+".img")) ERROR(`App ${app.id} has no JS icon`); diff --git a/js/appinfo.js b/js/appinfo.js index f4ab498b1..04c5da893 100644 --- a/js/appinfo.js +++ b/js/appinfo.js @@ -69,6 +69,19 @@ var AppInfo = { var fileList = fileContents.map(storageFile=>storageFile.name); fileList.unshift(appJSONName); // do we want this? makes life easier! json.files = fileList.join(","); + let dataFileList = [], storageFileList = []; + if ('data' in app) { + // add "data" files to appropriate list + app.data.forEach(d=>{ + if (d.storageFile) storageFileList.push(d.name||d.wildcard) + else dataFileList.push(d.name||d.wildcard) + }) + } else if (json.settings) { + // settings but no data files: assume app uses .settings.json file + dataFileList.push(app.id + '.settings.json') + } + if (dataFileList.length) json.dataFiles = dataFileList.join(","); + if (storageFileList.length) json.storageFiles = storageFileList.join(","); fileContents.push({ name : appJSONName, content : JSON.stringify(json) diff --git a/js/comms.js b/js/comms.js index 1f840ada7..1e8250305 100644 --- a/js/comms.js +++ b/js/comms.js @@ -94,10 +94,28 @@ getInstalledApps : () => { }); }, removeApp : app => { // expects an appid.info structure (i.e. with `files`) - if (app.files === '') return Promise.resolve(); // nothing to erase + if (!app.files && !app.dataFiles && !app.storageFiles) return Promise.resolve(); // nothing to erase Progress.show({title:`Removing ${app.name}`,sticky:true}); - var cmds = app.files.split(',').map(file=>{ - return `\x10require("Storage").erase(${toJS(file)});\n`; + let cmds = '\x10const s=require("Storage");\n'; + // remove App files (regular files, exact names only) + cmds += app.files.split(',').map(file => `\x10s.erase(${toJS(file)});\n`).join(""); + // remove Data files (regular files, can use wildcards) + cmds += (app.dataFiles||[]).split(',').map(file => { + const isGlob = (file.includes('*') || file.includes('?')) + if (!isGlob) return `\x10s.erase(${toJS(file)});\n`; + const regex = new RegExp(globToRegex(file)) + return `\x10s.list(${regex}).forEach(f=>s.erase(f));\n`; + }).join(""); + // remove Storage files (storageFiles, can use wildcards) + cmds += (app.storageFiles||[]).split(',').map(file => { + const isGlob = (file.includes('*') || file.includes('?')) + if (!isGlob) return `\x10s.open(${toJS(file)},'r').erase();\n`; + // storageFiles have a chunk number appended to their real name + const regex = globToRegex(file+'\u0001') + // open() doesn't want the chunk number though + let cmd = `\x10s.list(${regex}).forEach(f=>s.open(f.substring(0,f.length-1),'r').erase());\n` + // using a literal \u0001 char fails (not sure why), so escape it + return cmd.replace('\u0001', '\\x01') }).join(""); console.log("removeApp", cmds); return Comms.reset().then(new Promise((resolve,reject) => { diff --git a/js/index.js b/js/index.js index ef9bcb4f1..c48920315 100644 --- a/js/index.js +++ b/js/index.js @@ -349,6 +349,19 @@ function updateApp(app) { .filter(f => f !== app.id + '.info') .filter(f => !app.storage.some(s => s.name === f)) .join(','); + let dataFiles = (remove.dataFiles||'').split(','), + storageFiles = (remove.storageFiles||'').split(',') + if ('data' in app) { + // keep declared (in new version) data files + dataFiles = dataFiles.filter(f => app.data.some(d => (d.name||d.wildcard) === f)) + storageFiles = storageFiles.filter(f => app.data.some(d => (d.name||d.wildcard) === f)) + } + else if (remove.settings || app.settings) { + // app with settings but no data files declared: keep .settings.json + dataFiles = dataFiles.filter(f => f !== (app.id + '.settings.json')) + } + if (dataFiles.length) remove.dataFiles = dataFiles.join(','); + if (storageFiles.length) remove.storageFiles = storageFiles.join(',') return Comms.removeApp(remove); }).then(()=>{ showToast(`Updating ${app.name}...`); diff --git a/js/utils.js b/js/utils.js index d8c1b8063..50d319338 100644 --- a/js/utils.js +++ b/js/utils.js @@ -8,6 +8,18 @@ function escapeHtml(text) { }; return text.replace(/[&<>"']/g, function(m) { return map[m]; }); } +// simple glob to regex conversion, only supports "*" and "?" wildcards +function globToRegex(pattern) { + const ESCAPE = '.*+-?^${}()|[]\\'; + const regex = pattern.replace(/./g, c => { + switch (c) { + case '?': return '.'; + case '*': return '.*'; + default: return ESCAPE.includes(c) ? ('\\' + c) : c; + } + }); + return new RegExp('^'+regex+'$'); +} function htmlToArray(collection) { return [].slice.call(collection); } From 9e0fd91339dc48acd38c4c0687000f199ddb5c23 Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Wed, 15 Apr 2020 21:30:44 +0200 Subject: [PATCH 104/302] Data files: save all data files as a single string Separate regular and storage files by a semicolon --- README.md | 4 +--- bin/sanitycheck.js | 5 +++-- js/appinfo.js | 39 ++++++++++++++++++++++++++++++++------- js/comms.js | 14 ++++++++------ js/index.js | 18 ++++++++---------- 5 files changed, 52 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 7a96ad335..a04ee95b2 100644 --- a/README.md +++ b/README.md @@ -202,13 +202,11 @@ and which gives information about the app for the Launcher. "files:"file1,file2,file3", // added by BangleApps loader on upload - lists all files // that belong to the app so it can be deleted - "dataFiles":"appid.data.json,appid.data?.json" + "data":"appid.data.json,appid.data?.json;appidStorageFile,appidStorageFile*" // added by BangleApps loader on upload - lists files that // the app might write, so they can be deleted on uninstall // typically these files are not uploaded, but created by the app // these can include '*' or '?' wildcards - "storageFiles":" - // same as "dataFiles", except the app handles these as storageFile } ``` diff --git a/bin/sanitycheck.js b/bin/sanitycheck.js index fdf15a26b..197ebf57e 100755 --- a/bin/sanitycheck.js +++ b/bin/sanitycheck.js @@ -39,9 +39,10 @@ try{ const APP_KEYS = [ 'id', 'name', 'shortName', 'version', 'icon', 'description', 'tags', 'type', - 'sortorder', 'readme', 'custom', 'interface', 'storage', 'allow_emulator', + 'sortorder', 'readme', 'custom', 'interface', 'storage', 'data', 'allow_emulator', ]; const STORAGE_KEYS = ['name', 'url', 'content', 'evaluate']; +const DATA_KEYS = ['name', 'wildcard', 'storageFile']; apps.forEach((app,appIdx) => { if (!app.id) ERROR(`App ${appIdx} has no id`); @@ -142,7 +143,7 @@ apps.forEach((app,appIdx) => { if ('storageFile' in data && typeof data.storageFile !== 'boolean') ERROR(`App ${app.id} data file ${data.name||data.wildcard} has non-boolean value for "storageFile"`); for (const key in data) { - if (!['name','wildcard','storageFile'].includes(key)) + if (!DATA_KEYS.includes(key)) ERROR(`App ${app.id} data file ${data.name||data.wildcard} has unknown property "${key}"`); } }); diff --git a/js/appinfo.js b/js/appinfo.js index 04c5da893..413098bc4 100644 --- a/js/appinfo.js +++ b/js/appinfo.js @@ -69,26 +69,51 @@ var AppInfo = { var fileList = fileContents.map(storageFile=>storageFile.name); fileList.unshift(appJSONName); // do we want this? makes life easier! json.files = fileList.join(","); - let dataFileList = [], storageFileList = []; + let data = {dataFiles: [], storageFiles: []}; if ('data' in app) { // add "data" files to appropriate list app.data.forEach(d=>{ - if (d.storageFile) storageFileList.push(d.name||d.wildcard) - else dataFileList.push(d.name||d.wildcard) + if (d.storageFile) data.storageFiles.push(d.name||d.wildcard) + else data.dataFiles.push(d.name||d.wildcard) }) } else if (json.settings) { // settings but no data files: assume app uses .settings.json file - dataFileList.push(app.id + '.settings.json') + data.dataFiles.push(app.id + '.settings.json') } - if (dataFileList.length) json.dataFiles = dataFileList.join(","); - if (storageFileList.length) json.storageFiles = storageFileList.join(","); + const dataString = AppInfo.makeDataString(data) + if (dataString) json.data = dataString fileContents.push({ name : appJSONName, content : JSON.stringify(json) }); resolve(fileContents); }); - } + }, + // (.info).data holds filenames of data: both regular and storageFiles + // These are stored as: (note comma vs semicolons) + // "fil1,file2", "file1,file2;storageFileA,storageFileB" or ";storageFileA" + /** + * Convert appid.info "data" to object with file names/patterns + * Passing in undefined works + * @param data "data" as stored in appid.info + * @returns {{storageFiles:[], dataFiles:[]}} + */ + parseDataString(data) { + data = data || ''; + let [files = [], storage = []] = data.split(';').map(d => d.split(',')) + return {dataFiles: files, storageFiles: storage} + }, + /** + * Convert object with file names/patterns to appid.info "data" string + * Passing in an incomplete object will not work + * @param data {{storageFiles:[], dataFiles:[]}} + * @returns {string} "data" to store in appid.info + */ + makeDataString(data) { + if (!data.dataFiles.length && !data.storageFiles.length) { return '' } + if (!data.storageFiles.length) { return data.dataFiles.join(',') } + return [data.dataFiles.join(','),data.storageFiles.join(',')].join(';') + }, }; if ("undefined"!=typeof module) diff --git a/js/comms.js b/js/comms.js index 1e8250305..736a2b7c7 100644 --- a/js/comms.js +++ b/js/comms.js @@ -94,20 +94,22 @@ getInstalledApps : () => { }); }, removeApp : app => { // expects an appid.info structure (i.e. with `files`) - if (!app.files && !app.dataFiles && !app.storageFiles) return Promise.resolve(); // nothing to erase + if (!app.files && !app.data) return Promise.resolve(); // nothing to erase Progress.show({title:`Removing ${app.name}`,sticky:true}); let cmds = '\x10const s=require("Storage");\n'; - // remove App files (regular files, exact names only) + // remove App files: regular files, exact names only cmds += app.files.split(',').map(file => `\x10s.erase(${toJS(file)});\n`).join(""); - // remove Data files (regular files, can use wildcards) - cmds += (app.dataFiles||[]).split(',').map(file => { + // remove app Data: (dataFiles and storageFiles) + const data = AppInfo.parseDataString(app.data) + // regular files, can use wildcards + cmds += data.dataFiles.map(file => { const isGlob = (file.includes('*') || file.includes('?')) if (!isGlob) return `\x10s.erase(${toJS(file)});\n`; const regex = new RegExp(globToRegex(file)) return `\x10s.list(${regex}).forEach(f=>s.erase(f));\n`; }).join(""); - // remove Storage files (storageFiles, can use wildcards) - cmds += (app.storageFiles||[]).split(',').map(file => { + // storageFiles, can use wildcards + cmds += data.storageFiles.map(file => { const isGlob = (file.includes('*') || file.includes('?')) if (!isGlob) return `\x10s.open(${toJS(file)},'r').erase();\n`; // storageFiles have a chunk number appended to their real name diff --git a/js/index.js b/js/index.js index c48920315..41a43730e 100644 --- a/js/index.js +++ b/js/index.js @@ -349,19 +349,17 @@ function updateApp(app) { .filter(f => f !== app.id + '.info') .filter(f => !app.storage.some(s => s.name === f)) .join(','); - let dataFiles = (remove.dataFiles||'').split(','), - storageFiles = (remove.storageFiles||'').split(',') + let data = AppInfo.parseDataString(remove.data) if ('data' in app) { - // keep declared (in new version) data files - dataFiles = dataFiles.filter(f => app.data.some(d => (d.name||d.wildcard) === f)) - storageFiles = storageFiles.filter(f => app.data.some(d => (d.name||d.wildcard) === f)) - } - else if (remove.settings || app.settings) { + // only remove data files which are no longer declared in new app version + const removeData = (f) => !app.data.some(d => (d.name || d.wildcard)===f) + data.dataFiles = data.dataFiles.filter(removeData) + data.storageFiles = data.storageFiles.filter(removeData) + } else if (remove.settings || app.settings) { // app with settings but no data files declared: keep .settings.json - dataFiles = dataFiles.filter(f => f !== (app.id + '.settings.json')) + data.dataFiles = data.dataFiles.filter(f => f!==(app.id+'.settings.json')) } - if (dataFiles.length) remove.dataFiles = dataFiles.join(','); - if (storageFiles.length) remove.storageFiles = storageFiles.join(',') + remove.data = AppInfo.makeDataString(data) return Comms.removeApp(remove); }).then(()=>{ showToast(`Updating ${app.name}...`); From 3e5cfcdc12cdaadbf5f761ea2883905ca8ad4583 Mon Sep 17 00:00:00 2001 From: msdeibel Date: Thu, 16 Apr 2020 14:32:42 +0200 Subject: [PATCH 105/302] Fixed home button and added readme for WOHRM --- apps.json | 1 + apps/wohrm/ChangeLog | 1 + apps/wohrm/README.md | 29 +++++++++++++++++++++++++++++ apps/wohrm/app.js | 7 ++----- 4 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 apps/wohrm/README.md diff --git a/apps.json b/apps.json index 899eed03e..459ecb3ab 100644 --- a/apps.json +++ b/apps.json @@ -891,6 +891,7 @@ "name": "Workout HRM", "icon": "app.png", "version":"0.06", + "readme": "README.md", "description": "Workout heart rate monitor notifies you with a buzz if your heart rate goes above or below the set limits.", "tags": "hrm,workout", "type": "app", diff --git a/apps/wohrm/ChangeLog b/apps/wohrm/ChangeLog index f5c64dbee..53c451bcd 100644 --- a/apps/wohrm/ChangeLog +++ b/apps/wohrm/ChangeLog @@ -4,3 +4,4 @@ 0.04: Only buzz on high confidence (>85%) 0.05: Improved buzz timing and rendering 0.06: Removed debug outputs, fixed rendering for upper limit, improved rendering for +/- icons, changelog version order fixed +0.07: Home button fixed and README added \ No newline at end of file diff --git a/apps/wohrm/README.md b/apps/wohrm/README.md new file mode 100644 index 000000000..ad9e82525 --- /dev/null +++ b/apps/wohrm/README.md @@ -0,0 +1,29 @@ +# Summary +Workout heart rate monitor that buzzes when your heart rate hits the limits. + +This app is for the [Bangle.js watch](https://banglejs.com/). While active it monitors your heart rate +and will notify you with a buzz whenever your heart rate falls below or jumps above the set limits. + +# How it works +[Try it out](https://www.espruino.com/ide/emulator.html?codeurl=https://raw.githubusercontent.com/msdeibel/BangleApps/master/apps/wohrm/app.js&upload) using the [online Espruino emulator](https://www.espruino.com/ide/emulator.html). + +## Setting the limits +For setting the lower limit press button 4 (left part of the watch's touch screen). +Then adjust the value with the buttons 1 (top) and 3 (bottom) of the watch. + +For setting the upper limit act accordingly after pressing button 5 (the right part of the watch's screen). + +## Reading Reliability +As per the specs of the watch the HR monitor is not 100% reliable all the time. +That's why the WOHRM displays a confidence value for each reading of the current heart rate. + +To the left and right of the "Current" value two colored bars indicate the confidence in +the received value: For 85% and above the bars are green, between 84% and 50% the bars are yellow +and below 50% they turn red. + +## Closing the app +Pressing button 2 (middle) will switch off the HRM of the watch and return you to the launcher. + +# HRM usage +The HRM is switched on when the app is started. It stays switch on while the app is running, even +when the watch screen goes to stand-by. diff --git a/apps/wohrm/app.js b/apps/wohrm/app.js index 7e0af4219..b3ce8acc8 100644 --- a/apps/wohrm/app.js +++ b/apps/wohrm/app.js @@ -287,13 +287,11 @@ function resetHighlightTimeout() { setterHighlightTimeout = setTimeout(setLimitSetterToNone, 2000); } -// Show launcher when middle button pressed function switchOffApp(){ Bangle.setHRMPower(0); Bangle.showLauncher(); } -// special function to handle display switch on Bangle.on('lcdPower', (on) => { g.clear(); if (on) { @@ -312,19 +310,18 @@ Bangle.setHRMPower(1); Bangle.on('HRM', onHrm); setWatch(incrementLimit, BTN1, {edge:"rising", debounce:50, repeat:true}); -setWatch(switchOffApp, BTN2, {edge:"rising", debounce:50, repeat:true}); setWatch(decrementLimit, BTN3, {edge:"rising", debounce:50, repeat:true}); setWatch(setLimitSetterToLower, BTN4, {edge:"rising", debounce:50, repeat:true}); setWatch(setLimitSetterToUpper, BTN5, { edge: "rising", debounce: 50, repeat: true }); +setWatch(switchOffApp, BTN2, {edge:"falling", debounce:50, repeat:true}); + g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); -//drawTrainingHeartRate(); renderHomeIcon(); renderLowerLimitBackground(); renderUpperLimitBackground(); -// refesh every sec setInterval(drawTrainingHeartRate, 1000); From 9f0adf190019b5dd0bcef5f713c34dabd8ca1c3d Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Thu, 16 Apr 2020 17:06:25 +0200 Subject: [PATCH 106/302] Data files: remove settings magic, some more sanitychecks --- README.md | 13 +++++---- bin/sanitycheck.js | 69 ++++++++++++++++++++++++++++++++++++++++------ js/appinfo.js | 9 ++---- js/comms.js | 7 ++--- js/index.js | 3 -- 5 files changed, 73 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index a04ee95b2..a45647daf 100644 --- a/README.md +++ b/README.md @@ -263,9 +263,6 @@ and which gives information about the app for the Launcher. * tags is used for grouping apps in the library, separate multiple entries by comma. Known tags are `tool`, `system`, `clock`, `game`, `sound`, `gps`, `widget`, `launcher` or empty. * storage is used to identify the app files and how to handle them * data is used to clean up files when the app is uninstalled - (If the app has settings but no data section, it is assumed settings are - stored in `appid.settings.json`, so there is no need to add a data section - containing only that file) ### `apps.json`: `custom` element @@ -351,10 +348,10 @@ Example `settings.js` ```js // make sure to enclose the function in parentheses (function(back) { - let settings = require('Storage').readJSON('app.settings.json',1)||{}; + let settings = require('Storage').readJSON('app.json',1)||{}; function save(key, value) { settings[key] = value; - require('Storage').write('app.settings.json',settings); + require('Storage').write('app.json',settings); } const appMenu = { '': {'title': 'App Settings'}, @@ -367,13 +364,17 @@ Example `settings.js` E.showMenu(appMenu) }) ``` -In this example the app needs to add `app.settings.js` to `apps.json`: +In this example the app needs to add `app.settings.js` to `storage` in `apps.json`. +It should also add `app.json` to `data`, to make sure it is cleaned up when the app is uninstalled. ```json { "id": "app", ... "storage": [ ... {"name":"app.settings.js","url":"settings.js"}, + ], + "data": [ + {"name":"app.json"} ] }, ``` diff --git a/bin/sanitycheck.js b/bin/sanitycheck.js index 197ebf57e..51230f6fa 100755 --- a/bin/sanitycheck.js +++ b/bin/sanitycheck.js @@ -43,7 +43,22 @@ const APP_KEYS = [ ]; const STORAGE_KEYS = ['name', 'url', 'content', 'evaluate']; const DATA_KEYS = ['name', 'wildcard', 'storageFile']; +const FORBIDDEN_FILE_NAME_CHARS = /[,;]/; // used as separators in appid.info +function globToRegex(pattern) { + const ESCAPE = '.*+-?^${}()|[]\\'; + const regex = pattern.replace(/./g, c => { + switch (c) { + case '?': return '.'; + case '*': return '.*'; + default: return ESCAPE.includes(c) ? ('\\' + c) : c; + } + }); + return new RegExp('^'+regex+'$'); +} +const isGlob = f => /[?*]/.test(f) +// All storage+data files in all apps: {app:,[file: | data:]} +let allFiles = []; apps.forEach((app,appIdx) => { if (!app.id) ERROR(`App ${appIdx} has no id`); //console.log(`Checking ${app.id}...`); @@ -75,11 +90,13 @@ apps.forEach((app,appIdx) => { var fileNames = []; app.storage.forEach((file) => { if (!file.name) ERROR(`App ${app.id} has a file with no name`); - if (file.name.includes('?') || file.name.includes('*')) - ERROR(`App ${app.id} storage file ${file.name} contains wildcards`); + if (isGlob(file.name)) ERROR(`App ${app.id} storage file ${file.name} contains wildcards`); + let char = file.name.match(FORBIDDEN_FILE_NAME_CHARS) + if (char) ERROR(`App ${app.id} storage file ${file.name} contains invalid character "${char[0]}"`) if (fileNames.includes(file.name)) ERROR(`App ${app.id} file ${file.name} is a duplicate`); fileNames.push(file.name); + allFiles.push({app: app.id, file: file.name}); if (file.url) if (!fs.existsSync(appDir+file.url)) ERROR(`App ${app.id} file ${file.url} doesn't exist`); if (!file.url && !file.content && !app.custom) ERROR(`App ${app.id} file ${file.name} has no contents`); var fileContents = ""; @@ -124,14 +141,13 @@ apps.forEach((app,appIdx) => { if (dataNames.includes(data.name||data.wildcard)) ERROR(`App ${app.id} data file ${data.name||data.wildcard} is a duplicate`); dataNames.push(data.name||data.wildcard) + allFiles.push({app: app.id, data: (data.name||data.wildcard)}); if ('name' in data && 'wildcard' in data) ERROR(`App ${app.id} data file ${data.name} has both name and wildcard`); - if (data.name) { - if (data.name.includes('?') || data.name.includes('*')) - ERROR(`App ${app.id} data file name ${data.name} contains wildcards`); - } + if (isGlob(data.name)) + ERROR(`App ${app.id} data file name ${data.name} contains wildcards`); if (data.wildcard) { - if (!data.wildcard.includes('?') && !data.wildcard.includes('*')) + if (!isGlob(data.wildcard)) ERROR(`App ${app.id} data file wildcard ${data.wildcard} does not actually contains wildcard`); if (data.wildcard.replace(/\?|\*/g,'') === '') ERROR(`App ${app.id} data file wildcard ${data.wildcard} does not contain regular characters`); @@ -140,6 +156,8 @@ apps.forEach((app,appIdx) => { else if (!data.wildcard.includes(app.id)) WARN(`App ${app.id} data file wildcard ${data.wildcard} does not include app ID`); } + let char = (data.name||data.wildcard).match(FORBIDDEN_FILE_NAME_CHARS) + if (char) ERROR(`App ${app.id} data file ${data.name||data.wildcard} contains invalid character "${char[0]}"`) if ('storageFile' in data && typeof data.storageFile !== 'boolean') ERROR(`App ${app.id} data file ${data.name||data.wildcard} has non-boolean value for "storageFile"`); for (const key in data) { @@ -147,8 +165,24 @@ apps.forEach((app,appIdx) => { ERROR(`App ${app.id} data file ${data.name||data.wildcard} has unknown property "${key}"`); } }); - if (fileNames.includes(app.id+".settings.js") && dataNames.length===1 && dataNames[0] === app.id+'.settings.json') - WARN(`App ${app.id} has settings, so does not need to declare data file ${app.id+'.settings.json'}`) + // prefer "appid.json" over "appid.settings.json" (TODO: change to ERROR once all apps comply?) + if (dataNames.includes(app.id+".settings.json") && !dataNames.includes(app.id+".json")) + WARN(`App ${app.id} uses data file ${app.id+'.settings.json'} instead of ${app.id+'.json'}`) + // settings files should be listed under data, not storage (TODO: change to ERROR once all apps comply?) + if (fileNames.includes(app.id+".settings.json")) + WARN(`App ${app.id} uses storage file ${app.id+'.settings.json'} instead of data file`) + if (fileNames.includes(app.id+".json")) + WARN(`App ${app.id} uses storage file ${app.id+'.json'} instead of data file`) + // warn if storage file matches data file of same app + dataNames.forEach(dataName=>{ + const glob = globToRegex(dataName) + fileNames.forEach(fileName=>{ + if (glob.test(fileName)) { + if (isGlob(dataName)) WARN(`App ${app.id} storage file ${fileName} matches data wildcard ${dataName}`) + else WARN(`App ${app.id} storage file ${fileName} is also listed in data`) + } + }) + }) //console.log(fileNames); if (isApp && !fileNames.includes(app.id+".app.js")) ERROR(`App ${app.id} has no entrypoint`); if (isApp && !fileNames.includes(app.id+".img")) ERROR(`App ${app.id} has no JS icon`); @@ -157,3 +191,20 @@ apps.forEach((app,appIdx) => { if (!APP_KEYS.includes(key)) ERROR(`App ${app.id} has unknown key ${key}`); } }); +// Do not allow files from different apps to collide +let fileA +while(fileA=allFiles.pop()) { + const nameA = (fileA.file||fileA.data), + globA = globToRegex(nameA), + typeA = fileA.file?'storage':'data' + allFiles.forEach(fileB => { + const nameB = (fileB.file||fileB.data), + globB = globToRegex(nameB), + typeB = fileB.file?'storage':'data' + if (globA.test(nameB)||globB.test(nameA)) { + if (isGlob(nameA)||isGlob(nameB)) + ERROR(`App ${fileB.app} ${typeB} file ${nameB} matches app ${fileA.app} ${typeB} file ${nameA}`) + else ERROR(`App ${fileB.app} ${typeB} file ${nameB} is also listed as ${typeA} file for app ${fileA.app}`) + } + }) +} diff --git a/js/appinfo.js b/js/appinfo.js index 413098bc4..56b6ff2f8 100644 --- a/js/appinfo.js +++ b/js/appinfo.js @@ -69,19 +69,16 @@ var AppInfo = { var fileList = fileContents.map(storageFile=>storageFile.name); fileList.unshift(appJSONName); // do we want this? makes life easier! json.files = fileList.join(","); - let data = {dataFiles: [], storageFiles: []}; if ('data' in app) { + let data = {dataFiles: [], storageFiles: []}; // add "data" files to appropriate list app.data.forEach(d=>{ if (d.storageFile) data.storageFiles.push(d.name||d.wildcard) else data.dataFiles.push(d.name||d.wildcard) }) - } else if (json.settings) { - // settings but no data files: assume app uses .settings.json file - data.dataFiles.push(app.id + '.settings.json') + const dataString = AppInfo.makeDataString(data) + if (dataString) json.data = dataString } - const dataString = AppInfo.makeDataString(data) - if (dataString) json.data = dataString fileContents.push({ name : appJSONName, content : JSON.stringify(json) diff --git a/js/comms.js b/js/comms.js index 736a2b7c7..b825a06ad 100644 --- a/js/comms.js +++ b/js/comms.js @@ -101,17 +101,16 @@ removeApp : app => { // expects an appid.info structure (i.e. with `files`) cmds += app.files.split(',').map(file => `\x10s.erase(${toJS(file)});\n`).join(""); // remove app Data: (dataFiles and storageFiles) const data = AppInfo.parseDataString(app.data) + const isGlob = f => /[?*]/.test(f) // regular files, can use wildcards cmds += data.dataFiles.map(file => { - const isGlob = (file.includes('*') || file.includes('?')) - if (!isGlob) return `\x10s.erase(${toJS(file)});\n`; + if (!isGlob(file)) return `\x10s.erase(${toJS(file)});\n`; const regex = new RegExp(globToRegex(file)) return `\x10s.list(${regex}).forEach(f=>s.erase(f));\n`; }).join(""); // storageFiles, can use wildcards cmds += data.storageFiles.map(file => { - const isGlob = (file.includes('*') || file.includes('?')) - if (!isGlob) return `\x10s.open(${toJS(file)},'r').erase();\n`; + if (!isGlob(file)) return `\x10s.open(${toJS(file)},'r').erase();\n`; // storageFiles have a chunk number appended to their real name const regex = globToRegex(file+'\u0001') // open() doesn't want the chunk number though diff --git a/js/index.js b/js/index.js index 41a43730e..483dc09c7 100644 --- a/js/index.js +++ b/js/index.js @@ -355,9 +355,6 @@ function updateApp(app) { const removeData = (f) => !app.data.some(d => (d.name || d.wildcard)===f) data.dataFiles = data.dataFiles.filter(removeData) data.storageFiles = data.storageFiles.filter(removeData) - } else if (remove.settings || app.settings) { - // app with settings but no data files declared: keep .settings.json - data.dataFiles = data.dataFiles.filter(f => f!==(app.id+'.settings.json')) } remove.data = AppInfo.makeDataString(data) return Comms.removeApp(remove); From 28d3a6eb1d8e5c19c489380eeae3f3310d9b11c6 Mon Sep 17 00:00:00 2001 From: Markus Date: Fri, 17 Apr 2020 12:29:40 +0200 Subject: [PATCH 107/302] Fix failing dismissal of Gadgetbridge messages --- apps/batchart/widget.js | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/apps/batchart/widget.js b/apps/batchart/widget.js index 1b8ce79ba..aa81c2c5b 100644 --- a/apps/batchart/widget.js +++ b/apps/batchart/widget.js @@ -71,8 +71,9 @@ enabledConsumers = enabledConsumers | switchableConsumers.gps; if (hrmEventReceived) enabledConsumers = enabledConsumers | switchableConsumers.hrm; - //if (Bangle.isBluetoothOn()) - // enabledConsumers = enabledConsumers | switchableConsumers.bluetooth; + // First, coarse indication if the bluetooth device is enabled + if (NodeFilter.getSecuritystatus().connected) + enabledConsumers = enabledConsumers | switchableConsumers.bluetooth; // Reset the event registration vars compassEventReceived = false; @@ -110,19 +111,14 @@ } function reload() { - WIDGETS.batchart.width = 24; + WIDGETS["batchart"].width = 24; recordingInterval = setInterval(logBatteryData, recordingInterval10Min); - - logBatteryData(); } // add the widget - WIDGETS.batchart = { - area: "tl", width: 24, draw: draw, reload: function () { - reload(); - Bangle.drawWidgets(); - } + WIDGETS["batchart"] = { + area: "tl", width: 24, draw: draw, reload: reload }; reload(); From 933ce770950524599a28fc5ce93009c697d3577a Mon Sep 17 00:00:00 2001 From: Markus Date: Fri, 17 Apr 2020 12:33:26 +0200 Subject: [PATCH 108/302] Enable graphing of the bluetooth state, update version info --- apps.json | 2 +- apps/batchart/ChangeLog | 3 ++- apps/batchart/app.js | 16 ++++++++-------- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/apps.json b/apps.json index bbe0bbfcd..80c771c08 100644 --- a/apps.json +++ b/apps.json @@ -1194,7 +1194,7 @@ "name": "Battery Chart", "shortName":"Battery Chart", "icon": "app.png", - "version":"0.08", + "version":"0.09", "readme": "README.md", "description": "A widget and an app for recording and visualizing battery percentage over time.", "tags": "app,widget,battery,time,record,chart,tool", diff --git a/apps/batchart/ChangeLog b/apps/batchart/ChangeLog index 439d877be..66b40fbbf 100644 --- a/apps/batchart/ChangeLog +++ b/apps/batchart/ChangeLog @@ -5,4 +5,5 @@ 0.05: Display temperature and LCD state in chart 0.06: Fixes widget events and charting of component states 0.07: Improve logging and charting of component states and add widget icon -0.08: Fix for Home button in the app and README added. \ No newline at end of file +0.08: Fix for Home button in the app and README added. +0.09: Fix failing dismissal of Gadgetbridge notifications, record (coarse) bluetooth state \ No newline at end of file diff --git a/apps/batchart/app.js b/apps/batchart/app.js index 2d0d8e585..472fb3a8a 100644 --- a/apps/batchart/app.js +++ b/apps/batchart/app.js @@ -8,7 +8,7 @@ const GraphXMax = GraphXZero + MaxValueCount; const GraphLcdY = GraphYZero + 10; const GraphCompassY = GraphYZero + 16; -// const GraphBluetoothY = GraphYZero + 22; +const GraphBluetoothY = GraphYZero + 22; const GraphGpsY = GraphYZero + 28; const GraphHrmY = GraphYZero + 34; @@ -175,13 +175,13 @@ function renderData(dataArray) { g.drawLine(GraphXZero + i, GraphCompassY, GraphXZero + i, GraphCompassY + 1); } - // // Bluetooth state - // if (switchables & switchableConsumers.lcd == switchableConsumers.lcd) { - // g.setColor(0, 0, 1); - // g.setFontAlign(1, -1, 0); - // g.drawString("BLE", GraphXZero - GraphMarkerOffset, GraphBluetoothY - 2, true); - // g.drawLine(GraphXZero + i, GraphBluetoothY, GraphXZero + i, GraphBluetoothY + 1); - // } + // Bluetooth state + if (parseInt(dataInfo[switchabelsIndex]) & switchableConsumers.bluetooth) { + g.setColor(0, 0, 1); + g.setFontAlign(1, -1, 0); + g.drawString("BLE", GraphXZero - GraphMarkerOffset, GraphBluetoothY - 2, true); + g.drawLine(GraphXZero + i, GraphBluetoothY, GraphXZero + i, GraphBluetoothY + 1); + } // Gps state if (parseInt(dataInfo[switchabelsIndex]) & switchableConsumers.gps) { From 5782fa143957eee7714ca3212d1b2338b9d04b2b Mon Sep 17 00:00:00 2001 From: Markus Date: Fri, 17 Apr 2020 12:43:21 +0200 Subject: [PATCH 109/302] Use correct casing for getSecurityStatusMethod --- apps/batchart/widget.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/batchart/widget.js b/apps/batchart/widget.js index aa81c2c5b..2b00a83e2 100644 --- a/apps/batchart/widget.js +++ b/apps/batchart/widget.js @@ -72,7 +72,7 @@ if (hrmEventReceived) enabledConsumers = enabledConsumers | switchableConsumers.hrm; // First, coarse indication if the bluetooth device is enabled - if (NodeFilter.getSecuritystatus().connected) + if (NRF.getSecurityStatus().connected)) enabledConsumers = enabledConsumers | switchableConsumers.bluetooth; // Reset the event registration vars From 0c129bb10adc80db9d5cd791e0757356524360d0 Mon Sep 17 00:00:00 2001 From: Markus Date: Fri, 17 Apr 2020 12:45:13 +0200 Subject: [PATCH 110/302] Fix wrong ) --- apps/batchart/widget.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/batchart/widget.js b/apps/batchart/widget.js index 2b00a83e2..808c7781c 100644 --- a/apps/batchart/widget.js +++ b/apps/batchart/widget.js @@ -72,7 +72,7 @@ if (hrmEventReceived) enabledConsumers = enabledConsumers | switchableConsumers.hrm; // First, coarse indication if the bluetooth device is enabled - if (NRF.getSecurityStatus().connected)) + if (NRF.getSecurityStatus().connected) enabledConsumers = enabledConsumers | switchableConsumers.bluetooth; // Reset the event registration vars From 372f5123f4fe611284d0f6f5fc5a09d30a585ae8 Mon Sep 17 00:00:00 2001 From: Purple-Tentacle <59914607+Purple-Tentacle@users.noreply.github.com> Date: Fri, 17 Apr 2020 15:55:41 +0200 Subject: [PATCH 111/302] chronowid 0.02 --- apps/chronowid/ChangeLog | 3 ++- apps/chronowid/README.md | 9 ++++++--- apps/chronowid/app.js | 11 +++++++++-- apps/chronowid/widget.js | 18 +++++++++--------- 4 files changed, 26 insertions(+), 15 deletions(-) diff --git a/apps/chronowid/ChangeLog b/apps/chronowid/ChangeLog index a6f342f01..263145407 100644 --- a/apps/chronowid/ChangeLog +++ b/apps/chronowid/ChangeLog @@ -1 +1,2 @@ -0.01: New widget and app! +0.01: New widget and app! +0.02: Setting to reset values, timer buzzes at 00:00 and not later (see readme) \ No newline at end of file diff --git a/apps/chronowid/README.md b/apps/chronowid/README.md index f31c24c7b..f422dd956 100644 --- a/apps/chronowid/README.md +++ b/apps/chronowid/README.md @@ -5,6 +5,8 @@ The advantage is, that you can still see your normal watchface and other widgets The widget is always active, but only shown when the timer is on. Hours, minutes, seconds and timer status can be set with an app. +Depending on when you start the timer, it may alert up to 0,999 seconds early. This is because it checks only for full seconds. When there is less than one seconds left, it buzzes. This cannot be avoided without checking more than every second, which I would like to avoid. + ## Screenshots TBD @@ -18,18 +20,19 @@ TBD There are no settings section in the settings app, timer can be set using an app. +* Reset values: Reset hours, minutes, seconds to 0; set timer on to false; write to settings file * Hours: Set the hours for the timer * Minutes: Set the minutes for the timer * Seconds: Set the seconds for the timer -* Timer on: Starts the timer and displays the widget when set to 'On'. You have to leave the app. The widget is always there, but only visible when timer is on. +* Timer on: Starts the timer and displays the widget when set to 'On'. You have to leave the app to load the widget which starts the timer. The widget is always there, but only visible when timer is on. ## Releases -* Offifical app loader: Not yet published. +* Offifical app loader: https://github.com/espruino/BangleApps/tree/master/apps/chronowid (https://banglejs.com/apps/) * Forked app loader: https://github.com/Purple-Tentacle/BangleApps/tree/master/apps/chronowid (https://purple-tentacle.github.io/BangleApps/index.html#) * Development: https://github.com/Purple-Tentacle/BangleAppsDev/tree/master/apps/chronowid ## Requests -If you have any feature requests, please contact me on the Espruino forum: http://forum.espruino.com/profiles/155005/ \ No newline at end of file +If you have any feature requests, please write here: http://forum.espruino.com/conversations/345972/ \ No newline at end of file diff --git a/apps/chronowid/app.js b/apps/chronowid/app.js index 48401a7bb..dd9531233 100644 --- a/apps/chronowid/app.js +++ b/apps/chronowid/app.js @@ -30,7 +30,6 @@ settingsChronowid = storage.readJSON('chronowid.json',1); if (!settingsChronowid) resetSettings(); E.on('kill', () => { - print("-KILL-"); updateSettings(); }); @@ -45,6 +44,14 @@ function showMenu() { timerMenu.started.value = settingsChronowid.started; } }, + 'Reset values': function() { + settingsChronowid.hours = 0; + settingsChronowid.minutes = 0; + settingsChronowid.seconds = 0; + settingsChronowid.started = false; + updateSettings(); + showMenu(); + }, 'Hours': { value: settingsChronowid.hours, min: 0, @@ -88,4 +95,4 @@ function showMenu() { return E.showMenu(timerMenu); } -showMenu(); +showMenu(); \ No newline at end of file diff --git a/apps/chronowid/widget.js b/apps/chronowid/widget.js index 708bc6345..557104d92 100644 --- a/apps/chronowid/widget.js +++ b/apps/chronowid/widget.js @@ -36,19 +36,18 @@ //counts down, calculates and displays function countDown() { - //printDebug(); now = new Date(); diff = settingsChronowid.goal - now; //calculate difference WIDGETS["chronowid"].draw(); //time is up - if (settingsChronowid.started && diff <= 0) { + if (settingsChronowid.started && diff < 1000) { Bangle.buzz(1500); //write timer off to file settingsChronowid.started = false; storage.writeJSON('chronowid.json', settingsChronowid); clearInterval(interval); //stop interval - //printDebug(); } + //printDebug(); } // draw your widget @@ -72,12 +71,13 @@ g.drawString(getTime(diff), this.x+1, this.y+((height/2)-4)); //display hour 00:00:00 } } - else { - width = 58; - g.clearRect(this.x,this.y,this.x+width,this.y+height); - g.setFont("6x8", 2); - g.drawString("END", this.x+15, this.y+5); - } + // not needed anymoe, because we check if diff < 1000 now, so 00:00 is displayed. + // else { + // width = 58; + // g.clearRect(this.x,this.y,this.x+width,this.y+height); + // g.setFont("6x8", 2); + // g.drawString("END", this.x+15, this.y+5); + // } } if (settingsChronowid.started) interval = setInterval(countDown, 1000); //start countdown each second From dcb2a6c2d9b4f327796a3aaec53a43e33e4d715d Mon Sep 17 00:00:00 2001 From: Purple-Tentacle <59914607+Purple-Tentacle@users.noreply.github.com> Date: Fri, 17 Apr 2020 15:56:55 +0200 Subject: [PATCH 112/302] chronowid 0.02 --- apps.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps.json b/apps.json index 447d45154..e2b20cca4 100644 --- a/apps.json +++ b/apps.json @@ -1142,7 +1142,7 @@ "name": "Chrono Widget", "shortName":"Chrono Widget", "icon": "app.png", - "version":"0.01", + "version":"0.02", "description": "Chronometer (timer) which runs as widget.", "tags": "tools,widget", "readme": "README.md", From 9a664ecae4b9b64a7f9e773ff43adbd62c0ab3fa Mon Sep 17 00:00:00 2001 From: Purple-Tentacle <59914607+Purple-Tentacle@users.noreply.github.com> Date: Fri, 17 Apr 2020 16:38:33 +0200 Subject: [PATCH 113/302] Compass: 0.02 --- apps/compass/ChangeLog | 2 ++ apps/compass/compass.js | 77 +++++++++++++++++++++++------------------ 2 files changed, 45 insertions(+), 34 deletions(-) create mode 100644 apps/compass/ChangeLog diff --git a/apps/compass/ChangeLog b/apps/compass/ChangeLog new file mode 100644 index 000000000..efd778c72 --- /dev/null +++ b/apps/compass/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! +0.02: Show text if uncalibrated \ No newline at end of file diff --git a/apps/compass/compass.js b/apps/compass/compass.js index 10895e3cd..a014d79ff 100644 --- a/apps/compass/compass.js +++ b/apps/compass/compass.js @@ -1,34 +1,43 @@ -g.clear(); -g.setColor(0,0.5,1); -g.fillCircle(120,130,80,80); -g.setColor(0,0,0); -g.fillCircle(120,130,70,70); - -function arrow(r,c) { - r=r*Math.PI/180; - var p = Math.PI/2; - g.setColor(c); - g.fillPoly([ - 120+60*Math.sin(r), 130-60*Math.cos(r), - 120+10*Math.sin(r+p), 130-10*Math.cos(r+p), - 120+10*Math.sin(r+-p), 130-10*Math.cos(r-p), - ]); -} - -var oldHeading = 0; -Bangle.on('mag', function(m) { - if (!Bangle.isLCDOn()) return; - g.setFont("6x8",3); - g.setColor(0); - g.fillRect(70,0,170,24); - g.setColor(0xffff); - g.setFontAlign(0,0); - g.drawString(isNaN(m.heading)?"---":Math.round(m.heading),120,12); - g.setColor(0,0,0); - arrow(oldHeading,0); - arrow(oldHeading+180,0); - arrow(m.heading,0xF800); - arrow(m.heading+180,0x001F); - oldHeading = m.heading; -}); -Bangle.setCompassPower(1); +g.clear(); +g.setColor(0,0.5,1); +g.fillCircle(120,130,80,80); +g.setColor(0,0,0); +g.fillCircle(120,130,70,70); + +function arrow(r,c) { + r=r*Math.PI/180; + var p = Math.PI/2; + g.setColor(c); + g.fillPoly([ + 120+60*Math.sin(r), 130-60*Math.cos(r), + 120+10*Math.sin(r+p), 130-10*Math.cos(r+p), + 120+10*Math.sin(r+-p), 130-10*Math.cos(r-p), + ]); +} + +var oldHeading = 0; +Bangle.on('mag', function(m) { + if (!Bangle.isLCDOn()) return; + g.setFont("6x8",3); + g.setColor(0); + g.fillRect(0,0,230,40); + g.setColor(0xffff); + if (isNaN(m.heading)) { + g.setFontAlign(-1,-1); + g.setFont("6x8",2); + g.drawString("Uncalibrated",50,12); + g.drawString("turn 360° around",25,26); + } + else { + g.setFontAlign(0,0); + g.setFont("6x8",3); + g.drawString(Math.round(m.heading),120,12); + } + g.setColor(0,0,0); + arrow(oldHeading,0); + arrow(oldHeading+180,0); + arrow(m.heading,0xF800); + arrow(m.heading+180,0x001F); + oldHeading = m.heading; +}); +Bangle.setCompassPower(1); From 1017090cbe5fc6fb42101a0544fdd6e8ada983d0 Mon Sep 17 00:00:00 2001 From: Purple-Tentacle <59914607+Purple-Tentacle@users.noreply.github.com> Date: Fri, 17 Apr 2020 16:40:25 +0200 Subject: [PATCH 114/302] compass 0.02 --- apps.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps.json b/apps.json index e2b20cca4..d7bd6a7a1 100644 --- a/apps.json +++ b/apps.json @@ -235,7 +235,7 @@ { "id": "compass", "name": "Compass", "icon": "compass.png", - "version":"0.01", + "version":"0.02", "description": "Simple compass that points North", "tags": "tool,outdoors", "storage": [ From 1fff62e48da25d2455294643ac66321033088c76 Mon Sep 17 00:00:00 2001 From: paul Date: Fri, 17 Apr 2020 16:55:35 +0200 Subject: [PATCH 115/302] init hid camera app --- apps/hidcam/ChangeLog | 1 + apps/hidcam/add_to_apps.json | 13 +++++++++++++ apps/hidcam/app-icon.js | 1 + apps/hidcam/app.js | 12 ++++++++++++ apps/hidcam/app.png | Bin 0 -> 1620 bytes 5 files changed, 27 insertions(+) create mode 100644 apps/hidcam/ChangeLog create mode 100644 apps/hidcam/add_to_apps.json create mode 100644 apps/hidcam/app-icon.js create mode 100644 apps/hidcam/app.js create mode 100644 apps/hidcam/app.png diff --git a/apps/hidcam/ChangeLog b/apps/hidcam/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/hidcam/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/hidcam/add_to_apps.json b/apps/hidcam/add_to_apps.json new file mode 100644 index 000000000..ca75a7bd8 --- /dev/null +++ b/apps/hidcam/add_to_apps.json @@ -0,0 +1,13 @@ +// Create an entry in apps.json as follows: +{ "id": "7chname", + "name": "My app's human readable name", + "shortName":"Short Name", + "icon": "app.png", + "version":"0.01", + "description": "A detailed description of my great app", + "tags": "", + "storage": [ + {"name":"7chname.app.js","url":"app.js"}, + {"name":"7chname.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/hidcam/app-icon.js b/apps/hidcam/app-icon.js new file mode 100644 index 000000000..49232b838 --- /dev/null +++ b/apps/hidcam/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwJC/AH4A/AH4AgA==")) diff --git a/apps/hidcam/app.js b/apps/hidcam/app.js new file mode 100644 index 000000000..af367779a --- /dev/null +++ b/apps/hidcam/app.js @@ -0,0 +1,12 @@ +// place your const, vars, functions or classes here + +// special function to handle display switch on +Bangle.on('lcdPower', (on) => { + if (on) { + // call your app function here + // If you clear the screen, do Bangle.drawWidgets(); + } +}); + +g.clear(); +// call your app function here diff --git a/apps/hidcam/app.png b/apps/hidcam/app.png new file mode 100644 index 0000000000000000000000000000000000000000..582cb2e0853a5a2899a3afbd7eb19cde2ee7f6a0 GIT binary patch literal 1620 zcmV-a2CMmrP)1gXjloC|3_d8m;N2OpV(|i0q4YwBna<2! zK9thw%-*|urnNbV{Gax^?eD+#{x0kLJ~)lj_;W+1>qV*k8akT^^dvctZccUyj4}H~#M%Wwee_v` zHMv7o%BM8@dBrLshn{wGD9BDl?^eV5vSM3T96;NnHvtc6La=(qzq)xrX1d8bK-TN- zrd_f$_O`9nEmS+_S7HTXK<&u;LDIW|qlN&KJvM}tt6TVVqL-AvNv`B*{NzNpBfSQwQP5~Sf(Dp@Vq1+3Q`N9wBQN2`J_?M^u0FIMlt?p^8 z%U3%80kIwg!T{E9<8J18S&$k1`eO)@HP+=TZKo(z3_A3VFYJB=sn`2^Q$mRE>02(+W)np;)L1!GUvU2{O{<&F_nE6Qe#D~Xf|dD z+?d3-D1(IUiL`C2;PPv4CKw8H)v7h8^obJ&Z6D0CjVUe8Xq_NAymxUyPAMU^CCrIu z%1M71EC`5o2if_~7E&h??0jeQ1Y3N6p?}G72FmS*)xQD)%wBE=2tW6@(+MTi!fk9H1pWKew2(jTXVu4%vk26QvSQCbGmk`Z)Y! zBIhh)6vG2)h6mF8wC^|l$M(Eo9D?JiW}=_T2jUA>LC80foTera{^p)Wi`>}Gf;(|ZwEZQ zS^k|*9wyt=f4ZOo!xty7{%}HKD9tBZ50g$=%v&&vMa!#@Nsf>EkEEDA*ST6fiC+An zsNK1#>!x0obq@j$QqYU-ad3ZvbjqUU+%iw(0WahgmHV6yeLWqoYkSl4pzFQ(_Vp&I ztO{WI-48rGLwQb?#vgVvduyd9_6W)rFRoQJq3I(J?{Xmin45#=3l9BmL6Bp<*MZej zrsWN7oRPUr7IvrHoIHOjS=gPTCw>d)^LQK+B|=f2qbGjrWaOd5D<<9Dv>MTW0X3z> zyPy}9`<>1~?NCx@m8G$_@rRTy5zH12YM&P)=tU+L^fgY z^0Z&_6^qdVuwgN3wt_Ze(10?J@%{C2grBk42hsu74qEo^nd&v`X`IHN9lrxzS~GeF S(*#!l0000 Date: Fri, 17 Apr 2020 18:56:38 +0200 Subject: [PATCH 116/302] app code and config --- apps/hidcam/ChangeLog | 2 +- apps/hidcam/add_to_apps.json | 13 ------------- apps/hidcam/app-icon.js | 2 +- apps/hidcam/app.js | 12 ------------ apps/hidcam/app.png | Bin 1620 -> 1425 bytes apps/hidcam/hidcam.app.js | 28 ++++++++++++++++++++++++++++ apps/hidcam/hidcam.app.json | 13 +++++++++++++ 7 files changed, 43 insertions(+), 27 deletions(-) delete mode 100644 apps/hidcam/add_to_apps.json delete mode 100644 apps/hidcam/app.js create mode 100644 apps/hidcam/hidcam.app.js create mode 100644 apps/hidcam/hidcam.app.json diff --git a/apps/hidcam/ChangeLog b/apps/hidcam/ChangeLog index 5560f00bc..665c0df6e 100644 --- a/apps/hidcam/ChangeLog +++ b/apps/hidcam/ChangeLog @@ -1 +1 @@ -0.01: New App! +0.01: Init diff --git a/apps/hidcam/add_to_apps.json b/apps/hidcam/add_to_apps.json deleted file mode 100644 index ca75a7bd8..000000000 --- a/apps/hidcam/add_to_apps.json +++ /dev/null @@ -1,13 +0,0 @@ -// Create an entry in apps.json as follows: -{ "id": "7chname", - "name": "My app's human readable name", - "shortName":"Short Name", - "icon": "app.png", - "version":"0.01", - "description": "A detailed description of my great app", - "tags": "", - "storage": [ - {"name":"7chname.app.js","url":"app.js"}, - {"name":"7chname.img","url":"app-icon.js","evaluate":true} - ] -} diff --git a/apps/hidcam/app-icon.js b/apps/hidcam/app-icon.js index 49232b838..aa9d5e194 100644 --- a/apps/hidcam/app-icon.js +++ b/apps/hidcam/app-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mEwwJC/AH4A/AH4AgA==")) +E.toArrayBuffer(atob("MDCEAzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMxERERERETMzMzMzMzMzMzMzMzMzMzMzMRERERERERMzMzMzMzMzMzMzMzMzMzMzMREREREREREzMzMzMzMzMzMzMzMAAAAzEREREREREREzMzMzMzMzMzMzMzMAAAAxERERERERERETMzMzMzMzMzMzMxERERERERERERERERERERERETMzMzMzMRERERERERERERERERERERERERMzMzMzEREREREREREAAAAAEREREREREREzMzMzEREREREREQAAAAAAABERESIiIREzMzMzEREREREREAAAAAAAAAERESIiIREzMzMzEREREREQAAAKqqqgAAABESIiIREzMzMzEREREREQAAqqqqqqoAABESIiIREzMzMzEREREREAAKqqqqqqqgAAEREREREzMzMzERERERAACqqqqqqqqqAAAREREREzMzMzERERERAAqqqiIiIqqqoAAREREREzMzMzqqqqqgAAqqoiIiIiKqoAAKqqqqozMzMzqqqqqgAKqqIiIiIiKqqgAKqqqqozMzMzqqqqqgAKqqIiqqqiKqqgAKqqqqozMzMzqqqqqgAKqqqqqqqqqqqgAKqqqqozMzMzqqqqqgAKqqqqqqqqqqqgAKqqqqozMzMzqqqqqgAKqqqqqqqqqqqgAKqqqqozMzMzqqqqqgAKqqqqqqqqqqqgAKqqqqozMzMzqqqqqgAAqqqqqqqqqqoAAKqqqqozMzMzqqqqqqAAqqqqqqqqqqoACqqqqqozMzMzqqqqqqAACqqqqqqqqqAACqqqqqozMzMzqqqqqqoAAKqqqqqqqgAAqqqqqqozMzMzqqqqqqoAAAqqqqqqoAAAqqqqqqozMzMzqqqqqqqgAAAKqqqgAAAKqqqqqqozMzMzqqqqqqqqAAAAAAAAAACqqqqqqqozMzMzqqqqqqqqqgAAAAAAAKqqqqqqqqozMzMzqqqqqqqqqqoAAAAAqqqqqqqqqqozMzMzOqqqqqqqqqqqqqqqqqqqqqqqqqMzMzMzM6qqqqqqqqqqqqqqqqqqqqqqqjMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMw==")) diff --git a/apps/hidcam/app.js b/apps/hidcam/app.js deleted file mode 100644 index af367779a..000000000 --- a/apps/hidcam/app.js +++ /dev/null @@ -1,12 +0,0 @@ -// place your const, vars, functions or classes here - -// special function to handle display switch on -Bangle.on('lcdPower', (on) => { - if (on) { - // call your app function here - // If you clear the screen, do Bangle.drawWidgets(); - } -}); - -g.clear(); -// call your app function here diff --git a/apps/hidcam/app.png b/apps/hidcam/app.png index 582cb2e0853a5a2899a3afbd7eb19cde2ee7f6a0..3f631a0d8c2c9a2f82ad03564f6e503e97ec3b74 100644 GIT binary patch delta 1389 zcmV-z1(N#I43P_vHGc(0Nkl2XW^HHNnRPdH7YVh3 zAi4-C0tqZh0&gNIp^K!1F0{>?F8ZK?L=!~R#889`D}{vAtGp@*QYh5@80TYmE$3rD zXXnhv+r_N&?d+VhW?jr54xID6@AEwW|9PMDejI3`i6)w8Vt*MB_an&A!4sV_lRgYQ z(U60bA)%^oee%v5e=Ilm@&!mKhW3ATK_WL9^bF=wV%=wZcFZg%y|Nrf4_+Hu9Ce*+^R zc(KwX1zV6(7k9~?h!Hyfbl2<$#^#4n_>Pe{25aG9oee=sBAVos>< zeZK4Uo0W}K0(Kub;#YIxcL|;c!>=(2h$%VihL7Ie9x1h3sya$#pG2A>uj?sd(I~Ss z(fP@7oD9euxgs&*b+QBp??8vTYo};@fk(eQ-ni7;;|S=S+iZ& zQ%oh|M8YB59uK{}eKm#b*!n`1?bz81c@c`J;#MU9gEb$E-r?5R7`a@wF$XD$N25$9 z;`H^e#@*T3crrpoc}d^j=`$FZd*5QTQ}=G&k&it!R5#a z5~;sHN+RJ9nx^6DT=C!p=z5AY=&?+XoiY$ z0cl;&&o+yA*7s@|d7*&A4IY2~>ga(^KeTm$wPd}uTO2&!yS?xnk48!Bx?urTB{Y8O zp}Gz;soP|d`93RsYw&MaTMTA}HgEpTmqCEmlE3B>kete!eU3qVk*5icj{q=sO~q-e zpMSp+hDSipOcn0eKC-^7&AFmTK9h#|DVU2xY93ges2m+&u@!v=RuyBUGRAk-hCkwT zHd(mu>FzIU%Vt2;Ai4OKOP92ymMNh=r@|E=O@~DvjMJ4404)_dY^MYJZcj2U)uHs1y5MFf2e3*24Yl+_d3@l_-s& zD%LtZ@4?5YgGDLMg^}9mAF27tr*+VQKx5Z z7#3i+xVd`{3XYEuniw^l^e(~^VJCsEUR|D=s!a{d|_>g7N8ZA(iCfm4ELF z&rVwxsxW@gF|`+JYw@Fq{E9U_6X*QpQ!J#COL!HrkVtXhxX+mFkcUw@=#W~T_>4sc`SYWX)K1a5mj_QiwG0~G+^w0W_p zHez$bVew zFhhQ-(okyzE&*GOL{)@^c6%>Q%}X*JXCa*+BNxbK^Lrar)vzjd>=rk6t!Bg2Yy*Jc zVx_5CfXEIZ)s29G-vxQ7iiXqX#cA`JBUl$AmdnVYN^4aKef`VU8(5UbfNwBZPX7|7 v5qz7=%BN2Lwq^LgCeuU{O*GNO{|o;Bs>Z}7R{Rfy00000NkvXXu0mjfd2ysS delta 1586 zcmV-22F>}A3)BpdHGc*PNklVsi@IIDWLJF z<-tS+MbQVNMvWngMiMVMF($}~PbV60iJqKGq6sl-z#5IgNDvG@D81m_8L(pU0#u>& zLWh~o%w9f}(&^0JyL+a!If?wA_PXuwzxMtv>-#=9jnnvZLw~00MXDHdHZ-F~E6@xy zAk0IFoNQ?rJ?=L3M zHi1NI%W6w_1%Iftvl|hOhaVrR!sN{2xw@hkSSKyk8akT z^^R7(Pu73-SLjz9%wK^oEKM9^v2d+uZBzb_ElWR@^923y?MQ5Sh3y}H359v|8R9g6# zr~d$slVYvzX{^gvI{yK&9t6Sw)^Fo(=6P9=8U6ZW2$40`DO|&}2{l-??t>KM^xP{PQp9J>sMDt1wG0P7(^*|FxpxgxUNu=e>XY@94oX zm34@@sD<7z%UMsx_zdi4z8GAI!3iDJ{-uogg{9 zcYkp~PAMU^CCrIu%1M71EC`5o2if_~7E&h??0jeQ1Y3N6p?}G72FSn>E+sATQ)Q)70!M+WViTAl9FdlI^_bw) zg$_FC{~s`6^Y4_v-WecL8bso`qEiB0GeD&@Sajx!_6dmf;f#FXiqAg!jge#)(`PVmVSj|$`UsHZGo<5&Mq0i(4E(oP@&iXk=-U1(Bgrgt{}SSg8|!FVQpN1*ApW3%X$qBd z0$g%sHPPlXP=%gv2Ryu4{+&-ACfn41x}XBX7bma&a6)1z%_mzAlTKO8TYoT%Ma!#@ zNsf>EkEEDA*ST6fiC+AnsNK1#>!x0obq@j$QqYU-ad3ZvbjqUU+%iw(0WahgmHV6y zeLWqoYkSl4pzFQ(_Vp&ItO{WI-48rGLwQb?#vgVvduyd9_6W)rFRoQJq3I(J?{Xmi zn45#=3l9BmL6Bp<*MZejrhnxPZJd$0O%`^i+?+grDp}Z_q9=X}=<|3S6D2}Xo}(vz z>ty7i#49G<-Lx9fe*rb6#JivumHVB}LG4gdXO*R~xABLfz>ADt4^0(GkQeZWqQ#Jh z>AK{zSO Date: Fri, 17 Apr 2020 19:00:45 +0200 Subject: [PATCH 117/302] adding the extra comma as mentioned on bangle js documentation --- apps/hidcam/hidcam.app.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/hidcam/hidcam.app.json b/apps/hidcam/hidcam.app.json index df35404e4..ad3ea89fe 100644 --- a/apps/hidcam/hidcam.app.json +++ b/apps/hidcam/hidcam.app.json @@ -1,4 +1,4 @@ -// Create an entry in apps.json as follows: +}, { "id": "hidcam", "name": "HID camera shutter", "shortName":"HID cam", @@ -11,3 +11,4 @@ {"name":"hidcam.img","url":"app-icon.js","evaluate":true} ], } +] From 15535b0f19c232425c01311e1f1b5c33da50d78f Mon Sep 17 00:00:00 2001 From: paul Date: Fri, 17 Apr 2020 19:06:09 +0200 Subject: [PATCH 118/302] move json info to the good place --- apps.json | 14 +++++++++++++- apps/hidcam/hidcam.app.json | 14 -------------- 2 files changed, 13 insertions(+), 15 deletions(-) delete mode 100644 apps/hidcam/hidcam.app.json diff --git a/apps.json b/apps.json index effa14aa4..d57f218be 100644 --- a/apps.json +++ b/apps.json @@ -1293,5 +1293,17 @@ "evaluate": true } ] - } + }, +{ "id": "hidcam", + "name": "HID camera shutter", + "shortName":"HID cam", + "icon": "app.png", + "version":"0.01", + "description": "Enable HID, connect to your phone, start your camera and trigger the shot on your Bangle", + "tags": "tools", + "storage": [ + {"name":"hidcam.app.js","url":"hidcam.app.js"}, + {"name":"hidcam.img","url":"app-icon.js","evaluate":true} + ], +} ] diff --git a/apps/hidcam/hidcam.app.json b/apps/hidcam/hidcam.app.json deleted file mode 100644 index ad3ea89fe..000000000 --- a/apps/hidcam/hidcam.app.json +++ /dev/null @@ -1,14 +0,0 @@ -}, -{ "id": "hidcam", - "name": "HID camera shutter", - "shortName":"HID cam", - "icon": "app.png", - "version":"0.01", - "description": "Enable HID, connect to your phone, start your camera and trigger the shot on your Bangle", - "tags": "tools", - "storage": [ - {"name":"hidcam.app.js","url":"hidcam.app.js"}, - {"name":"hidcam.img","url":"app-icon.js","evaluate":true} - ], -} -] From 1dc2e3805d0cbbde633669b88c9f36465931ab69 Mon Sep 17 00:00:00 2001 From: paul Date: Fri, 17 Apr 2020 19:12:17 +0200 Subject: [PATCH 119/302] json format --- apps.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/apps.json b/apps.json index d57f218be..d89fe95b0 100644 --- a/apps.json +++ b/apps.json @@ -1294,16 +1294,16 @@ } ] }, -{ "id": "hidcam", - "name": "HID camera shutter", - "shortName":"HID cam", - "icon": "app.png", - "version":"0.01", - "description": "Enable HID, connect to your phone, start your camera and trigger the shot on your Bangle", - "tags": "tools", - "storage": [ - {"name":"hidcam.app.js","url":"hidcam.app.js"}, - {"name":"hidcam.img","url":"app-icon.js","evaluate":true} - ], + { "id": "hidcam", + "name": "HID camera shutter", + "shortName":"HID cam", + "icon": "app.png", + "version":"0.01", + "description": "Enable HID, connect to your phone, start your camera and trigger the shot on your Bangle", + "tags": "tools", + "storage": [ + {"name":"hidcam.app.js","url":"hidcam.app.js"}, + {"name":"hidcam.img","url":"app-icon.js","evaluate":true} + ] } ] From c7f2a18caaff5a472db6f7f3765cd4234f936da1 Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Fri, 17 Apr 2020 21:08:07 +0200 Subject: [PATCH 120/302] Remove "settings" from appid.info --- apps.json | 2 +- apps/setting/ChangeLog | 1 + apps/setting/settings.js | 22 ++++++++++++++-------- js/appinfo.js | 2 -- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/apps.json b/apps.json index 0c97b9e57..4b8b4a6b3 100644 --- a/apps.json +++ b/apps.json @@ -120,7 +120,7 @@ { "id": "setting", "name": "Settings", "icon": "settings.png", - "version":"0.16", + "version":"0.17", "description": "A menu for setting up Bangle.js", "tags": "tool,system", "storage": [ diff --git a/apps/setting/ChangeLog b/apps/setting/ChangeLog index 3acfb5fb0..81ac1fa81 100644 --- a/apps/setting/ChangeLog +++ b/apps/setting/ChangeLog @@ -18,3 +18,4 @@ 0.14: Reduce memory usage when running app settings page 0.15: Reduce memory usage when running default clock chooser (#294) 0.16: Reduce memory usage further when running app settings page +0.17: Remove need for "settings" in appid.info diff --git a/apps/setting/settings.js b/apps/setting/settings.js index d0d88ce20..97ce464ad 100644 --- a/apps/setting/settings.js +++ b/apps/setting/settings.js @@ -416,10 +416,19 @@ function showAppSettingsMenu() { '': { 'title': 'App Settings' }, '< Back': ()=>showMainMenu(), } - const apps = storage.list(/\.info$/) - .map(app => {var a=storage.readJSON(app, 1);return (a&&a.settings)?{sortorder:a.sortorder,name:a.name,settings:a.settings}:undefined}) - .filter(app => app) // filter out any undefined apps - .sort((a, b) => a.sortorder - b.sortorder) + const apps = storage.list(/\.settings\.js$/) + .map(s => s.substr(0, s.length-12)) + .map(id => { + const a=storage.readJSON(id+'.info',1); + return {id:id,name:a.name,sortorder:a.sortorder}; + }) + .sort((a, b) => { + const n = (0|a.sortorder)-(0|b.sortorder); + if (n) return n; // do sortorder first + if (a.nameb.name) return 1; + return 0; + }) if (apps.length === 0) { appmenu['No app has settings'] = () => { }; } @@ -433,10 +442,7 @@ function showAppSettings(app) { E.showMessage(`${app.name}:\n${msg}!\n\nBTN1 to go back`); setWatch(showAppSettingsMenu, BTN1, { repeat: false }); } - let appSettings = storage.read(app.settings); - if (!appSettings) { - return showError('Missing settings'); - } + let appSettings = storage.read(app.id+'.settings.js'); try { appSettings = eval(appSettings); } catch (e) { diff --git a/js/appinfo.js b/js/appinfo.js index 56b6ff2f8..9fff7c92a 100644 --- a/js/appinfo.js +++ b/js/appinfo.js @@ -60,8 +60,6 @@ var AppInfo = { if (app.type && app.type!="app") json.type = app.type; if (fileContents.find(f=>f.name==app.id+".app.js")) json.src = app.id+".app.js"; - if (fileContents.find(f=>f.name==app.id+".settings.js")) - json.settings = app.id+".settings.js"; if (fileContents.find(f=>f.name==app.id+".img")) json.icon = app.id+".img"; if (app.sortorder) json.sortorder = app.sortorder; From 43b0d8897e7a50556b93a58eff19d068d2f43544 Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Fri, 17 Apr 2020 21:25:56 +0200 Subject: [PATCH 121/302] widbatpc: Save settings in data file --- apps.json | 8 +++++--- apps/widbatpc/ChangeLog | 1 + apps/widbatpc/settings.js | 2 +- apps/widbatpc/widget.js | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/apps.json b/apps.json index 0c97b9e57..315120cc7 100644 --- a/apps.json +++ b/apps.json @@ -342,14 +342,16 @@ "name": "Battery Level Widget (with percentage)", "shortName": "Battery Widget", "icon": "widget.png", - "version":"0.09", + "version":"0.10", "description": "Show the current battery level and charging status in the top right of the clock, with charge percentage", "tags": "widget,battery", "type":"widget", "storage": [ {"name":"widbatpc.wid.js","url":"widget.js"}, - {"name":"widbatpc.settings.js","url":"settings.js"}, - {"name":"widbatpc.settings.json","content": "{}"} + {"name":"widbatpc.settings.js","url":"settings.js"} + ], + "data": [ + {"name":"widbatpc.json"} ] }, { "id": "widbt", diff --git a/apps/widbatpc/ChangeLog b/apps/widbatpc/ChangeLog index 129707320..138c1adc8 100644 --- a/apps/widbatpc/ChangeLog +++ b/apps/widbatpc/ChangeLog @@ -6,3 +6,4 @@ 0.07: Add settings: percentage/color/charger icon 0.08: Draw percentage as inverted on monochrome battery 0.09: Fix regression stopping correct widget updates +0.10: Don't overwrite existing settings on app update diff --git a/apps/widbatpc/settings.js b/apps/widbatpc/settings.js index 5c0bdbcae..88988cf22 100644 --- a/apps/widbatpc/settings.js +++ b/apps/widbatpc/settings.js @@ -3,7 +3,7 @@ * @param {function} back Use back() to return to settings menu */ (function(back) { - const SETTINGS_FILE = 'widbatpc.settings.json' + const SETTINGS_FILE = 'widbatpc.json' const COLORS = ['By Level', 'Green', 'Monochrome'] // initialize with default settings... diff --git a/apps/widbatpc/widget.js b/apps/widbatpc/widget.js index aca690ce0..189ca215f 100644 --- a/apps/widbatpc/widget.js +++ b/apps/widbatpc/widget.js @@ -11,7 +11,7 @@ const COLORS = { 'ok': 0xFD20, // "Orange" 'low':0xF800, // "Red" } -const SETTINGS_FILE = 'widbatpc.settings.json' +const SETTINGS_FILE = 'widbatpc.json' let settings function loadSettings() { From b691f783aa7cb956c0644658907a93e27ce836c9 Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Fri, 17 Apr 2020 21:29:56 +0200 Subject: [PATCH 122/302] welcome: Save settings in data file fwelcom --- apps.json | 6 ++++-- apps/welcome/ChangeLog | 1 + apps/welcome/boot.js | 4 ++-- apps/welcome/settings-default.json | 3 --- apps/welcome/settings.js | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) delete mode 100644 apps/welcome/settings-default.json diff --git a/apps.json b/apps.json index 315120cc7..a8b29b685 100644 --- a/apps.json +++ b/apps.json @@ -78,7 +78,7 @@ { "id": "welcome", "name": "Welcome", "icon": "app.png", - "version":"0.07", + "version":"0.08", "description": "Appears at first boot and explains how to use Bangle.js", "tags": "start,welcome", "allow_emulator":true, @@ -86,8 +86,10 @@ {"name":"welcome.boot.js","url":"boot.js"}, {"name":"welcome.app.js","url":"app.js"}, {"name":"welcome.settings.js","url":"settings.js"}, - {"name":"welcome.settings.json","url":"settings-default.json","evaluate":true}, {"name":"welcome.img","url":"app-icon.js","evaluate":true} + ], + "data": [ + {"name":"welcome.json"} ] }, { "id": "gbridge", diff --git a/apps/welcome/ChangeLog b/apps/welcome/ChangeLog index b8786af6a..a377fc81e 100644 --- a/apps/welcome/ChangeLog +++ b/apps/welcome/ChangeLog @@ -7,3 +7,4 @@ 0.07: Run again when updated Don't run again when settings app is updated (or absent) Add "Run Now" option to settings +0.08: Don't overwrite existing settings on app update diff --git a/apps/welcome/boot.js b/apps/welcome/boot.js index bc5afcc66..f6ba6d2d6 100644 --- a/apps/welcome/boot.js +++ b/apps/welcome/boot.js @@ -1,11 +1,11 @@ (function() { - let s = require('Storage').readJSON('welcome.settings.json', 1) + let s = require('Storage').readJSON('welcome.json', 1) || require('Storage').readJSON('setting.json', 1) || {welcomed: true} // do NOT run if global settings are also absent if (!s.welcomed && require('Storage').read('welcome.app.js')) { setTimeout(() => { s.welcomed = true - require('Storage').write('welcome.settings.json', {welcomed: "yes"}) + require('Storage').write('welcome.json', {welcomed: "yes"}) load('welcome.app.js') }) } diff --git a/apps/welcome/settings-default.json b/apps/welcome/settings-default.json deleted file mode 100644 index d250efff5..000000000 --- a/apps/welcome/settings-default.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "welcomed": false -} diff --git a/apps/welcome/settings.js b/apps/welcome/settings.js index b11921646..e873c2785 100644 --- a/apps/welcome/settings.js +++ b/apps/welcome/settings.js @@ -1,13 +1,13 @@ // The welcome app is special, and gets to use global settings (function(back) { - let settings = require('Storage').readJSON('welcome.settings.json', 1) + let settings = require('Storage').readJSON('welcome.sjson', 1) || require('Storage').readJSON('setting.json', 1) || {} E.showMenu({ '': { 'title': 'Welcome App' }, 'Run on Next Boot': { value: !settings.welcomed, format: v => v ? 'OK' : 'No', - onchange: v => require('Storage').write('welcome.settings.json', {welcomed: !v}), + onchange: v => require('Storage').write('welcome.json', {welcomed: !v}), }, 'Run Now': () => load('welcome.app.js'), '< Back': back, From b047f14d4ada97c510edc7a773a3e1f08982bc5d Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Fri, 17 Apr 2020 21:35:59 +0200 Subject: [PATCH 123/302] setting: Save settings in data file --- apps.json | 6 ++++-- apps/setting/ChangeLog | 1 + apps/setting/settings-default.json | 25 ------------------------- 3 files changed, 5 insertions(+), 27 deletions(-) delete mode 100644 apps/setting/settings-default.json diff --git a/apps.json b/apps.json index a8b29b685..4d5493e49 100644 --- a/apps.json +++ b/apps.json @@ -122,15 +122,17 @@ { "id": "setting", "name": "Settings", "icon": "settings.png", - "version":"0.16", + "version":"0.17", "description": "A menu for setting up Bangle.js", "tags": "tool,system", "storage": [ {"name":"setting.app.js","url":"settings.js"}, {"name":"setting.boot.js","url":"boot.js"}, - {"name":"setting.json","url":"settings-default.json","evaluate":true}, {"name":"setting.img","url":"settings-icon.js","evaluate":true} ], + "data": [ + {"name":"setting.json"} + ], "sortorder" : -2 }, { "id": "alarm", diff --git a/apps/setting/ChangeLog b/apps/setting/ChangeLog index 3acfb5fb0..773b510f0 100644 --- a/apps/setting/ChangeLog +++ b/apps/setting/ChangeLog @@ -18,3 +18,4 @@ 0.14: Reduce memory usage when running app settings page 0.15: Reduce memory usage when running default clock chooser (#294) 0.16: Reduce memory usage further when running app settings page +0.17: Don't overwrite existing settings on app update diff --git a/apps/setting/settings-default.json b/apps/setting/settings-default.json deleted file mode 100644 index c61fd6109..000000000 --- a/apps/setting/settings-default.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - ble: true, // Bluetooth enabled by default - blerepl: true, // Is REPL on Bluetooth - can Espruino IDE be used? - log: false, // Do log messages appear on screen? - timeout: 10, // Default LCD timeout in seconds - vibrate: true, // Vibration enabled by default. App must support - beep: "vib", // Beep enabled by default. App must support - timezone: 0, // Set the timezone for the device - HID : false, // BLE HID mode, off by default - clock: null, // a string for the default clock's name - "12hour" : false, // 12 or 24 hour clock? - // welcomed : undefined/true (whether welcome app should show) - brightness: 1, // LCD brightness from 0 to 1 - options: { - wakeOnBTN1: true, - wakeOnBTN2: true, - wakeOnBTN3: true, - wakeOnFaceUp: false, - wakeOnTouch: false, - wakeOnTwist: true, - twistThreshold: 819.2, - twistMaxY: -800, - twistTimeout: 1000 - } -} From 9358b4b5995e2efa6a49a1ca7865a0484f0c2c3f Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Fri, 17 Apr 2020 21:39:27 +0200 Subject: [PATCH 124/302] alarm: Save settings in data file --- apps.json | 6 ++++-- apps/alarm/ChangeLog | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/apps.json b/apps.json index 4d5493e49..30fb669d2 100644 --- a/apps.json +++ b/apps.json @@ -139,16 +139,18 @@ "name": "Default Alarm", "shortName":"Alarms", "icon": "app.png", - "version":"0.06", + "version":"0.07", "description": "Set and respond to alarms", "tags": "tool,alarm,widget", "storage": [ {"name":"alarm.app.js","url":"app.js"}, {"name":"alarm.boot.js","url":"boot.js"}, {"name":"alarm.js","url":"alarm.js"}, - {"name":"alarm.json","content":"[]"}, {"name":"alarm.img","url":"app-icon.js","evaluate":true}, {"name":"alarm.wid.js","url":"widget.js"} + ], + "data": [ + {"name":"alarm.json"} ] }, { "id": "wclock", diff --git a/apps/alarm/ChangeLog b/apps/alarm/ChangeLog index 2ff60e658..ca92a0d97 100644 --- a/apps/alarm/ChangeLog +++ b/apps/alarm/ChangeLog @@ -4,3 +4,4 @@ 0.04: Tweaks for variable size widget system 0.05: Add alarm.boot.js and move code from the bootloader 0.06: Change 'New Alarm' to 'Save', allow Deletion of Alarms +0.07: Don't overwrite existing settings on app update From e4c0574ab77ad2b8064e3c50dd1e900f5a2fe7fb Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Fri, 17 Apr 2020 21:45:48 +0200 Subject: [PATCH 125/302] heart: Save settings in data file, add recording files to data --- apps.json | 7 +++++-- apps/heart/ChangeLog | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/apps.json b/apps.json index 30fb669d2..6509458bf 100644 --- a/apps.json +++ b/apps.json @@ -300,15 +300,18 @@ { "id": "heart", "name": "Heart Rate Recorder", "icon": "app.png", - "version":"0.01", + "version":"0.02", "interface": "interface.html", "description": "Application that allows you to record your heart rate. Can run in background", "tags": "tool,health,widget", "storage": [ {"name":"heart.app.js","url":"app.js"}, - {"name":"heart.json","url":"app-settings.json","evaluate":true}, {"name":"heart.img","url":"app-icon.js","evaluate":true}, {"name":"heart.wid.js","url":"widget.js"} + ], + "data": [ + {"name":"heart.json"}, + {"wildcard":".heart?","storageFile": true} ] }, { "id": "slevel", diff --git a/apps/heart/ChangeLog b/apps/heart/ChangeLog index 4c4db83bc..70134af27 100644 --- a/apps/heart/ChangeLog +++ b/apps/heart/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App! - +0.02: Don't overwrite existing settings on app update + Clean up recordings on app removal From db35edede6527b4613ed147298be6497def457ee Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Fri, 17 Apr 2020 21:49:57 +0200 Subject: [PATCH 126/302] ncstart: Save settings in data file --- apps.json | 6 ++++-- apps/ncstart/ChangeLog | 1 + apps/ncstart/boot.js | 4 ++-- apps/ncstart/settings-default.json | 3 --- apps/ncstart/settings.js | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) delete mode 100644 apps/ncstart/settings-default.json diff --git a/apps.json b/apps.json index 6509458bf..c3544abee 100644 --- a/apps.json +++ b/apps.json @@ -528,20 +528,22 @@ "id": "ncstart", "name": "NCEU Startup", "icon": "start.png", - "version":"0.04", + "version":"0.05", "description": "NodeConfEU 2019 'First Start' Sequence", "tags": "start,welcome", "storage": [ {"name":"ncstart.app.js","url":"start.js"}, {"name":"ncstart.boot.js","url":"boot.js"}, {"name":"ncstart.settings.js","url":"settings.js"}, - {"name":"ncstart.settings.json","url":"settings-default.json","evaluate":true}, {"name":"ncstart.img","url":"start-icon.js","evaluate":true}, {"name":"nc-bangle.img","url":"start-bangle.js","evaluate":true}, {"name":"nc-nceu.img","url":"start-nceu.js","evaluate":true}, {"name":"nc-nfr.img","url":"start-nfr.js","evaluate":true}, {"name":"nc-nodew.img","url":"start-nodew.js","evaluate":true}, {"name":"nc-tf.img","url":"start-tf.js","evaluate":true} + ], + "data": [ + {"name":"ncstart.json"} ] }, { "id": "ncfrun", diff --git a/apps/ncstart/ChangeLog b/apps/ncstart/ChangeLog index f4418827e..522633f7b 100644 --- a/apps/ncstart/ChangeLog +++ b/apps/ncstart/ChangeLog @@ -5,3 +5,4 @@ 0.04: Run again when updated Don't run again when settings app is updated (or absent) Add "Run Now" option to settings +0.05: Don't overwrite existing settings on app update diff --git a/apps/ncstart/boot.js b/apps/ncstart/boot.js index e3f514f5b..094033094 100644 --- a/apps/ncstart/boot.js +++ b/apps/ncstart/boot.js @@ -1,11 +1,11 @@ (function() { - let s = require('Storage').readJSON('ncstart.settings.json', 1) + let s = require('Storage').readJSON('ncstart.json', 1) || require('Storage').readJSON('setting.json', 1) || {welcomed: true} // do NOT run if global settings are also absent if (!s.welcomed && require('Storage').read('ncstart.app.js')) { setTimeout(() => { s.welcomed = true - require('Storage').write('ncstart.settings.json', s) + require('Storage').write('ncstart.json', s) load('ncstart.app.js') }) } diff --git a/apps/ncstart/settings-default.json b/apps/ncstart/settings-default.json deleted file mode 100644 index d250efff5..000000000 --- a/apps/ncstart/settings-default.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "welcomed": false -} diff --git a/apps/ncstart/settings.js b/apps/ncstart/settings.js index 2b24095cf..6780264a7 100644 --- a/apps/ncstart/settings.js +++ b/apps/ncstart/settings.js @@ -1,13 +1,13 @@ // The welcome app is special, and gets to use global settings (function(back) { - let settings = require('Storage').readJSON('ncstart.settings.json', 1) + let settings = require('Storage').readJSON('ncstart.json', 1) || require('Storage').readJSON('setting.json', 1) || {} E.showMenu({ '': { 'title': 'NCEU Startup' }, 'Run on Next Boot': { value: !settings.welcomed, format: v => v ? 'OK' : 'No', - onchange: v => require('Storage').write('ncstart.settings.json', {welcomed: !v}), + onchange: v => require('Storage').write('ncstart.json', {welcomed: !v}), }, 'Run Now': () => load('ncstart.app.js'), '< Back': back, From 595de45e348799bb38d0728dd8a82aecc5c09287 Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Fri, 17 Apr 2020 21:52:42 +0200 Subject: [PATCH 127/302] numerals: Save settings in data file --- apps.json | 8 +++++--- apps/numerals/ChangeLog | 3 ++- apps/numerals/numerals-default.json | 5 ----- 3 files changed, 7 insertions(+), 9 deletions(-) delete mode 100644 apps/numerals/numerals-default.json diff --git a/apps.json b/apps.json index c3544abee..18fad4cfe 100644 --- a/apps.json +++ b/apps.json @@ -1235,7 +1235,7 @@ "name": "Numerals Clock", "shortName": "Numerals Clock", "icon": "numerals.png", - "version":"0.03", + "version":"0.04", "description": "A simple big numerals clock", "tags": "numerals,clock", "type":"clock", @@ -1243,8 +1243,10 @@ "storage": [ {"name":"numerals.app.js","url":"numerals.app.js"}, {"name":"numerals.img","url":"numerals-icon.js","evaluate":true}, - {"name":"numerals.settings.js","url":"numerals.settings.js"}, - {"name":"numerals.json","url":"numerals-default.json","evaluate":true} + {"name":"numerals.settings.js","url":"numerals.settings.js"} + ], + "data":[ + {"name":"numerals.json"} ] }, { "id": "bledetect", diff --git a/apps/numerals/ChangeLog b/apps/numerals/ChangeLog index ec465a83f..927c4ff5f 100644 --- a/apps/numerals/ChangeLog +++ b/apps/numerals/ChangeLog @@ -1,3 +1,4 @@ 0.01: New App! 0.02: Use BTN2 for settings menu like other clocks -0.03: maximize numerals, make menu button configurable, change icon to mac palette, add default settings file, respect 12hour setting \ No newline at end of file +0.03: maximize numerals, make menu button configurable, change icon to mac palette, add default settings file, respect 12hour setting +0.04: Don't overwrite existing settings on app update diff --git a/apps/numerals/numerals-default.json b/apps/numerals/numerals-default.json deleted file mode 100644 index aa6a25047..000000000 --- a/apps/numerals/numerals-default.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - color:0, - drawMode:"fill", - menuButton:22 -} \ No newline at end of file From a8d1ef3e53b35732bb8871956d1b2d1d13875d5e Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Fri, 17 Apr 2020 21:42:29 +0200 Subject: [PATCH 128/302] gpsrec: Save settings in data file, add track files to data --- apps.json | 7 +++++-- apps/gpsrec/ChangeLog | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/apps.json b/apps.json index 18fad4cfe..a18029abc 100644 --- a/apps.json +++ b/apps.json @@ -286,15 +286,18 @@ { "id": "gpsrec", "name": "GPS Recorder", "icon": "app.png", - "version":"0.07", + "version":"0.08", "interface": "interface.html", "description": "Application that allows you to record a GPS track. Can run in background", "tags": "tool,outdoors,gps,widget", "storage": [ {"name":"gpsrec.app.js","url":"app.js"}, - {"name":"gpsrec.json","url":"app-settings.json","evaluate":true}, {"name":"gpsrec.img","url":"app-icon.js","evaluate":true}, {"name":"gpsrec.wid.js","url":"widget.js"} + ], + "data": [ + {"name":"gpsrec.json"}, + {"wildcard":".gpsrc?","storageFile": true} ] }, { "id": "heart", diff --git a/apps/gpsrec/ChangeLog b/apps/gpsrec/ChangeLog index 8f1c575a1..17678bf3a 100644 --- a/apps/gpsrec/ChangeLog +++ b/apps/gpsrec/ChangeLog @@ -5,3 +5,5 @@ 0.05: Tweaks for variable size widget system 0.06: Ensure widget update itself (fix #118) and change to using icons 0.07: Added @jeffmer's awesome track viewer +0.08: Don't overwrite existing settings on app update + Clean up recorded tracks on app removal From e615b49cd027976ca9bceb869d18cab3e30d9d7f Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Fri, 17 Apr 2020 22:17:36 +0200 Subject: [PATCH 129/302] wohrm: fix `apps.json` version --- apps.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps.json b/apps.json index 0c97b9e57..7499b352c 100644 --- a/apps.json +++ b/apps.json @@ -890,7 +890,7 @@ { "id": "wohrm", "name": "Workout HRM", "icon": "app.png", - "version":"0.06", + "version":"0.07", "readme": "README.md", "description": "Workout heart rate monitor notifies you with a buzz if your heart rate goes above or below the set limits.", "tags": "hrm,workout", From 2278947b3d4b7a2ff0cea136bfa16d7bbb0a9497 Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Sat, 18 Apr 2020 00:51:22 +0200 Subject: [PATCH 130/302] App Manager: Add support for data files --- apps.json | 2 +- apps/files/ChangeLog | 1 + apps/files/files.js | 94 +++++++++++++++++++++++++++++++++----------- 3 files changed, 74 insertions(+), 23 deletions(-) diff --git a/apps.json b/apps.json index 0c97b9e57..bac885598 100644 --- a/apps.json +++ b/apps.json @@ -319,7 +319,7 @@ { "id": "files", "name": "App Manager", "icon": "files.png", - "version":"0.02", + "version":"0.03", "description": "Show currently installed apps, free space, and allow their deletion from the watch", "tags": "tool,system,files", "storage": [ diff --git a/apps/files/ChangeLog b/apps/files/ChangeLog index 8b7be7640..1140000fe 100644 --- a/apps/files/ChangeLog +++ b/apps/files/ChangeLog @@ -1 +1,2 @@ 0.02: Fix deletion of apps - now use files list in app.info (fix #262) +0.03: Add support for data files diff --git a/apps/files/files.js b/apps/files/files.js index 4775d35d0..ef0481f0c 100644 --- a/apps/files/files.js +++ b/apps/files/files.js @@ -30,29 +30,80 @@ function showMainMenu() { return E.showMenu(mainmenu); } -function eraseApp(app) { - E.showMessage('Erasing\n' + app.name + '...'); +function isGlob(f) {return /[?*]/.test(f)} +function globToRegex(pattern) { + const ESCAPE = '.*+-?^${}()|[]\\'; + const regex = pattern.replace(/./g, c => { + switch (c) { + case '?': return '.'; + case '*': return '.*'; + default: return ESCAPE.includes(c) ? ('\\' + c) : c; + } + }); + return new RegExp('^'+regex+'$'); +} + +function eraseFiles(app) { app.files.split(",").forEach(f=>storage.erase(f)); } +function eraseData(app) { + if(!app.data) return; + const d=app.data.split(';'), + files=d[0].split(','), + sFiles=(d[1]||'').split(','); + let erase = f=>storage.erase(f); + files.forEach(f=>{ + if (!isGlob(f)) erase(f); + else storage.list(globToRegex(f)).forEach(erase); + }) + erase = sf=>storage.open(sf,'r').erase(); + sFiles.forEach(sf=>{ + if (!isGlob(sf)) erase(sf); + else storage.list(globToRegex(sf+'\u0001')) + .forEach(fs=>erase(fs.substring(0,fs.length-1))); + }) +} +function eraseApp(app, files,data) { + E.showMessage('Erasing\n' + app.name + '...'); + if (files) eraseFiles(app) + if (data) eraseData(app) +} +function eraseOne(app, files,data){ + E.showPrompt('Erase\n'+app.name+'?').then((v) => { + if (v) { + Bangle.buzz(100, 1); + eraseApp(app, files,data) + showApps(); + } else { + showAppMenu(app) + } + }) +} +function eraseAll(apps, files,data) { + E.showPrompt('Erase all?').then((v) => { + if (v) { + Bangle.buzz(100, 1); + for(var n = 0; n m = showApps(), - 'Erase': () => { - E.showPrompt('Erase\n' + app.name + '?').then((v) => { - if (v) { - Bangle.buzz(100, 1); - eraseApp(app); - m = showApps(); - } else { - m = showAppMenu(app) - } - }); - } - }; + } + if (app.data) { + appmenu['Erase Completely'] = () => eraseOne(app, true, true) + appmenu['Erase App,Keep Data'] = () => eraseOne(app,true, false) + appmenu['Only Erase Data'] = () => eraseOne(app,false, true) + } else { + appmenu['Erase'] = () => eraseOne(app,true, false) + } return E.showMenu(appmenu); } @@ -78,13 +129,12 @@ function showApps() { return menu; }, appsmenu); appsmenu['Erase All'] = () => { - E.showPrompt('Erase all?').then((v) => { - if (v) { - Bangle.buzz(100, 1); - for (var n = 0; n < list.length; n++) - eraseApp(list[n]); - } - m = showApps(); + E.showMenu({ + '': {'title': 'Erase All'}, + 'Erase Everything': () => eraseAll(list, true, true), + 'Erase Apps,Keep Data': () => eraseAll(list, true, false), + 'Only Erase Data': () => eraseAll(list, false, true), + '< Back': () => showApps(), }); }; } else { From c0f9c8b3e28b56680037a74f1666f0bac59c3cd9 Mon Sep 17 00:00:00 2001 From: Paul Cockrell Date: Sat, 18 Apr 2020 00:38:12 +0100 Subject: [PATCH 131/302] Store GPS coords, for use on next start if user desires --- apps.json | 2 +- apps/astrocalc/ChangeLog | 2 ++ apps/astrocalc/astrocalc-app.js | 51 +++++++++++++++++++++++++++++---- 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/apps.json b/apps.json index bbe0bbfcd..60b47dbd8 100644 --- a/apps.json +++ b/apps.json @@ -1018,7 +1018,7 @@ { "id": "astrocalc", "name": "Astrocalc", "icon": "astrocalc.png", - "version":"0.01", + "version":"0.02", "description": "Calculates interesting information on the sun and moon cycles for the current day based on your location.", "tags": "app,sun,moon,cycles,tool,outdoors", "allow_emulator":true, diff --git a/apps/astrocalc/ChangeLog b/apps/astrocalc/ChangeLog index 0c8adeb61..188fc287b 100644 --- a/apps/astrocalc/ChangeLog +++ b/apps/astrocalc/ChangeLog @@ -1 +1,3 @@ 0.01: Create astrocalc app +0.02: Store last GPS lock, can be used instead of waiting for new GPS on +start. diff --git a/apps/astrocalc/astrocalc-app.js b/apps/astrocalc/astrocalc-app.js index 318147b13..0b0c63658 100644 --- a/apps/astrocalc/astrocalc-app.js +++ b/apps/astrocalc/astrocalc-app.js @@ -1,8 +1,18 @@ /** + * BangleJS ASTROCALC + * * Inspired by: https://www.timeanddate.com + * + * Original Author: Paul Cockrell https://github.com/paulcockrell + * Created: April 2020 + * + * Calculate the Sun and Moon positions based on watch GPS and display graphically */ const SunCalc = require("suncalc.js"); +const storage = require("Storage"); +const LAST_GPS_FILE = "astrocalc.gps.json"; +let lastGPS = (storage.readJSON(LAST_GPS_FILE, 1) || null); function drawMoon(phase, x, y) { const moonImgFiles = [ @@ -296,22 +306,50 @@ function indexPageMenu(gps) { return E.showMenu(menu); } +function getCenterStringX(str) { + return (g.getWidth() - g.stringWidth(str)) / 2; +} + /** * GPS wait page, shows GPS locating animation until it gets a lock, then moves to the Sun page */ function drawGPSWaitPage() { - const img = require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AW43GF1wwsFwYwqFwowoFw4wmFxIwdE5YAPF/4vM5nN6YAE5vMF8YtHGIgvhFpQxKF7AuOGA4vXFyAwGF63MFyIABF6xeWMC4UDLwvNGpAJG5gwSdhIIDRBLyWCIgcJHAgJJDoouQF4vMQoICBBJoeGFx6GGACIfHL6YvaX6gvZeCIdFc4gAFXogvGFxgwFDwovQCAguOGAnMMBxeG5guTGAggGGAwNKFySREcA3N5vM5gDBdpQvXEY4AKXqovGGCKbFF7AwPZQwvZGJgtGF7vGdQItG5gSIF7gASF/44WEzgwRF0wwHF1AwFF1QwDF1gvwAH4A/AFAA==")) - + const img = require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AW43GF1wwsFwYwqFwowoFw4wmFxIwdE5YAPF/4vM5nN6YAE5vMF8YtHGIgvhFpQxKF7AuOGA4vXFyAwGF63MFyIABF6xeWMC4UDLwvNGpAJG5gwSdhIIDRBLyWCIgcJHAgJJDoouQF4vMQoICBBJoeGFx6GGACIfHL6YvaX6gvZeCIdFc4gAFXogvGFxgwFDwovQCAguOGAnMMBxeG5guTGAggGGAwNKFySREcA3N5vM5gDBdpQvXEY4AKXqovGGCKbFF7AwPZQwvZGJgtGF7vGdQItG5gSIF7gASF/44WEzgwRF0wwHF1AwFF1QwDF1gvwAH4A/AFAA==")); + const str1 = "Astrocalc v0.02"; + const str2 = "Locating GPS"; + const str3 = "Please wait..."; + g.clear(); g.drawImage(img, 100, 50); g.setFont("6x8", 1); - g.drawString("Astrocalc v0.01", 80, 105); - g.drawString("Locating GPS", 85, 140); - g.drawString("Please wait...", 80, 155); + g.drawString(str1, getCenterStringX(str1), 105); + g.drawString(str2, getCenterStringX(str2), 140); + g.drawString(str3, getCenterStringX(str3), 155); + + if (lastGPS) { + lastGPS = JSON.parse(lastGPS); + lastGPS.time = new Date(); + + const str4 = "Press Button 3 to use last GPS"; + g.setColor("#d32e29"); + g.fillRect(0, 190, g.getWidth(), 215); + g.setColor("#ffffff"); + g.drawString(str4, getCenterStringX(str4), 200); + + setWatch(() => { + clearWatch(); + Bangle.setGPSPower(0); + lastGPS.time = new Date(); + m = indexPageMenu(lastGPS); + }, BTN3, {repeat: false}); + } + g.flip(); const DEBUG = false; if (DEBUG) { + clearWatch(); + const gps = { "lat": 56.45783133333, "lon": -3.02188583333, @@ -330,7 +368,10 @@ function drawGPSWaitPage() { Bangle.on('GPS', (gps) => { if (gps.fix === 0) return; + clearWatch(); + if (isNaN(gps.course)) gps.course = 0; + require("Storage").writeJSON(LAST_GPS_FILE, JSON.stringify(gps)); Bangle.setGPSPower(0); Bangle.buzz(); Bangle.setLCDPower(true); From 173969839e0a80e96b2d55065dc82382ce536cc7 Mon Sep 17 00:00:00 2001 From: Paul Cockrell Date: Sat, 18 Apr 2020 00:39:32 +0100 Subject: [PATCH 132/302] Fix Changelog --- apps/astrocalc/ChangeLog | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/astrocalc/ChangeLog b/apps/astrocalc/ChangeLog index 188fc287b..60ef5da0a 100644 --- a/apps/astrocalc/ChangeLog +++ b/apps/astrocalc/ChangeLog @@ -1,3 +1,2 @@ 0.01: Create astrocalc app -0.02: Store last GPS lock, can be used instead of waiting for new GPS on -start. +0.02: Store last GPS lock, can be used instead of waiting for new GPS on start From e052a6e65fdaad4c14c99798ea7dd316cadf7345 Mon Sep 17 00:00:00 2001 From: Paul Cockrell Date: Sat, 18 Apr 2020 00:41:15 +0100 Subject: [PATCH 133/302] Remove repeated assignment of date to gps object --- apps/astrocalc/astrocalc-app.js | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/astrocalc/astrocalc-app.js b/apps/astrocalc/astrocalc-app.js index 0b0c63658..6b848abda 100644 --- a/apps/astrocalc/astrocalc-app.js +++ b/apps/astrocalc/astrocalc-app.js @@ -339,7 +339,6 @@ function drawGPSWaitPage() { setWatch(() => { clearWatch(); Bangle.setGPSPower(0); - lastGPS.time = new Date(); m = indexPageMenu(lastGPS); }, BTN3, {repeat: false}); } From bf4a3e0321abc7007a1fc1acede9616ff4a9cd49 Mon Sep 17 00:00:00 2001 From: Fredrik Lautrup Date: Sat, 18 Apr 2020 09:01:15 +0200 Subject: [PATCH 134/302] Added rclock app --- apps.json | 12 +++ apps/rclock/ChangeLog | 1 + apps/rclock/app-icon.js | 1 + apps/rclock/app.png | Bin 0 -> 1620 bytes apps/rclock/rclock.app.js | 163 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 177 insertions(+) create mode 100644 apps/rclock/ChangeLog create mode 100644 apps/rclock/app-icon.js create mode 100644 apps/rclock/app.png create mode 100644 apps/rclock/rclock.app.js diff --git a/apps.json b/apps.json index bbe0bbfcd..ac7181bb4 100644 --- a/apps.json +++ b/apps.json @@ -1291,6 +1291,18 @@ "name": "dane.img", "url": "app-icon.js", "evaluate": true + }, + { "id": "rclock", + "name": "Round clock with seconds, minutes and date", + "shortName":"Round Clock", + "icon": "app.png", + "version":"0.01", + "description": "Designed round clock with ticks for minutes and seconds", + "tags": "", + "storage": [ + {"name":"rclock.app.js","url":"app.js"}, + {"name":"rclock.img","url":"app-icon.js","evaluate":true} + ] } ] } diff --git a/apps/rclock/ChangeLog b/apps/rclock/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/rclock/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/rclock/app-icon.js b/apps/rclock/app-icon.js new file mode 100644 index 000000000..49232b838 --- /dev/null +++ b/apps/rclock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwJC/AH4A/AH4AgA==")) diff --git a/apps/rclock/app.png b/apps/rclock/app.png new file mode 100644 index 0000000000000000000000000000000000000000..582cb2e0853a5a2899a3afbd7eb19cde2ee7f6a0 GIT binary patch literal 1620 zcmV-a2CMmrP)1gXjloC|3_d8m;N2OpV(|i0q4YwBna<2! zK9thw%-*|urnNbV{Gax^?eD+#{x0kLJ~)lj_;W+1>qV*k8akT^^dvctZccUyj4}H~#M%Wwee_v` zHMv7o%BM8@dBrLshn{wGD9BDl?^eV5vSM3T96;NnHvtc6La=(qzq)xrX1d8bK-TN- zrd_f$_O`9nEmS+_S7HTXK<&u;LDIW|qlN&KJvM}tt6TVVqL-AvNv`B*{NzNpBfSQwQP5~Sf(Dp@Vq1+3Q`N9wBQN2`J_?M^u0FIMlt?p^8 z%U3%80kIwg!T{E9<8J18S&$k1`eO)@HP+=TZKo(z3_A3VFYJB=sn`2^Q$mRE>02(+W)np;)L1!GUvU2{O{<&F_nE6Qe#D~Xf|dD z+?d3-D1(IUiL`C2;PPv4CKw8H)v7h8^obJ&Z6D0CjVUe8Xq_NAymxUyPAMU^CCrIu z%1M71EC`5o2if_~7E&h??0jeQ1Y3N6p?}G72FmS*)xQD)%wBE=2tW6@(+MTi!fk9H1pWKew2(jTXVu4%vk26QvSQCbGmk`Z)Y! zBIhh)6vG2)h6mF8wC^|l$M(Eo9D?JiW}=_T2jUA>LC80foTera{^p)Wi`>}Gf;(|ZwEZQ zS^k|*9wyt=f4ZOo!xty7{%}HKD9tBZ50g$=%v&&vMa!#@Nsf>EkEEDA*ST6fiC+An zsNK1#>!x0obq@j$QqYU-ad3ZvbjqUU+%iw(0WahgmHV6yeLWqoYkSl4pzFQ(_Vp&I ztO{WI-48rGLwQb?#vgVvduyd9_6W)rFRoQJq3I(J?{Xmin45#=3l9BmL6Bp<*MZej zrsWN7oRPUr7IvrHoIHOjS=gPTCw>d)^LQK+B|=f2qbGjrWaOd5D<<9Dv>MTW0X3z> zyPy}9`<>1~?NCx@m8G$_@rRTy5zH12YM&P)=tU+L^fgY z^0Z&_6^qdVuwgN3wt_Ze(10?J@%{C2grBk42hsu74qEo^nd&v`X`IHN9lrxzS~GeF S(*#!l0000 Date: Sat, 18 Apr 2020 11:17:50 +0200 Subject: [PATCH 135/302] Fix error in app.json --- apps.json | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/apps.json b/apps.json index ac7181bb4..89fd1a0a3 100644 --- a/apps.json +++ b/apps.json @@ -1291,19 +1291,18 @@ "name": "dane.img", "url": "app-icon.js", "evaluate": true - }, - { "id": "rclock", - "name": "Round clock with seconds, minutes and date", - "shortName":"Round Clock", - "icon": "app.png", - "version":"0.01", - "description": "Designed round clock with ticks for minutes and seconds", - "tags": "", - "storage": [ - {"name":"rclock.app.js","url":"app.js"}, - {"name":"rclock.img","url":"app-icon.js","evaluate":true} - ] } ] - } + }, + { "id": "rclock", + "name": "Round clock with seconds, minutes and date", + "shortName":"Round Clock", + "icon": "app.png", + "version":"0.01", + "description": "Designed round clock with ticks for minutes and seconds", + "tags": "", + "storage": [ + {"name":"rclock.app.js","url":"app.js"}, + {"name":"rclock.img","url":"app-icon.js","evaluate":true} + ] ] From 8f562d817782203bdf250c7d60803b9ac3102b40 Mon Sep 17 00:00:00 2001 From: Fredrik Lautrup Date: Sat, 18 Apr 2020 11:21:14 +0200 Subject: [PATCH 136/302] Fix error in app.json --- apps.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apps.json b/apps.json index 89fd1a0a3..d7e857066 100644 --- a/apps.json +++ b/apps.json @@ -1305,4 +1305,5 @@ {"name":"rclock.app.js","url":"app.js"}, {"name":"rclock.img","url":"app-icon.js","evaluate":true} ] + } ] From 06705a6e5d0a3de5db7131c6a56684a917b6a710 Mon Sep 17 00:00:00 2001 From: Fredrik Lautrup Date: Sat, 18 Apr 2020 11:28:08 +0200 Subject: [PATCH 137/302] Fixes --- apps.json | 6 ++++-- apps/rclock/ChangeLog | 2 +- apps/rclock/rclock.app.js | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/apps.json b/apps.json index d7e857066..dc768218b 100644 --- a/apps.json +++ b/apps.json @@ -1294,13 +1294,15 @@ } ] }, - { "id": "rclock", + { + "id": "rclock", "name": "Round clock with seconds, minutes and date", "shortName":"Round Clock", "icon": "app.png", "version":"0.01", "description": "Designed round clock with ticks for minutes and seconds", - "tags": "", + "tags": "clock", + "type": "clock", "storage": [ {"name":"rclock.app.js","url":"app.js"}, {"name":"rclock.img","url":"app-icon.js","evaluate":true} diff --git a/apps/rclock/ChangeLog b/apps/rclock/ChangeLog index 5560f00bc..a8f708a0a 100644 --- a/apps/rclock/ChangeLog +++ b/apps/rclock/ChangeLog @@ -1 +1 @@ -0.01: New App! +0.01: First published version of app diff --git a/apps/rclock/rclock.app.js b/apps/rclock/rclock.app.js index 27fbda772..c681e0588 100644 --- a/apps/rclock/rclock.app.js +++ b/apps/rclock/rclock.app.js @@ -104,6 +104,7 @@ } first = false; } + // Reset seconds if (seconds == 59) { g.setColor('#000000'); From 7216b188672dfe4f5141c3a0470c38128f089cae Mon Sep 17 00:00:00 2001 From: Fredrik Lautrup Date: Sat, 18 Apr 2020 11:48:22 +0200 Subject: [PATCH 138/302] Fix error in reference to files --- apps.json | 2 +- package.json | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/apps.json b/apps.json index dc768218b..bf771882a 100644 --- a/apps.json +++ b/apps.json @@ -1304,7 +1304,7 @@ "tags": "clock", "type": "clock", "storage": [ - {"name":"rclock.app.js","url":"app.js"}, + {"name":"rclock.app.js","url":"rclock.app.js"}, {"name":"rclock.img","url":"app-icon.js","evaluate":true} ] } diff --git a/package.json b/package.json index 400385139..24793d86a 100644 --- a/package.json +++ b/package.json @@ -9,5 +9,8 @@ "scripts": { "test": "node bin/sanitycheck.js", "start": "npx http-server" + }, + "dependencies": { + "acorn": "^7.1.1" } } From a1f1da0341f4a8985e4f5baafcbdf1b1a1469f6a Mon Sep 17 00:00:00 2001 From: Fredrik Lautrup Date: Sat, 18 Apr 2020 11:52:30 +0200 Subject: [PATCH 139/302] Updated grey colors --- apps/rclock/rclock.app.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/rclock/rclock.app.js b/apps/rclock/rclock.app.js index c681e0588..405da7052 100644 --- a/apps/rclock/rclock.app.js +++ b/apps/rclock/rclock.app.js @@ -31,8 +31,8 @@ center: screen.center, }, circle: { - colormin: '#777777', - colorsec: '#444444', + colormin: '#999999', + colorsec: '#777777', width: 10, middle: screen.middle, center: screen.center, From afeff2c5761ed15d7164b1b3e338b7113ddee2b3 Mon Sep 17 00:00:00 2001 From: Fredrik Lautrup Date: Sat, 18 Apr 2020 11:55:40 +0200 Subject: [PATCH 140/302] Updated grey colors --- apps/rclock/rclock.app.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/rclock/rclock.app.js b/apps/rclock/rclock.app.js index 405da7052..25ca757cd 100644 --- a/apps/rclock/rclock.app.js +++ b/apps/rclock/rclock.app.js @@ -31,8 +31,8 @@ center: screen.center, }, circle: { - colormin: '#999999', - colorsec: '#777777', + colormin: '#bbbbbb', + colorsec: '#999999', width: 10, middle: screen.middle, center: screen.center, From 0a7a9e0b97652f96738c1a39e4bb6132c2aa9424 Mon Sep 17 00:00:00 2001 From: Fredrik Lautrup Date: Sat, 18 Apr 2020 12:02:01 +0200 Subject: [PATCH 141/302] Align fonts --- apps/rclock/rclock.app.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/rclock/rclock.app.js b/apps/rclock/rclock.app.js index 25ca757cd..bd8395116 100644 --- a/apps/rclock/rclock.app.js +++ b/apps/rclock/rclock.app.js @@ -20,7 +20,7 @@ font: 'Vector', size: 60, middle: screen.middle - 30, - center: screen.center + 5 , + center: screen.center, }, date: { color: '#f0af00', @@ -31,8 +31,8 @@ center: screen.center, }, circle: { - colormin: '#bbbbbb', - colorsec: '#999999', + colormin: '#eeeeee', + colorsec: '#bbbbbb', width: 10, middle: screen.middle, center: screen.center, @@ -149,6 +149,7 @@ // clean app screen g.clear(); + g.setFontAlign( 0, 0, 0); Bangle.loadWidgets(); Bangle.drawWidgets(); From 6d1e87a765de9a48dee5939d66aeff0a97f3760b Mon Sep 17 00:00:00 2001 From: Fredrik Lautrup Date: Sat, 18 Apr 2020 12:39:13 +0200 Subject: [PATCH 142/302] Added icons --- .DS_Store | Bin 0 -> 6148 bytes apps/.DS_Store | Bin 0 -> 6148 bytes apps/rclock/.DS_Store | Bin 0 -> 6148 bytes apps/rclock/app-icon.js | 2 +- apps/rclock/app.png | Bin 1620 -> 0 bytes apps/rclock/appp.png | Bin 0 -> 10357 bytes 6 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 .DS_Store create mode 100644 apps/.DS_Store create mode 100644 apps/rclock/.DS_Store delete mode 100644 apps/rclock/app.png create mode 100644 apps/rclock/appp.png diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..e9892b6bcf444f36764922b8e4fcf88ecca916a8 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4I%_5-~#-K2??Z%JxAx+@j_rm6?#VYi=A4u-_X<| zqMPSsEi#Bm4>y&Sg~k+lCr5eUH(oE}`EJ);>!>lNF#jv{Xd}rZGKew5pFG zR`+&jitBP{sao4bWBAbcXSFE?rnOx(A%SUjVITn#Xb8+}KG^xcgMXX3Z5S>vG!N$@uSMUZw^aNf&RBRL$6#K0_mq+u>r%+ZqEtEGfdC6p6LSC`6 zBO*G#>}Dbp5gEY^}dS)lX)N&paQ?HfPEhd+^{CLf&S^h;4J`ffUq0p-b(H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T01gXjloC|3_d8m;N2OpV(|i0q4YwBna<2! zK9thw%-*|urnNbV{Gax^?eD+#{x0kLJ~)lj_;W+1>qV*k8akT^^dvctZccUyj4}H~#M%Wwee_v` zHMv7o%BM8@dBrLshn{wGD9BDl?^eV5vSM3T96;NnHvtc6La=(qzq)xrX1d8bK-TN- zrd_f$_O`9nEmS+_S7HTXK<&u;LDIW|qlN&KJvM}tt6TVVqL-AvNv`B*{NzNpBfSQwQP5~Sf(Dp@Vq1+3Q`N9wBQN2`J_?M^u0FIMlt?p^8 z%U3%80kIwg!T{E9<8J18S&$k1`eO)@HP+=TZKo(z3_A3VFYJB=sn`2^Q$mRE>02(+W)np;)L1!GUvU2{O{<&F_nE6Qe#D~Xf|dD z+?d3-D1(IUiL`C2;PPv4CKw8H)v7h8^obJ&Z6D0CjVUe8Xq_NAymxUyPAMU^CCrIu z%1M71EC`5o2if_~7E&h??0jeQ1Y3N6p?}G72FmS*)xQD)%wBE=2tW6@(+MTi!fk9H1pWKew2(jTXVu4%vk26QvSQCbGmk`Z)Y! zBIhh)6vG2)h6mF8wC^|l$M(Eo9D?JiW}=_T2jUA>LC80foTera{^p)Wi`>}Gf;(|ZwEZQ zS^k|*9wyt=f4ZOo!xty7{%}HKD9tBZ50g$=%v&&vMa!#@Nsf>EkEEDA*ST6fiC+An zsNK1#>!x0obq@j$QqYU-ad3ZvbjqUU+%iw(0WahgmHV6yeLWqoYkSl4pzFQ(_Vp&I ztO{WI-48rGLwQb?#vgVvduyd9_6W)rFRoQJq3I(J?{Xmin45#=3l9BmL6Bp<*MZej zrsWN7oRPUr7IvrHoIHOjS=gPTCw>d)^LQK+B|=f2qbGjrWaOd5D<<9Dv>MTW0X3z> zyPy}9`<>1~?NCx@m8G$_@rRTy5zH12YM&P)=tU+L^fgY z^0Z&_6^qdVuwgN3wt_Ze(10?J@%{C2grBk42hsu74qEo^nd&v`X`IHN9lrxzS~GeF S(*#!l0000 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 TkibuO00000NkvXXu0mjfp7a2c literal 0 HcmV?d00001 From cdba3a32c22bc330ba64fd3ba7088349b49ad78f Mon Sep 17 00:00:00 2001 From: Fredrik Lautrup Date: Sat, 18 Apr 2020 12:42:03 +0200 Subject: [PATCH 143/302] FIx name error --- apps/rclock/{appp.png => app.png} | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename apps/rclock/{appp.png => app.png} (100%) diff --git a/apps/rclock/appp.png b/apps/rclock/app.png similarity index 100% rename from apps/rclock/appp.png rename to apps/rclock/app.png From 2f834a585632acdd4f0ea5fc78effcb86a4e0fb2 Mon Sep 17 00:00:00 2001 From: Fredrik Lautrup Date: Sat, 18 Apr 2020 13:00:59 +0200 Subject: [PATCH 144/302] Changes to permissions on file --- apps/rclock/.DS_Store | Bin 6148 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 apps/rclock/.DS_Store diff --git a/apps/rclock/.DS_Store b/apps/rclock/.DS_Store deleted file mode 100644 index 5008ddfcf53c02e82d7eee2e57c38e5672ef89f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 Date: Sat, 18 Apr 2020 13:06:12 +0200 Subject: [PATCH 145/302] Updated .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b83632eaa..f1811806d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .htaccess node_modules package-lock.json +.DS_Store From 3ed6448596080caa7ade69163e9482c8c45ad910 Mon Sep 17 00:00:00 2001 From: Fredrik Lautrup Date: Sat, 18 Apr 2020 13:08:23 +0200 Subject: [PATCH 146/302] Delete .DS_Store --- apps/.DS_Store | Bin 6148 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 apps/.DS_Store diff --git a/apps/.DS_Store b/apps/.DS_Store deleted file mode 100644 index f1e36c314ed1b52c7092fec1d7f2c0441196819d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKI|>3Z5S>vG!N$@uSMUZw^aNf&RBRL$6#K0_mq+u>r%+ZqEtEGfdC6p6LSC`6 zBO*G#>}Dbp5gEY^}dS)lX)N&paQ?HfPEhd+^{CLf&S^h;4J`ffUq0p-b( Date: Sat, 18 Apr 2020 13:09:31 +0200 Subject: [PATCH 147/302] Delete .DS_Store --- .DS_Store | Bin 6148 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index e9892b6bcf444f36764922b8e4fcf88ecca916a8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4I%_5-~#-K2??Z%JxAx+@j_rm6?#VYi=A4u-_X<| zqMPSsEi#Bm4>y&Sg~k+lCr5eUH(oE}`EJ);>!>lNF#jv{Xd}rZGKew5pFG zR`+&jitBP{sao4bWBAbcXSFE?rnOx(A%SUjVITn#Xb8+}KG^xcgMXX Date: Sat, 18 Apr 2020 15:34:35 +0200 Subject: [PATCH 148/302] shorter JS file name --- apps.json | 6 +++--- apps/hidcam/{hidcam.app.js => app.js} | 0 2 files changed, 3 insertions(+), 3 deletions(-) rename apps/hidcam/{hidcam.app.js => app.js} (100%) diff --git a/apps.json b/apps.json index 1e6b5e945..0546d6f65 100644 --- a/apps.json +++ b/apps.json @@ -1295,14 +1295,14 @@ ] }, { "id": "hidcam", - "name": "HID camera shutter", - "shortName":"HID cam", + "name": "Camera shutter", + "shortName":"Cam shutter", "icon": "app.png", "version":"0.01", "description": "Enable HID, connect to your phone, start your camera and trigger the shot on your Bangle", "tags": "tools", "storage": [ - {"name":"hidcam.app.js","url":"hidcam.app.js"}, + {"name":"hidcam.app.js","url":"app.js"}, {"name":"hidcam.img","url":"app-icon.js","evaluate":true} ] } diff --git a/apps/hidcam/hidcam.app.js b/apps/hidcam/app.js similarity index 100% rename from apps/hidcam/hidcam.app.js rename to apps/hidcam/app.js From 37e26d6139782852411b61328006860a38034c47 Mon Sep 17 00:00:00 2001 From: jeffmer Date: Sat, 18 Apr 2020 14:38:32 +0100 Subject: [PATCH 149/302] Create ChangeLog --- apps/gpsnav/ChangeLog | 1 + 1 file changed, 1 insertion(+) create mode 100644 apps/gpsnav/ChangeLog diff --git a/apps/gpsnav/ChangeLog b/apps/gpsnav/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/gpsnav/ChangeLog @@ -0,0 +1 @@ +0.01: New App! From ed51586dd0afdf9dd2df5c315e031c41ed62b162 Mon Sep 17 00:00:00 2001 From: jeffmer Date: Sat, 18 Apr 2020 14:39:36 +0100 Subject: [PATCH 150/302] Add files via upload --- apps/gpsnav/README.md | 66 ++++++++++ apps/gpsnav/app-icon.js | 1 + apps/gpsnav/app.js | 224 ++++++++++++++++++++++++++++++++ apps/gpsnav/first_screen.jpg | Bin 0 -> 65452 bytes apps/gpsnav/gpsnav.jpg | Bin 0 -> 47970 bytes apps/gpsnav/icon.png | Bin 0 -> 1887 bytes apps/gpsnav/marked_screen.jpg | Bin 0 -> 65895 bytes apps/gpsnav/select_screen.jpg | Bin 0 -> 64877 bytes apps/gpsnav/waypoint_screen.jpg | Bin 0 -> 67409 bytes apps/gpsnav/waypoints.json | 23 ++++ 10 files changed, 314 insertions(+) create mode 100644 apps/gpsnav/README.md create mode 100644 apps/gpsnav/app-icon.js create mode 100644 apps/gpsnav/app.js create mode 100644 apps/gpsnav/first_screen.jpg create mode 100644 apps/gpsnav/gpsnav.jpg create mode 100644 apps/gpsnav/icon.png create mode 100644 apps/gpsnav/marked_screen.jpg create mode 100644 apps/gpsnav/select_screen.jpg create mode 100644 apps/gpsnav/waypoint_screen.jpg create mode 100644 apps/gpsnav/waypoints.json diff --git a/apps/gpsnav/README.md b/apps/gpsnav/README.md new file mode 100644 index 000000000..80c6c1d00 --- /dev/null +++ b/apps/gpsnav/README.md @@ -0,0 +1,66 @@ +## gpsnav - navigate to waypoints + +The app is aimed at small boat navigation although it can also be used to mark the location of your car, bicycle etc and then get directions back to it. Please note that it would be foolish in the extreme to rely on this as your only boat navigation aid! + +The app displays direction of travel (course), speed, direction to waypoint (bearing) and distance to waypoint. The screen shot below is before the app has got a GPS fix. + +![](first_screen.jpg) + +The large digits are the course and speed. The top of the display is a linear compass which displays the direction of travel when a fix is received and you are moving. The blue text is the name of the current waypoint. NONE means that there is no waypoint set and so bearing and distance will remain at 0. To select a waypoint, press BTN2 (middle) and wait for the blue text to turn white. Then use BTN1 and BTN3 to select a waypoint. The waypoint choice is fixed by pressing BTN2 again. In the screen shot below a waypoint giving the location of Stone Henge has been selected. + +![](waypoint_screen.jpg) + +The display shows that Stone Henge is 108.75Km from the location where I made the screenshot and the direction is 255 degrees - approximately west. The display shows that I am currently moving approximately north - albeit slowly!. The position of the blue circle indicates that I need to turn left to get on course to Stone Henge. When the circle and red triangle line up you are on course and course will equal bearing. + +### Marking Waypoints + +The app lets you mark your current location as follows. There are vacant slots in the waypoint file which can be allocated a location. In the distributed waypoint file these are labelled WP0 to WP4. Select one of these - WP2 is shown below. + +![](select_screen.jpg) + +Bearing and distance are both zero as WP1 has currently no GPS location associated with it. To mark the location, press BTN2. + +![](marked_screen.jpg) + +The app indicates that WP2 is now marked by adding the prefix @ to it's name. The distance should be small as shown in the screen shot as you have just marked your current location. + +### Waypoint JSON file + +When the app is loaded from the app loader, a file named waypoints.json is loaded along with the javascript etc. The file has the following contents: + +~~~ +[ + { + "mark":0, + "name":"NONE" + }, + { + "mark":1, + "name":"No10", + "lat":51.5032, + "lon":-0.1269 + }, + { + "mark":1, + "name":"Stone", + "lat":51.1788, + "lon":-1.8260 + }, + { "name":"WP0" }, + { "name":"WP1" }, + { "name":"WP2" }, + { "name":"WP3" }, + { "name":"WP4" } +] +~~~ + +The file contains the initial NONE waypoint which is useful if you just want to display course and speed. The next two entries are waypoints to No 10 Downing Street and to Stone Henge - obtained from Google Maps. The last five entries are entries which can be *marked*. + +You add and delete entries using the Web IDE to load and then save the file from and to watch storage. The app itself does not limit the number of entries although it does load the entire file into RAM which will obviously limit this. + +I plan to release an accompanying watch app to edit waypoint files in the near future and a way to download your own waypoint file using the app loader. + + + + + diff --git a/apps/gpsnav/app-icon.js b/apps/gpsnav/app-icon.js new file mode 100644 index 000000000..890981d5a --- /dev/null +++ b/apps/gpsnav/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AFmACysDC9+IC6szC/8AgUgLwYXBPAgLDAA8kC5MyC5cyogXHmYiDURMkDAMzC4JgBmcyoAXMGANCC4YDBkgXMHwVEC4hQDC5kyF4kjJ4QAMOgMjC4eCohGNMARbCC4ODkilLAAQSBCYJ3EmYVLhAWCCgQaCAAUwCpowCFwYADIRAYHC4wZFRQIAGnAhJXgwAFxAYHwC9JFwiQCFhIZISAQwDX5sCoQTCDYUjUpAAFglElAXDmS9JAAtEoUyC4ckkbvMC4QQBC4YeBC5sEB4IXEkgfBJBkEH4QXCCYMkoQXMHwcIC4ZQCUpYMDC4oiBC5YEDC40AkCRNAAIXBCJ4X2URgAJhAXvCyoA/ACoA=")) \ No newline at end of file diff --git a/apps/gpsnav/app.js b/apps/gpsnav/app.js new file mode 100644 index 000000000..2a480410c --- /dev/null +++ b/apps/gpsnav/app.js @@ -0,0 +1,224 @@ +const Yoff = 40; +var pal2color = new Uint16Array([0x0000,0xffff,0x07ff,0xC618],0,2); +var buf = Graphics.createArrayBuffer(240,50,2,{msb:true}); + +function flip(b,y) { + g.drawImage({width:240,height:50,bpp:2,buffer:b.buffer, palette:pal2color},0,y); + b.clear(); +} + +var brg=0; +var wpindex=0; +const labels = ["N","NE","E","SE","S","SW","W","NW"]; + +function drawCompass(course) { + buf.setColor(1); + buf.setFont("Vector",16); + var start = course-90; + if (start<0) start+=360; + buf.fillRect(28,45,212,49); + var xpos = 30; + var frag = 15 - start%15; + if (frag<15) xpos+=frag; else frag = 0; + for (var i=frag;i<=180-frag;i+=15){ + var res = start + i; + if (res%90==0) { + buf.drawString(labels[Math.floor(res/45)%8],xpos-8,0); + buf.fillRect(xpos-2,25,xpos+2,45); + } else if (res%45==0) { + buf.drawString(labels[Math.floor(res/45)%8],xpos-12,0); + buf.fillRect(xpos-2,30,xpos+2,45); + } else if (res%15==0) { + buf.fillRect(xpos,35,xpos+1,45); + } + xpos+=15; + } + if (wpindex!=0) { + var bpos = brg - course; + if (bpos>180) bpos -=360; + if (bpos<-180) bpos +=360; + bpos+=120; + if (bpos<30) bpos = 14; + if (bpos>210) bpos = 226; + buf.setColor(2); + buf.fillCircle(bpos,40,8); + } + flip(buf,Yoff); +} + +//displayed heading +var heading = 0; +function newHeading(m,h){ + var s = Math.abs(m - h); + var delta = 1; + if (s<2) return h; + if (m > h){ + if (s >= 180) { delta = -1; s = 360 - s;} + } else if (m <= h){ + if (s < 180) delta = -1; + else s = 360 -s; + } + delta = delta * (1 + Math.round(s/15)); + heading+=delta; + if (heading<0) heading += 360; + if (heading>360) heading -= 360; + return heading; +} + +var course =0; +var speed = 0; +var satellites = 0; +var wp; +var dist=0; + +function radians(a) { + return a*Math.PI/180; +} + +function degrees(a) { + var d = a*180/Math.PI; + return (d+360)%360; +} + +function bearing(a,b){ + var delta = radians(b.lon-a.lon); + var alat = radians(a.lat); + var blat = radians(b.lat); + var y = Math.sin(delta) * Math.cos(blat); + var x = Math.cos(alat)*Math.sin(blat) - + Math.sin(alat)*Math.cos(blat)*Math.cos(delta); + return Math.round(degrees(Math.atan2(y, x))); +} + +function distance(a,b){ + var x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2)); + var y = radians(b.lat-a.lat); + return Math.round(Math.sqrt(x*x + y*y) * 6371000); +} + +var selected = false; + +function drawN(){ + buf.setColor(1); + buf.setFont("6x8",2); + buf.drawString("o",100,0); + buf.setFont("6x8",1); + buf.drawString("kph",220,40); + buf.setFont("Vector",40); + var cs = course.toString(); + cs = course<10?"00"+cs : course<100 ?"0"+cs : cs; + buf.drawString(cs,10,0); + var txt = (speed<10) ? speed.toFixed(1) : Math.round(speed); + buf.drawString(txt,140,4); + flip(buf,Yoff+70); + buf.setColor(1); + buf.setFont("Vector",20); + var bs = brg.toString(); + bs = brg<10?"00"+bs : brg<100 ?"0"+bs : bs; + buf.setColor(3); + buf.drawString("Brg: ",0,0); + buf.drawString("Dist: ",0,30); + buf.setColor(selected?1:2); + buf.drawString(wp.name,140,0); + buf.setColor(1); + buf.drawString(bs,60,0); + if (dist<1000) + buf.drawString(dist.toString()+"m",60,30); + else + buf.drawString((dist/1000).toFixed(2)+"Km",60,30); + flip(buf,Yoff+130); + g.setFont("6x8",1); + g.setColor(0,0,0); + g.fillRect(10,230,60,239); + g.setColor(1,1,1); + g.drawString("Sats " + satellites.toString(),10,230); +} + +var savedfix; + +function onGPS(fix) { + savedfix = fix; + if (fix!==undefined){ + course = isNaN(fix.course) ? course : Math.round(fix.course); + speed = isNaN(fix.speed) ? speed : fix.speed; + satellites = fix.satellites; + } + if (Bangle.isLCDOn()) { + if (fix!==undefined && fix.fix==1){ + dist = distance(fix,wp); + if (isNaN(dist)) dist = 0; + brg = bearing(fix,wp); + if (isNaN(brg)) brg = 0; + } + drawN(); + } +} + +var intervalRef; + +function clearTimers() { + if(intervalRef) {clearInterval(intervalRef);} +} + +function startTimers() { + intervalRefSec = setInterval(function() { + newHeading(course,heading); + if (course!=heading) drawCompass(heading); + },200); +} + +Bangle.on('lcdPower',function(on) { + if (on) { + g.clear(); + Bangle.drawWidgets(); + startTimers(); + drawAll(); + }else { + clearTimers(); + } +}); + +function drawAll(){ + g.setColor(1,0.5,0.5); + g.fillPoly([120,Yoff+50,110,Yoff+70,130,Yoff+70]); + g.setColor(1,1,1); + drawN(); + drawCompass(heading); +} + +var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"NONE"}]; +wp=waypoints[0]; + +function nextwp(inc){ + if (!selected) return; + wpindex+=inc; + if (wpindex>=waypoints.length) wpindex=0; + if (wpindex<0) wpindex = waypoints.length-1; + wp = waypoints[wpindex]; + drawN(); +} + +function doselect(){ + if (selected && waypoints[wpindex].mark===undefined && savedfix.fix) { + waypoints[wpindex] ={mark:1, name:"@"+wp.name, lat:savedfix.lat, lon:savedfix.lon}; + wp = waypoints[wpindex]; + require("Storage").writeJSON("waypoints.json", waypoints); + } + selected=!selected; + drawN(); +} + +g.clear(); +Bangle.setLCDBrightness(1); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +// load widgets can turn off GPS +Bangle.setGPSPower(1); +drawAll(); +startTimers(); +Bangle.on('GPS', onGPS); +// Toggle selected +setWatch(nextwp.bind(null,-1), BTN1, {repeat:true,edge:"falling"}); +setWatch(doselect, BTN2, {repeat:true,edge:"falling"}); +setWatch(nextwp.bind(null,1), BTN3, {repeat:true,edge:"falling"}); + diff --git a/apps/gpsnav/first_screen.jpg b/apps/gpsnav/first_screen.jpg new file mode 100644 index 0000000000000000000000000000000000000000..34fbe1b506bf3b8951abadeac5589b11b202dc0f GIT binary patch literal 65452 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 AZvX%Q literal 0 HcmV?d00001 diff --git a/apps/gpsnav/gpsnav.jpg b/apps/gpsnav/gpsnav.jpg new file mode 100644 index 0000000000000000000000000000000000000000..975fe39032cb28fc755874e231842684fe3a73bb GIT binary patch literal 47970 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

M#X#R-X}cOVGl^%PqF>L!1JtcqDhm zdBLy7-w$}lTh?JWa<;=1OtJt-8<=p!bq5@A#ct`J5PU^rWj>h(=MU5zGo3k0<4I@1|hjEoZ}o0)p&eV&{}D% ze$Y}-{3M4VqwZH8g!Zq=tIrkqgG$nL>wPxxUrBur%^M7PMF=5%2yFDgti3nlFN!tY zay<)CMz^@O-x|7P`E18I1F81p;8jP&JQwn_8F*WmEpEQ^_3AH)I#srlJkZ@qaLI;M z;|MnuVh6bOtyuhNr_B|!OMFU9>>fg+DTFLHj)3H2K9%_!rg)RYFi9-`0B>bgY%nZ- zY-b>Y$G<&2>!G&zgRf}13l_dIWz1-tgStVSpYJX|D%L(AU^?8wei!B)8bANk{zvpl zPOE(bUI4`4amU^7^{qCzl5J{I+!Y`UdK_l4{3rhaAFzHkqMz~^pZ(?jV!Y;h1*jB@ zv0_<_WEa6FuOhW=blXdb=1CXstAIyd7b3G_{{WD(f4F~{u7^$k0FSGWqyGTt6|BJ3 z(HqOs;kIGEIY|r~oZxX>b^A$YZ!N8}6knL|eGe7a=^ydqYyR_pBV2y5)?fEu`O=7^ zvCi7uNp&P^HzI83wmy}a@fSeTMU+~CCfNcmNWjn6zw1{t-}TnNjb8YR{{SFJ{{Y-y z=T|L-J&z@`fg`pBayOhG%9_!xcCjoEud&5gw*LT+RQ~{b`c!TA$K(G1p-~l~o7oVU zm*v~WH)9^(=bE8~Wnw_V86&aAc=}eE?f(D+ti}6{{^_i)E`>QQAu)zwl1c0ADX+(kAb+hN@Gs_Tam(Q~Srs7+qn)9-=bn`7)s7oje6<|ppL4q(%85VJ z5BCT2rPROU)(^-307I#eLvl@ss*Ixr@ssrb0QFUgAPX2Fj|6_ap4Ah5$bS#yYUJPQ zht+C?_b`O>?0FT+pOo=|)YAErm6An~l!shn9B_TA75@NG{{Te(bs+x$Qz!fD{HnVd zK-aTRHX}TQmKYrO`MaLAy8D*SA(DTLGF;^s`lT1@in5~>X-hyN&KrQd6I5gK_1Y60HbPv z0LBkE$4d2EJ9w;gg}Ai=T6Sc90pkPISD0FV?8*GA)rb6mZT|r4tA8O{)7aW8L(+ax zWd*vZ92~Iw#3{mpeFbLQ*us28HkeC!M;-aW86!TKtt0-b52OD8=v583{Ekxn6aN4~ ztt3&dY~Zx=C4gb2kz;e*sa*b@tBuli8{H;(ZR}(ylOLFN`MUmOS9y2$Er09bKlB>& zX#W7?_wgUria!usmg4T?Qqm`B*oH=83i}*)$o(s^ourS#7B=d~W3+pRuI^26w*LU~ z2fF&V{)Ju5*Zhk5cl*(QDzyFMDQ?d1>{iLH+S;o8rqj$myNMm^GSf_tUAfe)Z{(22 z6YUBQ1P@$;Tdn^9BQ>Y}c(4A1DyN430OTX9f7eE>B-tx-nA5CaeKPXPkcbNmaB>O! z>k3(3Ee7Tx$WCKsNVHQ8(5de{EDC-SZ;@BTj1 O{<@#%S|_lrq5s*41p!O| literal 0 HcmV?d00001 diff --git a/apps/gpsnav/icon.png b/apps/gpsnav/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f899683d1d7e98b906c52bb0a5913de6f4dd2534 GIT binary patch literal 1887 zcmV-l2cY`(ry)@ zWg;nB2-><0R?!l+PBTp>?PycSfGwlYQ9@gxDi^5^Y!XysZ9`Hv!b-Z9ktKmZOG1;1 zCXVAvocMaqw;z!+C)oJ-up{jU&#!a&-tT!Hzu)Ekj)D8QkN;mB`Dd_r^P7X;l9Pjx zvvYyRCa<4=UQP~$oSjq9a;Gm;A}5Dz8F=jyHG+^e4tG$u`R$Hae0FUrS0IWbM zCMNh7<>Hae0420MdhPLrpOlM776XTGP!)j3qxZL6fv%nM{B=n=IV+3j}L&T$&Cy2NL|Z1ywhXg^}xUB_|UUE>&v^4ZV4e*T$c z6s_+xJvRAy_unUgh*(*o4OCWE^79H7A57O|{v>Vin)i>jHE|~6KF0_i~U{%gVDj4ZUBY__!88|{>mj)A_OwZjAb6Qdc%y{TwQc@h&! z_AV;2ps;i~JT9kU=(Fn$2X<{AJXZVS1^@t0HzkU7Pw+bc;M}REg{}66dO;Ab0H_$~ z>%r$;m!Z$i{Uyh7LP~m4H2?su_Qt;gcp^#+01RPZ$jiH`*m{Uig{nB(cK^w~>A0LM zrN6G|9E>5Rr_ zrRtEp6F*V8y`O--<>0kA<1I#mx*DJO7bK0#$>3@ar!=X60a&UV13EVc}j)QO~NW$2w~pc zy+YAJ2%$&;q71A*(XZC>?6a=%@Qz_u@F8Cy!rrZYtk9@|c;e&88KIQEM+t3FWUJoa zy_f_g8+f{YxG*62cX=iwPu+F~^MesSrEEn7??2ghM^;2)54iURXPNN16( z*pQV149!2raYX)NE{fm3@PqC# zpWmV`Tm{wFSE4hp039*nftdyX;9~D6h8-R(%bvvQ%r9Y5j33)jvF$mj8YJ(;$6l^k z=N!3h5qx8qymA)$tAB>W;Yk{|LVXM0^}L1htZT?&$3ZB5_}f*u?mbo3u)QLNX!JZPI3Z z#IRGeF$W^TgZ@T}mXr+~t=;p2*Ewp85l;cM2n?}2R$0B%pi^B+6ieB(mOZb&v1Vam zNttQxysNP$2=Ms(EsfSx^`v|ra65)yi4osQ1ZuNxt^PFnUZ2Sx2e3Y_0g%%Gu;Z2e z7Xg@${$kJ5KG)chfxeyx0HCmR`94bNs#Ge{l$OyWw&xG3)EPf3u#|QZ2rpSHx3|S> z_aN6B9-oXTfEml^V9mqIh4I9_{!-0Om(#J|IA^Y|pk&#W4bN1c601q{5aKvasnzM% z6+X1I(ppiqI+1t)n4?o&oBfP{oKE!1KDY1H`r6~o`UoGk4c)l<^Y-?3(IZ2&0W+Xn zxW-y$t*B~Amg1~*_MO?!C>72m8{bdZxSvkZ{buANgnzX+}uL(%f;Ug8Y8OUJO z2bigCnGch+vvTpsie>w*yAO2+&dbRm>%Wvnjq(BkK9rL~Rs&h->}!;uUrvtuxQ~0p Ze*jRY!kx=d0W$yq002ovPDHLkV1iy}i`f7G literal 0 HcmV?d00001 diff --git a/apps/gpsnav/marked_screen.jpg b/apps/gpsnav/marked_screen.jpg new file mode 100644 index 0000000000000000000000000000000000000000..accd3b15fe5fa0996c51ada2f75ddd416dd17394 GIT binary patch literal 65895 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%0D85DN}Kju4Ys-^?RDWah8DK1Yt!XB1!a-x5(? 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

SKQuyodtT#X^!9_vAdcfo!M@gV% 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

2$hmvlvTQ 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 literal 0 HcmV?d00001 diff --git a/apps/gpsnav/waypoints.json b/apps/gpsnav/waypoints.json new file mode 100644 index 000000000..143316b19 --- /dev/null +++ b/apps/gpsnav/waypoints.json @@ -0,0 +1,23 @@ +[ + { + "mark":0, + "name":"NONE" + }, + { + "mark":1, + "name":"No10", + "lat":51.5032, + "lon":-0.1269 + }, + { + "mark":1, + "name":"Stone", + "lat":51.1788, + "lon":-1.8260 + }, + { "name":"WP0" }, + { "name":"WP1" }, + { "name":"WP2" }, + { "name":"WP3" }, + { "name":"WP4" } +] \ No newline at end of file From 73366a600cdf52d0f108f2aacd7542ee502bb583 Mon Sep 17 00:00:00 2001 From: jeffmer Date: Sat, 18 Apr 2020 14:44:00 +0100 Subject: [PATCH 151/302] Update apps.json --- apps.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/apps.json b/apps.json index 0c97b9e57..0b7001a5e 100644 --- a/apps.json +++ b/apps.json @@ -291,6 +291,18 @@ {"name":"gpsrec.wid.js","url":"widget.js"} ] }, + { "id": "gpsnav", + "name": "GPS Navigation", + "icon": "icon.png", + "version":"0.01", + "description": "Displays GPS Course and Speed, + Directions to waypoint and waypoint recording", + "tags": "tool,outdoors,gps", + "storage": [ + {"name":"gpsnav.app.js","url":"app.js"}, + {"name":"waypoints.json","url":"waypoints.json","evaluate":false}, + {"name":"gpsnav.img","url":"app-icon.js","evaluate":true} + ] + }, { "id": "heart", "name": "Heart Rate Recorder", "icon": "app.png", From 696afd7f6bae48410d27df696298b239b31c1f3f Mon Sep 17 00:00:00 2001 From: paul Date: Sat, 18 Apr 2020 16:08:33 +0200 Subject: [PATCH 152/302] ble hid control is removed from require and is now hard written in app --- apps/hidcam/app.js | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/apps/hidcam/app.js b/apps/hidcam/app.js index 6a4150673..f529deff2 100644 --- a/apps/hidcam/app.js +++ b/apps/hidcam/app.js @@ -1,16 +1,46 @@ -var controls = require("ble_hid_controls"); var storage = require('Storage'); const settings = storage.readJSON('setting.json',1) || { HID: false }; +// hidcontrol module selective and manual import : +report = new Uint8Array([ + 0x05, 0x0c, // USAGE_PAGE (Consumer Devices) + 0x09, 0x01, // USAGE (Consumer Control) + 0xa1, 0x01, // COLLECTION (Application) + // -------------------- common global items + 0x15, 0x00, // LOGICAL_MINIMUM (0) + 0x25, 0x01, // LOGICAL_MAXIMUM (1) + 0x75, 0x01, // REPORT_SIZE (1) - each field occupies 1 bit + // -------------------- misc bits + 0x95, 0x05, // REPORT_COUNT (5) + 0x09, 0xb5, // USAGE (Scan Next Track) + 0x09, 0xb6, // USAGE (Scan Previous Track) + 0x09, 0xb7, // USAGE (Stop) + 0x09, 0xcd, // USAGE (Play/Pause) + 0x09, 0xe2, // USAGE (Mute) + 0x81, 0x06, // INPUT (Data,Var,Rel) - relative inputs + // -------------------- volume up/down bits + 0x95, 0x02, // REPORT_COUNT (2) + 0x09, 0xe9, // USAGE (Volume Up) + 0x09, 0xea, // USAGE (Volume Down) + 0x81, 0x02, // INPUT (Data,Var,Abs) - absolute inputs + // -------------------- padding bit + 0x95, 0x01, // REPORT_COUNT (1) + 0x81, 0x01, // INPUT (Cnst,Ary,Abs) + 0xc0 // END_COLLECTION +]); +function p(c,cb) { NRF.sendHIDReport(c, function() { NRF.sendHIDReport(0, cb) }); } +volumeUp = function(cb) { p(0x20,cb) }; +//end of manual selective import + g.clear(); E.showMessage('BTN2 to trigger','camTrigger'); Bangle.loadWidgets(); Bangle.drawWidgets(); if (settings.HID) { - NRF.setServices(undefined, { hid : controls.report }); - shotTrigger = function() {controls.volumeUp();}; + NRF.setServices(undefined, { hid : report }); + shotTrigger = function() {volumeUp();}; } else { E.showMessage('HID disabled'); setTimeout(load, 1000); From 41d206cdbe03461808b54f71149f6598b7ac0a24 Mon Sep 17 00:00:00 2001 From: paul Date: Sat, 18 Apr 2020 16:24:33 +0200 Subject: [PATCH 153/302] typos in code and no more useless timeout --- apps/hidcam/app.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/hidcam/app.js b/apps/hidcam/app.js index f529deff2..9117d94f5 100644 --- a/apps/hidcam/app.js +++ b/apps/hidcam/app.js @@ -33,6 +33,8 @@ function p(c,cb) { NRF.sendHIDReport(c, function() { NRF.sendHIDReport(0, cb) }) volumeUp = function(cb) { p(0x20,cb) }; //end of manual selective import +NRF.setServices(undefined, { hid : report }); + g.clear(); E.showMessage('BTN2 to trigger','camTrigger'); Bangle.loadWidgets(); @@ -50,7 +52,6 @@ setWatch(function(e){ E.showMessage('capture'); Bangle.beep(); shotTrigger(); - set.Timeout(load,1000); g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); From fc6d6aa4bbe1fdfefade4a0e85ab96460d1feda2 Mon Sep 17 00:00:00 2001 From: paul Date: Sat, 18 Apr 2020 16:41:43 +0200 Subject: [PATCH 154/302] hid code correction --- apps/hidcam/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/hidcam/app.js b/apps/hidcam/app.js index 9117d94f5..e114863cc 100644 --- a/apps/hidcam/app.js +++ b/apps/hidcam/app.js @@ -30,7 +30,7 @@ report = new Uint8Array([ 0xc0 // END_COLLECTION ]); function p(c,cb) { NRF.sendHIDReport(c, function() { NRF.sendHIDReport(0, cb) }); } -volumeUp = function(cb) { p(0x20,cb) }; +volumeUp = function(cb) { p(0x80,cb) }; //end of manual selective import NRF.setServices(undefined, { hid : report }); From b58069fba13b50bba26dadf667dc55616d64bdc2 Mon Sep 17 00:00:00 2001 From: paul Date: Sat, 18 Apr 2020 16:52:30 +0200 Subject: [PATCH 155/302] massive rewrite with really heavy inspiration from music control app --- apps/hidcam/app.js | 89 +++++++++++++++++++++------------------------- 1 file changed, 41 insertions(+), 48 deletions(-) diff --git a/apps/hidcam/app.js b/apps/hidcam/app.js index e114863cc..57d4a6f05 100644 --- a/apps/hidcam/app.js +++ b/apps/hidcam/app.js @@ -2,58 +2,51 @@ var storage = require('Storage'); const settings = storage.readJSON('setting.json',1) || { HID: false }; -// hidcontrol module selective and manual import : -report = new Uint8Array([ - 0x05, 0x0c, // USAGE_PAGE (Consumer Devices) - 0x09, 0x01, // USAGE (Consumer Control) - 0xa1, 0x01, // COLLECTION (Application) - // -------------------- common global items - 0x15, 0x00, // LOGICAL_MINIMUM (0) - 0x25, 0x01, // LOGICAL_MAXIMUM (1) - 0x75, 0x01, // REPORT_SIZE (1) - each field occupies 1 bit - // -------------------- misc bits - 0x95, 0x05, // REPORT_COUNT (5) - 0x09, 0xb5, // USAGE (Scan Next Track) - 0x09, 0xb6, // USAGE (Scan Previous Track) - 0x09, 0xb7, // USAGE (Stop) - 0x09, 0xcd, // USAGE (Play/Pause) - 0x09, 0xe2, // USAGE (Mute) - 0x81, 0x06, // INPUT (Data,Var,Rel) - relative inputs - // -------------------- volume up/down bits - 0x95, 0x02, // REPORT_COUNT (2) - 0x09, 0xe9, // USAGE (Volume Up) - 0x09, 0xea, // USAGE (Volume Down) - 0x81, 0x02, // INPUT (Data,Var,Abs) - absolute inputs - // -------------------- padding bit - 0x95, 0x01, // REPORT_COUNT (1) - 0x81, 0x01, // INPUT (Cnst,Ary,Abs) - 0xc0 // END_COLLECTION -]); -function p(c,cb) { NRF.sendHIDReport(c, function() { NRF.sendHIDReport(0, cb) }); } -volumeUp = function(cb) { p(0x80,cb) }; -//end of manual selective import - -NRF.setServices(undefined, { hid : report }); - -g.clear(); -E.showMessage('BTN2 to trigger','camTrigger'); -Bangle.loadWidgets(); -Bangle.drawWidgets(); +var sendHid, camShot, profile; if (settings.HID) { - NRF.setServices(undefined, { hid : report }); - shotTrigger = function() {volumeUp();}; + profile = 'camShutter'; + sendHid = function (code, cb) { + try { + NRF.sendHIDReport([1,code], () => { + NRF.sendHIDReport([1,0], () => { + if (cb) cb(); + }); + }); + } catch(e) { + print(e); + } + }; + camShot = function (cb) { sendHid(0x80, cb); }; } else { E.showMessage('HID disabled'); setTimeout(load, 1000); } - -setWatch(function(e){ - E.showMessage('capture'); - Bangle.beep(); - shotTrigger(); +function drawApp() { g.clear(); - Bangle.loadWidgets(); - Bangle.drawWidgets(); - E.showMessage('BTN2 to trigger','camTrigger'); -},BTN2,{ repeat:true, edge:'falling' }); + g.setFont("6x8",2); + g.setFontAlign(0,0); + g.drawString(profile, 120, 120); + const d = g.getWidth() - 18; + + function c(a) { + return { + width: 8, + height: a.length, + bpp: 1, + buffer: (new Uint8Array(a)).buffer + }; + } + + g.drawImage(c([0,8,12,14,255,14,12,8]),d,116); +} + +if (camShot) { + setWatch(function(e) { + E.showMessage('camShot !'); + setTimeout(drawApp, 1000); + camShot(() => {}); + }, BTN2, { edge:"falling",repeat:true,debounce:50}); + + drawApp(); +} From 58e826accc018bbc638721f822b105959a19df52 Mon Sep 17 00:00:00 2001 From: paul Date: Sat, 18 Apr 2020 17:01:01 +0200 Subject: [PATCH 156/302] adding widgets --- apps/hidcam/app.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/hidcam/app.js b/apps/hidcam/app.js index 57d4a6f05..c3c672d4c 100644 --- a/apps/hidcam/app.js +++ b/apps/hidcam/app.js @@ -24,6 +24,8 @@ if (settings.HID) { } function drawApp() { g.clear(); + Bangle.loadWidgets() + Bangle.drawWidgets() g.setFont("6x8",2); g.setFontAlign(0,0); g.drawString(profile, 120, 120); From 63526c49e227a423eea5eaab25226c029d3e37b9 Mon Sep 17 00:00:00 2001 From: paul Date: Sat, 18 Apr 2020 17:08:01 +0200 Subject: [PATCH 157/302] using app icon instead of text --- apps/hidcam/app.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/hidcam/app.js b/apps/hidcam/app.js index c3c672d4c..6e599959c 100644 --- a/apps/hidcam/app.js +++ b/apps/hidcam/app.js @@ -26,9 +26,10 @@ function drawApp() { g.clear(); Bangle.loadWidgets() Bangle.drawWidgets() - g.setFont("6x8",2); - g.setFontAlign(0,0); - g.drawString(profile, 120, 120); +// g.setFont("6x8",2); +// g.setFontAlign(0,0); +// g.drawString(profile, 120, 120); + g.drawImage(storage.read("hidcam.img"),120,120) const d = g.getWidth() - 18; function c(a) { From aa168c3d5c5dce983dc4f3a52a24bd5e1d435868 Mon Sep 17 00:00:00 2001 From: fredericrous Date: Sat, 18 Apr 2020 16:30:05 +0100 Subject: [PATCH 158/302] Fix App Calculator issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Javascript rounded float issues - Reset after equals wasn't done properly - Introduce unit tests 🤗 --- apps.json | 4 +- apps/calculator/ChangeLog | 1 + apps/calculator/app.js | 146 +++++++++++++------- apps/calculator/tests.html | 273 +++++++++++++++++++++++++++++++++++++ 4 files changed, 369 insertions(+), 55 deletions(-) create mode 100644 apps/calculator/tests.html diff --git a/apps.json b/apps.json index 0c97b9e57..9bd4ee6fb 100644 --- a/apps.json +++ b/apps.json @@ -1264,8 +1264,8 @@ "name": "Calculator", "shortName":"Calculator", "icon": "calculator.png", - "version":"0.01", - "description": "Basic calculator reminiscent of MacOs's one. Handy for small calculus. Push button1 and 3 to navigate up/down, tap right or left to navigate the sides, push button 2 to select.", + "version":"0.02", + "description": "Basic calculator reminiscent of MacOs's one. Handy for small calculus.", "tags": "app,tool", "storage": [ {"name":"calculator.app.js","url":"app.js"}, diff --git a/apps/calculator/ChangeLog b/apps/calculator/ChangeLog index 5560f00bc..3b9b23270 100644 --- a/apps/calculator/ChangeLog +++ b/apps/calculator/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: fix precision rounding issue + no reset when equals pressed diff --git a/apps/calculator/app.js b/apps/calculator/app.js index 91dd7c49d..ad26d2d22 100644 --- a/apps/calculator/app.js +++ b/apps/calculator/app.js @@ -144,19 +144,57 @@ function drawKey(name, k, selected) { g.drawString(k.val || name, k.xy[0] + RIGHT_MARGIN + rMargin, k.xy[1] + BOTTOM_MARGIN + bMargin); } +function getIntWithPrecision(x) { + var xStr = x.toString(); + var xRadix = xStr.indexOf('.'); + var xPrecision = xRadix === -1 ? 0 : xStr.length - xRadix - 1; + return { + num: Number(xStr.replace('.', '')), + p: xPrecision + }; +} + +function multiply(x, y) { + var xNum = getIntWithPrecision(x); + var yNum = getIntWithPrecision(y); + return xNum.num * yNum.num / Math.pow(10, xNum.p + yNum.p); +} + +function divide(x, y) { + var xNum = getIntWithPrecision(x); + var yNum = getIntWithPrecision(y); + return xNum.num / yNum.num / Math.pow(10, xNum.p - yNum.p); +} + +function sum(x, y) { + let xNum = getIntWithPrecision(x); + let yNum = getIntWithPrecision(y); + + let diffPrecision = Math.abs(xNum.p - yNum.p); + if (diffPrecision > 0) { + if (xNum.p > yNum.p) { + yNum.num = yNum.num * Math.pow(10, diffPrecision); + } else { + xNum.num = xNum.num * Math.pow(10, diffPrecision); + } + } + return (xNum.num + yNum.num) / Math.pow(10, Math.max(xNum.p, yNum.p)); +} + +function subtract(x, y) { + return sum(x, -y); +} + function doMath(x, y, operator) { - // might not be a number due to display of dot "." algo - x = Number(x); - y = Number(y); switch (operator) { case '/': - return x / y; + return divide(x, y); case '*': - return x * y; + return multiply(x, y); case '+': - return x + y; + return sum(x, y); case '-': - return x - y; + return subtract(x, y); } } @@ -204,7 +242,7 @@ function displayOutput(num) { } len = (num + '').length; - if (numNumeric < 0) { + if (numNumeric < 0 || (numNumeric === 0 && 1/numNumeric === -Infinity)) { // minus is not available in font 7x11Numeric7Seg, we use Vector g.setFont('Vector', 20); g.drawString('-', 220 - (len * 15), 10); @@ -214,18 +252,33 @@ function displayOutput(num) { } g.drawString(num, 220 - (len * 15) + minusMarge, 10); } - +var wasPressedEquals = false; +var hasPressedNumber = false; function calculatorLogic(x) { - if (hasPressedEquals) { - currNumber = results; + if (wasPressedEquals && hasPressedNumber !== false) { prevNumber = null; - operator = null; - results = null; - isDecimal = null; - displayOutput(currNumber); - hasPressedEquals = false; + currNumber = hasPressedNumber; + wasPressedEquals = false; + hasPressedNumber = false; + return; } - if (prevNumber != null && currNumber != null && operator != null) { + if (hasPressedEquals) { + if (hasPressedNumber) { + prevNumber = null; + hasPressedNumber = false; + operator = null; + } else { + currNumber = null; + prevNumber = results; + } + hasPressedEquals = false; + wasPressedEquals = true; + } + + if (currNumber == null && operator != null && '/*-+'.indexOf(x) !== -1) { + operator = x; + displayOutput(prevNumber); + } else if (prevNumber != null && currNumber != null && operator != null) { // we execute the calculus only when there was a previous number entered before and an operator results = doMath(prevNumber, currNumber, operator); operator = x; @@ -255,8 +308,10 @@ function buttonPress(val) { operator = null; } else { keys.R.val = 'AC'; - drawKey('R', keys.R); + drawKey('R', keys.R, true); } + wasPressedEquals = false; + hasPressedNumber = false; displayOutput(0); break; case '%': @@ -265,11 +320,12 @@ function buttonPress(val) { } else if (currNumber != null) { displayOutput(currNumber /= 100); } + hasPressedNumber = false; break; case 'N': if (results != null) { displayOutput(results *= -1); - } else if (currNumber != null) { + } else { displayOutput(currNumber *= -1); } break; @@ -278,6 +334,7 @@ function buttonPress(val) { case '-': case '+': calculatorLogic(val); + hasPressedNumber = false; break; case '.': keys.R.val = 'C'; @@ -290,18 +347,24 @@ function buttonPress(val) { results = doMath(prevNumber, currNumber, operator); prevNumber = results; displayOutput(results); - hasPressedEquals = true; + hasPressedEquals = 1; } + hasPressedNumber = false; break; default: keys.R.val = 'C'; drawKey('R', keys.R); + const is0Negative = (currNumber === 0 && 1/currNumber === -Infinity); if (isDecimal) { - currNumber = currNumber == null ? 0 + '.' + val : currNumber + '.' + val; + currNumber = currNumber == null || hasPressedEquals === 1 ? 0 + '.' + val : currNumber + '.' + val; isDecimal = false; } else { - currNumber = currNumber == null ? val : currNumber + val; + currNumber = currNumber == null || hasPressedEquals === 1 ? val : (is0Negative ? '-' + val : currNumber + val); } + if (hasPressedEquals === 1) { + hasPressedEquals = 2; + } + hasPressedNumber = currNumber; displayOutput(currNumber); break; } @@ -315,38 +378,15 @@ for (var k in keys) { g.setFont('7x11Numeric7Seg', 2.8); g.drawString('0', 205, 10); - -setWatch(function() { - drawKey(selected, keys[selected]); - // key 0 is 2 keys wide, go up to 1 if it was previously selected - if (selected == '0' && prevSelected === '1') { - prevSelected = selected; - selected = '1'; - } else { - prevSelected = selected; - selected = keys[selected].trbl[0]; - } - drawKey(selected, keys[selected], true); -}, BTN1, {repeat: true, debounce: 100}); - -setWatch(function() { +function moveDirection(d) { drawKey(selected, keys[selected]); prevSelected = selected; - selected = keys[selected].trbl[2]; + selected = (d === 0 && selected == '0' && prevSelected === '1') ? '1' : keys[selected].trbl[d]; drawKey(selected, keys[selected], true); -}, BTN3, {repeat: true, debounce: 100}); +} -Bangle.on('touch', function(direction) { - drawKey(selected, keys[selected]); - prevSelected = selected; - if (direction == 1) { - selected = keys[selected].trbl[3]; - } else if (direction == 2) { - selected = keys[selected].trbl[1]; - } - drawKey(selected, keys[selected], true); -}); - -setWatch(function() { - buttonPress(selected); -}, BTN2, {repeat: true, debounce: 100}); +setWatch(_ => moveDirection(0), BTN1, {repeat: true, debounce: 100}); +setWatch(_ => moveDirection(2), BTN3, {repeat: true, debounce: 100}); +setWatch(_ => moveDirection(3), BTN4, {repeat: true, debounce: 100}); +setWatch(_ => moveDirection(1), BTN5, {repeat: true, debounce: 100}); +setWatch(_ => buttonPress(selected), BTN2, {repeat: true, debounce: 100}); diff --git a/apps/calculator/tests.html b/apps/calculator/tests.html new file mode 100644 index 000000000..1cbfdf617 --- /dev/null +++ b/apps/calculator/tests.html @@ -0,0 +1,273 @@ + + + + + + Calculator tests + + + + + + +
+ + + + + + + From 20f7c80462ce905288a987eda8455d9016771041 Mon Sep 17 00:00:00 2001 From: paul Date: Sat, 18 Apr 2020 17:30:28 +0200 Subject: [PATCH 159/302] graphical adjustments --- apps/hidcam/app.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/apps/hidcam/app.js b/apps/hidcam/app.js index 6e599959c..89b8ac4a1 100644 --- a/apps/hidcam/app.js +++ b/apps/hidcam/app.js @@ -24,12 +24,10 @@ if (settings.HID) { } function drawApp() { g.clear(); - Bangle.loadWidgets() - Bangle.drawWidgets() -// g.setFont("6x8",2); -// g.setFontAlign(0,0); -// g.drawString(profile, 120, 120); - g.drawImage(storage.read("hidcam.img"),120,120) + Bangle.loadWidgets(); + Bangle.drawWidgets(); + g.fillCircle(122,127,60); + g.drawImage(storage.read("hidcam.img"),100,105); const d = g.getWidth() - 18; function c(a) { @@ -40,8 +38,7 @@ function drawApp() { buffer: (new Uint8Array(a)).buffer }; } - - g.drawImage(c([0,8,12,14,255,14,12,8]),d,116); + g.fillRect(180,130, 240, 124); } if (camShot) { From d545a7a87d8a73270f90263e337a232339355360 Mon Sep 17 00:00:00 2001 From: paul Date: Sat, 18 Apr 2020 17:36:47 +0200 Subject: [PATCH 160/302] changelog update --- apps/hidcam/ChangeLog | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/hidcam/ChangeLog b/apps/hidcam/ChangeLog index 665c0df6e..73b3268b7 100644 --- a/apps/hidcam/ChangeLog +++ b/apps/hidcam/ChangeLog @@ -1 +1 @@ -0.01: Init +0.01: Core functionnality From 406deafadd762bd45c427362a40a210c61fb8bbf Mon Sep 17 00:00:00 2001 From: Ignas Bukys Date: Sat, 18 Apr 2020 21:22:04 +0300 Subject: [PATCH 161/302] BlackJack game --- apps/blackjack/ChangeLog | 1 + apps/blackjack/blackjack.app.js | 191 ++++++++++++++++++++++++++++++++ apps/blackjack/blackjack.img | Bin 0 -> 706 bytes apps/blackjack/blackjack.info | 1 + 4 files changed, 193 insertions(+) create mode 100644 apps/blackjack/ChangeLog create mode 100644 apps/blackjack/blackjack.app.js create mode 100644 apps/blackjack/blackjack.img create mode 100644 apps/blackjack/blackjack.info diff --git a/apps/blackjack/ChangeLog b/apps/blackjack/ChangeLog new file mode 100644 index 000000000..c941d90e5 --- /dev/null +++ b/apps/blackjack/ChangeLog @@ -0,0 +1 @@ +0.01: New game! BTN4- Hit card, BTN5- Stand \ No newline at end of file diff --git a/apps/blackjack/blackjack.app.js b/apps/blackjack/blackjack.app.js new file mode 100644 index 000000000..dc5d35494 --- /dev/null +++ b/apps/blackjack/blackjack.app.js @@ -0,0 +1,191 @@ +const Clubs = { width : 48, height : 48, bpp : 1, + buffer : require("heatshrink").decompress(atob("ACcP+AFDn/8Aod//wFD///AgUBAoOAApsDAoPAAr4vLI4pTEgP8L4M/wEH/5rB//gh//x/x//wj//9/3//4n4iBAAIZBAol/Aof+Apv5z4FP+OPAo41BAoX8I4Pj45HBAoPD4YFBLIOD4JZBRAMD4CKC/AFBj59Cg/gQYYFXAB4=")) +}; + +const Spades = { width : 48, height : 48, bpp : 1, + buffer : require("heatshrink").decompress(atob("ABsBwAFDgfAAocH8AFDh/wAocf/AFDn/8Aod//wFD///FwYFBGAUDAoIwCg4FBGAUPAoIwCj4FBGAU/AoIwCv4FBGAQEBGAQuCGAQuCGAQFLHQQ8CAupHLL4prB+fPTgU/8fHVwbLLApbXFbpYFLdIoADA==")) +}; + +const Hearts = { width : 48, height : 48, bpp : 4, + buffer : require("heatshrink").decompress(atob("ADlVqtQBQ8FBYIKIrnMAAINGqoKC4okGCwYAB4AKDhgKE4oWKAAILDBQwYEBYwwDFwojFgoLHEgQ6H5hhCBZAkCBRAjLEgI6IC4YLIC5Y7BBZXBjgjVABYX/C8CnKABbXLABTvMC8sMC6fAC4KQURwIABRypgULwRgULwRIUCwhIRIwiRSRoZITCwx5POoowRCxAwNFxIwNCxQwLFxYwLCxgwJFxowJCxwwHFx4wHCyAwFFyIwFCyQwDFycAgoXBqAXTgFc4oWUJAJGUJARGVAEo")) + }; + +const Diamonds = { width : 48, height : 48, bpp : 4, + buffer : require("heatshrink").decompress(atob("AHUFC60M4AXV5nFIyvM5hGVC4JIUCwJIUIwRIUIwRIUCwZISIwgABqBGUJCQWFPKBGGJCFcC455OCw4wOOox5QIxB5NOpBIOFxZ5LCxYwKOpQwMIxh5KOxipLL6xgNR5QwMX5TvXPJZ1JJBpGLPJR1LJBZGNPJIWOJA5GOPJB1NJBIWQPIpGRJApGRPIoWSJAa8PJA5GTJAYWUJAJGVAAJGVAHo=")) + }; + + +var deck = []; +var player = {Hand:[]}; +var computer = {Hand:[]}; + +function createDeck() { + var suits = ["Spades", "Hearts", "Diamonds", "Clubs"]; + var values = ["2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"]; + + var dck = []; + for (var i = 0 ; i < values.length; i++) { + for(var x = 0; x < suits.length; x++) { + dck.push({ Value: values[i], Suit: suits[x] }); + } + } + return dck; +} + +function shuffle(a) { + var j, x, i; + for (i = a.length - 1; i > 0; i--) { + j = Math.floor(Math.random() * (i + 1)); + x = a[i]; + a[i] = a[j]; + a[j] = x; + } + return a; +} + +function EndGameMessdage(msg){ + g.drawString(msg, 155, 200); + setTimeout(function(){ + startGame(); + }, 2500); + +} + +function hitMe() { + player.Hand.push(deck.pop()); + renderOnScreen(1); + var playerWeight = calcWeight(player.Hand, 0); + + if(playerWeight == 21) + EndGameMessdage('WINNER'); + else if(playerWeight > 21) + EndGameMessdage('LOOSER'); +} + +function calcWeight(hand, hideCard) { + + if(hideCard === 1) { + if (hand[0].Value == "J" || hand[0].Value == "Q" || hand[0].Value == "K") + return "10 +"; + else if (hand[0].Value == "A") + return "11 +"; + else + return parseInt(hand[0].Value) +" +"; + } + else { + var weight = 0; + for(i=0; i 21 || bangleWeight < playerWeight) + EndGameMessdage('WINNER'); + else if(bangleWeight > playerWeight) + EndGameMessdage('LOOSER'); +} + +function renderOnScreen(HideCard) { + const fontName = "6x8"; + + g.clear(); // clear screen + g.reset(); // default draw styles + g.setFont(fontName, 1); + + g.drawString('RST', 220, 35); + g.drawString('Hit', 60, 230); + g.drawString('Stand', 165, 230); + + g.setFont(fontName, 3); + for(i=0; i9WgH5ul2)7_0>|Km^WM! ktsjd-R^R}YcOcfVu~7y4Cx08IkIR{#J2 literal 0 HcmV?d00001 diff --git a/apps/blackjack/blackjack.info b/apps/blackjack/blackjack.info new file mode 100644 index 000000000..b30333f2e --- /dev/null +++ b/apps/blackjack/blackjack.info @@ -0,0 +1 @@ +{"id":"blackjack","name":"Black Jack","src":"blackjack.app.js","icon":"blackjack.img","version":"0.1","files":"blackjack.info,blackjack.app.js,blackjack.img"} \ No newline at end of file From 58d640a2e2dc08e79423ce984f9c1c0156e23a80 Mon Sep 17 00:00:00 2001 From: Ignas Bukys Date: Sat, 18 Apr 2020 22:11:50 +0300 Subject: [PATCH 162/302] added blackjack game to apps.json --- apps.json | 12 ++++++++++++ apps/blackjack/blackjack.png | Bin 0 -> 646 bytes 2 files changed, 12 insertions(+) create mode 100644 apps/blackjack/blackjack.png diff --git a/apps.json b/apps.json index 0c97b9e57..0a6281c37 100644 --- a/apps.json +++ b/apps.json @@ -1293,5 +1293,17 @@ "evaluate": true } ] + }, + { "id": "blackjack", + "name": "Black Jack game", + "shortName":"BlackJack game", + "icon": "blackjack.png", + "version":"0.01", + "description": "Simplle implementation of card game Black Jack", + "tags": "", + "storage": [ + {"name":"blackjack.app.js","url":"blackjack.app.js"}, + {"name":"blackjack.img","url":"blackjack-icon.js","evaluate":true} + ] } ] diff --git a/apps/blackjack/blackjack.png b/apps/blackjack/blackjack.png new file mode 100644 index 0000000000000000000000000000000000000000..53715b5e44226d70a129a36fc2f663e4e6157f0e GIT binary patch literal 646 zcmV;10(t$3P)Px#1ZP1_K>z@;j|==^1poj5Fi=cXMF0Q*78VvLC@3~IHc3fIR8&-GXlQnJc87+Amh{@>F7O#lD@32;bRa{vGi!vFvd!vV){sAK>D0pCeP zK~y-)rIWu;6hRcnzXx|W!-?iPS{f2UNu$=DlaT_>83S%9tk^I!6*|as>t=o1 zrJvG>k!LXqxx|Z4A74gs{_PI%Akg(TfJI{Cugo!h<(+25uNnN{ERuBfM1wo3)LJn|XA$T-T{l3VA%^j=gFO901_m@WD5=}vb6`NG zp~0hQy+xe_M*_^V$jA-SJqspuhUDwPhYMI>u2sI{R&{~_ zO#|9M{X0KJ$pE7kvV(yxZ{BVjVAMjiQUAui9s?Oa?~ Date: Sat, 18 Apr 2020 22:20:09 +0300 Subject: [PATCH 163/302] adde icon for game --- apps/blackjack/blackjack-icon.js | 1 + 1 file changed, 1 insertion(+) create mode 100644 apps/blackjack/blackjack-icon.js diff --git a/apps/blackjack/blackjack-icon.js b/apps/blackjack/blackjack-icon.js new file mode 100644 index 000000000..5c184fe4a --- /dev/null +++ b/apps/blackjack/blackjack-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("ABsD4AECn//2AEBg/4gEQAoMIAQIFFjACHBYoFIh+AgEMGQXwAokfAoMP8EHwPghk//AXBmEMnl8n+AjEMvl8/4FCvE9AoMQAoM894XBgkYvgdCgkAAocGgF4AoNgAoPwAongAocB4AFDgeAAoPAg0HL4QFBhwFEjkGh+BwEGnEOAoUCv/+Apd//a3BAorBDAohLBgf+AocB/gFDgFMAogCCUoUH8AFDDASxDn/AYYQrB/DMCAAN/zAFDg7LBAAYlBAAoA==")) \ No newline at end of file From 98795281a190d9dd5ba9e6ff2217ba919d9f331e Mon Sep 17 00:00:00 2001 From: Ignas Bukys Date: Sat, 18 Apr 2020 22:20:57 +0300 Subject: [PATCH 164/302] fix typo in description --- apps.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps.json b/apps.json index 0a6281c37..3b7ac0def 100644 --- a/apps.json +++ b/apps.json @@ -1299,7 +1299,7 @@ "shortName":"BlackJack game", "icon": "blackjack.png", "version":"0.01", - "description": "Simplle implementation of card game Black Jack", + "description": "Simple implementation of card game Black Jack", "tags": "", "storage": [ {"name":"blackjack.app.js","url":"blackjack.app.js"}, From 34e3748ee6ee01eddbf09e47b08ecc1c7dbc5303 Mon Sep 17 00:00:00 2001 From: Ignas Bukys Date: Sat, 18 Apr 2020 22:27:58 +0300 Subject: [PATCH 165/302] add game tag and change icon --- apps.json | 5 +++-- apps/blackjack/blackjack-icon.js | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 apps/blackjack/blackjack-icon.js diff --git a/apps.json b/apps.json index 3b7ac0def..a62fa9ece 100644 --- a/apps.json +++ b/apps.json @@ -1300,10 +1300,11 @@ "icon": "blackjack.png", "version":"0.01", "description": "Simple implementation of card game Black Jack", - "tags": "", + "tags": "game", + "allow_emulator":true, "storage": [ {"name":"blackjack.app.js","url":"blackjack.app.js"}, - {"name":"blackjack.img","url":"blackjack-icon.js","evaluate":true} + {"name":"blackjack.img","url":"blackjack.img"} ] } ] diff --git a/apps/blackjack/blackjack-icon.js b/apps/blackjack/blackjack-icon.js deleted file mode 100644 index 5c184fe4a..000000000 --- a/apps/blackjack/blackjack-icon.js +++ /dev/null @@ -1 +0,0 @@ -require("heatshrink").decompress(atob("ABsD4AECn//2AEBg/4gEQAoMIAQIFFjACHBYoFIh+AgEMGQXwAokfAoMP8EHwPghk//AXBmEMnl8n+AjEMvl8/4FCvE9AoMQAoM894XBgkYvgdCgkAAocGgF4AoNgAoPwAongAocB4AFDgeAAoPAg0HL4QFBhwFEjkGh+BwEGnEOAoUCv/+Apd//a3BAorBDAohLBgf+AocB/gFDgFMAogCCUoUH8AFDDASxDn/AYYQrB/DMCAAN/zAFDg7LBAAYlBAAoA==")) \ No newline at end of file From aee60d97c0476dd7fd694a78e73e85d45ce4badf Mon Sep 17 00:00:00 2001 From: Ignas Bukys Date: Sat, 18 Apr 2020 22:43:29 +0300 Subject: [PATCH 166/302] Fix icon --- apps.json | 4 ++-- apps/blackjack/blackjack-icon.js | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 apps/blackjack/blackjack-icon.js diff --git a/apps.json b/apps.json index a62fa9ece..6e221c778 100644 --- a/apps.json +++ b/apps.json @@ -1296,7 +1296,7 @@ }, { "id": "blackjack", "name": "Black Jack game", - "shortName":"BlackJack game", + "shortName":"Black Jack game", "icon": "blackjack.png", "version":"0.01", "description": "Simple implementation of card game Black Jack", @@ -1304,7 +1304,7 @@ "allow_emulator":true, "storage": [ {"name":"blackjack.app.js","url":"blackjack.app.js"}, - {"name":"blackjack.img","url":"blackjack.img"} + {"name":"blackjack.img","url":"blackjack-icon.js","evaluate":true} ] } ] diff --git a/apps/blackjack/blackjack-icon.js b/apps/blackjack/blackjack-icon.js new file mode 100644 index 000000000..f0976a5be --- /dev/null +++ b/apps/blackjack/blackjack-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgIQNgfAAgU/+UQAgMHmEAAoUIAQIFFjACHBYoFIgeAAocA+EAhgFCg4FBg/ggeB8EMj/4h+AmEMnl4j+AjEMvF8v4FDnv3wEQhk4nnnC4MEjE8g4FCgF4Ao8gAoNwAoNgg0A8AFDgPAAoZXBAoPAgxQBAocOAokcBoOAwEGnEOg+BwECv/+AosPAol//MAAoP8v/7RgLBDAokCAoO8AoJRBgHMAolMAogCCNIJLBcQKrCgP8WIk7wC9Dgf4ZgQABu4FEYIIFDRYIFEAAI")) \ No newline at end of file From 0c03bce926a8cac1282001c885dfa9d28eafde33 Mon Sep 17 00:00:00 2001 From: Ignas Bukys Date: Sat, 18 Apr 2020 22:45:33 +0300 Subject: [PATCH 167/302] icon brightness --- apps/blackjack/blackjack-icon.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/blackjack/blackjack-icon.js b/apps/blackjack/blackjack-icon.js index f0976a5be..cb4d00cdd 100644 --- a/apps/blackjack/blackjack-icon.js +++ b/apps/blackjack/blackjack-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mEwgIQNgfAAgU/+UQAgMHmEAAoUIAQIFFjACHBYoFIgeAAocA+EAhgFCg4FBg/ggeB8EMj/4h+AmEMnl4j+AjEMvF8v4FDnv3wEQhk4nnnC4MEjE8g4FCgF4Ao8gAoNwAoNgg0A8AFDgPAAoZXBAoPAgxQBAocOAokcBoOAwEGnEOg+BwECv/+AosPAol//MAAoP8v/7RgLBDAokCAoO8AoJRBgHMAolMAogCCNIJLBcQKrCgP8WIk7wC9Dgf4ZgQABu4FEYIIFDRYIFEAAI")) \ No newline at end of file +require("heatshrink").decompress(atob("mEwgIQNgfAAgU///wAgMH/4dBAoMMAQMQAQMIAQMYAQ4RCApcPwAFDgIwBAoQ4BAoMP8EHwfghk//AXBuEMv38n+AjEMvl8/4FDvoFBmEMvF994FBg04vgdBAoMAAot4AoNgAoPwAoZFBAongAoPggyIBAoPAg0HwAFDh4BBAoUeh0PwOAg08AocDv/+Ao3DAod//a3BAorBDAohRBgf+AocBAokApgCBhzSCWIkHVYgYCWIngYwQrB/gFDgF//AFDD4QAD8AFEAAIA=")) \ No newline at end of file From 79f18015ef8ae54f9ff24eb1573fb2acb6f9bb8a Mon Sep 17 00:00:00 2001 From: fredericrous Date: Sat, 18 Apr 2020 22:05:36 +0100 Subject: [PATCH 168/302] Add Calculator App Readme --- apps/calculator/README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 apps/calculator/README.md diff --git a/apps/calculator/README.md b/apps/calculator/README.md new file mode 100644 index 000000000..78848908a --- /dev/null +++ b/apps/calculator/README.md @@ -0,0 +1,23 @@ +# Calculator + +Basic calculator reminiscent of MacOs's one. Handy for small calculus. + + + +## Features + +- add / substract / divide / multiply +- handles floats +- basic memory button + +## Controls + +- UP: BTN1 +- DOWN: BTN3 +- LEFT: BTN4 +- RIGHT: BTN5 +- SELECT: BTN2 + +## Creator + + From 0df1fb19e3d45f92ed5e490c11a59017df9d27eb Mon Sep 17 00:00:00 2001 From: Michael Bengfort Date: Sun, 19 Apr 2020 13:55:20 +0200 Subject: [PATCH 169/302] initial commit adding metronome app --- apps.json | 20 +++++++ apps/metronome/README.md | 10 ++++ apps/metronome/metronome-icon.js | 1 + apps/metronome/metronome.info | 1 + apps/metronome/metronome.js | 93 ++++++++++++++++++++++++++++++ apps/metronome/metronome_icon.png | Bin 0 -> 7575 bytes 6 files changed, 125 insertions(+) create mode 100644 apps/metronome/README.md create mode 100644 apps/metronome/metronome-icon.js create mode 100644 apps/metronome/metronome.info create mode 100644 apps/metronome/metronome.js create mode 100644 apps/metronome/metronome_icon.png diff --git a/apps.json b/apps.json index 0c97b9e57..590a85e38 100644 --- a/apps.json +++ b/apps.json @@ -1293,5 +1293,25 @@ "evaluate": true } ] + }, + { + "id": "Metronome", + "name": "Metronome", + "icon": "metronome_icon.png", + "version": "0.03", + "description": "Makes the watch blinking and vibrating with a given rate", + "tags": "tool", + "allow_emulator": true, + "storage": [ + { + "name": "metronome.app.js", + "url": "metronome.js" + }, + { + "name": "metronome.img", + "url": "metronome-icon.js", + "evaluate": true + } + ] } ] diff --git a/apps/metronome/README.md b/apps/metronome/README.md new file mode 100644 index 000000000..19d489327 --- /dev/null +++ b/apps/metronome/README.md @@ -0,0 +1,10 @@ +# Metronome + +This metronome makes your watch blink and vibrate with a given rate. + +## Usage + +* Tap the screen at least three times. The app calculates the mean rate of your tapping. This rate is displayed in bmp while the text blinks and the watch softly vibrates with every beat. +* Use `BTN1` to increase the bmp value by one. +* Use `BTN3` to decrease the bmp value by one. +* You can change the bpm value any time by tapping the screen or using `BTN1` and `BTN3`. diff --git a/apps/metronome/metronome-icon.js b/apps/metronome/metronome-icon.js new file mode 100644 index 000000000..8b45f233b --- /dev/null +++ b/apps/metronome/metronome-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+ABt4AB4fOFyFOABtUGDotOAAYvcp4ARqovbq0rACAvbqwABF98yGCAvdGAcHgAAEF8tWmIuGGA6QaF4lWFw4vgFwovPmIvuYDIvd0ejF59cF6qQFFwIvnMAguSqxfaFyQvYvOi0QuTF64uCAAQuRXwIvUqouEF6guFF5+cAAiOZF6iOaF5sxv+iF6xfRmVWFwWjv8rp4tSL6YvBqwuDMgQvnFwovURwIvQRggAELygvPgwuIF8ouEBwIvnFwwwXF54uBvwuFq0yF6buCF5guClQuFGAgvfFwcAF49WmIvRFwQvKFwkAmQvHYQMxF7l+FwgvKGAIvalQuGF5dWFx1VABVUvF4p0qAAdPCZNPF51OAD4vOKQIACF/4waF9wuEqgv/F/gwMF97vvAAUqADYtQAAMAADYuRGDgmLA=")) diff --git a/apps/metronome/metronome.info b/apps/metronome/metronome.info new file mode 100644 index 000000000..74dcbd2c7 --- /dev/null +++ b/apps/metronome/metronome.info @@ -0,0 +1 @@ +{"id":"metronome","name":"Metronome","src":"metronome.app.js", "icon": "metronome_icon.png", "sortorder":-2,"version":"0.03","files":"metronome.info,metronome.app.js, metronome_icon.png"} diff --git a/apps/metronome/metronome.js b/apps/metronome/metronome.js new file mode 100644 index 000000000..acd4b70b8 --- /dev/null +++ b/apps/metronome/metronome.js @@ -0,0 +1,93 @@ +var tStart = Date.now(); +var cindex=0; // index to iterate through colous +var bpm=60; // ininital bpm value +var time_diffs = [1000, 1000, 1000]; //array to calculate mean bpm +var tindex=0; //index to iterate through time_diffs + +Bangle.setLCDTimeout(undefined); //do not deaktivate display while running this app + +function changecolor() { + const maxColors = 2; + const colors = { + 0: { value: 0xFFFF, name: "White" }, + 1: { value: 0x000F, name: "Navy" }, + // 2: { value: 0x03E0, name: "DarkGreen" }, + // 3: { value: 0x03EF, name: "DarkCyan" }, + // 4: { value: 0x7800, name: "Maroon" }, + // 5: { value: 0x780F, name: "Purple" }, + // 6: { value: 0x7BE0, name: "Olive" }, + // 7: { value: 0xC618, name: "LightGray" }, + // 8: { value: 0x7BEF, name: "DarkGrey" }, + // 9: { value: 0x001F, name: "Blue" }, + // 10: { value: 0x07E0, name: "Green" }, + // 11: { value: 0x07FF, name: "Cyan" }, + // 12: { value: 0xF800, name: "Red" }, + // 13: { value: 0xF81F, name: "Magenta" }, + // 14: { value: 0xFFE0, name: "Yellow" }, + // 15: { value: 0xFFFF, name: "White" }, + // 16: { value: 0xFD20, name: "Orange" }, + // 17: { value: 0xAFE5, name: "GreenYellow" }, + // 18: { value: 0xF81F, name: "Pink" }, + }; + g.setColor(colors[cindex].value); + if (cindex == maxColors-1) { + cindex = 0; + } + else { + cindex += 1; + } + return cindex; +} + +function updateScreen() { + g.clear(); + changecolor(); + Bangle.buzz(50, 0.75); + g.setFont("Vector",48); + g.drawString(Math.floor(bpm)+"bpm", -1, 70); +} + +Bangle.on('touch', function(button) { +// setting bpm by tapping the screen. Uses the mean time difference between several tappings. + if (tindex < time_diffs.length) { + if (Date.now()-tStart < 5000) { + time_diffs[tindex] = Date.now()-tStart; + } + } else { + tindex=0; + time_diffs[tindex] = Date.now()-tStart; + } + tindex += 1; + mean_time = 0.0; + for(count = 0; count < time_diffs.length; count++) { + mean_time += time_diffs[count]; + } + time_diff = mean_time/count; + + tStart = Date.now(); + clearInterval(time_diff); + g.clear(); + g.setFont("Vector",48); + bpm = (60 * 1000/(time_diff)); + g.drawString(Math.floor(bpm)+"bpm", -1, 70); + clearInterval(interval); + interval = setInterval(updateScreen, 60000 / bpm); + return bpm; +}); + +// enable bpm finetuning via buttons. +setWatch(() => { + bpm += 1; + clearInterval(interval); + interval = setInterval(updateScreen, 60000 / bpm); +}, BTN1, {repeat:true}); + +setWatch(() => { + if (bpm > 1) { + bpm -= 1; + clearInterval(interval); + interval = setInterval(updateScreen, 60000 / bpm); + } +}, BTN3, {repeat:true}); + +interval = setInterval(updateScreen, 60000 / bpm); diff --git a/apps/metronome/metronome_icon.png b/apps/metronome/metronome_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..4dac7117f9e91221b0cfec54538e339c81df1939 GIT binary patch literal 7575 zcmV;I9cbc-P) zaB^>EX>4U6ba`-PAZ2)IW&i+q+O3*rcI3Kth5us|9s+V04&qv#frsyJgH@7Ro$j@t z)U80VNDybA9e^_XfByTJ|KcOHSWQf&=9aVNBevLl=Z9*akLSCy@qWD@;q@ne|KYg# z_=D$A_&hW3*ZG~}>GK68zK)L{kDD@I*Qu`yz5ejNV9=dCKfKPXuL}kL9CxqZO?$nr z$1jib`d_|X^!x9zFc~Xxz2J@C!3E3Ls=I{ef9HSXs|(q`kGN3y8iN1z+?kIbzfIo9 zPv7Rw_Sf<7<5U=%Unk^mbM!jy(y!<4&d}Epxz9WQ@CTn=wqHNK%-uPwp0l5)>oGG^ z*}NX={V4Y*P8>{SxG&3m7XBxGF88zXS$V`s#TL~Zd`{z<^UU^f(Jj~Aar=IsZZgE^ zw=aD6e)@2a)lhu%lj(PqgHLyS2`wxWDpi?OEpXU>jK$siwtK(nDtBIvGri+tj#vEj zX?}I^|MK~9p>qUX=WP9$E5_x88it|D=__YZ5O-d4n(qAGPxA#g_Df*{6-;-UD-YQ1 zcrG!L`@~kbbq<`DY&7<14OiCd0!$J2P7KBcDqyn<$z^AY_r*Epu~MJT$bE=@Pyv^c zU(OtA6X=BEtJl4GO>y^JpZs$eYM~G*WSXQ>p-!=4OyH-)3WjeD-EkI$Z+)=oI_q?1oM z^|aGJs`jetk5zNuD);BAxmVRxv3SobpH<^aj{B(HXXXA_H0Xulxo*Fx z+Ui$P!Yz=xP;+X-f!H{y`^rD~_}AaV+-0V)eI0(~6KCD4ukJ_Xxf_ql5z{+G_QUQO zPhZ9Dxn10L=QcvDIjF{t<8$RQPib%*cSwm5w{${F)PgzKCO5ao%qVrAb*z(HJTdpM zmv6f+DsY&y6O#_bv+GD<)>MM)hwin68h1U8n}~QXj+x|T$UsjXpq8p@ypcwX z!`7g8>MnW;%rj>O-v;E$r1A1*dSRe+fLip7-0E6ASN^xf?f1kIfIhnQIbWwooo&mH zC!2qGcBg4;3@6crJyuX#j_b88TUYHhWkdB+SaU=e(JeJtiL^mJOn2X$GFwCAU7gjf z+PKmx{e%wv#JPMPBcK|Tf0hy44reg|7dUpMv{Eftsy-rfl5%_|nfZ#pEra>pAJV4O zVYHURDjw2AWiclw(;?-Ixzet5T+twsEy?T}4h3rEoal4kaprVZgV#=lhQfy2MiE(5 znmXk^=(Dr?EGu^&Q^eTZYzDFhX9;GX%~c=k9x2s2U0+aSxauF<9&N*NdwbBbB_u&u z!HCu#fNgPRjhNkQtvz(jg%mL4|lhN%I}C| z$Do!OuxB5XPsLeYDl?0`8}qWj;4B$MvTf|fcNr?Y->HeOzT*Rl5R1M%rvZ$jmz9ab zIht)|a*cVAvisGB;lpwpb5}Ow7%f74jK%Vdy&Jvg@T#8j>U}z}*h-A;`g4YTpJ*pU zaLr}|f5Y1|n=k=*?x!|?JhW?k72cxD^UMja%v|ko+!EP>U~9q)7Ppv=p3$KT5Bi`Y z+q=zxD0ylM`Qtu2kJepJrK2d2%?ue;oRH7O4uDzu{+!YhJf6cG=vpE>XW0_^mwJ7Pri|z{# zH3{kn3xC6~gAv#{5J3X0XJPE9>7Qo>%7I@?6TmRB*o#`ACs*QT#G5D7VCgC%N;f=G zNp^7pY>x2($4EoRkl_2!J8q)lL|pt}kQc;4&y; z;M;a5ylC{M)4qGJG$$lw)XFssl{XFyWj6EH@ZiAzc~a-w6OQmD)X5SR`_S@2Xd zDn(&C@D+>?Wf^u7EWEOKM7l5xW$qjXToMbu3}3O;$Y6-sNX7I@9xDjI8o6sBqI=R; zs@Tvupn)6+U9eJ;XQ*M>mE#Qb5IG9B?Wx+%#4j^RF~L(F1~6r0KvZRLDZ;Y5XNE*m z?G3HrS=7i2%R!sUb63z=+oq+Q87S+FfT!X|H{qZWI4=Z4{06}`>$Mj&2^>Fr7?yxV z*C`|6j|M>CXDl3pT48FDY!xz9I5lEgC`99;B!_%C@Ms~yej`Y0z8^TAI#)!sQ+cv6 z)~KQ+qt$Z(@X00a2jrMHbO8200uR*w37;?6F@Q%wp<_bJy-t^%Ow0^{Fk%14@tedQ zyC(l&#j#&ruODUOeBQsn*$fG3!aos%>e{dpECF9vXc}pvK?B)>USAmb*{iH447E;E zTzX#8>9qnU4k$Vj)8HM4zpIb*orNW)YJZ8&R+E$rjobu%9OxRfa{|1>Yrzl#g*rUJ zj%)itk7LQ5Oz=^(ZnVedWiMM~GRcHS8ViA9U|>rrCZp_-0dgx$#BgDK%lc6Wyj_CI zaY$%aUxw#Y5=k##&ne&OiEKb3nVf-X!gfRCT9QN%q0oh?#aFp$m_oRbS&DuKG`vOH z78tESMBJkagqb?J07hwn0GX==(jw%p?EI3p&1B?Bm6$q(Ko<;h_nf$ede!A=`uLundl& zDk~ygLbQz1z-t>c6K_2*CwSv7FUM5jyn@!`x1(LTy*)$z2D831(3*>E87eVHY^ywF zhrt2Rh7En3TtD$BmCnV?Y%s-m5LoiX%A|iHXmg704MSPsO_-75CLGi#rb=*mQo0-v zI3Hz)^3@O+w=IM_lG=P!Ly|GG^h72P674|VjXQ!y)ewZu^Xx)-nBV#U1?dGT|G>CI zHuBIWdeRQkW!)1Kpx^TO=b!&-eao{1O~p9KGmZvK`)tAqU@--Kk& zv}MxWiiO>(Yja;fVFsz6+*1PVJc$Ds#>E(&Om&c{26kTFpH@nm2_BSa3n#sx8Q$2` zE`}%SkZouhuxSQH3Unp;g?Wnbl=eZXwXU%tJD}CIyhJX~O7WBe3UH_cnHpmWm~76Z zBciOu!(q0tTLh-c>*-zP{WfKR7jhP_^XxJOS>vTHAhIi@v?rv(RN_nRrimvxWhy@~7g{OxY-Cmk})BJM3ms)Y8tcA^juAnXoUzwa95QXj*SBlB^ zLV1D*VjJ@93TBRI5lA<&Li)P)5eC5#(_bczA5KMLwy1YJ)SDja#Re^RVbAb@E!g}8 z?9C_cU3lUC=nCREE$-(B_HWz=9N58-V98PaFTgh-AE3UjMsgi|64R=l(q&{wPE2bi zTBK%jiC!?!XowX`ie)UzAwv;{3_DkXP2)qkrZN+XqaZg+r9i3o3{0y{)Z;crp!YG1 z6+y3^M?^;q*q|XeN5mW6O9Q&%#q|ZjtWZsO;#mC_$6`8`@EHvLIajUZxlTCExUivo zjitdzYg0ry=)y}Y9KUY@^VtQS$`Mmq)t)gtUH6cKq6#1q<|ac{j0W}Yg ztBfUUlZUcl_iykbA<2l~RflT@6`@fq$=YtBlnPvcav?)DXhe(YmdJqLXq0fHN7OGM zKbugAEHk_0SlRb&&|}&vLHF3`$$al0kWZ7`YGE+?;o!|A)N^P}cyJFyt~>a};8`*8 z^i#eoG>MEJkG6iao1;AuI03`p--|iB>vou;K_Ee3R>K2FEe1Tx zz{HJ3OXC0|?hwPa?^>-~%w1f`-JZQ-Q}6|xQ7f(_=1J!a8r`-O^;|qFMj%Es3`Ns+ zLOGaGz)L52?fjq#od<<#aRVe)tZ?!7yuY2sWN%RGA#q?0^`l!36w;))>u;cUN1%6z z6G8##nd(o_n{TM+nJaAU$Pi>=KO>nJ_YL2n$B1F%iJP>YTsN2V5C$?+=Qg5z`QsZ3 zeE)FUaypmtHxxxrB*=}H;X*mrVN(+3qg7uk zE^?BCm9f{wGqOnfhM+b4W+#aQYMM}O{UE1@o51ghgvRY=c=lm3Ulg7Dv0C}V6~YqZ zZi_T#d(uC{;F||v=j)*mf{L&7n3inmmO-c#K(tl{-36vBolHO&9&~tueo0VZK1?Pv?C-Ex-sRO)XY- z7~KwnZ7}U_419Stz7Wez8m?~9{Ms*jdZ7#$acqo=C955$fC9rbZH!MQJI;PLeVtY# zWn$1<)c4rAb8dmiTd4BoNeFwEJ7?vlk=&I9yr{VtS6s<2z0wNeH>czWxqQR1^lRx? zOZr`@ml10n6U-SKSv1Hma>)(Trbg$65wG2PI=^cp!%kop?wZ>fAXddJa4H9sj^0Pi z>(J8T^27!Z-P7zeLteqtD~-`y@jj>DrNn6%%*lvr(hl--PP*$m8w2^ij(eX1B$Gq6 z$4|F1x>p@|lHc9xPj~Vl$0A+0B_ri3*!pBs_>}XZMUnCluGVGo6irP-YT{h*AvZI$ zgqjh&XZi4i{1j8rC>Yi|g{kdsnThUGw0GW#Z?pZpH@$18y zAD{X+-|M8~`gRJTam&U>{#F<6zP z@NgWj%njOk9jZCkppw%Llbl_p?v@<=JUOQE=Qoj{yR|;s3S>|1JMV@-(y({8JgNjU zNdC{WlZE>tt<1ZVW!v3fS4L!trb5yQ*rf2B?RIC578DewI^9;SpvXojIEmmLPZTixS!4MhH^h$CmS)y9g{v*!^x>XEk{570&Ow)X`;5$ubgs?gW=}5~mp6K2h;6@PFxN_r@WCO^0hrh+gL9?gXXN6sLAChkEhG%U@ z9=#*#uD)(Xp}PQx@-xTES?9iz*G(c84gq8?td-WVzny`am>;h)WML)DsPp&NP>$D# zc->DgGrxV^$G!g9t<2x=_0Mi){u7U-mzn?1W93>14?fW2-9((6o6mbFFo{)`jV!QJYEYi89Z+pjkwtsi80Iyn-Z-R<`n!pw z99+q=+U(^_2UhjIkhWda4l7+(jNk3_G6&IWl59Yw*FwsDM++Vrvwk-RlOtRwt(+$w zF|LgHK2>WruGgZf;g@M!)Qdz{7oiQ8i`jg?2LL0lt=-k79JKDH0h(5Ex#!M&?)eam zS$YHJi;ZE6X8spFiELGB``5|kZ^9C$t(J9t!RKdd@!RF z!GH&hlKtO$;^e=a5)kYz;M~|Uw^-PHeW`$YV?NE$(-FP{K=TNc;7xp1jKlPrbrUE6 z5EV07&pm7t(By%nCUj%r`gOPwgcgz~{_qgK_QU5Lt+#DJ&C{Cj`g{n6!>$l0m}0NG zbT;U8y)6PKjvj`=qyzxS!65vSRKX{_{MjrrC6O+goqL;-Lh9mVu+*8{4(I@Ya5M_T zFrXKUFmwYKrq1ENQ|GZbKVPm+j4^E8whjFQ188k;v%K$2F_r}I&xR{tc)Io!g+O@? zAq3G_42&_Ho1DbSf4qy&XJ?`7I*jFyrm8r9_ALJP+H1(?^OpC`mBeIIQb-N&O`7Yn zf-#0@EQV{dGga4`r4q8~w6(8H)*J$cH=3}@YtiF@AbM)(A=aMIIU9I=>@K0<9x$0N zX=<`y#CG<3%bXu<7CfWS1ZM+izce;0x% zRvu9}xzz&v+gc&>rD7p{kx+U^*S81oY^9`5H>;eI=VB+4dE5}GP!_E zp5WMc2%jy9Xl-jNgJ0Prsfs$}3V}brc*F}r67>>jk1!m0yc;dj0y24uW8)!AFNxUI z-;ef=4y%pNh$|i_`8yH-5jWdWZ4SN@?V$p)tHAHeH%B*sUx`RGTHY^k&U;=u{%X7~0@-XD0MGmQN>hRznf4F2Ow zCG__7S%SaO>?vAev6YPx(TjSjE&|8LUk-xM-F6jd4N;8#D2lGu(i-q9M=;fb2?CZsqI#K%!MVoCR=Zc4Z<4;#R|eG6^WQ z#7H_KAd}0HFYfH;M!sA_geo6`XiY)@1L&wUWVC(X<8L{5b zOrW|g0^fe(y)@_i7t6h?98uW|MjKCJwV^4c7bTzP*{Z*{8V_002ovPDHLkV1n;TjMM-C literal 0 HcmV?d00001 From 28dc51c377cb8b1c692df97f5f0bbc741b16ed7d Mon Sep 17 00:00:00 2001 From: Michael Bengfort Date: Sun, 19 Apr 2020 14:03:11 +0200 Subject: [PATCH 170/302] correct app path --- apps.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps.json b/apps.json index 590a85e38..83c0ba88f 100644 --- a/apps.json +++ b/apps.json @@ -1295,7 +1295,7 @@ ] }, { - "id": "Metronome", + "id": "metronome", "name": "Metronome", "icon": "metronome_icon.png", "version": "0.03", From 0bdf476ea6fe78aafaaf5318d92ac7f0fe8b5b60 Mon Sep 17 00:00:00 2001 From: Michael Bengfort Date: Sun, 19 Apr 2020 14:13:26 +0200 Subject: [PATCH 171/302] remove metronome.info --- apps/metronome/metronome.info | 1 - 1 file changed, 1 deletion(-) delete mode 100644 apps/metronome/metronome.info diff --git a/apps/metronome/metronome.info b/apps/metronome/metronome.info deleted file mode 100644 index 74dcbd2c7..000000000 --- a/apps/metronome/metronome.info +++ /dev/null @@ -1 +0,0 @@ -{"id":"metronome","name":"Metronome","src":"metronome.app.js", "icon": "metronome_icon.png", "sortorder":-2,"version":"0.03","files":"metronome.info,metronome.app.js, metronome_icon.png"} From 1596b0246b9693bdf7fd26a04417e49aa61dfb9d Mon Sep 17 00:00:00 2001 From: Amos Blanton Date: Sun, 19 Apr 2020 16:18:13 +0200 Subject: [PATCH 172/302] Add setting to hide widget when battery is over 20%. --- apps/widbatpc/{settings.js => widbatpc.settings.js} | 12 +++++++++--- apps/widbatpc/widbatpc.settings.json | 1 + apps/widbatpc/{widget.js => widbatpc.wid.js} | 13 ++++++++++--- 3 files changed, 20 insertions(+), 6 deletions(-) rename apps/widbatpc/{settings.js => widbatpc.settings.js} (88%) create mode 100644 apps/widbatpc/widbatpc.settings.json rename apps/widbatpc/{widget.js => widbatpc.wid.js} (95%) diff --git a/apps/widbatpc/settings.js b/apps/widbatpc/widbatpc.settings.js similarity index 88% rename from apps/widbatpc/settings.js rename to apps/widbatpc/widbatpc.settings.js index 5c0bdbcae..9f39b5d07 100644 --- a/apps/widbatpc/settings.js +++ b/apps/widbatpc/widbatpc.settings.js @@ -11,6 +11,7 @@ 'color': COLORS[0], 'percentage': true, 'charger': true, + 'hideifmorethan20pct': false, } // ...and overwrite them with any saved values // This way saved values are preserved if a new version adds more settings @@ -51,8 +52,13 @@ const newIndex = (oldIndex + 1) % COLORS.length s.color = COLORS[newIndex] save('color')(s.color) - }, - }, - } + } + }, + 'Hide when \> 20\%': { + value: s.hideifmorethan20pct, + format: onOffFormat, + onchange: save('hideifmorethan20pct'), + }, + } E.showMenu(menu) }) diff --git a/apps/widbatpc/widbatpc.settings.json b/apps/widbatpc/widbatpc.settings.json new file mode 100644 index 000000000..7a22adfc0 --- /dev/null +++ b/apps/widbatpc/widbatpc.settings.json @@ -0,0 +1 @@ +{"color":"By Level","percentage":true,"charger":true,"hideifmorethan20pct":false} diff --git a/apps/widbatpc/widget.js b/apps/widbatpc/widbatpc.wid.js similarity index 95% rename from apps/widbatpc/widget.js rename to apps/widbatpc/widbatpc.wid.js index aca690ce0..959ec211f 100644 --- a/apps/widbatpc/widget.js +++ b/apps/widbatpc/widbatpc.wid.js @@ -3,6 +3,7 @@ const DEFAULTS = { 'color': 'By Level', 'percentage': true, 'charger': true, + 'hideifmorethan20pct': false, } const COLORS = { 'white': -1, @@ -53,8 +54,16 @@ function setWidth() { } } function draw() { + var s = 39; var x = this.x, y = this.y; + const l = E.getBattery(), + c = levelColor(l); + const xl = x+4+l*(s-12)/100 + + if(!Bangle.isCharging() && setting('hideifmorethan20pct') && l > 20){ + return;} + if (Bangle.isCharging() && setting('charger')) { g.setColor(chargerColor()).drawImage(atob( "DhgBHOBzgc4HOP////////////////////3/4HgB4AeAHgB4AeAHgB4AeAHg"),x,y); @@ -64,9 +73,7 @@ function draw() { g.fillRect(x,y+2,x+s-4,y+21); g.clearRect(x+2,y+4,x+s-6,y+19); g.fillRect(x+s-3,y+10,x+s,y+14); - const l = E.getBattery(), - c = levelColor(l); - const xl = x+4+l*(s-12)/100 + g.setColor(c).fillRect(x+4,y+6,xl,y+17); g.setColor(-1); if (!setting('percentage')) { From d2270eaae6565a322895e25afb3c638a26c2b186 Mon Sep 17 00:00:00 2001 From: Frederic R Date: Sun, 19 Apr 2020 16:48:33 +0100 Subject: [PATCH 173/302] Update App Calculator's Readme. Larger image --- apps/calculator/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/calculator/README.md b/apps/calculator/README.md index 78848908a..b25d355bf 100644 --- a/apps/calculator/README.md +++ b/apps/calculator/README.md @@ -2,7 +2,7 @@ Basic calculator reminiscent of MacOs's one. Handy for small calculus. - + ## Features From 4d52de224ceddc49aabc107bf246374da8393106 Mon Sep 17 00:00:00 2001 From: Stefano Baldan Date: Sun, 19 Apr 2020 18:03:14 +0200 Subject: [PATCH 174/302] Added app for running --- apps.json | 21 +++ apps/banglerun/ChangeLog | 1 + apps/banglerun/app-icon.js | 1 + apps/banglerun/app.js | 314 +++++++++++++++++++++++++++++++++++ apps/banglerun/banglerun.png | Bin 0 -> 10456 bytes 5 files changed, 337 insertions(+) create mode 100755 apps/banglerun/ChangeLog create mode 100644 apps/banglerun/app-icon.js create mode 100644 apps/banglerun/app.js create mode 100644 apps/banglerun/banglerun.png diff --git a/apps.json b/apps.json index 0c97b9e57..143b02bd3 100644 --- a/apps.json +++ b/apps.json @@ -1293,5 +1293,26 @@ "evaluate": true } ] + }, + { + "id": "banglerun", + "name": "BangleRun", + "shortName": "BangleRun", + "icon": "banglerun.png", + "version": "0.01", + "description": "An app for running sessions.", + "tags": "run,running,fitness,outdoors", + "allow_emulator": false, + "storage": [ + { + "name": "banglerun.app.js", + "url": "app.js" + }, + { + "name": "banglerun.img", + "url": "app-icon.js", + "evaluate": true + } + ] } ] diff --git a/apps/banglerun/ChangeLog b/apps/banglerun/ChangeLog new file mode 100755 index 000000000..7b83706bf --- /dev/null +++ b/apps/banglerun/ChangeLog @@ -0,0 +1 @@ +0.01: First release diff --git a/apps/banglerun/app-icon.js b/apps/banglerun/app-icon.js new file mode 100644 index 000000000..0ccbedab4 --- /dev/null +++ b/apps/banglerun/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwMB/4ACx4ED/0DApP8AqAXB84GDg/DAgXj/+DCAUABgIFB4EAv4FCwEAj0PAoJPBgwFEgEfDgMOAoM/AoMegFAAoP8jkA8F/AoM8gP4DgP4nBvD/F4KQfwuAFE+A/CAoPgAofx8A/CKYRwELIIFDLII6BAoZSBLIYeC/0BwAFDgfAGAQFBHgf8g4BBIIUH/wFBSYMPAoXwAog/Bj4FEv4FDDQQCBQoQFCZYYFi/6KE/+P/4A=")) diff --git a/apps/banglerun/app.js b/apps/banglerun/app.js new file mode 100644 index 000000000..fc21e3627 --- /dev/null +++ b/apps/banglerun/app.js @@ -0,0 +1,314 @@ +/** Global constants */ +const DEG_TO_RAD = Math.PI / 180; +const EARTH_RADIUS = 6371008.8; + +/** Utilities for handling vectors */ +class Vector { + static magnitude(a) { + let sum = 0; + for (const key of Object.keys(a)) { + sum += a[key] * a[key]; + } + return Math.sqrt(sum); + } + + static add(a, b) { + const result = {}; + for (const key of Object.keys(a)) { + result[key] = a[key] + b[key]; + } + return result; + } + + static sub(a, b) { + const result = {}; + for (const key of Object.keys(a)) { + result[key] = a[key] - b[key]; + } + return result; + } + + static multiplyScalar(a, x) { + const result = {}; + for (const key of Object.keys(a)) { + result[key] = a[key] * x; + } + return result; + } + + static divideScalar(a, x) { + const result = {}; + for (const key of Object.keys(a)) { + result[key] = a[key] / x; + } + return result; + } +} + +/** Interquartile range filter, to detect outliers */ +class IqrFilter { + constructor(size, threshold) { + const q = Math.floor(size / 4); + this._buffer = []; + this._size = 4 * q + 2; + this._i1 = q; + this._i3 = 3 * q + 1; + this._threshold = threshold; + } + + isReady() { + return this._buffer.length === this._size; + } + + isOutlier(point) { + let result = true; + if (this._buffer.length === this._size) { + result = false; + for (const key of Object.keys(point)) { + const data = this._buffer.map(item => item[key]); + data.sort((a, b) => (a - b) / Math.abs(a - b)); + const q1 = data[this._i1]; + const q3 = data[this._i3]; + const iqr = q3 - q1; + const lower = q1 - this._threshold * iqr; + const upper = q3 + this._threshold * iqr; + if (point[key] < lower || point[key] > upper) { + result = true; + break; + } + } + } + this._buffer.push(point); + this._buffer = this._buffer.slice(-this._size); + return result; + } +} + +/** Process GPS data */ +class Gps { + constructor() { + this._lastCall = Date.now(); + this._lastValid = 0; + this._coords = null; + this._filter = new IqrFilter(10, 1.5); + this._shift = { x: 0, y: 0, z: 0 }; + } + + isReady() { + return this._filter.isReady(); + } + + getDistance(gps) { + const time = Date.now(); + const interval = (time - this._lastCall) / 1000; + this._lastCall = time; + + if (!gps.fix) { + return { t: interval, d: 0 }; + } + + const p = gps.lat * DEG_TO_RAD; + const q = gps.lon * DEG_TO_RAD; + const coords = { + x: EARTH_RADIUS * Math.sin(p) * Math.cos(q), + y: EARTH_RADIUS * Math.sin(p) * Math.sin(q), + z: EARTH_RADIUS * Math.cos(p), + }; + + if (!this._coords) { + this._coords = coords; + this._lastValid = time; + return { t: interval, d: 0 }; + } + + const ds = Vector.sub(coords, this._coords); + const dt = (time - this._lastValid) / 1000; + const v = Vector.divideScalar(ds, dt); + + if (this._filter.isOutlier(v)) { + return { t: interval, d: 0 }; + } + + this._shift = Vector.add(this._shift, ds); + const length = Vector.magnitude(this._shift); + const remainder = length % 10; + const distance = length - remainder; + + this._coords = coords; + this._lastValid = time; + if (distance > 0) { + this._shift = Vector.multiplyScalar(this._shift, remainder / length); + } + + return { t: interval, d: distance }; + } +} + +/** Process step counter data */ +class Step { + constructor(size) { + this._buffer = []; + this._size = size; + } + + getCadence() { + this._buffer.push(Date.now() / 1000); + this._buffer = this._buffer.slice(-this._size); + const interval = this._buffer[this._buffer.length - 1] - this._buffer[0]; + return interval ? Math.round(60 * (this._buffer.length - 1) / interval) : 0; + } +} + +const gps = new Gps(); +const step = new Step(10); + +let totDist = 0; +let totTime = 0; +let totSteps = 0; + +let speed = 0; +let cadence = 0; +let heartRate = 0; + +let gpsReady = false; +let hrmReady = false; +let running = false; + +function formatClock(date) { + return ('0' + date.getHours()).substr(-2) + ':' + ('0' + date.getMinutes()).substr(-2); +} + +function formatDistance(m) { + return ('0' + (m / 1000).toFixed(2) + ' km').substr(-7); +} + +function formatTime(s) { + const hrs = Math.floor(s / 3600); + const min = Math.floor(s / 60); + const sec = Math.floor(s % 60); + return (hrs ? hrs + ':' : '') + ('0' + min).substr(-2) + `:` + ('0' + sec).substr(-2); +} + +function formatSpeed(kmh) { + if (kmh <= 0.6) { + return `__'__"`; + } + const skm = 3600 / kmh; + const min = Math.floor(skm / 60); + const sec = Math.floor(skm % 60); + return ('0' + min).substr(-2) + `'` + ('0' + sec).substr(-2) + `"`; +} + +function drawBackground() { + g.setColor(running ? 0x00E0 : 0x0000); + g.fillRect(0, 30, 240, 240); + + g.setColor(0xFFFF); + g.setFontAlign(0, -1, 0); + g.setFont('6x8', 2); + + g.drawString('DISTANCE', 120, 50); + g.drawString('TIME', 60, 100); + g.drawString('PACE', 180, 100); + g.drawString('STEPS', 60, 150); + g.drawString('STP/m', 180, 150); + g.drawString('SPEED', 40, 200); + g.drawString('HEART', 120, 200); + g.drawString('CADENCE', 200, 200); +} + +function draw() { + const totSpeed = totTime ? 3.6 * totDist / totTime : 0; + const totCadence = totTime ? Math.round(60 * totSteps / totTime) : 0; + + g.setColor(running ? 0x00E0 : 0x0000); + g.fillRect(0, 30, 240, 50); + g.fillRect(0, 70, 240, 100); + g.fillRect(0, 120, 240, 150); + g.fillRect(0, 170, 240, 200); + g.fillRect(0, 220, 240, 240); + + g.setFont('6x8', 2); + + g.setFontAlign(-1, -1, 0); + g.setColor(gpsReady ? 0x07E0 : 0xF800); + g.drawString(' GPS', 6, 30); + + g.setFontAlign(1, -1, 0); + g.setColor(0xFFFF); + g.drawString(formatClock(new Date()), 234, 30); + + g.setFontAlign(0, -1, 0); + g.setFontVector(20); + g.drawString(formatDistance(totDist), 120, 70); + g.drawString(formatTime(totTime), 60, 120); + g.drawString(formatSpeed(totSpeed), 180, 120); + g.drawString(totSteps, 60, 170); + g.drawString(totCadence, 180, 170); + + g.setFont('6x8', 2); + g.drawString(formatSpeed(speed), 40, 220); + + g.setColor(hrmReady ? 0x07E0 : 0xF800); + g.drawString(heartRate, 120, 220); + + g.setColor(0xFFFF); + g.drawString(cadence, 200, 220); +} + +function handleGps(coords) { + const step = gps.getDistance(coords); + gpsReady = coords.fix > 0 && gps.isReady(); + speed = isFinite(gps.speed) ? gps.speed : 0; + if (running) { + totDist += step.d; + totTime += step.t; + } +} + +function handleHrm(hrm) { + hrmReady = hrm.confidence > 50; + heartRate = hrm.bpm; +} + +function handleStep() { + cadence = step.getCadence(); + if (running) { + totSteps += 1; + } +} + +function start() { + running = true; + drawBackground(); + draw(); +} + +function stop() { + if (!running) { + totDist = 0; + totTime = 0; + totSteps = 0; + } + running = false; + drawBackground(); + draw(); +} + +Bangle.on('GPS', handleGps); +Bangle.on('HRM', handleHrm); +Bangle.on('step', handleStep); + +Bangle.setGPSPower(1); +Bangle.setHRMPower(1); + +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +drawBackground(); +draw(); + +setInterval(draw, 500); + +setWatch(start, BTN1, { repeat: true }); +setWatch(stop, BTN3, { repeat: true }); diff --git a/apps/banglerun/banglerun.png b/apps/banglerun/banglerun.png new file mode 100644 index 0000000000000000000000000000000000000000..bf2cd8af3ef5f9711d18df1656a03a98283bcc67 GIT binary patch literal 10456 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 O0000 Date: Mon, 20 Apr 2020 09:27:57 +0200 Subject: [PATCH 175/302] Removed href="#" that scrolls page on top --- js/index.js | 2 +- js/utils.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/js/index.js b/js/index.js index ef9bcb4f1..d8c7a4473 100644 --- a/js/index.js +++ b/js/index.js @@ -207,7 +207,7 @@ function refreshLibrary() { var version = getVersionInfo(app, appInstalled); var versionInfo = version.text; if (versionInfo) versionInfo = " ("+versionInfo+")"; - var readme = `Read more...`; + var readme = `Read more...`; var favourite = favourites.find(e => e == app.id); return `
diff --git a/js/utils.js b/js/utils.js index 4913c7129..406e3a0a1 100644 --- a/js/utils.js +++ b/js/utils.js @@ -49,7 +49,7 @@ function getVersionInfo(appListing, appInstalled) { var versionText = ""; var canUpdate = false; function clicky(v) { - return `${v}`; + return `${v}`; } if (!appInstalled) { From 9cc58597cd5d0934f34da6a935b2187cb1b3df36 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Mon, 20 Apr 2020 09:21:27 +0100 Subject: [PATCH 176/302] merge with new namings --- apps/widbatpc/{widbatpc.settings.js => settings.js} | 0 apps/widbatpc/{widbatpc.wid.js => widget.js} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename apps/widbatpc/{widbatpc.settings.js => settings.js} (100%) rename apps/widbatpc/{widbatpc.wid.js => widget.js} (100%) diff --git a/apps/widbatpc/widbatpc.settings.js b/apps/widbatpc/settings.js similarity index 100% rename from apps/widbatpc/widbatpc.settings.js rename to apps/widbatpc/settings.js diff --git a/apps/widbatpc/widbatpc.wid.js b/apps/widbatpc/widget.js similarity index 100% rename from apps/widbatpc/widbatpc.wid.js rename to apps/widbatpc/widget.js From 20bb1aed8456dcfc6028d77400970e45ebf6c2db Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Mon, 20 Apr 2020 10:06:23 +0100 Subject: [PATCH 177/302] Add 'hide if charge greater than' Move 'DEFAULTS' to try and reduce memory usage a little --- apps.json | 4 +-- apps/widbatpc/ChangeLog | 1 + apps/widbatpc/settings.js | 23 +++++++------ apps/widbatpc/widbatpc.settings.json | 1 - apps/widbatpc/widget.js | 50 +++++++++++++++++----------- 5 files changed, 46 insertions(+), 33 deletions(-) delete mode 100644 apps/widbatpc/widbatpc.settings.json diff --git a/apps.json b/apps.json index fe3d5f3eb..5b024eb97 100644 --- a/apps.json +++ b/apps.json @@ -354,7 +354,7 @@ "name": "Battery Level Widget (with percentage)", "shortName": "Battery Widget", "icon": "widget.png", - "version":"0.09", + "version":"0.10", "description": "Show the current battery level and charging status in the top right of the clock, with charge percentage", "tags": "widget,battery", "type":"widget", @@ -1372,7 +1372,7 @@ {"name":"hidcam.img","url":"app-icon.js","evaluate":true} ] }, - { + { "id": "rclock", "name": "Round clock with seconds, minutes and date", "shortName":"Round Clock", diff --git a/apps/widbatpc/ChangeLog b/apps/widbatpc/ChangeLog index 129707320..4c5f16a04 100644 --- a/apps/widbatpc/ChangeLog +++ b/apps/widbatpc/ChangeLog @@ -6,3 +6,4 @@ 0.07: Add settings: percentage/color/charger icon 0.08: Draw percentage as inverted on monochrome battery 0.09: Fix regression stopping correct widget updates +0.10: Add 'hide if charge greater than' diff --git a/apps/widbatpc/settings.js b/apps/widbatpc/settings.js index 9f39b5d07..bfed48f09 100644 --- a/apps/widbatpc/settings.js +++ b/apps/widbatpc/settings.js @@ -11,22 +11,22 @@ 'color': COLORS[0], 'percentage': true, 'charger': true, - 'hideifmorethan20pct': false, + 'hideifmorethan': 100, } // ...and overwrite them with any saved values // This way saved values are preserved if a new version adds more settings const storage = require('Storage') const saved = storage.readJSON(SETTINGS_FILE, 1) || {} for (const key in saved) { - s[key] = saved[key] + s[key] = saved[key]; } // creates a function to safe a specific setting, e.g. save('color')(1) function save(key) { return function (value) { - s[key] = value - storage.write(SETTINGS_FILE, s) - WIDGETS["batpc"].reload() + s[key] = value; + storage.write(SETTINGS_FILE, s); + WIDGETS["batpc"].reload(); } } @@ -54,11 +54,14 @@ save('color')(s.color) } }, - 'Hide when \> 20\%': { - value: s.hideifmorethan20pct, - format: onOffFormat, - onchange: save('hideifmorethan20pct'), + 'Hide if >': { + value: s.hideifmorethan||100, + min: 10, + max : 100, + step: 10, + format: x => x+"%", + onchange: save('hideifmorethan'), }, - } + } E.showMenu(menu) }) diff --git a/apps/widbatpc/widbatpc.settings.json b/apps/widbatpc/widbatpc.settings.json deleted file mode 100644 index 7a22adfc0..000000000 --- a/apps/widbatpc/widbatpc.settings.json +++ /dev/null @@ -1 +0,0 @@ -{"color":"By Level","percentage":true,"charger":true,"hideifmorethan20pct":false} diff --git a/apps/widbatpc/widget.js b/apps/widbatpc/widget.js index 959ec211f..fe6f8550c 100644 --- a/apps/widbatpc/widget.js +++ b/apps/widbatpc/widget.js @@ -1,10 +1,4 @@ (function(){ -const DEFAULTS = { - 'color': 'By Level', - 'percentage': true, - 'charger': true, - 'hideifmorethan20pct': false, -} const COLORS = { 'white': -1, 'charging': 0x07E0, // "Green" @@ -17,10 +11,19 @@ const SETTINGS_FILE = 'widbatpc.settings.json' let settings function loadSettings() { settings = require('Storage').readJSON(SETTINGS_FILE, 1) || {} + const DEFAULTS = { + 'color': 'By Level', + 'percentage': true, + 'charger': true, + 'hideifmorethan': 100, + }; + Object.keys(DEFAULTS).forEach(k=>{ + if (settings[k]===undefined) settings[k]=DEFAULTS[k] + }); } function setting(key) { if (!settings) { loadSettings() } - return (key in settings) ? settings[key] : DEFAULTS[key] + return settings[key]; } const levelColor = (l) => { @@ -46,24 +49,27 @@ const levelColor = (l) => { const chargerColor = () => { return (setting('color') === 'Monochrome') ? COLORS.white : COLORS.charging } - +// sets width, returns true if it changed function setWidth() { - WIDGETS["batpc"].width = 40; - if (Bangle.isCharging() && setting('charger')) { - WIDGETS["batpc"].width += 16; - } + var w = 40; + if (Bangle.isCharging() && setting('charger')) + w += 16; + if (E.getBattery() > setting('hideifmorethan')) + w = 0; + var changed = WIDGETS["batpc"].width != w; + WIDGETS["batpc"].width = w; + return changed; } function draw() { - + // if hidden, don't draw + if (!WIDGETS["batpc"].width) return; + // else... var s = 39; var x = this.x, y = this.y; const l = E.getBattery(), c = levelColor(l); const xl = x+4+l*(s-12)/100 - if(!Bangle.isCharging() && setting('hideifmorethan20pct') && l > 20){ - return;} - if (Bangle.isCharging() && setting('charger')) { g.setColor(chargerColor()).drawImage(atob( "DhgBHOBzgc4HOP////////////////////3/4HgB4AeAHgB4AeAHgB4AeAHg"),x,y); @@ -104,20 +110,24 @@ function reload() { g.clear(); Bangle.drawWidgets(); } +// update widget - redraw just widget, or all widgets if size changed +function update() { + if (setWidth()) Bangle.drawWidgets(); + else WIDGETS["batpc"].draw(); +} Bangle.on('charging',function(charging) { if(charging) Bangle.buzz(); - setWidth(); - Bangle.drawWidgets(); // relayout widgets + update(); g.flip(); }); var batteryInterval; Bangle.on('lcdPower', function(on) { if (on) { - WIDGETS["batpc"].draw(); + update(); // refresh once a minute if LCD on if (!batteryInterval) - batteryInterval = setInterval(()=>WIDGETS["batpc"].draw(), 60000); + batteryInterval = setInterval(update, 60000); } else { if (batteryInterval) { clearInterval(batteryInterval); From 5fe7f48477818c1bb31b7650e9d93f45d594f7e0 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Mon, 20 Apr 2020 10:07:55 +0100 Subject: [PATCH 178/302] Add 'update all' button (fix #237) --- index.html | 1 + js/index.js | 42 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/index.html b/index.html index f016ffb49..3c8b440e4 100644 --- a/index.html +++ b/index.html @@ -113,6 +113,7 @@
+
diff --git a/js/index.js b/js/index.js index 2b1fb1cfc..51b1d71c3 100644 --- a/js/index.js +++ b/js/index.js @@ -218,7 +218,7 @@ function refreshLibrary() {

${escapeHtml(app.description)}${app.readme?`
${readme}`:""}

See the code on GitHub
-
+
@@ -401,10 +401,18 @@ function showLoadingIndicator(id) { panelbody.innerHTML = '
'; } +function getAppsToUpdate() { + var appsToUpdate = []; + appsInstalled.forEach(appInstalled => { + var app = appNameToApp(appInstalled.id); + if (app.version != appInstalled.version) + appsToUpdate.push(app); + }); + return appsToUpdate; +} + function refreshMyApps() { var panelbody = document.querySelector("#myappscontainer .panel-body"); - var tab = document.querySelector("#tab-myappscontainer a"); - tab.setAttribute("data-badge", appsInstalled.length); panelbody.innerHTML = appsInstalled.map(appInstalled => { var app = appNameToApp(appInstalled.id); var version = getVersionInfo(app, appInstalled); @@ -436,6 +444,17 @@ return `
if (icon.classList.contains("icon-download")) handleAppInterface(app); }); }); + var appsToUpdate = getAppsToUpdate(); + var tab = document.querySelector("#tab-myappscontainer a"); + var updateApps = document.querySelector("#myappscontainer .updateapps"); + if (appsToUpdate.length) { + updateApps.innerHTML = `Update ${appsToUpdate.length} apps`; + updateApps.classList.remove("hidden"); + tab.setAttribute("data-badge", `${appsInstalled.length} ⬆${appsToUpdate.length}`); + } else { + updateApps.classList.add("hidden"); + tab.setAttribute("data-badge", appsInstalled.length); + } } let haveInstalledApps = false; @@ -471,6 +490,22 @@ htmlToArray(document.querySelectorAll(".btn.refresh")).map(button => button.addE showToast("Getting app list failed, "+err,"error"); }); })); +htmlToArray(document.querySelectorAll(".btn.updateapps")).map(button => button.addEventListener("click", () => { + var appsToUpdate = getAppsToUpdate(); + var count = appsToUpdate.length; + function updater() { + if (!appsToUpdate.length) return; + var app = appsToUpdate.pop(); + return updateApp(app).then(function() { + return updater(); + }); + } + updater().then(err => { + showToast(`Updated ${count} apps`,"success"); + }).catch(err => { + showToast("Update failed, "+err,"error"); + }); +})); connectMyDeviceBtn.addEventListener("click", () => { if (connectMyDeviceBtn.classList.contains('is-connected')) { Comms.disconnectDevice(); @@ -621,4 +656,3 @@ document.getElementById("installfavourite").addEventListener("click",event=>{ showToast("App Install failed, "+err,"error"); }); }); - From 0f8247e7e49f3b976e96dea0626a3369ea2a9456 Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Mon, 20 Apr 2020 11:52:19 +0200 Subject: [PATCH 179/302] Data file fixes for settings/welcome/ncstart settings: never delete settings file welcome: fix typo, remove outdated comment ncstart: remove outdated comment --- apps.json | 3 --- apps/ncstart/settings.js | 1 - apps/welcome/settings.js | 3 +-- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/apps.json b/apps.json index a18029abc..32a7e7dff 100644 --- a/apps.json +++ b/apps.json @@ -130,9 +130,6 @@ {"name":"setting.boot.js","url":"boot.js"}, {"name":"setting.img","url":"settings-icon.js","evaluate":true} ], - "data": [ - {"name":"setting.json"} - ], "sortorder" : -2 }, { "id": "alarm", diff --git a/apps/ncstart/settings.js b/apps/ncstart/settings.js index 6780264a7..560fad8ba 100644 --- a/apps/ncstart/settings.js +++ b/apps/ncstart/settings.js @@ -1,4 +1,3 @@ -// The welcome app is special, and gets to use global settings (function(back) { let settings = require('Storage').readJSON('ncstart.json', 1) || require('Storage').readJSON('setting.json', 1) || {} diff --git a/apps/welcome/settings.js b/apps/welcome/settings.js index e873c2785..20c2e9b13 100644 --- a/apps/welcome/settings.js +++ b/apps/welcome/settings.js @@ -1,6 +1,5 @@ -// The welcome app is special, and gets to use global settings (function(back) { - let settings = require('Storage').readJSON('welcome.sjson', 1) + let settings = require('Storage').readJSON('welcome.json', 1) || require('Storage').readJSON('setting.json', 1) || {} E.showMenu({ '': { 'title': 'Welcome App' }, From ae738e9d99b9f54f20b230ea23a5e0ad043b7471 Mon Sep 17 00:00:00 2001 From: Purple-Tentacle <59914607+Purple-Tentacle@users.noreply.github.com> Date: Mon, 20 Apr 2020 15:57:26 +0200 Subject: [PATCH 180/302] chronowid 0.03 --- apps/chronowid/ChangeLog | 5 +- apps/chronowid/widget.js | 184 +++++++++++++++++++-------------------- 2 files changed, 95 insertions(+), 94 deletions(-) diff --git a/apps/chronowid/ChangeLog b/apps/chronowid/ChangeLog index 263145407..e173467a1 100644 --- a/apps/chronowid/ChangeLog +++ b/apps/chronowid/ChangeLog @@ -1,2 +1,3 @@ -0.01: New widget and app! -0.02: Setting to reset values, timer buzzes at 00:00 and not later (see readme) \ No newline at end of file +0.01: New widget and app! +0.02: Setting to reset values, timer buzzes at 00:00 and not later (see readme) +0.03: Display only minutes:seconds when less than 1 hour left \ No newline at end of file diff --git a/apps/chronowid/widget.js b/apps/chronowid/widget.js index 557104d92..0c9366b86 100644 --- a/apps/chronowid/widget.js +++ b/apps/chronowid/widget.js @@ -1,93 +1,93 @@ -(() => { - const storage = require('Storage'); - settingsChronowid = storage.readJSON("chronowid.json",1)||{}; //read settingsChronowid from file - var height = 23; - var width = 58; - var interval = 0; //used for the 1 second interval timer - var now = new Date(); - - var time = 0; - var diff = settingsChronowid.goal - now; - - //Convert ms to time - function getTime(t) { - var milliseconds = parseInt((t % 1000) / 100), - seconds = Math.floor((t / 1000) % 60), - minutes = Math.floor((t / (1000 * 60)) % 60), - hours = Math.floor((t / (1000 * 60 * 60)) % 24); - - hours = (hours < 10) ? "0" + hours : hours; - minutes = (minutes < 10) ? "0" + minutes : minutes; - seconds = (seconds < 10) ? "0" + seconds : seconds; - - return hours + ":" + minutes + ":" + seconds; - } - - function printDebug() { - print ("Nowtime: " + getTime(now)); - print ("Now: " + now); - print ("Goaltime: " + getTime(settingsChronowid.goal)); - print ("Goal: " + settingsChronowid.goal); - print("Difftime: " + getTime(diff)); - print("Diff: " + diff); - print ("Started: " + settingsChronowid.started); - print ("----"); - } - - //counts down, calculates and displays - function countDown() { - now = new Date(); - diff = settingsChronowid.goal - now; //calculate difference - WIDGETS["chronowid"].draw(); - //time is up - if (settingsChronowid.started && diff < 1000) { - Bangle.buzz(1500); - //write timer off to file - settingsChronowid.started = false; - storage.writeJSON('chronowid.json', settingsChronowid); - clearInterval(interval); //stop interval - } - //printDebug(); - } - - // draw your widget - function draw() { - if (!settingsChronowid.started) { - width = 0; - return; //do not draw anything if timer is not started - } - g.reset(); - if (diff >= 0) { - if (diff < 600000) { //less than 1 hour left - width = 58; - g.clearRect(this.x,this.y,this.x+width,this.y+height); - g.setFont("6x8", 2); - g.drawString(getTime(diff).substring(3), this.x+1, this.y+5); //remove hour part 00:00:00 -> 00:00 - } - if (diff >= 600000) { //one hour or more left - width = 48; - g.clearRect(this.x,this.y,this.x+width,this.y+height); - g.setFont("6x8", 1); - g.drawString(getTime(diff), this.x+1, this.y+((height/2)-4)); //display hour 00:00:00 - } - } - // not needed anymoe, because we check if diff < 1000 now, so 00:00 is displayed. - // else { - // width = 58; - // g.clearRect(this.x,this.y,this.x+width,this.y+height); - // g.setFont("6x8", 2); - // g.drawString("END", this.x+15, this.y+5); - // } - } - - if (settingsChronowid.started) interval = setInterval(countDown, 1000); //start countdown each second - - // add the widget - WIDGETS["chronowid"]={area:"bl",width:width,draw:draw,reload:function() { - reload(); - Bangle.drawWidgets(); // relayout all widgets - }}; - - //printDebug(); - countDown(); +(() => { + const storage = require('Storage'); + settingsChronowid = storage.readJSON("chronowid.json",1)||{}; //read settingsChronowid from file + var height = 23; + var width = 58; + var interval = 0; //used for the 1 second interval timer + var now = new Date(); + + var time = 0; + var diff = settingsChronowid.goal - now; + + //Convert ms to time + function getTime(t) { + var milliseconds = parseInt((t % 1000) / 100), + seconds = Math.floor((t / 1000) % 60), + minutes = Math.floor((t / (1000 * 60)) % 60), + hours = Math.floor((t / (1000 * 60 * 60)) % 24); + + hours = (hours < 10) ? "0" + hours : hours; + minutes = (minutes < 10) ? "0" + minutes : minutes; + seconds = (seconds < 10) ? "0" + seconds : seconds; + + return hours + ":" + minutes + ":" + seconds; + } + + function printDebug() { + print ("Nowtime: " + getTime(now)); + print ("Now: " + now); + print ("Goaltime: " + getTime(settingsChronowid.goal)); + print ("Goal: " + settingsChronowid.goal); + print("Difftime: " + getTime(diff)); + print("Diff: " + diff); + print ("Started: " + settingsChronowid.started); + print ("----"); + } + + //counts down, calculates and displays + function countDown() { + now = new Date(); + diff = settingsChronowid.goal - now; //calculate difference + WIDGETS["chronowid"].draw(); + //time is up + if (settingsChronowid.started && diff < 1000) { + Bangle.buzz(1500); + //write timer off to file + settingsChronowid.started = false; + storage.writeJSON('chronowid.json', settingsChronowid); + clearInterval(interval); //stop interval + } + //printDebug(); + } + + // draw your widget + function draw() { + if (!settingsChronowid.started) { + width = 0; + return; //do not draw anything if timer is not started + } + g.reset(); + if (diff >= 0) { + if (diff < 3600000) { //less than 1 hour left + width = 58; + g.clearRect(this.x,this.y,this.x+width,this.y+height); + g.setFont("6x8", 2); + g.drawString(getTime(diff).substring(3), this.x+1, this.y+5); //remove hour part 00:00:00 -> 00:00 + } + if (diff >= 3600000) { //one hour or more left + width = 48; + g.clearRect(this.x,this.y,this.x+width,this.y+height); + g.setFont("6x8", 1); + g.drawString(getTime(diff), this.x+1, this.y+((height/2)-4)); //display hour 00:00:00 + } + } + // not needed anymoe, because we check if diff < 1000 now, so 00:00 is displayed. + // else { + // width = 58; + // g.clearRect(this.x,this.y,this.x+width,this.y+height); + // g.setFont("6x8", 2); + // g.drawString("END", this.x+15, this.y+5); + // } + } + + if (settingsChronowid.started) interval = setInterval(countDown, 1000); //start countdown each second + + // add the widget + WIDGETS["chronowid"]={area:"bl",width:width,draw:draw,reload:function() { + reload(); + Bangle.drawWidgets(); // relayout all widgets + }}; + + //printDebug(); + countDown(); })(); \ No newline at end of file From af0ac4041cb772856b7c12ac6d3ada6f360a246e Mon Sep 17 00:00:00 2001 From: Purple-Tentacle <59914607+Purple-Tentacle@users.noreply.github.com> Date: Mon, 20 Apr 2020 15:58:05 +0200 Subject: [PATCH 181/302] chronowid 0.03 --- apps.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps.json b/apps.json index d7bd6a7a1..274a3a8b2 100644 --- a/apps.json +++ b/apps.json @@ -1142,7 +1142,7 @@ "name": "Chrono Widget", "shortName":"Chrono Widget", "icon": "app.png", - "version":"0.02", + "version":"0.03", "description": "Chronometer (timer) which runs as widget.", "tags": "tools,widget", "readme": "README.md", From 74bd784b091ad97964a7923fa888816fd8770412 Mon Sep 17 00:00:00 2001 From: Paul Cockrell Date: Mon, 20 Apr 2020 15:00:06 +0100 Subject: [PATCH 182/302] Initial commit of BuffGym 5x5 training program --- apps.json | 14 ++ apps/buffgym/.eslintrc.json | 33 ++++ apps/buffgym/buffgym-exercise.js | 187 +++++++++++++++++++ apps/buffgym/buffgym-icon.js | 1 + apps/buffgym/buffgym-program.js | 68 +++++++ apps/buffgym/buffgym-set.js | 46 +++++ apps/buffgym/buffgym.app.js | 311 +++++++++++++++++++++++++++++++ apps/buffgym/buffgym.png | Bin 0 -> 1800 bytes 8 files changed, 660 insertions(+) create mode 100644 apps/buffgym/.eslintrc.json create mode 100644 apps/buffgym/buffgym-exercise.js create mode 100644 apps/buffgym/buffgym-icon.js create mode 100644 apps/buffgym/buffgym-program.js create mode 100644 apps/buffgym/buffgym-set.js create mode 100755 apps/buffgym/buffgym.app.js create mode 100644 apps/buffgym/buffgym.png diff --git a/apps.json b/apps.json index 60b47dbd8..e993f0e18 100644 --- a/apps.json +++ b/apps.json @@ -1293,5 +1293,19 @@ "evaluate": true } ] + }, + { + "id": "buffgym", + "name": "BuffGym", + "icon": "buffgym.png", + "version":"0.01", + "description": "BuffGym is the famous 5x5 workout program for the BangleJS", + "tags": "tool,outdoors", + "type": "app", + "storage": [ + {"name":"buffgym"}, + {"name":"buffgym.app.js"}, + {"name":"buffgym.img","url":"buffgym-icon.js","evaluate":true} + ] } ] diff --git a/apps/buffgym/.eslintrc.json b/apps/buffgym/.eslintrc.json new file mode 100644 index 000000000..c91a72544 --- /dev/null +++ b/apps/buffgym/.eslintrc.json @@ -0,0 +1,33 @@ +{ + "env": { + "browser": true, + "commonjs": true, + "es6": true + }, + "extends": "eslint:recommended", + "globals": { + "Atomics": "readonly", + "SharedArrayBuffer": "readonly" + }, + "parserOptions": { + "ecmaVersion": 2018 + }, + "rules": { + "indent": [ + "error", + 2 + ], + "linebreak-style": [ + "error", + "windows" + ], + "quotes": [ + "error", + "double" + ], + "semi": [ + "error", + "always" + ] + } +} \ No newline at end of file diff --git a/apps/buffgym/buffgym-exercise.js b/apps/buffgym/buffgym-exercise.js new file mode 100644 index 000000000..b7a1e3e15 --- /dev/null +++ b/apps/buffgym/buffgym-exercise.js @@ -0,0 +1,187 @@ +const STARTED = 1; +const RESTING = 2; +const COMPLETED = 3; +const ONE_SECOND = 1000; +const INCREMENT = "increment"; +const DECREMENT = "decrement"; + +class Exercise { + constructor(params /*{title, weight, unit, restPeriod}*/) { + const DEFAULTS = { + title: "Unknown", + weight: 0, + unit: "Kg", + restPeriod: 90, + weightIncrement: 2.5, + }; + const p = Object.assign({}, DEFAULTS, params); + + this._title = p.title; + this._weight = p.weight; + this._unit = p.unit; + this._originalRestPeriod = p.restPeriod; // Used when reseting _restPeriod + this._restPeriod = p.restPeriod; + this._weightIncrement = p.weightIncrement; + this._started = new Date(); + this._completed = false; + this._sets = []; + this._restTimeout = null; + this._restInterval = null; + this._state = null; + } + + get title() { + return this._title; + } + + get humanTitle() { + return `${this._title} ${this._weight}${this._unit}`; + } + + get subTitle() { + const totalSets = this._sets.length; + const uncompletedSets = this._sets.filter((set) => !set.isCompleted()).length; + const currentSet = (totalSets - uncompletedSets) + 1; + return `Set ${currentSet} of ${totalSets}`; + } + + get restPeriod() { + return this._restPeriod; + } + + decRestPeriod() { + this._restPeriod--; + } + + get weight() { + return this._weight; + } + + get unit() { + return this._unit; + } + + get started() { + return this._started; + } + + addSet(set) { + this._sets.push(set); + } + + addSets(sets) { + sets.forEach(set => this.addSet(set)); + } + + get currentSet() { + return this._sets.filter(set => !set.isCompleted())[0]; + } + + isLastSet() { + return this._sets.filter(set => !set.isCompleted()).length === 1; + } + + isCompleted() { + return !!this._completed; + } + + canSetCompleted() { + return this._sets.filter(set => set.isCompleted()).length === this._sets.length; + } + + setCompleted() { + if (!this.canSetCompleted()) throw "All sets must be completed"; + if (this.canProgress) this._weight += this._weightIncrement; + this._completed = true; + } + + get canProgress() { + let completedRepsTotalSum = 0; + let targetRepsTotalSum = 0; + + const completedRepsTotal = this._sets.forEach(set => completedRepsTotalSum += set.reps); + const targetRepsTotal = this._sets.forEach(set => targetRepsTotalSum += set.maxReps); + + return (targetRepsTotalSum - completedRepsTotalSum) === 0; + } + + startRestTimer(program) { + this._restTimeout = setTimeout(() => { + this.next(); + }, ONE_SECOND * this._restPeriod); + + this._restInterval = setInterval(() => { + program.emit("redraw"); + }, ONE_SECOND); + } + + resetRestTimer() { + clearTimeout(this._restTimeout); + clearInterval(this._restInterval); + this._restTimeout = null; + this._restInterval = null; + this._restPeriod = this._originalRestPeriod; + } + + isRestTimerRunning() { + return this._restTimeout != null; + } + + setupStartedButtons(program) { + clearWatch(); + + setWatch(() => { + this.currentSet.incReps(); + program.emit("redraw"); + }, BTN1, {repeat: true}); + + setWatch(program.next.bind(program), BTN2, {repeat: false}); + + setWatch(() => { + this.currentSet.decReps(); + program.emit("redraw"); + }, BTN3, {repeat: true}); + } + + setupRestingButtons(program) { + clearWatch(); + setWatch(program.next.bind(program), BTN2, {repeat: true}); + } + + next(program) { + global.poo = this; + switch(this._state) { + case null: + console.log("XXX 1 moving null -> STARTED"); + this._state = STARTED; + this.setupStartedButtons(program); + break; + case STARTED: + console.log("XXX 2 moving STARTED -> RESTING"); + this._state = RESTING; + this.startRestTimer(program); + this.setupRestingButtons(program); + break; + case RESTING: + this.resetRestTimer(); + this.currentSet.setCompleted(); + + if (this.canSetCompleted()) { + console.log("XXX 3b moving RESTING -> COMPLETED"); + this._state = COMPLETED; + this.setCompleted(); + } else { + console.log("XXX 3a moving RESTING -> null"); + this._state = null; + } + // As we are changing state and require it to be reprocessed + // invoke the next step of program + program.next(program); + break; + default: + throw "Exercise: Attempting to move to an unknown state"; + } + + program.emit("redraw"); + } +} \ No newline at end of file diff --git a/apps/buffgym/buffgym-icon.js b/apps/buffgym/buffgym-icon.js new file mode 100644 index 000000000..949b0e45b --- /dev/null +++ b/apps/buffgym/buffgym-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AFEEolAC6lN7vdDCcECwPd6guVGCYuDC4cCBQMikQXQJAMjkECmcyIx4XDmUjmYvLC4XUDARHBIoIWLgATCGQdA7tEonQC5ouDDYg0BOxgSEAggwKRwgUCC6ZIDSwoXNogWDDgNCAgIWIkUEoUk6kiCgMkokipsiBIQXIki2CAgNCAoYADC5Eic4Mic4ICCAIIJCC5MzAAcykYGEAAIXOABAXTmUzGoIXVAIIXLB4SICDIovjO76PZbYR3PDI4XiI6530MIh3SC6R33C/oAOC48CCxsgC44A/ADY=")) diff --git a/apps/buffgym/buffgym-program.js b/apps/buffgym/buffgym-program.js new file mode 100644 index 000000000..22f39f10b --- /dev/null +++ b/apps/buffgym/buffgym-program.js @@ -0,0 +1,68 @@ +class Program { + constructor(params) { + const DEFAULTS = { + title: "Unknown", + trainDay: "", // Day of week + }; + const p = Object.assign({}, DEFAULTS, params); + + this._title = p.title; + this._trainDay = p.trainDay; + this._exercises = []; + + this.on("redraw", redraw.bind(null, this)); + } + + get title() { + return `${this._title} - ${this._trainDay}`; + } + + addExercise(exercise) { + this._exercises.push(exercise); + } + + addExercises(exercises) { + exercises.forEach(exercise => this.addExercise(exercise)); + } + + currentExercise() { + return ( + this._exercises + .filter(exercise => !exercise.isCompleted())[0] + ); + } + + canComplete() { + return ( + this._exercises + .filter(exercise => exercise.isCompleted()) + .length === this._exercises.length + ); + } + + setCompleted() { + if (!this.canComplete()) throw "All exercises must be completed"; + this._completed = true; + } + + isCompleted() { + return !!this._completed; + } + + // State machine + next() { + console.log("XXX Program.next"); + const exercise = this.currentExercise(); + + // All exercises are completed so mark the + // Program as comleted + if (this.canComplete()) { + this.setCompleted(); + this.emit("redraw"); + + return; + } + + exercise.next(this); + } +} \ No newline at end of file diff --git a/apps/buffgym/buffgym-set.js b/apps/buffgym/buffgym-set.js new file mode 100644 index 000000000..d7d610a78 --- /dev/null +++ b/apps/buffgym/buffgym-set.js @@ -0,0 +1,46 @@ +class Set { + constructor(maxReps) { + this._minReps = 0; + this._maxReps = maxReps; + this._reps = 0; + this._completed = false; + } + + get title() { + return this._title; + } + + get weight() { + return this._weight; + } + + isCompleted() { + return !!this._completed; + } + + setCompleted() { + this._completed = true; + } + + get reps() { + return this._reps; + } + + get maxReps() { + return this._maxReps; + } + + incReps() { + if (this._completed) return; + if (this._reps >= this._maxReps) return; + + this._reps++; + } + + decReps() { + if (this._completed) return; + if (this._reps <= this._minReps) return; + + this._reps--; + } +} \ No newline at end of file diff --git a/apps/buffgym/buffgym.app.js b/apps/buffgym/buffgym.app.js new file mode 100755 index 000000000..e74b7062a --- /dev/null +++ b/apps/buffgym/buffgym.app.js @@ -0,0 +1,311 @@ +/* global g, setWatch, clearWatch, reset, BTN1, BTN2, BTN3 */ + +(() => { + const W = g.getWidth(); + const H = g.getHeight(); + const RED = "#d32e29"; + const PINK = "#f05a56"; + const WHITE = "#ffffff"; + + const Set = require("set.js"); + const Exercise = require("exercise.js"); + const Program = require("program.js"); + + function centerStringX(str) { + return (W - g.stringWidth(str)) / 2; + } + + function iconIncrement() { + const img = require("heatshrink").decompress(atob("ikUxH+AA3XAAgNHCJIVMBYXQ5PC4XJ6AUJCIQQBAAoVCCQwjCAA/JCgglHA4IpJBYwTHA4RMJCY5oDJo4THKIQKET5IMGCaY7TMaKLTWajbTFJIlICgoVBFYXJYQYSGCggAGCRAVIBgw")); + return img; + } + + function iconDecrement() { + const img = require("heatshrink").decompress(atob("ikUxH+AA3XAAgNHCJIVMBYXQ5PC4XJ6AUJCIQQBAAoVCCQwjCAA/JCgglKFJADBCRYABCYQmOFAhNMKIw6FTw4LHCaY7TMaKLTWajbTFJglFCgoVBFYXJYQYSGCggAGCRAVIBgw=")); + return img; + } + + function iconOk() { + const img = require("heatshrink").decompress(atob("ikUxH+AA3XAAgNHCJIVMBYXQ5PC4XJ6AUJCIQQBAAoVCCQwjCAA/JCgglKFJADBCJQxCCYQmMIwZoDJpQMCKIg6KBYwTGFQgeHHYouCCRI7EMYTXFRhILEK5SfFRgYSIborbSbpglFCgoVBFYXJYQYSGCggAGCRAVIBgwA==")); + return img; + } + + function drawMenu(params) { + const DEFAULT_PARAMS = { + showBTN1: false, + showBTN2: false, + showBTN3: false, + }; + const p = Object.assign({}, DEFAULT_PARAMS, params); + if (p.showBTN1) g.drawImage(iconIncrement(), W - 30, 10); + if (p.showBTN2) g.drawImage(iconOk(), W - 30, 110); + if (p.showBTN3) g.drawImage(iconDecrement(), W - 30, 210); + } + + function clearScreen() { + g.setColor(RED); + g.fillRect(0,0,W,H); + } + + function drawTitle(exercise) { + const title = exercise.humanTitle; + + g.setFont("Vector",20); + g.setColor(WHITE); + g.drawString(title, centerStringX(title), 5); + } + + function drawReps(exercise) { + const set = exercise.currentSet; + if (set.isCompleted()) return; + + g.setColor(PINK); + g.fillCircle(W / 2, H / 2, 50); + g.setColor(WHITE); + g.setFont("Vector", 40); + g.drawString(set.reps, centerStringX(set.reps), (H - 45) / 2); + g.setFont("Vector", 15); + const note = `of ${set.maxReps}`; + g.drawString(note, centerStringX(note), (H / 2) + 25); + } + + function drawSets(exercise) { + const sets = exercise.subTitle; + + g.setColor(WHITE); + g.setFont("Vector", 15); + g.drawString(sets, centerStringX(sets), H - 25); + } + + function drawSetProgress(exercise) { + drawTitle(exercise); + drawReps(exercise); + drawSets(exercise); + drawMenu({showBTN1: true, showBTN2: true, showBTN3: true}); + } + + function drawStartNextExercise() { + const title = "Good work"; + const msg = "No need to rest\nmove straight on\nto the next exercise"; + + g.setColor(WHITE); + g.setFont("Vector", 35); + g.drawString(title, centerStringX(title), 10); + g.setFont("Vector", 15); + g.drawString(msg, 30, 150); + drawMenu({showBTN1: false, showBTN2: true, showBTN3: false}); + } + + function drawProgramCompleted() { + const title1 = "You did"; + const title2 = "GREAT!"; + const msg = "That's the program\ncompleted. Now eat\nsome food and\nget plenty of rest."; + + clearWatch(); + setWatch(reset, BTN2, {repeat: false}); + + g.setColor(WHITE); + g.setFont("Vector", 35); + g.drawString(title1, centerStringX(title1), 10); + g.setFont("Vector", 40); + g.drawString(title2, centerStringX(title2), 50); + g.setFont("Vector", 15); + g.drawString(msg, 30, 150); + drawMenu({showBTN1: false, showBTN2: true, showBTN3: false}); + } + + /* + function drawExerciseCompleted(program) { + const exercise = program.currentExercise(); + const title = exercise.canProgress? + "WELL DONE!" : + "NOT BAD!"; + const msg = exercise.canProgress? + `You weight is automatically increased\nfor ${exercise.title} to ${exercise.weight}${exercise.unit}` : + "It looks like you struggled\non a few sets, your weight will\nstay the same"; + const action = "Move straight on to the next exercise"; + + clearScreen(); + g.setColor(WHITE); + g.setFont("Vector", 20); + g.drawString(title, centerStringX(title), 10); + g.setFont("Vector", 10); + g.drawString(msg, centerStringX(msg), 180); + g.drawString(action, centerStringX(action), 210); + drawMenu({showBTN2: true}); + + clearWatch(); + setWatch(() => { + init(program); + }, BTN2, {repeat: false}); + } + */ + + function drawRestTimer(program) { + const exercise = program.currentExercise(); + const motivation = "Take a breather.."; + clearScreen(); + drawMenu({showBTN2: true}); + + g.setColor(PINK); + g.fillCircle(W / 2, H / 2, 50); + g.setColor(WHITE); + g.setFont("Vector", 15); + g.drawString(motivation, centerStringX(motivation), 25); + g.setFont("Vector", 40); + g.drawString(exercise.restPeriod, centerStringX(exercise.restPeriod), (H - 45) / 2); + exercise.decRestPeriod(); + + if (exercise.restPeriod <= 0) { + exercise.resetRestTimer(); + redraw(program); + } + } + + function redraw(program) { + const exercise = program.currentExercise(); + + clearScreen(); + + if (program.isCompleted()) { + drawProgramCompleted(program); + return; + } + + if (exercise.isRestTimerRunning()) { + if (exercise.isLastSet()) { + drawStartNextExercise(program); + } else { + drawRestTimer(program); + } + + return; + } + + drawSetProgress(exercise); + } + + function init(program) { + clearWatch(); + program.next(); + } + + // Setup training program. This should come from file + + // Squats + function buildPrograms() { + const squats = new Exercise({ + title: "Squats", + weight: 40, + unit: "Kg", + }); + squats.addSets([ + new Set(5), + new Set(5), + new Set(5), + new Set(5), + new Set(5), + ]); + + const bench = new Exercise({ + title: "Bench press", + weight: 20, + unit: "Kg", + }); + bench.addSets([ + new Set(5), + new Set(5), + new Set(5), + new Set(5), + new Set(5), + ]); + + const row = new Exercise({ + title: "Row", + weight: 20, + unit: "Kg", + }); + row.addSets([ + new Set(5), + new Set(5), + new Set(5), + new Set(5), + new Set(5), + ]); + + const ohPress = new Exercise({ + title: "Overhead press", + weight: 20, + unit: "Kg", + }); + ohPress.addSets([ + new Set(5), + new Set(5), + new Set(5), + new Set(5), + new Set(5), + ]); + + const deadlift = new Exercise({ + title: "Deadlift", + weight: 20, + unit: "Kg", + }); + deadlift.addSets([ + new Set(5), + ]); + + const pullups = new Exercise({ + title: "Pullups", + weight: 0, + unit: "Kg", + }); + pullups.addSets([ + new Set(10), + new Set(10), + new Set(10), + ]); + + const triceps = new Exercise({ + title: "Tricep extension", + weight: 20, + unit: "Kg", + }); + triceps.addSets([ + new Set(10), + new Set(10), + new Set(10), + ]); + + const programA = new Program({ + title: "Program A", + }); + programA.addExercises([ + squats, + ohPress, + deadlift, + pullups, + ]); + + const programB = new Program({ + title: "Program B", + }); + programB.addExercises([ + squats, + bench, + row, + triceps, + ]); + + const programs = [ + programA, + programB, + ]; + return programs; + } + + // For this spike, just run the first program, what will + // really happen is the user picks a program to do from + // some menu on a start page. + init(buildPrograms()[0]); +})(); \ No newline at end of file diff --git a/apps/buffgym/buffgym.png b/apps/buffgym/buffgym.png new file mode 100644 index 0000000000000000000000000000000000000000..93a29a4a686f172e5e6dbd2ecb2a1d3edf4751c0 GIT binary patch literal 1800 zcmV+j2lx1iP)tlxqC9)f@Omu&Na^VuQ3o*OkLX&0x0b91a zC7LC3CR;|G&UwKyoC)@U)%oe$2-|YHkk(v4M!t35mBz$@a#wDU>?rxOl>4iDU0;K~&^NeJ~rZV1qG0 z2VEWRN{fW6B-AV=vyLG{LNQb2i6*D;)z1+>HV9oV)YY-AYv;CEAGyt1BxHeIB6`O1 zgNbCKvH5&(h}tjTY&oMqX-z=6c>c(BKect-eBUPt!g0uGg&+h40X>Fzw5_e#pVR#$ zXoP~+BFSrtk0nGN171%U%*9ZeH|HLjvY%#M?bNRbI9pq_zjAQwp^ph~HbFcpN~ZRz z(p@Mo-ho770lfp=4bOss8*F%I4msLmKr#xaD)J*)`yvYx`ky+nJwKX`iBeY-qKGqH z?QS=3P!M$53s7Hu5JniTm@SU>rp6YK0dui1iN|DU+@DO!eyGs{ttdh(`FAE0vla{o z?WidZt%?gW#AR7;<X-pbXX@H zNxpMYdq4RMO4a}WV;wc05JSkxHD~SsCa-dTj zNTxVqQheR_7dUJ%8VslVKYYW7jWB?j;7(;1x|h0^D=>+_|8G8A);PQcVdy~=1vqUM z=Bmmm)pMW?ZosIy0KK;IbR&uXN!$f_n-$fCGwJquLBgH9YRnj{JTt}EHBxNEeXlt^ z=25u`mrr_dyKd3t92OJZd{9ZwAii{~WsvTQjEnfS8xO2y_@!_U?;}HwP2R(Ijz)}m zit&g9Oe?63KH;Lj-FATAd+p`e9q7aT9cJ8VbS}; zQ^kGnzzKFl@TSGDoSS-YQ$q_XStXt~30!=tPYtT;z`;H5V0>y6M#+fsq8;frcBVm9 z*_*JM9oV^KK>zTcC~y_yt(v2JXX>jDabwn+OJjC>_p^QzY!*8o^Zl)_Rq0Y~0Idq0 zS2Ejbz57Z043707FgeP1%EK<&HQJ?2qjG*1bTS%v z%ubCHDDjk|*z-F39AIq&YmVQ|gsn7eCKBY&B;IDWBhQ|XP-F^P(QUxw%mhX!9y3?4 za|0-g(DPbUZ?Ai9XPitl89;gYdW9(B>6do0 z&{d=<|EJJx0R4iRi$%B)8xs*{tC-D)*IL!li9sItRi$t8xdUT=@j7sD@4L`~ZUg#< z@9|7Teoy(EeEDwwZ_kbU;PA;c-%(!VWd_vn&m{hDmP)mv+W`74ORG=SmA{Aypl2;4 z5^=uzMCz^;-3HXM(oa{4%%IK%Ex`EHFzC+|RLpt@yWwJmOe>of#Mu@=51VEH#S6s{ z9o#%D1JN_>#7hfe`U6y8YJ?wyDGkFo zERDt!OF!~i5Yltbc`JfdVhJF1ywFnv_&cBz%HNa*=^#c>zCWO1`&}!lb?WRyb7@D_ zoMJQ4ZGbZ)W6%m}S;fo+s9pL&qm@ky;%p0`2i^anc~wxo1C;V!MN}J*?VR%}kkbG~ qkv|qi@hCO{S(a~Li!HWTm;V6Sm{)4DoNDL*0000 Date: Mon, 20 Apr 2020 15:27:21 +0100 Subject: [PATCH 183/302] Update apps.json --- apps.json | 4 + apps/buffgym/buffgym-programs.json | 101 ++++++++++++++++++++++++ apps/buffgym/buffgym.app.js | 121 +++++------------------------ 3 files changed, 125 insertions(+), 101 deletions(-) create mode 100644 apps/buffgym/buffgym-programs.json diff --git a/apps.json b/apps.json index e993f0e18..bff5fe66b 100644 --- a/apps.json +++ b/apps.json @@ -1305,6 +1305,10 @@ "storage": [ {"name":"buffgym"}, {"name":"buffgym.app.js"}, + {"name":"buffgym-set.js","url":"buffgym-set.js"}, + {"name":"buffgym-exercise.js","url":"buffgym-exercise.js"}, + {"name":"buffgym-program.js","url":"buffgym-program.js"}, + {"name":"buffgym-programs.json","url":"buffgym-program.json"}, {"name":"buffgym.img","url":"buffgym-icon.js","evaluate":true} ] } diff --git a/apps/buffgym/buffgym-programs.json b/apps/buffgym/buffgym-programs.json new file mode 100644 index 000000000..bf5aa0e0d --- /dev/null +++ b/apps/buffgym/buffgym-programs.json @@ -0,0 +1,101 @@ +[ + { + title: "Program A", + exercises: [ + { + title: "Squats", + weight: 40, + unit: "Kg", + sets: [ + 5, + 5, + 5, + 5, + 5 + ] + }, + { + title: "Overhead press", + weight: 20, + unit: "Kg", + sets: [ + 5, + 5, + 5, + 5, + 5 + ] + }, + { + title: "Deadlift", + weight: 20, + unit: "Kg", + sets: [ + 5 + ] + }, + { + title: "Pullups", + weight: 0, + unit: "Kg", + sets: [ + 10, + 10, + 10 + ] + } + ] + }, + { + title: "Program B", + exercises: [ + { + title: "Squats", + weight: 40, + unit: "Kg", + sets: [ + 5, + 5, + 5, + 5, + 5 + ] + }, + { + title: "Bench press", + weight: 20, + unit: "Kg", + sets: [ + 5, + 5, + 5, + 5, + 5 + ] + }, + { + title: "Row", + weight: 20, + unit:"Kg", + sets: [ + 5, + 5, + 5, + 5, + 5 + ] + + }, + { + title: "Tricep extension", + weight: 20, + unit: "Kg", + sets: [ + 10, + 10, + 10 + ] + } + ] + } +] \ No newline at end of file diff --git a/apps/buffgym/buffgym.app.js b/apps/buffgym/buffgym.app.js index e74b7062a..d3a4e6834 100755 --- a/apps/buffgym/buffgym.app.js +++ b/apps/buffgym/buffgym.app.js @@ -194,113 +194,32 @@ // Squats function buildPrograms() { - const squats = new Exercise({ - title: "Squats", - weight: 40, - unit: "Kg", - }); - squats.addSets([ - new Set(5), - new Set(5), - new Set(5), - new Set(5), - new Set(5), - ]); + const programsJSON = require("Storage").readJSON("buffgym-programs.json", 1); - const bench = new Exercise({ - title: "Bench press", - weight: 20, - unit: "Kg", - }); - bench.addSets([ - new Set(5), - new Set(5), - new Set(5), - new Set(5), - new Set(5), - ]); + if (!programsJSON) throw "No programs JSON found"; - const row = new Exercise({ - title: "Row", - weight: 20, - unit: "Kg", - }); - row.addSets([ - new Set(5), - new Set(5), - new Set(5), - new Set(5), - new Set(5), - ]); + const programs = []; - const ohPress = new Exercise({ - title: "Overhead press", - weight: 20, - unit: "Kg", - }); - ohPress.addSets([ - new Set(5), - new Set(5), - new Set(5), - new Set(5), - new Set(5), - ]); + programsJSON.forEach(programJSON => { + const program = new Program({ + title: programJSON.title, + }); + const exercises = programJSON.exercises.map(exerciseJSON => { + const exercise = new Exercise({ + title: exerciseJSON.title, + weight: exerciseJSON.weight, + unit: exerciseJSON.unit, + }); + exerciseJSON.sets.forEach(setJSON => { + exercise.addSet(new Set(setJSON)); + }); - const deadlift = new Exercise({ - title: "Deadlift", - weight: 20, - unit: "Kg", + return exercise; + }); + program.addExercises(exercises); + programs.push(program); }); - deadlift.addSets([ - new Set(5), - ]); - const pullups = new Exercise({ - title: "Pullups", - weight: 0, - unit: "Kg", - }); - pullups.addSets([ - new Set(10), - new Set(10), - new Set(10), - ]); - - const triceps = new Exercise({ - title: "Tricep extension", - weight: 20, - unit: "Kg", - }); - triceps.addSets([ - new Set(10), - new Set(10), - new Set(10), - ]); - - const programA = new Program({ - title: "Program A", - }); - programA.addExercises([ - squats, - ohPress, - deadlift, - pullups, - ]); - - const programB = new Program({ - title: "Program B", - }); - programB.addExercises([ - squats, - bench, - row, - triceps, - ]); - - const programs = [ - programA, - programB, - ]; return programs; } From 8238bb766a6f4fc80c14e2c781f5dd1ff25da22a Mon Sep 17 00:00:00 2001 From: RenaudG <34276398+renaudgweb@users.noreply.github.com> Date: Mon, 20 Apr 2020 16:40:26 +0200 Subject: [PATCH 184/302] Create app.js --- apps/hamloc/app.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 apps/hamloc/app.js diff --git a/apps/hamloc/app.js b/apps/hamloc/app.js new file mode 100644 index 000000000..ac608a5f4 --- /dev/null +++ b/apps/hamloc/app.js @@ -0,0 +1,21 @@ +latLonToGridSquare=function(o,a){var t,e,n,s,l,i,r,h,M,f=-100,g=0,u="ABCDEFGHIJKLMNOPQRSTUVWX",d=u.toLowerCase();function N(o){return"number"==typeof o?o:"string"==typeof o?parseFloat(o):"function"==typeof o?parseFloat(o()):void E.showMessage("can't convert \ninput: "+o)}return"object"==typeof o?2===o.length?(f=N(o[0]),g=N(o[1])):"lat"in o&&"lon"in o?(f=N(o.lat),g=N(o.lon)):"latitude"in o&&"longitude"in o?(f=N(o.latitude),g=N(o.longitude)):E.showMessage("can't convert \nobject "+o):(f=N(o),g=N(a)),isNaN(f)&&E.showMessage("lat is NaN"),isNaN(g)&&E.showMessage("lon is NaN"),90===Math.abs(f)&&E.showMessage("grid invalid \nat N/S"),90 Date: Mon, 20 Apr 2020 16:43:00 +0200 Subject: [PATCH 185/302] Add files via upload --- apps/hamloc/app.png | Bin 0 -> 3697 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 apps/hamloc/app.png diff --git a/apps/hamloc/app.png b/apps/hamloc/app.png new file mode 100644 index 0000000000000000000000000000000000000000..54dac5882e4aa4b14cb1e20d04471c4a5efc027b GIT binary patch literal 3697 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 Date: Mon, 20 Apr 2020 16:44:35 +0200 Subject: [PATCH 186/302] Create app-icon.js --- apps/hamloc/app-icon.js | 1 + 1 file changed, 1 insertion(+) create mode 100644 apps/hamloc/app-icon.js diff --git a/apps/hamloc/app-icon.js b/apps/hamloc/app-icon.js new file mode 100644 index 000000000..175b492c7 --- /dev/null +++ b/apps/hamloc/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4ACwVeAAM6nQECJsYqDFYItCAgQ0DFrxWE2ZhBxOJA4ICBGwhbbxGtAYOz2etnWt2YwBnQCBGgWtxBjXJQQpBLgQABEQIDBMAgwE2YYBwRcUxOtDwgABGgYvGTQQLCMSRNGEQaODF4SQDHgYwUFIlfAgY2DYQQGDsouCGAQSEGBouEKANl1onEwQvGA4QACA4IwQaIISFFwIwDXwI2ExL2BB4YABJgwwKnQAkRpzYDAAbuBA4oABe4aaDD44uGxCNEQwStEdwgAFR4IHGF4iRBxJeLBwIdCcwgvJxLwFFIRgLBo4dBcwj2CF45xIKIxeLAww4FAAuINIYbKMAteso8FAwovPCQllQQtlF4gLFUYwvDsq/HfIShEDhCQDIgIAFnQHGBBITRnWIXwdfAAYGFSYblBNI5KBsobEDo4GCdxyFDr2tR4+tDQrwNF5YlEnQvJaZIvKr4RIEoovaSAIvyXArbBF6OJR6mCXAgvBDwIlFd5IfBd6tfSQQRGCgeIBJE6DIIAFDoIGGF4OCAgIAEnQHGBBITRnWIF4P+V4ZMCWxBoBRw5PBNI7IGnQuCSAIfE1oSB1oADF4WIXxAvHsoIFH4IvEdIJWEDg4vICRTuJSAwABxAcJF5A5C1ovKRwhgCwSQGF6CpEXxBeGMA59JZIIvGA4hhBLw+JF4xRFSBBNDFIhHFe4VfLxhgCAEguIMAJSB1oABSA4HEB4RwBAgQXHss6LxKRDPQTxBsovIRIdexCOGRpwwIr5gFAwbuEAoheBFyQwFRIrwDLwZuBdwgTBC4IuRYYowGRAq+BAoeCAoRABFyIABD4IsCGAgpFGoguBd4eIFyRiEFoIwCEoKJDRwYqCCAJcUGJICBK4JgDKgOtOIQtce4s6AAI2EBAgteAAizBGgYECwQsiAD4")) From 9f5cbc6022ee275ed40e6942cb69388f4d6c6e07 Mon Sep 17 00:00:00 2001 From: RenaudG <34276398+renaudgweb@users.noreply.github.com> Date: Mon, 20 Apr 2020 16:49:49 +0200 Subject: [PATCH 187/302] Update apps.json --- apps.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/apps.json b/apps.json index b58fad5ce..e55190ec0 100644 --- a/apps.json +++ b/apps.json @@ -1400,5 +1400,17 @@ {"name":"rclock.app.js","url":"rclock.app.js"}, {"name":"rclock.img","url":"app-icon.js","evaluate":true} ] + }, + { "id": "hamloc", + "name": "QTH Locator / Maidenhead Locator System", + "shortName": "QTH Locator", + "icon": "app.png", + "version":"0.01", + "description": "Convert your current GPS location to the Maidenhead locator system used by HAM amateur radio operators", + "tags": "tool,outdoors,gps", + "storage": [ + {"name":"hamloc.app.js","url":"app.js"}, + {"name":"hamloc.img","url":"app-icon.js","evaluate":true} + ] } ] From 02af1e4b1518b35d09e541df63f4130924df29d2 Mon Sep 17 00:00:00 2001 From: RenaudG <34276398+renaudgweb@users.noreply.github.com> Date: Mon, 20 Apr 2020 16:55:59 +0200 Subject: [PATCH 188/302] Create ChangeLog --- apps/hamloc/ChangeLog | 1 + 1 file changed, 1 insertion(+) create mode 100644 apps/hamloc/ChangeLog diff --git a/apps/hamloc/ChangeLog b/apps/hamloc/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/hamloc/ChangeLog @@ -0,0 +1 @@ +0.01: New App! From e513031a8770146f7fbe34c672d283d177e31f4a Mon Sep 17 00:00:00 2001 From: RenaudG <34276398+renaudgweb@users.noreply.github.com> Date: Mon, 20 Apr 2020 17:11:51 +0200 Subject: [PATCH 189/302] Create README.md --- apps/hamloc/README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 apps/hamloc/README.md diff --git a/apps/hamloc/README.md b/apps/hamloc/README.md new file mode 100644 index 000000000..493afa899 --- /dev/null +++ b/apps/hamloc/README.md @@ -0,0 +1,14 @@ +# QTH Locator + +Convert your current GPS location to the [Maidenhead](https://en.wikipedia.org/wiki/Maidenhead_Locator_System) locator system used by HAM amateur radio operators. + +## Description + +A Maidenhead locator compresses latitude and longitude into a short string of characters, which is similar in concept to the World Geographic Reference System or GEOREF. This position information is presented in a limited level of precision to limit the number of characters needed for its transmission using voice, Morse code, or any other operating mode.[4] + +The chosen coding uses alternating pairs of letters and digits, like so: + +* BL11bh + +support Paul Brewer KI6CQ HamGridSquare.js +support Chris Veness 2002-2012 LatLon library From 5f9ee09daf8b9baf63e19d067efddd589da20648 Mon Sep 17 00:00:00 2001 From: RenaudG <34276398+renaudgweb@users.noreply.github.com> Date: Mon, 20 Apr 2020 17:13:07 +0200 Subject: [PATCH 190/302] Update README.md --- apps/hamloc/README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/hamloc/README.md b/apps/hamloc/README.md index 493afa899..273e1f54b 100644 --- a/apps/hamloc/README.md +++ b/apps/hamloc/README.md @@ -4,11 +4,12 @@ Convert your current GPS location to the [Maidenhead](https://en.wikipedia.org/w ## Description -A Maidenhead locator compresses latitude and longitude into a short string of characters, which is similar in concept to the World Geographic Reference System or GEOREF. This position information is presented in a limited level of precision to limit the number of characters needed for its transmission using voice, Morse code, or any other operating mode.[4] +A Maidenhead locator compresses latitude and longitude into a short string of characters, which is similar in concept to the World Geographic Reference System or GEOREF. This position information is presented in a limited level of precision to limit the number of characters needed for its transmission using voice, Morse code, or any other operating mode. The chosen coding uses alternating pairs of letters and digits, like so: * BL11bh -support Paul Brewer KI6CQ HamGridSquare.js -support Chris Veness 2002-2012 LatLon library + +* support Paul Brewer KI6CQ HamGridSquare.js +* support Chris Veness 2002-2012 LatLon library From a47ebfaf4841be23a45f4b64533e1c7e55769a95 Mon Sep 17 00:00:00 2001 From: RenaudG <34276398+renaudgweb@users.noreply.github.com> Date: Mon, 20 Apr 2020 17:14:16 +0200 Subject: [PATCH 191/302] Update README.md --- apps/hamloc/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/hamloc/README.md b/apps/hamloc/README.md index 273e1f54b..5710493bb 100644 --- a/apps/hamloc/README.md +++ b/apps/hamloc/README.md @@ -9,7 +9,6 @@ A Maidenhead locator compresses latitude and longitude into a short string of ch The chosen coding uses alternating pairs of letters and digits, like so: * BL11bh - - +## * support Paul Brewer KI6CQ HamGridSquare.js * support Chris Veness 2002-2012 LatLon library From 4b512af0e2012986ea1d5f3e2f916cb2f88e47b8 Mon Sep 17 00:00:00 2001 From: Purple-Tentacle <59914607+Purple-Tentacle@users.noreply.github.com> Date: Mon, 20 Apr 2020 17:34:33 +0200 Subject: [PATCH 192/302] activepedom 0.03 --- apps/activepedom/ChangeLog | 3 +- apps/activepedom/README.md | 20 +++++- apps/activepedom/app.js | 133 +++++++++++++++++++++++++++++++++++++ apps/activepedom/widget.js | 51 +++++++++----- 4 files changed, 190 insertions(+), 17 deletions(-) create mode 100644 apps/activepedom/app.js diff --git a/apps/activepedom/ChangeLog b/apps/activepedom/ChangeLog index fb0bc78e5..c1b9ec011 100644 --- a/apps/activepedom/ChangeLog +++ b/apps/activepedom/ChangeLog @@ -1,2 +1,3 @@ 0.01: New Widget! -0.02: Distance calculation and display \ No newline at end of file +0.02: Distance calculation and display +0.03: Data logging and display \ No newline at end of file diff --git a/apps/activepedom/README.md b/apps/activepedom/README.md index 055a91f56..f45297e57 100644 --- a/apps/activepedom/README.md +++ b/apps/activepedom/README.md @@ -1,4 +1,4 @@ -# Active Pedometer +# Active Pedometer Pedometer that filters out arm movement and displays a step goal progress. I changed the step counting algorithm completely. @@ -6,6 +6,8 @@ Now every step is counted when in status 'active', if the time difference betwee To get in 'active' mode, you have to reach the step threshold before the active timer runs out. When you reach the step threshold, the steps needed to reach the threshold are counted as well. +Steps are saved to a datafile every 5 minutes. You can watch a graph using the app. + ## Screenshots * 600 steps ![](600.png) @@ -30,6 +32,22 @@ When you reach the step threshold, the steps needed to reach the threshold are c * Steps are saved to a file and read-in at start (to not lose step progress) * Settings can be changed in Settings - App/widget settings - Active Pedometer +## Data storage + +* Data is stored to a file +* Format: now,stepsCounted,active,stepsTooShort,stepsTooLong,stepsOutsideTime +* now is UNIX timestamp in ms +* You can chose the app to watch a steps graph +* You can import the file into Excel +* The file does not include a header +* You can convert UNIX timestamp to a date in Excel using this formula: =DATUM(1970;1;1)+(LINKS(A2;10)/86400) +* You have to format the cell with the formula to a date cell. Example: JJJJ-MM-TT-hh-mm-ss + +## App + +* The app accesses the data stored for the current day +* Timespan is choseable (1h, 4h, 8h, 12h, 16h, 20, 24h), standard is 24h, the whole current day + ## Settings * Max time (ms): Maximum time between two steps in milliseconds, steps will not be counted if exceeded. Standard: 1100 diff --git a/apps/activepedom/app.js b/apps/activepedom/app.js new file mode 100644 index 000000000..f966530d0 --- /dev/null +++ b/apps/activepedom/app.js @@ -0,0 +1,133 @@ +(() => { + +const storage = require("Storage"); +var history = 86400000; // 28800000=8h 43200000=12h //86400000=24h + +//Convert ms to time +function getTime(t) { + date = new Date(t); + offset = date.getTimezoneOffset() / 60; + //var milliseconds = parseInt((t % 1000) / 100), + seconds = Math.floor((t / 1000) % 60); + minutes = Math.floor((t / (1000 * 60)) % 60); + hours = Math.floor((t / (1000 * 60 * 60)) % 24); + hours = hours - offset; + hours = (hours < 10) ? "0" + hours : hours; + minutes = (minutes < 10) ? "0" + minutes : minutes; + seconds = (seconds < 10) ? "0" + seconds : seconds; + return hours + ":" + minutes + ":" + seconds; +} + +function getDate(t) { + date = new Date(t*1); + year = date.getFullYear(); + month = date.getMonth()+1; //month is zero-based + day = date.getDate(); + month = (month < 10) ? "0" + month : month; + day = (day < 10) ? "0" + day : day; + return year + "-" + month + "-" + day; +} + +//columns: 0=time, 1=stepsCounted, 2=active, 3=stepsTooShort, 4=stepsTooLong, 5=stepsOutsideTime +function getArrayFromCSV(file, column) { + i = 0; + array = []; + now = new Date(); + while ((nextLine = file.readLine())) { //as long as there is a next line + if(nextLine) { + dataSplitted = nextLine.split(','); //split line, + diff = now - dataSplitted[0]; //calculate difference between now and stored time + if (diff <= history) { //only entries from the last x ms + array.push(dataSplitted[column]); + } + } + i++; + } + return array; +} + +function drawGraph() { + //times + // actives = getArrayFromCSV(csvFile, 2); + // shorts = getArrayFromCSV(csvFile, 3); + // longs = getArrayFromCSV(csvFile, 4); + // outsides = getArrayFromCSV(csvFile, 5); //array.push(dataSplitted[5].slice(0,-1)); + now = new Date(); + month = now.getMonth() + 1; + if (month < 10) month = "0" + month; + filename = filename = "activepedom-" + now.getFullYear() + month + now.getDate() + ".data"; + var csvFile = storage.open(filename, "r"); + times = getArrayFromCSV(csvFile, 0); + first = getDate(times[0]) + " " + getTime(times[0]); + last = getDate (times[times.length-1]) + " " + getTime(times[times.length-1]); + //free memory + csvFile = undefined; + times = undefined; + + //steps + var csvFile = storage.open(filename, "r"); + steps = getArrayFromCSV(csvFile, 1); + //define y-axis grid labels + stepsLastEntry = steps[steps.length-1]; + if (stepsLastEntry < 1000) gridyValue = 100; + if (stepsLastEntry >= 1000 && stepsLastEntry < 10000) gridyValue = 500; + if (stepsLastEntry > 10000) gridyValue = 5000; + + //draw + drawMenu(); + g.drawString("First: " + first, 40, 30); + g.drawString(" Last: " + last, 40, 40); + require("graph").drawLine(g, steps, { + //title: "Steps Counted", + axes : true, + gridy : gridyValue, + y : 50, //offset on screen + x : 5, //offset on screen + }); + //free memory from big variables + allData = undefined; + allDataFile = undefined; + csvFile = undefined; + times = undefined; +} + +function drawMenu () { + g.clear(); + g.setFont("6x8", 1); + g.drawString("BTN1:Timespan | BTN2:Draw", 20, 10); + g.drawString("Timespan: " + history/1000/60/60 + " hours", 20, 20); +} + +setWatch(function() { //BTN1 + switch(history) { + case 3600000 : //1h + history = 14400000; //4h + break; + case 86400000 : //24 + history = 3600000; //1h + break; + default : + history = history + 14400000; //4h + break; + } + drawMenu(); +}, BTN1, {edge:"rising", debounce:50, repeat:true}); + +setWatch(function() { //BTN2 + g.setFont("6x8", 2); + g.drawString ("Drawing...",30,60); + drawGraph(); +}, BTN2, {edge:"rising", debounce:50, repeat:true}); + +setWatch(function() { //BTN3 +}, BTN3, {edge:"rising", debounce:50, repeat:true}); + +setWatch(function() { //BTN4 +}, BTN4, {edge:"rising", debounce:50, repeat:true}); + +setWatch(function() { //BTN5 +}, BTN5, {edge:"rising", debounce:50, repeat:true}); + +drawMenu(); + +})(); \ No newline at end of file diff --git a/apps/activepedom/widget.js b/apps/activepedom/widget.js index d569716ec..7879b2056 100644 --- a/apps/activepedom/widget.js +++ b/apps/activepedom/widget.js @@ -3,13 +3,14 @@ var startTimeStep = new Date(); //set start time var stopTimeStep = 0; //Time after one step var timerResetActive = 0; //timer to reset active + var timerStoreData = 0; //timer to store data var steps = 0; //steps taken var stepsCounted = 0; //active steps counted var active = 0; //x steps in y seconds achieved var stepGoalPercent = 0; //percentage of step goal var stepGoalBarLength = 0; //length og progress bar var lastUpdate = new Date(); //used to reset counted steps on new day - var width = 45; //width of widget + var width = 46; //width of widget //used for statistics and debugging var stepsTooShort = 0; @@ -18,13 +19,33 @@ var distance = 0; //distance travelled + const s = require('Storage'); const SETTINGS_FILE = 'activepedom.settings.json'; const PEDOMFILE = "activepedom.steps.json"; + var dataFile; + var storeDataInterval = 5*60*1000; //ms let settings; //load settings function loadSettings() { - settings = require('Storage').readJSON(SETTINGS_FILE, 1) || {}; + settings = s.readJSON(SETTINGS_FILE, 1) || {}; + } + + function storeData() { + now = new Date(); + month = now.getMonth() + 1; + if (month < 10) month = "0" + month; + filename = filename = "activepedom-" + now.getFullYear() + month + now.getDate() + ".data"; + dataFile = s.open(filename,"a"); + if (dataFile) dataFile.write([ + now.getTime(), + stepsCounted, + active, + stepsTooShort, + stepsTooLong, + stepsOutsideTime, + ].join(",")+"\n"); + dataFile = undefined; } //return setting @@ -77,20 +98,20 @@ //Remove step if time between first and second step is too long if (stepTimeDiff >= setting('cMaxTime')) { //milliseconds - stepsTooLong++; //count steps which are note counted, because time too long + stepsTooLong++; //count steps which are not counted, because time too long steps--; } - //Remove step if time between first and second step is too short if (stepTimeDiff <= setting('cMinTime')) { //milliseconds - stepsTooShort++; //count steps which are note counted, because time too short + stepsTooShort++; //count steps which are not counted, because time too short steps--; } + //Step threshold reached if (steps >= setting('stepThreshold')) { if (active == 0) { stepsCounted = stepsCounted + (setting('stepThreshold') -1) ; //count steps needed to reach active status, last step is counted anyway, so treshold -1 - stepsOutsideTime = stepsOutsideTime - 10; //substract steps needed to reac active status + stepsOutsideTime = stepsOutsideTime - 10; //substract steps needed to reach active status } active = 1; clearInterval(timerResetActive); //stop timer which resets active @@ -109,14 +130,17 @@ function draw() { var height = 23; //width is deined globally - distance = (stepsCounted * setting('stepLength')) / 100 /1000 //distance in km + distance = (stepsCounted * setting('stepLength')) / 100 /1000; //distance in km //Check if same day let date = new Date(); if (lastUpdate.getDate() == date.getDate()){ //if same day } - else { - stepsCounted = 1; //set stepcount to 1 + else { //different day, set all steps to 0 + stepsCounted = 0; + stepsTooShort = 0; + stepsTooLong = 0; + stepsOutsideTime = 0; } lastUpdate = date; @@ -166,7 +190,7 @@ stepsTooLong : stepsTooLong, stepsOutsideTime : stepsOutsideTime }; - require("Storage").write(PEDOMFILE,d); //write array to file + s.write(PEDOMFILE,d); //write array to file }); //When Step is registered by firmware @@ -182,8 +206,7 @@ }); //Read data from file and set variables - let pedomData = require("Storage").readJSON(PEDOMFILE,1); - + let pedomData = s.readJSON(PEDOMFILE,1); if (pedomData) { if (pedomData.lastUpdate) lastUpdate = new Date(pedomData.lastUpdate); stepsCounted = pedomData.stepsToday|0; @@ -191,12 +214,10 @@ stepsTooLong = pedomData.stepsTooLong; stepsOutsideTime = pedomData.stepsOutsideTime; } - pedomdata = 0; //reset pedomdata to save memory setStepSensitivity(setting('stepSensitivity')); //set step sensitivity (80 is standard, 400 is muss less sensitive) - + timerStoreData = setInterval(storeData, storeDataInterval); //store data regularly //Add widget WIDGETS["activepedom"]={area:"tl",width:width,draw:draw}; - })(); \ No newline at end of file From 35583eb8371551bc055dfd55316c8f2216aedb9e Mon Sep 17 00:00:00 2001 From: Purple-Tentacle <59914607+Purple-Tentacle@users.noreply.github.com> Date: Mon, 20 Apr 2020 17:36:42 +0200 Subject: [PATCH 193/302] activepedom 0.03 --- apps.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps.json b/apps.json index 274a3a8b2..eda9203bc 100644 --- a/apps.json +++ b/apps.json @@ -1127,15 +1127,15 @@ "name": "Active Pedometer", "shortName":"Active Pedometer", "icon": "app.png", - "version":"0.02", - "description": "Pedometer that filters out arm movement and displays a step goal progress.", + "version":"0.03", + "description": "Pedometer that filters out arm movement and displays a step goal progress. Steps are saved to a daily file and can be viewed as graph.", "tags": "outdoors,widget", - "type":"widget", "readme": "README.md", "storage": [ {"name":"activepedom.wid.js","url":"widget.js"}, {"name":"activepedom.settings.js","url":"settings.js"}, - {"name":"activepedom.img","url":"app-icon.js","evaluate":true} + {"name":"activepedom.img","url":"app-icon.js","evaluate":true}, + {"name":"activepedom.app.js","url":"app.js"}, ] }, { "id": "chronowid", From 5d3264e51eb68116677838fc4d3c5e545448b729 Mon Sep 17 00:00:00 2001 From: Purple-Tentacle <59914607+Purple-Tentacle@users.noreply.github.com> Date: Mon, 20 Apr 2020 17:37:45 +0200 Subject: [PATCH 194/302] activepedom 0.03 --- apps.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps.json b/apps.json index eda9203bc..36498423e 100644 --- a/apps.json +++ b/apps.json @@ -1135,7 +1135,7 @@ {"name":"activepedom.wid.js","url":"widget.js"}, {"name":"activepedom.settings.js","url":"settings.js"}, {"name":"activepedom.img","url":"app-icon.js","evaluate":true}, - {"name":"activepedom.app.js","url":"app.js"}, + {"name":"activepedom.app.js","url":"app.js"} ] }, { "id": "chronowid", From 3e9f581b9fb3adcf23421fff4956490f20fdc452 Mon Sep 17 00:00:00 2001 From: Paul Cockrell Date: Mon, 20 Apr 2020 17:09:27 +0100 Subject: [PATCH 195/302] Fix apps.json --- apps.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps.json b/apps.json index bff5fe66b..7a1a3f5fb 100644 --- a/apps.json +++ b/apps.json @@ -1308,7 +1308,7 @@ {"name":"buffgym-set.js","url":"buffgym-set.js"}, {"name":"buffgym-exercise.js","url":"buffgym-exercise.js"}, {"name":"buffgym-program.js","url":"buffgym-program.js"}, - {"name":"buffgym-programs.json","url":"buffgym-program.json"}, + {"name":"buffgym-programs.json","url":"buffgym-programs.json"}, {"name":"buffgym.img","url":"buffgym-icon.js","evaluate":true} ] } From 900805e71b2a670ef5098f87a71525c0b7672c36 Mon Sep 17 00:00:00 2001 From: Paul Cockrell Date: Mon, 20 Apr 2020 17:20:23 +0100 Subject: [PATCH 196/302] Fix apps.json --- apps.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps.json b/apps.json index 7a1a3f5fb..4eeee4e32 100644 --- a/apps.json +++ b/apps.json @@ -1304,7 +1304,7 @@ "type": "app", "storage": [ {"name":"buffgym"}, - {"name":"buffgym.app.js"}, + {"name":"buffgym.app.js", "url": "buffgym.app.js"}, {"name":"buffgym-set.js","url":"buffgym-set.js"}, {"name":"buffgym-exercise.js","url":"buffgym-exercise.js"}, {"name":"buffgym-program.js","url":"buffgym-program.js"}, From fae5b02277014aae97e31109ffd01adc66f04430 Mon Sep 17 00:00:00 2001 From: RenaudG <34276398+renaudgweb@users.noreply.github.com> Date: Mon, 20 Apr 2020 20:39:59 +0200 Subject: [PATCH 197/302] Create osmpoi.html --- apps/osmpoi/osmpoi.html | 228 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 apps/osmpoi/osmpoi.html diff --git a/apps/osmpoi/osmpoi.html b/apps/osmpoi/osmpoi.html new file mode 100644 index 000000000..56a1f8eb6 --- /dev/null +++ b/apps/osmpoi/osmpoi.html @@ -0,0 +1,228 @@ + + + + + + + + +
+
+ +
+ +

Click

+

If ok, Click

+
+ + + + + + + + + From 83253aa5be1e48b6cfabe9fa77d4520e994067f2 Mon Sep 17 00:00:00 2001 From: RenaudG <34276398+renaudgweb@users.noreply.github.com> Date: Mon, 20 Apr 2020 20:40:28 +0200 Subject: [PATCH 198/302] Add files via upload --- apps/osmpoi/app.png | Bin 0 -> 1989 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 apps/osmpoi/app.png diff --git a/apps/osmpoi/app.png b/apps/osmpoi/app.png new file mode 100644 index 0000000000000000000000000000000000000000..31f09b5316318426fe64e8e427541af999cc72ae GIT binary patch literal 1989 zcmV;$2RitPP)<|5#K+5}}kD(!|)9(u%gVg{sw(nl_lqL$MVQDH;j|7FN0}AX{0Khb+6W@0r<| zojY^q-h2AT?8590%RDZWH1V5EGWVSGdwkFDJbvdK_&QOkEyJ_by8*nGm+3wY})8rT{P> zXe?AmAq0ZJI#tug&41xVW858IoL;E0q43XN|EjkB^-WWO$AMP?D__}s2q?f$fG2MI z#=jzaCK2q2C_s3V*$yiJRo&es;M=lqE@xVsS@AofQ_b1=TDN`pq zbu<$f>gpPzT^)KN95%x(7mY|$lYRE!foa0FM*_2f1G#kMBCr^ES@n27l4MC_R;b>R z-bdg0#|&9f)MA`BV$taHcYoCf)Bv488_?1BzXR9~#B%9z<*45PoJl40ETTp{zP{_+ zx$~95V6af<0TfNs00G<$*v068+yr94?jh=QA@ZJUSx{8B8%O}9K)6^va?u`K2S~Eq z1C%+@aZ?G%E>{dFcckMw`BD_zUT+k*#fgqwbKr);TUH*|Bax~>#lwI~uB?zY5b676 zP4=4_f#IV^r+~2;MazJTD9XQ3l!Hi8J(6^k^b%{4USdy?PR$p=O^!fd`~*03Ff#&o zdB6XI1lt6j8t2#JYoi<$~eOw(7ftMt=`)f*hc{^^m zEc^VDYh2{UREj2;570G6`WjI}m9;znk==1^6bgUY&9K z33b}^S=W+FH5?bf5x6pm`+8GkhprD&X`qz!uDO`>3LzwfqbO*8pPO_9zKbMn7`AlT zx#7<|I~!TP)@_L}PoJJlI)}PUzyj)xrlzw)Nx6f;8G(lu#U)jJ5cmxsgzb!gyVS1% z6Og2Jfi=GmdFITWnM=m}@IN17Stf8SlK{Yb$zw;anf=Lu!05okk5nKl-vffCksLB& zc@gmXJb=f)Z24)|l&N_og}rZVZUeSn_9>%ohk5DJ;7uh9Qb|c!>4F7GBq^HGgy)}JNv&g3H%$&@>{W3doCreF=KC)-0lmhcsx~*$Ak5FJYK+a ztu`VakH*%$ycZ#a-PzGfDm4n&*I%DWzy@klr%s;9%evw+jJWP_2zkALB8=44JHocj z@baY}B(}aa%lzd0C;Iknr+~er({La=l{jpC^l@HJ$(lPE4|O|=fF#Qjpxe<%B&*Ui zjnG32>#UX*6ZmO(>5?*i%Nx^y^_k5ErxJUt_O>79WnA-^y`5b}pQQRByIftOt1Cgp zh$~l)mSu&OJW_A9x5a?(0C9xa0#pID1LOzkdtX@A2s;$I&|lE|?ymRD)=Q_d2zWhq zytBjcZmfDd5xb-F%IYVC9e!-dA?xDBDDZ6{-dEE+*!imwFoD16hiXm(#1Gc&PBxtQ zO3$W^n%UNNa)6-EM{hXf_-7Lhzmb^LvfIlP~8$TcU z^DFxkwTGqwv$5?}JsbX7VYXhnd~+t3_+&j4cKp8JtEe#SmSzLMI9z)yS+jo%aDP6| z4@ICJm~Vx{yMR0TzW;&{D|rY2(1qOMdj5g0qxwpyAck3K#|7-jg X&}z;-%?VoU00000NkvXXu0mjfBg)CA literal 0 HcmV?d00001 From 4bdedc08fbb7f12c67094666256953a6c2af5a24 Mon Sep 17 00:00:00 2001 From: RenaudG <34276398+renaudgweb@users.noreply.github.com> Date: Mon, 20 Apr 2020 20:46:00 +0200 Subject: [PATCH 199/302] Update apps.json --- apps.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/apps.json b/apps.json index e55190ec0..95669f250 100644 --- a/apps.json +++ b/apps.json @@ -1412,5 +1412,17 @@ {"name":"hamloc.app.js","url":"app.js"}, {"name":"hamloc.img","url":"app-icon.js","evaluate":true} ] + }, + { "id": "osmpoi", + "name": "POI Compass", + "icon": "app.png", + "version":"0.01", + "description": "Uploads all the points of interest in an area onto your watch, same as Beer Compass with more p.o.i.", + "tags": "tool,outdoors,gps", + "custom": "osmpoi.html", + "storage": [ + {"name":"osmpoi.app.js"}, + {"name":"osmpoi.img"} + ] } ] From 95ddcbf4670c1e50376b9c7640221f72dcb698d6 Mon Sep 17 00:00:00 2001 From: RenaudG <34276398+renaudgweb@users.noreply.github.com> Date: Mon, 20 Apr 2020 20:48:07 +0200 Subject: [PATCH 200/302] Create ChangeLog --- apps/osmpoi/ChangeLog | 1 + 1 file changed, 1 insertion(+) create mode 100644 apps/osmpoi/ChangeLog diff --git a/apps/osmpoi/ChangeLog b/apps/osmpoi/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/osmpoi/ChangeLog @@ -0,0 +1 @@ +0.01: New App! From 3363999e806864910ecb252c636bf92eb2694bcd Mon Sep 17 00:00:00 2001 From: bengwalker <63957296+bengwalker@users.noreply.github.com> Date: Mon, 20 Apr 2020 20:49:54 +0200 Subject: [PATCH 201/302] add ChangeLog, attribute to source for icon --- apps/metronome/ChangeLog | 3 +++ apps/metronome/README.md | 4 ++++ 2 files changed, 7 insertions(+) create mode 100644 apps/metronome/ChangeLog diff --git a/apps/metronome/ChangeLog b/apps/metronome/ChangeLog new file mode 100644 index 000000000..a65efbaaf --- /dev/null +++ b/apps/metronome/ChangeLog @@ -0,0 +1,3 @@ +0.01: New App! +0.02: Watch vibrates with every beat +0.03: Uses mean of three time intervalls to calculate bmp diff --git a/apps/metronome/README.md b/apps/metronome/README.md index 19d489327..aab2d5a3f 100644 --- a/apps/metronome/README.md +++ b/apps/metronome/README.md @@ -8,3 +8,7 @@ This metronome makes your watch blink and vibrate with a given rate. * Use `BTN1` to increase the bmp value by one. * Use `BTN3` to decrease the bmp value by one. * You can change the bpm value any time by tapping the screen or using `BTN1` and `BTN3`. + +## Attributions + +"Icon made by Roundicons from www.flaticon.com" \ No newline at end of file From 7d8700a3e91e17bdccbdb940ffa2ec45627e772e Mon Sep 17 00:00:00 2001 From: RenaudG <34276398+renaudgweb@users.noreply.github.com> Date: Mon, 20 Apr 2020 20:50:55 +0200 Subject: [PATCH 202/302] Create README.md --- apps/osmpoi/README.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 apps/osmpoi/README.md diff --git a/apps/osmpoi/README.md b/apps/osmpoi/README.md new file mode 100644 index 000000000..4c59e16f6 --- /dev/null +++ b/apps/osmpoi/README.md @@ -0,0 +1,5 @@ +# Points Of Interest Compass + +## Description + +Uploads all the points of interest in an area onto your watch, same as Beer Compass with more p.o.i. From 707f4a1ccdd4e3f89095529060de429d6d1fa827 Mon Sep 17 00:00:00 2001 From: Paul Cockrell Date: Mon, 20 Apr 2020 21:39:30 +0100 Subject: [PATCH 203/302] More fixes --- apps/buffgym/buffgym-exercise.js | 4 +- apps/buffgym/buffgym-program.js | 2 +- apps/buffgym/buffgym-programs.json | 102 +---- apps/buffgym/buffgym-programs.json.unminified | 101 +++++ apps/buffgym/buffgym-set.js | 2 +- apps/buffgym/buffgym.app.js | 420 +++++++++--------- 6 files changed, 313 insertions(+), 318 deletions(-) create mode 100644 apps/buffgym/buffgym-programs.json.unminified diff --git a/apps/buffgym/buffgym-exercise.js b/apps/buffgym/buffgym-exercise.js index b7a1e3e15..99d658571 100644 --- a/apps/buffgym/buffgym-exercise.js +++ b/apps/buffgym/buffgym-exercise.js @@ -2,10 +2,8 @@ const STARTED = 1; const RESTING = 2; const COMPLETED = 3; const ONE_SECOND = 1000; -const INCREMENT = "increment"; -const DECREMENT = "decrement"; -class Exercise { +exports = class Exercise { constructor(params /*{title, weight, unit, restPeriod}*/) { const DEFAULTS = { title: "Unknown", diff --git a/apps/buffgym/buffgym-program.js b/apps/buffgym/buffgym-program.js index 22f39f10b..956827f56 100644 --- a/apps/buffgym/buffgym-program.js +++ b/apps/buffgym/buffgym-program.js @@ -1,4 +1,4 @@ -class Program { +exports = class Program { constructor(params) { const DEFAULTS = { title: "Unknown", diff --git a/apps/buffgym/buffgym-programs.json b/apps/buffgym/buffgym-programs.json index bf5aa0e0d..7551c7a47 100644 --- a/apps/buffgym/buffgym-programs.json +++ b/apps/buffgym/buffgym-programs.json @@ -1,101 +1 @@ -[ - { - title: "Program A", - exercises: [ - { - title: "Squats", - weight: 40, - unit: "Kg", - sets: [ - 5, - 5, - 5, - 5, - 5 - ] - }, - { - title: "Overhead press", - weight: 20, - unit: "Kg", - sets: [ - 5, - 5, - 5, - 5, - 5 - ] - }, - { - title: "Deadlift", - weight: 20, - unit: "Kg", - sets: [ - 5 - ] - }, - { - title: "Pullups", - weight: 0, - unit: "Kg", - sets: [ - 10, - 10, - 10 - ] - } - ] - }, - { - title: "Program B", - exercises: [ - { - title: "Squats", - weight: 40, - unit: "Kg", - sets: [ - 5, - 5, - 5, - 5, - 5 - ] - }, - { - title: "Bench press", - weight: 20, - unit: "Kg", - sets: [ - 5, - 5, - 5, - 5, - 5 - ] - }, - { - title: "Row", - weight: 20, - unit:"Kg", - sets: [ - 5, - 5, - 5, - 5, - 5 - ] - - }, - { - title: "Tricep extension", - weight: 20, - unit: "Kg", - sets: [ - 10, - 10, - 10 - ] - } - ] - } -] \ No newline at end of file +[{"title":"Program A","exercises":[{"title":"Squats","weight":40,"unit":"Kg","sets":[5,5,5,5,5]},{"title":"Overhead press","weight":20,"unit":"Kg","sets":[5,5,5,5,5]},{"title":"Deadlift","weight":20,"unit":"Kg","sets":[5]},{"title":"Pullups","weight":0,"unit":"Kg","sets":[10,10,10]}]},{"title":"Program B","exercises":[{"title":"Squats","weight":40,"unit":"Kg","sets":[5,5,5,5,5]},{"title":"Bench press","weight":20,"unit":"Kg","sets":[5,5,5,5,5]},{"title":"Row","weight":20,"unit":"Kg","sets":[5,5,5,5,5]},{"title":"Tricep extension","weight":20,"unit":"Kg","sets":[10,10,10]}]}] \ No newline at end of file diff --git a/apps/buffgym/buffgym-programs.json.unminified b/apps/buffgym/buffgym-programs.json.unminified new file mode 100644 index 000000000..cd005eeab --- /dev/null +++ b/apps/buffgym/buffgym-programs.json.unminified @@ -0,0 +1,101 @@ +[ + { + "title": "Program A", + "exercises": [ + { + "title": "Squats", + "weight": 40, + "unit": "Kg", + "sets": [ + 5, + 5, + 5, + 5, + 5 + ] + }, + { + "title": "Overhead press", + "weight": 20, + "unit": "Kg", + "sets": [ + 5, + 5, + 5, + 5, + 5 + ] + }, + { + "title": "Deadlift", + "weight": 20, + "unit": "Kg", + "sets": [ + 5 + ] + }, + { + "title": "Pullups", + "weight": 0, + "unit": "Kg", + "sets": [ + 10, + 10, + 10 + ] + } + ] + }, + { + "title": "Program B", + "exercises": [ + { + "title": "Squats", + "weight": 40, + "unit": "Kg", + "sets": [ + 5, + 5, + 5, + 5, + 5 + ] + }, + { + "title": "Bench press", + "weight": 20, + "unit": "Kg", + "sets": [ + 5, + 5, + 5, + 5, + 5 + ] + }, + { + "title": "Row", + "weight": 20, + "unit":"Kg", + "sets": [ + 5, + 5, + 5, + 5, + 5 + ] + + }, + { + "title": "Tricep extension", + "weight": 20, + "unit": "Kg", + "sets": [ + 10, + 10, + 10 + ] + } + ] + } +] \ No newline at end of file diff --git a/apps/buffgym/buffgym-set.js b/apps/buffgym/buffgym-set.js index d7d610a78..aed6df260 100644 --- a/apps/buffgym/buffgym-set.js +++ b/apps/buffgym/buffgym-set.js @@ -1,4 +1,4 @@ -class Set { +exports = class Set { constructor(maxReps) { this._minReps = 0; this._maxReps = maxReps; diff --git a/apps/buffgym/buffgym.app.js b/apps/buffgym/buffgym.app.js index d3a4e6834..11005900a 100755 --- a/apps/buffgym/buffgym.app.js +++ b/apps/buffgym/buffgym.app.js @@ -1,230 +1,226 @@ -/* global g, setWatch, clearWatch, reset, BTN1, BTN2, BTN3 */ +const W = g.getWidth(); +const H = g.getHeight(); +const RED = "#d32e29"; +const PINK = "#f05a56"; +const WHITE = "#ffffff"; -(() => { - const W = g.getWidth(); - const H = g.getHeight(); - const RED = "#d32e29"; - const PINK = "#f05a56"; - const WHITE = "#ffffff"; +const Set = require("buffgym-set.js"); +const Exercise = require("buffgym-exercise.js"); +const Program = require("buffgym-program.js"); - const Set = require("set.js"); - const Exercise = require("exercise.js"); - const Program = require("program.js"); +function centerStringX(str) { + return (W - g.stringWidth(str)) / 2; +} - function centerStringX(str) { - return (W - g.stringWidth(str)) / 2; +function iconIncrement() { + const img = require("heatshrink").decompress(atob("ikUxH+AA3XAAgNHCJIVMBYXQ5PC4XJ6AUJCIQQBAAoVCCQwjCAA/JCgglHA4IpJBYwTHA4RMJCY5oDJo4THKIQKET5IMGCaY7TMaKLTWajbTFJIlICgoVBFYXJYQYSGCggAGCRAVIBgw")); + return img; +} + +function iconDecrement() { + const img = require("heatshrink").decompress(atob("ikUxH+AA3XAAgNHCJIVMBYXQ5PC4XJ6AUJCIQQBAAoVCCQwjCAA/JCgglKFJADBCRYABCYQmOFAhNMKIw6FTw4LHCaY7TMaKLTWajbTFJglFCgoVBFYXJYQYSGCggAGCRAVIBgw=")); + return img; +} + +function iconOk() { + const img = require("heatshrink").decompress(atob("ikUxH+AA3XAAgNHCJIVMBYXQ5PC4XJ6AUJCIQQBAAoVCCQwjCAA/JCgglKFJADBCJQxCCYQmMIwZoDJpQMCKIg6KBYwTGFQgeHHYouCCRI7EMYTXFRhILEK5SfFRgYSIborbSbpglFCgoVBFYXJYQYSGCggAGCRAVIBgwA==")); + return img; +} + +function drawMenu(params) { + const DEFAULT_PARAMS = { + showBTN1: false, + showBTN2: false, + showBTN3: false, + }; + const p = Object.assign({}, DEFAULT_PARAMS, params); + if (p.showBTN1) g.drawImage(iconIncrement(), W - 30, 10); + if (p.showBTN2) g.drawImage(iconOk(), W - 30, 110); + if (p.showBTN3) g.drawImage(iconDecrement(), W - 30, 210); +} + +function clearScreen() { + g.setColor(RED); + g.fillRect(0,0,W,H); +} + +function drawTitle(exercise) { + const title = exercise.humanTitle; + + g.setFont("Vector",20); + g.setColor(WHITE); + g.drawString(title, centerStringX(title), 5); +} + +function drawReps(exercise) { + const set = exercise.currentSet; + if (set.isCompleted()) return; + + g.setColor(PINK); + g.fillCircle(W / 2, H / 2, 50); + g.setColor(WHITE); + g.setFont("Vector", 40); + g.drawString(set.reps, centerStringX(set.reps), (H - 45) / 2); + g.setFont("Vector", 15); + const note = `of ${set.maxReps}`; + g.drawString(note, centerStringX(note), (H / 2) + 25); +} + +function drawSets(exercise) { + const sets = exercise.subTitle; + + g.setColor(WHITE); + g.setFont("Vector", 15); + g.drawString(sets, centerStringX(sets), H - 25); +} + +function drawSetProgress(exercise) { + drawTitle(exercise); + drawReps(exercise); + drawSets(exercise); + drawMenu({showBTN1: true, showBTN2: true, showBTN3: true}); +} + +function drawStartNextExercise() { + const title = "Good work"; + const msg = "No need to rest\nmove straight on\nto the next exercise"; + + g.setColor(WHITE); + g.setFont("Vector", 35); + g.drawString(title, centerStringX(title), 10); + g.setFont("Vector", 15); + g.drawString(msg, 30, 150); + drawMenu({showBTN1: false, showBTN2: true, showBTN3: false}); +} + +function drawProgramCompleted() { + const title1 = "You did"; + const title2 = "GREAT!"; + const msg = "That's the program\ncompleted. Now eat\nsome food and\nget plenty of rest."; + + clearWatch(); + setWatch(reset, BTN2, {repeat: false}); + + g.setColor(WHITE); + g.setFont("Vector", 35); + g.drawString(title1, centerStringX(title1), 10); + g.setFont("Vector", 40); + g.drawString(title2, centerStringX(title2), 50); + g.setFont("Vector", 15); + g.drawString(msg, 30, 150); + drawMenu({showBTN1: false, showBTN2: true, showBTN3: false}); +} + +/* +function drawExerciseCompleted(program) { + const exercise = program.currentExercise(); + const title = exercise.canProgress? + "WELL DONE!" : + "NOT BAD!"; + const msg = exercise.canProgress? + `You weight is automatically increased\nfor ${exercise.title} to ${exercise.weight}${exercise.unit}` : + "It looks like you struggled\non a few sets, your weight will\nstay the same"; + const action = "Move straight on to the next exercise"; + + clearScreen(); + g.setColor(WHITE); + g.setFont("Vector", 20); + g.drawString(title, centerStringX(title), 10); + g.setFont("Vector", 10); + g.drawString(msg, centerStringX(msg), 180); + g.drawString(action, centerStringX(action), 210); + drawMenu({showBTN2: true}); + + clearWatch(); + setWatch(() => { + init(program); + }, BTN2, {repeat: false}); +} +*/ + +function drawRestTimer(program) { + const exercise = program.currentExercise(); + const motivation = "Take a breather.."; + clearScreen(); + drawMenu({showBTN2: true}); + + g.setColor(PINK); + g.fillCircle(W / 2, H / 2, 50); + g.setColor(WHITE); + g.setFont("Vector", 15); + g.drawString(motivation, centerStringX(motivation), 25); + g.setFont("Vector", 40); + g.drawString(exercise.restPeriod, centerStringX(exercise.restPeriod), (H - 45) / 2); + exercise.decRestPeriod(); + + if (exercise.restPeriod <= 0) { + exercise.resetRestTimer(); + redraw(program); + } +} + +function redraw(program) { + const exercise = program.currentExercise(); + + clearScreen(); + + if (program.isCompleted()) { + drawProgramCompleted(program); + return; } - function iconIncrement() { - const img = require("heatshrink").decompress(atob("ikUxH+AA3XAAgNHCJIVMBYXQ5PC4XJ6AUJCIQQBAAoVCCQwjCAA/JCgglHA4IpJBYwTHA4RMJCY5oDJo4THKIQKET5IMGCaY7TMaKLTWajbTFJIlICgoVBFYXJYQYSGCggAGCRAVIBgw")); - return img; - } - - function iconDecrement() { - const img = require("heatshrink").decompress(atob("ikUxH+AA3XAAgNHCJIVMBYXQ5PC4XJ6AUJCIQQBAAoVCCQwjCAA/JCgglKFJADBCRYABCYQmOFAhNMKIw6FTw4LHCaY7TMaKLTWajbTFJglFCgoVBFYXJYQYSGCggAGCRAVIBgw=")); - return img; - } - - function iconOk() { - const img = require("heatshrink").decompress(atob("ikUxH+AA3XAAgNHCJIVMBYXQ5PC4XJ6AUJCIQQBAAoVCCQwjCAA/JCgglKFJADBCJQxCCYQmMIwZoDJpQMCKIg6KBYwTGFQgeHHYouCCRI7EMYTXFRhILEK5SfFRgYSIborbSbpglFCgoVBFYXJYQYSGCggAGCRAVIBgwA==")); - return img; - } - - function drawMenu(params) { - const DEFAULT_PARAMS = { - showBTN1: false, - showBTN2: false, - showBTN3: false, - }; - const p = Object.assign({}, DEFAULT_PARAMS, params); - if (p.showBTN1) g.drawImage(iconIncrement(), W - 30, 10); - if (p.showBTN2) g.drawImage(iconOk(), W - 30, 110); - if (p.showBTN3) g.drawImage(iconDecrement(), W - 30, 210); - } - - function clearScreen() { - g.setColor(RED); - g.fillRect(0,0,W,H); - } - - function drawTitle(exercise) { - const title = exercise.humanTitle; - - g.setFont("Vector",20); - g.setColor(WHITE); - g.drawString(title, centerStringX(title), 5); - } - - function drawReps(exercise) { - const set = exercise.currentSet; - if (set.isCompleted()) return; - - g.setColor(PINK); - g.fillCircle(W / 2, H / 2, 50); - g.setColor(WHITE); - g.setFont("Vector", 40); - g.drawString(set.reps, centerStringX(set.reps), (H - 45) / 2); - g.setFont("Vector", 15); - const note = `of ${set.maxReps}`; - g.drawString(note, centerStringX(note), (H / 2) + 25); - } - - function drawSets(exercise) { - const sets = exercise.subTitle; - - g.setColor(WHITE); - g.setFont("Vector", 15); - g.drawString(sets, centerStringX(sets), H - 25); - } - - function drawSetProgress(exercise) { - drawTitle(exercise); - drawReps(exercise); - drawSets(exercise); - drawMenu({showBTN1: true, showBTN2: true, showBTN3: true}); - } - - function drawStartNextExercise() { - const title = "Good work"; - const msg = "No need to rest\nmove straight on\nto the next exercise"; - - g.setColor(WHITE); - g.setFont("Vector", 35); - g.drawString(title, centerStringX(title), 10); - g.setFont("Vector", 15); - g.drawString(msg, 30, 150); - drawMenu({showBTN1: false, showBTN2: true, showBTN3: false}); - } - - function drawProgramCompleted() { - const title1 = "You did"; - const title2 = "GREAT!"; - const msg = "That's the program\ncompleted. Now eat\nsome food and\nget plenty of rest."; - - clearWatch(); - setWatch(reset, BTN2, {repeat: false}); - - g.setColor(WHITE); - g.setFont("Vector", 35); - g.drawString(title1, centerStringX(title1), 10); - g.setFont("Vector", 40); - g.drawString(title2, centerStringX(title2), 50); - g.setFont("Vector", 15); - g.drawString(msg, 30, 150); - drawMenu({showBTN1: false, showBTN2: true, showBTN3: false}); - } - - /* - function drawExerciseCompleted(program) { - const exercise = program.currentExercise(); - const title = exercise.canProgress? - "WELL DONE!" : - "NOT BAD!"; - const msg = exercise.canProgress? - `You weight is automatically increased\nfor ${exercise.title} to ${exercise.weight}${exercise.unit}` : - "It looks like you struggled\non a few sets, your weight will\nstay the same"; - const action = "Move straight on to the next exercise"; - - clearScreen(); - g.setColor(WHITE); - g.setFont("Vector", 20); - g.drawString(title, centerStringX(title), 10); - g.setFont("Vector", 10); - g.drawString(msg, centerStringX(msg), 180); - g.drawString(action, centerStringX(action), 210); - drawMenu({showBTN2: true}); - - clearWatch(); - setWatch(() => { - init(program); - }, BTN2, {repeat: false}); - } - */ - - function drawRestTimer(program) { - const exercise = program.currentExercise(); - const motivation = "Take a breather.."; - clearScreen(); - drawMenu({showBTN2: true}); - - g.setColor(PINK); - g.fillCircle(W / 2, H / 2, 50); - g.setColor(WHITE); - g.setFont("Vector", 15); - g.drawString(motivation, centerStringX(motivation), 25); - g.setFont("Vector", 40); - g.drawString(exercise.restPeriod, centerStringX(exercise.restPeriod), (H - 45) / 2); - exercise.decRestPeriod(); - - if (exercise.restPeriod <= 0) { - exercise.resetRestTimer(); - redraw(program); - } - } - - function redraw(program) { - const exercise = program.currentExercise(); - - clearScreen(); - - if (program.isCompleted()) { - drawProgramCompleted(program); - return; + if (exercise.isRestTimerRunning()) { + if (exercise.isLastSet()) { + drawStartNextExercise(program); + } else { + drawRestTimer(program); } - if (exercise.isRestTimerRunning()) { - if (exercise.isLastSet()) { - drawStartNextExercise(program); - } else { - drawRestTimer(program); - } - - return; - } - - drawSetProgress(exercise); + return; } - function init(program) { - clearWatch(); - program.next(); - } + drawSetProgress(exercise); +} - // Setup training program. This should come from file +function init(program) { + clearWatch(); + program.next(); +} - // Squats - function buildPrograms() { - const programsJSON = require("Storage").readJSON("buffgym-programs.json", 1); +// Setup training program. This should come from file - if (!programsJSON) throw "No programs JSON found"; +// Squats +function buildPrograms() { + const programsJSON = require("Storage").readJSON("buffgym-programs.json", 1); - const programs = []; + if (!programsJSON) throw "No programs JSON found"; - programsJSON.forEach(programJSON => { - const program = new Program({ - title: programJSON.title, - }); - const exercises = programJSON.exercises.map(exerciseJSON => { - const exercise = new Exercise({ - title: exerciseJSON.title, - weight: exerciseJSON.weight, - unit: exerciseJSON.unit, - }); - exerciseJSON.sets.forEach(setJSON => { - exercise.addSet(new Set(setJSON)); - }); + const programs = []; - return exercise; - }); - program.addExercises(exercises); - programs.push(program); + programsJSON.forEach(programJSON => { + const program = new Program({ + title: programJSON.title, }); + const exercises = programJSON.exercises.map(exerciseJSON => { + const exercise = new Exercise({ + title: exerciseJSON.title, + weight: exerciseJSON.weight, + unit: exerciseJSON.unit, + }); + exerciseJSON.sets.forEach(setJSON => { + exercise.addSet(new Set(setJSON)); + }); - return programs; - } + return exercise; + }); + program.addExercises(exercises); + programs.push(program); + }); - // For this spike, just run the first program, what will - // really happen is the user picks a program to do from - // some menu on a start page. - init(buildPrograms()[0]); -})(); \ No newline at end of file + return programs; +} + +// For this spike, just run the first program, what will +// really happen is the user picks a program to do from +// some menu on a start page. +init(buildPrograms()[0]); \ No newline at end of file From 6deb376d99b4dab29c9ec31bd78432e08b08510e Mon Sep 17 00:00:00 2001 From: Paul Cockrell Date: Mon, 20 Apr 2020 22:02:55 +0100 Subject: [PATCH 204/302] Return to launcher on exit --- apps/buffgym/buffgym.app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/buffgym/buffgym.app.js b/apps/buffgym/buffgym.app.js index 11005900a..e1ab3e66b 100755 --- a/apps/buffgym/buffgym.app.js +++ b/apps/buffgym/buffgym.app.js @@ -99,7 +99,7 @@ function drawProgramCompleted() { const msg = "That's the program\ncompleted. Now eat\nsome food and\nget plenty of rest."; clearWatch(); - setWatch(reset, BTN2, {repeat: false}); + setWatch(Bangle.showLauncher, BTN2, {repeat: false}); g.setColor(WHITE); g.setFont("Vector", 35); From 15a92c1b38006dfd908c91e69b060ca457d35e9b Mon Sep 17 00:00:00 2001 From: singintime Date: Tue, 21 Apr 2020 01:10:10 +0200 Subject: [PATCH 205/302] Minion clock v0.02 --- apps.json | 2 +- apps/minionclk/ChangeLog | 1 + apps/minionclk/app.js | 10 +++++----- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/apps.json b/apps.json index b58fad5ce..924d2e75c 100644 --- a/apps.json +++ b/apps.json @@ -1126,7 +1126,7 @@ { "id": "minionclk", "name": "Minion clock", "icon": "minionclk.png", - "version": "0.01", + "version": "0.02", "description": "Minion themed clock.", "tags": "clock,minion", "type": "clock", diff --git a/apps/minionclk/ChangeLog b/apps/minionclk/ChangeLog index 7b83706bf..dbe920a80 100755 --- a/apps/minionclk/ChangeLog +++ b/apps/minionclk/ChangeLog @@ -1 +1,2 @@ 0.01: First release +0.02: Improved date readability, fixed drawing of widgets diff --git a/apps/minionclk/app.js b/apps/minionclk/app.js index 88fe446ae..7f00cd362 100755 --- a/apps/minionclk/app.js +++ b/apps/minionclk/app.js @@ -1,4 +1,4 @@ -const bob = require("heatshrink").decompress(atob("nk8hAaXlYLWAEsqvN/0gBBql5lQ2tquj1XV5wBJ52j0hACPsdP1QsBAQQAGBIIBF51/P8OkN5R1GIxF5HLmAFgoDLPZfOpzmZ6vPFwomCPaA6DAYOjeq2A1YyCdI4HGQJQ8F1T2SJ4Oq1XW1es1mtAQOrPoPUAIh3J54ZHIAR5S62s64cBwIBGQIOqHQK4HKQYVDAAIFC1g+BHh9VHAQAFDwQDDHoJ5E54BB6AaBKQ5YGqo6MwJzGHQ4BDeIj/BR4JxDABY8BvI6OOYgaEHwZADHgQ6BZA42GAIusPJNW64eFqzJDlcrERA8BHQI2FqwaBDYYGBPI45GCoIgCLoVWQ5NWXA2rKhaiGLAwOGEAmADxJPDVA51ElQaMC4ouEWALdEHRg8Dc4woCDJo8EAIYxCHQQIFHiwaRegJ5EcYWsHgbrKbBA8GDSrNDO4wfRKgR3FDSh3CN4UrdwZbSHYZ5DHajMFHYQGCHalWO4jtQDQwABwAGCAAQfTKoK0EHahwCeARdFHakASIZWVZ4Q8CO4YgWO4QbCO6hWGEIKYZKzZ3DLog7UG4I6C1lWDSdWO4bpCO4bwUwKYEHajMDwOAlUkLojTUd4gaTZoRWC0YIB1eJLqo4EWiqRE0mjlcr1QkEeKFWOooBCHiB2CC4WA5wzB52rEQgfPHQwABAYJXOHQ2iO4XO6omFEJh1BEAgBGPJlWDIQbC0ej50qgHV1XPEwohKcwRbEvJ3EBQTrLFomkOwOjlR3C5w8GMAR8ClYuBLIgOCvN4HgIZFDQYbBlaOBR4YNCwA5B0XOpzvBHgWqTw4AFxB1EvQ6BAAI8GDZILEdgQBCqp3DPIRfIEQwABvJ1CvGkvGiwA6IAYoBCv6wCAAVOlQ6DAIWkL5ABEwN40Z1CAYJfCv7zEAJNWOYZ3KAIWq5yYHFYLOBLIrVCAIh7BOIzpECoYDDpw7BHAQDG1WqwGkAIN/CwIABLY4LDAAZ9BwAABvLCBC5IBBvOrO44BEAAmjAIPN0XOAJyHIAAgPEquBHBi4BAId+HwWiHxqJDRpYBDq2I1R3LIAQ6BAAOpPogABGgIDDOomk0nP5pIGd42Aq1ewI8CeZI6CHAJhCEAIDCdIo2B0er1esAQIZBC4Z9JlVW1leeKGp0es5+s6+s6ABB0oFBAIervWr0ulub7OqtWmdexJ4BGxB5G0V6pF6wItB0t6p9PvQABvINBudJudPzwXCHQwBDlY6BO4WIwPPPJbvDvA0BFgNzAAIDGugGCzrtHdYh1DAIOBrzxBPIgAIeIXNHgoBCGwYADzoVB0fNOpMyHQdW5+Bro7BHgQAB6jxJAAOjOYhxCAIukOoPN5ujdZFWqyyD0d6AwUrquBwAuB1I1FHgRJBMoQ9BWg1zzw0BDYI6B0R3DAAJ1BvMyp8rAAV4IQWBIodewAeCHAZ5IAAJoBAAXHHAJWDO4TtEdYQvEHgejAwIKClcqIQRdDXYbzFeoQBIGwIDDOot/VgQ6FAAIGBlgBCAAMzmZPBF4LzDACB1FAAOi1WjvFVr0zGYQ7GAAMAAYpPBwNeAIOIwOrfYOA1eA1WkAIWjAIekv4PBwCVBruBq5eBEYIABlcBF4wCEHw55CHwQABIQQBBABkzAILlCHQR1CFYavEPgsAAAIDEDQNdAwQAaHQNWEwQ0DHAh3KleBLoI7dHQKuFWQo0EAIsISoKdBHbyyHNgwADlVVpwEBDANWro7fd4Q6HO495vF5QgIYCd75eBHYUINAN5lQ3EA")); +const bob = require("heatshrink").decompress(atob("nk8hAaXlYLWAEsqvN/0gBBql5lQ2tquj1XV5wBJ52j0hACPsdP1QsBAQQAGBIIBF51/P8OkN5R1GIxF5HLmAFgoDLPZfOpzmZ6vPFwomCPaA6DAYOjeq2A1YyCdI4HGQJQ8F1T2SJ4Oq1XW1es1mtAQOrPoPUAIh3J54ZHIAR5S62s64cBwIBGQIOqHQK4HKQYVDAAIFC1g+BHh9VHAQAFDwQDDHoJ5E54BB6AaBKQ5YGqo6MwJzGHQ4BDeIj/BR4JxDABY8BvI6OOYgaEHwZADHgQ6BZA42GAIusPJNW64eFqzJDlcrERA8BHQI2FqwaBDYYGBPI45GCoIgCLoVWQ5NWXA2rKhaiGLAwOGEAmADxJPDVA51ElQaMC4ouEWALdEHRg8Dc4woCDJo8EAIYxCHQQIFHiwaRegJ5EcYWsHgbrKbBA8GDSrNDO4wfRKgR3FDSh3CN4UrdwZbSHYZ5DHajMFHYQGCHalWO4jtQDQwABwAGCAAQfTKoK0EHahwCeARdFHakASIZWVZ4Q8CO4YgWO4QbCO6hWGEIKYZKzZ3DLog7UG4I6C1lWDSdWO4bpCO4bwUwKYEHajMDwOAlUkLojTUd4gaTZoRWC0YIB1eJLqo4EWiqRE0mjlcr1QkEeKFWOooBCHiB2CC4WA5wzB52rEQgfPHQwABAYJXOHQ2iO4XO6omFEJh1BEAgBGPJlWDIQbC0ej50qgHV1XPEwohKcwRbEvJ3EBQTrLFomkOwOjlR3C5w8GMAR8ClYuBLIgOCvN4HgIZFDQYbBlaOBR4YNCwA5B0XOpzvBHgWqTw4AFxB1EvQ6BAAI8GDZILEdgQBCqp3DPIRfIEQwABvJ1CvGkvGiwA6IAYoBCv6wCAAVOlQ6DAIWkL5ABEwN40Z1CAYJfCv7zEAJNWOYZ3KAIWq5yYHFYLOBLIrVCAIh7BOIzpECoYDDpw7BHAQDG1WqwGkAIN/CwIABLY4LDAAZ9BwAABvLCBC5IBBvOrO44BEAAmjAIPN0XOAJyHIAAgPEquBHBi4BAId+HwWiHxqJDRpYBDq2I1R3LIAQ6BAAOpPogABGgIDDOomk0nP5pIGd42Aq1ewI8CeZI6CHAJhCEAIDCdIo2B0er1esAQIZBC4Z9JlVW1leeKGp0es5+s6+s6ABB0oFBAIervWr0ultr7OqtWmdexJ4BGxB5G0V6pF6wItB0t6p9PvQABvINBttJttPzwXCHQwBDlY6BO4WIwPPPJbvDvA0BFgNtAAIDGtwGCzrtHdYh1DAIOBrzxBPIgAIeIXNHgoBCGwYADzoVB0fNOpMOHQdW5+Bro7BHgQAB6jxJAAOjOYhxCAIukOoPN5ujdZFWqyyD0d6AwUrquBwAuB1I1FHgRJBMoQ9BWg1tzw0BDYI6B0R3DAAJ1BvMOp8rAAV4IQWBIodewAeCHAZ5IAAJoBAAXHHAJWDO4TtEdYQvEHgejAwIKClcqIQRdDXYbzFeoQBIGwIDDOot/VgQ6FAAIGBlgBCAAMzmZPBF4LzDACB1FAAOi1WjvFVr0zGYQ7GAAMAAYpPBwNeAIOIwOrfYOA1eA1WkAIWjAIekv4PBwCVBruBq5eBEYIABlcBF4wCEHw8zgAAiFYivEPgoSEAYo9jGgY4EO5Q7kVwiyFGggBFhASBHkhsKAAcqqtOAgMzd8o6HO495vF5QgMzrw7lhBoBvMqG4g")); const locale = require("locale"); @@ -37,7 +37,7 @@ function draw() { } if (newDate !== date) { - g.setFontVector(12); + g.setFont('6x8', 2); g.setColor(black); g.drawString(date, 120, 228); g.setColor(0xFFFF); @@ -51,6 +51,8 @@ function drawAll() { minute = ''; date = ''; g.drawImage(bob, 0, 0, { scale: 4 }); + Bangle.loadWidgets(); + Bangle.drawWidgets(); draw(); } @@ -60,9 +62,7 @@ Bangle.on('lcdPower', function(on) { } }); -Bangle.loadWidgets(); -Bangle.drawWidgets(); setInterval(draw, 1000); drawAll(); -setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"}); +setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: 'falling' }); From bc4a05e314c1efd71d66a44788ee04ac81bb9cc8 Mon Sep 17 00:00:00 2001 From: singintime Date: Tue, 21 Apr 2020 10:43:54 +0200 Subject: [PATCH 206/302] Addressing review comments --- apps/minionclk/app.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/minionclk/app.js b/apps/minionclk/app.js index 7f00cd362..0f92b28bc 100755 --- a/apps/minionclk/app.js +++ b/apps/minionclk/app.js @@ -9,6 +9,8 @@ let hour; let minute; let date; +let timer; + function draw() { const d = new Date(); @@ -51,7 +53,6 @@ function drawAll() { minute = ''; date = ''; g.drawImage(bob, 0, 0, { scale: 4 }); - Bangle.loadWidgets(); Bangle.drawWidgets(); draw(); } @@ -59,10 +60,13 @@ function drawAll() { Bangle.on('lcdPower', function(on) { if (on) { drawAll(); + timer = setInterval(draw, 1000); + } else if (timer) { + clearInterval(timer); } }); -setInterval(draw, 1000); +Bangle.loadWidgets(); drawAll(); setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: 'falling' }); From a8cf48c794706420404136190fbdfede3e6080b4 Mon Sep 17 00:00:00 2001 From: singintime Date: Tue, 21 Apr 2020 11:52:36 +0200 Subject: [PATCH 207/302] Addressing review comments 2 --- apps/minionclk/app.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/apps/minionclk/app.js b/apps/minionclk/app.js index 0f92b28bc..0725f8fa6 100755 --- a/apps/minionclk/app.js +++ b/apps/minionclk/app.js @@ -48,25 +48,30 @@ function draw() { } } -function drawAll() { +function startDrawing() { hour = ''; minute = ''; date = ''; g.drawImage(bob, 0, 0, { scale: 4 }); Bangle.drawWidgets(); draw(); + setInterval(draw, 1000); +} + +function stopDrawing() { + if (timer) { + clearInterval(timer); + } } Bangle.on('lcdPower', function(on) { + stopDrawing(); if (on) { - drawAll(); - timer = setInterval(draw, 1000); - } else if (timer) { - clearInterval(timer); + startDrawing(); } }); Bangle.loadWidgets(); -drawAll(); +startDrawing(); setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: 'falling' }); From 7735a2ba97d6156c79450869a7f716d52cd9cffb Mon Sep 17 00:00:00 2001 From: fredericrous Date: Tue, 21 Apr 2020 11:18:29 +0100 Subject: [PATCH 208/302] =?UTF-8?q?New=20game:=20Pong=F0=9F=95=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps.json | 14 +++ apps/pong/ChangeLog | 1 + apps/pong/app-icon.js | 1 + apps/pong/app.js | 282 ++++++++++++++++++++++++++++++++++++++++++ apps/pong/pong.png | Bin 0 -> 894 bytes 5 files changed, 298 insertions(+) create mode 100644 apps/pong/ChangeLog create mode 100644 apps/pong/app-icon.js create mode 100644 apps/pong/app.js create mode 100644 apps/pong/pong.png diff --git a/apps.json b/apps.json index 95669f250..9b381e6f4 100644 --- a/apps.json +++ b/apps.json @@ -1424,5 +1424,19 @@ {"name":"osmpoi.app.js"}, {"name":"osmpoi.img"} ] + }, + { "id": "pong", + "name": "Pong", + "shortName": "Pong", + "icon": "pong.png", + "version": "0.01", + "description": "A clone of the Atari game Pong", + "tags": "game", + "type": "app", + "allow_emulator": true, + "storage": [ + {"name":"pong.app.js","url":"app.js"}, + {"name":"pong.img","url":"app-icon.js","evaluate":true} + ] } ] diff --git a/apps/pong/ChangeLog b/apps/pong/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/pong/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/pong/app-icon.js b/apps/pong/app-icon.js new file mode 100644 index 000000000..881e60ba9 --- /dev/null +++ b/apps/pong/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgIEBgOABQYFD8AUEApoXFDqIXV4BYGKZIANsIRE+IFE/IFEvCIFGrgXLDqIAOgc/9/2hv+g8///3AoUwvE3xuABYP4m3NzwFB7E2tu/CIMYm09wYFDjoFCj4pB/8HkEP+EBFII7EAosDJxYA=")) diff --git a/apps/pong/app.js b/apps/pong/app.js new file mode 100644 index 000000000..4531b3af8 --- /dev/null +++ b/apps/pong/app.js @@ -0,0 +1,282 @@ +/** + * BangleJS Pong game + * + * Original Author: Frederic Rousseau https://github.com/fredericrous + * Created: April 2020 + * + * Inspired by: + * - Let's make pong, One Man Army Studios, Youtube + * - Pong.js, KanoComputing, Github + * - Coding Challenge #67: Pong!, The Coding Train, Youtube + */ + +const SCREEN_WIDTH = 240; +const FPS = 16; +const MAX_SCORE = 11; +let scores = [0, 0]; +let aiSpeedRandom = 0; + +function Vector(x, y) { + this.x = x; + this.y = y; +} +Vector.prototype.add = function (x) { + this.x += x.x || 0; + this.y += x.y || 0; + return this; +}; + +const constrain = (n, low, high) => Math.max(Math.min(n, high), low); +const random = (min, max) => Math.random() * (max - min) + min; +const intersects = (circ, rect) => { + var c1 = circ.pos, c2 = {x: circ.pos.x+circ.r, y: circ.pos.y+circ.r}; + var r1 = rect.pos, r2 = {x: rect.pos.x+rect.width*2, y: rect.pos.y+rect.height}; + return !(c1.x > r2.x || c2.x < r1.x || + c1.y > r2.y || c2.y < r1.y); +}; + +///////////////////////////// Ball ////////////////////////////////////////// + +function Ball() { + this.r = 4; + this.prevPos = null; + this.originalSpeed = 4; + this.maxSpeed = 6; + + this.reset(); +} +Ball.prototype.show = function () { + if (this.prevPos != null) { + g.setColor(0); + g.fillCircle(this.prevPos.x, this.prevPos.y, this.prevPos.r); + } + g.setColor(-1); + g.fillCircle(this.pos.x, this.pos.y, this.r); + this.prevPos = { + x: this.pos.x, + y: this.pos.y, + r: this.r + }; +}; +Ball.prototype.bouncePlayer = function (multiplyX, multiplyY, player) { + this.speed = constrain(this.speed + 2, this.originalSpeed, this.maxSpeed); + var relativeIntersectY = (player.pos.y+(player.height/2)) - this.pos.y; + var normalizedRelativeIntersectionY = (relativeIntersectY/(player.height/2)); + var MAX_BOUNCE_ANGLE = 4 * Math.PI/12; + var bounceAngle = normalizedRelativeIntersectionY * MAX_BOUNCE_ANGLE; + this.velocity.x = this.speed * Math.cos(bounceAngle) * multiplyX; + this.velocity.y = this.speed * -Math.sin(bounceAngle) * multiplyY; +}; +Ball.prototype.bounce = function (multiplyX, multiplyY, player) { + if (player) + return this.bouncePlayer(multiplyX, multiplyY, player); + + if (multiplyX) { + this.velocity.x = Math.abs(this.velocity.x) * multiplyX; + } + if (multiplyY) { + this.velocity.y = Math.abs(this.velocity.y) * multiplyY; + } +}; +Ball.prototype.checkWallsCollision = function () { + if (this.pos.y < 0) { + this.bounce(0, 1); + } else if (this.pos.y > SCREEN_WIDTH) { + this.bounce(0, -1); + } else if (this.pos.x < 0) { + scores[1]++; + if (scores[1] >= MAX_SCORE) { + this.restart(); + state = 3; + winnerMessage = "AI Wins!"; + } else { + this.reset(); + } + } else if (this.pos.x > SCREEN_WIDTH) { + scores[0]++; + if (scores[0] >= MAX_SCORE) { + this.restart(); + state = 3; + winnerMessage = "You Win!"; + } else { + this.reset(); + } + } else { + return false; + } + return true; +}; +Ball.prototype.checkPlayerCollision = function (player) { + if (intersects(this, player)) { + if (this.pos.x < SCREEN_WIDTH/2) { + this.bounce(1, 1, player); + this.pos.add(new Vector(this.width, 0)); + aiSpeedRandom = random(-1.6, 1.6); + } else { + this.bounce(-1, 1, player); + this.pos.add(new Vector(-(this.width / 2 + 1), 0)); + } + return true; + } + return false; +}; +Ball.prototype.checkCollisions = function () { + return this.checkWallsCollision() || this.checkPlayerCollision(player) || this.checkPlayerCollision(ai); +}; +Ball.prototype.updatePosition = function () { + var elapsed = new Date().getTime() - this.lastUpdate; + var x = (elapsed / 50) * this.velocity.x; + var y = (elapsed / 50) * this.velocity.y; + this.pos.add(new Vector(x, y)); +}; +Ball.prototype.update = function () { + this.updatePosition(); + this.lastUpdate = new Date().getTime(); + this.checkCollisions(); +}; +Ball.prototype.reset = function() { + this.speed = this.originalSpeed; + var x = scores[0] < scores[1] || (scores[0] === 0 && scores[1] === 0) ? -this.speed : this.speed; + var bounceAngle = Math.PI/6; + this.velocity = new Vector(x * Math.cos(bounceAngle), this.speed * -Math.sin(bounceAngle)); + this.pos = new Vector(SCREEN_WIDTH/2, random(0, SCREEN_WIDTH)); +}; +Ball.prototype.restart = function() { + ai.pos = new Vector(SCREEN_WIDTH - ai.width*2, SCREEN_WIDTH/2 - ai.height/2); + player.pos = new Vector(player.width*2, SCREEN_WIDTH/2 - player.height/2); + this.pos = new Vector(SCREEN_WIDTH/2, SCREEN_WIDTH/2); +}; + +//////////////////////////// Player ///////////////////////////////////////// + +function Player() { + this.width = 4; + this.height = 30; + this.pos = new Vector(this.width*2, SCREEN_WIDTH/2 - this.height/2); + this.acc = new Vector(0, 0); + this.speed = 15; + this.maxSpeed = 25; + this.prevPos = null; +} +Player.prototype.show = function () { + if (this.prevPos != null) { + g.setColor(0); + g.fillRect(this.prevPos.x1, this.prevPos.y1, this.prevPos.x2, this.prevPos.y2); + } + g.setColor(-1); + g.fillRect(this.pos.x, this.pos.y, this.pos.x+this.width, this.pos.y+this.height); + this.prevPos = { + x1: this.pos.x, + y1: this.pos.y, + x2: this.pos.x+this.width, + y2: this.pos.y+this.height + }; +}; +Player.prototype.up = function () { + this.acc.y -= this.speed; +}; +Player.prototype.down = function () { + this.acc.y += this.speed; +}; +Player.prototype.stop = function () { + this.acc.y = 0; +}; +Player.prototype.update = function () { + this.acc.y = constrain(this.acc.y, -this.maxSpeed, this.maxSpeed); + this.pos.add(this.acc); + this.pos.y = constrain(this.pos.y, 0, SCREEN_WIDTH-this.height); +}; + +////////////////////////////// AI /////////////////////////////////////////// + +function AI() { + Player.call(this); + this.pos = new Vector(SCREEN_WIDTH-this.width*2, SCREEN_WIDTH/2 - this.height/2); +} +AI.prototype = Object.create(Player.prototype); +AI.prototype.constructor = Player; +AI.prototype.update = function () { + var y = ball.pos.y - (this.height/2 * aiSpeedRandom); + var yConstrained = constrain(y, 0, SCREEN_WIDTH-this.height); + this.pos = new Vector(this.pos.x, yConstrained); +}; + +function net() { + var dashSize = 5; + for (let y = dashSize/2; y < SCREEN_WIDTH; y += dashSize*2) { + g.setColor(-1); + let halfScreen = SCREEN_WIDTH/2; + g.fillRect(halfScreen-dashSize/2, y, halfScreen+dashSize/2, y+dashSize); + } +} + +var player = new Player(); +var ai = new AI(); +var ball = new Ball(); +var state = 0; +var prevScores = [0, 0]; + +function drawScores() { + let x1 = SCREEN_WIDTH/4-5; + let x2 = SCREEN_WIDTH*3/4-5; + + g.setColor(0); + g.setFont('Vector', 20); + g.drawString(prevScores[0], x1, 7); + g.drawString(prevScores[1], x2, 7); + g.setColor(-1); + g.setFont('Vector', 20); + g.drawString(scores[0], x1, 7); + g.drawString(scores[1], x2, 7); + prevScores = scores.slice(); +} + +function drawGameOver() { + g.setFont("Vector", 20); + g.drawString(winnerMessage, 75, SCREEN_WIDTH/2 - 10); +} + +function draw() { + if (state === 1) { + ball.update(); + player.update(); + ai.update(); + ball.show(); + player.show(); + ai.show(); + net(); + ball.show(); + } else if (state === 3) { + g.clear(); + g.setColor(0); + g.fillRect(0,0,240,240); + state++; + } else if (state === 4) { + drawGameOver(); + } else { + player.show(); + ai.show(); + net(); + } + drawScores(); +} + +g.clear(); +g.setColor(0); +g.fillRect(0,0,240,240); + +setInterval(draw, 1000 / FPS); + +setWatch(o => o.state ? player.up() : player.stop(), BTN1, {repeat: true, edge: 'both'}); +setWatch(o => o.state ? player.down() : player.stop(), BTN3, {repeat: true, edge: 'both'}); +//setWatch(o => o.state ? player.down() : player.stop(), BTN5, {repeat: true, edge: 'both'}); +setWatch(o => { + state++; + if (state >= 2) { + ball.restart(); + g.setColor(0); + g.fillRect(0,0,240,240); + scores = [0, 0]; + state = 1; + } +}, BTN2, {repeat: true}); diff --git a/apps/pong/pong.png b/apps/pong/pong.png new file mode 100644 index 0000000000000000000000000000000000000000..cc97f58f7826efaf0e2fafa1b7322a4d17d86f2c GIT binary patch literal 894 zcmV-^1A+XBP)fQkj&A4+ z8^a&FIPdd5`=9@LKc4dekPw)j2~5u*kvfUS5ju7aCH~Xa(S0~!hr{V`I8pjKq9KH! znX*PhOQWF$i4+<`YPCwORw0qfyG4B(iOThBSCQ!C@wPuAgMA0;}Q^Gn(3nfI?o>1%Xt0A&)LYqt!7?yz6xjKWMe}S#5neaHCKt872;E z#Khzj7th(EcDr2*!9@VT-|cOJIRCcy9gMG1^eROoF?1NlyZQ zrf1JvxtK<)(`a>h;tdpdVLXLIg^9C%zyJI45)!pS1Yu!B^MH0yMk3KcW+<0c@t9*$ zI02g^DI*AJdbi89^M0dL3V%p&xtt^^0|3x8#g7XH_oLC!0zXtF-^WQ(_B$3!2w^l? zYU}C+yAp|nQmGpF@Nqp9PB|A0lB8@s6c+r|*41;#Z5`cE4K{bSJwZstIR2)`8jVD_ zJQ0RHe)JFkfMG=dfYujne^{35JtL7kxPK2Gi^cfFl>g?9@)TPHAuTH{ne+uVH-FO9 zp3z!f+V>`ttV7XAq`9eqYbup0Xb3Xqal`3@7)=)LI2L18R#w+TVTD|tK26?nFJCqz zNkbL4Dn!^zI1C~TlSm}lt9ZvgVHjpKTSiBQ?e_lk1fA~grOQ{iX29>`Uml;Nq2jZW z;;Kr1Unso6vT-Og zb*`**@!Jy1vPDH$N@BC2!Dj7+$DLiB2k2ybFT-)1zxhR2>M0?bJJoKFr>wLj6yD&+ zX`0Hi3S?_&U?8if3!0`Ba=FoL;fji0U!&L8@N>jsJcFUNwIBAup@82fm=W;%Oy;M8 zT`rds$MLz@Jx|j#O^uEW&&|$2S;0Sk_Ks(|wA>+37<0?7t*z;=v-vw<`2_&rA3^G( Uj#*TE-v9sr07*qoM6N<$g3p|vv;Y7A literal 0 HcmV?d00001 From 6f00f0ee0b25d504a69be770ed70d3af0bb7d830 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Tue, 21 Apr 2020 11:35:32 +0100 Subject: [PATCH 209/302] minor tweaks --- apps/minionclk/app.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/minionclk/app.js b/apps/minionclk/app.js index 0725f8fa6..3453f49e1 100755 --- a/apps/minionclk/app.js +++ b/apps/minionclk/app.js @@ -1,5 +1,3 @@ -const bob = require("heatshrink").decompress(atob("nk8hAaXlYLWAEsqvN/0gBBql5lQ2tquj1XV5wBJ52j0hACPsdP1QsBAQQAGBIIBF51/P8OkN5R1GIxF5HLmAFgoDLPZfOpzmZ6vPFwomCPaA6DAYOjeq2A1YyCdI4HGQJQ8F1T2SJ4Oq1XW1es1mtAQOrPoPUAIh3J54ZHIAR5S62s64cBwIBGQIOqHQK4HKQYVDAAIFC1g+BHh9VHAQAFDwQDDHoJ5E54BB6AaBKQ5YGqo6MwJzGHQ4BDeIj/BR4JxDABY8BvI6OOYgaEHwZADHgQ6BZA42GAIusPJNW64eFqzJDlcrERA8BHQI2FqwaBDYYGBPI45GCoIgCLoVWQ5NWXA2rKhaiGLAwOGEAmADxJPDVA51ElQaMC4ouEWALdEHRg8Dc4woCDJo8EAIYxCHQQIFHiwaRegJ5EcYWsHgbrKbBA8GDSrNDO4wfRKgR3FDSh3CN4UrdwZbSHYZ5DHajMFHYQGCHalWO4jtQDQwABwAGCAAQfTKoK0EHahwCeARdFHakASIZWVZ4Q8CO4YgWO4QbCO6hWGEIKYZKzZ3DLog7UG4I6C1lWDSdWO4bpCO4bwUwKYEHajMDwOAlUkLojTUd4gaTZoRWC0YIB1eJLqo4EWiqRE0mjlcr1QkEeKFWOooBCHiB2CC4WA5wzB52rEQgfPHQwABAYJXOHQ2iO4XO6omFEJh1BEAgBGPJlWDIQbC0ej50qgHV1XPEwohKcwRbEvJ3EBQTrLFomkOwOjlR3C5w8GMAR8ClYuBLIgOCvN4HgIZFDQYbBlaOBR4YNCwA5B0XOpzvBHgWqTw4AFxB1EvQ6BAAI8GDZILEdgQBCqp3DPIRfIEQwABvJ1CvGkvGiwA6IAYoBCv6wCAAVOlQ6DAIWkL5ABEwN40Z1CAYJfCv7zEAJNWOYZ3KAIWq5yYHFYLOBLIrVCAIh7BOIzpECoYDDpw7BHAQDG1WqwGkAIN/CwIABLY4LDAAZ9BwAABvLCBC5IBBvOrO44BEAAmjAIPN0XOAJyHIAAgPEquBHBi4BAId+HwWiHxqJDRpYBDq2I1R3LIAQ6BAAOpPogABGgIDDOomk0nP5pIGd42Aq1ewI8CeZI6CHAJhCEAIDCdIo2B0er1esAQIZBC4Z9JlVW1leeKGp0es5+s6+s6ABB0oFBAIervWr0ultr7OqtWmdexJ4BGxB5G0V6pF6wItB0t6p9PvQABvINBttJttPzwXCHQwBDlY6BO4WIwPPPJbvDvA0BFgNtAAIDGtwGCzrtHdYh1DAIOBrzxBPIgAIeIXNHgoBCGwYADzoVB0fNOpMOHQdW5+Bro7BHgQAB6jxJAAOjOYhxCAIukOoPN5ujdZFWqyyD0d6AwUrquBwAuB1I1FHgRJBMoQ9BWg1tzw0BDYI6B0R3DAAJ1BvMOp8rAAV4IQWBIodewAeCHAZ5IAAJoBAAXHHAJWDO4TtEdYQvEHgejAwIKClcqIQRdDXYbzFeoQBIGwIDDOot/VgQ6FAAIGBlgBCAAMzmZPBF4LzDACB1FAAOi1WjvFVr0zGYQ7GAAMAAYpPBwNeAIOIwOrfYOA1eA1WkAIWjAIekv4PBwCVBruBq5eBEYIABlcBF4wCEHw8zgAAiFYivEPgoSEAYo9jGgY4EO5Q7kVwiyFGggBFhASBHkhsKAAcqqtOAgMzd8o6HO495vF5QgMzrw7lhBoBvMqG4g")); - const locale = require("locale"); const black = 0x0000; @@ -52,15 +50,17 @@ function startDrawing() { hour = ''; minute = ''; date = ''; + var bob = require("heatshrink").decompress(atob("nk8hAaXlYLWAEsqvN/0gBBql5lQ2tquj1XV5wBJ52j0hACPsdP1QsBAQQAGBIIBF51/P8OkN5R1GIxF5HLmAFgoDLPZfOpzmZ6vPFwomCPaA6DAYOjeq2A1YyCdI4HGQJQ8F1T2SJ4Oq1XW1es1mtAQOrPoPUAIh3J54ZHIAR5S62s64cBwIBGQIOqHQK4HKQYVDAAIFC1g+BHh9VHAQAFDwQDDHoJ5E54BB6AaBKQ5YGqo6MwJzGHQ4BDeIj/BR4JxDABY8BvI6OOYgaEHwZADHgQ6BZA42GAIusPJNW64eFqzJDlcrERA8BHQI2FqwaBDYYGBPI45GCoIgCLoVWQ5NWXA2rKhaiGLAwOGEAmADxJPDVA51ElQaMC4ouEWALdEHRg8Dc4woCDJo8EAIYxCHQQIFHiwaRegJ5EcYWsHgbrKbBA8GDSrNDO4wfRKgR3FDSh3CN4UrdwZbSHYZ5DHajMFHYQGCHalWO4jtQDQwABwAGCAAQfTKoK0EHahwCeARdFHakASIZWVZ4Q8CO4YgWO4QbCO6hWGEIKYZKzZ3DLog7UG4I6C1lWDSdWO4bpCO4bwUwKYEHajMDwOAlUkLojTUd4gaTZoRWC0YIB1eJLqo4EWiqRE0mjlcr1QkEeKFWOooBCHiB2CC4WA5wzB52rEQgfPHQwABAYJXOHQ2iO4XO6omFEJh1BEAgBGPJlWDIQbC0ej50qgHV1XPEwohKcwRbEvJ3EBQTrLFomkOwOjlR3C5w8GMAR8ClYuBLIgOCvN4HgIZFDQYbBlaOBR4YNCwA5B0XOpzvBHgWqTw4AFxB1EvQ6BAAI8GDZILEdgQBCqp3DPIRfIEQwABvJ1CvGkvGiwA6IAYoBCv6wCAAVOlQ6DAIWkL5ABEwN40Z1CAYJfCv7zEAJNWOYZ3KAIWq5yYHFYLOBLIrVCAIh7BOIzpECoYDDpw7BHAQDG1WqwGkAIN/CwIABLY4LDAAZ9BwAABvLCBC5IBBvOrO44BEAAmjAIPN0XOAJyHIAAgPEquBHBi4BAId+HwWiHxqJDRpYBDq2I1R3LIAQ6BAAOpPogABGgIDDOomk0nP5pIGd42Aq1ewI8CeZI6CHAJhCEAIDCdIo2B0er1esAQIZBC4Z9JlVW1leeKGp0es5+s6+s6ABB0oFBAIervWr0ultr7OqtWmdexJ4BGxB5G0V6pF6wItB0t6p9PvQABvINBttJttPzwXCHQwBDlY6BO4WIwPPPJbvDvA0BFgNtAAIDGtwGCzrtHdYh1DAIOBrzxBPIgAIeIXNHgoBCGwYADzoVB0fNOpMOHQdW5+Bro7BHgQAB6jxJAAOjOYhxCAIukOoPN5ujdZFWqyyD0d6AwUrquBwAuB1I1FHgRJBMoQ9BWg1tzw0BDYI6B0R3DAAJ1BvMOp8rAAV4IQWBIodewAeCHAZ5IAAJoBAAXHHAJWDO4TtEdYQvEHgejAwIKClcqIQRdDXYbzFeoQBIGwIDDOot/VgQ6FAAIGBlgBCAAMzmZPBF4LzDACB1FAAOi1WjvFVr0zGYQ7GAAMAAYpPBwNeAIOIwOrfYOA1eA1WkAIWjAIekv4PBwCVBruBq5eBEYIABlcBF4wCEHw8zgAAiFYivEPgoSEAYo9jGgY4EO5Q7kVwiyFGggBFhASBHkhsKAAcqqtOAgMzd8o6HO495vF5QgMzrw7lhBoBvMqG4g")); g.drawImage(bob, 0, 0, { scale: 4 }); Bangle.drawWidgets(); draw(); - setInterval(draw, 1000); + timer = setInterval(draw, 1000); } function stopDrawing() { if (timer) { clearInterval(timer); + timer = undefined; } } From 4a53e768d60837f2554e0b1935529aadc16e8662 Mon Sep 17 00:00:00 2001 From: RenaudG <34276398+renaudgweb@users.noreply.github.com> Date: Tue, 21 Apr 2020 14:03:17 +0200 Subject: [PATCH 210/302] change img_nofix --- apps/osmpoi/osmpoi.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/osmpoi/osmpoi.html b/apps/osmpoi/osmpoi.html index 56a1f8eb6..e0275784e 100644 --- a/apps/osmpoi/osmpoi.html +++ b/apps/osmpoi/osmpoi.html @@ -141,7 +141,7 @@ }); document.getElementById("upload").addEventListener("click", function() { var app = `var features = ${JSON.stringify(features)}; -var img_nofix = require("heatshrink").decompress(atob("mEwxH+AH4A0PgYurg9kr0rGM4nBg9dsgADmUHGUYtHAAddGIJcgFpIxEMTsAlYtMAAZiaLh4AFmQwXLiSTaLiosBnMymUrGCYTBAAgvPCgjwaMh4raF/4v/F/4vUg4vulZgDgAAIF8EyEQUAh0cAAkVisHGA4+HF6gVBiwwFjkONo0HAAMOAAIvTnIhEiovMFQQADNgYvQroUDg4uGjj9EF448DF6a+HAAS+EF5CQCF6SMIeAQvFXYYwHF59eLpSOBF4hgKGAIvPskAFxKOFF80VM4KOFSBs5F6EWRY4xBF43+F5UyF6DuEizZBKwIuHSBQvXdIwvKMYsHlYvQW4IuPYAYRBMggvRgIvCFxwwCTYZlEg4vPlcHjkVFx6WJF6YuXMoTwCF58yVgIvXY4YvQrqqDGDAvvPYIvPsguaYQYv/F7owBF/4vfg4AGTIIAHF7gA/AH4AwA=")); +var img_nofix = require("heatshrink").decompress(atob("mEwxH+6+s6/+AB4TB1gGFq1WlYABAgOBEQQnH68rlQRBD4orGwIkDleBAwQoBEgIABCAY4CgAjGwNbw1bmJFCDYIrGB4OwAIUAGIIrDCIICBA4WBlcxquBF4uslex2AABGYgrE2GxB4WAFoY8ClS0BPwJLCBgUAqyAGlYuCAAmGxgqDF4ZdBEIJSBHoWGwACBAANbTwRqBAYIwFqxTCAAmMF4+GlSDBq0qqotEGYQxDlVWGATBFYAJgIFYIvEJoSKBE4gADxgFESgIwC67AFF51VDQKMBFxAAErg0BGATQBYBy7D2OrlRJBgCGFLgVcGZEqOoKRFYBOGF4WymIXCqovCAQmwrYDBxgJGI4RgEYBQvBGAKlBUILnHdQaPEYQhgGwMxBgQuDRQIvCwCnBqwQCEwY/BFQeAFgQvEmJJBwOBR4dcLIQAD2J6BF4KODEAQhBwA0CSwoMBBAIxClcASAgeCRo2rDIICBOQKOBWoTLBFYYDBrdVG4NVFgK/EDQIrBF4IeCAApbBEgOydwIvBmJuFAAYJBFwIVBAAeMrlWRoIvDwNVF5AeB62xUgVVBAIsGPIRpBHoovBFwIvEXwYvHd4SPDb4ThEAob4DZoQvBDAIvEXwQkBFwpIBAQKPDCILeBwGAEYQnCdQIGDGAcq69WX4YdBKgQAE2GM1YDB2NVIgS0CABFcXwZnDFQJ6Cqy+HMQZeCGoLWBI4KDDAA46EF4VVC4LaCwK+BwBaDAAi+DAgIVCMAJeJAooGBXwusF4JMBQwLAFA4srCwVVCgK4DAQOwrYDErgQBFQJIBAQSPBra3BGIL/CF4ICBA4QoBMAhXCYg2AFwI0CR4MqI4NWRwLvDLQQAB1erD4IuBM4cxDAUxLYKDCFIdVNYT0DmJeF/x8BRgoABdwQAEmLZBGAMrQAIoBLIoAEFwi9BAATwDE4gwBAwlVDIIACq0qFwYRBLQZqClRDCbAIuDYARbCMAgZCqqnBFwgABYYMxBoIwBMAZqBFQJdCqwuEYAQsCFwexrcACYMqgAtFEAIxCAAKGBAAYMEUwIvFM4OMFoY0Bw0AC4RGCLgokD1msG4OBAgIVDler2LtDYAi3FFwgACVQQuBCQOAEQNWFYJrEfgMrwBSCdwrACqouLa4QAB2DKBCIVbRosxagOx2SuDd4ovBmLSBFxRhCrYuBAASmBAAQ4DVoeG1eGqrAGq1cCQLqBFxPXP4ImBOIYADAoOMFoIrBBAWwmKPFP4LKBLpYvCmIlBaQheDNA4ABd4z7B2OAFxmsgBVCF4xbBFo4SBla+GrZdNFwL/BWoYvHAwruCXw8xFyBeIFAIKDFoYABXw3+lYuRF44lCBQIEDXxWBFyQvJFwa+MEAIuNqx7EF4oqC2OrFwy+HqxdOFoewwAvBGwjuHAAWMla+FAwIuKlVb2OMwFVlcrqwCBqozCMAeML4WGrgTDXwsrL5IuBldbC4WBIIKmCwIzFAAI/ECYi/GMA4uBlQXKDQYzCIQITNAAOBGAwuBNIIXLGYwTRGAouCFqIAVGAJyBwIupGATRBewIrmA==")); var img_fix = require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AG2JnYv/FzovBGFgvuFwIvCGFQvuFwQvDGFAvuFwYvEGEwvuFwgvFGEgvuFwovGGEQvuFwwvHGEAvuFw4vIGDwvuFxAvJGDgvuFxIvKGDQvuFxQvLGDAvuFxYvMGCwvuFxgvNGCgvuFxovOGCQvuFxwvPGCAvuFx4vQGBwtOCQYvbFRwAJGCwqTGh4uMFS40LEcIA/AH4A/ACQA=")); // https://github.com/Leaflet/Leaflet/blob/master/src/geo/projection/Projection.SphericalMercator.js From 2814e367c0dda1cb3787f1f151416372d16f5c65 Mon Sep 17 00:00:00 2001 From: RenaudG <34276398+renaudgweb@users.noreply.github.com> Date: Tue, 21 Apr 2020 14:09:03 +0200 Subject: [PATCH 211/302] Update ChangeLog --- apps/osmpoi/ChangeLog | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/osmpoi/ChangeLog b/apps/osmpoi/ChangeLog index 5560f00bc..8e73a192e 100644 --- a/apps/osmpoi/ChangeLog +++ b/apps/osmpoi/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Change img when no fix From 25f2e3d99732017e64b86e3dfcca212d19f3684a Mon Sep 17 00:00:00 2001 From: RenaudG <34276398+renaudgweb@users.noreply.github.com> Date: Tue, 21 Apr 2020 14:32:55 +0200 Subject: [PATCH 212/302] Update osmpoi.html --- apps/osmpoi/osmpoi.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/osmpoi/osmpoi.html b/apps/osmpoi/osmpoi.html index e0275784e..b4a954c24 100644 --- a/apps/osmpoi/osmpoi.html +++ b/apps/osmpoi/osmpoi.html @@ -142,7 +142,7 @@ document.getElementById("upload").addEventListener("click", function() { var app = `var features = ${JSON.stringify(features)}; var img_nofix = require("heatshrink").decompress(atob("mEwxH+6+s6/+AB4TB1gGFq1WlYABAgOBEQQnH68rlQRBD4orGwIkDleBAwQoBEgIABCAY4CgAjGwNbw1bmJFCDYIrGB4OwAIUAGIIrDCIICBA4WBlcxquBF4uslex2AABGYgrE2GxB4WAFoY8ClS0BPwJLCBgUAqyAGlYuCAAmGxgqDF4ZdBEIJSBHoWGwACBAANbTwRqBAYIwFqxTCAAmMF4+GlSDBq0qqotEGYQxDlVWGATBFYAJgIFYIvEJoSKBE4gADxgFESgIwC67AFF51VDQKMBFxAAErg0BGATQBYBy7D2OrlRJBgCGFLgVcGZEqOoKRFYBOGF4WymIXCqovCAQmwrYDBxgJGI4RgEYBQvBGAKlBUILnHdQaPEYQhgGwMxBgQuDRQIvCwCnBqwQCEwY/BFQeAFgQvEmJJBwOBR4dcLIQAD2J6BF4KODEAQhBwA0CSwoMBBAIxClcASAgeCRo2rDIICBOQKOBWoTLBFYYDBrdVG4NVFgK/EDQIrBF4IeCAApbBEgOydwIvBmJuFAAYJBFwIVBAAeMrlWRoIvDwNVF5AeB62xUgVVBAIsGPIRpBHoovBFwIvEXwYvHd4SPDb4ThEAob4DZoQvBDAIvEXwQkBFwpIBAQKPDCILeBwGAEYQnCdQIGDGAcq69WX4YdBKgQAE2GM1YDB2NVIgS0CABFcXwZnDFQJ6Cqy+HMQZeCGoLWBI4KDDAA46EF4VVC4LaCwK+BwBaDAAi+DAgIVCMAJeJAooGBXwusF4JMBQwLAFA4srCwVVCgK4DAQOwrYDErgQBFQJIBAQSPBra3BGIL/CF4ICBA4QoBMAhXCYg2AFwI0CR4MqI4NWRwLvDLQQAB1erD4IuBM4cxDAUxLYKDCFIdVNYT0DmJeF/x8BRgoABdwQAEmLZBGAMrQAIoBLIoAEFwi9BAATwDE4gwBAwlVDIIACq0qFwYRBLQZqClRDCbAIuDYARbCMAgZCqqnBFwgABYYMxBoIwBMAZqBFQJdCqwuEYAQsCFwexrcACYMqgAtFEAIxCAAKGBAAYMEUwIvFM4OMFoY0Bw0AC4RGCLgokD1msG4OBAgIVDler2LtDYAi3FFwgACVQQuBCQOAEQNWFYJrEfgMrwBSCdwrACqouLa4QAB2DKBCIVbRosxagOx2SuDd4ovBmLSBFxRhCrYuBAASmBAAQ4DVoeG1eGqrAGq1cCQLqBFxPXP4ImBOIYADAoOMFoIrBBAWwmKPFP4LKBLpYvCmIlBaQheDNA4ABd4z7B2OAFxmsgBVCF4xbBFo4SBla+GrZdNFwL/BWoYvHAwruCXw8xFyBeIFAIKDFoYABXw3+lYuRF44lCBQIEDXxWBFyQvJFwa+MEAIuNqx7EF4oqC2OrFwy+HqxdOFoewwAvBGwjuHAAWMla+FAwIuKlVb2OMwFVlcrqwCBqozCMAeML4WGrgTDXwsrL5IuBldbC4WBIIKmCwIzFAAI/ECYi/GMA4uBlQXKDQYzCIQITNAAOBGAwuBNIIXLGYwTRGAouCFqIAVGAJyBwIupGATRBewIrmA==")); -var img_fix = require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AG2JnYv/FzovBGFgvuFwIvCGFQvuFwQvDGFAvuFwYvEGEwvuFwgvFGEgvuFwovGGEQvuFwwvHGEAvuFw4vIGDwvuFxAvJGDgvuFxIvKGDQvuFxQvLGDAvuFxYvMGCwvuFxgvNGCgvuFxovOGCQvuFxwvPGCAvuFx4vQGBwtOCQYvbFRwAJGCwqTGh4uMFS40LEcIA/AH4A/ACQA=")); +var img_fix = require("heatshrink").decompress(atob("mEwxH+6+s6/+AB4TB1gGFq1WlYABAgOBEQQnH68rlQRBD4orGwIkDleBAwQoBEgIABCAY4CgAjGwNbw1bmJFCDYIrGB4OwAIUAGIIrDCIICBA4WBlcxquBF4uslex2AABGYgrE2GxB4WAFoY8ClS0BPwJLCBgUAqyAGlYuCAAmGxgqDF4ZdBEIJSBHoWGwACBAANbTwRqBAYIwFqxTCAAmMF4+GlSDBq0qqotEGYQxDlVWGATBFYAJgIFYIvEJoSKBE4gADxgFESgIwC67AFF51VDQKMBFxAAErg0BGATQBYBy7D2OrlRJBgCGFLgVcGZEqOoKRFYBOGF4WymIXCqovCAQmwrYDBxgJGI4RgEYBQvBGAKlBUILnHdQaPEYQhgGwMxBgQuDRQIvCwCnBqwQCEwY/BFQeAFgQvEmJJBwOBR4dcLIQAD2J6BF4KODEAQhBwA0CSwoMBBAIxClcASAgeCRo2rDIICBOQKOBWoTLBFYYDBrdVG4NVFgK/EDQIrBF4IeCAApbBEgOydwIvBmJuFAAYJBFwIVBAAeMrlWRoIvDwNVF5AeB62xUgVVBAIsGPIRpBHoovBFwIvEXwYvHd4SPDb4ThEAob4DZoQvBDAIvEXwQkBFwpIBAQKPDCILeBwGAEYQnCdQIGDGAcq69WX4YdBKgQAE2GM1YDB2NVIgS0CABFcXwZnDFQJ6Cqy+HMQZeCGoLWBI4KDDAA46EF4VVC4LaCwK+BwBaDAAi+DAgIVCMAJeJAooGBXwusF4JMBQwLAFA4srCwVVCgK4DAQOwrYDErgQBFQJIBAQSPBra3BGIL/CF4ICBA4QoBMAhXCYg2AFwI0CR4MqI4NWRwLvDLQQAB1erD4IuBM4cxDAUxLYKDCFIdVNYT0DmJeF/x8BRgoABdwQAEmLZBGAMrQAIoBLIoAEFwi9BAATwDE4gwBAwlVDIIACq0qFwYRBLQZqClRDCbAIuDYARbCMAgZCqqnBFwgABYYMxBoIwBMAZqBFQJdCqwuEYAQsCFwexrcACYMqgAtFEAIxCAAKGBAAYMEUwIvFM4OMFoY0Bw0AC4RGCLgokD1msG4OBAgIVDler2LtDYAi3FFwgACVQQuBCQOAEQNWFYJrEfgMrwBSCdwrACqouLa4QAB2DKBCIVbRosxagOx2SuDd4ovBmLSBFxRhCrYuBAASmBAAQ4DVoeG1eGqrAGq1cCQLqBFxPXP4ImBOIYADAoOMFoIrBBAWwmKPFP4LKBLpYvCmIlBaQheDNA4ABd4z7B2OAFxmsgBVCF4xbBFo4SBla+GrZdNFwL/BWoYvHAwruCXw8xFyBeIFAIKDFoYABXw3+lYuRF44lCBQIEDXxWBFyQvJFwa+MEAIuNqx7EF4oqC2OrFwy+HqxdOFoewwAvBGwjuHAAWMla+FAwIuKlVb2OMwFVlcrqwCBqozCMAeML4WGrgTDXwsrL5IuBldbC4WBIIKmCwIzFAAI/ECYi/GMA4uBlQXKDQYzCIQITNAAOBGAwuBNIIXLGYwTRGAouCFqIAVGAJyBwIupGATRBewIrmA==")); // https://github.com/Leaflet/Leaflet/blob/master/src/geo/projection/Projection.SphericalMercator.js function project(latlong) { From c23c11eb51b9411772a52e048761c779a89bbdb9 Mon Sep 17 00:00:00 2001 From: RenaudG <34276398+renaudgweb@users.noreply.github.com> Date: Tue, 21 Apr 2020 14:50:44 +0200 Subject: [PATCH 213/302] Update osmpoi.html --- apps/osmpoi/osmpoi.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/osmpoi/osmpoi.html b/apps/osmpoi/osmpoi.html index b4a954c24..fa22bead5 100644 --- a/apps/osmpoi/osmpoi.html +++ b/apps/osmpoi/osmpoi.html @@ -141,8 +141,8 @@ }); document.getElementById("upload").addEventListener("click", function() { var app = `var features = ${JSON.stringify(features)}; -var img_nofix = require("heatshrink").decompress(atob("mEwxH+6+s6/+AB4TB1gGFq1WlYABAgOBEQQnH68rlQRBD4orGwIkDleBAwQoBEgIABCAY4CgAjGwNbw1bmJFCDYIrGB4OwAIUAGIIrDCIICBA4WBlcxquBF4uslex2AABGYgrE2GxB4WAFoY8ClS0BPwJLCBgUAqyAGlYuCAAmGxgqDF4ZdBEIJSBHoWGwACBAANbTwRqBAYIwFqxTCAAmMF4+GlSDBq0qqotEGYQxDlVWGATBFYAJgIFYIvEJoSKBE4gADxgFESgIwC67AFF51VDQKMBFxAAErg0BGATQBYBy7D2OrlRJBgCGFLgVcGZEqOoKRFYBOGF4WymIXCqovCAQmwrYDBxgJGI4RgEYBQvBGAKlBUILnHdQaPEYQhgGwMxBgQuDRQIvCwCnBqwQCEwY/BFQeAFgQvEmJJBwOBR4dcLIQAD2J6BF4KODEAQhBwA0CSwoMBBAIxClcASAgeCRo2rDIICBOQKOBWoTLBFYYDBrdVG4NVFgK/EDQIrBF4IeCAApbBEgOydwIvBmJuFAAYJBFwIVBAAeMrlWRoIvDwNVF5AeB62xUgVVBAIsGPIRpBHoovBFwIvEXwYvHd4SPDb4ThEAob4DZoQvBDAIvEXwQkBFwpIBAQKPDCILeBwGAEYQnCdQIGDGAcq69WX4YdBKgQAE2GM1YDB2NVIgS0CABFcXwZnDFQJ6Cqy+HMQZeCGoLWBI4KDDAA46EF4VVC4LaCwK+BwBaDAAi+DAgIVCMAJeJAooGBXwusF4JMBQwLAFA4srCwVVCgK4DAQOwrYDErgQBFQJIBAQSPBra3BGIL/CF4ICBA4QoBMAhXCYg2AFwI0CR4MqI4NWRwLvDLQQAB1erD4IuBM4cxDAUxLYKDCFIdVNYT0DmJeF/x8BRgoABdwQAEmLZBGAMrQAIoBLIoAEFwi9BAATwDE4gwBAwlVDIIACq0qFwYRBLQZqClRDCbAIuDYARbCMAgZCqqnBFwgABYYMxBoIwBMAZqBFQJdCqwuEYAQsCFwexrcACYMqgAtFEAIxCAAKGBAAYMEUwIvFM4OMFoY0Bw0AC4RGCLgokD1msG4OBAgIVDler2LtDYAi3FFwgACVQQuBCQOAEQNWFYJrEfgMrwBSCdwrACqouLa4QAB2DKBCIVbRosxagOx2SuDd4ovBmLSBFxRhCrYuBAASmBAAQ4DVoeG1eGqrAGq1cCQLqBFxPXP4ImBOIYADAoOMFoIrBBAWwmKPFP4LKBLpYvCmIlBaQheDNA4ABd4z7B2OAFxmsgBVCF4xbBFo4SBla+GrZdNFwL/BWoYvHAwruCXw8xFyBeIFAIKDFoYABXw3+lYuRF44lCBQIEDXxWBFyQvJFwa+MEAIuNqx7EF4oqC2OrFwy+HqxdOFoewwAvBGwjuHAAWMla+FAwIuKlVb2OMwFVlcrqwCBqozCMAeML4WGrgTDXwsrL5IuBldbC4WBIIKmCwIzFAAI/ECYi/GMA4uBlQXKDQYzCIQITNAAOBGAwuBNIIXLGYwTRGAouCFqIAVGAJyBwIupGATRBewIrmA==")); -var img_fix = require("heatshrink").decompress(atob("mEwxH+6+s6/+AB4TB1gGFq1WlYABAgOBEQQnH68rlQRBD4orGwIkDleBAwQoBEgIABCAY4CgAjGwNbw1bmJFCDYIrGB4OwAIUAGIIrDCIICBA4WBlcxquBF4uslex2AABGYgrE2GxB4WAFoY8ClS0BPwJLCBgUAqyAGlYuCAAmGxgqDF4ZdBEIJSBHoWGwACBAANbTwRqBAYIwFqxTCAAmMF4+GlSDBq0qqotEGYQxDlVWGATBFYAJgIFYIvEJoSKBE4gADxgFESgIwC67AFF51VDQKMBFxAAErg0BGATQBYBy7D2OrlRJBgCGFLgVcGZEqOoKRFYBOGF4WymIXCqovCAQmwrYDBxgJGI4RgEYBQvBGAKlBUILnHdQaPEYQhgGwMxBgQuDRQIvCwCnBqwQCEwY/BFQeAFgQvEmJJBwOBR4dcLIQAD2J6BF4KODEAQhBwA0CSwoMBBAIxClcASAgeCRo2rDIICBOQKOBWoTLBFYYDBrdVG4NVFgK/EDQIrBF4IeCAApbBEgOydwIvBmJuFAAYJBFwIVBAAeMrlWRoIvDwNVF5AeB62xUgVVBAIsGPIRpBHoovBFwIvEXwYvHd4SPDb4ThEAob4DZoQvBDAIvEXwQkBFwpIBAQKPDCILeBwGAEYQnCdQIGDGAcq69WX4YdBKgQAE2GM1YDB2NVIgS0CABFcXwZnDFQJ6Cqy+HMQZeCGoLWBI4KDDAA46EF4VVC4LaCwK+BwBaDAAi+DAgIVCMAJeJAooGBXwusF4JMBQwLAFA4srCwVVCgK4DAQOwrYDErgQBFQJIBAQSPBra3BGIL/CF4ICBA4QoBMAhXCYg2AFwI0CR4MqI4NWRwLvDLQQAB1erD4IuBM4cxDAUxLYKDCFIdVNYT0DmJeF/x8BRgoABdwQAEmLZBGAMrQAIoBLIoAEFwi9BAATwDE4gwBAwlVDIIACq0qFwYRBLQZqClRDCbAIuDYARbCMAgZCqqnBFwgABYYMxBoIwBMAZqBFQJdCqwuEYAQsCFwexrcACYMqgAtFEAIxCAAKGBAAYMEUwIvFM4OMFoY0Bw0AC4RGCLgokD1msG4OBAgIVDler2LtDYAi3FFwgACVQQuBCQOAEQNWFYJrEfgMrwBSCdwrACqouLa4QAB2DKBCIVbRosxagOx2SuDd4ovBmLSBFxRhCrYuBAASmBAAQ4DVoeG1eGqrAGq1cCQLqBFxPXP4ImBOIYADAoOMFoIrBBAWwmKPFP4LKBLpYvCmIlBaQheDNA4ABd4z7B2OAFxmsgBVCF4xbBFo4SBla+GrZdNFwL/BWoYvHAwruCXw8xFyBeIFAIKDFoYABXw3+lYuRF44lCBQIEDXxWBFyQvJFwa+MEAIuNqx7EF4oqC2OrFwy+HqxdOFoewwAvBGwjuHAAWMla+FAwIuKlVb2OMwFVlcrqwCBqozCMAeML4WGrgTDXwsrL5IuBldbC4WBIIKmCwIzFAAI/ECYi/GMA4uBlQXKDQYzCIQITNAAOBGAwuBNIIXLGYwTRGAouCFqIAVGAJyBwIupGATRBewIrmA==")); +var img_nofix = require("heatshrink").decompress(atob("mEwxH+h8dj3+AB8eCYsejkYhEVAAMIjEcBwQTBh4bE/8ViUVCAgrIjMWifX68VjMHjETi8bHAMPCAIJChATBiMdEAsZxlH0nsD4IzDNYIrB6/WB4NGAIURicb///FgUcjhYBBAMHinsx0aF4sd61FowABGYOP68UiYrDo1FrwDB0nXjIkChETiURiMS60Tiw6CjMRhCAG64uCAAkao4qDAAIEBLoMH/8cinW0kZjOko4ACxgyBB4MdiccGAsWKYQAHF4tHiUd/8YiWOFAIwBjQCBAwICBxkSi//j0TYIrABFo44BF4aeB65NBjHXLAYACMQIWBo8aGIPWixhCcQLAEF5tex0YVoIuDjSgBGAYDD0gbB6xhBjbCEh7AIF4tGiUPjsRW4SIEfIIGDHIIFCiT1BiqREi0aGBDwCruPjX/imOxhTCX4YvBBIIzDBITDBI4MWF4caxovIrwwC68PjiNBFoaRCAQQIDAgiRBJALBEg+PBgQwFdYNe0jYBhGPcoJQBQYLpCMYQABF4aQCx8Ta4MaF4UYZoIZEGYZeBRwLQBP4WkEYIwBAoQJBGQg0D60Rj0deIUPihcGGoQ5C60cj3WWIKiBSIeMAYOkxwDBAQTJDiTuBh8VF4Me64tGLYIhBrtd9hEBx5uFX4ZCCAYR8EVAMaYAIvCg+OF5NGrpfBj0cx1FFwooCCoKsDXgQvCjMHjIvDi4RCdgteo9er1FF4Md6xUCXoQjBSYUZxhkBY4QMBJAMdjZfDXwJYCLwgGCAQKPBjsd69HxrhEdAQCDd4gwBiUPjEcX4UeiZXBAAlFRwQDBr2OjRBBL4SCDAAh9BY4T7DFQMVPQMWXxKnCr5hC0kI/8WxotBQoLuDF4YDCS4VHx0YI4P/UgK+BjRZCF4yOBeYUTh8bSAMaWoQnCFwQJESoS+BFgK+Bjv+izuCr7BFX4YAB60bh8Tx2NXAsZowGCAYLOBx2k68cicej0Vh/+jONLASjCGoRiDEgUT/8HMAIqBQgTEDXYOMxg7Dx0SLwMIg/+/0d6wmCeQIsCPAJkFx4YBi+PL4QkBAQONxmOLQIzCJAPsiv/jpeC/x8BK4YAEBA2Pi3/h8I62M64oBxmNRIJjCYIUZFwMPjyQBFwIABhDwBFwxfFxwZB/4wCiTDBXYaVBLQJqCiUWh8PiiNCAAUaxqNBGIYFBDIONx0ZFwgACjUT9iLBxsaXYJcBxvW68a/5dBjAuEYAgqBGQNFoukiMVKwMRFwkZEAMPjUV6/Wx/WAAUVjITBjcTx8IF4rABF4QtBAgMZiIkBIwUIFwfXV4McBgUcjQ4BjkeBALpBKgXXdoTAEjK3EFwMbLIceiUIh4uBXAOk60UjAqBNgQ0BjEU60aVwXWjovFjWOFxRhDiZMCfYTNB9gzBBgXsxwMBrqBBr2NjIvFjePo4QBFxBhDxgrBAAR1EHAbZBb4IGCxzwGi+kBYLqBFxAABjBfCFIY0EFAVfBQYGB9iPFh8UIAJdKF4fsEoZdGM47vCjwvEj3Xr0aFxkdiMZF5FGRIIAHo8VXw2No4uOf4QvHXAgAEoukXw+PFzIvFr1eeILyBXw3+iYuRLxIvDSQy+GjguSF44pCo9fFwy+GEAMHFxmlPYJUBr4vFBQLsIXxEIjJdNFgIlCjQvBGoRYDrzJI6y+FicdFxUSxgrB0mO68ThEVienGYS+EMYVHCYcXh4vEisaFxXXxnWFYMajoZCj0cjEU6+OGYIABFYgTEdwxgHFwMSFYoAHGYcSFZYAEjYwGFwMZC5gzGCaIwFFwQtRACowBjZ6BF1LDCisTikbFcwA==")); +var img_fix = require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AG1Omwv/FzovBGFgvuFwIvCGFQvuFwQvDGFAvuFwYvEGEwvuFwgvFGEgvuFwovGGEQvuFwwvHGEAvuFw4vIGDwvuFxAvJGDgvuFxIvKGDQvuFxQvLGDAvuFxYvMGCwvuFxgvNGCgvuFxovOGCQvuFxwvPGCAvuFx4vQGBwtOCQYvbFRwAJGCwpEprTPGhouLFTA0LEcIA/AH4A/ACQA=")); // https://github.com/Leaflet/Leaflet/blob/master/src/geo/projection/Projection.SphericalMercator.js function project(latlong) { From f938dc3de0933f3b81f9f25a7fff50e9ae57ac5f Mon Sep 17 00:00:00 2001 From: RenaudG <34276398+renaudgweb@users.noreply.github.com> Date: Tue, 21 Apr 2020 14:59:52 +0200 Subject: [PATCH 214/302] Update osmpoi.html --- apps/osmpoi/osmpoi.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/osmpoi/osmpoi.html b/apps/osmpoi/osmpoi.html index fa22bead5..e0275784e 100644 --- a/apps/osmpoi/osmpoi.html +++ b/apps/osmpoi/osmpoi.html @@ -141,8 +141,8 @@ }); document.getElementById("upload").addEventListener("click", function() { var app = `var features = ${JSON.stringify(features)}; -var img_nofix = require("heatshrink").decompress(atob("mEwxH+h8dj3+AB8eCYsejkYhEVAAMIjEcBwQTBh4bE/8ViUVCAgrIjMWifX68VjMHjETi8bHAMPCAIJChATBiMdEAsZxlH0nsD4IzDNYIrB6/WB4NGAIURicb///FgUcjhYBBAMHinsx0aF4sd61FowABGYOP68UiYrDo1FrwDB0nXjIkChETiURiMS60Tiw6CjMRhCAG64uCAAkao4qDAAIEBLoMH/8cinW0kZjOko4ACxgyBB4MdiccGAsWKYQAHF4tHiUd/8YiWOFAIwBjQCBAwICBxkSi//j0TYIrABFo44BF4aeB65NBjHXLAYACMQIWBo8aGIPWixhCcQLAEF5tex0YVoIuDjSgBGAYDD0gbB6xhBjbCEh7AIF4tGiUPjsRW4SIEfIIGDHIIFCiT1BiqREi0aGBDwCruPjX/imOxhTCX4YvBBIIzDBITDBI4MWF4caxovIrwwC68PjiNBFoaRCAQQIDAgiRBJALBEg+PBgQwFdYNe0jYBhGPcoJQBQYLpCMYQABF4aQCx8Ta4MaF4UYZoIZEGYZeBRwLQBP4WkEYIwBAoQJBGQg0D60Rj0deIUPihcGGoQ5C60cj3WWIKiBSIeMAYOkxwDBAQTJDiTuBh8VF4Me64tGLYIhBrtd9hEBx5uFX4ZCCAYR8EVAMaYAIvCg+OF5NGrpfBj0cx1FFwooCCoKsDXgQvCjMHjIvDi4RCdgteo9er1FF4Md6xUCXoQjBSYUZxhkBY4QMBJAMdjZfDXwJYCLwgGCAQKPBjsd69HxrhEdAQCDd4gwBiUPjEcX4UeiZXBAAlFRwQDBr2OjRBBL4SCDAAh9BY4T7DFQMVPQMWXxKnCr5hC0kI/8WxotBQoLuDF4YDCS4VHx0YI4P/UgK+BjRZCF4yOBeYUTh8bSAMaWoQnCFwQJESoS+BFgK+Bjv+izuCr7BFX4YAB60bh8Tx2NXAsZowGCAYLOBx2k68cicej0Vh/+jONLASjCGoRiDEgUT/8HMAIqBQgTEDXYOMxg7Dx0SLwMIg/+/0d6wmCeQIsCPAJkFx4YBi+PL4QkBAQONxmOLQIzCJAPsiv/jpeC/x8BK4YAEBA2Pi3/h8I62M64oBxmNRIJjCYIUZFwMPjyQBFwIABhDwBFwxfFxwZB/4wCiTDBXYaVBLQJqCiUWh8PiiNCAAUaxqNBGIYFBDIONx0ZFwgACjUT9iLBxsaXYJcBxvW68a/5dBjAuEYAgqBGQNFoukiMVKwMRFwkZEAMPjUV6/Wx/WAAUVjITBjcTx8IF4rABF4QtBAgMZiIkBIwUIFwfXV4McBgUcjQ4BjkeBALpBKgXXdoTAEjK3EFwMbLIceiUIh4uBXAOk60UjAqBNgQ0BjEU60aVwXWjovFjWOFxRhDiZMCfYTNB9gzBBgXsxwMBrqBBr2NjIvFjePo4QBFxBhDxgrBAAR1EHAbZBb4IGCxzwGi+kBYLqBFxAABjBfCFIY0EFAVfBQYGB9iPFh8UIAJdKF4fsEoZdGM47vCjwvEj3Xr0aFxkdiMZF5FGRIIAHo8VXw2No4uOf4QvHXAgAEoukXw+PFzIvFr1eeILyBXw3+iYuRLxIvDSQy+GjguSF44pCo9fFwy+GEAMHFxmlPYJUBr4vFBQLsIXxEIjJdNFgIlCjQvBGoRYDrzJI6y+FicdFxUSxgrB0mO68ThEVienGYS+EMYVHCYcXh4vEisaFxXXxnWFYMajoZCj0cjEU6+OGYIABFYgTEdwxgHFwMSFYoAHGYcSFZYAEjYwGFwMZC5gzGCaIwFFwQtRACowBjZ6BF1LDCisTikbFcwA==")); -var img_fix = require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AG1Omwv/FzovBGFgvuFwIvCGFQvuFwQvDGFAvuFwYvEGEwvuFwgvFGEgvuFwovGGEQvuFwwvHGEAvuFw4vIGDwvuFxAvJGDgvuFxIvKGDQvuFxQvLGDAvuFxYvMGCwvuFxgvNGCgvuFxovOGCQvuFxwvPGCAvuFx4vQGBwtOCQYvbFRwAJGCwpEprTPGhouLFTA0LEcIA/AH4A/ACQA=")); +var img_nofix = require("heatshrink").decompress(atob("mEwxH+6+s6/+AB4TB1gGFq1WlYABAgOBEQQnH68rlQRBD4orGwIkDleBAwQoBEgIABCAY4CgAjGwNbw1bmJFCDYIrGB4OwAIUAGIIrDCIICBA4WBlcxquBF4uslex2AABGYgrE2GxB4WAFoY8ClS0BPwJLCBgUAqyAGlYuCAAmGxgqDF4ZdBEIJSBHoWGwACBAANbTwRqBAYIwFqxTCAAmMF4+GlSDBq0qqotEGYQxDlVWGATBFYAJgIFYIvEJoSKBE4gADxgFESgIwC67AFF51VDQKMBFxAAErg0BGATQBYBy7D2OrlRJBgCGFLgVcGZEqOoKRFYBOGF4WymIXCqovCAQmwrYDBxgJGI4RgEYBQvBGAKlBUILnHdQaPEYQhgGwMxBgQuDRQIvCwCnBqwQCEwY/BFQeAFgQvEmJJBwOBR4dcLIQAD2J6BF4KODEAQhBwA0CSwoMBBAIxClcASAgeCRo2rDIICBOQKOBWoTLBFYYDBrdVG4NVFgK/EDQIrBF4IeCAApbBEgOydwIvBmJuFAAYJBFwIVBAAeMrlWRoIvDwNVF5AeB62xUgVVBAIsGPIRpBHoovBFwIvEXwYvHd4SPDb4ThEAob4DZoQvBDAIvEXwQkBFwpIBAQKPDCILeBwGAEYQnCdQIGDGAcq69WX4YdBKgQAE2GM1YDB2NVIgS0CABFcXwZnDFQJ6Cqy+HMQZeCGoLWBI4KDDAA46EF4VVC4LaCwK+BwBaDAAi+DAgIVCMAJeJAooGBXwusF4JMBQwLAFA4srCwVVCgK4DAQOwrYDErgQBFQJIBAQSPBra3BGIL/CF4ICBA4QoBMAhXCYg2AFwI0CR4MqI4NWRwLvDLQQAB1erD4IuBM4cxDAUxLYKDCFIdVNYT0DmJeF/x8BRgoABdwQAEmLZBGAMrQAIoBLIoAEFwi9BAATwDE4gwBAwlVDIIACq0qFwYRBLQZqClRDCbAIuDYARbCMAgZCqqnBFwgABYYMxBoIwBMAZqBFQJdCqwuEYAQsCFwexrcACYMqgAtFEAIxCAAKGBAAYMEUwIvFM4OMFoY0Bw0AC4RGCLgokD1msG4OBAgIVDler2LtDYAi3FFwgACVQQuBCQOAEQNWFYJrEfgMrwBSCdwrACqouLa4QAB2DKBCIVbRosxagOx2SuDd4ovBmLSBFxRhCrYuBAASmBAAQ4DVoeG1eGqrAGq1cCQLqBFxPXP4ImBOIYADAoOMFoIrBBAWwmKPFP4LKBLpYvCmIlBaQheDNA4ABd4z7B2OAFxmsgBVCF4xbBFo4SBla+GrZdNFwL/BWoYvHAwruCXw8xFyBeIFAIKDFoYABXw3+lYuRF44lCBQIEDXxWBFyQvJFwa+MEAIuNqx7EF4oqC2OrFwy+HqxdOFoewwAvBGwjuHAAWMla+FAwIuKlVb2OMwFVlcrqwCBqozCMAeML4WGrgTDXwsrL5IuBldbC4WBIIKmCwIzFAAI/ECYi/GMA4uBlQXKDQYzCIQITNAAOBGAwuBNIIXLGYwTRGAouCFqIAVGAJyBwIupGATRBewIrmA==")); +var img_fix = require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AG2JnYv/FzovBGFgvuFwIvCGFQvuFwQvDGFAvuFwYvEGEwvuFwgvFGEgvuFwovGGEQvuFwwvHGEAvuFw4vIGDwvuFxAvJGDgvuFxIvKGDQvuFxQvLGDAvuFxYvMGCwvuFxgvNGCgvuFxovOGCQvuFxwvPGCAvuFx4vQGBwtOCQYvbFRwAJGCwqTGh4uMFS40LEcIA/AH4A/ACQA=")); // https://github.com/Leaflet/Leaflet/blob/master/src/geo/projection/Projection.SphericalMercator.js function project(latlong) { From 9a64b8aa35a4108b1745c812dac119b0943290f8 Mon Sep 17 00:00:00 2001 From: RenaudG <34276398+renaudgweb@users.noreply.github.com> Date: Tue, 21 Apr 2020 15:17:57 +0200 Subject: [PATCH 215/302] Update osmpoi.html --- apps/osmpoi/osmpoi.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/osmpoi/osmpoi.html b/apps/osmpoi/osmpoi.html index e0275784e..3b1fc5ba8 100644 --- a/apps/osmpoi/osmpoi.html +++ b/apps/osmpoi/osmpoi.html @@ -141,8 +141,8 @@ }); document.getElementById("upload").addEventListener("click", function() { var app = `var features = ${JSON.stringify(features)}; -var img_nofix = require("heatshrink").decompress(atob("mEwxH+6+s6/+AB4TB1gGFq1WlYABAgOBEQQnH68rlQRBD4orGwIkDleBAwQoBEgIABCAY4CgAjGwNbw1bmJFCDYIrGB4OwAIUAGIIrDCIICBA4WBlcxquBF4uslex2AABGYgrE2GxB4WAFoY8ClS0BPwJLCBgUAqyAGlYuCAAmGxgqDF4ZdBEIJSBHoWGwACBAANbTwRqBAYIwFqxTCAAmMF4+GlSDBq0qqotEGYQxDlVWGATBFYAJgIFYIvEJoSKBE4gADxgFESgIwC67AFF51VDQKMBFxAAErg0BGATQBYBy7D2OrlRJBgCGFLgVcGZEqOoKRFYBOGF4WymIXCqovCAQmwrYDBxgJGI4RgEYBQvBGAKlBUILnHdQaPEYQhgGwMxBgQuDRQIvCwCnBqwQCEwY/BFQeAFgQvEmJJBwOBR4dcLIQAD2J6BF4KODEAQhBwA0CSwoMBBAIxClcASAgeCRo2rDIICBOQKOBWoTLBFYYDBrdVG4NVFgK/EDQIrBF4IeCAApbBEgOydwIvBmJuFAAYJBFwIVBAAeMrlWRoIvDwNVF5AeB62xUgVVBAIsGPIRpBHoovBFwIvEXwYvHd4SPDb4ThEAob4DZoQvBDAIvEXwQkBFwpIBAQKPDCILeBwGAEYQnCdQIGDGAcq69WX4YdBKgQAE2GM1YDB2NVIgS0CABFcXwZnDFQJ6Cqy+HMQZeCGoLWBI4KDDAA46EF4VVC4LaCwK+BwBaDAAi+DAgIVCMAJeJAooGBXwusF4JMBQwLAFA4srCwVVCgK4DAQOwrYDErgQBFQJIBAQSPBra3BGIL/CF4ICBA4QoBMAhXCYg2AFwI0CR4MqI4NWRwLvDLQQAB1erD4IuBM4cxDAUxLYKDCFIdVNYT0DmJeF/x8BRgoABdwQAEmLZBGAMrQAIoBLIoAEFwi9BAATwDE4gwBAwlVDIIACq0qFwYRBLQZqClRDCbAIuDYARbCMAgZCqqnBFwgABYYMxBoIwBMAZqBFQJdCqwuEYAQsCFwexrcACYMqgAtFEAIxCAAKGBAAYMEUwIvFM4OMFoY0Bw0AC4RGCLgokD1msG4OBAgIVDler2LtDYAi3FFwgACVQQuBCQOAEQNWFYJrEfgMrwBSCdwrACqouLa4QAB2DKBCIVbRosxagOx2SuDd4ovBmLSBFxRhCrYuBAASmBAAQ4DVoeG1eGqrAGq1cCQLqBFxPXP4ImBOIYADAoOMFoIrBBAWwmKPFP4LKBLpYvCmIlBaQheDNA4ABd4z7B2OAFxmsgBVCF4xbBFo4SBla+GrZdNFwL/BWoYvHAwruCXw8xFyBeIFAIKDFoYABXw3+lYuRF44lCBQIEDXxWBFyQvJFwa+MEAIuNqx7EF4oqC2OrFwy+HqxdOFoewwAvBGwjuHAAWMla+FAwIuKlVb2OMwFVlcrqwCBqozCMAeML4WGrgTDXwsrL5IuBldbC4WBIIKmCwIzFAAI/ECYi/GMA4uBlQXKDQYzCIQITNAAOBGAwuBNIIXLGYwTRGAouCFqIAVGAJyBwIupGATRBewIrmA==")); -var img_fix = require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AG2JnYv/FzovBGFgvuFwIvCGFQvuFwQvDGFAvuFwYvEGEwvuFwgvFGEgvuFwovGGEQvuFwwvHGEAvuFw4vIGDwvuFxAvJGDgvuFxIvKGDQvuFxQvLGDAvuFxYvMGCwvuFxgvNGCgvuFxovOGCQvuFxwvPGCAvuFx4vQGBwtOCQYvbFRwAJGCwqTGh4uMFS40LEcIA/AH4A/ACQA=")); +var img_nofix = require("heatshrink").decompress(atob("mEwhHX1nX/wAPCYOsAwtWq0rAAIEBwIiCE4/XlcqCIIfFFY2BEgcrwIGCFAIkBAAIQDHAUAEY2BreGrcxIoQbBFYwPB2ABCgAxBFYYRBAQIHCwMrmNVwIvF1kr2OwAAIzEFYmw2IPCwAtDHgUqWYJ+BJYQMCgFWQA0rFwQAEw2MFQYvDLoIhBKQI9Cw2AAQIABraeCNQIDBGAtWKYQAExgvHw0qQYNWlVVFogzCGIcqqwwCYIrABMBArBF4hNCRQInEAAeMAoiUBGAXXYAovOqoaBRgIuIAAlcGgIwCaALAOXYex1cqJIMAQwpcCrgzIlR1BSIrAJwwvC2UxC4VVF4QCE2FbAYOMBIxHCMAjAKF4IwBUoKhBc47qDR4jCEMA2BmIMCFwaKBF4WAU4NWCAQmDH4IqDwAsCF4kxJIOBwKPDrhZCAAexPQIvBRwYgCEIOAGgSWFBgIIBGIUrgCQEDwSNG1YZBAQJyBRwK1CZYIrDAYNbqo3BqosBX4gaBFYIvBDwQAFLYIkB2TuBF4MxNwoADBIIuBCoIADxlcqyNBF4eBqovIDwPW2KkCqoIBFgx5CNII9FF4IuBF4i+DF47vCR4bfCcIgFDfAbNCF4IYBF4i+CEgIuFJAICBR4YRBbwOAwAjCE4TqBAwYwDlXXqy/DDoJUCAAmwxmrAYOxqpECWgQAIri+DM4YqBPQVWXw5iDLwQ1BawJHBQYYAHHQgvCqoXBbQWBXwOALQYAEXwYEBCoRgBLxIFFAwK+F1gvBJgKGBYAoHFlYWCqoUBXAYCB2FbAYlcCAIqBJAICCR4NbW4IxBf4QvBAQIHCFAJgEK4TEGwAuBGgSPBlRHBqyOBd4ZaCAAOr1YfBFwJnDmIYCmJbBQYQpDqprCegcxLwv+PgKMFAALuCAAkxbIIwBlaABFAJZFAAguEXoIACeAYnEGAIGEqoZBAAVWlQuDCIJaDNQUqIYTYBFwbACLYRgEDIVVU4IuEAALDBmINBGAJgDNQIqBLoVWFwjACFgQuD2NbgATBlUAFoogBGIQABQwIADBgimBF4pnBxgtDGgOGgAXCIwRcFEges1g3BwIEBCocr1exdobAEW4ouEAASqCFwISBwAiBqwrBNYj8BleAKQTuFYAVVFxbXCAAOwZQIRCraNFmLUB2OyVwbvFF4MxaQIuKMIVbFwIACUwIACHAatDw2rw1VYA1WrgSBdQIuJ65/BEwJxDAAYFBxgtBFYIIC2ExR4p/BZQJdLF4UxEoLSELwZoHAALvGfYOxwAuM1kAKoQvGLYItHCQMrXw1bLpouBf4K1DF44GFdwS+HmIuQLxAoBBQYtDAAK+G/0rFyIvHEoQKBAga+KwIuSF5IuDXxggBFxtWPYgvFFQWx1YuGXw9WLpwtD2GAF4I2Edw4ACxkrXwoGBFxUqrexxmAqsrldWAQNVGYRgDxhfCw1cCYa+FlZfJFwMrrYXCwJBBUwWBGYoABH4gTEX4xgHFwMqC5QaDGYRCBCZoABwIwGFwJpBC5YzGCaIwFFwQtRACowBOQOBF1IwCaIL2BFcw")); +var img_fix = require("heatshrink").decompress(atob("mEwhH+AH4A/AH4A/AGuJnYv/FzovBGFgvuFwIvCGFQvuFwQvDGFAvuFwYvEGEwvuFwgvFGEgvuFwovGGEQvuFwwvHGEAvuFw4vIGDwvuFxAvJGDgvuFxIvKGDQvuFxQvLGDAvuFxYvMGCwvuFxgvNGCgvuFxovOGCQvuFxwvPGCAvuFx4vQGBwtOCQYvbFRwAJGCwqTGh4uMFS40LEcIA/AH4A/ACQA=")); // https://github.com/Leaflet/Leaflet/blob/master/src/geo/projection/Projection.SphericalMercator.js function project(latlong) { From b711e10d4f09293bb9a9e0bbe615777268cd1321 Mon Sep 17 00:00:00 2001 From: RenaudG <34276398+renaudgweb@users.noreply.github.com> Date: Tue, 21 Apr 2020 15:31:26 +0200 Subject: [PATCH 216/302] Update osmpoi.html --- apps/osmpoi/osmpoi.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/osmpoi/osmpoi.html b/apps/osmpoi/osmpoi.html index 3b1fc5ba8..58a182721 100644 --- a/apps/osmpoi/osmpoi.html +++ b/apps/osmpoi/osmpoi.html @@ -141,8 +141,8 @@ }); document.getElementById("upload").addEventListener("click", function() { var app = `var features = ${JSON.stringify(features)}; -var img_nofix = require("heatshrink").decompress(atob("mEwhHX1nX/wAPCYOsAwtWq0rAAIEBwIiCE4/XlcqCIIfFFY2BEgcrwIGCFAIkBAAIQDHAUAEY2BreGrcxIoQbBFYwPB2ABCgAxBFYYRBAQIHCwMrmNVwIvF1kr2OwAAIzEFYmw2IPCwAtDHgUqWYJ+BJYQMCgFWQA0rFwQAEw2MFQYvDLoIhBKQI9Cw2AAQIABraeCNQIDBGAtWKYQAExgvHw0qQYNWlVVFogzCGIcqqwwCYIrABMBArBF4hNCRQInEAAeMAoiUBGAXXYAovOqoaBRgIuIAAlcGgIwCaALAOXYex1cqJIMAQwpcCrgzIlR1BSIrAJwwvC2UxC4VVF4QCE2FbAYOMBIxHCMAjAKF4IwBUoKhBc47qDR4jCEMA2BmIMCFwaKBF4WAU4NWCAQmDH4IqDwAsCF4kxJIOBwKPDrhZCAAexPQIvBRwYgCEIOAGgSWFBgIIBGIUrgCQEDwSNG1YZBAQJyBRwK1CZYIrDAYNbqo3BqosBX4gaBFYIvBDwQAFLYIkB2TuBF4MxNwoADBIIuBCoIADxlcqyNBF4eBqovIDwPW2KkCqoIBFgx5CNII9FF4IuBF4i+DF47vCR4bfCcIgFDfAbNCF4IYBF4i+CEgIuFJAICBR4YRBbwOAwAjCE4TqBAwYwDlXXqy/DDoJUCAAmwxmrAYOxqpECWgQAIri+DM4YqBPQVWXw5iDLwQ1BawJHBQYYAHHQgvCqoXBbQWBXwOALQYAEXwYEBCoRgBLxIFFAwK+F1gvBJgKGBYAoHFlYWCqoUBXAYCB2FbAYlcCAIqBJAICCR4NbW4IxBf4QvBAQIHCFAJgEK4TEGwAuBGgSPBlRHBqyOBd4ZaCAAOr1YfBFwJnDmIYCmJbBQYQpDqprCegcxLwv+PgKMFAALuCAAkxbIIwBlaABFAJZFAAguEXoIACeAYnEGAIGEqoZBAAVWlQuDCIJaDNQUqIYTYBFwbACLYRgEDIVVU4IuEAALDBmINBGAJgDNQIqBLoVWFwjACFgQuD2NbgATBlUAFoogBGIQABQwIADBgimBF4pnBxgtDGgOGgAXCIwRcFEges1g3BwIEBCocr1exdobAEW4ouEAASqCFwISBwAiBqwrBNYj8BleAKQTuFYAVVFxbXCAAOwZQIRCraNFmLUB2OyVwbvFF4MxaQIuKMIVbFwIACUwIACHAatDw2rw1VYA1WrgSBdQIuJ65/BEwJxDAAYFBxgtBFYIIC2ExR4p/BZQJdLF4UxEoLSELwZoHAALvGfYOxwAuM1kAKoQvGLYItHCQMrXw1bLpouBf4K1DF44GFdwS+HmIuQLxAoBBQYtDAAK+G/0rFyIvHEoQKBAga+KwIuSF5IuDXxggBFxtWPYgvFFQWx1YuGXw9WLpwtD2GAF4I2Edw4ACxkrXwoGBFxUqrexxmAqsrldWAQNVGYRgDxhfCw1cCYa+FlZfJFwMrrYXCwJBBUwWBGYoABH4gTEX4xgHFwMqC5QaDGYRCBCZoABwIwGFwJpBC5YzGCaIwFFwQtRACowBOQOBF1IwCaIL2BFcw")); -var img_fix = require("heatshrink").decompress(atob("mEwhH+AH4A/AH4A/AGuJnYv/FzovBGFgvuFwIvCGFQvuFwQvDGFAvuFwYvEGEwvuFwgvFGEgvuFwovGGEQvuFwwvHGEAvuFw4vIGDwvuFxAvJGDgvuFxIvKGDQvuFxQvLGDAvuFxYvMGCwvuFxgvNGCgvuFxovOGCQvuFxwvPGCAvuFx4vQGBwtOCQYvbFRwAJGCwqTGh4uMFS40LEcIA/AH4A/ACQA=")); +var img_nofix = require("heatshrink").decompress(atob("mEwhBC/AH4AS6+sq0AgMrBo8rgICB1mBgIABAoNWAAusDYOBwWBF5IPBldWwAhCGQQJBgOBwNWqsxmIoF64AC1g2ClcGmIvKgGAwGwrcxqtWwItBFYdbw2x2APBG4IqBKgI0CLQIIBDISCBABAlBmOxAAOww1bqtVrY5BBIPVBYMxlYkBKoIFBAAJHBAoI6BQQSwIAANWmJQBAAIyCxmx6xbCBANbFwIhCwuGAAZHCBQKZBSgQvIgNWQQYACDgIHD6tcFwKBBSwIADrgFEGIJiBqASBSBFWrYvGMAIIB2OGJwJdBrYMBwArEAAWAxgwBewQvJwNVF4uMM4IvCquA68rqoqHGgQ3DUIT8BeJNVRAhgCfAWGmPXwExRoIlB2AsB2FbSwSTBAIOwQYLCBSBWFFwgvErYZBXgS+FLgoDCSIRFBwIvLC4WMMoIACqusbIKACEoYCCeAYLDwEAwLBBX5ExXgmG1eGwoCBRwL+BEQVcEYKMCAQxKBqovBC4LvOLYLwC1aoCwqGEIAR1BL4QLEqtWF5RQBF4+M62GF4QnCAAoHBeQILFrmBR5VVKwIvFA4PVw1VR4eAAAIkDA4L3CTIILBwtW1mAL48r1kxc4YADF4KPBF4gpCF4TmCeoYECraOBR5C+DAAjyB1YDB2NV1gPBQQgxDBA8xq1QF5EBV4KOEVIQvBAANbVAMrQoQAKRwREBSAJhBX4y+CLgQACFwewVQJIBmKRD2C4BxlbAwKLBrlbRwQVC1gvGq2FLIWGxiKB1YCBJwUrwRgDeAmMAYQsBrYKBlbkBMIK/Ira2CFgIyBGoOrGoQcB6+BmOAKgIyBFQVbqr0CGwMGXoReHd4aLBRQbuCBAVbJYIcBlcxSYIADYAgRBwARBCQIvGGANV1fVFIiPBTAJeCFwIABwIwBQ4SPCCIJiBKIIuBqAuIF4NWSAKRDAAJIBAAQuDAAKuBmIKBrcxqoCBDwJvCgwuJSAYtEFwMAwOBqEBFgY1D1mBqxZBFgOBBIJsBMYKOJgEBJQJdFgBaDDAIDBUIJWBFAZoEGwMxwvWwC/BMBVWWwK6BrcBEAowBJwLNCwpTBKgIABqqSBDgPV6uGL5TADFwS4GGAUGc4WMfYQABwuFAYJ5CPwOGTQIvJIgOMdQQuHXoWGL4WwGgQzCFgOr1Y8BDwKPKd4XWFxTeCLoYyEFQI6EL4IvBL5MrwGGwC7GLwkBToIlFEwKHBBAqtCRxMBwFVFxkxV4IvLX4QGBq1QF5UGgAuKlYuBKo7oCdobuDq2BFxLJBFxrlCFwwnBFgSOEXxTeBFxdVKAbuEK4erFwgEBriOKqGBFxHXFwerfoOAMAKGEXwmwxmGrdVRxMALxSMBDQNcqtWPgICBGYOFMIaaBCAIRBqAPBF5ILBRg8BgFcFYeBA4IGCA4IoCHoYsBwItKF4UrSAq7BLAgXIwErgIQDwArLGBQuBDYIZPLQIsQGAqSBLoQbUGCquDF1IA/AH4AKA==")); +var img_fix = require("heatshrink").decompress(atob("mEwhBC/AH4A/AH4A/AE0rg4vtq8PF1kHxM7gJesF4Jgrg+IF4M6MFReBF4JgqXoIvDMFJeCF4RgoLwYvDnZgmLwYvEMEpeEF4jBlmYvIMEheFF4pgjXogvGMEReGF407MEBeGF45gfg+IF5rBfLw4vHMDxeIF5BgdLxAvIMDkHFxAvJMDZeJF5JgaLxQvKnZgYLxQvLMC5eLF5bBXmYvWMCxeMF5hgVXpYvNMCheNF5s7MCReNF5xgRg+IFZNersylcHhEPmjBbLw1er1XlkHIhI0CnRgUmQqEKwKnRgIzBiBpBnUICpszryCCFiIyGM4TBRAH4A/AH4A/AH4AFA")); // https://github.com/Leaflet/Leaflet/blob/master/src/geo/projection/Projection.SphericalMercator.js function project(latlong) { From 3e55bc973ca9c95cb4887eec26bd9650e3af13a1 Mon Sep 17 00:00:00 2001 From: RenaudG <34276398+renaudgweb@users.noreply.github.com> Date: Tue, 21 Apr 2020 15:38:52 +0200 Subject: [PATCH 217/302] Update osmpoi.html --- apps/osmpoi/osmpoi.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/osmpoi/osmpoi.html b/apps/osmpoi/osmpoi.html index 58a182721..651e34e7a 100644 --- a/apps/osmpoi/osmpoi.html +++ b/apps/osmpoi/osmpoi.html @@ -141,7 +141,7 @@ }); document.getElementById("upload").addEventListener("click", function() { var app = `var features = ${JSON.stringify(features)}; -var img_nofix = require("heatshrink").decompress(atob("mEwhBC/AH4AS6+sq0AgMrBo8rgICB1mBgIABAoNWAAusDYOBwWBF5IPBldWwAhCGQQJBgOBwNWqsxmIoF64AC1g2ClcGmIvKgGAwGwrcxqtWwItBFYdbw2x2APBG4IqBKgI0CLQIIBDISCBABAlBmOxAAOww1bqtVrY5BBIPVBYMxlYkBKoIFBAAJHBAoI6BQQSwIAANWmJQBAAIyCxmx6xbCBANbFwIhCwuGAAZHCBQKZBSgQvIgNWQQYACDgIHD6tcFwKBBSwIADrgFEGIJiBqASBSBFWrYvGMAIIB2OGJwJdBrYMBwArEAAWAxgwBewQvJwNVF4uMM4IvCquA68rqoqHGgQ3DUIT8BeJNVRAhgCfAWGmPXwExRoIlB2AsB2FbSwSTBAIOwQYLCBSBWFFwgvErYZBXgS+FLgoDCSIRFBwIvLC4WMMoIACqusbIKACEoYCCeAYLDwEAwLBBX5ExXgmG1eGwoCBRwL+BEQVcEYKMCAQxKBqovBC4LvOLYLwC1aoCwqGEIAR1BL4QLEqtWF5RQBF4+M62GF4QnCAAoHBeQILFrmBR5VVKwIvFA4PVw1VR4eAAAIkDA4L3CTIILBwtW1mAL48r1kxc4YADF4KPBF4gpCF4TmCeoYECraOBR5C+DAAjyB1YDB2NV1gPBQQgxDBA8xq1QF5EBV4KOEVIQvBAANbVAMrQoQAKRwREBSAJhBX4y+CLgQACFwewVQJIBmKRD2C4BxlbAwKLBrlbRwQVC1gvGq2FLIWGxiKB1YCBJwUrwRgDeAmMAYQsBrYKBlbkBMIK/Ira2CFgIyBGoOrGoQcB6+BmOAKgIyBFQVbqr0CGwMGXoReHd4aLBRQbuCBAVbJYIcBlcxSYIADYAgRBwARBCQIvGGANV1fVFIiPBTAJeCFwIABwIwBQ4SPCCIJiBKIIuBqAuIF4NWSAKRDAAJIBAAQuDAAKuBmIKBrcxqoCBDwJvCgwuJSAYtEFwMAwOBqEBFgY1D1mBqxZBFgOBBIJsBMYKOJgEBJQJdFgBaDDAIDBUIJWBFAZoEGwMxwvWwC/BMBVWWwK6BrcBEAowBJwLNCwpTBKgIABqqSBDgPV6uGL5TADFwS4GGAUGc4WMfYQABwuFAYJ5CPwOGTQIvJIgOMdQQuHXoWGL4WwGgQzCFgOr1Y8BDwKPKd4XWFxTeCLoYyEFQI6EL4IvBL5MrwGGwC7GLwkBToIlFEwKHBBAqtCRxMBwFVFxkxV4IvLX4QGBq1QF5UGgAuKlYuBKo7oCdobuDq2BFxLJBFxrlCFwwnBFgSOEXxTeBFxdVKAbuEK4erFwgEBriOKqGBFxHXFwerfoOAMAKGEXwmwxmGrdVRxMALxSMBDQNcqtWPgICBGYOFMIaaBCAIRBqAPBF5ILBRg8BgFcFYeBA4IGCA4IoCHoYsBwItKF4UrSAq7BLAgXIwErgIQDwArLGBQuBDYIZPLQIsQGAqSBLoQbUGCquDF1IA/AH4AKA==")); +var img_nofix = require("heatshrink").decompress(atob("mEwhBC/AH4A/AGnXwMrgMrBg4KBgOBwIOBldWwNWAAmBCIOB1kBF5mBlVVCwInBqwzBAQIcBEQNVqtcFwIRB64AD1gPBlcGGYIAMFIOG2FbrdcFgVWrgHBw2x2Owqss1h0Bq0slcsGoIIClR8IQQ2BrYjBEgOGwwrC2AIB6uwlVWEgMxqo6BAAcxq2s1hsBGB1b2AAExg1CGAUxRQNWlQ9BAA1blQOCqzBMQ4KQBAAYFBF4aYBD4NVNoQAIGAJhOBoIvFxhgBAgOrmJdJwAwGeoRgMqyQGdYReBJoMqZAIBBAQ1cAgOwrZxCeZlWrmMF5ExPgIuEXYiRHMAOBL5oVDGYItB6uGqxeBFBCPDBQcqMAQuKlgvBXAWMw2rDIOxraOBLwaMEwAHCBQhQBF5jvGLoIZB6wvBwKwBLopABXgIAFq2BF5krqy+FF4OxF5ItBIgJaBBQlcCYLUBFxMBq1VRwRfEX4IvCqqCBRISSFSAQEBqumL5krwNc6orBAARMBAYOAJQIvBAB0xWAIvMXwIqBFweMAgSrB1kxwCLCABVbXwKPMgCOBFoYABLwQGBmIaBmKyCR5UxwJeBGIK/KQAOw1ereQIsCLwOMYAPXlQnBF4VcMoIFBXwdbWAKOBR5mAdgerw+G1YuBBIJgBwMxrdcGQQrCqo3C2AvBFwOBgLvMRoRdCw6XElRNBwMqmNVFoVbwDtEFwKiBXxZgCXwgECJoStB64ABGASICBoWGqsqFwUrlYuLgA9BdIex6orBlcxgwuD1gABegIyBAAaJBHgMGLpgvDraJFlgnCqwuB0wqBwIyCwIACHIdVMgOBGB1cRoQuBFQQABGAOmmLjCLAWmGQQsBFoJJBBYJgPEIIuGGAUAWwQOBwzABFYLEC6p5Cra/NF4Nb2GMFw4ABlYvCxgxCAAYrB1b2CqrANd4YuJF4YAExg2BBIIEBBAS/OF4SuBFxFWlQkELYZmBBAmGLxsAldcLpVWgxXCF44EDAANcF55hBFxSbBKwooCBQIABBASOOlcrFxuxFw+MFoYACmJQBXxouK2PV6pWEFQWx1aVGRx4uIwLqBDoNVqtWquAGIhnBGgOGwFVmOsgIvN1hdHgFbrcxq2BAwIDCmNbwwACBwI8BwMsRxodBMA2BZAWmDYJMFlgmCVAIPJGCIFBwJ3OwIpQGBCSBAYJ2OADYsCAQIupAH4A/AH4AoA")); var img_fix = require("heatshrink").decompress(atob("mEwhBC/AH4A/AH4A/AE0rg4vtq8PF1kHxM7gJesF4Jgrg+IF4M6MFReBF4JgqXoIvDMFJeCF4RgoLwYvDnZgmLwYvEMEpeEF4jBlmYvIMEheFF4pgjXogvGMEReGF407MEBeGF45gfg+IF5rBfLw4vHMDxeIF5BgdLxAvIMDkHFxAvJMDZeJF5JgaLxQvKnZgYLxQvLMC5eLF5bBXmYvWMCxeMF5hgVXpYvNMCheNF5s7MCReNF5xgRg+IFZNersylcHhEPmjBbLw1er1XlkHIhI0CnRgUmQqEKwKnRgIzBiBpBnUICpszryCCFiIyGM4TBRAH4A/AH4A/AH4AFA")); // https://github.com/Leaflet/Leaflet/blob/master/src/geo/projection/Projection.SphericalMercator.js From 00c37da578024838586fbbb0b78e91da12e2b93b Mon Sep 17 00:00:00 2001 From: RenaudG <34276398+renaudgweb@users.noreply.github.com> Date: Tue, 21 Apr 2020 15:45:31 +0200 Subject: [PATCH 218/302] Update osmpoi.html --- apps/osmpoi/osmpoi.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/osmpoi/osmpoi.html b/apps/osmpoi/osmpoi.html index 651e34e7a..0096b78a0 100644 --- a/apps/osmpoi/osmpoi.html +++ b/apps/osmpoi/osmpoi.html @@ -141,7 +141,7 @@ }); document.getElementById("upload").addEventListener("click", function() { var app = `var features = ${JSON.stringify(features)}; -var img_nofix = require("heatshrink").decompress(atob("mEwhBC/AH4A/AGnXwMrgMrBg4KBgOBwIOBldWwNWAAmBCIOB1kBF5mBlVVCwInBqwzBAQIcBEQNVqtcFwIRB64AD1gPBlcGGYIAMFIOG2FbrdcFgVWrgHBw2x2Owqss1h0Bq0slcsGoIIClR8IQQ2BrYjBEgOGwwrC2AIB6uwlVWEgMxqo6BAAcxq2s1hsBGB1b2AAExg1CGAUxRQNWlQ9BAA1blQOCqzBMQ4KQBAAYFBF4aYBD4NVNoQAIGAJhOBoIvFxhgBAgOrmJdJwAwGeoRgMqyQGdYReBJoMqZAIBBAQ1cAgOwrZxCeZlWrmMF5ExPgIuEXYiRHMAOBL5oVDGYItB6uGqxeBFBCPDBQcqMAQuKlgvBXAWMw2rDIOxraOBLwaMEwAHCBQhQBF5jvGLoIZB6wvBwKwBLopABXgIAFq2BF5krqy+FF4OxF5ItBIgJaBBQlcCYLUBFxMBq1VRwRfEX4IvCqqCBRISSFSAQEBqumL5krwNc6orBAARMBAYOAJQIvBAB0xWAIvMXwIqBFweMAgSrB1kxwCLCABVbXwKPMgCOBFoYABLwQGBmIaBmKyCR5UxwJeBGIK/KQAOw1ereQIsCLwOMYAPXlQnBF4VcMoIFBXwdbWAKOBR5mAdgerw+G1YuBBIJgBwMxrdcGQQrCqo3C2AvBFwOBgLvMRoRdCw6XElRNBwMqmNVFoVbwDtEFwKiBXxZgCXwgECJoStB64ABGASICBoWGqsqFwUrlYuLgA9BdIex6orBlcxgwuD1gABegIyBAAaJBHgMGLpgvDraJFlgnCqwuB0wqBwIyCwIACHIdVMgOBGB1cRoQuBFQQABGAOmmLjCLAWmGQQsBFoJJBBYJgPEIIuGGAUAWwQOBwzABFYLEC6p5Cra/NF4Nb2GMFw4ABlYvCxgxCAAYrB1b2CqrANd4YuJF4YAExg2BBIIEBBAS/OF4SuBFxFWlQkELYZmBBAmGLxsAldcLpVWgxXCF44EDAANcF55hBFxSbBKwooCBQIABBASOOlcrFxuxFw+MFoYACmJQBXxouK2PV6pWEFQWx1aVGRx4uIwLqBDoNVqtWquAGIhnBGgOGwFVmOsgIvN1hdHgFbrcxq2BAwIDCmNbwwACBwI8BwMsRxodBMA2BZAWmDYJMFlgmCVAIPJGCIFBwJ3OwIpQGBCSBAYJ2OADYsCAQIupAH4A/AH4AoA")); +var img_nofix = require("heatshrink").decompress(atob("mEwhBC/AH4A/AH4A/AC2swMllcrBQoHBktWwIEBlYEBqwAEgAICF6FWldbrgDBAIMrFIIhBrlbAAIIBwOsAAgsClcxF6ErrmG2GGw1cqsxrmAwuG2OxxkxFoOCq1VFANVLgIyBPI4vLq2F2OwAAQrCAgPVw0xEgVVMoQ7BwsxmOm1iUCMCIuD2GM2AvC2IuDFQIACwADCrZrCGCOBqovI2NcEAIuBTwQAHlZhCL6FVYAKQG2NV1i3BZYIpDrZiFlb1BYSFWwAvG6tbwOCmIuFF4YAC2FVwOBMCFWraBCRwReDBYIpELQOAAQQ5CqpgBF57RBFYK+C2IcBw4vBEQIwDrYEDrlbGYOMmMr64vQd4hcBX4VcF4LsGOIYGCxhMBYAKORGAhhBF4TtBAAzvFwuAwIvPlbuBXIQAE2BMBHgIoBE4QvEricBqtWF4KPOF4KICAAReCd4WBdoYAJmKPCF50lrhYCYIuxreBwUxR5RrBrmCwIABF54pCFwrkBJoMxFAKGBF4lcFwNbR4JxBX6GGFYeHSoOGSQJgBGARVBFoIqCMIWMlS+ClbvRMIbFEJ4PXwMrmIqBAARjDleB67ABFxowCYAgDBQQVbFwIAB00xGAQMDA4OCNoJdOAAWBYAex6uGNAIcBFwSvB1mCq1VFYMxqwICA4IvRDgKKDw0qwQrBPgIuBX4IoCAAmBDILQCF6AwBwBgBFwgwCldVTQKJBqtcwB1BSgXV6uAXyAvCrYuHAAMrrYuBGIQABwoGBOgWxF6SEBFwWmFwpgBF4IADxgyC2IFCR6Z5BLpHX1krLoQADLYQHEqrvRWQMrFxKOEF4eMxgyBGgVWkovRgCMH1kqZIInBLwwtCRwZfQqxdLEYQuFAQYHCraORq2BXZPVKwIABEoIGBFoYCCRwNWRyIuIquGrgfBldWwGAwqMBFYVbrlcqovSL42sksrwCsBD4QDBkoyBqsxFQINBmItRL4UrGAZdBLIIABCQ8lgAsBH4IsSGA4uBDq4wTq0xmIuqAH4A/AH4A/AH4AHA=")); var img_fix = require("heatshrink").decompress(atob("mEwhBC/AH4A/AH4A/AE0rg4vtq8PF1kHxM7gJesF4Jgrg+IF4M6MFReBF4JgqXoIvDMFJeCF4RgoLwYvDnZgmLwYvEMEpeEF4jBlmYvIMEheFF4pgjXogvGMEReGF407MEBeGF45gfg+IF5rBfLw4vHMDxeIF5BgdLxAvIMDkHFxAvJMDZeJF5JgaLxQvKnZgYLxQvLMC5eLF5bBXmYvWMCxeMF5hgVXpYvNMCheNF5s7MCReNF5xgRg+IFZNersylcHhEPmjBbLw1er1XlkHIhI0CnRgUmQqEKwKnRgIzBiBpBnUICpszryCCFiIyGM4TBRAH4A/AH4A/AH4AFA")); // https://github.com/Leaflet/Leaflet/blob/master/src/geo/projection/Projection.SphericalMercator.js From 9d678772943b6e19cdfa53113b18933f31524911 Mon Sep 17 00:00:00 2001 From: Purple-Tentacle <59914607+Purple-Tentacle@users.noreply.github.com> Date: Tue, 21 Apr 2020 16:04:56 +0200 Subject: [PATCH 219/302] ActivePedom 0.03 --- apps/activepedom/app.js | 44 ++++++++++++++++++++++++++++++++------ apps/activepedom/widget.js | 26 ++++++++++++++-------- 2 files changed, 55 insertions(+), 15 deletions(-) diff --git a/apps/activepedom/app.js b/apps/activepedom/app.js index f966530d0..0680e2d1a 100644 --- a/apps/activepedom/app.js +++ b/apps/activepedom/app.js @@ -1,8 +1,31 @@ (() => { +//Graph module, as long as modules are not added by the app loader +Modules.addCached("graph",function(){exports.drawAxes=function(b,c,a){function h(a){return e+m*(a-t)/x}function l(a){return f+g-g*(a-n)/u}var k=a.padx||0,d=a.pady||0,t=-k,w=c.length+k-1,n=(void 0!==a.miny?a.miny:a.miny=c.reduce(function(a,b){return Math.min(a,b)},c[0]))-d;c=(void 0!==a.maxy?a.maxy:a.maxy=c.reduce(function(a,b){return Math.max(a,b)},c[0]))+d;a.gridy&&(d=a.gridy,n=d*Math.floor(n/d),c=d*Math.ceil(c/d));var e=a.x||0,f=a.y||0,m=a.width||b.getWidth()-(e+1),g=a.height||b.getHeight()-(f+1);a.axes&&(null!==a.ylabel&& + (e+=6,m-=6),null!==a.xlabel&&(g-=6));a.title&&(f+=6,g-=6);a.axes&&(b.drawLine(e,f,e,f+g),b.drawLine(e,f+g,e+m,f+g));a.title&&(b.setFontAlign(0,-1),b.drawString(a.title,e+m/2,f-6));var x=w-t,u=c-n;u||(u=1);if(a.gridx){b.setFontAlign(0,-1,0);var v=a.gridx;for(d=Math.ceil((t+k)/v)*v;d<=w-k;d+=v){var r=h(d),p=a.xlabel?a.xlabel(d):d;b.setPixel(r,f+g-1);var q=b.stringWidth(p)/2;null!==a.xlabel&&r>q&&b.getWidth()>r+q&&b.drawString(p,r,f+g+2)}}if(a.gridy)for(b.setFontAlign(0,0,1),d=n;d<=c;d+=a.gridy)k=l(d), + p=a.ylabel?a.ylabel(d):d,b.setPixel(e+1,k),q=b.stringWidth(p)/2,null!==a.ylabel&&k>q&&b.getHeight()>k+q&&b.drawString(p,e-5,k+1);b.setFontAlign(-1,-1,0);return{x:e,y:f,w:m,h:g,getx:h,gety:l}};exports.drawLine=function(b,c,a){a=a||{};a=exports.drawAxes(b,c,a);var h=!0,l;for(l in c)h?b.moveTo(a.getx(l),a.gety(c[l])):b.lineTo(a.getx(l),a.gety(c[l])),h=!1;return a};exports.drawBar=function(b,c,a){a=a||{};a.padx=1;a=exports.drawAxes(b,c,a);for(var h in c)b.fillRect(a.getx(h-.5)+1,a.gety(c[h]),a.getx(h+ + .5)-1,a.gety(0));return a}}); + const storage = require("Storage"); +const SETTINGS_FILE = 'activepedom.settings.json'; var history = 86400000; // 28800000=8h 43200000=12h //86400000=24h +//return setting +function setting(key) { +//define default settings +const DEFAULTS = { + 'cMaxTime' : 1100, + 'cMinTime' : 240, + 'stepThreshold' : 30, + 'intervalResetActive' : 30000, + 'stepSensitivity' : 80, + 'stepGoal' : 10000, + 'stepLength' : 75, +}; +if (!settings) { loadSettings(); } +return (key in settings) ? settings[key] : DEFAULTS[key]; +} + //Convert ms to time function getTime(t) { date = new Date(t); @@ -58,8 +81,8 @@ function drawGraph() { filename = filename = "activepedom-" + now.getFullYear() + month + now.getDate() + ".data"; var csvFile = storage.open(filename, "r"); times = getArrayFromCSV(csvFile, 0); - first = getDate(times[0]) + " " + getTime(times[0]); - last = getDate (times[times.length-1]) + " " + getTime(times[times.length-1]); + first = getDate(times[0]) + " " + getTime(times[0]); //first entry in datafile + last = getDate (times[times.length-1]) + " " + getTime(times[times.length-1]); //last entry in datafile //free memory csvFile = undefined; times = undefined; @@ -67,21 +90,24 @@ function drawGraph() { //steps var csvFile = storage.open(filename, "r"); steps = getArrayFromCSV(csvFile, 1); + first = first + " " + steps[0] + "/" + setting('stepGoal'); + last = last + " " + steps[steps.length-1] + "/" + setting('stepGoal'); + //define y-axis grid labels stepsLastEntry = steps[steps.length-1]; if (stepsLastEntry < 1000) gridyValue = 100; - if (stepsLastEntry >= 1000 && stepsLastEntry < 10000) gridyValue = 500; + if (stepsLastEntry >= 1000 && stepsLastEntry < 10000) gridyValue = 1000; if (stepsLastEntry > 10000) gridyValue = 5000; //draw drawMenu(); - g.drawString("First: " + first, 40, 30); - g.drawString(" Last: " + last, 40, 40); + g.drawString("First: " + first, 10, 30); + g.drawString(" Last: " + last, 10, 40); require("graph").drawLine(g, steps, { //title: "Steps Counted", axes : true, gridy : gridyValue, - y : 50, //offset on screen + y : 60, //offset on screen x : 5, //offset on screen }); //free memory from big variables @@ -128,6 +154,12 @@ setWatch(function() { //BTN4 setWatch(function() { //BTN5 }, BTN5, {edge:"rising", debounce:50, repeat:true}); +//load settings +let settings; +function loadSettings() { +settings = storage.readJSON(SETTINGS_FILE, 1) || {}; +} + drawMenu(); })(); \ No newline at end of file diff --git a/apps/activepedom/widget.js b/apps/activepedom/widget.js index 7879b2056..c6bd410ce 100644 --- a/apps/activepedom/widget.js +++ b/apps/activepedom/widget.js @@ -35,16 +35,24 @@ now = new Date(); month = now.getMonth() + 1; if (month < 10) month = "0" + month; - filename = filename = "activepedom-" + now.getFullYear() + month + now.getDate() + ".data"; + filename = filename = "activepedom" + now.getFullYear() + month + now.getDate() + ".data"; dataFile = s.open(filename,"a"); - if (dataFile) dataFile.write([ - now.getTime(), - stepsCounted, - active, - stepsTooShort, - stepsTooLong, - stepsOutsideTime, - ].join(",")+"\n"); + if (dataFile) { + if (dataFile.getLength() == 0) { + stepsToWrite = 0; + } + else { + stepsToWrite = stepsCounted; + } + dataFile.write([ + now.getTime(), + stepsToWrite, + active, + stepsTooShort, + stepsTooLong, + stepsOutsideTime, + ].join(",")+"\n"); + } dataFile = undefined; } From 8dd71947bdc0f791717add161121d053e423e91c Mon Sep 17 00:00:00 2001 From: Purple-Tentacle <59914607+Purple-Tentacle@users.noreply.github.com> Date: Tue, 21 Apr 2020 16:32:15 +0200 Subject: [PATCH 220/302] Fix typo --- apps/activepedom/app.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/activepedom/app.js b/apps/activepedom/app.js index 0680e2d1a..0a9b3b93f 100644 --- a/apps/activepedom/app.js +++ b/apps/activepedom/app.js @@ -78,7 +78,7 @@ function drawGraph() { now = new Date(); month = now.getMonth() + 1; if (month < 10) month = "0" + month; - filename = filename = "activepedom-" + now.getFullYear() + month + now.getDate() + ".data"; + filename = filename = "activepedom" + now.getFullYear() + month + now.getDate() + ".data"; var csvFile = storage.open(filename, "r"); times = getArrayFromCSV(csvFile, 0); first = getDate(times[0]) + " " + getTime(times[0]); //first entry in datafile @@ -162,4 +162,4 @@ settings = storage.readJSON(SETTINGS_FILE, 1) || {}; drawMenu(); -})(); \ No newline at end of file +})(); From 8272b19bb6517b96950502ff3f422cab8e6fecea Mon Sep 17 00:00:00 2001 From: Paul Cockrell Date: Tue, 21 Apr 2020 19:38:02 +0100 Subject: [PATCH 221/302] Finish BuffGym app --- apps/buffgym/buffgym-exercise.js | 115 ++---- apps/buffgym/buffgym-icon.js | 2 +- apps/buffgym/buffgym-program-a.json | 33 ++ apps/buffgym/buffgym-program-b.json | 33 ++ apps/buffgym/buffgym-program-index.json | 10 + apps/buffgym/buffgym-program.js | 62 ++- apps/buffgym/buffgym-programs.json | 1 - apps/buffgym/buffgym-programs.json.unminified | 101 ----- apps/buffgym/buffgym-set.js | 42 +- apps/buffgym/buffgym.app.js | 373 ++++++++++-------- apps/buffgym/buffgym.png | Bin 1800 -> 7576 bytes 11 files changed, 363 insertions(+), 409 deletions(-) create mode 100644 apps/buffgym/buffgym-program-a.json create mode 100644 apps/buffgym/buffgym-program-b.json create mode 100644 apps/buffgym/buffgym-program-index.json delete mode 100644 apps/buffgym/buffgym-programs.json delete mode 100644 apps/buffgym/buffgym-programs.json.unminified mode change 100644 => 100755 apps/buffgym/buffgym.png diff --git a/apps/buffgym/buffgym-exercise.js b/apps/buffgym/buffgym-exercise.js index 99d658571..68e49be84 100644 --- a/apps/buffgym/buffgym-exercise.js +++ b/apps/buffgym/buffgym-exercise.js @@ -1,116 +1,76 @@ -const STARTED = 1; -const RESTING = 2; -const COMPLETED = 3; -const ONE_SECOND = 1000; - exports = class Exercise { - constructor(params /*{title, weight, unit, restPeriod}*/) { - const DEFAULTS = { - title: "Unknown", - weight: 0, - unit: "Kg", - restPeriod: 90, - weightIncrement: 2.5, - }; - const p = Object.assign({}, DEFAULTS, params); - - this._title = p.title; - this._weight = p.weight; - this._unit = p.unit; - this._originalRestPeriod = p.restPeriod; // Used when reseting _restPeriod - this._restPeriod = p.restPeriod; - this._weightIncrement = p.weightIncrement; - this._started = new Date(); - this._completed = false; - this._sets = []; + constructor(params) { + this.title = params.title; + this.weight = params.weight; + this.unit = params.unit; + this.restPeriod = params.restPeriod; + this.completed = false; + this.sets = []; this._restTimeout = null; this._restInterval = null; this._state = null; - } - - get title() { - return this._title; + this._originalRestPeriod = params.restPeriod; + this._weightIncrement = params.weightIncrement || 2.5; } get humanTitle() { - return `${this._title} ${this._weight}${this._unit}`; + return `${this.title} ${this.weight}${this.unit}`; } get subTitle() { - const totalSets = this._sets.length; - const uncompletedSets = this._sets.filter((set) => !set.isCompleted()).length; + const totalSets = this.sets.length; + const uncompletedSets = this.sets.filter((set) => !set.isCompleted()).length; const currentSet = (totalSets - uncompletedSets) + 1; return `Set ${currentSet} of ${totalSets}`; } - get restPeriod() { - return this._restPeriod; - } - decRestPeriod() { - this._restPeriod--; - } - - get weight() { - return this._weight; - } - - get unit() { - return this._unit; - } - - get started() { - return this._started; + this.restPeriod--; } addSet(set) { - this._sets.push(set); + this.sets.push(set); } - addSets(sets) { - sets.forEach(set => this.addSet(set)); - } - - get currentSet() { - return this._sets.filter(set => !set.isCompleted())[0]; + currentSet() { + return this.sets.filter(set => !set.isCompleted())[0]; } isLastSet() { - return this._sets.filter(set => !set.isCompleted()).length === 1; + return this.sets.filter(set => !set.isCompleted()).length === 1; } isCompleted() { - return !!this._completed; + return !!this.completed; } canSetCompleted() { - return this._sets.filter(set => set.isCompleted()).length === this._sets.length; + return this.sets.filter(set => set.isCompleted()).length === this.sets.length; } setCompleted() { if (!this.canSetCompleted()) throw "All sets must be completed"; - if (this.canProgress) this._weight += this._weightIncrement; - this._completed = true; + if (this.canProgress()) this.weight += this._weightIncrement; + this.completed = true; } - get canProgress() { + canProgress() { let completedRepsTotalSum = 0; let targetRepsTotalSum = 0; - - const completedRepsTotal = this._sets.forEach(set => completedRepsTotalSum += set.reps); - const targetRepsTotal = this._sets.forEach(set => targetRepsTotalSum += set.maxReps); + this.sets.forEach(set => completedRepsTotalSum += set.reps); + this.sets.forEach(set => targetRepsTotalSum += set.maxReps); return (targetRepsTotalSum - completedRepsTotalSum) === 0; } startRestTimer(program) { this._restTimeout = setTimeout(() => { - this.next(); - }, ONE_SECOND * this._restPeriod); + this.next(program); + }, 1000 * this.restPeriod); this._restInterval = setInterval(() => { program.emit("redraw"); - }, ONE_SECOND); + }, 1000 ); } resetRestTimer() { @@ -118,7 +78,7 @@ exports = class Exercise { clearInterval(this._restInterval); this._restTimeout = null; this._restInterval = null; - this._restPeriod = this._originalRestPeriod; + this.restPeriod = this._originalRestPeriod; } isRestTimerRunning() { @@ -129,52 +89,51 @@ exports = class Exercise { clearWatch(); setWatch(() => { - this.currentSet.incReps(); + this.currentSet().incReps(); program.emit("redraw"); }, BTN1, {repeat: true}); setWatch(program.next.bind(program), BTN2, {repeat: false}); setWatch(() => { - this.currentSet.decReps(); + this.currentSet().decReps(); program.emit("redraw"); }, BTN3, {repeat: true}); } setupRestingButtons(program) { clearWatch(); - setWatch(program.next.bind(program), BTN2, {repeat: true}); + setWatch(program.next.bind(program), BTN2, {repeat: false}); } next(program) { - global.poo = this; + const STARTED = 1; + const RESTING = 2; + const COMPLETED = 3; + switch(this._state) { case null: - console.log("XXX 1 moving null -> STARTED"); this._state = STARTED; this.setupStartedButtons(program); break; case STARTED: - console.log("XXX 2 moving STARTED -> RESTING"); this._state = RESTING; this.startRestTimer(program); this.setupRestingButtons(program); break; case RESTING: this.resetRestTimer(); - this.currentSet.setCompleted(); + this.currentSet().setCompleted(); if (this.canSetCompleted()) { - console.log("XXX 3b moving RESTING -> COMPLETED"); this._state = COMPLETED; this.setCompleted(); } else { - console.log("XXX 3a moving RESTING -> null"); this._state = null; } // As we are changing state and require it to be reprocessed // invoke the next step of program - program.next(program); + program.next(); break; default: throw "Exercise: Attempting to move to an unknown state"; diff --git a/apps/buffgym/buffgym-icon.js b/apps/buffgym/buffgym-icon.js index 949b0e45b..31764acbb 100644 --- a/apps/buffgym/buffgym-icon.js +++ b/apps/buffgym/buffgym-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mEwwhC/AFEEolAC6lN7vdDCcECwPd6guVGCYuDC4cCBQMikQXQJAMjkECmcyIx4XDmUjmYvLC4XUDARHBIoIWLgATCGQdA7tEonQC5ouDDYg0BOxgSEAggwKRwgUCC6ZIDSwoXNogWDDgNCAgIWIkUEoUk6kiCgMkokipsiBIQXIki2CAgNCAoYADC5Eic4Mic4ICCAIIJCC5MzAAcykYGEAAIXOABAXTmUzGoIXVAIIXLB4SICDIovjO76PZbYR3PDI4XiI6530MIh3SC6R33C/oAOC48CCxsgC44A/ADY=")) +require("heatshrink").decompress(atob("mEwxH+ACPI5AUSADAtB5vNGFQtBAIfNF95hoF4wwoF5AwmF5BhmXYbAEF/6QbF1QwIF04qB54ADAwIwoF4oRKBoIvsB4gvZ58kkgCDFxoxaF5wuHGDQcMF5IwXDZwLDGDmlDIWlkgJDSwIABCRAwPDQohCFgIABDQIOCFwYABr4RCCQIvQDYguEAAwtFF5owJDZAvHFw4vFOYQvKFAowMBxIvFMQwvPAB4wFUQ4vJGDYvUGC4vNdgyuEGDIsNFwYwGNAgAPExAvMGIdfTIovfTpYvrfRCOkZ44ugF44NGF05gUFyQvKGIoueGKIufGJ4uhG5oupGItfr4vvAAgvlGAQvt/wrEF9oEGF841IF9QGHX0oGIAD8kAAYJOFzwEBBQoMFACA=")); \ No newline at end of file diff --git a/apps/buffgym/buffgym-program-a.json b/apps/buffgym/buffgym-program-a.json new file mode 100644 index 000000000..7ebaf3741 --- /dev/null +++ b/apps/buffgym/buffgym-program-a.json @@ -0,0 +1,33 @@ +{ + "title": "Program A", + "exercises": [ + { + "title": "Squats", + "weight": 40, + "unit": "Kg", + "sets": [5, 5, 5, 5, 5], + "restPeriod": 90 + }, + { + "title": "Overhead press", + "weight": 20, + "unit": "Kg", + "sets": [5, 5, 5, 5, 5], + "restPeriod": 90 + }, + { + "title": "Deadlift", + "weight": 20, + "unit": "Kg", + "sets": [5], + "restPeriod": 90 + }, + { + "title": "Pullups", + "weight": 0, + "unit": "Kg", + "sets": [10, 10, 10], + "restPeriod": 90 + } + ] +} \ No newline at end of file diff --git a/apps/buffgym/buffgym-program-b.json b/apps/buffgym/buffgym-program-b.json new file mode 100644 index 000000000..b93348621 --- /dev/null +++ b/apps/buffgym/buffgym-program-b.json @@ -0,0 +1,33 @@ +{ + "title": "Program B", + "exercises": [ + { + "title": "Squats", + "weight": 40, + "unit": "Kg", + "sets": [5, 5, 5, 5, 5], + "restPeriod": 90 + }, + { + "title": "Bench press", + "weight": 20, + "unit": "Kg", + "sets": [5, 5, 5, 5, 5], + "restPeriod": 90 + }, + { + "title": "Row", + "weight": 20, + "unit":"Kg", + "sets": [5, 5, 5, 5, 5], + "restPeriod": 90 + }, + { + "title": "Tricep extension", + "weight": 20, + "unit": "Kg", + "sets": [10, 10, 10], + "restPeriod": 90 + } + ] +} \ No newline at end of file diff --git a/apps/buffgym/buffgym-program-index.json b/apps/buffgym/buffgym-program-index.json new file mode 100644 index 000000000..3bb51f1b5 --- /dev/null +++ b/apps/buffgym/buffgym-program-index.json @@ -0,0 +1,10 @@ +[ + { + "title": "Program A", + "file": "buffgym-program-a.json" + }, + { + "title": "Program B", + "file": "buffgym-program-b.json" + } +] \ No newline at end of file diff --git a/apps/buffgym/buffgym-program.js b/apps/buffgym/buffgym-program.js index 956827f56..68a069da5 100644 --- a/apps/buffgym/buffgym-program.js +++ b/apps/buffgym/buffgym-program.js @@ -1,68 +1,56 @@ exports = class Program { constructor(params) { - const DEFAULTS = { - title: "Unknown", - trainDay: "", // Day of week - }; - const p = Object.assign({}, DEFAULTS, params); - - this._title = p.title; - this._trainDay = p.trainDay; - this._exercises = []; - + this.title = params.title; + this.exercises = []; + this.completed = false; this.on("redraw", redraw.bind(null, this)); } - get title() { - return `${this._title} - ${this._trainDay}`; - } - - addExercise(exercise) { - this._exercises.push(exercise); - } - addExercises(exercises) { - exercises.forEach(exercise => this.addExercise(exercise)); + exercises.forEach(exercise => this.exercises.push(exercise)); } currentExercise() { - return ( - this._exercises - .filter(exercise => !exercise.isCompleted())[0] - ); + return this.exercises.filter(exercise => !exercise.isCompleted())[0]; } canComplete() { - return ( - this._exercises - .filter(exercise => exercise.isCompleted()) - .length === this._exercises.length - ); + return this.exercises.filter(exercise => exercise.isCompleted()).length === this.exercises.length; } setCompleted() { if (!this.canComplete()) throw "All exercises must be completed"; - this._completed = true; + this.completed = true; } isCompleted() { - return !!this._completed; + return !!this.completed; + } + + toJSON() { + return { + title: this.title, + exercises: this.exercises.map(exercise => { + return { + title: exercise.title, + weight: exercise.weight, + unit: exercise.unit, + sets: exercise.sets.map(set => set.maxReps), + restPeriod: exercise.restPeriod, + }; + }), + }; } // State machine next() { - console.log("XXX Program.next"); - const exercise = this.currentExercise(); - - // All exercises are completed so mark the - // Program as comleted if (this.canComplete()) { this.setCompleted(); this.emit("redraw"); - return; } - exercise.next(this); + // Call current exercise state machine + this.currentExercise().next(this); } } \ No newline at end of file diff --git a/apps/buffgym/buffgym-programs.json b/apps/buffgym/buffgym-programs.json deleted file mode 100644 index 7551c7a47..000000000 --- a/apps/buffgym/buffgym-programs.json +++ /dev/null @@ -1 +0,0 @@ -[{"title":"Program A","exercises":[{"title":"Squats","weight":40,"unit":"Kg","sets":[5,5,5,5,5]},{"title":"Overhead press","weight":20,"unit":"Kg","sets":[5,5,5,5,5]},{"title":"Deadlift","weight":20,"unit":"Kg","sets":[5]},{"title":"Pullups","weight":0,"unit":"Kg","sets":[10,10,10]}]},{"title":"Program B","exercises":[{"title":"Squats","weight":40,"unit":"Kg","sets":[5,5,5,5,5]},{"title":"Bench press","weight":20,"unit":"Kg","sets":[5,5,5,5,5]},{"title":"Row","weight":20,"unit":"Kg","sets":[5,5,5,5,5]},{"title":"Tricep extension","weight":20,"unit":"Kg","sets":[10,10,10]}]}] \ No newline at end of file diff --git a/apps/buffgym/buffgym-programs.json.unminified b/apps/buffgym/buffgym-programs.json.unminified deleted file mode 100644 index cd005eeab..000000000 --- a/apps/buffgym/buffgym-programs.json.unminified +++ /dev/null @@ -1,101 +0,0 @@ -[ - { - "title": "Program A", - "exercises": [ - { - "title": "Squats", - "weight": 40, - "unit": "Kg", - "sets": [ - 5, - 5, - 5, - 5, - 5 - ] - }, - { - "title": "Overhead press", - "weight": 20, - "unit": "Kg", - "sets": [ - 5, - 5, - 5, - 5, - 5 - ] - }, - { - "title": "Deadlift", - "weight": 20, - "unit": "Kg", - "sets": [ - 5 - ] - }, - { - "title": "Pullups", - "weight": 0, - "unit": "Kg", - "sets": [ - 10, - 10, - 10 - ] - } - ] - }, - { - "title": "Program B", - "exercises": [ - { - "title": "Squats", - "weight": 40, - "unit": "Kg", - "sets": [ - 5, - 5, - 5, - 5, - 5 - ] - }, - { - "title": "Bench press", - "weight": 20, - "unit": "Kg", - "sets": [ - 5, - 5, - 5, - 5, - 5 - ] - }, - { - "title": "Row", - "weight": 20, - "unit":"Kg", - "sets": [ - 5, - 5, - 5, - 5, - 5 - ] - - }, - { - "title": "Tricep extension", - "weight": 20, - "unit": "Kg", - "sets": [ - 10, - 10, - 10 - ] - } - ] - } -] \ No newline at end of file diff --git a/apps/buffgym/buffgym-set.js b/apps/buffgym/buffgym-set.js index aed6df260..4bd12d7ec 100644 --- a/apps/buffgym/buffgym-set.js +++ b/apps/buffgym/buffgym-set.js @@ -1,46 +1,28 @@ exports = class Set { constructor(maxReps) { - this._minReps = 0; - this._maxReps = maxReps; - this._reps = 0; - this._completed = false; - } - - get title() { - return this._title; - } - - get weight() { - return this._weight; + this.minReps = 0; + this.maxReps = maxReps; + this.reps = 0; + this.completed = false; } isCompleted() { - return !!this._completed; + return !!this.completed; } setCompleted() { - this._completed = true; - } - - get reps() { - return this._reps; - } - - get maxReps() { - return this._maxReps; + this.completed = true; } incReps() { - if (this._completed) return; - if (this._reps >= this._maxReps) return; - - this._reps++; + if (this.completed) return; + if (this.reps >= this.maxReps) return; + this.reps++; } decReps() { - if (this._completed) return; - if (this._reps <= this._minReps) return; - - this._reps--; + if (this.completed) return; + if (this.reps <= this.minReps) return; + this.reps--; } } \ No newline at end of file diff --git a/apps/buffgym/buffgym.app.js b/apps/buffgym/buffgym.app.js index e1ab3e66b..eeabd5c29 100755 --- a/apps/buffgym/buffgym.app.js +++ b/apps/buffgym/buffgym.app.js @@ -1,177 +1,135 @@ +Bangle.setLCDMode("120x120"); + const W = g.getWidth(); const H = g.getHeight(); const RED = "#d32e29"; const PINK = "#f05a56"; const WHITE = "#ffffff"; -const Set = require("buffgym-set.js"); -const Exercise = require("buffgym-exercise.js"); -const Program = require("buffgym-program.js"); - -function centerStringX(str) { - return (W - g.stringWidth(str)) / 2; -} - -function iconIncrement() { - const img = require("heatshrink").decompress(atob("ikUxH+AA3XAAgNHCJIVMBYXQ5PC4XJ6AUJCIQQBAAoVCCQwjCAA/JCgglHA4IpJBYwTHA4RMJCY5oDJo4THKIQKET5IMGCaY7TMaKLTWajbTFJIlICgoVBFYXJYQYSGCggAGCRAVIBgw")); - return img; -} - -function iconDecrement() { - const img = require("heatshrink").decompress(atob("ikUxH+AA3XAAgNHCJIVMBYXQ5PC4XJ6AUJCIQQBAAoVCCQwjCAA/JCgglKFJADBCRYABCYQmOFAhNMKIw6FTw4LHCaY7TMaKLTWajbTFJglFCgoVBFYXJYQYSGCggAGCRAVIBgw=")); - return img; -} - -function iconOk() { - const img = require("heatshrink").decompress(atob("ikUxH+AA3XAAgNHCJIVMBYXQ5PC4XJ6AUJCIQQBAAoVCCQwjCAA/JCgglKFJADBCJQxCCYQmMIwZoDJpQMCKIg6KBYwTGFQgeHHYouCCRI7EMYTXFRhILEK5SfFRgYSIborbSbpglFCgoVBFYXJYQYSGCggAGCRAVIBgwA==")); - return img; -} - function drawMenu(params) { + const hs = require("heatshrink"); + const incImg = hs.decompress(atob("gsFwMAkM+oUA")); + const decImg = hs.decompress(atob("gsFwIEBnwCBA")); + const okImg = hs.decompress(atob("gsFwMAhGFo0A")); const DEFAULT_PARAMS = { showBTN1: false, showBTN2: false, showBTN3: false, }; const p = Object.assign({}, DEFAULT_PARAMS, params); - if (p.showBTN1) g.drawImage(iconIncrement(), W - 30, 10); - if (p.showBTN2) g.drawImage(iconOk(), W - 30, 110); - if (p.showBTN3) g.drawImage(iconDecrement(), W - 30, 210); + if (p.showBTN1) g.drawImage(incImg, W - 10, 10); + if (p.showBTN2) g.drawImage(okImg, W - 10, 60); + if (p.showBTN3) g.drawImage(decImg, W - 10, 110); } -function clearScreen() { - g.setColor(RED); - g.fillRect(0,0,W,H); -} - -function drawTitle(exercise) { - const title = exercise.humanTitle; - - g.setFont("Vector",20); - g.setColor(WHITE); - g.drawString(title, centerStringX(title), 5); -} - -function drawReps(exercise) { - const set = exercise.currentSet; +function drawSet(exercise) { + const set = exercise.currentSet(); if (set.isCompleted()) return; + g.clear(); + + // Draw exercise title g.setColor(PINK); - g.fillCircle(W / 2, H / 2, 50); + g.fillRect(15, 0, W - 15, 18); + g.setFontAlign(0, -1); + g.setFont("6x8", 1); g.setColor(WHITE); - g.setFont("Vector", 40); - g.drawString(set.reps, centerStringX(set.reps), (H - 45) / 2); - g.setFont("Vector", 15); - const note = `of ${set.maxReps}`; - g.drawString(note, centerStringX(note), (H / 2) + 25); -} - -function drawSets(exercise) { - const sets = exercise.subTitle; - + g.drawString(exercise.title, W / 2, 5); + g.setFont("6x8", 1); + g.drawString(exercise.weight + " " + exercise.unit, W / 2, 27); + // Draw completed reps counter + g.setFontAlign(0, 0); + g.setColor(PINK); + g.fillRect(15, 42, W - 15, 80); g.setColor(WHITE); - g.setFont("Vector", 15); - g.drawString(sets, centerStringX(sets), H - 25); -} + g.setFont("6x8", 5); + g.drawString(set.reps, (W / 2) + 2, (H / 2) + 1); + g.setFont("6x8", 1); + const note = `Target reps: ${set.maxReps}`; + g.drawString(note, W / 2, H - 24); + // Draw sets monitor + g.drawString(exercise.subTitle, W / 2, H - 12); -function drawSetProgress(exercise) { - drawTitle(exercise); - drawReps(exercise); - drawSets(exercise); drawMenu({showBTN1: true, showBTN2: true, showBTN3: true}); + + g.flip(); } -function drawStartNextExercise() { - const title = "Good work"; - const msg = "No need to rest\nmove straight on\nto the next exercise"; - - g.setColor(WHITE); - g.setFont("Vector", 35); - g.drawString(title, centerStringX(title), 10); - g.setFont("Vector", 15); - g.drawString(msg, 30, 150); - drawMenu({showBTN1: false, showBTN2: true, showBTN3: false}); -} - -function drawProgramCompleted() { +function drawProgDone() { const title1 = "You did"; const title2 = "GREAT!"; const msg = "That's the program\ncompleted. Now eat\nsome food and\nget plenty of rest."; clearWatch(); setWatch(Bangle.showLauncher, BTN2, {repeat: false}); - - g.setColor(WHITE); - g.setFont("Vector", 35); - g.drawString(title1, centerStringX(title1), 10); - g.setFont("Vector", 40); - g.drawString(title2, centerStringX(title2), 50); - g.setFont("Vector", 15); - g.drawString(msg, 30, 150); - drawMenu({showBTN1: false, showBTN2: true, showBTN3: false}); -} - -/* -function drawExerciseCompleted(program) { - const exercise = program.currentExercise(); - const title = exercise.canProgress? - "WELL DONE!" : - "NOT BAD!"; - const msg = exercise.canProgress? - `You weight is automatically increased\nfor ${exercise.title} to ${exercise.weight}${exercise.unit}` : - "It looks like you struggled\non a few sets, your weight will\nstay the same"; - const action = "Move straight on to the next exercise"; - - clearScreen(); - g.setColor(WHITE); - g.setFont("Vector", 20); - g.drawString(title, centerStringX(title), 10); - g.setFont("Vector", 10); - g.drawString(msg, centerStringX(msg), 180); - g.drawString(action, centerStringX(action), 210); drawMenu({showBTN2: true}); - clearWatch(); - setWatch(() => { - init(program); - }, BTN2, {repeat: false}); + g.setFontAlign(0, -1); + g.setColor(WHITE); + g.setFont("6x8", 2); + g.drawString(title1, W / 2, 10); + g.drawString(title2, W / 2, 30); + g.setFont("6x8", 1); + g.drawString(msg, (W / 2) + 3, 70); + g.flip(); +} + +function drawSetComp() { + const title = "Good work"; + const msg = "No need to rest\nmove straight on\nto the next\nexercise.Your\nweight has been\nincreased for\nnext time!"; + + g.clear(); + drawMenu({showBTN2: true}); + + g.setFontAlign(0, -1); + g.setColor(WHITE); + g.setFont("6x8", 2); + g.drawString(title, W / 2, 10); + g.setFont("6x8", 1); + g.drawString(msg, (W / 2) - 2, 45); + + g.flip(); } -*/ function drawRestTimer(program) { const exercise = program.currentExercise(); const motivation = "Take a breather.."; - clearScreen(); - drawMenu({showBTN2: true}); - - g.setColor(PINK); - g.fillCircle(W / 2, H / 2, 50); - g.setColor(WHITE); - g.setFont("Vector", 15); - g.drawString(motivation, centerStringX(motivation), 25); - g.setFont("Vector", 40); - g.drawString(exercise.restPeriod, centerStringX(exercise.restPeriod), (H - 45) / 2); - exercise.decRestPeriod(); if (exercise.restPeriod <= 0) { exercise.resetRestTimer(); - redraw(program); + program.next(); + + return; } + + g.clear(); + drawMenu({showBTN2: true}); + g.setFontAlign(0, -1); + g.setColor(PINK); + g.fillRect(15, 42, W - 15, 80); + g.setColor(WHITE); + g.setFont("6x8", 1); + g.drawString("Have a short\nrest period.", W / 2, 10); + g.setFont("6x8", 5); + g.drawString(exercise.restPeriod, (W / 2) + 2, (H / 2) - 19); + g.flip(); + + exercise.decRestPeriod(); } function redraw(program) { const exercise = program.currentExercise(); - - clearScreen(); + g.clear(); if (program.isCompleted()) { - drawProgramCompleted(program); + saveProg(program); + drawProgDone(program); return; } if (exercise.isRestTimerRunning()) { if (exercise.isLastSet()) { - drawStartNextExercise(program); + drawSetComp(program); } else { drawRestTimer(program); } @@ -179,48 +137,141 @@ function redraw(program) { return; } - drawSetProgress(exercise); + drawSet(exercise); } -function init(program) { - clearWatch(); - program.next(); -} +function drawProgMenu(programs, selProgIdx) { + g.clear(); + g.setFontAlign(0, -1); + g.setColor(WHITE); + g.setFont("6x8", 2); + g.drawString("BuffGym", W / 2, 10); -// Setup training program. This should come from file - -// Squats -function buildPrograms() { - const programsJSON = require("Storage").readJSON("buffgym-programs.json", 1); - - if (!programsJSON) throw "No programs JSON found"; - - const programs = []; - - programsJSON.forEach(programJSON => { - const program = new Program({ - title: programJSON.title, - }); - const exercises = programJSON.exercises.map(exerciseJSON => { - const exercise = new Exercise({ - title: exerciseJSON.title, - weight: exerciseJSON.weight, - unit: exerciseJSON.unit, - }); - exerciseJSON.sets.forEach(setJSON => { - exercise.addSet(new Set(setJSON)); - }); - - return exercise; - }); - program.addExercises(exercises); - programs.push(program); + g.setFont("6x8", 1); + g.setFontAlign(-1, -1); + let selectedProgram = programs[selProgIdx].title; + let yPos = 50; + programs.forEach(program => { + g.setColor("#f05a56"); + g.fillRect(0, yPos, W, yPos + 11); + g.setColor("#ffffff"); + if (selectedProgram === program.title) { + g.drawRect(0, yPos, W - 1, yPos + 11); + } + g.drawString(program.title, 10, yPos + 2); + yPos += 15; }); - - return programs; + g.flip(); } -// For this spike, just run the first program, what will -// really happen is the user picks a program to do from -// some menu on a start page. -init(buildPrograms()[0]); \ No newline at end of file +function setupMenu() { + clearWatch(); + const progs = getProgIndex(); + let selProgIdx = 0; + drawProgMenu(progs, selProgIdx); + + setWatch(()=>{ + selProgIdx--; + if (selProgIdx< 0) selProgIdx = 0; + drawProgMenu(progs, selProgIdx); + }, BTN1, {repeat: true}); + + setWatch(()=>{ + const prog = buildProg(progs[selProgIdx].file); + prog.next(); + }, BTN2, {repeat: false}); + + setWatch(()=>{ + selProgIdx++; + if (selProgIdx > progs.length - 1) selProgIdx = progs.length - 1; + drawProgMenu(progs, selProgIdx); + }, BTN3, {repeat: true}); +} + +function drawSplash() { + g.reset(); + g.setBgColor(RED); + g.clear(); + g.setColor(WHITE); + g.setFontAlign(0,-1); + g.setFont("6x8", 2); + g.drawString("BuffGym", W / 2, 10); + g.setFont("6x8", 1); + g.drawString("5x5", W / 2, 42); + g.drawString("training app", W / 2, 55); + g.drawRect(19, 38, 100, 99); + const img = require("heatshrink").decompress(atob("lkdxH+AB/I5ASQACwpB5vNFkwpBAIfNFdZZkFYwskFZAsiFZBZiVYawEFf6ETFUwsIFUYmB54ADAwIskFYoRKBoIroB4grV58kkgCDFRotWFZwqHFiwYMFZIsTC5wLDFjGlCoWlkgJDRQIABCRAsLCwodCFAIABCwIOCFQYABr4RCCQIrMC4gqEAAwpFFZosFC5ArHFQ4rFNYQrGEgosMBxIrFLQwrLAB4sFSw4rFFjYrQFi4rNbASeEFjIoJFQYsGMAgAPEQgAIGwosCRoorbA=")); + g.drawImage(img, 40, 70); + g.flip(); + + let flasher = false; + let bgCol, txtCol; + const i = setInterval(() => { + if (flasher) { + bgCol = WHITE; + txtCol = RED; + } else { + bgCol = RED; + txtCol = WHITE; + } + flasher = !flasher; + g.setColor(bgCol); + g.fillRect(0, 108, W, 120); + g.setColor(txtCol); + g.drawString("Press btn to begin", W / 2, 110); + g.flip(); + }, 250); + + setWatch(()=>{ + clearInterval(i); + setupMenu(); + }, BTN1, {repeat: false}); + + setWatch(()=>{ + clearInterval(i); + setupMenu(); + }, BTN2, {repeat: false}); + + setWatch(()=>{ + clearInterval(i); + setupMenu(); + }, BTN3, {repeat: false}); +} + +function getProgIndex() { + const progIdx = require("Storage").readJSON("buffgym-program-index.json"); + return progIdx; +} + +function buildProg(fName) { + const Set = require("buffgym-set.js"); + const Exercise = require("buffgym-exercise.js"); + const Program = require("buffgym-program.js"); + const progJSON = require("Storage").readJSON(fName); + const prog = new Program({ + title: progJSON.title, + }); + const exercises = progJSON.exercises.map(exerciseJSON => { + const exercise = new Exercise({ + title: exerciseJSON.title, + weight: exerciseJSON.weight, + unit: exerciseJSON.unit, + restPeriod: exerciseJSON.restPeriod, + }); + exerciseJSON.sets.forEach(setJSON => { + exercise.addSet(new Set(setJSON)); + }); + + return exercise; + }); + prog.addExercises(exercises); + + return prog; +} + +function saveProg(program) { + const fName = getProgIndex().find(prog => prog.title === program.title).file; + require("Storage").writeJSON(fName, program.toJSON()); +} + +drawSplash(); \ No newline at end of file diff --git a/apps/buffgym/buffgym.png b/apps/buffgym/buffgym.png old mode 100644 new mode 100755 index 93a29a4a686f172e5e6dbd2ecb2a1d3edf4751c0..9bde64cc4b4a6cbea919ae5d61fe2f01694da589 GIT binary patch literal 7576 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 Date: Tue, 21 Apr 2020 20:10:56 +0100 Subject: [PATCH 222/302] Add README --- apps/buffgym/README.md | 43 +++++++++++++++++++++++++++++++++ apps/buffgym/buffgym-scrn1.png | Bin 0 -> 4141 bytes apps/buffgym/buffgym-scrn2.png | Bin 0 -> 2801 bytes apps/buffgym/buffgym-scrn3.png | Bin 0 -> 3569 bytes apps/buffgym/buffgym-scrn4.png | Bin 0 -> 3299 bytes apps/buffgym/buffgym-scrn5.png | Bin 0 -> 4500 bytes apps/buffgym/buffgym-scrn6.png | Bin 0 -> 3912 bytes 7 files changed, 43 insertions(+) create mode 100644 apps/buffgym/README.md create mode 100755 apps/buffgym/buffgym-scrn1.png create mode 100755 apps/buffgym/buffgym-scrn2.png create mode 100755 apps/buffgym/buffgym-scrn3.png create mode 100755 apps/buffgym/buffgym-scrn4.png create mode 100755 apps/buffgym/buffgym-scrn5.png create mode 100755 apps/buffgym/buffgym-scrn6.png diff --git a/apps/buffgym/README.md b/apps/buffgym/README.md new file mode 100644 index 000000000..e9e217828 --- /dev/null +++ b/apps/buffgym/README.md @@ -0,0 +1,43 @@ +# BuffGym + +This gym training assistant trains you on the famous [Stronglifts 5x5 workout](https://stronglifts.com/5x5) program. + +## Usage + +When you start the app it will wait on a splash screen until you are ready to start the work out. Press any of the buttons to start + +![](buffgym-scrn1.png) + +You are then presented with the programs menu, use BTN1 to move up the list, and BTN3 to move down the list. Once you have made your selection, press BTN2 to select the program. + +![](buffgym-scrn2.png) + +You will now begin moving through the exercises in the program. You will see the exercise information on the display. +1. At the top is the exercise name, e.g 'Squats' +2. Next is the weight you must train +3. In the center is where you record the number of *reps* you completed (more on that shortly) +4. Below the *reps* value, is the target reps you must try to reach. +5. Below the target reps is the current set you are training, out of the total sets for the exercise. +6. The *reps* value is used to store what you achieved for the current set, you enter this after you have trained on your current set. To alter this value, use BTN1 to increase the value (it will stop at the maximum required reps) and BTN3 to decreas the value to a minimum of 0 (this is the default value). Pressing BTN2 will confirm your reps + +![](buffgym-scrn3.png) + +You will then be presented with a rest timer screen, it counts down and automatically moves to the next exercise when it reaches 0. You can cancel the timer early if you wish by pressing BTN2. If it is the last set of an exercise, you don't need to rest, so it lets you know you have completed all the sets in the exercise and can start the next exercise. + +![](buffgym-scrn4.png) +![](buffgym-scrn5.png) + +Once all exercises are done, you are presented with a pat-on-the-back screen to tell you how awesome you are. + +![](buffgym-scrn6.png) + +## Features + +* If you successfully complete all reps and sets for an exercise, it will automatically update your weights for next time +* Has a neat rest timer to make sure you are training optimally +* Doesn't require a mobile phone, most 'smart watches' are just a visual presentation of the mobile phone app, this runs purley on the watch. So why not leave your phone and its distractions out of the gym! +* Clear and simple user interface + +## Created by + +[Paul Cockrell](https://github.com/paulcockrell) April 2020. \ No newline at end of file diff --git a/apps/buffgym/buffgym-scrn1.png b/apps/buffgym/buffgym-scrn1.png new file mode 100755 index 0000000000000000000000000000000000000000..07b79386f1503317acee9ff894d8dcf4c0035bc4 GIT binary patch literal 4141 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 literal 0 HcmV?d00001 diff --git a/apps/buffgym/buffgym-scrn2.png b/apps/buffgym/buffgym-scrn2.png new file mode 100755 index 0000000000000000000000000000000000000000..ec70fb79110bb029f286b66194fea8be4fe50e28 GIT binary patch literal 2801 zcmeHJT~t%q6~6c81_G&qM!*3faZNQ;$Kps?APU64A}vE?AV8rImeMlH@MAS3CI33M zqYDUQDJ}(*ahQUVhJb>^0Lf)hfhARHF+@}#%M1uyNPv(Z5<+@yANtg{K6I_l!#U^S z?C(4KoW1wA&(%{YiJN?beE|U2lyvg=r`Wpmeh|E{Ii4|h9RPe)Nym?*bMh7^KED3l zz!vc;_?1rXtTOE$w@lwq zGKKDX&lXDA&&3fCZ7Sa+4ZI@|=0SMw5pGLAEXyFI`s&)}D#jzyc76FHFaLGp$ zBzp328kK@P1H>Uj!ctZA$O|7rTjYNM##{No;WRU|Lp>I|u*1ZR6BHdN$@?({8NTXy zlg~mjB*FtX%CCq38Si+8_vcmz^}<;yqL0PNaStoji_YG9IxR&ea1QmAiIGaV1VJmR zrBma^%)CUUa^Aj*LOl++A+)U#_kw>UPVITS_{hBk^>B!I&c{przYj7z_NIw~a0xue zu(!%p_h;W^R&T5ygKytgXpdYUH?rMn+78XiZ*%FLG99l;Ni1_j{4Cx3o9;eFehV(! zidHixm{xAD)T^mQ$!l@PR`lk|Om65SL>L>%psBr+MQQD1J+TcSE|KBG;{Kq9`F*FN zc%9%JsVQfwSDQWcoZg``hQAX-?i2RCZi*pKhx1gc!weZ$JVl1p*;Nj0(!%*%vz^%!Aa3~-}7^U>M(7v z(iM6lnx=DhTTq*F;9pcWxFk-DcPxDA96YJOWf#-!0cx&Lr*%~uXP05sUbRg?&!-#4 zhz)>pQvv%T7-+~4$#G-+!RP!VoJHYk;TzNXUWrbR&%%80gPiWMQKeHapJwH%+tB^KBMlz(>hx@?mh39_RmaohvYJt2rt7&? zIe@lyRe(uK_PcK9#<0-K1YPv20X$hMbku4#l?EdnnnP1MJF(0Xu`7M*Qn_sVoL8pz z%IvZnb9!s6O^v;@AH9}ZW@EBLL-g9}@<)I&uWebmtE@BV%c8j#54IRy4TTc%a7=%I z;jeU7I0gCOYoJijats8Y0r(h8+9&zxG*RMO6t%{OP&U(i3%M%-Br8QJ6p}v%h!UrQ zVA!<2fwr51+?1Vz8X@TL6Xij1GEEejpo4#khdH&(3(!r`ds3fDU&2zjb_PCxv9G6- zwg>ndQUy*xeL1)qxrr%N)5AP?b6{4W!Hg?S(HAaWX5Zg# zI+LK1p&E5PpbXRuIUwFwRWY34VO)>(ny2FHhZ~;33i?bA0_YI2j@`k($*))LDqj2>rsu%SaG+Tkeg$b4Fn) z^{(r0IvWf3A@aw&C>JgXG0g8nC#fDZhO6z=I13FX;uC5ATPf;8SgIFz1sc#R#g8lN z@SMj5lI^2i_Y=c|je8(yoljMlK2qKn&n$l~9>hDwUl&`=SRR`nHjY`s#ah^rD(}jI z%sUO4Kbks2R=m9f30}JIgp`h(cVbrUl8oh3ASjp}$gyz=geC4mBjYeuEM)a^ojVMp z4|9DM;1}RnS!8-v^*&_vS{+{qf*Ma)cHd<@?C7g6owEn|8(ii)0f)0NT+dpa;NJ$- zeoK=~A#cC)?|-r|pKv?-@Q!nbu0Kl@rS;|LFzvNwLL<&Ir^&C|?fIqd1#~b*r_Ss0 zEF_f7BrcW&AQ$gL>%qxj%1Et~gl{Do6x3f@kIgEx5+}6mc-^lVmV2$4#!`h){!HRv zorvIT0k%w~%XwkK!p)4OWXj&C0W%-^Ry^yqyH{A0J6E`{8gZ;gFs6)`bGI_%>bqyC zNMam_=jg>3M%y8*yVwH+R#J>RP`%PR!|bJ{>*X7|L)T2!k_5X1t9kB zL@e3M=Hv_#b0k literal 0 HcmV?d00001 diff --git a/apps/buffgym/buffgym-scrn3.png b/apps/buffgym/buffgym-scrn3.png new file mode 100755 index 0000000000000000000000000000000000000000..0888fc507c229f1eb1b757770531a56ffa63b63e GIT binary patch literal 3569 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 DipYXY literal 0 HcmV?d00001 diff --git a/apps/buffgym/buffgym-scrn4.png b/apps/buffgym/buffgym-scrn4.png new file mode 100755 index 0000000000000000000000000000000000000000..3078d25dce7b2b114c17804571e16c858aaf2d7f GIT binary patch literal 3299 zcmeH~X;4#H7RSTO6M|w4FcAR*aRC((5k#D@7(g}`pxJ^5Bx<)oY(bHL>^zsYU2#Ag zv;`S)5E3BTfRF@8zy)PT36Uibki||Q1n2+)N#@zse3+@44>M&=)lAKY_g=lKbN}bu zbL#)Qb+7n%xvyMqupEIvtn~EQ>kEHnpC6Pu{B{iZH3NarqI>Rj@;?_gk>ix1`$lK) zWkdP-W$r^BYp*O@5xMhNSKbbZ)m2$x%pI!Pz(6wNw)=3?IWw%z^T(4>Q7Ywg6&?YZ zXeK#}OxkV~tdn^mAbk*JegFaKIobzbMgZ1GOi(rg*q(H*9IXZ(SI0lGR0G52W#Kpg z)vQIN905?(vcV1lh{@JZ%1c9&p?l;a3sg)UKaX;pgH zx-?!h0$gpH##>=*RN{hw?yCKLLLI2mcwj)FSXy!oP8B>nw8%cGLM7?_`cl$t)>xb@ zr1dqSLU8^xg+(4Sm>O7HHcJz%#mhn;=`pPtn?L`Z;a2m~k&b|aV^hF|)3Ko~+=RTc z;Ro`m_k!-Sr#-4aO-|9MW3*SaXH+?3j9i0;Qgxk0>G?xC>d@9@4W-*RovsL#=gf`c zxa>^(QZlwyOs)yzGo#r|W*6sNuCRmQO%$_8A%UEmJT7;2{A?OqBVSxe7bGWF7(rWy z+E}&sd1Bl@0JKp(wx*ujwAaEPy8GZUitF>#zc5^Jv~*KN^fif_1%REr6qvgbO7?MeGbio^Q`lkR&Xm>MpOTA7Vg0 zFZ~4p&M{2#1oyBzHorGLv8LzoNfl$bx|m0L0JY_I(A5YWWihq0BmEGphC1L*xoanj84gDQ zx)`FU&ZW>X82!q7ahfxi99MIq&e*_3RF~O9dm`?kWj$F5;rY(Fe0{}> zGsBFO`ayJYZc-Euc(9aAfhT~c4Nx#un+n(qBglG>VV|b3QBTw z7)p3<>(h9b_gs>q(ST}wJN6fjvW}G?bU&xuikTwK0vCnQI7m zCs<}yT;gToP!q&F*e`bFK`Wp)D7@@j4QD&dM|dXaZC%b3D*$ER4Yi{|*o##z*D5gC z*AaksEWR8G!OoSoAKeC;Zh#g4_hc}-^Aj_@-);`q06u^z%K1MUz4ai9)Xjf#&V!qc zU03QtISMytYOvh^Gy_l}+Cv1;6y}evF}lR*pM>lSRn2bm`y>$@5DI5cKD^E|y)~OH zR8Oc>+*4PF4&odWn@>6Ghh}u{jc1)0=V-dEp-ujjW;h zP*twCd&m*oh)}txQ$dd}puy5x8VWDmF<29qDz(R!2a{U%=eTzm0}PFi!H>~_5tKd! zYo_z8kAxmyG(!}T!GcfeJw<<85U{(V^j;QW73fBxxeW-4E-=yMcHYA4u#DdvWAtv< zly(p3d-EHOtu~fYWna8m_?LJouMgtukD!zqAtxeJkxx&mf= z$Les|c25ZTeGm&tDyzo@)mT63pAy2w&@~f7NWYgOLO0`raym?;JbE_$w6uEl_|(g# zEx+x49#%h&{2-00tnS~b#dH%Lx!8i3Bic4)H3=0HKaqf03nFEE8==Lao%XVSd0Z-I zL>|8CKN7bFJ3rAdH}^wIigLZptL;zwklT(#nMco69u7lR9NPleZFbz?f?J|?23q-X zV|-bU7X04p3qr6}iDSv-@MazCX+C`WIreS5S(bllWUH$YFrXD;@fb~C8y=Revv(vz z+;*LH)kSpGN6aP^7Abwt7l!yMgh@uIBoNEZocHV$_ADj{q%B&}e4?Vp-6?I|A{4H;f1YvGMVi}Mg~3So(G&#HW_pibrn}>1h@G89CPi_>{uy7st0LT*=&_Ufis<^!u7w) zF1k{{$d;>#d3^f{&wp7?4;zFte}`W(&NS$R4Nb63d41$7nE2nJMaCM`Mil<^d4kP4 z3XCz+TQ7)sCj_M1S){3^s|JH+qdK4opzh~+9YkfLV4`f?f$@M31CSQs%-f5A8Fd9B zo<{(i3jK5eQ=k|1{gA5`_{?QdiF#eu|B09E2|~2v1k9 Ky`|2S^M3`WxUwt& literal 0 HcmV?d00001 diff --git a/apps/buffgym/buffgym-scrn5.png b/apps/buffgym/buffgym-scrn5.png new file mode 100755 index 0000000000000000000000000000000000000000..b34a5b124aba6acacdcd671c2401563958faa509 GIT binary patch literal 4500 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 HT+Y7%cOZEH literal 0 HcmV?d00001 From a49397ed3dac75b38e3fd3537fe080d2a661a5a4 Mon Sep 17 00:00:00 2001 From: Paul Cockrell Date: Tue, 21 Apr 2020 20:11:32 +0100 Subject: [PATCH 223/302] Disable emulator --- apps.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apps.json b/apps.json index 4eeee4e32..139192339 100644 --- a/apps.json +++ b/apps.json @@ -1302,6 +1302,7 @@ "description": "BuffGym is the famous 5x5 workout program for the BangleJS", "tags": "tool,outdoors", "type": "app", + "allow_emulator": false, "storage": [ {"name":"buffgym"}, {"name":"buffgym.app.js", "url": "buffgym.app.js"}, From 5f19c8bd80688b8f78c139b7185b5e19b2812f5d Mon Sep 17 00:00:00 2001 From: jeffmer Date: Tue, 21 Apr 2020 20:22:26 +0100 Subject: [PATCH 224/302] Update apps.json --- apps.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apps.json b/apps.json index 0b7001a5e..8df240cf6 100644 --- a/apps.json +++ b/apps.json @@ -297,6 +297,7 @@ "version":"0.01", "description": "Displays GPS Course and Speed, + Directions to waypoint and waypoint recording", "tags": "tool,outdoors,gps", + "readme": "README.md", "storage": [ {"name":"gpsnav.app.js","url":"app.js"}, {"name":"waypoints.json","url":"waypoints.json","evaluate":false}, From b5fd14b4fb39e313544758f35950d9068f738fe0 Mon Sep 17 00:00:00 2001 From: Paul Cockrell Date: Tue, 21 Apr 2020 20:45:02 +0100 Subject: [PATCH 225/302] Add training programs to apps file --- apps.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps.json b/apps.json index 139192339..4b51a4689 100644 --- a/apps.json +++ b/apps.json @@ -1309,7 +1309,9 @@ {"name":"buffgym-set.js","url":"buffgym-set.js"}, {"name":"buffgym-exercise.js","url":"buffgym-exercise.js"}, {"name":"buffgym-program.js","url":"buffgym-program.js"}, - {"name":"buffgym-programs.json","url":"buffgym-programs.json"}, + {"name":"buffgym-program-a.json","url":"buffgym-program-a.json"}, + {"name":"buffgym-program-b.json","url":"buffgym-program-b.json"}, + {"name":"buffgym-program-index.json","url":"buffgym-program-index.json"}, {"name":"buffgym.img","url":"buffgym-icon.js","evaluate":true} ] } From 7e305033814aa2946c68686d82e94f85e70cbf6f Mon Sep 17 00:00:00 2001 From: Paul Cockrell Date: Tue, 21 Apr 2020 20:46:30 +0100 Subject: [PATCH 226/302] Add readme to apps.json --- apps.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apps.json b/apps.json index 4b51a4689..d800050ee 100644 --- a/apps.json +++ b/apps.json @@ -1303,6 +1303,7 @@ "tags": "tool,outdoors", "type": "app", "allow_emulator": false, + "readme": "README.md", "storage": [ {"name":"buffgym"}, {"name":"buffgym.app.js", "url": "buffgym.app.js"}, From 14e209b020c0eca0e54fc591c378c8a52f1fab4b Mon Sep 17 00:00:00 2001 From: Paul Cockrell Date: Tue, 21 Apr 2020 20:47:45 +0100 Subject: [PATCH 227/302] Add tags to buffgym app in apps.json --- apps.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps.json b/apps.json index d800050ee..96f758c07 100644 --- a/apps.json +++ b/apps.json @@ -1300,7 +1300,7 @@ "icon": "buffgym.png", "version":"0.01", "description": "BuffGym is the famous 5x5 workout program for the BangleJS", - "tags": "tool,outdoors", + "tags": "tools,outdoors,gym,exercise", "type": "app", "allow_emulator": false, "readme": "README.md", From bb9747da0924adbef19929b29c833bdc707fd70d Mon Sep 17 00:00:00 2001 From: Paul Cockrell Date: Tue, 21 Apr 2020 20:51:15 +0100 Subject: [PATCH 228/302] Add app comments --- apps/buffgym/buffgym.app.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/apps/buffgym/buffgym.app.js b/apps/buffgym/buffgym.app.js index eeabd5c29..4dc6ffd5a 100755 --- a/apps/buffgym/buffgym.app.js +++ b/apps/buffgym/buffgym.app.js @@ -1,3 +1,14 @@ +/** + * BangleJS Stronglifts 5x5 training aid + * + * Original Author: Paul Cockrell https://github.com/paulcockrell + * Created: April 2020 + * + * Inspired by: + * - Stronglifts 5x5 training program https://stronglifts.com/5x5/ + * - Stronglifts smart watch app + */ + Bangle.setLCDMode("120x120"); const W = g.getWidth(); From 94bb31889cba0b31be983a6dcfda7d506c367fa0 Mon Sep 17 00:00:00 2001 From: Paul Cockrell Date: Tue, 21 Apr 2020 21:14:18 +0100 Subject: [PATCH 229/302] Update apps.json --- apps.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps.json b/apps.json index 96f758c07..65f0090b3 100644 --- a/apps.json +++ b/apps.json @@ -1300,7 +1300,7 @@ "icon": "buffgym.png", "version":"0.01", "description": "BuffGym is the famous 5x5 workout program for the BangleJS", - "tags": "tools,outdoors,gym,exercise", + "tags": "tool,outdoors,gym,exercise", "type": "app", "allow_emulator": false, "readme": "README.md", From 26d8855ea20bba5d7833fe331d4696f15d602e98 Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Tue, 21 Apr 2020 23:13:04 +0200 Subject: [PATCH 230/302] locale: Measure temperature in Kelvin Because that is what Gadgetbridge sends for the weather --- apps.json | 2 +- apps/locale/ChangeLog | 1 + apps/locale/locale.html | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps.json b/apps.json index 1cacc920e..95a845f54 100644 --- a/apps.json +++ b/apps.json @@ -65,7 +65,7 @@ { "id": "locale", "name": "Languages", "icon": "locale.png", - "version":"0.06", + "version":"0.07", "description": "Translations for different countries", "tags": "tool,system,locale,translate", "type": "locale", diff --git a/apps/locale/ChangeLog b/apps/locale/ChangeLog index 3d983150d..a3cc0c9a3 100644 --- a/apps/locale/ChangeLog +++ b/apps/locale/ChangeLog @@ -6,3 +6,4 @@ Add correct scaling for speed/distance/temperature 0.06: Remove translations if not required Ensure 'on' is always supplied for translations +0.07: Measure temperature in Kelvin diff --git a/apps/locale/locale.html b/apps/locale/locale.html index 21bf37f29..936d2e9f0 100644 --- a/apps/locale/locale.html +++ b/apps/locale/locale.html @@ -112,8 +112,8 @@ exports = { name : "en_GB", currencySym:"£", `${js(locale.currency_symbol)} + n.toFixed(2)`: `n.toFixed(2) + ${js(locale.currency_symbol)}`; var temperature; - if (locale.temperature=='°C') temperature="t"; - else if (locale.temperature=='°F') temperature="(t*9/5)+32"; + if (locale.temperature=='°C') temperature="(t-273.15)"; + else if (locale.temperature=='°F') temperature="((t-273.15)*9/5)+32"; else throw new Error("Unknown temperature unit "+locale.temperature); var localeModule = `var l = ${JSON.stringify({ From 319307cdd1831b27a94b6a9150cf24e24379ae22 Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Tue, 21 Apr 2020 23:18:21 +0200 Subject: [PATCH 231/302] weather: Show Gadgetbridge weather reports Based on http://forum.espruino.com/comments/15194626/, where NebbishHacker did most of the actual work :-) --- apps.json | 17 ++++ apps/weather/app.js | 54 +++++++++++ apps/weather/icon.js | 1 + apps/weather/icon.png | Bin 0 -> 1173 bytes apps/weather/lib.js | 176 ++++++++++++++++++++++++++++++++++++ apps/weather/readme.md | 16 ++++ apps/weather/screenshot.png | Bin 0 -> 4039 bytes apps/weather/widget.js | 40 ++++++++ 8 files changed, 304 insertions(+) create mode 100644 apps/weather/app.js create mode 100644 apps/weather/icon.js create mode 100644 apps/weather/icon.png create mode 100644 apps/weather/lib.js create mode 100644 apps/weather/readme.md create mode 100644 apps/weather/screenshot.png create mode 100644 apps/weather/widget.js diff --git a/apps.json b/apps.json index 95a845f54..3921e8ae1 100644 --- a/apps.json +++ b/apps.json @@ -348,6 +348,23 @@ {"name":"files.img","url":"files-icon.js","evaluate":true} ] }, + { "id": "weather", + "name": "Weather", + "icon": "icon.png", + "version":"0.01", + "description": "Show Gadgetbridge weather report", + "readme": "readme.md", + "tags": "widget,outdoors", + "storage": [ + {"name":"weather.app.js","url":"app.js"}, + {"name":"weather.wid.js","url":"widget.js"}, + {"name":"weather","url":"lib.js"}, + {"name":"weather.img","url":"icon.js","evaluate":true} + ], + "data": [ + {"name": "weather.json"} + ] + }, { "id": "widbat", "name": "Battery Level Widget", "icon": "widget.png", diff --git a/apps/weather/app.js b/apps/weather/app.js new file mode 100644 index 000000000..fdf28f0e0 --- /dev/null +++ b/apps/weather/app.js @@ -0,0 +1,54 @@ +(() => { + function draw(w) { + g.reset(); + g.setColor(0).fillRect(0, 24, 239, 239); + + require('weather').drawIcon(w.txt, 65, 90, 55); + const locale = require("locale"); + + g.setColor(-1); + + const temp = locale.temp(w.temp).match(/^(\D*\d*)(.*)$/); + let width = g.setFont("Vector", 40).stringWidth(temp[1]); + width += g.setFont("Vector", 20).stringWidth(temp[2]); + g.setFont("Vector", 40).setFontAlign(-1, -1, 0); + g.drawString(temp[1], 180-width/2, 70); + g.setFont("Vector", 20).setFontAlign(1, -1, 0); + g.drawString(temp[2], 180+width/2, 70); + + g.setFont("6x8", 1); + g.setFontAlign(-1, 0, 0); + g.drawString("Humidity", 135, 130); + g.drawString("Wind", 135, 142); + g.setFontAlign(1, 0, 0); + g.drawString(w.hum+"%", 225, 130); + g.drawString(locale.speed(w.wind), 225, 142); + + g.setFont("6x8", 2).setFontAlign(0, 0, 0); + g.drawString(w.loc, 120, 170); + + g.setFont("6x8", 1).setFontAlign(0, 0, 0); + g.drawString(w.txt.charAt(0).toUpperCase()+w.txt.slice(1), 120, 190); + + g.flip(); + } + + const _GB = global.GB; + global.GB = (event) => { + if (event.t==="weather") draw(event); + if (_GB) setTimeout(_GB, 0, event); + }; + + Bangle.loadWidgets(); + Bangle.drawWidgets(); + + const weather = require('weather').load(); + if (weather) { + draw(weather); + } else { + E.showMessage('Weather unknown\n\nIs Gadgetbridge\nconnected?'); + } + + // Show launcher when middle button pressed + setWatch(Bangle.showLauncher, BTN2, {repeat: false, edge: 'falling'}) +})() diff --git a/apps/weather/icon.js b/apps/weather/icon.js new file mode 100644 index 000000000..18ca2b0c9 --- /dev/null +++ b/apps/weather/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AE8N6AXV7vdFyoXBGCQUBAAoXp73u93tC6YWBAAIXSFwQwDRiAWDGASSBmYABIx5IDgYXCmC7KCoYRBnvUCwQwKC4gSEAAgwKC5gwKCwPjC6inBC6owBC6wVKPBXd6YXMDBAuNJJQXWfwZITC/6QIBw073ezR6m73anOJAwuHeBAYFIw4WJAAsL3YQOAAxeBCiWIC4e72AJChGACpMIxAXBIwIwEBIIXKBgouDEIYuLC4ghEC6ELLoYXPU4YXFLxQNBBgcLEQqqQFwYA/AH4AYA==")) diff --git a/apps/weather/icon.png b/apps/weather/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..bbfc0ace0b8d25c4afefcb336ea04df799a70f22 GIT binary patch literal 1173 zcmV;G1Zw+fgKelF z+6!B~5F)8y)e1%MqEWnPiWg$RgkaGND+=ED(@M!jy(#RCC2ci@HZ&m;H8wSEeq1-3 zna_*eO*h%I$(hL}OJN@v*!|9#@4WBKnQzV+C{Un)Z={DV>E%eib@(DFUXn-NOejAm zNe^8p4lfCJt4`j{fhJF^Xh3?ARSnST?;@FEt!47{!1jEGWkFpPY~BdvcO+sr|AoPp7;j(Z^c9nYrv8 zkRG}g{?t`1FFr+i3Dj4?(4UDMy&OqZr_)1HX<73JfL%aC@{G&CN!PWHJ+npq?bSfm z3XxGNw?6LPs7g_(j(pdPtX6NkmEpEmb0c6>eVOf?QhKnNuV{Lt@F=40(L-~ zBr4U6#P%`av@$0Zg!qTMN57+X49^FVjkbLQP)KEm&RPQbv6&-%R5y%N$)W|^G z3HAX4eumxfNt%QZOugspBgMdEG}xzWFfy>SLB*_~-Ht~Sp@@Yn(5USLDV>K=tk6T&Gr@s zpI_!oxJY!Hg>_~@60*CA@DI5jXU)oJfI#||^z>SYhgIBx&7a<=4-nV|vaT9J`>ZY?00000NkvXXu0mjfi9tF% literal 0 HcmV?d00001 diff --git a/apps/weather/lib.js b/apps/weather/lib.js new file mode 100644 index 000000000..f87984fe5 --- /dev/null +++ b/apps/weather/lib.js @@ -0,0 +1,176 @@ +exports = { + save: weather => { + let json = require('Storage').readJSON('weather.json')||{} + json.weather = Object.assign({}, weather) // don't mutate GB events + delete json.weather.t // don't save the event type (if present) + require('Storage').write('weather.json', json) + }, + load: () => { + let json = require('Storage').readJSON('weather.json')||{} + return json.weather + }, + drawIcon: (cond, x, y, r) => { + function drawSun(x, y, r) { + g.setColor("#FF7700"); + g.fillCircle(x, y, r); + } + + function drawCloud(x, y, r, c) { + const u = r/12; + if (c==null) c = "#EEEEEE"; + g.setColor(c); + g.fillCircle(x-8*u, y+3*u, 4*u); + g.fillCircle(x-4*u, y-2*u, 5*u); + g.fillCircle(x+4*u, y+0*u, 4*u); + g.fillCircle(x+9*u, y+4*u, 3*u); + g.fillPoly([ + x-8*u, y+7*u, + x-8*u, y+3*u, + x-4*u, y-2*u, + x+4*u, y+0*u, + x+9*u, y+4*u, + x+9*u, y+7*u, + ]); + } + + function drawBrokenClouds(x, y, r) { + drawCloud(x+1/8*r, y-1/8*r, 7/8*r, "#777777"); + drawCloud(x-1/8*r, y+1/8*r, 7/8*r); + } + + function drawFewClouds(x, y, r) { + drawSun(x+3/8*r, y-1/8*r, 5/8*r); + drawCloud(x-1/8*r, y+1/8*r, 7/8*r); + } + + function drawRainLines(x, y, r) { + g.setColor("#FFFFFF"); + const y1 = y+1/2*r; + const y2 = y+1*r; + g.fillPoly([ + x-6/12*r+1, y1, + x-8/12*r+1, y2, + x-7/12*r, y2, + x-5/12*r, y1, + ]); + g.fillPoly([ + x-2/12*r+1, y1, + x-4/12*r+1, y2, + x-3/12*r, y2, + x-1/12*r, y1, + ]); + g.fillPoly([ + x+2/12*r+1, y1, + x+0/12*r+1, y2, + x+1/12*r, y2, + x+3/12*r, y1, + ]); + } + + function drawShowerRain(x, y, r) { + drawFewClouds(x, y-1/3*r, r); + drawRainLines(x, y, r); + } + + function drawRain(x, y, r) { + drawBrokenClouds(x, y-1/3*r, r); + drawRainLines(x, y, r); + } + + function drawThunderstorm(x, y, r) { + function drawLightning(x, y, r) { + g.setColor("#FF7700"); + g.fillPoly([ + x-2/6*r, y-r, + x-4/6*r, y+1/6*r, + x-1/6*r, y+1/6*r, + x-3/6*r, y+1*r, + x+3/6*r, y-1/6*r, + x+0/6*r, y-1/6*r, + x+3/6*r, y-r, + ]); + } + + drawBrokenClouds(x, y-1/3*r, r); + drawLightning(x-1/12*r, y+1/2*r, 1/2*r); + } + + function drawSnow(x, y, r) { + function rotatePoints(points, pivotX, pivotY, angle) { + for(let i = 0; i {}; + condition = condition.toLowerCase(); + if (condition.includes("thunderstorm")) return drawThunderstorm; + if (condition.includes("freezing")||condition.includes("snow")|| + condition.includes("sleet")) { + return drawSnow; + } + if (condition.includes("drizzle")|| + condition.includes("shower")) { + return drawRain; + } + if (condition.includes("rain")) return drawShowerRain; + if (condition.includes("clear")) return drawSun; + if (condition.includes("few clouds")) return drawFewClouds; + if (condition.includes("scattered clouds")) return drawCloud; + if (condition.includes("clouds")) return drawBrokenClouds; + return drawMist; + } + + chooseIcon(cond)(x, y, r) + }, +} diff --git a/apps/weather/readme.md b/apps/weather/readme.md new file mode 100644 index 000000000..a326b67dd --- /dev/null +++ b/apps/weather/readme.md @@ -0,0 +1,16 @@ +# Weather + +Shows Gadgetbridge weather reports. + +This adds a widget with a weather pictogram and the temperature. +You can view the full report through the app: +![Screenshot](screenshot.png) + +## Setup + +See [this guide](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Weather) +to setup Gadgetbridge weather reporting. + +## Controls + +BTN2: opens the launcher diff --git a/apps/weather/screenshot.png b/apps/weather/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..ce79b3b646bfebdad6c29f57779c018ce0ce457e GIT binary patch literal 4039 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 Fe*nCRR)+up literal 0 HcmV?d00001 diff --git a/apps/weather/widget.js b/apps/weather/widget.js new file mode 100644 index 000000000..7488b7657 --- /dev/null +++ b/apps/weather/widget.js @@ -0,0 +1,40 @@ +(() => { + function draw() { + const w = require('weather').load() + if (!w) return; + g.reset(); + g.setColor(0).fillRect(this.x, this.y, this.x+this.width, this.y+24) + if (w.txt) { + require('weather').drawIcon(w.txt, this.x+10, this.y+8, 8); + } + if (w.temp) { + let t = require('locale').temp(w.temp); // applies conversion + t = t.substr(0, t.length-2); // but we have no room for units + g.setFontAlign(0, 1); // center horizontally at bottom of widget + g.setFont('6x8', 1); + g.setColor(-1) + g.drawString(t, this.x+10, this.y+24) + } + } + + function update(weather) { + require('weather').save(weather); + if (!WIDGETS["weather"].width) { + WIDGETS["weather"].width = 20 + Bangle.drawWidgets() + } else if (Bangle.isLCDOn()) { + WIDGETS["weather"].draw() + } + } + + const _GB = global.GB; + global.GB = (event) => { + if (event.t==="weather") update(event); + if (_GB) setTimeout(_GB, 0, event); + }; + + WIDGETS["weather"] = {area: "tl", width: 20, draw: draw}; + if (!require('weather').load()) { + WIDGETS["weather"].width = 0 + } +})(); From 06fc54f83171cf175af359e2113cc239e24cad4c Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Wed, 22 Apr 2020 08:56:50 +0100 Subject: [PATCH 232/302] oops - fix sanity check errors --- apps.json | 3 +-- apps/buffgym/buffgym-icon.js | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/apps.json b/apps.json index 70fb6cac7..84b8fb643 100644 --- a/apps.json +++ b/apps.json @@ -1332,7 +1332,6 @@ "allow_emulator": false, "readme": "README.md", "storage": [ - {"name":"buffgym"}, {"name":"buffgym.app.js", "url": "buffgym.app.js"}, {"name":"buffgym-set.js","url":"buffgym-set.js"}, {"name":"buffgym-exercise.js","url":"buffgym-exercise.js"}, @@ -1438,7 +1437,7 @@ { "id": "osmpoi", "name": "POI Compass", "icon": "app.png", - "version":"0.01", + "version":"0.02", "description": "Uploads all the points of interest in an area onto your watch, same as Beer Compass with more p.o.i.", "tags": "tool,outdoors,gps", "custom": "osmpoi.html", diff --git a/apps/buffgym/buffgym-icon.js b/apps/buffgym/buffgym-icon.js index 31764acbb..523ed35b6 100644 --- a/apps/buffgym/buffgym-icon.js +++ b/apps/buffgym/buffgym-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mEwxH+ACPI5AUSADAtB5vNGFQtBAIfNF95hoF4wwoF5AwmF5BhmXYbAEF/6QbF1QwIF04qB54ADAwIwoF4oRKBoIvsB4gvZ58kkgCDFxoxaF5wuHGDQcMF5IwXDZwLDGDmlDIWlkgJDSwIABCRAwPDQohCFgIABDQIOCFwYABr4RCCQIvQDYguEAAwtFF5owJDZAvHFw4vFOYQvKFAowMBxIvFMQwvPAB4wFUQ4vJGDYvUGC4vNdgyuEGDIsNFwYwGNAgAPExAvMGIdfTIovfTpYvrfRCOkZ44ugF44NGF05gUFyQvKGIoueGKIufGJ4uhG5oupGItfr4vvAAgvlGAQvt/wrEF9oEGF841IF9QGHX0oGIAD8kAAYJOFzwEBBQoMFACA=")); \ No newline at end of file +require("heatshrink").decompress(atob("mEwxH+ACPI5AUSADAtB5vNGFQtBAIfNF95hoF4wwoF5AwmF5BhmXYbAEF/6QbF1QwIF04qB54ADAwIwoF4oRKBoIvsB4gvZ58kkgCDFxoxaF5wuHGDQcMF5IwXDZwLDGDmlDIWlkgJDSwIABCRAwPDQohCFgIABDQIOCFwYABr4RCCQIvQDYguEAAwtFF5owJDZAvHFw4vFOYQvKFAowMBxIvFMQwvPAB4wFUQ4vJGDYvUGC4vNdgyuEGDIsNFwYwGNAgAPExAvMGIdfTIovfTpYvrfRCOkZ44ugF44NGF05gUFyQvKGIoueGKIufGJ4uhG5oupGItfr4vvAAgvlGAQvt/wrEF9oEGF841IF9QGHX0oGIAD8kAAYJOFzwEBBQoMFACA=")) From 6d07882eae4bb6a8b4d17c7bdac38c9ec6cbe0af Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Wed, 22 Apr 2020 10:59:00 +0200 Subject: [PATCH 233/302] Revert "locale: Measure temperature in Kelvin" This reverts commit 26d8855e --- apps.json | 2 +- apps/locale/ChangeLog | 1 - apps/locale/locale.html | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/apps.json b/apps.json index 3921e8ae1..3a76c645b 100644 --- a/apps.json +++ b/apps.json @@ -65,7 +65,7 @@ { "id": "locale", "name": "Languages", "icon": "locale.png", - "version":"0.07", + "version":"0.06", "description": "Translations for different countries", "tags": "tool,system,locale,translate", "type": "locale", diff --git a/apps/locale/ChangeLog b/apps/locale/ChangeLog index a3cc0c9a3..3d983150d 100644 --- a/apps/locale/ChangeLog +++ b/apps/locale/ChangeLog @@ -6,4 +6,3 @@ Add correct scaling for speed/distance/temperature 0.06: Remove translations if not required Ensure 'on' is always supplied for translations -0.07: Measure temperature in Kelvin diff --git a/apps/locale/locale.html b/apps/locale/locale.html index 936d2e9f0..21bf37f29 100644 --- a/apps/locale/locale.html +++ b/apps/locale/locale.html @@ -112,8 +112,8 @@ exports = { name : "en_GB", currencySym:"£", `${js(locale.currency_symbol)} + n.toFixed(2)`: `n.toFixed(2) + ${js(locale.currency_symbol)}`; var temperature; - if (locale.temperature=='°C') temperature="(t-273.15)"; - else if (locale.temperature=='°F') temperature="((t-273.15)*9/5)+32"; + if (locale.temperature=='°C') temperature="t"; + else if (locale.temperature=='°F') temperature="(t*9/5)+32"; else throw new Error("Unknown temperature unit "+locale.temperature); var localeModule = `var l = ${JSON.stringify({ From c3940972b58039afc7a60df46a49b512f3ab3b54 Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Wed, 22 Apr 2020 11:00:16 +0200 Subject: [PATCH 234/302] weather: Convert Gadgetbridge temperature from Kelvin to Celsius --- apps/weather/app.js | 2 +- apps/weather/widget.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/weather/app.js b/apps/weather/app.js index fdf28f0e0..8493144f7 100644 --- a/apps/weather/app.js +++ b/apps/weather/app.js @@ -8,7 +8,7 @@ g.setColor(-1); - const temp = locale.temp(w.temp).match(/^(\D*\d*)(.*)$/); + const temp = locale.temp(w.temp-273.15).match(/^(\D*\d*)(.*)$/); let width = g.setFont("Vector", 40).stringWidth(temp[1]); width += g.setFont("Vector", 20).stringWidth(temp[2]); g.setFont("Vector", 40).setFontAlign(-1, -1, 0); diff --git a/apps/weather/widget.js b/apps/weather/widget.js index 7488b7657..e02591543 100644 --- a/apps/weather/widget.js +++ b/apps/weather/widget.js @@ -8,7 +8,7 @@ require('weather').drawIcon(w.txt, this.x+10, this.y+8, 8); } if (w.temp) { - let t = require('locale').temp(w.temp); // applies conversion + let t = require('locale').temp(w.temp-273.15); // applies conversion t = t.substr(0, t.length-2); // but we have no room for units g.setFontAlign(0, 1); // center horizontally at bottom of widget g.setFont('6x8', 1); From bf5bf91967eb37d703533ce775f57aa8b31696f8 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Wed, 22 Apr 2020 13:17:31 +0100 Subject: [PATCH 235/302] mclock: Improve performance, attempt to remove occasional glitch when LCD on (fix #279) --- apps.json | 2 +- apps/mclock/ChangeLog | 1 + apps/mclock/clock-morphing.js | 107 ++++++++++++++++++++-------------- 3 files changed, 66 insertions(+), 44 deletions(-) diff --git a/apps.json b/apps.json index 837b94669..65b681390 100644 --- a/apps.json +++ b/apps.json @@ -108,7 +108,7 @@ { "id": "mclock", "name": "Morphing Clock", "icon": "clock-morphing.png", - "version":"0.03", + "version":"0.04", "description": "7 segment clock that morphs between minutes and hours", "tags": "clock", "type":"clock", diff --git a/apps/mclock/ChangeLog b/apps/mclock/ChangeLog index e6a689e9e..4bc1ea352 100644 --- a/apps/mclock/ChangeLog +++ b/apps/mclock/ChangeLog @@ -1,2 +1,3 @@ 0.02: Modified for use with new bootloader and firmware 0.03: Added Locale based date +0.04: Improve performance, attempt to remove occasional glitch when LCD on (fix #279) diff --git a/apps/mclock/clock-morphing.js b/apps/mclock/clock-morphing.js index ce30ad033..e9365db52 100644 --- a/apps/mclock/clock-morphing.js +++ b/apps/mclock/clock-morphing.js @@ -1,14 +1,15 @@ var locale = require("locale"); +var CHARW = 34; // how tall are digits? +var CHARP = 2; // how chunky are digits? +var Y = 50; // start height // Offscreen buffer -var buf = Graphics.createArrayBuffer(240,86,1,{msb:true}); -function flip() { - g.setColor(1,1,1); - g.drawImage({width:buf.getWidth(),height:buf.getHeight(),buffer:buf.buffer},0,50); -} +var buf = Graphics.createArrayBuffer(CHARW+CHARP*2,CHARW*2 + CHARP*2,1,{msb:true}); +var bufimg = {width:buf.getWidth(),height:buf.getHeight(),buffer:buf.buffer}; // The last time that we displayed var lastTime = " "; // If animating, this is the interval's id var animInterval; +var timeInterval; /* Get array of lines from digit d to d+1. n is the amount (0..1) @@ -49,7 +50,7 @@ const DIGITS = { [0,1,1,1], [1,1,1,2], [1-n,2,1,2]], -"5": (n,maxFive)=>maxFive ? [ // 5 -> 0 +"5to0": n=>[ // 5 -> 0 [0,0,0,1], [0,0,1,0], [n,1,1,1], @@ -57,7 +58,8 @@ const DIGITS = { [0,2,1,2], [0,2,0,2], [1,1-n,1,1], -[0,1,0,1+n]] : [ // 5 -> 6 +[0,1,0,1+n]], +"5to6": n=>[ // 5 -> 6 [0,0,0,1], [0,0,1,0], [0,1,1,1], @@ -109,59 +111,66 @@ const DIGITS = { /* Draw a transition between lastText and thisText. 'n' is the amount - 0..1 */ -function draw(lastText,thisText,n) { - buf.clear(); - var x = 1; // x offset - const p = 2; // padding around digits - var y = p; // y offset - const s = 34; // character size +function drawDigits(lastText,thisText,n) { + const p = CHARP; // padding around digits + const s = CHARW; // character size + var x = 0; // x offset + g.reset(); for (var i=0;i{ + if (c[0]!=c[2]) // horiz + buf.fillRect(p+c[0]*s,c[1]*s,p+c[2]*s,2*p+c[3]*s); + else if (c[1]!=c[3]) // vert + buf.fillRect(c[0]*s,p+c[1]*s,2*p+c[2]*s,p+c[3]*s); + }); + g.drawImage(bufimg,x,Y); } - var l = DIGITS[ch](chn,lastCh==5 && thisCh==0); - l.forEach(c=>{ - if (c[0]!=c[2]) // horiz - buf.fillRect(x+c[0]*s,y+c[1]*s-p,x+c[2]*s,y+c[3]*s+p); - else if (c[1]!=c[3]) // vert - buf.fillRect(x+c[0]*s-p,y+c[1]*s,x+c[2]*s+p,y+c[3]*s); - }); if (thisCh==":") x-=4; x+=s+p+7; } - y += 2*s; +} +function drawSeconds() { + var x = (CHARW + CHARP + 6)*5; + var y = Y + 2*CHARW + CHARP; var d = new Date(); - buf.setFont("6x8"); - buf.setFontAlign(-1,-1); - buf.drawString(("0"+d.getSeconds()).substr(-2), x, y-8); + g.reset(); + g.setFont("6x8"); + g.setFontAlign(-1,-1); + g.drawString(("0"+d.getSeconds()).substr(-2), x, y-8, true); // date - buf.setFontAlign(0,-1); + g.setFontAlign(0,-1); var date = locale.date(d,false); - buf.drawString(date, buf.getWidth()/2, y+8); - flip(); + g.drawString(date, g.getWidth()/2, y+8, true); } /* Show the current time, and animate if needed */ function showTime() { - if (!Bangle.isLCDOn()) return; if (animInterval) return; // in animation - quit var d = new Date(); var t = (" "+d.getHours()).substr(-2)+":"+ ("0"+d.getMinutes()).substr(-2); var l = lastTime; // same - don't animate - if (t==l) { - draw(t,l,0); + if (t==l || l==" ") { + drawDigits(l,t,0); + drawSeconds(); + lastTime = t; return; } var n = 0; @@ -170,23 +179,35 @@ function showTime() { if (n>=1) { n=1; clearInterval(animInterval); - animInterval=0; + animInterval = undefined; } - draw(l,t,n); + drawDigits(l,t,n); }, 20); lastTime = t; } Bangle.on('lcdPower',function(on) { - if (on) + if (animInterval) { + clearInterval(animInterval); + animInterval = undefined; + } + if (timeInterval) { + clearInterval(timeInterval); + timeInterval = undefined; + } + if (on) { showTime(); + timeInterval = setInterval(showTime, 1000); + } else { + lastTime = " "; + } }); g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); // Update time once a second -setInterval(showTime, 1000); +timeInterval = setInterval(showTime, 1000); showTime(); // Show launcher when middle button pressed From 9e8dea08e4888ea602e6440402a3a3d5d9a873cb Mon Sep 17 00:00:00 2001 From: Paul Cockrell Date: Thu, 23 Apr 2020 06:54:08 +0100 Subject: [PATCH 236/302] Rename program to workout --- apps/buffgym/buffgym-exercise.js | 40 ++++----- apps/buffgym/buffgym-program-index.json | 10 --- apps/buffgym/buffgym-set.js | 6 +- ...-program-a.json => buffgym-workout-a.json} | 2 +- ...-program-b.json => buffgym-workout-b.json} | 2 +- apps/buffgym/buffgym-workout-index.json | 10 +++ ...{buffgym-program.js => buffgym-workout.js} | 6 +- apps/buffgym/buffgym.app.js | 87 ++++++++++--------- 8 files changed, 84 insertions(+), 79 deletions(-) delete mode 100644 apps/buffgym/buffgym-program-index.json rename apps/buffgym/{buffgym-program-a.json => buffgym-workout-a.json} (96%) rename apps/buffgym/{buffgym-program-b.json => buffgym-workout-b.json} (96%) create mode 100644 apps/buffgym/buffgym-workout-index.json rename apps/buffgym/{buffgym-program.js => buffgym-workout.js} (95%) diff --git a/apps/buffgym/buffgym-exercise.js b/apps/buffgym/buffgym-exercise.js index 68e49be84..078e206de 100644 --- a/apps/buffgym/buffgym-exercise.js +++ b/apps/buffgym/buffgym-exercise.js @@ -1,16 +1,16 @@ exports = class Exercise { constructor(params) { + this.completed = false; + this.sets = []; this.title = params.title; this.weight = params.weight; this.unit = params.unit; this.restPeriod = params.restPeriod; - this.completed = false; - this.sets = []; + this._originalRestPeriod = params.restPeriod; + this._weightIncrement = params.weightIncrement; this._restTimeout = null; this._restInterval = null; this._state = null; - this._originalRestPeriod = params.restPeriod; - this._weightIncrement = params.weightIncrement || 2.5; } get humanTitle() { @@ -63,13 +63,13 @@ exports = class Exercise { return (targetRepsTotalSum - completedRepsTotalSum) === 0; } - startRestTimer(program) { + startRestTimer(workout) { this._restTimeout = setTimeout(() => { - this.next(program); + this.next(workout); }, 1000 * this.restPeriod); this._restInterval = setInterval(() => { - program.emit("redraw"); + workout.emit("redraw"); }, 1000 ); } @@ -85,28 +85,28 @@ exports = class Exercise { return this._restTimeout != null; } - setupStartedButtons(program) { + setupStartedButtons(workout) { clearWatch(); setWatch(() => { this.currentSet().incReps(); - program.emit("redraw"); + workout.emit("redraw"); }, BTN1, {repeat: true}); - setWatch(program.next.bind(program), BTN2, {repeat: false}); + setWatch(workout.next.bind(workout), BTN2, {repeat: false}); setWatch(() => { this.currentSet().decReps(); - program.emit("redraw"); + workout.emit("redraw"); }, BTN3, {repeat: true}); } - setupRestingButtons(program) { + setupRestingButtons(workout) { clearWatch(); - setWatch(program.next.bind(program), BTN2, {repeat: false}); + setWatch(workout.next.bind(workout), BTN2, {repeat: false}); } - next(program) { + next(workout) { const STARTED = 1; const RESTING = 2; const COMPLETED = 3; @@ -114,12 +114,12 @@ exports = class Exercise { switch(this._state) { case null: this._state = STARTED; - this.setupStartedButtons(program); + this.setupStartedButtons(workout); break; case STARTED: this._state = RESTING; - this.startRestTimer(program); - this.setupRestingButtons(program); + this.startRestTimer(workout); + this.setupRestingButtons(workout); break; case RESTING: this.resetRestTimer(); @@ -132,13 +132,13 @@ exports = class Exercise { this._state = null; } // As we are changing state and require it to be reprocessed - // invoke the next step of program - program.next(); + // invoke the next step of workout + workout.next(); break; default: throw "Exercise: Attempting to move to an unknown state"; } - program.emit("redraw"); + workout.emit("redraw"); } } \ No newline at end of file diff --git a/apps/buffgym/buffgym-program-index.json b/apps/buffgym/buffgym-program-index.json deleted file mode 100644 index 3bb51f1b5..000000000 --- a/apps/buffgym/buffgym-program-index.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - { - "title": "Program A", - "file": "buffgym-program-a.json" - }, - { - "title": "Program B", - "file": "buffgym-program-b.json" - } -] \ No newline at end of file diff --git a/apps/buffgym/buffgym-set.js b/apps/buffgym/buffgym-set.js index 4bd12d7ec..dc0c05671 100644 --- a/apps/buffgym/buffgym-set.js +++ b/apps/buffgym/buffgym-set.js @@ -1,9 +1,9 @@ exports = class Set { constructor(maxReps) { - this.minReps = 0; - this.maxReps = maxReps; - this.reps = 0; this.completed = false; + this.minReps = 0; + this.reps = 0; + this.maxReps = maxReps; } isCompleted() { diff --git a/apps/buffgym/buffgym-program-a.json b/apps/buffgym/buffgym-workout-a.json similarity index 96% rename from apps/buffgym/buffgym-program-a.json rename to apps/buffgym/buffgym-workout-a.json index 7ebaf3741..8eb8611d6 100644 --- a/apps/buffgym/buffgym-program-a.json +++ b/apps/buffgym/buffgym-workout-a.json @@ -1,5 +1,5 @@ { - "title": "Program A", + "title": "Workout A", "exercises": [ { "title": "Squats", diff --git a/apps/buffgym/buffgym-program-b.json b/apps/buffgym/buffgym-workout-b.json similarity index 96% rename from apps/buffgym/buffgym-program-b.json rename to apps/buffgym/buffgym-workout-b.json index b93348621..43845a98b 100644 --- a/apps/buffgym/buffgym-program-b.json +++ b/apps/buffgym/buffgym-workout-b.json @@ -1,5 +1,5 @@ { - "title": "Program B", + "title": "Workout B", "exercises": [ { "title": "Squats", diff --git a/apps/buffgym/buffgym-workout-index.json b/apps/buffgym/buffgym-workout-index.json new file mode 100644 index 000000000..af74d5e3b --- /dev/null +++ b/apps/buffgym/buffgym-workout-index.json @@ -0,0 +1,10 @@ +[ + { + "title": "Workout A", + "file": "buffgym-workout-a.json" + }, + { + "title": "Workout B", + "file": "buffgym-workout-b.json" + } +] \ No newline at end of file diff --git a/apps/buffgym/buffgym-program.js b/apps/buffgym/buffgym-workout.js similarity index 95% rename from apps/buffgym/buffgym-program.js rename to apps/buffgym/buffgym-workout.js index 68a069da5..811125293 100644 --- a/apps/buffgym/buffgym-program.js +++ b/apps/buffgym/buffgym-workout.js @@ -1,4 +1,4 @@ -exports = class Program { +exports = class Workout { constructor(params) { this.title = params.title; this.exercises = []; @@ -27,6 +27,10 @@ exports = class Program { return !!this.completed; } + static fromJSON(workout) { + + } + toJSON() { return { title: this.title, diff --git a/apps/buffgym/buffgym.app.js b/apps/buffgym/buffgym.app.js index 4dc6ffd5a..b92b2bb98 100755 --- a/apps/buffgym/buffgym.app.js +++ b/apps/buffgym/buffgym.app.js @@ -5,7 +5,7 @@ * Created: April 2020 * * Inspired by: - * - Stronglifts 5x5 training program https://stronglifts.com/5x5/ + * - Stronglifts 5x5 training workout https://stronglifts.com/5x5/ * - Stronglifts smart watch app */ @@ -66,10 +66,10 @@ function drawSet(exercise) { g.flip(); } -function drawProgDone() { +function drawWorkoutDone() { const title1 = "You did"; const title2 = "GREAT!"; - const msg = "That's the program\ncompleted. Now eat\nsome food and\nget plenty of rest."; + const msg = "That's the workout\ncompleted. Now eat\nsome food and\nget plenty of rest."; clearWatch(); setWatch(Bangle.showLauncher, BTN2, {repeat: false}); @@ -102,13 +102,13 @@ function drawSetComp() { g.flip(); } -function drawRestTimer(program) { - const exercise = program.currentExercise(); +function drawRestTimer(workout) { + const exercise = workout.currentExercise(); const motivation = "Take a breather.."; if (exercise.restPeriod <= 0) { exercise.resetRestTimer(); - program.next(); + workout.next(); return; } @@ -128,21 +128,21 @@ function drawRestTimer(program) { exercise.decRestPeriod(); } -function redraw(program) { - const exercise = program.currentExercise(); +function redraw(workout) { + const exercise = workout.currentExercise(); g.clear(); - if (program.isCompleted()) { - saveProg(program); - drawProgDone(program); + if (workout.isCompleted()) { + saveWorkout(workout); + drawWorkoutDone(workout); return; } if (exercise.isRestTimerRunning()) { if (exercise.isLastSet()) { - drawSetComp(program); + drawSetComp(workout); } else { - drawRestTimer(program); + drawRestTimer(workout); } return; @@ -151,7 +151,7 @@ function redraw(program) { drawSet(exercise); } -function drawProgMenu(programs, selProgIdx) { +function drawWorkoutMenu(workouts, selWorkoutIdx) { g.clear(); g.setFontAlign(0, -1); g.setColor(WHITE); @@ -160,16 +160,16 @@ function drawProgMenu(programs, selProgIdx) { g.setFont("6x8", 1); g.setFontAlign(-1, -1); - let selectedProgram = programs[selProgIdx].title; + let selectedWorkout = workouts[selWorkoutIdx].title; let yPos = 50; - programs.forEach(program => { + workouts.forEach(workout => { g.setColor("#f05a56"); g.fillRect(0, yPos, W, yPos + 11); g.setColor("#ffffff"); - if (selectedProgram === program.title) { + if (selectedWorkout === workout.title) { g.drawRect(0, yPos, W - 1, yPos + 11); } - g.drawString(program.title, 10, yPos + 2); + g.drawString(workout.title, 10, yPos + 2); yPos += 15; }); g.flip(); @@ -177,25 +177,25 @@ function drawProgMenu(programs, selProgIdx) { function setupMenu() { clearWatch(); - const progs = getProgIndex(); - let selProgIdx = 0; - drawProgMenu(progs, selProgIdx); + const workouts = getWorkoutIndex(); + let selWorkoutIdx = 0; + drawWorkoutMenu(workouts, selWorkoutIdx); setWatch(()=>{ - selProgIdx--; - if (selProgIdx< 0) selProgIdx = 0; - drawProgMenu(progs, selProgIdx); + selWorkoutIdx--; + if (selWorkoutIdx< 0) selWorkoutIdx = 0; + drawWorkoutMenu(workouts, selWorkoutIdx); }, BTN1, {repeat: true}); setWatch(()=>{ - const prog = buildProg(progs[selProgIdx].file); - prog.next(); + const workout = buildWorkout(workouts[selWorkoutIdx].file); + workout.next(); }, BTN2, {repeat: false}); setWatch(()=>{ - selProgIdx++; - if (selProgIdx > progs.length - 1) selProgIdx = progs.length - 1; - drawProgMenu(progs, selProgIdx); + selWorkoutIdx++; + if (selWorkoutIdx > workouts.length - 1) selWorkoutIdx = workouts.length - 1; + drawWorkoutMenu(workouts, selWorkoutIdx); }, BTN3, {repeat: true}); } @@ -249,23 +249,24 @@ function drawSplash() { }, BTN3, {repeat: false}); } -function getProgIndex() { - const progIdx = require("Storage").readJSON("buffgym-program-index.json"); - return progIdx; +function getWorkoutIndex() { + const workoutIdx = require("Storage").readJSON("buffgym-workout-index.json"); + return workoutIdx; } -function buildProg(fName) { +function buildWorkout(fName) { const Set = require("buffgym-set.js"); const Exercise = require("buffgym-exercise.js"); - const Program = require("buffgym-program.js"); - const progJSON = require("Storage").readJSON(fName); - const prog = new Program({ - title: progJSON.title, + const Workout = require("buffgym-workout.js"); + const workoutJSON = require("Storage").readJSON(fName); + const workout = new Workout({ + title: workoutJSON.title, }); - const exercises = progJSON.exercises.map(exerciseJSON => { + const exercises = workoutJSON.exercises.map(exerciseJSON => { const exercise = new Exercise({ title: exerciseJSON.title, weight: exerciseJSON.weight, + weightIncrement: exerciseJSON.weightIncrement, unit: exerciseJSON.unit, restPeriod: exerciseJSON.restPeriod, }); @@ -275,14 +276,14 @@ function buildProg(fName) { return exercise; }); - prog.addExercises(exercises); + workout.addExercises(exercises); - return prog; + return workout; } -function saveProg(program) { - const fName = getProgIndex().find(prog => prog.title === program.title).file; - require("Storage").writeJSON(fName, program.toJSON()); +function saveWorkout(workout) { + const fName = getWorkoutIndex().find(workout => workout.title === workout.title).file; + require("Storage").writeJSON(fName, workout.toJSON()); } drawSplash(); \ No newline at end of file From bf1fe5de4e7d383fd2be7184ca4012e696c6d61a Mon Sep 17 00:00:00 2001 From: Paul Cockrell Date: Thu, 23 Apr 2020 06:58:14 +0100 Subject: [PATCH 237/302] Fix logic leak, make sure draw functions only deal with drawing UI --- apps/buffgym/buffgym-exercise.js | 9 +++++++++ apps/buffgym/buffgym.app.js | 10 ---------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/apps/buffgym/buffgym-exercise.js b/apps/buffgym/buffgym-exercise.js index 078e206de..7d631182e 100644 --- a/apps/buffgym/buffgym-exercise.js +++ b/apps/buffgym/buffgym-exercise.js @@ -69,6 +69,15 @@ exports = class Exercise { }, 1000 * this.restPeriod); this._restInterval = setInterval(() => { + this.decRestPeriod(); + + if (this.restPeriod < 0) { + this.resetRestTimer(); + this.next(); + + return; + } + workout.emit("redraw"); }, 1000 ); } diff --git a/apps/buffgym/buffgym.app.js b/apps/buffgym/buffgym.app.js index b92b2bb98..d1c0386ff 100755 --- a/apps/buffgym/buffgym.app.js +++ b/apps/buffgym/buffgym.app.js @@ -104,14 +104,6 @@ function drawSetComp() { function drawRestTimer(workout) { const exercise = workout.currentExercise(); - const motivation = "Take a breather.."; - - if (exercise.restPeriod <= 0) { - exercise.resetRestTimer(); - workout.next(); - - return; - } g.clear(); drawMenu({showBTN2: true}); @@ -124,8 +116,6 @@ function drawRestTimer(workout) { g.setFont("6x8", 5); g.drawString(exercise.restPeriod, (W / 2) + 2, (H / 2) - 19); g.flip(); - - exercise.decRestPeriod(); } function redraw(workout) { From 3384f493e250fff949d032e57a4fed6b609e427b Mon Sep 17 00:00:00 2001 From: Paul Cockrell Date: Thu, 23 Apr 2020 07:06:25 +0100 Subject: [PATCH 238/302] Fix set complete UI message --- apps/buffgym/buffgym.app.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/apps/buffgym/buffgym.app.js b/apps/buffgym/buffgym.app.js index d1c0386ff..2c7416dba 100755 --- a/apps/buffgym/buffgym.app.js +++ b/apps/buffgym/buffgym.app.js @@ -85,9 +85,12 @@ function drawWorkoutDone() { g.flip(); } -function drawSetComp() { +function drawSetComp(exercise) { const title = "Good work"; - const msg = "No need to rest\nmove straight on\nto the next\nexercise.Your\nweight has been\nincreased for\nnext time!"; + const msg1= "No need to rest\nmove straight on\nto the next\nexercise."; + const msg2 = exercise.canProgress()? + "Your\nweight has been\nincreased for\nnext time!": + "You'll\nsmash it next\ntime!"; g.clear(); drawMenu({showBTN2: true}); @@ -97,14 +100,12 @@ function drawSetComp() { g.setFont("6x8", 2); g.drawString(title, W / 2, 10); g.setFont("6x8", 1); - g.drawString(msg, (W / 2) - 2, 45); + g.drawString(msg1 + msg2, (W / 2) - 2, 45); g.flip(); } -function drawRestTimer(workout) { - const exercise = workout.currentExercise(); - +function drawRestTimer(exercise) { g.clear(); drawMenu({showBTN2: true}); g.setFontAlign(0, -1); @@ -124,15 +125,15 @@ function redraw(workout) { if (workout.isCompleted()) { saveWorkout(workout); - drawWorkoutDone(workout); + drawWorkoutDone(); return; } if (exercise.isRestTimerRunning()) { if (exercise.isLastSet()) { - drawSetComp(workout); + drawSetComp(exercise); } else { - drawRestTimer(workout); + drawRestTimer(exercise); } return; From d3c58fb323cf50439a831b50930fed678e834380 Mon Sep 17 00:00:00 2001 From: Paul Cockrell Date: Thu, 23 Apr 2020 09:41:57 +0100 Subject: [PATCH 239/302] Add interface to set weight values, update README --- apps/buffgym/README.md | 25 +++- apps/buffgym/buffgym.html | 245 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 266 insertions(+), 4 deletions(-) create mode 100644 apps/buffgym/buffgym.html diff --git a/apps/buffgym/README.md b/apps/buffgym/README.md index e9e217828..bcadf22c4 100644 --- a/apps/buffgym/README.md +++ b/apps/buffgym/README.md @@ -2,17 +2,32 @@ This gym training assistant trains you on the famous [Stronglifts 5x5 workout](https://stronglifts.com/5x5) program. +## Configuration + +### Setting your start weight values + +You will want to set your own starting weight values for your 5x5 training program. To do this is easy! After installing this app, go to the BangleJS app store, connect to your watch, and navigate to the `My Apps` tab. In there you will find this app in the list, and an icon (a down arrow) to the right of the app title. Click that icon to reveal a configuration page. Enter your weights and other details, and click upload. That is it, you are now ready to train! + ## Usage +### Start screen + When you start the app it will wait on a splash screen until you are ready to start the work out. Press any of the buttons to start ![](buffgym-scrn1.png) -You are then presented with the programs menu, use BTN1 to move up the list, and BTN3 to move down the list. Once you have made your selection, press BTN2 to select the program. +### Workouts menu + +You are then presented with the workouts menu, use BTN1 to move up the list, and BTN3 to move down the list. Once you have made your selection, press BTN2 to select the workout. ![](buffgym-scrn2.png) -You will now begin moving through the exercises in the program. You will see the exercise information on the display. +### Recording your training + +You will now begin moving through the exercises in the workout. You will see the exercise information on the display. + +![](buffgym-scrn3.png) + 1. At the top is the exercise name, e.g 'Squats' 2. Next is the weight you must train 3. In the center is where you record the number of *reps* you completed (more on that shortly) @@ -20,13 +35,15 @@ You will now begin moving through the exercises in the program. You will see the 5. Below the target reps is the current set you are training, out of the total sets for the exercise. 6. The *reps* value is used to store what you achieved for the current set, you enter this after you have trained on your current set. To alter this value, use BTN1 to increase the value (it will stop at the maximum required reps) and BTN3 to decreas the value to a minimum of 0 (this is the default value). Pressing BTN2 will confirm your reps -![](buffgym-scrn3.png) +### Rest timers You will then be presented with a rest timer screen, it counts down and automatically moves to the next exercise when it reaches 0. You can cancel the timer early if you wish by pressing BTN2. If it is the last set of an exercise, you don't need to rest, so it lets you know you have completed all the sets in the exercise and can start the next exercise. ![](buffgym-scrn4.png) ![](buffgym-scrn5.png) +### Workout completed + Once all exercises are done, you are presented with a pat-on-the-back screen to tell you how awesome you are. ![](buffgym-scrn6.png) @@ -40,4 +57,4 @@ Once all exercises are done, you are presented with a pat-on-the-back screen to ## Created by -[Paul Cockrell](https://github.com/paulcockrell) April 2020. \ No newline at end of file +[Paul Cockrell](https://github.com/paulcockrell) April 2020. diff --git a/apps/buffgym/buffgym.html b/apps/buffgym/buffgym.html new file mode 100644 index 000000000..c5766bcfa --- /dev/null +++ b/apps/buffgym/buffgym.html @@ -0,0 +1,245 @@ + + + + + +

BuffGym

+

+ Enter in your weights for each exercise, start light and keep consistent with your training. The weight increment field is how much the app will increase your weights for an exercise if you successfully complete all the reps and sets for an exercise. Make sure its a value that matches the weights in your gym. +

+

+ For more information on how to train this program refer the Stronglifts website +

+
+

Workout A

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ExerciseSets / RepsWeightWeight increment
+ Squats + + 5x5 + + + + +
+ Overhead press + + 5x5 + + + + +
+ Deadlift + + 1x5 + + + + +
+ Pullups + + 3x10 + + + + +
+

Workout B

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ExerciseSets / RepsWeightWeight increment
+ Squats + + 5x5 + + + + +
+ Bench press + + 5x5 + + + + +
+ Row + + 5x5 + + + + +
+ Tricep extension + + 3x10 + + + + +
+
+

+ + + + + + + From 04f1e523e85676a16b3058ba6718cf9519efccd4 Mon Sep 17 00:00:00 2001 From: Paul Cockrell Date: Thu, 23 Apr 2020 09:48:31 +0100 Subject: [PATCH 240/302] Serialise uploading. Add buzz on watch to indicate completed --- apps/buffgym/buffgym.html | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/buffgym/buffgym.html b/apps/buffgym/buffgym.html index c5766bcfa..4025ee19d 100644 --- a/apps/buffgym/buffgym.html +++ b/apps/buffgym/buffgym.html @@ -237,8 +237,13 @@ } document.getElementById("upload").addEventListener("click", function() { - Puck.eval(`require("Storage").writeJSON("buffgym-workout-a.json",${JSON.stringify(workoutA())})`, ()=>{console.log("Done uploading workout A")}); - Puck.eval(`require("Storage").writeJSON("buffgym-workout-b.json",${JSON.stringify(workoutB())})`, ()=>{console.log("Done uploading workout B")}); + Puck.eval(`require("Storage").writeJSON("buffgym-workout-a.json",${JSON.stringify(workoutA())})`, ()=>{ + Puck.eval(`require("Storage").writeJSON("buffgym-workout-b.json",${JSON.stringify(workoutB())})`, ()=>{ + Puck.eval(`Bangle.buzz();`, () => { + console.log("all done"); + }) + }) + }); }); From a0d6f8741e81f2861f491a857594fb844b8537ef Mon Sep 17 00:00:00 2001 From: Paul Cockrell Date: Thu, 23 Apr 2020 09:51:15 +0100 Subject: [PATCH 241/302] Add interface to apps.json for BuffGym app --- apps.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apps.json b/apps.json index 65b681390..4f0346bd8 100644 --- a/apps.json +++ b/apps.json @@ -1347,6 +1347,7 @@ "description": "BuffGym is the famous 5x5 workout program for the BangleJS", "tags": "tool,outdoors,gym,exercise", "type": "app", + "interface": "buffgym.html", "allow_emulator": false, "readme": "README.md", "storage": [ From edd1b73bc00dcb2a2e9c43d5d5aa9d7da4224b25 Mon Sep 17 00:00:00 2001 From: Paul Cockrell Date: Thu, 23 Apr 2020 10:11:28 +0100 Subject: [PATCH 242/302] Update apps.json to use new buffgym file names. Fix buzz command --- apps.json | 8 ++++---- apps/buffgym/buffgym.html | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps.json b/apps.json index 4f0346bd8..60d973e23 100644 --- a/apps.json +++ b/apps.json @@ -1354,10 +1354,10 @@ {"name":"buffgym.app.js", "url": "buffgym.app.js"}, {"name":"buffgym-set.js","url":"buffgym-set.js"}, {"name":"buffgym-exercise.js","url":"buffgym-exercise.js"}, - {"name":"buffgym-program.js","url":"buffgym-program.js"}, - {"name":"buffgym-program-a.json","url":"buffgym-program-a.json"}, - {"name":"buffgym-program-b.json","url":"buffgym-program-b.json"}, - {"name":"buffgym-program-index.json","url":"buffgym-program-index.json"}, + {"name":"buffgym-workout.js","url":"buffgym-workout.js"}, + {"name":"buffgym-workout-a.json","url":"buffgym-workout-a.json"}, + {"name":"buffgym-workout-b.json","url":"buffgym-workout-b.json"}, + {"name":"buffgym-workout-index.json","url":"buffgym-workout-index.json"}, {"name":"buffgym.img","url":"buffgym-icon.js","evaluate":true} ] }, diff --git a/apps/buffgym/buffgym.html b/apps/buffgym/buffgym.html index 4025ee19d..3c18932e9 100644 --- a/apps/buffgym/buffgym.html +++ b/apps/buffgym/buffgym.html @@ -239,7 +239,7 @@ document.getElementById("upload").addEventListener("click", function() { Puck.eval(`require("Storage").writeJSON("buffgym-workout-a.json",${JSON.stringify(workoutA())})`, ()=>{ Puck.eval(`require("Storage").writeJSON("buffgym-workout-b.json",${JSON.stringify(workoutB())})`, ()=>{ - Puck.eval(`Bangle.buzz();`, () => { + Puck.eval(`Bangle.buzz()`, () => { console.log("all done"); }) }) From a389ed5dbee7108ca55eb7853da3f307d962ec87 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Thu, 23 Apr 2020 10:28:42 +0100 Subject: [PATCH 243/302] Torch: Change start sequence to BTN1/3/1/3 to avoid accidental turning on (fix #342) --- apps.json | 4 ++-- apps/torch/ChangeLog | 1 + apps/torch/widget.js | 24 ++++++++++++++++-------- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/apps.json b/apps.json index 65b681390..4c5910a6f 100644 --- a/apps.json +++ b/apps.json @@ -921,8 +921,8 @@ "name": "Torch", "shortName":"Torch", "icon": "app.png", - "version":"0.01", - "description": "Turns screen white to help you see in the dark. Select from the launcher or press BTN3 four times in quick succession to start when in normal clock mode", + "version":"0.02", + "description": "Turns screen white to help you see in the dark. Select from the launcher or press BTN1,BTN3,BTN1,BTN3 quickly to start when in any app that shows widgets", "tags": "tool,torch", "storage": [ {"name":"torch.app.js","url":"app.js"}, diff --git a/apps/torch/ChangeLog b/apps/torch/ChangeLog index 5560f00bc..8e76b717a 100644 --- a/apps/torch/ChangeLog +++ b/apps/torch/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Change start sequence to BTN1/3/1/3 to avoid accidental turning on (fix #342) diff --git a/apps/torch/widget.js b/apps/torch/widget.js index e3d1ea22f..a5002ea71 100644 --- a/apps/torch/widget.js +++ b/apps/torch/widget.js @@ -1,18 +1,26 @@ +(function() { var clickTimes = []; -var CLICK_COUNT = 4; // number of taps -var CLICK_PERIOD = 1; // second +var clickPattern = ""; +var TAPS = 4; // number of taps +var PERIOD = 1; // seconds // we don't actually create/draw a widget here at all... - Bangle.on("lcdPower",function(on) { // First click (that turns LCD on) isn't given to // setWatch, so handle it here - if (on) clickTimes=[getTime()]; + if (!on) return; + clickTimes=[getTime()]; + clickPattern="x"; }); -setWatch(function(e) { - while (clickTimes.length>=CLICK_COUNT) clickTimes.shift(); +function tap(e,c) { + clickPattern = clickPattern.substr(-3)+c; + while (clickTimes.length>=TAPS) clickTimes.shift(); clickTimes.push(e.time); var clickPeriod = e.time-clickTimes[0]; - if (clickTimes.length==CLICK_COUNT && clickPeriod Date: Thu, 23 Apr 2020 10:53:09 +0100 Subject: [PATCH 244/302] stopwatch: Ensure seconds counter is in sync with subseconds (fix #341) --- apps.json | 2 +- apps/swatch/ChangeLog | 1 + apps/swatch/stopwatch.js | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps.json b/apps.json index 4c5910a6f..127d87d89 100644 --- a/apps.json +++ b/apps.json @@ -452,7 +452,7 @@ { "id": "swatch", "name": "Stopwatch", "icon": "stopwatch.png", - "version":"0.06", + "version":"0.07", "interface": "interface.html", "description": "Simple stopwatch with Lap Time logging to a JSON file", "tags": "health", diff --git a/apps/swatch/ChangeLog b/apps/swatch/ChangeLog index 9a037fa41..caa74a1ba 100644 --- a/apps/swatch/ChangeLog +++ b/apps/swatch/ChangeLog @@ -6,3 +6,4 @@ 0.04: Changed save file filename, add interface.html to allow laps to be loaded 0.05: Added widgets 0.06: Added total running time, moved lap time to smaller display, total run time now appends as first entry in array, saving now saves last lap as well +0.07: Ensure seconds counter is in sync with subseconds (fix #341) diff --git a/apps/swatch/stopwatch.js b/apps/swatch/stopwatch.js index 659f0606d..478de2712 100644 --- a/apps/swatch/stopwatch.js +++ b/apps/swatch/stopwatch.js @@ -94,7 +94,8 @@ setWatch(function() { // Start/stop displayInterval = setInterval(function() { var last = tCurrent; if (started) tCurrent = Date.now(); - if (Math.floor(last/1000)!=Math.floor(tCurrent/1000)) + if (Math.floor((last-tStart)/1000)!=Math.floor((tCurrent-tStart)/1000) || + Math.floor((last-tTotal)/1000)!=Math.floor((tCurrent-tTotal)/1000)) drawsecs(); else drawms(); From c36bd7f97f7b89c2d7445a62255686f399333ed7 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Thu, 23 Apr 2020 10:57:59 +0100 Subject: [PATCH 245/302] Add readme to example apps (fix #300) --- apps/_example_app/README.md | 25 +++++++++++++++++++++++++ apps/_example_app/add_to_apps.json | 1 + apps/_example_widget/README.md | 25 +++++++++++++++++++++++++ apps/_example_widget/add_to_apps.json | 1 + 4 files changed, 52 insertions(+) create mode 100644 apps/_example_app/README.md create mode 100644 apps/_example_widget/README.md diff --git a/apps/_example_app/README.md b/apps/_example_app/README.md new file mode 100644 index 000000000..dc139bc9a --- /dev/null +++ b/apps/_example_app/README.md @@ -0,0 +1,25 @@ +# App Name + +Describe the app... + +Add screen shots (if possible) to the app folder and link then into this file with ![](.png) + +## Usage + +Describe how to use it + +## Features + +Name the function + +## Controls + +Name the buttons and what they are used for + +## Requests + +Name who should be contacted for support/update requests + +## Creator + +Your name diff --git a/apps/_example_app/add_to_apps.json b/apps/_example_app/add_to_apps.json index ca75a7bd8..bb0377b66 100644 --- a/apps/_example_app/add_to_apps.json +++ b/apps/_example_app/add_to_apps.json @@ -6,6 +6,7 @@ "version":"0.01", "description": "A detailed description of my great app", "tags": "", + "readme": "README.md", "storage": [ {"name":"7chname.app.js","url":"app.js"}, {"name":"7chname.img","url":"app-icon.js","evaluate":true} diff --git a/apps/_example_widget/README.md b/apps/_example_widget/README.md new file mode 100644 index 000000000..a909e9e7e --- /dev/null +++ b/apps/_example_widget/README.md @@ -0,0 +1,25 @@ +# Widget Name + +Describe the app... + +Add screen shots (if possible) to the app folder and link then into this file with ![](.png) + +## Usage + +Describe how to use it + +## Features + +Name the function + +## Controls + +Name the buttons and what they are used for + +## Requests + +Name who should be contacted for support/update requests + +## Creator + +Your name diff --git a/apps/_example_widget/add_to_apps.json b/apps/_example_widget/add_to_apps.json index 5d0057960..527c698a0 100644 --- a/apps/_example_widget/add_to_apps.json +++ b/apps/_example_widget/add_to_apps.json @@ -7,6 +7,7 @@ "description": "A detailed description of my great widget", "tags": "widget", "type": "widget", + "readme": "README.md", "storage": [ {"name":"7chname.wid.js","url":"widget.js"} ] From f8590bdcf58df24f66f88f694c24c51159354f02 Mon Sep 17 00:00:00 2001 From: Paul Cockrell Date: Thu, 23 Apr 2020 11:18:10 +0100 Subject: [PATCH 246/302] Refactor workout json loading. Fix file name matching bug --- apps/buffgym/buffgym-exercise.js | 4 ++-- apps/buffgym/buffgym-workout.js | 25 ++++++++++++++++++++++++- apps/buffgym/buffgym.app.js | 23 ++--------------------- 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/apps/buffgym/buffgym-exercise.js b/apps/buffgym/buffgym-exercise.js index 7d631182e..ea20aa132 100644 --- a/apps/buffgym/buffgym-exercise.js +++ b/apps/buffgym/buffgym-exercise.js @@ -4,10 +4,10 @@ exports = class Exercise { this.sets = []; this.title = params.title; this.weight = params.weight; + this.weightIncrement = params.weightIncrement; this.unit = params.unit; this.restPeriod = params.restPeriod; this._originalRestPeriod = params.restPeriod; - this._weightIncrement = params.weightIncrement; this._restTimeout = null; this._restInterval = null; this._state = null; @@ -50,7 +50,7 @@ exports = class Exercise { setCompleted() { if (!this.canSetCompleted()) throw "All sets must be completed"; - if (this.canProgress()) this.weight += this._weightIncrement; + if (this.canProgress()) this.weight += this.weightIncrement; this.completed = true; } diff --git a/apps/buffgym/buffgym-workout.js b/apps/buffgym/buffgym-workout.js index 811125293..124c27f4b 100644 --- a/apps/buffgym/buffgym-workout.js +++ b/apps/buffgym/buffgym-workout.js @@ -27,8 +27,30 @@ exports = class Workout { return !!this.completed; } - static fromJSON(workout) { + static fromJSON(workoutJSON) { + const Set = require("buffgym-set.js"); + const Exercise = require("buffgym-exercise.js"); + const workout = new this({ + title: workoutJSON.title, + }); + const exercises = workoutJSON.exercises.map(exerciseJSON => { + const exercise = new Exercise({ + title: exerciseJSON.title, + weight: exerciseJSON.weight, + weightIncrement: exerciseJSON.weightIncrement, + unit: exerciseJSON.unit, + restPeriod: exerciseJSON.restPeriod, + }); + exerciseJSON.sets.forEach(setJSON => { + exercise.addSet(new Set(setJSON)); + }); + return exercise; + }); + + workout.addExercises(exercises); + + return workout; } toJSON() { @@ -38,6 +60,7 @@ exports = class Workout { return { title: exercise.title, weight: exercise.weight, + weightIncrement: exercise.weightIncrement, unit: exercise.unit, sets: exercise.sets.map(set => set.maxReps), restPeriod: exercise.restPeriod, diff --git a/apps/buffgym/buffgym.app.js b/apps/buffgym/buffgym.app.js index 2c7416dba..fc2be83f9 100755 --- a/apps/buffgym/buffgym.app.js +++ b/apps/buffgym/buffgym.app.js @@ -246,34 +246,15 @@ function getWorkoutIndex() { } function buildWorkout(fName) { - const Set = require("buffgym-set.js"); - const Exercise = require("buffgym-exercise.js"); const Workout = require("buffgym-workout.js"); const workoutJSON = require("Storage").readJSON(fName); - const workout = new Workout({ - title: workoutJSON.title, - }); - const exercises = workoutJSON.exercises.map(exerciseJSON => { - const exercise = new Exercise({ - title: exerciseJSON.title, - weight: exerciseJSON.weight, - weightIncrement: exerciseJSON.weightIncrement, - unit: exerciseJSON.unit, - restPeriod: exerciseJSON.restPeriod, - }); - exerciseJSON.sets.forEach(setJSON => { - exercise.addSet(new Set(setJSON)); - }); - - return exercise; - }); - workout.addExercises(exercises); + const workout = Workout.fromJSON(workoutJSON); return workout; } function saveWorkout(workout) { - const fName = getWorkoutIndex().find(workout => workout.title === workout.title).file; + const fName = getWorkoutIndex().find(w => w.title === workout.title).file; require("Storage").writeJSON(fName, workout.toJSON()); } From 2ebdf0c6c309e8983b08b8906cd1bc5cb96fb6e1 Mon Sep 17 00:00:00 2001 From: Paul Cockrell Date: Thu, 23 Apr 2020 11:45:45 +0100 Subject: [PATCH 247/302] Add Changelog and bump version in apps.json --- apps.json | 2 +- apps/buffgym/ChangeLog | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 apps/buffgym/ChangeLog diff --git a/apps.json b/apps.json index 60d973e23..009f510b9 100644 --- a/apps.json +++ b/apps.json @@ -1343,7 +1343,7 @@ "id": "buffgym", "name": "BuffGym", "icon": "buffgym.png", - "version":"0.01", + "version":"0.02", "description": "BuffGym is the famous 5x5 workout program for the BangleJS", "tags": "tool,outdoors,gym,exercise", "type": "app", diff --git a/apps/buffgym/ChangeLog b/apps/buffgym/ChangeLog new file mode 100644 index 000000000..6efdd865a --- /dev/null +++ b/apps/buffgym/ChangeLog @@ -0,0 +1,2 @@ +0.01: Create BuffGym app +0.02: Add web interface for personalising workout From e40829ea73f2402dc1abf0e0edad7d6fd23f1205 Mon Sep 17 00:00:00 2001 From: Dimitri Gigot Date: Thu, 23 Apr 2020 13:46:06 +0000 Subject: [PATCH 248/302] 0.06: Complete rewrite in 80x80, better perf, add settings --- apps.json | 6 +- apps/toucher/ChangeLog | 2 +- apps/toucher/app.js | 299 +++++++++++++++++++++++---------------- apps/toucher/settings.js | 59 ++++++++ 4 files changed, 239 insertions(+), 127 deletions(-) create mode 100644 apps/toucher/settings.js diff --git a/apps.json b/apps.json index 43e7e4b9d..ad5b20820 100644 --- a/apps.json +++ b/apps.json @@ -1092,14 +1092,16 @@ }, { "id": "toucher", "name": "Touch Launcher", - "shortName":"Menu", + "shortName":"Toucher", "icon": "app.png", "version":"0.06", "description": "Touch enable left to right launcher.", "tags": "tool,system,launcher", "type":"launch", "storage": [ - {"name":"toucher.app.js","url":"app.js"} + {"name":"toucher.app.js","url":"app.js"}, + {"name":"toucher.settings.js","url":"settings.js"}, + {"name":"toucher.json"} ], "sortorder" : -10 }, diff --git a/apps/toucher/ChangeLog b/apps/toucher/ChangeLog index 0c97d9e13..494110d55 100644 --- a/apps/toucher/ChangeLog +++ b/apps/toucher/ChangeLog @@ -3,4 +3,4 @@ 0.03: Close launcher when lcd turn off 0.04: Complete rewrite to add animation and loop ( issue #210 ) 0.05: Improve perf -0.06: Only store relevant app data (saves RAM when many apps) +0.06: Complete rewrite in 80x80, better perf, add settings \ No newline at end of file diff --git a/apps/toucher/app.js b/apps/toucher/app.js index cf7d5333b..5aac55134 100644 --- a/apps/toucher/app.js +++ b/apps/toucher/app.js @@ -1,160 +1,206 @@ -Bangle.setLCDMode("120x120"); +const Storage = require("Storage"); +const filename = 'toucher.json'; +let settings = Storage.readJSON(filename,1) || { + hightres: true, + animation : true, + frame : 3, + debug: false +}; + +if(!settings.highres) Bangle.setLCDMode("80x80"); +else Bangle.setLCDMode(); + g.clear(); g.flip(); -const Storage = require("Storage"); - -function getApps(){ - return Storage.list(/\.info$/).map(app=>{var a=Storage.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src,version:a.version}}) - .filter(app=>app && (app.type=="app" || app.type=="clock" || !app.type)) - .sort((a,b)=>{ - var n=(0|a.sortorder)-(0|b.sortorder); - if (n) return n; // do sortorder first - if (a.nameb.name) return 1; - return 0; - }); -} +let icons = {}; const HEIGHT = g.getHeight(); const WIDTH = g.getWidth(); const HALF = WIDTH/2; -const ANIMATION_FRAME = 4; -const ANIMATION_STEP = HALF / ANIMATION_FRAME; +const ORIGINAL_ICON_SIZE = 48; + +const STATE = { + settings_open: false, + index: 0, + target: 240, + offset: 0 +}; function getPosition(index){ return (index*HALF); } -let current_app = 0; -let target = 0; -let slideOffset = 0; +function getApps(){ + const exit_app = { + name: 'Exit', + special: true + }; + const raw_apps = Storage.list(/\.info$/).filter(app => app.endsWith('.info')).map(app => Storage.readJSON(app,1) || { name: "DEAD: "+app.substr(1) }) + .filter(app=>app.type=="app" || app.type=="clock" || !app.type) + .sort((a,b)=>{ + var n=(0|a.sortorder)-(0|b.sortorder); + if (n) return n; // do sortorder first + if (a.nameb.name) return 1; + return 0; + }).map(raw => ({ + name: raw.name, + src: raw.src, + icon: raw.icon, + version: raw.version + })); -const back = { - name: 'BACK', - back: true -}; - -let icons = {}; - -const apps = [back].concat(getApps()); -apps.push(back); - -function noIcon(x, y, size){ - const half = size/2; - g.setColor(1,1,1); - g.setFontAlign(-0,0); - const fontSize = Math.floor(size / 30 * 2); - g.setFont('6x8', fontSize); - if(fontSize) g.drawString('-?-', x+1.5, y); - g.drawRect(x-half, y-half, x+half, y+half); + const apps = [Object.assign({}, exit_app)].concat(raw_apps); + apps.push(exit_app); + return apps.map((app, i) => { + app.x = getPosition(i); + return app; + }); } -function drawIcons(offset){ - apps.forEach((app, i) => { - const x = getPosition(i) + HALF - offset; - const y = HALF - (HALF*0.3);//-(HALF*0.7); - let diff = (x - HALF); - if(diff < 0) diff *=-1; +const APPS = getApps(); - const dontRender = x+(HALF/2)<0 || x-(HALF/2)>120; - if(dontRender) { - delete icons[app.name]; +function noIcon(x, y, scale){ + if(scale < 0.2) return; + g.setColor(scale, scale, scale); + g.setFontAlign(0,0); + g.setFont('6x8',settings.highres ? 6:3); + g.drawString('x_x', x+1.5, y); + const h = (ORIGINAL_ICON_SIZE/3); + g.drawRect(x-h, y-h, x+h, y+h); +} + +function render(){ + const start = Date.now(); + + const ANIMATION_FRAME = settings.frame; + const ANIMATION_STEP = Math.floor(HALF / ANIMATION_FRAME); + const THRESHOLD = ANIMATION_STEP - 1; + + g.clear(); + const visibleApps = APPS.filter(app => app.x >= STATE.offset-HALF && app.x <= STATE.offset+WIDTH-HALF ); + + visibleApps.forEach(app => { + + const x = app.x+HALF-STATE.offset; + const y = HALF - (HALF*0.3); + + let dist = HALF - x; + if(dist < 0) dist *= -1; + + const scale = 1 - (dist / HALF); + + if(!scale) return; + + if(app.special){ + const font = settings.highres ? '6x8' : '4x6'; + const fontSize = settings.highres ? 2 : 1; + g.setFont(font, fontSize); + g.setColor(scale,scale,scale); + g.setFontAlign(0,0); + g.drawString(app.name, HALF, HALF); return; } - let size = 30; - if((diff*0.5) < size) size -= (diff*0.5); - else size = 0; - const scale = size / 30; - if(size){ - let c = size / 30 * 2; - c = c -1; - if(c < 0) c = 0; + //draw icon + const icon = app.icon ? + icons[app.name] ? icons[app.name] : Storage.read(app.icon) + : null; - if(app.back){ - g.setFont('6x8', 1); - g.setFontAlign(0, -1); - g.setColor(c,c,c); - g.drawString('Back', HALF, HALF); - return; + if(icon){ + icons[app.name] = icon; + try { + const rescale = settings.highres ? scale*ORIGINAL_ICON_SIZE : (scale*(ORIGINAL_ICON_SIZE/2)); + const imageScale = settings.highres ? scale*2 : scale; + g.drawImage(icon, x-rescale, y-rescale, { scale: imageScale }); + } catch(e){ + noIcon(x, y, scale); } - // icon - - const icon = app.icon ? - icons[app.name] ? icons[app.name] : Storage.read(app.icon) - : null; - if(icon){ - icons[app.name] = icon; - try { - g.drawImage(icon, x-(scale*24), y-(scale*24), { scale: scale }); - } catch(e){ - noIcon(x, y, size); - } - }else{ - noIcon(x, y, size); - } - //text - g.setFont('6x8', 1); - g.setFontAlign(0, -1); - g.setColor(c,c,c); - g.drawString(app.name, HALF, HEIGHT - (HALF*0.7)); - - const type = app.type ? app.type : 'App'; - const version = app.version ? app.version : '0.00'; - const info = type+' v'+version; - g.setFontAlign(0,1); - g.setFont('4x6', 0.25); - g.setColor(c,c,c); - g.drawString(info, HALF, 110, { scale: scale }); + }else{ + noIcon(x, y, scale); } - }); -} -function draw(ignoreLoop){ - g.setColor(0,0,0); - g.fillRect(0,0,WIDTH,HEIGHT); - drawIcons(slideOffset); + //draw text + g.setColor(scale,scale,scale); + if(scale > 0.1){ + const font = settings.highres ? '6x8': '4x6'; + const fontSize = settings.highres ? 2 : 1; + g.setFont(font, fontSize); + g.setFontAlign(0,0); + g.drawString(app.name, HALF, HEIGHT/4*3); + } + + if(settings.highres){ + const type = app.type ? app.type : 'App'; + const version = app.version ? app.version : '0.00'; + const info = type+' v'+version; + g.setFontAlign(0,1); + g.setFont('6x8', 1.5); + g.setColor(scale,scale,scale); + g.drawString(info, HALF, 215, { scale: scale }); + } + + }); + + const duration = Math.floor(Date.now()-start); + if(settings.debug){ + g.setFontAlign(0,1); + g.setColor(0, 1, 0); + const fontSize = settings.highres ? 2 : 1; + g.setFont('4x6',fontSize); + g.drawString('Render: '+duration+'ms', HALF, HEIGHT); + } g.flip(); - if(slideOffset == target) return; - if(slideOffset < target) slideOffset+= ANIMATION_STEP; - else if(slideOffset > target) slideOffset -= ANIMATION_STEP; - if(!ignoreLoop) draw(); + if(STATE.offset == STATE.target) return; + + if(STATE.offset < STATE.target) STATE.offset += ANIMATION_STEP; + else if(STATE.offset > STATE.target) STATE.offset -= ANIMATION_STEP; + + if(STATE.offset >= STATE.target-THRESHOLD && STATE.offset < STATE.target) STATE.offset = STATE.target; + if(STATE.offset <= STATE.target+THRESHOLD && STATE.offset > STATE.target) STATE.offset = STATE.target; + setTimeout(render, 0); } function animateTo(index){ - target = getPosition(index); - draw(); -} -function goTo(index){ - current_app = index; - target = getPosition(index); - slideOffset = target; - draw(true); + STATE.index = index; + STATE.target = getPosition(index); + render(); } -goTo(1); +function jumpTo(index){ + STATE.index = index; + STATE.target = getPosition(index); + STATE.offset = STATE.target; + render(); +} function prev(){ - if(current_app == 0) goTo(apps.length-1); - current_app -= 1; - if(current_app < 0) current_app = 0; - animateTo(current_app); + if(STATE.settings_open) return; + if(STATE.index == 0) jumpTo(APPS.length-1); + setTimeout(() => { + if(!settings.animation) jumpTo(STATE.index-1); + else animateTo(STATE.index-1); + },1); } function next(){ - if(current_app == apps.length-1) goTo(0); - current_app += 1; - if(current_app > apps.length-1) current_app = apps.length-1; - animateTo(current_app); + if(STATE.settings_open) return; + if(STATE.index == APPS.length-1) jumpTo(0); + setTimeout(() => { + if(!settings.animation) jumpTo(STATE.index+1); + else animateTo(STATE.index+1); + },1); } -function run() { - const app = apps[current_app]; - if(app.back) return load(); +function run(){ + + const app = APPS[STATE.index]; + if(app.name == 'Exit') return load(); + if (Storage.read(app.src)===undefined) { E.showMessage("App Source\nNot found"); - setTimeout(draw, 2000); + setTimeout(render, 2000); } else { Bangle.setLCDMode(); g.clear(); @@ -162,15 +208,12 @@ function run() { E.showMessage("Loading..."); load(app.src); } + } - -setWatch(prev, BTN1, { repeat: true }); -setWatch(next, BTN3, { repeat: true }); -setWatch(run, BTN2, {repeat:true,edge:"falling"}); - // Screen event Bangle.on('touch', function(button){ + if(STATE.settings_open) return; switch(button){ case 1: prev(); @@ -185,6 +228,7 @@ Bangle.on('touch', function(button){ }); Bangle.on('swipe', dir => { + if(STATE.settings_open) return; if(dir == 1) prev(); else next(); }); @@ -193,3 +237,10 @@ Bangle.on('swipe', dir => { Bangle.on('lcdPower', on => { if(!on) return load(); }); + + +setWatch(prev, BTN1, { repeat: true }); +setWatch(next, BTN3, { repeat: true }); +setWatch(run, BTN2, { repeat:true }); + +jumpTo(1); \ No newline at end of file diff --git a/apps/toucher/settings.js b/apps/toucher/settings.js new file mode 100644 index 000000000..6f7320513 --- /dev/null +++ b/apps/toucher/settings.js @@ -0,0 +1,59 @@ +(function(back) { + + const Storage = require("Storage"); + const filename = 'toucher.json'; + let settings = Storage.readJSON(filename,1)|| null; + + function getSettings(){ + return { + highres: true, + animation : true, + frame : 3, + debug: true + }; + } + + function updateSettings() { + require("Storage").writeJSON(filename, settings); + Bangle.buzz(); + } + + if(!settings){ + settings = getSettings(); + updateSettings(); + } + + function saveChange(name){ + return function(v){ + settings[name] = v; + updateSettings(); + } + } + + E.showMenu({ + '': { 'title': 'Toucher settings' }, + "Resolution" : { + value : settings.highres, + format : v => v?"High":"Low", + onchange: v => { + saveChange('highres')(!settings.highres); + } + }, + "Animation" : { + value : settings.animation, + format : v => v?"On":"Off", + onchange : saveChange('animation') + }, + "Frame rate" : { + value : settings.frame, + min: 1, max: 10, step: 1, + onchange : saveChange('frame') + }, + "Debug" : { + value : settings.debug, + format : v => v?"On":"Off", + onchange : saveChange('debug') + }, + '< Back': back + }); +}); \ No newline at end of file From 1cdbe79bac650fa639cf57ea4378c5148711ac10 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Thu, 23 Apr 2020 15:47:57 +0100 Subject: [PATCH 249/302] Tweak favourite apps code to remove the duplicated app uploader --- js/index.js | 130 +++++++++++++++++++++------------------------------- js/utils.js | 13 ------ 2 files changed, 51 insertions(+), 92 deletions(-) diff --git a/js/index.js b/js/index.js index 51b1d71c3..665b5b62b 100644 --- a/js/index.js +++ b/js/index.js @@ -1,7 +1,6 @@ var appJSON = []; // List of apps and info from apps.json var appsInstalled = []; // list of app JSON var files = []; // list of files on Bangle -var favourites = []; // list of user favourite app const FAVOURITE = "favouriteapps.json"; httpGet("apps.json").then(apps=>{ @@ -144,7 +143,13 @@ function handleAppInterface(app) { }); } -function handleAppFavourite(favourite, app){ +function getAppFavourites() { + var f = localStorage.getItem(FAVOURITE); + return (f === null) ? ["boot","launch","setting"] : JSON.parse(f); +} + +function changeAppFavourite(favourite, app) { + var favourites = getAppFavourites(); if (favourite) { favourites = favourites.concat([app.id]); } else { @@ -154,7 +159,7 @@ function handleAppFavourite(favourite, app){ favourites = favourites.filter(e => e != app.id); } } - localStorage.setItem("favouriteapps.json", JSON.stringify(favourites)); + localStorage.setItem(FAVOURITE, JSON.stringify(favourites)); refreshLibrary(); } @@ -187,6 +192,7 @@ function refreshFilter(){ function refreshLibrary() { var panelbody = document.querySelector("#librarycontainer .panel-body"); var visibleApps = appJSON; + var favourites = getAppFavourites(); if (activeFilter) { if ( activeFilter == "favourites" ) { @@ -200,8 +206,6 @@ function refreshLibrary() { visibleApps = visibleApps.filter(app => app.name.toLowerCase().includes(currentSearch) || app.tags.includes(currentSearch)); } - favourites = (localStorage.getItem(FAVOURITE)) === null ? JSON.parse('["boot","launch","setting"]') : JSON.parse(localStorage.getItem("favouriteapps.json")); - panelbody.innerHTML = visibleApps.map((app,idx) => { var appInstalled = appsInstalled.find(a=>a.id==app.id); var version = getVersionInfo(app, appInstalled); @@ -275,9 +279,9 @@ function refreshLibrary() { } else if (icon.classList.contains("icon-download")) { handleAppInterface(app); } else if ( button.innerText == String.fromCharCode(0x2661)) { - handleAppFavourite(true, app); + changeAppFavourite(true, app); } else if ( button.innerText == String.fromCharCode(0x2665) ) { - handleAppFavourite(false, app); + changeAppFavourite(false, app); } }); }); @@ -478,6 +482,43 @@ function getInstalledApps(refresh) { }); } +/// Removes everything and install the given apps, eg: installMultipleApps(["boot","mclock"], "minimal") +function installMultipleApps(appIds, promptName) { + var apps = appIds.map( appid => appJSON.find(app=>app.id==appid) ); + if (apps.some(x=>x===undefined)) + return Promise.reject("Not all apps found"); + var appCount = apps.length; + return showPrompt("Install Defaults",`Remove everything and install ${promptName} apps?`).then(() => { + return Comms.removeAllApps(); + }).then(()=>{ + Progress.hide({sticky:true}); + appsInstalled = []; + showToast(`Existing apps removed. Installing ${appCount} apps...`); + return new Promise((resolve,reject) => { + function upload() { + var app = apps.shift(); + if (app===undefined) return resolve(); + Progress.show({title:`${app.name} (${appCount-apps.length}/${appCount})`,sticky:true}); + Comms.uploadApp(app,"skip_reset").then((appJSON) => { + Progress.hide({sticky:true}); + if (appJSON) appsInstalled.push(appJSON); + showToast(`(${appCount-apps.length}/${appCount}) ${app.name} Uploaded`); + upload(); + }).catch(function() { + Progress.hide({sticky:true}); + reject(); + }); + } + upload(); + }); + }).then(()=>{ + return Comms.setTime(); + }).then(()=>{ + showToast("Apps successfully installed!","success"); + return getInstalledApps(true); + }); +} + var connectMyDeviceBtn = document.getElementById("connectmydevice"); function handleConnectionChange(connected) { @@ -571,42 +612,8 @@ document.getElementById("removeall").addEventListener("click",event=>{ }); // Install all default apps in one go document.getElementById("installdefault").addEventListener("click",event=>{ - var defaultApps, appCount; httpGet("defaultapps.json").then(json=>{ - defaultApps = JSON.parse(json); - defaultApps = defaultApps.map( appid => appJSON.find(app=>app.id==appid) ); - if (defaultApps.some(x=>x===undefined)) - throw "Not all apps found"; - appCount = defaultApps.length; - return showPrompt("Install Defaults","Remove everything and install default apps?"); - }).then(() => { - return Comms.removeAllApps(); - }).then(()=>{ - Progress.hide({sticky:true}); - appsInstalled = []; - showToast(`Existing apps removed. Installing ${appCount} apps...`); - return new Promise((resolve,reject) => { - function upload() { - var app = defaultApps.shift(); - if (app===undefined) return resolve(); - Progress.show({title:`${app.name} (${appCount-defaultApps.length}/${appCount})`,sticky:true}); - Comms.uploadApp(app,"skip_reset").then((appJSON) => { - Progress.hide({sticky:true}); - if (appJSON) appsInstalled.push(appJSON); - showToast(`(${appCount-defaultApps.length}/${appCount}) ${app.name} Uploaded`); - upload(); - }).catch(function() { - Progress.hide({sticky:true}); - reject(); - }); - } - upload(); - }); - }).then(()=>{ - return Comms.setTime(); - }).then(()=>{ - showToast("Default apps successfully installed!","success"); - return getInstalledApps(true); + return installMultipleApps(JSON.parse(json), "default"); }).catch(err=>{ Progress.hide({sticky:true}); showToast("App Install failed, "+err,"error"); @@ -615,43 +622,8 @@ document.getElementById("installdefault").addEventListener("click",event=>{ // Install all favoutrie apps in one go document.getElementById("installfavourite").addEventListener("click",event=>{ - var defaultApps, appCount; - asyncLocalStorage.getItem(FAVOURITE).then(json=>{ - defaultApps = JSON.parse(json); - defaultApps = defaultApps.map( appid => appJSON.find(app=>app.id==appid) ); - if (defaultApps.some(x=>x===undefined)) - throw "Not all apps found"; - appCount = defaultApps.length; - return showPrompt("Install Defaults","Remove everything and install favourite apps?"); - }).then(() => { - return Comms.removeAllApps(); - }).then(()=>{ - Progress.hide({sticky:true}); - appsInstalled = []; - showToast(`Existing apps removed. Installing ${appCount} apps...`); - return new Promise((resolve,reject) => { - function upload() { - var app = defaultApps.shift(); - if (app===undefined) return resolve(); - Progress.show({title:`${app.name} (${appCount-defaultApps.length}/${appCount})`,sticky:true}); - Comms.uploadApp(app,"skip_reset").then((appJSON) => { - Progress.hide({sticky:true}); - if (appJSON) appsInstalled.push(appJSON); - showToast(`(${appCount-defaultApps.length}/${appCount}) ${app.name} Uploaded`); - upload(); - }).catch(function() { - Progress.hide({sticky:true}); - reject(); - }); - } - upload(); - }); - }).then(()=>{ - return Comms.setTime(); - }).then(()=>{ - showToast("Favourites apps successfully installed!","success"); - return getInstalledApps(true); - }).catch(err=>{ + var favApps = getAppFavourites(); + installMultipleApps(favApps, "favourite").catch(err=>{ Progress.hide({sticky:true}); showToast("App Install failed, "+err,"error"); }); diff --git a/js/utils.js b/js/utils.js index f4670da3c..53eeb1868 100644 --- a/js/utils.js +++ b/js/utils.js @@ -79,16 +79,3 @@ function getVersionInfo(appListing, appInstalled) { canUpdate : canUpdate } } - -const asyncLocalStorage = { - setItem: function (key, value) { - return Promise.resolve().then(function () { - localStorage.setItem(key, value); - }); - }, - getItem: function (key) { - return Promise.resolve().then(function () { - return localStorage.getItem(key); - }); - } -}; From e93a9125e4a5999f189364d5860e6ce0546802e3 Mon Sep 17 00:00:00 2001 From: Dimitri Gigot Date: Thu, 23 Apr 2020 15:53:51 +0000 Subject: [PATCH 250/302] put toucher.json correctly in the apps.json --- apps.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps.json b/apps.json index ad5b20820..6d67d8b40 100644 --- a/apps.json +++ b/apps.json @@ -1098,10 +1098,12 @@ "description": "Touch enable left to right launcher.", "tags": "tool,system,launcher", "type":"launch", + "data": [ + {"name":"toucher.json"} + ], "storage": [ {"name":"toucher.app.js","url":"app.js"}, - {"name":"toucher.settings.js","url":"settings.js"}, - {"name":"toucher.json"} + {"name":"toucher.settings.js","url":"settings.js"} ], "sortorder" : -10 }, From 4441f3055cf5db49660e2b5ec07e78b1ba0ca161 Mon Sep 17 00:00:00 2001 From: Fredrik Lautrup Date: Thu, 23 Apr 2020 19:05:22 +0200 Subject: [PATCH 251/302] Added local and support for 12h clock --- apps/rclock/rclock.app.js | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/apps/rclock/rclock.app.js b/apps/rclock/rclock.app.js index bd8395116..a8debe282 100644 --- a/apps/rclock/rclock.app.js +++ b/apps/rclock/rclock.app.js @@ -4,6 +4,8 @@ var hours; var date; var first = true; + var locale = require('locale'); + var _12hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"]||false; const screen = { width: g.getWidth(), @@ -41,17 +43,7 @@ }; const dateStr = function (date) { - day = date.getDate(); - month = date.getMonth(); - year = date.getFullYear(); - if (day < 10) { - day = "0" + day; - } - if (month < 10) { - month = "0" + month; - } - - return year + "-" + month + "-" + day; + return locale.date(new Date(),1); }; const getArcXY = function (centerX, centerY, radius, angle) { @@ -133,9 +125,22 @@ //Write the time as configured in the settings hours = currentTime.getHours(); + if(_12hour && hours>13) { + hours=hours-12; + } + + var medidian=locale.medidian(new Date()); + var timestr; + + if(medidian.length>0) { + timestr=hour+" "+medidian; + } else { + timestr=hour; + } + g.setColor(settings.time.color); g.setFont(settings.time.font, settings.time.size); - g.drawString(hours, settings.time.center, settings.time.middle); + g.drawString(timestr, settings.time.center, settings.time.middle); //Write the date as configured in the settings g.setColor(settings.date.color); From 9ce908761a2d826f801e8289123ad195bfce8d3f Mon Sep 17 00:00:00 2001 From: Fredrik Lautrup Date: Thu, 23 Apr 2020 19:09:31 +0200 Subject: [PATCH 252/302] Updated changelog --- apps/rclock/ChangeLog | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/rclock/ChangeLog b/apps/rclock/ChangeLog index a8f708a0a..23b1a6e87 100644 --- a/apps/rclock/ChangeLog +++ b/apps/rclock/ChangeLog @@ -1 +1,2 @@ 0.01: First published version of app +0.02: Added support for locale and 12H clock \ No newline at end of file From 0874fb698a882b22fa473a4b59d7fa7acf36555b Mon Sep 17 00:00:00 2001 From: bengwalker <63957296+bengwalker@users.noreply.github.com> Date: Thu, 23 Apr 2020 19:14:18 +0200 Subject: [PATCH 253/302] Update README.md --- apps/metronome/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/metronome/README.md b/apps/metronome/README.md index aab2d5a3f..1bb9a893c 100644 --- a/apps/metronome/README.md +++ b/apps/metronome/README.md @@ -11,4 +11,4 @@ This metronome makes your watch blink and vibrate with a given rate. ## Attributions -"Icon made by Roundicons from www.flaticon.com" \ No newline at end of file +Icon made by Roundicons from www.flaticon.com From 5c3e5ff2eeb7b7e396ef5193f9a911c24329bce0 Mon Sep 17 00:00:00 2001 From: Fredrik Lautrup Date: Thu, 23 Apr 2020 19:35:18 +0200 Subject: [PATCH 254/302] Fixing so that hour is shown right --- apps/rclock/rclock.app.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/apps/rclock/rclock.app.js b/apps/rclock/rclock.app.js index a8debe282..15bc3caf0 100644 --- a/apps/rclock/rclock.app.js +++ b/apps/rclock/rclock.app.js @@ -129,13 +129,20 @@ hours=hours-12; } - var medidian=locale.medidian(new Date()); + var meridian; + + if (typeof locale.meridian === "function") { + meridian=locale.meridian(new Date()); + } else { + meridian=""; + } + var timestr; - if(medidian.length>0) { - timestr=hour+" "+medidian; + if(meridian.length>0 && _12hour) { + timestr=hours+" "+meridian; } else { - timestr=hour; + timestr=hours; } g.setColor(settings.time.color); From 43ae16f1c0154024ececda3c1786483a911b4bdb Mon Sep 17 00:00:00 2001 From: Fredrik Lautrup Date: Thu, 23 Apr 2020 19:43:48 +0200 Subject: [PATCH 255/302] Clean up code format --- apps/rclock/rclock.app.js | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/apps/rclock/rclock.app.js b/apps/rclock/rclock.app.js index 15bc3caf0..4e63fe36a 100644 --- a/apps/rclock/rclock.app.js +++ b/apps/rclock/rclock.app.js @@ -5,7 +5,7 @@ var date; var first = true; var locale = require('locale'); - var _12hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"]||false; + var _12hour = (require("Storage").readJSON("setting.json", 1) || {})["12hour"] || false; const screen = { width: g.getWidth(), @@ -43,7 +43,7 @@ }; const dateStr = function (date) { - return locale.date(new Date(),1); + return locale.date(new Date(), 1); }; const getArcXY = function (centerX, centerY, radius, angle) { @@ -64,7 +64,7 @@ //g.setPixel(r[0],r[1]); g.drawLine(r1[0], r1[1], r2[0], r2[1]); g.setColor('#333333'); - g.drawCircle(settings.circle.middle, settings.circle.center, rad - settings.circle.width-4) + g.drawCircle(settings.circle.middle, settings.circle.center, rad - settings.circle.width - 4) }; const drawSecArc = function (sections, color) { @@ -76,7 +76,7 @@ //g.setPixel(r[0],r[1]); g.drawLine(r1[0], r1[1], r2[0], r2[1]); g.setColor('#333333'); - g.drawCircle(settings.circle.middle, settings.circle.center, rad - settings.circle.width-4) + g.drawCircle(settings.circle.middle, settings.circle.center, rad - settings.circle.width - 4) }; const drawClock = function () { @@ -96,7 +96,7 @@ } first = false; } - + // Reset seconds if (seconds == 59) { g.setColor('#000000'); @@ -120,29 +120,29 @@ //Update seconds when needed if (seconds != currentTime.getSeconds()) { seconds = currentTime.getSeconds(); - drawSecArc(seconds, settings.circle.colorsec); + drawSecArc(seconds, settings.circle.colorsec); } //Write the time as configured in the settings hours = currentTime.getHours(); - if(_12hour && hours>13) { - hours=hours-12; + if (_12hour && hours > 13) { + hours = hours - 12; } var meridian; - if (typeof locale.meridian === "function") { - meridian=locale.meridian(new Date()); + if (typeof locale.meridian === "function") { + meridian = locale.meridian(new Date()); } else { - meridian=""; + meridian = ""; } var timestr; - if(meridian.length>0 && _12hour) { - timestr=hours+" "+meridian; + if (meridian.length > 0 && _12hour) { + timestr = hours + " " + meridian; } else { - timestr=hours; + timestr = hours; } g.setColor(settings.time.color); @@ -161,7 +161,7 @@ // clean app screen g.clear(); - g.setFontAlign( 0, 0, 0); + g.setFontAlign(0, 0, 0); Bangle.loadWidgets(); Bangle.drawWidgets(); From 1223184ee027e66aad9be82976d89e6eb1f3658c Mon Sep 17 00:00:00 2001 From: Amos Blanton Date: Sat, 25 Apr 2020 12:11:07 +0200 Subject: [PATCH 256/302] Add imprecise word clock. --- apps.json | 13 +++ apps/impwclock/clock-impword-icon.js | 1 + apps/impwclock/clock-impword.js | 160 +++++++++++++++++++++++++++ apps/impwclock/clock-impword.png | Bin 0 -> 10763 bytes 4 files changed, 174 insertions(+) create mode 100644 apps/impwclock/clock-impword-icon.js create mode 100644 apps/impwclock/clock-impword.js create mode 100644 apps/impwclock/clock-impword.png diff --git a/apps.json b/apps.json index 6d67d8b40..65f321a91 100644 --- a/apps.json +++ b/apps.json @@ -163,6 +163,19 @@ {"name":"wclock.img","url":"clock-word-icon.js","evaluate":true} ] }, + { "id": "impwclock", + "name": "Imprecise Word Clock", + "icon": "clock-impword.png", + "version":"0.01", + "description": "Imprecise word clock for vacations, weekends, and those who never need accurate time.", + "tags": "clock", + "type":"clock", + "allow_emulator":true, + "storage": [ + {"name":"impwclock.app.js","url":"clock-impword.js"}, + {"name":"impwclock.img","url":"clock-impword-icon.js","evaluate":true} + ] + }, { "id": "aclock", "name": "Analog Clock", "icon": "clock-analog.png", diff --git a/apps/impwclock/clock-impword-icon.js b/apps/impwclock/clock-impword-icon.js new file mode 100644 index 000000000..f5ed47f1f --- /dev/null +++ b/apps/impwclock/clock-impword-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkEIf4A3iIBEn8ggP//8wgX/+cQl8Agc/BQPyCokQgHzmEB+ET+EfmMj+AXCmABBF4MBiIABiEC+PxC4Uwn4NB+QXMBAMzI4UxmYOBC5sfCgIvBgPzF4cfC5BgCFAMPkPwiXzL4cPmMvkAXDPAnzEgMxR4wDCGITl/AH4ApgUQbIICBAgXwBYMD+UAYoP/l4CBiUhd4QXFgIXCh73BfQUfAgIPBC4cQiIACC4cvj4PBC5AuCC48zgcwC4ZHBC5sBCAIEBF5EAC4RgDCQItCPAIXLCoQBBFgM/IoZHER4QA/AH4Anj8wgXzgX/+cQWoPyYQK9Bn/zj/wb4MTCAMf+MDAYMxkfwj8BmYXBmEzCYMf+cDmPzkMvj8zAIM/eoPyC4fy+IXDl8TmfwI4UvmYABAwIXB//xgPwBIIXCgYFBmEP/8fh/yF4sDC4QjBC4RvBF4UPB4JUBL4kAn8ROIJbBC4IIBL4hDBmaPEgBuB+EB+aPCUQUjCALn/AH4A/A")) \ No newline at end of file diff --git a/apps/impwclock/clock-impword.js b/apps/impwclock/clock-impword.js new file mode 100644 index 000000000..dafbbfcf5 --- /dev/null +++ b/apps/impwclock/clock-impword.js @@ -0,0 +1,160 @@ +/* Imprecise Word Clock - A. Blanton +A remix of word clock +by Gordon Williams https://github.com/gfwilliams +- Changes the representation of time to be more general +- Shows accurate digital time when button 1 is pressed +*/ +/* jshint esversion: 6 */ + +const allWords = [ + "AEARLYDN", + "LATEYRZO", + "MORNINGO", + "KMIDDLEN", + "AFTERDAY", + "OFDZTHEC", + "EVENINGR", + "ORMNIGHT" +]; + + +const timeOfDay = { + 0: ["", 0, 0], + 1: ["EARLYMORNING", 10, 11, 12, 13, 14, 02, 12, 22, 32, 42, 52, 62], + 2: ["MORNING", 02, 12, 22, 32, 42, 52, 62], + 3: ["LATEMORNING", 01, 11, 21, 31, 02, 12, 22, 32, 42, 52, 62], + 4: ["MIDDAY", 13, 23, 33, 54, 64, 74], + 5: ["EARLYAFTERNOON", 10, 20, 30, 40, 50, 04, 14, 24, 34, 44, 70, 71, 72, 73], + 6: ["AFTERNOON", 04, 14, 24, 34, 44, 70, 71, 72, 73], + 7: ["LATEAFTERNOON", 01, 11, 21, 31, 04, 14, 24, 34, 44, 70, 71, 72, 73], + 8: ["EARLYEVENING", 10, 20, 30, 40, 50, 06, 16, 26, 36, 46, 56, 66], + 9: ["EVENING", 06, 16, 26, 36, 46, 56, 66], + 10: ["NIGHT", 37, 47, 57, 67, 77], + 11: ["MIDDLEOFTHENIGHT", 32, 33, 34, 35, 36, 37, 50, 51, 54, 55, 56, 73,74,75,76,77 ], +}; + + +// offsets and increments +const xs = 35; +const ys = 31; +const dy = 22; +const dx = 25; + +// font size and color +const fontSize = 3; // "6x8" +const passivColor = 0x3186 /*grey*/ ; +const activeColorNight = 0xF800 /*red*/ ; +const activeColorDay = 0xFFFF /* white */; + +function drawWordClock() { + + + // get time + var t = new Date(); + var h = t.getHours(); + var m = t.getMinutes(); + var time = ("0" + h).substr(-2) + ":" + ("0" + m).substr(-2); + var day = t.getDay(); + + var hidx; + + var activeColor = activeColorDay; + if(h < 7 || h > 19) {activeColor = activeColorNight;} + + g.setFont("6x8",fontSize); + g.setColor(passivColor); + g.setFontAlign(0, -1, 0); + + // draw allWords + var c; + var y = ys; + var x = xs; + allWords.forEach((line) => { + x = xs; + for (c in line) { + g.drawString(line[c], x, y); + x += dx; + } + y += dy; + }); + + + // Switch case isn't good for this in Js apparently so... + if(h < 3){ + // Middle of the Night + hidx = 11; + } + else if (h < 7){ + // Early Morning + hidx = 1; + } + else if (h < 10){ + // Morning + hidx = 2; + } + else if (h < 12){ + // Late Morning + hidx = 3; + } + else if (h < 13){ + // Midday + hidx = 4; + } + else if (h < 14){ + // Early afternoon + hidx = 5; + } + else if (h < 16){ + // Afternoon + hidx = 6; + } + else if (h < 17){ + // Late Afternoon + hidx = 7; + } + else if (h < 19){ + // Early evening + hidx = 8; + } + else if (h < 21){ + // evening + hidx = 9; + } + else if (h < 24){ + // Night + hidx = 10; + } + + // write hour in active color + g.setColor(activeColor); + timeOfDay[hidx][0].split('').forEach((c, pos) => { + x = xs + (timeOfDay[hidx][pos + 1] / 10 | 0) * dx; + y = ys + (timeOfDay[hidx][pos + 1] % 10) * dy; + g.drawString(c, x, y); + }); + + + // Display digital time while button 1 is pressed + if (BTN1.read()){ + g.setColor(activeColor); + g.clearRect(0, 215, 240, 240); + g.drawString(time, 120, 215); + } else { g.clearRect(0, 215, 240, 240); } + +} + +Bangle.on('lcdPower', function(on) { + if (on) drawWordClock(); +}); + +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +setInterval(drawWordClock, 1E4); +drawWordClock(); + +// Show digital time while top button is pressed +setWatch(drawWordClock, BTN1, {repeat:true,edge:"both"}); + +// Show launcher when middle button pressed +setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"}); diff --git a/apps/impwclock/clock-impword.png b/apps/impwclock/clock-impword.png new file mode 100644 index 0000000000000000000000000000000000000000..e7ed0e8282583a96e7bbd8bc444813af273260f7 GIT binary patch literal 10763 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 JPDHLkV1n2bV Date: Sat, 25 Apr 2020 18:25:13 +0200 Subject: [PATCH 257/302] Add readme. --- apps/impwclock/README.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 apps/impwclock/README.md diff --git a/apps/impwclock/README.md b/apps/impwclock/README.md new file mode 100644 index 000000000..30e42c95e --- /dev/null +++ b/apps/impwclock/README.md @@ -0,0 +1,4 @@ +# Imprecise Word Clock + +This clock tells time in very rough approximation, as in "Late morning" or "Early afternoon." Good for vacations and weekends. Press button 1 to see the time in accurate, digital form. But do you really need to know the exact time? + From 152b0e617b9b852058aee788726da41fb7a0abe3 Mon Sep 17 00:00:00 2001 From: Ignas Bukys Date: Sat, 25 Apr 2020 23:02:10 +0300 Subject: [PATCH 258/302] locale.meridian function does not exists if languages app is not installed --- apps/barclock/clock-bar.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/barclock/clock-bar.js b/apps/barclock/clock-bar.js index da436daee..0f2609298 100644 --- a/apps/barclock/clock-bar.js +++ b/apps/barclock/clock-bar.js @@ -12,7 +12,12 @@ date.setMonth(1, 3) // februari: months are zero-indexed const localized = locale.date(date, true) locale.dayFirst = /3.*2/.test(localized) - locale.hasMeridian = (locale.meridian(date) !== '') + + locale.hasMeridian = false + if(typeof locale.meridian === 'function') { // function does not exists if languages app is not installed + locale.hasMeridian = (locale.meridian(date) !== '') + } + } const screen = { width: g.getWidth(), From cff313e89db2141d060d484cc73d1eba3b125e4b Mon Sep 17 00:00:00 2001 From: Ignas Bukys Date: Sat, 25 Apr 2020 23:05:36 +0300 Subject: [PATCH 259/302] version update --- apps.json | 2 +- apps/barclock/ChangeLog | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps.json b/apps.json index 6d67d8b40..e39756b84 100644 --- a/apps.json +++ b/apps.json @@ -1012,7 +1012,7 @@ { "id": "barclock", "name": "Bar Clock", "icon": "clock-bar.png", - "version":"0.04", + "version":"0.05", "description": "A simple digital clock showing seconds as a bar", "tags": "clock", "type":"clock", diff --git a/apps/barclock/ChangeLog b/apps/barclock/ChangeLog index 2e0fd088c..616ee66e9 100644 --- a/apps/barclock/ChangeLog +++ b/apps/barclock/ChangeLog @@ -2,3 +2,4 @@ 0.02: Apply locale, 12-hour setting 0.03: Fix dates drawing over each other at midnight 0.04: Small bugfix +0.05: Clock does not start if app Languages is not installed \ No newline at end of file From 66264ea61c300564f433a87515b6d982e2b9f79c Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Sun, 26 Apr 2020 03:29:45 +0200 Subject: [PATCH 260/302] ballmaze game: Navigate a ball through a maze using the accelerometer --- apps.json | 16 ++ apps/ballmaze/README.md | 15 + apps/ballmaze/app.js | 514 ++++++++++++++++++++++++++++++++++ apps/ballmaze/icon.js | 1 + apps/ballmaze/icon.png | Bin 0 -> 444 bytes apps/ballmaze/maze.png | Bin 0 -> 2850 bytes apps/ballmaze/size_select.png | Bin 0 -> 4409 bytes 7 files changed, 546 insertions(+) create mode 100644 apps/ballmaze/README.md create mode 100644 apps/ballmaze/app.js create mode 100644 apps/ballmaze/icon.js create mode 100644 apps/ballmaze/icon.png create mode 100644 apps/ballmaze/maze.png create mode 100644 apps/ballmaze/size_select.png diff --git a/apps.json b/apps.json index 6d67d8b40..9652b3e19 100644 --- a/apps.json +++ b/apps.json @@ -1482,5 +1482,21 @@ {"name":"pong.app.js","url":"app.js"}, {"name":"pong.img","url":"app-icon.js","evaluate":true} ] + }, + { "id": "ballmaze", + "name": "Ball Maze", + "icon": "icon.png", + "version": "0.01", + "description": "Navigate a ball through a maze by tilting your watch.", + "readme": "README.md", + "tags": "game", + "type": "app", + "storage": [ + {"name": "ballmaze.app.js","url":"app.js"}, + {"name": "ballmaze.img","url":"icon.js","evaluate": true} + ], + "data": [ + {"name": "ballmaze.json"} + ] } ] diff --git a/apps/ballmaze/README.md b/apps/ballmaze/README.md new file mode 100644 index 000000000..22a295686 --- /dev/null +++ b/apps/ballmaze/README.md @@ -0,0 +1,15 @@ +# Ball Maze + +Navigate a ball through a maze by tilting your watch. + +![Screenshot](size_select.png) +![Screenshot](maze.png) + +## Usage + +Select a maze size to begin the game. +Tilt your watch to steer the ball towards the target and advance to the next level. + +## Creator + +Richard de Boer diff --git a/apps/ballmaze/app.js b/apps/ballmaze/app.js new file mode 100644 index 000000000..b249d6494 --- /dev/null +++ b/apps/ballmaze/app.js @@ -0,0 +1,514 @@ +(() => { + let intervalID; + let settings = require("Storage").readJSON("ballmaze.json") || {}; + + // density, elasticity of bounces, "drag coefficient" + const rho = 100, e = 0.3, C = 0.01; + // screen width & height in pixels + const sW = 240, sH = 160; + // gravity constant (lowercase was already taken) + const G = 9.80665; + + // wall bit flags + const TOP = 1<<0, LEFT = 1<<1, BOTTOM = 1<<2, RIGHT = 1<<3, + LINKED = 1<<4; // used in maze generation + + // The play area is 240x160, sizes are the ball radius, so we can use common + // denominators of 120x80 to get square rooms + // Reverse the order to show the easiest on top of the menu + const sizes = [1, 2, 4, 5, 8, 10, 16, 20, 40].reverse(), + // even size 1 actually works, but larger mazes take forever to generate + minSize = 4, defaultSize = 10; + const sizeNames = { + 1: "Insane", 2: "Gigantic", 4: "Enormous", 5: "Huge", 8: "Large", + 10: "Medium", 16: "Small", 20: "Tiny", 40: "Trivial", + }; + + /** + * Draw something to all screen buffers + * @param draw {function} Callback which performs the drawing + */ + function drawAll(draw) { + draw(); + g.flip(); + draw(); + g.flip(); + } + + /** + * Clear all buffers + */ + function clearAll() { + drawAll(() => g.clear()); + } + + // use unbuffered graphics for UI stuff + function showMessage(message, title) { + Bangle.setLCDMode(); + return E.showMessage(message, title); + } + + function showPrompt(prompt, options) { + Bangle.setLCDMode(); + return E.showPrompt(prompt, options); + } + + function showMenu(menu) { + Bangle.setLCDMode(); + return E.showMenu(menu); + } + + const sign = (n) => n<0?-1:1; // we don't really care about zero + + /** + * Play the game, using a ball with radius size + * @param size {number} + */ + function playMaze(size) { + const r = size; + // ball mass, weight, "drag" + // Yes, larger maze = larger ball = heavier ball + // (atm our physics is so oversimplified that mass cancels out though) + const m = rho*(r*r*r), w = G*m, d = C*w; + + // number of columns/rows + const cols = Math.round(sW/(r*2.5)), + rows = Math.round(sH/(r*2.5)); + // width & height of one column/row in pixels + const cW = sW/cols, rH = sH/rows; + + // list of rooms, every room can have one or more wall bits set + // actual layout: 0 1 2 + // 3 4 5 + // this means that for room with index "i": (except edge cases!) + // i-1 = room to the left + // i+1 = room to the right + // i-cols = room above + // i+cols = room below + let rooms = new Uint8Array(rows*cols); + // shortest route from start to finish + let route; + + let x, y, // current position + px, py, ppx, ppy, // previous positions (for erasing old image) + vx, vy; // velocity + + function start() { + // start in top left corner + x = cW/2; + y = rH/2; + vx = vy = 0; + ppx = px = x; + ppy = py = y + + clearWatch(); + generateMaze(); // this shows unbuffered progress messages + if (settings.cheat && r>1) findRoute(); // not enough memory for r==1 :-( + + Bangle.setLCDMode("doublebuffered"); + clearAll(); + drawAll(drawMaze); + intervalID = setInterval(tick, 100); + } + + // Position conversions + // index: index of room in rooms[] + // rowcol: position measured in roomsizes + // xy: position measured in pixels + /** + * Index from RowCol + * @param row {number} + * @param col {number} + * @returns {number} rooms[] index + */ + function iFromRC(row, col) { + return row*cols+col; + } + + /** + * RowCol from index + * @param index {number} + * @returns {(number)[]} [row,column] + */ + function rcFromI(index) { + return [ + Math.floor(index/cols), + index%cols, + ]; + } + + /** + * RowCol from Xy + * @param x {number} + * @param y {number} + * @returns {(number)[]} [row,column] + */ + function rcFromXy(x, y) { + return [ + Math.floor(y/sH*rows), + Math.floor(x/sW*cols), + ]; + } + + /** + * Link another room up + * @param index {number} Dig from already linked room with this index + * @param dir {number} in this direction + * @return {number} index of room we just linked up + */ + function dig(index, dir) { + rooms[index] &= ~dir; + let neighbour; + switch(dir) { + case LEFT: + neighbour = index-1; + rooms[neighbour] &= ~RIGHT; + break; + case RIGHT: + neighbour = index+1; + rooms[neighbour] &= ~LEFT; + break; + case TOP: + neighbour = index-cols; + rooms[neighbour] &= ~BOTTOM; + break; + case BOTTOM: + neighbour = index+cols; + rooms[neighbour] &= ~TOP; + break; + } + rooms[neighbour] |= LINKED; + return neighbour; + } + + /** + * Generate the maze + */ + function generateMaze() { + // Maze generation basically works like this: + // 1. Start with all rooms set to completely walled off and "unlinked" + // 2. Then mark a room as "linked", and add it to the "to do" list + // 3. When the "to do" list is empty, we're done + // 4. pick a random room from the list + // 5. if all adjacent rooms are linked -> remove room from list, goto 3 + // 6. pick a random unlinked adjacent room + // 7. remove the walls between the rooms + // 8. mark the adjacent room as linked and add it to the "to do" list + // 9. go to 4 + let pdotnum = 0; + const title = "Please wait", + message = "Generating maze\n", + showProgress = (done, total) => { + const dotnum = Math.floor(done/total*10); + if (dotnum>pdotnum) { + const dots = ".".repeat(dotnum)+" ".repeat(10-dotnum); + showMessage(message+dots, title); + pdotnum = dotnum; + } + }; + showProgress(0, 100); + // start with all rooms completely walled off + rooms.fill(TOP|LEFT|BOTTOM|RIGHT); + const + // is room at row,col already linked? + linked = (row, col) => !!(rooms[iFromRC(row, col)]&LINKED), + // pick random array element + pickRandom = (arr) => arr[Math.floor(Math.random()*arr.length)]; + // starting with top-right room seems to generate more interesting mazes + rooms[cols] |= LINKED; + let todo = [cols], done = 1; + while(todo.length) { + const index = pickRandom(todo); + const rc = rcFromI(index), + row = rc[0], col = rc[1]; + let sides = []; + if ((col>0) && !linked(row, col-1)) sides.push(LEFT); + if ((col0) && !linked(row-1, col)) sides.push(TOP); + if ((row0 && !(walls&LEFT) && dist[i-1]>d+1) { + dist[i-1] = d+1; + todo.push(i-1); + } + if (row>0 && !(walls&TOP) && dist[i-cols]>d+1) { + dist[i-cols] = d+1; + todo.push(i-cols); + } + if (cold+1) { + dist[i+1] = d+1; + todo.push(i+1); + } + if (rowd+1) { + dist[i+cols] = d+1; + todo.push(i+cols); + } + } + + route = [rooms.length-1]; + while(true) { + const i = route[0], d = dist[i], walls = rooms[i], + rc = rcFromI(i), + row = rc[0], col = rc[1]; + if (i===0) { break; } + if (col0 && !(walls&TOP) && dist[i-cols]0 && !(walls&LEFT) && dist[i-1] { + const rc = rcFromI(i), + row = rc[0], col = rc[1], + x = (col+0.5)*cW, y = (row+0.5)*rH; + g.lineTo(x, y); + }); + } + + /** + * Move the ball + */ + function move() { + const a = Bangle.getAccel(); + const fx = (-a.x*w)-(sign(vx)*d*a.z), fy = (-a.y*w)-(sign(vy)*d*a.z); + vx += fx/m; + vy += fy/m; + const s = Math.ceil(Math.max(Math.abs(vx), Math.abs(vy))); + for(let n = s; n>0; n--) { + x += vx/s; + y += vy/s; + bounce(); + } + if (x>sW-cW+r && y>sH-rH+r) win(); + } + + /** + * Check whether we hit any walls, and if so: Bounce. + * + * Bounce = reverse velocity in bounce direction, multiply with elasticity + * Also apply drag in perpendicular direction ("friction with the wall") + */ + function bounce() { + const row = Math.floor(y/sH*rows), col = Math.floor(x/sW*cols), + i = row*cols+col, walls = rooms[i]; + if (vx<0) { + const left = col*cW+r; + if ((walls&LEFT) && x<=left) { + x += (1+e)*(left-x); + const fy = sign(vy)*d*Math.abs(vx); + vy -= fy/m; + vx = -vx*e; + } + } else { + const right = (col+1)*cW-r; + if ((walls&RIGHT) && x>=right) { + x -= (1+e)*(x-right); + const fy = sign(vy)*d*Math.abs(vx); + vy -= fy/m; + vx = -vx*e; + } + } + if (vy<0) { + const top = row*rH+r; + if ((walls&TOP) && y<=top) { + y += (1+e)*(top-y); + const fx = sign(vx)*d*Math.abs(vy); + vx -= fx/m; + vy = -vy*e; + } + } else { + const bottom = (row+1)*rH-r; + if ((walls&BOTTOM) && y>=bottom) { + y -= (1+e)*(y-bottom); + const fx = sign(vx)*d*Math.abs(vy); + vx -= fx/m; + vy = -vy*e; + } + } + } + + /** + * You reached the bottom-right corner, you win! + */ + function win() { + clearInterval(intervalID); + Bangle.buzz().then(askAgain); + } + + /** + * You solved the maze, try the next one? + */ + function askAgain() { + const nextLevel = (size>minSize)?"next level":"again"; + const nextSize = (size>minSize)?sizes[sizes.indexOf(size)+1]:size; + showPrompt(`Well done!\n\nPlay ${nextLevel}?`, + {"title": "Congratulations!"}) + .then(function(again) { + if (again) { + playMaze(nextSize); + } else { + startGame(); + } + }); + } + + function tick() { + ppx = px; + ppy = py; + px = x; + py = y; + move(); + drawUpdate(); + } + + start(); + } + + /** + * Ask player what size maze they would like to play + */ + function startGame() { + let menu = { + "": { + title: "Select Maze Size", + selected: sizes.indexOf(settings.size || defaultSize), + }, + }; + sizes.filter(s => s>=minSize).forEach(size => { + let name = sizeNames[size]; + if (size { + // remember chosen size + settings.size = size; + require("Storage").write("ballmaze.json", settings); + playMaze(size); + }; + }); + menu["< Exit"] = () => load(); + showMenu(menu); + } + + startGame(); +})(); diff --git a/apps/ballmaze/icon.js b/apps/ballmaze/icon.js new file mode 100644 index 000000000..10b5a502e --- /dev/null +++ b/apps/ballmaze/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AH4AU9wAOCw0OC5/gFyowHC+Hs5gACC7HhiMRjwXSCoIADC5wCB4MSkIXDGIoXKiUikQwJC5PhCwIXFGAgXJFwRHEGAnOC5HhC5IwC5gXJIw4XF4AXKFwwXEGAoXCiKlFMAzNCgDpDC4QAKcgZJBC6wADF6kAhgXP5xfEC58SC4iNCC4nhC5McC4S/DC6a9DC4IACC5MhC4XOC5HuLxPMC4PuC5IwHkUeC44ABA4IACFw5cBC5owEkUhjwXPGAyMCC5wxDLgIACC54ADC94AGC7sOCx/gC4owQCwwA/AH4AMA")) diff --git a/apps/ballmaze/icon.png b/apps/ballmaze/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..44697db4b75e49c76f668a4da1275b040f1d6fd7 GIT binary patch literal 444 zcmV;t0Ym4UUAPlroCJC$1Svpf^DXX-r@I8EuYDFP7;1rO2 zQeP^R-1+hGBB5-u_oCr624NNa!kt}*6zh+GX3n<0y z77qV^KI8A&C@o@%1NRV$UR)|ht%%q3{q4zz5FiMJ=Xyj_;3`>vEr zAOGRLg@Rs%ygP%_Ou0Mekl)-Ium z2h$Y%LDzIM<$KYAM%vCa(uZruYsycoDhIUEh?38x4|i0pPN7y2v~XLV)qF2hvoocU z-^k|L2s$mUcVn(3EKzbwYXoEN$y)sby6#WCcip|#ep&mo_x?Wn z`@`PPrvZF#UF}`k6beO`=i_;l?1^_3)+AfbvAGhmgHk+rfiPJ_aLfe?MSGOz=^iKy zUzl`DKJm}(e?Mn!j-shJMNWs2lh5ezPSDi!5E=uY-?}>sHpX>~^yrt7=I2YywPEGT zT$(Iwq57*6uac`C$U3INs|7vx2Zw`#f}C19MW}>piY0&seE%L;WX6s7h+9)m!3qoN z(ja)B2E2`lCjCM*U7}#8C|DE-y=Q!Vh=De4K?D3Gc-P~BgCKMTfG#5RM?ztUw*+^P zaQ6eC0ZlLu(}OkXR+e1F^uOYF17HsY?1JDEZ1&gY+=!ij7LiArZ!*;1VgLXgL?)1p z6;;6xi;2=)<+vbdO8sl--_*Oj6@g&OVmM5Gm7~+@T6H<|F&e5RMKOL=87vH1@P4j^D5GF~tn1oWt_< z*{QVymPwIIO``2!AvJe-p^~MQyD{}Nt`a4V$KHZLZ-l*FJ;b_+ClnPasWy;bY8>QZ4(XgO~11;04$bDW8p$UBM?J#`9L#G4kAY zwX+YoT4c*bk3^K=1^fy8r%bC$xDDt{4-MtB)Ii$FQ zXV_J`saRW2YRO}*`0f$bfSQ?LZ9PvPF~z#ThPj-(4QjdaWsgt7m(dFr3}Vvfs-wSb zsf$6V@RJkITnP&X>7Pnt{Zpf>j#;u~1o{ioHmtScLWHw!H8=ub9dfsrdzpgeCzW}j z{t--!W_NDDq}$}@TtwBf%$sF}xpj$|*g(?Qe4Sf?U|6s`tnN9FT;osWt<6ckoVOD` zxnZ%!A_64+l>>nPaN_L1M5m#%YdiI~fxO$l;|vWN)vDcP_m8QnbWWd2iVX~e2DsS< zw{GTn8-?5yJNLLIuP0cte4U?>FmhMNBN*4KlZPEErE_Tp_`2CZ-g>+zSUGNVH&TIX!e)2tTwz_)< z-IdMQ8>VSc%b*`9!}*z#%$f<+!?un2SvH6w>+Lm%(SZF;IJGRq#{6iD#=_+YaoodlTuOrAbP6FuC* zX3a6aMMMYIPM$S&BHBioznin{#ZzSdY&R1F$_!t{VYAJO7>Inel5(VPV{0YtFlt{+ z!Dhe7Z~z}eFuN!KfMysd!Cv8eMUd%J0mDXuvq92xc{jX;i4yi6RC+m?9xac2xD$&6 zA!DyQ6mXfmmvJ`B+@4IO9>e|Dl2ZB&jK*Tw7Baet8lU@q%FyHf@M?~Jg0gi3qu>b%7 literal 0 HcmV?d00001 diff --git a/apps/ballmaze/size_select.png b/apps/ballmaze/size_select.png new file mode 100644 index 0000000000000000000000000000000000000000..cac278820c92c15dce2a7897279c0299fe50b84a GIT binary patch literal 4409 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 Pg+ToG1^HHalhXbJ%Igu= literal 0 HcmV?d00001 From 615aeef4cdd0b226a432c4b50b1c9f0f916d74b5 Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Sun, 26 Apr 2020 05:35:50 +0200 Subject: [PATCH 261/302] ballmaze: also bounce off corners --- apps/ballmaze/app.js | 68 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 53 insertions(+), 15 deletions(-) diff --git a/apps/ballmaze/app.js b/apps/ballmaze/app.js index b249d6494..cce296f2c 100644 --- a/apps/ballmaze/app.js +++ b/apps/ballmaze/app.js @@ -99,9 +99,8 @@ y = rH/2; vx = vy = 0; ppx = px = x; - ppy = py = y + ppy = py = y; - clearWatch(); generateMaze(); // this shows unbuffered progress messages if (settings.cheat && r>1) findRoute(); // not enough memory for r==1 :-( @@ -395,7 +394,7 @@ y += vy/s; bounce(); } - if (x>sW-cW+r && y>sH-rH+r) win(); + if (x>sW-cW && y>sH-rH) win(); } /** @@ -407,40 +406,79 @@ function bounce() { const row = Math.floor(y/sH*rows), col = Math.floor(x/sW*cols), i = row*cols+col, walls = rooms[i]; + const left = col*cW, + right = (col+1)*cW, + top = row*rH, + bottom = (row+1)*rH; + let bounced = false; if (vx<0) { - const left = col*cW+r; - if ((walls&LEFT) && x<=left) { - x += (1+e)*(left-x); + if ((walls&LEFT) && x<=left+r) { + x += (1+e)*(left+r-x); const fy = sign(vy)*d*Math.abs(vx); vy -= fy/m; vx = -vx*e; + bounced = true; } } else { - const right = (col+1)*cW-r; - if ((walls&RIGHT) && x>=right) { - x -= (1+e)*(x-right); + if ((walls&RIGHT) && x>=right-r) { + x -= (1+e)*(x+r-right); const fy = sign(vy)*d*Math.abs(vx); vy -= fy/m; vx = -vx*e; + bounced = true; } } if (vy<0) { - const top = row*rH+r; - if ((walls&TOP) && y<=top) { - y += (1+e)*(top-y); + if ((walls&TOP) && y<=top+r) { + y += (1+e)*(top+r-y); const fx = sign(vx)*d*Math.abs(vy); vx -= fx/m; vy = -vy*e; + bounced = true; } } else { - const bottom = (row+1)*rH-r; - if ((walls&BOTTOM) && y>=bottom) { - y -= (1+e)*(y-bottom); + if ((walls&BOTTOM) && y>=bottom-r) { + y -= (1+e)*(y+r-bottom); const fx = sign(vx)*d*Math.abs(vy); vx -= fx/m; vy = -vy*e; + bounced = true; } } + if (bounced) return; + let cx, cy; + if ((rooms[i-1]&TOP) || rooms[i-cols]&LEFT) { + if ((x-left)*(x-left)+(y-top)*(y-top)<=r*r) { + cx = left; + cy = top; + } + } + else if ((rooms[i-1]&BOTTOM) || rooms[i+cols]&LEFT) { + if ((x-left)*(x-left)+(bottom-y)*(bottom-y)<=r*r) { + cx = left; + cy = bottom; + } + } + else if ((rooms[i+1]&TOP) || rooms[i-cols]&RIGHT) { + if ((right-x)*(right-x)+(y-top)*(y-top)<=r*r) { + cx = right; + cy = top; + } + } + else if ((rooms[i+1]&BOTTOM) || rooms[i+cols]&RIGHT) { + if ((right-x)*(right-x)+(bottom-y)*(bottom-y)<=r*r) { + cx = right; + cy = bottom; + } + } + if (!cx) return; + let nx = x-cx, ny = y-cy; + const l = Math.sqrt(nx*nx+ny*ny); + nx /= l; + ny /= l; + const p = vx*nx+vy*ny; + vx -= 2*p*nx*e; + vy -= 2*p*ny*e; } /** From a08c0383b4d7dcd2ea61c608385527d03a19268f Mon Sep 17 00:00:00 2001 From: Amos Blanton Date: Sun, 26 Apr 2020 08:45:35 +0200 Subject: [PATCH 262/302] Bug fix for hours in early morning. --- apps/impwclock/clock-impword.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/impwclock/clock-impword.js b/apps/impwclock/clock-impword.js index dafbbfcf5..c54fa7976 100644 --- a/apps/impwclock/clock-impword.js +++ b/apps/impwclock/clock-impword.js @@ -20,7 +20,7 @@ const allWords = [ const timeOfDay = { 0: ["", 0, 0], - 1: ["EARLYMORNING", 10, 11, 12, 13, 14, 02, 12, 22, 32, 42, 52, 62], + 1: ["EARLYMORNING", 10, 20, 30, 40, 50, 02, 12, 22, 32, 42, 52, 62], 2: ["MORNING", 02, 12, 22, 32, 42, 52, 62], 3: ["LATEMORNING", 01, 11, 21, 31, 02, 12, 22, 32, 42, 52, 62], 4: ["MIDDAY", 13, 23, 33, 54, 64, 74], @@ -30,7 +30,7 @@ const timeOfDay = { 8: ["EARLYEVENING", 10, 20, 30, 40, 50, 06, 16, 26, 36, 46, 56, 66], 9: ["EVENING", 06, 16, 26, 36, 46, 56, 66], 10: ["NIGHT", 37, 47, 57, 67, 77], - 11: ["MIDDLEOFTHENIGHT", 32, 33, 34, 35, 36, 37, 50, 51, 54, 55, 56, 73,74,75,76,77 ], + 11: ["MIDDLEOFTHENIGHT", 13, 23, 33, 43, 53, 63, 05, 15, 45, 55, 65, 37,47,57,67,77 ], }; From 02eb4d5c407cdce9a9519b8823499f749e62aa5f Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Sun, 26 Apr 2020 14:59:41 +0200 Subject: [PATCH 263/302] ballmaze: handle broken settings JSON --- apps/ballmaze/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ballmaze/app.js b/apps/ballmaze/app.js index cce296f2c..3e26277b7 100644 --- a/apps/ballmaze/app.js +++ b/apps/ballmaze/app.js @@ -1,6 +1,6 @@ (() => { let intervalID; - let settings = require("Storage").readJSON("ballmaze.json") || {}; + let settings = require("Storage").readJSON("ballmaze.json",true) || {}; // density, elasticity of bounces, "drag coefficient" const rho = 100, e = 0.3, C = 0.01; From dd014d5a268ef3fcbce70e1e51ffd618058ba35c Mon Sep 17 00:00:00 2001 From: Purple-Tentacle <59914607+Purple-Tentacle@users.noreply.github.com> Date: Mon, 27 Apr 2020 10:35:11 +0200 Subject: [PATCH 264/302] Active Pedometer 0.04 --- apps/activepedom/ChangeLog | 3 ++- apps/activepedom/README.md | 19 ++++++++++--------- apps/activepedom/app.js | 2 +- apps/activepedom/widget.js | 21 +++++++++++---------- 4 files changed, 24 insertions(+), 21 deletions(-) diff --git a/apps/activepedom/ChangeLog b/apps/activepedom/ChangeLog index c1b9ec011..ca26a648a 100644 --- a/apps/activepedom/ChangeLog +++ b/apps/activepedom/ChangeLog @@ -1,3 +1,4 @@ 0.01: New Widget! 0.02: Distance calculation and display -0.03: Data logging and display \ No newline at end of file +0.03: Data logging and display +0.04: Steps are set to 0 in log on new day \ No newline at end of file diff --git a/apps/activepedom/README.md b/apps/activepedom/README.md index f45297e57..a2a351a12 100644 --- a/apps/activepedom/README.md +++ b/apps/activepedom/README.md @@ -18,7 +18,7 @@ Steps are saved to a datafile every 5 minutes. You can watch a graph using the a * 10600 steps ![](10600.png) -## Features +## Features Widget * Two line display * Can display distance (in km) or steps in each line @@ -32,22 +32,23 @@ Steps are saved to a datafile every 5 minutes. You can watch a graph using the a * Steps are saved to a file and read-in at start (to not lose step progress) * Settings can be changed in Settings - App/widget settings - Active Pedometer +## Features App + +* The app accesses the data stored for the current day +* Timespan is choseable (1h, 4h, 8h, 12h, 16h, 20, 24h), standard is 24h, the whole current day + ## Data storage -* Data is stored to a file +* Data is stored to a file named activepedomYYYYMMDD.data (activepedom20200427.data) +* One file is created for each day * Format: now,stepsCounted,active,stepsTooShort,stepsTooLong,stepsOutsideTime -* now is UNIX timestamp in ms -* You can chose the app to watch a steps graph +* 'now' is UNIX timestamp in ms +* You can use the app to watch a steps graph * You can import the file into Excel * The file does not include a header * You can convert UNIX timestamp to a date in Excel using this formula: =DATUM(1970;1;1)+(LINKS(A2;10)/86400) * You have to format the cell with the formula to a date cell. Example: JJJJ-MM-TT-hh-mm-ss -## App - -* The app accesses the data stored for the current day -* Timespan is choseable (1h, 4h, 8h, 12h, 16h, 20, 24h), standard is 24h, the whole current day - ## Settings * Max time (ms): Maximum time between two steps in milliseconds, steps will not be counted if exceeded. Standard: 1100 diff --git a/apps/activepedom/app.js b/apps/activepedom/app.js index 0a9b3b93f..cc875f371 100644 --- a/apps/activepedom/app.js +++ b/apps/activepedom/app.js @@ -162,4 +162,4 @@ settings = storage.readJSON(SETTINGS_FILE, 1) || {}; drawMenu(); -})(); +})(); \ No newline at end of file diff --git a/apps/activepedom/widget.js b/apps/activepedom/widget.js index c6bd410ce..2ae1b9b62 100644 --- a/apps/activepedom/widget.js +++ b/apps/activepedom/widget.js @@ -33,27 +33,28 @@ function storeData() { now = new Date(); - month = now.getMonth() + 1; - if (month < 10) month = "0" + month; - filename = filename = "activepedom" + now.getFullYear() + month + now.getDate() + ".data"; + month = now.getMonth() + 1; //month is 0-based + if (month < 10) month = "0" + month; //leading 0 + filename = filename = "activepedom" + now.getFullYear() + month + now.getDate() + ".data"; //new file for each day dataFile = s.open(filename,"a"); - if (dataFile) { + if (dataFile) { //check if filen already exists if (dataFile.getLength() == 0) { - stepsToWrite = 0; - } - else { - stepsToWrite = stepsCounted; + //new day, set steps to 0 + stepsCounted = 0; + stepsTooShort = 0; + stepsTooLong = 0; + stepsOutsideTime = 0; } dataFile.write([ now.getTime(), - stepsToWrite, + stepsCounted, active, stepsTooShort, stepsTooLong, stepsOutsideTime, ].join(",")+"\n"); } - dataFile = undefined; + dataFile = undefined; //save memory } //return setting From 7630f1f23e8580735d8b278789cc8b1d7b26dcfe Mon Sep 17 00:00:00 2001 From: Purple-Tentacle <59914607+Purple-Tentacle@users.noreply.github.com> Date: Mon, 27 Apr 2020 10:35:59 +0200 Subject: [PATCH 265/302] Active Pedometer 0.04 --- apps.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps.json b/apps.json index 36498423e..f405e14ab 100644 --- a/apps.json +++ b/apps.json @@ -1127,7 +1127,7 @@ "name": "Active Pedometer", "shortName":"Active Pedometer", "icon": "app.png", - "version":"0.03", + "version":"0.04", "description": "Pedometer that filters out arm movement and displays a step goal progress. Steps are saved to a daily file and can be viewed as graph.", "tags": "outdoors,widget", "readme": "README.md", From e3e57267b48cf927d405deba64e70698c1a1f17a Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Mon, 27 Apr 2020 11:43:35 +0100 Subject: [PATCH 266/302] new about page pixels --- apps.json | 2 +- apps/about/ChangeLog | 1 + apps/about/app.js | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps.json b/apps.json index 43e7e4b9d..57ceac04d 100644 --- a/apps.json +++ b/apps.json @@ -53,7 +53,7 @@ { "id": "about", "name": "About", "icon": "app.png", - "version":"0.04", + "version":"0.05", "description": "Bangle.js About page - showing software version, stats, and a collaborative mural from the Bangle.js KickStarter backers", "tags": "tool,system", "allow_emulator":true, diff --git a/apps/about/ChangeLog b/apps/about/ChangeLog index 2c81c0537..16aea0610 100644 --- a/apps/about/ChangeLog +++ b/apps/about/ChangeLog @@ -2,3 +2,4 @@ 0.02: Update version checker for new filename type 0.03: Actual pixels as of 5 Mar 2020 0.04: Actual pixels as of 9 Mar 2020 +0.05: Actual pixels as of 27 Apr 2020 diff --git a/apps/about/app.js b/apps/about/app.js index dc7b0cad8..57c85563d 100644 --- a/apps/about/app.js +++ b/apps/about/app.js @@ -29,5 +29,5 @@ g.drawString(NRF.getAddress(),120,232); g.flip(); // Pixel chooser image -g.drawImage(require("heatshrink").decompress(atob("+FQgl+xnu8AIBwGQgHuAoN3gF/hcLgEHu943G3gHdhvdDwIBCAAV3uEAhoBBhsO90OgHgoACBh0IhP5AAQZD8Hw+GwAwXn4AECxGAh0MEAOeJAMP3+/Lw0GswGEHgMM9gCBAIX//5PBhvQ7gJBxAAB9ng8vs5nMDgOg8HnOwIBBgBHDAAfQNAJBBgBQDgF4HQfd7veKoKbBO4Pr30IEAhgBAIIAG3oJDx+AQwLBBYgR3JsABCzOQzOeO4cP4HPc4QCBPoPN4HNO4QoB9wAByDvBO4L2COwZ4Gd4UP/7vEf4LvGKoUAooDB9x3FgEQI4TwBgEIN4NpwEMXILvBO4bvD/Y3BO46eDgGdO4n8CoXw+cQh/w/kNd4fodoXJhLvCKYJ4Dhe7AYJXFwBHBUAgABewMPhvQd4bwB8FQqDvHO4YADhH4B4XM9nABQTsCAAf/awbXBO4Vmd4xED57vD+EwFgOIBoUNxv/1////5zOAy8AvPN6AQCbQIiCOIIKB7EILwZIEO4YACKYlFoB3CHIZ2CAIJHBEAToCMwLvBAArvCAAnAAALvDAIIPByA5BEQUM/n8O4TzCAAQtBhvd/X8d4YYBvwOBO4bBFO4b2D4ASELoP/d4IbGABMBiINLV4YAD9LyFO5bvCYYfPCARKBmAcDh3ud4Wt7vdDgONwF8O4Q8Bh5jCBAOPO4o0BgFAAoLcB/4UBLIgBDAAPI5DeKIQIDChcLL4IABGIOAJITvHAAkGs0HgG7AAO99p3Dhi2N43N7rLCxGHgF56AHCRwUwAYIlBhsNGoR3CqALCh54CFAXHAIg/CRAIDBIgtHGIR3D3ZhCWwXQwA1CAAMP5/M/nPMhp3BwAJGWIQ7Dgczt1pzIHCa4IABhpkBOgQACD4ZRCs1m4AyEO4IBBABUMXYYZDgEEvoRFd4TwBO5IAJ5nAFAMNTYZEBGgRiD7p0CO4nM43JmZABAIICBAAOA+HwgUgkEiGxFsAQOwGQLeBhPpz2QChEO8AoCd4R5CdwZpCNgdVqq0B7vQ7vdMQWIbYJkFAAIjBEoR3DCoOA8A3CYAOvh/wgH/d4hVBd4VAgn/eIYAGX4cAgw2DNQ2e9I0DBgxIBxGAWgS1DAAZrBLAi2DeAJwDOoLcFNQOA5jbCd4gACO4OgAgMHu4aBDokKgGIZ4LtBogABBgXw4HwhnL5lwEQRmJb4bvBO4/uIAfQKAJ3Gh7sC6/XcgR3NDwR3DA4K4CAQJ3GV4JrBCoZuBAIMK1Wg4eAhwRB91AdpENdwbwEAAkHP5D8DPoIrBQ4LvMNYICDO4z7Bd5HM5jvD4DxBd4PQGwIBCHIMAeAQAEhQIC4GIboTfGT4JcBO4TvINQV2sDvCAAw6DRZIcB+APEhoxDACJ3BBZPwAAIsDhTwDXwbvFO5LvQhnMu1wNQoABBAMOM4RqDuFwY4IUEGpKUCcYPwAQIXEAAnu9wbJBQPg+ArCcoIBBhkMMoqCBO4IVBEYfuNYsNLISHDZYkM/93CgmIOwJtBh3uAIPuNQZ3BLwsOSYuIAIOABYPex2P9+JxncZAJcCO5VgXYRPCWQQzF4AABDohHB5gACBYPeSAYAHdwcJQYfc/OQIAQZBwB2BABQMBhiBBcQcP///AoLkBgH4+DvI1GKxGoFRVmXYThFAAwNFh0PawUNxoDC95fBDAsP+AnFFox3B9vtO4LvBG47/CcofOPoYABWIJ3Cd4jYBB4NwgwFBd4LxCIoQuGdwJIBdAoAHBoixBAQMJhvdBALuBBAJ3Gh/ADQkNLwboBAQLvDZAMP54ACMoJcCsAYC5nOV4OXcgQADd4QADs8HsF2g1QSwQAE+AcGRILhD/5cHMAgEFg2AzuNV4bvFhp3C5igN73u6DQBMwIAC/4/BcgaQDhwtBy8A3ewEAjvBAAdQgoCEDYbHCLgRIBeAwMCQoKdDwEMg6XBBgIXDO4WJhuNHQyOF+DvFAAwLB9vdVg7vJAAeXhYjHhGAAIKpL6CoBd4UDgbvDO44gDAYMHW4bCECIWdOoI2FKA0A0AABAwfu9oOFOwPgPI4ABWAICBE4p3KAARaBJQQDCAgJ3DdYLsEdwm3FwP/dwRiCd4nwQoYfDxEN7uIVxh3B1R3Bh0ONo/u93gAIIfMbozvY7oFELoMwA4h3CAAMJzOQAgOIO4LvG6ENAQP4xCjDAAiBBh6aBgEKd4139xNFd4SEBAAY6BhgHExAuG3ewO4zxCTBgnBAAMAgZKCEoo9EO4QAEdAIBBO4mPx5eBuCTDCYWfh/P6AeFNgVwg53EfITvC4BIB4B3HMgv/Vw3d7p3CFIPgHAwAMG4IAROwR1BAIWI/GAhm3gHMLAUAg1md4Q/Fh3uRgN3d4o+CPQPAAAWQ/7GB5nMH48DO4xDCF4YFCP4OAwD4GJgQCBhkJJQquGAwvAAQZsBAALvChfLuAICTKGIwBSDhoEB9yEBNwMM4GfgH8hnPO4wuBmB3ChYfFTYivBhAwBfAQABuA/GVAKKCADH4xHwhm8RYSICAALNIO4vQfgZfB8Hgd5H//gqBeYIrB5fLF4gAC6ENzIQBd453FYoUPO4ZUBCQMP/5SLuHwSg5UBAoggBxCiEJoe8714zUQCYbvBO4pDFXwRPBd4UOfwIzB5e7O44ABzP/LYp3CPAIHCu4XGhgiBBwR3IRQcP54ECyEJzJ3DkYUDGIIABRQTvJhvcZghFCu4XBZgRKGbQQAEO4m7hewGIIAEEJJjIKASKDNwh3Id4cJhJ5BOoMOgE9mAQCxGAd4jBHDAMN3p2Dd4Z+FSYThHhYDCnm8AgWwPAIVB/nM9nDO5kP//wBZD+DF4kPOoIBBC4rtCLwMO8EAgchd4w6JzwYBhHdegYkBO4oMDJwxKEgcAQgZ3D5//53Onk8O4a+BAIO62DbJwEJKIMIZoa1D+AABR4X/O4jvDO4PHyEQu0GfoIADegIAB5vmwGrd4YADSYMGy2WO4jODd4j5EAA52BMwLvB53uO4MNTIUBgIRB1WgCwXuEZYABg4EDHYI9CXAK6FLQcOO4IFBsACBGoMRgGHO4mJO4IAChkKyENNoTvFKwLGHhh5BhnMPoQEDBAnM5jvB4YIBFQUQ+EQd4vgV4LuDAAI0F6DUDO5eZzIFDO4TvDGYIBBd4OHw53BxR3E4GqyHA2ArBgwJBhe7XRH/O4UAhzONAAp3Bh8B+KWBAAnu8CRCAAVVgtQAoULeAq3GABOOSwp3DBIMICg0LW4MJyEIBoTvC38vYgeQyGZBYI3BfAx/DO5wcBSoLsDEILuBhn8BQdA+FAeIw/DBAbuDuEHf4adDbgQBB4IiF2ELbwQBBAwIMDEAuy+R3DOgJ4BO4vQIwfMGQJdB5nM55rELYo4CAAXvO4cIxDdEbw5MDO4n/PAMHAAQJCg/ud4UMAAYMCzOIwB3CEwWwO4oABJQbvFAAg3BHAPgFIKpDO4TgB//5RYIABjUAhUQeAYABxAeC7qWDABJXDOwYABBAsHu7vEAwIbD5h3FhKCBd45qD7ACB1StDBwK4CXY7vGO4cJzOZznMKgoUBO4g/BLYp5MO4sNO4UODYbuCKITvB54TBd453Fd48NhADBZwSnD/7aBh7KBOYZNNhx9CAAQoCO4uIOCIbCAAaiBI4Xg8AUGaoLvB4HwO4bzB34MBhI3BhZxBd4YGBd4t3agRCI7sNAAJsDAQMMN4oKB5jvEAAUNSIhkBh7tDAIcADQuIAALMBd4YBCh0JeAZ3G93Ah7RDAAO7+EJd4QAKd4IOB9x3LOwoADOwxJB5wgBhZHEAYq3B+Hw/8AuAIBAQScBDQQBBd4RtBF4OQAALvOzJ2DRATvCzJ3McQh3BhIfCZghrH7Z3CPAZEC+P4ZwwAHh7vBh/wg4ABTgpRBAIPuEwXteAhlEAAkL3YEC/PwAgW5VoYAGFIYACJ4nMRYIxCc4vMNgUJm4MBIoR3DhxFC/8QDAYiBu7cBRIdwUwLvBAAp3DdwYlBNga3LAA7vHLIZmBBQYMEhGIAodVDwQfB7sNHAf/JgUJMIML7wGBMogACiMf/4VBhKZBuFwhgODuHQE4LwBgDvFCIO7hbNCYokNAgMLXYUPAAp4G+xPCd4vHvgSGPIbvEAAKVCGITwDUAcJ06uHEQSsFhZ3Cd4ZBCO4bqCuAJCO4ULhZ4Bd4Y7C4AqCCQQAK+B9B/9gIQ53FwBxEhAFB5ncDYIsMAA5CD8DCBAQQADd5AFB7ruCh7sBAIaQCAARMBhAzGd52ZzMAsx3CYAZFB5nMTQTMFBgOAJQPQBghYCAQJBBO5wAKIQNwg7vBO4buBABewAAK+DGime9L0DNoI2BeQXAWoZ2Ef4Z3ILAMJyG5IQKoD9wABgHN8F5f5wAGcgJ3GdocAgjuDABLvCdQcGAoh3Fh/vdIJ3CcQLbFPAgAD5ncgEKAIPdRoMJCoJCD/4CBEYIaB4HguGgKBYDGTAKBKfIYQBCQnwaoICCd49gsDKGzLvHKYQADxAIC8HuAQINDd4Wg0HQ5j4ByAaEHoTvFO4OwMouYmcwh//AIIKDhByGZgZ3Bg7dBgxoFCAWACYjoDh7uBgwGDBocN5YfFhz1Bg4GCxOAd5B3BOILwBd4PMZJQAOxEwRoJFCqACBxw3DAASEEd4I7BAwQ4Sd46OCLQIAHO4cIH4R2BPAwAHgYIHhpODO55qBMwMI9HoeYZBC5kM4DvEZ4XAxGAg93zLeC3ew2DwFdwIFEO4kJFoRxDFoQFDBwMA8B2ChjrBAAaAFyBeBAA3QzOZOxQrBUoLvDVYXdSIR3DhnMAALvC6Hgd4YQCIAXwgELfCMPqAcCuF3O4l3AwgAF4AABIQJ3HyYCB1MK7gOCYwOQB4cMNYP/WoYMByDtBBAQHBhv9/p3FOwXMeAK6ChKMCKYV5U4Z3Bd4bqDAAZ3F81wdA14KQggEd4ZlBhn8Qg7vCyGQ6EMgF3O4LvLhQEDxEIMAOgO4MPDQJ3G553DABC4EO4zvM8HgFoQAB+CiBHoIgCAQbwFPQcAgjvHSgPQCINwvvQgEJhe7AAIbBhIWCGARrCwACBKoPd+H9DQJ3DGgPMVwfHyBwEO4ziDWoLvJCgXw9wDBO4f/gHcSYcMDwT0CAAgJDolANAPpeQgfBDQNwuDvD2CaC4HACALuEd4iRB7vzO4MIhEHJITwCZIMMvLYIgf/+RwBaoLWBAYQAHhwLBd4YACqHwAILlFAILyHPAUEAAIkBTIQAGO4QXDO4wAJdQMN7vddwOIg93XIXMhxRBdwIcJ+Hw/7iChnsBgkNhsMHoUOCAJ3BegQABgtVNQwzBAYMLWYIADO4VAOwNAd4oAEKwR3GgEJWwaREVAS6EAA4PCOA7KEO4QDBAIIjBSIPMDYxyDhaCBb4zvJ9wAE2C4CO4IAGFQPgLoVt5nODoJ3B3YTGWQhnIBQkMQoSGMAAwXCh///5/BNgJtC7q9D2HQ2G9BAT/BhLDChgfCCYYADSwZ3I93gAIJ3FABMO7wECCoJmMhkN7o2ChOQzOQcgQAD3ewKYJVFg93u9wEgp3Dd4R6CVYXA2GQgyLCfhTvHyBZCO5vvvaVBD4QkE9wRE/5mDAQR3BhoWCOgIBBAA2q0D3Md4IOMABBPDO5DvGO47YIh8O+65GNAQRF/7dFgHMd4mIwABBQoISEBAMOAAUA8DjDAA/MAYRAF7rxCABsPd5oAN995Z4mAwHM4AQF/+IO4wAGyDvFepB3BgBhCNYNwg93hGIgHAGoUHCwibDoAeDagQXBAIIRCC4h3EgxRLXQQLIhDUBO4cIhZ3Bd44AFzJxDCIMM/IxEd4kNDIsHg8IAgJ3DeAt3AoJiBRIUO9zFDJwIAB2BIJ8C2JIogMJwBBEAAMwaQoAQHBYAChruBd4QHB5iBECgzaCN4MMCQTvF35mGQYR3Ex2wAYP8O4gvG9ns8GIwEMO4cLeAQlCO4hNHAAS4CHAQaBhgACd4sOuHnd4RdDdwYBBCwK+GRIOIJALuBSQUPIQV3DIIABhGZwB3EP4UGRAjXEhp9CdQruI9x4BDIPgEwUA3YABNwQAC4GQHIOwV4QAUUIRpBAwUGKwLvCxjvGVgVwTYIfDBgJvExx3Cd4gBCAAPdpxjCHwigBhLwCBQnuUoVQHARqBAARCDhn5DQIABDIUEYAbnFABDuCAAIJEDIUM5iPKO4tAgGQMIbvGhwACdwR/Dd4MHu48Bh5oCAAkOd4cwbogEBdwgABdwLvJIAJCCdxjvEP4NgB4mIDpF3AAJBCHoZ3EBQTvDc4TwDBIh1BO4X/O44FEfgLvEO4JuHQIQoBd4Z3Gh8Pdw4ABdwqWGS5LuEADp3CBQ/uCpLvH5n5eASQBSIuIaIsP+BCOMoUIDwcIhGIO6DFDABpLEuAhC/4ABDJpXBhe7gG7dw4AC8AABaAjPIAAmgdZoDCAoX8ShIJEzOZXAetFZTDFX4f/FZHP/ieQFQgrFO4g2HTQOqEBLpBeAPAPonAAwTNBKwnvd5Pb6ADB9wACFALDBIALEGAA71C4EMVBAAMFIcLO4o0EKgMPhcz9zEKOIMMHYI8DXAcHg8AxApCIwIHBAAzvEOIUAu9wO40IO5EJzIoBd4p3Fh3dAwg7Eh6TCuDFEhxRDd4uu3QFBokEoEA9RHCY4J1BhnMHYbvCuGAvAPBeoZlBH4V3GYOOXgsOFAJNBO4YSB+/3MgPMhJLBJoUJ/JvFgcAmAHE93QOoZtBAQSKDhcIeAKHIgHA53u93qeAVAAAJWB1wRDd4wAEsEIO4MGs1mu4ABHQQCBhHIO4wDB2GwG4Pu8BRBv9/CwMM/ON6ABBd4h3KhzvEOgMHAQKeBO4TvGIwQAD5nA8Hg92u1R3BAITwEd4Z3Hg0GgGIgB2BO4d2IITvJO4ZDEKQKRCd40P/+QGwsiAwsOd4hnCOAQbBKYLuLMoJFB9w=")),0,135); +g.drawImage(require("heatshrink").decompress(atob("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")),0,135); g.flip(); From 35470a151e99f851360a216290a615bad151211e Mon Sep 17 00:00:00 2001 From: fredericrous Date: Mon, 27 Apr 2020 12:15:30 +0100 Subject: [PATCH 267/302] improve ui, add 2 player local prepare the ground for 2 players bluetooth --- apps.json | 3 +- apps/pong/ChangeLog | 1 + apps/pong/app.js | 288 +++++++++++++++++++++++++++++++++----------- 3 files changed, 218 insertions(+), 74 deletions(-) diff --git a/apps.json b/apps.json index f167acf5d..5708e6d62 100644 --- a/apps.json +++ b/apps.json @@ -1486,11 +1486,12 @@ "name": "Pong", "shortName": "Pong", "icon": "pong.png", - "version": "0.01", + "version": "0.02", "description": "A clone of the Atari game Pong", "tags": "game", "type": "app", "allow_emulator": true, + "readme": "README.md", "storage": [ {"name":"pong.app.js","url":"app.js"}, {"name":"pong.img","url":"app-icon.js","evaluate":true} diff --git a/apps/pong/ChangeLog b/apps/pong/ChangeLog index 5560f00bc..6433ebce4 100644 --- a/apps/pong/ChangeLog +++ b/apps/pong/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: 2 players local + improve ai diff --git a/apps/pong/app.js b/apps/pong/app.js index 4531b3af8..272eaf2e7 100644 --- a/apps/pong/app.js +++ b/apps/pong/app.js @@ -8,6 +8,7 @@ * - Let's make pong, One Man Army Studios, Youtube * - Pong.js, KanoComputing, Github * - Coding Challenge #67: Pong!, The Coding Train, Youtube + * - Pixl.js Multiplayer Pong, espruino website */ const SCREEN_WIDTH = 240; @@ -15,6 +16,13 @@ const FPS = 16; const MAX_SCORE = 11; let scores = [0, 0]; let aiSpeedRandom = 0; +let winnerMessage = ''; + +const sound = { + ping: () => Bangle.beep(8, 466), + pong: () => Bangle.beep(8, 220), + fall: () => Bangle.beep(16*3, 494).then(_ => Bangle.beep(32*3, 3322)) +}; function Vector(x, y) { this.x = x; @@ -28,12 +36,18 @@ Vector.prototype.add = function (x) { const constrain = (n, low, high) => Math.max(Math.min(n, high), low); const random = (min, max) => Math.random() * (max - min) + min; -const intersects = (circ, rect) => { - var c1 = circ.pos, c2 = {x: circ.pos.x+circ.r, y: circ.pos.y+circ.r}; - var r1 = rect.pos, r2 = {x: rect.pos.x+rect.width*2, y: rect.pos.y+rect.height}; - return !(c1.x > r2.x || c2.x < r1.x || - c1.y > r2.y || c2.y < r1.y); -}; +const intersects = (circ, rect, right) => { + var c = circ.pos; + c.r = circ.r; + if (c.y - c.r < rect.pos.y + rect.height && c.y + c.r > rect.pos.y) { + if (right) { + return c.x + c.r > rect.pos.x - rect.width*2 && c.x < rect.pos.x + rect.width + } else { + return c.x - c.r < rect.pos.x + rect.width*2 && c.x > rect.pos.x - rect.width + } + } + return false; +} ///////////////////////////// Ball ////////////////////////////////////////// @@ -45,12 +59,26 @@ function Ball() { this.reset(); } -Ball.prototype.show = function () { +Ball.prototype.reset = function() { + this.speed = this.originalSpeed; + var x = scores[0] < scores[1] || (scores[0] === 0 && scores[1] === 0) ? -this.speed : this.speed; + var bounceAngle = Math.PI/6; + this.velocity = new Vector(x * Math.cos(bounceAngle), this.speed * -Math.sin(bounceAngle)); + this.pos = new Vector(SCREEN_WIDTH/2, random(0, SCREEN_WIDTH)); + this.ballReturn = 0; +}; +Ball.prototype.restart = function() { + this.reset(); + ai.pos = new Vector(SCREEN_WIDTH - ai.width*2, SCREEN_WIDTH/2 - ai.height/2); + player.pos = new Vector(player.width*2, SCREEN_WIDTH/2 - player.height/2); + this.pos = new Vector(SCREEN_WIDTH/2, SCREEN_WIDTH/2); +}; +Ball.prototype.show = function (invert) { if (this.prevPos != null) { - g.setColor(0); + g.setColor(invert ? -1 : 0); g.fillCircle(this.prevPos.x, this.prevPos.y, this.prevPos.r); } - g.setColor(-1); + g.setColor(invert ? 0 : -1); g.fillCircle(this.pos.x, this.pos.y, this.r); this.prevPos = { x: this.pos.x, @@ -58,55 +86,62 @@ Ball.prototype.show = function () { r: this.r }; }; -Ball.prototype.bouncePlayer = function (multiplyX, multiplyY, player) { +function bounceAngle(playerY, ballY, playerHeight, maxHangle) { + let relativeIntersectY = (playerY + (playerHeight/2)) - ballY; + let normalizedRelativeIntersectionY = relativeIntersectY / (playerHeight/2); + let bounceAngle = normalizedRelativeIntersectionY * maxHangle; + return { x: Math.cos(bounceAngle), y: -Math.sin(bounceAngle) }; +} +Ball.prototype.bouncePlayer = function (directionX, directionY, player) { + this.ballReturn++; this.speed = constrain(this.speed + 2, this.originalSpeed, this.maxSpeed); - var relativeIntersectY = (player.pos.y+(player.height/2)) - this.pos.y; - var normalizedRelativeIntersectionY = (relativeIntersectY/(player.height/2)); var MAX_BOUNCE_ANGLE = 4 * Math.PI/12; - var bounceAngle = normalizedRelativeIntersectionY * MAX_BOUNCE_ANGLE; - this.velocity.x = this.speed * Math.cos(bounceAngle) * multiplyX; - this.velocity.y = this.speed * -Math.sin(bounceAngle) * multiplyY; + var angle = bounceAngle(player.pos.y, this.pos.y, player.height, MAX_BOUNCE_ANGLE) + this.velocity.x = this.speed * angle.x * directionX; + this.velocity.y = this.speed * angle.y * directionY; + this.ballReturn % 2 === 0 ? sound.ping() : sound.pong(); }; -Ball.prototype.bounce = function (multiplyX, multiplyY, player) { +Ball.prototype.bounce = function (directionX, directionY, player) { if (player) - return this.bouncePlayer(multiplyX, multiplyY, player); + return this.bouncePlayer(directionX, directionY, player); - if (multiplyX) { - this.velocity.x = Math.abs(this.velocity.x) * multiplyX; + if (directionX) { + this.velocity.x = Math.abs(this.velocity.x) * directionX; } - if (multiplyY) { - this.velocity.y = Math.abs(this.velocity.y) * multiplyY; + if (directionY) { + this.velocity.y = Math.abs(this.velocity.y) * directionY; } }; -Ball.prototype.checkWallsCollision = function () { +Ball.prototype.fall = function (playerId) { + scores[playerId]++; + if (scores[playerId] >= MAX_SCORE) { + this.restart(); + state = 3; + if (playerId === 1) { + winnerMessage = startOption === 0 ? "AI Wins!" : "Player 2 Wins!"; + } else { + winnerMessage = startOption === 0 ? "You Win!" : "Player 1 Wins!"; + } + } else { + sound.fall(); + this.reset(); + } +}; +Ball.prototype.wallCollision = function () { if (this.pos.y < 0) { this.bounce(0, 1); } else if (this.pos.y > SCREEN_WIDTH) { this.bounce(0, -1); } else if (this.pos.x < 0) { - scores[1]++; - if (scores[1] >= MAX_SCORE) { - this.restart(); - state = 3; - winnerMessage = "AI Wins!"; - } else { - this.reset(); - } + this.fall(1); } else if (this.pos.x > SCREEN_WIDTH) { - scores[0]++; - if (scores[0] >= MAX_SCORE) { - this.restart(); - state = 3; - winnerMessage = "You Win!"; - } else { - this.reset(); - } + this.fall(0); } else { return false; } return true; }; -Ball.prototype.checkPlayerCollision = function (player) { +Ball.prototype.playerCollision = function (player) { if (intersects(this, player)) { if (this.pos.x < SCREEN_WIDTH/2) { this.bounce(1, 1, player); @@ -120,8 +155,8 @@ Ball.prototype.checkPlayerCollision = function (player) { } return false; }; -Ball.prototype.checkCollisions = function () { - return this.checkWallsCollision() || this.checkPlayerCollision(player) || this.checkPlayerCollision(ai); +Ball.prototype.collisions = function () { + return this.wallCollision() || this.playerCollision(player) || this.playerCollision(ai); }; Ball.prototype.updatePosition = function () { var elapsed = new Date().getTime() - this.lastUpdate; @@ -132,31 +167,20 @@ Ball.prototype.updatePosition = function () { Ball.prototype.update = function () { this.updatePosition(); this.lastUpdate = new Date().getTime(); - this.checkCollisions(); -}; -Ball.prototype.reset = function() { - this.speed = this.originalSpeed; - var x = scores[0] < scores[1] || (scores[0] === 0 && scores[1] === 0) ? -this.speed : this.speed; - var bounceAngle = Math.PI/6; - this.velocity = new Vector(x * Math.cos(bounceAngle), this.speed * -Math.sin(bounceAngle)); - this.pos = new Vector(SCREEN_WIDTH/2, random(0, SCREEN_WIDTH)); -}; -Ball.prototype.restart = function() { - ai.pos = new Vector(SCREEN_WIDTH - ai.width*2, SCREEN_WIDTH/2 - ai.height/2); - player.pos = new Vector(player.width*2, SCREEN_WIDTH/2 - player.height/2); - this.pos = new Vector(SCREEN_WIDTH/2, SCREEN_WIDTH/2); + this.collisions(); }; //////////////////////////// Player ///////////////////////////////////////// -function Player() { +function Player(right) { this.width = 4; this.height = 30; - this.pos = new Vector(this.width*2, SCREEN_WIDTH/2 - this.height/2); + this.pos = new Vector(right ? SCREEN_WIDTH-this.width : this.width, SCREEN_WIDTH/2 - this.height/2); this.acc = new Vector(0, 0); this.speed = 15; this.maxSpeed = 25; this.prevPos = null; + this.right = right; } Player.prototype.show = function () { if (this.prevPos != null) { @@ -196,11 +220,14 @@ function AI() { AI.prototype = Object.create(Player.prototype); AI.prototype.constructor = Player; AI.prototype.update = function () { - var y = ball.pos.y - (this.height/2 * aiSpeedRandom); - var yConstrained = constrain(y, 0, SCREEN_WIDTH-this.height); + var y = ball.pos.y - this.height/2; + var randomizedY = ball.ballReturn < 3 ? y : y + (aiSpeedRandom * this.height/2); + var yConstrained = constrain(randomizedY, 0, SCREEN_WIDTH-this.height); this.pos = new Vector(this.pos.x, yConstrained); }; +/////////////////////////////// Scenes //////////////////////////////////////// + function net() { var dashSize = 5; for (let y = dashSize/2; y < SCREEN_WIDTH; y += dashSize*2) { @@ -210,12 +237,6 @@ function net() { } } -var player = new Player(); -var ai = new AI(); -var ball = new Ball(); -var state = 0; -var prevScores = [0, 0]; - function drawScores() { let x1 = SCREEN_WIDTH/4-5; let x2 = SCREEN_WIDTH*3/4-5; @@ -233,10 +254,80 @@ function drawScores() { function drawGameOver() { g.setFont("Vector", 20); - g.drawString(winnerMessage, 75, SCREEN_WIDTH/2 - 10); + g.drawString(winnerMessage, startOption === 0 ? 55 : 75, SCREEN_WIDTH/2 - 10); } -function draw() { +function showControls(hide) { + g.setColor(hide ? 0 : -1); + g.setFont("Vector", 8); + var topArrowString = ` + ######## + ## + ## ## + ### ## + ### ## + ### +## +`; + + var arrows = [Graphics.createImage(topArrowString), Graphics.createImage(` + ## + ## +#################### + ## + ## +`), Graphics.createImage(topArrowString.split('\n').reverse().join('\n')) + ]; + + g.drawString('UP', 170, 50); + g.drawImage(arrows[0], 200, 40); + g.drawString('DOWN', 156, 120); + g.drawImage(arrows[1], 200, 120); + g.drawString('START', 152, 190); + g.drawImage(arrows[2], 200, 200); +} + +function drawStartScreen(hide) { + g.setColor(hide ? 0 : -1); + g.setFont("Vector", 10); + g.drawString("1 PLAYER", 95, 80); + g.drawString("2 PLAYERS", 95, 110); + + const ball1 = new Ball(); + ball1.prevPos = null; + ball1.pos = new Vector(87, 86); + ball1.show(hide || !(startOption === 0)); + + const ball2 = new Ball(); + ball2.prevPos = null; + ball2.pos = new Vector(87, 116); + ball2.show(hide || !(startOption === 1)); +} + +function drawStartTimer(count, callback) { + setTimeout(_ => { + player.show(); + ai.show(); + net(); + g.setColor(0); + g.fillRect(117-7, 115-7, 117+14, 115+14); + if (count >= 0) { + g.setFont("Vector", 10); + g.drawString(count+1, 115, 115); + g.setColor(-1); + g.drawString(count === 0 ? 'Go!' : count, 115 - (count === 0 ? 4: 0), 115); + drawStartTimer(count - 1, callback); + } else { + g.setColor(0); + g.fillRect(117-7, 115-7, 117+14, 115+14); + callback(); + } + }, 800); +} + +//////////////////////////////// Main ///////////////////////////////////////// + +function onFrame() { if (state === 1) { ball.update(); player.update(); @@ -261,22 +352,73 @@ function draw() { drawScores(); } +function startThatGame() { + player.show(); + ai.show(); + net(); + drawScores(); + drawStartTimer(3, () => setInterval(onFrame, 1000 / FPS)); +} + +var player = new Player(); +var ai; +var ball = new Ball(); +var state = 0; +var prevScores = [0, 0]; +var playerBle = null; +var startOption = 0; + g.clear(); g.setColor(0); g.fillRect(0,0,240,240); +showControls(); +setTimeout(() => { + showControls(true); + drawStartScreen(); +}, 2000); -setInterval(draw, 1000 / FPS); +////////////////////////////// Controls /////////////////////////////////////// -setWatch(o => o.state ? player.up() : player.stop(), BTN1, {repeat: true, edge: 'both'}); -setWatch(o => o.state ? player.down() : player.stop(), BTN3, {repeat: true, edge: 'both'}); -//setWatch(o => o.state ? player.down() : player.stop(), BTN5, {repeat: true, edge: 'both'}); +setWatch(o => { + if (state === 0) { + if (o.state) { + startOption = startOption === 0 ? startOption : startOption - 1; + drawStartScreen(); + } + } else o.state ? player.up() : player.stop(); +}, BTN1, {repeat: true, edge: 'both'}); +setWatch(o => { + if (state === 0) { + if (o.state) { + startOption = startOption === 1 ? startOption : startOption + 1; + drawStartScreen(); + } + } else o.state ? player.down() : player.stop(); +}, BTN2, {repeat: true, edge: 'both'}); setWatch(o => { state++; + clearInterval(); if (state >= 2) { - ball.restart(); g.setColor(0); - g.fillRect(0,0,240,240); + g.fillRect(0, 0, 240, 240); + ball.show(true); scores = [0, 0]; + playerBle = null; + ball = new Ball(); state = 1; + startThatGame(); + } else { + drawStartScreen(true); + showControls(true); + if (startOption === 1) { + ai = new Player(true); + startThatGame(); + } else { + ai = new AI(); + startThatGame(); + } } -}, BTN2, {repeat: true}); +}, BTN3, {repeat: true}); + +setWatch(o => startOption === 1 && (o.state ? ai.up() : ai.stop()), BTN4, {repeat: true, edge: 'both'}); +setWatch(o => startOption === 1 && (o.state ? ai.down() : ai.stop()), BTN5, {repeat: true, edge: 'both'}); From e03d33edc10fef8a720e0f51744d8608f0a4d8e7 Mon Sep 17 00:00:00 2001 From: fredericrous Date: Mon, 27 Apr 2020 14:13:26 +0100 Subject: [PATCH 268/302] add readme, remove impurity --- apps/pong/README.md | 28 ++++++++++++++++++++++++++++ apps/pong/app.js | 8 ++++---- 2 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 apps/pong/README.md diff --git a/apps/pong/README.md b/apps/pong/README.md new file mode 100644 index 000000000..ea4939539 --- /dev/null +++ b/apps/pong/README.md @@ -0,0 +1,28 @@ +# Pong + +A clone of the Atari game Pong + + + +## Features + +- Play against a dumb AI +- Play local Multiplayer against your friends + +## Controls + +Player's controls: +- UP: BTN1 +- DOWN: BTN2 +long press to move faster + +Restart a game: +- RESET: BTN3 + +Buttons for player 2: +- UP: BTN4 +- DOWN: BTN5 + +## Creator + + diff --git a/apps/pong/app.js b/apps/pong/app.js index 272eaf2e7..ba34d60b5 100644 --- a/apps/pong/app.js +++ b/apps/pong/app.js @@ -38,12 +38,12 @@ const constrain = (n, low, high) => Math.max(Math.min(n, high), low); const random = (min, max) => Math.random() * (max - min) + min; const intersects = (circ, rect, right) => { var c = circ.pos; - c.r = circ.r; - if (c.y - c.r < rect.pos.y + rect.height && c.y + c.r > rect.pos.y) { + var r = circ.r; + if (c.y - r < rect.pos.y + rect.height && c.y + r > rect.pos.y) { if (right) { - return c.x + c.r > rect.pos.x - rect.width*2 && c.x < rect.pos.x + rect.width + return c.x + r > rect.pos.x - rect.width*2 && c.x < rect.pos.x + rect.width } else { - return c.x - c.r < rect.pos.x + rect.width*2 && c.x > rect.pos.x - rect.width + return c.x - r < rect.pos.x + rect.width*2 && c.x > rect.pos.x - rect.width } } return false; From 727d7a1452f4ed87952e7b4293cbca4275bca553 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Mon, 27 Apr 2020 16:12:13 +0100 Subject: [PATCH 269/302] Online minification test --- index.html | 1 + js/appinfo.js | 10 + js/espruinotools.js | 33208 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 33219 insertions(+) create mode 100644 js/espruinotools.js diff --git a/index.html b/index.html index 3c8b440e4..c13bc0b36 100644 --- a/index.html +++ b/index.html @@ -156,6 +156,7 @@ + diff --git a/js/appinfo.js b/js/appinfo.js index 9fff7c92a..e5e50aa5c 100644 --- a/js/appinfo.js +++ b/js/appinfo.js @@ -11,6 +11,16 @@ var AppInfo = { return Promise.resolve(storageFile); else if (storageFile.url) return fileGetter(`apps/${app.id}/${storageFile.url}`).then(content => { + if (storageFile.url.endsWith(".js") && !storageFile.url.endsWith(".min.js")) { // if original file ends in '.js'... + return Espruino.transform(content, { + SET_TIME_ON_WRITE : false, + //PRETOKENISE : true, // pretokenise still has an issue with number passing + MINIFICATION_LEVEL : "ESPRIMA", + builtinModules : "Flash,Storage,heatshrink,tensorflow,locale" + }); + } else + return content; + }).then(content => { return { name : storageFile.name, content : content, diff --git a/js/espruinotools.js b/js/espruinotools.js new file mode 100644 index 000000000..d40ab7c77 --- /dev/null +++ b/js/espruinotools.js @@ -0,0 +1,33208 @@ +// EspruinoTools (https://github.com/espruino/EspruinoTools) + +if (typeof $ === "undefined") { + var jqReady = []; + var jqShim = { + ready : function(cb) { jqReady.push(cb); }, + css : function() {}, + html : function() {}, + width : function() {}, + height : function() {}, + addClass : function() {}, + removeClass : function() {}, + appendTo : function() { return jqShim; }, + show : function() {}, + hide : function() {}, + }; + var $ = function() { return jqShim; }; +} +/** + Copyright 2014 Gordon Williams (gw@pur3.co.uk) + + This Source Code is subject to the terms of the Mozilla Public + License, v2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + + ------------------------------------------------------------------ + Initialisation code + ------------------------------------------------------------------ +**/ +"use strict"; + +var Espruino; + +(function() { + + /** List of processors. These are functions that are called one + * after the other with the data received from the last one. + * + * Common processors are: + * + * sending - sending code to Espruino (no data) + * transformForEspruino - transform code ready to be sent to Espruino + * transformModuleForEspruino({code,name}) + * - transform module code before it's sent to Espruino with Modules.addCached (we only do this if we don't think it's been minified before) + * connected - connected to Espruino (no data) + * disconnected - disconnected from Espruino (no data) + * environmentVar - Board's process.env loaded (object to be saved into Espruino.Env.environmentData) + * boardJSONLoaded - Board's JSON was loaded into environmentVar + * getModule - Called with data={moduleName:"foo", moduleCode:undefined} - moduleCode should be filled in if the module can be found + * getURL - Called with data={url:"http://....", data:undefined) - data should be filled in if the URL is handled (See Espruino.Core.Utils.getURL to use this) + * terminalClear - terminal has been cleared + * terminalPrompt - we've received a '>' character (eg, `>` or `debug>`). The argument is the current line's contents. + * terminalNewLine - When we get a new line on the terminal, this gets called with the last line's contents + * debugMode - called with true or false when debug mode is entered or left + * editorHover - called with { node : htmlNode, showTooltip : function(htmlNode) } when something is hovered over + * notification - called with { mdg, type:"success","error"/"warning"/"info" } + **/ + var processors = {}; + + function init() { + + Espruino.Core.Config.loadConfiguration(function() { + // Initialise all modules + function initModule(modName, mod) { + console.log("Initialising "+modName); + if (mod.init !== undefined) + mod.init(); + } + + var module; + for (module in Espruino.Core) initModule(module, Espruino.Core[module]); + for (module in Espruino.Plugins) initModule(module, Espruino.Plugins[module]); + + callProcessor("initialised", undefined, function() { + // We need the delay because of background.js's url_handler... + setTimeout(function() { + Espruino.initialised = true; + }, 1000); + }); + }); + } + + // workaround for broken chrome on Mac + if (navigator.userAgent.indexOf("Mac OS X")>=0 && + navigator.userAgent.indexOf("Chrome/33.0.1750")>=0) { + $(document).ready(function() { window.setTimeout(init,100); }); + } else { + $(document).ready(init); + } + + /** Add a processor function of type function(data,callback) */ + function addProcessor(eventType, processor) { + if (processors[eventType]===undefined) + processors[eventType] = []; + processors[eventType].push(processor); + } + + /** Call a processor function */ + function callProcessor(eventType, data, callback) { + var p = processors[eventType]; + // no processors + if (p===undefined || p.length==0) { + if (callback!==undefined) callback(data); + return; + } + // now go through all processors + var n = 0; + var cbCalled = false; + var cb = function(inData) { + if (cbCalled) throw new Error("Internal error in "+eventType+" processor. Callback is called TWICE."); + cbCalled = true; + if (n < p.length) { + cbCalled = false; + p[n++](inData, cb); + } else { + if (callback!==undefined) callback(inData); + } + }; + cb(data); + } + + // ----------------------------------- + Espruino = { + Core : { }, + Plugins : { }, + addProcessor : addProcessor, + callProcessor : callProcessor, + initialised : false, + init : init, // just in case we need to initialise this by hand + }; + + return Espruino; +})(); +Espruino.Core.Notifications = { + success : function(e) { console.log(e); }, + error : function(e) { console.error(e); }, + warning : function(e) { console.warn(e); }, + info : function(e) { console.log(e); }, +}; +Espruino.Core.Status = { + setStatus : function(e,len) { console.log(e); }, + hasProgress : function() { return false; }, + incrementProgress : function(amt) {} +}; +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define(['exports'], factory) : + (factory((global.acorn = {}))); +}(this, (function (exports) { 'use strict'; + +// Reserved word lists for various dialects of the language + +var reservedWords = { + 3: "abstract boolean byte char class double enum export extends final float goto implements import int interface long native package private protected public short static super synchronized throws transient volatile", + 5: "class enum extends super const export import", + 6: "enum", + strict: "implements interface let package private protected public static yield", + strictBind: "eval arguments" +}; + +// And the keywords + +var ecma5AndLessKeywords = "break case catch continue debugger default do else finally for function if return switch throw try var while with null true false instanceof typeof void delete new in this"; + +var keywords = { + 5: ecma5AndLessKeywords, + 6: ecma5AndLessKeywords + " const class extends export import super" +}; + +var keywordRelationalOperator = /^in(stanceof)?$/; + +// ## Character categories + +// Big ugly regular expressions that match characters in the +// whitespace, identifier, and identifier-start categories. These +// are only applied when a character is found to actually have a +// code point above 128. +// Generated by `bin/generate-identifier-regex.js`. + +var nonASCIIidentifierStartChars = "\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0370-\u0374\u0376\u0377\u037a-\u037d\u037f\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u048a-\u052f\u0531-\u0556\u0559\u0561-\u0587\u05d0-\u05ea\u05f0-\u05f2\u0620-\u064a\u066e\u066f\u0671-\u06d3\u06d5\u06e5\u06e6\u06ee\u06ef\u06fa-\u06fc\u06ff\u0710\u0712-\u072f\u074d-\u07a5\u07b1\u07ca-\u07ea\u07f4\u07f5\u07fa\u0800-\u0815\u081a\u0824\u0828\u0840-\u0858\u08a0-\u08b4\u08b6-\u08bd\u0904-\u0939\u093d\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098c\u098f\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bd\u09ce\u09dc\u09dd\u09df-\u09e1\u09f0\u09f1\u0a05-\u0a0a\u0a0f\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32\u0a33\u0a35\u0a36\u0a38\u0a39\u0a59-\u0a5c\u0a5e\u0a72-\u0a74\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2\u0ab3\u0ab5-\u0ab9\u0abd\u0ad0\u0ae0\u0ae1\u0af9\u0b05-\u0b0c\u0b0f\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32\u0b33\u0b35-\u0b39\u0b3d\u0b5c\u0b5d\u0b5f-\u0b61\u0b71\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99\u0b9a\u0b9c\u0b9e\u0b9f\u0ba3\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bd0\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c39\u0c3d\u0c58-\u0c5a\u0c60\u0c61\u0c80\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbd\u0cde\u0ce0\u0ce1\u0cf1\u0cf2\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d3a\u0d3d\u0d4e\u0d54-\u0d56\u0d5f-\u0d61\u0d7a-\u0d7f\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0e01-\u0e30\u0e32\u0e33\u0e40-\u0e46\u0e81\u0e82\u0e84\u0e87\u0e88\u0e8a\u0e8d\u0e94-\u0e97\u0e99-\u0e9f\u0ea1-\u0ea3\u0ea5\u0ea7\u0eaa\u0eab\u0ead-\u0eb0\u0eb2\u0eb3\u0ebd\u0ec0-\u0ec4\u0ec6\u0edc-\u0edf\u0f00\u0f40-\u0f47\u0f49-\u0f6c\u0f88-\u0f8c\u1000-\u102a\u103f\u1050-\u1055\u105a-\u105d\u1061\u1065\u1066\u106e-\u1070\u1075-\u1081\u108e\u10a0-\u10c5\u10c7\u10cd\u10d0-\u10fa\u10fc-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u1380-\u138f\u13a0-\u13f5\u13f8-\u13fd\u1401-\u166c\u166f-\u167f\u1681-\u169a\u16a0-\u16ea\u16ee-\u16f8\u1700-\u170c\u170e-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176c\u176e-\u1770\u1780-\u17b3\u17d7\u17dc\u1820-\u1877\u1880-\u18a8\u18aa\u18b0-\u18f5\u1900-\u191e\u1950-\u196d\u1970-\u1974\u1980-\u19ab\u19b0-\u19c9\u1a00-\u1a16\u1a20-\u1a54\u1aa7\u1b05-\u1b33\u1b45-\u1b4b\u1b83-\u1ba0\u1bae\u1baf\u1bba-\u1be5\u1c00-\u1c23\u1c4d-\u1c4f\u1c5a-\u1c7d\u1c80-\u1c88\u1ce9-\u1cec\u1cee-\u1cf1\u1cf5\u1cf6\u1d00-\u1dbf\u1e00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u2071\u207f\u2090-\u209c\u2102\u2107\u210a-\u2113\u2115\u2118-\u211d\u2124\u2126\u2128\u212a-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2160-\u2188\u2c00-\u2c2e\u2c30-\u2c5e\u2c60-\u2ce4\u2ceb-\u2cee\u2cf2\u2cf3\u2d00-\u2d25\u2d27\u2d2d\u2d30-\u2d67\u2d6f\u2d80-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303c\u3041-\u3096\u309b-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312d\u3131-\u318e\u31a0-\u31ba\u31f0-\u31ff\u3400-\u4db5\u4e00-\u9fd5\ua000-\ua48c\ua4d0-\ua4fd\ua500-\ua60c\ua610-\ua61f\ua62a\ua62b\ua640-\ua66e\ua67f-\ua69d\ua6a0-\ua6ef\ua717-\ua71f\ua722-\ua788\ua78b-\ua7ae\ua7b0-\ua7b7\ua7f7-\ua801\ua803-\ua805\ua807-\ua80a\ua80c-\ua822\ua840-\ua873\ua882-\ua8b3\ua8f2-\ua8f7\ua8fb\ua8fd\ua90a-\ua925\ua930-\ua946\ua960-\ua97c\ua984-\ua9b2\ua9cf\ua9e0-\ua9e4\ua9e6-\ua9ef\ua9fa-\ua9fe\uaa00-\uaa28\uaa40-\uaa42\uaa44-\uaa4b\uaa60-\uaa76\uaa7a\uaa7e-\uaaaf\uaab1\uaab5\uaab6\uaab9-\uaabd\uaac0\uaac2\uaadb-\uaadd\uaae0-\uaaea\uaaf2-\uaaf4\uab01-\uab06\uab09-\uab0e\uab11-\uab16\uab20-\uab26\uab28-\uab2e\uab30-\uab5a\uab5c-\uab65\uab70-\uabe2\uac00-\ud7a3\ud7b0-\ud7c6\ud7cb-\ud7fb\uf900-\ufa6d\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\ufb1d\ufb1f-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40\ufb41\ufb43\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\uff21-\uff3a\uff41-\uff5a\uff66-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc"; +var nonASCIIidentifierChars = "\u200c\u200d\xb7\u0300-\u036f\u0387\u0483-\u0487\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u0669\u0670\u06d6-\u06dc\u06df-\u06e4\u06e7\u06e8\u06ea-\u06ed\u06f0-\u06f9\u0711\u0730-\u074a\u07a6-\u07b0\u07c0-\u07c9\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0859-\u085b\u08d4-\u08e1\u08e3-\u0903\u093a-\u093c\u093e-\u094f\u0951-\u0957\u0962\u0963\u0966-\u096f\u0981-\u0983\u09bc\u09be-\u09c4\u09c7\u09c8\u09cb-\u09cd\u09d7\u09e2\u09e3\u09e6-\u09ef\u0a01-\u0a03\u0a3c\u0a3e-\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a66-\u0a71\u0a75\u0a81-\u0a83\u0abc\u0abe-\u0ac5\u0ac7-\u0ac9\u0acb-\u0acd\u0ae2\u0ae3\u0ae6-\u0aef\u0b01-\u0b03\u0b3c\u0b3e-\u0b44\u0b47\u0b48\u0b4b-\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b66-\u0b6f\u0b82\u0bbe-\u0bc2\u0bc6-\u0bc8\u0bca-\u0bcd\u0bd7\u0be6-\u0bef\u0c00-\u0c03\u0c3e-\u0c44\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0c66-\u0c6f\u0c81-\u0c83\u0cbc\u0cbe-\u0cc4\u0cc6-\u0cc8\u0cca-\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0ce6-\u0cef\u0d01-\u0d03\u0d3e-\u0d44\u0d46-\u0d48\u0d4a-\u0d4d\u0d57\u0d62\u0d63\u0d66-\u0d6f\u0d82\u0d83\u0dca\u0dcf-\u0dd4\u0dd6\u0dd8-\u0ddf\u0de6-\u0def\u0df2\u0df3\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0e50-\u0e59\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0ed0-\u0ed9\u0f18\u0f19\u0f20-\u0f29\u0f35\u0f37\u0f39\u0f3e\u0f3f\u0f71-\u0f84\u0f86\u0f87\u0f8d-\u0f97\u0f99-\u0fbc\u0fc6\u102b-\u103e\u1040-\u1049\u1056-\u1059\u105e-\u1060\u1062-\u1064\u1067-\u106d\u1071-\u1074\u1082-\u108d\u108f-\u109d\u135d-\u135f\u1369-\u1371\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b4-\u17d3\u17dd\u17e0-\u17e9\u180b-\u180d\u1810-\u1819\u18a9\u1920-\u192b\u1930-\u193b\u1946-\u194f\u19d0-\u19da\u1a17-\u1a1b\u1a55-\u1a5e\u1a60-\u1a7c\u1a7f-\u1a89\u1a90-\u1a99\u1ab0-\u1abd\u1b00-\u1b04\u1b34-\u1b44\u1b50-\u1b59\u1b6b-\u1b73\u1b80-\u1b82\u1ba1-\u1bad\u1bb0-\u1bb9\u1be6-\u1bf3\u1c24-\u1c37\u1c40-\u1c49\u1c50-\u1c59\u1cd0-\u1cd2\u1cd4-\u1ce8\u1ced\u1cf2-\u1cf4\u1cf8\u1cf9\u1dc0-\u1df5\u1dfb-\u1dff\u203f\u2040\u2054\u20d0-\u20dc\u20e1\u20e5-\u20f0\u2cef-\u2cf1\u2d7f\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua620-\ua629\ua66f\ua674-\ua67d\ua69e\ua69f\ua6f0\ua6f1\ua802\ua806\ua80b\ua823-\ua827\ua880\ua881\ua8b4-\ua8c5\ua8d0-\ua8d9\ua8e0-\ua8f1\ua900-\ua909\ua926-\ua92d\ua947-\ua953\ua980-\ua983\ua9b3-\ua9c0\ua9d0-\ua9d9\ua9e5\ua9f0-\ua9f9\uaa29-\uaa36\uaa43\uaa4c\uaa4d\uaa50-\uaa59\uaa7b-\uaa7d\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uaaeb-\uaaef\uaaf5\uaaf6\uabe3-\uabea\uabec\uabed\uabf0-\uabf9\ufb1e\ufe00-\ufe0f\ufe20-\ufe2f\ufe33\ufe34\ufe4d-\ufe4f\uff10-\uff19\uff3f"; + +var nonASCIIidentifierStart = new RegExp("[" + nonASCIIidentifierStartChars + "]"); +var nonASCIIidentifier = new RegExp("[" + nonASCIIidentifierStartChars + nonASCIIidentifierChars + "]"); + +nonASCIIidentifierStartChars = nonASCIIidentifierChars = null; + +// These are a run-length and offset encoded representation of the +// >0xffff code points that are a valid part of identifiers. The +// offset starts at 0x10000, and each pair of numbers represents an +// offset to the next range, and then a size of the range. They were +// generated by bin/generate-identifier-regex.js + +// eslint-disable-next-line comma-spacing +var astralIdentifierStartCodes = [0,11,2,25,2,18,2,1,2,14,3,13,35,122,70,52,268,28,4,48,48,31,17,26,6,37,11,29,3,35,5,7,2,4,43,157,19,35,5,35,5,39,9,51,157,310,10,21,11,7,153,5,3,0,2,43,2,1,4,0,3,22,11,22,10,30,66,18,2,1,11,21,11,25,71,55,7,1,65,0,16,3,2,2,2,26,45,28,4,28,36,7,2,27,28,53,11,21,11,18,14,17,111,72,56,50,14,50,785,52,76,44,33,24,27,35,42,34,4,0,13,47,15,3,22,0,2,0,36,17,2,24,85,6,2,0,2,3,2,14,2,9,8,46,39,7,3,1,3,21,2,6,2,1,2,4,4,0,19,0,13,4,159,52,19,3,54,47,21,1,2,0,185,46,42,3,37,47,21,0,60,42,86,25,391,63,32,0,449,56,264,8,2,36,18,0,50,29,881,921,103,110,18,195,2749,1070,4050,582,8634,568,8,30,114,29,19,47,17,3,32,20,6,18,881,68,12,0,67,12,65,0,32,6124,20,754,9486,1,3071,106,6,12,4,8,8,9,5991,84,2,70,2,1,3,0,3,1,3,3,2,11,2,0,2,6,2,64,2,3,3,7,2,6,2,27,2,3,2,4,2,0,4,6,2,339,3,24,2,24,2,30,2,24,2,30,2,24,2,30,2,24,2,30,2,24,2,7,4149,196,60,67,1213,3,2,26,2,1,2,0,3,0,2,9,2,3,2,0,2,0,7,0,5,0,2,0,2,0,2,2,2,1,2,0,3,0,2,0,2,0,2,0,2,0,2,1,2,0,3,3,2,6,2,3,2,3,2,0,2,9,2,16,6,2,2,4,2,16,4421,42710,42,4148,12,221,3,5761,10591,541]; + +// eslint-disable-next-line comma-spacing +var astralIdentifierCodes = [509,0,227,0,150,4,294,9,1368,2,2,1,6,3,41,2,5,0,166,1,1306,2,54,14,32,9,16,3,46,10,54,9,7,2,37,13,2,9,52,0,13,2,49,13,10,2,4,9,83,11,7,0,161,11,6,9,7,3,57,0,2,6,3,1,3,2,10,0,11,1,3,6,4,4,193,17,10,9,87,19,13,9,214,6,3,8,28,1,83,16,16,9,82,12,9,9,84,14,5,9,423,9,838,7,2,7,17,9,57,21,2,13,19882,9,135,4,60,6,26,9,1016,45,17,3,19723,1,5319,4,4,5,9,7,3,6,31,3,149,2,1418,49,513,54,5,49,9,0,15,0,23,4,2,14,1361,6,2,16,3,6,2,1,2,4,2214,6,110,6,6,9,792487,239]; + +// This has a complexity linear to the value of the code. The +// assumption is that looking up astral identifier characters is +// rare. +function isInAstralSet(code, set) { + var pos = 0x10000; + for (var i = 0; i < set.length; i += 2) { + pos += set[i]; + if (pos > code) { return false } + pos += set[i + 1]; + if (pos >= code) { return true } + } +} + +// Test whether a given character code starts an identifier. + +function isIdentifierStart(code, astral) { + if (code < 65) { return code === 36 } + if (code < 91) { return true } + if (code < 97) { return code === 95 } + if (code < 123) { return true } + if (code <= 0xffff) { return code >= 0xaa && nonASCIIidentifierStart.test(String.fromCharCode(code)) } + if (astral === false) { return false } + return isInAstralSet(code, astralIdentifierStartCodes) +} + +// Test whether a given character is part of an identifier. + +function isIdentifierChar(code, astral) { + if (code < 48) { return code === 36 } + if (code < 58) { return true } + if (code < 65) { return false } + if (code < 91) { return true } + if (code < 97) { return code === 95 } + if (code < 123) { return true } + if (code <= 0xffff) { return code >= 0xaa && nonASCIIidentifier.test(String.fromCharCode(code)) } + if (astral === false) { return false } + return isInAstralSet(code, astralIdentifierStartCodes) || isInAstralSet(code, astralIdentifierCodes) +} + +// ## Token types + +// The assignment of fine-grained, information-carrying type objects +// allows the tokenizer to store the information it has about a +// token in a way that is very cheap for the parser to look up. + +// All token type variables start with an underscore, to make them +// easy to recognize. + +// The `beforeExpr` property is used to disambiguate between regular +// expressions and divisions. It is set on all token types that can +// be followed by an expression (thus, a slash after them would be a +// regular expression). +// +// The `startsExpr` property is used to check if the token ends a +// `yield` expression. It is set on all token types that either can +// directly start an expression (like a quotation mark) or can +// continue an expression (like the body of a string). +// +// `isLoop` marks a keyword as starting a loop, which is important +// to know when parsing a label, in order to allow or disallow +// continue jumps to that label. + +var TokenType = function TokenType(label, conf) { + if ( conf === void 0 ) conf = {}; + + this.label = label; + this.keyword = conf.keyword; + this.beforeExpr = !!conf.beforeExpr; + this.startsExpr = !!conf.startsExpr; + this.isLoop = !!conf.isLoop; + this.isAssign = !!conf.isAssign; + this.prefix = !!conf.prefix; + this.postfix = !!conf.postfix; + this.binop = conf.binop || null; + this.updateContext = null; +}; + +function binop(name, prec) { + return new TokenType(name, {beforeExpr: true, binop: prec}) +} +var beforeExpr = {beforeExpr: true}; +var startsExpr = {startsExpr: true}; + +// Map keyword names to token types. + +var keywords$1 = {}; + +// Succinct definitions of keyword token types +function kw(name, options) { + if ( options === void 0 ) options = {}; + + options.keyword = name; + return keywords$1[name] = new TokenType(name, options) +} + +var types = { + num: new TokenType("num", startsExpr), + regexp: new TokenType("regexp", startsExpr), + string: new TokenType("string", startsExpr), + name: new TokenType("name", startsExpr), + eof: new TokenType("eof"), + + // Punctuation token types. + bracketL: new TokenType("[", {beforeExpr: true, startsExpr: true}), + bracketR: new TokenType("]"), + braceL: new TokenType("{", {beforeExpr: true, startsExpr: true}), + braceR: new TokenType("}"), + parenL: new TokenType("(", {beforeExpr: true, startsExpr: true}), + parenR: new TokenType(")"), + comma: new TokenType(",", beforeExpr), + semi: new TokenType(";", beforeExpr), + colon: new TokenType(":", beforeExpr), + dot: new TokenType("."), + question: new TokenType("?", beforeExpr), + arrow: new TokenType("=>", beforeExpr), + template: new TokenType("template"), + invalidTemplate: new TokenType("invalidTemplate"), + ellipsis: new TokenType("...", beforeExpr), + backQuote: new TokenType("`", startsExpr), + dollarBraceL: new TokenType("${", {beforeExpr: true, startsExpr: true}), + + // Operators. These carry several kinds of properties to help the + // parser use them properly (the presence of these properties is + // what categorizes them as operators). + // + // `binop`, when present, specifies that this operator is a binary + // operator, and will refer to its precedence. + // + // `prefix` and `postfix` mark the operator as a prefix or postfix + // unary operator. + // + // `isAssign` marks all of `=`, `+=`, `-=` etcetera, which act as + // binary operators with a very low precedence, that should result + // in AssignmentExpression nodes. + + eq: new TokenType("=", {beforeExpr: true, isAssign: true}), + assign: new TokenType("_=", {beforeExpr: true, isAssign: true}), + incDec: new TokenType("++/--", {prefix: true, postfix: true, startsExpr: true}), + prefix: new TokenType("!/~", {beforeExpr: true, prefix: true, startsExpr: true}), + logicalOR: binop("||", 1), + logicalAND: binop("&&", 2), + bitwiseOR: binop("|", 3), + bitwiseXOR: binop("^", 4), + bitwiseAND: binop("&", 5), + equality: binop("==/!=/===/!==", 6), + relational: binop("/<=/>=", 7), + bitShift: binop("<>/>>>", 8), + plusMin: new TokenType("+/-", {beforeExpr: true, binop: 9, prefix: true, startsExpr: true}), + modulo: binop("%", 10), + star: binop("*", 10), + slash: binop("/", 10), + starstar: new TokenType("**", {beforeExpr: true}), + + // Keyword token types. + _break: kw("break"), + _case: kw("case", beforeExpr), + _catch: kw("catch"), + _continue: kw("continue"), + _debugger: kw("debugger"), + _default: kw("default", beforeExpr), + _do: kw("do", {isLoop: true, beforeExpr: true}), + _else: kw("else", beforeExpr), + _finally: kw("finally"), + _for: kw("for", {isLoop: true}), + _function: kw("function", startsExpr), + _if: kw("if"), + _return: kw("return", beforeExpr), + _switch: kw("switch"), + _throw: kw("throw", beforeExpr), + _try: kw("try"), + _var: kw("var"), + _const: kw("const"), + _while: kw("while", {isLoop: true}), + _with: kw("with"), + _new: kw("new", {beforeExpr: true, startsExpr: true}), + _this: kw("this", startsExpr), + _super: kw("super", startsExpr), + _class: kw("class", startsExpr), + _extends: kw("extends", beforeExpr), + _export: kw("export"), + _import: kw("import"), + _null: kw("null", startsExpr), + _true: kw("true", startsExpr), + _false: kw("false", startsExpr), + _in: kw("in", {beforeExpr: true, binop: 7}), + _instanceof: kw("instanceof", {beforeExpr: true, binop: 7}), + _typeof: kw("typeof", {beforeExpr: true, prefix: true, startsExpr: true}), + _void: kw("void", {beforeExpr: true, prefix: true, startsExpr: true}), + _delete: kw("delete", {beforeExpr: true, prefix: true, startsExpr: true}) +}; + +// Matches a whole line break (where CRLF is considered a single +// line break). Used to count lines. + +var lineBreak = /\r\n?|\n|\u2028|\u2029/; +var lineBreakG = new RegExp(lineBreak.source, "g"); + +function isNewLine(code) { + return code === 10 || code === 13 || code === 0x2028 || code === 0x2029 +} + +var nonASCIIwhitespace = /[\u1680\u180e\u2000-\u200a\u202f\u205f\u3000\ufeff]/; + +var skipWhiteSpace = /(?:\s|\/\/.*|\/\*[^]*?\*\/)*/g; + +var ref = Object.prototype; +var hasOwnProperty = ref.hasOwnProperty; +var toString = ref.toString; + +// Checks if an object has a property. + +function has(obj, propName) { + return hasOwnProperty.call(obj, propName) +} + +var isArray = Array.isArray || (function (obj) { return ( + toString.call(obj) === "[object Array]" +); }); + +// These are used when `options.locations` is on, for the +// `startLoc` and `endLoc` properties. + +var Position = function Position(line, col) { + this.line = line; + this.column = col; +}; + +Position.prototype.offset = function offset (n) { + return new Position(this.line, this.column + n) +}; + +var SourceLocation = function SourceLocation(p, start, end) { + this.start = start; + this.end = end; + if (p.sourceFile !== null) { this.source = p.sourceFile; } +}; + +// The `getLineInfo` function is mostly useful when the +// `locations` option is off (for performance reasons) and you +// want to find the line/column position for a given character +// offset. `input` should be the code string that the offset refers +// into. + +function getLineInfo(input, offset) { + for (var line = 1, cur = 0;;) { + lineBreakG.lastIndex = cur; + var match = lineBreakG.exec(input); + if (match && match.index < offset) { + ++line; + cur = match.index + match[0].length; + } else { + return new Position(line, offset - cur) + } + } +} + +// A second optional argument can be given to further configure +// the parser process. These options are recognized: + +var defaultOptions = { + // `ecmaVersion` indicates the ECMAScript version to parse. Must + // be either 3, 5, 6 (2015), 7 (2016), or 8 (2017). This influences support + // for strict mode, the set of reserved words, and support for + // new syntax features. The default is 7. + ecmaVersion: 7, + // `sourceType` indicates the mode the code should be parsed in. + // Can be either `"script"` or `"module"`. This influences global + // strict mode and parsing of `import` and `export` declarations. + sourceType: "script", + // `onInsertedSemicolon` can be a callback that will be called + // when a semicolon is automatically inserted. It will be passed + // th position of the comma as an offset, and if `locations` is + // enabled, it is given the location as a `{line, column}` object + // as second argument. + onInsertedSemicolon: null, + // `onTrailingComma` is similar to `onInsertedSemicolon`, but for + // trailing commas. + onTrailingComma: null, + // By default, reserved words are only enforced if ecmaVersion >= 5. + // Set `allowReserved` to a boolean value to explicitly turn this on + // an off. When this option has the value "never", reserved words + // and keywords can also not be used as property names. + allowReserved: null, + // When enabled, a return at the top level is not considered an + // error. + allowReturnOutsideFunction: false, + // When enabled, import/export statements are not constrained to + // appearing at the top of the program. + allowImportExportEverywhere: false, + // When enabled, hashbang directive in the beginning of file + // is allowed and treated as a line comment. + allowHashBang: false, + // When `locations` is on, `loc` properties holding objects with + // `start` and `end` properties in `{line, column}` form (with + // line being 1-based and column 0-based) will be attached to the + // nodes. + locations: false, + // A function can be passed as `onToken` option, which will + // cause Acorn to call that function with object in the same + // format as tokens returned from `tokenizer().getToken()`. Note + // that you are not allowed to call the parser from the + // callback—that will corrupt its internal state. + onToken: null, + // A function can be passed as `onComment` option, which will + // cause Acorn to call that function with `(block, text, start, + // end)` parameters whenever a comment is skipped. `block` is a + // boolean indicating whether this is a block (`/* */`) comment, + // `text` is the content of the comment, and `start` and `end` are + // character offsets that denote the start and end of the comment. + // When the `locations` option is on, two more parameters are + // passed, the full `{line, column}` locations of the start and + // end of the comments. Note that you are not allowed to call the + // parser from the callback—that will corrupt its internal state. + onComment: null, + // Nodes have their start and end characters offsets recorded in + // `start` and `end` properties (directly on the node, rather than + // the `loc` object, which holds line/column data. To also add a + // [semi-standardized][range] `range` property holding a `[start, + // end]` array with the same numbers, set the `ranges` option to + // `true`. + // + // [range]: https://bugzilla.mozilla.org/show_bug.cgi?id=745678 + ranges: false, + // It is possible to parse multiple files into a single AST by + // passing the tree produced by parsing the first file as + // `program` option in subsequent parses. This will add the + // toplevel forms of the parsed file to the `Program` (top) node + // of an existing parse tree. + program: null, + // When `locations` is on, you can pass this to record the source + // file in every node's `loc` object. + sourceFile: null, + // This value, if given, is stored in every node, whether + // `locations` is on or off. + directSourceFile: null, + // When enabled, parenthesized expressions are represented by + // (non-standard) ParenthesizedExpression nodes + preserveParens: false, + plugins: {} +}; + +// Interpret and default an options object + +function getOptions(opts) { + var options = {}; + + for (var opt in defaultOptions) + { options[opt] = opts && has(opts, opt) ? opts[opt] : defaultOptions[opt]; } + + if (options.ecmaVersion >= 2015) + { options.ecmaVersion -= 2009; } + + if (options.allowReserved == null) + { options.allowReserved = options.ecmaVersion < 5; } + + if (isArray(options.onToken)) { + var tokens = options.onToken; + options.onToken = function (token) { return tokens.push(token); }; + } + if (isArray(options.onComment)) + { options.onComment = pushComment(options, options.onComment); } + + return options +} + +function pushComment(options, array) { + return function(block, text, start, end, startLoc, endLoc) { + var comment = { + type: block ? "Block" : "Line", + value: text, + start: start, + end: end + }; + if (options.locations) + { comment.loc = new SourceLocation(this, startLoc, endLoc); } + if (options.ranges) + { comment.range = [start, end]; } + array.push(comment); + } +} + +// Registered plugins +var plugins = {}; + +function keywordRegexp(words) { + return new RegExp("^(?:" + words.replace(/ /g, "|") + ")$") +} + +var Parser = function Parser(options, input, startPos) { + this.options = options = getOptions(options); + this.sourceFile = options.sourceFile; + this.keywords = keywordRegexp(keywords[options.ecmaVersion >= 6 ? 6 : 5]); + var reserved = ""; + if (!options.allowReserved) { + for (var v = options.ecmaVersion;; v--) + { if (reserved = reservedWords[v]) { break } } + if (options.sourceType == "module") { reserved += " await"; } + } + this.reservedWords = keywordRegexp(reserved); + var reservedStrict = (reserved ? reserved + " " : "") + reservedWords.strict; + this.reservedWordsStrict = keywordRegexp(reservedStrict); + this.reservedWordsStrictBind = keywordRegexp(reservedStrict + " " + reservedWords.strictBind); + this.input = String(input); + + // Used to signal to callers of `readWord1` whether the word + // contained any escape sequences. This is needed because words with + // escape sequences must not be interpreted as keywords. + this.containsEsc = false; + + // Load plugins + this.loadPlugins(options.plugins); + + // Set up token state + + // The current position of the tokenizer in the input. + if (startPos) { + this.pos = startPos; + this.lineStart = this.input.lastIndexOf("\n", startPos - 1) + 1; + this.curLine = this.input.slice(0, this.lineStart).split(lineBreak).length; + } else { + this.pos = this.lineStart = 0; + this.curLine = 1; + } + + // Properties of the current token: + // Its type + this.type = types.eof; + // For tokens that include more information than their type, the value + this.value = null; + // Its start and end offset + this.start = this.end = this.pos; + // And, if locations are used, the {line, column} object + // corresponding to those offsets + this.startLoc = this.endLoc = this.curPosition(); + + // Position information for the previous token + this.lastTokEndLoc = this.lastTokStartLoc = null; + this.lastTokStart = this.lastTokEnd = this.pos; + + // The context stack is used to superficially track syntactic + // context to predict whether a regular expression is allowed in a + // given position. + this.context = this.initialContext(); + this.exprAllowed = true; + + // Figure out if it's a module code. + this.inModule = options.sourceType === "module"; + this.strict = this.inModule || this.strictDirective(this.pos); + + // Used to signify the start of a potential arrow function + this.potentialArrowAt = -1; + + // Flags to track whether we are in a function, a generator, an async function. + this.inFunction = this.inGenerator = this.inAsync = false; + // Positions to delayed-check that yield/await does not exist in default parameters. + this.yieldPos = this.awaitPos = 0; + // Labels in scope. + this.labels = []; + + // If enabled, skip leading hashbang line. + if (this.pos === 0 && options.allowHashBang && this.input.slice(0, 2) === "#!") + { this.skipLineComment(2); } + + // Scope tracking for duplicate variable names (see scope.js) + this.scopeStack = []; + this.enterFunctionScope(); +}; + +// DEPRECATED Kept for backwards compatibility until 3.0 in case a plugin uses them +Parser.prototype.isKeyword = function isKeyword (word) { return this.keywords.test(word) }; +Parser.prototype.isReservedWord = function isReservedWord (word) { return this.reservedWords.test(word) }; + +Parser.prototype.extend = function extend (name, f) { + this[name] = f(this[name]); +}; + +Parser.prototype.loadPlugins = function loadPlugins (pluginConfigs) { + var this$1 = this; + + for (var name in pluginConfigs) { + var plugin = plugins[name]; + if (!plugin) { throw new Error("Plugin '" + name + "' not found") } + plugin(this$1, pluginConfigs[name]); + } +}; + +Parser.prototype.parse = function parse () { + var node = this.options.program || this.startNode(); + this.nextToken(); + return this.parseTopLevel(node) +}; + +var pp = Parser.prototype; + +// ## Parser utilities + +var literal = /^(?:'((?:\\.|[^'])*?)'|"((?:\\.|[^"])*?)"|;)/; +pp.strictDirective = function(start) { + var this$1 = this; + + for (;;) { + skipWhiteSpace.lastIndex = start; + start += skipWhiteSpace.exec(this$1.input)[0].length; + var match = literal.exec(this$1.input.slice(start)); + if (!match) { return false } + if ((match[1] || match[2]) == "use strict") { return true } + start += match[0].length; + } +}; + +// Predicate that tests whether the next token is of the given +// type, and if yes, consumes it as a side effect. + +pp.eat = function(type) { + if (this.type === type) { + this.next(); + return true + } else { + return false + } +}; + +// Tests whether parsed token is a contextual keyword. + +pp.isContextual = function(name) { + return this.type === types.name && this.value === name && !this.containsEsc +}; + +// Consumes contextual keyword if possible. + +pp.eatContextual = function(name) { + if (!this.isContextual(name)) { return false } + this.next(); + return true +}; + +// Asserts that following token is given contextual keyword. + +pp.expectContextual = function(name) { + if (!this.eatContextual(name)) { this.unexpected(); } +}; + +// Test whether a semicolon can be inserted at the current position. + +pp.canInsertSemicolon = function() { + return this.type === types.eof || + this.type === types.braceR || + lineBreak.test(this.input.slice(this.lastTokEnd, this.start)) +}; + +pp.insertSemicolon = function() { + if (this.canInsertSemicolon()) { + if (this.options.onInsertedSemicolon) + { this.options.onInsertedSemicolon(this.lastTokEnd, this.lastTokEndLoc); } + return true + } +}; + +// Consume a semicolon, or, failing that, see if we are allowed to +// pretend that there is a semicolon at this position. + +pp.semicolon = function() { + if (!this.eat(types.semi) && !this.insertSemicolon()) { this.unexpected(); } +}; + +pp.afterTrailingComma = function(tokType, notNext) { + if (this.type == tokType) { + if (this.options.onTrailingComma) + { this.options.onTrailingComma(this.lastTokStart, this.lastTokStartLoc); } + if (!notNext) + { this.next(); } + return true + } +}; + +// Expect a token of a given type. If found, consume it, otherwise, +// raise an unexpected token error. + +pp.expect = function(type) { + this.eat(type) || this.unexpected(); +}; + +// Raise an unexpected token error. + +pp.unexpected = function(pos) { + this.raise(pos != null ? pos : this.start, "Unexpected token"); +}; + +function DestructuringErrors() { + this.shorthandAssign = + this.trailingComma = + this.parenthesizedAssign = + this.parenthesizedBind = + this.doubleProto = + -1; +} + +pp.checkPatternErrors = function(refDestructuringErrors, isAssign) { + if (!refDestructuringErrors) { return } + if (refDestructuringErrors.trailingComma > -1) + { this.raiseRecoverable(refDestructuringErrors.trailingComma, "Comma is not permitted after the rest element"); } + var parens = isAssign ? refDestructuringErrors.parenthesizedAssign : refDestructuringErrors.parenthesizedBind; + if (parens > -1) { this.raiseRecoverable(parens, "Parenthesized pattern"); } +}; + +pp.checkExpressionErrors = function(refDestructuringErrors, andThrow) { + if (!refDestructuringErrors) { return false } + var shorthandAssign = refDestructuringErrors.shorthandAssign; + var doubleProto = refDestructuringErrors.doubleProto; + if (!andThrow) { return shorthandAssign >= 0 || doubleProto >= 0 } + if (shorthandAssign >= 0) + { this.raise(shorthandAssign, "Shorthand property assignments are valid only in destructuring patterns"); } + if (doubleProto >= 0) + { this.raiseRecoverable(doubleProto, "Redefinition of __proto__ property"); } +}; + +pp.checkYieldAwaitInDefaultParams = function() { + if (this.yieldPos && (!this.awaitPos || this.yieldPos < this.awaitPos)) + { this.raise(this.yieldPos, "Yield expression cannot be a default value"); } + if (this.awaitPos) + { this.raise(this.awaitPos, "Await expression cannot be a default value"); } +}; + +pp.isSimpleAssignTarget = function(expr) { + if (expr.type === "ParenthesizedExpression") + { return this.isSimpleAssignTarget(expr.expression) } + return expr.type === "Identifier" || expr.type === "MemberExpression" +}; + +var pp$1 = Parser.prototype; + +// ### Statement parsing + +// Parse a program. Initializes the parser, reads any number of +// statements, and wraps them in a Program node. Optionally takes a +// `program` argument. If present, the statements will be appended +// to its body instead of creating a new node. + +pp$1.parseTopLevel = function(node) { + var this$1 = this; + + var exports = {}; + if (!node.body) { node.body = []; } + while (this.type !== types.eof) { + var stmt = this$1.parseStatement(true, true, exports); + node.body.push(stmt); + } + this.adaptDirectivePrologue(node.body); + this.next(); + if (this.options.ecmaVersion >= 6) { + node.sourceType = this.options.sourceType; + } + return this.finishNode(node, "Program") +}; + +var loopLabel = {kind: "loop"}; +var switchLabel = {kind: "switch"}; + +pp$1.isLet = function() { + if (this.options.ecmaVersion < 6 || !this.isContextual("let")) { return false } + skipWhiteSpace.lastIndex = this.pos; + var skip = skipWhiteSpace.exec(this.input); + var next = this.pos + skip[0].length, nextCh = this.input.charCodeAt(next); + if (nextCh === 91 || nextCh == 123) { return true } // '{' and '[' + if (isIdentifierStart(nextCh, true)) { + var pos = next + 1; + while (isIdentifierChar(this.input.charCodeAt(pos), true)) { ++pos; } + var ident = this.input.slice(next, pos); + if (!keywordRelationalOperator.test(ident)) { return true } + } + return false +}; + +// check 'async [no LineTerminator here] function' +// - 'async /*foo*/ function' is OK. +// - 'async /*\n*/ function' is invalid. +pp$1.isAsyncFunction = function() { + if (this.options.ecmaVersion < 8 || !this.isContextual("async")) + { return false } + + skipWhiteSpace.lastIndex = this.pos; + var skip = skipWhiteSpace.exec(this.input); + var next = this.pos + skip[0].length; + return !lineBreak.test(this.input.slice(this.pos, next)) && + this.input.slice(next, next + 8) === "function" && + (next + 8 == this.input.length || !isIdentifierChar(this.input.charAt(next + 8))) +}; + +// Parse a single statement. +// +// If expecting a statement and finding a slash operator, parse a +// regular expression literal. This is to handle cases like +// `if (foo) /blah/.exec(foo)`, where looking at the previous token +// does not help. + +pp$1.parseStatement = function(declaration, topLevel, exports) { + var starttype = this.type, node = this.startNode(), kind; + + if (this.isLet()) { + starttype = types._var; + kind = "let"; + } + + // Most types of statements are recognized by the keyword they + // start with. Many are trivial to parse, some require a bit of + // complexity. + + switch (starttype) { + case types._break: case types._continue: return this.parseBreakContinueStatement(node, starttype.keyword) + case types._debugger: return this.parseDebuggerStatement(node) + case types._do: return this.parseDoStatement(node) + case types._for: return this.parseForStatement(node) + case types._function: + if (!declaration && this.options.ecmaVersion >= 6) { this.unexpected(); } + return this.parseFunctionStatement(node, false) + case types._class: + if (!declaration) { this.unexpected(); } + return this.parseClass(node, true) + case types._if: return this.parseIfStatement(node) + case types._return: return this.parseReturnStatement(node) + case types._switch: return this.parseSwitchStatement(node) + case types._throw: return this.parseThrowStatement(node) + case types._try: return this.parseTryStatement(node) + case types._const: case types._var: + kind = kind || this.value; + if (!declaration && kind != "var") { this.unexpected(); } + return this.parseVarStatement(node, kind) + case types._while: return this.parseWhileStatement(node) + case types._with: return this.parseWithStatement(node) + case types.braceL: return this.parseBlock() + case types.semi: return this.parseEmptyStatement(node) + case types._export: + case types._import: + if (!this.options.allowImportExportEverywhere) { + if (!topLevel) + { this.raise(this.start, "'import' and 'export' may only appear at the top level"); } + if (!this.inModule) + { this.raise(this.start, "'import' and 'export' may appear only with 'sourceType: module'"); } + } + return starttype === types._import ? this.parseImport(node) : this.parseExport(node, exports) + + // If the statement does not start with a statement keyword or a + // brace, it's an ExpressionStatement or LabeledStatement. We + // simply start parsing an expression, and afterwards, if the + // next token is a colon and the expression was a simple + // Identifier node, we switch to interpreting it as a label. + default: + if (this.isAsyncFunction()) { + if (!declaration) { this.unexpected(); } + this.next(); + return this.parseFunctionStatement(node, true) + } + + var maybeName = this.value, expr = this.parseExpression(); + if (starttype === types.name && expr.type === "Identifier" && this.eat(types.colon)) + { return this.parseLabeledStatement(node, maybeName, expr) } + else { return this.parseExpressionStatement(node, expr) } + } +}; + +pp$1.parseBreakContinueStatement = function(node, keyword) { + var this$1 = this; + + var isBreak = keyword == "break"; + this.next(); + if (this.eat(types.semi) || this.insertSemicolon()) { node.label = null; } + else if (this.type !== types.name) { this.unexpected(); } + else { + node.label = this.parseIdent(); + this.semicolon(); + } + + // Verify that there is an actual destination to break or + // continue to. + var i = 0; + for (; i < this.labels.length; ++i) { + var lab = this$1.labels[i]; + if (node.label == null || lab.name === node.label.name) { + if (lab.kind != null && (isBreak || lab.kind === "loop")) { break } + if (node.label && isBreak) { break } + } + } + if (i === this.labels.length) { this.raise(node.start, "Unsyntactic " + keyword); } + return this.finishNode(node, isBreak ? "BreakStatement" : "ContinueStatement") +}; + +pp$1.parseDebuggerStatement = function(node) { + this.next(); + this.semicolon(); + return this.finishNode(node, "DebuggerStatement") +}; + +pp$1.parseDoStatement = function(node) { + this.next(); + this.labels.push(loopLabel); + node.body = this.parseStatement(false); + this.labels.pop(); + this.expect(types._while); + node.test = this.parseParenExpression(); + if (this.options.ecmaVersion >= 6) + { this.eat(types.semi); } + else + { this.semicolon(); } + return this.finishNode(node, "DoWhileStatement") +}; + +// Disambiguating between a `for` and a `for`/`in` or `for`/`of` +// loop is non-trivial. Basically, we have to parse the init `var` +// statement or expression, disallowing the `in` operator (see +// the second parameter to `parseExpression`), and then check +// whether the next token is `in` or `of`. When there is no init +// part (semicolon immediately after the opening parenthesis), it +// is a regular `for` loop. + +pp$1.parseForStatement = function(node) { + this.next(); + var awaitAt = (this.options.ecmaVersion >= 9 && this.inAsync && this.eatContextual("await")) ? this.lastTokStart : -1; + this.labels.push(loopLabel); + this.enterLexicalScope(); + this.expect(types.parenL); + if (this.type === types.semi) { + if (awaitAt > -1) { this.unexpected(awaitAt); } + return this.parseFor(node, null) + } + var isLet = this.isLet(); + if (this.type === types._var || this.type === types._const || isLet) { + var init$1 = this.startNode(), kind = isLet ? "let" : this.value; + this.next(); + this.parseVar(init$1, true, kind); + this.finishNode(init$1, "VariableDeclaration"); + if ((this.type === types._in || (this.options.ecmaVersion >= 6 && this.isContextual("of"))) && init$1.declarations.length === 1 && + !(kind !== "var" && init$1.declarations[0].init)) { + if (this.options.ecmaVersion >= 9) { + if (this.type === types._in) { + if (awaitAt > -1) { this.unexpected(awaitAt); } + } else { node.await = awaitAt > -1; } + } + return this.parseForIn(node, init$1) + } + if (awaitAt > -1) { this.unexpected(awaitAt); } + return this.parseFor(node, init$1) + } + var refDestructuringErrors = new DestructuringErrors; + var init = this.parseExpression(true, refDestructuringErrors); + if (this.type === types._in || (this.options.ecmaVersion >= 6 && this.isContextual("of"))) { + if (this.options.ecmaVersion >= 9) { + if (this.type === types._in) { + if (awaitAt > -1) { this.unexpected(awaitAt); } + } else { node.await = awaitAt > -1; } + } + this.toAssignable(init, false, refDestructuringErrors); + this.checkLVal(init); + return this.parseForIn(node, init) + } else { + this.checkExpressionErrors(refDestructuringErrors, true); + } + if (awaitAt > -1) { this.unexpected(awaitAt); } + return this.parseFor(node, init) +}; + +pp$1.parseFunctionStatement = function(node, isAsync) { + this.next(); + return this.parseFunction(node, true, false, isAsync) +}; + +pp$1.parseIfStatement = function(node) { + this.next(); + node.test = this.parseParenExpression(); + // allow function declarations in branches, but only in non-strict mode + node.consequent = this.parseStatement(!this.strict && this.type == types._function); + node.alternate = this.eat(types._else) ? this.parseStatement(!this.strict && this.type == types._function) : null; + return this.finishNode(node, "IfStatement") +}; + +pp$1.parseReturnStatement = function(node) { + if (!this.inFunction && !this.options.allowReturnOutsideFunction) + { this.raise(this.start, "'return' outside of function"); } + this.next(); + + // In `return` (and `break`/`continue`), the keywords with + // optional arguments, we eagerly look for a semicolon or the + // possibility to insert one. + + if (this.eat(types.semi) || this.insertSemicolon()) { node.argument = null; } + else { node.argument = this.parseExpression(); this.semicolon(); } + return this.finishNode(node, "ReturnStatement") +}; + +pp$1.parseSwitchStatement = function(node) { + var this$1 = this; + + this.next(); + node.discriminant = this.parseParenExpression(); + node.cases = []; + this.expect(types.braceL); + this.labels.push(switchLabel); + this.enterLexicalScope(); + + // Statements under must be grouped (by label) in SwitchCase + // nodes. `cur` is used to keep the node that we are currently + // adding statements to. + + var cur; + for (var sawDefault = false; this.type != types.braceR;) { + if (this$1.type === types._case || this$1.type === types._default) { + var isCase = this$1.type === types._case; + if (cur) { this$1.finishNode(cur, "SwitchCase"); } + node.cases.push(cur = this$1.startNode()); + cur.consequent = []; + this$1.next(); + if (isCase) { + cur.test = this$1.parseExpression(); + } else { + if (sawDefault) { this$1.raiseRecoverable(this$1.lastTokStart, "Multiple default clauses"); } + sawDefault = true; + cur.test = null; + } + this$1.expect(types.colon); + } else { + if (!cur) { this$1.unexpected(); } + cur.consequent.push(this$1.parseStatement(true)); + } + } + this.exitLexicalScope(); + if (cur) { this.finishNode(cur, "SwitchCase"); } + this.next(); // Closing brace + this.labels.pop(); + return this.finishNode(node, "SwitchStatement") +}; + +pp$1.parseThrowStatement = function(node) { + this.next(); + if (lineBreak.test(this.input.slice(this.lastTokEnd, this.start))) + { this.raise(this.lastTokEnd, "Illegal newline after throw"); } + node.argument = this.parseExpression(); + this.semicolon(); + return this.finishNode(node, "ThrowStatement") +}; + +// Reused empty array added for node fields that are always empty. + +var empty = []; + +pp$1.parseTryStatement = function(node) { + this.next(); + node.block = this.parseBlock(); + node.handler = null; + if (this.type === types._catch) { + var clause = this.startNode(); + this.next(); + this.expect(types.parenL); + clause.param = this.parseBindingAtom(); + this.enterLexicalScope(); + this.checkLVal(clause.param, "let"); + this.expect(types.parenR); + clause.body = this.parseBlock(false); + this.exitLexicalScope(); + node.handler = this.finishNode(clause, "CatchClause"); + } + node.finalizer = this.eat(types._finally) ? this.parseBlock() : null; + if (!node.handler && !node.finalizer) + { this.raise(node.start, "Missing catch or finally clause"); } + return this.finishNode(node, "TryStatement") +}; + +pp$1.parseVarStatement = function(node, kind) { + this.next(); + this.parseVar(node, false, kind); + this.semicolon(); + return this.finishNode(node, "VariableDeclaration") +}; + +pp$1.parseWhileStatement = function(node) { + this.next(); + node.test = this.parseParenExpression(); + this.labels.push(loopLabel); + node.body = this.parseStatement(false); + this.labels.pop(); + return this.finishNode(node, "WhileStatement") +}; + +pp$1.parseWithStatement = function(node) { + if (this.strict) { this.raise(this.start, "'with' in strict mode"); } + this.next(); + node.object = this.parseParenExpression(); + node.body = this.parseStatement(false); + return this.finishNode(node, "WithStatement") +}; + +pp$1.parseEmptyStatement = function(node) { + this.next(); + return this.finishNode(node, "EmptyStatement") +}; + +pp$1.parseLabeledStatement = function(node, maybeName, expr) { + var this$1 = this; + + for (var i$1 = 0, list = this$1.labels; i$1 < list.length; i$1 += 1) + { + var label = list[i$1]; + + if (label.name === maybeName) + { this$1.raise(expr.start, "Label '" + maybeName + "' is already declared"); + } } + var kind = this.type.isLoop ? "loop" : this.type === types._switch ? "switch" : null; + for (var i = this.labels.length - 1; i >= 0; i--) { + var label$1 = this$1.labels[i]; + if (label$1.statementStart == node.start) { + // Update information about previous labels on this node + label$1.statementStart = this$1.start; + label$1.kind = kind; + } else { break } + } + this.labels.push({name: maybeName, kind: kind, statementStart: this.start}); + node.body = this.parseStatement(true); + if (node.body.type == "ClassDeclaration" || + node.body.type == "VariableDeclaration" && node.body.kind != "var" || + node.body.type == "FunctionDeclaration" && (this.strict || node.body.generator)) + { this.raiseRecoverable(node.body.start, "Invalid labeled declaration"); } + this.labels.pop(); + node.label = expr; + return this.finishNode(node, "LabeledStatement") +}; + +pp$1.parseExpressionStatement = function(node, expr) { + node.expression = expr; + this.semicolon(); + return this.finishNode(node, "ExpressionStatement") +}; + +// Parse a semicolon-enclosed block of statements, handling `"use +// strict"` declarations when `allowStrict` is true (used for +// function bodies). + +pp$1.parseBlock = function(createNewLexicalScope) { + var this$1 = this; + if ( createNewLexicalScope === void 0 ) createNewLexicalScope = true; + + var node = this.startNode(); + node.body = []; + this.expect(types.braceL); + if (createNewLexicalScope) { + this.enterLexicalScope(); + } + while (!this.eat(types.braceR)) { + var stmt = this$1.parseStatement(true); + node.body.push(stmt); + } + if (createNewLexicalScope) { + this.exitLexicalScope(); + } + return this.finishNode(node, "BlockStatement") +}; + +// Parse a regular `for` loop. The disambiguation code in +// `parseStatement` will already have parsed the init statement or +// expression. + +pp$1.parseFor = function(node, init) { + node.init = init; + this.expect(types.semi); + node.test = this.type === types.semi ? null : this.parseExpression(); + this.expect(types.semi); + node.update = this.type === types.parenR ? null : this.parseExpression(); + this.expect(types.parenR); + this.exitLexicalScope(); + node.body = this.parseStatement(false); + this.labels.pop(); + return this.finishNode(node, "ForStatement") +}; + +// Parse a `for`/`in` and `for`/`of` loop, which are almost +// same from parser's perspective. + +pp$1.parseForIn = function(node, init) { + var type = this.type === types._in ? "ForInStatement" : "ForOfStatement"; + this.next(); + if (type == "ForInStatement") { + if (init.type === "AssignmentPattern" || + (init.type === "VariableDeclaration" && init.declarations[0].init != null && + (this.strict || init.declarations[0].id.type !== "Identifier"))) + { this.raise(init.start, "Invalid assignment in for-in loop head"); } + } + node.left = init; + node.right = type == "ForInStatement" ? this.parseExpression() : this.parseMaybeAssign(); + this.expect(types.parenR); + this.exitLexicalScope(); + node.body = this.parseStatement(false); + this.labels.pop(); + return this.finishNode(node, type) +}; + +// Parse a list of variable declarations. + +pp$1.parseVar = function(node, isFor, kind) { + var this$1 = this; + + node.declarations = []; + node.kind = kind; + for (;;) { + var decl = this$1.startNode(); + this$1.parseVarId(decl, kind); + if (this$1.eat(types.eq)) { + decl.init = this$1.parseMaybeAssign(isFor); + } else if (kind === "const" && !(this$1.type === types._in || (this$1.options.ecmaVersion >= 6 && this$1.isContextual("of")))) { + this$1.unexpected(); + } else if (decl.id.type != "Identifier" && !(isFor && (this$1.type === types._in || this$1.isContextual("of")))) { + this$1.raise(this$1.lastTokEnd, "Complex binding patterns require an initialization value"); + } else { + decl.init = null; + } + node.declarations.push(this$1.finishNode(decl, "VariableDeclarator")); + if (!this$1.eat(types.comma)) { break } + } + return node +}; + +pp$1.parseVarId = function(decl, kind) { + decl.id = this.parseBindingAtom(kind); + this.checkLVal(decl.id, kind, false); +}; + +// Parse a function declaration or literal (depending on the +// `isStatement` parameter). + +pp$1.parseFunction = function(node, isStatement, allowExpressionBody, isAsync) { + this.initFunction(node); + if (this.options.ecmaVersion >= 9 || this.options.ecmaVersion >= 6 && !isAsync) + { node.generator = this.eat(types.star); } + if (this.options.ecmaVersion >= 8) + { node.async = !!isAsync; } + + if (isStatement) { + node.id = isStatement === "nullableID" && this.type != types.name ? null : this.parseIdent(); + if (node.id) { + this.checkLVal(node.id, "var"); + } + } + + var oldInGen = this.inGenerator, oldInAsync = this.inAsync, + oldYieldPos = this.yieldPos, oldAwaitPos = this.awaitPos, oldInFunc = this.inFunction; + this.inGenerator = node.generator; + this.inAsync = node.async; + this.yieldPos = 0; + this.awaitPos = 0; + this.inFunction = true; + this.enterFunctionScope(); + + if (!isStatement) + { node.id = this.type == types.name ? this.parseIdent() : null; } + + this.parseFunctionParams(node); + this.parseFunctionBody(node, allowExpressionBody); + + this.inGenerator = oldInGen; + this.inAsync = oldInAsync; + this.yieldPos = oldYieldPos; + this.awaitPos = oldAwaitPos; + this.inFunction = oldInFunc; + return this.finishNode(node, isStatement ? "FunctionDeclaration" : "FunctionExpression") +}; + +pp$1.parseFunctionParams = function(node) { + this.expect(types.parenL); + node.params = this.parseBindingList(types.parenR, false, this.options.ecmaVersion >= 8); + this.checkYieldAwaitInDefaultParams(); +}; + +// Parse a class declaration or literal (depending on the +// `isStatement` parameter). + +pp$1.parseClass = function(node, isStatement) { + var this$1 = this; + + this.next(); + + this.parseClassId(node, isStatement); + this.parseClassSuper(node); + var classBody = this.startNode(); + var hadConstructor = false; + classBody.body = []; + this.expect(types.braceL); + while (!this.eat(types.braceR)) { + var member = this$1.parseClassMember(classBody); + if (member && member.type === "MethodDefinition" && member.kind === "constructor") { + if (hadConstructor) { this$1.raise(member.start, "Duplicate constructor in the same class"); } + hadConstructor = true; + } + } + node.body = this.finishNode(classBody, "ClassBody"); + return this.finishNode(node, isStatement ? "ClassDeclaration" : "ClassExpression") +}; + +pp$1.parseClassMember = function(classBody) { + var this$1 = this; + + if (this.eat(types.semi)) { return null } + + var method = this.startNode(); + var tryContextual = function (k, noLineBreak) { + if ( noLineBreak === void 0 ) noLineBreak = false; + + var start = this$1.start, startLoc = this$1.startLoc; + if (!this$1.eatContextual(k)) { return false } + if (this$1.type !== types.parenL && (!noLineBreak || !this$1.canInsertSemicolon())) { return true } + if (method.key) { this$1.unexpected(); } + method.computed = false; + method.key = this$1.startNodeAt(start, startLoc); + method.key.name = k; + this$1.finishNode(method.key, "Identifier"); + return false + }; + + method.kind = "method"; + method.static = tryContextual("static"); + var isGenerator = this.eat(types.star); + var isAsync = false; + if (!isGenerator) { + if (this.options.ecmaVersion >= 8 && tryContextual("async", true)) { + isAsync = true; + isGenerator = this.options.ecmaVersion >= 9 && this.eat(types.star); + } else if (tryContextual("get")) { + method.kind = "get"; + } else if (tryContextual("set")) { + method.kind = "set"; + } + } + if (!method.key) { this.parsePropertyName(method); } + var key = method.key; + if (!method.computed && !method.static && (key.type === "Identifier" && key.name === "constructor" || + key.type === "Literal" && key.value === "constructor")) { + if (method.kind !== "method") { this.raise(key.start, "Constructor can't have get/set modifier"); } + if (isGenerator) { this.raise(key.start, "Constructor can't be a generator"); } + if (isAsync) { this.raise(key.start, "Constructor can't be an async method"); } + method.kind = "constructor"; + } else if (method.static && key.type === "Identifier" && key.name === "prototype") { + this.raise(key.start, "Classes may not have a static property named prototype"); + } + this.parseClassMethod(classBody, method, isGenerator, isAsync); + if (method.kind === "get" && method.value.params.length !== 0) + { this.raiseRecoverable(method.value.start, "getter should have no params"); } + if (method.kind === "set" && method.value.params.length !== 1) + { this.raiseRecoverable(method.value.start, "setter should have exactly one param"); } + if (method.kind === "set" && method.value.params[0].type === "RestElement") + { this.raiseRecoverable(method.value.params[0].start, "Setter cannot use rest params"); } + return method +}; + +pp$1.parseClassMethod = function(classBody, method, isGenerator, isAsync) { + method.value = this.parseMethod(isGenerator, isAsync); + classBody.body.push(this.finishNode(method, "MethodDefinition")); +}; + +pp$1.parseClassId = function(node, isStatement) { + node.id = this.type === types.name ? this.parseIdent() : isStatement === true ? this.unexpected() : null; +}; + +pp$1.parseClassSuper = function(node) { + node.superClass = this.eat(types._extends) ? this.parseExprSubscripts() : null; +}; + +// Parses module export declaration. + +pp$1.parseExport = function(node, exports) { + var this$1 = this; + + this.next(); + // export * from '...' + if (this.eat(types.star)) { + this.expectContextual("from"); + if (this.type !== types.string) { this.unexpected(); } + node.source = this.parseExprAtom(); + this.semicolon(); + return this.finishNode(node, "ExportAllDeclaration") + } + if (this.eat(types._default)) { // export default ... + this.checkExport(exports, "default", this.lastTokStart); + var isAsync; + if (this.type === types._function || (isAsync = this.isAsyncFunction())) { + var fNode = this.startNode(); + this.next(); + if (isAsync) { this.next(); } + node.declaration = this.parseFunction(fNode, "nullableID", false, isAsync); + } else if (this.type === types._class) { + var cNode = this.startNode(); + node.declaration = this.parseClass(cNode, "nullableID"); + } else { + node.declaration = this.parseMaybeAssign(); + this.semicolon(); + } + return this.finishNode(node, "ExportDefaultDeclaration") + } + // export var|const|let|function|class ... + if (this.shouldParseExportStatement()) { + node.declaration = this.parseStatement(true); + if (node.declaration.type === "VariableDeclaration") + { this.checkVariableExport(exports, node.declaration.declarations); } + else + { this.checkExport(exports, node.declaration.id.name, node.declaration.id.start); } + node.specifiers = []; + node.source = null; + } else { // export { x, y as z } [from '...'] + node.declaration = null; + node.specifiers = this.parseExportSpecifiers(exports); + if (this.eatContextual("from")) { + if (this.type !== types.string) { this.unexpected(); } + node.source = this.parseExprAtom(); + } else { + // check for keywords used as local names + for (var i = 0, list = node.specifiers; i < list.length; i += 1) { + var spec = list[i]; + + this$1.checkUnreserved(spec.local); + } + + node.source = null; + } + this.semicolon(); + } + return this.finishNode(node, "ExportNamedDeclaration") +}; + +pp$1.checkExport = function(exports, name, pos) { + if (!exports) { return } + if (has(exports, name)) + { this.raiseRecoverable(pos, "Duplicate export '" + name + "'"); } + exports[name] = true; +}; + +pp$1.checkPatternExport = function(exports, pat) { + var this$1 = this; + + var type = pat.type; + if (type == "Identifier") + { this.checkExport(exports, pat.name, pat.start); } + else if (type == "ObjectPattern") + { for (var i = 0, list = pat.properties; i < list.length; i += 1) + { + var prop = list[i]; + + this$1.checkPatternExport(exports, prop); + } } + else if (type == "ArrayPattern") + { for (var i$1 = 0, list$1 = pat.elements; i$1 < list$1.length; i$1 += 1) { + var elt = list$1[i$1]; + + if (elt) { this$1.checkPatternExport(exports, elt); } + } } + else if (type == "Property") + { this.checkPatternExport(exports, pat.value); } + else if (type == "AssignmentPattern") + { this.checkPatternExport(exports, pat.left); } + else if (type == "RestElement") + { this.checkPatternExport(exports, pat.argument); } + else if (type == "ParenthesizedExpression") + { this.checkPatternExport(exports, pat.expression); } +}; + +pp$1.checkVariableExport = function(exports, decls) { + var this$1 = this; + + if (!exports) { return } + for (var i = 0, list = decls; i < list.length; i += 1) + { + var decl = list[i]; + + this$1.checkPatternExport(exports, decl.id); + } +}; + +pp$1.shouldParseExportStatement = function() { + return this.type.keyword === "var" || + this.type.keyword === "const" || + this.type.keyword === "class" || + this.type.keyword === "function" || + this.isLet() || + this.isAsyncFunction() +}; + +// Parses a comma-separated list of module exports. + +pp$1.parseExportSpecifiers = function(exports) { + var this$1 = this; + + var nodes = [], first = true; + // export { x, y as z } [from '...'] + this.expect(types.braceL); + while (!this.eat(types.braceR)) { + if (!first) { + this$1.expect(types.comma); + if (this$1.afterTrailingComma(types.braceR)) { break } + } else { first = false; } + + var node = this$1.startNode(); + node.local = this$1.parseIdent(true); + node.exported = this$1.eatContextual("as") ? this$1.parseIdent(true) : node.local; + this$1.checkExport(exports, node.exported.name, node.exported.start); + nodes.push(this$1.finishNode(node, "ExportSpecifier")); + } + return nodes +}; + +// Parses import declaration. + +pp$1.parseImport = function(node) { + this.next(); + // import '...' + if (this.type === types.string) { + node.specifiers = empty; + node.source = this.parseExprAtom(); + } else { + node.specifiers = this.parseImportSpecifiers(); + this.expectContextual("from"); + node.source = this.type === types.string ? this.parseExprAtom() : this.unexpected(); + } + this.semicolon(); + return this.finishNode(node, "ImportDeclaration") +}; + +// Parses a comma-separated list of module imports. + +pp$1.parseImportSpecifiers = function() { + var this$1 = this; + + var nodes = [], first = true; + if (this.type === types.name) { + // import defaultObj, { x, y as z } from '...' + var node = this.startNode(); + node.local = this.parseIdent(); + this.checkLVal(node.local, "let"); + nodes.push(this.finishNode(node, "ImportDefaultSpecifier")); + if (!this.eat(types.comma)) { return nodes } + } + if (this.type === types.star) { + var node$1 = this.startNode(); + this.next(); + this.expectContextual("as"); + node$1.local = this.parseIdent(); + this.checkLVal(node$1.local, "let"); + nodes.push(this.finishNode(node$1, "ImportNamespaceSpecifier")); + return nodes + } + this.expect(types.braceL); + while (!this.eat(types.braceR)) { + if (!first) { + this$1.expect(types.comma); + if (this$1.afterTrailingComma(types.braceR)) { break } + } else { first = false; } + + var node$2 = this$1.startNode(); + node$2.imported = this$1.parseIdent(true); + if (this$1.eatContextual("as")) { + node$2.local = this$1.parseIdent(); + } else { + this$1.checkUnreserved(node$2.imported); + node$2.local = node$2.imported; + } + this$1.checkLVal(node$2.local, "let"); + nodes.push(this$1.finishNode(node$2, "ImportSpecifier")); + } + return nodes +}; + +// Set `ExpressionStatement#directive` property for directive prologues. +pp$1.adaptDirectivePrologue = function(statements) { + for (var i = 0; i < statements.length && this.isDirectiveCandidate(statements[i]); ++i) { + statements[i].directive = statements[i].expression.raw.slice(1, -1); + } +}; +pp$1.isDirectiveCandidate = function(statement) { + return ( + statement.type === "ExpressionStatement" && + statement.expression.type === "Literal" && + typeof statement.expression.value === "string" && + // Reject parenthesized strings. + (this.input[statement.start] === "\"" || this.input[statement.start] === "'") + ) +}; + +var pp$2 = Parser.prototype; + +// Convert existing expression atom to assignable pattern +// if possible. + +pp$2.toAssignable = function(node, isBinding, refDestructuringErrors) { + var this$1 = this; + + if (this.options.ecmaVersion >= 6 && node) { + switch (node.type) { + case "Identifier": + if (this.inAsync && node.name === "await") + { this.raise(node.start, "Can not use 'await' as identifier inside an async function"); } + break + + case "ObjectPattern": + case "ArrayPattern": + case "RestElement": + break + + case "ObjectExpression": + node.type = "ObjectPattern"; + if (refDestructuringErrors) { this.checkPatternErrors(refDestructuringErrors, true); } + for (var i = 0, list = node.properties; i < list.length; i += 1) { + var prop = list[i]; + + this$1.toAssignable(prop, isBinding); + // Early error: + // AssignmentRestProperty[Yield, Await] : + // `...` DestructuringAssignmentTarget[Yield, Await] + // + // It is a Syntax Error if |DestructuringAssignmentTarget| is an |ArrayLiteral| or an |ObjectLiteral|. + if ( + prop.type === "RestElement" && + (prop.argument.type === "ArrayPattern" || prop.argument.type === "ObjectPattern") + ) { + this$1.raise(prop.argument.start, "Unexpected token"); + } + } + break + + case "Property": + // AssignmentProperty has type == "Property" + if (node.kind !== "init") { this.raise(node.key.start, "Object pattern can't contain getter or setter"); } + this.toAssignable(node.value, isBinding); + break + + case "ArrayExpression": + node.type = "ArrayPattern"; + if (refDestructuringErrors) { this.checkPatternErrors(refDestructuringErrors, true); } + this.toAssignableList(node.elements, isBinding); + break + + case "SpreadElement": + node.type = "RestElement"; + this.toAssignable(node.argument, isBinding); + if (node.argument.type === "AssignmentPattern") + { this.raise(node.argument.start, "Rest elements cannot have a default value"); } + break + + case "AssignmentExpression": + if (node.operator !== "=") { this.raise(node.left.end, "Only '=' operator can be used for specifying default value."); } + node.type = "AssignmentPattern"; + delete node.operator; + this.toAssignable(node.left, isBinding); + // falls through to AssignmentPattern + + case "AssignmentPattern": + break + + case "ParenthesizedExpression": + this.toAssignable(node.expression, isBinding); + break + + case "MemberExpression": + if (!isBinding) { break } + + default: + this.raise(node.start, "Assigning to rvalue"); + } + } else if (refDestructuringErrors) { this.checkPatternErrors(refDestructuringErrors, true); } + return node +}; + +// Convert list of expression atoms to binding list. + +pp$2.toAssignableList = function(exprList, isBinding) { + var this$1 = this; + + var end = exprList.length; + for (var i = 0; i < end; i++) { + var elt = exprList[i]; + if (elt) { this$1.toAssignable(elt, isBinding); } + } + if (end) { + var last = exprList[end - 1]; + if (this.options.ecmaVersion === 6 && isBinding && last && last.type === "RestElement" && last.argument.type !== "Identifier") + { this.unexpected(last.argument.start); } + } + return exprList +}; + +// Parses spread element. + +pp$2.parseSpread = function(refDestructuringErrors) { + var node = this.startNode(); + this.next(); + node.argument = this.parseMaybeAssign(false, refDestructuringErrors); + return this.finishNode(node, "SpreadElement") +}; + +pp$2.parseRestBinding = function() { + var node = this.startNode(); + this.next(); + + // RestElement inside of a function parameter must be an identifier + if (this.options.ecmaVersion === 6 && this.type !== types.name) + { this.unexpected(); } + + node.argument = this.parseBindingAtom(); + + return this.finishNode(node, "RestElement") +}; + +// Parses lvalue (assignable) atom. + +pp$2.parseBindingAtom = function() { + if (this.options.ecmaVersion >= 6) { + switch (this.type) { + case types.bracketL: + var node = this.startNode(); + this.next(); + node.elements = this.parseBindingList(types.bracketR, true, true); + return this.finishNode(node, "ArrayPattern") + + case types.braceL: + return this.parseObj(true) + } + } + return this.parseIdent() +}; + +pp$2.parseBindingList = function(close, allowEmpty, allowTrailingComma) { + var this$1 = this; + + var elts = [], first = true; + while (!this.eat(close)) { + if (first) { first = false; } + else { this$1.expect(types.comma); } + if (allowEmpty && this$1.type === types.comma) { + elts.push(null); + } else if (allowTrailingComma && this$1.afterTrailingComma(close)) { + break + } else if (this$1.type === types.ellipsis) { + var rest = this$1.parseRestBinding(); + this$1.parseBindingListItem(rest); + elts.push(rest); + if (this$1.type === types.comma) { this$1.raise(this$1.start, "Comma is not permitted after the rest element"); } + this$1.expect(close); + break + } else { + var elem = this$1.parseMaybeDefault(this$1.start, this$1.startLoc); + this$1.parseBindingListItem(elem); + elts.push(elem); + } + } + return elts +}; + +pp$2.parseBindingListItem = function(param) { + return param +}; + +// Parses assignment pattern around given atom if possible. + +pp$2.parseMaybeDefault = function(startPos, startLoc, left) { + left = left || this.parseBindingAtom(); + if (this.options.ecmaVersion < 6 || !this.eat(types.eq)) { return left } + var node = this.startNodeAt(startPos, startLoc); + node.left = left; + node.right = this.parseMaybeAssign(); + return this.finishNode(node, "AssignmentPattern") +}; + +// Verify that a node is an lval — something that can be assigned +// to. +// bindingType can be either: +// 'var' indicating that the lval creates a 'var' binding +// 'let' indicating that the lval creates a lexical ('let' or 'const') binding +// 'none' indicating that the binding should be checked for illegal identifiers, but not for duplicate references + +pp$2.checkLVal = function(expr, bindingType, checkClashes) { + var this$1 = this; + + switch (expr.type) { + case "Identifier": + if (this.strict && this.reservedWordsStrictBind.test(expr.name)) + { this.raiseRecoverable(expr.start, (bindingType ? "Binding " : "Assigning to ") + expr.name + " in strict mode"); } + if (checkClashes) { + if (has(checkClashes, expr.name)) + { this.raiseRecoverable(expr.start, "Argument name clash"); } + checkClashes[expr.name] = true; + } + if (bindingType && bindingType !== "none") { + if ( + bindingType === "var" && !this.canDeclareVarName(expr.name) || + bindingType !== "var" && !this.canDeclareLexicalName(expr.name) + ) { + this.raiseRecoverable(expr.start, ("Identifier '" + (expr.name) + "' has already been declared")); + } + if (bindingType === "var") { + this.declareVarName(expr.name); + } else { + this.declareLexicalName(expr.name); + } + } + break + + case "MemberExpression": + if (bindingType) { this.raiseRecoverable(expr.start, "Binding member expression"); } + break + + case "ObjectPattern": + for (var i = 0, list = expr.properties; i < list.length; i += 1) + { + var prop = list[i]; + + this$1.checkLVal(prop, bindingType, checkClashes); + } + break + + case "Property": + // AssignmentProperty has type == "Property" + this.checkLVal(expr.value, bindingType, checkClashes); + break + + case "ArrayPattern": + for (var i$1 = 0, list$1 = expr.elements; i$1 < list$1.length; i$1 += 1) { + var elem = list$1[i$1]; + + if (elem) { this$1.checkLVal(elem, bindingType, checkClashes); } + } + break + + case "AssignmentPattern": + this.checkLVal(expr.left, bindingType, checkClashes); + break + + case "RestElement": + this.checkLVal(expr.argument, bindingType, checkClashes); + break + + case "ParenthesizedExpression": + this.checkLVal(expr.expression, bindingType, checkClashes); + break + + default: + this.raise(expr.start, (bindingType ? "Binding" : "Assigning to") + " rvalue"); + } +}; + +// A recursive descent parser operates by defining functions for all +// syntactic elements, and recursively calling those, each function +// advancing the input stream and returning an AST node. Precedence +// of constructs (for example, the fact that `!x[1]` means `!(x[1])` +// instead of `(!x)[1]` is handled by the fact that the parser +// function that parses unary prefix operators is called first, and +// in turn calls the function that parses `[]` subscripts — that +// way, it'll receive the node for `x[1]` already parsed, and wraps +// *that* in the unary operator node. +// +// Acorn uses an [operator precedence parser][opp] to handle binary +// operator precedence, because it is much more compact than using +// the technique outlined above, which uses different, nesting +// functions to specify precedence, for all of the ten binary +// precedence levels that JavaScript defines. +// +// [opp]: http://en.wikipedia.org/wiki/Operator-precedence_parser + +var pp$3 = Parser.prototype; + +// Check if property name clashes with already added. +// Object/class getters and setters are not allowed to clash — +// either with each other or with an init property — and in +// strict mode, init properties are also not allowed to be repeated. + +pp$3.checkPropClash = function(prop, propHash, refDestructuringErrors) { + if (this.options.ecmaVersion >= 9 && prop.type === "SpreadElement") + { return } + if (this.options.ecmaVersion >= 6 && (prop.computed || prop.method || prop.shorthand)) + { return } + var key = prop.key; + var name; + switch (key.type) { + case "Identifier": name = key.name; break + case "Literal": name = String(key.value); break + default: return + } + var kind = prop.kind; + if (this.options.ecmaVersion >= 6) { + if (name === "__proto__" && kind === "init") { + if (propHash.proto) { + if (refDestructuringErrors && refDestructuringErrors.doubleProto < 0) { refDestructuringErrors.doubleProto = key.start; } + // Backwards-compat kludge. Can be removed in version 6.0 + else { this.raiseRecoverable(key.start, "Redefinition of __proto__ property"); } + } + propHash.proto = true; + } + return + } + name = "$" + name; + var other = propHash[name]; + if (other) { + var redefinition; + if (kind === "init") { + redefinition = this.strict && other.init || other.get || other.set; + } else { + redefinition = other.init || other[kind]; + } + if (redefinition) + { this.raiseRecoverable(key.start, "Redefinition of property"); } + } else { + other = propHash[name] = { + init: false, + get: false, + set: false + }; + } + other[kind] = true; +}; + +// ### Expression parsing + +// These nest, from the most general expression type at the top to +// 'atomic', nondivisible expression types at the bottom. Most of +// the functions will simply let the function(s) below them parse, +// and, *if* the syntactic construct they handle is present, wrap +// the AST node that the inner parser gave them in another node. + +// Parse a full expression. The optional arguments are used to +// forbid the `in` operator (in for loops initalization expressions) +// and provide reference for storing '=' operator inside shorthand +// property assignment in contexts where both object expression +// and object pattern might appear (so it's possible to raise +// delayed syntax error at correct position). + +pp$3.parseExpression = function(noIn, refDestructuringErrors) { + var this$1 = this; + + var startPos = this.start, startLoc = this.startLoc; + var expr = this.parseMaybeAssign(noIn, refDestructuringErrors); + if (this.type === types.comma) { + var node = this.startNodeAt(startPos, startLoc); + node.expressions = [expr]; + while (this.eat(types.comma)) { node.expressions.push(this$1.parseMaybeAssign(noIn, refDestructuringErrors)); } + return this.finishNode(node, "SequenceExpression") + } + return expr +}; + +// Parse an assignment expression. This includes applications of +// operators like `+=`. + +pp$3.parseMaybeAssign = function(noIn, refDestructuringErrors, afterLeftParse) { + if (this.inGenerator && this.isContextual("yield")) { return this.parseYield() } + + var ownDestructuringErrors = false, oldParenAssign = -1, oldTrailingComma = -1; + if (refDestructuringErrors) { + oldParenAssign = refDestructuringErrors.parenthesizedAssign; + oldTrailingComma = refDestructuringErrors.trailingComma; + refDestructuringErrors.parenthesizedAssign = refDestructuringErrors.trailingComma = -1; + } else { + refDestructuringErrors = new DestructuringErrors; + ownDestructuringErrors = true; + } + + var startPos = this.start, startLoc = this.startLoc; + if (this.type == types.parenL || this.type == types.name) + { this.potentialArrowAt = this.start; } + var left = this.parseMaybeConditional(noIn, refDestructuringErrors); + if (afterLeftParse) { left = afterLeftParse.call(this, left, startPos, startLoc); } + if (this.type.isAssign) { + var node = this.startNodeAt(startPos, startLoc); + node.operator = this.value; + node.left = this.type === types.eq ? this.toAssignable(left, false, refDestructuringErrors) : left; + if (!ownDestructuringErrors) { DestructuringErrors.call(refDestructuringErrors); } + refDestructuringErrors.shorthandAssign = -1; // reset because shorthand default was used correctly + this.checkLVal(left); + this.next(); + node.right = this.parseMaybeAssign(noIn); + return this.finishNode(node, "AssignmentExpression") + } else { + if (ownDestructuringErrors) { this.checkExpressionErrors(refDestructuringErrors, true); } + } + if (oldParenAssign > -1) { refDestructuringErrors.parenthesizedAssign = oldParenAssign; } + if (oldTrailingComma > -1) { refDestructuringErrors.trailingComma = oldTrailingComma; } + return left +}; + +// Parse a ternary conditional (`?:`) operator. + +pp$3.parseMaybeConditional = function(noIn, refDestructuringErrors) { + var startPos = this.start, startLoc = this.startLoc; + var expr = this.parseExprOps(noIn, refDestructuringErrors); + if (this.checkExpressionErrors(refDestructuringErrors)) { return expr } + if (this.eat(types.question)) { + var node = this.startNodeAt(startPos, startLoc); + node.test = expr; + node.consequent = this.parseMaybeAssign(); + this.expect(types.colon); + node.alternate = this.parseMaybeAssign(noIn); + return this.finishNode(node, "ConditionalExpression") + } + return expr +}; + +// Start the precedence parser. + +pp$3.parseExprOps = function(noIn, refDestructuringErrors) { + var startPos = this.start, startLoc = this.startLoc; + var expr = this.parseMaybeUnary(refDestructuringErrors, false); + if (this.checkExpressionErrors(refDestructuringErrors)) { return expr } + return expr.start == startPos && expr.type === "ArrowFunctionExpression" ? expr : this.parseExprOp(expr, startPos, startLoc, -1, noIn) +}; + +// Parse binary operators with the operator precedence parsing +// algorithm. `left` is the left-hand side of the operator. +// `minPrec` provides context that allows the function to stop and +// defer further parser to one of its callers when it encounters an +// operator that has a lower precedence than the set it is parsing. + +pp$3.parseExprOp = function(left, leftStartPos, leftStartLoc, minPrec, noIn) { + var prec = this.type.binop; + if (prec != null && (!noIn || this.type !== types._in)) { + if (prec > minPrec) { + var logical = this.type === types.logicalOR || this.type === types.logicalAND; + var op = this.value; + this.next(); + var startPos = this.start, startLoc = this.startLoc; + var right = this.parseExprOp(this.parseMaybeUnary(null, false), startPos, startLoc, prec, noIn); + var node = this.buildBinary(leftStartPos, leftStartLoc, left, right, op, logical); + return this.parseExprOp(node, leftStartPos, leftStartLoc, minPrec, noIn) + } + } + return left +}; + +pp$3.buildBinary = function(startPos, startLoc, left, right, op, logical) { + var node = this.startNodeAt(startPos, startLoc); + node.left = left; + node.operator = op; + node.right = right; + return this.finishNode(node, logical ? "LogicalExpression" : "BinaryExpression") +}; + +// Parse unary operators, both prefix and postfix. + +pp$3.parseMaybeUnary = function(refDestructuringErrors, sawUnary) { + var this$1 = this; + + var startPos = this.start, startLoc = this.startLoc, expr; + if (this.inAsync && this.isContextual("await")) { + expr = this.parseAwait(); + sawUnary = true; + } else if (this.type.prefix) { + var node = this.startNode(), update = this.type === types.incDec; + node.operator = this.value; + node.prefix = true; + this.next(); + node.argument = this.parseMaybeUnary(null, true); + this.checkExpressionErrors(refDestructuringErrors, true); + if (update) { this.checkLVal(node.argument); } + else if (this.strict && node.operator === "delete" && + node.argument.type === "Identifier") + { this.raiseRecoverable(node.start, "Deleting local variable in strict mode"); } + else { sawUnary = true; } + expr = this.finishNode(node, update ? "UpdateExpression" : "UnaryExpression"); + } else { + expr = this.parseExprSubscripts(refDestructuringErrors); + if (this.checkExpressionErrors(refDestructuringErrors)) { return expr } + while (this.type.postfix && !this.canInsertSemicolon()) { + var node$1 = this$1.startNodeAt(startPos, startLoc); + node$1.operator = this$1.value; + node$1.prefix = false; + node$1.argument = expr; + this$1.checkLVal(expr); + this$1.next(); + expr = this$1.finishNode(node$1, "UpdateExpression"); + } + } + + if (!sawUnary && this.eat(types.starstar)) + { return this.buildBinary(startPos, startLoc, expr, this.parseMaybeUnary(null, false), "**", false) } + else + { return expr } +}; + +// Parse call, dot, and `[]`-subscript expressions. + +pp$3.parseExprSubscripts = function(refDestructuringErrors) { + var startPos = this.start, startLoc = this.startLoc; + var expr = this.parseExprAtom(refDestructuringErrors); + var skipArrowSubscripts = expr.type === "ArrowFunctionExpression" && this.input.slice(this.lastTokStart, this.lastTokEnd) !== ")"; + if (this.checkExpressionErrors(refDestructuringErrors) || skipArrowSubscripts) { return expr } + var result = this.parseSubscripts(expr, startPos, startLoc); + if (refDestructuringErrors && result.type === "MemberExpression") { + if (refDestructuringErrors.parenthesizedAssign >= result.start) { refDestructuringErrors.parenthesizedAssign = -1; } + if (refDestructuringErrors.parenthesizedBind >= result.start) { refDestructuringErrors.parenthesizedBind = -1; } + } + return result +}; + +pp$3.parseSubscripts = function(base, startPos, startLoc, noCalls) { + var this$1 = this; + + var maybeAsyncArrow = this.options.ecmaVersion >= 8 && base.type === "Identifier" && base.name === "async" && + this.lastTokEnd == base.end && !this.canInsertSemicolon() && this.input.slice(base.start, base.end) === "async"; + for (var computed = (void 0);;) { + if ((computed = this$1.eat(types.bracketL)) || this$1.eat(types.dot)) { + var node = this$1.startNodeAt(startPos, startLoc); + node.object = base; + node.property = computed ? this$1.parseExpression() : this$1.parseIdent(true); + node.computed = !!computed; + if (computed) { this$1.expect(types.bracketR); } + base = this$1.finishNode(node, "MemberExpression"); + } else if (!noCalls && this$1.eat(types.parenL)) { + var refDestructuringErrors = new DestructuringErrors, oldYieldPos = this$1.yieldPos, oldAwaitPos = this$1.awaitPos; + this$1.yieldPos = 0; + this$1.awaitPos = 0; + var exprList = this$1.parseExprList(types.parenR, this$1.options.ecmaVersion >= 8, false, refDestructuringErrors); + if (maybeAsyncArrow && !this$1.canInsertSemicolon() && this$1.eat(types.arrow)) { + this$1.checkPatternErrors(refDestructuringErrors, false); + this$1.checkYieldAwaitInDefaultParams(); + this$1.yieldPos = oldYieldPos; + this$1.awaitPos = oldAwaitPos; + return this$1.parseArrowExpression(this$1.startNodeAt(startPos, startLoc), exprList, true) + } + this$1.checkExpressionErrors(refDestructuringErrors, true); + this$1.yieldPos = oldYieldPos || this$1.yieldPos; + this$1.awaitPos = oldAwaitPos || this$1.awaitPos; + var node$1 = this$1.startNodeAt(startPos, startLoc); + node$1.callee = base; + node$1.arguments = exprList; + base = this$1.finishNode(node$1, "CallExpression"); + } else if (this$1.type === types.backQuote) { + var node$2 = this$1.startNodeAt(startPos, startLoc); + node$2.tag = base; + node$2.quasi = this$1.parseTemplate({isTagged: true}); + base = this$1.finishNode(node$2, "TaggedTemplateExpression"); + } else { + return base + } + } +}; + +// Parse an atomic expression — either a single token that is an +// expression, an expression started by a keyword like `function` or +// `new`, or an expression wrapped in punctuation like `()`, `[]`, +// or `{}`. + +pp$3.parseExprAtom = function(refDestructuringErrors) { + var node, canBeArrow = this.potentialArrowAt == this.start; + switch (this.type) { + case types._super: + if (!this.inFunction) + { this.raise(this.start, "'super' outside of function or class"); } + node = this.startNode(); + this.next(); + // The `super` keyword can appear at below: + // SuperProperty: + // super [ Expression ] + // super . IdentifierName + // SuperCall: + // super Arguments + if (this.type !== types.dot && this.type !== types.bracketL && this.type !== types.parenL) + { this.unexpected(); } + return this.finishNode(node, "Super") + + case types._this: + node = this.startNode(); + this.next(); + return this.finishNode(node, "ThisExpression") + + case types.name: + var startPos = this.start, startLoc = this.startLoc, containsEsc = this.containsEsc; + var id = this.parseIdent(this.type !== types.name); + if (this.options.ecmaVersion >= 8 && !containsEsc && id.name === "async" && !this.canInsertSemicolon() && this.eat(types._function)) + { return this.parseFunction(this.startNodeAt(startPos, startLoc), false, false, true) } + if (canBeArrow && !this.canInsertSemicolon()) { + if (this.eat(types.arrow)) + { return this.parseArrowExpression(this.startNodeAt(startPos, startLoc), [id], false) } + if (this.options.ecmaVersion >= 8 && id.name === "async" && this.type === types.name && !containsEsc) { + id = this.parseIdent(); + if (this.canInsertSemicolon() || !this.eat(types.arrow)) + { this.unexpected(); } + return this.parseArrowExpression(this.startNodeAt(startPos, startLoc), [id], true) + } + } + return id + + case types.regexp: + var value = this.value; + node = this.parseLiteral(value.value); + node.regex = {pattern: value.pattern, flags: value.flags}; + return node + + case types.num: case types.string: + return this.parseLiteral(this.value) + + case types._null: case types._true: case types._false: + node = this.startNode(); + node.value = this.type === types._null ? null : this.type === types._true; + node.raw = this.type.keyword; + this.next(); + return this.finishNode(node, "Literal") + + case types.parenL: + var start = this.start, expr = this.parseParenAndDistinguishExpression(canBeArrow); + if (refDestructuringErrors) { + if (refDestructuringErrors.parenthesizedAssign < 0 && !this.isSimpleAssignTarget(expr)) + { refDestructuringErrors.parenthesizedAssign = start; } + if (refDestructuringErrors.parenthesizedBind < 0) + { refDestructuringErrors.parenthesizedBind = start; } + } + return expr + + case types.bracketL: + node = this.startNode(); + this.next(); + node.elements = this.parseExprList(types.bracketR, true, true, refDestructuringErrors); + return this.finishNode(node, "ArrayExpression") + + case types.braceL: + return this.parseObj(false, refDestructuringErrors) + + case types._function: + node = this.startNode(); + this.next(); + return this.parseFunction(node, false) + + case types._class: + return this.parseClass(this.startNode(), false) + + case types._new: + return this.parseNew() + + case types.backQuote: + return this.parseTemplate() + + default: + this.unexpected(); + } +}; + +pp$3.parseLiteral = function(value) { + var node = this.startNode(); + node.value = value; + node.raw = this.input.slice(this.start, this.end); + this.next(); + return this.finishNode(node, "Literal") +}; + +pp$3.parseParenExpression = function() { + this.expect(types.parenL); + var val = this.parseExpression(); + this.expect(types.parenR); + return val +}; + +pp$3.parseParenAndDistinguishExpression = function(canBeArrow) { + var this$1 = this; + + var startPos = this.start, startLoc = this.startLoc, val, allowTrailingComma = this.options.ecmaVersion >= 8; + if (this.options.ecmaVersion >= 6) { + this.next(); + + var innerStartPos = this.start, innerStartLoc = this.startLoc; + var exprList = [], first = true, lastIsComma = false; + var refDestructuringErrors = new DestructuringErrors, oldYieldPos = this.yieldPos, oldAwaitPos = this.awaitPos, spreadStart; + this.yieldPos = 0; + this.awaitPos = 0; + while (this.type !== types.parenR) { + first ? first = false : this$1.expect(types.comma); + if (allowTrailingComma && this$1.afterTrailingComma(types.parenR, true)) { + lastIsComma = true; + break + } else if (this$1.type === types.ellipsis) { + spreadStart = this$1.start; + exprList.push(this$1.parseParenItem(this$1.parseRestBinding())); + if (this$1.type === types.comma) { this$1.raise(this$1.start, "Comma is not permitted after the rest element"); } + break + } else { + exprList.push(this$1.parseMaybeAssign(false, refDestructuringErrors, this$1.parseParenItem)); + } + } + var innerEndPos = this.start, innerEndLoc = this.startLoc; + this.expect(types.parenR); + + if (canBeArrow && !this.canInsertSemicolon() && this.eat(types.arrow)) { + this.checkPatternErrors(refDestructuringErrors, false); + this.checkYieldAwaitInDefaultParams(); + this.yieldPos = oldYieldPos; + this.awaitPos = oldAwaitPos; + return this.parseParenArrowList(startPos, startLoc, exprList) + } + + if (!exprList.length || lastIsComma) { this.unexpected(this.lastTokStart); } + if (spreadStart) { this.unexpected(spreadStart); } + this.checkExpressionErrors(refDestructuringErrors, true); + this.yieldPos = oldYieldPos || this.yieldPos; + this.awaitPos = oldAwaitPos || this.awaitPos; + + if (exprList.length > 1) { + val = this.startNodeAt(innerStartPos, innerStartLoc); + val.expressions = exprList; + this.finishNodeAt(val, "SequenceExpression", innerEndPos, innerEndLoc); + } else { + val = exprList[0]; + } + } else { + val = this.parseParenExpression(); + } + + if (this.options.preserveParens) { + var par = this.startNodeAt(startPos, startLoc); + par.expression = val; + return this.finishNode(par, "ParenthesizedExpression") + } else { + return val + } +}; + +pp$3.parseParenItem = function(item) { + return item +}; + +pp$3.parseParenArrowList = function(startPos, startLoc, exprList) { + return this.parseArrowExpression(this.startNodeAt(startPos, startLoc), exprList) +}; + +// New's precedence is slightly tricky. It must allow its argument to +// be a `[]` or dot subscript expression, but not a call — at least, +// not without wrapping it in parentheses. Thus, it uses the noCalls +// argument to parseSubscripts to prevent it from consuming the +// argument list. + +var empty$1 = []; + +pp$3.parseNew = function() { + var node = this.startNode(); + var meta = this.parseIdent(true); + if (this.options.ecmaVersion >= 6 && this.eat(types.dot)) { + node.meta = meta; + var containsEsc = this.containsEsc; + node.property = this.parseIdent(true); + if (node.property.name !== "target" || containsEsc) + { this.raiseRecoverable(node.property.start, "The only valid meta property for new is new.target"); } + if (!this.inFunction) + { this.raiseRecoverable(node.start, "new.target can only be used in functions"); } + return this.finishNode(node, "MetaProperty") + } + var startPos = this.start, startLoc = this.startLoc; + node.callee = this.parseSubscripts(this.parseExprAtom(), startPos, startLoc, true); + if (this.eat(types.parenL)) { node.arguments = this.parseExprList(types.parenR, this.options.ecmaVersion >= 8, false); } + else { node.arguments = empty$1; } + return this.finishNode(node, "NewExpression") +}; + +// Parse template expression. + +pp$3.parseTemplateElement = function(ref) { + var isTagged = ref.isTagged; + + var elem = this.startNode(); + if (this.type === types.invalidTemplate) { + if (!isTagged) { + this.raiseRecoverable(this.start, "Bad escape sequence in untagged template literal"); + } + elem.value = { + raw: this.value, + cooked: null + }; + } else { + elem.value = { + raw: this.input.slice(this.start, this.end).replace(/\r\n?/g, "\n"), + cooked: this.value + }; + } + this.next(); + elem.tail = this.type === types.backQuote; + return this.finishNode(elem, "TemplateElement") +}; + +pp$3.parseTemplate = function(ref) { + var this$1 = this; + if ( ref === void 0 ) ref = {}; + var isTagged = ref.isTagged; if ( isTagged === void 0 ) isTagged = false; + + var node = this.startNode(); + this.next(); + node.expressions = []; + var curElt = this.parseTemplateElement({isTagged: isTagged}); + node.quasis = [curElt]; + while (!curElt.tail) { + this$1.expect(types.dollarBraceL); + node.expressions.push(this$1.parseExpression()); + this$1.expect(types.braceR); + node.quasis.push(curElt = this$1.parseTemplateElement({isTagged: isTagged})); + } + this.next(); + return this.finishNode(node, "TemplateLiteral") +}; + +pp$3.isAsyncProp = function(prop) { + return !prop.computed && prop.key.type === "Identifier" && prop.key.name === "async" && + (this.type === types.name || this.type === types.num || this.type === types.string || this.type === types.bracketL || this.type.keyword || (this.options.ecmaVersion >= 9 && this.type === types.star)) && + !lineBreak.test(this.input.slice(this.lastTokEnd, this.start)) +}; + +// Parse an object literal or binding pattern. + +pp$3.parseObj = function(isPattern, refDestructuringErrors) { + var this$1 = this; + + var node = this.startNode(), first = true, propHash = {}; + node.properties = []; + this.next(); + while (!this.eat(types.braceR)) { + if (!first) { + this$1.expect(types.comma); + if (this$1.afterTrailingComma(types.braceR)) { break } + } else { first = false; } + + var prop = this$1.parseProperty(isPattern, refDestructuringErrors); + if (!isPattern) { this$1.checkPropClash(prop, propHash, refDestructuringErrors); } + node.properties.push(prop); + } + return this.finishNode(node, isPattern ? "ObjectPattern" : "ObjectExpression") +}; + +pp$3.parseProperty = function(isPattern, refDestructuringErrors) { + var prop = this.startNode(), isGenerator, isAsync, startPos, startLoc; + if (this.options.ecmaVersion >= 9 && this.eat(types.ellipsis)) { + if (isPattern) { + prop.argument = this.parseIdent(false); + if (this.type === types.comma) { + this.raise(this.start, "Comma is not permitted after the rest element"); + } + return this.finishNode(prop, "RestElement") + } + // To disallow parenthesized identifier via `this.toAssignable()`. + if (this.type === types.parenL && refDestructuringErrors) { + if (refDestructuringErrors.parenthesizedAssign < 0) { + refDestructuringErrors.parenthesizedAssign = this.start; + } + if (refDestructuringErrors.parenthesizedBind < 0) { + refDestructuringErrors.parenthesizedBind = this.start; + } + } + // Parse argument. + prop.argument = this.parseMaybeAssign(false, refDestructuringErrors); + // To disallow trailing comma via `this.toAssignable()`. + if (this.type === types.comma && refDestructuringErrors && refDestructuringErrors.trailingComma < 0) { + refDestructuringErrors.trailingComma = this.start; + } + // Finish + return this.finishNode(prop, "SpreadElement") + } + if (this.options.ecmaVersion >= 6) { + prop.method = false; + prop.shorthand = false; + if (isPattern || refDestructuringErrors) { + startPos = this.start; + startLoc = this.startLoc; + } + if (!isPattern) + { isGenerator = this.eat(types.star); } + } + var containsEsc = this.containsEsc; + this.parsePropertyName(prop); + if (!isPattern && !containsEsc && this.options.ecmaVersion >= 8 && !isGenerator && this.isAsyncProp(prop)) { + isAsync = true; + isGenerator = this.options.ecmaVersion >= 9 && this.eat(types.star); + this.parsePropertyName(prop, refDestructuringErrors); + } else { + isAsync = false; + } + this.parsePropertyValue(prop, isPattern, isGenerator, isAsync, startPos, startLoc, refDestructuringErrors, containsEsc); + return this.finishNode(prop, "Property") +}; + +pp$3.parsePropertyValue = function(prop, isPattern, isGenerator, isAsync, startPos, startLoc, refDestructuringErrors, containsEsc) { + if ((isGenerator || isAsync) && this.type === types.colon) + { this.unexpected(); } + + if (this.eat(types.colon)) { + prop.value = isPattern ? this.parseMaybeDefault(this.start, this.startLoc) : this.parseMaybeAssign(false, refDestructuringErrors); + prop.kind = "init"; + } else if (this.options.ecmaVersion >= 6 && this.type === types.parenL) { + if (isPattern) { this.unexpected(); } + prop.kind = "init"; + prop.method = true; + prop.value = this.parseMethod(isGenerator, isAsync); + } else if (!isPattern && !containsEsc && + this.options.ecmaVersion >= 5 && !prop.computed && prop.key.type === "Identifier" && + (prop.key.name === "get" || prop.key.name === "set") && + (this.type != types.comma && this.type != types.braceR)) { + if (isGenerator || isAsync) { this.unexpected(); } + prop.kind = prop.key.name; + this.parsePropertyName(prop); + prop.value = this.parseMethod(false); + var paramCount = prop.kind === "get" ? 0 : 1; + if (prop.value.params.length !== paramCount) { + var start = prop.value.start; + if (prop.kind === "get") + { this.raiseRecoverable(start, "getter should have no params"); } + else + { this.raiseRecoverable(start, "setter should have exactly one param"); } + } else { + if (prop.kind === "set" && prop.value.params[0].type === "RestElement") + { this.raiseRecoverable(prop.value.params[0].start, "Setter cannot use rest params"); } + } + } else if (this.options.ecmaVersion >= 6 && !prop.computed && prop.key.type === "Identifier") { + this.checkUnreserved(prop.key); + prop.kind = "init"; + if (isPattern) { + prop.value = this.parseMaybeDefault(startPos, startLoc, prop.key); + } else if (this.type === types.eq && refDestructuringErrors) { + if (refDestructuringErrors.shorthandAssign < 0) + { refDestructuringErrors.shorthandAssign = this.start; } + prop.value = this.parseMaybeDefault(startPos, startLoc, prop.key); + } else { + prop.value = prop.key; + } + prop.shorthand = true; + } else { this.unexpected(); } +}; + +pp$3.parsePropertyName = function(prop) { + if (this.options.ecmaVersion >= 6) { + if (this.eat(types.bracketL)) { + prop.computed = true; + prop.key = this.parseMaybeAssign(); + this.expect(types.bracketR); + return prop.key + } else { + prop.computed = false; + } + } + return prop.key = this.type === types.num || this.type === types.string ? this.parseExprAtom() : this.parseIdent(true) +}; + +// Initialize empty function node. + +pp$3.initFunction = function(node) { + node.id = null; + if (this.options.ecmaVersion >= 6) { + node.generator = false; + node.expression = false; + } + if (this.options.ecmaVersion >= 8) + { node.async = false; } +}; + +// Parse object or class method. + +pp$3.parseMethod = function(isGenerator, isAsync) { + var node = this.startNode(), oldInGen = this.inGenerator, oldInAsync = this.inAsync, + oldYieldPos = this.yieldPos, oldAwaitPos = this.awaitPos, oldInFunc = this.inFunction; + + this.initFunction(node); + if (this.options.ecmaVersion >= 6) + { node.generator = isGenerator; } + if (this.options.ecmaVersion >= 8) + { node.async = !!isAsync; } + + this.inGenerator = node.generator; + this.inAsync = node.async; + this.yieldPos = 0; + this.awaitPos = 0; + this.inFunction = true; + this.enterFunctionScope(); + + this.expect(types.parenL); + node.params = this.parseBindingList(types.parenR, false, this.options.ecmaVersion >= 8); + this.checkYieldAwaitInDefaultParams(); + this.parseFunctionBody(node, false); + + this.inGenerator = oldInGen; + this.inAsync = oldInAsync; + this.yieldPos = oldYieldPos; + this.awaitPos = oldAwaitPos; + this.inFunction = oldInFunc; + return this.finishNode(node, "FunctionExpression") +}; + +// Parse arrow function expression with given parameters. + +pp$3.parseArrowExpression = function(node, params, isAsync) { + var oldInGen = this.inGenerator, oldInAsync = this.inAsync, + oldYieldPos = this.yieldPos, oldAwaitPos = this.awaitPos, oldInFunc = this.inFunction; + + this.enterFunctionScope(); + this.initFunction(node); + if (this.options.ecmaVersion >= 8) + { node.async = !!isAsync; } + + this.inGenerator = false; + this.inAsync = node.async; + this.yieldPos = 0; + this.awaitPos = 0; + this.inFunction = true; + + node.params = this.toAssignableList(params, true); + this.parseFunctionBody(node, true); + + this.inGenerator = oldInGen; + this.inAsync = oldInAsync; + this.yieldPos = oldYieldPos; + this.awaitPos = oldAwaitPos; + this.inFunction = oldInFunc; + return this.finishNode(node, "ArrowFunctionExpression") +}; + +// Parse function body and check parameters. + +pp$3.parseFunctionBody = function(node, isArrowFunction) { + var isExpression = isArrowFunction && this.type !== types.braceL; + var oldStrict = this.strict, useStrict = false; + + if (isExpression) { + node.body = this.parseMaybeAssign(); + node.expression = true; + this.checkParams(node, false); + } else { + var nonSimple = this.options.ecmaVersion >= 7 && !this.isSimpleParamList(node.params); + if (!oldStrict || nonSimple) { + useStrict = this.strictDirective(this.end); + // If this is a strict mode function, verify that argument names + // are not repeated, and it does not try to bind the words `eval` + // or `arguments`. + if (useStrict && nonSimple) + { this.raiseRecoverable(node.start, "Illegal 'use strict' directive in function with non-simple parameter list"); } + } + // Start a new scope with regard to labels and the `inFunction` + // flag (restore them to their old value afterwards). + var oldLabels = this.labels; + this.labels = []; + if (useStrict) { this.strict = true; } + + // Add the params to varDeclaredNames to ensure that an error is thrown + // if a let/const declaration in the function clashes with one of the params. + this.checkParams(node, !oldStrict && !useStrict && !isArrowFunction && this.isSimpleParamList(node.params)); + node.body = this.parseBlock(false); + node.expression = false; + this.adaptDirectivePrologue(node.body.body); + this.labels = oldLabels; + } + this.exitFunctionScope(); + + if (this.strict && node.id) { + // Ensure the function name isn't a forbidden identifier in strict mode, e.g. 'eval' + this.checkLVal(node.id, "none"); + } + this.strict = oldStrict; +}; + +pp$3.isSimpleParamList = function(params) { + for (var i = 0, list = params; i < list.length; i += 1) + { + var param = list[i]; + + if (param.type !== "Identifier") { return false + } } + return true +}; + +// Checks function params for various disallowed patterns such as using "eval" +// or "arguments" and duplicate parameters. + +pp$3.checkParams = function(node, allowDuplicates) { + var this$1 = this; + + var nameHash = {}; + for (var i = 0, list = node.params; i < list.length; i += 1) + { + var param = list[i]; + + this$1.checkLVal(param, "var", allowDuplicates ? null : nameHash); + } +}; + +// Parses a comma-separated list of expressions, and returns them as +// an array. `close` is the token type that ends the list, and +// `allowEmpty` can be turned on to allow subsequent commas with +// nothing in between them to be parsed as `null` (which is needed +// for array literals). + +pp$3.parseExprList = function(close, allowTrailingComma, allowEmpty, refDestructuringErrors) { + var this$1 = this; + + var elts = [], first = true; + while (!this.eat(close)) { + if (!first) { + this$1.expect(types.comma); + if (allowTrailingComma && this$1.afterTrailingComma(close)) { break } + } else { first = false; } + + var elt = (void 0); + if (allowEmpty && this$1.type === types.comma) + { elt = null; } + else if (this$1.type === types.ellipsis) { + elt = this$1.parseSpread(refDestructuringErrors); + if (refDestructuringErrors && this$1.type === types.comma && refDestructuringErrors.trailingComma < 0) + { refDestructuringErrors.trailingComma = this$1.start; } + } else { + elt = this$1.parseMaybeAssign(false, refDestructuringErrors); + } + elts.push(elt); + } + return elts +}; + +pp$3.checkUnreserved = function(ref) { + var start = ref.start; + var end = ref.end; + var name = ref.name; + + if (this.inGenerator && name === "yield") + { this.raiseRecoverable(start, "Can not use 'yield' as identifier inside a generator"); } + if (this.inAsync && name === "await") + { this.raiseRecoverable(start, "Can not use 'await' as identifier inside an async function"); } + if (this.isKeyword(name)) + { this.raise(start, ("Unexpected keyword '" + name + "'")); } + if (this.options.ecmaVersion < 6 && + this.input.slice(start, end).indexOf("\\") != -1) { return } + var re = this.strict ? this.reservedWordsStrict : this.reservedWords; + if (re.test(name)) { + if (!this.inAsync && name === "await") + { this.raiseRecoverable(start, "Can not use keyword 'await' outside an async function"); } + this.raiseRecoverable(start, ("The keyword '" + name + "' is reserved")); + } +}; + +// Parse the next token as an identifier. If `liberal` is true (used +// when parsing properties), it will also convert keywords into +// identifiers. + +pp$3.parseIdent = function(liberal, isBinding) { + var node = this.startNode(); + if (liberal && this.options.allowReserved == "never") { liberal = false; } + if (this.type === types.name) { + node.name = this.value; + } else if (this.type.keyword) { + node.name = this.type.keyword; + + // To fix https://github.com/acornjs/acorn/issues/575 + // `class` and `function` keywords push new context into this.context. + // But there is no chance to pop the context if the keyword is consumed as an identifier such as a property name. + // If the previous token is a dot, this does not apply because the context-managing code already ignored the keyword + if ((node.name === "class" || node.name === "function") && + (this.lastTokEnd !== this.lastTokStart + 1 || this.input.charCodeAt(this.lastTokStart) !== 46)) { + this.context.pop(); + } + } else { + this.unexpected(); + } + this.next(); + this.finishNode(node, "Identifier"); + if (!liberal) { this.checkUnreserved(node); } + return node +}; + +// Parses yield expression inside generator. + +pp$3.parseYield = function() { + if (!this.yieldPos) { this.yieldPos = this.start; } + + var node = this.startNode(); + this.next(); + if (this.type == types.semi || this.canInsertSemicolon() || (this.type != types.star && !this.type.startsExpr)) { + node.delegate = false; + node.argument = null; + } else { + node.delegate = this.eat(types.star); + node.argument = this.parseMaybeAssign(); + } + return this.finishNode(node, "YieldExpression") +}; + +pp$3.parseAwait = function() { + if (!this.awaitPos) { this.awaitPos = this.start; } + + var node = this.startNode(); + this.next(); + node.argument = this.parseMaybeUnary(null, true); + return this.finishNode(node, "AwaitExpression") +}; + +var pp$4 = Parser.prototype; + +// This function is used to raise exceptions on parse errors. It +// takes an offset integer (into the current `input`) to indicate +// the location of the error, attaches the position to the end +// of the error message, and then raises a `SyntaxError` with that +// message. + +pp$4.raise = function(pos, message) { + var loc = getLineInfo(this.input, pos); + message += " (" + loc.line + ":" + loc.column + ")"; + var err = new SyntaxError(message); + err.pos = pos; err.loc = loc; err.raisedAt = this.pos; + throw err +}; + +pp$4.raiseRecoverable = pp$4.raise; + +pp$4.curPosition = function() { + if (this.options.locations) { + return new Position(this.curLine, this.pos - this.lineStart) + } +}; + +var pp$5 = Parser.prototype; + +// Object.assign polyfill +var assign = Object.assign || function(target) { + var sources = [], len = arguments.length - 1; + while ( len-- > 0 ) sources[ len ] = arguments[ len + 1 ]; + + for (var i = 0, list = sources; i < list.length; i += 1) { + var source = list[i]; + + for (var key in source) { + if (has(source, key)) { + target[key] = source[key]; + } + } + } + return target +}; + +// The functions in this module keep track of declared variables in the current scope in order to detect duplicate variable names. + +pp$5.enterFunctionScope = function() { + // var: a hash of var-declared names in the current lexical scope + // lexical: a hash of lexically-declared names in the current lexical scope + // childVar: a hash of var-declared names in all child lexical scopes of the current lexical scope (within the current function scope) + // parentLexical: a hash of lexically-declared names in all parent lexical scopes of the current lexical scope (within the current function scope) + this.scopeStack.push({var: {}, lexical: {}, childVar: {}, parentLexical: {}}); +}; + +pp$5.exitFunctionScope = function() { + this.scopeStack.pop(); +}; + +pp$5.enterLexicalScope = function() { + var parentScope = this.scopeStack[this.scopeStack.length - 1]; + var childScope = {var: {}, lexical: {}, childVar: {}, parentLexical: {}}; + + this.scopeStack.push(childScope); + assign(childScope.parentLexical, parentScope.lexical, parentScope.parentLexical); +}; + +pp$5.exitLexicalScope = function() { + var childScope = this.scopeStack.pop(); + var parentScope = this.scopeStack[this.scopeStack.length - 1]; + + assign(parentScope.childVar, childScope.var, childScope.childVar); +}; + +/** + * A name can be declared with `var` if there are no variables with the same name declared with `let`/`const` + * in the current lexical scope or any of the parent lexical scopes in this function. + */ +pp$5.canDeclareVarName = function(name) { + var currentScope = this.scopeStack[this.scopeStack.length - 1]; + + return !has(currentScope.lexical, name) && !has(currentScope.parentLexical, name) +}; + +/** + * A name can be declared with `let`/`const` if there are no variables with the same name declared with `let`/`const` + * in the current scope, and there are no variables with the same name declared with `var` in the current scope or in + * any child lexical scopes in this function. + */ +pp$5.canDeclareLexicalName = function(name) { + var currentScope = this.scopeStack[this.scopeStack.length - 1]; + + return !has(currentScope.lexical, name) && !has(currentScope.var, name) && !has(currentScope.childVar, name) +}; + +pp$5.declareVarName = function(name) { + this.scopeStack[this.scopeStack.length - 1].var[name] = true; +}; + +pp$5.declareLexicalName = function(name) { + this.scopeStack[this.scopeStack.length - 1].lexical[name] = true; +}; + +var Node = function Node(parser, pos, loc) { + this.type = ""; + this.start = pos; + this.end = 0; + if (parser.options.locations) + { this.loc = new SourceLocation(parser, loc); } + if (parser.options.directSourceFile) + { this.sourceFile = parser.options.directSourceFile; } + if (parser.options.ranges) + { this.range = [pos, 0]; } +}; + +// Start an AST node, attaching a start offset. + +var pp$6 = Parser.prototype; + +pp$6.startNode = function() { + return new Node(this, this.start, this.startLoc) +}; + +pp$6.startNodeAt = function(pos, loc) { + return new Node(this, pos, loc) +}; + +// Finish an AST node, adding `type` and `end` properties. + +function finishNodeAt(node, type, pos, loc) { + node.type = type; + node.end = pos; + if (this.options.locations) + { node.loc.end = loc; } + if (this.options.ranges) + { node.range[1] = pos; } + return node +} + +pp$6.finishNode = function(node, type) { + return finishNodeAt.call(this, node, type, this.lastTokEnd, this.lastTokEndLoc) +}; + +// Finish node at given position + +pp$6.finishNodeAt = function(node, type, pos, loc) { + return finishNodeAt.call(this, node, type, pos, loc) +}; + +// The algorithm used to determine whether a regexp can appear at a +// given point in the program is loosely based on sweet.js' approach. +// See https://github.com/mozilla/sweet.js/wiki/design + +var TokContext = function TokContext(token, isExpr, preserveSpace, override, generator) { + this.token = token; + this.isExpr = !!isExpr; + this.preserveSpace = !!preserveSpace; + this.override = override; + this.generator = !!generator; +}; + +var types$1 = { + b_stat: new TokContext("{", false), + b_expr: new TokContext("{", true), + b_tmpl: new TokContext("${", false), + p_stat: new TokContext("(", false), + p_expr: new TokContext("(", true), + q_tmpl: new TokContext("`", true, true, function (p) { return p.tryReadTemplateToken(); }), + f_stat: new TokContext("function", false), + f_expr: new TokContext("function", true), + f_expr_gen: new TokContext("function", true, false, null, true), + f_gen: new TokContext("function", false, false, null, true) +}; + +var pp$7 = Parser.prototype; + +pp$7.initialContext = function() { + return [types$1.b_stat] +}; + +pp$7.braceIsBlock = function(prevType) { + var parent = this.curContext(); + if (parent === types$1.f_expr || parent === types$1.f_stat) + { return true } + if (prevType === types.colon && (parent === types$1.b_stat || parent === types$1.b_expr)) + { return !parent.isExpr } + + // The check for `tt.name && exprAllowed` detects whether we are + // after a `yield` or `of` construct. See the `updateContext` for + // `tt.name`. + if (prevType === types._return || prevType == types.name && this.exprAllowed) + { return lineBreak.test(this.input.slice(this.lastTokEnd, this.start)) } + if (prevType === types._else || prevType === types.semi || prevType === types.eof || prevType === types.parenR || prevType == types.arrow) + { return true } + if (prevType == types.braceL) + { return parent === types$1.b_stat } + if (prevType == types._var || prevType == types.name) + { return false } + return !this.exprAllowed +}; + +pp$7.inGeneratorContext = function() { + var this$1 = this; + + for (var i = this.context.length - 1; i >= 1; i--) { + var context = this$1.context[i]; + if (context.token === "function") + { return context.generator } + } + return false +}; + +pp$7.updateContext = function(prevType) { + var update, type = this.type; + if (type.keyword && prevType == types.dot) + { this.exprAllowed = false; } + else if (update = type.updateContext) + { update.call(this, prevType); } + else + { this.exprAllowed = type.beforeExpr; } +}; + +// Token-specific context update code + +types.parenR.updateContext = types.braceR.updateContext = function() { + if (this.context.length == 1) { + this.exprAllowed = true; + return + } + var out = this.context.pop(); + if (out === types$1.b_stat && this.curContext().token === "function") { + out = this.context.pop(); + } + this.exprAllowed = !out.isExpr; +}; + +types.braceL.updateContext = function(prevType) { + this.context.push(this.braceIsBlock(prevType) ? types$1.b_stat : types$1.b_expr); + this.exprAllowed = true; +}; + +types.dollarBraceL.updateContext = function() { + this.context.push(types$1.b_tmpl); + this.exprAllowed = true; +}; + +types.parenL.updateContext = function(prevType) { + var statementParens = prevType === types._if || prevType === types._for || prevType === types._with || prevType === types._while; + this.context.push(statementParens ? types$1.p_stat : types$1.p_expr); + this.exprAllowed = true; +}; + +types.incDec.updateContext = function() { + // tokExprAllowed stays unchanged +}; + +types._function.updateContext = types._class.updateContext = function(prevType) { + if (prevType.beforeExpr && prevType !== types.semi && prevType !== types._else && + !((prevType === types.colon || prevType === types.braceL) && this.curContext() === types$1.b_stat)) + { this.context.push(types$1.f_expr); } + else + { this.context.push(types$1.f_stat); } + this.exprAllowed = false; +}; + +types.backQuote.updateContext = function() { + if (this.curContext() === types$1.q_tmpl) + { this.context.pop(); } + else + { this.context.push(types$1.q_tmpl); } + this.exprAllowed = false; +}; + +types.star.updateContext = function(prevType) { + if (prevType == types._function) { + var index = this.context.length - 1; + if (this.context[index] === types$1.f_expr) + { this.context[index] = types$1.f_expr_gen; } + else + { this.context[index] = types$1.f_gen; } + } + this.exprAllowed = true; +}; + +types.name.updateContext = function(prevType) { + var allowed = false; + if (this.options.ecmaVersion >= 6) { + if (this.value == "of" && !this.exprAllowed || + this.value == "yield" && this.inGeneratorContext()) + { allowed = true; } + } + this.exprAllowed = allowed; +}; + +// Object type used to represent tokens. Note that normally, tokens +// simply exist as properties on the parser object. This is only +// used for the onToken callback and the external tokenizer. + +var Token = function Token(p) { + this.type = p.type; + this.value = p.value; + this.start = p.start; + this.end = p.end; + if (p.options.locations) + { this.loc = new SourceLocation(p, p.startLoc, p.endLoc); } + if (p.options.ranges) + { this.range = [p.start, p.end]; } +}; + +// ## Tokenizer + +var pp$8 = Parser.prototype; + +// Are we running under Rhino? +var isRhino = typeof Packages == "object" && Object.prototype.toString.call(Packages) == "[object JavaPackage]"; + +// Move to the next token + +pp$8.next = function() { + if (this.options.onToken) + { this.options.onToken(new Token(this)); } + + this.lastTokEnd = this.end; + this.lastTokStart = this.start; + this.lastTokEndLoc = this.endLoc; + this.lastTokStartLoc = this.startLoc; + this.nextToken(); +}; + +pp$8.getToken = function() { + this.next(); + return new Token(this) +}; + +// If we're in an ES6 environment, make parsers iterable +if (typeof Symbol !== "undefined") + { pp$8[Symbol.iterator] = function() { + var this$1 = this; + + return { + next: function () { + var token = this$1.getToken(); + return { + done: token.type === types.eof, + value: token + } + } + } + }; } + +// Toggle strict mode. Re-reads the next number or string to please +// pedantic tests (`"use strict"; 010;` should fail). + +pp$8.curContext = function() { + return this.context[this.context.length - 1] +}; + +// Read a single token, updating the parser object's token-related +// properties. + +pp$8.nextToken = function() { + var curContext = this.curContext(); + if (!curContext || !curContext.preserveSpace) { this.skipSpace(); } + + this.start = this.pos; + if (this.options.locations) { this.startLoc = this.curPosition(); } + if (this.pos >= this.input.length) { return this.finishToken(types.eof) } + + if (curContext.override) { return curContext.override(this) } + else { this.readToken(this.fullCharCodeAtPos()); } +}; + +pp$8.readToken = function(code) { + // Identifier or keyword. '\uXXXX' sequences are allowed in + // identifiers, so '\' also dispatches to that. + if (isIdentifierStart(code, this.options.ecmaVersion >= 6) || code === 92 /* '\' */) + { return this.readWord() } + + return this.getTokenFromCode(code) +}; + +pp$8.fullCharCodeAtPos = function() { + var code = this.input.charCodeAt(this.pos); + if (code <= 0xd7ff || code >= 0xe000) { return code } + var next = this.input.charCodeAt(this.pos + 1); + return (code << 10) + next - 0x35fdc00 +}; + +pp$8.skipBlockComment = function() { + var this$1 = this; + + var startLoc = this.options.onComment && this.curPosition(); + var start = this.pos, end = this.input.indexOf("*/", this.pos += 2); + if (end === -1) { this.raise(this.pos - 2, "Unterminated comment"); } + this.pos = end + 2; + if (this.options.locations) { + lineBreakG.lastIndex = start; + var match; + while ((match = lineBreakG.exec(this.input)) && match.index < this.pos) { + ++this$1.curLine; + this$1.lineStart = match.index + match[0].length; + } + } + if (this.options.onComment) + { this.options.onComment(true, this.input.slice(start + 2, end), start, this.pos, + startLoc, this.curPosition()); } +}; + +pp$8.skipLineComment = function(startSkip) { + var this$1 = this; + + var start = this.pos; + var startLoc = this.options.onComment && this.curPosition(); + var ch = this.input.charCodeAt(this.pos += startSkip); + while (this.pos < this.input.length && !isNewLine(ch)) { + ch = this$1.input.charCodeAt(++this$1.pos); + } + if (this.options.onComment) + { this.options.onComment(false, this.input.slice(start + startSkip, this.pos), start, this.pos, + startLoc, this.curPosition()); } +}; + +// Called at the start of the parse and after every token. Skips +// whitespace and comments, and. + +pp$8.skipSpace = function() { + var this$1 = this; + + loop: while (this.pos < this.input.length) { + var ch = this$1.input.charCodeAt(this$1.pos); + switch (ch) { + case 32: case 160: // ' ' + ++this$1.pos; + break + case 13: + if (this$1.input.charCodeAt(this$1.pos + 1) === 10) { + ++this$1.pos; + } + case 10: case 8232: case 8233: + ++this$1.pos; + if (this$1.options.locations) { + ++this$1.curLine; + this$1.lineStart = this$1.pos; + } + break + case 47: // '/' + switch (this$1.input.charCodeAt(this$1.pos + 1)) { + case 42: // '*' + this$1.skipBlockComment(); + break + case 47: + this$1.skipLineComment(2); + break + default: + break loop + } + break + default: + if (ch > 8 && ch < 14 || ch >= 5760 && nonASCIIwhitespace.test(String.fromCharCode(ch))) { + ++this$1.pos; + } else { + break loop + } + } + } +}; + +// Called at the end of every token. Sets `end`, `val`, and +// maintains `context` and `exprAllowed`, and skips the space after +// the token, so that the next one's `start` will point at the +// right position. + +pp$8.finishToken = function(type, val) { + this.end = this.pos; + if (this.options.locations) { this.endLoc = this.curPosition(); } + var prevType = this.type; + this.type = type; + this.value = val; + + this.updateContext(prevType); +}; + +// ### Token reading + +// This is the function that is called to fetch the next token. It +// is somewhat obscure, because it works in character codes rather +// than characters, and because operator parsing has been inlined +// into it. +// +// All in the name of speed. +// +pp$8.readToken_dot = function() { + var next = this.input.charCodeAt(this.pos + 1); + if (next >= 48 && next <= 57) { return this.readNumber(true) } + var next2 = this.input.charCodeAt(this.pos + 2); + if (this.options.ecmaVersion >= 6 && next === 46 && next2 === 46) { // 46 = dot '.' + this.pos += 3; + return this.finishToken(types.ellipsis) + } else { + ++this.pos; + return this.finishToken(types.dot) + } +}; + +pp$8.readToken_slash = function() { // '/' + var next = this.input.charCodeAt(this.pos + 1); + if (this.exprAllowed) { ++this.pos; return this.readRegexp() } + if (next === 61) { return this.finishOp(types.assign, 2) } + return this.finishOp(types.slash, 1) +}; + +pp$8.readToken_mult_modulo_exp = function(code) { // '%*' + var next = this.input.charCodeAt(this.pos + 1); + var size = 1; + var tokentype = code === 42 ? types.star : types.modulo; + + // exponentiation operator ** and **= + if (this.options.ecmaVersion >= 7 && code == 42 && next === 42) { + ++size; + tokentype = types.starstar; + next = this.input.charCodeAt(this.pos + 2); + } + + if (next === 61) { return this.finishOp(types.assign, size + 1) } + return this.finishOp(tokentype, size) +}; + +pp$8.readToken_pipe_amp = function(code) { // '|&' + var next = this.input.charCodeAt(this.pos + 1); + if (next === code) { return this.finishOp(code === 124 ? types.logicalOR : types.logicalAND, 2) } + if (next === 61) { return this.finishOp(types.assign, 2) } + return this.finishOp(code === 124 ? types.bitwiseOR : types.bitwiseAND, 1) +}; + +pp$8.readToken_caret = function() { // '^' + var next = this.input.charCodeAt(this.pos + 1); + if (next === 61) { return this.finishOp(types.assign, 2) } + return this.finishOp(types.bitwiseXOR, 1) +}; + +pp$8.readToken_plus_min = function(code) { // '+-' + var next = this.input.charCodeAt(this.pos + 1); + if (next === code) { + if (next == 45 && !this.inModule && this.input.charCodeAt(this.pos + 2) == 62 && + (this.lastTokEnd === 0 || lineBreak.test(this.input.slice(this.lastTokEnd, this.pos)))) { + // A `-->` line comment + this.skipLineComment(3); + this.skipSpace(); + return this.nextToken() + } + return this.finishOp(types.incDec, 2) + } + if (next === 61) { return this.finishOp(types.assign, 2) } + return this.finishOp(types.plusMin, 1) +}; + +pp$8.readToken_lt_gt = function(code) { // '<>' + var next = this.input.charCodeAt(this.pos + 1); + var size = 1; + if (next === code) { + size = code === 62 && this.input.charCodeAt(this.pos + 2) === 62 ? 3 : 2; + if (this.input.charCodeAt(this.pos + size) === 61) { return this.finishOp(types.assign, size + 1) } + return this.finishOp(types.bitShift, size) + } + if (next == 33 && code == 60 && !this.inModule && this.input.charCodeAt(this.pos + 2) == 45 && + this.input.charCodeAt(this.pos + 3) == 45) { + // `",device.name); + device.getPorts(function(devicePorts, instantPorts) { + //console.log("getPorts <--",device.name); + if (instantPorts===false) shouldCallAgain = true; + if (devicePorts) { + devicePorts.forEach(function(port) { + var ignored = false; + if (Espruino.Config.SERIAL_IGNORE) + Espruino.Config.SERIAL_IGNORE.split("|").forEach(function(wildcard) { + var regexp = "^"+wildcard.replace(/\./g,"\\.").replace(/\*/g,".*")+"$"; + if (port.path.match(new RegExp(regexp))) + ignored = true; + }); + + if (!ignored) { + if (port.usb && port.usb[0]==0x0483 && port.usb[1]==0x5740) + port.description = "Espruino board"; + ports.push(port); + newPortToDevice[port.path] = device; + } + }); + } + responses++; + if (responses == devices.length) { + portToDevice = newPortToDevice; + ports.sort(function(a,b) { + if (a.unimportant && !b.unimportant) return 1; + if (b.unimportant && !a.unimportant) return -1; + return 0; + }); + callback(ports, shouldCallAgain); + } + }); + }); + }; + + var openSerial=function(serialPort, connectCallback, disconnectCallback) { + return openSerialInternal(serialPort, connectCallback, disconnectCallback, 2); + } + + var openSerialInternal=function(serialPort, connectCallback, disconnectCallback, attempts) { + /* If openSerial is called, we need to have called getPorts first + in order to figure out which one of the serial_ implementations + we must call into. */ + if (portToDevice === undefined) { + portToDevice = []; // stop recursive calls if something errors + return getPorts(function() { + openSerialInternal(serialPort, connectCallback, disconnectCallback, attempts); + }); + } + + if (!(serialPort in portToDevice)) { + if (serialPort.toLowerCase() in portToDevice) { + serialPort = serialPort.toLowerCase(); + } else { + if (attempts>0) { + console.log("Port "+JSON.stringify(serialPort)+" not found - checking ports again ("+attempts+" attempts left)"); + return getPorts(function() { + openSerialInternal(serialPort, connectCallback, disconnectCallback, attempts-1); + }); + } else { + console.error("Port "+JSON.stringify(serialPort)+" not found"); + return connectCallback(undefined); + } + } + } + + connectionInfo = undefined; + flowControlXOFF = false; + currentDevice = portToDevice[serialPort]; + currentDevice.open(serialPort, function(cInfo) { // CONNECT + if (!cInfo) { +// Espruino.Core.Notifications.error("Unable to connect"); + console.error("Unable to open device (connectionInfo="+cInfo+")"); + connectCallback(undefined); + } else { + connectionInfo = cInfo; + connectedPort = serialPort; + console.log("Connected", cInfo); + var portInfo = { port:serialPort }; + if (connectionInfo.portName) + portInfo.portName = connectionInfo.portName; + Espruino.callProcessor("connected", portInfo, function() { + connectCallback(cInfo); + }); + } + }, function(data) { // RECEIEVE DATA + if (!(data instanceof ArrayBuffer)) console.warn("Serial port implementation is not returning ArrayBuffers"); + if (Espruino.Config.SERIAL_FLOW_CONTROL) { + var u = new Uint8Array(data); + for (var i=0;i resume upload"); + flowControlXOFF = false; + } + if (u[i]==19) { // XOFF + console.log("XOFF received => pause upload"); + flowControlXOFF = true; + } + } + } + if (readListener) readListener(data); + }, function() { // DISCONNECT + currentDevice = undefined; + if (!connectionInfo) { + // we got a disconnect when we hadn't connected... + // Just call connectCallback(undefined), don't bother sending disconnect + connectCallback(undefined); + return; + } + connectionInfo = undefined; + if (writeTimeout!==undefined) + clearTimeout(writeTimeout); + writeTimeout = undefined; + writeData = []; + sendingBinary = false; + flowControlXOFF = false; + + Espruino.callProcessor("disconnected", undefined, function() { + disconnectCallback(); + }); + }); + }; + + var str2ab=function(str) { + var buf=new ArrayBuffer(str.length); + var bufView=new Uint8Array(buf); + for (var i=0; i=256) { + console.warn("Attempted to send non-8 bit character - code "+ch); + ch = "?".charCodeAt(0); + } + bufView[i] = ch; + } + return buf; + }; + + var closeSerial=function() { + if (currentDevice) { + currentDevice.close(); + currentDevice = undefined; + } else + console.error("Close called, but serial port not open"); + }; + + var isConnected = function() { + return currentDevice!==undefined; + }; + + var writeSerialWorker = function(isStarting) { + writeTimeout = undefined; // we've been called + // check flow control + if (flowControlXOFF) { + /* flow control was enabled - bit hacky (we could use a callback) + but safe - just check again in a bit to see if we should send */ + writeTimeout = setTimeout(function() { + writeSerialWorker(); + }, 50); + return; + } + + // if we disconnected while sending, empty queue + if (currentDevice === undefined) { + if (writeData[0].callback) + writeData[0].callback(); + writeData.shift(); + if (writeData.length) setTimeout(function() { + writeSerialWorker(false); + }, 1); + return; + } + + if (writeData[0].data === "") { + if (writeData[0].showStatus) + Espruino.Core.Status.setStatus("Sent"); + if (writeData[0].callback) + writeData[0].callback(); + writeData.shift(); // remove this empty first element + if (!writeData.length) return; // anything left to do? + isStarting = true; + } + + if (isStarting) { + var blockSize = 512; + if (currentDevice.maxWriteLength) + blockSize = currentDevice.maxWriteLength; + /* if we're throttling our writes we want to send small + * blocks of data at once. We still limit the size of + * sent blocks to 512 because on Mac we seem to lose + * data otherwise (not on any other platforms!) */ + if (slowWrite) blockSize=19; + writeData[0].blockSize = blockSize; + + writeData[0].showStatus &= writeData[0].data.length>writeData[0].blockSize; + if (writeData[0].showStatus) { + Espruino.Core.Status.setStatus("Sending...", writeData[0].data.length); + console.log("---> "+JSON.stringify(writeData[0].data)); + } + } + + // Initial split use previous, or don't + var d = undefined; + var split = writeData[0].nextSplit || { start:0, end:writeData[0].data.length, delay:0 }; + // if we get something like Ctrl-C or `reset`, wait a bit for it to complete + if (!sendingBinary) { + function findSplitIdx(prev, substr, delay, reason) { + var match = writeData[0].data.match(substr); + // not found + if (match===null) return prev; + // or previous find was earlier in str + var end = match.index + match[0].length; + if (end > prev.end) return prev; + // found, and earlier + prev.start = match.index; + prev.end = end; + prev.delay = delay; + prev.match = match[0]; + prev.reason = reason; + return prev; + } + split = findSplitIdx(split, /\x03/, 250, "Ctrl-C"); // Ctrl-C + split = findSplitIdx(split, /reset\(\);\n/, 250, "reset()"); // Reset + split = findSplitIdx(split, /load\(\);\n/, 250, "load()"); // Load + split = findSplitIdx(split, /Modules.addCached\("[^\n]*"\);\n/, 250, "Modules.addCached"); // Adding a module + split = findSplitIdx(split, /\x10require\("Storage"\).write\([^\n]*\);\n/, 500, "Storage.write"); // Write chunk of data + } + // Otherwise split based on block size + if (!split.match || split.end >= writeData[0].blockSize) { + if (split.match) writeData[0].nextSplit = split; + split = { start:0, end:writeData[0].blockSize, delay:0 }; + } + if (split.match) console.log("Splitting for "+split.reason+", delay "+split.delay); + // Only send some of the data + if (writeData[0].data.length>split.end) { + if (slowWrite && split.delay==0) split.delay=50; + d = writeData[0].data.substr(0,split.end); + writeData[0].data = writeData[0].data.substr(split.end); + if (writeData[0].nextSplit) { + writeData[0].nextSplit.start -= split.end; + writeData[0].nextSplit.end -= split.end; + if (writeData[0].nextSplit.end<=0) + writeData[0].nextSplit = undefined; + } + } else { + d = writeData[0].data; + writeData[0].data = ""; + writeData[0].nextSplit = undefined; + } + // update status + if (writeData[0].showStatus) + Espruino.Core.Status.incrementProgress(d.length); + // actually write data + //console.log("Sending block "+JSON.stringify(d)+", wait "+split.delay+"ms"); + currentDevice.write(d, function() { + // Once written, start timeout + writeTimeout = setTimeout(function() { + writeSerialWorker(); + }, split.delay); + }); + } + + // Throttled serial write + var writeSerial = function(data, showStatus, callback) { + if (showStatus===undefined) showStatus=true; + + /* Queue our data to write. If there was previous data and no callback to + invoke on this data or the previous then just append data. This would happen + if typing in the terminal for example. */ + if (!callback && writeData.length && !writeData[writeData.length-1].callback) { + writeData[writeData.length-1].data += data; + } else { + writeData.push({data:data,callback:callback,showStatus:showStatus}); + /* if this is our first data, start sending now. Otherwise we're already + busy sending and will pull data off writeData when ready */ + if (writeData.length==1) + writeSerialWorker(true); + } + }; + + + // ---------------------------------------------------------- + Espruino.Core.Serial = { + "devices" : [], // List of devices that can provide a serial API + "init" : init, + "getPorts": getPorts, + "open": openSerial, + "isConnected": isConnected, + "startListening": startListening, + "write": writeSerial, + "close": closeSerial, + "isSlowWrite": function() { return slowWrite; }, + "setSlowWrite": function(isOn, force) { + if ((!force) && Espruino.Config.SERIAL_THROTTLE_SEND) { + console.log("ForceThrottle option is set - set Slow Write = true"); + isOn = true; + } else + console.log("Set Slow Write = "+isOn); + slowWrite = isOn; + }, + "setBinary": function(isOn) { + sendingBinary = isOn; + } + }; +})(); +/** + Copyright 2014 Gordon Williams (gw@pur3.co.uk) + + This Source Code is subject to the terms of the Mozilla Public + License, v2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + + ------------------------------------------------------------------ + The plugin that actually writes code out to Espruino + ------------------------------------------------------------------ +**/ +"use strict"; +(function(){ + + function init() { + Espruino.Core.Config.add("RESET_BEFORE_SEND", { + section : "Communications", + name : "Reset before Send", + description : "Reset Espruino before sending code from the editor pane?", + type : "boolean", + defaultValue : true + }); + Espruino.Core.Config.add("STORE_LINE_NUMBERS", { + section : "Communications", + name : "Store line numbers", + description : "Should Espruino store line numbers for each function? This uses one extra variable per function, but allows you to get source code debugging in the Web IDE", + type : "boolean", + defaultValue : true + }); + + } + + function writeToEspruino(code, callback) { + /* hack around non-K&R code formatting that would have + broken Espruino CLI's bracket counting */ + code = reformatCode(code); + if (code === undefined) return; // it should already have errored + + // We want to make sure we've got a prompt before sending. If not, + // this will issue a Ctrl+C + Espruino.Core.Utils.getEspruinoPrompt(function() { + // Make sure code ends in 2 newlines + while (code[code.length-2]!="\n" || code[code.length-1]!="\n") + code += "\n"; + + // If we're supposed to reset Espruino before sending... + if (Espruino.Config.RESET_BEFORE_SEND) { + code = "\x10reset();\n"+code; + } + + //console.log("Sending... "+data); + Espruino.Core.Serial.write(code, true, function() { + // give 5 seconds for sending with save and 2 seconds without save + var count = Espruino.Config.SAVE_ON_SEND ? 50 : 20; + setTimeout(function cb() { + if (Espruino.Core.Terminal!==undefined && + Espruino.Core.Terminal.getTerminalLine()!=">") { + count--; + if (count>0) { + setTimeout(cb, 100); + } else { + Espruino.Core.Notifications.error("Prompt not detected - upload failed. Trying to recover..."); + Espruino.Core.Serial.write("\x03\x03echo(1)\n", false, callback); + } + } else { + if (callback) callback(); + } + }, 100); + }); + }); + }; + + /// Parse and fix issues like `if (false)\n foo` in the root scope + function reformatCode(code) { + var APPLY_LINE_NUMBERS = false; + var lineNumberOffset = 0; + var ENV = Espruino.Core.Env.getData(); + if (ENV && ENV.VERSION_MAJOR && ENV.VERSION_MINOR) { + if (ENV.VERSION_MAJOR>1 || + ENV.VERSION_MINOR>=81.086) { + if (Espruino.Config.STORE_LINE_NUMBERS) + APPLY_LINE_NUMBERS = true; + } + } + // Turn cr/lf into just lf (eg. windows -> unix) + code = code.replace(/\r\n/g,"\n"); + // First off, try and fix funky characters + for (var i=0;i255) && ch!=9/*Tab*/ && ch!=10/*LF*/ && ch!=13/*CR*/) { + console.warn("Funky character code "+ch+" at position "+i+". Replacing with ?"); + code = code.substr(0,i)+"?"+code.substr(i+1); + } + } + + /* Search for lines added to the start of the code by the module handler. + Ideally there would be a better way of doing this so line numbers stayed correct, + but this hack works for now. Fixes EspruinoWebIDE#140 */ + if (APPLY_LINE_NUMBERS) { + var l = code.split("\n"); + var i = 0; + while (l[i] && (l[i].substr(0,8)=="Modules." || + l[i].substr(0,8)=="setTime(")) i++; + lineNumberOffset = -i; + } + + var resultCode = "\x10"; // 0x10 = echo off for line + /** we're looking for: + * `a = \n b` + * `for (.....) \n X` + * `if (.....) \n X` + * `if (.....) { } \n else foo` + * `while (.....) \n X` + * `do \n X` + * `function (.....) \n X` + * `function N(.....) \n X` + * `var a \n , b` `var a = 0 \n, b` + * `var a, \n b` `var a = 0, \n b` + * `a \n . b` + * `foo() \n . b` + * `try { } \n catch \n () \n {}` + * + * These are divided into two groups - where there are brackets + * after the keyword (statementBeforeBrackets) and where there aren't + * (statement) + * + * We fix them by replacing \n with what you get when you press + * Alt+Enter (Ctrl + LF). This tells Espruino that it's a newline + * but NOT to execute. + */ + var lex = Espruino.Core.Utils.getLexer(code); + var brackets = 0; + var curlyBrackets = 0; + var statementBeforeBrackets = false; + var statement = false; + var varDeclaration = false; + var lastIdx = 0; + var lastTok = {str:""}; + var tok = lex.next(); + while (tok!==undefined) { + var previousString = code.substring(lastIdx, tok.startIdx); + var tokenString = code.substring(tok.startIdx, tok.endIdx); + //console.log("prev "+JSON.stringify(previousString)+" next "+tokenString); + + /* Inserting Alt-Enter newline, which adds newline without trying + to execute */ + if (brackets>0 || // we have brackets - sending the alt-enter special newline means Espruino doesn't have to do a search itself - faster. + statement || // statement was before brackets - expecting something else + statementBeforeBrackets || // we have an 'if'/etc + varDeclaration || // variable declaration then newline + tok.str=="," || // comma on newline - there was probably something before + tok.str=="." || // dot on newline - there was probably something before + tok.str=="+" || tok.str=="-" || // +/- on newline - there was probably something before + tok.str=="=" || // equals on newline - there was probably something before + tok.str=="else" || // else on newline + lastTok.str=="else" || // else befgore newline + tok.str=="catch" || // catch on newline - part of try..catch + lastTok.str=="catch" + ) { + //console.log("Possible"+JSON.stringify(previousString)); + previousString = previousString.replace(/\n/g, "\x1B\x0A"); + } + + var previousBrackets = brackets; + if (tok.str=="(" || tok.str=="{" || tok.str=="[") brackets++; + if (tok.str=="{") curlyBrackets++; + if (tok.str==")" || tok.str=="}" || tok.str=="]") brackets--; + if (tok.str=="}") curlyBrackets--; + + if (brackets==0) { + if (tok.str=="for" || tok.str=="if" || tok.str=="while" || tok.str=="function" || tok.str=="throw") { + statementBeforeBrackets = true; + varDeclaration = false; + } else if (tok.str=="var") { + varDeclaration = true; + } else if (tok.type=="ID" && lastTok.str=="function") { + statementBeforeBrackets = true; + } else if (tok.str=="try" || tok.str=="catch") { + statementBeforeBrackets = true; + } else if (tok.str==")" && statementBeforeBrackets) { + statementBeforeBrackets = false; + statement = true; + } else if (["=","^","&&","||","+","+=","-","-=","*","*=","/","/=","%","%=","&","&=","|","|="].indexOf(tok.str)>=0) { + statement = true; + } else { + if (tok.str==";") varDeclaration = false; + statement = false; + statementBeforeBrackets = false; + } + } + /* If we're at root scope and had whitespace/comments between code, + remove it all and replace it with a single newline and a + 0x10 (echo off for line) character. However DON'T do this if we had + an alt-enter in the line, as it was there to stop us executing + prematurely */ + if (previousBrackets==0 && + previousString.indexOf("\n")>=0 && + previousString.indexOf("\x1B\x0A")<0) { + previousString = "\n\x10"; + // Apply line numbers to each new line sent, to aid debugger + if (APPLY_LINE_NUMBERS && tok.lineNumber && (tok.lineNumber+lineNumberOffset)>0) { + // Esc [ 1234 d + // This is the 'set line number' command that we're abusing :) + previousString += "\x1B\x5B"+(tok.lineNumber+lineNumberOffset)+"d"; + } + } + + // add our stuff back together + resultCode += previousString+tokenString; + // next + lastIdx = tok.endIdx; + lastTok = tok; + tok = lex.next(); + } + //console.log(resultCode); + if (brackets>0) { + Espruino.Core.Notifications.error("You have more open brackets than close brackets. Please see the hints in the Editor window."); + return undefined; + } + if (brackets<0) { + Espruino.Core.Notifications.error("You have more close brackets than open brackets. Please see the hints in the Editor window."); + return undefined; + } + return resultCode; + }; + + Espruino.Core.CodeWriter = { + init : init, + writeToEspruino : writeToEspruino, + }; +}()); +/** + Copyright 2014 Gordon Williams (gw@pur3.co.uk) + + This Source Code is subject to the terms of the Mozilla Public + License, v2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + + ------------------------------------------------------------------ + Automatically load any referenced modules + ------------------------------------------------------------------ +**/ +"use strict"; +(function(){ + + function init() { + Espruino.Core.Config.add("MODULE_URL", { + section : "Communications", + name : "Module URL", + description : "Where to search online for modules when `require()` is used", + type : "string", + defaultValue : "https://www.espruino.com/modules" + }); + Espruino.Core.Config.add("MODULE_EXTENSIONS", { + section : "Communications", + name : "Module Extensions", + description : "The file extensions to use for each module. These are checked in order and the first that exists is used. One or more file extensions (including the dot) separated by `|`", + type : "string", + defaultValue : ".min.js|.js" + }); + Espruino.Core.Config.add("MODULE_AS_FUNCTION", { + section : "Communications", + name : "Modules uploaded as functions", + description : "Espruino 1v90 and later ONLY. Upload modules as Functions, allowing any functions inside them to be loaded directly from flash when 'Save on Send' is enabled.", + type : "boolean", + defaultValue : true + }); + + Espruino.Core.Config.add("MODULE_PROXY_ENABLED", { + section : "Communications", + name : "Enable Proxy", + description : "Enable Proxy for loading the modules when `require()` is used (only in native IDE)", + type : "boolean", + defaultValue : false + }); + + Espruino.Core.Config.add("MODULE_PROXY_URL", { + section : "Communications", + name : "Proxy URL", + description : "Proxy URL for loading the modules when `require()` is used (only in native IDE)", + type : "string", + defaultValue : "" + }); + + Espruino.Core.Config.add("MODULE_PROXY_PORT", { + section : "Communications", + name : "Proxy Port", + description : "Proxy Port for loading the modules when `require()` is used (only in native IDE)", + type : "string", + defaultValue : "" + }); + + // When code is sent to Espruino, search it for modules and add extra code required to load them + Espruino.addProcessor("transformForEspruino", function(code, callback) { + loadModules(code, callback); + }); + + // Append the 'getModule' processor as the last (plugins get initialized after Espruino.Core modules) + Espruino.Plugins.CoreModules = { + init: function() { + Espruino.addProcessor("getModule", function(data, callback) { + if (data.moduleCode!==undefined) { // already provided be previous getModule processor + return callback(data); + } + + fetchGetModule(data, callback); + }); + } + }; + } + + function isBuiltIn(module) { + var d = Espruino.Core.Env.getData(); + // If we got data from the device itself, use that as the + // definitive answer + if ("string" == typeof d.MODULES) + return d.MODULES.split(",").indexOf(module)>=0; + // Otherwise try and figure it out from JSON + if ("info" in d && + "builtin_modules" in d.info && + d.info.builtin_modules.indexOf(module)>=0) + return true; + // Otherwise assume we don't have it + return false; + } + + /** Find any instances of require(...) in the code string and return a list */ + var getModulesRequired = function(code) { + var modules = []; + + var lex = Espruino.Core.Utils.getLexer(code); + var tok = lex.next(); + var state = 0; + while (tok!==undefined) { + if (state==0 && tok.str=="require") { + state=1; + } else if (state==1 && tok.str=="(") { + state=2; + } else if (state==2 && (tok.type=="STRING")) { + state=0; + var module = tok.value; + if (!isBuiltIn(module) && modules.indexOf(module)<0) + modules.push(module); + } else + state = 0; + tok = lex.next(); + } + + return modules; + }; + + /** Download modules from MODULE_URL/.. */ + function fetchGetModule(data, callback) { + var fullModuleName = data.moduleName; + + // try and load the module the old way... + console.log("loadModule("+fullModuleName+")"); + + var urls = []; // Array of where to look for this module + var modName; // Simple name of the module + if(Espruino.Core.Utils.isURL(fullModuleName)) { + modName = fullModuleName.substr(fullModuleName.lastIndexOf("/") + 1).split(".")[0]; + urls = [ fullModuleName ]; + } else { + modName = fullModuleName; + Espruino.Config.MODULE_URL.split("|").forEach(function (url) { + url = url.trim(); + if (url.length!=0) + Espruino.Config.MODULE_EXTENSIONS.split("|").forEach(function (extension) { + urls.push(url + "/" + fullModuleName + extension); + }) + }); + }; + + // Recursively go through all the urls + (function download(urls) { + if (urls.length==0) { + return callback(data); + } + var dlUrl = urls[0]; + Espruino.Core.Utils.getURL(dlUrl, function (code) { + if (code!==undefined) { + // we got it! + data.moduleCode = code; + data.isMinified = dlUrl.substr(-7)==".min.js"; + return callback(data); + } else { + // else try next + download(urls.slice(1)); + } + }); + })(urls); + } + + + /** Called from loadModule when a module is loaded. Parse it for other modules it might use + * and resolve dfd after all submodules have been loaded */ + function moduleLoaded(resolve, requires, modName, data, loadedModuleData, alreadyMinified){ + // Check for any modules used from this module that we don't already have + var newRequires = getModulesRequired(data); + console.log(" - "+modName+" requires "+JSON.stringify(newRequires)); + // if we need new modules, set them to load and get their promises + var newPromises = []; + for (var i in newRequires) { + if (requires.indexOf(newRequires[i])<0) { + console.log(" Queueing "+newRequires[i]); + requires.push(newRequires[i]); + newPromises.push(loadModule(requires, newRequires[i], loadedModuleData)); + } else { + console.log(" Already loading "+newRequires[i]); + } + } + + var loadProcessedModule = function (module) { + // if we needed to load something, wait until it's loaded before resolving this + Promise.all(newPromises).then(function(){ + // add the module to end of our array + if (Espruino.Config.MODULE_AS_FUNCTION) + loadedModuleData.push("Modules.addCached(" + JSON.stringify(module.name) + ",function(){" + module.code + "});"); + else + loadedModuleData.push("Modules.addCached(" + JSON.stringify(module.name) + "," + JSON.stringify(module.code) + ");"); + // We're done + resolve(); + }); + } + if (alreadyMinified) + loadProcessedModule({code:data,name:modName}); + else + Espruino.callProcessor("transformModuleForEspruino", {code:data,name:modName}, loadProcessedModule); + } + + /** Given a module name (which could be a URL), try and find it. Return + * a deferred thingybob which signals when we're done. */ + function loadModule(requires, fullModuleName, loadedModuleData) { + return new Promise(function(resolve, reject) { + // First off, try and find this module using callProcessor + Espruino.callProcessor("getModule", + { moduleName:fullModuleName, moduleCode:undefined, isMinified:false }, + function(data) { + if (data.moduleCode===undefined) { + Espruino.Core.Notifications.warning("Module "+fullModuleName+" not found"); + return resolve(); + } + + // great! it found something. Use it. + moduleLoaded(resolve, requires, fullModuleName, data.moduleCode, loadedModuleData, data.isMinified); + }); + }); + } + + /** Finds instances of 'require' and then ensures that + those modules are loaded into the module cache beforehand + (by inserting the relevant 'addCached' commands into 'code' */ + function loadModules(code, callback){ + var loadedModuleData = []; + var requires = getModulesRequired(code); + if (requires.length == 0) { + // no modules needed - just return + callback(code); + } else { + Espruino.Core.Status.setStatus("Loading modules"); + // Kick off the module loading (each returns a promise) + var promises = requires.map(function (moduleName) { + return loadModule(requires, moduleName, loadedModuleData); + }); + // When all promises are complete + Promise.all(promises).then(function(){ + callback(loadedModuleData.join("\n") + "\n" + code); + }); + } + }; + + + Espruino.Core.Modules = { + init : init + }; +}()); +/** + Copyright 2014 Gordon Williams (gw@pur3.co.uk) + + This Source Code is subject to the terms of the Mozilla Public + License, v2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + + ------------------------------------------------------------------ + Board Environment variables (process.env) - queried when board connects + ------------------------------------------------------------------ +**/ +"use strict"; +(function(){ + + var environmentData = {}; + var boardData = {}; + + function init() { + Espruino.Core.Config.add("ENV_ON_CONNECT", { + section : "Communications", + name : "Request board details on connect", + description : 'Just after the board is connected, should we query `process.env` to find out which board we\'re connected to? '+ + 'This enables the Web IDE\'s code completion, compiler features, and firmware update notice.', + type : "boolean", + defaultValue : true, + }); + + Espruino.addProcessor("connected", function(data, callback) { + // Give us some time for any stored data to come in + setTimeout(queryBoardProcess, 200, data, callback); + }); + } + + function queryBoardProcess(data, callback) { + if ((!Espruino.Config.ENV_ON_CONNECT) || + (Espruino.Core.MenuFlasher && Espruino.Core.MenuFlasher.isFlashing())) { + return callback(data); + } + + Espruino.Core.Utils.executeExpression("process.env", function(result) { + var json = {}; + if (result!==undefined) { + try { + json = JSON.parse(result); + } catch (e) { + console.log("JSON parse failed - " + e + " in " + JSON.stringify(result)); + } + } + if (Object.keys(json).length==0) { + Espruino.Core.Notifications.error("Unable to retrieve board information.\nConnection Error?"); + // make sure we don't remember a previous board's info + json = { + VERSION : undefined, + BOARD : undefined, + MODULES : undefined, + EXPTR : undefined + }; + } else { + if (json.BOARD && json.VERSION) + Espruino.Core.Notifications.info("Found " +json.BOARD+", "+json.VERSION); + } + // now process the enviroment variables + for (var k in json) { + boardData[k] = json[k]; + environmentData[k] = json[k]; + } + if (environmentData.VERSION) { + var v = environmentData.VERSION; + var vIdx = v.indexOf("v"); + if (vIdx>=0) { + environmentData.VERSION_MAJOR = parseInt(v.substr(0,vIdx)); + var minor = v.substr(vIdx+1); + var dot = minor.indexOf("."); + if (dot>=0) + environmentData.VERSION_MINOR = parseInt(minor.substr(0,dot)) + parseInt(minor.substr(dot+1))*0.001; + else + environmentData.VERSION_MINOR = parseFloat(minor); + } + } + + Espruino.callProcessor("environmentVar", environmentData, function(envData) { + environmentData = envData; + callback(data); + }); + }); + } + + /** Get all data merged in from the board */ + function getData() { + return environmentData; + } + + /** Get just the board's environment data */ + function getBoardData() { + return boardData; + } + + /** Get a list of boards that we know about */ + function getBoardList(callback) { + var jsonDir = Espruino.Config.BOARD_JSON_URL; + + // ensure jsonDir ends with slash + if (jsonDir.indexOf('/', jsonDir.length - 1) === -1) { + jsonDir += '/'; + } + + Espruino.Core.Utils.getJSONURL(jsonDir + "boards.json", function(boards){ + // now load all the individual JSON files + var promises = []; + for (var boardId in boards) { + promises.push((function() { + var id = boardId; + return new Promise(function(resolve, reject) { + Espruino.Core.Utils.getJSONURL(jsonDir + boards[boardId].json, function (data) { + boards[id]["json"] = data; + resolve(); + }); + }); + })()); + } + + // When all are loaded, load the callback + Promise.all(promises).then(function() { + callback(boards); + }); + }); + } + + Espruino.Core.Env = { + init : init, + getData : getData, + getBoardData : getBoardData, + getBoardList : getBoardList, + }; +}()); +/** + Copyright 2014 Gordon Williams (gw@pur3.co.uk) + + This Source Code is subject to the terms of the Mozilla Public + License, v2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + + ------------------------------------------------------------------ + Try and get any URLS that are from GitHub + ------------------------------------------------------------------ +**/ +"use strict"; +(function(){ + + function init() { + Espruino.addProcessor("getURL", getGitHub); + } + + function getGitHub(data, callback) { + var match = data.url.match(/^https?:\/\/github.com\/([^\/]+)\/([^\/]+)\/blob\/([^\/]+)\/(.*)$/); + if (match) { + var git = { + owner : match[1], + repo : match[2], + branch : match[3], + path : match[4] + }; + + var url = "https://raw.githubusercontent.com/"+git.owner+"/"+git.repo+"/"+git.branch+"/"+git.path; + console.log("Found GitHub", JSON.stringify(git)); + callback({url: url}); + } else + callback(data); // no match - continue as normal + } + + Espruino.Plugins.GetGitHub = { + init : init, + }; +}()); +/*! https://mths.be/utf8js v2.0.0 by @mathias */ +;(function(root) { + + // Detect free variables `exports` + var freeExports = typeof exports == 'object' && exports; + + // Detect free variable `module` + var freeModule = typeof module == 'object' && module && + module.exports == freeExports && module; + + // Detect free variable `global`, from Node.js or Browserified code, + // and use it as `root` + var freeGlobal = typeof global == 'object' && global; + if (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal) { + root = freeGlobal; + } + + /*--------------------------------------------------------------------------*/ + + var stringFromCharCode = String.fromCharCode; + + // Taken from https://mths.be/punycode + function ucs2decode(string) { + var output = []; + var counter = 0; + var length = string.length; + var value; + var extra; + while (counter < length) { + value = string.charCodeAt(counter++); + if (value >= 0xD800 && value <= 0xDBFF && counter < length) { + // high surrogate, and there is a next character + extra = string.charCodeAt(counter++); + if ((extra & 0xFC00) == 0xDC00) { // low surrogate + output.push(((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000); + } else { + // unmatched surrogate; only append this code unit, in case the next + // code unit is the high surrogate of a surrogate pair + output.push(value); + counter--; + } + } else { + output.push(value); + } + } + return output; + } + + // Taken from https://mths.be/punycode + function ucs2encode(array) { + var length = array.length; + var index = -1; + var value; + var output = ''; + while (++index < length) { + value = array[index]; + if (value > 0xFFFF) { + value -= 0x10000; + output += stringFromCharCode(value >>> 10 & 0x3FF | 0xD800); + value = 0xDC00 | value & 0x3FF; + } + output += stringFromCharCode(value); + } + return output; + } + + function checkScalarValue(codePoint) { + if (codePoint >= 0xD800 && codePoint <= 0xDFFF) { + throw Error( + 'Lone surrogate U+' + codePoint.toString(16).toUpperCase() + + ' is not a scalar value' + ); + } + } + /*--------------------------------------------------------------------------*/ + + function createByte(codePoint, shift) { + return stringFromCharCode(((codePoint >> shift) & 0x3F) | 0x80); + } + + function encodeCodePoint(codePoint) { + if ((codePoint & 0xFFFFFF80) == 0) { // 1-byte sequence + return stringFromCharCode(codePoint); + } + var symbol = ''; + if ((codePoint & 0xFFFFF800) == 0) { // 2-byte sequence + symbol = stringFromCharCode(((codePoint >> 6) & 0x1F) | 0xC0); + } + else if ((codePoint & 0xFFFF0000) == 0) { // 3-byte sequence + checkScalarValue(codePoint); + symbol = stringFromCharCode(((codePoint >> 12) & 0x0F) | 0xE0); + symbol += createByte(codePoint, 6); + } + else if ((codePoint & 0xFFE00000) == 0) { // 4-byte sequence + symbol = stringFromCharCode(((codePoint >> 18) & 0x07) | 0xF0); + symbol += createByte(codePoint, 12); + symbol += createByte(codePoint, 6); + } + symbol += stringFromCharCode((codePoint & 0x3F) | 0x80); + return symbol; + } + + function utf8encode(string) { + var codePoints = ucs2decode(string); + var length = codePoints.length; + var index = -1; + var codePoint; + var byteString = ''; + while (++index < length) { + codePoint = codePoints[index]; + byteString += encodeCodePoint(codePoint); + } + return byteString; + } + + /*--------------------------------------------------------------------------*/ + + function readContinuationByte() { + if (byteIndex >= byteCount) { + throw Error('Invalid byte index'); + } + + var continuationByte = byteArray[byteIndex] & 0xFF; + byteIndex++; + + if ((continuationByte & 0xC0) == 0x80) { + return continuationByte & 0x3F; + } + + // If we end up here, it’s not a continuation byte + throw Error('Invalid continuation byte'); + } + + function decodeSymbol() { + var byte1; + var byte2; + var byte3; + var byte4; + var codePoint; + + if (byteIndex > byteCount) { + throw Error('Invalid byte index'); + } + + if (byteIndex == byteCount) { + return false; + } + + // Read first byte + byte1 = byteArray[byteIndex] & 0xFF; + byteIndex++; + + // 1-byte sequence (no continuation bytes) + if ((byte1 & 0x80) == 0) { + return byte1; + } + + // 2-byte sequence + if ((byte1 & 0xE0) == 0xC0) { + var byte2 = readContinuationByte(); + codePoint = ((byte1 & 0x1F) << 6) | byte2; + if (codePoint >= 0x80) { + return codePoint; + } else { + throw Error('Invalid continuation byte'); + } + } + + // 3-byte sequence (may include unpaired surrogates) + if ((byte1 & 0xF0) == 0xE0) { + byte2 = readContinuationByte(); + byte3 = readContinuationByte(); + codePoint = ((byte1 & 0x0F) << 12) | (byte2 << 6) | byte3; + if (codePoint >= 0x0800) { + checkScalarValue(codePoint); + return codePoint; + } else { + throw Error('Invalid continuation byte'); + } + } + + // 4-byte sequence + if ((byte1 & 0xF8) == 0xF0) { + byte2 = readContinuationByte(); + byte3 = readContinuationByte(); + byte4 = readContinuationByte(); + codePoint = ((byte1 & 0x0F) << 0x12) | (byte2 << 0x0C) | + (byte3 << 0x06) | byte4; + if (codePoint >= 0x010000 && codePoint <= 0x10FFFF) { + return codePoint; + } + } + + throw Error('Invalid UTF-8 detected'); + } + + var byteArray; + var byteCount; + var byteIndex; + function utf8decode(byteString) { + byteArray = ucs2decode(byteString); + byteCount = byteArray.length; + byteIndex = 0; + var codePoints = []; + var tmp; + while ((tmp = decodeSymbol()) !== false) { + codePoints.push(tmp); + } + return ucs2encode(codePoints); + } + + /*--------------------------------------------------------------------------*/ + + var utf8 = { + 'version': '2.0.0', + 'encode': utf8encode, + 'decode': utf8decode + }; + + // Some AMD build optimizers, like r.js, check for specific condition patterns + // like the following: + if ( + typeof define == 'function' && + typeof define.amd == 'object' && + define.amd + ) { + define(function() { + return utf8; + }); + } else if (freeExports && !freeExports.nodeType) { + if (freeModule) { // in Node.js or RingoJS v0.8.0+ + freeModule.exports = utf8; + } else { // in Narwhal or RingoJS v0.7.0- + var object = {}; + var hasOwnProperty = object.hasOwnProperty; + for (var key in utf8) { + hasOwnProperty.call(utf8, key) && (freeExports[key] = utf8[key]); + } + } + } else { // in Rhino or a web browser + root.utf8 = utf8; + } + +}(this)); +/** + Copyright 2015 Gordon Williams (gw@pur3.co.uk), + Victor Nakoryakov (victor@amperka.ru) + + This Source Code is subject to the terms of the Mozilla Public + License, v2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + + ------------------------------------------------------------------ + Escape non-ASCII characters into \xHH UTF-8 sequences before send + ------------------------------------------------------------------ +**/ +"use strict"; +(function(){ + + // Node.js doesn't have utf8 installed + var utf8lib; + if ("undefined"==typeof utf8) { + if ("undefined"!=typeof require) { + console.log("Loading UTF8 with require"); + utf8lib = require('utf8'); + } else { + console.log("WARNING: Loading placeholder UTF8"); + utf8lib = { encode : function(c){return c} }; + } + } else { + console.log("UTF8 Library loaded successfully"); + utf8lib = utf8; + } + + function init() { + Espruino.addProcessor("transformForEspruino", function(code, callback) { + escapeUnicode(code, callback); + }); + } + + function escapeUnicode(code, callback) { + // Only correct unicode inside strings + var newCode = ""; + var lex = Espruino.Core.Utils.getLexer(code); + var lastIdx = 0; + var tok = lex.next(); + while (tok!==undefined) { + var previousString = code.substring(lastIdx, tok.startIdx); + var tokenString = code.substring(tok.startIdx, tok.endIdx); + if (tok.type=="STRING") { + var newTokenString = ""; + for (var i=0;i= 255) + newTokenString += escapeChar(tokenString[i]); + else + newTokenString += tokenString[i]; + } + tokenString = newTokenString; + } + newCode += previousString+tokenString; + // next + lastIdx = tok.endIdx; + tok = lex.next(); + } + newCode += code.substring(lastIdx); + callback(newCode); + } + + function escapeChar(c) { + // encode char into UTF-8 sequence in form of \xHH codes + var result = ''; + utf8lib.encode(c).split('').forEach(function(c) { + var code = c.charCodeAt(0) & 0xFF; + result += "\\x"; + if (code < 0x10) result += '0'; + result += code.toString(16).toUpperCase(); + }); + + return result; + } + + Espruino.Plugins.Unicode = { + init : init, + }; +}()); +(function webpackUniversalModuleDefinition(root, factory) { +/* istanbul ignore next */ + if(typeof exports === 'object' && typeof module === 'object') + module.exports = factory(); + else if(typeof define === 'function' && define.amd) + define([], factory); +/* istanbul ignore next */ + else if(typeof exports === 'object') + exports["esprima"] = factory(); + else + root["esprima"] = factory(); +})(this, function() { +return /******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; + +/******/ // The require function +/******/ function __webpack_require__(moduleId) { + +/******/ // Check if module is in cache +/* istanbul ignore if */ +/******/ if(installedModules[moduleId]) +/******/ return installedModules[moduleId].exports; + +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ exports: {}, +/******/ id: moduleId, +/******/ loaded: false +/******/ }; + +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); + +/******/ // Flag the module as loaded +/******/ module.loaded = true; + +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } + + +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; + +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; + +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; + +/******/ // Load entry module and return exports +/******/ return __webpack_require__(0); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + /* + Copyright JS Foundation and other contributors, https://js.foundation/ + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + Object.defineProperty(exports, "__esModule", { value: true }); + var comment_handler_1 = __webpack_require__(1); + var jsx_parser_1 = __webpack_require__(3); + var parser_1 = __webpack_require__(8); + var tokenizer_1 = __webpack_require__(15); + function parse(code, options, delegate) { + var commentHandler = null; + var proxyDelegate = function (node, metadata) { + if (delegate) { + delegate(node, metadata); + } + if (commentHandler) { + commentHandler.visit(node, metadata); + } + }; + var parserDelegate = (typeof delegate === 'function') ? proxyDelegate : null; + var collectComment = false; + if (options) { + collectComment = (typeof options.comment === 'boolean' && options.comment); + var attachComment = (typeof options.attachComment === 'boolean' && options.attachComment); + if (collectComment || attachComment) { + commentHandler = new comment_handler_1.CommentHandler(); + commentHandler.attach = attachComment; + options.comment = true; + parserDelegate = proxyDelegate; + } + } + var isModule = false; + if (options && typeof options.sourceType === 'string') { + isModule = (options.sourceType === 'module'); + } + var parser; + if (options && typeof options.jsx === 'boolean' && options.jsx) { + parser = new jsx_parser_1.JSXParser(code, options, parserDelegate); + } + else { + parser = new parser_1.Parser(code, options, parserDelegate); + } + var program = isModule ? parser.parseModule() : parser.parseScript(); + var ast = program; + if (collectComment && commentHandler) { + ast.comments = commentHandler.comments; + } + if (parser.config.tokens) { + ast.tokens = parser.tokens; + } + if (parser.config.tolerant) { + ast.errors = parser.errorHandler.errors; + } + return ast; + } + exports.parse = parse; + function parseModule(code, options, delegate) { + var parsingOptions = options || {}; + parsingOptions.sourceType = 'module'; + return parse(code, parsingOptions, delegate); + } + exports.parseModule = parseModule; + function parseScript(code, options, delegate) { + var parsingOptions = options || {}; + parsingOptions.sourceType = 'script'; + return parse(code, parsingOptions, delegate); + } + exports.parseScript = parseScript; + function tokenize(code, options, delegate) { + var tokenizer = new tokenizer_1.Tokenizer(code, options); + var tokens; + tokens = []; + try { + while (true) { + var token = tokenizer.getNextToken(); + if (!token) { + break; + } + if (delegate) { + token = delegate(token); + } + tokens.push(token); + } + } + catch (e) { + tokenizer.errorHandler.tolerate(e); + } + if (tokenizer.errorHandler.tolerant) { + tokens.errors = tokenizer.errors(); + } + return tokens; + } + exports.tokenize = tokenize; + var syntax_1 = __webpack_require__(2); + exports.Syntax = syntax_1.Syntax; + // Sync with *.json manifests. + exports.version = '4.0.1'; + + +/***/ }, +/* 1 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + var syntax_1 = __webpack_require__(2); + var CommentHandler = (function () { + function CommentHandler() { + this.attach = false; + this.comments = []; + this.stack = []; + this.leading = []; + this.trailing = []; + } + CommentHandler.prototype.insertInnerComments = function (node, metadata) { + // innnerComments for properties empty block + // `function a() {/** comments **\/}` + if (node.type === syntax_1.Syntax.BlockStatement && node.body.length === 0) { + var innerComments = []; + for (var i = this.leading.length - 1; i >= 0; --i) { + var entry = this.leading[i]; + if (metadata.end.offset >= entry.start) { + innerComments.unshift(entry.comment); + this.leading.splice(i, 1); + this.trailing.splice(i, 1); + } + } + if (innerComments.length) { + node.innerComments = innerComments; + } + } + }; + CommentHandler.prototype.findTrailingComments = function (metadata) { + var trailingComments = []; + if (this.trailing.length > 0) { + for (var i = this.trailing.length - 1; i >= 0; --i) { + var entry_1 = this.trailing[i]; + if (entry_1.start >= metadata.end.offset) { + trailingComments.unshift(entry_1.comment); + } + } + this.trailing.length = 0; + return trailingComments; + } + var entry = this.stack[this.stack.length - 1]; + if (entry && entry.node.trailingComments) { + var firstComment = entry.node.trailingComments[0]; + if (firstComment && firstComment.range[0] >= metadata.end.offset) { + trailingComments = entry.node.trailingComments; + delete entry.node.trailingComments; + } + } + return trailingComments; + }; + CommentHandler.prototype.findLeadingComments = function (metadata) { + var leadingComments = []; + var target; + while (this.stack.length > 0) { + var entry = this.stack[this.stack.length - 1]; + if (entry && entry.start >= metadata.start.offset) { + target = entry.node; + this.stack.pop(); + } + else { + break; + } + } + if (target) { + var count = target.leadingComments ? target.leadingComments.length : 0; + for (var i = count - 1; i >= 0; --i) { + var comment = target.leadingComments[i]; + if (comment.range[1] <= metadata.start.offset) { + leadingComments.unshift(comment); + target.leadingComments.splice(i, 1); + } + } + if (target.leadingComments && target.leadingComments.length === 0) { + delete target.leadingComments; + } + return leadingComments; + } + for (var i = this.leading.length - 1; i >= 0; --i) { + var entry = this.leading[i]; + if (entry.start <= metadata.start.offset) { + leadingComments.unshift(entry.comment); + this.leading.splice(i, 1); + } + } + return leadingComments; + }; + CommentHandler.prototype.visitNode = function (node, metadata) { + if (node.type === syntax_1.Syntax.Program && node.body.length > 0) { + return; + } + this.insertInnerComments(node, metadata); + var trailingComments = this.findTrailingComments(metadata); + var leadingComments = this.findLeadingComments(metadata); + if (leadingComments.length > 0) { + node.leadingComments = leadingComments; + } + if (trailingComments.length > 0) { + node.trailingComments = trailingComments; + } + this.stack.push({ + node: node, + start: metadata.start.offset + }); + }; + CommentHandler.prototype.visitComment = function (node, metadata) { + var type = (node.type[0] === 'L') ? 'Line' : 'Block'; + var comment = { + type: type, + value: node.value + }; + if (node.range) { + comment.range = node.range; + } + if (node.loc) { + comment.loc = node.loc; + } + this.comments.push(comment); + if (this.attach) { + var entry = { + comment: { + type: type, + value: node.value, + range: [metadata.start.offset, metadata.end.offset] + }, + start: metadata.start.offset + }; + if (node.loc) { + entry.comment.loc = node.loc; + } + node.type = type; + this.leading.push(entry); + this.trailing.push(entry); + } + }; + CommentHandler.prototype.visit = function (node, metadata) { + if (node.type === 'LineComment') { + this.visitComment(node, metadata); + } + else if (node.type === 'BlockComment') { + this.visitComment(node, metadata); + } + else if (this.attach) { + this.visitNode(node, metadata); + } + }; + return CommentHandler; + }()); + exports.CommentHandler = CommentHandler; + + +/***/ }, +/* 2 */ +/***/ function(module, exports) { + + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.Syntax = { + AssignmentExpression: 'AssignmentExpression', + AssignmentPattern: 'AssignmentPattern', + ArrayExpression: 'ArrayExpression', + ArrayPattern: 'ArrayPattern', + ArrowFunctionExpression: 'ArrowFunctionExpression', + AwaitExpression: 'AwaitExpression', + BlockStatement: 'BlockStatement', + BinaryExpression: 'BinaryExpression', + BreakStatement: 'BreakStatement', + CallExpression: 'CallExpression', + CatchClause: 'CatchClause', + ClassBody: 'ClassBody', + ClassDeclaration: 'ClassDeclaration', + ClassExpression: 'ClassExpression', + ConditionalExpression: 'ConditionalExpression', + ContinueStatement: 'ContinueStatement', + DoWhileStatement: 'DoWhileStatement', + DebuggerStatement: 'DebuggerStatement', + EmptyStatement: 'EmptyStatement', + ExportAllDeclaration: 'ExportAllDeclaration', + ExportDefaultDeclaration: 'ExportDefaultDeclaration', + ExportNamedDeclaration: 'ExportNamedDeclaration', + ExportSpecifier: 'ExportSpecifier', + ExpressionStatement: 'ExpressionStatement', + ForStatement: 'ForStatement', + ForOfStatement: 'ForOfStatement', + ForInStatement: 'ForInStatement', + FunctionDeclaration: 'FunctionDeclaration', + FunctionExpression: 'FunctionExpression', + Identifier: 'Identifier', + IfStatement: 'IfStatement', + ImportDeclaration: 'ImportDeclaration', + ImportDefaultSpecifier: 'ImportDefaultSpecifier', + ImportNamespaceSpecifier: 'ImportNamespaceSpecifier', + ImportSpecifier: 'ImportSpecifier', + Literal: 'Literal', + LabeledStatement: 'LabeledStatement', + LogicalExpression: 'LogicalExpression', + MemberExpression: 'MemberExpression', + MetaProperty: 'MetaProperty', + MethodDefinition: 'MethodDefinition', + NewExpression: 'NewExpression', + ObjectExpression: 'ObjectExpression', + ObjectPattern: 'ObjectPattern', + Program: 'Program', + Property: 'Property', + RestElement: 'RestElement', + ReturnStatement: 'ReturnStatement', + SequenceExpression: 'SequenceExpression', + SpreadElement: 'SpreadElement', + Super: 'Super', + SwitchCase: 'SwitchCase', + SwitchStatement: 'SwitchStatement', + TaggedTemplateExpression: 'TaggedTemplateExpression', + TemplateElement: 'TemplateElement', + TemplateLiteral: 'TemplateLiteral', + ThisExpression: 'ThisExpression', + ThrowStatement: 'ThrowStatement', + TryStatement: 'TryStatement', + UnaryExpression: 'UnaryExpression', + UpdateExpression: 'UpdateExpression', + VariableDeclaration: 'VariableDeclaration', + VariableDeclarator: 'VariableDeclarator', + WhileStatement: 'WhileStatement', + WithStatement: 'WithStatement', + YieldExpression: 'YieldExpression' + }; + + +/***/ }, +/* 3 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; +/* istanbul ignore next */ + var __extends = (this && this.__extends) || (function () { + var extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; + })(); + Object.defineProperty(exports, "__esModule", { value: true }); + var character_1 = __webpack_require__(4); + var JSXNode = __webpack_require__(5); + var jsx_syntax_1 = __webpack_require__(6); + var Node = __webpack_require__(7); + var parser_1 = __webpack_require__(8); + var token_1 = __webpack_require__(13); + var xhtml_entities_1 = __webpack_require__(14); + token_1.TokenName[100 /* Identifier */] = 'JSXIdentifier'; + token_1.TokenName[101 /* Text */] = 'JSXText'; + // Fully qualified element name, e.g. returns "svg:path" + function getQualifiedElementName(elementName) { + var qualifiedName; + switch (elementName.type) { + case jsx_syntax_1.JSXSyntax.JSXIdentifier: + var id = elementName; + qualifiedName = id.name; + break; + case jsx_syntax_1.JSXSyntax.JSXNamespacedName: + var ns = elementName; + qualifiedName = getQualifiedElementName(ns.namespace) + ':' + + getQualifiedElementName(ns.name); + break; + case jsx_syntax_1.JSXSyntax.JSXMemberExpression: + var expr = elementName; + qualifiedName = getQualifiedElementName(expr.object) + '.' + + getQualifiedElementName(expr.property); + break; + /* istanbul ignore next */ + default: + break; + } + return qualifiedName; + } + var JSXParser = (function (_super) { + __extends(JSXParser, _super); + function JSXParser(code, options, delegate) { + return _super.call(this, code, options, delegate) || this; + } + JSXParser.prototype.parsePrimaryExpression = function () { + return this.match('<') ? this.parseJSXRoot() : _super.prototype.parsePrimaryExpression.call(this); + }; + JSXParser.prototype.startJSX = function () { + // Unwind the scanner before the lookahead token. + this.scanner.index = this.startMarker.index; + this.scanner.lineNumber = this.startMarker.line; + this.scanner.lineStart = this.startMarker.index - this.startMarker.column; + }; + JSXParser.prototype.finishJSX = function () { + // Prime the next lookahead. + this.nextToken(); + }; + JSXParser.prototype.reenterJSX = function () { + this.startJSX(); + this.expectJSX('}'); + // Pop the closing '}' added from the lookahead. + if (this.config.tokens) { + this.tokens.pop(); + } + }; + JSXParser.prototype.createJSXNode = function () { + this.collectComments(); + return { + index: this.scanner.index, + line: this.scanner.lineNumber, + column: this.scanner.index - this.scanner.lineStart + }; + }; + JSXParser.prototype.createJSXChildNode = function () { + return { + index: this.scanner.index, + line: this.scanner.lineNumber, + column: this.scanner.index - this.scanner.lineStart + }; + }; + JSXParser.prototype.scanXHTMLEntity = function (quote) { + var result = '&'; + var valid = true; + var terminated = false; + var numeric = false; + var hex = false; + while (!this.scanner.eof() && valid && !terminated) { + var ch = this.scanner.source[this.scanner.index]; + if (ch === quote) { + break; + } + terminated = (ch === ';'); + result += ch; + ++this.scanner.index; + if (!terminated) { + switch (result.length) { + case 2: + // e.g. '{' + numeric = (ch === '#'); + break; + case 3: + if (numeric) { + // e.g. 'A' + hex = (ch === 'x'); + valid = hex || character_1.Character.isDecimalDigit(ch.charCodeAt(0)); + numeric = numeric && !hex; + } + break; + default: + valid = valid && !(numeric && !character_1.Character.isDecimalDigit(ch.charCodeAt(0))); + valid = valid && !(hex && !character_1.Character.isHexDigit(ch.charCodeAt(0))); + break; + } + } + } + if (valid && terminated && result.length > 2) { + // e.g. 'A' becomes just '#x41' + var str = result.substr(1, result.length - 2); + if (numeric && str.length > 1) { + result = String.fromCharCode(parseInt(str.substr(1), 10)); + } + else if (hex && str.length > 2) { + result = String.fromCharCode(parseInt('0' + str.substr(1), 16)); + } + else if (!numeric && !hex && xhtml_entities_1.XHTMLEntities[str]) { + result = xhtml_entities_1.XHTMLEntities[str]; + } + } + return result; + }; + // Scan the next JSX token. This replaces Scanner#lex when in JSX mode. + JSXParser.prototype.lexJSX = function () { + var cp = this.scanner.source.charCodeAt(this.scanner.index); + // < > / : = { } + if (cp === 60 || cp === 62 || cp === 47 || cp === 58 || cp === 61 || cp === 123 || cp === 125) { + var value = this.scanner.source[this.scanner.index++]; + return { + type: 7 /* Punctuator */, + value: value, + lineNumber: this.scanner.lineNumber, + lineStart: this.scanner.lineStart, + start: this.scanner.index - 1, + end: this.scanner.index + }; + } + // " ' + if (cp === 34 || cp === 39) { + var start = this.scanner.index; + var quote = this.scanner.source[this.scanner.index++]; + var str = ''; + while (!this.scanner.eof()) { + var ch = this.scanner.source[this.scanner.index++]; + if (ch === quote) { + break; + } + else if (ch === '&') { + str += this.scanXHTMLEntity(quote); + } + else { + str += ch; + } + } + return { + type: 8 /* StringLiteral */, + value: str, + lineNumber: this.scanner.lineNumber, + lineStart: this.scanner.lineStart, + start: start, + end: this.scanner.index + }; + } + // ... or . + if (cp === 46) { + var n1 = this.scanner.source.charCodeAt(this.scanner.index + 1); + var n2 = this.scanner.source.charCodeAt(this.scanner.index + 2); + var value = (n1 === 46 && n2 === 46) ? '...' : '.'; + var start = this.scanner.index; + this.scanner.index += value.length; + return { + type: 7 /* Punctuator */, + value: value, + lineNumber: this.scanner.lineNumber, + lineStart: this.scanner.lineStart, + start: start, + end: this.scanner.index + }; + } + // ` + if (cp === 96) { + // Only placeholder, since it will be rescanned as a real assignment expression. + return { + type: 10 /* Template */, + value: '', + lineNumber: this.scanner.lineNumber, + lineStart: this.scanner.lineStart, + start: this.scanner.index, + end: this.scanner.index + }; + } + // Identifer can not contain backslash (char code 92). + if (character_1.Character.isIdentifierStart(cp) && (cp !== 92)) { + var start = this.scanner.index; + ++this.scanner.index; + while (!this.scanner.eof()) { + var ch = this.scanner.source.charCodeAt(this.scanner.index); + if (character_1.Character.isIdentifierPart(ch) && (ch !== 92)) { + ++this.scanner.index; + } + else if (ch === 45) { + // Hyphen (char code 45) can be part of an identifier. + ++this.scanner.index; + } + else { + break; + } + } + var id = this.scanner.source.slice(start, this.scanner.index); + return { + type: 100 /* Identifier */, + value: id, + lineNumber: this.scanner.lineNumber, + lineStart: this.scanner.lineStart, + start: start, + end: this.scanner.index + }; + } + return this.scanner.lex(); + }; + JSXParser.prototype.nextJSXToken = function () { + this.collectComments(); + this.startMarker.index = this.scanner.index; + this.startMarker.line = this.scanner.lineNumber; + this.startMarker.column = this.scanner.index - this.scanner.lineStart; + var token = this.lexJSX(); + this.lastMarker.index = this.scanner.index; + this.lastMarker.line = this.scanner.lineNumber; + this.lastMarker.column = this.scanner.index - this.scanner.lineStart; + if (this.config.tokens) { + this.tokens.push(this.convertToken(token)); + } + return token; + }; + JSXParser.prototype.nextJSXText = function () { + this.startMarker.index = this.scanner.index; + this.startMarker.line = this.scanner.lineNumber; + this.startMarker.column = this.scanner.index - this.scanner.lineStart; + var start = this.scanner.index; + var text = ''; + while (!this.scanner.eof()) { + var ch = this.scanner.source[this.scanner.index]; + if (ch === '{' || ch === '<') { + break; + } + ++this.scanner.index; + text += ch; + if (character_1.Character.isLineTerminator(ch.charCodeAt(0))) { + ++this.scanner.lineNumber; + if (ch === '\r' && this.scanner.source[this.scanner.index] === '\n') { + ++this.scanner.index; + } + this.scanner.lineStart = this.scanner.index; + } + } + this.lastMarker.index = this.scanner.index; + this.lastMarker.line = this.scanner.lineNumber; + this.lastMarker.column = this.scanner.index - this.scanner.lineStart; + var token = { + type: 101 /* Text */, + value: text, + lineNumber: this.scanner.lineNumber, + lineStart: this.scanner.lineStart, + start: start, + end: this.scanner.index + }; + if ((text.length > 0) && this.config.tokens) { + this.tokens.push(this.convertToken(token)); + } + return token; + }; + JSXParser.prototype.peekJSXToken = function () { + var state = this.scanner.saveState(); + this.scanner.scanComments(); + var next = this.lexJSX(); + this.scanner.restoreState(state); + return next; + }; + // Expect the next JSX token to match the specified punctuator. + // If not, an exception will be thrown. + JSXParser.prototype.expectJSX = function (value) { + var token = this.nextJSXToken(); + if (token.type !== 7 /* Punctuator */ || token.value !== value) { + this.throwUnexpectedToken(token); + } + }; + // Return true if the next JSX token matches the specified punctuator. + JSXParser.prototype.matchJSX = function (value) { + var next = this.peekJSXToken(); + return next.type === 7 /* Punctuator */ && next.value === value; + }; + JSXParser.prototype.parseJSXIdentifier = function () { + var node = this.createJSXNode(); + var token = this.nextJSXToken(); + if (token.type !== 100 /* Identifier */) { + this.throwUnexpectedToken(token); + } + return this.finalize(node, new JSXNode.JSXIdentifier(token.value)); + }; + JSXParser.prototype.parseJSXElementName = function () { + var node = this.createJSXNode(); + var elementName = this.parseJSXIdentifier(); + if (this.matchJSX(':')) { + var namespace = elementName; + this.expectJSX(':'); + var name_1 = this.parseJSXIdentifier(); + elementName = this.finalize(node, new JSXNode.JSXNamespacedName(namespace, name_1)); + } + else if (this.matchJSX('.')) { + while (this.matchJSX('.')) { + var object = elementName; + this.expectJSX('.'); + var property = this.parseJSXIdentifier(); + elementName = this.finalize(node, new JSXNode.JSXMemberExpression(object, property)); + } + } + return elementName; + }; + JSXParser.prototype.parseJSXAttributeName = function () { + var node = this.createJSXNode(); + var attributeName; + var identifier = this.parseJSXIdentifier(); + if (this.matchJSX(':')) { + var namespace = identifier; + this.expectJSX(':'); + var name_2 = this.parseJSXIdentifier(); + attributeName = this.finalize(node, new JSXNode.JSXNamespacedName(namespace, name_2)); + } + else { + attributeName = identifier; + } + return attributeName; + }; + JSXParser.prototype.parseJSXStringLiteralAttribute = function () { + var node = this.createJSXNode(); + var token = this.nextJSXToken(); + if (token.type !== 8 /* StringLiteral */) { + this.throwUnexpectedToken(token); + } + var raw = this.getTokenRaw(token); + return this.finalize(node, new Node.Literal(token.value, raw)); + }; + JSXParser.prototype.parseJSXExpressionAttribute = function () { + var node = this.createJSXNode(); + this.expectJSX('{'); + this.finishJSX(); + if (this.match('}')) { + this.tolerateError('JSX attributes must only be assigned a non-empty expression'); + } + var expression = this.parseAssignmentExpression(); + this.reenterJSX(); + return this.finalize(node, new JSXNode.JSXExpressionContainer(expression)); + }; + JSXParser.prototype.parseJSXAttributeValue = function () { + return this.matchJSX('{') ? this.parseJSXExpressionAttribute() : + this.matchJSX('<') ? this.parseJSXElement() : this.parseJSXStringLiteralAttribute(); + }; + JSXParser.prototype.parseJSXNameValueAttribute = function () { + var node = this.createJSXNode(); + var name = this.parseJSXAttributeName(); + var value = null; + if (this.matchJSX('=')) { + this.expectJSX('='); + value = this.parseJSXAttributeValue(); + } + return this.finalize(node, new JSXNode.JSXAttribute(name, value)); + }; + JSXParser.prototype.parseJSXSpreadAttribute = function () { + var node = this.createJSXNode(); + this.expectJSX('{'); + this.expectJSX('...'); + this.finishJSX(); + var argument = this.parseAssignmentExpression(); + this.reenterJSX(); + return this.finalize(node, new JSXNode.JSXSpreadAttribute(argument)); + }; + JSXParser.prototype.parseJSXAttributes = function () { + var attributes = []; + while (!this.matchJSX('/') && !this.matchJSX('>')) { + var attribute = this.matchJSX('{') ? this.parseJSXSpreadAttribute() : + this.parseJSXNameValueAttribute(); + attributes.push(attribute); + } + return attributes; + }; + JSXParser.prototype.parseJSXOpeningElement = function () { + var node = this.createJSXNode(); + this.expectJSX('<'); + var name = this.parseJSXElementName(); + var attributes = this.parseJSXAttributes(); + var selfClosing = this.matchJSX('/'); + if (selfClosing) { + this.expectJSX('/'); + } + this.expectJSX('>'); + return this.finalize(node, new JSXNode.JSXOpeningElement(name, selfClosing, attributes)); + }; + JSXParser.prototype.parseJSXBoundaryElement = function () { + var node = this.createJSXNode(); + this.expectJSX('<'); + if (this.matchJSX('/')) { + this.expectJSX('/'); + var name_3 = this.parseJSXElementName(); + this.expectJSX('>'); + return this.finalize(node, new JSXNode.JSXClosingElement(name_3)); + } + var name = this.parseJSXElementName(); + var attributes = this.parseJSXAttributes(); + var selfClosing = this.matchJSX('/'); + if (selfClosing) { + this.expectJSX('/'); + } + this.expectJSX('>'); + return this.finalize(node, new JSXNode.JSXOpeningElement(name, selfClosing, attributes)); + }; + JSXParser.prototype.parseJSXEmptyExpression = function () { + var node = this.createJSXChildNode(); + this.collectComments(); + this.lastMarker.index = this.scanner.index; + this.lastMarker.line = this.scanner.lineNumber; + this.lastMarker.column = this.scanner.index - this.scanner.lineStart; + return this.finalize(node, new JSXNode.JSXEmptyExpression()); + }; + JSXParser.prototype.parseJSXExpressionContainer = function () { + var node = this.createJSXNode(); + this.expectJSX('{'); + var expression; + if (this.matchJSX('}')) { + expression = this.parseJSXEmptyExpression(); + this.expectJSX('}'); + } + else { + this.finishJSX(); + expression = this.parseAssignmentExpression(); + this.reenterJSX(); + } + return this.finalize(node, new JSXNode.JSXExpressionContainer(expression)); + }; + JSXParser.prototype.parseJSXChildren = function () { + var children = []; + while (!this.scanner.eof()) { + var node = this.createJSXChildNode(); + var token = this.nextJSXText(); + if (token.start < token.end) { + var raw = this.getTokenRaw(token); + var child = this.finalize(node, new JSXNode.JSXText(token.value, raw)); + children.push(child); + } + if (this.scanner.source[this.scanner.index] === '{') { + var container = this.parseJSXExpressionContainer(); + children.push(container); + } + else { + break; + } + } + return children; + }; + JSXParser.prototype.parseComplexJSXElement = function (el) { + var stack = []; + while (!this.scanner.eof()) { + el.children = el.children.concat(this.parseJSXChildren()); + var node = this.createJSXChildNode(); + var element = this.parseJSXBoundaryElement(); + if (element.type === jsx_syntax_1.JSXSyntax.JSXOpeningElement) { + var opening = element; + if (opening.selfClosing) { + var child = this.finalize(node, new JSXNode.JSXElement(opening, [], null)); + el.children.push(child); + } + else { + stack.push(el); + el = { node: node, opening: opening, closing: null, children: [] }; + } + } + if (element.type === jsx_syntax_1.JSXSyntax.JSXClosingElement) { + el.closing = element; + var open_1 = getQualifiedElementName(el.opening.name); + var close_1 = getQualifiedElementName(el.closing.name); + if (open_1 !== close_1) { + this.tolerateError('Expected corresponding JSX closing tag for %0', open_1); + } + if (stack.length > 0) { + var child = this.finalize(el.node, new JSXNode.JSXElement(el.opening, el.children, el.closing)); + el = stack[stack.length - 1]; + el.children.push(child); + stack.pop(); + } + else { + break; + } + } + } + return el; + }; + JSXParser.prototype.parseJSXElement = function () { + var node = this.createJSXNode(); + var opening = this.parseJSXOpeningElement(); + var children = []; + var closing = null; + if (!opening.selfClosing) { + var el = this.parseComplexJSXElement({ node: node, opening: opening, closing: closing, children: children }); + children = el.children; + closing = el.closing; + } + return this.finalize(node, new JSXNode.JSXElement(opening, children, closing)); + }; + JSXParser.prototype.parseJSXRoot = function () { + // Pop the opening '<' added from the lookahead. + if (this.config.tokens) { + this.tokens.pop(); + } + this.startJSX(); + var element = this.parseJSXElement(); + this.finishJSX(); + return element; + }; + JSXParser.prototype.isStartOfExpression = function () { + return _super.prototype.isStartOfExpression.call(this) || this.match('<'); + }; + return JSXParser; + }(parser_1.Parser)); + exports.JSXParser = JSXParser; + + +/***/ }, +/* 4 */ +/***/ function(module, exports) { + + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + // See also tools/generate-unicode-regex.js. + var Regex = { + // Unicode v8.0.0 NonAsciiIdentifierStart: + NonAsciiIdentifierStart: /[\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0-\u08B4\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2118-\u211D\u2124\u2126\u2128\u212A-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309B-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FD5\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AD\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF30-\uDF4A\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2]|\uD804[\uDC03-\uDC37\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDF00-\uDF19]|\uD806[\uDCA0-\uDCDF\uDCFF\uDEC0-\uDEF8]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50\uDF93-\uDF9F]|\uD82C[\uDC00\uDC01]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD83A[\uDC00-\uDCC4]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1]|\uD87E[\uDC00-\uDE1D]/, + // Unicode v8.0.0 NonAsciiIdentifierPart: + NonAsciiIdentifierPart: /[\xAA\xB5\xB7\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0300-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u0483-\u0487\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u05D0-\u05EA\u05F0-\u05F2\u0610-\u061A\u0620-\u0669\u066E-\u06D3\u06D5-\u06DC\u06DF-\u06E8\u06EA-\u06FC\u06FF\u0710-\u074A\u074D-\u07B1\u07C0-\u07F5\u07FA\u0800-\u082D\u0840-\u085B\u08A0-\u08B4\u08E3-\u0963\u0966-\u096F\u0971-\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BC-\u09C4\u09C7\u09C8\u09CB-\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E3\u09E6-\u09F1\u0A01-\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A59-\u0A5C\u0A5E\u0A66-\u0A75\u0A81-\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABC-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AD0\u0AE0-\u0AE3\u0AE6-\u0AEF\u0AF9\u0B01-\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3C-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B5C\u0B5D\u0B5F-\u0B63\u0B66-\u0B6F\u0B71\u0B82\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD0\u0BD7\u0BE6-\u0BEF\u0C00-\u0C03\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C58-\u0C5A\u0C60-\u0C63\u0C66-\u0C6F\u0C81-\u0C83\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBC-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CDE\u0CE0-\u0CE3\u0CE6-\u0CEF\u0CF1\u0CF2\u0D01-\u0D03\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D-\u0D44\u0D46-\u0D48\u0D4A-\u0D4E\u0D57\u0D5F-\u0D63\u0D66-\u0D6F\u0D7A-\u0D7F\u0D82\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DE6-\u0DEF\u0DF2\u0DF3\u0E01-\u0E3A\u0E40-\u0E4E\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB9\u0EBB-\u0EBD\u0EC0-\u0EC4\u0EC6\u0EC8-\u0ECD\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F18\u0F19\u0F20-\u0F29\u0F35\u0F37\u0F39\u0F3E-\u0F47\u0F49-\u0F6C\u0F71-\u0F84\u0F86-\u0F97\u0F99-\u0FBC\u0FC6\u1000-\u1049\u1050-\u109D\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u135D-\u135F\u1369-\u1371\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1714\u1720-\u1734\u1740-\u1753\u1760-\u176C\u176E-\u1770\u1772\u1773\u1780-\u17D3\u17D7\u17DC\u17DD\u17E0-\u17E9\u180B-\u180D\u1810-\u1819\u1820-\u1877\u1880-\u18AA\u18B0-\u18F5\u1900-\u191E\u1920-\u192B\u1930-\u193B\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19DA\u1A00-\u1A1B\u1A20-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AA7\u1AB0-\u1ABD\u1B00-\u1B4B\u1B50-\u1B59\u1B6B-\u1B73\u1B80-\u1BF3\u1C00-\u1C37\u1C40-\u1C49\u1C4D-\u1C7D\u1CD0-\u1CD2\u1CD4-\u1CF6\u1CF8\u1CF9\u1D00-\u1DF5\u1DFC-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u200C\u200D\u203F\u2040\u2054\u2071\u207F\u2090-\u209C\u20D0-\u20DC\u20E1\u20E5-\u20F0\u2102\u2107\u210A-\u2113\u2115\u2118-\u211D\u2124\u2126\u2128\u212A-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D7F-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2DE0-\u2DFF\u3005-\u3007\u3021-\u302F\u3031-\u3035\u3038-\u303C\u3041-\u3096\u3099-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FD5\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66F\uA674-\uA67D\uA67F-\uA6F1\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AD\uA7B0-\uA7B7\uA7F7-\uA827\uA840-\uA873\uA880-\uA8C4\uA8D0-\uA8D9\uA8E0-\uA8F7\uA8FB\uA8FD\uA900-\uA92D\uA930-\uA953\uA960-\uA97C\uA980-\uA9C0\uA9CF-\uA9D9\uA9E0-\uA9FE\uAA00-\uAA36\uAA40-\uAA4D\uAA50-\uAA59\uAA60-\uAA76\uAA7A-\uAAC2\uAADB-\uAADD\uAAE0-\uAAEF\uAAF2-\uAAF6\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABEA\uABEC\uABED\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE00-\uFE0F\uFE20-\uFE2F\uFE33\uFE34\uFE4D-\uFE4F\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF3F\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDDFD\uDE80-\uDE9C\uDEA0-\uDED0\uDEE0\uDF00-\uDF1F\uDF30-\uDF4A\uDF50-\uDF7A\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCA0-\uDCA9\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00-\uDE03\uDE05\uDE06\uDE0C-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE38-\uDE3A\uDE3F\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE6\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2]|\uD804[\uDC00-\uDC46\uDC66-\uDC6F\uDC7F-\uDCBA\uDCD0-\uDCE8\uDCF0-\uDCF9\uDD00-\uDD34\uDD36-\uDD3F\uDD50-\uDD73\uDD76\uDD80-\uDDC4\uDDCA-\uDDCC\uDDD0-\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE37\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEEA\uDEF0-\uDEF9\uDF00-\uDF03\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3C-\uDF44\uDF47\uDF48\uDF4B-\uDF4D\uDF50\uDF57\uDF5D-\uDF63\uDF66-\uDF6C\uDF70-\uDF74]|\uD805[\uDC80-\uDCC5\uDCC7\uDCD0-\uDCD9\uDD80-\uDDB5\uDDB8-\uDDC0\uDDD8-\uDDDD\uDE00-\uDE40\uDE44\uDE50-\uDE59\uDE80-\uDEB7\uDEC0-\uDEC9\uDF00-\uDF19\uDF1D-\uDF2B\uDF30-\uDF39]|\uD806[\uDCA0-\uDCE9\uDCFF\uDEC0-\uDEF8]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE60-\uDE69\uDED0-\uDEED\uDEF0-\uDEF4\uDF00-\uDF36\uDF40-\uDF43\uDF50-\uDF59\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50-\uDF7E\uDF8F-\uDF9F]|\uD82C[\uDC00\uDC01]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99\uDC9D\uDC9E]|\uD834[\uDD65-\uDD69\uDD6D-\uDD72\uDD7B-\uDD82\uDD85-\uDD8B\uDDAA-\uDDAD\uDE42-\uDE44]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB\uDFCE-\uDFFF]|\uD836[\uDE00-\uDE36\uDE3B-\uDE6C\uDE75\uDE84\uDE9B-\uDE9F\uDEA1-\uDEAF]|\uD83A[\uDC00-\uDCC4\uDCD0-\uDCD6]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1]|\uD87E[\uDC00-\uDE1D]|\uDB40[\uDD00-\uDDEF]/ + }; + exports.Character = { + /* tslint:disable:no-bitwise */ + fromCodePoint: function (cp) { + return (cp < 0x10000) ? String.fromCharCode(cp) : + String.fromCharCode(0xD800 + ((cp - 0x10000) >> 10)) + + String.fromCharCode(0xDC00 + ((cp - 0x10000) & 1023)); + }, + // https://tc39.github.io/ecma262/#sec-white-space + isWhiteSpace: function (cp) { + return (cp === 0x20) || (cp === 0x09) || (cp === 0x0B) || (cp === 0x0C) || (cp === 0xA0) || + (cp >= 0x1680 && [0x1680, 0x2000, 0x2001, 0x2002, 0x2003, 0x2004, 0x2005, 0x2006, 0x2007, 0x2008, 0x2009, 0x200A, 0x202F, 0x205F, 0x3000, 0xFEFF].indexOf(cp) >= 0); + }, + // https://tc39.github.io/ecma262/#sec-line-terminators + isLineTerminator: function (cp) { + return (cp === 0x0A) || (cp === 0x0D) || (cp === 0x2028) || (cp === 0x2029); + }, + // https://tc39.github.io/ecma262/#sec-names-and-keywords + isIdentifierStart: function (cp) { + return (cp === 0x24) || (cp === 0x5F) || + (cp >= 0x41 && cp <= 0x5A) || + (cp >= 0x61 && cp <= 0x7A) || + (cp === 0x5C) || + ((cp >= 0x80) && Regex.NonAsciiIdentifierStart.test(exports.Character.fromCodePoint(cp))); + }, + isIdentifierPart: function (cp) { + return (cp === 0x24) || (cp === 0x5F) || + (cp >= 0x41 && cp <= 0x5A) || + (cp >= 0x61 && cp <= 0x7A) || + (cp >= 0x30 && cp <= 0x39) || + (cp === 0x5C) || + ((cp >= 0x80) && Regex.NonAsciiIdentifierPart.test(exports.Character.fromCodePoint(cp))); + }, + // https://tc39.github.io/ecma262/#sec-literals-numeric-literals + isDecimalDigit: function (cp) { + return (cp >= 0x30 && cp <= 0x39); // 0..9 + }, + isHexDigit: function (cp) { + return (cp >= 0x30 && cp <= 0x39) || + (cp >= 0x41 && cp <= 0x46) || + (cp >= 0x61 && cp <= 0x66); // a..f + }, + isOctalDigit: function (cp) { + return (cp >= 0x30 && cp <= 0x37); // 0..7 + } + }; + + +/***/ }, +/* 5 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + var jsx_syntax_1 = __webpack_require__(6); + /* tslint:disable:max-classes-per-file */ + var JSXClosingElement = (function () { + function JSXClosingElement(name) { + this.type = jsx_syntax_1.JSXSyntax.JSXClosingElement; + this.name = name; + } + return JSXClosingElement; + }()); + exports.JSXClosingElement = JSXClosingElement; + var JSXElement = (function () { + function JSXElement(openingElement, children, closingElement) { + this.type = jsx_syntax_1.JSXSyntax.JSXElement; + this.openingElement = openingElement; + this.children = children; + this.closingElement = closingElement; + } + return JSXElement; + }()); + exports.JSXElement = JSXElement; + var JSXEmptyExpression = (function () { + function JSXEmptyExpression() { + this.type = jsx_syntax_1.JSXSyntax.JSXEmptyExpression; + } + return JSXEmptyExpression; + }()); + exports.JSXEmptyExpression = JSXEmptyExpression; + var JSXExpressionContainer = (function () { + function JSXExpressionContainer(expression) { + this.type = jsx_syntax_1.JSXSyntax.JSXExpressionContainer; + this.expression = expression; + } + return JSXExpressionContainer; + }()); + exports.JSXExpressionContainer = JSXExpressionContainer; + var JSXIdentifier = (function () { + function JSXIdentifier(name) { + this.type = jsx_syntax_1.JSXSyntax.JSXIdentifier; + this.name = name; + } + return JSXIdentifier; + }()); + exports.JSXIdentifier = JSXIdentifier; + var JSXMemberExpression = (function () { + function JSXMemberExpression(object, property) { + this.type = jsx_syntax_1.JSXSyntax.JSXMemberExpression; + this.object = object; + this.property = property; + } + return JSXMemberExpression; + }()); + exports.JSXMemberExpression = JSXMemberExpression; + var JSXAttribute = (function () { + function JSXAttribute(name, value) { + this.type = jsx_syntax_1.JSXSyntax.JSXAttribute; + this.name = name; + this.value = value; + } + return JSXAttribute; + }()); + exports.JSXAttribute = JSXAttribute; + var JSXNamespacedName = (function () { + function JSXNamespacedName(namespace, name) { + this.type = jsx_syntax_1.JSXSyntax.JSXNamespacedName; + this.namespace = namespace; + this.name = name; + } + return JSXNamespacedName; + }()); + exports.JSXNamespacedName = JSXNamespacedName; + var JSXOpeningElement = (function () { + function JSXOpeningElement(name, selfClosing, attributes) { + this.type = jsx_syntax_1.JSXSyntax.JSXOpeningElement; + this.name = name; + this.selfClosing = selfClosing; + this.attributes = attributes; + } + return JSXOpeningElement; + }()); + exports.JSXOpeningElement = JSXOpeningElement; + var JSXSpreadAttribute = (function () { + function JSXSpreadAttribute(argument) { + this.type = jsx_syntax_1.JSXSyntax.JSXSpreadAttribute; + this.argument = argument; + } + return JSXSpreadAttribute; + }()); + exports.JSXSpreadAttribute = JSXSpreadAttribute; + var JSXText = (function () { + function JSXText(value, raw) { + this.type = jsx_syntax_1.JSXSyntax.JSXText; + this.value = value; + this.raw = raw; + } + return JSXText; + }()); + exports.JSXText = JSXText; + + +/***/ }, +/* 6 */ +/***/ function(module, exports) { + + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.JSXSyntax = { + JSXAttribute: 'JSXAttribute', + JSXClosingElement: 'JSXClosingElement', + JSXElement: 'JSXElement', + JSXEmptyExpression: 'JSXEmptyExpression', + JSXExpressionContainer: 'JSXExpressionContainer', + JSXIdentifier: 'JSXIdentifier', + JSXMemberExpression: 'JSXMemberExpression', + JSXNamespacedName: 'JSXNamespacedName', + JSXOpeningElement: 'JSXOpeningElement', + JSXSpreadAttribute: 'JSXSpreadAttribute', + JSXText: 'JSXText' + }; + + +/***/ }, +/* 7 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + var syntax_1 = __webpack_require__(2); + /* tslint:disable:max-classes-per-file */ + var ArrayExpression = (function () { + function ArrayExpression(elements) { + this.type = syntax_1.Syntax.ArrayExpression; + this.elements = elements; + } + return ArrayExpression; + }()); + exports.ArrayExpression = ArrayExpression; + var ArrayPattern = (function () { + function ArrayPattern(elements) { + this.type = syntax_1.Syntax.ArrayPattern; + this.elements = elements; + } + return ArrayPattern; + }()); + exports.ArrayPattern = ArrayPattern; + var ArrowFunctionExpression = (function () { + function ArrowFunctionExpression(params, body, expression) { + this.type = syntax_1.Syntax.ArrowFunctionExpression; + this.id = null; + this.params = params; + this.body = body; + this.generator = false; + this.expression = expression; + this.async = false; + } + return ArrowFunctionExpression; + }()); + exports.ArrowFunctionExpression = ArrowFunctionExpression; + var AssignmentExpression = (function () { + function AssignmentExpression(operator, left, right) { + this.type = syntax_1.Syntax.AssignmentExpression; + this.operator = operator; + this.left = left; + this.right = right; + } + return AssignmentExpression; + }()); + exports.AssignmentExpression = AssignmentExpression; + var AssignmentPattern = (function () { + function AssignmentPattern(left, right) { + this.type = syntax_1.Syntax.AssignmentPattern; + this.left = left; + this.right = right; + } + return AssignmentPattern; + }()); + exports.AssignmentPattern = AssignmentPattern; + var AsyncArrowFunctionExpression = (function () { + function AsyncArrowFunctionExpression(params, body, expression) { + this.type = syntax_1.Syntax.ArrowFunctionExpression; + this.id = null; + this.params = params; + this.body = body; + this.generator = false; + this.expression = expression; + this.async = true; + } + return AsyncArrowFunctionExpression; + }()); + exports.AsyncArrowFunctionExpression = AsyncArrowFunctionExpression; + var AsyncFunctionDeclaration = (function () { + function AsyncFunctionDeclaration(id, params, body) { + this.type = syntax_1.Syntax.FunctionDeclaration; + this.id = id; + this.params = params; + this.body = body; + this.generator = false; + this.expression = false; + this.async = true; + } + return AsyncFunctionDeclaration; + }()); + exports.AsyncFunctionDeclaration = AsyncFunctionDeclaration; + var AsyncFunctionExpression = (function () { + function AsyncFunctionExpression(id, params, body) { + this.type = syntax_1.Syntax.FunctionExpression; + this.id = id; + this.params = params; + this.body = body; + this.generator = false; + this.expression = false; + this.async = true; + } + return AsyncFunctionExpression; + }()); + exports.AsyncFunctionExpression = AsyncFunctionExpression; + var AwaitExpression = (function () { + function AwaitExpression(argument) { + this.type = syntax_1.Syntax.AwaitExpression; + this.argument = argument; + } + return AwaitExpression; + }()); + exports.AwaitExpression = AwaitExpression; + var BinaryExpression = (function () { + function BinaryExpression(operator, left, right) { + var logical = (operator === '||' || operator === '&&'); + this.type = logical ? syntax_1.Syntax.LogicalExpression : syntax_1.Syntax.BinaryExpression; + this.operator = operator; + this.left = left; + this.right = right; + } + return BinaryExpression; + }()); + exports.BinaryExpression = BinaryExpression; + var BlockStatement = (function () { + function BlockStatement(body) { + this.type = syntax_1.Syntax.BlockStatement; + this.body = body; + } + return BlockStatement; + }()); + exports.BlockStatement = BlockStatement; + var BreakStatement = (function () { + function BreakStatement(label) { + this.type = syntax_1.Syntax.BreakStatement; + this.label = label; + } + return BreakStatement; + }()); + exports.BreakStatement = BreakStatement; + var CallExpression = (function () { + function CallExpression(callee, args) { + this.type = syntax_1.Syntax.CallExpression; + this.callee = callee; + this.arguments = args; + } + return CallExpression; + }()); + exports.CallExpression = CallExpression; + var CatchClause = (function () { + function CatchClause(param, body) { + this.type = syntax_1.Syntax.CatchClause; + this.param = param; + this.body = body; + } + return CatchClause; + }()); + exports.CatchClause = CatchClause; + var ClassBody = (function () { + function ClassBody(body) { + this.type = syntax_1.Syntax.ClassBody; + this.body = body; + } + return ClassBody; + }()); + exports.ClassBody = ClassBody; + var ClassDeclaration = (function () { + function ClassDeclaration(id, superClass, body) { + this.type = syntax_1.Syntax.ClassDeclaration; + this.id = id; + this.superClass = superClass; + this.body = body; + } + return ClassDeclaration; + }()); + exports.ClassDeclaration = ClassDeclaration; + var ClassExpression = (function () { + function ClassExpression(id, superClass, body) { + this.type = syntax_1.Syntax.ClassExpression; + this.id = id; + this.superClass = superClass; + this.body = body; + } + return ClassExpression; + }()); + exports.ClassExpression = ClassExpression; + var ComputedMemberExpression = (function () { + function ComputedMemberExpression(object, property) { + this.type = syntax_1.Syntax.MemberExpression; + this.computed = true; + this.object = object; + this.property = property; + } + return ComputedMemberExpression; + }()); + exports.ComputedMemberExpression = ComputedMemberExpression; + var ConditionalExpression = (function () { + function ConditionalExpression(test, consequent, alternate) { + this.type = syntax_1.Syntax.ConditionalExpression; + this.test = test; + this.consequent = consequent; + this.alternate = alternate; + } + return ConditionalExpression; + }()); + exports.ConditionalExpression = ConditionalExpression; + var ContinueStatement = (function () { + function ContinueStatement(label) { + this.type = syntax_1.Syntax.ContinueStatement; + this.label = label; + } + return ContinueStatement; + }()); + exports.ContinueStatement = ContinueStatement; + var DebuggerStatement = (function () { + function DebuggerStatement() { + this.type = syntax_1.Syntax.DebuggerStatement; + } + return DebuggerStatement; + }()); + exports.DebuggerStatement = DebuggerStatement; + var Directive = (function () { + function Directive(expression, directive) { + this.type = syntax_1.Syntax.ExpressionStatement; + this.expression = expression; + this.directive = directive; + } + return Directive; + }()); + exports.Directive = Directive; + var DoWhileStatement = (function () { + function DoWhileStatement(body, test) { + this.type = syntax_1.Syntax.DoWhileStatement; + this.body = body; + this.test = test; + } + return DoWhileStatement; + }()); + exports.DoWhileStatement = DoWhileStatement; + var EmptyStatement = (function () { + function EmptyStatement() { + this.type = syntax_1.Syntax.EmptyStatement; + } + return EmptyStatement; + }()); + exports.EmptyStatement = EmptyStatement; + var ExportAllDeclaration = (function () { + function ExportAllDeclaration(source) { + this.type = syntax_1.Syntax.ExportAllDeclaration; + this.source = source; + } + return ExportAllDeclaration; + }()); + exports.ExportAllDeclaration = ExportAllDeclaration; + var ExportDefaultDeclaration = (function () { + function ExportDefaultDeclaration(declaration) { + this.type = syntax_1.Syntax.ExportDefaultDeclaration; + this.declaration = declaration; + } + return ExportDefaultDeclaration; + }()); + exports.ExportDefaultDeclaration = ExportDefaultDeclaration; + var ExportNamedDeclaration = (function () { + function ExportNamedDeclaration(declaration, specifiers, source) { + this.type = syntax_1.Syntax.ExportNamedDeclaration; + this.declaration = declaration; + this.specifiers = specifiers; + this.source = source; + } + return ExportNamedDeclaration; + }()); + exports.ExportNamedDeclaration = ExportNamedDeclaration; + var ExportSpecifier = (function () { + function ExportSpecifier(local, exported) { + this.type = syntax_1.Syntax.ExportSpecifier; + this.exported = exported; + this.local = local; + } + return ExportSpecifier; + }()); + exports.ExportSpecifier = ExportSpecifier; + var ExpressionStatement = (function () { + function ExpressionStatement(expression) { + this.type = syntax_1.Syntax.ExpressionStatement; + this.expression = expression; + } + return ExpressionStatement; + }()); + exports.ExpressionStatement = ExpressionStatement; + var ForInStatement = (function () { + function ForInStatement(left, right, body) { + this.type = syntax_1.Syntax.ForInStatement; + this.left = left; + this.right = right; + this.body = body; + this.each = false; + } + return ForInStatement; + }()); + exports.ForInStatement = ForInStatement; + var ForOfStatement = (function () { + function ForOfStatement(left, right, body) { + this.type = syntax_1.Syntax.ForOfStatement; + this.left = left; + this.right = right; + this.body = body; + } + return ForOfStatement; + }()); + exports.ForOfStatement = ForOfStatement; + var ForStatement = (function () { + function ForStatement(init, test, update, body) { + this.type = syntax_1.Syntax.ForStatement; + this.init = init; + this.test = test; + this.update = update; + this.body = body; + } + return ForStatement; + }()); + exports.ForStatement = ForStatement; + var FunctionDeclaration = (function () { + function FunctionDeclaration(id, params, body, generator) { + this.type = syntax_1.Syntax.FunctionDeclaration; + this.id = id; + this.params = params; + this.body = body; + this.generator = generator; + this.expression = false; + this.async = false; + } + return FunctionDeclaration; + }()); + exports.FunctionDeclaration = FunctionDeclaration; + var FunctionExpression = (function () { + function FunctionExpression(id, params, body, generator) { + this.type = syntax_1.Syntax.FunctionExpression; + this.id = id; + this.params = params; + this.body = body; + this.generator = generator; + this.expression = false; + this.async = false; + } + return FunctionExpression; + }()); + exports.FunctionExpression = FunctionExpression; + var Identifier = (function () { + function Identifier(name) { + this.type = syntax_1.Syntax.Identifier; + this.name = name; + } + return Identifier; + }()); + exports.Identifier = Identifier; + var IfStatement = (function () { + function IfStatement(test, consequent, alternate) { + this.type = syntax_1.Syntax.IfStatement; + this.test = test; + this.consequent = consequent; + this.alternate = alternate; + } + return IfStatement; + }()); + exports.IfStatement = IfStatement; + var ImportDeclaration = (function () { + function ImportDeclaration(specifiers, source) { + this.type = syntax_1.Syntax.ImportDeclaration; + this.specifiers = specifiers; + this.source = source; + } + return ImportDeclaration; + }()); + exports.ImportDeclaration = ImportDeclaration; + var ImportDefaultSpecifier = (function () { + function ImportDefaultSpecifier(local) { + this.type = syntax_1.Syntax.ImportDefaultSpecifier; + this.local = local; + } + return ImportDefaultSpecifier; + }()); + exports.ImportDefaultSpecifier = ImportDefaultSpecifier; + var ImportNamespaceSpecifier = (function () { + function ImportNamespaceSpecifier(local) { + this.type = syntax_1.Syntax.ImportNamespaceSpecifier; + this.local = local; + } + return ImportNamespaceSpecifier; + }()); + exports.ImportNamespaceSpecifier = ImportNamespaceSpecifier; + var ImportSpecifier = (function () { + function ImportSpecifier(local, imported) { + this.type = syntax_1.Syntax.ImportSpecifier; + this.local = local; + this.imported = imported; + } + return ImportSpecifier; + }()); + exports.ImportSpecifier = ImportSpecifier; + var LabeledStatement = (function () { + function LabeledStatement(label, body) { + this.type = syntax_1.Syntax.LabeledStatement; + this.label = label; + this.body = body; + } + return LabeledStatement; + }()); + exports.LabeledStatement = LabeledStatement; + var Literal = (function () { + function Literal(value, raw) { + this.type = syntax_1.Syntax.Literal; + this.value = value; + this.raw = raw; + } + return Literal; + }()); + exports.Literal = Literal; + var MetaProperty = (function () { + function MetaProperty(meta, property) { + this.type = syntax_1.Syntax.MetaProperty; + this.meta = meta; + this.property = property; + } + return MetaProperty; + }()); + exports.MetaProperty = MetaProperty; + var MethodDefinition = (function () { + function MethodDefinition(key, computed, value, kind, isStatic) { + this.type = syntax_1.Syntax.MethodDefinition; + this.key = key; + this.computed = computed; + this.value = value; + this.kind = kind; + this.static = isStatic; + } + return MethodDefinition; + }()); + exports.MethodDefinition = MethodDefinition; + var Module = (function () { + function Module(body) { + this.type = syntax_1.Syntax.Program; + this.body = body; + this.sourceType = 'module'; + } + return Module; + }()); + exports.Module = Module; + var NewExpression = (function () { + function NewExpression(callee, args) { + this.type = syntax_1.Syntax.NewExpression; + this.callee = callee; + this.arguments = args; + } + return NewExpression; + }()); + exports.NewExpression = NewExpression; + var ObjectExpression = (function () { + function ObjectExpression(properties) { + this.type = syntax_1.Syntax.ObjectExpression; + this.properties = properties; + } + return ObjectExpression; + }()); + exports.ObjectExpression = ObjectExpression; + var ObjectPattern = (function () { + function ObjectPattern(properties) { + this.type = syntax_1.Syntax.ObjectPattern; + this.properties = properties; + } + return ObjectPattern; + }()); + exports.ObjectPattern = ObjectPattern; + var Property = (function () { + function Property(kind, key, computed, value, method, shorthand) { + this.type = syntax_1.Syntax.Property; + this.key = key; + this.computed = computed; + this.value = value; + this.kind = kind; + this.method = method; + this.shorthand = shorthand; + } + return Property; + }()); + exports.Property = Property; + var RegexLiteral = (function () { + function RegexLiteral(value, raw, pattern, flags) { + this.type = syntax_1.Syntax.Literal; + this.value = value; + this.raw = raw; + this.regex = { pattern: pattern, flags: flags }; + } + return RegexLiteral; + }()); + exports.RegexLiteral = RegexLiteral; + var RestElement = (function () { + function RestElement(argument) { + this.type = syntax_1.Syntax.RestElement; + this.argument = argument; + } + return RestElement; + }()); + exports.RestElement = RestElement; + var ReturnStatement = (function () { + function ReturnStatement(argument) { + this.type = syntax_1.Syntax.ReturnStatement; + this.argument = argument; + } + return ReturnStatement; + }()); + exports.ReturnStatement = ReturnStatement; + var Script = (function () { + function Script(body) { + this.type = syntax_1.Syntax.Program; + this.body = body; + this.sourceType = 'script'; + } + return Script; + }()); + exports.Script = Script; + var SequenceExpression = (function () { + function SequenceExpression(expressions) { + this.type = syntax_1.Syntax.SequenceExpression; + this.expressions = expressions; + } + return SequenceExpression; + }()); + exports.SequenceExpression = SequenceExpression; + var SpreadElement = (function () { + function SpreadElement(argument) { + this.type = syntax_1.Syntax.SpreadElement; + this.argument = argument; + } + return SpreadElement; + }()); + exports.SpreadElement = SpreadElement; + var StaticMemberExpression = (function () { + function StaticMemberExpression(object, property) { + this.type = syntax_1.Syntax.MemberExpression; + this.computed = false; + this.object = object; + this.property = property; + } + return StaticMemberExpression; + }()); + exports.StaticMemberExpression = StaticMemberExpression; + var Super = (function () { + function Super() { + this.type = syntax_1.Syntax.Super; + } + return Super; + }()); + exports.Super = Super; + var SwitchCase = (function () { + function SwitchCase(test, consequent) { + this.type = syntax_1.Syntax.SwitchCase; + this.test = test; + this.consequent = consequent; + } + return SwitchCase; + }()); + exports.SwitchCase = SwitchCase; + var SwitchStatement = (function () { + function SwitchStatement(discriminant, cases) { + this.type = syntax_1.Syntax.SwitchStatement; + this.discriminant = discriminant; + this.cases = cases; + } + return SwitchStatement; + }()); + exports.SwitchStatement = SwitchStatement; + var TaggedTemplateExpression = (function () { + function TaggedTemplateExpression(tag, quasi) { + this.type = syntax_1.Syntax.TaggedTemplateExpression; + this.tag = tag; + this.quasi = quasi; + } + return TaggedTemplateExpression; + }()); + exports.TaggedTemplateExpression = TaggedTemplateExpression; + var TemplateElement = (function () { + function TemplateElement(value, tail) { + this.type = syntax_1.Syntax.TemplateElement; + this.value = value; + this.tail = tail; + } + return TemplateElement; + }()); + exports.TemplateElement = TemplateElement; + var TemplateLiteral = (function () { + function TemplateLiteral(quasis, expressions) { + this.type = syntax_1.Syntax.TemplateLiteral; + this.quasis = quasis; + this.expressions = expressions; + } + return TemplateLiteral; + }()); + exports.TemplateLiteral = TemplateLiteral; + var ThisExpression = (function () { + function ThisExpression() { + this.type = syntax_1.Syntax.ThisExpression; + } + return ThisExpression; + }()); + exports.ThisExpression = ThisExpression; + var ThrowStatement = (function () { + function ThrowStatement(argument) { + this.type = syntax_1.Syntax.ThrowStatement; + this.argument = argument; + } + return ThrowStatement; + }()); + exports.ThrowStatement = ThrowStatement; + var TryStatement = (function () { + function TryStatement(block, handler, finalizer) { + this.type = syntax_1.Syntax.TryStatement; + this.block = block; + this.handler = handler; + this.finalizer = finalizer; + } + return TryStatement; + }()); + exports.TryStatement = TryStatement; + var UnaryExpression = (function () { + function UnaryExpression(operator, argument) { + this.type = syntax_1.Syntax.UnaryExpression; + this.operator = operator; + this.argument = argument; + this.prefix = true; + } + return UnaryExpression; + }()); + exports.UnaryExpression = UnaryExpression; + var UpdateExpression = (function () { + function UpdateExpression(operator, argument, prefix) { + this.type = syntax_1.Syntax.UpdateExpression; + this.operator = operator; + this.argument = argument; + this.prefix = prefix; + } + return UpdateExpression; + }()); + exports.UpdateExpression = UpdateExpression; + var VariableDeclaration = (function () { + function VariableDeclaration(declarations, kind) { + this.type = syntax_1.Syntax.VariableDeclaration; + this.declarations = declarations; + this.kind = kind; + } + return VariableDeclaration; + }()); + exports.VariableDeclaration = VariableDeclaration; + var VariableDeclarator = (function () { + function VariableDeclarator(id, init) { + this.type = syntax_1.Syntax.VariableDeclarator; + this.id = id; + this.init = init; + } + return VariableDeclarator; + }()); + exports.VariableDeclarator = VariableDeclarator; + var WhileStatement = (function () { + function WhileStatement(test, body) { + this.type = syntax_1.Syntax.WhileStatement; + this.test = test; + this.body = body; + } + return WhileStatement; + }()); + exports.WhileStatement = WhileStatement; + var WithStatement = (function () { + function WithStatement(object, body) { + this.type = syntax_1.Syntax.WithStatement; + this.object = object; + this.body = body; + } + return WithStatement; + }()); + exports.WithStatement = WithStatement; + var YieldExpression = (function () { + function YieldExpression(argument, delegate) { + this.type = syntax_1.Syntax.YieldExpression; + this.argument = argument; + this.delegate = delegate; + } + return YieldExpression; + }()); + exports.YieldExpression = YieldExpression; + + +/***/ }, +/* 8 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + var assert_1 = __webpack_require__(9); + var error_handler_1 = __webpack_require__(10); + var messages_1 = __webpack_require__(11); + var Node = __webpack_require__(7); + var scanner_1 = __webpack_require__(12); + var syntax_1 = __webpack_require__(2); + var token_1 = __webpack_require__(13); + var ArrowParameterPlaceHolder = 'ArrowParameterPlaceHolder'; + var Parser = (function () { + function Parser(code, options, delegate) { + if (options === void 0) { options = {}; } + this.config = { + range: (typeof options.range === 'boolean') && options.range, + loc: (typeof options.loc === 'boolean') && options.loc, + source: null, + tokens: (typeof options.tokens === 'boolean') && options.tokens, + comment: (typeof options.comment === 'boolean') && options.comment, + tolerant: (typeof options.tolerant === 'boolean') && options.tolerant + }; + if (this.config.loc && options.source && options.source !== null) { + this.config.source = String(options.source); + } + this.delegate = delegate; + this.errorHandler = new error_handler_1.ErrorHandler(); + this.errorHandler.tolerant = this.config.tolerant; + this.scanner = new scanner_1.Scanner(code, this.errorHandler); + this.scanner.trackComment = this.config.comment; + this.operatorPrecedence = { + ')': 0, + ';': 0, + ',': 0, + '=': 0, + ']': 0, + '||': 1, + '&&': 2, + '|': 3, + '^': 4, + '&': 5, + '==': 6, + '!=': 6, + '===': 6, + '!==': 6, + '<': 7, + '>': 7, + '<=': 7, + '>=': 7, + '<<': 8, + '>>': 8, + '>>>': 8, + '+': 9, + '-': 9, + '*': 11, + '/': 11, + '%': 11 + }; + this.lookahead = { + type: 2 /* EOF */, + value: '', + lineNumber: this.scanner.lineNumber, + lineStart: 0, + start: 0, + end: 0 + }; + this.hasLineTerminator = false; + this.context = { + isModule: false, + await: false, + allowIn: true, + allowStrictDirective: true, + allowYield: true, + firstCoverInitializedNameError: null, + isAssignmentTarget: false, + isBindingElement: false, + inFunctionBody: false, + inIteration: false, + inSwitch: false, + labelSet: {}, + strict: false + }; + this.tokens = []; + this.startMarker = { + index: 0, + line: this.scanner.lineNumber, + column: 0 + }; + this.lastMarker = { + index: 0, + line: this.scanner.lineNumber, + column: 0 + }; + this.nextToken(); + this.lastMarker = { + index: this.scanner.index, + line: this.scanner.lineNumber, + column: this.scanner.index - this.scanner.lineStart + }; + } + Parser.prototype.throwError = function (messageFormat) { + var values = []; + for (var _i = 1; _i < arguments.length; _i++) { + values[_i - 1] = arguments[_i]; + } + var args = Array.prototype.slice.call(arguments, 1); + var msg = messageFormat.replace(/%(\d)/g, function (whole, idx) { + assert_1.assert(idx < args.length, 'Message reference must be in range'); + return args[idx]; + }); + var index = this.lastMarker.index; + var line = this.lastMarker.line; + var column = this.lastMarker.column + 1; + throw this.errorHandler.createError(index, line, column, msg); + }; + Parser.prototype.tolerateError = function (messageFormat) { + var values = []; + for (var _i = 1; _i < arguments.length; _i++) { + values[_i - 1] = arguments[_i]; + } + var args = Array.prototype.slice.call(arguments, 1); + var msg = messageFormat.replace(/%(\d)/g, function (whole, idx) { + assert_1.assert(idx < args.length, 'Message reference must be in range'); + return args[idx]; + }); + var index = this.lastMarker.index; + var line = this.scanner.lineNumber; + var column = this.lastMarker.column + 1; + this.errorHandler.tolerateError(index, line, column, msg); + }; + // Throw an exception because of the token. + Parser.prototype.unexpectedTokenError = function (token, message) { + var msg = message || messages_1.Messages.UnexpectedToken; + var value; + if (token) { + if (!message) { + msg = (token.type === 2 /* EOF */) ? messages_1.Messages.UnexpectedEOS : + (token.type === 3 /* Identifier */) ? messages_1.Messages.UnexpectedIdentifier : + (token.type === 6 /* NumericLiteral */) ? messages_1.Messages.UnexpectedNumber : + (token.type === 8 /* StringLiteral */) ? messages_1.Messages.UnexpectedString : + (token.type === 10 /* Template */) ? messages_1.Messages.UnexpectedTemplate : + messages_1.Messages.UnexpectedToken; + if (token.type === 4 /* Keyword */) { + if (this.scanner.isFutureReservedWord(token.value)) { + msg = messages_1.Messages.UnexpectedReserved; + } + else if (this.context.strict && this.scanner.isStrictModeReservedWord(token.value)) { + msg = messages_1.Messages.StrictReservedWord; + } + } + } + value = token.value; + } + else { + value = 'ILLEGAL'; + } + msg = msg.replace('%0', value); + if (token && typeof token.lineNumber === 'number') { + var index = token.start; + var line = token.lineNumber; + var lastMarkerLineStart = this.lastMarker.index - this.lastMarker.column; + var column = token.start - lastMarkerLineStart + 1; + return this.errorHandler.createError(index, line, column, msg); + } + else { + var index = this.lastMarker.index; + var line = this.lastMarker.line; + var column = this.lastMarker.column + 1; + return this.errorHandler.createError(index, line, column, msg); + } + }; + Parser.prototype.throwUnexpectedToken = function (token, message) { + throw this.unexpectedTokenError(token, message); + }; + Parser.prototype.tolerateUnexpectedToken = function (token, message) { + this.errorHandler.tolerate(this.unexpectedTokenError(token, message)); + }; + Parser.prototype.collectComments = function () { + if (!this.config.comment) { + this.scanner.scanComments(); + } + else { + var comments = this.scanner.scanComments(); + if (comments.length > 0 && this.delegate) { + for (var i = 0; i < comments.length; ++i) { + var e = comments[i]; + var node = void 0; + node = { + type: e.multiLine ? 'BlockComment' : 'LineComment', + value: this.scanner.source.slice(e.slice[0], e.slice[1]) + }; + if (this.config.range) { + node.range = e.range; + } + if (this.config.loc) { + node.loc = e.loc; + } + var metadata = { + start: { + line: e.loc.start.line, + column: e.loc.start.column, + offset: e.range[0] + }, + end: { + line: e.loc.end.line, + column: e.loc.end.column, + offset: e.range[1] + } + }; + this.delegate(node, metadata); + } + } + } + }; + // From internal representation to an external structure + Parser.prototype.getTokenRaw = function (token) { + return this.scanner.source.slice(token.start, token.end); + }; + Parser.prototype.convertToken = function (token) { + var t = { + type: token_1.TokenName[token.type], + value: this.getTokenRaw(token) + }; + if (this.config.range) { + t.range = [token.start, token.end]; + } + if (this.config.loc) { + t.loc = { + start: { + line: this.startMarker.line, + column: this.startMarker.column + }, + end: { + line: this.scanner.lineNumber, + column: this.scanner.index - this.scanner.lineStart + } + }; + } + if (token.type === 9 /* RegularExpression */) { + var pattern = token.pattern; + var flags = token.flags; + t.regex = { pattern: pattern, flags: flags }; + } + return t; + }; + Parser.prototype.nextToken = function () { + var token = this.lookahead; + this.lastMarker.index = this.scanner.index; + this.lastMarker.line = this.scanner.lineNumber; + this.lastMarker.column = this.scanner.index - this.scanner.lineStart; + this.collectComments(); + if (this.scanner.index !== this.startMarker.index) { + this.startMarker.index = this.scanner.index; + this.startMarker.line = this.scanner.lineNumber; + this.startMarker.column = this.scanner.index - this.scanner.lineStart; + } + var next = this.scanner.lex(); + this.hasLineTerminator = (token.lineNumber !== next.lineNumber); + if (next && this.context.strict && next.type === 3 /* Identifier */) { + if (this.scanner.isStrictModeReservedWord(next.value)) { + next.type = 4 /* Keyword */; + } + } + this.lookahead = next; + if (this.config.tokens && next.type !== 2 /* EOF */) { + this.tokens.push(this.convertToken(next)); + } + return token; + }; + Parser.prototype.nextRegexToken = function () { + this.collectComments(); + var token = this.scanner.scanRegExp(); + if (this.config.tokens) { + // Pop the previous token, '/' or '/=' + // This is added from the lookahead token. + this.tokens.pop(); + this.tokens.push(this.convertToken(token)); + } + // Prime the next lookahead. + this.lookahead = token; + this.nextToken(); + return token; + }; + Parser.prototype.createNode = function () { + return { + index: this.startMarker.index, + line: this.startMarker.line, + column: this.startMarker.column + }; + }; + Parser.prototype.startNode = function (token, lastLineStart) { + if (lastLineStart === void 0) { lastLineStart = 0; } + var column = token.start - token.lineStart; + var line = token.lineNumber; + if (column < 0) { + column += lastLineStart; + line--; + } + return { + index: token.start, + line: line, + column: column + }; + }; + Parser.prototype.finalize = function (marker, node) { + if (this.config.range) { + node.range = [marker.index, this.lastMarker.index]; + } + if (this.config.loc) { + node.loc = { + start: { + line: marker.line, + column: marker.column, + }, + end: { + line: this.lastMarker.line, + column: this.lastMarker.column + } + }; + if (this.config.source) { + node.loc.source = this.config.source; + } + } + if (this.delegate) { + var metadata = { + start: { + line: marker.line, + column: marker.column, + offset: marker.index + }, + end: { + line: this.lastMarker.line, + column: this.lastMarker.column, + offset: this.lastMarker.index + } + }; + this.delegate(node, metadata); + } + return node; + }; + // Expect the next token to match the specified punctuator. + // If not, an exception will be thrown. + Parser.prototype.expect = function (value) { + var token = this.nextToken(); + if (token.type !== 7 /* Punctuator */ || token.value !== value) { + this.throwUnexpectedToken(token); + } + }; + // Quietly expect a comma when in tolerant mode, otherwise delegates to expect(). + Parser.prototype.expectCommaSeparator = function () { + if (this.config.tolerant) { + var token = this.lookahead; + if (token.type === 7 /* Punctuator */ && token.value === ',') { + this.nextToken(); + } + else if (token.type === 7 /* Punctuator */ && token.value === ';') { + this.nextToken(); + this.tolerateUnexpectedToken(token); + } + else { + this.tolerateUnexpectedToken(token, messages_1.Messages.UnexpectedToken); + } + } + else { + this.expect(','); + } + }; + // Expect the next token to match the specified keyword. + // If not, an exception will be thrown. + Parser.prototype.expectKeyword = function (keyword) { + var token = this.nextToken(); + if (token.type !== 4 /* Keyword */ || token.value !== keyword) { + this.throwUnexpectedToken(token); + } + }; + // Return true if the next token matches the specified punctuator. + Parser.prototype.match = function (value) { + return this.lookahead.type === 7 /* Punctuator */ && this.lookahead.value === value; + }; + // Return true if the next token matches the specified keyword + Parser.prototype.matchKeyword = function (keyword) { + return this.lookahead.type === 4 /* Keyword */ && this.lookahead.value === keyword; + }; + // Return true if the next token matches the specified contextual keyword + // (where an identifier is sometimes a keyword depending on the context) + Parser.prototype.matchContextualKeyword = function (keyword) { + return this.lookahead.type === 3 /* Identifier */ && this.lookahead.value === keyword; + }; + // Return true if the next token is an assignment operator + Parser.prototype.matchAssign = function () { + if (this.lookahead.type !== 7 /* Punctuator */) { + return false; + } + var op = this.lookahead.value; + return op === '=' || + op === '*=' || + op === '**=' || + op === '/=' || + op === '%=' || + op === '+=' || + op === '-=' || + op === '<<=' || + op === '>>=' || + op === '>>>=' || + op === '&=' || + op === '^=' || + op === '|='; + }; + // Cover grammar support. + // + // When an assignment expression position starts with an left parenthesis, the determination of the type + // of the syntax is to be deferred arbitrarily long until the end of the parentheses pair (plus a lookahead) + // or the first comma. This situation also defers the determination of all the expressions nested in the pair. + // + // There are three productions that can be parsed in a parentheses pair that needs to be determined + // after the outermost pair is closed. They are: + // + // 1. AssignmentExpression + // 2. BindingElements + // 3. AssignmentTargets + // + // In order to avoid exponential backtracking, we use two flags to denote if the production can be + // binding element or assignment target. + // + // The three productions have the relationship: + // + // BindingElements ⊆ AssignmentTargets ⊆ AssignmentExpression + // + // with a single exception that CoverInitializedName when used directly in an Expression, generates + // an early error. Therefore, we need the third state, firstCoverInitializedNameError, to track the + // first usage of CoverInitializedName and report it when we reached the end of the parentheses pair. + // + // isolateCoverGrammar function runs the given parser function with a new cover grammar context, and it does not + // effect the current flags. This means the production the parser parses is only used as an expression. Therefore + // the CoverInitializedName check is conducted. + // + // inheritCoverGrammar function runs the given parse function with a new cover grammar context, and it propagates + // the flags outside of the parser. This means the production the parser parses is used as a part of a potential + // pattern. The CoverInitializedName check is deferred. + Parser.prototype.isolateCoverGrammar = function (parseFunction) { + var previousIsBindingElement = this.context.isBindingElement; + var previousIsAssignmentTarget = this.context.isAssignmentTarget; + var previousFirstCoverInitializedNameError = this.context.firstCoverInitializedNameError; + this.context.isBindingElement = true; + this.context.isAssignmentTarget = true; + this.context.firstCoverInitializedNameError = null; + var result = parseFunction.call(this); + if (this.context.firstCoverInitializedNameError !== null) { + this.throwUnexpectedToken(this.context.firstCoverInitializedNameError); + } + this.context.isBindingElement = previousIsBindingElement; + this.context.isAssignmentTarget = previousIsAssignmentTarget; + this.context.firstCoverInitializedNameError = previousFirstCoverInitializedNameError; + return result; + }; + Parser.prototype.inheritCoverGrammar = function (parseFunction) { + var previousIsBindingElement = this.context.isBindingElement; + var previousIsAssignmentTarget = this.context.isAssignmentTarget; + var previousFirstCoverInitializedNameError = this.context.firstCoverInitializedNameError; + this.context.isBindingElement = true; + this.context.isAssignmentTarget = true; + this.context.firstCoverInitializedNameError = null; + var result = parseFunction.call(this); + this.context.isBindingElement = this.context.isBindingElement && previousIsBindingElement; + this.context.isAssignmentTarget = this.context.isAssignmentTarget && previousIsAssignmentTarget; + this.context.firstCoverInitializedNameError = previousFirstCoverInitializedNameError || this.context.firstCoverInitializedNameError; + return result; + }; + Parser.prototype.consumeSemicolon = function () { + if (this.match(';')) { + this.nextToken(); + } + else if (!this.hasLineTerminator) { + if (this.lookahead.type !== 2 /* EOF */ && !this.match('}')) { + this.throwUnexpectedToken(this.lookahead); + } + this.lastMarker.index = this.startMarker.index; + this.lastMarker.line = this.startMarker.line; + this.lastMarker.column = this.startMarker.column; + } + }; + // https://tc39.github.io/ecma262/#sec-primary-expression + Parser.prototype.parsePrimaryExpression = function () { + var node = this.createNode(); + var expr; + var token, raw; + switch (this.lookahead.type) { + case 3 /* Identifier */: + if ((this.context.isModule || this.context.await) && this.lookahead.value === 'await') { + this.tolerateUnexpectedToken(this.lookahead); + } + expr = this.matchAsyncFunction() ? this.parseFunctionExpression() : this.finalize(node, new Node.Identifier(this.nextToken().value)); + break; + case 6 /* NumericLiteral */: + case 8 /* StringLiteral */: + if (this.context.strict && this.lookahead.octal) { + this.tolerateUnexpectedToken(this.lookahead, messages_1.Messages.StrictOctalLiteral); + } + this.context.isAssignmentTarget = false; + this.context.isBindingElement = false; + token = this.nextToken(); + raw = this.getTokenRaw(token); + expr = this.finalize(node, new Node.Literal(token.value, raw)); + break; + case 1 /* BooleanLiteral */: + this.context.isAssignmentTarget = false; + this.context.isBindingElement = false; + token = this.nextToken(); + raw = this.getTokenRaw(token); + expr = this.finalize(node, new Node.Literal(token.value === 'true', raw)); + break; + case 5 /* NullLiteral */: + this.context.isAssignmentTarget = false; + this.context.isBindingElement = false; + token = this.nextToken(); + raw = this.getTokenRaw(token); + expr = this.finalize(node, new Node.Literal(null, raw)); + break; + case 10 /* Template */: + expr = this.parseTemplateLiteral(); + break; + case 7 /* Punctuator */: + switch (this.lookahead.value) { + case '(': + this.context.isBindingElement = false; + expr = this.inheritCoverGrammar(this.parseGroupExpression); + break; + case '[': + expr = this.inheritCoverGrammar(this.parseArrayInitializer); + break; + case '{': + expr = this.inheritCoverGrammar(this.parseObjectInitializer); + break; + case '/': + case '/=': + this.context.isAssignmentTarget = false; + this.context.isBindingElement = false; + this.scanner.index = this.startMarker.index; + token = this.nextRegexToken(); + raw = this.getTokenRaw(token); + expr = this.finalize(node, new Node.RegexLiteral(token.regex, raw, token.pattern, token.flags)); + break; + default: + expr = this.throwUnexpectedToken(this.nextToken()); + } + break; + case 4 /* Keyword */: + if (!this.context.strict && this.context.allowYield && this.matchKeyword('yield')) { + expr = this.parseIdentifierName(); + } + else if (!this.context.strict && this.matchKeyword('let')) { + expr = this.finalize(node, new Node.Identifier(this.nextToken().value)); + } + else { + this.context.isAssignmentTarget = false; + this.context.isBindingElement = false; + if (this.matchKeyword('function')) { + expr = this.parseFunctionExpression(); + } + else if (this.matchKeyword('this')) { + this.nextToken(); + expr = this.finalize(node, new Node.ThisExpression()); + } + else if (this.matchKeyword('class')) { + expr = this.parseClassExpression(); + } + else { + expr = this.throwUnexpectedToken(this.nextToken()); + } + } + break; + default: + expr = this.throwUnexpectedToken(this.nextToken()); + } + return expr; + }; + // https://tc39.github.io/ecma262/#sec-array-initializer + Parser.prototype.parseSpreadElement = function () { + var node = this.createNode(); + this.expect('...'); + var arg = this.inheritCoverGrammar(this.parseAssignmentExpression); + return this.finalize(node, new Node.SpreadElement(arg)); + }; + Parser.prototype.parseArrayInitializer = function () { + var node = this.createNode(); + var elements = []; + this.expect('['); + while (!this.match(']')) { + if (this.match(',')) { + this.nextToken(); + elements.push(null); + } + else if (this.match('...')) { + var element = this.parseSpreadElement(); + if (!this.match(']')) { + this.context.isAssignmentTarget = false; + this.context.isBindingElement = false; + this.expect(','); + } + elements.push(element); + } + else { + elements.push(this.inheritCoverGrammar(this.parseAssignmentExpression)); + if (!this.match(']')) { + this.expect(','); + } + } + } + this.expect(']'); + return this.finalize(node, new Node.ArrayExpression(elements)); + }; + // https://tc39.github.io/ecma262/#sec-object-initializer + Parser.prototype.parsePropertyMethod = function (params) { + this.context.isAssignmentTarget = false; + this.context.isBindingElement = false; + var previousStrict = this.context.strict; + var previousAllowStrictDirective = this.context.allowStrictDirective; + this.context.allowStrictDirective = params.simple; + var body = this.isolateCoverGrammar(this.parseFunctionSourceElements); + if (this.context.strict && params.firstRestricted) { + this.tolerateUnexpectedToken(params.firstRestricted, params.message); + } + if (this.context.strict && params.stricted) { + this.tolerateUnexpectedToken(params.stricted, params.message); + } + this.context.strict = previousStrict; + this.context.allowStrictDirective = previousAllowStrictDirective; + return body; + }; + Parser.prototype.parsePropertyMethodFunction = function () { + var isGenerator = false; + var node = this.createNode(); + var previousAllowYield = this.context.allowYield; + this.context.allowYield = true; + var params = this.parseFormalParameters(); + var method = this.parsePropertyMethod(params); + this.context.allowYield = previousAllowYield; + return this.finalize(node, new Node.FunctionExpression(null, params.params, method, isGenerator)); + }; + Parser.prototype.parsePropertyMethodAsyncFunction = function () { + var node = this.createNode(); + var previousAllowYield = this.context.allowYield; + var previousAwait = this.context.await; + this.context.allowYield = false; + this.context.await = true; + var params = this.parseFormalParameters(); + var method = this.parsePropertyMethod(params); + this.context.allowYield = previousAllowYield; + this.context.await = previousAwait; + return this.finalize(node, new Node.AsyncFunctionExpression(null, params.params, method)); + }; + Parser.prototype.parseObjectPropertyKey = function () { + var node = this.createNode(); + var token = this.nextToken(); + var key; + switch (token.type) { + case 8 /* StringLiteral */: + case 6 /* NumericLiteral */: + if (this.context.strict && token.octal) { + this.tolerateUnexpectedToken(token, messages_1.Messages.StrictOctalLiteral); + } + var raw = this.getTokenRaw(token); + key = this.finalize(node, new Node.Literal(token.value, raw)); + break; + case 3 /* Identifier */: + case 1 /* BooleanLiteral */: + case 5 /* NullLiteral */: + case 4 /* Keyword */: + key = this.finalize(node, new Node.Identifier(token.value)); + break; + case 7 /* Punctuator */: + if (token.value === '[') { + key = this.isolateCoverGrammar(this.parseAssignmentExpression); + this.expect(']'); + } + else { + key = this.throwUnexpectedToken(token); + } + break; + default: + key = this.throwUnexpectedToken(token); + } + return key; + }; + Parser.prototype.isPropertyKey = function (key, value) { + return (key.type === syntax_1.Syntax.Identifier && key.name === value) || + (key.type === syntax_1.Syntax.Literal && key.value === value); + }; + Parser.prototype.parseObjectProperty = function (hasProto) { + var node = this.createNode(); + var token = this.lookahead; + var kind; + var key = null; + var value = null; + var computed = false; + var method = false; + var shorthand = false; + var isAsync = false; + if (token.type === 3 /* Identifier */) { + var id = token.value; + this.nextToken(); + computed = this.match('['); + isAsync = !this.hasLineTerminator && (id === 'async') && + !this.match(':') && !this.match('(') && !this.match('*') && !this.match(','); + key = isAsync ? this.parseObjectPropertyKey() : this.finalize(node, new Node.Identifier(id)); + } + else if (this.match('*')) { + this.nextToken(); + } + else { + computed = this.match('['); + key = this.parseObjectPropertyKey(); + } + var lookaheadPropertyKey = this.qualifiedPropertyName(this.lookahead); + if (token.type === 3 /* Identifier */ && !isAsync && token.value === 'get' && lookaheadPropertyKey) { + kind = 'get'; + computed = this.match('['); + key = this.parseObjectPropertyKey(); + this.context.allowYield = false; + value = this.parseGetterMethod(); + } + else if (token.type === 3 /* Identifier */ && !isAsync && token.value === 'set' && lookaheadPropertyKey) { + kind = 'set'; + computed = this.match('['); + key = this.parseObjectPropertyKey(); + value = this.parseSetterMethod(); + } + else if (token.type === 7 /* Punctuator */ && token.value === '*' && lookaheadPropertyKey) { + kind = 'init'; + computed = this.match('['); + key = this.parseObjectPropertyKey(); + value = this.parseGeneratorMethod(); + method = true; + } + else { + if (!key) { + this.throwUnexpectedToken(this.lookahead); + } + kind = 'init'; + if (this.match(':') && !isAsync) { + if (!computed && this.isPropertyKey(key, '__proto__')) { + if (hasProto.value) { + this.tolerateError(messages_1.Messages.DuplicateProtoProperty); + } + hasProto.value = true; + } + this.nextToken(); + value = this.inheritCoverGrammar(this.parseAssignmentExpression); + } + else if (this.match('(')) { + value = isAsync ? this.parsePropertyMethodAsyncFunction() : this.parsePropertyMethodFunction(); + method = true; + } + else if (token.type === 3 /* Identifier */) { + var id = this.finalize(node, new Node.Identifier(token.value)); + if (this.match('=')) { + this.context.firstCoverInitializedNameError = this.lookahead; + this.nextToken(); + shorthand = true; + var init = this.isolateCoverGrammar(this.parseAssignmentExpression); + value = this.finalize(node, new Node.AssignmentPattern(id, init)); + } + else { + shorthand = true; + value = id; + } + } + else { + this.throwUnexpectedToken(this.nextToken()); + } + } + return this.finalize(node, new Node.Property(kind, key, computed, value, method, shorthand)); + }; + Parser.prototype.parseObjectInitializer = function () { + var node = this.createNode(); + this.expect('{'); + var properties = []; + var hasProto = { value: false }; + while (!this.match('}')) { + properties.push(this.parseObjectProperty(hasProto)); + if (!this.match('}')) { + this.expectCommaSeparator(); + } + } + this.expect('}'); + return this.finalize(node, new Node.ObjectExpression(properties)); + }; + // https://tc39.github.io/ecma262/#sec-template-literals + Parser.prototype.parseTemplateHead = function () { + assert_1.assert(this.lookahead.head, 'Template literal must start with a template head'); + var node = this.createNode(); + var token = this.nextToken(); + var raw = token.value; + var cooked = token.cooked; + return this.finalize(node, new Node.TemplateElement({ raw: raw, cooked: cooked }, token.tail)); + }; + Parser.prototype.parseTemplateElement = function () { + if (this.lookahead.type !== 10 /* Template */) { + this.throwUnexpectedToken(); + } + var node = this.createNode(); + var token = this.nextToken(); + var raw = token.value; + var cooked = token.cooked; + return this.finalize(node, new Node.TemplateElement({ raw: raw, cooked: cooked }, token.tail)); + }; + Parser.prototype.parseTemplateLiteral = function () { + var node = this.createNode(); + var expressions = []; + var quasis = []; + var quasi = this.parseTemplateHead(); + quasis.push(quasi); + while (!quasi.tail) { + expressions.push(this.parseExpression()); + quasi = this.parseTemplateElement(); + quasis.push(quasi); + } + return this.finalize(node, new Node.TemplateLiteral(quasis, expressions)); + }; + // https://tc39.github.io/ecma262/#sec-grouping-operator + Parser.prototype.reinterpretExpressionAsPattern = function (expr) { + switch (expr.type) { + case syntax_1.Syntax.Identifier: + case syntax_1.Syntax.MemberExpression: + case syntax_1.Syntax.RestElement: + case syntax_1.Syntax.AssignmentPattern: + break; + case syntax_1.Syntax.SpreadElement: + expr.type = syntax_1.Syntax.RestElement; + this.reinterpretExpressionAsPattern(expr.argument); + break; + case syntax_1.Syntax.ArrayExpression: + expr.type = syntax_1.Syntax.ArrayPattern; + for (var i = 0; i < expr.elements.length; i++) { + if (expr.elements[i] !== null) { + this.reinterpretExpressionAsPattern(expr.elements[i]); + } + } + break; + case syntax_1.Syntax.ObjectExpression: + expr.type = syntax_1.Syntax.ObjectPattern; + for (var i = 0; i < expr.properties.length; i++) { + this.reinterpretExpressionAsPattern(expr.properties[i].value); + } + break; + case syntax_1.Syntax.AssignmentExpression: + expr.type = syntax_1.Syntax.AssignmentPattern; + delete expr.operator; + this.reinterpretExpressionAsPattern(expr.left); + break; + default: + // Allow other node type for tolerant parsing. + break; + } + }; + Parser.prototype.parseGroupExpression = function () { + var expr; + this.expect('('); + if (this.match(')')) { + this.nextToken(); + if (!this.match('=>')) { + this.expect('=>'); + } + expr = { + type: ArrowParameterPlaceHolder, + params: [], + async: false + }; + } + else { + var startToken = this.lookahead; + var params = []; + if (this.match('...')) { + expr = this.parseRestElement(params); + this.expect(')'); + if (!this.match('=>')) { + this.expect('=>'); + } + expr = { + type: ArrowParameterPlaceHolder, + params: [expr], + async: false + }; + } + else { + var arrow = false; + this.context.isBindingElement = true; + expr = this.inheritCoverGrammar(this.parseAssignmentExpression); + if (this.match(',')) { + var expressions = []; + this.context.isAssignmentTarget = false; + expressions.push(expr); + while (this.lookahead.type !== 2 /* EOF */) { + if (!this.match(',')) { + break; + } + this.nextToken(); + if (this.match(')')) { + this.nextToken(); + for (var i = 0; i < expressions.length; i++) { + this.reinterpretExpressionAsPattern(expressions[i]); + } + arrow = true; + expr = { + type: ArrowParameterPlaceHolder, + params: expressions, + async: false + }; + } + else if (this.match('...')) { + if (!this.context.isBindingElement) { + this.throwUnexpectedToken(this.lookahead); + } + expressions.push(this.parseRestElement(params)); + this.expect(')'); + if (!this.match('=>')) { + this.expect('=>'); + } + this.context.isBindingElement = false; + for (var i = 0; i < expressions.length; i++) { + this.reinterpretExpressionAsPattern(expressions[i]); + } + arrow = true; + expr = { + type: ArrowParameterPlaceHolder, + params: expressions, + async: false + }; + } + else { + expressions.push(this.inheritCoverGrammar(this.parseAssignmentExpression)); + } + if (arrow) { + break; + } + } + if (!arrow) { + expr = this.finalize(this.startNode(startToken), new Node.SequenceExpression(expressions)); + } + } + if (!arrow) { + this.expect(')'); + if (this.match('=>')) { + if (expr.type === syntax_1.Syntax.Identifier && expr.name === 'yield') { + arrow = true; + expr = { + type: ArrowParameterPlaceHolder, + params: [expr], + async: false + }; + } + if (!arrow) { + if (!this.context.isBindingElement) { + this.throwUnexpectedToken(this.lookahead); + } + if (expr.type === syntax_1.Syntax.SequenceExpression) { + for (var i = 0; i < expr.expressions.length; i++) { + this.reinterpretExpressionAsPattern(expr.expressions[i]); + } + } + else { + this.reinterpretExpressionAsPattern(expr); + } + var parameters = (expr.type === syntax_1.Syntax.SequenceExpression ? expr.expressions : [expr]); + expr = { + type: ArrowParameterPlaceHolder, + params: parameters, + async: false + }; + } + } + this.context.isBindingElement = false; + } + } + } + return expr; + }; + // https://tc39.github.io/ecma262/#sec-left-hand-side-expressions + Parser.prototype.parseArguments = function () { + this.expect('('); + var args = []; + if (!this.match(')')) { + while (true) { + var expr = this.match('...') ? this.parseSpreadElement() : + this.isolateCoverGrammar(this.parseAssignmentExpression); + args.push(expr); + if (this.match(')')) { + break; + } + this.expectCommaSeparator(); + if (this.match(')')) { + break; + } + } + } + this.expect(')'); + return args; + }; + Parser.prototype.isIdentifierName = function (token) { + return token.type === 3 /* Identifier */ || + token.type === 4 /* Keyword */ || + token.type === 1 /* BooleanLiteral */ || + token.type === 5 /* NullLiteral */; + }; + Parser.prototype.parseIdentifierName = function () { + var node = this.createNode(); + var token = this.nextToken(); + if (!this.isIdentifierName(token)) { + this.throwUnexpectedToken(token); + } + return this.finalize(node, new Node.Identifier(token.value)); + }; + Parser.prototype.parseNewExpression = function () { + var node = this.createNode(); + var id = this.parseIdentifierName(); + assert_1.assert(id.name === 'new', 'New expression must start with `new`'); + var expr; + if (this.match('.')) { + this.nextToken(); + if (this.lookahead.type === 3 /* Identifier */ && this.context.inFunctionBody && this.lookahead.value === 'target') { + var property = this.parseIdentifierName(); + expr = new Node.MetaProperty(id, property); + } + else { + this.throwUnexpectedToken(this.lookahead); + } + } + else { + var callee = this.isolateCoverGrammar(this.parseLeftHandSideExpression); + var args = this.match('(') ? this.parseArguments() : []; + expr = new Node.NewExpression(callee, args); + this.context.isAssignmentTarget = false; + this.context.isBindingElement = false; + } + return this.finalize(node, expr); + }; + Parser.prototype.parseAsyncArgument = function () { + var arg = this.parseAssignmentExpression(); + this.context.firstCoverInitializedNameError = null; + return arg; + }; + Parser.prototype.parseAsyncArguments = function () { + this.expect('('); + var args = []; + if (!this.match(')')) { + while (true) { + var expr = this.match('...') ? this.parseSpreadElement() : + this.isolateCoverGrammar(this.parseAsyncArgument); + args.push(expr); + if (this.match(')')) { + break; + } + this.expectCommaSeparator(); + if (this.match(')')) { + break; + } + } + } + this.expect(')'); + return args; + }; + Parser.prototype.parseLeftHandSideExpressionAllowCall = function () { + var startToken = this.lookahead; + var maybeAsync = this.matchContextualKeyword('async'); + var previousAllowIn = this.context.allowIn; + this.context.allowIn = true; + var expr; + if (this.matchKeyword('super') && this.context.inFunctionBody) { + expr = this.createNode(); + this.nextToken(); + expr = this.finalize(expr, new Node.Super()); + if (!this.match('(') && !this.match('.') && !this.match('[')) { + this.throwUnexpectedToken(this.lookahead); + } + } + else { + expr = this.inheritCoverGrammar(this.matchKeyword('new') ? this.parseNewExpression : this.parsePrimaryExpression); + } + while (true) { + if (this.match('.')) { + this.context.isBindingElement = false; + this.context.isAssignmentTarget = true; + this.expect('.'); + var property = this.parseIdentifierName(); + expr = this.finalize(this.startNode(startToken), new Node.StaticMemberExpression(expr, property)); + } + else if (this.match('(')) { + var asyncArrow = maybeAsync && (startToken.lineNumber === this.lookahead.lineNumber); + this.context.isBindingElement = false; + this.context.isAssignmentTarget = false; + var args = asyncArrow ? this.parseAsyncArguments() : this.parseArguments(); + expr = this.finalize(this.startNode(startToken), new Node.CallExpression(expr, args)); + if (asyncArrow && this.match('=>')) { + for (var i = 0; i < args.length; ++i) { + this.reinterpretExpressionAsPattern(args[i]); + } + expr = { + type: ArrowParameterPlaceHolder, + params: args, + async: true + }; + } + } + else if (this.match('[')) { + this.context.isBindingElement = false; + this.context.isAssignmentTarget = true; + this.expect('['); + var property = this.isolateCoverGrammar(this.parseExpression); + this.expect(']'); + expr = this.finalize(this.startNode(startToken), new Node.ComputedMemberExpression(expr, property)); + } + else if (this.lookahead.type === 10 /* Template */ && this.lookahead.head) { + var quasi = this.parseTemplateLiteral(); + expr = this.finalize(this.startNode(startToken), new Node.TaggedTemplateExpression(expr, quasi)); + } + else { + break; + } + } + this.context.allowIn = previousAllowIn; + return expr; + }; + Parser.prototype.parseSuper = function () { + var node = this.createNode(); + this.expectKeyword('super'); + if (!this.match('[') && !this.match('.')) { + this.throwUnexpectedToken(this.lookahead); + } + return this.finalize(node, new Node.Super()); + }; + Parser.prototype.parseLeftHandSideExpression = function () { + assert_1.assert(this.context.allowIn, 'callee of new expression always allow in keyword.'); + var node = this.startNode(this.lookahead); + var expr = (this.matchKeyword('super') && this.context.inFunctionBody) ? this.parseSuper() : + this.inheritCoverGrammar(this.matchKeyword('new') ? this.parseNewExpression : this.parsePrimaryExpression); + while (true) { + if (this.match('[')) { + this.context.isBindingElement = false; + this.context.isAssignmentTarget = true; + this.expect('['); + var property = this.isolateCoverGrammar(this.parseExpression); + this.expect(']'); + expr = this.finalize(node, new Node.ComputedMemberExpression(expr, property)); + } + else if (this.match('.')) { + this.context.isBindingElement = false; + this.context.isAssignmentTarget = true; + this.expect('.'); + var property = this.parseIdentifierName(); + expr = this.finalize(node, new Node.StaticMemberExpression(expr, property)); + } + else if (this.lookahead.type === 10 /* Template */ && this.lookahead.head) { + var quasi = this.parseTemplateLiteral(); + expr = this.finalize(node, new Node.TaggedTemplateExpression(expr, quasi)); + } + else { + break; + } + } + return expr; + }; + // https://tc39.github.io/ecma262/#sec-update-expressions + Parser.prototype.parseUpdateExpression = function () { + var expr; + var startToken = this.lookahead; + if (this.match('++') || this.match('--')) { + var node = this.startNode(startToken); + var token = this.nextToken(); + expr = this.inheritCoverGrammar(this.parseUnaryExpression); + if (this.context.strict && expr.type === syntax_1.Syntax.Identifier && this.scanner.isRestrictedWord(expr.name)) { + this.tolerateError(messages_1.Messages.StrictLHSPrefix); + } + if (!this.context.isAssignmentTarget) { + this.tolerateError(messages_1.Messages.InvalidLHSInAssignment); + } + var prefix = true; + expr = this.finalize(node, new Node.UpdateExpression(token.value, expr, prefix)); + this.context.isAssignmentTarget = false; + this.context.isBindingElement = false; + } + else { + expr = this.inheritCoverGrammar(this.parseLeftHandSideExpressionAllowCall); + if (!this.hasLineTerminator && this.lookahead.type === 7 /* Punctuator */) { + if (this.match('++') || this.match('--')) { + if (this.context.strict && expr.type === syntax_1.Syntax.Identifier && this.scanner.isRestrictedWord(expr.name)) { + this.tolerateError(messages_1.Messages.StrictLHSPostfix); + } + if (!this.context.isAssignmentTarget) { + this.tolerateError(messages_1.Messages.InvalidLHSInAssignment); + } + this.context.isAssignmentTarget = false; + this.context.isBindingElement = false; + var operator = this.nextToken().value; + var prefix = false; + expr = this.finalize(this.startNode(startToken), new Node.UpdateExpression(operator, expr, prefix)); + } + } + } + return expr; + }; + // https://tc39.github.io/ecma262/#sec-unary-operators + Parser.prototype.parseAwaitExpression = function () { + var node = this.createNode(); + this.nextToken(); + var argument = this.parseUnaryExpression(); + return this.finalize(node, new Node.AwaitExpression(argument)); + }; + Parser.prototype.parseUnaryExpression = function () { + var expr; + if (this.match('+') || this.match('-') || this.match('~') || this.match('!') || + this.matchKeyword('delete') || this.matchKeyword('void') || this.matchKeyword('typeof')) { + var node = this.startNode(this.lookahead); + var token = this.nextToken(); + expr = this.inheritCoverGrammar(this.parseUnaryExpression); + expr = this.finalize(node, new Node.UnaryExpression(token.value, expr)); + if (this.context.strict && expr.operator === 'delete' && expr.argument.type === syntax_1.Syntax.Identifier) { + this.tolerateError(messages_1.Messages.StrictDelete); + } + this.context.isAssignmentTarget = false; + this.context.isBindingElement = false; + } + else if (this.context.await && this.matchContextualKeyword('await')) { + expr = this.parseAwaitExpression(); + } + else { + expr = this.parseUpdateExpression(); + } + return expr; + }; + Parser.prototype.parseExponentiationExpression = function () { + var startToken = this.lookahead; + var expr = this.inheritCoverGrammar(this.parseUnaryExpression); + if (expr.type !== syntax_1.Syntax.UnaryExpression && this.match('**')) { + this.nextToken(); + this.context.isAssignmentTarget = false; + this.context.isBindingElement = false; + var left = expr; + var right = this.isolateCoverGrammar(this.parseExponentiationExpression); + expr = this.finalize(this.startNode(startToken), new Node.BinaryExpression('**', left, right)); + } + return expr; + }; + // https://tc39.github.io/ecma262/#sec-exp-operator + // https://tc39.github.io/ecma262/#sec-multiplicative-operators + // https://tc39.github.io/ecma262/#sec-additive-operators + // https://tc39.github.io/ecma262/#sec-bitwise-shift-operators + // https://tc39.github.io/ecma262/#sec-relational-operators + // https://tc39.github.io/ecma262/#sec-equality-operators + // https://tc39.github.io/ecma262/#sec-binary-bitwise-operators + // https://tc39.github.io/ecma262/#sec-binary-logical-operators + Parser.prototype.binaryPrecedence = function (token) { + var op = token.value; + var precedence; + if (token.type === 7 /* Punctuator */) { + precedence = this.operatorPrecedence[op] || 0; + } + else if (token.type === 4 /* Keyword */) { + precedence = (op === 'instanceof' || (this.context.allowIn && op === 'in')) ? 7 : 0; + } + else { + precedence = 0; + } + return precedence; + }; + Parser.prototype.parseBinaryExpression = function () { + var startToken = this.lookahead; + var expr = this.inheritCoverGrammar(this.parseExponentiationExpression); + var token = this.lookahead; + var prec = this.binaryPrecedence(token); + if (prec > 0) { + this.nextToken(); + this.context.isAssignmentTarget = false; + this.context.isBindingElement = false; + var markers = [startToken, this.lookahead]; + var left = expr; + var right = this.isolateCoverGrammar(this.parseExponentiationExpression); + var stack = [left, token.value, right]; + var precedences = [prec]; + while (true) { + prec = this.binaryPrecedence(this.lookahead); + if (prec <= 0) { + break; + } + // Reduce: make a binary expression from the three topmost entries. + while ((stack.length > 2) && (prec <= precedences[precedences.length - 1])) { + right = stack.pop(); + var operator = stack.pop(); + precedences.pop(); + left = stack.pop(); + markers.pop(); + var node = this.startNode(markers[markers.length - 1]); + stack.push(this.finalize(node, new Node.BinaryExpression(operator, left, right))); + } + // Shift. + stack.push(this.nextToken().value); + precedences.push(prec); + markers.push(this.lookahead); + stack.push(this.isolateCoverGrammar(this.parseExponentiationExpression)); + } + // Final reduce to clean-up the stack. + var i = stack.length - 1; + expr = stack[i]; + var lastMarker = markers.pop(); + while (i > 1) { + var marker = markers.pop(); + var lastLineStart = lastMarker && lastMarker.lineStart; + var node = this.startNode(marker, lastLineStart); + var operator = stack[i - 1]; + expr = this.finalize(node, new Node.BinaryExpression(operator, stack[i - 2], expr)); + i -= 2; + lastMarker = marker; + } + } + return expr; + }; + // https://tc39.github.io/ecma262/#sec-conditional-operator + Parser.prototype.parseConditionalExpression = function () { + var startToken = this.lookahead; + var expr = this.inheritCoverGrammar(this.parseBinaryExpression); + if (this.match('?')) { + this.nextToken(); + var previousAllowIn = this.context.allowIn; + this.context.allowIn = true; + var consequent = this.isolateCoverGrammar(this.parseAssignmentExpression); + this.context.allowIn = previousAllowIn; + this.expect(':'); + var alternate = this.isolateCoverGrammar(this.parseAssignmentExpression); + expr = this.finalize(this.startNode(startToken), new Node.ConditionalExpression(expr, consequent, alternate)); + this.context.isAssignmentTarget = false; + this.context.isBindingElement = false; + } + return expr; + }; + // https://tc39.github.io/ecma262/#sec-assignment-operators + Parser.prototype.checkPatternParam = function (options, param) { + switch (param.type) { + case syntax_1.Syntax.Identifier: + this.validateParam(options, param, param.name); + break; + case syntax_1.Syntax.RestElement: + this.checkPatternParam(options, param.argument); + break; + case syntax_1.Syntax.AssignmentPattern: + this.checkPatternParam(options, param.left); + break; + case syntax_1.Syntax.ArrayPattern: + for (var i = 0; i < param.elements.length; i++) { + if (param.elements[i] !== null) { + this.checkPatternParam(options, param.elements[i]); + } + } + break; + case syntax_1.Syntax.ObjectPattern: + for (var i = 0; i < param.properties.length; i++) { + this.checkPatternParam(options, param.properties[i].value); + } + break; + default: + break; + } + options.simple = options.simple && (param instanceof Node.Identifier); + }; + Parser.prototype.reinterpretAsCoverFormalsList = function (expr) { + var params = [expr]; + var options; + var asyncArrow = false; + switch (expr.type) { + case syntax_1.Syntax.Identifier: + break; + case ArrowParameterPlaceHolder: + params = expr.params; + asyncArrow = expr.async; + break; + default: + return null; + } + options = { + simple: true, + paramSet: {} + }; + for (var i = 0; i < params.length; ++i) { + var param = params[i]; + if (param.type === syntax_1.Syntax.AssignmentPattern) { + if (param.right.type === syntax_1.Syntax.YieldExpression) { + if (param.right.argument) { + this.throwUnexpectedToken(this.lookahead); + } + param.right.type = syntax_1.Syntax.Identifier; + param.right.name = 'yield'; + delete param.right.argument; + delete param.right.delegate; + } + } + else if (asyncArrow && param.type === syntax_1.Syntax.Identifier && param.name === 'await') { + this.throwUnexpectedToken(this.lookahead); + } + this.checkPatternParam(options, param); + params[i] = param; + } + if (this.context.strict || !this.context.allowYield) { + for (var i = 0; i < params.length; ++i) { + var param = params[i]; + if (param.type === syntax_1.Syntax.YieldExpression) { + this.throwUnexpectedToken(this.lookahead); + } + } + } + if (options.message === messages_1.Messages.StrictParamDupe) { + var token = this.context.strict ? options.stricted : options.firstRestricted; + this.throwUnexpectedToken(token, options.message); + } + return { + simple: options.simple, + params: params, + stricted: options.stricted, + firstRestricted: options.firstRestricted, + message: options.message + }; + }; + Parser.prototype.parseAssignmentExpression = function () { + var expr; + if (!this.context.allowYield && this.matchKeyword('yield')) { + expr = this.parseYieldExpression(); + } + else { + var startToken = this.lookahead; + var token = startToken; + expr = this.parseConditionalExpression(); + if (token.type === 3 /* Identifier */ && (token.lineNumber === this.lookahead.lineNumber) && token.value === 'async') { + if (this.lookahead.type === 3 /* Identifier */ || this.matchKeyword('yield')) { + var arg = this.parsePrimaryExpression(); + this.reinterpretExpressionAsPattern(arg); + expr = { + type: ArrowParameterPlaceHolder, + params: [arg], + async: true + }; + } + } + if (expr.type === ArrowParameterPlaceHolder || this.match('=>')) { + // https://tc39.github.io/ecma262/#sec-arrow-function-definitions + this.context.isAssignmentTarget = false; + this.context.isBindingElement = false; + var isAsync = expr.async; + var list = this.reinterpretAsCoverFormalsList(expr); + if (list) { + if (this.hasLineTerminator) { + this.tolerateUnexpectedToken(this.lookahead); + } + this.context.firstCoverInitializedNameError = null; + var previousStrict = this.context.strict; + var previousAllowStrictDirective = this.context.allowStrictDirective; + this.context.allowStrictDirective = list.simple; + var previousAllowYield = this.context.allowYield; + var previousAwait = this.context.await; + this.context.allowYield = true; + this.context.await = isAsync; + var node = this.startNode(startToken); + this.expect('=>'); + var body = void 0; + if (this.match('{')) { + var previousAllowIn = this.context.allowIn; + this.context.allowIn = true; + body = this.parseFunctionSourceElements(); + this.context.allowIn = previousAllowIn; + } + else { + body = this.isolateCoverGrammar(this.parseAssignmentExpression); + } + var expression = body.type !== syntax_1.Syntax.BlockStatement; + if (this.context.strict && list.firstRestricted) { + this.throwUnexpectedToken(list.firstRestricted, list.message); + } + if (this.context.strict && list.stricted) { + this.tolerateUnexpectedToken(list.stricted, list.message); + } + expr = isAsync ? this.finalize(node, new Node.AsyncArrowFunctionExpression(list.params, body, expression)) : + this.finalize(node, new Node.ArrowFunctionExpression(list.params, body, expression)); + this.context.strict = previousStrict; + this.context.allowStrictDirective = previousAllowStrictDirective; + this.context.allowYield = previousAllowYield; + this.context.await = previousAwait; + } + } + else { + if (this.matchAssign()) { + if (!this.context.isAssignmentTarget) { + this.tolerateError(messages_1.Messages.InvalidLHSInAssignment); + } + if (this.context.strict && expr.type === syntax_1.Syntax.Identifier) { + var id = expr; + if (this.scanner.isRestrictedWord(id.name)) { + this.tolerateUnexpectedToken(token, messages_1.Messages.StrictLHSAssignment); + } + if (this.scanner.isStrictModeReservedWord(id.name)) { + this.tolerateUnexpectedToken(token, messages_1.Messages.StrictReservedWord); + } + } + if (!this.match('=')) { + this.context.isAssignmentTarget = false; + this.context.isBindingElement = false; + } + else { + this.reinterpretExpressionAsPattern(expr); + } + token = this.nextToken(); + var operator = token.value; + var right = this.isolateCoverGrammar(this.parseAssignmentExpression); + expr = this.finalize(this.startNode(startToken), new Node.AssignmentExpression(operator, expr, right)); + this.context.firstCoverInitializedNameError = null; + } + } + } + return expr; + }; + // https://tc39.github.io/ecma262/#sec-comma-operator + Parser.prototype.parseExpression = function () { + var startToken = this.lookahead; + var expr = this.isolateCoverGrammar(this.parseAssignmentExpression); + if (this.match(',')) { + var expressions = []; + expressions.push(expr); + while (this.lookahead.type !== 2 /* EOF */) { + if (!this.match(',')) { + break; + } + this.nextToken(); + expressions.push(this.isolateCoverGrammar(this.parseAssignmentExpression)); + } + expr = this.finalize(this.startNode(startToken), new Node.SequenceExpression(expressions)); + } + return expr; + }; + // https://tc39.github.io/ecma262/#sec-block + Parser.prototype.parseStatementListItem = function () { + var statement; + this.context.isAssignmentTarget = true; + this.context.isBindingElement = true; + if (this.lookahead.type === 4 /* Keyword */) { + switch (this.lookahead.value) { + case 'export': + if (!this.context.isModule) { + this.tolerateUnexpectedToken(this.lookahead, messages_1.Messages.IllegalExportDeclaration); + } + statement = this.parseExportDeclaration(); + break; + case 'import': + if (!this.context.isModule) { + this.tolerateUnexpectedToken(this.lookahead, messages_1.Messages.IllegalImportDeclaration); + } + statement = this.parseImportDeclaration(); + break; + case 'const': + statement = this.parseLexicalDeclaration({ inFor: false }); + break; + case 'function': + statement = this.parseFunctionDeclaration(); + break; + case 'class': + statement = this.parseClassDeclaration(); + break; + case 'let': + statement = this.isLexicalDeclaration() ? this.parseLexicalDeclaration({ inFor: false }) : this.parseStatement(); + break; + default: + statement = this.parseStatement(); + break; + } + } + else { + statement = this.parseStatement(); + } + return statement; + }; + Parser.prototype.parseBlock = function () { + var node = this.createNode(); + this.expect('{'); + var block = []; + while (true) { + if (this.match('}')) { + break; + } + block.push(this.parseStatementListItem()); + } + this.expect('}'); + return this.finalize(node, new Node.BlockStatement(block)); + }; + // https://tc39.github.io/ecma262/#sec-let-and-const-declarations + Parser.prototype.parseLexicalBinding = function (kind, options) { + var node = this.createNode(); + var params = []; + var id = this.parsePattern(params, kind); + if (this.context.strict && id.type === syntax_1.Syntax.Identifier) { + if (this.scanner.isRestrictedWord(id.name)) { + this.tolerateError(messages_1.Messages.StrictVarName); + } + } + var init = null; + if (kind === 'const') { + if (!this.matchKeyword('in') && !this.matchContextualKeyword('of')) { + if (this.match('=')) { + this.nextToken(); + init = this.isolateCoverGrammar(this.parseAssignmentExpression); + } + else { + this.throwError(messages_1.Messages.DeclarationMissingInitializer, 'const'); + } + } + } + else if ((!options.inFor && id.type !== syntax_1.Syntax.Identifier) || this.match('=')) { + this.expect('='); + init = this.isolateCoverGrammar(this.parseAssignmentExpression); + } + return this.finalize(node, new Node.VariableDeclarator(id, init)); + }; + Parser.prototype.parseBindingList = function (kind, options) { + var list = [this.parseLexicalBinding(kind, options)]; + while (this.match(',')) { + this.nextToken(); + list.push(this.parseLexicalBinding(kind, options)); + } + return list; + }; + Parser.prototype.isLexicalDeclaration = function () { + var state = this.scanner.saveState(); + this.scanner.scanComments(); + var next = this.scanner.lex(); + this.scanner.restoreState(state); + return (next.type === 3 /* Identifier */) || + (next.type === 7 /* Punctuator */ && next.value === '[') || + (next.type === 7 /* Punctuator */ && next.value === '{') || + (next.type === 4 /* Keyword */ && next.value === 'let') || + (next.type === 4 /* Keyword */ && next.value === 'yield'); + }; + Parser.prototype.parseLexicalDeclaration = function (options) { + var node = this.createNode(); + var kind = this.nextToken().value; + assert_1.assert(kind === 'let' || kind === 'const', 'Lexical declaration must be either let or const'); + var declarations = this.parseBindingList(kind, options); + this.consumeSemicolon(); + return this.finalize(node, new Node.VariableDeclaration(declarations, kind)); + }; + // https://tc39.github.io/ecma262/#sec-destructuring-binding-patterns + Parser.prototype.parseBindingRestElement = function (params, kind) { + var node = this.createNode(); + this.expect('...'); + var arg = this.parsePattern(params, kind); + return this.finalize(node, new Node.RestElement(arg)); + }; + Parser.prototype.parseArrayPattern = function (params, kind) { + var node = this.createNode(); + this.expect('['); + var elements = []; + while (!this.match(']')) { + if (this.match(',')) { + this.nextToken(); + elements.push(null); + } + else { + if (this.match('...')) { + elements.push(this.parseBindingRestElement(params, kind)); + break; + } + else { + elements.push(this.parsePatternWithDefault(params, kind)); + } + if (!this.match(']')) { + this.expect(','); + } + } + } + this.expect(']'); + return this.finalize(node, new Node.ArrayPattern(elements)); + }; + Parser.prototype.parsePropertyPattern = function (params, kind) { + var node = this.createNode(); + var computed = false; + var shorthand = false; + var method = false; + var key; + var value; + if (this.lookahead.type === 3 /* Identifier */) { + var keyToken = this.lookahead; + key = this.parseVariableIdentifier(); + var init = this.finalize(node, new Node.Identifier(keyToken.value)); + if (this.match('=')) { + params.push(keyToken); + shorthand = true; + this.nextToken(); + var expr = this.parseAssignmentExpression(); + value = this.finalize(this.startNode(keyToken), new Node.AssignmentPattern(init, expr)); + } + else if (!this.match(':')) { + params.push(keyToken); + shorthand = true; + value = init; + } + else { + this.expect(':'); + value = this.parsePatternWithDefault(params, kind); + } + } + else { + computed = this.match('['); + key = this.parseObjectPropertyKey(); + this.expect(':'); + value = this.parsePatternWithDefault(params, kind); + } + return this.finalize(node, new Node.Property('init', key, computed, value, method, shorthand)); + }; + Parser.prototype.parseObjectPattern = function (params, kind) { + var node = this.createNode(); + var properties = []; + this.expect('{'); + while (!this.match('}')) { + properties.push(this.parsePropertyPattern(params, kind)); + if (!this.match('}')) { + this.expect(','); + } + } + this.expect('}'); + return this.finalize(node, new Node.ObjectPattern(properties)); + }; + Parser.prototype.parsePattern = function (params, kind) { + var pattern; + if (this.match('[')) { + pattern = this.parseArrayPattern(params, kind); + } + else if (this.match('{')) { + pattern = this.parseObjectPattern(params, kind); + } + else { + if (this.matchKeyword('let') && (kind === 'const' || kind === 'let')) { + this.tolerateUnexpectedToken(this.lookahead, messages_1.Messages.LetInLexicalBinding); + } + params.push(this.lookahead); + pattern = this.parseVariableIdentifier(kind); + } + return pattern; + }; + Parser.prototype.parsePatternWithDefault = function (params, kind) { + var startToken = this.lookahead; + var pattern = this.parsePattern(params, kind); + if (this.match('=')) { + this.nextToken(); + var previousAllowYield = this.context.allowYield; + this.context.allowYield = true; + var right = this.isolateCoverGrammar(this.parseAssignmentExpression); + this.context.allowYield = previousAllowYield; + pattern = this.finalize(this.startNode(startToken), new Node.AssignmentPattern(pattern, right)); + } + return pattern; + }; + // https://tc39.github.io/ecma262/#sec-variable-statement + Parser.prototype.parseVariableIdentifier = function (kind) { + var node = this.createNode(); + var token = this.nextToken(); + if (token.type === 4 /* Keyword */ && token.value === 'yield') { + if (this.context.strict) { + this.tolerateUnexpectedToken(token, messages_1.Messages.StrictReservedWord); + } + else if (!this.context.allowYield) { + this.throwUnexpectedToken(token); + } + } + else if (token.type !== 3 /* Identifier */) { + if (this.context.strict && token.type === 4 /* Keyword */ && this.scanner.isStrictModeReservedWord(token.value)) { + this.tolerateUnexpectedToken(token, messages_1.Messages.StrictReservedWord); + } + else { + if (this.context.strict || token.value !== 'let' || kind !== 'var') { + this.throwUnexpectedToken(token); + } + } + } + else if ((this.context.isModule || this.context.await) && token.type === 3 /* Identifier */ && token.value === 'await') { + this.tolerateUnexpectedToken(token); + } + return this.finalize(node, new Node.Identifier(token.value)); + }; + Parser.prototype.parseVariableDeclaration = function (options) { + var node = this.createNode(); + var params = []; + var id = this.parsePattern(params, 'var'); + if (this.context.strict && id.type === syntax_1.Syntax.Identifier) { + if (this.scanner.isRestrictedWord(id.name)) { + this.tolerateError(messages_1.Messages.StrictVarName); + } + } + var init = null; + if (this.match('=')) { + this.nextToken(); + init = this.isolateCoverGrammar(this.parseAssignmentExpression); + } + else if (id.type !== syntax_1.Syntax.Identifier && !options.inFor) { + this.expect('='); + } + return this.finalize(node, new Node.VariableDeclarator(id, init)); + }; + Parser.prototype.parseVariableDeclarationList = function (options) { + var opt = { inFor: options.inFor }; + var list = []; + list.push(this.parseVariableDeclaration(opt)); + while (this.match(',')) { + this.nextToken(); + list.push(this.parseVariableDeclaration(opt)); + } + return list; + }; + Parser.prototype.parseVariableStatement = function () { + var node = this.createNode(); + this.expectKeyword('var'); + var declarations = this.parseVariableDeclarationList({ inFor: false }); + this.consumeSemicolon(); + return this.finalize(node, new Node.VariableDeclaration(declarations, 'var')); + }; + // https://tc39.github.io/ecma262/#sec-empty-statement + Parser.prototype.parseEmptyStatement = function () { + var node = this.createNode(); + this.expect(';'); + return this.finalize(node, new Node.EmptyStatement()); + }; + // https://tc39.github.io/ecma262/#sec-expression-statement + Parser.prototype.parseExpressionStatement = function () { + var node = this.createNode(); + var expr = this.parseExpression(); + this.consumeSemicolon(); + return this.finalize(node, new Node.ExpressionStatement(expr)); + }; + // https://tc39.github.io/ecma262/#sec-if-statement + Parser.prototype.parseIfClause = function () { + if (this.context.strict && this.matchKeyword('function')) { + this.tolerateError(messages_1.Messages.StrictFunction); + } + return this.parseStatement(); + }; + Parser.prototype.parseIfStatement = function () { + var node = this.createNode(); + var consequent; + var alternate = null; + this.expectKeyword('if'); + this.expect('('); + var test = this.parseExpression(); + if (!this.match(')') && this.config.tolerant) { + this.tolerateUnexpectedToken(this.nextToken()); + consequent = this.finalize(this.createNode(), new Node.EmptyStatement()); + } + else { + this.expect(')'); + consequent = this.parseIfClause(); + if (this.matchKeyword('else')) { + this.nextToken(); + alternate = this.parseIfClause(); + } + } + return this.finalize(node, new Node.IfStatement(test, consequent, alternate)); + }; + // https://tc39.github.io/ecma262/#sec-do-while-statement + Parser.prototype.parseDoWhileStatement = function () { + var node = this.createNode(); + this.expectKeyword('do'); + var previousInIteration = this.context.inIteration; + this.context.inIteration = true; + var body = this.parseStatement(); + this.context.inIteration = previousInIteration; + this.expectKeyword('while'); + this.expect('('); + var test = this.parseExpression(); + if (!this.match(')') && this.config.tolerant) { + this.tolerateUnexpectedToken(this.nextToken()); + } + else { + this.expect(')'); + if (this.match(';')) { + this.nextToken(); + } + } + return this.finalize(node, new Node.DoWhileStatement(body, test)); + }; + // https://tc39.github.io/ecma262/#sec-while-statement + Parser.prototype.parseWhileStatement = function () { + var node = this.createNode(); + var body; + this.expectKeyword('while'); + this.expect('('); + var test = this.parseExpression(); + if (!this.match(')') && this.config.tolerant) { + this.tolerateUnexpectedToken(this.nextToken()); + body = this.finalize(this.createNode(), new Node.EmptyStatement()); + } + else { + this.expect(')'); + var previousInIteration = this.context.inIteration; + this.context.inIteration = true; + body = this.parseStatement(); + this.context.inIteration = previousInIteration; + } + return this.finalize(node, new Node.WhileStatement(test, body)); + }; + // https://tc39.github.io/ecma262/#sec-for-statement + // https://tc39.github.io/ecma262/#sec-for-in-and-for-of-statements + Parser.prototype.parseForStatement = function () { + var init = null; + var test = null; + var update = null; + var forIn = true; + var left, right; + var node = this.createNode(); + this.expectKeyword('for'); + this.expect('('); + if (this.match(';')) { + this.nextToken(); + } + else { + if (this.matchKeyword('var')) { + init = this.createNode(); + this.nextToken(); + var previousAllowIn = this.context.allowIn; + this.context.allowIn = false; + var declarations = this.parseVariableDeclarationList({ inFor: true }); + this.context.allowIn = previousAllowIn; + if (declarations.length === 1 && this.matchKeyword('in')) { + var decl = declarations[0]; + if (decl.init && (decl.id.type === syntax_1.Syntax.ArrayPattern || decl.id.type === syntax_1.Syntax.ObjectPattern || this.context.strict)) { + this.tolerateError(messages_1.Messages.ForInOfLoopInitializer, 'for-in'); + } + init = this.finalize(init, new Node.VariableDeclaration(declarations, 'var')); + this.nextToken(); + left = init; + right = this.parseExpression(); + init = null; + } + else if (declarations.length === 1 && declarations[0].init === null && this.matchContextualKeyword('of')) { + init = this.finalize(init, new Node.VariableDeclaration(declarations, 'var')); + this.nextToken(); + left = init; + right = this.parseAssignmentExpression(); + init = null; + forIn = false; + } + else { + init = this.finalize(init, new Node.VariableDeclaration(declarations, 'var')); + this.expect(';'); + } + } + else if (this.matchKeyword('const') || this.matchKeyword('let')) { + init = this.createNode(); + var kind = this.nextToken().value; + if (!this.context.strict && this.lookahead.value === 'in') { + init = this.finalize(init, new Node.Identifier(kind)); + this.nextToken(); + left = init; + right = this.parseExpression(); + init = null; + } + else { + var previousAllowIn = this.context.allowIn; + this.context.allowIn = false; + var declarations = this.parseBindingList(kind, { inFor: true }); + this.context.allowIn = previousAllowIn; + if (declarations.length === 1 && declarations[0].init === null && this.matchKeyword('in')) { + init = this.finalize(init, new Node.VariableDeclaration(declarations, kind)); + this.nextToken(); + left = init; + right = this.parseExpression(); + init = null; + } + else if (declarations.length === 1 && declarations[0].init === null && this.matchContextualKeyword('of')) { + init = this.finalize(init, new Node.VariableDeclaration(declarations, kind)); + this.nextToken(); + left = init; + right = this.parseAssignmentExpression(); + init = null; + forIn = false; + } + else { + this.consumeSemicolon(); + init = this.finalize(init, new Node.VariableDeclaration(declarations, kind)); + } + } + } + else { + var initStartToken = this.lookahead; + var previousAllowIn = this.context.allowIn; + this.context.allowIn = false; + init = this.inheritCoverGrammar(this.parseAssignmentExpression); + this.context.allowIn = previousAllowIn; + if (this.matchKeyword('in')) { + if (!this.context.isAssignmentTarget || init.type === syntax_1.Syntax.AssignmentExpression) { + this.tolerateError(messages_1.Messages.InvalidLHSInForIn); + } + this.nextToken(); + this.reinterpretExpressionAsPattern(init); + left = init; + right = this.parseExpression(); + init = null; + } + else if (this.matchContextualKeyword('of')) { + if (!this.context.isAssignmentTarget || init.type === syntax_1.Syntax.AssignmentExpression) { + this.tolerateError(messages_1.Messages.InvalidLHSInForLoop); + } + this.nextToken(); + this.reinterpretExpressionAsPattern(init); + left = init; + right = this.parseAssignmentExpression(); + init = null; + forIn = false; + } + else { + if (this.match(',')) { + var initSeq = [init]; + while (this.match(',')) { + this.nextToken(); + initSeq.push(this.isolateCoverGrammar(this.parseAssignmentExpression)); + } + init = this.finalize(this.startNode(initStartToken), new Node.SequenceExpression(initSeq)); + } + this.expect(';'); + } + } + } + if (typeof left === 'undefined') { + if (!this.match(';')) { + test = this.parseExpression(); + } + this.expect(';'); + if (!this.match(')')) { + update = this.parseExpression(); + } + } + var body; + if (!this.match(')') && this.config.tolerant) { + this.tolerateUnexpectedToken(this.nextToken()); + body = this.finalize(this.createNode(), new Node.EmptyStatement()); + } + else { + this.expect(')'); + var previousInIteration = this.context.inIteration; + this.context.inIteration = true; + body = this.isolateCoverGrammar(this.parseStatement); + this.context.inIteration = previousInIteration; + } + return (typeof left === 'undefined') ? + this.finalize(node, new Node.ForStatement(init, test, update, body)) : + forIn ? this.finalize(node, new Node.ForInStatement(left, right, body)) : + this.finalize(node, new Node.ForOfStatement(left, right, body)); + }; + // https://tc39.github.io/ecma262/#sec-continue-statement + Parser.prototype.parseContinueStatement = function () { + var node = this.createNode(); + this.expectKeyword('continue'); + var label = null; + if (this.lookahead.type === 3 /* Identifier */ && !this.hasLineTerminator) { + var id = this.parseVariableIdentifier(); + label = id; + var key = '$' + id.name; + if (!Object.prototype.hasOwnProperty.call(this.context.labelSet, key)) { + this.throwError(messages_1.Messages.UnknownLabel, id.name); + } + } + this.consumeSemicolon(); + if (label === null && !this.context.inIteration) { + this.throwError(messages_1.Messages.IllegalContinue); + } + return this.finalize(node, new Node.ContinueStatement(label)); + }; + // https://tc39.github.io/ecma262/#sec-break-statement + Parser.prototype.parseBreakStatement = function () { + var node = this.createNode(); + this.expectKeyword('break'); + var label = null; + if (this.lookahead.type === 3 /* Identifier */ && !this.hasLineTerminator) { + var id = this.parseVariableIdentifier(); + var key = '$' + id.name; + if (!Object.prototype.hasOwnProperty.call(this.context.labelSet, key)) { + this.throwError(messages_1.Messages.UnknownLabel, id.name); + } + label = id; + } + this.consumeSemicolon(); + if (label === null && !this.context.inIteration && !this.context.inSwitch) { + this.throwError(messages_1.Messages.IllegalBreak); + } + return this.finalize(node, new Node.BreakStatement(label)); + }; + // https://tc39.github.io/ecma262/#sec-return-statement + Parser.prototype.parseReturnStatement = function () { + if (!this.context.inFunctionBody) { + this.tolerateError(messages_1.Messages.IllegalReturn); + } + var node = this.createNode(); + this.expectKeyword('return'); + var hasArgument = (!this.match(';') && !this.match('}') && + !this.hasLineTerminator && this.lookahead.type !== 2 /* EOF */) || + this.lookahead.type === 8 /* StringLiteral */ || + this.lookahead.type === 10 /* Template */; + var argument = hasArgument ? this.parseExpression() : null; + this.consumeSemicolon(); + return this.finalize(node, new Node.ReturnStatement(argument)); + }; + // https://tc39.github.io/ecma262/#sec-with-statement + Parser.prototype.parseWithStatement = function () { + if (this.context.strict) { + this.tolerateError(messages_1.Messages.StrictModeWith); + } + var node = this.createNode(); + var body; + this.expectKeyword('with'); + this.expect('('); + var object = this.parseExpression(); + if (!this.match(')') && this.config.tolerant) { + this.tolerateUnexpectedToken(this.nextToken()); + body = this.finalize(this.createNode(), new Node.EmptyStatement()); + } + else { + this.expect(')'); + body = this.parseStatement(); + } + return this.finalize(node, new Node.WithStatement(object, body)); + }; + // https://tc39.github.io/ecma262/#sec-switch-statement + Parser.prototype.parseSwitchCase = function () { + var node = this.createNode(); + var test; + if (this.matchKeyword('default')) { + this.nextToken(); + test = null; + } + else { + this.expectKeyword('case'); + test = this.parseExpression(); + } + this.expect(':'); + var consequent = []; + while (true) { + if (this.match('}') || this.matchKeyword('default') || this.matchKeyword('case')) { + break; + } + consequent.push(this.parseStatementListItem()); + } + return this.finalize(node, new Node.SwitchCase(test, consequent)); + }; + Parser.prototype.parseSwitchStatement = function () { + var node = this.createNode(); + this.expectKeyword('switch'); + this.expect('('); + var discriminant = this.parseExpression(); + this.expect(')'); + var previousInSwitch = this.context.inSwitch; + this.context.inSwitch = true; + var cases = []; + var defaultFound = false; + this.expect('{'); + while (true) { + if (this.match('}')) { + break; + } + var clause = this.parseSwitchCase(); + if (clause.test === null) { + if (defaultFound) { + this.throwError(messages_1.Messages.MultipleDefaultsInSwitch); + } + defaultFound = true; + } + cases.push(clause); + } + this.expect('}'); + this.context.inSwitch = previousInSwitch; + return this.finalize(node, new Node.SwitchStatement(discriminant, cases)); + }; + // https://tc39.github.io/ecma262/#sec-labelled-statements + Parser.prototype.parseLabelledStatement = function () { + var node = this.createNode(); + var expr = this.parseExpression(); + var statement; + if ((expr.type === syntax_1.Syntax.Identifier) && this.match(':')) { + this.nextToken(); + var id = expr; + var key = '$' + id.name; + if (Object.prototype.hasOwnProperty.call(this.context.labelSet, key)) { + this.throwError(messages_1.Messages.Redeclaration, 'Label', id.name); + } + this.context.labelSet[key] = true; + var body = void 0; + if (this.matchKeyword('class')) { + this.tolerateUnexpectedToken(this.lookahead); + body = this.parseClassDeclaration(); + } + else if (this.matchKeyword('function')) { + var token = this.lookahead; + var declaration = this.parseFunctionDeclaration(); + if (this.context.strict) { + this.tolerateUnexpectedToken(token, messages_1.Messages.StrictFunction); + } + else if (declaration.generator) { + this.tolerateUnexpectedToken(token, messages_1.Messages.GeneratorInLegacyContext); + } + body = declaration; + } + else { + body = this.parseStatement(); + } + delete this.context.labelSet[key]; + statement = new Node.LabeledStatement(id, body); + } + else { + this.consumeSemicolon(); + statement = new Node.ExpressionStatement(expr); + } + return this.finalize(node, statement); + }; + // https://tc39.github.io/ecma262/#sec-throw-statement + Parser.prototype.parseThrowStatement = function () { + var node = this.createNode(); + this.expectKeyword('throw'); + if (this.hasLineTerminator) { + this.throwError(messages_1.Messages.NewlineAfterThrow); + } + var argument = this.parseExpression(); + this.consumeSemicolon(); + return this.finalize(node, new Node.ThrowStatement(argument)); + }; + // https://tc39.github.io/ecma262/#sec-try-statement + Parser.prototype.parseCatchClause = function () { + var node = this.createNode(); + this.expectKeyword('catch'); + this.expect('('); + if (this.match(')')) { + this.throwUnexpectedToken(this.lookahead); + } + var params = []; + var param = this.parsePattern(params); + var paramMap = {}; + for (var i = 0; i < params.length; i++) { + var key = '$' + params[i].value; + if (Object.prototype.hasOwnProperty.call(paramMap, key)) { + this.tolerateError(messages_1.Messages.DuplicateBinding, params[i].value); + } + paramMap[key] = true; + } + if (this.context.strict && param.type === syntax_1.Syntax.Identifier) { + if (this.scanner.isRestrictedWord(param.name)) { + this.tolerateError(messages_1.Messages.StrictCatchVariable); + } + } + this.expect(')'); + var body = this.parseBlock(); + return this.finalize(node, new Node.CatchClause(param, body)); + }; + Parser.prototype.parseFinallyClause = function () { + this.expectKeyword('finally'); + return this.parseBlock(); + }; + Parser.prototype.parseTryStatement = function () { + var node = this.createNode(); + this.expectKeyword('try'); + var block = this.parseBlock(); + var handler = this.matchKeyword('catch') ? this.parseCatchClause() : null; + var finalizer = this.matchKeyword('finally') ? this.parseFinallyClause() : null; + if (!handler && !finalizer) { + this.throwError(messages_1.Messages.NoCatchOrFinally); + } + return this.finalize(node, new Node.TryStatement(block, handler, finalizer)); + }; + // https://tc39.github.io/ecma262/#sec-debugger-statement + Parser.prototype.parseDebuggerStatement = function () { + var node = this.createNode(); + this.expectKeyword('debugger'); + this.consumeSemicolon(); + return this.finalize(node, new Node.DebuggerStatement()); + }; + // https://tc39.github.io/ecma262/#sec-ecmascript-language-statements-and-declarations + Parser.prototype.parseStatement = function () { + var statement; + switch (this.lookahead.type) { + case 1 /* BooleanLiteral */: + case 5 /* NullLiteral */: + case 6 /* NumericLiteral */: + case 8 /* StringLiteral */: + case 10 /* Template */: + case 9 /* RegularExpression */: + statement = this.parseExpressionStatement(); + break; + case 7 /* Punctuator */: + var value = this.lookahead.value; + if (value === '{') { + statement = this.parseBlock(); + } + else if (value === '(') { + statement = this.parseExpressionStatement(); + } + else if (value === ';') { + statement = this.parseEmptyStatement(); + } + else { + statement = this.parseExpressionStatement(); + } + break; + case 3 /* Identifier */: + statement = this.matchAsyncFunction() ? this.parseFunctionDeclaration() : this.parseLabelledStatement(); + break; + case 4 /* Keyword */: + switch (this.lookahead.value) { + case 'break': + statement = this.parseBreakStatement(); + break; + case 'continue': + statement = this.parseContinueStatement(); + break; + case 'debugger': + statement = this.parseDebuggerStatement(); + break; + case 'do': + statement = this.parseDoWhileStatement(); + break; + case 'for': + statement = this.parseForStatement(); + break; + case 'function': + statement = this.parseFunctionDeclaration(); + break; + case 'if': + statement = this.parseIfStatement(); + break; + case 'return': + statement = this.parseReturnStatement(); + break; + case 'switch': + statement = this.parseSwitchStatement(); + break; + case 'throw': + statement = this.parseThrowStatement(); + break; + case 'try': + statement = this.parseTryStatement(); + break; + case 'var': + statement = this.parseVariableStatement(); + break; + case 'while': + statement = this.parseWhileStatement(); + break; + case 'with': + statement = this.parseWithStatement(); + break; + default: + statement = this.parseExpressionStatement(); + break; + } + break; + default: + statement = this.throwUnexpectedToken(this.lookahead); + } + return statement; + }; + // https://tc39.github.io/ecma262/#sec-function-definitions + Parser.prototype.parseFunctionSourceElements = function () { + var node = this.createNode(); + this.expect('{'); + var body = this.parseDirectivePrologues(); + var previousLabelSet = this.context.labelSet; + var previousInIteration = this.context.inIteration; + var previousInSwitch = this.context.inSwitch; + var previousInFunctionBody = this.context.inFunctionBody; + this.context.labelSet = {}; + this.context.inIteration = false; + this.context.inSwitch = false; + this.context.inFunctionBody = true; + while (this.lookahead.type !== 2 /* EOF */) { + if (this.match('}')) { + break; + } + body.push(this.parseStatementListItem()); + } + this.expect('}'); + this.context.labelSet = previousLabelSet; + this.context.inIteration = previousInIteration; + this.context.inSwitch = previousInSwitch; + this.context.inFunctionBody = previousInFunctionBody; + return this.finalize(node, new Node.BlockStatement(body)); + }; + Parser.prototype.validateParam = function (options, param, name) { + var key = '$' + name; + if (this.context.strict) { + if (this.scanner.isRestrictedWord(name)) { + options.stricted = param; + options.message = messages_1.Messages.StrictParamName; + } + if (Object.prototype.hasOwnProperty.call(options.paramSet, key)) { + options.stricted = param; + options.message = messages_1.Messages.StrictParamDupe; + } + } + else if (!options.firstRestricted) { + if (this.scanner.isRestrictedWord(name)) { + options.firstRestricted = param; + options.message = messages_1.Messages.StrictParamName; + } + else if (this.scanner.isStrictModeReservedWord(name)) { + options.firstRestricted = param; + options.message = messages_1.Messages.StrictReservedWord; + } + else if (Object.prototype.hasOwnProperty.call(options.paramSet, key)) { + options.stricted = param; + options.message = messages_1.Messages.StrictParamDupe; + } + } + /* istanbul ignore next */ + if (typeof Object.defineProperty === 'function') { + Object.defineProperty(options.paramSet, key, { value: true, enumerable: true, writable: true, configurable: true }); + } + else { + options.paramSet[key] = true; + } + }; + Parser.prototype.parseRestElement = function (params) { + var node = this.createNode(); + this.expect('...'); + var arg = this.parsePattern(params); + if (this.match('=')) { + this.throwError(messages_1.Messages.DefaultRestParameter); + } + if (!this.match(')')) { + this.throwError(messages_1.Messages.ParameterAfterRestParameter); + } + return this.finalize(node, new Node.RestElement(arg)); + }; + Parser.prototype.parseFormalParameter = function (options) { + var params = []; + var param = this.match('...') ? this.parseRestElement(params) : this.parsePatternWithDefault(params); + for (var i = 0; i < params.length; i++) { + this.validateParam(options, params[i], params[i].value); + } + options.simple = options.simple && (param instanceof Node.Identifier); + options.params.push(param); + }; + Parser.prototype.parseFormalParameters = function (firstRestricted) { + var options; + options = { + simple: true, + params: [], + firstRestricted: firstRestricted + }; + this.expect('('); + if (!this.match(')')) { + options.paramSet = {}; + while (this.lookahead.type !== 2 /* EOF */) { + this.parseFormalParameter(options); + if (this.match(')')) { + break; + } + this.expect(','); + if (this.match(')')) { + break; + } + } + } + this.expect(')'); + return { + simple: options.simple, + params: options.params, + stricted: options.stricted, + firstRestricted: options.firstRestricted, + message: options.message + }; + }; + Parser.prototype.matchAsyncFunction = function () { + var match = this.matchContextualKeyword('async'); + if (match) { + var state = this.scanner.saveState(); + this.scanner.scanComments(); + var next = this.scanner.lex(); + this.scanner.restoreState(state); + match = (state.lineNumber === next.lineNumber) && (next.type === 4 /* Keyword */) && (next.value === 'function'); + } + return match; + }; + Parser.prototype.parseFunctionDeclaration = function (identifierIsOptional) { + var node = this.createNode(); + var isAsync = this.matchContextualKeyword('async'); + if (isAsync) { + this.nextToken(); + } + this.expectKeyword('function'); + var isGenerator = isAsync ? false : this.match('*'); + if (isGenerator) { + this.nextToken(); + } + var message; + var id = null; + var firstRestricted = null; + if (!identifierIsOptional || !this.match('(')) { + var token = this.lookahead; + id = this.parseVariableIdentifier(); + if (this.context.strict) { + if (this.scanner.isRestrictedWord(token.value)) { + this.tolerateUnexpectedToken(token, messages_1.Messages.StrictFunctionName); + } + } + else { + if (this.scanner.isRestrictedWord(token.value)) { + firstRestricted = token; + message = messages_1.Messages.StrictFunctionName; + } + else if (this.scanner.isStrictModeReservedWord(token.value)) { + firstRestricted = token; + message = messages_1.Messages.StrictReservedWord; + } + } + } + var previousAllowAwait = this.context.await; + var previousAllowYield = this.context.allowYield; + this.context.await = isAsync; + this.context.allowYield = !isGenerator; + var formalParameters = this.parseFormalParameters(firstRestricted); + var params = formalParameters.params; + var stricted = formalParameters.stricted; + firstRestricted = formalParameters.firstRestricted; + if (formalParameters.message) { + message = formalParameters.message; + } + var previousStrict = this.context.strict; + var previousAllowStrictDirective = this.context.allowStrictDirective; + this.context.allowStrictDirective = formalParameters.simple; + var body = this.parseFunctionSourceElements(); + if (this.context.strict && firstRestricted) { + this.throwUnexpectedToken(firstRestricted, message); + } + if (this.context.strict && stricted) { + this.tolerateUnexpectedToken(stricted, message); + } + this.context.strict = previousStrict; + this.context.allowStrictDirective = previousAllowStrictDirective; + this.context.await = previousAllowAwait; + this.context.allowYield = previousAllowYield; + return isAsync ? this.finalize(node, new Node.AsyncFunctionDeclaration(id, params, body)) : + this.finalize(node, new Node.FunctionDeclaration(id, params, body, isGenerator)); + }; + Parser.prototype.parseFunctionExpression = function () { + var node = this.createNode(); + var isAsync = this.matchContextualKeyword('async'); + if (isAsync) { + this.nextToken(); + } + this.expectKeyword('function'); + var isGenerator = isAsync ? false : this.match('*'); + if (isGenerator) { + this.nextToken(); + } + var message; + var id = null; + var firstRestricted; + var previousAllowAwait = this.context.await; + var previousAllowYield = this.context.allowYield; + this.context.await = isAsync; + this.context.allowYield = !isGenerator; + if (!this.match('(')) { + var token = this.lookahead; + id = (!this.context.strict && !isGenerator && this.matchKeyword('yield')) ? this.parseIdentifierName() : this.parseVariableIdentifier(); + if (this.context.strict) { + if (this.scanner.isRestrictedWord(token.value)) { + this.tolerateUnexpectedToken(token, messages_1.Messages.StrictFunctionName); + } + } + else { + if (this.scanner.isRestrictedWord(token.value)) { + firstRestricted = token; + message = messages_1.Messages.StrictFunctionName; + } + else if (this.scanner.isStrictModeReservedWord(token.value)) { + firstRestricted = token; + message = messages_1.Messages.StrictReservedWord; + } + } + } + var formalParameters = this.parseFormalParameters(firstRestricted); + var params = formalParameters.params; + var stricted = formalParameters.stricted; + firstRestricted = formalParameters.firstRestricted; + if (formalParameters.message) { + message = formalParameters.message; + } + var previousStrict = this.context.strict; + var previousAllowStrictDirective = this.context.allowStrictDirective; + this.context.allowStrictDirective = formalParameters.simple; + var body = this.parseFunctionSourceElements(); + if (this.context.strict && firstRestricted) { + this.throwUnexpectedToken(firstRestricted, message); + } + if (this.context.strict && stricted) { + this.tolerateUnexpectedToken(stricted, message); + } + this.context.strict = previousStrict; + this.context.allowStrictDirective = previousAllowStrictDirective; + this.context.await = previousAllowAwait; + this.context.allowYield = previousAllowYield; + return isAsync ? this.finalize(node, new Node.AsyncFunctionExpression(id, params, body)) : + this.finalize(node, new Node.FunctionExpression(id, params, body, isGenerator)); + }; + // https://tc39.github.io/ecma262/#sec-directive-prologues-and-the-use-strict-directive + Parser.prototype.parseDirective = function () { + var token = this.lookahead; + var node = this.createNode(); + var expr = this.parseExpression(); + var directive = (expr.type === syntax_1.Syntax.Literal) ? this.getTokenRaw(token).slice(1, -1) : null; + this.consumeSemicolon(); + return this.finalize(node, directive ? new Node.Directive(expr, directive) : new Node.ExpressionStatement(expr)); + }; + Parser.prototype.parseDirectivePrologues = function () { + var firstRestricted = null; + var body = []; + while (true) { + var token = this.lookahead; + if (token.type !== 8 /* StringLiteral */) { + break; + } + var statement = this.parseDirective(); + body.push(statement); + var directive = statement.directive; + if (typeof directive !== 'string') { + break; + } + if (directive === 'use strict') { + this.context.strict = true; + if (firstRestricted) { + this.tolerateUnexpectedToken(firstRestricted, messages_1.Messages.StrictOctalLiteral); + } + if (!this.context.allowStrictDirective) { + this.tolerateUnexpectedToken(token, messages_1.Messages.IllegalLanguageModeDirective); + } + } + else { + if (!firstRestricted && token.octal) { + firstRestricted = token; + } + } + } + return body; + }; + // https://tc39.github.io/ecma262/#sec-method-definitions + Parser.prototype.qualifiedPropertyName = function (token) { + switch (token.type) { + case 3 /* Identifier */: + case 8 /* StringLiteral */: + case 1 /* BooleanLiteral */: + case 5 /* NullLiteral */: + case 6 /* NumericLiteral */: + case 4 /* Keyword */: + return true; + case 7 /* Punctuator */: + return token.value === '['; + default: + break; + } + return false; + }; + Parser.prototype.parseGetterMethod = function () { + var node = this.createNode(); + var isGenerator = false; + var previousAllowYield = this.context.allowYield; + this.context.allowYield = !isGenerator; + var formalParameters = this.parseFormalParameters(); + if (formalParameters.params.length > 0) { + this.tolerateError(messages_1.Messages.BadGetterArity); + } + var method = this.parsePropertyMethod(formalParameters); + this.context.allowYield = previousAllowYield; + return this.finalize(node, new Node.FunctionExpression(null, formalParameters.params, method, isGenerator)); + }; + Parser.prototype.parseSetterMethod = function () { + var node = this.createNode(); + var isGenerator = false; + var previousAllowYield = this.context.allowYield; + this.context.allowYield = !isGenerator; + var formalParameters = this.parseFormalParameters(); + if (formalParameters.params.length !== 1) { + this.tolerateError(messages_1.Messages.BadSetterArity); + } + else if (formalParameters.params[0] instanceof Node.RestElement) { + this.tolerateError(messages_1.Messages.BadSetterRestParameter); + } + var method = this.parsePropertyMethod(formalParameters); + this.context.allowYield = previousAllowYield; + return this.finalize(node, new Node.FunctionExpression(null, formalParameters.params, method, isGenerator)); + }; + Parser.prototype.parseGeneratorMethod = function () { + var node = this.createNode(); + var isGenerator = true; + var previousAllowYield = this.context.allowYield; + this.context.allowYield = true; + var params = this.parseFormalParameters(); + this.context.allowYield = false; + var method = this.parsePropertyMethod(params); + this.context.allowYield = previousAllowYield; + return this.finalize(node, new Node.FunctionExpression(null, params.params, method, isGenerator)); + }; + // https://tc39.github.io/ecma262/#sec-generator-function-definitions + Parser.prototype.isStartOfExpression = function () { + var start = true; + var value = this.lookahead.value; + switch (this.lookahead.type) { + case 7 /* Punctuator */: + start = (value === '[') || (value === '(') || (value === '{') || + (value === '+') || (value === '-') || + (value === '!') || (value === '~') || + (value === '++') || (value === '--') || + (value === '/') || (value === '/='); // regular expression literal + break; + case 4 /* Keyword */: + start = (value === 'class') || (value === 'delete') || + (value === 'function') || (value === 'let') || (value === 'new') || + (value === 'super') || (value === 'this') || (value === 'typeof') || + (value === 'void') || (value === 'yield'); + break; + default: + break; + } + return start; + }; + Parser.prototype.parseYieldExpression = function () { + var node = this.createNode(); + this.expectKeyword('yield'); + var argument = null; + var delegate = false; + if (!this.hasLineTerminator) { + var previousAllowYield = this.context.allowYield; + this.context.allowYield = false; + delegate = this.match('*'); + if (delegate) { + this.nextToken(); + argument = this.parseAssignmentExpression(); + } + else if (this.isStartOfExpression()) { + argument = this.parseAssignmentExpression(); + } + this.context.allowYield = previousAllowYield; + } + return this.finalize(node, new Node.YieldExpression(argument, delegate)); + }; + // https://tc39.github.io/ecma262/#sec-class-definitions + Parser.prototype.parseClassElement = function (hasConstructor) { + var token = this.lookahead; + var node = this.createNode(); + var kind = ''; + var key = null; + var value = null; + var computed = false; + var method = false; + var isStatic = false; + var isAsync = false; + if (this.match('*')) { + this.nextToken(); + } + else { + computed = this.match('['); + key = this.parseObjectPropertyKey(); + var id = key; + if (id.name === 'static' && (this.qualifiedPropertyName(this.lookahead) || this.match('*'))) { + token = this.lookahead; + isStatic = true; + computed = this.match('['); + if (this.match('*')) { + this.nextToken(); + } + else { + key = this.parseObjectPropertyKey(); + } + } + if ((token.type === 3 /* Identifier */) && !this.hasLineTerminator && (token.value === 'async')) { + var punctuator = this.lookahead.value; + if (punctuator !== ':' && punctuator !== '(' && punctuator !== '*') { + isAsync = true; + token = this.lookahead; + key = this.parseObjectPropertyKey(); + if (token.type === 3 /* Identifier */ && token.value === 'constructor') { + this.tolerateUnexpectedToken(token, messages_1.Messages.ConstructorIsAsync); + } + } + } + } + var lookaheadPropertyKey = this.qualifiedPropertyName(this.lookahead); + if (token.type === 3 /* Identifier */) { + if (token.value === 'get' && lookaheadPropertyKey) { + kind = 'get'; + computed = this.match('['); + key = this.parseObjectPropertyKey(); + this.context.allowYield = false; + value = this.parseGetterMethod(); + } + else if (token.value === 'set' && lookaheadPropertyKey) { + kind = 'set'; + computed = this.match('['); + key = this.parseObjectPropertyKey(); + value = this.parseSetterMethod(); + } + } + else if (token.type === 7 /* Punctuator */ && token.value === '*' && lookaheadPropertyKey) { + kind = 'init'; + computed = this.match('['); + key = this.parseObjectPropertyKey(); + value = this.parseGeneratorMethod(); + method = true; + } + if (!kind && key && this.match('(')) { + kind = 'init'; + value = isAsync ? this.parsePropertyMethodAsyncFunction() : this.parsePropertyMethodFunction(); + method = true; + } + if (!kind) { + this.throwUnexpectedToken(this.lookahead); + } + if (kind === 'init') { + kind = 'method'; + } + if (!computed) { + if (isStatic && this.isPropertyKey(key, 'prototype')) { + this.throwUnexpectedToken(token, messages_1.Messages.StaticPrototype); + } + if (!isStatic && this.isPropertyKey(key, 'constructor')) { + if (kind !== 'method' || !method || (value && value.generator)) { + this.throwUnexpectedToken(token, messages_1.Messages.ConstructorSpecialMethod); + } + if (hasConstructor.value) { + this.throwUnexpectedToken(token, messages_1.Messages.DuplicateConstructor); + } + else { + hasConstructor.value = true; + } + kind = 'constructor'; + } + } + return this.finalize(node, new Node.MethodDefinition(key, computed, value, kind, isStatic)); + }; + Parser.prototype.parseClassElementList = function () { + var body = []; + var hasConstructor = { value: false }; + this.expect('{'); + while (!this.match('}')) { + if (this.match(';')) { + this.nextToken(); + } + else { + body.push(this.parseClassElement(hasConstructor)); + } + } + this.expect('}'); + return body; + }; + Parser.prototype.parseClassBody = function () { + var node = this.createNode(); + var elementList = this.parseClassElementList(); + return this.finalize(node, new Node.ClassBody(elementList)); + }; + Parser.prototype.parseClassDeclaration = function (identifierIsOptional) { + var node = this.createNode(); + var previousStrict = this.context.strict; + this.context.strict = true; + this.expectKeyword('class'); + var id = (identifierIsOptional && (this.lookahead.type !== 3 /* Identifier */)) ? null : this.parseVariableIdentifier(); + var superClass = null; + if (this.matchKeyword('extends')) { + this.nextToken(); + superClass = this.isolateCoverGrammar(this.parseLeftHandSideExpressionAllowCall); + } + var classBody = this.parseClassBody(); + this.context.strict = previousStrict; + return this.finalize(node, new Node.ClassDeclaration(id, superClass, classBody)); + }; + Parser.prototype.parseClassExpression = function () { + var node = this.createNode(); + var previousStrict = this.context.strict; + this.context.strict = true; + this.expectKeyword('class'); + var id = (this.lookahead.type === 3 /* Identifier */) ? this.parseVariableIdentifier() : null; + var superClass = null; + if (this.matchKeyword('extends')) { + this.nextToken(); + superClass = this.isolateCoverGrammar(this.parseLeftHandSideExpressionAllowCall); + } + var classBody = this.parseClassBody(); + this.context.strict = previousStrict; + return this.finalize(node, new Node.ClassExpression(id, superClass, classBody)); + }; + // https://tc39.github.io/ecma262/#sec-scripts + // https://tc39.github.io/ecma262/#sec-modules + Parser.prototype.parseModule = function () { + this.context.strict = true; + this.context.isModule = true; + this.scanner.isModule = true; + var node = this.createNode(); + var body = this.parseDirectivePrologues(); + while (this.lookahead.type !== 2 /* EOF */) { + body.push(this.parseStatementListItem()); + } + return this.finalize(node, new Node.Module(body)); + }; + Parser.prototype.parseScript = function () { + var node = this.createNode(); + var body = this.parseDirectivePrologues(); + while (this.lookahead.type !== 2 /* EOF */) { + body.push(this.parseStatementListItem()); + } + return this.finalize(node, new Node.Script(body)); + }; + // https://tc39.github.io/ecma262/#sec-imports + Parser.prototype.parseModuleSpecifier = function () { + var node = this.createNode(); + if (this.lookahead.type !== 8 /* StringLiteral */) { + this.throwError(messages_1.Messages.InvalidModuleSpecifier); + } + var token = this.nextToken(); + var raw = this.getTokenRaw(token); + return this.finalize(node, new Node.Literal(token.value, raw)); + }; + // import {} ...; + Parser.prototype.parseImportSpecifier = function () { + var node = this.createNode(); + var imported; + var local; + if (this.lookahead.type === 3 /* Identifier */) { + imported = this.parseVariableIdentifier(); + local = imported; + if (this.matchContextualKeyword('as')) { + this.nextToken(); + local = this.parseVariableIdentifier(); + } + } + else { + imported = this.parseIdentifierName(); + local = imported; + if (this.matchContextualKeyword('as')) { + this.nextToken(); + local = this.parseVariableIdentifier(); + } + else { + this.throwUnexpectedToken(this.nextToken()); + } + } + return this.finalize(node, new Node.ImportSpecifier(local, imported)); + }; + // {foo, bar as bas} + Parser.prototype.parseNamedImports = function () { + this.expect('{'); + var specifiers = []; + while (!this.match('}')) { + specifiers.push(this.parseImportSpecifier()); + if (!this.match('}')) { + this.expect(','); + } + } + this.expect('}'); + return specifiers; + }; + // import ...; + Parser.prototype.parseImportDefaultSpecifier = function () { + var node = this.createNode(); + var local = this.parseIdentifierName(); + return this.finalize(node, new Node.ImportDefaultSpecifier(local)); + }; + // import <* as foo> ...; + Parser.prototype.parseImportNamespaceSpecifier = function () { + var node = this.createNode(); + this.expect('*'); + if (!this.matchContextualKeyword('as')) { + this.throwError(messages_1.Messages.NoAsAfterImportNamespace); + } + this.nextToken(); + var local = this.parseIdentifierName(); + return this.finalize(node, new Node.ImportNamespaceSpecifier(local)); + }; + Parser.prototype.parseImportDeclaration = function () { + if (this.context.inFunctionBody) { + this.throwError(messages_1.Messages.IllegalImportDeclaration); + } + var node = this.createNode(); + this.expectKeyword('import'); + var src; + var specifiers = []; + if (this.lookahead.type === 8 /* StringLiteral */) { + // import 'foo'; + src = this.parseModuleSpecifier(); + } + else { + if (this.match('{')) { + // import {bar} + specifiers = specifiers.concat(this.parseNamedImports()); + } + else if (this.match('*')) { + // import * as foo + specifiers.push(this.parseImportNamespaceSpecifier()); + } + else if (this.isIdentifierName(this.lookahead) && !this.matchKeyword('default')) { + // import foo + specifiers.push(this.parseImportDefaultSpecifier()); + if (this.match(',')) { + this.nextToken(); + if (this.match('*')) { + // import foo, * as foo + specifiers.push(this.parseImportNamespaceSpecifier()); + } + else if (this.match('{')) { + // import foo, {bar} + specifiers = specifiers.concat(this.parseNamedImports()); + } + else { + this.throwUnexpectedToken(this.lookahead); + } + } + } + else { + this.throwUnexpectedToken(this.nextToken()); + } + if (!this.matchContextualKeyword('from')) { + var message = this.lookahead.value ? messages_1.Messages.UnexpectedToken : messages_1.Messages.MissingFromClause; + this.throwError(message, this.lookahead.value); + } + this.nextToken(); + src = this.parseModuleSpecifier(); + } + this.consumeSemicolon(); + return this.finalize(node, new Node.ImportDeclaration(specifiers, src)); + }; + // https://tc39.github.io/ecma262/#sec-exports + Parser.prototype.parseExportSpecifier = function () { + var node = this.createNode(); + var local = this.parseIdentifierName(); + var exported = local; + if (this.matchContextualKeyword('as')) { + this.nextToken(); + exported = this.parseIdentifierName(); + } + return this.finalize(node, new Node.ExportSpecifier(local, exported)); + }; + Parser.prototype.parseExportDeclaration = function () { + if (this.context.inFunctionBody) { + this.throwError(messages_1.Messages.IllegalExportDeclaration); + } + var node = this.createNode(); + this.expectKeyword('export'); + var exportDeclaration; + if (this.matchKeyword('default')) { + // export default ... + this.nextToken(); + if (this.matchKeyword('function')) { + // export default function foo () {} + // export default function () {} + var declaration = this.parseFunctionDeclaration(true); + exportDeclaration = this.finalize(node, new Node.ExportDefaultDeclaration(declaration)); + } + else if (this.matchKeyword('class')) { + // export default class foo {} + var declaration = this.parseClassDeclaration(true); + exportDeclaration = this.finalize(node, new Node.ExportDefaultDeclaration(declaration)); + } + else if (this.matchContextualKeyword('async')) { + // export default async function f () {} + // export default async function () {} + // export default async x => x + var declaration = this.matchAsyncFunction() ? this.parseFunctionDeclaration(true) : this.parseAssignmentExpression(); + exportDeclaration = this.finalize(node, new Node.ExportDefaultDeclaration(declaration)); + } + else { + if (this.matchContextualKeyword('from')) { + this.throwError(messages_1.Messages.UnexpectedToken, this.lookahead.value); + } + // export default {}; + // export default []; + // export default (1 + 2); + var declaration = this.match('{') ? this.parseObjectInitializer() : + this.match('[') ? this.parseArrayInitializer() : this.parseAssignmentExpression(); + this.consumeSemicolon(); + exportDeclaration = this.finalize(node, new Node.ExportDefaultDeclaration(declaration)); + } + } + else if (this.match('*')) { + // export * from 'foo'; + this.nextToken(); + if (!this.matchContextualKeyword('from')) { + var message = this.lookahead.value ? messages_1.Messages.UnexpectedToken : messages_1.Messages.MissingFromClause; + this.throwError(message, this.lookahead.value); + } + this.nextToken(); + var src = this.parseModuleSpecifier(); + this.consumeSemicolon(); + exportDeclaration = this.finalize(node, new Node.ExportAllDeclaration(src)); + } + else if (this.lookahead.type === 4 /* Keyword */) { + // export var f = 1; + var declaration = void 0; + switch (this.lookahead.value) { + case 'let': + case 'const': + declaration = this.parseLexicalDeclaration({ inFor: false }); + break; + case 'var': + case 'class': + case 'function': + declaration = this.parseStatementListItem(); + break; + default: + this.throwUnexpectedToken(this.lookahead); + } + exportDeclaration = this.finalize(node, new Node.ExportNamedDeclaration(declaration, [], null)); + } + else if (this.matchAsyncFunction()) { + var declaration = this.parseFunctionDeclaration(); + exportDeclaration = this.finalize(node, new Node.ExportNamedDeclaration(declaration, [], null)); + } + else { + var specifiers = []; + var source = null; + var isExportFromIdentifier = false; + this.expect('{'); + while (!this.match('}')) { + isExportFromIdentifier = isExportFromIdentifier || this.matchKeyword('default'); + specifiers.push(this.parseExportSpecifier()); + if (!this.match('}')) { + this.expect(','); + } + } + this.expect('}'); + if (this.matchContextualKeyword('from')) { + // export {default} from 'foo'; + // export {foo} from 'foo'; + this.nextToken(); + source = this.parseModuleSpecifier(); + this.consumeSemicolon(); + } + else if (isExportFromIdentifier) { + // export {default}; // missing fromClause + var message = this.lookahead.value ? messages_1.Messages.UnexpectedToken : messages_1.Messages.MissingFromClause; + this.throwError(message, this.lookahead.value); + } + else { + // export {foo}; + this.consumeSemicolon(); + } + exportDeclaration = this.finalize(node, new Node.ExportNamedDeclaration(null, specifiers, source)); + } + return exportDeclaration; + }; + return Parser; + }()); + exports.Parser = Parser; + + +/***/ }, +/* 9 */ +/***/ function(module, exports) { + + "use strict"; + // Ensure the condition is true, otherwise throw an error. + // This is only to have a better contract semantic, i.e. another safety net + // to catch a logic error. The condition shall be fulfilled in normal case. + // Do NOT use this to enforce a certain condition on any user input. + Object.defineProperty(exports, "__esModule", { value: true }); + function assert(condition, message) { + /* istanbul ignore if */ + if (!condition) { + throw new Error('ASSERT: ' + message); + } + } + exports.assert = assert; + + +/***/ }, +/* 10 */ +/***/ function(module, exports) { + + "use strict"; + /* tslint:disable:max-classes-per-file */ + Object.defineProperty(exports, "__esModule", { value: true }); + var ErrorHandler = (function () { + function ErrorHandler() { + this.errors = []; + this.tolerant = false; + } + ErrorHandler.prototype.recordError = function (error) { + this.errors.push(error); + }; + ErrorHandler.prototype.tolerate = function (error) { + if (this.tolerant) { + this.recordError(error); + } + else { + throw error; + } + }; + ErrorHandler.prototype.constructError = function (msg, column) { + var error = new Error(msg); + try { + throw error; + } + catch (base) { + /* istanbul ignore else */ + if (Object.create && Object.defineProperty) { + error = Object.create(base); + Object.defineProperty(error, 'column', { value: column }); + } + } + /* istanbul ignore next */ + return error; + }; + ErrorHandler.prototype.createError = function (index, line, col, description) { + var msg = 'Line ' + line + ': ' + description; + var error = this.constructError(msg, col); + error.index = index; + error.lineNumber = line; + error.description = description; + return error; + }; + ErrorHandler.prototype.throwError = function (index, line, col, description) { + throw this.createError(index, line, col, description); + }; + ErrorHandler.prototype.tolerateError = function (index, line, col, description) { + var error = this.createError(index, line, col, description); + if (this.tolerant) { + this.recordError(error); + } + else { + throw error; + } + }; + return ErrorHandler; + }()); + exports.ErrorHandler = ErrorHandler; + + +/***/ }, +/* 11 */ +/***/ function(module, exports) { + + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + // Error messages should be identical to V8. + exports.Messages = { + BadGetterArity: 'Getter must not have any formal parameters', + BadSetterArity: 'Setter must have exactly one formal parameter', + BadSetterRestParameter: 'Setter function argument must not be a rest parameter', + ConstructorIsAsync: 'Class constructor may not be an async method', + ConstructorSpecialMethod: 'Class constructor may not be an accessor', + DeclarationMissingInitializer: 'Missing initializer in %0 declaration', + DefaultRestParameter: 'Unexpected token =', + DuplicateBinding: 'Duplicate binding %0', + DuplicateConstructor: 'A class may only have one constructor', + DuplicateProtoProperty: 'Duplicate __proto__ fields are not allowed in object literals', + ForInOfLoopInitializer: '%0 loop variable declaration may not have an initializer', + GeneratorInLegacyContext: 'Generator declarations are not allowed in legacy contexts', + IllegalBreak: 'Illegal break statement', + IllegalContinue: 'Illegal continue statement', + IllegalExportDeclaration: 'Unexpected token', + IllegalImportDeclaration: 'Unexpected token', + IllegalLanguageModeDirective: 'Illegal \'use strict\' directive in function with non-simple parameter list', + IllegalReturn: 'Illegal return statement', + InvalidEscapedReservedWord: 'Keyword must not contain escaped characters', + InvalidHexEscapeSequence: 'Invalid hexadecimal escape sequence', + InvalidLHSInAssignment: 'Invalid left-hand side in assignment', + InvalidLHSInForIn: 'Invalid left-hand side in for-in', + InvalidLHSInForLoop: 'Invalid left-hand side in for-loop', + InvalidModuleSpecifier: 'Unexpected token', + InvalidRegExp: 'Invalid regular expression', + LetInLexicalBinding: 'let is disallowed as a lexically bound name', + MissingFromClause: 'Unexpected token', + MultipleDefaultsInSwitch: 'More than one default clause in switch statement', + NewlineAfterThrow: 'Illegal newline after throw', + NoAsAfterImportNamespace: 'Unexpected token', + NoCatchOrFinally: 'Missing catch or finally after try', + ParameterAfterRestParameter: 'Rest parameter must be last formal parameter', + Redeclaration: '%0 \'%1\' has already been declared', + StaticPrototype: 'Classes may not have static property named prototype', + StrictCatchVariable: 'Catch variable may not be eval or arguments in strict mode', + StrictDelete: 'Delete of an unqualified identifier in strict mode.', + StrictFunction: 'In strict mode code, functions can only be declared at top level or inside a block', + StrictFunctionName: 'Function name may not be eval or arguments in strict mode', + StrictLHSAssignment: 'Assignment to eval or arguments is not allowed in strict mode', + StrictLHSPostfix: 'Postfix increment/decrement may not have eval or arguments operand in strict mode', + StrictLHSPrefix: 'Prefix increment/decrement may not have eval or arguments operand in strict mode', + StrictModeWith: 'Strict mode code may not include a with statement', + StrictOctalLiteral: 'Octal literals are not allowed in strict mode.', + StrictParamDupe: 'Strict mode function may not have duplicate parameter names', + StrictParamName: 'Parameter name eval or arguments is not allowed in strict mode', + StrictReservedWord: 'Use of future reserved word in strict mode', + StrictVarName: 'Variable name may not be eval or arguments in strict mode', + TemplateOctalLiteral: 'Octal literals are not allowed in template strings.', + UnexpectedEOS: 'Unexpected end of input', + UnexpectedIdentifier: 'Unexpected identifier', + UnexpectedNumber: 'Unexpected number', + UnexpectedReserved: 'Unexpected reserved word', + UnexpectedString: 'Unexpected string', + UnexpectedTemplate: 'Unexpected quasi %0', + UnexpectedToken: 'Unexpected token %0', + UnexpectedTokenIllegal: 'Unexpected token ILLEGAL', + UnknownLabel: 'Undefined label \'%0\'', + UnterminatedRegExp: 'Invalid regular expression: missing /' + }; + + +/***/ }, +/* 12 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + var assert_1 = __webpack_require__(9); + var character_1 = __webpack_require__(4); + var messages_1 = __webpack_require__(11); + function hexValue(ch) { + return '0123456789abcdef'.indexOf(ch.toLowerCase()); + } + function octalValue(ch) { + return '01234567'.indexOf(ch); + } + var Scanner = (function () { + function Scanner(code, handler) { + this.source = code; + this.errorHandler = handler; + this.trackComment = false; + this.isModule = false; + this.length = code.length; + this.index = 0; + this.lineNumber = (code.length > 0) ? 1 : 0; + this.lineStart = 0; + this.curlyStack = []; + } + Scanner.prototype.saveState = function () { + return { + index: this.index, + lineNumber: this.lineNumber, + lineStart: this.lineStart + }; + }; + Scanner.prototype.restoreState = function (state) { + this.index = state.index; + this.lineNumber = state.lineNumber; + this.lineStart = state.lineStart; + }; + Scanner.prototype.eof = function () { + return this.index >= this.length; + }; + Scanner.prototype.throwUnexpectedToken = function (message) { + if (message === void 0) { message = messages_1.Messages.UnexpectedTokenIllegal; } + return this.errorHandler.throwError(this.index, this.lineNumber, this.index - this.lineStart + 1, message); + }; + Scanner.prototype.tolerateUnexpectedToken = function (message) { + if (message === void 0) { message = messages_1.Messages.UnexpectedTokenIllegal; } + this.errorHandler.tolerateError(this.index, this.lineNumber, this.index - this.lineStart + 1, message); + }; + // https://tc39.github.io/ecma262/#sec-comments + Scanner.prototype.skipSingleLineComment = function (offset) { + var comments = []; + var start, loc; + if (this.trackComment) { + comments = []; + start = this.index - offset; + loc = { + start: { + line: this.lineNumber, + column: this.index - this.lineStart - offset + }, + end: {} + }; + } + while (!this.eof()) { + var ch = this.source.charCodeAt(this.index); + ++this.index; + if (character_1.Character.isLineTerminator(ch)) { + if (this.trackComment) { + loc.end = { + line: this.lineNumber, + column: this.index - this.lineStart - 1 + }; + var entry = { + multiLine: false, + slice: [start + offset, this.index - 1], + range: [start, this.index - 1], + loc: loc + }; + comments.push(entry); + } + if (ch === 13 && this.source.charCodeAt(this.index) === 10) { + ++this.index; + } + ++this.lineNumber; + this.lineStart = this.index; + return comments; + } + } + if (this.trackComment) { + loc.end = { + line: this.lineNumber, + column: this.index - this.lineStart + }; + var entry = { + multiLine: false, + slice: [start + offset, this.index], + range: [start, this.index], + loc: loc + }; + comments.push(entry); + } + return comments; + }; + Scanner.prototype.skipMultiLineComment = function () { + var comments = []; + var start, loc; + if (this.trackComment) { + comments = []; + start = this.index - 2; + loc = { + start: { + line: this.lineNumber, + column: this.index - this.lineStart - 2 + }, + end: {} + }; + } + while (!this.eof()) { + var ch = this.source.charCodeAt(this.index); + if (character_1.Character.isLineTerminator(ch)) { + if (ch === 0x0D && this.source.charCodeAt(this.index + 1) === 0x0A) { + ++this.index; + } + ++this.lineNumber; + ++this.index; + this.lineStart = this.index; + } + else if (ch === 0x2A) { + // Block comment ends with '*/'. + if (this.source.charCodeAt(this.index + 1) === 0x2F) { + this.index += 2; + if (this.trackComment) { + loc.end = { + line: this.lineNumber, + column: this.index - this.lineStart + }; + var entry = { + multiLine: true, + slice: [start + 2, this.index - 2], + range: [start, this.index], + loc: loc + }; + comments.push(entry); + } + return comments; + } + ++this.index; + } + else { + ++this.index; + } + } + // Ran off the end of the file - the whole thing is a comment + if (this.trackComment) { + loc.end = { + line: this.lineNumber, + column: this.index - this.lineStart + }; + var entry = { + multiLine: true, + slice: [start + 2, this.index], + range: [start, this.index], + loc: loc + }; + comments.push(entry); + } + this.tolerateUnexpectedToken(); + return comments; + }; + Scanner.prototype.scanComments = function () { + var comments; + if (this.trackComment) { + comments = []; + } + var start = (this.index === 0); + while (!this.eof()) { + var ch = this.source.charCodeAt(this.index); + if (character_1.Character.isWhiteSpace(ch)) { + ++this.index; + } + else if (character_1.Character.isLineTerminator(ch)) { + ++this.index; + if (ch === 0x0D && this.source.charCodeAt(this.index) === 0x0A) { + ++this.index; + } + ++this.lineNumber; + this.lineStart = this.index; + start = true; + } + else if (ch === 0x2F) { + ch = this.source.charCodeAt(this.index + 1); + if (ch === 0x2F) { + this.index += 2; + var comment = this.skipSingleLineComment(2); + if (this.trackComment) { + comments = comments.concat(comment); + } + start = true; + } + else if (ch === 0x2A) { + this.index += 2; + var comment = this.skipMultiLineComment(); + if (this.trackComment) { + comments = comments.concat(comment); + } + } + else { + break; + } + } + else if (start && ch === 0x2D) { + // U+003E is '>' + if ((this.source.charCodeAt(this.index + 1) === 0x2D) && (this.source.charCodeAt(this.index + 2) === 0x3E)) { + // '-->' is a single-line comment + this.index += 3; + var comment = this.skipSingleLineComment(3); + if (this.trackComment) { + comments = comments.concat(comment); + } + } + else { + break; + } + } + else if (ch === 0x3C && !this.isModule) { + if (this.source.slice(this.index + 1, this.index + 4) === '!--') { + this.index += 4; // `' is a single-line comment - this.index += 3; - var comment = this.skipSingleLineComment(3); - if (this.trackComment) { - comments = comments.concat(comment); - } - } - else { - break; - } - } - else if (ch === 0x3C && !this.isModule) { - if (this.source.slice(this.index + 1, this.index + 4) === '!--') { - this.index += 4; // `