diff --git a/.eslintignore b/.eslintignore
index fcbea07f9..4af79d129 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -3,4 +3,5 @@ apps/banglerun/rollup.config.js
apps/schoolCalendar/fullcalendar/main.js
apps/authentiwatch/qr_packed.js
apps/qrcode/qr-scanner.umd.min.js
+apps/gipy/pkg/gpconv.js
*.test.js
diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml
index a3469e7bb..7c0cfca3a 100644
--- a/.github/workflows/nodejs.yml
+++ b/.github/workflows/nodejs.yml
@@ -22,9 +22,6 @@ jobs:
- name: Install typescript dependencies
working-directory: ./typescript
run: npm ci
- - name: Build types
- working-directory: ./typescript
- run: npm run build:types
- name: Build all TS apps and widgets
working-directory: ./typescript
run: npm run build
diff --git a/.gitignore b/.gitignore
index a9398e871..7687a770a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,3 +11,7 @@ tests/Layout/testresult.bmp
apps.local.json
_site
.jekyll-cache
+.owncloudsync.log
+Desktop.ini
+.sync_*.db*
+*.swp
diff --git a/.gitmodules b/.gitmodules
index fd6663d2a..c2c1104c2 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,6 @@
[submodule "EspruinoAppLoaderCore"]
path = core
url = https://github.com/espruino/EspruinoAppLoaderCore.git
+[submodule "webtools"]
+ path = webtools
+ url = https://github.com/espruino/EspruinoWebTools.git
diff --git a/README.md b/README.md
index ea485da86..fed13a358 100644
--- a/README.md
+++ b/README.md
@@ -255,8 +255,11 @@ and which gives information about the app for the Launcher.
// 'app' - an application
// 'clock' - a clock - required for clocks to automatically start
// 'widget' - a widget
+ // 'module' - this provides a module that can be used with 'require'.
+ // 'provides_modules' should be used if type:module is specified
// 'bootloader' - an app that at startup (app.boot.js) but doesn't have a launcher entry for 'app.js'
// 'settings' - apps that appear in Settings->Apps (with appname.settings.js) but that have no 'app.js'
+ // 'clkinfo' - Provides a 'myapp.clkinfo.js' file that can be used to display info in clocks - see modules/clock_info.js
// 'RAM' - code that runs and doesn't upload anything to storage
// 'launch' - replacement 'Launcher'
// 'textinput' - provides a 'textinput' library that allows text to be input on the Bangle
@@ -266,10 +269,24 @@ and which gives information about the app for the Launcher.
// 'locale' - provides 'locale' library for language-specific date/distance/etc
// (a version of 'locale' is included in the firmware)
"tags": "", // comma separated tag list for searching
+ // common types are:
+ // 'clock' - it's a clock
+ // 'widget' - it is (or provides) a widget
+ // 'outdoors' - useful for outdoor activities
+ // 'tool' - a useful utility (timer, calculator, etc)
+ // 'game' - a game
+ // 'bluetooth' - uses Bluetooth LE
+ // 'system' - used by the system
+ // 'clkinfo' - provides or uses clock_info module for data on your clock face (see modules/clock_info.js)
"supports": ["BANGLEJS2"], // List of device IDs supported, either BANGLEJS or BANGLEJS2
"dependencies" : { "notify":"type" } // optional, app 'types' we depend on (see "type" above)
"dependencies" : { "messages":"app" } // optional, depend on a specific app ID
// for instance this will use notify/notifyfs is they exist, or will pull in 'notify'
+ "dependencies" : { "messageicons":"module" } // optional, depend on a specific library to be used with 'require' - see provides_modules
+ "dependencies" : { "message":"widget" } // optional, depend on a specific type of widget - see provides_widgets
+ "provides_modules" : ["messageicons"] // optional, this app provides a module that can be used with 'require'
+ "provides_widgets" : ["battery"] // optional, this app provides a type of widget - 'alarm/battery/bluetooth/pedometer/message'
+ "default" : true, // set if an app is the default implementer of something (a widget/module/etc)
"readme": "README.md", // if supplied, a link to a markdown-style text file
// that contains more information about this app (usage, etc)
// A 'Read more...' link will be added under the app
@@ -324,7 +341,7 @@ and which gives information about the app for the Launcher.
```
* 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.
+* 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`, `bluetooth` 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
@@ -454,7 +471,10 @@ It should also add `myappid.json` to `data`, to make sure it is cleaned up when
## Modules
You can include any of [Espruino's modules](https://www.espruino.com/Modules) as
-normal with `require("modulename")`. If you want to develop your own module for your
+normal with `require("modulename")`. To include [Bangle's modules](modules) for use in the Web
+IDE, [upload the modules to internal storage](modules#upload-the-module-to-the-bangles-internal-storage)
+or [change the IDE's search path](modules#change-the-web-ide-search-path-to-include-banglejs-modules).
+If you want to develop your own module for your
app(s) then you can do that too. Just add the module into the `modules` folder
then you can use it from your app as normal.
diff --git a/android.html b/android.html
index 93999008f..8a70a46e9 100644
--- a/android.html
+++ b/android.html
@@ -170,10 +170,10 @@
-
+
+
-
diff --git a/apps/7x7dotsclock/7x7dotsclock.app.js b/apps/7x7dotsclock/7x7dotsclock.app.js
index aa174b2d2..aa6672a4f 100644
--- a/apps/7x7dotsclock/7x7dotsclock.app.js
+++ b/apps/7x7dotsclock/7x7dotsclock.app.js
@@ -149,11 +149,11 @@ function drawHSeg(x1,y1,x2,y2,Num,Color,Size) {
if (Color == "fg") {
g.setColor(g.theme.fg);
} else {
- g.setColor(mColor[0],mColor[1],mColor[2]);
+ g.setColor(mColor[0],mColor[1],mColor[2]);
}
g.fillCircle(x1+Dx+(i-1)*(x2-x1)/7,y1+Dy+(j-1)*(y2-y1)/7,Size);
} else {
- g.setColor(bColor[0],bColor[1],bColor[2]);
+ g.setColor(bColor[0],bColor[1],bColor[2]);
g.fillCircle(x1+Dx+(i-1)*(x2-x1)/7,y1+Dy+(j-1)*(y2-y1)/7,1);
}
}
@@ -166,7 +166,7 @@ function drawSSeg(x1,y1,x2,y2,Num,Color,Size) {
for (let j = 1; j < 8; j++) {
if (Font[Num][j-1][i-1] == 1) {
if (Color == "fg") {
- g.setColor(sColor[0],sColor[1],sColor[2]);
+ g.setColor(sColor[0],sColor[1],sColor[2]);
} else {
g.setColor(g.theme.fg);
//g.setColor(0.7,0.7,0.7);
@@ -253,8 +253,8 @@ function actions(v){
if(BTN1.read() === true) {
print("BTN pressed");
Bangle.showLauncher();
- }
-
+ }
+
if(v==-1){
print("up swipe event");
if(settings.swupApp != "") load(settings.swupApp);
@@ -269,7 +269,7 @@ function actions(v){
}
// Get Messages status
-var messages = require("Storage").readJSON("messages.json",1)||[];
+var messages_installed = require("Storage").read("messages") !== undefined;
//var BTconnected = NRF.getSecurityStatus().connected;
//NRF.on('connect',BTconnected = NRF.getSecurityStatus().connected);
@@ -289,27 +289,27 @@ function drawWidgeds() {
g.setColor((g.getBPP()>8) ? "#07f" : (g.theme.dark ? "#0ff" : "#00f"));
else
g.setColor(g.theme.dark ? "#666" : "#999");
- g.drawImage(atob("CxQBBgDgFgJgR4jZMawfAcA4D4NYybEYIwTAsBwDAA=="),x1Bt,y1Bt);
+ g.drawImage(atob("CxQBBgDgFgJgR4jZMawfAcA4D4NYybEYIwTAsBwDAA=="),x1Bt,y1Bt);
+
-
//Battery
//print(E.getBattery());
//print(Bangle.isCharging());
-
+
var x1B = 130;
var y1B = 2;
var x2B = x1B + 20;
var y2B = y1B + 15;
-
+
g.setColor(g.theme.bg);
g.clearRect(x1B,y1B,x2B,y2B);
-
+
g.setColor(g.theme.fg);
g.drawRect(x1B,y1B,x2B,y2B);
g.fillRect(x1B,y1B,x1B+(E.getBattery()*(x2B-x1B)/100),y2B);
g.fillRect(x2B,y1B+(y2B-y1B)/2-3,x2B+4,y1B+(y2B-y1B)/2+3);
-
+
//Messages
@@ -318,25 +318,25 @@ function drawWidgeds() {
var x2M = x1M + 25;
var y2M = y2B;
- if (messages.some(m=>m.new)) {
+ if (messages_installed && require("messages").status() == "new") {
g.setColor(g.theme.fg);
g.fillRect(x1M,y1M,x2M,y2M);
g.setColor(g.theme.bg);
g.drawLine(x1M,y1M,x1M+(x2M-x1M)/2,y1M+(y2M-y1M)/2);
g.drawLine(x1M+(x2M-x1M)/2,y1M+(y2M-y1M)/2,x2M,y1M);
}
-
+
var strDow = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
var d = new Date();
var dow = d.getDay(),day = d.getDate(), month = d.getMonth() + 1, year = d.getFullYear();
print(strDow[dow] + ' ' + day + '.' + month + ' ' + year);
-
+
g.setColor(g.theme.fg);
g.setFontAlign(-1, -1,0);
g.setFont("Vector", 20);
g.drawString(strDow[dow] + ' ' + day, 0, 0, true);
-
+
}
@@ -354,7 +354,7 @@ function SetFull(on) {
} else {
Ys = 30;
Bangle.setUI("updown",actions);
- Bangle.on('swipe', function(direction) {
+ Bangle.on('swipe', function(direction) {
switch (direction) {
case 1:
print("swipe left event");
@@ -362,7 +362,7 @@ function SetFull(on) {
print(settings.swleftApp);
break;
case -1:
- print("swipe right event");
+ print("swipe right event");
if(settings.swrightApp != "") load(settings.swrightApp);
print(settings.swrightApp);
break;
@@ -374,7 +374,7 @@ function SetFull(on) {
SegH = (Ye-Ys)/2;
Dy = SegH/16;
-
+
draw();
if (on != true) {
diff --git a/apps/7x7dotsclock/ChangeLog b/apps/7x7dotsclock/ChangeLog
index d2c98a472..5e8e48b0b 100644
--- a/apps/7x7dotsclock/ChangeLog
+++ b/apps/7x7dotsclock/ChangeLog
@@ -1,2 +1,3 @@
0.01: Initial version for upload
-0.02: better theme support, configurable colors, small improvements
+0.02: Better theme support, configurable colors, small improvements
+0.03: Use `messages` library to check for new messages
\ No newline at end of file
diff --git a/apps/7x7dotsclock/metadata.json b/apps/7x7dotsclock/metadata.json
index 41f0836d3..ba1996544 100644
--- a/apps/7x7dotsclock/metadata.json
+++ b/apps/7x7dotsclock/metadata.json
@@ -1,7 +1,7 @@
{ "id": "7x7dotsclock",
"name": "7x7 Dots Clock",
"shortName":"7x7 Dots Clock",
- "version":"0.02",
+ "version":"0.03",
"description": "A clock with a big 7x7 dots Font",
"icon": "dotsfontclock.png",
"tags": "clock",
diff --git a/apps/90sclk/ChangeLog b/apps/90sclk/ChangeLog
index feb008f5f..057d6ff73 100644
--- a/apps/90sclk/ChangeLog
+++ b/apps/90sclk/ChangeLog
@@ -1,2 +1,3 @@
0.01: New App!
-0.02: Fullscreen settings.
\ No newline at end of file
+0.02: Fullscreen settings.
+0.03: Tell clock widgets to hide.
diff --git a/apps/90sclk/app.js b/apps/90sclk/app.js
index 6babbfec2..351c235e0 100644
--- a/apps/90sclk/app.js
+++ b/apps/90sclk/app.js
@@ -115,6 +115,9 @@ function draw() {
}
}
+// Show launcher when middle button pressed
+Bangle.setUI("clock");
+
Bangle.loadWidgets();
// Clear the screen once, at startup
@@ -140,5 +143,3 @@ Bangle.on('lock', function(isLocked) {
});
-// Show launcher when middle button pressed
-Bangle.setUI("clock");
diff --git a/apps/90sclk/metadata.json b/apps/90sclk/metadata.json
index fb2824a6f..59b627427 100644
--- a/apps/90sclk/metadata.json
+++ b/apps/90sclk/metadata.json
@@ -1,7 +1,7 @@
{
"id": "90sclk",
"name": "90s Clock",
- "version": "0.02",
+ "version": "0.03",
"description": "A 90s style watch-face",
"readme": "README.md",
"icon": "app.png",
diff --git a/apps/UI4swatch/Changelog b/apps/UI4swatch/ChangeLog
similarity index 100%
rename from apps/UI4swatch/Changelog
rename to apps/UI4swatch/ChangeLog
diff --git a/apps/a_dndtoggle/ChangeLog b/apps/a_dndtoggle/ChangeLog
new file mode 100644
index 000000000..ec66c5568
--- /dev/null
+++ b/apps/a_dndtoggle/ChangeLog
@@ -0,0 +1 @@
+0.01: Initial version
diff --git a/apps/a_dndtoggle/README.md b/apps/a_dndtoggle/README.md
new file mode 100644
index 000000000..bd0981c5b
--- /dev/null
+++ b/apps/a_dndtoggle/README.md
@@ -0,0 +1,13 @@
+# a_dndtoggle - Toggle Quiet Mode of the watch
+
+When Quiet mode is off, just start this app to set quiet mode. Start it again to turn off quiet mode.
+Work in progress.
+
+#ToDo
+Settings page, current status indicator.
+
+## Creator
+
+Hank - contact at http://forum.espruino.com
+
+
diff --git a/apps/a_dndtoggle/a_dndtoggle.app.js b/apps/a_dndtoggle/a_dndtoggle.app.js
new file mode 100644
index 000000000..c0b968f2c
--- /dev/null
+++ b/apps/a_dndtoggle/a_dndtoggle.app.js
@@ -0,0 +1,43 @@
+
+const modeNames = [/*LANG*/"Noisy", /*LANG*/"Alarms", /*LANG*/"Silent"];
+let bSettings = require('Storage').readJSON('setting.json',true)||{};
+let current = 0|bSettings.quiet;
+//0 off
+//1 alarms
+//2 silent
+
+console.log("old: " + current);
+
+switch (current) {
+ case 0:
+ bSettings.quiet = 2;
+ Bangle.buzz();
+ setTimeout('Bangle.buzz();',500);
+ break;
+ case 1:
+ bSettings.quiet = 0;
+ Bangle.buzz();
+ break;
+ case 2:
+ bSettings.quiet = 0;
+ Bangle.buzz();
+ break;
+ default:
+ bSettings.quiet = 0;
+ Bangle.buzz();
+}
+
+console.log("new: " + bSettings.quiet);
+
+E.showMessage(modeNames[current] + " -> " + modeNames[bSettings.quiet]);
+setTimeout('exitApp();', 2000);
+
+
+function exitApp(){
+
+require("Storage").writeJSON("setting.json", bSettings);
+// reload clocks with new theme, otherwise just wait for user to switch apps
+
+load()
+
+}
\ No newline at end of file
diff --git a/apps/a_dndtoggle/a_dndtoggle.png b/apps/a_dndtoggle/a_dndtoggle.png
new file mode 100644
index 000000000..4c8b74c0c
Binary files /dev/null and b/apps/a_dndtoggle/a_dndtoggle.png differ
diff --git a/apps/a_dndtoggle/app-icon.js b/apps/a_dndtoggle/app-icon.js
new file mode 100644
index 000000000..0b08cc65b
--- /dev/null
+++ b/apps/a_dndtoggle/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwJC/AAl/Agf/AAUAgIFDwEHAofgh/g/0Ag/wj+AnwVB/EegEfEIN4nkAh+AgE8vgVBAoV4Aoce/EAgfADQIFcjwpFHYIFCnxBFJopZBn5ZCMopxFPoqJFSowA/gA="))
\ No newline at end of file
diff --git a/apps/a_dndtoggle/metadata.json b/apps/a_dndtoggle/metadata.json
new file mode 100644
index 000000000..f5ae9cc31
--- /dev/null
+++ b/apps/a_dndtoggle/metadata.json
@@ -0,0 +1,16 @@
+{
+ "id": "a_dndtoggle",
+ "name": "a_dndtoggle - Toggle Quiet Mode of the watch",
+ "shortName": "A_DND Toggle",
+ "version": "0.01",
+ "description": "Toggle Quiet Mode of the watch just by starting this app.",
+ "icon": "a_dndtoggle.png",
+ "type": "app",
+ "tags": "tool",
+ "supports": ["BANGLEJS","BANGLEJS2"],
+ "storage": [
+ {"name":"a_dndtoggle.app.js","url":"a_dndtoggle.app.js"},
+ {"name":"a_dndtoggle.img","url":"app-icon.js","evaluate":true}
+ ],
+ "readme": "README.md"
+}
diff --git a/apps/about/ChangeLog b/apps/about/ChangeLog
index f5638fdd2..e236e4b34 100644
--- a/apps/about/ChangeLog
+++ b/apps/about/ChangeLog
@@ -10,3 +10,5 @@
0.10: Added separate Bangle.js 2 file with Bangle.js 2 kickstarter pixels (as of 28 Oct 2021)
0.11: Bangle.js2: New pixels, btn1 to exit
0.12: Actual pixels as of 29th Nov 2021
+0.13: Bangle.js 2: Use setUI to add software back button
+0.14: Add automatic translation of more strings
diff --git a/apps/about/app-bangle1.js b/apps/about/app-bangle1.js
index 28a292376..dd94c1e84 100644
--- a/apps/about/app-bangle1.js
+++ b/apps/about/app-bangle1.js
@@ -11,8 +11,8 @@ g.drawString("BANGLEJS.COM",120,y-4);
} else {
y=-(4+h); // small screen, start right at top
}
-g.drawString("Powered by Espruino",0,y+=4+h);
-g.drawString("Version "+ENV.VERSION,0,y+=h);
+g.drawString(/*LANG*/"Powered by Espruino",0,y+=4+h);
+g.drawString(/*LANG*/"Version "+ENV.VERSION,0,y+=h);
g.drawString("Commit "+ENV.GIT_COMMIT,0,y+=h);
function getVersion(name,file) {
var j = s.readJSON(file,1);
@@ -24,9 +24,9 @@ getVersion("Launcher","launch.info");
getVersion("Settings","setting.info");
y+=h;
-g.drawString(MEM.total+" JS Variables available",0,y+=h);
-g.drawString("Storage: "+(require("Storage").getFree()>>10)+"k free",0,y+=h);
-if (ENV.STORAGE) g.drawString(" "+(ENV.STORAGE>>10)+"k total",0,y+=h);
+g.drawString(MEM.total+/*LANG*/" JS Variables available",0,y+=h);
+g.drawString("Storage: "+(require("Storage").getFree()>>10)+/*LANG*/"k free",0,y+=h);
+if (ENV.STORAGE) g.drawString(" "+(ENV.STORAGE>>10)+/*LANG*/"k total",0,y+=h);
if (ENV.SPIFLASH) g.drawString("SPI Flash: "+(ENV.SPIFLASH>>10)+"k",0,y+=h);
g.setFontAlign(0,-1);
g.flip();
diff --git a/apps/about/app-bangle2.js b/apps/about/app-bangle2.js
index 978d36193..ccffd183f 100644
--- a/apps/about/app-bangle2.js
+++ b/apps/about/app-bangle2.js
@@ -10,7 +10,7 @@ var img = atob("sIwDkm2S66DYwA2AAAAAHAHGSRxJEkAAgmGGBxDIADIdAFJIbAHF9HP00kBUC6Dt
var imgHeight = g.imageMetrics(img).height;
var imgScroll = Math.floor(Math.random()*imgHeight);
-g.reset().setFont("6x15").setFontAlign(0,0);
+g.clear(1).setFont("6x15").setFontAlign(0,0);
g.drawString(ENV.VERSION + " " + NRF.getAddress(), g.getWidth()/2, 171);
g.drawImage(img,0,24);
@@ -35,17 +35,17 @@ function drawInfo() {
g.setFont("4x6").setFontAlign(0,0).drawString("BANGLEJS.COM",W-30,56);
var h=8, y = 24-h;
g.setFont("6x8").setFontAlign(-1,-1);
- g.drawString("Powered by Espruino",0,y+=4+h);
- g.drawString("Version "+ENV.VERSION,0,y+=h);
+ g.drawString(/*LANG*/"Powered by Espruino",0,y+=4+h);
+ g.drawString(/*LANG*/"Version "+ENV.VERSION,0,y+=h);
g.drawString("Commit "+ENV.GIT_COMMIT,0,y+=h);
getVersion("Bootloader","boot.info");
getVersion("Launcher","launch.info");
getVersion("Settings","setting.info");
- g.drawString(MEM.total+" JS Vars",0,y+=h);
- g.drawString("Storage: "+(require("Storage").getFree()>>10)+"k free",0,y+=h);
- if (ENV.STORAGE) g.drawString(" "+(ENV.STORAGE>>10)+"k total",0,y+=h);
+ g.drawString(MEM.total+/*LANG*/" JS Vars",0,y+=h);
+ g.drawString("Storage: "+(require("Storage").getFree()>>10)+/*LANG*/"k free",0,y+=h);
+ if (ENV.STORAGE) g.drawString(" "+(ENV.STORAGE>>10)+/*LANG*/"k total",0,y+=h);
if (ENV.SPIFLASH) g.drawString("SPI Flash: "+(ENV.SPIFLASH>>10)+"k",0,y+=h);
imageTop = y+h;
imgScroll = imgHeight-imageTop;
@@ -69,4 +69,7 @@ function drawImage() {
// TODO: a nice little animation before
setTimeout(drawInfo, 1000);
-setWatch(_=>load(), BTN1);
+Bangle.setUI({
+ mode : "custom",
+ back : load
+});
diff --git a/apps/about/metadata.json b/apps/about/metadata.json
index 6c22bdc56..52cd37b7d 100644
--- a/apps/about/metadata.json
+++ b/apps/about/metadata.json
@@ -1,7 +1,7 @@
{
"id": "about",
"name": "About",
- "version": "0.12",
+ "version": "0.14",
"description": "Bangle.js About page - showing software version, stats, and a collaborative mural from the Bangle.js KickStarter backers",
"icon": "app.png",
"tags": "tool,system",
diff --git a/apps/accellog/ChangeLog b/apps/accellog/ChangeLog
index 80981fe27..94241c7a7 100644
--- a/apps/accellog/ChangeLog
+++ b/apps/accellog/ChangeLog
@@ -2,3 +2,4 @@
0.02: Use the new multiplatform 'Layout' library
0.03: Exit as first menu option, dont show decimal places for seconds
0.04: Localisation, change Exit->Back to allow back-arrow to appear on 2v13 firmware
+0.05: Add max G values during recording, record actual G values and magnitude to CSV
diff --git a/apps/accellog/app.js b/apps/accellog/app.js
index f4c1b3c5a..147f7503f 100644
--- a/apps/accellog/app.js
+++ b/apps/accellog/app.js
@@ -1,5 +1,6 @@
var fileNumber = 0;
var MAXLOGS = 9;
+var logRawData = false;
function getFileName(n) {
return "accellog."+n+".csv";
@@ -24,6 +25,11 @@ function showMenu() {
/*LANG*/"View Logs" : function() {
viewLogs();
},
+ /*LANG*/"Log raw data" : {
+ value : logRawData,
+ format : v => v?/*LANG*/"Yes":/*LANG*/"No",
+ onchange : v => { logRawData=v; }
+ },
};
E.showMenu(menu);
}
@@ -78,6 +84,7 @@ function viewLogs() {
}
function startRecord(force) {
+ var stopped = false;
if (!force) {
// check for existing file
var f = require("Storage").open(getFileName(fileNumber), "r");
@@ -92,39 +99,101 @@ function startRecord(force) {
var Layout = require("Layout");
var layout = new Layout({ type: "v", c: [
- {type:"txt", font:"6x8", label:/*LANG*/"Samples", pad:2},
- {type:"txt", id:"samples", font:"6x8:2", label:" - ", pad:5, bgCol:g.theme.bg},
- {type:"txt", font:"6x8", label:/*LANG*/"Time", pad:2},
- {type:"txt", id:"time", font:"6x8:2", label:" - ", pad:5, bgCol:g.theme.bg},
- {type:"txt", font:"6x8:2", label:/*LANG*/"RECORDING", bgCol:"#f00", pad:5, fillx:1},
- ]
- },{btns:[ // Buttons...
- {label:/*LANG*/"STOP", cb:()=>{
- Bangle.removeListener('accel', accelHandler);
- showMenu();
+ { type: "h", c: [
+ { type: "v", c: [
+ {type:"txt", font:"6x8", label:/*LANG*/"Samples", pad:2},
+ {type:"txt", id:"samples", font:"6x8:2", label:" - ", pad:5, bgCol:g.theme.bg},
+ ]},
+ { type: "v", c: [
+ {type:"txt", font:"6x8", label:/*LANG*/"Time", pad:2},
+ {type:"txt", id:"time", font:"6x8:2", label:" - ", pad:5, bgCol:g.theme.bg},
+ ]},
+ ]},
+ { type: "h", c: [
+ { type: "v", c: [
+ {type:"txt", font:"6x8", label:/*LANG*/"Max X", pad:2},
+ {type:"txt", id:"maxX", font:"6x8", label:" - ", pad:5, bgCol:g.theme.bg},
+ ]},
+ { type: "v", c: [
+ {type:"txt", font:"6x8", label:/*LANG*/"Max Y", pad:2},
+ {type:"txt", id:"maxY", font:"6x8", label:" - ", pad:5, bgCol:g.theme.bg},
+ ]},
+ { type: "v", c: [
+ {type:"txt", font:"6x8", label:/*LANG*/"Max Z", pad:2},
+ {type:"txt", id:"maxZ", font:"6x8", label:" - ", pad:5, bgCol:g.theme.bg},
+ ]},
+ ]},
+ {type:"txt", font:"6x8", label:/*LANG*/"Max G", pad:2},
+ {type:"txt", id:"maxMag", font:"6x8:4", label:" - ", pad:5, bgCol:g.theme.bg},
+ {type:"txt", id:"state", font:"6x8:2", label:/*LANG*/"RECORDING", bgCol:"#f00", pad:5, fillx:1},
+ ]},
+ {
+ btns:[ // Buttons...
+ {id: "btnStop", label:/*LANG*/"STOP", cb:()=>{
+ if (stopped) {
+ showMenu();
+ }
+ else {
+ Bangle.removeListener('accel', accelHandler);
+ layout.state.label = /*LANG*/"STOPPED";
+ layout.state.bgCol = /*LANG*/"#0f0";
+ stopped = true;
+ layout.render();
+ }
}}
]});
layout.render();
// now start writing
var f = require("Storage").open(getFileName(fileNumber), "w");
- f.write("Time (ms),X,Y,Z\n");
+ f.write("Time (ms),X,Y,Z,Total\n");
var start = getTime();
var sampleCount = 0;
+ var maxMag = 0;
+ var maxX = 0;
+ var maxY = 0;
+ var maxZ = 0;
function accelHandler(accel) {
var t = getTime()-start;
- f.write([
- t*1000,
- accel.x*8192,
- accel.y*8192,
- accel.z*8192].map(n=>Math.round(n)).join(",")+"\n");
+ if (logRawData) {
+ f.write([
+ t*1000,
+ accel.x*8192,
+ accel.y*8192,
+ accel.z*8192,
+ accel.mag*8192,
+ ].map(n=>Math.round(n)).join(",")+"\n");
+ } else {
+ f.write([
+ Math.round(t*1000),
+ accel.x,
+ accel.y,
+ accel.z,
+ accel.mag,
+ ].join(",")+"\n");
+ }
+ if (accel.mag > maxMag) {
+ maxMag = accel.mag.toFixed(2);
+ }
+ if (accel.x > maxX) {
+ maxX = accel.x.toFixed(2);
+ }
+ if (accel.y > maxY) {
+ maxY = accel.y.toFixed(2);
+ }
+ if (accel.z > maxZ) {
+ maxZ = accel.z.toFixed(2);
+ }
sampleCount++;
layout.samples.label = sampleCount;
layout.time.label = Math.round(t)+"s";
- layout.render(layout.samples);
- layout.render(layout.time);
+ layout.maxX.label = maxX;
+ layout.maxY.label = maxY;
+ layout.maxZ.label = maxZ;
+ layout.maxMag.label = maxMag;
+ layout.render();
}
Bangle.setPollInterval(80); // 12.5 Hz - the default
diff --git a/apps/accellog/metadata.json b/apps/accellog/metadata.json
index fdf6cf320..903c57903 100644
--- a/apps/accellog/metadata.json
+++ b/apps/accellog/metadata.json
@@ -2,7 +2,7 @@
"id": "accellog",
"name": "Acceleration Logger",
"shortName": "Accel Log",
- "version": "0.04",
+ "version": "0.05",
"description": "Logs XYZ acceleration data to a CSV file that can be downloaded to your PC",
"icon": "app.png",
"tags": "outdoor",
diff --git a/apps/activepedom/README.md b/apps/activepedom/README.md
index ac32a1dd6..06ad280ee 100644
--- a/apps/activepedom/README.md
+++ b/apps/activepedom/README.md
@@ -1,6 +1,11 @@
# Active Pedometer
+
Pedometer that filters out arm movement and displays a step goal progress.
+**Note:** Since creation of this app, Bangle.js's step counting algorithm has
+improved significantly - and as a result the algorithm in this app (which
+ runs *on top* of Bangle.js's algorithm) may no longer be accurate.
+
I changed the step counting algorithm completely.
Now every step is counted when in status 'active', if the time difference between two steps is not too short or too long.
To get in 'active' mode, you have to reach the step threshold before the active timer runs out.
@@ -9,6 +14,7 @@ When you reach the step threshold, the steps needed to reach the threshold are c
Steps are saved to a datafile every 5 minutes. You can watch a graph using the app.
## Screenshots
+
* 600 steps

@@ -70,4 +76,4 @@ Steps are saved to a datafile every 5 minutes. You can watch a graph using the a
## Requests
-If you have any feature requests, please post in this forum thread: http://forum.espruino.com/conversations/345754/
\ No newline at end of file
+If you have any feature requests, please post in this forum thread: http://forum.espruino.com/conversations/345754/
diff --git a/apps/activepedom/metadata.json b/apps/activepedom/metadata.json
index 4deb7006d..81bafb573 100644
--- a/apps/activepedom/metadata.json
+++ b/apps/activepedom/metadata.json
@@ -3,7 +3,7 @@
"name": "Active Pedometer",
"shortName": "Active Pedometer",
"version": "0.09",
- "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.",
+ "description": "(NOT RECOMMENDED) 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. The `Health` app now provides step logging and graphs.",
"icon": "app.png",
"tags": "outdoors,widget",
"supports": ["BANGLEJS"],
diff --git a/apps/activityreminder/ChangeLog b/apps/activityreminder/ChangeLog
index 37820dce6..3811425ac 100644
--- a/apps/activityreminder/ChangeLog
+++ b/apps/activityreminder/ChangeLog
@@ -6,3 +6,5 @@
0.06: Add a temperature threshold to detect (and not alert) if the BJS isn't worn. Better support for the peoples using the app at night
0.07: Fix bug on the cutting edge firmware
0.08: Use default Bangle formatter for booleans
+0.09: New app screen (instead of showing settings or the alert) and some optimisations
+0.10: Add software back button via setUI
diff --git a/apps/activityreminder/alert.js b/apps/activityreminder/alert.js
new file mode 100644
index 000000000..96a9b76c4
--- /dev/null
+++ b/apps/activityreminder/alert.js
@@ -0,0 +1,37 @@
+(function () {
+ // load variable before defining functions cause it can trigger a ReferenceError
+ const activityreminder = require("activityreminder");
+ const storage = require("Storage");
+ let activityreminder_data = activityreminder.loadData();
+
+ function run() {
+ E.showPrompt("Inactivity detected", {
+ title: "Activity reminder",
+ buttons: { "Ok": 1, "Dismiss": 2, "Pause": 3 }
+ }).then(function (v) {
+ if (v == 1) {
+ activityreminder_data.okDate = new Date();
+ }
+ if (v == 2) {
+ activityreminder_data.dismissDate = new Date();
+ }
+ if (v == 3) {
+ activityreminder_data.pauseDate = new Date();
+ }
+ activityreminder.saveData(activityreminder_data);
+ load();
+ });
+
+ // Obey system quiet mode:
+ if (!(storage.readJSON('setting.json', 1) || {}).quiet) {
+ Bangle.buzz(400);
+ }
+ setTimeout(load, 20000);
+ }
+
+ g.clear();
+ Bangle.loadWidgets();
+ Bangle.drawWidgets();
+ run();
+
+})();
\ No newline at end of file
diff --git a/apps/activityreminder/app.js b/apps/activityreminder/app.js
index c2b626fb3..81e10d8dd 100644
--- a/apps/activityreminder/app.js
+++ b/apps/activityreminder/app.js
@@ -1,46 +1,58 @@
(function () {
- // load variable before defining functions cause it can trigger a ReferenceError
- const activityreminder = require("activityreminder");
- const storage = require("Storage");
- const activityreminder_settings = activityreminder.loadSettings();
- let activityreminder_data = activityreminder.loadData();
+ // load variable before defining functions cause it can trigger a ReferenceError
+ const activityreminder = require("activityreminder");
+ let activityreminder_data = activityreminder.loadData();
+ let W = g.getWidth();
+ // let H = g.getHeight();
- function drawAlert() {
- E.showPrompt("Inactivity detected", {
- title: "Activity reminder",
- buttons: { "Ok": 1, "Dismiss": 2, "Pause": 3 }
- }).then(function (v) {
- if (v == 1) {
- activityreminder_data.okDate = new Date();
- }
- if (v == 2) {
- activityreminder_data.dismissDate = new Date();
- }
- if (v == 3) {
- activityreminder_data.pauseDate = new Date();
- }
- activityreminder.saveData(activityreminder_data);
- load();
- });
-
- // Obey system quiet mode:
- if (!(storage.readJSON('setting.json', 1) || {}).quiet) {
- Bangle.buzz(400);
- }
- setTimeout(load, 20000);
- }
-
- function run() {
- if (activityreminder.mustAlert(activityreminder_data, activityreminder_settings)) {
- drawAlert();
- } else {
- eval(storage.read("activityreminder.settings.js"))(() => load());
- }
- }
+ function getHoursMins(date){
+ var h = date.getHours();
+ var m = date.getMinutes();
+ return (""+h).substr(-2) + ":" + ("0"+m).substr(-2);
+ }
+ function drawData(name, value, y){
+ g.drawString(name, 10, y);
+ g.drawString(value, 100, y);
+ }
+
+ function drawInfo() {
+ var h=18, y = h;
+ g.setColor(g.theme.fg);
+ g.setFont("Vector",h).setFontAlign(-1,-1);
+
+ // Header
+ g.drawLine(0,25,W,25);
+ g.drawLine(0,26,W,26);
+
+ g.drawString("Current Cycle", 10, y+=h);
+ drawData("Start", getHoursMins(activityreminder_data.stepsDate), y+=h);
+ drawData("Steps", getCurrentSteps(), y+=h);
+
+ /*
+ g.drawString("Button Press", 10, y+=h*2);
+ drawData("Ok", getHoursMins(activityreminder_data.okDate), y+=h);
+ drawData("Dismiss", getHoursMins(activityreminder_data.dismissDate), y+=h);
+ drawData("Pause", getHoursMins(activityreminder_data.pauseDate), y+=h);
+ */
+ }
+
+ function getCurrentSteps(){
+ let health = Bangle.getHealthStatus("day");
+ return health.steps - activityreminder_data.stepsOnDate;
+ }
+
+ function run() {
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
- run();
-
-})();
\ No newline at end of file
+ drawInfo();
+ Bangle.setUI({
+ mode : "custom",
+ back : load
+ })
+ }
+
+ run();
+
+})();
diff --git a/apps/activityreminder/boot.js b/apps/activityreminder/boot.js
index f97cf274d..5a11d73b8 100644
--- a/apps/activityreminder/boot.js
+++ b/apps/activityreminder/boot.js
@@ -1,70 +1,81 @@
(function () {
- // load variable before defining functions cause it can trigger a ReferenceError
- const activityreminder = require("activityreminder");
- const activityreminder_settings = activityreminder.loadSettings();
- let activityreminder_data = activityreminder.loadData();
-
- if (activityreminder_data.firstLoad) {
- activityreminder_data.firstLoad = false;
+ // load variable before defining functions cause it can trigger a ReferenceError
+ const activityreminder = require("activityreminder");
+ const activityreminder_settings = activityreminder.loadSettings();
+ let activityreminder_data = activityreminder.loadData();
+
+ if (activityreminder_data.firstLoad) {
+ activityreminder_data.firstLoad = false;
+ activityreminder.saveData(activityreminder_data);
+ }
+
+ function run() {
+ if (isNotWorn()) return;
+ let now = new Date();
+ let h = now.getHours();
+
+ if (isDuringAlertHours(h)) {
+ let health = Bangle.getHealthStatus("day");
+ if (health.steps - activityreminder_data.stepsOnDate >= activityreminder_settings.minSteps // more steps made than needed
+ || health.steps < activityreminder_data.stepsOnDate) { // new day or reboot of the watch
+ activityreminder_data.stepsOnDate = health.steps;
+ activityreminder_data.stepsDate = now;
activityreminder.saveData(activityreminder_data);
- }
-
- function run() {
- if (isNotWorn()) return;
- let now = new Date();
- let h = now.getHours();
-
- if (isDuringAlertHours(h)) {
- let health = Bangle.getHealthStatus("day");
- if (health.steps - activityreminder_data.stepsOnDate >= activityreminder_settings.minSteps // more steps made than needed
- || health.steps < activityreminder_data.stepsOnDate) { // new day or reboot of the watch
- activityreminder_data.stepsOnDate = health.steps;
- activityreminder_data.stepsDate = now;
- activityreminder.saveData(activityreminder_data);
- /* todo in a futur release
- Add settimer to trigger like 30 secs after going in this part cause the person have been walking
- (pass some argument to run() to handle long walks and not triggering so often)
- */
- }
-
- if (activityreminder.mustAlert(activityreminder_data, activityreminder_settings)) {
- load('activityreminder.app.js');
- }
- }
-
- }
-
- function isNotWorn() {
- return (Bangle.isCharging() || activityreminder_settings.tempThreshold >= E.getTemperature());
- }
-
- function isDuringAlertHours(h) {
- if (activityreminder_settings.startHour < activityreminder_settings.endHour) { // not passing through midnight
- return (h >= activityreminder_settings.startHour && h < activityreminder_settings.endHour);
- } else { // passing through midnight
- return (h >= activityreminder_settings.startHour || h < activityreminder_settings.endHour);
- }
- }
-
- Bangle.on('midnight', function () {
- /*
- Usefull trick to have the app working smothly for people using it at night
- */
- let now = new Date();
- let h = now.getHours();
- if (activityreminder_settings.enabled && isDuringAlertHours(h)) {
- // updating only the steps and keeping the original stepsDate on purpose
- activityreminder_data.stepsOnDate = 0;
- activityreminder.saveData(activityreminder_data);
- }
- });
-
-
- if (activityreminder_settings.enabled) {
- setInterval(run, 60000);
/* todo in a futur release
- increase setInterval time to something that is still sensible (5 mins ?)
- when we added a settimer
+ Add settimer to trigger like 30 secs after going in this part cause the person have been walking
+ (pass some argument to run() to handle long walks and not triggering so often)
*/
+ }
+
+ if (mustAlert(now)) {
+ load('activityreminder.alert.js');
+ }
}
+
+ }
+
+ function isNotWorn() {
+ return (Bangle.isCharging() || activityreminder_settings.tempThreshold >= E.getTemperature());
+ }
+
+ function isDuringAlertHours(h) {
+ if (activityreminder_settings.startHour < activityreminder_settings.endHour) { // not passing through midnight
+ return (h >= activityreminder_settings.startHour && h < activityreminder_settings.endHour);
+ } else { // passing through midnight
+ return (h >= activityreminder_settings.startHour || h < activityreminder_settings.endHour);
+ }
+ }
+
+ function mustAlert(now) {
+ if ((now - activityreminder_data.stepsDate) / 60000 > activityreminder_settings.maxInnactivityMin) { // inactivity detected
+ if ((now - activityreminder_data.okDate) / 60000 > 3 && // last alert anwsered with ok was more than 3 min ago
+ (now - activityreminder_data.dismissDate) / 60000 > activityreminder_settings.dismissDelayMin && // last alert was more than dismissDelayMin ago
+ (now - activityreminder_data.pauseDate) / 60000 > activityreminder_settings.pauseDelayMin) { // last alert was more than pauseDelayMin ago
+ return true;
+ }
+ }
+ return false;
+ }
+
+ Bangle.on('midnight', function () {
+ /*
+ Usefull trick to have the app working smothly for people using it at night
+ */
+ let now = new Date();
+ let h = now.getHours();
+ if (activityreminder_settings.enabled && isDuringAlertHours(h)) {
+ // updating only the steps and keeping the original stepsDate on purpose
+ activityreminder_data.stepsOnDate = 0;
+ activityreminder.saveData(activityreminder_data);
+ }
+ });
+
+
+ if (activityreminder_settings.enabled) {
+ setInterval(run, 60000);
+ /* todo in a futur release
+ increase setInterval time to something that is still sensible (5 mins ?)
+ when we added a settimer
+ */
+ }
})();
diff --git a/apps/activityreminder/lib.js b/apps/activityreminder/lib.js
index 704d35641..a5c35190c 100644
--- a/apps/activityreminder/lib.js
+++ b/apps/activityreminder/lib.js
@@ -1,56 +1,44 @@
exports.loadSettings = function () {
- return Object.assign({
- enabled: true,
- startHour: 9,
- endHour: 20,
- maxInnactivityMin: 30,
- dismissDelayMin: 15,
- pauseDelayMin: 120,
- minSteps: 50,
- tempThreshold: 27
- }, require("Storage").readJSON("activityreminder.s.json", true) || {});
+ return Object.assign({
+ enabled: true,
+ startHour: 9,
+ endHour: 20,
+ maxInnactivityMin: 30,
+ dismissDelayMin: 15,
+ pauseDelayMin: 120,
+ minSteps: 50,
+ tempThreshold: 27
+ }, require("Storage").readJSON("activityreminder.s.json", true) || {});
};
exports.writeSettings = function (settings) {
- require("Storage").writeJSON("activityreminder.s.json", settings);
+ require("Storage").writeJSON("activityreminder.s.json", settings);
};
exports.saveData = function (data) {
- require("Storage").writeJSON("activityreminder.data.json", data);
+ require("Storage").writeJSON("activityreminder.data.json", data);
};
exports.loadData = function () {
- let health = Bangle.getHealthStatus("day");
- let data = Object.assign({
- firstLoad: true,
- stepsDate: new Date(),
- stepsOnDate: health.steps,
- okDate: new Date(1970),
- dismissDate: new Date(1970),
- pauseDate: new Date(1970),
- },
+ let health = Bangle.getHealthStatus("day");
+ let data = Object.assign({
+ firstLoad: true,
+ stepsDate: new Date(),
+ stepsOnDate: health.steps,
+ okDate: new Date(1970),
+ dismissDate: new Date(1970),
+ pauseDate: new Date(1970),
+ },
require("Storage").readJSON("activityreminder.data.json") || {});
- if(typeof(data.stepsDate) == "string")
- data.stepsDate = new Date(data.stepsDate);
- if(typeof(data.okDate) == "string")
- data.okDate = new Date(data.okDate);
- if(typeof(data.dismissDate) == "string")
- data.dismissDate = new Date(data.dismissDate);
- if(typeof(data.pauseDate) == "string")
- data.pauseDate = new Date(data.pauseDate);
+ if (typeof (data.stepsDate) == "string")
+ data.stepsDate = new Date(data.stepsDate);
+ if (typeof (data.okDate) == "string")
+ data.okDate = new Date(data.okDate);
+ if (typeof (data.dismissDate) == "string")
+ data.dismissDate = new Date(data.dismissDate);
+ if (typeof (data.pauseDate) == "string")
+ data.pauseDate = new Date(data.pauseDate);
- return data;
+ return data;
};
-
-exports.mustAlert = function(activityreminder_data, activityreminder_settings) {
- let now = new Date();
- if ((now - activityreminder_data.stepsDate) / 60000 > activityreminder_settings.maxInnactivityMin) { // inactivity detected
- if ((now - activityreminder_data.okDate) / 60000 > 3 && // last alert anwsered with ok was more than 3 min ago
- (now - activityreminder_data.dismissDate) / 60000 > activityreminder_settings.dismissDelayMin && // last alert was more than dismissDelayMin ago
- (now - activityreminder_data.pauseDate) / 60000 > activityreminder_settings.pauseDelayMin) { // last alert was more than pauseDelayMin ago
- return true;
- }
- }
- return false;
-}
\ No newline at end of file
diff --git a/apps/activityreminder/metadata.json b/apps/activityreminder/metadata.json
index 75ebf80b2..a7fb0c487 100644
--- a/apps/activityreminder/metadata.json
+++ b/apps/activityreminder/metadata.json
@@ -3,7 +3,7 @@
"name": "Activity Reminder",
"shortName":"Activity Reminder",
"description": "A reminder to take short walks for the ones with a sedentary lifestyle",
- "version":"0.08",
+ "version":"0.10",
"icon": "app.png",
"type": "app",
"tags": "tool,activity",
@@ -13,11 +13,12 @@
{"name": "activityreminder.app.js", "url":"app.js"},
{"name": "activityreminder.boot.js", "url": "boot.js"},
{"name": "activityreminder.settings.js", "url": "settings.js"},
+ {"name": "activityreminder.alert.js", "url": "alert.js"},
{"name": "activityreminder", "url": "lib.js"},
{"name": "activityreminder.img", "url": "app-icon.js", "evaluate": true}
],
"data": [
{"name": "activityreminder.s.json"},
- {"name": "activityreminder.data.json"}
+ {"name": "activityreminder.data.json", "storageFile": true}
]
}
diff --git a/apps/activityreminder/settings.js b/apps/activityreminder/settings.js
index de490b796..051c0dcd8 100644
--- a/apps/activityreminder/settings.js
+++ b/apps/activityreminder/settings.js
@@ -1,80 +1,86 @@
(function (back) {
- // Load settings
- const activityreminder = require("activityreminder");
- let settings = activityreminder.loadSettings();
+ // Load settings
+ const activityreminder = require("activityreminder");
+ let settings = activityreminder.loadSettings();
- // Show the menu
- E.showMenu({
- "": { "title": "Activity Reminder" },
- "< Back": () => back(),
- 'Enable': {
- value: settings.enabled,
- onchange: v => {
- settings.enabled = v;
- activityreminder.writeSettings(settings);
- }
- },
- 'Start hour': {
- value: settings.startHour,
- min: 0, max: 24,
- onchange: v => {
- settings.startHour = v;
- activityreminder.writeSettings(settings);
- }
- },
- 'End hour': {
- value: settings.endHour,
- min: 0, max: 24,
- onchange: v => {
- settings.endHour = v;
- activityreminder.writeSettings(settings);
- }
- },
- 'Max inactivity': {
- value: settings.maxInnactivityMin,
- min: 15, max: 120,
- onchange: v => {
- settings.maxInnactivityMin = v;
- activityreminder.writeSettings(settings);
- },
- format: x => x + "m"
- },
- 'Dismiss delay': {
- value: settings.dismissDelayMin,
- min: 5, max: 60,
- onchange: v => {
- settings.dismissDelayMin = v;
- activityreminder.writeSettings(settings);
- },
- format: x => x + "m"
- },
- 'Pause delay': {
- value: settings.pauseDelayMin,
- min: 30, max: 240, step: 5,
- onchange: v => {
- settings.pauseDelayMin = v;
- activityreminder.writeSettings(settings);
- },
- format: x => {
- return x + "m";
- }
- },
- 'Min steps': {
- value: settings.minSteps,
- min: 10, max: 500, step: 10,
- onchange: v => {
- settings.minSteps = v;
- activityreminder.writeSettings(settings);
- }
- },
- 'Temp Threshold': {
- value: settings.tempThreshold,
- min: 20, max: 40, step: 0.5,
- format: v => v + "°C",
- onchange: v => {
- settings.tempThreshold = v;
- activityreminder.writeSettings(settings);
- }
+ function getMainMenu(){
+ var mainMenu = {
+ "": { "title": "Activity Reminder" },
+ "< Back": () => back(),
+ 'Enable': {
+ value: settings.enabled,
+ onchange: v => {
+ settings.enabled = v;
+ activityreminder.writeSettings(settings);
}
- });
+ },
+ 'Start hour': {
+ value: settings.startHour,
+ min: 0, max: 24,
+ onchange: v => {
+ settings.startHour = v;
+ activityreminder.writeSettings(settings);
+ }
+ },
+ 'End hour': {
+ value: settings.endHour,
+ min: 0, max: 24,
+ onchange: v => {
+ settings.endHour = v;
+ activityreminder.writeSettings(settings);
+ }
+ },
+ 'Max inactivity': {
+ value: settings.maxInnactivityMin,
+ min: 15, max: 120,
+ onchange: v => {
+ settings.maxInnactivityMin = v;
+ activityreminder.writeSettings(settings);
+ },
+ format: x => x + "m"
+ },
+ 'Dismiss delay': {
+ value: settings.dismissDelayMin,
+ min: 5, max: 60,
+ onchange: v => {
+ settings.dismissDelayMin = v;
+ activityreminder.writeSettings(settings);
+ },
+ format: x => x + "m"
+ },
+ 'Pause delay': {
+ value: settings.pauseDelayMin,
+ min: 30, max: 240, step: 5,
+ onchange: v => {
+ settings.pauseDelayMin = v;
+ activityreminder.writeSettings(settings);
+ },
+ format: x => {
+ return x + "m";
+ }
+ },
+ 'Min steps': {
+ value: settings.minSteps,
+ min: 10, max: 500, step: 10,
+ onchange: v => {
+ settings.minSteps = v;
+ activityreminder.writeSettings(settings);
+ }
+ },
+ 'Temp Threshold': {
+ value: settings.tempThreshold,
+ min: 20, max: 40, step: 0.5,
+ format: v => v + "°C",
+ onchange: v => {
+ settings.tempThreshold = v;
+ activityreminder.writeSettings(settings);
+ }
+ }
+ };
+
+ return mainMenu;
+ }
+
+ // Show the menu
+ E.showMenu(getMainMenu());
})
diff --git a/apps/advcasio/ChangeLog b/apps/advcasio/ChangeLog
index 7de176672..fd37c324e 100644
--- a/apps/advcasio/ChangeLog
+++ b/apps/advcasio/ChangeLog
@@ -1 +1,4 @@
0.01: AdvCasio first version
+0.02: Remove un-needed fonts to improve memory usage
+0.03: Tell clock widgets to hide.
+0.04: Swipe down to see widgets, step counter now just uses getHealthStatus
diff --git a/apps/advcasio/app.js b/apps/advcasio/app.js
index 8c27b7823..9d246b7ef 100644
--- a/apps/advcasio/app.js
+++ b/apps/advcasio/app.js
@@ -1,306 +1,160 @@
const storage = require('Storage');
require("Font6x12").add(Graphics);
-require("Font6x8").add(Graphics);
require("Font8x12").add(Graphics);
require("Font7x11Numeric7Seg").add(Graphics);
function bigThenSmall(big, small, x, y) {
- g.setFont("7x11Numeric7Seg", 2);
- g.drawString(big, x, y);
- x += g.stringWidth(big);
- g.setFont("8x12");
- g.drawString(small, x, y);
+ g.setFont("7x11Numeric7Seg", 2);
+ g.drawString(big, x, y);
+ x += g.stringWidth(big);
+ g.setFont("8x12");
+ g.drawString(small, x, y);
}
-function getClockBg() {
- return require("heatshrink").decompress(atob("icVgf/ABv8v4DBx4CB+PH8F+nAGB48fwEHBwXjxwqBuPH//+nAGBBwIjCAwI2D/wGBgIyDI4QGDwAGBHYX/4AGBn4UFEYQpCEYYpCAAMfMhP4FIgABwJ8OEBIA=="));
-}
-
-
-// sun, cloud, rain, thunder
-var iconsWeather = [
- require("heatshrink").decompress(atob("i8Ugf/ACcfA434BA/AAwsAv0/8F/BAcDwEHHIpECFI3wn4GC/gOC+PAGoXggEH/+ODQgXBGQv/wAbBBAnguEACIn4gfxI4JXFwJmG/kPBA3jSynw")), require("heatshrink").decompress(atob("i0Ugf/AEXggIGE/0A/kPBAmBCIN/A4Y8CgAICwEHBYoUE/ACCj4sDn4CBC4YyDwBrDCgYA3A")), require("heatshrink").decompress(atob("h8Rgf/AAuBAgf8h4FDCwM/AgPA/gFC/0HgEBBQPwnEfDoWAg4jC/gOCAoQmBAQXjFIV//8f//4IQP4j/+gAIB4EcHII4CAoI+DLQJXF/AA==")), require("heatshrink").decompress(atob("h0Pgf/AA8fAYX+g4EC8EBAgXADAeAgAECgAOC/wrCDQIOBBYfwgAaC/kAn4EB/EAv4aDHAeBIg38"))
-];
-
-
function getBackgroundImage() {
- return require("heatshrink").decompress(atob("2GwghC/AH4A/AH4AMl////wAwURiQECgUzmcxBQQCBiYUBBARW+LAcCAgcPBYgFBkAIFG7kQiAKIiIKBgISOAAJBD//zKQfxK4vyAoMQCgn/ERBhBBYR5BAwR1DB4Y2DgYPCGIQRCCQcP+EfGJI0FEgRSCGAQCCX4JXCkAhDn4lI+HyK4YWBFIPzJYJXHAIMSK4cwJ4I3CAYMzA4cfcRMBdwytBK4i6FK4IUCMgYAEGIITBK4cCaAPwgJXB+fzK4sAgYtCK5EfA4pXR+AmBaIZYCK6KcCAwSjDEYXx/8vK5QRCK4kPK6cDkJREBIMBfgIrDK5svUAIQBAwIaCK4w+DK4YGBK7IaBboIuCK4gFCJwYBBiBCCCgQhHHYgGDgArBK5IGDAYMgJ4Xwn53BGgLVDmBXKAAinDLpJXCAAYhHR4YODn/wJIPyTYZXDE4RXD+ECNILIDAIPwj4xIAAYNCR4fyVIYLFA4KEBBAglKAGUCmcykEAiMQBIURBYM/BgIUEgcz+bTKAH4A/AH4A/AHP/AGY1d+BWCh5X/LCpW1K74fgG/5X/AH5X/K9Bg/K63wK/5XWgBX/K6pWBK/5XU+BWBh5J/K6auCK/5XTVwRfFAH5XOKwRX/K6auDh5I/K6SuDWP5XSVwYADWX6vXK/5XQWQpW/K6auDJP5XWV35XT+Cu/K7Ku/K65H/K6hW/K7EPI35XWIv5XWAH5X/K/4A/K/5X/K/4A/K9cAAH4A/AFzz/AHRX/K/5X/AH5X/K/5X/AH5X/K/4A/K/5X/K/4A/K/5X/K/4A/K/5X/AH5X/K/5X/AH5X/K/5X/AH5X/K/4A/K/5X/K/4A/K/5X/K/4A/K/5X/AH5X/K/5X/AH5X/K/5X/AH5X/K/4A/K/5X/K/4A/K/5X/K/4A/K/5X/AH5X/K/5X/AH5X/K/5X/AH5X/K/4A/K/5X/K/4A/K/5X/K/4A/K/5X/AH5X/K/5X/AH5X/K/40VAH4A/AFzLb+EPDm4AdK/5X/K+PwgEAHy5X9HgMAK/5XXH6xX/H65X/K/5X/K98AK7sAgBX3DjBWFO644DSTHwGzJXED4RXaDoLqcK7weWDIQcXK8I6YK77KXK4o8DPbY6ZK7qvDDy6vdR7JXDh60EDyw5BAIRXYSwjMbAgIhUDwJZCHwJX0GwjRWNwIAEHSwBCDSpXFH4pXzDS5XIEARXVSYbQEDaYzCK+6vcKaxXNDypX9HwQkbHS40COSpXKK2A6CHgRXcPIhX0SwpXYVuQ6EgBX/K644YODBXkSDJX/K/5X/DtRX6gA3YOkRWbLDZX4KwYA/AG8F5vdABncKH4AGhpRJAYXNAgPAKP4AF5vMJwoDBAQIKE6BR/AAvc5vO9wAB7oCB9veAoPcAoPcK+kwh8AgcA98An//gH/+sD//wCISgBJ4IABAYpaC9vdK4UP/9AAQNQr/zgHwEYNQFYQAh+EP+FegH+A4QBCMQIKBAAPNK4yxBA4RXCV4YZBE4IjChwCDmApCK8VdmHggHgFYf0SQJXE5nMK4anCAoYHC5pXCaQJXBop+BqAGEK7f/AAQeEKwQrBqCtDAILjBCQfNK4JTCAYZXF7qvD//gV4S2DgEFFIYAECgIACMC8PKoIBB8n1K4ivF5vc5xOCWYZbBAYavHU4RXCr4pEAEMDfoNQGoMEgEwYQPwAoIBBAAPM5ipC7oDCVIIAE7hXCD4SdBiEP+gGBgihCFYIAz5pXBAAnN7oIB7nc5gOBK4QA/K4pNCWgSpCBInNK/4AGhncKIStC7gCBA4QAC4BR/AAysCABZW/AHwA="));
+ return require("heatshrink").decompress(atob("2GwwkGIf4AfgMRkUiiIHCiMRiAMDAwYCCBAYVDAHMv/4ACkBIBAgPxBgM/BYXyAoICBCowA5gRADKQUDKAYMCmYCBiBXBCo4A5J4MxiMSKQUf+YBBBgSiBgc/kBXBBAMyCoK2CK/btCiUhfAJLCkBkDiMQgBXDCoUvNAJX+AAU/+MB/8wAQIAC+cQK5hoDgIEBBIQFEAYIPHBIgBBAQQIDBwZXSKIMxgJaBgEjmZYCmBXLgLBBkkAgUhiMxBIM0iMSCoMRkZECkQJEichBINDiETAgISBiQTDK6MvJAXzVIQrBBYMCK5E/K4kwGIJXFgdAMgQQBiYiCDgU0HQSlCgMikIEBEAMTDYJXQ+UikYDBj6nCAAMTWoJ6BK4oVEK4c0oQ+BK4MjAgMDJoJXHNYJXHBwa0BohcDY4QAKgJQE+LzBNwJVBkQMEkBXBCoyvFJAVAKISaBiMiHQRIDkVBoSyCK5CvBAgavNDAJAC+cQn5DCgSpBl4MDgBXBgCsBCoYoMLAKREgIKDBJIdKK5oA/AH4A/AH4A/ADUBIH4APiAFEi1mAGUADrkRKwUGK2ZXes1gK2xXfD8A3/K/4AWgxX/ACtga2AwIHLkAgCwvJw6RcDgIABK+w4cK/I4dsEGP5BXtSAQ6BV/5XSG4RX/K6Y3fK+42CK/5XTGwcGK/5XSVwY5cK+o1DAAayYsAhDsCv4K7BTBK4YeYK7CyFVzJXFFIpXtVwYiYK/rmZKYYDDELJXXG4YiaK/Y0aKgQAEK+gkdKt5XGKzqv5GTpX6ETlgK4xWrKTyxKVthXmAGRX/K/5X/AH5X/K/4gBAH4A/AFz/uAH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AHNggEGHfEAgAEHKyQXVK0qTCAggbUK+6SDAApzXK/5BRDYZX3KxBBSYqxXngyvaV25XEd4ZCSsAcBAoRZ2dQZXBLwgaQCIYeCAGirCS4YGCDSJXCC6ZaodYICBZzSw4S4I+XDgSv4K4rzCK/47RAQTMaWHI9YV3TscV3aVagByBK3SwCSqyt8AAQ+XK/4A/AH4A/AH4A3gAA/AH4AuZbdggwc3ADpX/K/5XxsEAgA+XK/o8BgBX/K64/WK/4/XK/5X/K/5XvgBX/K64cYHrw4CSTFggCuXK4oDCEQJXYDS6ScDgg4CPKyRCAAZX0HAgBDK+LlYK4oeBAwZ9aK+lgAoQGBgyvzDIIDBK66sCG4JXYCwIBDK7ADCK+xZCHwJXzGoQ8BK7DpBAAaSXSgRXZO4okCK+IaXV4oABEILSWSYjRCHSo3BDSxXEAAIcBAISvyKawcIAYIGCK/4cUH4YlaHS0AHgI1XOg5YBPrY6WHgRXfAGRXDHzBX8VoJX/K68ADjRX6sBX/K/5X/K8wdcK/UAG7B0iKzZYbK/BWDAH4A/hWpzWhIf4ASgOpzIAB0EAhhH/AB8ZzGJ1WazMA4pH/AB+pxOZxOpzVMqA2ugUzmcgD7cKVYOqzGqpnRFw8ykchK8kviEBmQFBgMiFocSCAcSkUQAgMikRsHhWqxOq0Ut4mqBw0DC4IxBD4wpBHAQMCA4cCGJIAFj8hDIQuBkMTCwU/AYQJBiUxFoPxiIVDK4kyxUz4cxl+KK5MfDQXyD4UCmMSmAEBAQQHDgMTmIxHAAqpBmaqCFwMDEYZRBgEjCQQBB+USK5E/ns/0Uzwc6K48ykYkCK4IfCc4I4CK4QHEBAYAMiICBmYuDmQEBh8iAgRXCLISvJO4MqwcklEiK5CADV4oaBV4oHEK6Eve4JNCbwRfCiMTFoMDkMRSAJXCD49azWp0UqzWayJXIQwcAO4cCkMCFIJOCA4XxK6KPBkR6DTwYyBAwYPEAggfFzORpWK1OZyAOHJ4QfERAUSEgQxIIIgAr1URWIOZzOgGtwAhgMZzWq1OaIv4ASKgOqzTkvAEmq1WgFtQA=="));
}
+function getRocketSequences() {
+ return {
+ 1: require("heatshrink").decompress(atob("qFGwkCkQAiiEBEkUgKQhPhE8ogCE8YhCiQoEE7pKEPIgncTQ4neEwpQCPoh1eJYYwCJ7QmHKAh1hZIpOjPAUBJ0ZQCTzEhExZ1lPAZ1kKDQmOJ65O2E65OPOy5O2E64mPOyxO/J2wnPJyx2QJ35O/J2khE0p2POq52PEy4nOiQnlOrEhiSfMJrEggQnLJzB1CPBQmZkInMEzBQDPBImbPBR1ZEoRMCZYImhgQgEE0BzFKAgmaDwLDFKAbqdYQwHBOrcgDgLBFJrsiiRNGYbpLBY4Ymhd4omkkUhE0pQEEwUBJjrHBd4QmCdzoiBDwYrCPLyZHF4QnagQeCE8UgJwYniJwgnIOzwfFO0wJCJzMQE4gyFEzR2FBQombkInDQI4AakAnBTYS+ZE5BMDE0LEES7YnLE0R3FAEQA=")),
+ 2: require("heatshrink").decompress(atob("qFGwkCkQAikMAgIliKYon/AA0gEAQniEwIhCAgYndEIjqBE8CaGKogmgKAp1fKAgncExBQBBQR1gKAp7BJ0IndExR4CE0idaOpYnbExqeYJxxPYEx0BJ0x2XExx2XJ20QE6xONJi5OPGwJOlBwLFkLoLFlBwJOkOwJOlE4JOkTjBOOE/52Pdi5OPEy7FnE5wmXE5xOZT5gmYEoMiiB1lgR4KTLAkDPBJ1WIAYDDKA4mWJwchDwYEDTjQiDJQh4GYLAhHFosSJy6OCTIxaEEywbBKYwjEEzMgUQxQFBogAURwZOGOjTKJdTYnOEryfHE0JQEfIpQgYQMAgJLeAgrtfTI4ndgSaFE4h0bdQkSZQpOfEAgIBO0AnEdrh2FJAb1EdbInEBIpObOwhOEEzYnFXzZ2HE4QlhE4QlDFMKcDYooniO0QnDT0YnCE0ciA")),
+ 3: require("heatshrink").decompress(atob("qFGwkEogAjiMUEkVAKYgnhPYolgOQIniOYZ4FOcLqBE8CaGKojpgKAomhEYUQE7gmHKAIxCE0QkCPYR1gZIgnZExR4CJ0idmE7ZONYzImNgEUJ0p3YJRh2ZJJwnXOpQhBdkpaETsMEGQhOhE7jFLUYpOfTzgmKE4hOiE4hOigEUJ0rvCEywnPEqx2OTjBOOE7ImOTsqeZE5zFYoJOmT5kBJzEAih4LdK5mBAQInKOqoYDEgR4JEypHDEYbxJOq5ABdgZ7CEzZOEJQgnGihOYEIzJFTionCKYxWGEy9ADAYnGUIYmWog/EdBFAEy7KIKAwnjKwLqWE5pMeT48CVQpQfgMjKEtEiAnfEQJQCgJSCTcB6FJzkEdYcUE8FAdQghDOzonKTjh2EZAidcDoInHJzodBOwx/BE8JxcOwsAOwQmhJgSXDObwnFEwUUO0LFGE8aeiE4YmiokQE0tE")),
+ 4: require("heatshrink").decompress(atob("qFGwkCkQAjiMSEkRTFE/4AGkMAgQCBE8MgEIYEDE7whDdQIngTQxVEE0ChFTjxQFE7jnFKAgxCOsBQFZgJ1gE7wmKPAROkTrTEHGAwnYiBHJFAaeXOoyXBEQZPac5AsFgJOhAoh2XJwwnFKoROdE4J9GJzwnIiQmVkInPAC0QE5AJFE64mHY5DFdE4SBEYr5JDJ0hKDJ0jCZJxoACgInmKLAmOTq5OOEy5OPTsxOYE5wmXO5wlYkAnMOqshiRNCgR4LOC8CkJCCEzxHDAgYnJOqpAEDoZ4HEyodDEQpQHdCsQOwwFHEyzoCPYzJGEy0gEwaZGA4acVEQSjHKAomXkQYEYAwlZeRKYDE8gjCYa7zJEwcCkImfKAb4FAD0hdTh4LgRSBOcR0CJz0gYYrrgN4QnEYrxOEE4bEeiAnGF4J2idL6VDE8ohBE0gnFE0J0BE4QGBiROgdIQABgJ2hJoTtjYgZSEE8ScgE4omikUQTcQADA=")),
+ 5: require("heatshrink").decompress(atob("qFGwkCkQAikMAgIliKYonhiAnjkEATIIniEwIhCAgYndEIhQFYUZVEE0BQFOr5QEeQQmiKAL1DOr5QEE7ROCDgZVEAoInZDwchFQQoDPAJOdEQYrBdrZFDOYwncEJDsDVIpOXgJxEE4pObEAgGFgJOaE48BaIhOZJ5ZObY5ROcE441CE6xOGPAwtCJzpGCJ0hHDkI1DJzwoEJzInLFg52dUo5O/J35OzE54mWOx4mXJxx1XE54mXkUhExkSJzCfMOrAlBPBiZXgQDBAQQmgJgh4JOqoYEFYwmaDoZzEFgh1YDgkiiAFEKAroXJJAGFiQmVkCNDTIz5EJy57HKAomXkQYEJoqaYeRadEJrAnJEQUAgJPiAoYmeT4cCkAnBE0BKCJkT1EkDCeJYYiDOkLDFFL5wBE4guCPDhEBEwQiDY70CkInDiQnCJzkhOwhKDdzp2Idb4nEE0B0Bdo4niE0J0CeYhOhgESUYYnidsgnEE0KeCE0gnDE0ciA")),
+ 6: require("heatshrink").decompress(atob("qFGwkCkQA/ABEgKQZPhEwgABEsAoGJkBxBE8JKEAowAbJIhQEgLDiPooAdKA4ncTZAndSwhQEFoInaJQkSKAwlZdgwnfSgYADE4h1ZDwInlcggnIOzAdCE8i7EY5J3XDgYhGd4pOZEI52bSYwGCOAJ2bYIodEOzZOFFAjFcEwwAIE6xOHABBO/J34ndEyx2PJ00BJ00SJ0p1XE54mXOxxO/J5wmYgQnMOrB2BPBgkWiJ1CPBbBYAYR4KiTAXRwIrFTjgZDJYZ4IEyoiEIwrDcEJJQFOqwiBDARxFFwgmXkAYDEogsBF4QmXEQJ7GUYYkBEzDKJAgYmdEQbKFEzonEKYgngJwgmfZggmjKQghgiBRGkBzeTgUikJRgc47LDErTnDEAkQJzkCJwYnEJzonEJIaddOwhJEJzgdBE4hYEJzieJADgnEE0KUCXzoAGkJLEiB2hOgQDBT0TsDT0YmlE4YmjkQ=")),
+ 7: require("heatshrink").decompress(atob("qFGwkCkQAhkIpBiQlhkBSEJ8InlEIIoFE7whEE8pQFE7giBJQoneI4MCTYhQDE7YdCYYondEQYnEPwZ1bE5BQCJzonHkR2ZEAkBE4pNBE7zHFYrYhFUgonaXAQeEEwruZEYcgiROHJ7AfDAwxOeAAURiAmHE65HIOzwmOJ35OPE6xOPO35O/J35O/J1gnPEyx2PEy5OOOq5OnE5xOYO5omZgJQMJrQnLiQnagR4JOq5nCDgZ1fEYRLDE5DoZkUQNoZ4GOrJKGAoomXOw7lCAwYmYDgJSEAAUBA4QDBJzB6FOQrDXJwTJFdLjJKE9jDYZRAmkKAwmhKAgmiKAYmBkApdJIgjCKYIncOQYvJYTovGE84lagR2DE4xOakBOEgJXFOjYnEJAbtdOwggEkAmbDgInDE0B0BE4QgcE5AkiXYbpCOLonGYo4nhPMYnCUEgnBY0kiA==")),
+ 8: require("heatshrink").decompress(atob("qFGwkCkQA/ABBSEJ8MgE4kBEsBPFE7xMCOIJ3hOYgFEE7rCGE70gE4pQBiAndYQwjBUohOZD4ZQFE7YkBE5AICYbZ2GE7sggJRCAA8iYzZOITroALE7EhExh4CAC0QExpPXOponZExx2XJ24nWdh52XdhzF/Yu5O/J35O0E55OXOx5O/J2omXE5x1XO54mYgQnMJrR4LOrciiAmiJgR4KEzIjDPBAlYiAiEeI51YkEBE4J5CD4KceTQQcBJgRQFdTZDCJIjDcNIqhGdTQmCkByFTTInDKgoAEE7ZEEJwhPdE1R1FE0InEE0R3DEwTGcDwomEE7hKFPYqafE8ROCE5DJbE5B/IEqh2ED4gnCJrMCJwgnEiB2bE4qeFEzUggQmIBQLEaEQImHLIImaE4YfcOw4lEFMLECS7onJO8wmkE4QljAAIA==")),
+ };
+}
+let rocketSequence = 1;
+let settings = storage.readJSON("cassioWatch.settings.json", true) || {};
+let rocketSpeed = settings.rocketSpeed || 700;
+delete settings;
// schedule a draw for the next minute
let rocketInterval;
var drawTimeout;
function queueDraw() {
- if (drawTimeout) clearTimeout(drawTimeout);
- drawTimeout = setTimeout(function() {
- drawTimeout = undefined;
- draw();
- }, 60000 - (Date.now() % 60000));
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = setTimeout(function() {
+ drawTimeout = undefined;
+ draw();
+ }, 60000 - (Date.now() % 60000));
}
function clearIntervals() {
- if (rocketInterval) clearInterval(rocketInterval);
- rocketInterval = undefined;
- if (drawTimeout) clearTimeout(drawTimeout);
- drawTimeout = undefined;
+ if (rocketInterval) clearInterval(rocketInterval);
+ rocketInterval = undefined;
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = undefined;
}
-////////////////////////////////////////////
-// TIMER FUNC
-//
-var timer_time = 0;
-var alreadyListenTouch = false;
-function initTouchTimer () {
- if (alreadyListenTouch) return;
- alreadyListenTouch = true;
-
- Bangle.on('swipe', function(dirX,dirY) {
- if (canTouch === false) return;
- var njson = getDataJson();
- if (!njson) return;
-
- if (dirX === -1) {
- timer_time = 0;
- delete njson.timer;
- setDataJson(njson);
- }
- else if (dirX === 1) {
- var now = new Date().getTime();
- njson.timer = now + (timer_time * 1000 * 60);
- Bangle.setLocked(true);
- setDataJson(njson);
- Bangle.buzz(200, 0);
- timer_time = 0;
- }
- else if (dirY === -1) {
- if (canTouch === false || njson.timer) return;
- timer_time = timer_time + 5;
- }
- else if (dirY === 1) {
- if (canTouch === false || njson.timer) return;
- timer_time = timer_time - 5;
- }
- draw();
- });
-}
-setTimeout(() => {
- initTouchTimer ();
-});
-
-function getTimerTime() {
- // if timer_time !== -1, take it
- if (timer_time !== 0) {
- return timer_time + "m";
- } else {
- // else, show diff between njsontime and now
- var njson = getDataJson();
- if (!njson) return false;
- var now = new Date().getTime();
- var diff = Math.round((njson.timer - now) / (1000 * 60));
- //console.log(123, njson, diff, now, njson.timer - now);
- if (diff > 0) return diff + "m";
- else if (njson.timer) {
- Bangle.buzz(1000, 1);
- console.log("END OF TIMER");
- delete njson.timer;
- setDataJson(njson);
- return false;
- } else {
- return false;
- }
- // if diff is <0, delete timer from json
- }
-}
-function drawTimer() {
- //g.drawString(getTimerTime(), 100, 100);
- g.setFont("8x12", 2);
- var t = 97;
- var l = 105;
- var time = getTimerTime();
- if (time || timer_time !== 0) g.drawString(time, l+5, t+0);
- if (time && timer_time === 0) g.drawImage(getClockBg(), l-20, t+2, { scale: 1 });
-}
-
-
-////////////////////////////////////////////
-// DATA READING
-//
-function getDataJson(){
- var res = {"tasks":"", "weather":[]};
- try {
- res = storage.readJSON('advcasio.data.json');
- } catch(ex) {
- return res;
- }
- return res;
-}
-function setDataJson(resJson){
- try {
- res = storage.writeJSON('advcasio.data.json', resJson);
- } catch(ex) {
- return res;
- }
- return res;
-}
-var dataJson = getDataJson();
-
-////////////////////////////////////////////
-// WEATHER!
-//
-function drawWeather(arr) {
- g.setFont("6x8", 1);
- var p = {l: 8, tText: 40, tIcon:20, decal:25};
- var today = new Date().getTime();
- var yesterday = today - (1000 * 60 * 60 * 24);
- var testday = today + (1000 * 60 * 60 * 24 * 2);
- //12h auj > 12h hier qui est sup a 0h auj
- //23h59 hier est sup a 0h auj
- var j = 0;
- for(var i = 0; i yesterday && j < 4) {
- g.drawString(arr[i][0], p.l + p.decal*j + 4, p.tText);
- g.drawImage(iconsWeather[arr[i][1]], p.l + p.decal*j, p.tIcon, { scale: 1 });
- j++
- }
- }
-}
-
-
-////////////////////////////////////////////
-// DRAWING FUNCS
-//
-function drawTasks(str) {
- g.setFont("6x8", 1);
- var t = 57;
- var l = 0;
- g.drawString(str, l+5, t+0);
-}
-
-function drawSteps() {
- g.setFont("8x12", 2);
- var t = 132;
- var l = 150;
- g.drawString(getSteps(), l+5, t+0);
-}
-
-
function drawClock() {
- g.setFont("7x11Numeric7Seg", 3);
- g.clearRect(80, 57, 170, 96);
- g.setColor(255, 255, 255);
- var l = 77;
- var t = 57;
- var w = 170;
- var h = 116;
- g.drawRect(l, t, w, h);
- g.fillRect(l, t, w, h);
- g.setColor(0, 0, 0);
- g.drawString(require("locale").time(new Date(), 1), 76, 60);
-
- // day
- //g.setFont("8x12", 1);
- //g.setFont("9x18", 1);
- //g.drawString(require("locale").dow(new Date(), 2).toUpperCase(), 25, 136);
- g.setFont("8x12", 2);
- g.drawString(require("locale").dow(new Date(), 2), 18, 130);
-
- // month
- g.setFont("8x12");
- g.drawString(require("locale").month(new Date(), 2).toUpperCase(), 80, 127);
-
- // day nb
- g.setFont("8x12", 2);
- const time = new Date().getDate();
- g.drawString(time < 10 ? "0" + time : time, 78, 137);
+ g.setFont("7x11Numeric7Seg", 3);
+ g.clearRect(80, 57, 170, 96);
+ g.setColor(0, 255, 255);
+ g.drawRect(80, 57, 170, 96);
+ g.fillRect(80, 57, 170, 96);
+ g.setColor(0, 0, 0);
+ g.drawString(require("locale").time(new Date(), 1), 70, 60);
+ g.setFont("8x12", 2);
+ g.drawString(require("locale").dow(new Date(), 2).toUpperCase(), 18, 130);
+ g.setFont("8x12");
+ g.drawString(require("locale").month(new Date(), 2).toUpperCase(), 80, 126);
+ g.setFont("8x12", 2);
+ const time = new Date().getDate();
+ g.drawString(time < 10 ? "0" + time : time, 78, 137);
}
function drawBattery() {
- bigThenSmall(E.getBattery(), "%", 140, 23);
+ bigThenSmall(E.getBattery(), "%", 135, 21);
}
+function drawRocket() {
+ let Rocket = getRocketSequences();
+ g.clearRect(5, 62, 63, 115);
+ g.setColor(0, 255, 255);
+ g.drawRect(5, 62, 63, 115);
+ g.fillRect(5, 62, 63, 115);
+ g.drawImage(Rocket[rocketSequence], 5, 65, { scale: 0.7 });
+ g.setColor(0, 0, 0);
+ rocketSequence = rocketSequence + 1;
+ if(rocketSequence > 8) rocketSequence = 1;
+}
+
+function getTemperature(){
+ try {
+ var weatherJson = storage.readJSON('weather.json');
+ var weather = weatherJson.weather;
+ return Math.round(weather.temp-273.15);
+
+ } catch(ex) {
+ print(ex)
+ return "?"
+ }
+}
function getSteps() {
- var steps = 0;
- try{
- if (WIDGETS.wpedom !== undefined) {
- steps = WIDGETS.wpedom.getSteps();
- } else if (WIDGETS.activepedom !== undefined) {
- steps = WIDGETS.activepedom.getSteps();
- } else {
- steps = Bangle.getHealthStatus("day").steps;
- }
- } catch(ex) {
- // In case we failed, we can only show 0 steps.
- return "? k";
- }
-
- steps = Math.round(steps/1000);
- return steps + "k";
+ var steps = Bangle.getHealthStatus("day").steps;
+ steps = Math.round(steps/1000);
+ return steps + "k";
}
-
function draw() {
-
- queueDraw();
+ queueDraw();
- g.reset();
- g.clear();
- g.setColor(255, 255, 255);
- g.fillRect(0, 0, g.getWidth(), g.getHeight());
- let background = getBackgroundImage();
- g.drawImage(background, 0, 0, { scale: 1 });
-
-
- g.setColor(0, 0, 0);
- g.setFont("6x12");
- if(dataJson && dataJson.weather) drawWeather(dataJson.weather);
- if(dataJson && dataJson.tasks) drawTasks(dataJson.tasks);
-
+ g.clear(1);
+ g.setColor(0, 255, 255);
+ g.fillRect(0, 0, g.getWidth(), g.getHeight());
+ let background = getBackgroundImage();
+ g.drawImage(background, 0, 0, { scale: 1 });
+ g.setColor(0, 0, 0);
+ g.setFont("6x12");
+ g.drawString("Launching Process", 30, 20);
+ g.setFont("8x12");
+ g.drawString("ACTIVATE", 40, 35);
- g.setFontAlign(0,-1);
- g.setFont("8x12", 2);
+ g.setFontAlign(0,-1);
+ g.setFont("8x12", 2);
+ g.drawString(getTemperature(), 155, 132);
+ g.drawString(Math.round(Bangle.getHealthStatus("last").bpm), 109, 98);
+ g.drawString(getSteps(), 158, 98);
- drawSteps();
- g.setFontAlign(-1,-1);
- drawClock();
- drawBattery();
- drawTimer();
- // Hide widgets
- for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
+ g.setFontAlign(-1,-1);
+ drawClock();
+ drawRocket();
+ drawBattery();
+
+ // Hide widgets
+ for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
}
-// save batt power, does not seem to work although...
-var canTouch = true;
Bangle.on("lcdPower", (on) => {
- if (on) {
- draw();
- } else {
- canTouch = false;
- clearIntervals();
- }
+ if (on) {
+ draw();
+ } else {
+ clearIntervals();
+ }
});
Bangle.on("lock", (locked) => {
- clearIntervals();
- draw();
- if (!locked) {
- canTouch = true;
- } else {
- canTouch = false;
- }
+ clearIntervals();
+ draw();
+ if (!locked) {
+ rocketInterval = setInterval(drawRocket, rocketSpeed);
+ }
});
+Bangle.setUI("clock");
// Load widgets, but don't show them
Bangle.loadWidgets();
-Bangle.setUI("clock");
-
-g.reset();
-g.clear();
+require("widget_utils").swipeOn(); // hide widgets, make them visible with a swipe
+g.clear(1);
draw();
diff --git a/apps/advcasio/metadata.json b/apps/advcasio/metadata.json
index 0f0c75c07..25dc1243a 100644
--- a/apps/advcasio/metadata.json
+++ b/apps/advcasio/metadata.json
@@ -1,7 +1,7 @@
{ "id": "advcasio",
"name": "Advanced Casio Clock",
"shortName":"advcasio",
- "version":"0.01",
+ "version":"0.04",
"description": "An over-engineered clock inspired by Casio watches. It has a 4 days weather, a timer using swipe and a scratchpad. Can be updated using a dedicated webapp.",
"icon": "app.png",
"tags": "clock",
@@ -12,7 +12,7 @@
{ "url": "screenshot-clock-3.jpg" },
{ "url": "screenshot-webapp.jpg" }
],
- "supports" : ["BANGLEJS", "BANGLEJS2"],
+ "supports" : ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
"allow_emulator":true,
"storage": [
diff --git a/apps/agenda/ChangeLog b/apps/agenda/ChangeLog
index 56dfffa0d..77e11c92e 100644
--- a/apps/agenda/ChangeLog
+++ b/apps/agenda/ChangeLog
@@ -1 +1,11 @@
0.01: Basic agenda with events from GB
+0.02: Added settings page to force calendar sync
+0.03: Disable past events display from settings
+0.04: Added awareness of allDay field
+0.05: Displaying calendar colour and name
+0.06: Added clkinfo for clocks.
+0.07: Clkinfo improvements.
+0.08: Fix error in clkinfo (didn't require Storage & locale)
+ Fix clkinfo icon
+0.09: Ensure Agenda supplies an image for clkinfo items
+0.10: Update clock_info to avoid a redraw
diff --git a/apps/agenda/README.md b/apps/agenda/README.md
index a546e0a89..1a0ec9264 100644
--- a/apps/agenda/README.md
+++ b/apps/agenda/README.md
@@ -1,3 +1,30 @@
# Agenda
-Basic agenda reading the events synchronised from GadgetBridge
+Basic agenda reading the events synchronised from GadgetBridge.
+
+### Functionalities
+
+* List all events in the next week (or whatever is synchronized)
+* Optionally view past events (until GB removes them)
+* Show start time and location of the events in the list
+* Show the colour of the calendar in the list
+* Display description, location and calendar name after tapping on events
+
+### Troubleshooting
+
+For the events sync to work, GadgetBridge needs to have the calendar permission and calendar sync should be enabled in the devices settings (gear sign in GB, also check the blacklisted calendars there, if events are missing).
+Keep in mind that GadgetBridge won't synchronize all events on your calendar, just the ones in a time window of 7 days (you don't want your watch to explode), ideally every day old events get deleted since they appear out of such window.
+
+#### Force Sync
+
+If for any reason events still cannot sync or some are missing, you can try any of the following (just one, you normally don't need to do this):
+1. from GB open the burger menu (side), tap debug and set time.
+2. from the bangle, open settings > apps > agenda > Force calendar sync, then select not to delete the local events (this is equivalent to option 1).
+3. do like option 2 but delete events, GB will synchronize a fresh database instead of patching the old one (good in case you somehow cannot get rid of older events)
+
+After any of the options, you may need to disconnect/force close Gadgetbridge before reconnecting and let it sync (give it some time for that too), restart the agenda app on the bangle after a while to see the changes.
+
+### Report a bug
+
+You can easily open an issue in the espruino repo, but I won't be notified and it might take time.
+If you want a (hopefully) quicker response, just report [on my fork](https://github.com/glemco/BangleApps).
diff --git a/apps/agenda/agenda.clkinfo.js b/apps/agenda/agenda.clkinfo.js
new file mode 100644
index 000000000..7c89446a2
--- /dev/null
+++ b/apps/agenda/agenda.clkinfo.js
@@ -0,0 +1,29 @@
+(function() {
+ var agendaItems = {
+ name: "Agenda",
+ img: atob("GBiBAAAAAAAAAADGMA///w///wf//wAAAA///w///w///w///x///h///h///j///D///X//+f//8wAABwAADw///w///wf//gAAAA=="),
+ items: []
+ };
+ var locale = require("locale");
+ var now = new Date();
+ var agenda = require("Storage").readJSON("android.calendar.json")
+ .filter(ev=>ev.timestamp + ev.durationInSeconds > now/1000)
+ .sort((a,b)=>a.timestamp - b.timestamp);
+
+ agenda.forEach((entry, i) => {
+
+ var title = entry.title.slice(0,12);
+ var date = new Date(entry.timestamp*1000);
+ var dateStr = locale.date(date).replace(/\d\d\d\d/,"");
+ dateStr += entry.durationInSeconds < 86400 ? "/ " + locale.time(date,1) : "";
+
+ agendaItems.items.push({
+ name: "Agenda "+i,
+ get: () => ({ text: title + "\n" + dateStr, img: agendaItems.img }),
+ show: function() {},
+ hide: function () {}
+ });
+ });
+
+ return agendaItems;
+})
diff --git a/apps/agenda/agenda.js b/apps/agenda/agenda.js
index f39e31c75..9cffe0265 100644
--- a/apps/agenda/agenda.js
+++ b/apps/agenda/agenda.js
@@ -6,6 +6,8 @@
title,
description,
location,
+ color:int,
+ calName,
allDay: bool,
}
*/
@@ -24,19 +26,23 @@ var fontLarge = g.getFonts().includes("6x15")?"6x15:2":"6x8:4";
//FIXME maybe write the end from GB already? Not durationInSeconds here (or do while receiving?)
var CALENDAR = require("Storage").readJSON("android.calendar.json",true)||[];
+var settings = require("Storage").readJSON("agenda.settings.json",true)||{};
-CALENDAR=CALENDAR.sort((a,b)=>a.timestamp - b.timestamp)
+CALENDAR=CALENDAR.sort((a,b)=>a.timestamp - b.timestamp);
function getDate(timestamp) {
return new Date(timestamp*1000);
}
-function formatDateLong(date, includeDay) {
- if(includeDay)
- return Locale.date(date)+" "+Locale.time(date,1);
- return Locale.time(date,1);
+function formatDateLong(date, includeDay, allDay) {
+ let shortTime = Locale.time(date,1)+Locale.meridian(date);
+ if(allDay) shortTime = "";
+ if(includeDay || allDay)
+ return Locale.date(date)+" "+shortTime;
+ return shortTime;
}
-function formatDateShort(date) {
- return Locale.date(date).replace(/\d\d\d\d/,"")+Locale.time(date,1);
+function formatDateShort(date, allDay) {
+ return Locale.date(date).replace(/\d\d\d\d/,"")+(allDay?
+ "" : Locale.time(date,1)+Locale.meridian(date));
}
var lines = [];
@@ -45,7 +51,7 @@ function showEvent(ev) {
if(!ev) return;
g.setFont(bodyFont);
//var lines = [];
- if (ev.title) lines = g.wrapString(ev.title, g.getWidth()-10)
+ if (ev.title) lines = g.wrapString(ev.title, g.getWidth()-10);
var titleCnt = lines.length;
var start = getDate(ev.timestamp);
var end = getDate((+ev.timestamp) + (+ev.durationInSeconds));
@@ -53,22 +59,24 @@ function showEvent(ev) {
if (titleCnt) lines.push(""); // add blank line after title
if(start.getDay() == end.getDay() && start.getMonth() == end.getMonth())
includeDay = false;
- if(includeDay) {
+ if(includeDay || ev.allDay) {
lines = lines.concat(
/*LANG*/"Start:",
- g.wrapString(formatDateLong(start, includeDay), g.getWidth()-10),
+ g.wrapString(formatDateLong(start, includeDay, ev.allDay), g.getWidth()-10),
/*LANG*/"End:",
- g.wrapString(formatDateLong(end, includeDay), g.getWidth()-10));
+ g.wrapString(formatDateLong(end, includeDay, ev.allDay), g.getWidth()-10));
} else {
lines = lines.concat(
g.wrapString(Locale.date(start), g.getWidth()-10),
- g.wrapString(/*LANG*/"Start"+": "+formatDateLong(start, includeDay), g.getWidth()-10),
- g.wrapString(/*LANG*/"End"+": "+formatDateLong(end, includeDay), g.getWidth()-10));
+ g.wrapString(/*LANG*/"Start"+": "+formatDateLong(start, includeDay, ev.allDay), g.getWidth()-10),
+ g.wrapString(/*LANG*/"End"+": "+formatDateLong(end, includeDay, ev.allDay), g.getWidth()-10));
}
if(ev.location)
lines = lines.concat(/*LANG*/"Location"+": ", g.wrapString(ev.location, g.getWidth()-10));
if(ev.description)
lines = lines.concat("",g.wrapString(ev.description, g.getWidth()-10));
+ if(ev.calName)
+ lines = lines.concat(/*LANG*/"Calendar"+": ", g.wrapString(ev.calName, g.getWidth()-10));
lines = lines.concat(["",/*LANG*/"< Back"]);
E.showScroller({
h : g.getFontHeight(), // height of each menu item in pixels
@@ -89,6 +97,12 @@ function showEvent(ev) {
}
function showList() {
+ //it might take time for GB to delete old events, decide whether to show them grayed out or hide entirely
+ if(!settings.pastEvents) {
+ let now = new Date();
+ //TODO add threshold here?
+ CALENDAR = CALENDAR.filter(ev=>ev.timestamp + ev.durationInSeconds > now/1000);
+ }
if(CALENDAR.length == 0) {
E.showMessage("No events");
return;
@@ -101,24 +115,21 @@ function showList() {
g.setColor(g.theme.fg);
g.clearRect(r.x,r.y,r.x+r.w, r.y+r.h);
if (!ev) return;
- var isPast = ev.timestamp + ev.durationInSeconds < (new Date())/1000;
+ var isPast = false;
var x = r.x+2, title = ev.title;
- var body = formatDateShort(getDate(ev.timestamp))+"\n"+ev.location;
- var m = ev.title+"\n"+ev.location, longBody=false;
+ var body = formatDateShort(getDate(ev.timestamp),ev.allDay)+"\n"+(ev.location?ev.location:/*LANG*/"No location");
+ if(settings.pastEvents) isPast = ev.timestamp + ev.durationInSeconds < (new Date())/1000;
if (title) g.setFontAlign(-1,-1).setFont(fontBig)
- .setColor(isPast ? "#888" : g.theme.fg).drawString(title, x,r.y+2);
+ .setColor(isPast ? "#888" : g.theme.fg).drawString(title, x+4,r.y+2);
if (body) {
g.setFontAlign(-1,-1).setFont(fontMedium).setColor(isPast ? "#888" : g.theme.fg);
- var l = g.wrapString(body, r.w-(x+14));
- if (l.length>3) {
- l = l.slice(0,3);
- l[l.length-1]+="...";
- }
- longBody = l.length>2;
- g.drawString(l.join("\n"), x+10,r.y+20);
+ g.drawString(body, x+10,r.y+20);
}
- //if (!longBody && msg.src) g.setFontAlign(1,1).setFont("6x8").drawString(msg.src, r.x+r.w-2, r.y+r.h-2);
g.setColor("#888").fillRect(r.x,r.y+r.h-1,r.x+r.w-1,r.y+r.h-1); // dividing line between items
+ if(ev.color) {
+ g.setColor("#"+(0x1000000+Number(ev.color)).toString(16).padStart(6,"0"));
+ g.fillRect(r.x,r.y+4,r.x+3, r.y+r.h-4);
+ }
},
select : idx => showEvent(CALENDAR[idx]),
back : () => load()
diff --git a/apps/agenda/metadata.json b/apps/agenda/metadata.json
index ce8438686..8253b36bc 100644
--- a/apps/agenda/metadata.json
+++ b/apps/agenda/metadata.json
@@ -1,17 +1,19 @@
{
"id": "agenda",
"name": "Agenda",
- "version": "0.02",
+ "version": "0.10",
"description": "Simple agenda",
"icon": "agenda.png",
"screenshots": [{"url":"screenshot_agenda_overview.png"}, {"url":"screenshot_agenda_event1.png"}, {"url":"screenshot_agenda_event2.png"}],
- "tags": "agenda",
+ "tags": "agenda,clkinfo",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"allow_emulator": true,
"storage": [
{"name":"agenda.app.js","url":"agenda.js"},
{"name":"agenda.settings.js","url":"settings.js"},
+ {"name":"agenda.clkinfo.js","url":"agenda.clkinfo.js"},
{"name":"agenda.img","url":"agenda-icon.js","evaluate":true}
- ]
+ ],
+ "data": [{"name":"agenda.settings.json"}]
}
diff --git a/apps/agenda/settings.js b/apps/agenda/settings.js
index fe9dab2d8..4220fcb63 100644
--- a/apps/agenda/settings.js
+++ b/apps/agenda/settings.js
@@ -3,6 +3,10 @@
Bluetooth.println("");
Bluetooth.println(JSON.stringify(message));
}
+ var settings = require("Storage").readJSON("agenda.settings.json",1)||{};
+ function updateSettings() {
+ require("Storage").writeJSON("agenda.settings.json", settings);
+ }
var CALENDAR = require("Storage").readJSON("android.calendar.json",true)||[];
var mainmenu = {
"" : { "title" : "Agenda" },
@@ -32,6 +36,13 @@
E.showAlert(/*LANG*/"You are not connected").then(()=>E.showMenu(mainmenu));
}
},
+ /*LANG*/"Show past events" : {
+ value : !!settings.pastEvents,
+ onchange: v => {
+ settings.pastEvents = v;
+ updateSettings();
+ }
+ },
};
E.showMenu(mainmenu);
})
diff --git a/apps/agpsdata/ChangeLog b/apps/agpsdata/ChangeLog
index c17eac852..8ada244d7 100644
--- a/apps/agpsdata/ChangeLog
+++ b/apps/agpsdata/ChangeLog
@@ -1 +1,5 @@
0.01: First, proof of concept
+0.02: Load AGPS data on app start and automatically in background
+0.03: Do not load AGPS data on boot
+ Increase minimum interval to 6 hours
+0.04: Write AGPS data chunks with delay to improve reliability
diff --git a/apps/agpsdata/README.md b/apps/agpsdata/README.md
index 93cc94259..57bb055a1 100644
--- a/apps/agpsdata/README.md
+++ b/apps/agpsdata/README.md
@@ -1,18 +1,19 @@
# A-GPS Data
-Load assisted GPS data directly to the watch using the new http requests on Android GadgetBridge.
+Load assisted GPS (A-GPS) data directly to your Bangle.js using the new http requests on Android GadgetBridge.
+
+Will download A-GPS data in background (if enabled in settings).
+
+The GNSS type can be configured in the settings.
Make sure:
* your GadgetBridge version supports http requests
* turn on internet access in GadgetBridge settings
-Currently proof of concept on Bangle2 only. Will eventually add a widget for automatic download.
-
-
-
-
-
-
+Currently proof of concept on Bangle.js 2 only.
## Creator
[@pidajo](https://github.com/pidajo)
+
+## Contributor
+[@myxor](https://github.com/myxor)
diff --git a/apps/agpsdata/app.js b/apps/agpsdata/app.js
index 825eda273..4a6d2ba5c 100644
--- a/apps/agpsdata/app.js
+++ b/apps/agpsdata/app.js
@@ -1,125 +1,54 @@
-var _GB = global.GB;
-var counter = 0;
-
-function GB(msg) {
- console.log(msg);
- if (msg.t == "http") {
- display("Received", "(" + msg.resp.length + ") Touch to apply", () => {
- display("Apply data..", "");
- setTimeout(() => {
- if (setAGPS(msg.resp)) {
- display("Success", "Touch for restart", httpTest);
- }
- else {
- display("Error", "Touch for restart", httpTest);
- }
- }, 1);
- });
- }
- if (_GB) {
- _GB(msg);
- }
-}
-
-function setAGPS(data) {
- var js = jsFromBase64(data);
- console.log(js);
- try {
- eval(js);
- return true;
- }
- catch(e) {
- console.log("Error:", e);
- }
- return false;
-}
-
-function jsFromBase64(b64) {
- var bin = atob(b64);
- var chunkSize = 128;
- var js = "Bangle.setGPSPower(1);\n"; // turn GPS on
- var gnss_select="1";
- js += `Serial1.println("${CASIC_CHECKSUM("$PCAS04,"+gnss_select)}")\n`; // set GNSS mode
- // What about:
- // NAV-TIMEUTC (0x01 0x10)
- // NAV-PV (0x01 0x03)
- // or AGPS.zip uses AID-INI (0x0B 0x01)
-
- for (var i=0;i {
- display("Request...", "Touch for restart", httpTest);
- if (Bluetooth.println) {
- console.log("On device");
- Bluetooth.println(JSON.stringify({t:"info", msg:"HTTP Request"}));
- Bluetooth.println(JSON.stringify({t:"http", url:"https://www.espruino.com/agps/casic.base64"}));
- }
- else {
- console.log("Testing on Emulator");
- setTimeout(() => {
- GB({t:"http", resp:testData});
- }, 1);
- }
- });
-}
-
-var nextStep = null;
-
-Bangle.on("touch", () => {
- if (nextStep) {
- nextStep();
- }
-});
-
-httpTest();
-
// Show launcher when middle button pressed
// Load widgets
Bangle.loadWidgets();
Bangle.drawWidgets();
+let waiting = false;
-/*
-require("Storage").write("httptest.info",{
- "id":"httptest",
- "name":"Http Test",
- "src":"httptest.js",
- "icon":"wristlight.img"
-});
-*/
+function start() {
+ g.reset();
+ g.clear();
+ waiting = false;
+ display("Retry?", "touch to retry");
+ Bangle.on("touch", () => { updateAgps(); });
+}
-var testData = "QUdOU1MgZGF0YSBmcm9tIENBU0lDLgpEYXRhTGVuZ3RoOiAyNTk4LgpMaW1pdGF0aW9uOiAzLzEwMDAuCrrOSAAIB7YdxSr+Sg2h8NYlBux1jiUgQbrXgJk/KJvFZVv8pP//uy3i/PH6rv9EMQH6SwBfAOxepgDsXgAAlCULALv/AAtCAAAAAQMAALQ7kly6zkgACAdBzVam9HANoXGycgoqGmnG5X9h3mKrWicvBKhXAp7//+00U/9j/jP/SDHM/Vn/JADrXqYA614AAF6d6v8DAADaEAAAAAIDAADKmrVTus5IAAgHTUirJrjvDKHJDjACcmXJJ+8Lv6rw5LQnl4OChUCt//9XKG3/+u6ZE84ZQuwDAMf/7F6mAOxeAADa0Pb/mP8ABDUAAAADAwAA4pBeVLrOSAAIB5291DTIzAyhJGfzAJ7pCIcIUQMgcfEoJ2TIjrHkqv//YjDTCKj/wRPdGIz/8/9NAOxepgDsXgAAl9/6/yQAAPbhAAAABAMAAIJ7sXC6zkgACAeKKDOgmwEOocKrFwP8Jmgqq4lOQ1VtLSfEi8yDVqn//5MskP6E7WQRPxzv6vv/0//sXqYA7F4AAM4w/f/0/wDoJwAAAAUDAABcUW5Hus5IAAgHu+Va48nxDaFaw0UBkVc73Y3FB+HFmDgoUWIPW+Ck//+iLcf9X/t5/ikzgPrT/wgA7F6mAOxeAAADVgsAiQAACB0AAAAGAwAAvsu9zbrOSAAIB0OVp/7+JQ2hFANPCF+F6KOOMwe9/nC5JsX0CtthqP//WzhzADr/dgqbIRj/nwDj/+xepgDsXgAAP4oKAPv/AOg4AAAABwMAAM4qVwS6zkgACAey9J9b+zwOofLIzwObDg0HLdEmMOA3PydWaUQvr6b//0wxwf/7EJ8K/yLREhkANADsXqYA7F4AALug/f/y/wALLAAAAAgDAACs6Ue+us5IAAgHm7NJLLhaDKEumRQBAFtVTExfuke3AeEmNg9cr2Cq//92MTIJm/63FCkXPv4gABgA616mAOteAACQQfX/HgAAAzUAAAAJAwAAfmebX7rOSAAIB9fgVnnchw2hNlTrAyWnJ5rnBMWFV9GyJzPfZYV8rf//Oyh2AA3xmhLdGtXu/f/Z/+xepgDsXgAA8gXx/37/AAU/AAAACgMAAPbBtfm6zkgACAc+F7Ct97wMoVEVPQCJJG1yeakXMm80PSet+BBd+aH//5AzPf2J+uX97jG7+fj/DgDsXqYA7F4AAGqE//8WAADufQIAAAsDAADELmhius5IAAgHW3g6rwRJDqFpPmYEPf8lNRy9lKO/IX8nJPRjCLOt//90Lir7QQcEGFYUTAguAA4A7F6mAOxeAADrZfj/zP8A5SsAAAAMAwAA/vB8ZbrOSAAIB1mVSsfiXw2hUX8RA60JSyUgLkEgC910JykTq7Usqv//8S9rBw//HhIpGyT/+v8fAOxepgDsXgAAitgKAD4AAOcpAAAADQMAAPoqnZW6zkgACAcCLzz8Q1ANocBvBQFuUWqAxVSgoRjR0SaS5AkH3qr//7cx3vlcB2AYPROjCOr/vf/sXqYA7F4AAA5Y/P/7/wDvGwMAAA4DAABMXoD/us5IAAgH+RiyseCLDKGnOjkHATiXLOud6AnbGuclr2roqg2l///FNxcHi/0hFLoW4fyj/10A7F6mAOxeAAANRf7/GgAA6TQAAAAPAwAAOjJsarrOSAAIB1/+xFM3Xg2hVDKBBg6Tsx25u5hYROV9J/QyJQmLrf//zC1e+7QGxRfmFOAHhf+s/+xepgDsXgAAaE/v/+j/AOonAAAAEAMAAAb9ka66zkgACAc74z4AUZEMofLN6wbbUEjD/w54MWPC3SfOFa4yQ6f//4wtrwH5EOwKOCRTFLz/UP/sXqYA7F4AAOaAFAApAADoOQAAABEDAAC+xoUHus5IAAgH1yXSbYfZDaFOrzwBIM5keona3uYD8JYnC6ekWw+l//8JMC/9ivsaAGEwM/ssAN//7F6mAOxeAAAymwQAof8A7l8AAAASAwAA9kus4rrOSAAIB/9znedCsA2h5VDBBLdHG1Uw8P6P93bRJw5UgTR9qf//LS1BAj0TAwkqJioWBABHAOxepgDsXgAAD54FACwAAN6xAAAAEwMAAEboQta6zkgACAdVJvSzetsMoRclcgKU4/OAvUl5A73wdCYbpBB/P6X//08yQ/4N7voNgx5160gAFwDsXqYA7F4AADa+EADk/wDuLwAAABQDAADyTPBuus5IAAgHhpZXHJnuDaGfsmkMbSLS2QeTnDZBLR8ntwGPV8mj//+SM6n8rftj/EUyZfw7AaX/7F6mAOxeAAAvSQUAAAAA6j4AAAAVAwAAVC23P7rOSAAIB8FSSGuHmw2hXoTeBoU+tLS9TdsrYGopJ+h3h7O9p///mjEIB3X/GhN5GXP/c//V/+xepgDsXgAASREJADgAAO4rAAAAFgMAAMqlmN26zkgACAeGsPJwF8ENoU2cJwHhxiN62PG7uVyKfydPWVyEG6z//8spSgCQ8NkRnxsl7sr/EQDsXqYA7F4AAI0S///u/wDudQEAABcDAABUYe3ous5IAAgHtHJyvWVdDaFr1mwGwPVWIZcaxOQXwAwmeHqW1+Sh//+PPnAAQgAOCxwhof8sAGwA7F6mAOxeAAAhMQcAtf8ABjsAAAAYAwAAsOXsgbrOSAAIB4nxObhoSQ2hOjBcBf2n5ijflOaX/Iz8JpPiNAUTrP//ajEL++oEchfzE9AFSgDT/+tepgDrXgAAohMLACkAAAweAAAAGQMAAFrje3e6zkgACAexAdJ/MyYOoeXvjwPazb0PcXcXfIVaNCZ2mjMDkqn//z02W/qPBMcW7BM7BcX/6//sXqYA7F4AABv4BgAVAAAPIgAAABoDAACqA6wGus5IAAgHxYz/b8dNDaH6/WQF3AILHLKDgTJ+VponRocZMKSm//8bLycAMRANDG8i/RHR/2wA7F6mAOxeAAArEwcAHAAABEgAAQAbAwAA0hkH57rOSAAIB5HXkJcOjA2h8B4kAebzvl2gmkU1K1TzJ86wODP6qP//0CzCAM4OmQrkI1UR7f/n/+xepgDsXgAAcs/u/9//AOplAAAAHQMAAGqvKTa6zkgACAekogIGh+8NoYBTBQMDtQeTiKQ4u0KYHiYkF4HbzaT//7A80v9N/gQLmSC4/icA6v/sXqYA7F4AAB2V7v/2/wAIGQAAAB4DAACQRQ0Tus5IAAgHUsOCUJ71DaHkPFgFdJcpEDguP6ldR+UmgbrM2+6m//9vOPT/S/7vC1sh6/49AJH/7F6mAOxeAAAQCfr/8/8A4wwAAAAfAwAA7IYNqLrOSAAIByZDZIfa4AyhxlEVA9QtFKN+R+1NPIIIJ8xm1q8Qqv//djDzB9H+pBNQGGj+rv++/+xepgDsXgAA3f/6/67/AAFUAAAAIAMAAJSG0BW6zhQACAWVGZOmAAAAAPr///8SEpCmiQcDAD4zLlK6zhAACAZIDf33DwP+/jYK//gDAAAAoBoC9g==";
+function updateAgps() {
+ g.reset();
+ g.clear();
+ if (!waiting) {
+ waiting = true;
+ display("Updating A-GPS...", "takes ~ 10 seconds");
+ require("agpsdata").pull(function() {
+ waiting = false;
+ display("A-GPS updated.", "touch to close");
+ Bangle.on("touch", () => { load(); });
+ },
+ function(error) {
+ waiting = false;
+ E.showAlert(error, "Error")
+ .then(() => { start(); });
+ });
+ } else {
+ display("Waiting...");
+ }
+}
+updateAgps();
diff --git a/apps/agpsdata/boot.js b/apps/agpsdata/boot.js
new file mode 100644
index 000000000..2b1e6819c
--- /dev/null
+++ b/apps/agpsdata/boot.js
@@ -0,0 +1,26 @@
+(function() {
+ let waiting = false;
+ let settings = require("Storage").readJSON("agpsdata.settings.json", 1) || {
+ enabled: true,
+ refresh: 1440
+ };
+
+ if (settings.refresh == undefined) settings.refresh = 1440;
+
+ function successCallback(){
+ waiting = false;
+ }
+
+ function errorCallback(){
+ waiting = false;
+ }
+
+ if (settings.enabled) {
+ setInterval(() => {
+ if (!waiting && NRF.getSecurityStatus().connected){
+ waiting = true;
+ require("agpsdata").pull(successCallback, errorCallback);
+ }
+ }, settings.refresh * 1000 * 60);
+ }
+})();
diff --git a/apps/agpsdata/default.json b/apps/agpsdata/default.json
new file mode 100644
index 000000000..0b6e0cecf
--- /dev/null
+++ b/apps/agpsdata/default.json
@@ -0,0 +1 @@
+{"enabled":true,"refresh":1440,"gnsstype":1}
diff --git a/apps/agpsdata/lib.js b/apps/agpsdata/lib.js
new file mode 100644
index 000000000..34608a5c6
--- /dev/null
+++ b/apps/agpsdata/lib.js
@@ -0,0 +1,93 @@
+function readSettings() {
+ settings = Object.assign(
+ require('Storage').readJSON("agpsdata.default.json", true) || {},
+ require('Storage').readJSON(FILE, true) || {});
+}
+
+var FILE = "agpsdata.settings.json";
+var settings;
+readSettings();
+
+function setAGPS(b64) {
+ return new Promise(function(resolve, reject) {
+ var initCommands = "Bangle.setGPSPower(1);\n"; // turn GPS on
+ const gnsstype = settings.gnsstype || 1; // default GPS
+ initCommands += `Serial1.println("${CASIC_CHECKSUM("$PCAS04," + gnsstype)}")\n`; // set GNSS mode
+ // What about:
+ // NAV-TIMEUTC (0x01 0x10)
+ // NAV-PV (0x01 0x03)
+ // or AGPS.zip uses AID-INI (0x0B 0x01)
+
+ eval(initCommands);
+
+ try {
+ writeChunks(atob(b64), resolve);
+ } catch (e) {
+ console.log("error:", e);
+ reject();
+ }
+ });
+}
+
+var chunkI = 0;
+function writeChunks(bin, resolve) {
+ return new Promise(function(resolve2) {
+ const chunkSize = 128;
+ setTimeout(function() {
+ if (chunkI < bin.length) {
+ var chunk = bin.substr(chunkI, chunkSize);
+ js = `Serial1.write(atob("${btoa(chunk)}"))\n`;
+ eval(js);
+
+ chunkI += chunkSize;
+ writeChunks(bin, resolve);
+ } else {
+ if (resolve)
+ resolve(); // call outer resolve
+ }
+ }, 200);
+ });
+}
+
+function CASIC_CHECKSUM(cmd) {
+ var cs = 0;
+ for (var i = 1; i < cmd.length; i++)
+ cs = cs ^ cmd.charCodeAt(i);
+ return cmd + "*" + cs.toString(16).toUpperCase().padStart(2, '0');
+}
+
+function updateLastUpdate() {
+ const file = "agpsdata.json";
+ let data = require("Storage").readJSON(file, 1) || {};
+ data.lastUpdate = Math.round(Date.now());
+ require("Storage").writeJSON(file, data);
+}
+
+exports.pull = function(successCallback, failureCallback) {
+ const uri = "https://www.espruino.com/agps/casic.base64";
+ if (Bangle.http) {
+ Bangle.http(uri, {timeout : 10000})
+ .then(event => {
+ setAGPS(event.resp)
+ .then(r => {
+ updateLastUpdate();
+ if (successCallback)
+ successCallback();
+ })
+ .catch((e) => {
+ console.log("error", e);
+ if (failureCallback)
+ failureCallback(e);
+ });
+ })
+ .catch((e) => {
+ console.log("error", e);
+ if (failureCallback)
+ failureCallback(e);
+ });
+ } else {
+ console.log("error: No http method found");
+ if (failureCallback)
+ failureCallback(/*LANG*/ "No http method");
+ }
+};
diff --git a/apps/agpsdata/metadata.json b/apps/agpsdata/metadata.json
index af51f3a10..203a00f72 100644
--- a/apps/agpsdata/metadata.json
+++ b/apps/agpsdata/metadata.json
@@ -1,16 +1,24 @@
{ "id": "agpsdata",
- "name": "A-GPS Data",
- "shortName":"AGPS Data",
+ "name": "A-GPS Data Downloader App",
+ "shortName":"A-GPS Data",
"icon": "agpsdata.png",
- "version":"0.01",
- "description": "Download assisted GPS data directly to watch",
- "tags": "assisted,gps,agps,http",
+ "version":"0.04",
+ "description": "Once installed, this app allows you to download assisted GPS (A-GPS) data directly to your Bangle.js **via Gadgetbridge on an Android phone** when you run the app. If you just want to upload the latest AGPS data from this app loader, please use the `Assisted GPS Update (AGPS)` app.",
+ "tags": "boot,tool,assisted,gps,agps,http",
"allow_emulator":true,
"supports": ["BANGLEJS2"],
"readme":"README.md",
- "screenshots" : [ { "url":"screenshot.png" }, { "url":"screenshot2.png" }, { "url":"screenshot3.png" }, { "url":"screenshot4.png" }, { "url":"screenshot5.png" } ],
+ "screenshots" : [ { "url":"screenshot.png" }, { "url":"screenshot2.png" } ],
"storage": [
{"name":"agpsdata.app.js","url":"app.js"},
- {"name":"agpsdata.img","url":"agpsdata-icon.js","evaluate":true}
+ {"name":"agpsdata.img","url":"agpsdata-icon.js","evaluate":true},
+ {"name":"agpsdata.default.json","url":"default.json"},
+ {"name":"agpsdata.boot.js","url":"boot.js"},
+ {"name":"agpsdata","url":"lib.js"},
+ {"name":"agpsdata.settings.js","url":"settings.js"}
+ ],
+ "data": [
+ {"name": "agpsdata.json"},
+ {"name": "agpsdata.settings.json"}
]
}
diff --git a/apps/agpsdata/screenshot.png b/apps/agpsdata/screenshot.png
index fae53ba85..1fcb2d8ee 100644
Binary files a/apps/agpsdata/screenshot.png and b/apps/agpsdata/screenshot.png differ
diff --git a/apps/agpsdata/screenshot2.png b/apps/agpsdata/screenshot2.png
index 7cdba1487..7c546e4b5 100644
Binary files a/apps/agpsdata/screenshot2.png and b/apps/agpsdata/screenshot2.png differ
diff --git a/apps/agpsdata/screenshot3.png b/apps/agpsdata/screenshot3.png
deleted file mode 100644
index be152ba28..000000000
Binary files a/apps/agpsdata/screenshot3.png and /dev/null differ
diff --git a/apps/agpsdata/screenshot4.png b/apps/agpsdata/screenshot4.png
deleted file mode 100644
index 305a166d0..000000000
Binary files a/apps/agpsdata/screenshot4.png and /dev/null differ
diff --git a/apps/agpsdata/screenshot5.png b/apps/agpsdata/screenshot5.png
deleted file mode 100644
index 6468a1872..000000000
Binary files a/apps/agpsdata/screenshot5.png and /dev/null differ
diff --git a/apps/agpsdata/settings.js b/apps/agpsdata/settings.js
new file mode 100644
index 000000000..64fa25330
--- /dev/null
+++ b/apps/agpsdata/settings.js
@@ -0,0 +1,71 @@
+(function(back) {
+function writeSettings(key, value) {
+ var s = Object.assign(
+ require('Storage').readJSON(settingsDefaultFile, true) || {},
+ require('Storage').readJSON(settingsFile, true) || {});
+ s[key] = value;
+ require('Storage').writeJSON(settingsFile, s);
+ readSettings();
+}
+
+function readSettings() {
+ settings = Object.assign(
+ require('Storage').readJSON(settingsDefaultFile, true) || {},
+ require('Storage').readJSON(settingsFile, true) || {});
+}
+
+var settingsFile = "agpsdata.settings.json";
+var settingsDefaultFile = "agpsdata.default.json";
+
+var settings;
+readSettings();
+
+const gnsstypes = [
+ "", "GPS", "BDS", "GPS+BDS", "GLONASS", "GPS+GLONASS", "BDS+GLONASS",
+ "GPS+BDS+GLON."
+];
+
+function buildMainMenu() {
+ var mainmenu = {
+ '' : {'title' : 'AGPS download'},
+ '< Back' : back,
+ "Enabled" : {
+ value : !!settings.enabled,
+ onchange : v => { writeSettings("enabled", v); }
+ },
+ "Refresh every" : {
+ value : settings.refresh / 60,
+ min : 6,
+ max : 168,
+ step : 1,
+ format : v => v + "h",
+ onchange : v => { writeSettings("refresh", Math.round(v * 60)); }
+ },
+ "GNSS type" : {
+ value : settings.gnsstype,
+ min : 1,
+ max : 7,
+ step : 1,
+ format : v => gnsstypes[v],
+ onchange : x => writeSettings('gnsstype', x)
+ },
+ "Force refresh" : () => {
+ E.showMessage("Loading A-GPS data");
+ require("agpsdata")
+ .pull(
+ function() {
+ E.showAlert("Success").then(
+ () => { E.showMenu(buildMainMenu()); });
+ },
+ function(error) {
+ E.showAlert(error, "Error")
+ .then(() => { E.showMenu(buildMainMenu()); });
+ });
+ }
+ };
+
+ return mainmenu;
+}
+
+E.showMenu(buildMainMenu());
+});
diff --git a/apps/aiclock/ChangeLog b/apps/aiclock/ChangeLog
new file mode 100644
index 000000000..fb5aed3e3
--- /dev/null
+++ b/apps/aiclock/ChangeLog
@@ -0,0 +1,5 @@
+0.01: New app!
+0.02: Design improvements and fixes.
+0.03: Indicate battery level through line occurrence.
+0.04: Use widget_utils module.
+0.05: Support for clkinfo.
\ No newline at end of file
diff --git a/apps/aiclock/README.md b/apps/aiclock/README.md
new file mode 100644
index 000000000..31dd5aa29
--- /dev/null
+++ b/apps/aiclock/README.md
@@ -0,0 +1,25 @@
+# AI Clock
+This clock was designed by stable diffusion ([paper](https://arxiv.org/abs/2112.10752)) using the following prompt:
+
+`A rectangle banglejs watchface`
+
+
+The original output of stable diffusion is shown here:
+
+
+
+My implementation is shown below. Note that horizontal lines occur randomly, but the
+probability is correlated with the battery level. So if your screen contains only
+a few lines its time to charge your bangle again ;) Also note that the upper text
+implementes the clkinfo module and can be configured via touch left/right/up/down.
+Touch at the center to trigger the selected action.
+
+
+
+
+# Thanks to
+The great open-source community: I used an open-source diffusion model (https://github.com/CompVis/stable-diffusion)
+to generate a watch face for the open-source smartwatch BangleJs.
+
+## Creator
+- [David Peer](https://github.com/peerdavid).
\ No newline at end of file
diff --git a/apps/aiclock/aiclock.app.js b/apps/aiclock/aiclock.app.js
new file mode 100644
index 000000000..b5bb30b9d
--- /dev/null
+++ b/apps/aiclock/aiclock.app.js
@@ -0,0 +1,437 @@
+/************************************************
+ * AI Clock
+ */
+ const storage = require('Storage');
+ const clock_info = require("clock_info");
+
+
+
+ /************************************************
+ * Assets
+ */
+require("Font7x11Numeric7Seg").add(Graphics);
+Graphics.prototype.setFontGochiHand = function(scale) {
+ // Actual height 27 (29 - 3)
+ this.setFontCustom(
+ atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAA8AAAAADwAAAAAPAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAA/+AAAB//4AAH///gAH///gAAf//AAAB/+AAAAH8AAAAAAAAAAAAAAAAAAAAH8AAAAB/8AAAAP/4AAAB//wAAAPx/AAAB8B+AAAHgD4AAA+AHgAADwAeAAAPAB4AAA8AHgAAD4AeAAAPgB4AAAeAPgAAB8A8AAAH4HwAAAP/+AAAAf/wAAAA/+AAAAB/wAAAAB8AAAAAAAAAAADgAAAAAfAAAAAB4AAAAAPAAAAAB8AAAAAHgAAAAA8AAAAADwAAAAAf4AAAAB//8AAAD//4AAAH//gAAAD/+AAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4AHgAAHgA+AAA/AD4AAD4AfgAAfAD+AAB4Af4AAHgD/gAAeAfeAAB4D54AAHw/HgAAf/4fAAA//B8AAD/4DwAAH+APAAAHgA8AAAAADwAAAAAOAAAAAAAAABgAAAAAPAAAAAB8AAAAAHwAYAAAeAD4AAD4APwAAPA4fgAA8Hw+AADwfB4AAPh4HwAA+HgPAAB/+A8AAH/4DwAAP/weAAAf/j4AAAc//gAAAB/8AAAAD/gAAAAD8AAAAAAAAAAAAAAAAAADAAAAAA+AAAAAP4AAAAB/wAAAAP/AAAAD+8AAAAfzwAAAf8HAAAB/gcAAAH/hwAAAf//gAAA//+AAAAf//gAAAP//gAAAD/+AAAAB/4AAAAH/AAAAAeAAAAAAgAAAAAAAAAAAAcAAAB8H8AAAP4f4AAA/x/wAAD/H/gAAf+A+AAB74B4AAHnwHgAAefAfAAB58A8AAHj4DwAAePgPAAB4fA8AAHh+HgAAeD8+AAB4P/4AAHgf/AAAeA/4AAAAA+AAAAAAAAAAAAAAAAAAHgAAAAD/wAAAA//gAAAH//AAAA//+AAAD4H8AAAfA/wAAB4D/AAAHgP+AAAeB54AAB4HngAAHweeAAAfB54AAA4HngAAAAeeAAAAB/4AAAAH/AAAAAP4AAAAAfAAADwAAAAAPAAAAAA8HgAAADweAAAAPB4AAAA8HgAAADweAAAAPh4AAAA+HgAAAB4eAAAAHx4AAAAf//8AAA///wAAD//+AAAH//4AAAAeAAAAAB4AAAAAHgAAAAAeAAAAAB4AAAAAHgAAAAAAAAAAAAAAAAAAD+AAAA+f+AAAH//8AAA///wAAH/4fgAAePgeAAB4+B4AAHj4HwAAePgPAAB4+A8AAHz4DwAAfngeAAA//B4AAD/+HgAAH//8AAAP//wAAAAf+AAAAA/wAAAAAYAAAAAAAAAAA/gAAAAH/AAAAA/8AAAAD34AAAAeHgAAAB4eAAAAHh4AAAA8HgAAADweAAAAPDwAAAA8PAAAADx4AAAAPvgAAAAf///AAB///8AAH///wAAP///AAA/wA4AABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOA4AAAA8DwAAADwPAAAAPA8AAAAYBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=='),
+ 46,
+ atob("DQoXEBQVExUUFRYUDQ=="),
+ 40+(scale<<8)+(1<<16)
+ );
+ return this;
+}
+
+/************************************************
+ * Set some important constants such as width, height and center
+ */
+var W = g.getWidth(),R=W/2;
+var H = g.getHeight();
+var cx = W/2;
+var cy = H/2;
+var drawTimeout;
+var lock_input = false;
+
+
+/************************************************
+ * SETTINGS
+ */
+const SETTINGS_FILE = "aiclock.setting.json";
+let settings = {
+ menuPosX: 0,
+ menuPosY: 0,
+};
+let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings;
+for (const key in saved_settings) {
+ settings[key] = saved_settings[key]
+}
+
+
+/************************************************
+ * Menu
+ */
+function getDate(){
+ var date = new Date();
+ return ("0"+date.getDate()).substr(-2) + "/" + ("0"+(date.getMonth()+1)).substr(-2)
+}
+
+
+// Custom clockItems menu - therefore, its added here and not in a clkinfo.js file.
+var clockItems = {
+ name: getDate(),
+ img: null,
+ items: [
+ { name: "Week",
+ get: () => ({ text: "Week " + weekOfYear(), img: null}),
+ show: function() { clockItems.items[0].emit("redraw"); },
+ hide: function () {}
+ },
+ ]
+ };
+
+function weekOfYear() {
+ var date = new Date();
+ date.setHours(0, 0, 0, 0);
+ // Thursday in current week decides the year.
+ date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7);
+ // January 4 is always in week 1.
+ var week1 = new Date(date.getFullYear(), 0, 4);
+ // Adjust to Thursday in week 1 and count number of weeks from date to week1.
+ return 1 + Math.round(((date.getTime() - week1.getTime()) / 86400000
+ - 3 + (week1.getDay() + 6) % 7) / 7);
+}
+
+
+
+// Load menu
+var menu = clock_info.load();
+menu = menu.concat(clockItems);
+
+
+ // Ensure that our settings are still in range (e.g. app uninstall). Otherwise reset the position it.
+ if(settings.menuPosX >= menu.length || settings.menuPosY > menu[settings.menuPosX].items.length ){
+ settings.menuPosX = 0;
+ settings.menuPosY = 0;
+ }
+
+ // Set draw functions for each item
+ menu.forEach((menuItm, x) => {
+ menuItm.items.forEach((item, y) => {
+ function drawItem() {
+ // For the clock, we have a special case, as we don't wanna redraw
+ // immediately when something changes. Instead, we update data each minute
+ // to save some battery etc. Therefore, we hide (and disable the listener)
+ // immedeately after redraw...
+ item.hide();
+
+ // After drawing the item, we enable inputs again...
+ lock_input = false;
+
+ var info = item.get();
+ drawMenuItem(info.text, info.img);
+ }
+
+ item.on('redraw', drawItem);
+ })
+ });
+
+
+ function canRunMenuItem(){
+ if(settings.menuPosY == 0){
+ return false;
+ }
+
+ var menuEntry = menu[settings.menuPosX];
+ var item = menuEntry.items[settings.menuPosY-1];
+ return item.run !== undefined;
+ }
+
+
+ function runMenuItem(){
+ if(settings.menuPosY == 0){
+ return;
+ }
+
+ var menuEntry = menu[settings.menuPosX];
+ var item = menuEntry.items[settings.menuPosY-1];
+ try{
+ var ret = item.run();
+ if(ret){
+ Bangle.buzz(300, 0.6);
+ }
+ } catch (ex) {
+ // Simply ignore it...
+ }
+ }
+
+
+/*
+ * Based on the great multi clock from https://github.com/jeffmer/BangleApps/
+ */
+Graphics.prototype.drawRotRect = function(w, r1, r2, angle) {
+ angle = angle % 360;
+ var w2=w/2, h=r2-r1, theta=angle*Math.PI/180;
+ return this.fillPoly(this.transformVertices([-w2,0,-w2,-h,w2,-h,w2,0],
+ {x:cx+r1*Math.sin(theta),y:cy-r1*Math.cos(theta),rotate:theta}));
+};
+
+
+function drawBackground() {
+ g.setFontAlign(0,0);
+ g.setColor(g.theme.fg);
+
+ var bat = E.getBattery() / 100.0;
+ var y = 0;
+ while(y < H){
+ // Show less lines in case of small battery level.
+ if(Math.random() > bat){
+ y += 5;
+ continue;
+ }
+
+ y += 3 + Math.floor(Math.random() * 10);
+ g.drawLine(0, y, W, y);
+ g.drawLine(0, y+1, W, y+1);
+ g.drawLine(0, y+2, W, y+2);
+ y += 2;
+ }
+}
+
+
+function drawCircle(isLocked){
+ g.setColor(g.theme.fg);
+ g.fillCircle(cx, cy, 12);
+
+ var c = isLocked ? "#f00" : g.theme.bg;
+ g.setColor(c);
+ g.fillCircle(cx, cy, 6);
+}
+
+function toAngle(a){
+ if (a < 0){
+ return 360 + a;
+ }
+
+ if(a > 360) {
+ return 360 - a;
+ }
+
+ return a
+}
+
+
+function drawMenuItem(text, image){
+ if(text == null){
+ drawTime();
+ return
+ }
+ // image = atob("GBiBAAD+AAH+AAH+AAH+AAH/AAOHAAYBgAwAwBgwYBgwYBgwIBAwOBAwOBgYIBgMYBgAYAwAwAYBgAOHAAH/AAH+AAH+AAH+AAD+AA==");
+
+ text = String(text);
+
+ g.reset().setBgColor("#fff").setColor("#000");
+ g.setFontAlign(0,0);
+ g.setFont("Vector", 20);
+
+ var imgWidth = image == null ? 0 : 24;
+ var strWidth = g.stringWidth(text);
+ var strHeight = text.split('\n').length > 1 ? 40 : Math.max(24, imgWidth+2);
+ var w = imgWidth + strWidth;
+
+ g.clearRect(cx-w/2-8, 40-strHeight/2-1, cx+w/2+4, 40+strHeight/2)
+
+ // Draw right line as designed by stable diffusion
+ g.drawLine(cx+w/2+5, 40-strHeight/2-1, cx+w/2+5, 40+strHeight/2);
+ g.drawLine(cx+w/2+6, 40-strHeight/2-1, cx+w/2+6, 40+strHeight/2);
+ g.drawLine(cx+w/2+7, 40-strHeight/2-1, cx+w/2+7, 40+strHeight/2);
+
+ // And finally the text
+ g.drawString(text, cx+imgWidth/2, 42);
+ g.drawString(text, cx+1+imgWidth/2, 41);
+
+ if(image != null) {
+ var scale = image.width ? imgWidth / image.width : 1;
+ g.drawImage(image, W/2 + -strWidth/2-4 - parseInt(imgWidth/2), 41-12, {scale: scale});
+ }
+
+ drawTime();
+}
+
+
+function drawTime(){
+ // Draw digital time first
+ drawDigits();
+
+ // And now the analog time
+ var drawHourHand = g.drawRotRect.bind(g,8,12,R-38);
+ var drawMinuteHand = g.drawRotRect.bind(g,6,12,R-12 );
+
+ g.setFontAlign(0,0);
+
+ // Compute angles
+ var date = new Date();
+ var m = parseInt(date.getMinutes() * 360 / 60);
+ var h = date.getHours();
+ h = h > 12 ? h-12 : h;
+ h += date.getMinutes()/60.0;
+ h = parseInt(h*360/12);
+
+ // Draw minute and hour fg
+ g.setColor(g.theme.fg);
+ drawHourHand(h);
+ drawMinuteHand(m);
+}
+
+
+function drawDigits(){
+ var date = new Date();
+
+ g.setFontAlign(0,0);
+ g.setFont("7x11Numeric7Seg",3);
+
+ var text = ("0"+date.getHours()).substr(-2) + ":" + ("0"+date.getMinutes()).substr(-2); //Bangle.getHealthStatus("day").steps;
+ var w = g.stringWidth(text);
+ g.setColor(g.theme.bg);
+ g.fillRect(cx-w/2-4, 120, cx+w/2+4, 140+20);
+
+ // Draw right line as designed by stable diffusion
+ g.setColor(g.theme.fg);
+ g.drawLine(cx+w/2+5, 120, cx+w/2+5, 140+20);
+ g.drawLine(cx+w/2+6, 120, cx+w/2+6, 140+20);
+ g.drawLine(cx+w/2+7, 120, cx+w/2+7, 140+20);
+
+ // And the 7set text
+ g.setColor("#BBB");
+ g.drawString("88:88", cx, 140);
+ g.drawString("88:88", cx+1, 140);
+ g.drawString("88:88", cx, 141);
+
+ g.setColor(g.theme.fg);
+ g.drawString(text, cx, 140);
+ g.drawString(text, cx+1, 140);
+ g.drawString(text, cx, 141);
+}
+
+
+function drawDate(){
+ var menuEntry = menu[settings.menuPosX];
+
+ // The first entry is the overview...
+ if(settings.menuPosY == 0){
+ drawMenuItem(menuEntry.name, menuEntry.img);
+ return;
+ }
+
+ // Draw item if needed
+ lock_input = true;
+ var item = menuEntry.items[settings.menuPosY-1];
+ item.show();
+}
+
+
+
+
+
+function draw(){
+ // Queue draw in one minute
+ queueDraw();
+
+ g.reset();
+ g.clearRect(0, 0, g.getWidth(), g.getHeight());
+ g.setColor(1,1,1);
+
+ drawBackground();
+ drawDate();
+ drawCircle(Bangle.isLocked());
+}
+
+
+/*
+ * Listeners
+ */
+Bangle.on('lcdPower',on=>{
+ if (on) {
+ draw(true);
+ } else { // stop draw timer
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = undefined;
+ }
+});
+
+Bangle.on('lock', function(isLocked) {
+ drawCircle(isLocked);
+});
+
+Bangle.on('touch', function(btn, e){
+ var left = parseInt(g.getWidth() * 0.22);
+ var right = g.getWidth() - left;
+ var upper = parseInt(g.getHeight() * 0.22);
+ var lower = g.getHeight() - upper;
+
+ var is_upper = e.y < upper;
+ var is_lower = e.y > lower;
+ var is_left = e.x < left && !is_upper && !is_lower;
+ var is_right = e.x > right && !is_upper && !is_lower;
+ var is_center = !is_upper && !is_lower && !is_left && !is_right;
+
+ if(lock_input){
+ return;
+ }
+
+ if(is_lower){
+ Bangle.buzz(40, 0.6);
+ settings.menuPosY = (settings.menuPosY+1) % (menu[settings.menuPosX].items.length+1);
+
+ draw();
+ }
+
+ if(is_upper){
+ Bangle.buzz(40, 0.6);
+ settings.menuPosY = settings.menuPosY-1;
+ settings.menuPosY = settings.menuPosY < 0 ? menu[settings.menuPosX].items.length : settings.menuPosY;
+
+ draw();
+ }
+
+ if(is_right){
+ Bangle.buzz(40, 0.6);
+ settings.menuPosX = (settings.menuPosX+1) % menu.length;
+ settings.menuPosY = 0;
+ draw();
+ }
+
+ if(is_left){
+ Bangle.buzz(40, 0.6);
+ settings.menuPosY = 0;
+ settings.menuPosX = settings.menuPosX-1;
+ settings.menuPosX = settings.menuPosX < 0 ? menu.length-1 : settings.menuPosX;
+ draw();
+ }
+
+ if(is_center){
+ if(canRunMenuItem()){
+ runMenuItem();
+ }
+ }
+});
+
+
+E.on("kill", function(){
+ try{
+ storage.write(SETTINGS_FILE, settings);
+ } catch(ex){
+ // If this fails, we still kill the app...
+ }
+});
+
+
+/*
+ * Some helpers
+ */
+function queueDraw() {
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = setTimeout(function() {
+ drawTimeout = undefined;
+ draw();
+ }, 60000 - (Date.now() % 60000));
+}
+
+
+/*
+ * Lets start widgets, listen for btn etc.
+ */
+// Show launcher when middle button pressed
+Bangle.setUI("clock");
+Bangle.loadWidgets();
+/*
+ * we are not drawing the widgets as we are taking over the whole screen
+ * so we will blank out the draw() functions of each widget and change the
+ * area to the top bar doesn't get cleared.
+ */
+require('widget_utils').hide();
+
+// Clear the screen once, at startup and draw clock
+g.setTheme({bg:"#fff",fg:"#000",dark:false}).clear();
+draw();
+
+// After drawing the watch face, we can draw the widgets
+// Bangle.drawWidgets();
diff --git a/apps/aiclock/aiclock.icon.js b/apps/aiclock/aiclock.icon.js
new file mode 100644
index 000000000..0033b3848
--- /dev/null
+++ b/apps/aiclock/aiclock.icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwgP/ACfAEZU/ECZELIKhSR/+PAoWAv4FDhk/x/ggP+j0fx/AgP8n8PCIX8CwIFC/F/w4FBgP4gEHC4QFE//w//DC4QFB8YFC+P/8IdCAoYdBAoPxDoQAd+CiKh4dQwDhfAA4A="))
\ No newline at end of file
diff --git a/apps/aiclock/aiclock.png b/apps/aiclock/aiclock.png
new file mode 100644
index 000000000..104261254
Binary files /dev/null and b/apps/aiclock/aiclock.png differ
diff --git a/apps/aiclock/impl.png b/apps/aiclock/impl.png
new file mode 100644
index 000000000..8a9e43e2d
Binary files /dev/null and b/apps/aiclock/impl.png differ
diff --git a/apps/aiclock/impl_2.png b/apps/aiclock/impl_2.png
new file mode 100644
index 000000000..be3519a4b
Binary files /dev/null and b/apps/aiclock/impl_2.png differ
diff --git a/apps/aiclock/impl_3.png b/apps/aiclock/impl_3.png
new file mode 100644
index 000000000..c2a036d14
Binary files /dev/null and b/apps/aiclock/impl_3.png differ
diff --git a/apps/aiclock/metadata.json b/apps/aiclock/metadata.json
new file mode 100644
index 000000000..1dcda427f
--- /dev/null
+++ b/apps/aiclock/metadata.json
@@ -0,0 +1,22 @@
+{
+ "id": "aiclock",
+ "name": "AI Clock",
+ "shortName":"AI Clock",
+ "icon": "aiclock.png",
+ "version":"0.05",
+ "readme": "README.md",
+ "supports": ["BANGLEJS2"],
+ "description": "A watch face that was designed by an AI (stable diffusion) and implemented by a human.",
+ "type": "clock",
+ "tags": "clock",
+ "screenshots": [
+ {"url":"orig.png"},
+ {"url":"impl.png"},
+ {"url":"impl_2.png"},
+ {"url":"impl_3.png"}
+ ],
+ "storage": [
+ {"name":"aiclock.app.js","url":"aiclock.app.js"},
+ {"name":"aiclock.img","url":"aiclock.icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/aiclock/orig.png b/apps/aiclock/orig.png
new file mode 100644
index 000000000..009826454
Binary files /dev/null and b/apps/aiclock/orig.png differ
diff --git a/apps/alarm/ChangeLog b/apps/alarm/ChangeLog
index 0ac863909..9994d33d9 100644
--- a/apps/alarm/ChangeLog
+++ b/apps/alarm/ChangeLog
@@ -33,4 +33,7 @@
0.31: Add seconds to timers
0.32: Fix wrong hidden filter
Add option for auto-delete a timer after it expires
-
+0.33: Allow hiding timers&alarms
+0.34: Add "Confirm" option to alarm/timer edit menus
+0.35: Add automatic translation of more strings
+0.36: alarm widget moved out of app
diff --git a/apps/alarm/app.js b/apps/alarm/app.js
index bc0b2cf0e..1414c0b90 100644
--- a/apps/alarm/app.js
+++ b/apps/alarm/app.js
@@ -124,7 +124,16 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex) {
value: alarm.as,
onchange: v => alarm.as = v
},
- /*LANG*/"Cancel": () => showMainMenu()
+ /*LANG*/"Hidden": {
+ value: alarm.hidden || false,
+ onchange: v => alarm.hidden = v
+ },
+ /*LANG*/"Cancel": () => showMainMenu(),
+ /*LANG*/"Confirm": () => {
+ prepareAlarmForSave(alarm, alarmIndex, time);
+ saveAndReload();
+ showMainMenu();
+ }
};
if (!isNew) {
@@ -174,7 +183,7 @@ function decodeDOW(alarm) {
.map((day, index) => alarm.dow & (1 << (index + firstDayOfWeek)) ? day : "_")
.join("")
.toLowerCase()
- : "Once"
+ : /*LANG*/"Once"
}
function showEditRepeatMenu(repeat, dow, dowChangeCallback) {
@@ -284,8 +293,17 @@ function showEditTimerMenu(selectedTimer, timerIndex) {
value: timer.del,
onchange: v => timer.del = v
},
+ /*LANG*/"Hidden": {
+ value: timer.hidden || false,
+ onchange: v => timer.hidden = v
+ },
/*LANG*/"Vibrate": require("buzz_menu").pattern(timer.vibrate, v => timer.vibrate = v),
- /*LANG*/"Cancel": () => showMainMenu()
+ /*LANG*/"Cancel": () => showMainMenu(),
+ /*LANG*/"Confirm": () => {
+ prepareTimerForSave(timer, timerIndex, time);
+ saveAndReload();
+ showMainMenu();
+ }
};
if (!isNew) {
diff --git a/apps/alarm/metadata.json b/apps/alarm/metadata.json
index b2d25b77c..dbf090774 100644
--- a/apps/alarm/metadata.json
+++ b/apps/alarm/metadata.json
@@ -2,17 +2,16 @@
"id": "alarm",
"name": "Alarms & Timers",
"shortName": "Alarms",
- "version": "0.32",
+ "version": "0.36",
"description": "Set alarms and timers on your Bangle",
"icon": "app.png",
- "tags": "tool,alarm,widget",
+ "tags": "tool,alarm",
"supports": [ "BANGLEJS", "BANGLEJS2" ],
"readme": "README.md",
- "dependencies": { "scheduler":"type" },
+ "dependencies": { "scheduler":"type", "alarm":"widget" },
"storage": [
{ "name": "alarm.app.js", "url": "app.js" },
- { "name": "alarm.img", "url": "app-icon.js", "evaluate": true },
- { "name": "alarm.wid.js", "url": "widget.js" }
+ { "name": "alarm.img", "url": "app-icon.js", "evaluate": true }
],
"screenshots": [
{ "url": "screenshot-1.png" },
diff --git a/apps/alpinenav/ChangeLog b/apps/alpinenav/ChangeLog
new file mode 100644
index 000000000..b3d1e0874
--- /dev/null
+++ b/apps/alpinenav/ChangeLog
@@ -0,0 +1,2 @@
+0.01: New App!
+0.02: Added adjustment for Bangle.js magnetometer heading fix
diff --git a/apps/alpinenav/app.js b/apps/alpinenav/app.js
index 29eeab0c9..7cffc39c3 100644
--- a/apps/alpinenav/app.js
+++ b/apps/alpinenav/app.js
@@ -224,7 +224,7 @@ Bangle.on('mag', function (m) {
if (isNaN(m.heading))
compass_heading = "---";
else
- compass_heading = 360 - Math.round(m.heading);
+ compass_heading = Math.round(m.heading);
current_colour = g.getColor();
g.reset();
g.setColor(background_colour);
diff --git a/apps/alpinenav/metadata.json b/apps/alpinenav/metadata.json
index dcb56e912..c5a0e0611 100644
--- a/apps/alpinenav/metadata.json
+++ b/apps/alpinenav/metadata.json
@@ -1,7 +1,7 @@
{
"id": "alpinenav",
"name": "Alpine Nav",
- "version": "0.01",
+ "version": "0.02",
"description": "App that performs GPS monitoring to track and display position relative to a given origin in realtime",
"icon": "app-icon.png",
"tags": "outdoors,gps",
diff --git a/apps/android/ChangeLog b/apps/android/ChangeLog
index ee927c752..86dbdb649 100644
--- a/apps/android/ChangeLog
+++ b/apps/android/ChangeLog
@@ -9,4 +9,12 @@
0.08: Handling of alarms
0.09: Alarm vibration, repeat, and auto-snooze now handled by sched
0.10: Fix SMS bug
-0.11: Use default Bangle formatter for booleans
+0.12: Use default Bangle formatter for booleans
+0.13: Added Bangle.http function (see Readme file for more info)
+0.14: Fix timeout of http function not being cleaned up
+0.15: Allow method/body/headers to be specified for `http` (needs Gadgetbridge 0.68.0b or later)
+0.16: Bangle.http now fails immediately if there is no Bluetooth connection (fix #2152)
+0.17: Now kick off Calendar sync as soon as connected to Gadgetbridge
+0.18: Use new message library
+ If connected to Gadgetbridge, allow GPS forwarding from phone (Gadgetbridge code still not merged)
+0.19: Add automatic translation for a couple of strings.
diff --git a/apps/android/README.md b/apps/android/README.md
index c10718aac..c76e6e528 100644
--- a/apps/android/README.md
+++ b/apps/android/README.md
@@ -20,6 +20,8 @@ It contains:
of Gadgetbridge - making your phone make noise so you can find it.
* `Keep Msgs` - default is `Off`. When Gadgetbridge disconnects, should Bangle.js
keep any messages it has received, or should it delete them?
+* `Overwrite GPS` - when GPS is requested by an app, this doesn't use Bangle.js's GPS
+but instead asks Gadgetbridge on the phone to use the phone's GPS
* `Messages` - launches the messages app, showing a list of messages
## How it works
@@ -32,6 +34,25 @@ Responses are sent back to Gadgetbridge simply as one line of JSON.
More info on message formats on http://www.espruino.com/Gadgetbridge
+## Functions provided
+
+The boot code also provides some useful functions:
+
+* `Bangle.messageResponse = function(msg,response)` - send a yes/no response to a message. `msg` is a message object, and `response` is a boolean.
+* `Bangle.musicControl = function(cmd)` - control music, cmd = `play/pause/next/previous/volumeup/volumedown`
+* `Bangle.http = function(url,options)` - make an HTTPS request to a URL and return a promise with the data. Requires the [internet enabled `Bangle.js Gadgetbridge` app](http://www.espruino.com/Gadgetbridge#http-requests). `options` can contain:
+ * `id` - a custom (string) ID
+ * `timeout` - a timeout for the request in milliseconds (default 30000ms)
+ * `xpath` an xPath query to run on the request (but right now the URL requested must be XML - HTML is rarely XML compliant)
+
+eg:
+
+```
+Bangle.http("https://pur3.co.uk/hello.txt").then(data=>{
+ console.log("Got ",data);
+});
+```
+
## Testing
Bangle.js can only hold one connection open at a time, so it's hard to see
diff --git a/apps/android/boot.js b/apps/android/boot.js
index 9cdc019a6..e1e5b028b 100644
--- a/apps/android/boot.js
+++ b/apps/android/boot.js
@@ -57,7 +57,7 @@
t:event.cmd=="incoming"?"add":"remove",
id:"call", src:"Phone",
positive:true, negative:true,
- title:event.name||"Call", body:"Incoming call\n"+event.number});
+ title:event.name||/*LANG*/"Call", body:/*LANG*/"Incoming call\n"+event.number});
require("messages").pushMessage(event);
},
"alarm" : function() {
@@ -91,10 +91,6 @@
sched.reload();
},
//TODO perhaps move those in a library (like messages), used also for viewing events?
- //simple package with events all together
- "calendarevents" : function() {
- require("Storage").writeJSON("android.calendar.json", event.events);
- },
//add and remove events based on activity on phone (pebble-like)
"calendar" : function() {
var cal = require("Storage").readJSON("android.calendar.json",true);
@@ -109,7 +105,7 @@
"calendar-" : function() {
var cal = require("Storage").readJSON("android.calendar.json",true);
//if any of those happen we are out of sync!
- if (!cal || !Array.isArray(cal)) return;
+ if (!cal || !Array.isArray(cal)) cal = [];
cal = cal.filter(e=>e.id!=event.id);
require("Storage").writeJSON("android.calendar.json", cal);
},
@@ -118,15 +114,74 @@
var cal = require("Storage").readJSON("android.calendar.json",true);
if (!cal || !Array.isArray(cal)) cal = [];
gbSend({t:"force_calendar_sync", ids: cal.map(e=>e.id)});
+ },
+ "http":function() {
+ //get the promise and call the promise resolve
+ if (Bangle.httpRequest === undefined) return;
+ var request=Bangle.httpRequest[event.id];
+ if (request === undefined) return; //already timedout or wrong id
+ delete Bangle.httpRequest[event.id];
+ clearTimeout(request.t); //t = timeout variable
+ if(event.err!==undefined) //if is error
+ request.j(event.err); //r = reJect function
+ else
+ request.r(event); //r = resolve function
+ },
+ "gps": function() {
+ const settings = require("Storage").readJSON("android.settings.json",1)||{};
+ if (!settings.overwriteGps) return;
+ delete event.t;
+ event.satellites = NaN;
+ event.course = NaN;
+ event.fix = 1;
+ Bangle.emit('gps', event);
+ },
+ "is_gps_active": function() {
+ gbSend({ t: "gps_power", status: Bangle._PWR && Bangle._PWR.GPS && Bangle._PWR.GPS.length>0 });
}
};
var h = HANDLERS[event.t];
if (h) h(); else console.log("GB Unknown",event);
};
+ // HTTP request handling - see the readme
+ // options = {id,timeout,xpath}
+ Bangle.http = (url,options)=>{
+ options = options||{};
+ if (!NRF.getSecurityStatus().connected)
+ return Promise.reject(/*LANG*/"Not connected to Bluetooth");
+ if (Bangle.httpRequest === undefined)
+ Bangle.httpRequest={};
+ if (options.id === undefined) {
+ // try and create a unique ID
+ do {
+ options.id = Math.random().toString().substr(2);
+ } while( Bangle.httpRequest[options.id]!==undefined);
+ }
+ //send the request
+ var req = {t: "http", url:url, id:options.id};
+ if (options.xpath) req.xpath = options.xpath;
+ if (options.method) req.method = options.method;
+ if (options.body) req.body = options.body;
+ if (options.headers) req.headers = options.headers;
+ gbSend(req);
+ //create the promise
+ var promise = new Promise(function(resolve,reject) {
+ //save the resolve function in the dictionary and create a timeout (30 seconds default)
+ Bangle.httpRequest[options.id]={r:resolve,j:reject,t:setTimeout(()=>{
+ //if after "timeoutMillisec" it still hasn't answered -> reject
+ delete Bangle.httpRequest[options.id];
+ reject("Timeout");
+ },options.timeout||30000)};
+ });
+ return promise;
+ }
// Battery monitor
function sendBattery() { gbSend({ t: "status", bat: E.getBattery(), chg: Bangle.isCharging()?1:0 }); }
- NRF.on("connect", () => setTimeout(sendBattery, 2000));
+ NRF.on("connect", () => setTimeout(function() {
+ sendBattery();
+ GB({t:"force_calendar_sync_start"}); // send a list of our calendar entries to start off the sync process
+ }, 2000));
Bangle.on("charging", sendBattery);
if (!settings.keep)
NRF.on("disconnect", () => require("messages").clearAll()); // remove all messages on disconnect
@@ -146,6 +201,30 @@
if (isFinite(msg.id)) return gbSend({ t: "notify", n:response?"OPEN":"DISMISS", id: msg.id });
// error/warn here?
};
+ // GPS overwrite logic
+ if (settings.overwriteGps) { // if the overwrite option is set../
+ // Save current logic
+ const originalSetGpsPower = Bangle.setGPSPower;
+ // Replace set GPS power logic to suppress activation of gps (and instead request it from the phone)
+ Bangle.setGPSPower = (isOn, appID) => {
+ // if not connected, use old logic
+ if (!NRF.getSecurityStatus().connected) return originalSetGpsPower(isOn, appID);
+ // Emulate old GPS power logic
+ if (!Bangle._PWR) Bangle._PWR={};
+ if (!Bangle._PWR.GPS) Bangle._PWR.GPS=[];
+ if (!appID) appID="?";
+ if (isOn && !Bangle._PWR.GPS.includes(appID)) Bangle._PWR.GPS.push(appID);
+ if (!isOn && Bangle._PWR.GPS.includes(appID)) Bangle._PWR.GPS.splice(Bangle._PWR.GPS.indexOf(appID),1);
+ let pwr = Bangle._PWR.GPS.length>0;
+ gbSend({ t: "gps_power", status: pwr });
+ return pwr;
+ }
+ // Replace check if the GPS is on to check the _PWR variable
+ Bangle.isGPSOn = () => {
+ return Bangle._PWR && Bangle._PWR.GPS && Bangle._PWR.GPS.length>0;
+ }
+ }
+
// remove settings object so it's not taking up RAM
delete settings;
})();
diff --git a/apps/android/metadata.json b/apps/android/metadata.json
index ec8b8b0fe..d5a45edb7 100644
--- a/apps/android/metadata.json
+++ b/apps/android/metadata.json
@@ -2,11 +2,11 @@
"id": "android",
"name": "Android Integration",
"shortName": "Android",
- "version": "0.12",
+ "version": "0.19",
"description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.",
"icon": "app.png",
"tags": "tool,system,messages,notifications,gadgetbridge",
- "dependencies": {"messages":"app"},
+ "dependencies": {"messages":"module"},
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"storage": [
diff --git a/apps/android/settings.js b/apps/android/settings.js
index c7c34a76f..3e04e0f9d 100644
--- a/apps/android/settings.js
+++ b/apps/android/settings.js
@@ -1,4 +1,7 @@
(function(back) {
+
+
+
function gb(j) {
Bluetooth.println(JSON.stringify(j));
}
@@ -23,7 +26,17 @@
updateSettings();
}
},
- /*LANG*/"Messages" : ()=>load("messages.app.js"),
+ /*LANG*/"Overwrite GPS" : {
+ value : !!settings.overwriteGps,
+ onchange: newValue => {
+ if (newValue) {
+ Bangle.setGPSPower(false, 'android');
+ }
+ settings.overwriteGps = newValue;
+ updateSettings();
+ }
+ },
+ /*LANG*/"Messages" : ()=>require("message").openGUI(),
};
E.showMenu(mainmenu);
})
diff --git a/apps/animclk/ChangeLog b/apps/animclk/ChangeLog
index 348448c34..76d15bdb1 100644
--- a/apps/animclk/ChangeLog
+++ b/apps/animclk/ChangeLog
@@ -1,3 +1,5 @@
0.01: New App!
0.02: Fix bug if image clock wasn't installed
0.03: Update to use setUI
+0.04: Tell clock widgets to hide. Move loadWidgets() so it only runs on
+startup and not on every draw.
diff --git a/apps/animclk/app.js b/apps/animclk/app.js
index 4bf63daf6..bdc399fbe 100644
--- a/apps/animclk/app.js
+++ b/apps/animclk/app.js
@@ -87,7 +87,6 @@ if (g.drawImages) {
draw();
var secondInterval = setInterval(draw,100);
// load widgets
- Bangle.loadWidgets();
Bangle.drawWidgets();
// Stop when LCD goes off
Bangle.on('lcdPower',on=>{
@@ -104,3 +103,5 @@ if (g.drawImages) {
}
// Show launcher when button pressed
Bangle.setUI("clock");
+
+Bangle.loadWidgets();
diff --git a/apps/animclk/metadata.json b/apps/animclk/metadata.json
index 31dfe453f..0b426a37d 100644
--- a/apps/animclk/metadata.json
+++ b/apps/animclk/metadata.json
@@ -2,7 +2,7 @@
"id": "animclk",
"name": "Animated Clock",
"shortName": "Anim Clock",
- "version": "0.03",
+ "version": "0.04",
"description": "An animated clock face using Mark Ferrari's amazing 8 bit game art and palette cycling: http://www.markferrari.com/art/8bit-game-art",
"icon": "app.png",
"type": "clock",
diff --git a/apps/antonclk/ChangeLog b/apps/antonclk/ChangeLog
index f7e95b5fa..4ef0cee75 100644
--- a/apps/antonclk/ChangeLog
+++ b/apps/antonclk/ChangeLog
@@ -10,4 +10,7 @@
week is buffered until date or timezone changes
0.07: align default settings with app.js (otherwise the initial displayed settings will be confusing to users)
0.08: fixed calendar weeknumber not shortened to two digits
-0.09: Use default Bangle formatter for booleans
\ No newline at end of file
+0.09: Use default Bangle formatter for booleans
+0.10: Use Bangle.setUI({remove:...}) to allow loading the launcher without a full reset on 2v16
+0.11: Moved enhanced Anton clock to 'Anton Clock Plus' and stripped this clock back down to make it faster for new users (270ms -> 170ms)
+ Modified to avoid leaving functions defined when using setUI({remove:...})
diff --git a/apps/antonclk/app.js b/apps/antonclk/app.js
index 4b1e71bda..528866588 100644
--- a/apps/antonclk/app.js
+++ b/apps/antonclk/app.js
@@ -1,230 +1,45 @@
// Clock with large digits using the "Anton" bold font
-
-const SETTINGSFILE = "antonclk.json";
-
Graphics.prototype.setFontAnton = function(scale) {
// Actual height 69 (68 - 0)
g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAA/gAAAAAAAAAAP/gAAAAAAAAAH//gAAAAAAAAB///gAAAAAAAAf///gAAAAAAAP////gAAAAAAD/////gAAAAAA//////gAAAAAP//////gAAAAH///////gAAAB////////gAAAf////////gAAP/////////gAD//////////AA//////////gAA/////////4AAA////////+AAAA////////gAAAA///////wAAAAA//////8AAAAAA//////AAAAAAA/////gAAAAAAA////4AAAAAAAA///+AAAAAAAAA///gAAAAAAAAA//wAAAAAAAAAA/8AAAAAAAAAAA/AAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//////AAAAAB///////8AAAAH////////AAAAf////////wAAA/////////4AAB/////////8AAD/////////+AAH//////////AAP//////////gAP//////////gAP//////////gAf//////////wAf//////////wAf//////////wAf//////////wA//8AAAAAB//4A//wAAAAAAf/4A//gAAAAAAP/4A//gAAAAAAP/4A//gAAAAAAP/4A//wAAAAAAf/4A///////////4Af//////////wAf//////////wAf//////////wAf//////////wAP//////////gAP//////////gAH//////////AAH//////////AAD/////////+AAB/////////8AAA/////////4AAAP////////gAAAD///////+AAAAAf//////4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/gAAAAAAAAAAP/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/AAAAAAAAAAA//AAAAAAAAAAA/+AAAAAAAAAAB/8AAAAAAAAAAD//////////gAH//////////gAP//////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/4AAAAB/gAAD//4AAAAf/gAAP//4AAAB//gAA///4AAAH//gAB///4AAAf//gAD///4AAA///gAH///4AAD///gAP///4AAH///gAP///4AAP///gAf///4AAf///gAf///4AB////gAf///4AD////gA////4AH////gA////4Af////gA////4A/////gA//wAAB/////gA//gAAH/////gA//gAAP/////gA//gAA///8//gA//gAD///w//gA//wA////g//gA////////A//gA///////8A//gA///////4A//gAf//////wA//gAf//////gA//gAf/////+AA//gAP/////8AA//gAP/////4AA//gAH/////gAA//gAD/////AAA//gAB////8AAA//gAA////wAAA//gAAP///AAAA//gAAD//8AAAA//gAAAP+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/+AAAAAD/wAAB//8AAAAP/wAAB///AAAA//wAAB///wAAB//wAAB///4AAD//wAAB///8AAH//wAAB///+AAP//wAAB///+AAP//wAAB////AAf//wAAB////AAf//wAAB////gAf//wAAB////gA///wAAB////gA///wAAB////gA///w//AAf//wA//4A//AAA//wA//gA//AAAf/wA//gB//gAAf/wA//gB//gAAf/wA//gD//wAA//wA//wH//8AB//wA///////////gA///////////gA///////////gA///////////gAf//////////AAf//////////AAP//////////AAP/////////+AAH/////////8AAH///+/////4AAD///+f////wAAA///8P////gAAAf//4H///+AAAAH//gB///wAAAAAP4AAH/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/wAAAAAAAAAA//wAAAAAAAAAP//wAAAAAAAAB///wAAAAAAAAf///wAAAAAAAH////wAAAAAAA/////wAAAAAAP/////wAAAAAB//////wAAAAAf//////wAAAAH///////wAAAA////////wAAAP////////wAAA///////H/wAAA//////wH/wAAA/////8AH/wAAA/////AAH/wAAA////gAAH/wAAA///4AAAH/wAAA//+AAAAH/wAAA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gAAAAAAAAH/4AAAAAAAAAAH/wAAAAAAAAAAH/wAAAAAAAAAAH/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB//8AAA/////+B///AAA/////+B///wAA/////+B///4AA/////+B///8AA/////+B///8AA/////+B///+AA/////+B////AA/////+B////AA/////+B////AA/////+B////gA/////+B////gA/////+B////gA/////+A////gA//gP/gAAB//wA//gf/AAAA//wA//gf/AAAAf/wA//g//AAAAf/wA//g//AAAA//wA//g//gAAA//wA//g//+AAP//wA//g////////gA//g////////gA//g////////gA//g////////gA//g////////AA//gf///////AA//gf//////+AA//gP//////+AA//gH//////8AA//gD//////4AA//gB//////wAA//gA//////AAAAAAAH////8AAAAAAAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//////gAAAAB///////+AAAAH////////gAAAf////////4AAB/////////8AAD/////////+AAH//////////AAH//////////gAP//////////gAP//////////gAf//////////wAf//////////wAf//////////wAf//////////wAf//////////4A//wAD/4AAf/4A//gAH/wAAP/4A//gAH/wAAP/4A//gAP/wAAP/4A//gAP/4AAf/4A//wAP/+AD//4A///wP//////4Af//4P//////wAf//4P//////wAf//4P//////wAf//4P//////wAP//4P//////gAP//4H//////gAH//4H//////AAH//4D/////+AAD//4D/////8AAB//4B/////4AAA//4A/////wAAAP/4AP////AAAAB/4AD///4AAAAAAAAAH/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//AAAAAAAAAAA//gAAAAAAAAAA//gAAAAAAAAAA//gAAAAAAADgA//gAAAAAAP/gA//gAAAAAH//gA//gAAAAB///gA//gAAAAP///gA//gAAAD////gA//gAAAf////gA//gAAB/////gA//gAAP/////gA//gAB//////gA//gAH//////gA//gA///////gA//gD///////gA//gf///////gA//h////////gA//n////////gA//////////gAA/////////AAAA////////wAAAA///////4AAAAA///////AAAAAA//////4AAAAAA//////AAAAAAA/////4AAAAAAA/////AAAAAAAA////8AAAAAAAA////gAAAAAAAA///+AAAAAAAAA///4AAAAAAAAA///AAAAAAAAAA//4AAAAAAAAAA/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//gB///wAAAAP//4H///+AAAA///8P////gAAB///+f////4AAD///+/////8AAH/////////+AAH//////////AAP//////////gAP//////////gAf//////////gAf//////////wAf//////////wAf//////////wA///////////wA//4D//wAB//4A//wB//gAA//4A//gA//gAAf/4A//gA//AAAf/4A//gA//gAAf/4A//wB//gAA//4A///P//8AH//4Af//////////wAf//////////wAf//////////wAf//////////wAf//////////gAP//////////gAP//////////AAH//////////AAD/////////+AAD///+/////8AAB///8f////wAAAf//4P////AAAAH//wD///8AAAAA/+AAf//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH//gAAAAAAAAB///+AA/+AAAAP////gA//wAAAf////wA//4AAB/////4A//8AAD/////8A//+AAD/////+A///AAH/////+A///AAP//////A///gAP//////A///gAf//////A///wAf//////A///wAf//////A///wAf//////A///wA///////AB//4A//4AD//AAP/4A//gAB//AAP/4A//gAA//AAP/4A//gAA/+AAP/4A//gAB/8AAP/4A//wAB/8AAf/4Af//////////wAf//////////wAf//////////wAf//////////wAf//////////wAP//////////gAP//////////gAH//////////AAH/////////+AAD/////////8AAB/////////4AAAf////////wAAAP////////AAAAB///////4AAAAAD/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/AAB/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="), 46, atob("EiAnGicnJycnJycnEw=="), 78 + (scale << 8) + (1 << 16));
};
-Graphics.prototype.setFontAntonSmall = function(scale) {
- // Actual height 53 (52 - 0)
- g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAAAAAAAAAAAAAMAAAAAAAAD8AAAAAAAA/8AAAAAAAf/8AAAAAAH//8AAAAAB///8AAAAA////8AAAAP////8AAAD/////8AAB//////8AAf//////8AH///////4A///////+AA///////AAA//////wAAA/////8AAAA////+AAAAA////gAAAAA///4AAAAAA//8AAAAAAA//AAAAAAAA/wAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/////wAAA//////8AAB//////+AAH///////gAH///////gAP///////wAf///////4Af///////4A////////8A////////8A////////8A//AAAAD/8A/8AAAAA/8A/8AAAAA/8A/8AAAAA/8A/+AAAAB/8A////////8A////////8A////////8Af///////4Af///////4AP///////wAP///////wAH///////gAD///////AAA//////8AAAP/////wAAAAAAAAAAAAAAAAAAAAAAAfwAAAAAAAA/4AAAAAAAA/4AAAAAAAB/wAAAAAAAB/wAAAAAAAD/wAAAAAAAD/gAAAAAAAH///////8AP///////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/4AAAP8AA//4AAA/8AB//4AAH/8AH//4AAP/8AP//4AA//8AP//4AB//8Af//4AD//8Af//4AP//8A///4Af//8A///4A///8A///4D///8A//AAH///8A/8AAP///8A/8AA//+/8A/8AD//8/8A/+Af//w/8A//////g/8A/////+A/8A/////8A/8Af////4A/8Af////wA/8AP////AA/8AP///+AA/8AH///8AA/8AD///wAA/8AA///AAA/8AAP/4AAA/8AAAAAAAAAAAAAAAAAAAAAAH4AAf/gAAA/4AAf/8AAD/4AAf//AAH/4AAf//gAP/4AAf//wAP/4AAf//wAf/4AAf//4Af/4AAf//4A//4AAf//8A//4AAf//8A//4AAP//8A//A/8AB/8A/8A/8AA/8A/8B/8AA/8A/8B/8AA/8A/+D//AB/8A////////8A////////8A////////8Af///////4Af///////4Af///////wAP///////gAH//9////gAD//4///+AAB//wf//4AAAP/AH//gAAAAAAAAAAAAAAAAAAAAAAAAAAAH/wAAAAAAB//wAAAAAAP//wAAAAAD///wAAAAA////wAAAAH////wAAAB/////wAAAf/////wAAD//////wAA///////wAA/////h/wAA////wB/wAA///8AB/wAA///AAB/wAA//gAAB/wAA////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8AAAAAAB/wAAAAAAAB/wAAAAAAAB/wAAAAAAAAAAAAAAAAAAAAAAAAAAAP/4AA////4P/+AA////4P//AA////4P//gA////4P//wA////4P//wA////4P//4A////4P//4A////4P//8A////4P//8A////4P//8A/8H/AAB/8A/8H+AAA/8A/8P+AAA/8A/8P+AAA/8A/8P/gAD/8A/8P/////8A/8P/////8A/8P/////8A/8P/////4A/8H/////4A/8H/////wA/8D/////wA/8B/////gA/8A////+AA/8AP///4AAAAAB///AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////wAAAf/////8AAB///////AAH///////gAP///////wAP///////wAf///////4Af///////4A////////8A////////8A////////8A/+AH/AB/8A/8AP+AA/8A/4Af+AA/8A/8Af+AA/8A/8Af/gH/8A//4f////8A//4f////8A//4f////8Af/4f////4Af/4f////4AP/4P////wAP/4P////gAH/4H////AAD/4D///+AAB/4B///4AAAP4AP//gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8AAAAAAAA/8AAAAAAAA/8AAAAAB8A/8AAAAB/8A/8AAAAf/8A/8AAAH//8A/8AAA///8A/8AAH///8A/8AA////8A/8AD////8A/8Af////8A/8B/////8A/8P/////8A/8//////8A////////AA///////AAA//////gAAA/////4AAAA/////AAAAA////4AAAAA////AAAAAA///8AAAAAA///gAAAAAA//+AAAAAAA//wAAAAAAA/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/gD//gAAA//4P//8AAD//8f///AAH//+////gAH///////wAP///////4AP///////8Af///////8Af///////+Af///////+A////////+A//B//AB/+A/+A/+AA/+A/8Af+AA/+A/+Af+AA/+A//A//AB/+A////////+Af///////+Af///////+Af///////8Af///////8AP///////4AH///////4AH//+////wAD//+////AAA//4P//+AAAP/gH//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH//gAfgAAA///8A/8AAB///+A//AAH////A//gAH////g//wAP////g//wAf////w//4Af////w//4A/////w//8A/////w//8A/////w//8A//gP/wA/8A/8AD/wA/8A/8AD/wAf8A/8AD/gA/8A/+AH/AB/8A////////8A////////8A////////8Af///////4Af///////4Af///////wAP///////wAH///////gAD//////+AAA//////4AAAP/////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="), 46, atob("DhgeFB4eHh4eHh4eDw=="), 60 + (scale << 8) + (1 << 16));
-};
+{ // must be inside our own scope here so that when we are unloaded everything disappears
+ // we also define functions using 'let fn = function() {..}' for the same reason. function decls are global
+let drawTimeout;
-// variables defined from settings
-var secondsMode;
-var secondsColoured;
-var secondsWithColon;
-var dateOnMain;
-var dateOnSecs;
-var weekDay;
-var calWeek;
-var upperCase;
-var vectorFont;
+// Actually draw the watch face
+let draw = function() {
+ var x = g.getWidth() / 2;
+ var y = g.getHeight() / 2;
+ g.reset().clearRect(Bangle.appRect); // clear whole background (w/o widgets)
+ var date = new Date();
+ var timeStr = require("locale").time(date, 1); // Hour and minute
+ g.setFontAlign(0, 0).setFont("Anton").drawString(timeStr, x, y);
+ // Show date and day of week
+ var dateStr = require("locale").date(date, 0).toUpperCase()+"\n"+
+ require("locale").dow(date, 0).toUpperCase();
+ g.setFontAlign(0, 0).setFont("6x8", 2).drawString(dateStr, x, y+48);
-// dynamic variables
-var drawTimeout;
-var queueMillis = 1000;
-var secondsScreen = true;
-
-var isBangle1 = (process.env.HWVERSION == 1);
-
-//For development purposes
-/*
-require('Storage').writeJSON(SETTINGSFILE, {
- secondsMode: "Unlocked", // "Never", "Unlocked", "Always"
- secondsColoured: true,
- secondsWithColon: true,
- dateOnMain: "Long", // "Short", "Long", "ISO8601"
- dateOnSecs: "Year", // "No", "Year", "Weekday", LEGACY: true/false
- weekDay: true,
- calWeek: true,
- upperCase: true,
- vectorFont: true,
-});
-*/
-
-// OR (also for development purposes)
-/*
-require('Storage').erase(SETTINGSFILE);
-*/
-
-// Load settings
-function loadSettings() {
- // Helper function default setting
- function def (value, def) {return value !== undefined ? value : def;}
-
- var settings = require('Storage').readJSON(SETTINGSFILE, true) || {};
- secondsMode = def(settings.secondsMode, "Never");
- secondsColoured = def(settings.secondsColoured, true);
- secondsWithColon = def(settings.secondsWithColon, true);
- dateOnMain = def(settings.dateOnMain, "Long");
- dateOnSecs = def(settings.dateOnSecs, "Year");
- weekDay = def(settings.weekDay, true);
- calWeek = def(settings.calWeek, false);
- upperCase = def(settings.upperCase, true);
- vectorFont = def(settings.vectorFont, false);
-
- // Legacy
- if (dateOnSecs === true)
- dateOnSecs = "Year";
- if (dateOnSecs === false)
- dateOnSecs = "No";
-}
-
-// schedule a draw for the next second or minute
-function queueDraw() {
+ // queue next draw
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
- }, queueMillis - (Date.now() % queueMillis));
-}
+ }, 60000 - (Date.now() % 60000));
+};
-function updateState() {
- if (Bangle.isLCDOn()) {
- if ((secondsMode === "Unlocked" && !Bangle.isLocked()) || secondsMode === "Always") {
- secondsScreen = true;
- queueMillis = 1000;
- } else {
- secondsScreen = false;
- queueMillis = 60000;
- }
- draw(); // draw immediately, queue redraw
- } else { // stop draw timer
+// Show launcher when middle button pressed
+Bangle.setUI({
+ mode : "clock",
+ remove : function() {
+ // Called to unload all of the clock app
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
- }
-}
-
-function isoStr(date) {
- return date.getFullYear() + "-" + ("0" + (date.getMonth() + 1)).slice(-2) + "-" + ("0" + date.getDate()).slice(-2);
-}
-
-var calWeekBuffer = [false,false,false]; //buffer tz, date, week no (once calculated until other tz or date is requested)
-function ISO8601calWeek(date) { //copied from: https://gist.github.com/IamSilviu/5899269#gistcomment-3035480
- dateNoTime = date; dateNoTime.setHours(0,0,0,0);
- if (calWeekBuffer[0] === date.getTimezoneOffset() && calWeekBuffer[1] === dateNoTime) return calWeekBuffer[2];
- calWeekBuffer[0] = date.getTimezoneOffset();
- calWeekBuffer[1] = dateNoTime;
- var tdt = new Date(date.valueOf());
- var dayn = (date.getDay() + 6) % 7;
- tdt.setDate(tdt.getDate() - dayn + 3);
- var firstThursday = tdt.valueOf();
- tdt.setMonth(0, 1);
- if (tdt.getDay() !== 4) {
- tdt.setMonth(0, 1 + ((4 - tdt.getDay()) + 7) % 7);
- }
- calWeekBuffer[2] = 1 + Math.ceil((firstThursday - tdt) / 604800000);
- return calWeekBuffer[2];
-}
-
-function doColor() {
- return !isBangle1 && !Bangle.isLocked() && secondsColoured;
-}
-
-// Actually draw the watch face
-function draw() {
- var x = g.getWidth() / 2;
- var y = g.getHeight() / 2 - (secondsMode !== "Never" ? 24 : (vectorFont ? 12 : 0));
- g.reset();
- /* This is to mark the widget areas during development.
- g.setColor("#888")
- .fillRect(0, 0, g.getWidth(), 23)
- .fillRect(0, g.getHeight() - 23, g.getWidth(), g.getHeight()).reset();
- /* */
- g.clearRect(0, 24, g.getWidth(), g.getHeight() - 24); // clear whole background (w/o widgets)
- var date = new Date(); // Actually the current date, this one is shown
- var timeStr = require("locale").time(date, 1); // Hour and minute
- g.setFontAlign(0, 0).setFont("Anton").drawString(timeStr, x, y); // draw time
- if (secondsScreen) {
- y += 65;
- var secStr = (secondsWithColon ? ":" : "") + ("0" + date.getSeconds()).slice(-2);
- if (doColor())
- g.setColor(0, 0, 1);
- g.setFont("AntonSmall");
- if (dateOnSecs !== "No") { // A bit of a complex drawing with seconds on the right and date on the left
- g.setFontAlign(1, 0).drawString(secStr, g.getWidth() - (isBangle1 ? 32 : 2), y); // seconds
- y -= (vectorFont ? 15 : 13);
- x = g.getWidth() / 4 + (isBangle1 ? 12 : 4) + (secondsWithColon ? 0 : g.stringWidth(":") / 2);
- var dateStr2 = (dateOnMain === "ISO8601" ? isoStr(date) : require("locale").date(date, 1));
- var year;
- var md;
- var yearfirst;
- if (dateStr2.match(/\d\d\d\d$/)) { // formatted date ends with year
- year = (dateOnSecs === "Year" ? dateStr2.slice(-4) : require("locale").dow(date, 1));
- md = dateStr2.slice(0, -4);
- if (!md.endsWith(".")) // keep separator before the year only if it is a dot (31.12. but 31/12)
- md = md.slice(0, -1);
- yearfirst = false;
- } else { // formatted date begins with year
- if (!dateStr2.match(/^\d\d\d\d/)) // if year position cannot be detected...
- dateStr2 = isoStr(date); // ...use ISO date format instead
- year = (dateOnSecs === "Year" ? dateStr2.slice(0, 4) : require("locale").dow(date, 1));
- md = dateStr2.slice(5); // never keep separator directly after year
- yearfirst = true;
- }
- if (dateOnSecs === "Weekday" && upperCase)
- year = year.toUpperCase();
- g.setFontAlign(0, 0);
- if (vectorFont)
- g.setFont("Vector", 24);
- else
- g.setFont("6x8", 2);
- if (doColor())
- g.setColor(1, 0, 0);
- g.drawString(md, x, (yearfirst ? y + (vectorFont ? 26 : 16) : y));
- g.drawString(year, x, (yearfirst ? y : y + (vectorFont ? 26 : 16)));
- } else {
- g.setFontAlign(0, 0).drawString(secStr, x, y); // Just the seconds centered
- }
- } else { // No seconds screen: Show date and optionally day of week
- y += (vectorFont ? 50 : (secondsMode !== "Never") ? 52 : 40);
- var dateStr = (dateOnMain === "ISO8601" ? isoStr(date) : require("locale").date(date, (dateOnMain === "Long" ? 0 : 1)));
- if (upperCase)
- dateStr = dateStr.toUpperCase();
- g.setFontAlign(0, 0);
- if (vectorFont)
- g.setFont("Vector", 24);
- else
- g.setFont("6x8", 2);
- g.drawString(dateStr, x, y);
- if (calWeek || weekDay) {
- var dowcwStr = "";
- if (calWeek)
- dowcwStr = " #" + ("0" + ISO8601calWeek(date)).slice(-2);
- if (weekDay)
- dowcwStr = require("locale").dow(date, calWeek ? 1 : 0) + dowcwStr; //weekDay e.g. Monday or weekDayShort # e.g. Mon #01
- else //week #01
- dowcwStr = /*LANG*/"week" + dowcwStr;
- if (upperCase)
- dowcwStr = dowcwStr.toUpperCase();
- g.drawString(dowcwStr, x, y + (vectorFont ? 26 : 16));
- }
- }
-
- // queue next draw
- queueDraw();
-}
-
-// Init the settings of the app
-loadSettings();
-// Clear the screen once, at startup
-g.clear();
-// Set dynamic state and perform initial drawing
-updateState();
-// Register hooks for LCD on/off event and screen lock on/off event
-Bangle.on('lcdPower', on => {
- updateState();
-});
-Bangle.on('lock', on => {
- updateState();
-});
-// Show launcher when middle button pressed
-Bangle.setUI("clock");
+ delete Graphics.prototype.setFontAnton;
+ }});
// Load widgets
Bangle.loadWidgets();
-Bangle.drawWidgets();
-
-// end of file
\ No newline at end of file
+draw();
+setTimeout(Bangle.drawWidgets,0);
+}
diff --git a/apps/antonclk/app.png b/apps/antonclk/app.png
index a38093c5f..bb764d2a1 100644
Binary files a/apps/antonclk/app.png and b/apps/antonclk/app.png differ
diff --git a/apps/antonclk/metadata.json b/apps/antonclk/metadata.json
index 16bdf3aa8..b8242f11a 100644
--- a/apps/antonclk/metadata.json
+++ b/apps/antonclk/metadata.json
@@ -1,9 +1,8 @@
{
"id": "antonclk",
"name": "Anton Clock",
- "version": "0.09",
- "description": "A clock using the bold Anton font, optionally showing seconds and date in ISO-8601 format.",
- "readme":"README.md",
+ "version": "0.11",
+ "description": "A simple clock using the bold Anton font. See `Anton Clock Plus` for an enhanced version",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "clock",
@@ -12,8 +11,6 @@
"allow_emulator": true,
"storage": [
{"name":"antonclk.app.js","url":"app.js"},
- {"name":"antonclk.settings.js","url":"settings.js"},
{"name":"antonclk.img","url":"app-icon.js","evaluate":true}
- ],
- "data": [{"name":"antonclk.json"}]
+ ]
}
diff --git a/apps/antonclk/screenshot.png b/apps/antonclk/screenshot.png
index e949b8a24..9b38e90d5 100644
Binary files a/apps/antonclk/screenshot.png and b/apps/antonclk/screenshot.png differ
diff --git a/apps/antonclkplus/ChangeLog b/apps/antonclkplus/ChangeLog
new file mode 100644
index 000000000..3b0a3d8b8
--- /dev/null
+++ b/apps/antonclkplus/ChangeLog
@@ -0,0 +1,15 @@
+0.01: New App!
+0.02: Load widgets after setUI so widclk knows when to hide
+0.03: Clock now shows day of week under date.
+0.04: Clock can optionally show seconds, date optionally in ISO-8601 format, weekdays and uppercase configurable, too.
+0.05: Clock can optionally show ISO-8601 calendar weeknumber (default: Off)
+ when weekday name "Off": week #:
+ when weekday name "On": weekday name is cut at 6th position and .# is added
+0.06: fixes #1271 - wrong settings name
+ when weekday name and calendar weeknumber are on then display is #
+ week is buffered until date or timezone changes
+0.07: align default settings with app.js (otherwise the initial displayed settings will be confusing to users)
+0.08: fixed calendar weeknumber not shortened to two digits
+0.09: Use default Bangle formatter for booleans
+0.10: Use Bangle.setUI({remove:...}) to allow loading the launcher without a full reset on 2v16
+ Modified to avoid leaving functions defined when using setUI({remove:...})
diff --git a/apps/antonclk/README.md b/apps/antonclkplus/README.md
similarity index 87%
rename from apps/antonclk/README.md
rename to apps/antonclkplus/README.md
index 28a38f5fd..25b478dd9 100644
--- a/apps/antonclk/README.md
+++ b/apps/antonclkplus/README.md
@@ -1,6 +1,6 @@
-# Anton Clock - Large font digital watch with seconds and date
+# Anton Clock Plus - Large font digital watch with seconds and date
-Anton clock uses the "Anton" bold font to show the time in a clear, easily readable manner. On the Bangle.js 2, the time can be read easily even if the screen is locked and unlit.
+Anton Clock Plus uses the "Anton" bold font to show the time in a clear, easily readable manner. On the Bangle.js 2, the time can be read easily even if the screen is locked and unlit.
## Features
@@ -16,16 +16,16 @@ The basic time representation only shows hours and minutes of the current time.
## Usage
-Install Anton clock through the Bangle.js app loader.
-Configure it through the default Bangle.js configuration mechanism
+* Install Anton Clock Plus through the Bangle.js app loader.
+* Configure it through the default Bangle.js configuration mechanism
(Settings app, "Apps" menu, "Anton clock" submenu).
-If you like it, make it your default watch face
+* If you like it, make it your default watch face
(Settings app, "System" menu, "Clock" submenu, select "Anton clock").
## Configuration
-Anton clock is configured by the standard settings mechanism of Bangle.js's operating system:
-Open the "Settings" app, then the "Apps" submenu and below it the "Anton clock" menu.
+Anton Clock is configured by the standard settings mechanism of Bangle.js's operating system:
+Open the `Settings` app, then the `Apps` submenu and below it the `Anton Clock+` menu.
You configure Anton clock through several "on/off" switches in two menus.
### The main menu
diff --git a/apps/antonclkplus/app-icon.js b/apps/antonclkplus/app-icon.js
new file mode 100644
index 000000000..0c3aeb210
--- /dev/null
+++ b/apps/antonclkplus/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwgf/AH4At/l/Aofgh4DB+EAj4REQoM/AgP4AoeACIoLCg4FB4AFDCIwLCgAROgYIB8EBAoUH/gVBCIxQBCKYHBCJp9DI4ICBLJYRCn4RQEYMOR5ARDIgIRMYQZZBgARGZwZBDCKQrCgEDR5AdBUIQRJDoLXFCJD7J/xrICIQFCn4RH/4LDAoTaCCI4Ar/LLDCBfypMkCgMkyV/CJOSCIOf5IRGFwOfCJNP//JnmT588z/+pM/BYIRCk4RC/88+f/n4RCngRCz1JCIf5/nzGoQRIHwXPCIPJI4f8CJHJGQJKCCI59LCI5ZCCJ/+v/kBoM/+V/HIJrHBYJWB/JKB5x9JEYP8AQKdBpwRL841Dp41KZoTxBHYTXBWY77PCKKhJ/4/CcgMkXoQAiA="))
diff --git a/apps/antonclkplus/app.js b/apps/antonclkplus/app.js
new file mode 100644
index 000000000..409d7d487
--- /dev/null
+++ b/apps/antonclkplus/app.js
@@ -0,0 +1,238 @@
+// Clock with large digits using the "Anton" bold font
+Graphics.prototype.setFontAnton = function(scale) {
+ // Actual height 69 (68 - 0)
+ g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAA/gAAAAAAAAAAP/gAAAAAAAAAH//gAAAAAAAAB///gAAAAAAAAf///gAAAAAAAP////gAAAAAAD/////gAAAAAA//////gAAAAAP//////gAAAAH///////gAAAB////////gAAAf////////gAAP/////////gAD//////////AA//////////gAA/////////4AAA////////+AAAA////////gAAAA///////wAAAAA//////8AAAAAA//////AAAAAAA/////gAAAAAAA////4AAAAAAAA///+AAAAAAAAA///gAAAAAAAAA//wAAAAAAAAAA/8AAAAAAAAAAA/AAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//////AAAAAB///////8AAAAH////////AAAAf////////wAAA/////////4AAB/////////8AAD/////////+AAH//////////AAP//////////gAP//////////gAP//////////gAf//////////wAf//////////wAf//////////wAf//////////wA//8AAAAAB//4A//wAAAAAAf/4A//gAAAAAAP/4A//gAAAAAAP/4A//gAAAAAAP/4A//wAAAAAAf/4A///////////4Af//////////wAf//////////wAf//////////wAf//////////wAP//////////gAP//////////gAH//////////AAH//////////AAD/////////+AAB/////////8AAA/////////4AAAP////////gAAAD///////+AAAAAf//////4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/gAAAAAAAAAAP/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/AAAAAAAAAAA//AAAAAAAAAAA/+AAAAAAAAAAB/8AAAAAAAAAAD//////////gAH//////////gAP//////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/4AAAAB/gAAD//4AAAAf/gAAP//4AAAB//gAA///4AAAH//gAB///4AAAf//gAD///4AAA///gAH///4AAD///gAP///4AAH///gAP///4AAP///gAf///4AAf///gAf///4AB////gAf///4AD////gA////4AH////gA////4Af////gA////4A/////gA//wAAB/////gA//gAAH/////gA//gAAP/////gA//gAA///8//gA//gAD///w//gA//wA////g//gA////////A//gA///////8A//gA///////4A//gAf//////wA//gAf//////gA//gAf/////+AA//gAP/////8AA//gAP/////4AA//gAH/////gAA//gAD/////AAA//gAB////8AAA//gAA////wAAA//gAAP///AAAA//gAAD//8AAAA//gAAAP+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/+AAAAAD/wAAB//8AAAAP/wAAB///AAAA//wAAB///wAAB//wAAB///4AAD//wAAB///8AAH//wAAB///+AAP//wAAB///+AAP//wAAB////AAf//wAAB////AAf//wAAB////gAf//wAAB////gA///wAAB////gA///wAAB////gA///w//AAf//wA//4A//AAA//wA//gA//AAAf/wA//gB//gAAf/wA//gB//gAAf/wA//gD//wAA//wA//wH//8AB//wA///////////gA///////////gA///////////gA///////////gAf//////////AAf//////////AAP//////////AAP/////////+AAH/////////8AAH///+/////4AAD///+f////wAAA///8P////gAAAf//4H///+AAAAH//gB///wAAAAAP4AAH/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/wAAAAAAAAAA//wAAAAAAAAAP//wAAAAAAAAB///wAAAAAAAAf///wAAAAAAAH////wAAAAAAA/////wAAAAAAP/////wAAAAAB//////wAAAAAf//////wAAAAH///////wAAAA////////wAAAP////////wAAA///////H/wAAA//////wH/wAAA/////8AH/wAAA/////AAH/wAAA////gAAH/wAAA///4AAAH/wAAA//+AAAAH/wAAA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gAAAAAAAAH/4AAAAAAAAAAH/wAAAAAAAAAAH/wAAAAAAAAAAH/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB//8AAA/////+B///AAA/////+B///wAA/////+B///4AA/////+B///8AA/////+B///8AA/////+B///+AA/////+B////AA/////+B////AA/////+B////AA/////+B////gA/////+B////gA/////+B////gA/////+A////gA//gP/gAAB//wA//gf/AAAA//wA//gf/AAAAf/wA//g//AAAAf/wA//g//AAAA//wA//g//gAAA//wA//g//+AAP//wA//g////////gA//g////////gA//g////////gA//g////////gA//g////////AA//gf///////AA//gf//////+AA//gP//////+AA//gH//////8AA//gD//////4AA//gB//////wAA//gA//////AAAAAAAH////8AAAAAAAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//////gAAAAB///////+AAAAH////////gAAAf////////4AAB/////////8AAD/////////+AAH//////////AAH//////////gAP//////////gAP//////////gAf//////////wAf//////////wAf//////////wAf//////////wAf//////////4A//wAD/4AAf/4A//gAH/wAAP/4A//gAH/wAAP/4A//gAP/wAAP/4A//gAP/4AAf/4A//wAP/+AD//4A///wP//////4Af//4P//////wAf//4P//////wAf//4P//////wAf//4P//////wAP//4P//////gAP//4H//////gAH//4H//////AAH//4D/////+AAD//4D/////8AAB//4B/////4AAA//4A/////wAAAP/4AP////AAAAB/4AD///4AAAAAAAAAH/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//AAAAAAAAAAA//gAAAAAAAAAA//gAAAAAAAAAA//gAAAAAAADgA//gAAAAAAP/gA//gAAAAAH//gA//gAAAAB///gA//gAAAAP///gA//gAAAD////gA//gAAAf////gA//gAAB/////gA//gAAP/////gA//gAB//////gA//gAH//////gA//gA///////gA//gD///////gA//gf///////gA//h////////gA//n////////gA//////////gAA/////////AAAA////////wAAAA///////4AAAAA///////AAAAAA//////4AAAAAA//////AAAAAAA/////4AAAAAAA/////AAAAAAAA////8AAAAAAAA////gAAAAAAAA///+AAAAAAAAA///4AAAAAAAAA///AAAAAAAAAA//4AAAAAAAAAA/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//gB///wAAAAP//4H///+AAAA///8P////gAAB///+f////4AAD///+/////8AAH/////////+AAH//////////AAP//////////gAP//////////gAf//////////gAf//////////wAf//////////wAf//////////wA///////////wA//4D//wAB//4A//wB//gAA//4A//gA//gAAf/4A//gA//AAAf/4A//gA//gAAf/4A//wB//gAA//4A///P//8AH//4Af//////////wAf//////////wAf//////////wAf//////////wAf//////////gAP//////////gAP//////////AAH//////////AAD/////////+AAD///+/////8AAB///8f////wAAAf//4P////AAAAH//wD///8AAAAA/+AAf//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH//gAAAAAAAAB///+AA/+AAAAP////gA//wAAAf////wA//4AAB/////4A//8AAD/////8A//+AAD/////+A///AAH/////+A///AAP//////A///gAP//////A///gAf//////A///wAf//////A///wAf//////A///wAf//////A///wA///////AB//4A//4AD//AAP/4A//gAB//AAP/4A//gAA//AAP/4A//gAA/+AAP/4A//gAB/8AAP/4A//wAB/8AAf/4Af//////////wAf//////////wAf//////////wAf//////////wAf//////////wAP//////////gAP//////////gAH//////////AAH/////////+AAD/////////8AAB/////////4AAAf////////wAAAP////////AAAAB///////4AAAAAD/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/AAB/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="), 46, atob("EiAnGicnJycnJycnEw=="), 78 + (scale << 8) + (1 << 16));
+};
+
+Graphics.prototype.setFontAntonSmall = function(scale) {
+ // Actual height 53 (52 - 0)
+ g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAAAAAAAAAAAAAMAAAAAAAAD8AAAAAAAA/8AAAAAAAf/8AAAAAAH//8AAAAAB///8AAAAA////8AAAAP////8AAAD/////8AAB//////8AAf//////8AH///////4A///////+AA///////AAA//////wAAA/////8AAAA////+AAAAA////gAAAAA///4AAAAAA//8AAAAAAA//AAAAAAAA/wAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/////wAAA//////8AAB//////+AAH///////gAH///////gAP///////wAf///////4Af///////4A////////8A////////8A////////8A//AAAAD/8A/8AAAAA/8A/8AAAAA/8A/8AAAAA/8A/+AAAAB/8A////////8A////////8A////////8Af///////4Af///////4AP///////wAP///////wAH///////gAD///////AAA//////8AAAP/////wAAAAAAAAAAAAAAAAAAAAAAAfwAAAAAAAA/4AAAAAAAA/4AAAAAAAB/wAAAAAAAB/wAAAAAAAD/wAAAAAAAD/gAAAAAAAH///////8AP///////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/4AAAP8AA//4AAA/8AB//4AAH/8AH//4AAP/8AP//4AA//8AP//4AB//8Af//4AD//8Af//4AP//8A///4Af//8A///4A///8A///4D///8A//AAH///8A/8AAP///8A/8AA//+/8A/8AD//8/8A/+Af//w/8A//////g/8A/////+A/8A/////8A/8Af////4A/8Af////wA/8AP////AA/8AP///+AA/8AH///8AA/8AD///wAA/8AA///AAA/8AAP/4AAA/8AAAAAAAAAAAAAAAAAAAAAAH4AAf/gAAA/4AAf/8AAD/4AAf//AAH/4AAf//gAP/4AAf//wAP/4AAf//wAf/4AAf//4Af/4AAf//4A//4AAf//8A//4AAf//8A//4AAP//8A//A/8AB/8A/8A/8AA/8A/8B/8AA/8A/8B/8AA/8A/+D//AB/8A////////8A////////8A////////8Af///////4Af///////4Af///////wAP///////gAH//9////gAD//4///+AAB//wf//4AAAP/AH//gAAAAAAAAAAAAAAAAAAAAAAAAAAAH/wAAAAAAB//wAAAAAAP//wAAAAAD///wAAAAA////wAAAAH////wAAAB/////wAAAf/////wAAD//////wAA///////wAA/////h/wAA////wB/wAA///8AB/wAA///AAB/wAA//gAAB/wAA////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8AAAAAAB/wAAAAAAAB/wAAAAAAAB/wAAAAAAAAAAAAAAAAAAAAAAAAAAAP/4AA////4P/+AA////4P//AA////4P//gA////4P//wA////4P//wA////4P//4A////4P//4A////4P//8A////4P//8A////4P//8A/8H/AAB/8A/8H+AAA/8A/8P+AAA/8A/8P+AAA/8A/8P/gAD/8A/8P/////8A/8P/////8A/8P/////8A/8P/////4A/8H/////4A/8H/////wA/8D/////wA/8B/////gA/8A////+AA/8AP///4AAAAAB///AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////wAAAf/////8AAB///////AAH///////gAP///////wAP///////wAf///////4Af///////4A////////8A////////8A////////8A/+AH/AB/8A/8AP+AA/8A/4Af+AA/8A/8Af+AA/8A/8Af/gH/8A//4f////8A//4f////8A//4f////8Af/4f////4Af/4f////4AP/4P////wAP/4P////gAH/4H////AAD/4D///+AAB/4B///4AAAP4AP//gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8AAAAAAAA/8AAAAAAAA/8AAAAAB8A/8AAAAB/8A/8AAAAf/8A/8AAAH//8A/8AAA///8A/8AAH///8A/8AA////8A/8AD////8A/8Af////8A/8B/////8A/8P/////8A/8//////8A////////AA///////AAA//////gAAA/////4AAAA/////AAAAA////4AAAAA////AAAAAA///8AAAAAA///gAAAAAA//+AAAAAAA//wAAAAAAA/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/gD//gAAA//4P//8AAD//8f///AAH//+////gAH///////wAP///////4AP///////8Af///////8Af///////+Af///////+A////////+A//B//AB/+A/+A/+AA/+A/8Af+AA/+A/+Af+AA/+A//A//AB/+A////////+Af///////+Af///////+Af///////8Af///////8AP///////4AH///////4AH//+////wAD//+////AAA//4P//+AAAP/gH//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH//gAfgAAA///8A/8AAB///+A//AAH////A//gAH////g//wAP////g//wAf////w//4Af////w//4A/////w//8A/////w//8A/////w//8A//gP/wA/8A/8AD/wA/8A/8AD/wAf8A/8AD/gA/8A/+AH/AB/8A////////8A////////8A////////8Af///////4Af///////4Af///////wAP///////wAH///////gAD//////+AAA//////4AAAP/////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="), 46, atob("DhgeFB4eHh4eHh4eDw=="), 60 + (scale << 8) + (1 << 16));
+};
+
+{ // must be inside our own scope here so that when we are unloaded everything disappears
+ // we also define functions using 'let fn = function() {..}' for the same reason. function decls are global
+
+const SETTINGSFILE = "antonclk.json";
+const isBangle1 = (process.env.HWVERSION == 1);
+
+// variables defined from settings
+let secondsMode;
+let secondsColoured;
+let secondsWithColon;
+let dateOnMain;
+let dateOnSecs;
+let weekDay;
+let calWeek;
+let upperCase;
+let vectorFont;
+
+// dynamic variables
+let drawTimeout;
+let queueMillis = 1000;
+let secondsScreen = true;
+
+
+
+//For development purposes
+/*
+require('Storage').writeJSON(SETTINGSFILE, {
+ secondsMode: "Unlocked", // "Never", "Unlocked", "Always"
+ secondsColoured: true,
+ secondsWithColon: true,
+ dateOnMain: "Long", // "Short", "Long", "ISO8601"
+ dateOnSecs: "Year", // "No", "Year", "Weekday", LEGACY: true/false
+ weekDay: true,
+ calWeek: true,
+ upperCase: true,
+ vectorFont: true,
+});
+*/
+
+// OR (also for development purposes)
+/*
+require('Storage').erase(SETTINGSFILE);
+*/
+
+// Load settings
+let loadSettings = function() {
+ // Helper function default setting
+ function def (value, def) {return value !== undefined ? value : def;}
+
+ var settings = require('Storage').readJSON(SETTINGSFILE, true) || {};
+ secondsMode = def(settings.secondsMode, "Never");
+ secondsColoured = def(settings.secondsColoured, true);
+ secondsWithColon = def(settings.secondsWithColon, true);
+ dateOnMain = def(settings.dateOnMain, "Long");
+ dateOnSecs = def(settings.dateOnSecs, "Year");
+ weekDay = def(settings.weekDay, true);
+ calWeek = def(settings.calWeek, false);
+ upperCase = def(settings.upperCase, true);
+ vectorFont = def(settings.vectorFont, false);
+
+ // Legacy
+ if (dateOnSecs === true)
+ dateOnSecs = "Year";
+ if (dateOnSecs === false)
+ dateOnSecs = "No";
+}
+
+// schedule a draw for the next second or minute
+let queueDraw = function() {
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = setTimeout(function() {
+ drawTimeout = undefined;
+ draw();
+ }, queueMillis - (Date.now() % queueMillis));
+}
+
+let updateState = function() {
+ if (Bangle.isLCDOn()) {
+ if ((secondsMode === "Unlocked" && !Bangle.isLocked()) || secondsMode === "Always") {
+ secondsScreen = true;
+ queueMillis = 1000;
+ } else {
+ secondsScreen = false;
+ queueMillis = 60000;
+ }
+ draw(); // draw immediately, queue redraw
+ } else { // stop draw timer
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = undefined;
+ }
+}
+
+let isoStr = function(date) {
+ return date.getFullYear() + "-" + ("0" + (date.getMonth() + 1)).slice(-2) + "-" + ("0" + date.getDate()).slice(-2);
+}
+
+let calWeekBuffer = [false,false,false]; //buffer tz, date, week no (once calculated until other tz or date is requested)
+let ISO8601calWeek = function(date) { //copied from: https://gist.github.com/IamSilviu/5899269#gistcomment-3035480
+ dateNoTime = date; dateNoTime.setHours(0,0,0,0);
+ if (calWeekBuffer[0] === date.getTimezoneOffset() && calWeekBuffer[1] === dateNoTime) return calWeekBuffer[2];
+ calWeekBuffer[0] = date.getTimezoneOffset();
+ calWeekBuffer[1] = dateNoTime;
+ var tdt = new Date(date.valueOf());
+ var dayn = (date.getDay() + 6) % 7;
+ tdt.setDate(tdt.getDate() - dayn + 3);
+ var firstThursday = tdt.valueOf();
+ tdt.setMonth(0, 1);
+ if (tdt.getDay() !== 4) {
+ tdt.setMonth(0, 1 + ((4 - tdt.getDay()) + 7) % 7);
+ }
+ calWeekBuffer[2] = 1 + Math.ceil((firstThursday - tdt) / 604800000);
+ return calWeekBuffer[2];
+}
+
+let doColor = function() {
+ return !isBangle1 && !Bangle.isLocked() && secondsColoured;
+}
+
+// Actually draw the watch face
+let draw = function() {
+ var x = g.getWidth() / 2;
+ var y = g.getHeight() / 2 - (secondsMode !== "Never" ? 24 : (vectorFont ? 12 : 0));
+ g.reset();
+ /* This is to mark the widget areas during development.
+ g.setColor("#888")
+ .fillRect(0, 0, g.getWidth(), 23)
+ .fillRect(0, g.getHeight() - 23, g.getWidth(), g.getHeight()).reset();
+ /* */
+ g.clearRect(0, 24, g.getWidth(), g.getHeight() - 24); // clear whole background (w/o widgets)
+ var date = new Date(); // Actually the current date, this one is shown
+ var timeStr = require("locale").time(date, 1); // Hour and minute
+ g.setFontAlign(0, 0).setFont("Anton").drawString(timeStr, x, y); // draw time
+ if (secondsScreen) {
+ y += 65;
+ var secStr = (secondsWithColon ? ":" : "") + ("0" + date.getSeconds()).slice(-2);
+ if (doColor())
+ g.setColor(0, 0, 1);
+ g.setFont("AntonSmall");
+ if (dateOnSecs !== "No") { // A bit of a complex drawing with seconds on the right and date on the left
+ g.setFontAlign(1, 0).drawString(secStr, g.getWidth() - (isBangle1 ? 32 : 2), y); // seconds
+ y -= (vectorFont ? 15 : 13);
+ x = g.getWidth() / 4 + (isBangle1 ? 12 : 4) + (secondsWithColon ? 0 : g.stringWidth(":") / 2);
+ var dateStr2 = (dateOnMain === "ISO8601" ? isoStr(date) : require("locale").date(date, 1));
+ var year;
+ var md;
+ var yearfirst;
+ if (dateStr2.match(/\d\d\d\d$/)) { // formatted date ends with year
+ year = (dateOnSecs === "Year" ? dateStr2.slice(-4) : require("locale").dow(date, 1));
+ md = dateStr2.slice(0, -4);
+ if (!md.endsWith(".")) // keep separator before the year only if it is a dot (31.12. but 31/12)
+ md = md.slice(0, -1);
+ yearfirst = false;
+ } else { // formatted date begins with year
+ if (!dateStr2.match(/^\d\d\d\d/)) // if year position cannot be detected...
+ dateStr2 = isoStr(date); // ...use ISO date format instead
+ year = (dateOnSecs === "Year" ? dateStr2.slice(0, 4) : require("locale").dow(date, 1));
+ md = dateStr2.slice(5); // never keep separator directly after year
+ yearfirst = true;
+ }
+ if (dateOnSecs === "Weekday" && upperCase)
+ year = year.toUpperCase();
+ g.setFontAlign(0, 0);
+ if (vectorFont)
+ g.setFont("Vector", 24);
+ else
+ g.setFont("6x8", 2);
+ if (doColor())
+ g.setColor(1, 0, 0);
+ g.drawString(md, x, (yearfirst ? y + (vectorFont ? 26 : 16) : y));
+ g.drawString(year, x, (yearfirst ? y : y + (vectorFont ? 26 : 16)));
+ } else {
+ g.setFontAlign(0, 0).drawString(secStr, x, y); // Just the seconds centered
+ }
+ } else { // No seconds screen: Show date and optionally day of week
+ y += (vectorFont ? 50 : (secondsMode !== "Never") ? 52 : 40);
+ var dateStr = (dateOnMain === "ISO8601" ? isoStr(date) : require("locale").date(date, (dateOnMain === "Long" ? 0 : 1)));
+ if (upperCase)
+ dateStr = dateStr.toUpperCase();
+ g.setFontAlign(0, 0);
+ if (vectorFont)
+ g.setFont("Vector", 24);
+ else
+ g.setFont("6x8", 2);
+ g.drawString(dateStr, x, y);
+ if (calWeek || weekDay) {
+ var dowcwStr = "";
+ if (calWeek)
+ dowcwStr = " #" + ("0" + ISO8601calWeek(date)).slice(-2);
+ if (weekDay)
+ dowcwStr = require("locale").dow(date, calWeek ? 1 : 0) + dowcwStr; //weekDay e.g. Monday or weekDayShort # e.g. Mon #01
+ else //week #01
+ dowcwStr = /*LANG*/"week" + dowcwStr;
+ if (upperCase)
+ dowcwStr = dowcwStr.toUpperCase();
+ g.drawString(dowcwStr, x, y + (vectorFont ? 26 : 16));
+ }
+ }
+
+ // queue next draw
+ queueDraw();
+}
+
+// Init the settings of the app
+loadSettings();
+// Clear the screen once, at startup
+g.clear();
+// Set dynamic state and perform initial drawing
+updateState();
+// Register hooks for LCD on/off event and screen lock on/off event
+Bangle.on('lcdPower', updateState);
+Bangle.on('lock', updateState);
+// Show launcher when middle button pressed
+Bangle.setUI({
+ mode : "clock",
+ remove : function() {
+ // Called to unload all of the clock app
+ Bangle.removeListener('lcdPower', updateState);
+ Bangle.removeListener('lock', updateState);
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = undefined;
+ delete Graphics.prototype.setFontAnton;
+ delete Graphics.prototype.setFontAntonSmall;
+ }});
+// Load widgets
+Bangle.loadWidgets();
+Bangle.drawWidgets();
+}
diff --git a/apps/antonclkplus/app.png b/apps/antonclkplus/app.png
new file mode 100644
index 000000000..a38093c5f
Binary files /dev/null and b/apps/antonclkplus/app.png differ
diff --git a/apps/antonclkplus/metadata.json b/apps/antonclkplus/metadata.json
new file mode 100644
index 000000000..05c59a4fb
--- /dev/null
+++ b/apps/antonclkplus/metadata.json
@@ -0,0 +1,20 @@
+{
+ "id": "antonclkplus",
+ "name": "Anton Clock Plus",
+ "shortName": "Anton Clock+",
+ "version": "0.10",
+ "description": "A clock using the bold Anton font, optionally showing seconds and date in ISO-8601 format.",
+ "readme":"README.md",
+ "icon": "app.png",
+ "screenshots": [{"url":"screenshot.png"}],
+ "type": "clock",
+ "tags": "clock",
+ "supports": ["BANGLEJS","BANGLEJS2"],
+ "allow_emulator": true,
+ "storage": [
+ {"name":"antonclkplus.app.js","url":"app.js"},
+ {"name":"antonclkplus.settings.js","url":"settings.js"},
+ {"name":"antonclkplus.img","url":"app-icon.js","evaluate":true}
+ ],
+ "data": [{"name":"antonclkplus.json"}]
+}
diff --git a/apps/antonclkplus/screenshot.png b/apps/antonclkplus/screenshot.png
new file mode 100644
index 000000000..e949b8a24
Binary files /dev/null and b/apps/antonclkplus/screenshot.png differ
diff --git a/apps/antonclk/settings.js b/apps/antonclkplus/settings.js
similarity index 100%
rename from apps/antonclk/settings.js
rename to apps/antonclkplus/settings.js
diff --git a/apps/aptsciclk/metadata.json b/apps/aptsciclk/metadata.json
index c450d926e..77e40f843 100644
--- a/apps/aptsciclk/metadata.json
+++ b/apps/aptsciclk/metadata.json
@@ -5,6 +5,7 @@
"version": "0.08",
"description": "A clock based on the portal series",
"icon": "app.png",
+ "screenshots": [{"url":"screenshot.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS2"],
diff --git a/apps/aptsciclk/screenshot.png b/apps/aptsciclk/screenshot.png
new file mode 100644
index 000000000..4803e4b13
Binary files /dev/null and b/apps/aptsciclk/screenshot.png differ
diff --git a/apps/assistedgps/metadata.json b/apps/assistedgps/metadata.json
index 1dbc42c87..4c91dcd35 100644
--- a/apps/assistedgps/metadata.json
+++ b/apps/assistedgps/metadata.json
@@ -1,11 +1,12 @@
{
"id": "assistedgps",
- "name": "Assisted GPS Update (AGPS)",
+ "name": "Assisted GPS Updater (AGPS)",
"version": "0.03",
- "description": "Downloads assisted GPS (AGPS) data to Bangle.js 1 or 2 for faster GPS startup and more accurate fixes. **No app will be installed**, this just uploads new data to the GPS chip.",
+ "description": "Downloads assisted GPS (AGPS) data to Bangle.js for faster GPS startup and more accurate fixes. **No app will be installed**, this just uploads new data to the GPS chip.",
+ "sortorder": -1,
"icon": "app.png",
"type": "RAM",
- "tags": "tool,outdoors,agps",
+ "tags": "tool,outdoors,agps,gps,a-gps",
"supports": ["BANGLEJS","BANGLEJS2"],
"custom": "custom.html",
"customConnect": true,
diff --git a/apps/astral/ChangeLog b/apps/astral/ChangeLog
index a51c96760..747e5ac2e 100644
--- a/apps/astral/ChangeLog
+++ b/apps/astral/ChangeLog
@@ -1,3 +1,5 @@
0.01: Create astral clock app
0.02: Fixed Whirlpool galaxy RA/DA, larger compass display, fixed moonphase overlapping battery widget
0.03: Update to use Bangle.setUI instead of setWatch
+0.04: Tell clock widgets to hide.
+0.05: Added adjustment for Bangle.js magnetometer heading fix
diff --git a/apps/astral/app.js b/apps/astral/app.js
index c445463f2..a435ca9e3 100644
--- a/apps/astral/app.js
+++ b/apps/astral/app.js
@@ -767,6 +767,24 @@ function draw() {
g.clear();
current_moonphase = getMoonPhase();
+Bangle.setUI("clockupdown", btn => {
+ if (btn==0) {
+ if (!processing) {
+ if (!modeswitch) {
+ modeswitch = true;
+ if (mode == "planetary") mode = "extras";
+ else mode = "planetary";
+ }
+ else
+ modeswitch = false;
+ }
+ } else {
+ if (!processing)
+ ready_to_compute = true;
+ }
+});
+
+
// Load widgets
Bangle.loadWidgets();
Bangle.drawWidgets();
@@ -799,23 +817,6 @@ Bangle.setGPSPower(1);
// Show launcher when button pressed
Bangle.setClockMode();
-Bangle.setUI("clockupdown", btn => {
- if (btn==0) {
- if (!processing) {
- if (!modeswitch) {
- modeswitch = true;
- if (mode == "planetary") mode = "extras";
- else mode = "planetary";
- }
- else
- modeswitch = false;
- }
- } else {
- if (!processing)
- ready_to_compute = true;
- }
-});
-
setWatch(function () {
if (!astral_settings.astral_default) {
colours_switched = true;
@@ -833,7 +834,7 @@ Bangle.on('mag', function (m) {
if (isNaN(m.heading))
compass_heading = "---";
else
- compass_heading = 360 - Math.round(m.heading);
+ compass_heading = Math.round(m.heading);
// g.setColor("#000000");
// g.fillRect(160, 10, 160, 20);
g.setColor(display_colour);
diff --git a/apps/astral/metadata.json b/apps/astral/metadata.json
index 3317092db..647066a13 100644
--- a/apps/astral/metadata.json
+++ b/apps/astral/metadata.json
@@ -1,7 +1,7 @@
{
"id": "astral",
"name": "Astral Clock",
- "version": "0.03",
+ "version": "0.05",
"description": "Clock that calculates and displays Alt Az positions of all planets, Sun as well as several other astronomy targets (customizable) and current Moon phase. Coordinates are calculated by GPS & time and onscreen compass assists orienting. See Readme before using.",
"icon": "app-icon.png",
"type": "clock",
diff --git a/apps/astrocalc/ChangeLog b/apps/astrocalc/ChangeLog
index 60ef5da0a..11b2d7177 100644
--- a/apps/astrocalc/ChangeLog
+++ b/apps/astrocalc/ChangeLog
@@ -1,2 +1,4 @@
0.01: Create astrocalc app
0.02: Store last GPS lock, can be used instead of waiting for new GPS on start
+0.03: Use 'modules/suncalc.js' to avoid it being copied 8 times for different apps
+0.04: Compatibility with Bangle.js 2, get location from My Location
diff --git a/apps/astrocalc/astrocalc-app.js b/apps/astrocalc/astrocalc-app.js
index 4e7aa0b40..6629842cf 100644
--- a/apps/astrocalc/astrocalc-app.js
+++ b/apps/astrocalc/astrocalc-app.js
@@ -9,10 +9,9 @@
* Calculate the Sun and Moon positions based on watch GPS and display graphically
*/
-const SunCalc = require("suncalc.js");
+const SunCalc = require("suncalc"); // from modules folder
const storage = require("Storage");
-const LAST_GPS_FILE = "astrocalc.gps.json";
-let lastGPS = (storage.readJSON(LAST_GPS_FILE, 1) || null);
+const BANGLEJS2 = process.env.HWVERSION == 2; // check for bangle 2
function drawMoon(phase, x, y) {
const moonImgFiles = [
@@ -73,7 +72,7 @@ function drawTitle(key) {
*/
function drawPoint(angle, radius, color) {
const pRad = Math.PI / 180;
- const faceWidth = 80; // watch face radius
+ const faceWidth = g.getWidth()/3; // watch face radius
const centerPx = g.getWidth() / 2;
const a = angle * pRad;
@@ -141,6 +140,7 @@ function drawData(title, obj, startX, startY) {
function drawMoonPositionPage(gps, title) {
const pos = SunCalc.getMoonPosition(new Date(), gps.lat, gps.lon);
+ const moonColor = g.theme.dark ? {r: 1, g: 1, b: 1} : {r: 0, g: 0, b: 0};
const pageData = {
Azimuth: pos.azimuth.toFixed(2),
@@ -150,59 +150,61 @@ function drawMoonPositionPage(gps, title) {
};
const azimuthDegrees = parseInt(pos.azimuth * 180 / Math.PI);
- drawData(title, pageData, null, 80);
+ drawData(title, pageData, null, g.getHeight()/2 - Object.keys(pageData).length/2*20);
drawPoints();
- drawPoint(azimuthDegrees, 8, {r: 1, g: 1, b: 1});
+ drawPoint(azimuthDegrees, 8, moonColor);
let m = setWatch(() => {
let m = moonIndexPageMenu(gps);
- }, BTN3, {repeat: false, edge: "falling"});
+ }, BANGLEJS2 ? BTN : BTN3, {repeat: false, edge: "falling"});
}
function drawMoonIlluminationPage(gps, title) {
const phaseNames = [
- "New Moon", "Waxing Crescent", "First Quarter", "Waxing Gibbous",
- "Full Moon", "Waning Gibbous", "Last Quater", "Waning Crescent",
+ /*LANG*/"New Moon", /*LANG*/"Waxing Crescent", /*LANG*/"First Quarter", /*LANG*/"Waxing Gibbous",
+ /*LANG*/"Full Moon", /*LANG*/"Waning Gibbous", /*LANG*/"Last Quater", /*LANG*/"Waning Crescent",
];
const phase = SunCalc.getMoonIllumination(new Date());
+ const phaseIdx = Math.round(phase.phase*8);
const pageData = {
- Phase: phaseNames[phase.phase],
+ Phase: phaseNames[phaseIdx],
};
drawData(title, pageData, null, 35);
- drawMoon(phase.phase, g.getWidth() / 2, g.getHeight() / 2);
+ drawMoon(phaseIdx, g.getWidth() / 2, g.getHeight() / 2);
let m = setWatch(() => {
let m = moonIndexPageMenu(gps);
- }, BTN3, {repease: false, edge: "falling"});
+ }, BANGLEJS2 ? BTN : BTN3, {repease: false, edge: "falling"});
}
function drawMoonTimesPage(gps, title) {
const times = SunCalc.getMoonTimes(new Date(), gps.lat, gps.lon);
+ const moonColor = g.theme.dark ? {r: 1, g: 1, b: 1} : {r: 0, g: 0, b: 0};
const pageData = {
Rise: dateToTimeString(times.rise),
Set: dateToTimeString(times.set),
};
- drawData(title, pageData, null, 105);
+ drawData(title, pageData, null, g.getHeight()/2 - Object.keys(pageData).length/2*20 + 5);
drawPoints();
// Draw the moon rise position
const risePos = SunCalc.getMoonPosition(times.rise, gps.lat, gps.lon);
const riseAzimuthDegrees = parseInt(risePos.azimuth * 180 / Math.PI);
- drawPoint(riseAzimuthDegrees, 8, {r: 1, g: 1, b: 1});
+ drawPoint(riseAzimuthDegrees, 8, moonColor);
// Draw the moon set position
const setPos = SunCalc.getMoonPosition(times.set, gps.lat, gps.lon);
const setAzimuthDegrees = parseInt(setPos.azimuth * 180 / Math.PI);
- drawPoint(setAzimuthDegrees, 8, {r: 1, g: 1, b: 1});
+ drawPoint(setAzimuthDegrees, 8, moonColor);
let m = setWatch(() => {
let m = moonIndexPageMenu(gps);
- }, BTN3, {repease: false, edge: "falling"});
+ }, BANGLEJS2 ? BTN : BTN3, {repease: false, edge: "falling"});
}
function drawSunShowPage(gps, key, date) {
@@ -224,7 +226,7 @@ function drawSunShowPage(gps, key, date) {
Degrees: azimuthDegrees
};
- drawData(key, pageData, null, 85);
+ drawData(key, pageData, null, g.getHeight()/2 - Object.keys(pageData).length/2*20 + 5);
drawPoints();
@@ -233,7 +235,7 @@ function drawSunShowPage(gps, key, date) {
m = setWatch(() => {
m = sunIndexPageMenu(gps);
- }, BTN3, {repeat: false, edge: "falling"});
+ }, BANGLEJS2 ? BTN : BTN3, {repeat: false, edge: "falling"});
return null;
}
@@ -273,15 +275,15 @@ function moonIndexPageMenu(gps) {
},
"Times": () => {
m = E.showMenu();
- drawMoonTimesPage(gps, "Times");
+ drawMoonTimesPage(gps, /*LANG*/"Times");
},
"Position": () => {
m = E.showMenu();
- drawMoonPositionPage(gps, "Position");
+ drawMoonPositionPage(gps, /*LANG*/"Position");
},
"Illumination": () => {
m = E.showMenu();
- drawMoonIlluminationPage(gps, "Illumination");
+ drawMoonIlluminationPage(gps, /*LANG*/"Illumination");
},
"< Back": () => m = indexPageMenu(gps),
};
@@ -292,15 +294,15 @@ function moonIndexPageMenu(gps) {
function indexPageMenu(gps) {
const menu = {
"": {
- "title": "Select",
+ "title": /*LANG*/"Select",
},
- "Sun": () => {
+ /*LANG*/"Sun": () => {
m = sunIndexPageMenu(gps);
},
- "Moon": () => {
+ /*LANG*/"Moon": () => {
m = moonIndexPageMenu(gps);
},
- "< Exit": () => { load(); }
+ "< Back": () => { load(); }
};
return E.showMenu(menu);
@@ -310,79 +312,10 @@ 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 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(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);
- m = indexPageMenu(lastGPS);
- }, BTN3, {repeat: false});
- }
-
- g.flip();
-
- const DEBUG = false;
- if (DEBUG) {
- clearWatch();
-
- const gps = {
- "lat": 56.45783133333,
- "lon": -3.02188583333,
- "alt": 75.3,
- "speed": 0.070376,
- "course": NaN,
- "time":new Date(),
- "satellites": 4,
- "fix": 1
- };
-
- m = indexPageMenu(gps);
-
- return;
- }
-
- 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);
-
- m = indexPageMenu(gps);
- });
-}
-
function init() {
- Bangle.setGPSPower(1);
- drawGPSWaitPage();
+ let location = require("Storage").readJSON("mylocation.json",1)||{"lat":51.5072,"lon":0.1276,"location":"London"};
+ indexPageMenu(location);
}
let m;
-init();
\ No newline at end of file
+init();
diff --git a/apps/astrocalc/metadata.json b/apps/astrocalc/metadata.json
index 384c7fa1e..653c097da 100644
--- a/apps/astrocalc/metadata.json
+++ b/apps/astrocalc/metadata.json
@@ -1,15 +1,15 @@
{
"id": "astrocalc",
"name": "Astrocalc",
- "version": "0.02",
- "description": "Calculates interesting information on the sun and moon cycles for the current day based on your location.",
+ "version": "0.04",
+ "description": "Calculates interesting information on the sun like sunset and sunrise and moon cycles for the current day based on your location from MyLocation app",
"icon": "astrocalc.png",
- "tags": "app,sun,moon,cycles,tool,outdoors",
- "supports": ["BANGLEJS"],
+ "tags": "app,sun,moon,cycles,tool",
+ "supports": ["BANGLEJS", "BANGLEJS2"],
"allow_emulator": true,
+ "dependencies": {"mylocation":"app"},
"storage": [
{"name":"astrocalc.app.js","url":"astrocalc-app.js"},
- {"name":"suncalc.js","url":"suncalc.js"},
{"name":"astrocalc.img","url":"astrocalc-icon.js","evaluate":true},
{"name":"first-quarter.img","url":"first-quarter-icon.js","evaluate":true},
{"name":"last-quarter.img","url":"last-quarter-icon.js","evaluate":true},
diff --git a/apps/astrocalc/suncalc.js b/apps/astrocalc/suncalc.js
deleted file mode 100644
index e2beaedca..000000000
--- a/apps/astrocalc/suncalc.js
+++ /dev/null
@@ -1,328 +0,0 @@
-/*
- (c) 2011-2015, Vladimir Agafonkin
- SunCalc is a JavaScript library for calculating sun/moon position and light phases.
- https://github.com/mourner/suncalc
-*/
-
-(function () { 'use strict';
-
- // shortcuts for easier to read formulas
-
- var PI = Math.PI,
- sin = Math.sin,
- cos = Math.cos,
- tan = Math.tan,
- asin = Math.asin,
- atan = Math.atan2,
- acos = Math.acos,
- rad = PI / 180;
-
- // sun calculations are based on http://aa.quae.nl/en/reken/zonpositie.html formulas
-
-
- // date/time constants and conversions
-
- var dayMs = 1000 * 60 * 60 * 24,
- J1970 = 2440588,
- J2000 = 2451545;
-
- function toJulian(date) { return date.valueOf() / dayMs - 0.5 + J1970; }
- function fromJulian(j) { return (j + 0.5 - J1970) * dayMs; }
- function toDays(date) { return toJulian(date) - J2000; }
-
-
- // general calculations for position
-
- var e = rad * 23.4397; // obliquity of the Earth
-
- function rightAscension(l, b) { return atan(sin(l) * cos(e) - tan(b) * sin(e), cos(l)); }
- function declination(l, b) { return asin(sin(b) * cos(e) + cos(b) * sin(e) * sin(l)); }
-
- function azimuth(H, phi, dec) { return atan(sin(H), cos(H) * sin(phi) - tan(dec) * cos(phi)); }
- function altitude(H, phi, dec) { return asin(sin(phi) * sin(dec) + cos(phi) * cos(dec) * cos(H)); }
-
- function siderealTime(d, lw) { return rad * (280.16 + 360.9856235 * d) - lw; }
-
- function astroRefraction(h) {
- if (h < 0) // the following formula works for positive altitudes only.
- h = 0; // if h = -0.08901179 a div/0 would occur.
-
- // formula 16.4 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.
- // 1.02 / tan(h + 10.26 / (h + 5.10)) h in degrees, result in arc minutes -> converted to rad:
- return 0.0002967 / Math.tan(h + 0.00312536 / (h + 0.08901179));
- }
-
- // general sun calculations
-
- function solarMeanAnomaly(d) { return rad * (357.5291 + 0.98560028 * d); }
-
- function eclipticLongitude(M) {
-
- var C = rad * (1.9148 * sin(M) + 0.02 * sin(2 * M) + 0.0003 * sin(3 * M)), // equation of center
- P = rad * 102.9372; // perihelion of the Earth
-
- return M + C + P + PI;
- }
-
- function sunCoords(d) {
-
- var M = solarMeanAnomaly(d),
- L = eclipticLongitude(M);
-
- return {
- dec: declination(L, 0),
- ra: rightAscension(L, 0)
- };
- }
-
-
- var SunCalc = {};
-
-
- // calculates sun position for a given date and latitude/longitude
-
- SunCalc.getPosition = function (date, lat, lng) {
-
- var lw = rad * -lng,
- phi = rad * lat,
- d = toDays(date),
-
- c = sunCoords(d),
- H = siderealTime(d, lw) - c.ra;
-
- return {
- azimuth: azimuth(H, phi, c.dec),
- altitude: altitude(H, phi, c.dec)
- };
- };
-
-
- // sun times configuration (angle, morning name, evening name)
-
- var times = SunCalc.times = [
- [-0.833, 'sunrise', 'sunset' ],
- [ -0.3, 'sunriseEnd', 'sunsetStart' ],
- [ -6, 'dawn', 'dusk' ],
- [ -12, 'nauticalDawn', 'nauticalDusk'],
- [ -18, 'nightEnd', 'night' ],
- [ 6, 'goldenHourEnd', 'goldenHour' ]
- ];
-
- // adds a custom time to the times config
-
- SunCalc.addTime = function (angle, riseName, setName) {
- times.push([angle, riseName, setName]);
- };
-
-
- // calculations for sun times
-
- var J0 = 0.0009;
-
- function julianCycle(d, lw) { return Math.round(d - J0 - lw / (2 * PI)); }
-
- function approxTransit(Ht, lw, n) { return J0 + (Ht + lw) / (2 * PI) + n; }
- function solarTransitJ(ds, M, L) { return J2000 + ds + 0.0053 * sin(M) - 0.0069 * sin(2 * L); }
-
- function hourAngle(h, phi, d) { return acos((sin(h) - sin(phi) * sin(d)) / (cos(phi) * cos(d))); }
- function observerAngle(height) { return -2.076 * Math.sqrt(height) / 60; }
-
- // returns set time for the given sun altitude
- function getSetJ(h, lw, phi, dec, n, M, L) {
-
- var w = hourAngle(h, phi, dec),
- a = approxTransit(w, lw, n);
- return solarTransitJ(a, M, L);
- }
-
-
- // calculates sun times for a given date, latitude/longitude, and, optionally,
- // the observer height (in meters) relative to the horizon
-
- SunCalc.getTimes = function (date, lat, lng, height) {
-
- height = height || 0;
-
- var lw = rad * -lng,
- phi = rad * lat,
-
- dh = observerAngle(height),
-
- d = toDays(date),
- n = julianCycle(d, lw),
- ds = approxTransit(0, lw, n),
-
- M = solarMeanAnomaly(ds),
- L = eclipticLongitude(M),
- dec = declination(L, 0),
-
- Jnoon = solarTransitJ(ds, M, L),
-
- i, len, time, h0, Jset, Jrise;
-
-
- var result = {
- solarNoon: new Date(fromJulian(Jnoon)),
- nadir: new Date(fromJulian(Jnoon - 0.5))
- };
-
- for (i = 0, len = times.length; i < len; i += 1) {
- time = times[i];
- h0 = (time[0] + dh) * rad;
-
- Jset = getSetJ(h0, lw, phi, dec, n, M, L);
- Jrise = Jnoon - (Jset - Jnoon);
-
- result[time[1]] = new Date(fromJulian(Jrise) - (dayMs / 2));
- result[time[2]] = new Date(fromJulian(Jset) + (dayMs / 2));
- }
-
- return result;
- };
-
-
- // moon calculations, based on http://aa.quae.nl/en/reken/hemelpositie.html formulas
-
- function moonCoords(d) { // geocentric ecliptic coordinates of the moon
-
- var L = rad * (218.316 + 13.176396 * d), // ecliptic longitude
- M = rad * (134.963 + 13.064993 * d), // mean anomaly
- F = rad * (93.272 + 13.229350 * d), // mean distance
-
- l = L + rad * 6.289 * sin(M), // longitude
- b = rad * 5.128 * sin(F), // latitude
- dt = 385001 - 20905 * cos(M); // distance to the moon in km
-
- return {
- ra: rightAscension(l, b),
- dec: declination(l, b),
- dist: dt
- };
- }
-
- SunCalc.getMoonPosition = function (date, lat, lng) {
-
- var lw = rad * -lng,
- phi = rad * lat,
- d = toDays(date),
-
- c = moonCoords(d),
- H = siderealTime(d, lw) - c.ra,
- h = altitude(H, phi, c.dec),
- // formula 14.1 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.
- pa = atan(sin(H), tan(phi) * cos(c.dec) - sin(c.dec) * cos(H));
-
- h = h + astroRefraction(h); // altitude correction for refraction
-
- return {
- azimuth: azimuth(H, phi, c.dec),
- altitude: h,
- distance: c.dist,
- parallacticAngle: pa
- };
- };
-
-
- // calculations for illumination parameters of the moon,
- // based on http://idlastro.gsfc.nasa.gov/ftp/pro/astro/mphase.pro formulas and
- // Chapter 48 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.
-
- // Function updated from gist: https://gist.github.com/endel/dfe6bb2fbe679781948c
-
- SunCalc.getMoonIllumination = function (date) {
- let month = date.getMonth();
- let year = date.getFullYear();
- let day = date.getDate();
-
- let c = 0;
- let e = 0;
- let jd = 0;
- let b = 0;
-
- if (month < 3) {
- year--;
- month += 12;
- }
-
- ++month;
- c = 365.25 * year;
- e = 30.6 * month;
- jd = c + e + day - 694039.09; // jd is total days elapsed
- jd /= 29.5305882; // divide by the moon cycle
- b = parseInt(jd); // int(jd) -> b, take integer part of jd
- jd -= b; // subtract integer part to leave fractional part of original jd
- b = Math.round(jd * 8); // scale fraction from 0-8 and round
-
- if (b >= 8) b = 0; // 0 and 8 are the same so turn 8 into 0
-
- return {phase: b};
- };
-
-
- function hoursLater(date, h) {
- return new Date(date.valueOf() + h * dayMs / 24);
- }
-
- // calculations for moon rise/set times are based on http://www.stargazing.net/kepler/moonrise.html article
-
- SunCalc.getMoonTimes = function (date, lat, lng, inUTC) {
- var t = date;
- if (inUTC) t.setUTCHours(0, 0, 0, 0);
- else t.setHours(0, 0, 0, 0);
-
- var hc = 0.133 * rad,
- h0 = SunCalc.getMoonPosition(t, lat, lng).altitude - hc,
- h1, h2, rise, set, a, b, xe, ye, d, roots, x1, x2, dx;
-
- // go in 2-hour chunks, each time seeing if a 3-point quadratic curve crosses zero (which means rise or set)
- for (var i = 1; i <= 24; i += 2) {
- h1 = SunCalc.getMoonPosition(hoursLater(t, i), lat, lng).altitude - hc;
- h2 = SunCalc.getMoonPosition(hoursLater(t, i + 1), lat, lng).altitude - hc;
-
- a = (h0 + h2) / 2 - h1;
- b = (h2 - h0) / 2;
- xe = -b / (2 * a);
- ye = (a * xe + b) * xe + h1;
- d = b * b - 4 * a * h1;
- roots = 0;
-
- if (d >= 0) {
- dx = Math.sqrt(d) / (Math.abs(a) * 2);
- x1 = xe - dx;
- x2 = xe + dx;
- if (Math.abs(x1) <= 1) roots++;
- if (Math.abs(x2) <= 1) roots++;
- if (x1 < -1) x1 = x2;
- }
-
- if (roots === 1) {
- if (h0 < 0) rise = i + x1;
- else set = i + x1;
-
- } else if (roots === 2) {
- rise = i + (ye < 0 ? x2 : x1);
- set = i + (ye < 0 ? x1 : x2);
- }
-
- if (rise && set) break;
-
- h0 = h2;
- }
-
- var result = {};
-
- if (rise) result.rise = hoursLater(t, rise);
- if (set) result.set = hoursLater(t, set);
-
- if (!rise && !set) result[ye > 0 ? 'alwaysUp' : 'alwaysDown'] = true;
-
- return result;
- };
-
-
- // export as Node module / AMD module / browser variable
- if (typeof exports === 'object' && typeof module !== 'undefined') module.exports = SunCalc;
- else if (typeof define === 'function' && define.amd) define(SunCalc);
- else global.SunCalc = SunCalc;
-
-}());
diff --git a/apps/banglexercise/ChangeLog b/apps/banglexercise/ChangeLog
index 5f1d3bd7d..6cf589541 100644
--- a/apps/banglexercise/ChangeLog
+++ b/apps/banglexercise/ChangeLog
@@ -2,3 +2,4 @@
0.02: Add sit ups
Add more feedback to the user about the exercises
Clean up code
+0.03: Add software back button on main menu
diff --git a/apps/banglexercise/app.js b/apps/banglexercise/app.js
index bc6e35f07..9659ee81f 100644
--- a/apps/banglexercise/app.js
+++ b/apps/banglexercise/app.js
@@ -71,7 +71,8 @@ function showMainMenu() {
let menu;
menu = {
"": {
- title: "BanglExercise"
+ title: "BanglExercise",
+ back: load
}
};
@@ -381,4 +382,5 @@ Bangle.on('HRM', function(hrm) {
});
g.clear(1);
+Bangle.loadWidgets();
showMainMenu();
diff --git a/apps/banglexercise/metadata.json b/apps/banglexercise/metadata.json
index 9bb93f112..f4ce1894b 100644
--- a/apps/banglexercise/metadata.json
+++ b/apps/banglexercise/metadata.json
@@ -1,7 +1,7 @@
{ "id": "banglexercise",
"name": "BanglExercise",
"shortName":"BanglExercise",
- "version":"0.02",
+ "version":"0.03",
"description": "Can automatically track exercises while wearing the Bangle.js watch.",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
diff --git a/apps/barclock/ChangeLog b/apps/barclock/ChangeLog
index ba44ecef8..88f4eaf00 100644
--- a/apps/barclock/ChangeLog
+++ b/apps/barclock/ChangeLog
@@ -12,3 +12,5 @@
0.12: Add settings to hide date,widgets
0.13: Add font setting
0.14: Use ClockFace_menu.addItems
+0.15: Add Power saving option
+0.16: Support Fast Loading
diff --git a/apps/barclock/README.md b/apps/barclock/README.md
index ff66a5cbb..28572e37c 100644
--- a/apps/barclock/README.md
+++ b/apps/barclock/README.md
@@ -7,4 +7,5 @@ A simple digital clock showing seconds as a horizontal bar.
## Settings
* `Show date`: display date at the bottom of screen
-* `Font`: choose between bitmap or vector fonts
\ No newline at end of file
+* `Font`: choose between bitmap or vector fonts
+* `Power saving`: (Bangle.js 2 only) don't draw the seconds bar while the watch is locked
\ No newline at end of file
diff --git a/apps/barclock/clock-bar.js b/apps/barclock/clock-bar.js
index 61ce07dfb..f2499189b 100644
--- a/apps/barclock/clock-bar.js
+++ b/apps/barclock/clock-bar.js
@@ -1,105 +1,128 @@
/* jshint esversion: 6 */
-/**
- * A simple digital clock showing seconds as a bar
- **/
+{
+ /**
+ * A simple digital clock showing seconds as a bar
+ **/
// Check settings for what type our clock should be
-let locale = require("locale");
-{ // add some more info to locale
- let date = new Date();
- date.setFullYear(1111);
- 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)!=="");
-}
-
-function renderBar(l) {
- if (!this.fraction) {
- // zero-size fillRect stills draws one line of pixels, we don't want that
- return;
+ let locale = require("locale");
+ { // add some more info to locale
+ let date = new Date();
+ date.setFullYear(1111);
+ 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)!=="");
}
- const width = this.fraction*l.w;
- g.fillRect(l.x, l.y, l.x+width-1, l.y+l.height-1);
-}
-
-function timeText(date) {
- if (!clock.is12Hour) {
- return locale.time(date, true);
+ let barW = 0, prevX = 0;
+ const renderBar = function (l) {
+ "ram";
+ if (l) prevX = 0; // called from Layout: drawing area was cleared
+ else l = clock.layout.bar;
+ let x2 = l.x+barW;
+ if (clock.powerSave && Bangle.isLocked()) x2 = 0; // hide bar
+ if (x2===prevX) return; // nothing to do
+ if (x2===0) x2--; // don't leave 1px line
+ if (x212) {
- date12.setHours(hours-12);
+
+ const timeText = function(date) {
+ if (!clock.is12Hour) {
+ return locale.time(date, true);
+ }
+ const date12 = new Date(date.getTime());
+ const hours = date12.getHours();
+ if (hours===0) {
+ date12.setHours(12);
+ } else if (hours>12) {
+ date12.setHours(hours-12);
+ }
+ return locale.time(date12, true);
}
- return locale.time(date12, true);
-}
-function ampmText(date) {
- return (clock.is12Hour && locale.hasMeridian) ? locale.meridian(date) : "";
-}
-function dateText(date) {
- const dayName = locale.dow(date, true),
- month = locale.month(date, true),
- day = date.getDate();
- const dayMonth = locale.dayFirst ? `${day} ${month}` : `${month} ${day}`;
- return `${dayName} ${dayMonth}`;
-}
+ const ampmText = date => (clock.is12Hour && locale.hasMeridian) ? locale.meridian(date) : "";
+ const dateText = date => {
+ const dayName = locale.dow(date, true),
+ month = locale.month(date, true),
+ day = date.getDate();
+ const dayMonth = locale.dayFirst ? `${day} ${month}` : `${month} ${day}`;
+ return `${dayName} ${dayMonth}`;
+ };
-
-const ClockFace = require("ClockFace"),
- clock = new ClockFace({
- precision:1,
- settingsFile:'barclock.settings.json',
- init: function() {
- const Layout = require("Layout");
- this.layout = new Layout({
- type: "v", c: [
- {
- type: "h", c: [
- {id: "time", label: "88:88", type: "txt", font: "6x8:5", col:g.theme.fg, bgCol: g.theme.bg}, // updated below
- {id: "ampm", label: " ", type: "txt", font: "6x8:2", col:g.theme.fg, bgCol: g.theme.bg},
- ],
- },
- {id: "bar", type: "custom", fraction: 0, fillx: 1, height: 6, col: g.theme.fg2, render: renderBar},
- this.showDate ? {height: 40} : {},
- this.showDate ? {id: "date", type: "txt", font: "10%", valign: 1} : {},
- ],
- }, {lazy: true});
- // adjustments based on screen size and whether we display am/pm
- let thickness; // bar thickness, same as time font "pixel block" size
- if (this.is12Hour && locale.hasMeridian) {
- // Maximum font size = ( - ) / (5chars * 6px)
- thickness = Math.floor((Bangle.appRect.w-24)/(5*6));
- } else {
- this.layout.ampm.label = "";
- thickness = Math.floor(Bangle.appRect.w/(5*6));
- }
- this.layout.bar.height = thickness+1;
- if (this.font===1) { // vector
- const B2 = process.env.HWVERSION>1;
+ const ClockFace = require("ClockFace"),
+ clock = new ClockFace({
+ precision: 1,
+ settingsFile: "barclock.settings.json",
+ init: function() {
+ const Layout = require("Layout");
+ this.layout = new Layout({
+ type: "v", c: [
+ {
+ type: "h", c: [
+ {id: "time", label: "88:88", type: "txt", font: "6x8:5", col: g.theme.fg, bgCol: g.theme.bg}, // updated below
+ {id: "ampm", label: " ", type: "txt", font: "6x8:2", col: g.theme.fg, bgCol: g.theme.bg},
+ ],
+ },
+ {id: "bar", type: "custom", fillx: 1, height: 6, col: g.theme.fg2, render: renderBar},
+ this.showDate ? {height: 40} : {},
+ this.showDate ? {id: "date", type: "txt", font: "10%", valign: 1} : {},
+ ],
+ }, {lazy: true});
+ // adjustments based on screen size and whether we display am/pm
+ let thickness; // bar thickness, same as time font "pixel block" size
if (this.is12Hour && locale.hasMeridian) {
- this.layout.time.font = "Vector:"+(B2 ? 50 : 60);
- this.layout.ampm.font = "Vector:"+(B2 ? 20 : 40);
+ // Maximum font size = ( - ) / (5chars * 6px)
+ thickness = Math.floor((Bangle.appRect.w-24)/(5*6));
} else {
- this.layout.time.font = "Vector:"+(B2 ? 60 : 80);
+ this.layout.ampm.label = "";
+ thickness = Math.floor(Bangle.appRect.w/(5*6));
}
- } else {
- this.layout.time.font = "6x8:"+thickness;
- }
- this.layout.update();
- },
- update: function(date, c) {
- if (c.m) this.layout.time.label = timeText(date);
- if (c.h) this.layout.ampm.label = ampmText(date);
- if (c.d && this.showDate) this.layout.date.label = dateText(date);
- const SECONDS_PER_MINUTE = 60;
- if (c.s) this.layout.bar.fraction = date.getSeconds()/SECONDS_PER_MINUTE;
- this.layout.render();
- },
- resume: function() {
- this.layout.forgetLazyState();
- },
- });
-clock.start();
+ let bar = this.layout.bar;
+ bar.height = thickness+1;
+ if (this.font===1) { // vector
+ const B2 = process.env.HWVERSION>1;
+ if (this.is12Hour && locale.hasMeridian) {
+ this.layout.time.font = "Vector:"+(B2 ? 50 : 60);
+ this.layout.ampm.font = "Vector:"+(B2 ? 20 : 40);
+ } else {
+ this.layout.time.font = "Vector:"+(B2 ? 60 : 80);
+ }
+ } else {
+ this.layout.time.font = "6x8:"+thickness;
+ }
+ this.layout.update();
+ bar.y2 = bar.y+bar.height-1;
+ },
+ update: function(date, c) {
+ "ram";
+ if (c.m) this.layout.time.label = timeText(date);
+ if (c.h) this.layout.ampm.label = ampmText(date);
+ if (c.d && this.showDate) this.layout.date.label = dateText(date);
+ if (c.m) this.layout.render();
+ if (c.s) {
+ barW = Math.round(date.getSeconds()/60*this.layout.bar.w);
+ renderBar();
+ }
+ },
+ resume: function() {
+ prevX = 0; // force redraw of bar
+ this.layout.forgetLazyState();
+ },
+ remove: function() {
+ if (this.onLock) Bangle.removeListener("lock", this.onLock);
+ },
+ });
+
+ // power saving: only update once a minute while locked, hide bar
+ if (clock.powerSave) {
+ clock.onLock = lock => {
+ clock.precision = lock ? 60 : 1;
+ clock.tick();
+ renderBar(); // hide/redraw bar right away
+ }
+ Bangle.on("lock", clock.onLock);
+ }
+
+ clock.start();
+}
\ No newline at end of file
diff --git a/apps/barclock/metadata.json b/apps/barclock/metadata.json
index 0c227dc52..785c228b0 100644
--- a/apps/barclock/metadata.json
+++ b/apps/barclock/metadata.json
@@ -1,7 +1,7 @@
{
"id": "barclock",
"name": "Bar Clock",
- "version": "0.14",
+ "version": "0.16",
"description": "A simple digital clock showing seconds as a bar",
"icon": "clock-bar.png",
"screenshots": [{"url":"screenshot.png"},{"url":"screenshot_pm.png"}],
diff --git a/apps/barclock/settings.js b/apps/barclock/settings.js
index dfe25581c..7b88b7021 100644
--- a/apps/barclock/settings.js
+++ b/apps/barclock/settings.js
@@ -17,10 +17,14 @@
onchange: v => save("font", v),
},
};
- require("ClockFace_menu").addItems(menu, save, {
+ let items = {
showDate: s.showDate,
loadWidgets: s.loadWidgets,
- });
-
+ };
+ // Power saving for Bangle.js 1 doesn't make sense (no updates while screen is off anyway)
+ if (process.env.HWVERSION>1) {
+ items.powerSave = s.powerSave;
+ }
+ require("ClockFace_menu").addItems(menu, save, items);
E.showMenu(menu);
});
diff --git a/apps/barcode/ChangeLog b/apps/barcode/ChangeLog
index 4f99f15ac..7ab5d8587 100644
--- a/apps/barcode/ChangeLog
+++ b/apps/barcode/ChangeLog
@@ -7,3 +7,4 @@
0.07: Step count resets at midnight
0.08: Step count stored in memory to survive reloads. Now shows step count daily and since last reboot.
0.09: NOW it really should reset daily (instead of every other day...)
+0.10: Tell clock widgets to hide.
diff --git a/apps/barcode/barcode.app.js b/apps/barcode/barcode.app.js
index 89419f33c..0d9df78d5 100644
--- a/apps/barcode/barcode.app.js
+++ b/apps/barcode/barcode.app.js
@@ -416,13 +416,13 @@ var layout = new Layout( {
// Clear the screen once, at startup
g.clear();
+Bangle.setUI("clock");
Bangle.loadWidgets();
Bangle.drawWidgets();
-Bangle.setUI("clock");
layout.render();
Bangle.on('lock', function(locked) {
if(!locked) {
layout.render();
}
-});
\ No newline at end of file
+});
diff --git a/apps/barcode/metadata.json b/apps/barcode/metadata.json
index cef267b2b..3f6bf06e6 100644
--- a/apps/barcode/metadata.json
+++ b/apps/barcode/metadata.json
@@ -2,7 +2,7 @@
"name": "Barcode clock",
"shortName":"Barcode clock",
"icon": "barcode.icon.png",
- "version":"0.09",
+ "version":"0.10",
"description": "EAN-8 compatible barcode clock.",
"tags": "barcode,ean,ean-8,watchface,clock,clockface",
"type": "clock",
diff --git a/apps/barometer/ChangeLog b/apps/barometer/ChangeLog
index de3a5cb96..b429dda17 100644
--- a/apps/barometer/ChangeLog
+++ b/apps/barometer/ChangeLog
@@ -1,3 +1,4 @@
0.01: Display pressure as number and hand
0.02: Use theme color
0.03: workaround for some firmwares that return 'undefined' for first call to barometer
+0.04: Update every second, go back with short button press
diff --git a/apps/barometer/app.js b/apps/barometer/app.js
index 77d4c974f..7e793af4f 100644
--- a/apps/barometer/app.js
+++ b/apps/barometer/app.js
@@ -59,6 +59,7 @@ function drawTicks(){
function drawScaleLabels(){
g.setColor(g.theme.fg);
g.setFont("Vector",12);
+ g.setFontAlign(-1,-1);
let label = MIN;
for (let i=0;i <= NUMBER_OF_LABELS; i++){
@@ -103,22 +104,29 @@ function drawIcons() {
}
g.setBgColor(g.theme.bg);
-g.clear();
-
-drawTicks();
-drawScaleLabels();
-drawIcons();
try {
function baroHandler(data) {
- if (data===undefined) // workaround for https://github.com/espruino/BangleApps/issues/1429
- setTimeout(() => Bangle.getPressure().then(baroHandler), 500);
- else
+ g.clear();
+
+ drawTicks();
+ drawScaleLabels();
+ drawIcons();
+ if (data!==undefined) {
drawHand(Math.round(data.pressure));
+ }
}
Bangle.getPressure().then(baroHandler);
+ setInterval(() => Bangle.getPressure().then(baroHandler), 1000);
} catch(e) {
- print(e.message);
- print("barometer not supporter, show a demo value");
+ if (e !== undefined) {
+ print(e.message);
+ }
+ print("barometer not supported, show a demo value");
drawHand(MIN);
}
+
+Bangle.setUI({
+ mode : "custom",
+ back : function() {load();}
+});
diff --git a/apps/barometer/metadata.json b/apps/barometer/metadata.json
index a385f2be2..767fa630b 100644
--- a/apps/barometer/metadata.json
+++ b/apps/barometer/metadata.json
@@ -1,7 +1,7 @@
{ "id": "barometer",
"name": "Barometer",
"shortName":"Barometer",
- "version":"0.03",
+ "version":"0.04",
"description": "A simple barometer that displays the current air pressure",
"icon": "barometer.png",
"tags": "tool,outdoors",
diff --git a/apps/barwatch/ChangeLog b/apps/barwatch/ChangeLog
new file mode 100644
index 000000000..7f837e50e
--- /dev/null
+++ b/apps/barwatch/ChangeLog
@@ -0,0 +1 @@
+0.01: First version
diff --git a/apps/barwatch/README.md b/apps/barwatch/README.md
new file mode 100644
index 000000000..c37caa6e4
--- /dev/null
+++ b/apps/barwatch/README.md
@@ -0,0 +1,5 @@
+# BarWatch - an experimental watch
+
+For too long the watches have shown the time with digits or hands. No more!
+With this stylish watch the time is represented by bars. Up to 24 as the day goes by.
+Practical? Not really, but a different look!
\ No newline at end of file
diff --git a/apps/barwatch/app-icon.js b/apps/barwatch/app-icon.js
new file mode 100644
index 000000000..82416ee28
--- /dev/null
+++ b/apps/barwatch/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("l0uwkE/4A/AH4A/AB0gicQmUB+EPgEigExh8gj8A+ECAgMQn4WCgcACyotWC34W/C34W/CycACw0wgYWFBYIWCAAc/+YGHCAgNFACkxl8hGYwAMLYUvCykQC34WycoIW/C34W0gAWTmUjkUzkbmSAFY="))
\ No newline at end of file
diff --git a/apps/barwatch/app.js b/apps/barwatch/app.js
new file mode 100644
index 000000000..e0ed15ce6
--- /dev/null
+++ b/apps/barwatch/app.js
@@ -0,0 +1,76 @@
+// timeout used to update every minute
+var drawTimeout;
+
+// schedule a draw for the next minute
+function queueDraw() {
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = setTimeout(function() {
+ drawTimeout = undefined;
+ draw();
+ }, 60000 - (Date.now() % 60000));
+}
+
+
+function draw() {
+ g.reset();
+
+ if(g.theme.dark){
+ g.setColor(1,1,1);
+ }else{
+ g.setColor(0,0,0);
+ }
+
+ // work out how to display the current time
+ var d = new Date();
+ var h = d.getHours(), m = d.getMinutes();
+
+ // hour bars
+ var bx_offset = 10, by_offset = 35;
+ var b_width = 8, b_height = 60;
+ var b_space = 5;
+
+ for(var i=0; i 11){
+ by_offset = 105;
+ }
+ var iter = i % 12;
+ //console.log(iter);
+ g.fillRect(bx_offset+(b_width*(iter+1))+(b_space*iter),
+ by_offset,
+ bx_offset+(b_width*iter)+(b_space*iter),
+ by_offset+b_height);
+ }
+
+ // minute bar
+ if(h > 11){
+ by_offset = 105;
+ }
+ var m_bar = h % 12;
+ if(m != 0){
+ g.fillRect(bx_offset+(b_width*(m_bar+1))+(b_space*m_bar),
+ by_offset+b_height-m,
+ bx_offset+(b_width*m_bar)+(b_space*m_bar),
+ by_offset+b_height);
+ }
+
+ // queue draw in one minute
+ queueDraw();
+}
+
+// Clear the screen once, at startup
+g.clear();
+// draw immediately at first
+draw();
+// Stop updates when LCD is off, restart when on
+Bangle.on('lcdPower',on=>{
+ if (on) {
+ draw(); // draw immediately, queue redraw
+ } else { // stop draw timer
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = undefined;
+ }
+});
+
+Bangle.setUI("clock");
+Bangle.loadWidgets();
+Bangle.drawWidgets();
diff --git a/apps/barwatch/app.png b/apps/barwatch/app.png
new file mode 100644
index 000000000..134de9424
Binary files /dev/null and b/apps/barwatch/app.png differ
diff --git a/apps/barwatch/metadata.json b/apps/barwatch/metadata.json
new file mode 100644
index 000000000..adcd44107
--- /dev/null
+++ b/apps/barwatch/metadata.json
@@ -0,0 +1,18 @@
+{
+ "id": "barwatch",
+ "name": "BarWatch",
+ "shortName":"BarWatch",
+ "version":"0.01",
+ "description": "A watch that displays the time using bars. One bar for each hour.",
+ "readme": "README.md",
+ "icon": "screenshot.png",
+ "tags": "clock",
+ "type": "clock",
+ "allow_emulator":true,
+ "screenshots" : [ { "url": "screenshot.png" } ],
+ "supports" : ["BANGLEJS2"],
+ "storage": [
+ {"name":"barwatch.app.js","url":"app.js"},
+ {"name":"barwatch.img","url":"app-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/barwatch/screenshot.png b/apps/barwatch/screenshot.png
new file mode 100644
index 000000000..305138252
Binary files /dev/null and b/apps/barwatch/screenshot.png differ
diff --git a/apps/batclock/ChangeLog b/apps/batclock/ChangeLog
index e6e21b146..2a2d91b74 100644
--- a/apps/batclock/ChangeLog
+++ b/apps/batclock/ChangeLog
@@ -1,2 +1,3 @@
0.01: App Created!
0.02: Update to use Bangle.setUI instead of setWatch
+0.03: Tell clock widgets to hide.
diff --git a/apps/batclock/bat-clock.app.js b/apps/batclock/bat-clock.app.js
index 31b8f5b9b..dc649160f 100644
--- a/apps/batclock/bat-clock.app.js
+++ b/apps/batclock/bat-clock.app.js
@@ -249,6 +249,9 @@ g.clear();
g.setColor(0, 0.5, 0).drawImage(bg_crack);
g.setColor(1, 1, 1).drawImage(batman);
+// Show launcher when button pressed
+Bangle.setUI("clock");
+
Bangle.loadWidgets();
Bangle.drawWidgets();
@@ -256,5 +259,3 @@ Bangle.drawWidgets();
timeInterval = setInterval(showTime, 1000);
showTime();
-// Show launcher when button pressed
-Bangle.setUI("clock");
diff --git a/apps/batclock/metadata.json b/apps/batclock/metadata.json
index 8aa115780..e6520cb90 100644
--- a/apps/batclock/metadata.json
+++ b/apps/batclock/metadata.json
@@ -2,7 +2,7 @@
"id": "batclock",
"name": "Bat Clock",
"shortName": "Bat Clock",
- "version": "0.02",
+ "version": "0.03",
"description": "Morphing Clock, with an awesome \"The Dark Knight\" themed logo.",
"icon": "bat-clock.png",
"screenshots": [{"url":"screenshot.png"}],
diff --git a/apps/bclock/ChangeLog b/apps/bclock/ChangeLog
index 5b2cf598c..79c198431 100644
--- a/apps/bclock/ChangeLog
+++ b/apps/bclock/ChangeLog
@@ -1,2 +1,3 @@
0.02: Modified for use with new bootloader and firmware
0.03: Update to use Bangle.setUI instead of setWatch
+0.04: Tell clock widgets to hide.
diff --git a/apps/bclock/clock-binary.js b/apps/bclock/clock-binary.js
index fdf945ee6..c08a7abe6 100644
--- a/apps/bclock/clock-binary.js
+++ b/apps/bclock/clock-binary.js
@@ -100,10 +100,12 @@ Bangle.on('lcdPower', on => {
if (on) drawClock();
});
+// Show launcher when button pressed
+Bangle.setUI("clock");
+
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
setInterval(() => { drawClock(); }, 1000);
drawClock();
-// Show launcher when button pressed
-Bangle.setUI("clock");
+
diff --git a/apps/bclock/metadata.json b/apps/bclock/metadata.json
index 94219a30b..c6a24d89f 100644
--- a/apps/bclock/metadata.json
+++ b/apps/bclock/metadata.json
@@ -1,7 +1,7 @@
{
"id": "bclock",
"name": "Binary Clock",
- "version": "0.03",
+ "version": "0.04",
"description": "A simple binary clock watch face",
"icon": "clock-binary.png",
"type": "clock",
diff --git a/apps/beer/ChangeLog b/apps/beer/ChangeLog
new file mode 100644
index 000000000..21ec45242
--- /dev/null
+++ b/apps/beer/ChangeLog
@@ -0,0 +1,3 @@
+0.01: New App!
+0.02: Added adjustment for Bangle.js magnetometer heading fix
+ Bangle.js 2 compatibility
diff --git a/apps/beer/app-icon.js b/apps/beer/app-icon.js
index c700b3bd2..734985cb5 100644
--- a/apps/beer/app-icon.js
+++ b/apps/beer/app-icon.js
@@ -1 +1 @@
-require("heatshrink").decompress(atob("mEwghC/AB0O/4AG8AXNgYXHmAXl94XH+AXNn4XH/wXW+YX/C6oWHAAIXN7sz9vdAAoXN9sznvuAAXf/vuC53jC4Xd7wXQ93jn3u9vv9vt7wXT/4tBAgIXQ7wvCC4PgC5sO6czIQJfBC6PumaPDC6wwCC50NYAJcBVgIDBCxrAFbgYXP7yoDF6TADL4YXPVAIXCRyAXC7wXW9zwBC6cNC9zABC4gWQC653CR4fQC6x3TF6gXXI4M9d6wAEC9EN73dAAZfQgczAAkwC/4XXAH4"))
+require("heatshrink").decompress(atob("mEw4cA///wH9/++1P+u3//3/qv/gv+KHkJkmABxcBBwNJkmQCJYOByQCCCBUCCItJkARQkgQHggLBku25IRDJQ4LCtu27Mt2RKJCInbAQIRLpYROglt24OB6wSC7dwLQ4LB9u2EgfbsARJ8u2mwRO+u3CNJtHCJFpCINALJoRCpCiGBoMSdQcpegIRGyaPB+QRDkARIyQRBc4YRKyet23iCJxHB6QRBzOJCJ+dCJY1CpfMGphrCp2YNZlL54CBEZgLBAQoRBiTFFCNMvmQRPndiEcJHEyQQECJMpAYIRQyARQwAROI4IAGB4wCBNAoRmhIRHCA4A/AAo"))
diff --git a/apps/beer/custom.html b/apps/beer/custom.html
index a357ab378..f0895f93f 100644
--- a/apps/beer/custom.html
+++ b/apps/beer/custom.html
@@ -127,6 +127,8 @@
var img_nofix = require("heatshrink").decompress(atob("mUyxH+ACYhJDygtYGsqLVF8u02gziGBoyhQ5gwDGRozRGCQydGCgybGCwyZC5gAaGPQwnGRAwpGQ4xwGFYyFDKsrlYxYDCsBmUyg4yXLyUsFwMyq1WAgUsNCRjUmVXroAEq8yMbcllkskwCEkplDmQwDq0sC54xEHQ9RqQAGqIwCFgOBAASYBSgMBltRAA0sgJsOGJeBxAAGwMrgIXIloxOJYNSvl8CwIDCqMBlYxNC4wxQDIOCwVYDIIDBGJ9YwV8rADBwRJCSqAVCAYaVMC4oxCPYYxQSo4xMSpIxPY4T5HY54XIMbIxKgwXKfKjhEllWGJNWlgXJGLNXruCGI+CrtXGKP+GJB9HMZ6VO/wxJcI8lfJclfKAxKfJEAGJIXLGKSvBWYQZCMZbfEqTHBGJYyFfIo1DGJ4tDGJQwCGJB9IMZyVNGIYyEfJQxPfJgwEMgoZJgAxMltRAA0tGJQyEksslkmAQklGINXxDTBFwIDCq8rC4YACC4gwJMowAJldWAAwwBABowIGJ4AYGJIymGBQylGBgyjGBwyhGCAzeF6YycGCwzYF7IzVF7o1PDqYA=="));
var img_fix = require("heatshrink").decompress(atob("mUyxH+ACYhJDygtYGsqLVF94zaDYkq6wAOlQyYJo2A63VAAIoC2m0GI16My5/H5/V64ABGQIwBGQ+rTKwWHkhiBGIYwDGQ3VZioVIqoiBGAJhEGRFPGSYTIYwQxCGA4yFqodJGKeqSgQwJGQmkGKQSJfAYwLGQfPDxQwRgHVfAi/EAA4xLGQwRLYwb5BABoxQCBcA43G5wABAgIAMEBgxQ0QxB54xB5gAG4xgBBYOiGJ4PMGInPGIhcCGIt4EJoxPvHM5oxBGAnO6xrCGoXMqgxdpwxD5qQFL4QADlQxdgAhBGILIDMYoADEBwwPgCHBfQzHDAAb4NACTIIAA74OACLIIMo7GOACQoBZAoHBHQPNA4QwggGiZBA5B54HBY0DIKMYtUGMMqFYLIGY4jGhZAr6FAAYwiZAgxIY0TIFfQgADvAfR/zISGJTGR/wxRkj6CGJBiSGKL6DGP4xOGSKVDGAwxRGAQxU5oxcGR75DGJEkGCYxPlXM5vPGA/MlQxUGR1OGIL4I5lOGCgyOqgxBShHMqgwVGJt4GJd4GKwyMvHG5vGABAxMGBQyM1mtABWsGC4yLGBYABGDAyKGKwwQGZKVUF6b/OABowWGbAvZGaovdGp4dTA"));
+var W = g.getWidth(), H = g.getHeight();
+
// https://github.com/Leaflet/Leaflet/blob/master/src/geo/projection/Projection.SphericalMercator.js
function project(latlong) {
var d = Math.PI / 180,
@@ -170,32 +172,30 @@ Bangle.on('GPS', function(f) {
Bangle.on('mag', function(m) {
if (!Bangle.isLCDOn()) return;
- var headingrad = m.heading*Math.PI/180; // in radians
+ var headingrad = (360-m.heading)*Math.PI/180; // in radians
if (!isFinite(headingrad)) headingrad=0;
if (nearest)
- g.drawImage(img_fix,120,120,{
+ g.drawImage(img_fix,W/2,H/2,{
rotate: (Math.PI/2)+headingrad-nearestangle,
scale:3,
});
else
- g.drawImage(img_nofix,120,120,{
+ g.drawImage(img_nofix,W/2,H/2,{
rotate: headingrad,
scale:2,
});
- g.clearRect(60,0,180,24);
- g.setFontAlign(0,0);
- g.setFont("6x8");
+ g.clearRect(0,0,W,24).setFontAlign(0,0).setFont("6x8");
if (fix.fix) {
- g.drawString(nearest ? nearest.name : "---",120,4);
+ g.drawString(nearest ? nearest.name : "---",W/2,4);
g.setFont("6x8",2);
- g.drawString(nearest ? Math.round(nearestdist)+"m" : "---",120,16);
+ g.drawString(nearest ? Math.round(nearestdist)+"m" : "---",W/2,16);
} else {
- g.drawString(fix.satellites+" satellites",120,4);
+ g.drawString(fix.satellites+" satellites",W/2,4);
}
});
Bangle.setCompassPower(1);
Bangle.setGPSPower(1);
-g.clear();`;
+g.setColor("#fff").setBgColor("#000").clear();`;
sendCustomizedApp({
storage:[
diff --git a/apps/beer/metadata.json b/apps/beer/metadata.json
index cf69aee90..3a2421bd1 100644
--- a/apps/beer/metadata.json
+++ b/apps/beer/metadata.json
@@ -1,11 +1,11 @@
{
"id": "beer",
"name": "Beer Compass",
- "version": "0.01",
+ "version": "0.02",
"description": "Uploads all the pubs in an area onto your watch, so it can always point you at the nearest one",
"icon": "app.png",
"tags": "",
- "supports": ["BANGLEJS"],
+ "supports": ["BANGLEJS","BANGLEJS2"],
"custom": "custom.html",
"storage": [
{"name":"beer.app.js"},
diff --git a/apps/bigdclock/ChangeLog b/apps/bigdclock/ChangeLog
index 09cc978fb..c92d139bb 100644
--- a/apps/bigdclock/ChangeLog
+++ b/apps/bigdclock/ChangeLog
@@ -3,3 +3,5 @@
0.03: Internationalisation; bug fix - battery icon responds promptly to charging state
0.04: bug fix
0.05: proper fix for the race condition in queueDraw()
+0.06: Tell clock widgets to hide.
+0.07: Better battery graphic - now has green, yellow and red sections; battery status reflected in the bar across the middle of the screen; current battery state checked only once every 15 minutes, leading to longer-lasting battery charge
diff --git a/apps/bigdclock/bigdclock.app.js b/apps/bigdclock/bigdclock.app.js
index c013c6188..a8e2b38df 100644
--- a/apps/bigdclock/bigdclock.app.js
+++ b/apps/bigdclock/bigdclock.app.js
@@ -11,6 +11,8 @@ Graphics.prototype.setFontOpenSans = function(scale) {
};
var drawTimeout;
+var lastBattCheck = 0;
+var width = 0;
function queueDraw(millis_now) {
if (drawTimeout) clearTimeout(drawTimeout);
@@ -24,12 +26,15 @@ function draw() {
var date = new Date();
var h = date.getHours(),
m = date.getMinutes();
- var d = date.getDate(),
- w = date.getDay(); // d=1..31; w=0..6
- const level = E.getBattery();
- const width = level + (level/2);
+ var d = date.getDate();
var is12Hour = (require("Storage").readJSON("setting.json", 1) || {})["12hour"];
- var dows = require("date_utils").dows(0,1);
+ var dow = require("date_utils").dows(0,1)[date.getDay()];
+
+ if ((date.getTime() >= lastBattCheck + 15*60000) || Bangle.isCharging()) {
+ lastBattcheck = date.getTime();
+ width = E.getBattery();
+ width += width/2;
+ }
g.reset();
g.clear();
@@ -47,24 +52,35 @@ function draw() {
g.drawString(d, g.getWidth() -6, 98);
g.setFont('Vector', 52);
g.setFontAlign(-1, -1);
- g.drawString(dows[w].slice(0,2).toUpperCase(), 6, 103);
+ g.drawString(dow.slice(0,2).toUpperCase(), 6, 103);
g.fillRect(9,159,166,171);
g.fillRect(167,163,170,167);
if (Bangle.isCharging()) {
g.setColor(1,1,0);
- } else if (level > 40) {
- g.setColor(0,1,0);
+ g.fillRect(12,162,12+width,168);
} else {
g.setColor(1,0,0);
+ g.fillRect(12,162,57,168);
+ g.setColor(1,1,0);
+ g.fillRect(58,162,72,168);
+ g.setColor(0,1,0);
+ g.fillRect(73,162,162,168);
}
- g.fillRect(12,162,12+width,168);
- if (level < 100) {
+ if (width < 150) {
g.setColor(g.theme.bg);
g.fillRect(12+width+1,162,162,168);
}
- g.setColor(0, 1, 0);
+ if (Bangle.isCharging()) {
+ g.setColor(1,1,0);
+ } else if (width <= 45) {
+ g.setColor(1,0,0);
+ } else if (width <= 60) {
+ g.setColor(1,1,0);
+ } else {
+ g.setColor(0, 1, 0);
+ }
g.fillRect(0, 90, g.getWidth(), 94);
// widget redraw
@@ -85,7 +101,8 @@ Bangle.on('charging', (charging) => {
draw();
});
+Bangle.setUI("clock");
+
Bangle.loadWidgets();
draw();
-Bangle.setUI("clock");
diff --git a/apps/bigdclock/metadata.json b/apps/bigdclock/metadata.json
index 7359bcf20..30352ca1a 100644
--- a/apps/bigdclock/metadata.json
+++ b/apps/bigdclock/metadata.json
@@ -1,7 +1,7 @@
{ "id": "bigdclock",
"name": "Big digit clock containing just the essentials",
"shortName":"Big digit clk",
- "version":"0.05",
+ "version":"0.07",
"description": "A clock containing just the essentials, made as easy to read as possible for those of us that need glasses. It contains the time, the day-of-week, the day-of-month, and the current battery state-of-charge.",
"icon": "bigdclock.png",
"type": "clock",
diff --git a/apps/bigdclock/screenshot.png b/apps/bigdclock/screenshot.png
index 8a12b266e..acac53ea9 100644
Binary files a/apps/bigdclock/screenshot.png and b/apps/bigdclock/screenshot.png differ
diff --git a/apps/binclock/ChangeLog b/apps/binclock/ChangeLog
index dc4ed8308..7c31cc0d3 100644
--- a/apps/binclock/ChangeLog
+++ b/apps/binclock/ChangeLog
@@ -1,3 +1,4 @@
0.01: New App!
0.02: Fixed bug where screen didn't clear so incorrect time displayed.
0.03: Update to use Bangle.setUI instead of setWatch
+0.04: Tell clock widgets to hide.
diff --git a/apps/binclock/app.js b/apps/binclock/app.js
index f8cbe8dd5..d9c74e6ce 100644
--- a/apps/binclock/app.js
+++ b/apps/binclock/app.js
@@ -164,9 +164,6 @@ Bangle.on('lcdPower',on=>{
draw(); // draw immediately
}
});
-// Load widgets
-Bangle.loadWidgets();
-Bangle.drawWidgets();
// Show launcher when button pressed
Bangle.setUI("clockupdown", btn=>{
if (btn!=1) return;
@@ -176,3 +173,6 @@ Bangle.setUI("clockupdown", btn=>{
displayTime = 0;
}
});
+// Load widgets
+Bangle.loadWidgets();
+Bangle.drawWidgets();
diff --git a/apps/binclock/metadata.json b/apps/binclock/metadata.json
index d17045868..2ca2755a6 100644
--- a/apps/binclock/metadata.json
+++ b/apps/binclock/metadata.json
@@ -2,7 +2,7 @@
"id": "binclock",
"name": "Binary Clock",
"shortName": "Binary Clock",
- "version": "0.03",
+ "version": "0.04",
"description": "A binary clock with hours and minutes. BTN1 toggles a digital clock.",
"icon": "app.png",
"type": "clock",
diff --git a/apps/binwatch/ChangeLog b/apps/binwatch/ChangeLog
index 1e54f489c..e355155b3 100644
--- a/apps/binwatch/ChangeLog
+++ b/apps/binwatch/ChangeLog
@@ -2,3 +2,5 @@
0.02: first running version for BangleJs2
0.03: corrected icon, added screen shot, extended description
0.04: corrected format of background image (raw binary)
+0.05: move setUI() up before draw() as to not have a false positive 'sanity
+check' when building on github.
diff --git a/apps/binwatch/app.js b/apps/binwatch/app.js
index 28d7a06a5..153bebb32 100644
--- a/apps/binwatch/app.js
+++ b/apps/binwatch/app.js
@@ -334,6 +334,7 @@ function setRuntimeValues(resolution) {
var hour = 0, minute = 1, second = 50;
var batVLevel = 20;
+Bangle.setUI("clock");
function draw() {
var d = new Date();
@@ -371,7 +372,6 @@ function draw() {
}
// Show launcher when button pressed
-Bangle.setUI("clock");
setRuntimeValues(g.getWidth());
g.reset().clear();
Bangle.loadWidgets();
diff --git a/apps/binwatch/metadata.json b/apps/binwatch/metadata.json
index 0b5fb2c72..0b4dbc697 100644
--- a/apps/binwatch/metadata.json
+++ b/apps/binwatch/metadata.json
@@ -3,7 +3,7 @@
"shortName":"BinWatch",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
- "version":"0.04",
+ "version":"0.05",
"supports": ["BANGLEJS2"],
"readme": "README.md",
"allow_emulator":true,
diff --git a/apps/blobclk/ChangeLog b/apps/blobclk/ChangeLog
index 9c4ef5b7b..193eb5024 100644
--- a/apps/blobclk/ChangeLog
+++ b/apps/blobclk/ChangeLog
@@ -5,3 +5,4 @@
0.04: Modified to account for changes in the behavior of Graphics.fillPoly
0.05: Slight increase to draw speed after LCD on
0.06: Update to use Bangle.setUI instead of setWatch, allow themes and different size screens
+0.07: Tell clock widgets to hide.
diff --git a/apps/blobclk/clock-blob.js b/apps/blobclk/clock-blob.js
index c84b8a1e6..d23e18ff9 100644
--- a/apps/blobclk/clock-blob.js
+++ b/apps/blobclk/clock-blob.js
@@ -99,6 +99,10 @@ function startTimers() {
Bangle.drawWidgets();
intervalRef = setInterval(redraw,1000);
}
+
+// Show launcher when button pressed
+Bangle.setUI("clock");
+
Bangle.loadWidgets();
startTimers();
Bangle.on('lcdPower',function(on) {
@@ -108,5 +112,3 @@ Bangle.on('lcdPower',function(on) {
clearTimers();
}
});
-// Show launcher when button pressed
-Bangle.setUI("clock");
diff --git a/apps/blobclk/metadata.json b/apps/blobclk/metadata.json
index 85d7deabe..3ae8de222 100644
--- a/apps/blobclk/metadata.json
+++ b/apps/blobclk/metadata.json
@@ -2,7 +2,7 @@
"id": "blobclk",
"name": "Large Digit Blob Clock",
"shortName": "Blob Clock",
- "version": "0.06",
+ "version": "0.07",
"description": "A clock with big digits",
"icon": "clock-blob.png",
"type": "clock",
diff --git a/apps/boldclk/ChangeLog b/apps/boldclk/ChangeLog
index 30ac31c61..0c6e8cb52 100644
--- a/apps/boldclk/ChangeLog
+++ b/apps/boldclk/ChangeLog
@@ -3,3 +3,4 @@
0.04: Work with themes, smaller screens
0.05: Adjust hand lengths to be within 'tick' points
0.06: Removed "wake LCD on face-up"-feature: A watch-face should not set things like "wake LCD on face-up".
+0.07: Tell clock widgets to hide.
diff --git a/apps/boldclk/bold_clock.js b/apps/boldclk/bold_clock.js
index 9d3ea0756..763530a32 100644
--- a/apps/boldclk/bold_clock.js
+++ b/apps/boldclk/bold_clock.js
@@ -130,9 +130,10 @@ Bangle.on('lcdPower', (on) => {
}
});
+// Show launcher when button pressed
+Bangle.setUI("clock");
+
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
startTimers();
-// Show launcher when button pressed
-Bangle.setUI("clock");
diff --git a/apps/boldclk/metadata.json b/apps/boldclk/metadata.json
index cf961347d..086203142 100644
--- a/apps/boldclk/metadata.json
+++ b/apps/boldclk/metadata.json
@@ -1,7 +1,7 @@
{
"id": "boldclk",
"name": "Bold Clock",
- "version": "0.06",
+ "version": "0.07",
"description": "Simple, readable and practical clock",
"icon": "bold_clock.png",
"screenshots": [{"url":"screenshot_bold.png"}],
diff --git a/apps/boot/ChangeLog b/apps/boot/ChangeLog
index a43ecf86e..780d9cc7d 100644
--- a/apps/boot/ChangeLog
+++ b/apps/boot/ChangeLog
@@ -52,3 +52,15 @@
0.46: Fix no clock found error on Bangle.js 2
0.47: Add polyfill for setUI with an object as an argument (fix regression for 2v12 devices after Layout module changed)
0.48: Workaround for BTHRM issues on Bangle.js 1 (write .boot files in chunks)
+0.49: Store first found clock as a setting to speed up further boots
+0.50: Allow setting of screen rotation
+ Remove support for 2v11 and earlier firmware
+0.51: Remove patches for 2v10 firmware (BEEPSET and setUI)
+ Add patch to ensure that compass heading is corrected on pre-2v15.68 firmware
+ Ensure clock is only fast-loaded if it doesn't contain widgets
+0.52: Ensure heading patch for pre-2v15.68 firmware applies to getCompass
+0.53: Add polyfills for pre-2v15.135 firmware for Bangle.load and Bangle.showClock
+0.54: Fix for invalid version comparison in polyfill
+0.55: Add toLocalISOString polyfill for pre-2v15 firmwares
+ Only add boot info comments if settings.bootDebug was set
+ If settings.bootDebug is set, output timing for each section of .boot0
diff --git a/apps/boot/bootloader.js b/apps/boot/bootloader.js
index 45e271f30..6e6466f48 100644
--- a/apps/boot/bootloader.js
+++ b/apps/boot/bootloader.js
@@ -1,8 +1,13 @@
// This runs after a 'fresh' boot
-var clockApp=(require("Storage").readJSON("setting.json",1)||{}).clock;
-if (clockApp) clockApp = require("Storage").read(clockApp);
-if (!clockApp) {
- clockApp = require("Storage").list(/\.info$/)
+var s = require("Storage").readJSON("setting.json",1)||{};
+/* If were being called from JS code in order to load the clock quickly (eg from a launcher)
+and the clock in question doesn't have widgets, force a normal 'load' as this will then
+reset everything and remove the widgets. */
+if (global.__FILE__ && !s.clockHasWidgets) {load();throw "Clock has no widgets, can't fast load";}
+// Otherwise continue to try and load the clock
+var _clkApp = require("Storage").read(s.clock);
+if (!_clkApp) {
+ _clkApp = require("Storage").list(/\.info$/)
.map(file => {
const app = require("Storage").readJSON(file,1);
if (app && app.type == "clock") {
@@ -11,9 +16,14 @@ if (!clockApp) {
})
.filter(x=>x)
.sort((a, b) => a.sortorder - b.sortorder)[0];
- if (clockApp)
- clockApp = require("Storage").read(clockApp.src);
+ if (_clkApp){
+ s.clock = _clkApp.src;
+ _clkApp = require("Storage").read(_clkApp.src);
+ s.clockHasWidgets = _clkApp.includes("Bangle.loadWidgets");
+ require("Storage").writeJSON("setting.json", s);
+ }
}
-if (!clockApp) clockApp=`E.showMessage("No Clock Found");setWatch(()=>{Bangle.showLauncher();}, global.BTN2||BTN, {repeat:false,edge:"falling"});`;
-eval(clockApp);
-delete clockApp;
+delete s;
+if (!_clkApp) _clkApp=`E.showMessage("No Clock Found");setWatch(()=>{Bangle.showLauncher();}, global.BTN2||BTN, {repeat:false,edge:"falling"});`;
+eval(_clkApp);
+delete _clkApp;
diff --git a/apps/boot/bootupdate.js b/apps/boot/bootupdate.js
index 4cb3c52e4..112dfeba8 100644
--- a/apps/boot/bootupdate.js
+++ b/apps/boot/bootupdate.js
@@ -1,15 +1,22 @@
/* This rewrites boot0.js based on current settings. If settings changed then it
recalculates, but this avoids us doing a whole bunch of reconfiguration most
of the time. */
+{ // execute in our own scope so we don't have to free variables...
E.showMessage(/*LANG*/"Updating boot0...");
-var s = require('Storage').readJSON('setting.json',1)||{};
-var BANGLEJS2 = process.env.HWVERSION==2; // Is Bangle.js 2
-var boot = "", bootPost = "";
+let s = require('Storage').readJSON('setting.json',1)||{};
+const BANGLEJS2 = process.env.HWVERSION==2; // Is Bangle.js 2
+const FWVERSION = parseFloat(process.env.VERSION.replace("v","").replace(/\.(\d\d)$/,".0$1"));
+const DEBUG = s.bootDebug; // we can set this to enable debugging output in boot0
+let boot = "", bootPost = "";
+if (DEBUG) {
+ boot += "var _tm=Date.now()\n";
+ bootPost += "delete _tm;";
+}
if (require('Storage').hash) { // new in 2v11 - helps ensure files haven't changed
- var CRC = E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\.boot\.js/)+E.CRC32(process.env.GIT_COMMIT);
+ let CRC = E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\.boot\.js/)+E.CRC32(process.env.GIT_COMMIT);
boot += `if (E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\\.boot\\.js/)+E.CRC32(process.env.GIT_COMMIT)!=${CRC})`;
} else {
- var CRC = E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\.boot\.js/))+E.CRC32(process.env.GIT_COMMIT);
+ let CRC = E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\.boot\.js/))+E.CRC32(process.env.GIT_COMMIT);
boot += `if (E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\\.boot\\.js/))+E.CRC32(process.env.GIT_COMMIT)!=${CRC})`;
}
boot += ` { eval(require('Storage').read('bootupdate.js')); throw "Storage Updated!"}\n`;
@@ -62,23 +69,6 @@ if (s.ble===false) boot += `if (!NRF.getSecurityStatus().connected) NRF.sleep();
if (s.timeout!==undefined) boot += `Bangle.setLCDTimeout(${s.timeout});\n`;
if (!s.timeout) boot += `Bangle.setLCDPower(1);\n`;
boot += `E.setTimeZone(${s.timezone});`;
-// Set vibrate, beep, etc IF on older firmwares
-if (!Bangle.F_BEEPSET) {
- if (!s.vibrate) boot += `Bangle.buzz=Promise.resolve;\n`
- if (s.beep===false) boot += `Bangle.beep=Promise.resolve;\n`
- else if (s.beep=="vib" && !BANGLEJS2) boot += `Bangle.beep = function (time, freq) {
- return new Promise(function(resolve) {
- if ((0|freq)<=0) freq=4000;
- if ((0|time)<=0) time=200;
- if (time>5000) time=5000;
- analogWrite(D13,0.1,{freq:freq});
- setTimeout(function() {
- digitalWrite(D13,0);
- resolve();
- }, time);
- });
- };\n`;
-}
// Draw out of memory errors onto the screen
boot += `E.on('errorFlag', function(errorFlags) {
g.reset(1).setColor("#ff0000").setFont("6x8").setFontAlign(0,1).drawString(errorFlags,g.getWidth()/2,g.getHeight()-1).flip();
@@ -92,82 +82,37 @@ if (s.options) boot+=`Bangle.setOptions(${E.toJS(s.options)});\n`;
if (s.brightness && s.brightness!=1) boot+=`Bangle.setLCDBrightness(${s.brightness});\n`;
if (s.passkey!==undefined && s.passkey.length==6) boot+=`NRF.setSecurity({passkey:${E.toJS(s.passkey.toString())}, mitm:1, display:1});\n`;
if (s.whitelist) boot+=`NRF.on('connect', function(addr) { if (!(require('Storage').readJSON('setting.json',1)||{}).whitelist.includes(addr)) NRF.disconnect(); });\n`;
-// Pre-2v10 firmwares without a theme/setUI
-delete g.theme; // deleting stops us getting confused by our own decl. builtins can't be deleted
-if (!g.theme) {
- boot += `g.theme={fg:-1,bg:0,fg2:-1,bg2:7,fgH:-1,bgH:0x02F7,dark:true};\n`;
-}
-try {
- Bangle.setUI({}); // In 2v12.xx we added the option for mode to be an object - for 2v12 and earlier, add a fix if it fails with an object supplied
-} catch(e) {
- boot += `Bangle._setUI = Bangle.setUI;
-Bangle.setUI=function(mode, cb) {
- if (Bangle.uiRemove) {
- Bangle.uiRemove();
- delete Bangle.uiRemove;
- }
- if ("object"==typeof mode) {
- // TODO: handle mode.back?
- mode = mode.mode;
- }
- Bangle._setUI(mode, cb);
-};\n`;
-}
-delete E.showScroller; // deleting stops us getting confused by our own decl. builtins can't be deleted
-if (!E.showScroller) { // added in 2v11 - this is a limited functionality polyfill
- boot += `E.showScroller = (function(a){function n(){g.reset();b>=l+c&&(c=1+b-l);bm||m>=a.c)break;var f=24+d*a.h;a.draw(m,{x:0,y:f,w:h,h:a.h});d+c==b&&g.setColor(g.theme.fg).drawRect(0,f,h-1,f+a.h-1).drawRect(1,f+1,h-2,f+a.h-2)}g.setColor(c?g.theme.fg:g.theme.bg);g.fillPoly([e,6,e-14,20,e+14,20]);g.setColor(a.c>l+c?g.theme.fg:g.theme.bg);g.fillPoly([e,k-7,e-14,k-21,e+14,k-21])}if(!a)return Bangle.setUI();var b=0,c=0,h=g.getWidth(),
-k=g.getHeight(),e=h/2,l=Math.floor((k-48)/a.h);g.reset().clearRect(0,24,h-1,k-1);n();Bangle.setUI("updown",d=>{d?(b+=d,0>b&&(b=a.c-1),b>=a.c&&(b=0),n()):a.select(b)})});\n`;
-}
-delete g.imageMetrics; // deleting stops us getting confused by our own decl. builtins can't be deleted
-if (!g.imageMetrics) { // added in 2v11 - this is a limited functionality polyfill
- boot += `Graphics.prototype.imageMetrics=function(src) {
- if (src[0]) return {width:src[0],height:src[1]};
- else if ('object'==typeof src) return {
- width:("width" in src) ? src.width : src.getWidth(),
- height:("height" in src) ? src.height : src.getHeight()};
- var im = E.toString(src);
- return {width:im.charCodeAt(0), height:im.charCodeAt(1)};
-};\n`;
-}
-delete g.stringMetrics; // deleting stops us getting confused by our own decl. builtins can't be deleted
-if (!g.stringMetrics) { // added in 2v11 - this is a limited functionality polyfill
- boot += `Graphics.prototype.stringMetrics=function(txt) {
- txt = txt.toString().split("\\n");
- return {width:Math.max.apply(null,txt.map(x=>g.stringWidth(x))), height:this.getFontHeight()*txt.length};
-};\n`;
-}
-delete g.wrapString; // deleting stops us getting confused by our own decl. builtins can't be deleted
-if (!g.wrapString) { // added in 2v11 - this is a limited functionality polyfill
- boot += `Graphics.prototype.wrapString=function(str, maxWidth) {
- var lines = [];
- for (var unwrappedLine of str.split("\\n")) {
- var words = unwrappedLine.split(" ");
- var line = words.shift();
- for (var word of words) {
- if (g.stringWidth(line + " " + word) > maxWidth) {
- lines.push(line);
- line = word;
- } else {
- line += " " + word;
- }
- }
- lines.push(line);
- }
- return lines;
-};\n`;
-}
-delete Bangle.appRect; // deleting stops us getting confused by our own decl. builtins can't be deleted
-if (!Bangle.appRect) { // added in 2v11 - polyfill for older firmwares
- boot += `Bangle.appRect = ((y,w,h)=>({x:0,y:0,w:w,h:h,x2:w-1,y2:h-1}))(g.getWidth(),g.getHeight());
- (lw=>{ Bangle.loadWidgets = () => { lw(); Bangle.appRect = ((y,w,h)=>({x:0,y:y,w:w,h:h-y,x2:w-1,y2:h-(1+h)}))(global.WIDGETS?24:0,g.getWidth(),g.getHeight()); }; })(Bangle.loadWidgets);\n`;
-}
+if (s.rotate) boot+=`g.setRotation(${s.rotate&3},${s.rotate>>2});\n` // screen rotation
+// ================================================== FIXING OLDER FIRMWARES
+if (FWVERSION<215.068) // 2v15.68 and before had compass heading inverted.
+ boot += `Bangle.on('mag',e=>{if(!isNaN(e.heading))e.heading=360-e.heading;});
+Bangle.getCompass=(c=>(()=>{e=c();if(!isNaN(e.heading))e.heading=360-e.heading;return e;}))(Bangle.getCompass);`;
+// deleting stops us getting confused by our own decl. builtins can't be deleted
+// this is a polyfill without fastloading capability
+delete Bangle.showClock;
+if (!Bangle.showClock) boot += `Bangle.showClock = ()=>{load(".bootcde")};\n`;
+delete Bangle.load;
+if (!Bangle.load) boot += `Bangle.load = load;\n`;
+let date = new Date();
+delete date.toLocalISOString; // toLocalISOString was only introduced in 2v15
+if (!date.toLocalISOString) boot += `Date.prototype.toLocalISOString = function() {
+ var o = this.getTimezoneOffset();
+ var d = new Date(this.getTime() - o*60000);
+ var sign = o>0?"-":"+";
+ o = Math.abs(o);
+ return d.toISOString().slice(0,-1)+sign+Math.floor(o/60).toString().padStart(2,0)+(o%60).toString().padStart(2,0);
+};\n`;
+
+// show timings
+if (DEBUG) boot += `print(".boot0",0|(Date.now()-_tm),"ms");_tm=Date.now();\n`
+// ================================================== BOOT.JS
// Append *.boot.js files
// These could change bleServices/bleServiceOptions if needed
-var bootFiles = require('Storage').list(/\.boot\.js$/).sort((a,b)=>{
- var getPriority = /.*\.(\d+)\.boot\.js$/;
- var aPriority = a.match(getPriority);
- var bPriority = b.match(getPriority);
+let bootFiles = require('Storage').list(/\.boot\.js$/).sort((a,b)=>{
+ let getPriority = /.*\.(\d+)\.boot\.js$/;
+ let aPriority = a.match(getPriority);
+ let bPriority = b.match(getPriority);
if (aPriority && bPriority){
return parseInt(aPriority[1]) - parseInt(bPriority[1]);
} else if (aPriority && !bPriority){
@@ -178,14 +123,16 @@ var bootFiles = require('Storage').list(/\.boot\.js$/).sort((a,b)=>{
return a==b ? 0 : (a>b ? 1 : -1);
});
// precalculate file size
-var fileSize = boot.length + bootPost.length;
+let fileSize = boot.length + bootPost.length;
bootFiles.forEach(bootFile=>{
// match the size of data we're adding below in bootFiles.forEach
- fileSize += 2+bootFile.length+1+require('Storage').read(bootFile).length+2;
+ if (DEBUG) fileSize += 2+bootFile.length+1; // `//${bootFile}\n` comment
+ fileSize += require('Storage').read(bootFile).length+2; // boot code plus ";\n"
+ if (DEBUG) fileSize += 48+E.toJS(bootFile).length; // `print(${E.toJS(bootFile)},0|(Date.now()-_tm),"ms");_tm=Date.now();\n`
});
// write file in chunks (so as not to use up all RAM)
require('Storage').write('.boot0',boot,0,fileSize);
-var fileOffset = boot.length;
+let fileOffset = boot.length;
bootFiles.forEach(bootFile=>{
// we add a semicolon so if the file is wrapped in (function(){ ... }()
// with no semicolon we don't end up with (function(){ ... }()(function(){ ... }()
@@ -194,16 +141,18 @@ bootFiles.forEach(bootFile=>{
// "//"+bootFile+"\n"+require('Storage').read(bootFile)+";\n";
// but we need to do this without ever loading everything into RAM as some
// boot files seem to be getting pretty big now.
- require('Storage').write('.boot0',"//"+bootFile+"\n",fileOffset);
- fileOffset+=2+bootFile.length+1;
- var bf = require('Storage').read(bootFile);
+ if (DEBUG) {
+ require('Storage').write('.boot0',`//${bootFile}\n`,fileOffset);
+ fileOffset+=2+bootFile.length+1;
+ }
+ let bf = require('Storage').read(bootFile);
// we can't just write 'bf' in one go because at least in 2v13 and earlier
// Espruino wants to read the whole file into RAM first, and on Bangle.js 1
// it can be too big (especially BTHRM).
- var bflen = bf.length;
- var bfoffset = 0;
+ let bflen = bf.length;
+ let bfoffset = 0;
while (bflen) {
- var bfchunk = Math.min(bflen, 2048);
+ let bfchunk = Math.min(bflen, 2048);
require('Storage').write('.boot0',bf.substr(bfoffset, bfchunk),fileOffset);
fileOffset+=bfchunk;
bfoffset+=bfchunk;
@@ -211,15 +160,14 @@ bootFiles.forEach(bootFile=>{
}
require('Storage').write('.boot0',";\n",fileOffset);
fileOffset+=2;
+ if (DEBUG) {
+ require('Storage').write('.boot0',`print(${E.toJS(bootFile)},0|(Date.now()-_tm),"ms");_tm=Date.now();\n`,fileOffset);
+ fileOffset += 48+E.toJS(bootFile).length
+ }
});
require('Storage').write('.boot0',bootPost,fileOffset);
-
-delete boot;
-delete bootPost;
-delete bootFiles;
-delete fileSize;
-delete fileOffset;
E.showMessage(/*LANG*/"Reloading...");
-eval(require('Storage').read('.boot0'));
+}
// .bootcde should be run automatically after if required, since
// we normally get called automatically from '.boot0'
+eval(require('Storage').read('.boot0'));
diff --git a/apps/boot/metadata.json b/apps/boot/metadata.json
index 62adc4db1..455563a16 100644
--- a/apps/boot/metadata.json
+++ b/apps/boot/metadata.json
@@ -1,7 +1,7 @@
{
"id": "boot",
"name": "Bootloader",
- "version": "0.48",
+ "version": "0.55",
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
"icon": "bootloader.png",
"type": "bootloader",
diff --git a/apps/bowserWF/ChangeLog b/apps/bowserWF/ChangeLog
new file mode 100644
index 000000000..dd2b05fb3
--- /dev/null
+++ b/apps/bowserWF/ChangeLog
@@ -0,0 +1,3 @@
+...
+0.02: First update with ChangeLog Added
+0.03: updated watch face to use the ClockFace library
diff --git a/apps/bowserWF/app.js b/apps/bowserWF/app.js
index e53d945cc..956c43602 100644
--- a/apps/bowserWF/app.js
+++ b/apps/bowserWF/app.js
@@ -1,102 +1,233 @@
var sprite = {
- width : 47, height : 47, bpp : 3,
- transparent : 1,
- buffer : require("heatshrink").decompress(atob("kmSpICFn/+BAwCImV//VICJuT//SogRMpmT/2SCJtSyQDB/4RMymRkmX/gRLygDC3/piVhCJElAYf/pNIkgRIlIDCl/6pVBkIRIGwWJEYPypMJCI9KGwQRBLANIPRI2CGoPkyVCBwmeyVLTYNJom8yImBz4gEqV/6Vf+g2BPwf/IIq8C/+kyVRkgDBp/5CIX/+mkz/+y/9BIOf0v6///5LdCz+kCIOk34RBYQMSp5XBGQVk/pNBAQP/9IyBxGSv4yCk/1OIK8EC4QgEpM/JgJ+EGoIRBTApQCEYvplLOFXIIdBO4SqBeQJABGoeTDQMlk5WCAAPSYQLgEz4aBlM/9IgB/7CCcAvP/QsBiVfUwOJBgUiCIcmpAVCy/+pMAKwMkRgIRCp6VBAwW6qVOgmSgPkwgRDv53E6WSuEkyEPRgmf2VJv5HBl2SgAKBwEJRgnJiVKp/Sr/0y/yBQOQv56DKwVSv2STwO/DgWD/BADmaDByRoBYoQRCgFCCIf/+jgDNwOUAwMg/kSPQbODX4IJBAwUH8B6DsmRl5oBl7OBklMyV+gBoDycSxMpiVLZwS8EAQeYyjaByR6BBIJBDAQnEIgbFCogOFRgQDBr//I4L0EAQsxAYP//5WCGQ6MCAAKbCpKYEAQiMB//kIQOUyf+CJF/CIIEBTYOfcgQRHBQv/CJKnBpP8GRTCDJIPkGRQCB5I3C/n/EZUgA"))
+ width: 47,
+ height: 47,
+ bpp: 3,
+ transparent: 1,
+ buffer: require("heatshrink").decompress(
+ atob(
+ "kmSpICFn/+BAwCImV//VICJuT//SogRMpmT/2SCJtSyQDB/4RMymRkmX/gRLygDC3/piVhCJElAYf/pNIkgRIlIDCl/6pVBkIRIGwWJEYPypMJCI9KGwQRBLANIPRI2CGoPkyVCBwmeyVLTYNJom8yImBz4gEqV/6Vf+g2BPwf/IIq8C/+kyVRkgDBp/5CIX/+mkz/+y/9BIOf0v6///5LdCz+kCIOk34RBYQMSp5XBGQVk/pNBAQP/9IyBxGSv4yCk/1OIK8EC4QgEpM/JgJ+EGoIRBTApQCEYvplLOFXIIdBO4SqBeQJABGoeTDQMlk5WCAAPSYQLgEz4aBlM/9IgB/7CCcAvP/QsBiVfUwOJBgUiCIcmpAVCy/+pMAKwMkRgIRCp6VBAwW6qVOgmSgPkwgRDv53E6WSuEkyEPRgmf2VJv5HBl2SgAKBwEJRgnJiVKp/Sr/0y/yBQOQv56DKwVSv2STwO/DgWD/BADmaDByRoBYoQRCgFCCIf/+jgDNwOUAwMg/kSPQbODX4IJBAwUH8B6DsmRl5oBl7OBklMyV+gBoDycSxMpiVLZwS8EAQeYyjaByR6BBIJBDAQnEIgbFCogOFRgQDBr//I4L0EAQsxAYP//5WCGQ6MCAAKbCpKYEAQiMB//kIQOUyf+CJF/CIIEBTYOfcgQRHBQv/CJKnBpP8GRTCDJIPkGRQCB5I3C/n/EZUgA"
+ )
+ ),
};
const boxes = {
- width : 122, height : 56, bpp : 3,
- transparent : 1,
- buffer : require("heatshrink").decompress(atob("kmZkmSpICPwgDBmQUQAQMJAYNkFiOSiQDB5JESAYQsSpADByYsSyBZBydt23bAR+wgFJkwUQAQNggGSposR23AgMkzZESwECpM2IiUAgmSFiW2gDlBFiVsgDlBFiXYgDNBL4MDWZy2FgEGWZy2FgENWZy2EL4MbWZpTBWwZfBXJpTCWwZiCWZpTBWwZiCWZsbWwhiCWZpWCWwTORWwgXRWwgXRWwZESWwZESWwZESWwYXRWwgXRW362/W362/W362/W362/W362/W362/W362/W362/W362/W362/WwuAgazOWwsAgyzOWwsAhqzOWwhfBjazNKYK2DL4K5NKYS2DMQSzNKYK2DMQSzNja2EMQSzNKwS2CZyK2EC6K2EC6K2DIiS2DIiS2DIiUAFoMAAFTkBFtckyAtrLgWSpICnLIIsqyVAgAsqpIA="))
+ width: 122,
+ height: 56,
+ bpp: 3,
+ transparent: 1,
+ buffer: require("heatshrink").decompress(
+ atob(
+ "kmZkmSpICPwgDBmQUQAQMJAYNkFiOSiQDB5JESAYQsSpADByYsSyBZBydt23bAR+wgFJkwUQAQNggGSposR23AgMkzZESwECpM2IiUAgmSFiW2gDlBFiVsgDlBFiXYgDNBL4MDWZy2FgEGWZy2FgENWZy2EL4MbWZpTBWwZfBXJpTCWwZiCWZpTBWwZiCWZsbWwhiCWZpWCWwTORWwgXRWwgXRWwZESWwZESWwZESWwYXRWwgXRW362/W362/W362/W362/W362/W362/W362/W362/W362/W362/WwuAgazOWwsAgyzOWwsAhqzOWwhfBjazNKYK2DL4K5NKYS2DMQSzNKYK2DMQSzNja2EMQSzNKwS2CZyK2EC6K2EC6K2DIiS2DIiS2DIiUAFoMAAFTkBFtckyAtrLgWSpICnLIIsqyVAgAsqpIA="
+ )
+ ),
};
const background = {
- width : 176, height : 176, bpp : 3,
- transparent : 5,
- buffer : require("heatshrink").decompress(atob("kmSpIC/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/ATWAgEAIP1///8iRB8gf/AAOCIPdIIARBBoJB/+E4IP4ABghB9v4CB8BB5g/92//9pB7wP/97FEIO9IgDACAAn8iVBIOlHH4xBDnA+wyY9IAAmB/BB//5B/IOQ/OAARBup5B/yV/IP5B/IP5BRt5B7/wDC7aD8/w+B+3bBgP7IP5B7HYNt23/AQPfIPX/9oCC24IDINwCBIRAAHIOACBHI3+g4EC/l/4BByAQkA//wpED//4gGAhJB3pMAgQFBgEBH3AC/AX4C/AX4C/AX4C/AX4C/AUOAgBB/v//ghB9gf///gH3UgiVIIAJBBwRB5j+CIIf8uBB5//wIIXb//+hJB6o/92/7v5B7/0/97GCIPYAG4MgIP/BjkSIP34/hB//5B/AAQ+0IP5B/IP5BN7ZB97///wCBIPX93yAB2wCB+5B5tv//dt24CB35B5v/+n/t+P/I4PH8ESIO38gFA/+CgH/+EIgiD3gACCPoMAgQ+2AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/ASVIgAACgRB/IPY8GkAHBiRB/IPBLKgJB/IP5B/AQUAkmQghB/IP2AgEAyVAiRB/IP5BBpMAIP5B/IIUkgBB/IP5BpoAsBgJBOgEEIIoIBIP5BlyE27dt2EEIJ4CBBAlIgRBgpEAhu2IIO24ESQwxB/IJQhGkEJIL8GHwQCDgOweQpB/IKMkwAKJILVgAofYeQhBzsEAIKICLoESILmBQARBBtuwgZB3kA4B4ENIgJBcpMAIMYCDIOcAgEbHYgCGsEJkhEBE6cBIP5BZfYQ+JIIkDsEBIP5BVyEAIKtAHxgCDwBEBINk2IKCGCIKmSpECIP5BUkEBHyACD2BBUFoMJIP5BSpEbHyQCDIP5BXkmAIP5B/AQcAbKJB/ILH/AAP8hM/AgWSv4KCAAP+gmfAoXJk4ME//gpIEC8mTBgvwkgEC+QRDAAX4gVPAgP5kgsCLwWQh/kMIUf5LuFg4jBAoMBKAJ5EwF/AoUA/yFFoE/CI6RDgY+BCIQsDIP5B/IP5B/IP5B/IJ/AIJfghJBKv0EIJcAIJfwIP5BMhMAAAMEz5BGgmABoVJII9IBgUkII8kBgUSII8CoAMBhJB/IIsQoMAYoP/AAP4YpAMC/+BII9/BgXAYpAMC8DFIBgXwIIcCIP6DCgkQh/kCIRBIbQcBIJAFCgBBICI5BE/IRDFgQA="))
+ width: 176,
+ height: 176,
+ bpp: 3,
+ transparent: 5,
+ buffer: require("heatshrink").decompress(
+ atob(
+ "kmSpIC/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/ATWAgEAIP1///8iRB8gf/AAOCIPdIIARBBoJB/+E4IP4ABghB9v4CB8BB5g/92//9pB7wP/97FEIO9IgDACAAn8iVBIOlHH4xBDnA+wyY9IAAmB/BB//5B/IOQ/OAARBup5B/yV/IP5B/IP5BRt5B7/wDC7aD8/w+B+3bBgP7IP5B7HYNt23/AQPfIPX/9oCC24IDINwCBIRAAHIOACBHI3+g4EC/l/4BByAQkA//wpED//4gGAhJB3pMAgQFBgEBH3AC/AX4C/AX4C/AX4C/AX4C/AUOAgBB/v//ghB9gf///gH3UgiVIIAJBBwRB5j+CIIf8uBB5//wIIXb//+hJB6o/92/7v5B7/0/97GCIPYAG4MgIP/BjkSIP34/hB//5B/AAQ+0IP5B/IP5BN7ZB97///wCBIPX93yAB2wCB+5B5tv//dt24CB35B5v/+n/t+P/I4PH8ESIO38gFA/+CgH/+EIgiD3gACCPoMAgQ+2AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/ASVIgAACgRB/IPY8GkAHBiRB/IPBLKgJB/IP5B/AQUAkmQghB/IP2AgEAyVAiRB/IP5BBpMAIP5B/IIUkgBB/IP5BpoAsBgJBOgEEIIoIBIP5BlyE27dt2EEIJ4CBBAlIgRBgpEAhu2IIO24ESQwxB/IJQhGkEJIL8GHwQCDgOweQpB/IKMkwAKJILVgAofYeQhBzsEAIKICLoESILmBQARBBtuwgZB3kA4B4ENIgJBcpMAIMYCDIOcAgEbHYgCGsEJkhEBE6cBIP5BZfYQ+JIIkDsEBIP5BVyEAIKtAHxgCDwBEBINk2IKCGCIKmSpECIP5BUkEBHyACD2BBUFoMJIP5BSpEbHyQCDIP5BXkmAIP5B/AQcAbKJB/ILH/AAP8hM/AgWSv4KCAAP+gmfAoXJk4ME//gpIEC8mTBgvwkgEC+QRDAAX4gVPAgP5kgsCLwWQh/kMIUf5LuFg4jBAoMBKAJ5EwF/AoUA/yFFoE/CI6RDgY+BCIQsDIP5B/IP5B/IP5B/IJ/AIJfghJBKv0EIJcAIJfwIP5BMhMAAAMEz5BGgmABoVJII9IBgUkII8kBgUSII8CoAMBhJB/IIsQoMAYoP/AAP4YpAMC/+BII9/BgXAYpAMC8DFIBgXwIIcCIP6DCgkQh/kCIRBIbQcBIJAFCgBBICI5BE/IRDFgQA="
+ )
+ ),
};
numbersDims = {
- width: 20,
- height: 44
+ width: 20,
+ height: 44,
};
-const numbers = [
- require("heatshrink").decompress(atob("ikswcBkmSpIC/ARGQKYQIDAwUEBxMAAQNAgECpMgAQMkB4IOIAQQLCgEQBwQaBgEBB1oCBBwYCCiRWDCIRWEO5wOHAX4CnA=")),
- require("heatshrink").decompress(atob("ikswcBkmSpIC/ARNIKYIIEwEAggOKNIQODyAHCBxQsWB3TUFgMgA4sSBwzU/AVA=")),
- require("heatshrink").decompress(atob("ikswcBkmSpIC/AQ8gKggIBAwkCBw+QCIQLCgIRCDQcQBwwyDDwUSCgVAAwIOBEwI7EpI7FBw4FDghZGHwgOEF4Y+CEYQ+DBxQADNAIAFNAIOFa/4CoA=")),
- require("heatshrink").decompress(atob("ikswcBkmSpIC/AQ8gKosSAwsBBw4aCoEAgQjEBoIpEBwtIBoIUEwEAggUDBwwyDDoWQA4ZWHhIIEJQoOCgI+EBwMQEAYOJO4oLBO4oRDJQrX/AU4")),
- require("heatshrink").decompress(atob("ikswcBkmSpIC/ARNIKgQIDwAGBgQOJNQYOCyAHDBxEggB6BBwYDBiVABxIjBCIIODF4YOEAAkBV40QBwxiDNAosEB0IC/AUg")),
- require("heatshrink").decompress(atob("ikswcBkmSpIC/AQ5UFkmQAwkCBxIdGCIIIDBxAsTgAaEkEASooOBiQOVJQgOBiBKDBxMSJQwRBLIgRCBwjX/AVA=")),
- require("heatshrink").decompress(atob("ikswcBkmSpIC/ARGQKgYICAwcCBxADBiQdDkEANYoOGEAYyEHYoOIHYqfFBxIdDBAMQFgZHCBysSFgwRBO46GFa/4CnA")),
- require("heatshrink").decompress(atob("ikswcBkmSpIC/AQ5VGiAGFgIOIDQUgBwUCEYQOJGQYNBHAlADQgOHwEAggUDpANBCgYpBBwmQAwJiGhIjDB1gC/AU4A=")),
- require("heatshrink").decompress(atob("ikswcBkmSpIC/AQ8gKYYICAwcEBxGQgAaDgVJgACBDQQOJgB6CBwcAiQODHa4AEhIRBpAHDiARBwAGCgIgCFIYOCFIYOHiQrEJQxlCBwzX/AVAA=")),
- require("heatshrink").decompress(atob("ikswcBkmSpIC/AQ8gKggIBAwkCBw+QCIQLCgIRCDQcQBzkSTAsBHYoOIL4gOCMooOENAYOCoA4EBwoqDgiGGF4gOEa/4CoA=")),
+const numbers = [
+ require("heatshrink").decompress(
+ atob(
+ "ikswcBkmSpIC/ARGQKYQIDAwUEBxMAAQNAgECpMgAQMkB4IOIAQQLCgEQBwQaBgEBB1oCBBwYCCiRWDCIRWEO5wOHAX4CnA="
+ )
+ ),
+ require("heatshrink").decompress(
+ atob("ikswcBkmSpIC/ARNIKYIIEwEAggOKNIQODyAHCBxQsWB3TUFgMgA4sSBwzU/AVA=")
+ ),
+ require("heatshrink").decompress(
+ atob(
+ "ikswcBkmSpIC/AQ8gKggIBAwkCBw+QCIQLCgIRCDQcQBwwyDDwUSCgVAAwIOBEwI7EpI7FBw4FDghZGHwgOEF4Y+CEYQ+DBxQADNAIAFNAIOFa/4CoA="
+ )
+ ),
+ require("heatshrink").decompress(
+ atob(
+ "ikswcBkmSpIC/AQ8gKosSAwsBBw4aCoEAgQjEBoIpEBwtIBoIUEwEAggUDBwwyDDoWQA4ZWHhIIEJQoOCgI+EBwMQEAYOJO4oLBO4oRDJQrX/AU4"
+ )
+ ),
+ require("heatshrink").decompress(
+ atob(
+ "ikswcBkmSpIC/ARNIKgQIDwAGBgQOJNQYOCyAHDBxEggB6BBwYDBiVABxIjBCIIODF4YOEAAkBV40QBwxiDNAosEB0IC/AUg"
+ )
+ ),
+ require("heatshrink").decompress(
+ atob(
+ "ikswcBkmSpIC/AQ5UFkmQAwkCBxIdGCIIIDBxAsTgAaEkEASooOBiQOVJQgOBiBKDBxMSJQwRBLIgRCBwjX/AVA="
+ )
+ ),
+ require("heatshrink").decompress(
+ atob(
+ "ikswcBkmSpIC/ARGQKgYICAwcCBxADBiQdDkEANYoOGEAYyEHYoOIHYqfFBxIdDBAMQFgZHCBysSFgwRBO46GFa/4CnA"
+ )
+ ),
+ require("heatshrink").decompress(
+ atob(
+ "ikswcBkmSpIC/AQ5VGiAGFgIOIDQUgBwUCEYQOJGQYNBHAlADQgOHwEAggUDpANBCgYpBBwmQAwJiGhIjDB1gC/AU4A="
+ )
+ ),
+ require("heatshrink").decompress(
+ atob(
+ "ikswcBkmSpIC/AQ8gKYYICAwcEBxGQgAaDgVJgACBDQQOJgB6CBwcAiQODHa4AEhIRBpAHDiARBwAGCgIgCFIYOCFIYOHiQrEJQxlCBwzX/AVAA="
+ )
+ ),
+ require("heatshrink").decompress(
+ atob(
+ "ikswcBkmSpIC/AQ8gKggIBAwkCBw+QCIQLCgIRCDQcQBzkSTAsBHYoOIL4gOCMooOENAYOCoA4EBwoqDgiGGF4gOEa/4CoA="
+ )
+ ),
];
-digitPositions = [ // relative to the box
- {x:13, y:6}, {x:32, y:6},
- {x:74, y:6}, {x:93, y:6},
+digitPositions = [
+ // relative to the box
+ { x: 13, y: 6 },
+ { x: 32, y: 6 },
+ { x: 74, y: 6 },
+ { x: 93, y: 6 },
];
-var drawTimeout;
const animation_duration = 1; // seconds
-const animation_steps = 20;
+const animation_steps = 20;
const jump_height = 45; // top coordinate of the jump
const seconds_per_minute = 60;
-function draw() {
- const now = new Date();
- g.drawImage(background, 0, 0);
- var boxTL_x = 27; var boxTL_y = 29;
- var sprite_TL_x = 72; var sprite_TL_y = 161 - sprite.height;
- const seconds = now.getSeconds()%seconds_per_minute + now.getMilliseconds()/1000;
- const hours = now.getHours();
- const minutes = now.getMinutes();
-
- var time_advance = seconds / animation_duration;
-
- if (time_advance < 0.5) {
- sprite_TL_y += (jump_height - sprite_TL_y) * time_advance * 2;
- } else if (time_advance < 1) {
- sprite_TL_y = jump_height + (sprite_TL_y-jump_height) * (time_advance-0.5) * 2;
- }
- const box_penetration = boxTL_y + boxes.height - sprite_TL_y;
- if (box_penetration > 0) {
- boxTL_y -= box_penetration;
- }
- g.drawImage(boxes, boxTL_x, boxTL_y);
- g.drawImage(numbers[(hours / 10) >> 0], boxTL_x+digitPositions[0].x, boxTL_y+digitPositions[0].y);
- g.drawImage(numbers[(hours % 10) >> 0], boxTL_x+digitPositions[1].x, boxTL_y+digitPositions[1].y);
- g.drawImage(numbers[(minutes / 10) >> 0], boxTL_x+digitPositions[2].x, boxTL_y+digitPositions[2].y);
- g.drawImage(numbers[(minutes % 10) >> 0], boxTL_x+digitPositions[3].x, boxTL_y+digitPositions[3].y);
- g.drawImage(sprite, sprite_TL_x, sprite_TL_y);
- Bangle.drawWidgets();
-
- const timeout = time_advance <= 1?
- animation_duration / animation_steps
- : (seconds_per_minute - seconds);
- setTimeout( _=>{
- drawTimeout = undefined;
- draw();
- }, timeout * 1000);
-}
+const ClockFace = require("ClockFace");
+const clock = new ClockFace({
+ precision: 60, // just once a minute
-// Clear the screen once, at startup
-g.setTheme({bg:"#00f",fg:"#fff",dark:true}).clear();
+ init: function() {
+ // Clear the screen once, at startup
+ g.setTheme({ bg: "#00f", fg: "#fff", dark: true }).clear();
-Bangle.on('lcdPower',on=>{
- if (on) {
- draw(); // draw immediately, queue redraw
- } else { // stop draw timer
- if (drawTimeout) {
- clearTimeout(drawTimeout);
- }
- drawTimeout = undefined;
- }
+ this.drawing = true;
+
+ this.simpleDraw = function(now) {
+ var boxTL_x = 27;
+ var boxTL_y = 29;
+ var sprite_TL_x = 72;
+ var sprite_TL_y = 161 - sprite.height;
+ const seconds =
+ (now.getSeconds() % seconds_per_minute) + now.getMilliseconds() / 1000;
+ const hours =
+ this.is12Hour && now.getHours() > 12
+ ? now.getHours() - 12
+ : now.getHours();
+
+ const minutes = now.getMinutes();
+
+ g.drawImage(boxes, boxTL_x, boxTL_y);
+ g.drawImage(
+ numbers[(hours / 10) >> 0],
+ boxTL_x + digitPositions[0].x,
+ boxTL_y + digitPositions[0].y
+ );
+ g.drawImage(
+ numbers[hours % 10 >> 0],
+ boxTL_x + digitPositions[1].x,
+ boxTL_y + digitPositions[1].y
+ );
+ g.drawImage(
+ numbers[(minutes / 10) >> 0],
+ boxTL_x + digitPositions[2].x,
+ boxTL_y + digitPositions[2].y
+ );
+ g.drawImage(
+ numbers[minutes % 10 >> 0],
+ boxTL_x + digitPositions[3].x,
+ boxTL_y + digitPositions[3].y
+ );
+ };
+ },
+
+ pause: function() {
+ this.drawing = false;
+ },
+
+ resume: function() {
+ this.drawing = true;
+ },
+
+ draw: function(now) {
+ if (!this.drawing) {
+ this.simpleDraw(now);
+ return;
+ }
+ g.drawImage(background, 0, 0);
+ var boxTL_x = 27;
+ var boxTL_y = 29;
+ var sprite_TL_x = 72;
+ var sprite_TL_y = 161 - sprite.height;
+ const seconds =
+ (now.getSeconds() % seconds_per_minute) + now.getMilliseconds() / 1000;
+ const hours =
+ this.is12Hour && now.getHours() > 12
+ ? now.getHours() - 12
+ : now.getHours();
+
+ const minutes = now.getMinutes();
+
+ var time_advance = seconds / animation_duration;
+
+ if (time_advance < 0.5) {
+ sprite_TL_y += (jump_height - sprite_TL_y) * time_advance * 2;
+ } else if (time_advance < 1) {
+ sprite_TL_y =
+ jump_height + (sprite_TL_y - jump_height) * (time_advance - 0.5) * 2;
+ }
+ const box_penetration = boxTL_y + boxes.height - sprite_TL_y;
+ if (box_penetration > 0) {
+ boxTL_y -= box_penetration;
+ }
+ g.drawImage(boxes, boxTL_x, boxTL_y);
+ g.drawImage(
+ numbers[(hours / 10) >> 0],
+ boxTL_x + digitPositions[0].x,
+ boxTL_y + digitPositions[0].y
+ );
+ g.drawImage(
+ numbers[hours % 10 >> 0],
+ boxTL_x + digitPositions[1].x,
+ boxTL_y + digitPositions[1].y
+ );
+ g.drawImage(
+ numbers[(minutes / 10) >> 0],
+ boxTL_x + digitPositions[2].x,
+ boxTL_y + digitPositions[2].y
+ );
+ g.drawImage(
+ numbers[minutes % 10 >> 0],
+ boxTL_x + digitPositions[3].x,
+ boxTL_y + digitPositions[3].y
+ );
+ g.drawImage(sprite, sprite_TL_x, sprite_TL_y);
+ // Bangle.drawWidgets();
+
+ if (this.drawing) {
+ const timeout =
+ time_advance <= 1 ? animation_duration / animation_steps : -999;
+ if (timeout > 0) {
+ setTimeout((_) => {
+ this.draw(new Date());
+ }, timeout * 1000);
+ }
+ }
+ },
+
+ update: function(date, changed) {
+ if (this.drawing && changed.m) {
+ this.draw(date);
+ }
+ },
});
-// Show launcher when middle button pressed
-Bangle.setUI("clock");
-// Load widgets
-Bangle.loadWidgets();
-
-draw();
+clock.start();
diff --git a/apps/bowserWF/metadata.json b/apps/bowserWF/metadata.json
index a0bdfb8e9..bba15e5df 100644
--- a/apps/bowserWF/metadata.json
+++ b/apps/bowserWF/metadata.json
@@ -1,18 +1,18 @@
-{
+{
"id": "bowserWF",
"name": "Bowser Watchface",
- "shortName":"Bowser Watchface",
- "version":"0.02",
+ "shortName": "Bowser Watchface",
+ "version": "0.03",
"description": "Let bowser show you the time",
"icon": "app.png",
"type": "clock",
"tags": "clock",
- "supports" : ["BANGLEJS2"],
+ "supports": ["BANGLEJS2"],
"allow_emulator": true,
"readme": "README.md",
"storage": [
- {"name":"bowserWF.app.js","url":"app.js"},
- {"name":"bowserWF.img","url":"app-icon.js","evaluate":true}
+ { "name": "bowserWF.app.js", "url": "app.js" },
+ { "name": "bowserWF.img", "url": "app-icon.js", "evaluate": true }
],
- "data": [{"name":"bowserWF.json"}]
+ "data": [{ "name": "bowserWF.json" }]
}
diff --git a/apps/ncfrun/ChangeLog b/apps/bthometemp/ChangeLog
similarity index 100%
rename from apps/ncfrun/ChangeLog
rename to apps/bthometemp/ChangeLog
diff --git a/apps/bthometemp/README.md b/apps/bthometemp/README.md
new file mode 100644
index 000000000..1a8212ea4
--- /dev/null
+++ b/apps/bthometemp/README.md
@@ -0,0 +1,9 @@
+# BTHome Temperature and Pressure
+
+This app displays temperature and pressure and advertises them over bluetooth using BTHome.io standard (along with battery level)
+
+This can be used to integrate with [Home Assistant](https://www.home-assistant.io/), so you can use your Bangle as a wireless temperature/pressure sensor.
+
+More info on the standard at https://bthome.io
+
+And the data format used is https://bthome.io/format/
diff --git a/apps/bthometemp/app-icon.js b/apps/bthometemp/app-icon.js
new file mode 100644
index 000000000..e2dff3eb9
--- /dev/null
+++ b/apps/bthometemp/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEw4kA///1N6BIPf//1gMIwdE8sG2me+9Y/8C/2snXsoUNpdnzdt/xj/AH4AYgMRAAUQCyoYSCQNXs1muoFBFyHm1X//+qtwwPiMX1+YmczxP6uIwNFwN6yeDnGDmc504wNFwOpnGYC4OJweaGBsR9WTmYtBmc4GAOuC5ZGBt4SBAAQEBwf2JBcBiupnIuCmedxGTzVRC5cX1AuDnPZF4OKuIXLi3zIoedMgMzn9hC5uICQON5IDBxAXSznYC6RdDPQYXNO4JcB7pdCO56nBnGZ7p6DU5zXBXgSqDa5sAiPqIgOZd4c510RCxQXBi+pRQIXBxODzVxC5hIBvR1DnE505GMGAevzAvC/QuNGAfm1X//+qtwuOGAURq9ms11AoIWOGAQAEFw1EDBwWFggBCkUgAQMigUAAIIAJoABDCgIXQFwYXBCYYBDHAMCEAIkCFgcEAIIKCCoQFCkAhBAQIlCkAsBOoIXCBoIvEAwQTCAYI2BIwgXIF4YXDQwIVCC4YIBMIwfCAQRfGYBSPNC6TBFACgwBACouWAH4AiA="))
diff --git a/apps/bthometemp/app.js b/apps/bthometemp/app.js
new file mode 100644
index 000000000..7b55777d1
--- /dev/null
+++ b/apps/bthometemp/app.js
@@ -0,0 +1,58 @@
+// history of temperature/pressure readings
+var history = [];
+
+// When we get temperature...
+function onTemperature(p) {
+ // Average the last 5 temperature readings
+ while (history.length>4) history.shift();
+ history.push(p);
+ var avrTemp = history.reduce((i,h)=>h.temperature+i,0) / history.length;
+ var avrPressure = history.reduce((i,h)=>h.pressure+i,0) / history.length;
+ var t = require('locale').temp(avrTemp).replace("'","°");
+ // Draw
+ var rect = Bangle.appRect;
+ g.reset(1).clearRect(rect.x, rect.y, rect.x2, rect.y2);
+ var x = (rect.x+rect.x2)/2;
+ var y = (rect.y+rect.y2)/2 + 10;
+ g.setFont("6x15").setFontAlign(0,0).drawString("Temperature:", x, y - 65);
+ g.setFontVector(50).setFontAlign(0,0).drawString(t, x, y-25);
+ g.setFont("6x15").setFontAlign(0,0).drawString("Pressure:", x, y+15 );
+ g.setFont("12x20").setFontAlign(0,0).drawString(Math.round(avrPressure)+" hPa", x, y+40);
+ // Set Bluetooth Advertising
+ // https://bthome.io/format/
+ var temp100 = Math.round(avrTemp*100);
+ var pressure100 = Math.round(avrPressure*100);
+
+ Bangle.bleAdvert[0xFCD2] = [ 0x40, /* BTHome Device Information
+ bit 0: "Encryption flag"
+ bit 1-4: "Reserved for future use"
+ bit 5-7: "BTHome Version" */
+
+ 0x01, // Battery, 8 bit
+ E.getBattery(),
+
+ 0x02, // Temperature, 16 bit
+ temp100&255,temp100>>8,
+
+ 0x04, // Pressure, 16 bit
+ pressure100&255,(pressure100>>8)&255,pressure100>>16
+ ];
+ NRF.setAdvertising(Bangle.bleAdvert);
+}
+
+// Gets the temperature in the most accurate way with pressure sensor
+function drawTemperature() {
+ Bangle.getPressure().then(p =>{if (p) onTemperature(p);});
+}
+
+if (!Bangle.bleAdvert) Bangle.bleAdvert = {};
+setInterval(function() {
+ drawTemperature();
+}, 10000); // update every 10s
+Bangle.loadWidgets();
+Bangle.setUI({
+ mode : "custom",
+ back : function() {load();}
+});
+E.showMessage("Reading temperature...");
+drawTemperature();
diff --git a/apps/bthometemp/app.png b/apps/bthometemp/app.png
new file mode 100644
index 000000000..6c8eb3f14
Binary files /dev/null and b/apps/bthometemp/app.png differ
diff --git a/apps/bthometemp/metadata.json b/apps/bthometemp/metadata.json
new file mode 100644
index 000000000..4bfd08c31
--- /dev/null
+++ b/apps/bthometemp/metadata.json
@@ -0,0 +1,14 @@
+{ "id": "bthometemp",
+ "name": "BTHome Temperature and Pressure",
+ "shortName":"BTHome T",
+ "version":"0.01",
+ "description": "Displays temperature and pressure, and advertises them over bluetooth using BTHome.io standard",
+ "icon": "app.png",
+ "tags": "bthome,bluetooth,temperature",
+ "supports" : ["BANGLEJS2"],
+ "readme": "README.md",
+ "storage": [
+ {"name":"bthometemp.app.js","url":"app.js"},
+ {"name":"bthometemp.img","url":"app-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/bthrm/ChangeLog b/apps/bthrm/ChangeLog
index 00ed856d6..000c5e3f8 100644
--- a/apps/bthrm/ChangeLog
+++ b/apps/bthrm/ChangeLog
@@ -23,3 +23,21 @@
0.08: Allow scanning for devices in settings
0.09: Misc Fixes and improvements (https://github.com/espruino/BangleApps/pull/1655)
0.10: Use default Bangle formatter for booleans
+0.11: App now shows status info while connecting
+ Fixes to allow cached BluetoothRemoteGATTCharacteristic to work with 2v14.14 onwards (>1 central)
+0.12: Fix HRM fallback handling
+ Use default boolean formatter in custom menu and directly apply config if useful
+ Allow recording unmodified internal HR
+ Better connection retry handling
+0.13: Less time used during boot if disabled
+0.14: Allow bonding (Debug menu)
+ Prevent mixing of BT and internal HRM events if both are enabled
+ Always use a grace period (default 0 ms) to decouple some connection steps
+ Device not found errors now utilize increasing timeouts
+0.15: Fix recording internal sensor
+ Handle fallback to internal sensor consistently if BT bpm is 0
+ Power internal sensor down if not needed for fallback
+0.16: Set powerdownRequested correctly on BTHRM power on
+ Additional logging on errors
+ Add debug option for disabling active scanning
+0.17: New GUI based on layout library
diff --git a/apps/bthrm/README.md b/apps/bthrm/README.md
index 8d5872670..f4eaf43af 100644
--- a/apps/bthrm/README.md
+++ b/apps/bthrm/README.md
@@ -19,7 +19,14 @@ Just install the app, then install an app that uses the heart rate monitor.
Once installed you will have to go into this app's settings while your heart rate monitor
is available for bluetooth pairing and scan for devices.
-**To disable this and return to normal HRM, uninstall the app**
+**To disable this and return to normal HRM, uninstall the app or change the settings**
+
+### Modes
+
+* Off - Internal HRM is used, no attempt on connecting to BT HRM.
+* Default - Replaces internal HRM with BT HRM and falls back to internal HRM if no valid measurements received.
+* Both - The BT HRM needs to be started explicitly by an app that wants to use it. BT HRM has its own event and is completely separated from the internal HRM. Apps not supporting the BT HRM will not see the BT HRM measurements.
+* Custom - Combine low level settings as you see fit.
## Compatible Heart Rate Monitors
@@ -35,6 +42,10 @@ So far it has been tested on:
* Polar OH1
* Wahoo TICKR X 2
+## Recorder plugin
+
+The recorder plugin can record the BT HRM event (blue) and the original unchanged HRM event (green). This is mainly useful for debugging purposes or comparing the BT with the internal HRM, as the resulting "merged" HRM can be recordet using the default HRM recorder.
+
## Internals
This replaces `Bangle.setHRMPower` with its own implementation.
diff --git a/apps/bthrm/boot.js b/apps/bthrm/boot.js
index e9e640563..3e3d35737 100644
--- a/apps/bthrm/boot.js
+++ b/apps/bthrm/boot.js
@@ -1,567 +1 @@
-(function() {
- var settings = Object.assign(
- require('Storage').readJSON("bthrm.default.json", true) || {},
- require('Storage').readJSON("bthrm.json", true) || {}
- );
-
- var log = function(text, param){
- if (settings.debuglog){
- var logline = new Date().toISOString() + " - " + text;
- if (param){
- logline += " " + JSON.stringify(param);
- }
- print(logline);
- }
- };
-
- log("Settings: ", settings);
-
- if (settings.enabled){
-
- var clearCache = function() {
- return require('Storage').erase("bthrm.cache.json");
- };
-
- var getCache = function() {
- var cache = require('Storage').readJSON("bthrm.cache.json", true) || {};
- if (settings.btid && settings.btid === cache.id) return cache;
- clearCache();
- return {};
- };
-
- var addNotificationHandler = function(characteristic) {
- log("Setting notification handler: " + supportedCharacteristics[characteristic.uuid].handler);
- characteristic.on('characteristicvaluechanged', (ev) => supportedCharacteristics[characteristic.uuid].handler(ev.target.value));
- };
-
- var writeCache = function(cache) {
- var oldCache = getCache();
- if (oldCache !== cache) {
- log("Writing cache");
- require('Storage').writeJSON("bthrm.cache.json", cache);
- } else {
- log("No changes, don't write cache");
- }
- };
-
- var characteristicsToCache = function(characteristics) {
- log("Cache characteristics");
- var cache = getCache();
- if (!cache.characteristics) cache.characteristics = {};
- for (var c of characteristics){
- //"handle_value":16,"handle_decl":15
- log("Saving handle " + c.handle_value + " for characteristic: ", c);
- cache.characteristics[c.uuid] = {
- "handle": c.handle_value,
- "uuid": c.uuid,
- "notify": c.properties.notify,
- "read": c.properties.read
- };
- }
- writeCache(cache);
- };
-
- var characteristicsFromCache = function() {
- log("Read cached characteristics");
- var cache = getCache();
- if (!cache.characteristics) return [];
- var restored = [];
- for (var c in cache.characteristics){
- var cached = cache.characteristics[c];
- var r = new BluetoothRemoteGATTCharacteristic();
- log("Restoring characteristic ", cached);
- r.handle_value = cached.handle;
- r.uuid = cached.uuid;
- r.properties = {};
- r.properties.notify = cached.notify;
- r.properties.read = cached.read;
- addNotificationHandler(r);
- log("Restored characteristic: ", r);
- restored.push(r);
- }
- return restored;
- };
-
- log("Start");
-
- var lastReceivedData={
- };
-
- var supportedServices = [
- "0x180d", // Heart Rate
- "0x180f", // Battery
- ];
-
- var supportedCharacteristics = {
- "0x2a37": {
- //Heart rate measurement
- handler: function (dv){
- var flags = dv.getUint8(0);
-
- var bpm = (flags & 1) ? (dv.getUint16(1) / 100 /* ? */ ) : dv.getUint8(1); // 8 or 16 bit
-
- var sensorContact;
-
- if (flags & 2){
- sensorContact = !!(flags & 4);
- }
-
- var idx = 2 + (flags&1);
-
- var energyExpended;
- if (flags & 8){
- energyExpended = dv.getUint16(idx,1);
- idx += 2;
- }
- var interval;
- if (flags & 16) {
- interval = [];
- var maxIntervalBytes = (dv.byteLength - idx);
- log("Found " + (maxIntervalBytes / 2) + " rr data fields");
- for(var i = 0 ; i < maxIntervalBytes / 2; i++){
- interval[i] = dv.getUint16(idx,1); // in milliseconds
- idx += 2;
- }
- }
-
- var location;
- if (lastReceivedData && lastReceivedData["0x180d"] && lastReceivedData["0x180d"]["0x2a38"]){
- location = lastReceivedData["0x180d"]["0x2a38"];
- }
-
- var battery;
- if (lastReceivedData && lastReceivedData["0x180f"] && lastReceivedData["0x180f"]["0x2a19"]){
- battery = lastReceivedData["0x180f"]["0x2a19"];
- }
-
- if (settings.replace){
- var repEvent = {
- bpm: bpm,
- confidence: (sensorContact || sensorContact === undefined)? 100 : 0,
- src: "bthrm"
- };
-
- log("Emitting HRM: ", repEvent);
- Bangle.emit("HRM", repEvent);
- }
-
- var newEvent = {
- bpm: bpm
- };
-
- if (location) newEvent.location = location;
- if (interval) newEvent.rr = interval;
- if (energyExpended) newEvent.energy = energyExpended;
- if (battery) newEvent.battery = battery;
- if (sensorContact) newEvent.contact = sensorContact;
-
- log("Emitting BTHRM: ", newEvent);
- Bangle.emit("BTHRM", newEvent);
- }
- },
- "0x2a38": {
- //Body sensor location
- handler: function(dv){
- if (!lastReceivedData["0x180d"]) lastReceivedData["0x180d"] = {};
- lastReceivedData["0x180d"]["0x2a38"] = parseInt(dv.buffer, 10);
- }
- },
- "0x2a19": {
- //Battery
- handler: function (dv){
- if (!lastReceivedData["0x180f"]) lastReceivedData["0x180f"] = {};
- lastReceivedData["0x180f"]["0x2a19"] = dv.getUint8(0);
- }
- }
- };
-
- var device;
- var gatt;
- var characteristics = [];
- var blockInit = false;
- var currentRetryTimeout;
- var initialRetryTime = 40;
- var maxRetryTime = 60000;
- var retryTime = initialRetryTime;
-
- var connectSettings = {
- minInterval: 7.5,
- maxInterval: 1500
- };
-
- var waitingPromise = function(timeout) {
- return new Promise(function(resolve){
- log("Start waiting for " + timeout);
- setTimeout(()=>{
- log("Done waiting for " + timeout);
- resolve();
- }, timeout);
- });
- };
-
- if (settings.enabled){
- Bangle.isBTHRMOn = function(){
- return (Bangle._PWR && Bangle._PWR.BTHRM && Bangle._PWR.BTHRM.length > 0);
- };
-
- Bangle.isBTHRMConnected = function(){
- return gatt && gatt.connected;
- };
- }
-
- if (settings.replace){
- var origIsHRMOn = Bangle.isHRMOn;
-
- Bangle.isHRMOn = function() {
- if (settings.enabled && !settings.replace){
- return origIsHRMOn();
- } else if (settings.enabled && settings.replace){
- return Bangle.isBTHRMOn();
- }
- return origIsHRMOn() || Bangle.isBTHRMOn();
- };
- }
-
- var clearRetryTimeout = function() {
- if (currentRetryTimeout){
- log("Clearing timeout " + currentRetryTimeout);
- clearTimeout(currentRetryTimeout);
- currentRetryTimeout = undefined;
- }
- };
-
- var retry = function() {
- log("Retry");
-
- if (!currentRetryTimeout){
-
- var clampedTime = retryTime < 100 ? 100 : retryTime;
-
- log("Set timeout for retry as " + clampedTime);
- clearRetryTimeout();
- currentRetryTimeout = setTimeout(() => {
- log("Retrying");
- currentRetryTimeout = undefined;
- initBt();
- }, clampedTime);
-
- retryTime = Math.pow(clampedTime, 1.1);
- if (retryTime > maxRetryTime){
- retryTime = maxRetryTime;
- }
- } else {
- log("Already in retry...");
- }
- };
-
- var buzzing = false;
- var onDisconnect = function(reason) {
- log("Disconnect: " + reason);
- log("GATT: ", gatt);
- log("Characteristics: ", characteristics);
- retryTime = initialRetryTime;
- clearRetryTimeout();
- switchInternalHrm();
- blockInit = false;
- if (settings.warnDisconnect && !buzzing){
- buzzing = true;
- Bangle.buzz(500,0.3).then(()=>waitingPromise(4500)).then(()=>{buzzing = false;});
- }
- if (Bangle.isBTHRMOn()){
- retry();
- }
- };
-
- var createCharacteristicPromise = function(newCharacteristic) {
- log("Create characteristic promise: ", newCharacteristic);
- var result = Promise.resolve();
- // For values that can be read, go ahead and read them, even if we might be notified in the future
- // Allows for getting initial state of infrequently updating characteristics, like battery
- if (newCharacteristic.readValue){
- result = result.then(()=>{
- log("Reading data for " + JSON.stringify(newCharacteristic));
- return newCharacteristic.readValue().then((data)=>{
- if (supportedCharacteristics[newCharacteristic.uuid] && supportedCharacteristics[newCharacteristic.uuid].handler) {
- supportedCharacteristics[newCharacteristic.uuid].handler(data);
- }
- });
- });
- }
- if (newCharacteristic.properties.notify){
- result = result.then(()=>{
- log("Starting notifications for: ", newCharacteristic);
- var startPromise = newCharacteristic.startNotifications().then(()=>log("Notifications started for ", newCharacteristic));
- if (settings.gracePeriodNotification > 0){
- log("Add " + settings.gracePeriodNotification + "ms grace period after starting notifications");
- startPromise = startPromise.then(()=>{
- log("Wait after connect");
- return waitingPromise(settings.gracePeriodNotification);
- });
- }
- return startPromise;
- });
- }
- return result.then(()=>log("Handled characteristic: ", newCharacteristic));
- };
-
- var attachCharacteristicPromise = function(promise, characteristic) {
- return promise.then(()=>{
- log("Handling characteristic:", characteristic);
- return createCharacteristicPromise(characteristic);
- });
- };
-
- var createCharacteristicsPromise = function(newCharacteristics) {
- log("Create characteristics promise: ", newCharacteristics);
- var result = Promise.resolve();
- for (var c of newCharacteristics){
- if (!supportedCharacteristics[c.uuid]) continue;
- log("Supporting characteristic: ", c);
- characteristics.push(c);
- if (c.properties.notify){
- addNotificationHandler(c);
- }
-
- result = attachCharacteristicPromise(result, c);
- }
- return result.then(()=>log("Handled characteristics"));
- };
-
- var createServicePromise = function(service) {
- log("Create service promise: ", service);
- var result = Promise.resolve();
- result = result.then(()=>{
- log("Handling service: " + service.uuid);
- return service.getCharacteristics().then((c)=>createCharacteristicsPromise(c));
- });
- return result.then(()=>log("Handled service" + service.uuid));
- };
-
- var attachServicePromise = function(promise, service) {
- return promise.then(()=>createServicePromise(service));
- };
-
- var initBt = function () {
- log("initBt with blockInit: " + blockInit);
- if (blockInit){
- retry();
- return;
- }
-
- blockInit = true;
-
- var promise;
- var filters;
-
- if (!device){
- if (settings.btid){
- log("Configured device id", settings.btid);
- filters = [{ id: settings.btid }];
- } else {
- return;
- }
- log("Requesting device with filters", filters);
- promise = NRF.requestDevice({ filters: filters, active: true });
-
- if (settings.gracePeriodRequest){
- log("Add " + settings.gracePeriodRequest + "ms grace period after request");
- }
-
- promise = promise.then((d)=>{
- log("Got device: ", d);
- d.on('gattserverdisconnected', onDisconnect);
- device = d;
- });
-
- promise = promise.then(()=>{
- log("Wait after request");
- return waitingPromise(settings.gracePeriodRequest);
- });
- } else {
- promise = Promise.resolve();
- log("Reuse device: ", device);
- }
-
- promise = promise.then(()=>{
- if (gatt){
- log("Reuse GATT: ", gatt);
- } else {
- log("GATT is new: ", gatt);
- characteristics = [];
- var cachedId = getCache().id;
- if (device.id !== cachedId){
- log("Device ID changed from " + cachedId + " to " + device.id + ", clearing cache");
- clearCache();
- }
- var newCache = getCache();
- newCache.id = device.id;
- writeCache(newCache);
- gatt = device.gatt;
- }
-
- return Promise.resolve(gatt);
- });
-
- promise = promise.then((gatt)=>{
- if (!gatt.connected){
- var connectPromise = gatt.connect(connectSettings);
- if (settings.gracePeriodConnect > 0){
- log("Add " + settings.gracePeriodConnect + "ms grace period after connecting");
- connectPromise = connectPromise.then(()=>{
- log("Wait after connect");
- return waitingPromise(settings.gracePeriodConnect);
- });
- }
- return connectPromise;
- } else {
- return Promise.resolve();
- }
- });
-
-/* promise = promise.then(() => {
- log(JSON.stringify(gatt.getSecurityStatus()));
- if (gatt.getSecurityStatus()['bonded']) {
- log("Already bonded");
- return Promise.resolve();
- } else {
- log("Start bonding");
- return gatt.startBonding()
- .then(() => console.log(gatt.getSecurityStatus()));
- }
- });*/
-
- promise = promise.then(()=>{
- if (!characteristics || characteristics.length === 0){
- characteristics = characteristicsFromCache();
- }
- });
-
- promise = promise.then(()=>{
- var characteristicsPromise = Promise.resolve();
- if (characteristics.length === 0){
- characteristicsPromise = characteristicsPromise.then(()=>{
- log("Getting services");
- return gatt.getPrimaryServices();
- });
-
- characteristicsPromise = characteristicsPromise.then((services)=>{
- log("Got services:", services);
- var result = Promise.resolve();
- for (var service of services){
- if (!(supportedServices.includes(service.uuid))) continue;
- log("Supporting service: ", service.uuid);
- result = attachServicePromise(result, service);
- }
- if (settings.gracePeriodService > 0) {
- log("Add " + settings.gracePeriodService + "ms grace period after services");
- result = result.then(()=>{
- log("Wait after services");
- return waitingPromise(settings.gracePeriodService);
- });
- }
- return result;
- });
- } else {
- for (var characteristic of characteristics){
- characteristicsPromise = attachCharacteristicPromise(characteristicsPromise, characteristic, true);
- }
- }
-
- return characteristicsPromise;
- });
-
- return promise.then(()=>{
- log("Connection established, waiting for notifications");
- characteristicsToCache(characteristics);
- clearRetryTimeout();
- }).catch((e) => {
- characteristics = [];
- log("Error:", e);
- onDisconnect(e);
- });
- };
-
- Bangle.setBTHRMPower = function(isOn, app) {
- // Do app power handling
- if (!app) app="?";
- if (Bangle._PWR===undefined) Bangle._PWR={};
- if (Bangle._PWR.BTHRM===undefined) Bangle._PWR.BTHRM=[];
- if (isOn && !Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM.push(app);
- if (!isOn && Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM = Bangle._PWR.BTHRM.filter(a=>a!==app);
- isOn = Bangle._PWR.BTHRM.length;
- // so now we know if we're really on
- if (isOn) {
- if (!Bangle.isBTHRMConnected()) initBt();
- } else { // not on
- log("Power off for " + app);
- if (gatt) {
- if (gatt.connected){
- log("Disconnect with gatt: ", gatt);
- try{
- gatt.disconnect().then(()=>{
- log("Successful disconnect");
- }).catch((e)=>{
- log("Error during disconnect promise", e);
- });
- } catch (e){
- log("Error during disconnect attempt", e);
- }
- }
- }
- }
- };
-
- var origSetHRMPower = Bangle.setHRMPower;
-
- if (settings.startWithHrm){
-
- Bangle.setHRMPower = function(isOn, app) {
- log("setHRMPower for " + app + ": " + (isOn?"on":"off"));
- if (settings.enabled){
- Bangle.setBTHRMPower(isOn, app);
- }
- if ((settings.enabled && !settings.replace) || !settings.enabled){
- origSetHRMPower(isOn, app);
- }
- };
- }
-
- var fallbackInterval;
-
- var switchInternalHrm = function() {
- if (settings.allowFallback && !fallbackInterval){
- log("Fallback to HRM enabled");
- origSetHRMPower(1, "bthrm_fallback");
- fallbackInterval = setInterval(()=>{
- if (Bangle.isBTHRMConnected()){
- origSetHRMPower(0, "bthrm_fallback");
- clearInterval(fallbackInterval);
- fallbackInterval = undefined;
- log("Fallback to HRM disabled");
- }
- }, settings.fallbackTimeout);
- }
- };
-
- if (settings.replace){
- log("Replace HRM event");
- if (Bangle._PWR && Bangle._PWR.HRM){
- for (var i = 0; i < Bangle._PWR.HRM.length; i++){
- var app = Bangle._PWR.HRM[i];
- log("Moving app " + app);
- origSetHRMPower(0, app);
- Bangle.setBTHRMPower(1, app);
- if (Bangle._PWR.HRM===undefined) break;
- }
- }
- switchInternalHrm();
- }
-
- E.on("kill", ()=>{
- if (gatt && gatt.connected){
- log("Got killed, trying to disconnect");
- gatt.disconnect().then(()=>log("Disconnected on kill")).catch((e)=>log("Error during disconnnect on kill", e));
- }
- });
- }
-})();
+if ((require('Storage').readJSON("bthrm.json", true) || {}).enabled != false) require("bthrm").enable();
diff --git a/apps/bthrm/bthrm.js b/apps/bthrm/bthrm.js
index dd9230386..b07e7bd37 100644
--- a/apps/bthrm/bthrm.js
+++ b/apps/bthrm/bthrm.js
@@ -1,5 +1,5 @@
-var intervalInt;
-var intervalBt;
+const BPM_FONT_SIZE="19%";
+const VALUE_TIMEOUT=3000;
var BODY_LOCS = {
0: 'Other',
@@ -7,83 +7,149 @@ var BODY_LOCS = {
2: 'Wrist',
3: 'Finger',
4: 'Hand',
- 5: 'Ear Lobe',
+ 5: 'Earlobe',
6: 'Foot',
+};
+
+var Layout = require("Layout");
+
+function border(l,c) {
+ g.setColor(c).drawLine(l.x+l.w*0.05, l.y-4, l.x+l.w*0.95, l.y-4);
}
-function clear(y){
- g.reset();
- g.clearRect(0,y,g.getWidth(),y+75);
-}
-
-function draw(y, type, event) {
- clear(y);
- var px = g.getWidth()/2;
- var str = event.bpm + "";
- g.reset();
- g.setFontAlign(0,0);
- g.setFontVector(40).drawString(str,px,y+20);
- str = "Event: " + type;
- if (type === "HRM") {
- str += " Confidence: " + event.confidence;
- g.setFontVector(12).drawString(str,px,y+40);
- str = " Source: " + (event.src ? event.src : "internal");
- g.setFontVector(12).drawString(str,px,y+50);
+function getRow(id, text, additionalInfo){
+ let additional = [];
+ let l = {
+ type:"h", c: [
+ {
+ type:"v",
+ width: g.getWidth()*0.4,
+ c: [
+ {type:"txt", halign:1, font:"8%", label:text, id:id+"text" },
+ {type:"txt", halign:1, font:BPM_FONT_SIZE, label:"--", id:id, bgCol: g.theme.bg }
+ ]
+ },{
+ type:undefined, fillx:1
+ },{
+ type:"v",
+ valign: -1,
+ width: g.getWidth()*0.45,
+ c: additional
+ },{
+ type:undefined, width:g.getWidth()*0.05
+ }
+ ]
+ };
+ for (let i of additionalInfo){
+ let label = {type:"txt", font:"6x8", label:i + ":" };
+ let value = {type:"txt", font:"6x8", label:"--", id:id + i };
+ additional.push({type:"h", halign:-1, c:[ label, {type:undefined, fillx:1}, value ]});
}
- if (type === "BTHRM"){
- if (event.battery) str += " Bat: " + (event.battery ? event.battery : "");
- g.setFontVector(12).drawString(str,px,y+40);
- str= "";
- if (event.location) str += "Loc: " + BODY_LOCS[event.location];
- if (event.rr && event.rr.length > 0) str += " RR: " + event.rr.join(",");
- g.setFontVector(12).drawString(str,px,y+50);
- str= "";
- if (event.contact) str += " Contact: " + event.contact;
- if (event.energy) str += " kJoule: " + event.energy.toFixed(0);
- g.setFontVector(12).drawString(str,px,y+60);
- }
-
+
+ return l;
}
-var firstEventBt = true;
-var firstEventInt = true;
+var layout = new Layout( {
+ type:"v", c: [
+ getRow("int", "INT", ["Confidence"]),
+ getRow("agg", "HRM", ["Confidence", "Source"]),
+ getRow("bt", "BT", ["Battery","Location","Contact", "RR", "Energy"]),
+ { type:undefined, height:8 } //dummy to protect debug output
+ ]
+}, {
+ lazy:true
+});
+
+var int,agg,bt;
+var firstEvent = true;
+
+function draw(){
+ if (!(int || agg || bt)) return;
+
+ if (firstEvent) {
+ g.clearRect(Bangle.appRect);
+ firstEvent = false;
+ }
+
+ let now = Date.now();
+
+ if (int && int.time > (now - VALUE_TIMEOUT)){
+ layout.int.label = int.bpm;
+ if (!isNaN(int.confidence)) layout.intConfidence.label = int.confidence;
+ } else {
+ layout.int.label = "--";
+ layout.intConfidence.label = "--";
+ }
+
+ if (agg && agg.time > (now - VALUE_TIMEOUT)){
+ layout.agg.label = agg.bpm;
+ if (!isNaN(agg.confidence)) layout.aggConfidence.label = agg.confidence;
+ if (agg.src) layout.aggSource.label = agg.src;
+ } else {
+ layout.agg.label = "--";
+ layout.aggConfidence.label = "--";
+ layout.aggSource.label = "--";
+ }
+
+ if (bt && bt.time > (now - VALUE_TIMEOUT)) {
+ layout.bt.label = bt.bpm;
+ if (!isNaN(bt.battery)) layout.btBattery.label = bt.battery + "%";
+ if (bt.rr) layout.btRR.label = bt.rr.join(",");
+ if (!isNaN(bt.location)) layout.btLocation.label = BODY_LOCS[bt.location];
+ if (bt.contact !== undefined) layout.btContact.label = bt.contact ? "Yes":"No";
+ if (!isNaN(bt.energy)) layout.btEnergy.label = bt.energy.toFixed(0) + "kJ";
+ } else {
+ layout.bt.label = "--";
+ layout.btBattery.label = "--";
+ layout.btRR.label = "--";
+ layout.btLocation.label = "--";
+ layout.btContact.label = "--";
+ layout.btEnergy.label = "--";
+ }
+
+ layout.update();
+ layout.render();
+ let first = true;
+ for (let c of layout.l.c){
+ if (first) {
+ first = false;
+ continue;
+ }
+ if (c.type && c.type == "h")
+ border(c,g.theme.fg);
+ }
+}
+
+
+// This can get called for the boot code to show what's happening
+function showStatusInfo(txt) {
+ var R = Bangle.appRect;
+ g.reset().clearRect(R.x,R.y2-8,R.x2,R.y2).setFont("6x8");
+ txt = g.wrapString(txt, R.w)[0];
+ g.setFontAlign(0,1).drawString(txt, (R.x+R.x2)/2, R.y2);
+}
function onBtHrm(e) {
- if (firstEventBt){
- clear(24);
- firstEventBt = false;
- }
- draw(100, "BTHRM", e);
- if (e.bpm === 0){
- Bangle.buzz(100,0.2);
- }
- if (intervalBt){
- clearInterval(intervalBt);
- }
- intervalBt = setInterval(()=>{
- clear(100);
- }, 2000);
+ bt = e;
+ bt.time = Date.now();
}
-function onHrm(e) {
- if (firstEventInt){
- clear(24);
- firstEventInt = false;
- }
- draw(24, "HRM", e);
- if (intervalInt){
- clearInterval(intervalInt);
- }
- intervalInt = setInterval(()=>{
- clear(24);
- }, 2000);
+function onInt(e) {
+ int = e;
+ int.time = Date.now();
}
+function onAgg(e) {
+ agg = e;
+ agg.time = Date.now();
+}
var settings = require('Storage').readJSON("bthrm.json", true) || {};
Bangle.on('BTHRM', onBtHrm);
-Bangle.on('HRM', onHrm);
+Bangle.on('HRM_int', onInt);
+Bangle.on('HRM', onAgg);
+
Bangle.setHRMPower(1,'bthrm');
if (!(settings.startWithHrm)){
@@ -95,10 +161,11 @@ Bangle.loadWidgets();
Bangle.drawWidgets();
if (Bangle.setBTHRMPower){
g.reset().setFont("6x8",2).setFontAlign(0,0);
- g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2 - 24);
+ g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2);
+ setInterval(draw, 1000);
} else {
g.reset().setFont("6x8",2).setFontAlign(0,0);
- g.drawString("BTHRM disabled",g.getWidth()/2,g.getHeight()/2 + 32);
+ g.drawString("BTHRM disabled",g.getWidth()/2,g.getHeight()/2);
}
E.on('kill', ()=>Bangle.setBTHRMPower(0,'bthrm'));
diff --git a/apps/bthrm/default.json b/apps/bthrm/default.json
index fb284bcd2..79605b412 100644
--- a/apps/bthrm/default.json
+++ b/apps/bthrm/default.json
@@ -16,5 +16,7 @@
"gracePeriodNotification": 0,
"gracePeriodConnect": 0,
"gracePeriodService": 0,
- "gracePeriodRequest": 0
+ "gracePeriodRequest": 0,
+ "bonding": false,
+ "active": true
}
diff --git a/apps/bthrm/lib.js b/apps/bthrm/lib.js
new file mode 100644
index 000000000..a792167ca
--- /dev/null
+++ b/apps/bthrm/lib.js
@@ -0,0 +1,664 @@
+exports.enable = () => {
+ var settings = Object.assign(
+ require('Storage').readJSON("bthrm.default.json", true) || {},
+ require('Storage').readJSON("bthrm.json", true) || {}
+ );
+
+ var log = function(text, param){
+ if (global.showStatusInfo)
+ showStatusInfo(text);
+ if (settings.debuglog){
+ var logline = new Date().toISOString() + " - " + text;
+ if (param) logline += ": " + JSON.stringify(param);
+ print(logline);
+ }
+ };
+
+ log("Settings: ", settings);
+
+ if (settings.enabled){
+
+ var clearCache = function() {
+ return require('Storage').erase("bthrm.cache.json");
+ };
+
+ var getCache = function() {
+ var cache = require('Storage').readJSON("bthrm.cache.json", true) || {};
+ if (settings.btid && settings.btid === cache.id) return cache;
+ clearCache();
+ return {};
+ };
+
+ var addNotificationHandler = function(characteristic) {
+ log("Setting notification handler"/*supportedCharacteristics[characteristic.uuid].handler*/);
+ characteristic.on('characteristicvaluechanged', (ev) => supportedCharacteristics[characteristic.uuid].handler(ev.target.value));
+ };
+
+ var writeCache = function(cache) {
+ var oldCache = getCache();
+ if (oldCache !== cache) {
+ log("Writing cache");
+ require('Storage').writeJSON("bthrm.cache.json", cache);
+ } else {
+ log("No changes, don't write cache");
+ }
+ };
+
+ var characteristicsToCache = function(characteristics) {
+ log("Cache characteristics");
+ var cache = getCache();
+ if (!cache.characteristics) cache.characteristics = {};
+ for (var c of characteristics){
+ //"handle_value":16,"handle_decl":15
+ log("Saving handle " + c.handle_value + " for characteristic: ", c);
+ cache.characteristics[c.uuid] = {
+ "handle": c.handle_value,
+ "uuid": c.uuid,
+ "notify": c.properties.notify,
+ "read": c.properties.read
+ };
+ }
+ writeCache(cache);
+ };
+
+ var characteristicsFromCache = function(device) {
+ var service = { device : device }; // fake a BluetoothRemoteGATTService
+ log("Read cached characteristics");
+ var cache = getCache();
+ if (!cache.characteristics) return [];
+ var restored = [];
+ for (var c in cache.characteristics){
+ var cached = cache.characteristics[c];
+ var r = new BluetoothRemoteGATTCharacteristic();
+ log("Restoring characteristic ", cached);
+ r.handle_value = cached.handle;
+ r.uuid = cached.uuid;
+ r.properties = {};
+ r.properties.notify = cached.notify;
+ r.properties.read = cached.read;
+ r.service = service;
+ addNotificationHandler(r);
+ log("Restored characteristic: ", r);
+ restored.push(r);
+ }
+ return restored;
+ };
+
+ log("Start");
+
+ var lastReceivedData={
+ };
+
+ var supportedServices = [
+ "0x180d", // Heart Rate
+ "0x180f", // Battery
+ ];
+
+ var bpmTimeout;
+
+ var supportedCharacteristics = {
+ "0x2a37": {
+ //Heart rate measurement
+ active: false,
+ handler: function (dv){
+ var flags = dv.getUint8(0);
+
+ var bpm = (flags & 1) ? (dv.getUint16(1) / 100 /* ? */ ) : dv.getUint8(1); // 8 or 16 bit
+ supportedCharacteristics["0x2a37"].active = bpm > 0;
+ log("BTHRM BPM " + supportedCharacteristics["0x2a37"].active);
+ switchFallback();
+ if (bpmTimeout) clearTimeout(bpmTimeout);
+ bpmTimeout = setTimeout(()=>{
+ bpmTimeout = undefined;
+ supportedCharacteristics["0x2a37"].active = false;
+ startFallback();
+ }, 3000);
+
+ var sensorContact;
+
+ if (flags & 2){
+ sensorContact = !!(flags & 4);
+ }
+
+ var idx = 2 + (flags&1);
+
+ var energyExpended;
+ if (flags & 8){
+ energyExpended = dv.getUint16(idx,1);
+ idx += 2;
+ }
+ var interval;
+ if (flags & 16) {
+ interval = [];
+ var maxIntervalBytes = (dv.byteLength - idx);
+ log("Found " + (maxIntervalBytes / 2) + " rr data fields");
+ for(var i = 0 ; i < maxIntervalBytes / 2; i++){
+ interval[i] = dv.getUint16(idx,1); // in milliseconds
+ idx += 2;
+ }
+ }
+
+ var location;
+ if (lastReceivedData && lastReceivedData["0x180d"] && lastReceivedData["0x180d"]["0x2a38"]){
+ location = lastReceivedData["0x180d"]["0x2a38"];
+ }
+
+ var battery;
+ if (lastReceivedData && lastReceivedData["0x180f"] && lastReceivedData["0x180f"]["0x2a19"]){
+ battery = lastReceivedData["0x180f"]["0x2a19"];
+ }
+
+ if (settings.replace && bpm > 0){
+ var repEvent = {
+ bpm: bpm,
+ confidence: (sensorContact || sensorContact === undefined)? 100 : 0,
+ src: "bthrm"
+ };
+
+ log("Emitting HRM_R(bt)", repEvent);
+ Bangle.emit("HRM_R", repEvent);
+ }
+
+ var newEvent = {
+ bpm: bpm
+ };
+
+ if (location) newEvent.location = location;
+ if (interval) newEvent.rr = interval;
+ if (energyExpended) newEvent.energy = energyExpended;
+ if (battery) newEvent.battery = battery;
+ if (sensorContact) newEvent.contact = sensorContact;
+
+ log("Emitting BTHRM", newEvent);
+ Bangle.emit("BTHRM", newEvent);
+ }
+ },
+ "0x2a38": {
+ //Body sensor location
+ handler: function(dv){
+ if (!lastReceivedData["0x180d"]) lastReceivedData["0x180d"] = {};
+ lastReceivedData["0x180d"]["0x2a38"] = parseInt(dv.buffer, 10);
+ }
+ },
+ "0x2a19": {
+ //Battery
+ handler: function (dv){
+ if (!lastReceivedData["0x180f"]) lastReceivedData["0x180f"] = {};
+ lastReceivedData["0x180f"]["0x2a19"] = dv.getUint8(0);
+ }
+ }
+ };
+
+ var device;
+ var gatt;
+ var characteristics = [];
+ var blockInit = false;
+ var currentRetryTimeout;
+ var initialRetryTime = 40;
+ var maxRetryTime = 60000;
+ var retryTime = initialRetryTime;
+
+ var connectSettings = {
+ minInterval: 7.5,
+ maxInterval: 1500
+ };
+
+ var waitingPromise = function(timeout) {
+ return new Promise(function(resolve){
+ log("Start waiting for " + timeout);
+ setTimeout(()=>{
+ log("Done waiting for " + timeout);
+ resolve();
+ }, timeout);
+ });
+ };
+
+ if (settings.enabled){
+ Bangle.isBTHRMActive = function (){
+ return supportedCharacteristics["0x2a37"].active;
+ };
+
+ Bangle.isBTHRMOn = function(){
+ return (Bangle._PWR && Bangle._PWR.BTHRM && Bangle._PWR.BTHRM.length > 0);
+ };
+
+ Bangle.isBTHRMConnected = function(){
+ return gatt && gatt.connected;
+ };
+ }
+
+ if (settings.replace){
+ Bangle.origIsHRMOn = Bangle.isHRMOn;
+
+ Bangle.isHRMOn = function() {
+ if (settings.enabled && !settings.replace){
+ return Bangle.origIsHRMOn();
+ } else if (settings.enabled && settings.replace){
+ return Bangle.isBTHRMOn();
+ }
+ return Bangle.origIsHRMOn() || Bangle.isBTHRMOn();
+ };
+ }
+
+ var clearRetryTimeout = function(resetTime) {
+ if (currentRetryTimeout){
+ log("Clearing timeout " + currentRetryTimeout);
+ clearTimeout(currentRetryTimeout);
+ currentRetryTimeout = undefined;
+ }
+ if (resetTime) {
+ log("Resetting retry time");
+ retryTime = initialRetryTime;
+ }
+ };
+
+ var retry = function() {
+ log("Retry");
+
+ if (!currentRetryTimeout && !powerdownRequested){
+
+ var clampedTime = retryTime < 100 ? 100 : retryTime;
+
+ log("Set timeout for retry as " + clampedTime);
+ clearRetryTimeout();
+ currentRetryTimeout = setTimeout(() => {
+ log("Retrying");
+ currentRetryTimeout = undefined;
+ initBt();
+ }, clampedTime);
+
+ retryTime = Math.pow(clampedTime, 1.1);
+ if (retryTime > maxRetryTime){
+ retryTime = maxRetryTime;
+ }
+ } else {
+ log("Already in retry...");
+ }
+ };
+
+ var buzzing = false;
+ var onDisconnect = function(reason) {
+ log("Disconnect: " + reason);
+ log("GATT", gatt);
+ log("Characteristics", characteristics);
+
+ var retryTimeResetNeeded = true;
+ retryTimeResetNeeded &= reason != "Connection Timeout";
+ retryTimeResetNeeded &= reason != "No device found matching filters";
+ clearRetryTimeout(retryTimeResetNeeded);
+ supportedCharacteristics["0x2a37"].active = false;
+ if (!powerdownRequested) startFallback();
+ blockInit = false;
+ if (settings.warnDisconnect && !buzzing){
+ buzzing = true;
+ Bangle.buzz(500,0.3).then(()=>waitingPromise(4500)).then(()=>{buzzing = false;});
+ }
+ if (Bangle.isBTHRMOn()){
+ retry();
+ }
+ };
+
+ var createCharacteristicPromise = function(newCharacteristic) {
+ log("Create characteristic promise", newCharacteristic);
+ var result = Promise.resolve();
+ // For values that can be read, go ahead and read them, even if we might be notified in the future
+ // Allows for getting initial state of infrequently updating characteristics, like battery
+ if (newCharacteristic.readValue){
+ result = result.then(()=>{
+ log("Reading data", newCharacteristic);
+ return newCharacteristic.readValue().then((data)=>{
+ if (supportedCharacteristics[newCharacteristic.uuid] && supportedCharacteristics[newCharacteristic.uuid].handler) {
+ supportedCharacteristics[newCharacteristic.uuid].handler(data);
+ }
+ });
+ });
+ }
+ if (newCharacteristic.properties.notify){
+ result = result.then(()=>{
+ log("Starting notifications", newCharacteristic);
+ var startPromise = newCharacteristic.startNotifications().then(()=>log("Notifications started", newCharacteristic));
+
+ log("Add " + settings.gracePeriodNotification + "ms grace period after starting notifications");
+ startPromise = startPromise.then(()=>{
+ log("Wait after connect");
+ return waitingPromise(settings.gracePeriodNotification);
+ });
+
+ return startPromise;
+ });
+ }
+ return result.then(()=>log("Handled characteristic", newCharacteristic));
+ };
+
+ var attachCharacteristicPromise = function(promise, characteristic) {
+ return promise.then(()=>{
+ log("Handling characteristic:", characteristic);
+ return createCharacteristicPromise(characteristic);
+ });
+ };
+
+ var createCharacteristicsPromise = function(newCharacteristics) {
+ log("Create characteristics promis ", newCharacteristics);
+ var result = Promise.resolve();
+ for (var c of newCharacteristics){
+ if (!supportedCharacteristics[c.uuid]) continue;
+ log("Supporting characteristic", c);
+ characteristics.push(c);
+ if (c.properties.notify){
+ addNotificationHandler(c);
+ }
+
+ result = attachCharacteristicPromise(result, c);
+ }
+ return result.then(()=>log("Handled characteristics"));
+ };
+
+ var createServicePromise = function(service) {
+ log("Create service promise", service);
+ var result = Promise.resolve();
+ result = result.then(()=>{
+ log("Handling service" + service.uuid);
+ return service.getCharacteristics().then((c)=>createCharacteristicsPromise(c));
+ });
+ return result.then(()=>log("Handled service" + service.uuid));
+ };
+
+ var attachServicePromise = function(promise, service) {
+ return promise.then(()=>createServicePromise(service));
+ };
+
+ var initBt = function () {
+ log("initBt with blockInit: " + blockInit);
+ if (blockInit && !powerdownRequested){
+ retry();
+ return;
+ }
+
+ blockInit = true;
+
+ var promise;
+ var filters;
+
+ if (!device){
+ if (settings.btid){
+ log("Configured device id", settings.btid);
+ filters = [{ id: settings.btid }];
+ } else {
+ return;
+ }
+ log("Requesting device with filters", filters);
+ try {
+ promise = NRF.requestDevice({ filters: filters, active: settings.active });
+ } catch (e){
+ log("Error during initial request:", e);
+ onDisconnect(e);
+ return;
+ }
+
+ if (settings.gracePeriodRequest){
+ log("Add " + settings.gracePeriodRequest + "ms grace period after request");
+ }
+
+ promise = promise.then((d)=>{
+ log("Got device", d);
+ d.on('gattserverdisconnected', onDisconnect);
+ device = d;
+ });
+
+ promise = promise.then(()=>{
+ log("Wait after request");
+ return waitingPromise(settings.gracePeriodRequest);
+ });
+ } else {
+ promise = Promise.resolve();
+ log("Reuse device", device);
+ }
+
+ promise = promise.then(()=>{
+ if (gatt){
+ log("Reuse GATT", gatt);
+ } else {
+ log("GATT is new", gatt);
+ characteristics = [];
+ var cachedId = getCache().id;
+ if (device.id !== cachedId){
+ log("Device ID changed from " + cachedId + " to " + device.id + ", clearing cache");
+ clearCache();
+ }
+ var newCache = getCache();
+ newCache.id = device.id;
+ writeCache(newCache);
+ gatt = device.gatt;
+ }
+
+ return Promise.resolve(gatt);
+ });
+
+ promise = promise.then((gatt)=>{
+ if (!gatt.connected){
+ log("Connecting...");
+ var connectPromise = gatt.connect(connectSettings).then(function() {
+ log("Connected.");
+ });
+ log("Add " + settings.gracePeriodConnect + "ms grace period after connecting");
+ connectPromise = connectPromise.then(()=>{
+ log("Wait after connect");
+ return waitingPromise(settings.gracePeriodConnect);
+ });
+ return connectPromise;
+ } else {
+ return Promise.resolve();
+ }
+ });
+
+ if (settings.bonding){
+ promise = promise.then(() => {
+ log(JSON.stringify(gatt.getSecurityStatus()));
+ if (gatt.getSecurityStatus()['bonded']) {
+ log("Already bonded");
+ return Promise.resolve();
+ } else {
+ log("Start bonding");
+ return gatt.startBonding()
+ .then(() => log("Security status" + gatt.getSecurityStatus()));
+ }
+ });
+ }
+
+ promise = promise.then(()=>{
+ if (!characteristics || characteristics.length === 0){
+ characteristics = characteristicsFromCache(device);
+ }
+ });
+
+ promise = promise.then(()=>{
+ var characteristicsPromise = Promise.resolve();
+ if (characteristics.length === 0){
+ characteristicsPromise = characteristicsPromise.then(()=>{
+ log("Getting services");
+ return gatt.getPrimaryServices();
+ });
+
+ characteristicsPromise = characteristicsPromise.then((services)=>{
+ log("Got services", services);
+ var result = Promise.resolve();
+ for (var service of services){
+ if (!(supportedServices.includes(service.uuid))) continue;
+ log("Supporting service", service.uuid);
+ result = attachServicePromise(result, service);
+ }
+ log("Add " + settings.gracePeriodService + "ms grace period after services");
+ result = result.then(()=>{
+ log("Wait after services");
+ return waitingPromise(settings.gracePeriodService);
+ });
+ return result;
+ });
+ } else {
+ for (var characteristic of characteristics){
+ characteristicsPromise = attachCharacteristicPromise(characteristicsPromise, characteristic, true);
+ }
+ }
+
+ return characteristicsPromise;
+ });
+
+ return promise.then(()=>{
+ log("Connection established, waiting for notifications");
+ characteristicsToCache(characteristics);
+ clearRetryTimeout(true);
+ }).catch((e) => {
+ characteristics = [];
+ log("Error:", e);
+ onDisconnect(e);
+ });
+ };
+
+ var powerdownRequested = false;
+
+ Bangle.setBTHRMPower = function(isOn, app) {
+ // Do app power handling
+ if (!app) app="?";
+ if (Bangle._PWR===undefined) Bangle._PWR={};
+ if (Bangle._PWR.BTHRM===undefined) Bangle._PWR.BTHRM=[];
+ if (isOn && !Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM.push(app);
+ if (!isOn && Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM = Bangle._PWR.BTHRM.filter(a=>a!==app);
+ isOn = Bangle._PWR.BTHRM.length;
+ // so now we know if we're really on
+ if (isOn) {
+ powerdownRequested = false;
+ switchFallback();
+ if (!Bangle.isBTHRMConnected()) initBt();
+ } else { // not on
+ log("Power off for " + app);
+ powerdownRequested = true;
+ clearRetryTimeout(true);
+ stopFallback();
+ if (gatt) {
+ if (gatt.connected){
+ log("Disconnect with gatt", gatt);
+ try{
+ gatt.disconnect().then(()=>{
+ log("Successful disconnect");
+ }).catch((e)=>{
+ log("Error during disconnect promise", e);
+ });
+ } catch (e){
+ log("Error during disconnect attempt", e);
+ }
+ }
+ }
+ }
+ };
+
+ if (settings.replace){
+ // register a listener for original HRM events and emit as HRM_int
+ Bangle.on("HRM", (o) => {
+ let e = Object.assign({},o);
+ log("Emitting HRM_int", e);
+ Bangle.emit("HRM_int", e);
+ if (fallbackActive){
+ // if fallback to internal HRM is active, emit as HRM_R to which everyone listens
+ o.src = "int";
+ log("Emitting HRM_R(int)", o);
+ Bangle.emit("HRM_R", o);
+ }
+ });
+
+ // force all apps wanting to listen to HRM to actually get events for HRM_R
+ Bangle.on = ( o => (name, cb) => {
+ o = o.bind(Bangle);
+ if (name == "HRM") o("HRM_R", cb);
+ else o(name, cb);
+ })(Bangle.on);
+
+ Bangle.removeListener = ( o => (name, cb) => {
+ o = o.bind(Bangle);
+ if (name == "HRM") o("HRM_R", cb);
+ else o(name, cb);
+ })(Bangle.removeListener);
+ } else {
+ Bangle.on("HRM", (o)=>{
+ o.src = "int";
+ let e = Object.assign({},o);
+ log("Emitting HRM_int", e);
+ Bangle.emit("HRM_int", e);
+ });
+ }
+
+ Bangle.origSetHRMPower = Bangle.setHRMPower;
+
+ if (settings.startWithHrm){
+ Bangle.setHRMPower = function(isOn, app) {
+ log("setHRMPower for " + app + ": " + (isOn?"on":"off"));
+ if (settings.enabled){
+ Bangle.setBTHRMPower(isOn, app);
+ if (Bangle._PWR && Bangle._PWR.HRM && Object.keys(Bangle._PWR.HRM).length == 0) {
+ Bangle._PWR.BTHRM = [];
+ Bangle.setBTHRMPower(0);
+ if (!isOn) stopFallback();
+ }
+ }
+ if ((settings.enabled && !settings.replace) || !settings.enabled){
+ Bangle.origSetHRMPower(isOn, app);
+ }
+ };
+ }
+
+ var fallbackActive = false;
+ var inSwitch = false;
+
+ var stopFallback = function(){
+ if (fallbackActive){
+ Bangle.origSetHRMPower(0, "bthrm_fallback");
+ fallbackActive = false;
+ log("Fallback to HRM disabled");
+ }
+ };
+
+ var startFallback = function(){
+ if (!fallbackActive && settings.allowFallback) {
+ fallbackActive = true;
+ Bangle.origSetHRMPower(1, "bthrm_fallback");
+ log("Fallback to HRM enabled");
+ }
+ };
+
+ var switchFallback = function() {
+ log("Check falling back to HRM");
+ if (!inSwitch){
+ inSwitch = true;
+ if (Bangle.isBTHRMActive()){
+ stopFallback();
+ } else {
+ startFallback();
+ }
+ }
+ inSwitch = false;
+ };
+
+ if (settings.replace){
+ log("Replace HRM event");
+ if (Bangle._PWR && Bangle._PWR.HRM){
+ for (var i = 0; i < Bangle._PWR.HRM.length; i++){
+ var app = Bangle._PWR.HRM[i];
+ log("Moving app " + app);
+ Bangle.origSetHRMPower(0, app);
+ Bangle.setBTHRMPower(1, app);
+ if (Bangle._PWR.HRM===undefined) break;
+ }
+ }
+ }
+
+ E.on("kill", ()=>{
+ if (gatt && gatt.connected){
+ log("Got killed, trying to disconnect");
+ try {
+ gatt.disconnect().then(()=>log("Disconnected on kill")).catch((e)=>log("Error during disconnnect promise on kill", e));
+ } catch (e) {
+ log("Error during disconnnect on kill", e)
+ }
+ }
+ });
+ }
+};
diff --git a/apps/bthrm/metadata.json b/apps/bthrm/metadata.json
index 9e40896f0..fea274ff3 100644
--- a/apps/bthrm/metadata.json
+++ b/apps/bthrm/metadata.json
@@ -2,9 +2,10 @@
"id": "bthrm",
"name": "Bluetooth Heart Rate Monitor",
"shortName": "BT HRM",
- "version": "0.10",
+ "version": "0.17",
"description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.",
"icon": "app.png",
+ "screenshots": [{"url":"screen.png"}],
"type": "app",
"tags": "health,bluetooth,hrm,bthrm",
"supports": ["BANGLEJS","BANGLEJS2"],
@@ -15,6 +16,7 @@
{"name":"bthrm.0.boot.js","url":"boot.js"},
{"name":"bthrm.img","url":"app-icon.js","evaluate":true},
{"name":"bthrm.settings.js","url":"settings.js"},
+ {"name":"bthrm","url":"lib.js"},
{"name":"bthrm.default.json","url":"default.json"}
]
}
diff --git a/apps/bthrm/recorder.js b/apps/bthrm/recorder.js
index 21345a907..fcfed47c3 100644
--- a/apps/bthrm/recorder.js
+++ b/apps/bthrm/recorder.js
@@ -32,8 +32,42 @@
Bangle.removeListener('BTHRM', onHRM);
if (Bangle.setBTRHMPower) Bangle.setBTHRMPower(0,"recorder");
},
- draw : (x,y) => g.setColor((bpm != "")?"#00f":"#88f").drawImage(atob("DAwBAAAAMMeef+f+f+P8H4DwBgAA"),x,y)
+ draw : (x,y) => g.setColor((Bangle.isBTHRMActive && Bangle.isBTHRMActive())?"#00f":"#88f").drawImage(atob("DAwBAAAAMMeef+f+f+P8H4DwBgAA"),x,y)
};
- }
+ };
+ recorders.hrmint = function() {
+ var active = false;
+ var bpmTimeout;
+ var bpm = "", bpmConfidence = "";
+ function onHRM(h) {
+ bpmConfidence = h.confidence;
+ bpm = h.bpm;
+ if (h.bpm > 0){
+ active = true;
+ if (bpmTimeout) clearTimeout(bpmTimeout);
+ bpmTimeout = setTimeout(()=>{
+ active = false;
+ },3000);
+ }
+ }
+ return {
+ name : "HR int",
+ fields : ["Int Heartrate", "Int Confidence"],
+ getValues : () => {
+ var r = [bpm,bpmConfidence];
+ bpm = ""; bpmConfidence = "";
+ return r;
+ },
+ start : () => {
+ Bangle.on('HRM_int', onHRM);
+ if (Bangle.origSetHRMPower) Bangle.origSetHRMPower(1,"recorder");
+ },
+ stop : () => {
+ Bangle.removeListener('HRM_int', onHRM);
+ if (Bangle.origSetHRMPower) Bangle.origSetHRMPower(0,"recorder");
+ },
+ draw : (x,y) => g.setColor(( Bangle.origIsHRMOn && Bangle.origIsHRMOn() && active)?"#0f0":"#8f8").drawImage(atob("DAwBAAAAMMeef+f+f+P8H4DwBgAA"),x,y)
+ };
+ };
})
diff --git a/apps/bthrm/screen.png b/apps/bthrm/screen.png
new file mode 100644
index 000000000..6b6b85227
Binary files /dev/null and b/apps/bthrm/screen.png differ
diff --git a/apps/bthrm/settings.js b/apps/bthrm/settings.js
index 8887ee81e..459ed29fc 100644
--- a/apps/bthrm/settings.js
+++ b/apps/bthrm/settings.js
@@ -17,6 +17,14 @@
var settings;
readSettings();
+ function applyCustomSettings(){
+ writeSettings("enabled",true);
+ writeSettings("replace",settings.custom_replace);
+ writeSettings("startWithHrm",settings.custom_startWithHrm);
+ writeSettings("allowFallback",settings.custom_allowFallback);
+ writeSettings("fallbackTimeout",settings.custom_fallbackTimeout);
+ }
+
function buildMainMenu(){
var mainmenu = {
'': { 'title': 'Bluetooth HRM' },
@@ -35,7 +43,6 @@
case 1:
writeSettings("enabled",true);
writeSettings("replace",true);
- writeSettings("debuglog",false);
writeSettings("startWithHrm",true);
writeSettings("allowFallback",true);
writeSettings("fallbackTimeout",10);
@@ -43,17 +50,11 @@
case 2:
writeSettings("enabled",true);
writeSettings("replace",false);
- writeSettings("debuglog",false);
writeSettings("startWithHrm",false);
writeSettings("allowFallback",false);
break;
case 3:
- writeSettings("enabled",true);
- writeSettings("replace",settings.custom_replace);
- writeSettings("debuglog",settings.custom_debuglog);
- writeSettings("startWithHrm",settings.custom_startWithHrm);
- writeSettings("allowFallback",settings.custom_allowFallback);
- writeSettings("fallbackTimeout",settings.custom_fallbackTimeout);
+ applyCustomSettings();
break;
}
writeSettings("mode",v);
@@ -95,6 +96,18 @@
writeSettings("debuglog",v);
}
},
+ 'Use bonding': {
+ value: !!settings.bonding,
+ onchange: v => {
+ writeSettings("bonding",v);
+ }
+ },
+ 'Use active scanning': {
+ value: !!settings.active,
+ onchange: v => {
+ writeSettings("active",v);
+ }
+ },
'Grace periods': function() { E.showMenu(submenu_grace); }
};
@@ -138,23 +151,23 @@
'< Back': function() { E.showMenu(buildMainMenu()); },
'Replace HRM': {
value: !!settings.custom_replace,
- format: v => settings.custom_replace ? "On" : "Off",
onchange: v => {
writeSettings("custom_replace",v);
+ if (settings.mode == 3) applyCustomSettings();
}
},
'Start w. HRM': {
value: !!settings.custom_startWithHrm,
- format: v => settings.custom_startWithHrm ? "On" : "Off",
onchange: v => {
writeSettings("custom_startWithHrm",v);
+ if (settings.mode == 3) applyCustomSettings();
}
},
'HRM Fallback': {
value: !!settings.custom_allowFallback,
- format: v => settings.custom_allowFallback ? "On" : "Off",
onchange: v => {
writeSettings("custom_allowFallback",v);
+ if (settings.mode == 3) applyCustomSettings();
}
},
'Fallback Timeout': {
@@ -165,6 +178,7 @@
format: v=>v+"s",
onchange: v => {
writeSettings("custom_fallbackTimout",v*1000);
+ if (settings.mode == 3) applyCustomSettings();
}
},
};
diff --git a/apps/bwclk/ChangeLog b/apps/bwclk/ChangeLog
index ecd0c355f..e3e059318 100644
--- a/apps/bwclk/ChangeLog
+++ b/apps/bwclk/ChangeLog
@@ -6,4 +6,19 @@
0.06: Design and usability improvements.
0.07: Improved positioning.
0.08: Select the color of widgets correctly. Additional settings to hide colon.
-0.09: Larger font size if colon is hidden to improve readability further.
\ No newline at end of file
+0.09: Larger font size if colon is hidden to improve readability further.
+0.10: HomeAssistant integration if HomeAssistant is installed.
+0.11: Performance improvements.
+0.12: Implements a 2D menu.
+0.13: Clicks < 24px are for widgets, if fullscreen mode is disabled.
+0.14: Adds humidity to weather data.
+0.15: Added option for a dynamic mode to show widgets only if unlocked.
+0.16: You can now show your agenda if your calendar is synced with Gadgetbridge.
+0.17: Fix - Step count was no more shown in the menu.
+0.18: Set timer for an agenda entry by simply clicking in the middle of the screen. Only one timer can be set.
+0.19: Fix - Compatibility with "Digital clock widget"
+0.20: Better handling of async data such as getPressure.
+0.21: On the default menu the week of year can be shown.
+0.22: Use the new clkinfo module for the menu.
+0.23: Feedback of apps after run is now optional and decided by the corresponding clkinfo.
+0.24: Update clock_info to avoid a redraw
diff --git a/apps/bwclk/README.md b/apps/bwclk/README.md
index f6a1c6522..d869fa2cf 100644
--- a/apps/bwclk/README.md
+++ b/apps/bwclk/README.md
@@ -1,17 +1,49 @@
# BW Clock
+A very minimalistic clock.

## Features
-- Fullscreen on/off
-- Tab left/right of screen to show steps, temperature etc.
-- Enable / disable lock icon in the settings.
-- If the "sched" app is installed tab top / bottom of the screen to set the timer.
-- The design is adapted to the theme of your bangle.
-- The colon (e.g. 7:35 = 735) can be hidden now in the settings.
+The BW clock implements features that are exposed by other apps through the `clkinfo` module.
+For example, if you install the HomeAssistant app, this menu item will be shown if you click right
+and additionally allows you to send triggers directly from the clock (select triggers via up/down and
+send via click center). Here are examples of other apps that are integrated:
+
+- Bangle data such as steps, heart rate, battery or charging state.
+- Show agenda entries. A timer for an agenda entry can also be set by simply clicking in the middle of the screen. This can be used to not forget a meeting etc. Note that only one agenda-timer can be set at a time. *Requirement: Gadgetbridge calendar sync enabled*
+- Weather temperature as well as the wind speed can be shown. *Requirement: Weather app*
+- HomeAssistant triggers can be executed directly. *Requirement: HomeAssistant app*
+
+Note: If some apps are not installed (e.gt. weather app), then this menu item is hidden.
+
+## Settings
+- Screen: Normal (widgets shown), Dynamic (widgets shown if unlocked) or Full (widgets are hidden).
+- Enable/disable lock icon in the settings. Useful if fullscreen mode is on.
+- The colon (e.g. 7:35 = 735) can be hidden in the settings for an even larger time font to improve readability further.
+- Your bangle uses the sys color settings so you can change the color too.
+
+## Menu structure
+2D menu allows you to display lots of different data including data from 3rd party apps and it's also possible to control things e.g. to trigger HomeAssistant.
+
+Simply click left / right to go through the menu entries such as Bangle, Weather etc.
+and click up/down to move into this sub-menu. You can then click in the middle of the screen
+to e.g. send a trigger via HomeAssistant once you selected it. The actions really depend
+on the app that provide this sub-menu through the `clkinfo` module.
+
+```
+ Bangle -- Agenda -- Weather -- HomeAssistant
+ | | | |
+ Battery Entry 1 Temperature Trigger1
+ | | | |
+ Steps ... ... ...
+ |
+ ...
+```
+
## Thanks to
-Icons created by Flaticon
+- Thanks to Gordon Williams not only for the great BangleJs, but specifically also for the implementation of `clkinfo` which simplified the BWClock a lot and moved complexety to the apps where it should be located.
+- Icons created by Flaticon
## Creator
-- [David Peer](https://github.com/peerdavid)
+[David Peer](https://github.com/peerdavid)
diff --git a/apps/bwclk/app.js b/apps/bwclk/app.js
index 5bfec4097..c29fdf2ef 100644
--- a/apps/bwclk/app.js
+++ b/apps/bwclk/app.js
@@ -1,25 +1,29 @@
-/*
+/************************************************
* Includes
*/
const locale = require('locale');
const storage = require('Storage');
+const clock_info = require("clock_info");
-/*
- * Statics
+
+/************************************************
+ * Globals
*/
const SETTINGS_FILE = "bwclk.setting.json";
-const TIMER_IDX = "bwclk";
const W = g.getWidth();
const H = g.getHeight();
+var lock_input = false;
-/*
+
+/************************************************
* Settings
*/
let settings = {
- fullscreen: false,
+ screen: "Normal",
showLock: true,
hideColon: false,
- showInfo: 0,
+ menuPosX: 0,
+ menuPosY: 0,
};
let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings;
@@ -27,33 +31,21 @@ for (const key in saved_settings) {
settings[key] = saved_settings[key]
}
-
-/*
+/************************************************
* Assets
*/
-
// Manrope font
Graphics.prototype.setLargeFont = function(scale) {
- // Actual height 48 (49 - 2)
+ // Actual height 47 (48 - 2)
this.setFontCustom(
- E.toString(require('heatshrink').decompress(atob('AFcH+AHFh/gA4sf4AHFn+AA4t/E43+AwsB/gHFgf4PH4AMgJ9Ngf/Pot//6bF/59F///PokfA4J9DEgIABEwYkB/7DDEgIlFCoRMDEgQsEDoRLEEgpoBA4JhGOIsHZ40PdwwA/L4SjHNAgGCP4cHA4wWDA4aVCA4gGDA4SNBe4IiBA4MPHYRBBEwScCA4d/EQUBaoRKDA4UBLQYECgb+EAgMHYYcHa4MPHoLBCBgMfYgcfBgM/PIc/BgN/A4YECIIQEDHwkDHwQHDGwQHENQUHA4d/QIQnCRIJJCSgYTCA4hqCA4hqCA4hiCA4ZCEA4RFBGYbrFAHxDGSohdDcgagFAAjPCEzicDToU/A4jPCAwbQCBwgrBgIHEFYKrDWoa7DaggA/AC0PAYV+AYSBCgKpCg4DDVIUfAYZ9BToIDDPoKVBAYfARoQDDXgMPFwTIBdYSYCv4LCv7zCXgYKCXAK8CHoUPXgY9Cn/vEYMPEwX/z46Bj4mBgf+n77CDwX4v54EIIIzCOgX/4I+CAQI9BHYQCCQ4I7CRASDBHYQHCv/Aj4+BGYIeBGAI+Bj/8AIIRBQIZjCRIiWBXgYHCPQgHBBgJ6DA4IEBPQaKBGYQ+BbgiCCAGZFDIIUBaAZBCgYHCQAQTBA4SACUwS8DDYQHBQAbVCQAYwBA4SABgYEBPoQCBFgU/CQWACgRDCHwKVCIYX+aYRDCHwMPAgY+Cn4EDHwX/AgY+B8bEFj/HA4RGCn+f94MBv45Cv+fA4J6C//+j5gBGIMBFoJWBQoRMB8E//4DBHIJcBv4HBEwJUCA4ImCj5MBA4KZCPYQHBZgRBCE4LICvwaCXAYA5PgQAEMIQAEUwQADQAJlCAARlBWYIACT4JtDAAMPA4IWESgg8CAwI+EEoPhHwYlCgY+DEoP4g4+DEoPAh4+CEoReBHwUfLYU/CwgMBXARqBHYQCCGoIjBgI+CgZSCHwcHAYY+Ch4lBJ4IbCjhACPwqUBPwqFCPwhQBIQZ+DOAKVFXooHCXop9DFAi8EFAT0GPoYAygwFEgOATISLDwBWDTQc/A4L6CTQKkCVQX+BYIHBDwX+BYIHBVQX8B4KqD+/wA4aBBj/AgK8CQIIJBA4a/BBIMBAgL/BAgUDYgL/BAII7BAQXgAII7BAQXAYQQxBYARrCMwQ0BAgV/HwYECHwgEBgY+EA4MPGwI8BA4UfGwI8BgYHBPofAQYOHPoeAR4QmBHwQHCEwI+CA4RVBHwQHCaggnBDwQHEHoIAEEQIA6v5NFfgSECBwZtEf4IHFOYQHEj4HGDwYHCDwPgv/jA4UHXQS8E/ED/AHDZ4MPSYKlCv+AYwIHDDwL7EgL7DAgTzCEwIpCeYTZBg4CBeYIJBAgICBFgIJBAgICBeYIEDHII0BAgg+EgI5CMocHGwJBCA4MfGwMD/h/BwF/PoQHC451CJIMDSgIjBA4PAA4QmBA4IhBA4JVBgEMA4bUDV4QeCAAf/HoIAENIIApOoIAEW4QAEW4QAEW4QAEWQRSFNIcDfYQMDny8DO4Q7BAQQjCewh+EHwcPToQ+Dv//ewkHUoI+En68DeIS0EHwMf/46CeYYlCHwQ0BKIY+BGgJ4Dh/nGgZZCAwKPEHYLpFDoKuFGgj4JgY0EHwQ0EYhIA6MAkf+BRBLIa5BQAJSCBgP4R4iVB/YHERoIACA4QGDE4SFBAoV/A4MH/ggBWIL7C8EfVoL4DwBHBFYIHBfYIRBAgT7CDgQEBgP4BgUBEIMDDgIMBgYMBg/gBgS5Ch/ABgUPFIMf4EHA4IEBHwUPCgJGCIIM/CgLgCAQJlBFIQFB44HBEIUBQYc/EIIHDAAIuBA4oeBRoSfBLAIHC/gHBEwIXC+AHBZghHBDwQADj4WCAHEPAwpWBKYYOCLwIHELYJUBghlDA4UcQogHBvgeDD4K0DDwIHBWgQeB4CyBh68CUAMf8DeCdIYHDdIfAfYjxCAgj2BAgbHCvwJCIIYCBBIMDHIX4BgUHFwMD+AMCA4Q0BAgg5CHwxICAQY5BdgQHBEgMDIYV/DgR1CA4PwP4KvDRgIACEYIHFWggABMQQHEZwd/Dwq1DHoTFEdooA/ACrBBcAZmC8DTCAATGBaYR+DwDTCRwbYDAASLBCIIGCFgQRBAG4='))),
+ atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP8AAAAAAAAD/AAAAAAAAA/wAAAAAAAAP8AAAAAAAAD/AAAAAAAAA/wAAAAAAAAP8AAAAAAAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAAAAAD/AAAAAAAAP/wAAAAAAAf/8AAAAAAB///AAAAAAH///wAAAAAf///8AAAAB/////AAAAH////8AAAAP////wAAAA/////AAAAB////+AAAAA////4AAAAAP///gAAAAAD//+AAAAAAA//4AAAAAAAP/gAAAAAAAD/AAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///+AAAAAB////8AAAAB/////wAAAA/////+AAAA//////wAAAf/////+AAAH//////wAAD//////+AAB/+AAAf/gAAf+AAAA/8AAH/AAAAH/AAD/gAAAA/4AA/wAAAAH+AAP8AAAAB/gAD+AAAAAf4AA/gAAAAH+AAP4AAAAA/gAD+AAAAAf4AA/wAAAAH+AAP8AAAAB/gAD/AAAAA/4AA/4AAAAP+AAH/AAAAH/AAB/4AAAH/wAAP/wAAP/4AAD//////+AAAf//////AAAD//////gAAAf/////wAAAD/////4AAAAf////4AAAAB////4AAAAAB///gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP8AAAAAAAAH/AAAAAAAAD/gAAAAAAAA/4AAAAAAAAf8AAAAAAAAH+AAAAAAAAD/gAAAAAAAB/wAAAAAAAAf8AAAAAAAAP///////AAD///////wAA///////8AAP///////AAD///////wAA///////8AAP///////AAD///////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAB/AAAA/gAAA/wAAA/4AAAf8AAAf+AAAP/AAAP/gAAH/wAAH/4AAD/8AAD/+AAB//AAA//gAA//wAAf/AAAP/8AAH/AAAH//AAD/gAAD//wAA/wAAB//8AAP8AAA///AAD/AAAf+fwAA/gAAP/n8AAP4AAH/x/AAD+AAD/4fwAA/gAB/8H8AAP8AAf+B/AAD/AAP/AfwAA/4AH/gH8AAH/AH/wB/AAB/8H/4AfwAAP///8AH8AAD////AB/AAAf///gAfwAAD///wAH8AAAf//4AB/AAAD//4AAfwAAAP/8AAH8AAAAf4AAB/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAADgAAAfwAAAB+AAAH8AAAAfwAAB/AAAAH+AAAfwAAAB/wAAH8AAAA/+AAB/AAAAP/gAAfwA4AA/8AAH8AfgAH/AAB/AP8AA/4AAfwD/gAH+AAH8B/4AB/gAB/A/8AAf4AAfwf/AAD+AAH8P/wAA/gAB/H/8AAf4AAfz//gAH+AAH8//4AB/gAB/f//AA/4AAf/+/4Af8AAH//P/AP/AAB//j////gAAf/wf///4AAH/4H///8AAB/8A///+AAAf+AH///AAAH/AA///gAAB/gAD//wAAAfwAAP/wAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAAH/wAAAAAAAH/8AAAAAAAH//AAAAAAAH//wAAAAAAH//8AAAAAAH///AAAAAAH///wAAAAAH///8AAAAAP//9/AAAAAP//8fwAAAAP//4H8AAAAP//4B/AAAAP//4AfwAAAP//4AH8AAAD//4AB/AAAA//4AAfwAAAP/4AAH8AAAD/wAAB/AAAA/wAAAfwAAAPwAH////AADwAB////wAAwAAf///8AAAAAH////AAAAAB////wAAAAAf///8AAAAAH////AAAAAA////wAAAAAAAfwAAAAAAAAH8AAAAAAAAB/AAAAAAAAAfwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcAAAAAAAGAHwAAAB///gB+AAAH///8AfwAAB////AP+AAAf///wD/wAAH///+A/+AAB////gP/gAAf///4A/8AAH/8P8AH/AAB/AD+AA/4AAfwA/gAH+AAH8AfwAB/gAB/AH8AAf4AAfwB/AAH+AAH8AfwAB/gAB/AH8AAf4AAfwB/gAH+AAH8Af4AB/gAB/AH/AA/wAAfwB/4Af8AAH8AP/AP/AAB/AD////gAAfwAf///wAAH8AD///8AAB/AA///+AAAfwAH///AAAAAAA///gAAAAAAD//gAAAAAAAP/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB///4AAAAAH////wAAAAH/////AAAAD/////4AAAB//////AAAA//////4AAAf//////AAAP//////4AAD/8D/w/+AAB/4B/wD/wAAf8A/wAf8AAP+AP4AD/gAD/AD+AAf4AA/wB/AAH+AAP4AfwAB/gAD+AH8AAf4AA/gB/AAH+AAP4AfwAB/gAD+AH+AAf4AA/wB/gAH+AAP8Af8AD/gAD/gH/gB/wAAf8A/8A/8AAH/AP///+AAB/gB////gAAPwAP///wAAB4AD///4AAAMAAf//8AAAAAAD//+AAAAAAAP/+AAAAAAAA/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfwAAAAAAAAH8AAAAAAAAB/AAAAAAAAAfwAAAAAAAAH8AAAAAAAAB/AAAAABwAAfwAAAAB8AAH8AAAAD/AAB/AAAAD/wAAfwAAAH/8AAH8AAAH//AAB/AAAP//wAAfwAAP//8AAH8AAf//+AAB/AAf//8AAAfwA///8AAAH8A///4AAAB/A///4AAAAfx///wAAAAH9///wAAAAB////gAAAAAf///gAAAAAH///AAAAAAB///AAAAAAAf/+AAAAAAAH/+AAAAAAAB/8AAAAAAAAf8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAf/AAAAAP+Af/8AAAAP/4P//wAAAP//P//+AAAH//////wAAB//////8AAA///////gAAf//////8AAH////gP/AAD/wf/wA/wAA/4D/4AP+AAP8Af8AB/gAD/AH/AAf4AA/gA/wAH+AAP4AP4AA/gAD+AD/AAP4AA/gA/wAH+AAP8Af8AB/gAD/AH/AAf4AA/4D/4AP+AAP/B//AH/AAB////4D/wAAf//////8AAD//////+AAAf//////AAAH//////wAAA//8///4AAAD/+D//8AAAAP+Af/8AAAAAAAB/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/gAAAAAAAB//AAAAAAAB//8AAAAAAB///gAAgAAA///8AAcAAAf///gAPAAAH///8AH4AAD////AD/AAB/+H/4B/wAAf+Af+Af8AAP+AB/wD/gAD/gAf8Af4AA/wAD/AH+AAP8AA/wB/gAD+AAH8AP4AA/gAB/AD+AAP4AAfwB/gAD+AAH8Af4AA/wAD/AH+AAP8AA/gD/gAD/gAf4A/wAAf8AP8A/8AAH/gH/Af/AAA///////gAAP//////wAAB//////8AAAP/////+AAAB//////AAAAP/////AAAAA/////gAAAAD////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wA/wAAAAAP8AP8AAAAAD/AD/AAAAAA/wA/wAAAAAP8AP8AAAAAD/AD/AAAAAA/wA/wAAAAAP8AP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=='),
46,
- atob("EhooGyUkJiUnISYnFQ=="),
- 63+(scale<<8)+(1<<16)
+ atob("ExspGyUkJiQnISYnFQ=="),
+ 62+(scale<<8)+(1<<16)
);
return this;
};
-Graphics.prototype.setXLargeFont = function(scale) {
- // Actual height 53 (55 - 3)
- this.setFontCustom(
- E.toString(require('heatshrink').decompress(atob('AHM/8AIG/+AA4sD/wQGh/4EWQA/AC8YA40HNA0BRY8/RY0P/6LFgf//4iFA4IiFj4HBEQkHCAQiDHIIZGv4HCFQY5BDAo5CAAIpDDAfACA3wLYv//hsFKYxcCMgoiBOooiBQwwiBS40AHIgA/ACS/DLYjYCBAjQEBAYQDBAgHDUAbyDZQi3CegoHEVQQZFagUfW4Y0DaAgECaIJSEFYMPbIYNDv5ACGAIrBCgJ1EFYILCAAQWCj4zDGgILCegcDEQRNDHIIiCHgZ2BEQShFIqUDFYidCh5ODg4NCn40DAgd/AYR5BDILZEAAIMDAAYVCh7aHdYhKDbQg4Dv7rGBAihFCAwIDCAgA/AB3/eoa7GAAk/dgbVGDJrvCDK67DDIjaGdYpbCdYonCcQjjDEVUBEQ4A/AEMcAYV/NAUHcYUDawd/cYUPRYSmBBgaLBToP8BgYiBSgIiCj4iCg//EQSuDW4IMDVwYiCBgIiBBgrRDCATeBaIYqCv70DCgT4CEQMfIgQZBBoRnDv/3EQIvBDIffEQMHFwReBRYUfOgX/+IiDKIeHEQRRECwUHKwIuB8AiDIoJEBCwZFCv/4HIZaBIgPAEQS2CUYQiCD4SABEQcfOwIZBEQaHBO4RcEAAI/BEQQgBSIQiDTIRZBEQZuBVYQiDHoKWCEQQICFQIiDBAQeCEQQA/AANwA40BLIJ5BO4JWCBAUPAYR5En7RBUIQECN4SYCQQIiEh6CCEQk/BoQiBgYeCBoTrCAgT0CCgIfCFYQiBg4IBGgIiDj6rBg4rCBYLRDFYIiBbYIfBLgQiBIQYiD4JCCLgf/bQIWDBYV/EQV/BYXz/5FBgIiD5//IowZBD4M/NAX/BIPgDIJoC//5GgKUDn//4f/8KLE/wTBAAI8BEQPwj4HBVwYmBDgIZDN4QZCGYKJCHQP/JoSgCBATrCh5dBKITVDG4gICAAbvDAH5SCL4QADK4J5CCAiTCCAp1BCAqCDCAgiGCAIiFCAQiFeoIiFg6/FCAgiECAXnEQgQB/kfEQYQC4F/EQYQCgIiDfoIQBg4iDCAUAEQZUCcgIiDDIIQBEQhuBBoIiENoYiFDwQiECAQiFwEBPQQNCAQKDDEYMDDoMfRh4iGUwqvEESBiBaQ5oEbgr0FNAo+EEIwA+oAHGgJoFRAMHe4L0CAALNBBAT0BfwScDCAXweAL0DWgUPQYQiDwF/QYQiC/zTB+C0FBAL0CEQYIBGgMPCgIxBg4rCJIKsCh5IBBwTPCj4WBgYLBZ4V/MAIiBBQQrBEQYtCBYQiCO4QLFCwgiDIQIiGIoMHEQpFBn5FFD4JoENwRoGDgSUCAoKfBw//DgIiCT4auCFwN/T4RRET4TaCEQKoCDIQiCGgK/DAAQICdYQACHoIqCBAoQFEwIhFAH4AFQIROEj4IGXwIIGNwIACbgIhEBAiRCVwoqDTogHEW4QZFXgIZB/z9Cv49CF4MPBwI0Ca4LlB8ATCJoP4AoINDfQPAg7PBg4cBBwUfD4MfFYILCCwgOCf4QLEwEPCwILCgJaBn4WBBYQxCIQQiD+EDCYI5CBYRQBIo4fBMQIuBC4N/NAv8AoIcBSgU/FYIIBZIYrCW4hOCXIQZCgYUBv7jEh4uBZAscewZ8CgEgUYT0EEoQIBA4gICFQQIEHYQA+KQzdDAArdCAArpCEScHaIQiEvwiGe4QiFUwQiEbgIiFYIL0DEQTkBEQrJEEQc/cYYiCg4HBDIQiCfoRoEHQLaDEQQHBbQYiBCAT8Dn/BCAoXBJYP/OgZKC/6OEEARLCEQZLEEQZLEEQjKFEQI6EEQZLDEQbsGEQLjGYYYA/JIxzEg/AfgJSDAoPgfgiDC8COFAoPnaQj6CAAR+CW4TCFA4i6CDIqhCDIfwHoYHCYIN/GgKuBJ4JDBFYUf/C5CBYIZBv/Ag4ZBg4rBBYQTBAQIcBg4FBn5UBAQUfFwIfCEQeAgYfBAQUBFAKbCAQQiCGwIiE+A2BwBFNwE/AoM/EQJoIWwKCCh4cBFYKUERYV/W46uHFYIZGaJA0B/glBGYT0JIITiEMIJvCFQQAEHYQA/ABBlEOIhdGQAIRFSgQIBgQICn4IB8EAjiBCUYglCbQYeBEoQZCTwM/CYIZD/gEBUwIzBJ4UHYAU/EwIrBh4rCAoIXCn4rBCgUDAQN/FYMfBYIXBCYJnCBYXggf8HgQLCwEPEQQuBgJOECwILDCwgiLHIUHBYJFGD4IxBgYWCn4rBBwJoFDIYNBCgPADgKHBRYfDBQN/GAIrBToTLDVwYACDILiCWAb8DAAYzBYAjTCAAI9BAARNCBAoqCBAgQDFgbYCAH4AufgQACf4T8CAAT/CfgQACBwITCAAYOBCYQioh4iEAHQA=='))),
- 46,
- atob("FR4uHyopKyksJSssGA=="),
- 70+(scale<<8)+(1<<16)
- );
-};
-
Graphics.prototype.setMediumFont = function(scale) {
// Actual height 41 (42 - 2)
this.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAA/AAAAAAAA/AAAAAAAA/AAAAAAAA/AAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAAAAAAB/AAAAAAAP/AAAAAAD//AAAAAA///AAAAAP///AAAAB///8AAAAf///AAAAH///wAAAB///+AAAAH///gAAAAH//4AAAAAH/+AAAAAAH/wAAAAAAH8AAAAAAAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///8AAAAH////AAAAP////wAAAf////4AAA/////8AAB/////+AAD/gAAH+AAD+AAAD/AAH8AAAB/AAH4AAAA/gAH4AAAAfgAH4AAAAfgAPwAAAAfgAPwAAAAfgAPwAAAAfgAHwAAAAfgAH4AAAAfgAH4AAAA/gAH8AAAA/AAD+AAAD/AAD/gAAH/AAB/////+AAB/////8AAA/////4AAAf////wAAAH////gAAAB///+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAAAAfwAAAAAAA/gAAAAAAA/AAAAAAAB/AAAAAAAD+AAAAAAAD8AAAAAAAH8AAAAAAAH//////AAH//////AAH//////AAH//////AAH//////AAH//////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AAA/AAAP4AAB/AAAf4AAD/AAA/4AAD/AAB/4AAH/AAD/4AAP/AAH/AAAf/AAH8AAA//AAH4AAB//AAP4AAD//AAPwAAH+/AAPwAAP8/AAPwAAf4/AAPwAA/4/AAPwAA/w/AAPwAB/g/AAPwAD/A/AAP4AH+A/AAH8AP8A/AAH/A/4A/AAD///wA/AAD///gA/AAB///AA/AAA//+AA/AAAP/8AA/AAAD/wAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAH4AAAHwAAH4AAAH4AAH4AAAH8AAH4AAAP+AAH4AAAH+AAH4A4AB/AAH4A+AA/AAH4B/AA/gAH4D/AAfgAH4H+AAfgAH4P+AAfgAH4f+AAfgAH4/+AAfgAH5/+AAfgAH5//AAfgAH7+/AA/gAH/8/gB/AAH/4f4H/AAH/wf//+AAH/gP//8AAH/AH//8AAH+AD//wAAH8AB//gAAD4AAf+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAAAAD/AAAAAAAP/AAAAAAB//AAAAAAH//AAAAAAf//AAAAAB///AAAAAH///AAAAAf/8/AAAAB//w/AAAAH/+A/AAAA//4A/AAAD//gA/AAAH/+AA/AAAH/4AA/AAAH/gAA/AAAH+AAA/AAAHwAAA/AAAHAAf///AAEAAf///AAAAAf///AAAAAf///AAAAAf///AAAAAf///AAAAAAA/AAAAAAAA/AAAAAAAA/AAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAAP/AHgAAH///AP4AAH///gP8AAH///gP8AAH///gP+AAH///gD/AAH/A/AB/AAH4A/AA/gAH4A+AAfgAH4B+AAfgAH4B+AAfgAH4B8AAfgAH4B8AAfgAH4B+AAfgAH4B+AAfgAH4B+AA/gAH4B/AA/AAH4A/gD/AAH4A/4H+AAH4Af//+AAH4AP//8AAH4AP//4AAHwAD//wAAAAAB//AAAAAAAf8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///8AAAAD////AAAAP////wAAAf////4AAA/////8AAB/////+AAD/gP4H+AAD/AfgD/AAH8A/AB/AAH8A/AA/gAH4B+AAfgAH4B+AAfgAPwB8AAfgAPwB8AAfgAPwB+AAfgAPwB+AAfgAH4B+AAfgAH4B/AA/gAH8B/AB/AAH+A/wD/AAD+A/8P+AAB8Af//+AAB4AP//8AAAwAH//4AAAAAD//gAAAAAA//AAAAAAAP4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAPwAAAAAAAPwAAAAAAAPwAAAAAAAPwAAAAHAAPwAAAA/AAPwAAAD/AAPwAAAf/AAPwAAB//AAPwAAP//AAPwAA//8AAPwAH//wAAPwAf/+AAAPwB//4AAAPwP//AAAAPw//8AAAAP3//gAAAAP//+AAAAAP//wAAAAAP//AAAAAAP/4AAAAAAP/gAAAAAAP+AAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+AAAAH+A//gAAAf/h//4AAA//z//8AAB/////+AAD/////+AAD///+H/AAH+H/4B/AAH8B/wA/gAH4A/gAfgAH4A/gAfgAPwA/AAfgAPwA/AAfgAPwA/AAfgAPwA/AAfgAH4A/gAfgAH4A/gAfgAH8B/wA/gAH/H/4B/AAD///+H/AAD/////+AAB/////+AAA//z//8AAAf/h//4AAAH+A//gAAAAAAH+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAAD/8AAAAAAP/+AAAAAAf//AAcAAA///gA8AAB///wB+AAD/x/4B/AAD+AP4B/AAH8AH8A/gAH4AH8A/gAH4AD8AfgAP4AD8AfgAPwAB8AfgAPwAB8AfgAPwAB8AfgAPwAB8AfgAH4AD8AfgAH4AD4A/gAH8AH4B/AAD+APwD/AAD/g/wP+AAB/////+AAA/////8AAAf////4AAAP////wAAAH////AAAAA///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8APwAAAAD8APwAAAAD8APwAAAAD8APwAAAAD8APwAAAAD8APwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="), 46, atob("DxcjFyAfISAiHCAiEg=="), 54+(scale<<8)+(1<<16));
@@ -62,211 +54,136 @@ Graphics.prototype.setMediumFont = function(scale) {
Graphics.prototype.setSmallFont = function(scale) {
// Actual height 28 (27 - 0)
- this.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//84D//zgP/+GAAAAAAAAAAAAAAAAAAAD4AAAPgAAA+AAAAAAAAAAAAA+AAAD4AAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAg4AAHDgAAcOCABw54AHD/gAf/8AD/8AB//gAP8OAA9w4YCHD/gAcf+AB//gAf/gAP/uAA/w4ADnDgAAcOAABw4AAHAAAAcAAAAAAAAAAAAAAAIAA+A4AH8HwA/4PgHjgOAcHAcBwcBw/BwH78DgfvwOB8HA4HAOBw8A+HngB4P8ADgfgAAAYAAAAAAAAAAB4AAAf4AQB/gDgOHAeA4cDwDhweAOHDwA88eAB/nwAD88AAAHgAAA8AAAHn4AA8/wAHnvgA8cOAHhg4A8GDgHgcOA8B74BgD/AAAH4AAAAAAAAAAAAAAAAAMAAAH8AD8/4Af/3wB/8HgODwOA4HA4DgODgOAcOA4A44DwDzgHAH8AMAPwAQP+AAA/8AAAB4AAADAAAAAA+AAAD4AAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/8AD//+A/+/+H4AD98AAB3gAADIAAAAAAAAAAAAAIAAABwAAAXwAAHPwAB8P8D/gP//4AH/8AAAAAAAAAAAAAAAAAAAAAAAAGAAAA4gAAB/AAAH8AAD/AAAP8AAAH4AAAfwAADiAAAOAAAAAAAAAAAAAAGAAAAYAAABgAAAGAAAAYAAABgAAD/+AAP/4AABgAAAGAAAAYAAABgAAAGAAAAYAAAAAAAAAAAAAADkAAAPwAAA/AAAAAAAAAAAAAAAAAAAAAAAAABgAAAGAAAAYAAABgAAAGAAAAYAAABgAAAGAAAAYAAAAAAAAAAAAAAAAAAAAAAADgAAAOAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAA4AAA/gAA/+AA//AA//AAP/AAA/AAADAAAAAAAAAAAAAAAAAAA//gAP//gB///AHgA8A8AB4DgADgOAAOA4AA4DgADgPAAeAeADwB///AD//4AD/+AAAAAAAAAAAAAAAA4AAAHgAAAcAAADwAAAP//+A///4D///gAAAAAAAAAAAAAAAAAAAAAAYAeADgD4AeAfAD4DwAfgOAD+A4Ae4DgDzgOAeOA4Dw4DweDgH/wOAP+A4AfwDgAAAAAAAAAAAAIAOAA4A4ADwDggHAOHgOA48A4DnwDgO/AOA7uA4D84HgPh/8A8H/gDgH8AAACAAAAAAAAAAAAAHgAAB+AAA/4AAP7gAD+OAA/g4AP4DgA+AOADAA4AAB/+AAH/4AAf/gAADgAAAOAAAAAAAAAAAAAAAAD4cAP/h4A/+HwDw4HgOHAOA4cA4DhwDgOHAOA4cA4Dh4HAOD58A4H/gAAP8AAAGAAAAAAAAAAAAAAAAD/+AAf/8AD//4AePDwDw4HgOHAOA4cA4DhwDgOHAOA4cB4Bw8PAHD/8AIH/gAAH4AAAAAAAAAADgAAAOAAAA4AAYDgAHgOAD+A4B/wDgf4AOP+AA7/AAD/gAAP4AAA8AAAAAAAAAAAAAAAAAAeH8AD+/4Af//wDz8HgOHgOA4OA4Dg4DgODgOA4eA4Dz8HgH//8AP7/gAeH8AAAAAAAAAAAAAAAA+AAAH+AgB/8HAHh4cA8Dg4DgODgOAcOA4Bw4DgODgPA4eAeHDwB///AD//4AD/+AAAAAAAAAAAAAAAAAAAAAAAAAAODgAA4OAADg4AAAAAAAAAAAAAAAAAAAAAAAAAAAAABwA5AHAD8AcAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAB8AAAP4AAB5wAAPDgAB4HAAHAOAAIAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGMAAAYwAABjAAAGMAAAYwAABjAAAGMAAAYwAABjAAAGMAAAYwAABjAAAGMAAAYwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAEAAcA4AB4HAADw4AADnAAAH4AAAPAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAB8AAAHgAAA4AAADgDzgOA/OA4D84DgeAAPHwAAf+AAA/wAAB8AAAAAAAAAAAAAAAAAAD+AAB/+AAP/8AB4B4AOABwBwADgHB8OA4P4cDhxxwMGDDAwYMMDBgwwOHHHA4f4cDh/xwHAHCAcAMAA8AwAB8PAAD/4AAD/AAAAAAAAAAAAAACAAAB4AAB/gAA/8AAf+AAP/wAH/nAA/gcADwBwAPwHAA/4cAA/9wAAf/AAAP/AAAD/gAAB+AAAA4AAAAAAAAAAAAAAD///gP//+A///4DgcDgOBwOA4HA4DgcDgOBwOA4HA4Dg8DgPHwOAf/h4A///AB8f4AAAfAAAAAAAP+AAD/+AAf/8AD4D4AeADwBwAHAOAAOA4AA4DgADgOAAOA4AA4DgADgOAAOAcABwB4APAD4D4AHgPAAOA4AAAAAAAAAAAAAAAP//+A///4D///gOAAOA4AA4DgADgOAAOA4AA4DgADgOAAOA8AB4BwAHAHwB8AP//gAP/4AAP+AAAAAAAAAAAAAAAA///4D///gP//+A4HA4DgcDgOBwOA4HA4DgcDgOBwOA4HA4DgcDgOBgOA4AA4AAAAAAAAAAAAAAD///gP//+A///4DgcAAOBwAA4HAADgcAAOBwAA4HAADgcAAOAwAA4AAAAAAAAAf+AAD/+AA//+ADwB4AeADwDwAHgOAAOA4AA4DgADgOAAOA4AA4DgMDgPAweAcDBwB8MfADw/4AHD/AAAPwAAAAAAAAAAAAAAAP//+A///4D///gABwAAAHAAAAcAAABwAAAHAAAAcAAABwAAAHAAAAcAAABwAA///4D///gP//+AAAAAAAAAAAAAAAAAAAD///gP//+A///4AAAAAAAAAAAADgAAAPAAAA+AAAA4AAADgAAAOAAAA4AAAHgP//8A///wD//8AAAAAAAAAAAAAAAAAAAA///4D///gP//+AAHAAAA+AAAP8AAB54AAPDwAB4HgAPAPAB4AfAPAA+A4AA4DAABgAAACAAAAAAAAAAP//+A///4D///gAAAOAAAA4AAADgAAAOAAAA4AAADgAAAOAAAA4AAADgAAAAAAAAAAAAAAP//+A///4D///gD+AAAD+AAAB+AAAB/AAAB/AAAB/AAAB+AAAH4AAB+AAA/gAAP4AAD+AAA/AAAfwAAD///gP//+A///4AAAAAAAAAAAAAAAAAAAP//+A///4D///gHwAAAPwAAAPgAAAfgAAAfAAAAfAAAA/AAAA+AAAB+AAAB8A///4D///gP//+AAAAAAAAAAAP+AAD/+AAf/8AD4D4AeADwBwAHAOAAOA4AA4DgADgOAAOA4AA4DgADgOAAOAcABwB4APAD4D4AH//AAP/4AAP+AAAAAAAAAAAP//+A///4D///gOAcAA4BwADgHAAOAcAA4BwADgHAAOAcAA4DgAD4eAAH/wAAP+AAAPgAAAAAAAA/4AAP/4AB//wAPgPgB4APAHAAcA4AA4DgADgOAAOA4AA4DgADgOAAOA4AO4BwA/AHgB8APgPwAf//gA//uAA/4QAAAAAAAAAA///4D///gP//+A4BwADgHAAOAcAA4BwADgHAAOAcAA4B8ADgP8APh/8Af/H4A/4HgA+AGAAAAAAAAAAAABgAHwHAA/g+AH/A8A8cBwDg4DgODgOA4OA4DgcDgOBwOA4HA4DwODgHg4cAPh/wAcH+AAwPwAAAAADgAAAOAAAA4AAADgAAAOAAAA4AAAD///gP//+A///4DgAAAOAAAA4AAADgAAAOAAAA4AAADgAAAAAAAAAAAAAAAAAP//AA///AD//+AAAB8AAABwAAADgAAAOAAAA4AAADgAAAOAAAA4AAAHgAAA8A///gD//8AP//gAAAAAAAAAAIAAAA8AAAD+AAAH/AAAD/wAAB/4AAA/8AAAf4AAAPgAAB+AAA/4AAf+AAP/AAH/gAD/wAAP4AAA4AAAAAAAAPAAAA/gAAD/4AAA/+AAAf/AAAH/gAAB+AAAf4AAf/AAf/AAP/gAD/gAAPwAAA/4AAA/+AAAf/AAAH/wAAB/gAAB+AAB/4AA/+AA/+AA/+AAD/AAAPAAAAgAAAAAAAAMAAGA4AA4D4APgHwB8APwfAAPn4AAf+AAAfwAAB/AAAf+AAD4+AA/B8AHwB8A+AD4DgADgMAAGAwAAADwAAAPwAAAPwAAAfgAAAfgAAAf/4AAf/gAH/+AB+AAAPwAAD8AAA/AAADwAAAMAAAAgAAAAAAAAMAACA4AA4DgAPgOAD+A4Af4DgH7gOB+OA4Pw4Dj8DgO/AOA/4A4D+ADgPgAOA4AA4DAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/////////gAAAOAAAA4AAADAAAAAAAAAAAAAAAAAAAAAAA4AAAD+AAAP/gAAH/4AAB/+AAAf+AAAH4AAABgAAAAAAAAADAAAAOAAAA4AAADgAAAP////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAADgAAAcAAADgAAAcAAADgAAAcAAAB4AAADwAAADgAAAHAAAAOAAAAYAAAAAAAAAAAAAAAAAAAAMAAAAwAAADAAAAMAAAAwAAADAAAAMAAAAwAAADAAAAMAAAAwAAADAAAAMAAAAwAAADAAAAMAAAAwAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAHH8AA8/4AHzjgAcMOABxwYAHHBgAccOABxwwAHGHAAP/4AA//4AA//gAAAAAAAAAAAAAAAAAAA///4D///gP//+AA4BwAHADgAcAOABwA4AHADgAcAOAB4B4ADwPAAP/8AAf/AAAf4AAAAAAAAAAAAPwAAD/wAAf/gADwPAAeAeABwA4AHADgAcAOABwA4AHADgAeAeAA8DwABwOAADAwAAAAAAAAAAAA/AAAP/AAD//AAPA8AB4B4AHADgAcAOABwA4AHADgAcAOAA4BwD///gP//+A///4AAAAAAAAAAAAAAAAPwAAD/wAAf/gAD2PAAeYeABxg4AHGDgAcYOABxg4AHGDgAeYeAA/jwAB+OAAD4wAABgAAAAAAAAAAABgAAAGAAAB//+Af//4D///gPcAAA5gAADGAAAMYAAAAAAAAAPwAAD/wMA//w4DwPHgeAePBwA4cHADhwcAOHBwA4cHADhwOAcPB///4H///Af//wAAAAAAAAAAAAAAAAAAD///gP//+AA//4ADgAAAcAAABwAAAHAAAAcAAABwAAAHgAAAP/+AAf/4AA//gAAAAAAAAAAAAAAMf/+A5//4Dn//gAAAAAAAAAAAAAAAAAAHAAAAfn///+f//+5///wAAAAAAAAAAAAAAAAAAP//+A///4D///gAAcAAAD8AAAf4AADzwAAeHgAHwPAAeAeABgA4AEABgAAAAAAAAAD///gP//+A///4AAAAAAAAAAAAAAAAAAAAf/+AB//4AH//gAOAAABwAAAHAAAAcAAABwAAAHgAAAP/+AA//4AB//gAOAAABwAAAHAAAAcAAABwAAAHgAAAf/+AA//4AA//gAAAAAAAAAAAAAAAf/+AB//4AD//gAOAAABwAAAHAAAAcAAABwAAAHAAAAeAAAA//4AB//gAD/+AAAAAAAAAAAAAAAAD8AAA/8AAH/4AA8DwAHgHgAcAOABwA4AHADgAcAOABwA4AHgHgAPh8AAf/gAA/8AAA/AAAAAAAAAAAAAAAAB///8H///wf///A4BwAHADgAcAOABwA4AHADgAcAOAB4B4ADwPAAP/8AAf/AAAf4AAAAAAAAAAAAPwAAD/wAA//wADwPAAeAeABwA4AHADgAcAOABwA4AHADgAOAcAB///8H///wf///AAAAAAAAAAAAAAAAAAAH//gAf/+AB//4ADwAAAcAAABwAAAHAAAAcAAAAAAAAAAMAAHw4AA/jwAH+HgAcYOABxw4AHHDgAcMOABw44AHjjgAPH+AA8fwAAw+AAAAAABgAAAGAAAAcAAAf//wB///AH//+ABgA4AGADgAYAOABgA4AAAAAAAAAAAAAAAH/AAAf/wAB//wAAB/AAAAeAAAA4AAADgAAAOAAAA4AAADgAAAcAB//4AH//gAf/+AAAAAAAAAAAAAAABwAAAH4AAAf8AAAP8AAAH+AAAD+AAAD4AAA/gAAf8AAP+AAH/AAAfgAABwAAAAAAAAAAAABwAAAH8AAAf+AAAP/gAAD/gAAB+AAAf4AAP8AAP+AAB/AAAH4AAAf8AAAP+AAAD/gAAB+AAAf4AAf/AAP/AAB/gAAHgAAAQAAABAAIAHADgAeAeAA8HwAB8+AAD/gAAD8AAAPwAAD/gAAfPgADwfAAeAeABwA4AEAAgAAAAABAAAAHgAAAfwAAA/wAAAf4BwAP4/AAP/8AAP+AAD/AAB/wAA/4AAP8AAB+AAAHAAAAQAAAAAAIAHADgAcAeABwD4AHA/gAcHuABx84AHPDgAf4OAB/A4AHwDgAeAOABgA4AEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAH4Af//////n//AAAA4AAADgAAAAAAAAAAAAAAAAAP//+A///4D///gAAAAAAAAAAAAAAAAAAA4AAADgAAAOAAAA//5/9////wAH4AAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAeAAAD4AAAOAAAA4AAADgAAAHAAAAcAAAA4AAADgAAAOAAAD4AAAPAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), 32, atob("BgkMGhEZEgYMDAwQCAwICxILEBAREBEOEREJCREVEQ8ZEhEUExAOFBQHDREPGBMUERQSEhEUERsREBIMCwwTEg4QERAREQoREQcHDgcYEREREQoPDBEPFg8PDwwIDBMc"), 28+(scale<<8)+(1<<16));
+ this.setFontCustom(
+ atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/+cB//5wH//nAAAAAAAAAAAAAAAAAAAB8AAAHwAAAfAAAAAAAAAAAAAfAAAB8AAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAAQcAADhwAAOHBAA4c8ADh/wAP/+AB/+AA//wAH+HAAe4cMBDh/wAOP/AA//wAP/wAH/3AAf4cABzhwAAOHAAA4cAADgAAAOAAAAAAAAAAAAAAAAAAAAwAH8HwA/4PgD/geAePA8BwcBw/BwH78DgfvwOB+HA4HAeBwcA8HDgB4f+ADg/wAGB+AAAAAAAAAAAAAAAH4AAA/wBwHngPAcOB4Bw4PAHDh4AcOPAA/x4AD/PAADx4AAAPAAAB5wAAPPwAB5/gAPOPAB4wcAPDBwB4MHAPA4cA4B/gBAH8AAAHAAAAAAAAAAAAAPAAHD/AB/f+AP/x4B4+DwHB4HAcDwcBwHhwHAPHAcAccB4A5wDgB+AGA/4AAH/AAAf+AAAA8AAABgAAAAAfAAAB8AAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/8AD//+A/+/+H4AD98AAB3gAADIAAAAAAAAAAAAAIAAABwAAAXwAAHPwAB8P8D/gP//4AH/8AAAAAAAAAAAAAAAAAAAAAAAAHAAAAcwAAA/gAAb8AAB/gAAH+AAAD+AAAOwAABxAAADAAAAAAAAAAAAAADAAAAMAAAAwAAADAAAAMAAAAwAAB//AAH/8AAAwAAADAAAAMAAAAwAAADAAAAMAAAAAAAAAAAAAABwAAAHIAAAfgAAB8AAAAAAAAAAAAAAAAAAAAAwAAADAAAAMAAAAwAAADAAAAMAAAAwAAADAAAAMAAAAAAAAAAAAAAAAAAAAAAABwAAAHAAAAcAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAA/wAA//AA//AA//AAH/AAAfAAABAAAAAAAAAAAAAAAAAAAf/wAH//wA///gDgAOAcAAcBwABwHAAHAcAAcBwABwHgAPAPAB4Af//AA//4AA/+AAAAAAAAAAAAAAAAMAAABwAAAOAAAB4AAAH///Af//8B///wAAAAAAAAAAAAAAAAAAAAwAcAPADwB8AfAPAB8B4APwHAB/AcAPcBwB5wHAPHAcB4cA8PBwD/4HAH/AcAHwBwAAAAAAAAAAAAGAHAAcAcAB4BwYDwHDwHAceAcBz4BwHfgHAf3AcB+eDwHw/+AeB/wBwD+AAAAAAAAAAAAAAAAABwAAAfAAAP8AAD/wAA/nAAP4cAD+BwAfgHAB4AcAEA//AAD/8AAP/wAABwAAAHAAAAMAAAAAAAAAAAAAEAH/w4Af/D4B/8HgHDgPAcOAcBw4BwHDgHAcOAcBw8DwHB4eAcH/wBgP+AAAPwAAAAAAAAAAAAAAAB//AAf//AD//+AOHB4Bw4BwHDgHAcOAcBw4BwHDgHAcPA8A4eHgDh/8AEB/gAAD4AAAAAAAAAABwAAAHAAAAcAAMBwADwHAB/AcA/4BwP8AHH/AAd/gAB/wAAH8AAAeAAAAAAAAAAAAAAAEAAPD+AB/f8AP//4B4+DwHDwHAcHAcBwcBwHBwHAcPAcB/+DgD//+AH5/wACB8AAAAAAAAAAAAAAAAEAAAD+AAAf+DAD74OAODw8BwHBwHAOHAcA4cBwDBwHAcHAeBw8A+ePgB//8AD//gAB/wAAAAAAAAAAAAAAAAAAAAAHBwAAcHAABwcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4AcgDgB+AOAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAeAAAD8AAAf4AADzwAAeHgADwPAAGAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADGAAAMYAAAxgAADGAAAMYAAAxgAADGAAAMYAAAxgAADGAAAMYAAAxgAADGAAAMYAAAxgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABACAAOAcAA8DgAB4cAABzgAAD8AAAHgAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAHgAAA+AAAHgAAAcAAABwD5wHAfnAcD8cBweAAHzwAAP+AAAfwAAAcAAAAAAAAAAAAAAAAAAB/AAA//AAH/+AA8A8AHAA4A4ABwDg+HAcH8OBw444GDBhgYMGGBgwYYHDjjgcP8OBw/44DgDhAOAGAAeAYAA+HgAB/8AAB/gAAAAAAAAAAAAABAAAA8AAAfwAAP/AAH/gAD/4AB/zgAf4OAB8A4AHwDgAf4OAA/84AAP/gAAH/AAAD/gAAB/AAAA8AAAAQAAAAAAAAAB///wH///Af//8BwOBwHA4HAcDgcBwOBwHA4HAcDgcBweBwHj4HAP/58Afz/gAcH8AAAPAAAAAAAH/AAB//AAf//AB4A8APAB4B4ADwHAAHAcAAcBwABwHAAHAcAAcBwABwHAAHAOAA4A8AHgB8B8ADwHgADAYAAAAAAAAAAAAAAAH///Af//8B///wHAAHAcAAcBwABwHAAHAcAAcBwABwHAAHAeAA8A8AHgB8B+AD//gAH/8AAD/AAAAAAAAAAAAAAAAf//8B///wH///AcDgcBwOBwHA4HAcDgcBwOBwHA4HAcDgcBwOBwHAAHAcAAcAAAAAAAAAAAAAAB///wH///Af//8BwOAAHA4AAcDgABwOAAHA4AAcDgABwOAAHAAAAcAAAAAAAAAP/AAB//AAf//AB4A8APAB4B4ADwHAAHAcAAcBwABwHAAHAcAAcBwGBwHgYPAOBg4A+GPgB4f8ADh/gAAH4AAAAAAAAAAAAAAAH///Af//8B///wAA4AAADgAAAOAAAA4AAADgAAAOAAAA4AAADgAAAOAAAA4AAf//8B///wH///AAAAAAAAAAAAAAAAAAAB///wH///Af//8AAAAAAAAAAAABgAAAHgAAAeAAAA8AAABwAAAHAAAAcAAABwH///Af//4B///AAAAAAAAAAAAAAAAAAAAf//8B///wH///AAHgAAA/AAAH+AAA88AAHh8AA8D4AHgDwA8AHgHgAPAYAAcBAAAwAAABAAAAAAAAAAH///Af//8B///wAAAHAAAAcAAABwAAAHAAAAcAAABwAAAHAAAAcAAABwAAAAAAAAAAAAAAH///Af//8B///wB/AAAB/AAAA/AAAA/gAAA/gAAA/gAAA/AAAD8AAA/AAAfwAAH8AAB/AAAfgAAP4AAB///wH///Af//8AAAAAAAAAAAAAAAAAAAH///Af//8B///wD8AAAD4AAAH4AAAHwAAAPwAAAPgAAAPgAAAfAAAAfAAAA/Af//8B///wH///AAAAAAAAAAAH/AAB//AAf//AB4A8APAB4B4ADwHAAHAcAAcBwABwHAAHAcAAcBwABwHAAHAOAA4A8AHgB+D8AD//gAH/8AAD+AAAAAAAAAAAH///Af//8B///wHAOAAcA4ABwDgAHAOAAcA4ABwDgAHAeAAeBwAA+fAAD/4AAD/AAADgAAAAAAAAf8AAH/8AB//8AHgDwA8AHgHgAPAcAAcBwABwHAAHAcAAcBwABwHAAnAcAHcA4AfgDwA+AH4P4AP//wAf/3AAP4AAAAAAAAAAAf//8B///wH///AcA4ABwDgAHAOAAcA4ABwDgAHAOAAcB+AB4H+AD59/AP/h8AP8BwAOABAAAAAAAAAAAAAwAD4HwA/4fAD/geAePA8BwcBwHBwHAcDgcBwOBwHA4HAcDgcA4HDwD4eeAHw/4AOD/AAIDwAAAAABwAAAHAAAAcAAABwAAAHAAAAcAAABwAAAH///Af//8B///wHAAAAcAAABwAAAHAAAAcAAABwAAAGAAAAAAAAAAAAAH//wAf//gB///AAAAeAAAA8AAABwAAAHAAAAcAAABwAAAHAAAAcAAADgAAAeAf//wB//+AH//gAAAAAAAAAAGAAAAfAAAB/gAAB/wAAA/4AAAf8AAAP/AAAH8AAADwAAA/AAAf8AAP+AAP/AAH/gAB/wAAH4AAAcAAABAAAAHwAAAf4AAA/+AAAP/gAAH/wAAB/wAAA/AAAf8AAf/AAP/gAP/gAB/gAAH4AAAf+AAAf/AAAH/wAAB/8AAAfwAAB/AAB/8AA/+AA/+AAf+AAB/AAAHAAAAAAAAAAAAQGAADAeAA8B8AHwD8B+AD4PgAH74AAH/AAAPwAAA/gAAP/gAD8fAAfA/AH4A+AeAA8BwABwEAABAQAAABwAAAHwAAAPwAAAfwAAAfgAAAfgAAAf/wAB//AAf/8AH8AAA/AAAPwAAB8AAAHAAAAQAAAAAAAAAAABAcAAcBwADwHAA/AcAP8BwD/wHAfnAcH4cBx+BwHPwHAf8AcB/ABwH4AHAeAAcBgABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/////////gAAAOAAAA4AAADAAAAAAAAAAAAAAAAAAAAAAAeAAAB/gAAH/4AAB/+AAAf/gAAH/AAAB8AAAAQAAAAAAAAAAAAAAOAAAA4AAADgAAAP/////////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAADgAAAcAAADgAAAcAAADgAAAcAAAB4AAADwAAADgAAAHAAAAOAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAYAAABgAAAGAAAAYAAABgAAAGAAAAYAAABgAAAGAAAAYAAABgAAAGAAAAYAAABgAAAGAAAAYAAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAADj+AAef8AD5xwAOGHAA44MADjgwAOOHAA44YADjDgAH/8AAf/8AAf/wAAAAAAAAAAAAAAAAAAAf//8B///wH//+AAcA4ADgBwAOAHAA4AcADgBwAOAHAA8A8AB8PgAD/8AAH/gAAH4AAAAAAAAAAAAH4AAB/4AAP/wAB4HgAPAPAA4AcADgBwAOAHAA4AcADgBwAPAPAAeB4AA4HAABgYAAAAAAAAAAAAfgAAH/gAB//gAHgeAA8A8ADgBwAOAHAA4AcADgBwAOAHAAcA4B///wH///Af//8AAAAAAAAAAAAAAAAH4AAB/4AAP/wAB7HgAPMPAA4wcADjBwAOMHAA4wcADjBwAPMPAAfx4AA/HAAB8YAAAwAAAAAAAAAAAAwAAADAAAB///AP//8B///wHMAAAYwAABjAAAGMAAAAAAAAAPwAAD/wMA//w4DwPHgeAePBwA4cHADhwcAOHBwA4cHADhwOAcPB///4H///Af//wAAAAAAAAAAAAAAAAAAB///wH///AAf/8ABwAAAOAAAA4AAADgAAAOAAAA4AAADwAAAH//AAP/8AAf/wAAAAAAAAAAAAAAAAAAAc//8Bz//wHP//AAAAAAAAAAAAAAHAAAAcAAAH+f///5///7H//8AAAAAAAAAAAAAAH///Af//8B///wAAPAAAB+AAAP8AAB54AAfDwAD4HgAOAPAAwAcACAAwAAAAAAAAAB///wH///Af//8AAAAAAAAAAAAAAAAAAAAP//AA//8AB//wAHAAAA4AAADgAAAOAAAA4AAAD4AAAH//AAP/8AB//wAHAAAA4AAADgAAAOAAAA4AAADwAAAH//AAP/8AAf/wAAAAAAAAAAAAAAAP//AA//8AB//wAHAAAA4AAADgAAAOAAAA4AAADgAAAPAAAAf/8AA//wAB//AAAAAAAAAAAAAAAAB+AAAf+AAD/8AAeB4ADwDwAOAHAA4AcADgBwAOAHAA4AcADwDwAHw+AAP/wAAf+AAAfgAAAAAAAAAAAAAAAB///8H///wP///A4BwAHADgAcAOABwA4AHADgAcAOAB4B4AD4fAAH/4AAP/AAAPwAAAAAAAAAAAAPwAAD/wAA//wADwPAAeAeABwA4AHADgAcAOABwA4AHADgAOAcAB///8H///wf///AAAAAAAAAAAAAAAAAAAD//wAP//AAf/8ABwAAAOAAAA4AAADgAAAOAAAAAAAAAYGAAD4cAAfx4AD3DwAOOHAA44cADjhwAOGHAA4ccADxzwAHj+AAOP4AAYOAAAAAAAwAAADAAAAMAAAP//wA///gD///AAwAcADABwAMAHAAwAcADAAwAAAAAAAAAAD/gAAP/4AA//4AAA/gAAAPAAAAcAAABwAAAHAAAAcAAABwAAAOAA//8AD//wAP//AAAAAAAAAAAIAAAA4AAAD8AAAH+AAAH/AAAD/gAAB/AAAB8AAA/wAAf8AAP+AAD/AAAPgAAAwAAAAAAAAIAAAA8AAAD/AAAH/gAAD/wAAA/wAAA/AAAf8AAP+AAP+AAA/AAAD+AAAH/AAAD/gAAA/wAAA/AAAf8AAP/AAP/AAA/gAADgAAAAAAAAAAEADAAwAOAHAA+B8AB8PgAB74AAD/AAAH4AAA/wAAHvgAB8PgAPgfAA4AcADAAwAAABABAAAAHAAAAfgAAA/wAAA/wAwAf4fAAP/8AAP/AAB/gAA/wAAf4AAP+AAB/AAAHgAAAQAAAAAAEADAAwAOAPAA4B8ADgPwAOD/AA4ecADnxwAO8HAA/gcAD8BwAPAHAA4AcACAAwAAAAAAAAAAAAAAAAAAAAAAAAAA8AB////f//////n/+AAAA4AAADgAAAAAAAAAAAAAAAAAH///Af//8B///wAAAAAAAAAAAAAAAAAAA4AAADgAAAOAAAA//5/9////z////AAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAB8AAAHwAAAcAAABwAAAHgAAAOAAAA8AAABwAAAHAAAB8AAAHwAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAYAAABgAAAGAAAAYAAABgAAAGAAAAYAAABgAAAGAAAAGAAABwAAAOAAABwAAAHAAAAcAAAA4AAABwAAABgAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAOAB4B4ADwPAAHh4AAPPAAAf4AAA/AAAB4AAAPwAAB/gAAPPAAB4eAAPA8AB4B4AHADgAIAEAAAAAAADAAAAMAAAAwAAADAAAAMAAAAwAAHDDgA8MPADww8AGDBgAAMAAAAwAAADAAAAMAAAAwAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADn//gOf/+A5//4AAAAAAAAAAAAAAAAAAAD/AAA//AAH/+AA+B8ADgBwAOAHAHwAPgfAA+B8AD4A4AcADwDwAHgeAAOBwAAQCAAAAAAAAAAAADgcAAOBwAA4HAD//8A///wD///AeDgcBwOBwHA4HAcDgcB4GBwD4AHAHgAcAOAAAAAAAAAAAAAMAGAB7+8AD//gAHx8AAcBwADgDgAOAOAA4A4ADgDgAOAOAA4A4ABwHAAHg8AA//4AH//wAMOGAAAAAAQAAABwAAAHwMYAPwxgAfjGAAfsYAAf7gAAf/wAB//AAf/8AH7GAA/MYAPwxgB8DGAHAAAAQAAAAAAAAAAAAAf/D/5/8P/n/w/+AAAAAAAAAAAAAAAAAAAABwAAffhwD//Hgf+cfBzwwcGHDhwYcOHBxw4cHDhxwfOPvA8//4Bx//AADwwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAP/wAB//gAPAPAB4AOAPDw8A4/xwHH/jgc4HOBzgc4HMAzgcwDOBzgc4HPDjgOcOeA4whwBwAOAHwD4APw/AAf/4AAf+AAAPAAAAAAAATgAAD/AAANsAAA2wAADTAAAP8AAAfwAAAAAAAAAAAAAAAAAAgAAAPAAAB+AAAOeAADw8AAOIwAADxAAAfgAADngAA8PAADgMAAEAQAAAAAAAAAAABgAAAGAAAAYAAABgAAAGAAAAYAAABgAAAGAAAAYAAABgAAAGAAAAYAAAB+AAAH4AAAAAAAAAAAAAAAAAAAAD8AAA/8AAHh4AAYDgAD/3AAN/MAA0QwADRjAAN/MAA7hwABwOAADhwAAH+AAAPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAD/AAAeeAABw4AAGDgAAYOAABw4AAH/AAAP8AAAfAAAAAAAAAAAAAAAAAAAwYAADBgAAMGAAAwYAADBgAAMGAAP+YAA/5gAD/mAAAwYAADBgAAMGAAAwYAADBgAAAAAAAAAAAAAAAMDAABwcAAPDwAAwPAADB8AAMOwAA5zAAB+MAADwwAAAAAAAAAAAIBAAAwGAADMcAANwwAA/DAAD8MAAO/wAAx+AAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf///B///8H//AAAAeAAAA4AAADgAAAOAAAA4AAADgAAAcAB//gAH//gAf/+AAAAAAAAAAAAAAAAAAAAP4AAB/4AAP/gAB//AAH/8AAf/wAB//AAH///8f///x////AAAAAAAAAB////H///8f///wAAAAAAAAAAAAAAABAAAAOAAAB4AAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAzAAAPMAAA/wAAAeAAAAAAAAAAAAAAAAAAAIAAABgAAAMAAAA//AAD/8AAAAAAAAAAAAAAAAAAAAAA8AAAP4AAAwwAADDAAAMMAAA5wAAB+AAADwAAAAAAAAAAAAAAAAAMAwAA8HAAB44AAD/AAAD4AADGMAAOBwAAeOAAA/wAAA+AAABgAAAAAAAAAAAAAAABAAAAMAAABwAAAH/8CAf/wcAAAHgAAA8AAAHgAAB4AAAPAAAB4AAAeAAADwAAA+AAAHgCAA8A8APAfwB4H7AHB+MAAHAwAAQ/wAAD/AAAAwAAADAAAAAAAAAAAAAAAAAAAAEAAAAwAAAHAAAAf/wIB//BwAAAeAAADwAAAeAAAHgAAA8AAAHgAAB4AAAPAAAD4AAAeAAADwAAA8GAwHg4HAcHA8AAYHwABg7AAGHMAAf4wAA/DAAA4MAAAAAAAAAAYBgABgHAAGMOAAZwYABvBgAH8OCAe/wcBx+HgABg8AAAHgAAB4AAAPAAAB4AAAeAAADwAAA+AAAHgHAA8B8APAfwB4HzADB8MAAHAwAAQ/wAAD/AAAAwAAAAAAAAAAAAAAAAAA4AAAP4AAB/wAAPHgABwOA4/A4Dn4DgOfAOAAAA4AAAHgAAB8AAAHgAAAYAAAAAAAAEAAADwAAB/AAA/8AAf+AAP/gAH/OAB/g4AHwDgAfAOAB/g4AD/zgAA/+AAAf8AAAP+AAAH8AAADwAAABAAAAEAAADwAAB/AAA/8AAf+AAP/gAH/OAB/g4AHwDgAfAOAB/g4AD/zgAA/+AAAf8AAAP+AAAH8AAADwAAABAAAAEAAADwAAB/AAA/8AAf+AAP/gAH/OAB/g4AHwDgAfAOAB/g4AD/zgAA/+AAAf8AAAP+AAAH8AAADwAAABAAAAEAAADwAAB/AAA/8AAf+AAP/gAH/OAB/g4AHwDgAfAOAB/g4AD/zgAA/+AAAf8AAAP+AAAH8AAADwAAABAAAAEAAADwAAB/AAA/8AAf+AAP/gAH/OAB/g4AHwDgAfAOAB/g4AD/zgAA/+AAAf8AAAP+AAAH8AAADwAAABAAAAAAAAABwAAA/AAAf8AAP/AAH/wfD/nD+/wcMb4BwxvgHD+/wcHx/5wEAf/AAAP+AAAH/AAAD8AAABwAAAAAAAEAAADwAAB/AAA/8AAf+AAP/gAH/OAB/g4AHwDgAcAOABwA4AHADgAf//8B///wHA4HAcDgcBwOBwHA4HAcDgcBwOBwHA4HAcDgcBwOBwHAAHAYAAMAAAAAAAAAAA/4AAP/4AD//4APAHgB4APAPAAeA4AA4DgADg+AAPz4AA//gAD/+AAOe4AA4BwAHAHgA8APgPgAeA8AAYDAAAAAAAAAAAAAAAAf//8B///wH///AcDgcBwOBwHA4HAcDgcBwOBwHA4HAcDgcBwOBwHAAHAcAAcAAAAAAAAAAAAAAB///wH///Af//8BwOBwHA4HAcDgcBwOBwHA4HAcDgcBwOBwHA4HAcAAcBwABwAAAAAAAAAAAAAAH///Af//8B///wHA4HAcDgcBwOBwHA4HAcDgcBwOBwHA4HAcDgcBwABwHAAHAAAAAAAAAAAAAAAf//8B///wH///AcDgcBwOBwHA4HAcDgcBwOBwHA4HAcDgcBwOBwHAAHAcAAcAAAAAAAAAAAAAAB///wH///Af//8AAAAAAAAAAAAAAAAAAAH///Af//8B///wAAAAAAAAAAAAAAAAAAAf//8B///wH///AAAAAAAAAAAAAAAAAAAB///wH///Af//8AAAAAAAAAAABgAAAGAAH///Af//8B///wHAYHAcBgcBwGBwHAYHAcBgcBwABwHAAHAeAA8A8AHgB+D8AD//gAH/8AAD+AAAAAAAAAAAAAAAAf//8B///wH///APwAAAPgAAAfgAAAfAAAA/AAAA+AAAA+AAAB8AAAB8AAAD8B///wH///Af//8AAAAAAAAAAAf8AAH/8AB//8AHgDwA8AHgHgAPAcAAcBwABwHAAHAcAAcBwABwHAAHAcAAcA4ADgDwAeAH4PwAP/+AAf/wAAP4AAAAAAAH/AAB//AAf//AB4A8APAB4B4ADwHAAHAcAAcBwABwHAAHAcAAcBwABwHAAHAOAA4A8AHgB+D8AD//gAH/8AAD+AAAAAAAB/wAAf/wAH//wAeAPADwAeAeAA8BwABwHAAHAcAAcBwABwHAAHAcAAcBwABwDgAOAPAB4Afg/AA//4AB//AAA/gAAAAAAAf8AAH/8AB//8AHgDwA8AHgHgAPAcAAcBwABwHAAHAcAAcBwABwHAAHAcAAcA4ADgDwAeAH4PwAP/+AAf/wAAP4AAAAAAAH/AAB//AAf//AB4A8APAB4B4ADwHAAHAcAAcBwABwHAAHAcAAcBwABwHAAHAOAA4A8AHgB+D8AD//gAH/8AAD+AAAAAAAAAAAAGDgAA8eAAB7wAAD+AAAHwAAAfAAAD+AAAe8AADw4AAGBAAAAAAAAAAAAAAAAAf8MAH//4B///AHgD4A8AfgHgD/AcAecBwDxwHAeHAcDwcBw+BwHHgHAc8AcA/gDgD8AeAH4PwA//+AH//wAMP4AAAAAAAAAAAf//AB//+AH//8AAAB4AAADwAAAHAAAAcAAABwAAAHAAAAcAAABwAAAOAAAB4B///AH//4Af/+AAAAAAAAAAAAAAAAAAAAH//wAf//gB///AAAAeAAAA8AAABwAAAHAAAAcAAABwAAAHAAAAcAAADgAAAeAf//wB//+AH//gAAAAAAAAAAAAAAAAAAAB//8AH//4Af//wAAAHgAAAPAAAAcAAABwAAAHAAAAcAAABwAAAHAAAA4AAAHgH//8Af//gB//4AAAAAAAAAAAAAAAAAAAAf//AB//+AH//8AAAB4AAADwAAAHAAAAcAAABwAAAHAAAAcAAABwAAAOAAAB4B///AH//4Af/+AAAAAAAAAAAQAAABwAAAHwAAAPwAAAfwAAAfgAAAfgAAAf/wAB//AAf/8AH8AAA/AAAPwAAB8AAAHAAAAQAAAAAAAAAAAAAf//8B///wH///ABwHAAHAcAAcBwABwHAAHAcAAcBwABwHAAHg8AAP/gAAf8AAA/gAAAAAAAAAAAAAAAA///AP//8A///wHgAAAcAAcBwABwHBwHAcHAcB4+BwD/4PAH954APn/gAAP8AAAOAAAAAAAAAAAAAD4AAcfwADz/gAfOOCBww4PHHBg+ccGAZxw4AHHDAAcYcAA//gAD//gAD/+AAAAAAAAAAAAAAAAAPgABx/AAPP+AB844AHDDgAccGAZxwYPnHDg8ccMDBxhwAD/+AAP/+AAP/4AAAAAAAAAAAAAAAAA+AAHH8AA8/4BnzjgOcMOBxxwYOHHBg4ccOBxxwwDnGHAGP/4AA//4AA//gAAAAAAAAAAAAAAAAHwAA4/gAHn/A8+ccDzhhwMOODA444MBjjhwHOOGAM4w4Dx//AOH//AAH/8AAAAAAAAAAAAAAAAAfAADj+AAef8Bz5xwHOGHAc44MADjgwAOOHAY44YBzjDgHH/8AAf/8AAf/wAAAAAAAAAAAAAAAAAfAADj+AAef8AD5xweOGHD844MMzjgwzOOHD844YHjjDgAH/8AAf/8AAf/wAAAAAAAAAAAAAAAAHwAAx/gAHn/AAc4cADjhwAOMDAA4wcADjBwAOMHAA4w4AB//AAH/4AAP/wAB/fgAPMPAA4wcADjBwAOMHAA4wcADjBwAPMPAAfx4AA/HAAB8YAAAAAAAAAAAA/AAAP/AAB/+AAPA8AB4B4AHADgwcAPzBwA/8HADngcAOMB4B4ADwPAAHA4AAMDAAAAAAAAAAAAA/AAAP/AAB/+AAPY8AB5h4OHGDg+cYOB5xg4AnGDgAcYOAB5h4AD+PAAH44AAPjAAAGAAAAAAAAAAAAAPwAAD/wAAf/gAD2PAAeYeABxg4AHGDgOcYOD5xg4OHGDggeYeAA/jwAB+OAAD4wAABgAAAAAAAAAAAAD8AAA/8AAH/4AY9jwDnmHgecYODhxg4OHGDg8cYOB5xg4BnmHgCP48AAfjgAA+MAAAYAAAAAAAAAAAAB+AAAf+AAD/8Acex4BzzDwHOMHAA4wcADjBwAOMHAc4wcBzzDwGH8eAAPxwAAfGAAAMAAAAAAAAAAAOAAAA+f/+A5//4An//gAAAAAAAAAAAAAAAAAAAJ//4Dn//g+f/+DgAAAAAAAAMAAABwAAAOP//Aw//8Dj//wHAAAAMAAABwAAAHAAAAA//8AD//wAP//AcAAABwAAAAAAAAAA/gAAP/AAB//AAPA8AA4A4DDgDgPMAOA/wA4D7ADgPOAOB+8B4C/+/AA//4AB//AAAHAAAAAAAAAAAAP//AA//8Bx//wPHAAAw4AADjgAAGOAAAc4AAAzgAAPPAAA4f/8AA//wAB//AAAAAAAAAAAAAAAAA/AAAP/AAB/+AAPA8CB4B4OHADg+cAOA5wA4AHADgAcAOAB4B4AD4fAAH/4AAP/AAAPwAAAAAAAAAAAAPwAAD/wAAf/gADwPAAeAeABwA4AnADgecAOD5wA4OHADgAeAeAA+HwAB/+AAD/wAAD8AAAAAAAAAAAAD8AAA/8AAH/4AY8DwDngHgecAODhwA4OHADg8cAOB5wA4BngHgCPh8AAf/gAA/8AAA/AAAAAAAAAAAAB+AAAf+AAD/8AceB4DzwDwMOAHA44AcBjgBwHOAHAM4AcDzwDwOHw+AAP/wAAf+AAAfgAAAAAAAAAAAAfgAAH/gAA//AHHgeAc8A8BzgBwAOAHAA4AcADgBwHOAHAc8A8Bh8PgAD/8AAH/gAAH4AAAAAAAAAAAAMAAAAwAAADAAAAMAAAAwAAADAAADtwAAO3AAA7cAAAMAAAAwAAADAAAAMAAAAAAAAAAAAAH5gAB//AAP/4AB4PgAPB/AA4PcADh5wAOPHAA54cADvBwAP4PAAfD4AB//AAP/4AAZ+AAAAAAAAAAAAf8AAB//AAH//AAAH8AAAB4OAADg+AAOB4AA4AgADgAAAOAAABwAH//gAf/+AB//4AAAAAAAAAAAAAAAH/AAAf/wAB//wAAB/AAAAeAAAA4BgADgeAAOD4AA4MAADgAAAcAB//4AH//gAf/+AAAAAAAAAAAAAAAB/wAAH/8AAf/8AYAfwDgAHgcAAODgAA4OAADg8AAOB4AA4BgAHACf/+AB//4AH//gAAAAAAAAAAAAAAA/4AAD/+AAP/+AcAP4BwADwHAAHAAAAcAAABwAAAHAcAAcBwADgGP//AA//8AD//wAAAAAAAAAABAAAAHAAAAfgAAA/wAAA/wAAAf4cAAP/zgAP/+AB/jgA/wAAf4AAP+AAB/AAAHgAAAQAAAAAAAAAAAA//////////////A4BwAHADgAcAOABwA4AHADgAcAOAB4B4AD4fAAH/4AAP/AAAPwAAAAAABAAAAHAAAAfgAAw/wADg/wA+Af4fAAP/8AAP/AAB/g4A/wDgf4AOP+AAB/AAAHgAAAQAAA=='),
+ 32,
+ atob("BgkMGhEZEgYMDAwQCAwICxILEBAREBEOEREJCREVEQ8ZEhEUExAOFBQHDREPGBMUERQSEhEUERsREBIMCwwTEg4QERAREQoREQcHDgcYEREREQoPDBEPFg8PDwwIDBMcCgoAAAAAAAAAAAAAACERESEAAAAAAAAAAAAAAAAhIQAGCRAQEhAIDw8XCQ8RABIODRELCw4REwcLCQoPHBscDxISEhISEhoUEBAQEAcHBwcTExQUFBQUDhQUFBQUEBEREBAQEBAQGhARERERBwcHBxAREREREREPEREREREPEQ8="),
+ 28+(scale<<8)+(1<<16)
+ );
return this;
};
-var imgLock = {
- width : 16, height : 16, bpp : 1,
- transparent : 0,
- buffer : E.toArrayBuffer(atob("A8AH4A5wDDAYGBgYP/w//D/8Pnw+fD58Pnw//D/8P/w="))
+Graphics.prototype.setMiniFont = function(scale) {
+ // Actual height 16 (15 - 0)
+ this.setFontCustom(
+ atob('AAAAAAAAAAAAAP+w/5AAAAAA4ADgAOAA4AAAAAAAAAABgBmAGbAb8D+A+YDZ8B/wf4D5gJmAGQAQAAAAAAAeOD8cMwzxj/GPMYwc/Az4AAAAAHAA+DDIYMjA+YBzAAYADeA7MHMw4zDD4ADAAAAz4H/wzjDHMMMwwbBj4APgADAAAAAA4ADgAAAAAAAAAAfwH/54B+ABAAAAAOABeAcf/gfwAAAAACAAaAD4APgAOABgAAAAAAACAAIAAgA/wAMAAgACAAAAAAAAPAA4AAAAAAIAAgACAAIAAgAAAAAAADAAMAAAAAAAcAfwf4D4AIAAAAA/wH/gwDDAMMAwwDB/4D/AAAAAAGAAwAD/8P/wAAAAAHAw8HDA8MHww7DnMH4wGBAAAMBgyHDcMPww/DDv4MfAAAAAAAHgD+A+YPhgwGAH8AfwAEAAAAAA/GD8cMwwzDDMMM5wx+ABgAAAP8B/4MwwzDDMMMwwx+ADwAAAgADAAMBwwfDPgP4A8ADAAAAAe+D/8M4wxjDGMP5wf+ABwAAAfAB+cMYwwjDCMMYwf+A/wAAAAAAAAAxgBCAAAAAAAAAYPBA4AAAAAAAAAgAHAA+AHMAYYAAAAAAAAAAAAAAJAAkACQAJAAkACQAJAAkAAAAAAAAAAAAAABhgHMAPgAcAAgAAAAAAAABgAOAAwbDDsMYA/AA4AAAAAAAD4A/wGBgxzDPsMyQjJDPkM+wYIBxgD+AAAAAAABAA8A/gf8DwwODA/sAfwAHwADAAAP/w//DGMMYwxjDOMP9we+ABwA8AP8Bw4MAwwDDAMMAwwDDgcHDgMMAAAAAA//D/8MAwwDDAMMAw4HB/4D/AAAAAAP/w//DGMMYwxjDGMMQwgBAAAP/w//DDAMMAwwDDAMAADwA/wHDgwDDAMMAwwDDCMOJwc+ADwAAA//D/8AMAAwADAAMAAwD/8P/wAAAAAP/w//AAAABgAHAAMAAwAHD/4P+AAAAAAP/w//AOAB+AOcBw4MBwgDAAEAAA//D/8AAwADAAMAAwADAAAP/w//A8AA8AA+AA8AHwB8AeAHgA//D/8AAAAAD/8P/wcAAcAA8AA4AB4P/w//AAAA8AP8Bw4MAwwDDAMMAwwDDgcH/gP8AAAAAA//D/8MMAwwDDAMYA7gB8ABgADwA/wHDgwDDAMMAwwDDA8ODwf/A/8AAAAAD/8P/wwwDDAMMAx4Dv4HxwEBAAAHjg/HDMMMYwxjDGMONwc+ABwMAAwADAAMAA//D/8MAAwADAAIAAAAD/wP/gAHAAMAAwADAAMAHg/8AAAMAA+AA/AAfgAPAA8AfgPwD4AMAAwAD4AD+AA/AA8A/g/gDwAP4AH8AB8APwH8D8AMAAgBDAMPDgO8APAB8AOcDw8MAwgBCAAOAAeAAeAAfwH/B4AOAAwAAAAMAwwPDB8Mew3jD4MPAwwDAAAAAAAAB//3//QABAAAAAAADgAP4AH+AB8AAQAABAAEAAf/9//wAAAAAAAAAAGAAwAGAAwABgADAAGAAAAAAAAAQABAAEAAQABAAEAAQABAAEAAQABAAAAAAAAAAAAAAAAAAAAAAAQA3wHbAZMBswGzAf4A/wAAAAAP/w//AYMBgwGDAYMA/gB8AAAAEAD+Ae8BgwGDAYMBgwDGAAAAMAD+Ae8BgwGDAYMBhw//D/8AAAAYAP4B/wGTAZMBkwGTAP4AcAEAAYAP/w//CQAJAAAwAP4hz3GDMQMxAzGHcf/h/8AAAAAP/w//AYABgAGAAYAA/wB/AAAAAA3/Df8AAAAAOf/9//AAAAAP/w//ADgAfADGAYMBAQAAD/8P/wAAAAAB/wH/AYABgAGAAf8A/wGAAYABgAH/AP8AAAAAAf8B/wGAAYABgAGAAP8AfwAAADAA/gHvAYMBgwGDAYMA/gB8AAAAAAH/8f/xgwGDAYMBgwD+AHwAAAAwAP4B7wGDAYMBgwGHAf/x//AAAAAB/wH/AYABgAEAAAAA5gHzAbMBkwGbAd8AzgEAAYAP/wf/AQMBAwAAAAAB/gH/AAMAAwADAAcB/wH/AAABAAHgAPwAHwAPAH4B8AGAAQAB8AB+AA8APwHwAeAA/AAPAD8B+AHAAQEBgwHOAHwAOAD+AccBAwAAAQAB4AD4EB/wB8A/APgBwAAAAAEBgwGPAZ8B8wHjAcMBAQAAAAAABgf/9/n2AAAAAAAP/w//AAAEAAYAB/nz//AGAAAAAAAAAAAAcABgAGAAcAAwAHAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'),
+ 32,
+ atob("AwUHDwoOCwQHBwcJBAcEBgoGCQkKCQoICQoFBQoMCgkPCgoMCwkICwsECAoIDgsMCgwKCgoLCg8KCQoHBgcLCwgJCgkKCQYKCgQECAQOCgoKCgYIBwoIDAkJCAcEBwsQ"),
+ 16+(scale<<8)+(1<<16)
+ );
+ return this;
};
-var imgSteps = {
- width : 24, height : 24, bpp : 1,
- transparent : 1,
- buffer : require("heatshrink").decompress(atob("/H///wv4CBn4CD8ACCj4IBj8f+Eeh/wjgCBngCCg/4nEH//4h/+jEP/gRBAQX+jkf/wgB//8GwP4FoICDHgICCBwIA=="))
-};
-
-var imgBattery = {
- width : 24, height : 24, bpp : 1,
- transparent : 1,
- buffer : require("heatshrink").decompress(atob("/4AN4EAg4TBgd///9oEAAQv8ARQRDDQQgCEwQ4OA"))
-};
-
-var imgBpm = {
- width : 24, height : 24, bpp : 1,
- transparent : 1,
- buffer : require("heatshrink").decompress(atob("/4AOn4CD/wCCjgCCv/8jF/wGYgOA5MB//BC4PDAQnjAQPnAQgANA"))
-};
-
-var imgTemperature = {
- width : 24, height : 24, bpp : 1,
- transparent : 1,
- buffer : require("heatshrink").decompress(atob("//D///wICBjACBngCNkgCP/0kv/+s1//nDn/8wICEBAIOC/08v//IYJECA=="))
-};
-
-var imgWind = {
- width : 24, height : 24, bpp : 1,
- transparent : 1,
- buffer : require("heatshrink").decompress(atob("/0f//8h///Pn//zAQXzwf/88B//mvGAh18gEevn/DIICB/PwgEBAQMHBAIADFwM/wEAGAP/54CD84CE+eP//wIQU/A=="))
-};
-
-var imgTimer = {
- width : 24, height : 24, bpp : 1,
- transparent : 1,
- buffer : require("heatshrink").decompress(atob("/+B/4CD84CEBAPygFP+F+h/x/+P+fz5/n+HnAQNn5/wuYCBmYCC5kAAQfOgFz80As/ngHn+fD54mC/F+j/+gF/HAQA=="))
-};
-
-var imgCharging = {
- width : 24, height : 24, bpp : 1,
- transparent : 1,
- buffer : require("heatshrink").decompress(atob("//+v///k///4AQPwBANgBoMxBoMb/P+h/w/kH8H4gfB+EBwfggHH4EAt4CBn4CBj4CBh4FCCIO/8EB//Agf/wEH/8Gh//x////fAQIA="))
-};
-
-var imgWatch = {
- width : 24, height : 24, bpp : 1,
- transparent : 1,
- buffer : require("heatshrink").decompress(atob("/8B//+ARANB/l4//5/1/+f/n/n5+fAQnf9/P44CC8/n7/n+YOB/+fDQQgCEwQsCHBBEC"))
-};
-
-
-/*
- * INFO ENTRIES
- */
-var infoArray = [
- function(){ return [ null, null, "left" ] },
- function(){ return [ "Bangle", imgWatch, "right" ] },
- function(){ return [ E.getBattery() + "%", imgBattery, "left" ] },
- function(){ return [ getSteps(), imgSteps, "left" ] },
- function(){ return [ Math.round(Bangle.getHealthStatus("last").bpm) + " bpm", imgBpm, "left"] },
- function(){ return [ getWeather().temp, imgTemperature, "left" ] },
- function(){ return [ getWeather().wind, imgWind, "left" ] },
-];
-const NUM_INFO=infoArray.length;
-
-
-function getInfoEntry(){
- if(isAlarmEnabled()){
- return [getAlarmMinutes() + " min.", imgTimer, "left"]
- } else if(Bangle.isCharging()){
- return [E.getBattery() + "%", imgCharging, "left"]
- } else{
- return infoArray[settings.showInfo]();
- }
-}
-
-
-/*
- * Helper
- */
-function getSteps() {
- var steps = 0;
- try{
- if (WIDGETS.wpedom !== undefined) {
- steps = WIDGETS.wpedom.getSteps();
- } else if (WIDGETS.activepedom !== undefined) {
- steps = WIDGETS.activepedom.getSteps();
- } else {
- steps = Bangle.getHealthStatus("day").steps;
- }
- } catch(ex) {
- // In case we failed, we can only show 0 steps.
- }
-
- steps = Math.round(steps/100) / 10; // This ensures that we do not show e.g. 15.0k and 15k instead
- return steps + "k";
-}
-
-
-function getWeather(){
- var weatherJson;
-
- try {
- weatherJson = storage.readJSON('weather.json');
- var weather = weatherJson.weather;
-
- // Temperature
- weather.temp = locale.temp(weather.temp-273.15);
-
- // Humidity
- weather.hum = weather.hum + "%";
-
- // Wind
- const wind = locale.speed(weather.wind).match(/^(\D*\d*)(.*)$/);
- weather.wind = Math.round(wind[1]) + " km/h";
-
- return weather
-
- } catch(ex) {
- // Return default
- }
-
+function imgLock(){
return {
- temp: "? °C",
- hum: "-",
- txt: "-",
- wind: "? km/h",
- wdir: "-",
- wrose: "-"
- };
+ width : 16, height : 16, bpp : 1,
+ transparent : 0,
+ buffer : E.toArrayBuffer(atob("A8AH4A5wDDAYGBgYP/w//D/8Pnw+fD58Pnw//D/8P/w="))
+ }
}
-function isAlarmEnabled(){
- try{
- var alarm = require('sched');
- var alarmObj = alarm.getAlarm(TIMER_IDX);
- if(alarmObj===undefined || !alarmObj.on){
- return false;
+
+/************************************************
+ * Menu
+ */
+// Custom bwItems menu - therefore, its added here and not in a clkinfo.js file.
+var bwItems = {
+ name: null,
+ img: null,
+ items: [
+ { name: "WeekOfYear",
+ get: () => ({ text: "Week " + weekOfYear(), img: null}),
+ show: function() {},
+ hide: function () {}
+ },
+ ]
+};
+
+function weekOfYear() {
+ var date = new Date();
+ date.setHours(0, 0, 0, 0);
+ // Thursday in current week decides the year.
+ date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7);
+ // January 4 is always in week 1.
+ var week1 = new Date(date.getFullYear(), 0, 4);
+ // Adjust to Thursday in week 1 and count number of weeks from date to week1.
+ return 1 + Math.round(((date.getTime() - week1.getTime()) / 86400000
+ - 3 + (week1.getDay() + 6) % 7) / 7);
+}
+
+
+// Load menu
+var menu = clock_info.load();
+menu = menu.concat(bwItems);
+
+
+// Ensure that our settings are still in range (e.g. app uninstall). Otherwise reset the position it.
+if(settings.menuPosX >= menu.length || settings.menuPosY > menu[settings.menuPosX].items.length ){
+ settings.menuPosX = 0;
+ settings.menuPosY = 0;
+}
+
+// Set draw functions for each item
+menu.forEach((menuItm, x) => {
+ menuItm.items.forEach((item, y) => {
+ function drawItem() {
+ // For the clock, we have a special case, as we don't wanna redraw
+ // immediately when something changes. Instead, we update data each minute
+ // to save some battery etc. Therefore, we hide (and disable the listener)
+ // immedeately after redraw...
+ item.hide();
+
+ // After drawing the item, we enable inputs again...
+ lock_input = false;
+
+ var info = item.get();
+ drawMenuItem(info.text, info.img);
}
- return true;
+ item.on('redraw', drawItem);
+ })
+});
- } catch(ex){ }
- return false;
-}
-function getAlarmMinutes(){
- if(!isAlarmEnabled()){
- return -1;
+function canRunMenuItem(){
+ if(settings.menuPosY == 0){
+ return false;
}
- var alarm = require('sched');
- var alarmObj = alarm.getAlarm(TIMER_IDX);
- return Math.round(alarm.getTimeToAlarm(alarmObj)/(60*1000));
+ var menuEntry = menu[settings.menuPosX];
+ var item = menuEntry.items[settings.menuPosY-1];
+ return item.run !== undefined;
}
-function increaseAlarm(){
+
+function runMenuItem(){
+ if(settings.menuPosY == 0){
+ return;
+ }
+
+ var menuEntry = menu[settings.menuPosX];
+ var item = menuEntry.items[settings.menuPosY-1];
try{
- var minutes = isAlarmEnabled() ? getAlarmMinutes() : 0;
- var alarm = require('sched')
- alarm.setAlarm(TIMER_IDX, {
- timer : (minutes+5)*60*1000,
- });
- alarm.reload();
- } catch(ex){ }
-}
-
-function decreaseAlarm(){
- try{
- var minutes = getAlarmMinutes();
- minutes -= 5;
-
- var alarm = require('sched')
- alarm.setAlarm(TIMER_IDX, undefined);
-
- if(minutes > 0){
- alarm.setAlarm(TIMER_IDX, {
- timer : minutes*60*1000,
- });
+ var ret = item.run();
+ if(ret){
+ Bangle.buzz(300, 0.6);
}
-
- alarm.reload();
- } catch(ex){ }
+ } catch (ex) {
+ // Simply ignore it...
+ }
}
-/*
- * DRAW functions
+/************************************************
+ * Draw
*/
-
function draw() {
// Queue draw again
queueDraw();
// Draw clock
drawDate();
- drawTime();
+ drawMenuAndTime();
drawLock();
drawWidgets();
}
@@ -274,12 +191,12 @@ function draw() {
function drawDate(){
// Draw background
- var y = H/5*2;
- g.reset().clearRect(0,0,W,W);
+ var y = H/5*2 + (isFullscreen() ? 0 : 8);
+ g.reset().clearRect(0,0,W,y);
// Draw date
- y = parseInt(y/2);
- y += settings.fullscreen ? 2 : 15;
+ y = parseInt(y/2)+4;
+ y += isFullscreen() ? 0 : 8;
var date = new Date();
var dateStr = date.getDate();
dateStr = ("0" + dateStr).substr(-2);
@@ -293,21 +210,17 @@ function drawDate(){
var fullDateW = dateW + 10 + dayW;
g.setFontAlign(-1,0);
- g.setMediumFont();
- g.setColor(g.theme.fg);
- g.drawString(dateStr, W/2 - fullDateW / 2, y+1);
-
- g.setSmallFont();
g.drawString(dayStr, W/2 - fullDateW/2 + 10 + dateW, y-12);
g.drawString(monthStr, W/2 - fullDateW/2 + 10 + dateW, y+11);
+
+ g.setMediumFont();
+ g.setColor(g.theme.fg);
+ g.drawString(dateStr, W/2 - fullDateW / 2, y+2);
}
-function drawTime(){
+function drawTime(y, smallText){
// Draw background
- var y = H/5*2 + (settings.fullscreen ? 0 : 8);
- g.setColor(g.theme.fg);
- g.fillRect(0,y,W,H);
var date = new Date();
// Draw time
@@ -323,56 +236,78 @@ function drawTime(){
// Set y coordinates correctly
y += parseInt((H - y)/2) + 5;
- var infoEntry = getInfoEntry();
- var infoStr = infoEntry[0];
- var infoImg = infoEntry[1];
- var printImgLeft = infoEntry[2] == "left";
-
// Show large or small time depending on info entry
- if(infoStr == null){
- if(settings.hideColon){
- g.setXLargeFont();
- } else {
- g.setLargeFont();
- }
- } else {
+ if(smallText){
y -= 15;
g.setMediumFont();
+ } else {
+ g.setLargeFont();
}
g.drawString(timeStr, W/2, y);
+}
- // Draw info if set
- if(infoStr == null){
+function drawMenuItem(text, image){
+ // First clear the time region
+ var y = H/5*2 + (isFullscreen() ? 0 : 8);
+
+ g.setColor(g.theme.fg);
+ g.fillRect(0,y,W,H);
+
+ // Draw menu text
+ var hasText = (text != null && text != "");
+ if(hasText){
+ g.setFontAlign(0,0);
+
+ // For multiline text we show an even smaller font...
+ text = String(text);
+ if(text.split('\n').length > 1){
+ g.setMiniFont();
+ } else {
+ g.setSmallFont();
+ }
+
+ var imgWidth = image == null ? 0 : 24;
+ var strWidth = g.stringWidth(text);
+ g.setColor(g.theme.fg).fillRect(0, 149-14, W, H);
+ g.setColor(g.theme.bg).drawString(text, W/2 + imgWidth/2 + 2, 149+3);
+
+ if(image != null){
+ var scale = imgWidth / image.width;
+ g.drawImage(image, W/2 + -strWidth/2-4 - parseInt(imgWidth/2), 149 - parseInt(imgWidth/2), {scale: scale});
+ }
+ }
+
+ // Draw time
+ drawTime(y, hasText);
+}
+
+
+function drawMenuAndTime(){
+ var menuEntry = menu[settings.menuPosX];
+
+ // The first entry is the overview...
+ if(settings.menuPosY == 0){
+ drawMenuItem(menuEntry.name, menuEntry.img);
return;
}
- y += 35;
- g.setFontAlign(0,0);
- g.setSmallFont();
- var imgWidth = 0;
- if(infoImg !== undefined){
- imgWidth = infoImg.width;
- var strWidth = g.stringWidth(infoStr);
- g.drawImage(
- infoImg,
- W/2 + (printImgLeft ? -strWidth/2-2 : strWidth/2+2) - infoImg.width/2,
- y - infoImg.height/2
- );
- }
- g.drawString(infoStr, printImgLeft ? W/2 + imgWidth/2 + 2 : W/2 - imgWidth/2 - 2, y+3);
+ // Draw item if needed
+ lock_input = true;
+ var item = menuEntry.items[settings.menuPosY-1];
+ item.show();
}
function drawLock(){
if(settings.showLock && Bangle.isLocked()){
g.setColor(g.theme.fg);
- g.drawImage(imgLock, W-16, 2);
+ g.drawImage(imgLock(), W-16, 2);
}
}
function drawWidgets(){
- if(settings.fullscreen){
+ if(isFullscreen()){
for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
} else {
Bangle.drawWidgets();
@@ -380,9 +315,19 @@ function drawWidgets(){
}
+function isFullscreen(){
+ var s = settings.screen.toLowerCase();
+ if(s == "dynamic"){
+ return Bangle.isLocked()
+ } else {
+ return s == "full"
+ }
+}
-/*
- * Draw timeout
+
+
+/************************************************
+ * Listener
*/
// timeout used to update every minute
var drawTimeout;
@@ -410,69 +355,112 @@ Bangle.on('lcdPower',on=>{
Bangle.on('lock', function(isLocked) {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
+
+ if(!isLocked && settings.screen.toLowerCase() == "dynamic"){
+ // If we have to show the widgets again, we load it from our
+ // cache and not through Bangle.loadWidgets as its much faster!
+ for (let wd of WIDGETS) {wd.draw=wd._draw;wd.area=wd._area;}
+ }
+
draw();
});
Bangle.on('charging',function(charging) {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
+
+ // Jump to battery
+ settings.menuPosX = 0;
+ settings.menuPosY = 1;
draw();
});
Bangle.on('touch', function(btn, e){
- var left = parseInt(g.getWidth() * 0.2);
+ var widget_size = isFullscreen() ? 0 : 20; // Its not exactly 24px -- empirically it seems that 20 worked better...
+ var left = parseInt(g.getWidth() * 0.22);
var right = g.getWidth() - left;
- var upper = parseInt(g.getHeight() * 0.2);
+ var upper = parseInt(g.getHeight() * 0.22) + widget_size;
var lower = g.getHeight() - upper;
- var is_left = e.x < left;
- var is_right = e.x > right;
var is_upper = e.y < upper;
var is_lower = e.y > lower;
+ var is_left = e.x < left && !is_upper && !is_lower;
+ var is_right = e.x > right && !is_upper && !is_lower;
+ var is_center = !is_upper && !is_lower && !is_left && !is_right;
- if(is_upper){
- Bangle.buzz(40, 0.6);
- increaseAlarm();
- drawTime();
+ if(lock_input){
+ return;
}
if(is_lower){
Bangle.buzz(40, 0.6);
- decreaseAlarm();
- drawTime();
+ settings.menuPosY = (settings.menuPosY+1) % (menu[settings.menuPosX].items.length+1);
+
+ drawMenuAndTime();
+ }
+
+ if(is_upper){
+ if(e.y < widget_size){
+ return;
+ }
+
+ Bangle.buzz(40, 0.6);
+ settings.menuPosY = settings.menuPosY-1;
+ settings.menuPosY = settings.menuPosY < 0 ? menu[settings.menuPosX].items.length : settings.menuPosY;
+
+ drawMenuAndTime();
}
if(is_right){
Bangle.buzz(40, 0.6);
- settings.showInfo = (settings.showInfo+1) % NUM_INFO;
- drawTime();
+ settings.menuPosX = (settings.menuPosX+1) % menu.length;
+ settings.menuPosY = 0;
+ drawMenuAndTime();
}
if(is_left){
Bangle.buzz(40, 0.6);
- settings.showInfo = settings.showInfo-1;
- settings.showInfo = settings.showInfo < 0 ? NUM_INFO-1 : settings.showInfo;
- drawTime();
+ settings.menuPosY = 0;
+ settings.menuPosX = settings.menuPosX-1;
+ settings.menuPosX = settings.menuPosX < 0 ? menu.length-1 : settings.menuPosX;
+ drawMenuAndTime();
+ }
+
+ if(is_center){
+ if(canRunMenuItem()){
+ runMenuItem();
+ }
}
});
E.on("kill", function(){
- storage.write(SETTINGS_FILE, settings);
+ try{
+ storage.write(SETTINGS_FILE, settings);
+ } catch(ex){
+ // If this fails, we still kill the app...
+ }
});
-/*
- * Draw clock the first time
+/************************************************
+ * Startup Clock
*/
+
// The upper part is inverse i.e. light if dark and dark if light theme
// is enabled. In order to draw the widgets correctly, we invert the
// dark/light theme as well as the colors.
g.setTheme({bg:g.theme.fg,fg:g.theme.bg, dark:!g.theme.dark}).clear();
-// Load widgets and draw clock the first time
-Bangle.loadWidgets();
-draw();
-
// Show launcher when middle button pressed
Bangle.setUI("clock");
+
+// Load widgets and draw clock the first time
+Bangle.loadWidgets();
+
+// Cache draw function for dynamic screen to hide / show widgets
+// Bangle.loadWidgets() could also be called later on but its much slower!
+for (let wd of WIDGETS) {wd._draw=wd.draw; wd._area=wd.area;}
+
+// Draw first time
+draw();
diff --git a/apps/bwclk/metadata.json b/apps/bwclk/metadata.json
index eba1449a6..8ef812f41 100644
--- a/apps/bwclk/metadata.json
+++ b/apps/bwclk/metadata.json
@@ -1,13 +1,13 @@
{
"id": "bwclk",
"name": "BW Clock",
- "version": "0.09",
- "description": "BW Clock.",
+ "version": "0.24",
+ "description": "A very minimalistic clock to mainly show date and time.",
"readme": "README.md",
"icon": "app.png",
- "screenshots": [{"url":"screenshot.png"}, {"url":"screenshot_2.png"}, {"url":"screenshot_3.png"}],
+ "screenshots": [{"url":"screenshot.png"}, {"url":"screenshot_2.png"}, {"url":"screenshot_3.png"}, {"url":"screenshot_4.png"}],
"type": "clock",
- "tags": "clock",
+ "tags": "clock,clkinfo",
"supports": ["BANGLEJS2"],
"allow_emulator": true,
"storage": [
diff --git a/apps/bwclk/screenshot.png b/apps/bwclk/screenshot.png
index 550913422..3a75f13d1 100644
Binary files a/apps/bwclk/screenshot.png and b/apps/bwclk/screenshot.png differ
diff --git a/apps/bwclk/screenshot_2.png b/apps/bwclk/screenshot_2.png
index ccbc9aae1..31bf6373e 100644
Binary files a/apps/bwclk/screenshot_2.png and b/apps/bwclk/screenshot_2.png differ
diff --git a/apps/bwclk/screenshot_3.png b/apps/bwclk/screenshot_3.png
index 5bf7083f0..8d982cac4 100644
Binary files a/apps/bwclk/screenshot_3.png and b/apps/bwclk/screenshot_3.png differ
diff --git a/apps/bwclk/screenshot_4.png b/apps/bwclk/screenshot_4.png
new file mode 100644
index 000000000..83de5c2ce
Binary files /dev/null and b/apps/bwclk/screenshot_4.png differ
diff --git a/apps/bwclk/settings.js b/apps/bwclk/settings.js
index a421e81a9..116253fda 100644
--- a/apps/bwclk/settings.js
+++ b/apps/bwclk/settings.js
@@ -4,7 +4,7 @@
// initialize with default settings...
const storage = require('Storage')
let settings = {
- fullscreen: false,
+ screen: "Normal",
showLock: true,
hideColon: false,
};
@@ -17,15 +17,16 @@
storage.write(SETTINGS_FILE, settings)
}
-
+ var screenOptions = ["Normal", "Dynamic", "Full"];
E.showMenu({
'': { 'title': 'BW Clock' },
'< Back': back,
- 'Fullscreen': {
- value: settings.fullscreen,
- format: () => (settings.fullscreen ? 'Yes' : 'No'),
- onchange: () => {
- settings.fullscreen = !settings.fullscreen;
+ 'Screen': {
+ value: 0 | screenOptions.indexOf(settings.screen),
+ min: 0, max: 2,
+ format: v => screenOptions[v],
+ onchange: v => {
+ settings.screen = screenOptions[v];
save();
},
},
diff --git a/apps/calclock/ChangeLog b/apps/calclock/ChangeLog
new file mode 100644
index 000000000..90bcfb9d4
--- /dev/null
+++ b/apps/calclock/ChangeLog
@@ -0,0 +1,6 @@
+0.01: Initial version
+0.02: More compact rendering & app icon
+0.03: Tell clock widgets to hide.
+0.04: Improve current time readability in light theme.
+0.05: Show calendar colors & improved all day events.
+0.06: Improved multi-line locations & titles
diff --git a/apps/calclock/README.md b/apps/calclock/README.md
new file mode 100644
index 000000000..2b4e93a0c
--- /dev/null
+++ b/apps/calclock/README.md
@@ -0,0 +1,9 @@
+# Calendar Clock - Your day at a glance
+
+This clock shows a chronological view of your current and future events.
+It uses events synced from Gadgetbridge to achieve this.
+
+The current time and date is highlighted in cyan.
+
+## Screenshot
+
diff --git a/apps/calclock/calclock-icon.js b/apps/calclock/calclock-icon.js
new file mode 100644
index 000000000..9d5514d80
--- /dev/null
+++ b/apps/calclock/calclock-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwgpm5gAB4AVRhgWCAAQWWDCARC/4ACJR4uB54WDAAP8DBotFGIgXLFwv4GAouQC4gwMLooXF/gXJOowXGJBIXBCIgXQxgXLMAIXXMAmIC5OIx4XJhH/wAXIxnIC78IxGIHoIABI44MBC4wQBEQIDB5gXGPAJgEC6IxBC5oABC4wwDa4YTCxAWD5nPDAzvGFYgAB5AXWJBK+GcAq5CGBIuBC5X4GBIJBdoQXB/GIx4CDPJAuEC5JoCDAgWBFwYXJxCBIFwYXKYwoACCwZ3IPQoWIC5YABGYIABCwpHKAQYMBCwwX/C5QAMC8R3/R/4XNhAXNwAXHgGIABgWIAFwA=="))
diff --git a/apps/calclock/calclock.js b/apps/calclock/calclock.js
new file mode 100644
index 000000000..1f98502ef
--- /dev/null
+++ b/apps/calclock/calclock.js
@@ -0,0 +1,135 @@
+var calendar = [];
+var current = [];
+var next = [];
+
+function updateCalendar() {
+ calendar = require("Storage").readJSON("android.calendar.json",true)||[];
+ calendar = calendar.filter(e => isActive(e) || getTime() <= e.timestamp);
+ calendar.sort((a,b) => a.timestamp - b.timestamp);
+
+ current = calendar.filter(isActive);
+ next = calendar.filter(e=>!isActive(e));
+}
+
+function isActive(event) {
+ var timeActive = getTime() - event.timestamp;
+ return timeActive >= 0 && timeActive <= event.durationInSeconds;
+}
+function zp(str) {
+ return ("0"+str).substr(-2);
+}
+
+function drawEventHeader(event, y) {
+ var x = 0;
+ var time = isActive(event) ? new Date() : new Date(event.timestamp * 1000);
+
+ //Don't need to know what time the event is at if its all day
+ if (isActive(event) || !event.allDay) {
+ g.setFont("Vector", 24);
+ var timeStr = zp(time.getHours()) + ":" + zp(time.getMinutes());
+ g.drawString(timeStr, 0, y);
+ y += 3;
+ x = 13*timeStr.length+5;
+ }
+
+ g.setFont("12x20", 1);
+
+ if (isActive(event)) {
+ g.drawString(zp(time.getDate())+". " + require("locale").month(time,1),x,y);
+ } else {
+ var offset = 0-time.getTimezoneOffset()/1440;
+ var days = Math.floor((time.getTime()/1000)/86400+offset)-Math.floor(getTime()/86400+offset);
+ if(days > 0 || event.allDay) {
+ var daysStr = days===1?/*LANG*/"tomorrow":/*LANG*/"in "+days+/*LANG*/" days";
+ g.drawString(daysStr,x,y);
+ }
+ }
+ y += 21;
+ return y;
+}
+
+function drawEventBody(event, y) {
+ g.setFont("12x20", 1);
+ var lines = g.wrapString(event.title, g.getWidth()-15);
+ var yStart = y;
+ if (lines.length > 2) {
+ lines = lines.slice(0,2);
+ lines[1] += "...";
+ }
+ g.drawString(lines.join('\n'),10,y);
+ y+=20 * lines.length;
+ if(event.location) {
+ g.drawImage(atob("DBSBAA8D/H/nDuB+B+B+B3Dn/j/B+A8A8AYAYAYAAAAAAA=="),10,y);
+ var loclines = g.wrapString(event.location, g.getWidth()-30);
+ if(loclines.length>1) loclines[0] += "...";
+ g.drawString(loclines[0],25,y);
+ y+=20;
+ }
+ if (event.color) {
+ var oldColor = g.getColor();
+ g.setColor("#"+(0x1000000+Number(event.color)).toString(16).padStart(6,"0"));
+ g.fillRect(0,yStart,5,y-3);
+ g.setColor(oldColor);
+ }
+ y+=5;
+ return y;
+}
+
+function drawEvent(event, y) {
+ y = drawEventHeader(event, y);
+ y = drawEventBody(event, y);
+ return y;
+}
+
+var curEventHeight = 0;
+
+function drawCurrentEvents(y) {
+ g.setColor(g.theme.dark ? "#0ff" : "#00f");
+ g.clearRect(0,y,g.getWidth()-5,y+curEventHeight);
+ curEventHeight = y;
+
+ if(current.length === 0) {
+ y = drawEvent({timestamp: getTime(), durationInSeconds: 100}, y);
+ } else {
+ y = drawEventHeader(current[0],y);
+ for (var e of current) {
+ y = drawEventBody(e,y);
+ }
+ }
+ curEventHeight = y-curEventHeight;
+ return y;
+}
+
+function drawFutureEvents(y) {
+ g.setColor(g.theme.fg);
+ for (var e of next) {
+ y = drawEvent(e, y);
+ if(y>g.getHeight())break;
+ }
+ return y;
+}
+
+function fullRedraw() {
+ g.clearRect(0,24,g.getWidth()-5,g.getHeight());
+ updateCalendar();
+ var y = 30;
+ y = drawCurrentEvents(y);
+ drawFutureEvents(y);
+}
+
+function redraw() {
+ g.reset();
+ if (current.find(e=>!isActive(e)) || next.find(isActive)) {
+ fullRedraw();
+ } else {
+ drawCurrentEvents(30);
+ }
+}
+
+g.clear();
+fullRedraw();
+var minuteInterval = setInterval(redraw, 60 * 1000);
+
+Bangle.setUI("clock");
+Bangle.loadWidgets();
+Bangle.drawWidgets();
diff --git a/apps/calclock/calclock.png b/apps/calclock/calclock.png
new file mode 100644
index 000000000..5f953c1ee
Binary files /dev/null and b/apps/calclock/calclock.png differ
diff --git a/apps/calclock/location.png b/apps/calclock/location.png
new file mode 100644
index 000000000..619e55775
Binary files /dev/null and b/apps/calclock/location.png differ
diff --git a/apps/calclock/metadata.json b/apps/calclock/metadata.json
new file mode 100644
index 000000000..bfd847595
--- /dev/null
+++ b/apps/calclock/metadata.json
@@ -0,0 +1,17 @@
+{
+ "id": "calclock",
+ "name": "Calendar Clock",
+ "shortName": "CalClock",
+ "version": "0.06",
+ "description": "Show the current and upcoming events synchronized from Gadgetbridge",
+ "icon": "calclock.png",
+ "type": "clock",
+ "tags": "clock agenda",
+ "supports": ["BANGLEJS2"],
+ "readme": "README.md",
+ "storage": [
+ {"name":"calclock.app.js","url":"calclock.js"},
+ {"name":"calclock.img","url":"calclock-icon.js","evaluate":true}
+ ],
+ "screenshots": [{"url":"screenshot.png"}]
+}
diff --git a/apps/calclock/screenshot.patch b/apps/calclock/screenshot.patch
new file mode 100644
index 000000000..3fdbf79d1
--- /dev/null
+++ b/apps/calclock/screenshot.patch
@@ -0,0 +1,32 @@
+diff --git a/apps/calclock/calclock.js b/apps/calclock/calclock.js
+index cb8c6100e..2092c1a4e 100644
+--- a/apps/calclock/calclock.js
++++ b/apps/calclock/calclock.js
+@@ -3,9 +3,24 @@ var current = [];
+ var next = [];
+
+ function updateCalendar() {
+- calendar = require("Storage").readJSON("android.calendar.json",true)||[];
+- calendar = calendar.filter(e => isActive(e) || getTime() <= e.timestamp);
+- calendar.sort((a,b) => a.timestamp - b.timestamp);
++ calendar = [
++ {
++ t: "calendar",
++ id: 2, type: 0, timestamp: getTime(), durationInSeconds: 200,
++ title: "Capture Screenshot",
++ description: "Capture Screenshot",
++ location: "",
++ calName: "",
++ color: -7151168, allDay: true },
++ {
++ t: "calendar",
++ id: 7186, type: 0, timestamp: getTime() + 2000, durationInSeconds: 100,
++ title: "Upload to BangleApps",
++ description: "",
++ location: "",
++ calName: "",
++ color: -509406, allDay: false }
++ ];
+
+ current = calendar.filter(isActive);
+ next = calendar.filter(e=>!isActive(e));
diff --git a/apps/calclock/screenshot.png b/apps/calclock/screenshot.png
new file mode 100644
index 000000000..8b2e39784
Binary files /dev/null and b/apps/calclock/screenshot.png differ
diff --git a/apps/calculator/ChangeLog b/apps/calculator/ChangeLog
index a08a0f5a7..2e1ace7bf 100644
--- a/apps/calculator/ChangeLog
+++ b/apps/calculator/ChangeLog
@@ -3,3 +3,5 @@
0.03: Support for different screen sizes and touchscreen
0.04: Display current operation on LHS
0.05: Grid positioning and swipe controls to switch between numbers, operators and special (for Bangle.js 2)
+0.06: Bangle.js 2: Exit with a short press of the physical button
+0.07: Bangle.js 2: Exit by pressing upper left corner of the screen
diff --git a/apps/calculator/README.md b/apps/calculator/README.md
index b25d355bf..62f6cef24 100644
--- a/apps/calculator/README.md
+++ b/apps/calculator/README.md
@@ -12,12 +12,20 @@ Basic calculator reminiscent of MacOs's one. Handy for small calculus.
## Controls
+Bangle.js 1
- UP: BTN1
- DOWN: BTN3
- LEFT: BTN4
- RIGHT: BTN5
- SELECT: BTN2
+Bangle.js 2
+- Swipes to change visible buttons
+- Click physical button to exit
+- Press upper left corner of screen to exit (where the red back button would be)
## Creator
+
+## Contributors
+[thyttan](https://github.com/thyttan)
diff --git a/apps/calculator/app.js b/apps/calculator/app.js
index 40953254e..d9a89a989 100644
--- a/apps/calculator/app.js
+++ b/apps/calculator/app.js
@@ -3,6 +3,8 @@
*
* Original Author: Frederic Rousseau https://github.com/fredericrous
* Created: April 2020
+ *
+ * Contributors: thyttan https://github.com/thyttan
*/
g.clear();
@@ -402,43 +404,42 @@ if (process.env.HWVERSION==1) {
swipeEnabled = false;
drawGlobal();
} else { // touchscreen?
- selected = "NONE";
+ selected = "NONE";
swipeEnabled = true;
prepareScreen(numbers, numbersGrid, COLORS.DEFAULT);
prepareScreen(operators, operatorsGrid, COLORS.OPERATOR);
prepareScreen(specials, specialsGrid, COLORS.SPECIAL);
drawNumbers();
- Bangle.on('touch',(n,e)=>{
- for (var key in screen) {
- if (typeof screen[key] == "undefined") break;
- var r = screen[key].xy;
- if (e.x>=r[0] && e.y>=r[1] &&
- e.x{
+ for (var key in screen) {
+ if (typeof screen[key] == "undefined") break;
+ var r = screen[key].xy;
+ if (e.x>=r[0] && e.y>=r[1] && e.x {
- if (!e.b) {
- if (lastX > 50) { // right
+ },
+ swipe : (LR, UD) => {
+ if (LR == 1) { // right
drawSpecials();
- } else if (lastX < -50) { // left
+ }
+ if (LR == -1) { // left
drawOperators();
- } else if (lastY > 50) { // down
- drawNumbers();
- } else if (lastY < -50) { // up
+ }
+ if (UD == 1) { // down
+ drawNumbers();
+ }
+ if (UD == -1) { // up
drawNumbers();
}
- lastX = 0;
- lastY = 0;
- } else {
- lastX = lastX + e.dx;
- lastY = lastY + e.dy;
}
});
+
}
-
displayOutput(0);
diff --git a/apps/calculator/metadata.json b/apps/calculator/metadata.json
index e78e4d54f..1674b7843 100644
--- a/apps/calculator/metadata.json
+++ b/apps/calculator/metadata.json
@@ -2,7 +2,7 @@
"id": "calculator",
"name": "Calculator",
"shortName": "Calculator",
- "version": "0.05",
+ "version": "0.07",
"description": "Basic calculator reminiscent of MacOs's one. Handy for small calculus.",
"icon": "calculator.png",
"screenshots": [{"url":"screenshot_calculator.png"}],
diff --git a/apps/calendar/ChangeLog b/apps/calendar/ChangeLog
index 0583ea45f..db455679c 100644
--- a/apps/calendar/ChangeLog
+++ b/apps/calendar/ChangeLog
@@ -9,3 +9,4 @@
read start of week from system settings
0.09: Fix scope of let variables
0.10: Use default Bangle formatter for booleans
+0.11: Fix off-by-one-error on next year
diff --git a/apps/calendar/calendar.js b/apps/calendar/calendar.js
index f4676fc22..f8785e52c 100644
--- a/apps/calendar/calendar.js
+++ b/apps/calendar/calendar.js
@@ -226,15 +226,14 @@ drawCalendar(date);
clearWatch();
Bangle.on("touch", area => {
const month = date.getMonth();
- let prevMonth;
if (area == 1) {
let prevMonth = month > 0 ? month - 1 : 11;
if (prevMonth === 11) date.setFullYear(date.getFullYear() - 1);
date.setMonth(prevMonth);
} else {
- let prevMonth = month < 11 ? month + 1 : 0;
- if (prevMonth === 0) date.setFullYear(date.getFullYear() + 1);
- date.setMonth(month + 1);
+ let nextMonth = month < 11 ? month + 1 : 0;
+ if (nextMonth === 0) date.setFullYear(date.getFullYear() + 1);
+ date.setMonth(nextMonth);
}
drawCalendar(date);
});
diff --git a/apps/calendar/metadata.json b/apps/calendar/metadata.json
index 48fd52d3e..88f20026d 100644
--- a/apps/calendar/metadata.json
+++ b/apps/calendar/metadata.json
@@ -1,7 +1,7 @@
{
"id": "calendar",
"name": "Calendar",
- "version": "0.10",
+ "version": "0.11",
"description": "Simple calendar",
"icon": "calendar.png",
"screenshots": [{"url":"screenshot_calendar.png"}],
diff --git a/apps/calibration/metadata.json b/apps/calibration/metadata.json
index b60650300..f428bd538 100644
--- a/apps/calibration/metadata.json
+++ b/apps/calibration/metadata.json
@@ -3,7 +3,7 @@
"shortName":"Calibration",
"icon": "calibration.png",
"version":"0.03",
- "description": "A simple calibration app for the touchscreen",
+ "description": "(NOT RECOMMENDED) A simple calibration app for the touchscreen. Please use the Touchscreen Calibration in the Settings app instead.",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"tags": "tool",
diff --git a/apps/cassioWatch/ChangeLog b/apps/cassioWatch/ChangeLog
index 419810021..1180554ff 100644
--- a/apps/cassioWatch/ChangeLog
+++ b/apps/cassioWatch/ChangeLog
@@ -8,4 +8,6 @@
0.7: Update Rocket Sequences Scope to not use memory all time
0.8: Update Some Variable Scopes to not use memory until need
0.9: Remove ESLint spaces
-0.10: Show daily steps, heartrate and the temperature if weather information is available.
\ No newline at end of file
+0.10: Show daily steps, heartrate and the temperature if weather information is available.
+0.11: Tell clock widgets to hide.
+0.12: Swipe down to see widgets, step counter now just uses getHealthStatus
diff --git a/apps/cassioWatch/README.md b/apps/cassioWatch/README.md
index aaeb3f122..6c13cdcac 100644
--- a/apps/cassioWatch/README.md
+++ b/apps/cassioWatch/README.md
@@ -8,4 +8,5 @@ It displays current temperature,day,steps,battery.heartbeat and weather.
**To-do**:
-Align and change size of some elements.
+
+* Align and change size of some elements
diff --git a/apps/cassioWatch/app.js b/apps/cassioWatch/app.js
index 6bbb9e823..19dd883d2 100644
--- a/apps/cassioWatch/app.js
+++ b/apps/cassioWatch/app.js
@@ -91,7 +91,6 @@ function getTemperature(){
var weatherJson = storage.readJSON('weather.json');
var weather = weatherJson.weather;
return Math.round(weather.temp-273.15);
-
} catch(ex) {
print(ex)
return "?"
@@ -99,20 +98,7 @@ function getTemperature(){
}
function getSteps() {
- var steps = 0;
- try{
- if (WIDGETS.wpedom !== undefined) {
- steps = WIDGETS.wpedom.getSteps();
- } else if (WIDGETS.activepedom !== undefined) {
- steps = WIDGETS.activepedom.getSteps();
- } else {
- steps = Bangle.getHealthStatus("day").steps;
- }
- } catch(ex) {
- // In case we failed, we can only show 0 steps.
- return "? k";
- }
-
+ var steps = Bangle.getHealthStatus("day").steps;
steps = Math.round(steps/1000);
return steps + "k";
}
@@ -121,8 +107,7 @@ function getSteps() {
function draw() {
queueDraw();
- g.reset();
- g.clear();
+ g.clear(1);
g.setColor(0, 255, 255);
g.fillRect(0, 0, g.getWidth(), g.getHeight());
let background = getBackgroundImage();
@@ -143,9 +128,6 @@ function draw() {
drawClock();
drawRocket();
drawBattery();
-
- // Hide widgets
- for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
}
Bangle.on("lcdPower", (on) => {
@@ -165,11 +147,10 @@ Bangle.on("lock", (locked) => {
}
});
+Bangle.setUI("clock");
// Load widgets, but don't show them
Bangle.loadWidgets();
-Bangle.setUI("clock");
-
-g.reset();
-g.clear();
-draw();
\ No newline at end of file
+require("widget_utils").swipeOn(); // hide widgets, make them visible with a swipe
+g.clear(1);
+draw();
diff --git a/apps/cassioWatch/metadata.json b/apps/cassioWatch/metadata.json
index dabdc2c93..5ac4502fd 100644
--- a/apps/cassioWatch/metadata.json
+++ b/apps/cassioWatch/metadata.json
@@ -4,7 +4,7 @@
"description": "Animated Clock with Space Cassio Watch Style",
"screenshots": [{ "url": "screens/screen_night.png" },{ "url": "screens/screen_day.png" }],
"icon": "app.png",
- "version": "0.10",
+ "version": "0.12",
"type": "clock",
"tags": "clock, weather, cassio, retro",
"supports": ["BANGLEJS2"],
diff --git a/apps/chimer/ChangeLog b/apps/chimer/ChangeLog
new file mode 100644
index 000000000..01bd00a0a
--- /dev/null
+++ b/apps/chimer/ChangeLog
@@ -0,0 +1,2 @@
+0.01: Initial Creation
+0.02: Fixed some sleep bugs. Added a sleep mode toggle
\ No newline at end of file
diff --git a/apps/chimer/README.MD b/apps/chimer/README.MD
new file mode 100644
index 000000000..a78c677f2
--- /dev/null
+++ b/apps/chimer/README.MD
@@ -0,0 +1,11 @@
+# Chimer - For the BangleJS
+
+A fork of [Hour Chime](https://github.com/espruino/BangleApps/tree/master/apps/widchime) that adds extra features such as:
+
+- Buzz or beep on every 60, 30 or 15 minutes.
+- Repeat Chime up to 3 times
+- Set hours to disable chime
+
+Setting the hours you don't want your watch to chime for is done by setting the hour you want it to stop, and the hour you want it to start.
+
+Hours range from 0 - 23.
diff --git a/apps/chimer/icon.txt b/apps/chimer/icon.txt
new file mode 100644
index 000000000..cc969bc81
--- /dev/null
+++ b/apps/chimer/icon.txt
@@ -0,0 +1,2 @@
+
+widget.png: "https://icons8.com/icon/114436/alarm"
\ No newline at end of file
diff --git a/apps/chimer/metadata.json b/apps/chimer/metadata.json
new file mode 100644
index 000000000..d5bc04950
--- /dev/null
+++ b/apps/chimer/metadata.json
@@ -0,0 +1,16 @@
+{
+ "id": "chimer",
+ "name": "Chimer",
+ "version": "0.02",
+ "description": "A fork of Hour Chime that adds extra features such as: \n - Buzz or beep on every 60, 30 or 15 minutes. \n - Reapeat Chime up to 3 times \n - Set hours to disable chime",
+ "icon": "widget.png",
+ "type": "widget",
+ "tags": "widget",
+ "supports": ["BANGLEJS", "BANGLEJS2"],
+ "readme": "README.MD",
+ "storage": [
+ { "name": "chimer.wid.js", "url": "widget.js" },
+ { "name": "chimer.settings.js", "url": "settings.js" }
+ ],
+ "data": [{ "name": "chimer.json" }]
+}
diff --git a/apps/chimer/settings.js b/apps/chimer/settings.js
new file mode 100644
index 000000000..55160c9be
--- /dev/null
+++ b/apps/chimer/settings.js
@@ -0,0 +1,94 @@
+/**
+ * @param {function} back Use back() to return to settings menu
+ */
+
+(function (back) {
+ // default to buzzing
+ var FILE = "chimer.json";
+ var settings = {};
+ const chimes = ["Off", "Buzz", "Beep", "Both"];
+ const frequency = ["60 min", "30 min", "15 min", "1 min"];
+
+ var showMainMenu = () => {
+ E.showMenu({
+ "": { title: "Chimer" },
+ "< Back": () => back(),
+ "Chime Type": {
+ value: settings.type,
+ min: 0,
+ max: 2, // both is just silly
+ format: (v) => chimes[v],
+ onchange: (v) => {
+ settings.type = v;
+ writeSettings(settings);
+ },
+ },
+ Frequency: {
+ value: settings.freq,
+ min: 0,
+ max: 2,
+ format: (v) => frequency[v],
+ onchange: (v) => {
+ settings.freq = v;
+ writeSettings(settings);
+ },
+ },
+ Repetition: {
+ value: settings.repeat,
+ min: 1,
+ max: 5,
+ format: (v) => v,
+ onchange: (v) => {
+ settings.repeat = v;
+ writeSettings(settings);
+ },
+ },
+ "Sleep Mode": {
+ value: !!settings.sleep,
+ onchange: (v) => {
+ settings.sleep = v;
+ writeSettings(settings);
+ },
+ },
+ "Sleep Start": {
+ value: settings.start,
+ min: 0,
+ max: 23,
+ format: (v) => v,
+ onchange: (v) => {
+ settings.start = v;
+ writeSettings(settings);
+ },
+ },
+ "Sleep End": {
+ value: settings.end,
+ min: 0,
+ max: 23,
+ format: (v) => v,
+ onchange: (v) => {
+ settings.end = v;
+ writeSettings(settings);
+ },
+ },
+ });
+ };
+
+ var readSettings = () => {
+ var settings = require("Storage").readJSON(FILE, 1) || {
+ type: 1,
+ freq: 0,
+ repeat: 1,
+ sleep: true,
+ start: 6,
+ end: 22,
+ };
+ return settings;
+ };
+
+ var writeSettings = (settings) => {
+ require("Storage").writeJSON(FILE, settings);
+ };
+
+ settings = readSettings();
+ showMainMenu();
+});
diff --git a/apps/chimer/widget.js b/apps/chimer/widget.js
new file mode 100644
index 000000000..18358df9e
--- /dev/null
+++ b/apps/chimer/widget.js
@@ -0,0 +1,134 @@
+(function () {
+ // 0: off, 1: buzz, 2: beep, 3: both
+ var FILE = "chimer.json";
+
+ var readSettings = () => {
+ var settings = require("Storage").readJSON(FILE, 1) || {
+ type: 1,
+ freq: 0,
+ repeat: 1,
+ sleep: true,
+ start: 6,
+ end: 22,
+ };
+ return settings;
+ };
+
+ var settings = readSettings();
+
+ function sleep(milliseconds) {
+ const date = Date.now();
+ let currentDate = null;
+ do {
+ currentDate = Date.now();
+ } while (currentDate - date < milliseconds);
+ }
+
+ function chime() {
+ for (var i = 0; i < settings.repeat; i++) {
+ if (settings.type === 1) {
+ Bangle.buzz(100);
+ } else if (settings.type === 2) {
+ Bangle.beep();
+ } else {
+ return;
+ }
+ sleep(150);
+ }
+ }
+
+ let lastHour = new Date().getHours();
+ let lastMinute = new Date().getMinutes(); // don't chime when (re)loaded at a whole hour
+ function check() {
+ const now = new Date(),
+ h = now.getHours(),
+ m = now.getMinutes(),
+ s = now.getSeconds(),
+ ms = now.getMilliseconds();
+ if (
+ (settings.sleep && h > settings.end) ||
+ (settings.sleep && h >= settings.end && m !== 0) ||
+ (settings.sleep && h < settings.start)
+ ) {
+ var mLeft = 60 - m,
+ sLeft = mLeft * 60 - s,
+ msLeft = sLeft * 1000 - ms;
+ setTimeout(check, msLeft);
+ return;
+ }
+ if (settings.freq === 1) {
+ if ((m !== lastMinute && m === 0) || (m !== lastMinute && m === 30))
+ chime();
+ lastHour = h;
+ lastMinute = m;
+ // check again in 30 minutes
+ switch (true) {
+ case m / 30 >= 1:
+ var mLeft = 30 - (m - 30),
+ sLeft = mLeft * 60 - s,
+ msLeft = sLeft * 1000 - ms;
+ break;
+ case m / 30 < 1:
+ var mLeft = 30 - m,
+ sLeft = mLeft * 60 - s,
+ msLeft = sLeft * 1000 - ms;
+ break;
+ }
+ setTimeout(check, msLeft);
+ } else if (settings.freq === 2) {
+ if (
+ (m !== lastMinute && m === 0) ||
+ (m !== lastMinute && m === 15) ||
+ (m !== lastMinute && m === 30) ||
+ (m !== lastMinute && m === 45)
+ )
+ chime();
+ lastHour = h;
+ lastMinute = m;
+ // check again in 15 minutes
+ switch (true) {
+ case m / 15 >= 3:
+ var mLeft = 15 - (m - 45),
+ sLeft = mLeft * 60 - s,
+ msLeft = sLeft * 1000 - ms;
+ break;
+ case m / 15 >= 2:
+ var mLeft = 15 - (m - 30),
+ sLeft = mLeft * 60 - s,
+ msLeft = sLeft * 1000 - ms;
+ break;
+ case m / 15 >= 1:
+ var mLeft = 15 - (m - 15),
+ sLeft = mLeft * 60 - s,
+ msLeft = sLeft * 1000 - ms;
+ break;
+ case m / 15 < 1:
+ var mLeft = 15 - m,
+ sLeft = mLeft * 60 - s,
+ msLeft = sLeft * 1000 - ms;
+ break;
+ }
+ setTimeout(check, msLeft);
+ } else if (settings.freq === 3) {
+ if (m !== lastMinute) chime();
+ lastHour = h;
+ lastMinute = m;
+ // check again in 1 minute
+
+ var mLeft = 1,
+ sLeft = mLeft * 60 - s,
+ msLeft = sLeft * 1000 - ms;
+ setTimeout(check, msLeft);
+ } else {
+ if (h !== lastHour && m === 0) chime();
+ lastHour = h;
+ // check again in 60 minutes
+ var mLeft = 60 - m,
+ sLeft = mLeft * 60 - s,
+ msLeft = sLeft * 1000 - ms;
+ setTimeout(check, msLeft);
+ }
+ }
+
+ check();
+})();
diff --git a/apps/chimer/widget.png b/apps/chimer/widget.png
new file mode 100644
index 000000000..14edf4150
Binary files /dev/null and b/apps/chimer/widget.png differ
diff --git a/apps/choozi/ChangeLog b/apps/choozi/ChangeLog
index 03f7ef832..35adc7430 100644
--- a/apps/choozi/ChangeLog
+++ b/apps/choozi/ChangeLog
@@ -1,3 +1,9 @@
0.01: New App!
0.02: Support Bangle.js 2
0.03: Fix bug for Bangle.js 2 where g.flip was not being called.
+0.04: Combine code for both apps
+ Better colors for Bangle.js 2
+ Fix selection animation for Bangle.js 2
+ New icon
+ Slightly wider arc segments for better visibility
+ Extract arc drawing code in library
diff --git a/apps/choozi/README.md b/apps/choozi/README.md
index f1e4255bc..ccaa97a27 100644
--- a/apps/choozi/README.md
+++ b/apps/choozi/README.md
@@ -11,16 +11,21 @@ the players seated in a circle, set the number of segments equal to the number
of players, ensure that each person knows which colour represents them, and then
choose a segment. After a short animation, the chosen segment will fill the screen.
-You can use Choozi to randomly select an element from any set with 2 to 13 members,
+You can use Choozi to randomly select an element from any set with 2 to 15 members,
as long as you can define a bijection between members of the set and coloured
segments on the Bangle.js display.
-## Controls
+## Controls Bangle 1
BTN1: increase the number of segments
BTN2: choose a segment at random
BTN3: decrease the number of segments
+## Controls Bangle 2
+
+Swipe up/down: increase/decrease the number of segments
+BTN1 or tap: choose a segment at random
+
## Creator
James Stanley
diff --git a/apps/choozi/app-icon.js b/apps/choozi/app-icon.js
index 51b3bead3..560286098 100644
--- a/apps/choozi/app-icon.js
+++ b/apps/choozi/app-icon.js
@@ -1 +1 @@
-require("heatshrink").decompress(atob("mEwggLIrnM4uqAAIhPgvMAAPFzIABzWgCxkMCweqC4QABDBYtC5QVFDBoWCCo5KLOQIWKDARFICxhJIFwOpC5owFFyAwGUYIuOGAwuRC4guSJAgXBCyIwDIyQXF5IXSzJeVMAReUAAOQhheTMAVcC6yOUC4aOUC7GZUyoXXzWqhQXVxGqC9mYC7OqC9eoxEKC6uBC6uIwAXBPCSmBwEAC6Z2BiAXBJCR2BgEAjQXSlGBC4JgSLwYABJCJGBLwJIDGB+IIwRIDGByNBIwZIDGBhdBRoQwSLoIuFGAYYKCwIuGGAgYI1QWBRgYYJMYmaFoSMEAAyrBAAgVCCxgYGjAWQAAMBC4UILZQA=="))
+require("heatshrink").decompress(atob("mEwwcH/4AW/u27dt2wQL/YOBCIXbv4QI+AODAQVsh4RHwEbCI0LCI9gCIOANAXbsFbG437tkDPg1btoRFFoILBgmSpMggECHQO/CAf2CIVJkgRBAQIjC24RFsECCItIgIRFMYMAiQRFpMAlqmDVwPYgAOEAQUggu274RD4BWCCIskCIPbCIPt20ABwwCCwARFgIRJyEWCIVt2EJCJi2BCJmSUgIRCwARNt/7CIIOICI1sWAwCFoFbCOtt8EACJsAgARR8hwBCJlJk4RlgARQAgIRKDwMn/gRBdJgRPyARBn4RBpARLiQRB/4RBgIRJwAREpIRLAYP///ypMgCJMACI0ECI4JCp4RB/wZECIsAAYN/CIP/5JPDCIhjDCIraHTIWTCAX//K7DCI+fCIf/EZA1CCAn//ipCLIsBk4RF/5ZHCIIQG//wPo8vCI//6QRFpYQIAAPpCIeXCBQAC/VfBI4="))
\ No newline at end of file
diff --git a/apps/choozi/app.js b/apps/choozi/app.js
index 1a5b2f17e..b9f53bc89 100644
--- a/apps/choozi/app.js
+++ b/apps/choozi/app.js
@@ -4,15 +4,16 @@
*
* James Stanley 2021
*/
-
-var colours = ['#ff0000', '#ff8080', '#00ff00', '#80ff80', '#0000ff', '#8080ff', '#ffff00', '#00ffff', '#ff00ff', '#ff8000', '#ff0080', '#8000ff', '#0080ff'];
+const GU = require("graphics_utils");
+var colours = ['#ff0000', '#00ff00', '#0000ff', '#ffff00', '#00ffff', '#ff00ff', '#ffffff'];
+var colours2 = ['#808080', '#404040', '#000040', '#004000', '#400000', '#ff8000', '#804000', '#4000c0'];
var stepAngle = 0.18; // radians - resolution of polygon
var gapAngle = 0.035; // radians - gap between segments
-var perimMin = 110; // px - min. radius of perimeter
-var perimMax = 120; // px - max. radius of perimeter
+var perimMin = g.getWidth()*0.40; // px - min. radius of perimeter
+var perimMax = g.getWidth()*0.49; // px - max. radius of perimeter
-var segmentMax = 106; // px - max radius of filled-in segment
+var segmentMax = g.getWidth()*0.38; // px - max radius of filled-in segment
var segmentStep = 5; // px - step size of segment fill animation
var circleStep = 4; // px - step size of circle fill animation
@@ -22,10 +23,10 @@ var minSpeed = 0.001; // rad/sec
var animStartSteps = 300; // how many steps before it can start slowing?
var accel = 0.0002; // rad/sec/sec - acc-/deceleration rate
var ballSize = 3; // px - ball radius
-var ballTrack = 100; // px - radius of ball path
+var ballTrack = perimMin - ballSize*2; // px - radius of ball path
-var centreX = 120; // px - centre of screen
-var centreY = 120; // px - centre of screen
+var centreX = g.getWidth()*0.5; // px - centre of screen
+var centreY = g.getWidth()*0.5; // px - centre of screen
var fontSize = 50; // px
@@ -33,7 +34,6 @@ var radians = 2*Math.PI; // radians per circle
var defaultN = 3; // default value for N
var minN = 2;
-var maxN = colours.length;
var N;
var arclen;
@@ -51,42 +51,14 @@ function shuffle (array) {
}
}
-// draw an arc between radii minR and maxR, and between
-// angles minAngle and maxAngle
-function arc(minR, maxR, minAngle, maxAngle) {
- var step = stepAngle;
- var angle = minAngle;
- var inside = [];
- var outside = [];
- var c, s;
- while (angle < maxAngle) {
- c = Math.cos(angle);
- s = Math.sin(angle);
- inside.push(centreX+c*minR); // x
- inside.push(centreY+s*minR); // y
- // outside coordinates are built up in reverse order
- outside.unshift(centreY+s*maxR); // y
- outside.unshift(centreX+c*maxR); // x
- angle += step;
- }
- c = Math.cos(maxAngle);
- s = Math.sin(maxAngle);
- inside.push(centreX+c*minR);
- inside.push(centreY+s*minR);
- outside.unshift(centreY+s*maxR);
- outside.unshift(centreX+c*maxR);
-
- var vertices = inside.concat(outside);
- g.fillPoly(vertices, true);
-}
-
// draw the arc segments around the perimeter
function drawPerimeter() {
+ g.setBgColor('#000000');
g.clear();
for (var i = 0; i < N; i++) {
g.setColor(colours[i%colours.length]);
var minAngle = (i/N)*radians;
- arc(perimMin,perimMax,minAngle,minAngle+arclen);
+ GU.fillArc(g, centreX, centreY, perimMin,perimMax,minAngle,minAngle+arclen, stepAngle);
}
}
@@ -131,6 +103,7 @@ function animateChoice(target) {
g.fillCircle(x, y, ballSize);
oldx=x;
oldy=y;
+ if (process.env.HWVERSION == 2) g.flip();
}
}
@@ -141,11 +114,15 @@ function choose() {
var maxAngle = minAngle + arclen;
animateChoice((minAngle+maxAngle)/2);
g.setColor(colours[chosen%colours.length]);
- for (var i = segmentMax-segmentStep; i >= 0; i -= segmentStep)
- arc(i, perimMax, minAngle, maxAngle);
- arc(0, perimMax, minAngle, maxAngle);
- for (var r = 1; r < segmentMax; r += circleStep)
+ for (var i = segmentMax-segmentStep; i >= 0; i -= segmentStep){
+ GU.fillArc(g, centreX, centreY, i, perimMax, minAngle, maxAngle, stepAngle);
+ if (process.env.HWVERSION == 2) g.flip();
+ }
+ GU.fillArc(g, centreX, centreY, 0, perimMax, minAngle, maxAngle, stepAngle);
+ for (var r = 1; r < segmentMax; r += circleStep){
g.fillCircle(centreX,centreY,r);
+ if (process.env.HWVERSION == 2) g.flip();
+ }
g.fillCircle(centreX,centreY,segmentMax);
}
@@ -171,38 +148,47 @@ function setN(n) {
drawPerimeter();
}
-// save N to choozi.txt
+// save N to choozi.save
function writeN() {
- var file = require("Storage").open("choozi.txt","w");
- file.write(N);
+ var savedN = read();
+ if (savedN != N) require("Storage").write("choozi.save","" + N);
}
-// load N from choozi.txt
+function read(){
+ var n = require("Storage").read("choozi.save");
+ if (n !== undefined) return parseInt(n);
+ return defaultN;
+}
+
+// load N from choozi.save
function readN() {
- var file = require("Storage").open("choozi.txt","r");
- var n = file.readLine();
- if (n !== undefined) setN(parseInt(n));
- else setN(defaultN);
+ setN(read());
}
-shuffle(colours); // is this really best?
-Bangle.setLCDMode("direct");
-Bangle.setLCDTimeout(0); // keep screen on
+if (process.env.HWVERSION == 1){
+ colours=colours.concat(colours2);
+ shuffle(colours);
+} else {
+ shuffle(colours);
+ shuffle(colours2);
+ colours=colours.concat(colours2);
+}
+
+var maxN = colours.length;
+if (process.env.HWVERSION == 1){
+ Bangle.setLCDMode("direct");
+ Bangle.setLCDTimeout(0); // keep screen on
+}
readN();
drawN();
-setWatch(() => {
- setN(N+1);
- drawN();
-}, BTN1, {repeat:true});
-
-setWatch(() => {
- writeN();
- drawPerimeter();
- choose();
-}, BTN2, {repeat:true});
-
-setWatch(() => {
- setN(N-1);
- drawN();
-}, BTN3, {repeat:true});
+Bangle.setUI("updown", (v)=>{
+ if (!v){
+ writeN();
+ drawPerimeter();
+ choose();
+ } else {
+ setN(N-v);
+ drawN();
+ }
+});
diff --git a/apps/choozi/app.png b/apps/choozi/app.png
index 99c9fa07a..50f09f164 100644
Binary files a/apps/choozi/app.png and b/apps/choozi/app.png differ
diff --git a/apps/choozi/appb2.js b/apps/choozi/appb2.js
deleted file mode 100644
index 5f217f638..000000000
--- a/apps/choozi/appb2.js
+++ /dev/null
@@ -1,207 +0,0 @@
-/* Choozi - Choose people or things at random using Bangle.js.
- * Inspired by the "Chwazi" Android app
- *
- * James Stanley 2021
- */
-
-var colours = ['#ff0000', '#ff8080', '#00ff00', '#80ff80', '#0000ff', '#8080ff', '#ffff00', '#00ffff', '#ff00ff', '#ff8000', '#ff0080', '#8000ff', '#0080ff'];
-
-var stepAngle = 0.18; // radians - resolution of polygon
-var gapAngle = 0.035; // radians - gap between segments
-var perimMin = 80; // px - min. radius of perimeter
-var perimMax = 87; // px - max. radius of perimeter
-
-var segmentMax = 70; // px - max radius of filled-in segment
-var segmentStep = 5; // px - step size of segment fill animation
-var circleStep = 4; // px - step size of circle fill animation
-
-// rolling ball animation:
-var maxSpeed = 0.08; // rad/sec
-var minSpeed = 0.001; // rad/sec
-var animStartSteps = 300; // how many steps before it can start slowing?
-var accel = 0.0002; // rad/sec/sec - acc-/deceleration rate
-var ballSize = 3; // px - ball radius
-var ballTrack = 75; // px - radius of ball path
-
-var centreX = 88; // px - centre of screen
-var centreY = 88; // px - centre of screen
-
-var fontSize = 50; // px
-
-var radians = 2*Math.PI; // radians per circle
-
-var defaultN = 3; // default value for N
-var minN = 2;
-var maxN = colours.length;
-var N;
-var arclen;
-
-// https://www.frankmitchell.org/2015/01/fisher-yates/
-function shuffle (array) {
- var i = 0
- , j = 0
- , temp = null;
-
- for (i = array.length - 1; i > 0; i -= 1) {
- j = Math.floor(Math.random() * (i + 1));
- temp = array[i];
- array[i] = array[j];
- array[j] = temp;
- }
-}
-
-// draw an arc between radii minR and maxR, and between
-// angles minAngle and maxAngle
-function arc(minR, maxR, minAngle, maxAngle) {
- var step = stepAngle;
- var angle = minAngle;
- var inside = [];
- var outside = [];
- var c, s;
- while (angle < maxAngle) {
- c = Math.cos(angle);
- s = Math.sin(angle);
- inside.push(centreX+c*minR); // x
- inside.push(centreY+s*minR); // y
- // outside coordinates are built up in reverse order
- outside.unshift(centreY+s*maxR); // y
- outside.unshift(centreX+c*maxR); // x
- angle += step;
- }
- c = Math.cos(maxAngle);
- s = Math.sin(maxAngle);
- inside.push(centreX+c*minR);
- inside.push(centreY+s*minR);
- outside.unshift(centreY+s*maxR);
- outside.unshift(centreX+c*maxR);
-
- var vertices = inside.concat(outside);
- g.fillPoly(vertices, true);
-}
-
-// draw the arc segments around the perimeter
-function drawPerimeter() {
- g.clear();
- for (var i = 0; i < N; i++) {
- g.setColor(colours[i%colours.length]);
- var minAngle = (i/N)*radians;
- arc(perimMin,perimMax,minAngle,minAngle+arclen);
- }
-}
-
-// animate a ball rolling around and settling at "target" radians
-function animateChoice(target) {
- var angle = 0;
- var speed = 0;
- var oldx = -10;
- var oldy = -10;
- var decelFromAngle = -1;
- var allowDecel = false;
- for (var i = 0; true; i++) {
- angle = angle + speed;
- if (angle > radians) angle -= radians;
- if (i < animStartSteps || (speed < maxSpeed && !allowDecel)) {
- speed = speed + accel;
- if (speed > maxSpeed) {
- speed = maxSpeed;
- /* when we reach max speed, we know how long it takes
- * to accelerate, and therefore how long to decelerate, so
- * we can work out what angle to start decelerating from */
- if (decelFromAngle < 0) {
- decelFromAngle = target-angle;
- while (decelFromAngle < 0) decelFromAngle += radians;
- while (decelFromAngle > radians) decelFromAngle -= radians;
- }
- }
- } else {
- if (!allowDecel && (angle < decelFromAngle) && (angle+speed >= decelFromAngle)) allowDecel = true;
- if (allowDecel) speed = speed - accel;
- if (speed < minSpeed) speed = minSpeed;
- if (speed == minSpeed && angle < target && angle+speed >= target) return;
- }
-
- var r = i/2;
- if (r > ballTrack) r = ballTrack;
- var x = centreX+Math.cos(angle)*r;
- var y = centreY+Math.sin(angle)*r;
- g.setColor('#000000');
- g.fillCircle(oldx,oldy,ballSize+1);
- g.setColor('#ffffff');
- g.fillCircle(x, y, ballSize);
- oldx=x;
- oldy=y;
- g.flip();
- }
-}
-
-// choose a winning segment and animate its selection
-function choose() {
- var chosen = Math.floor(Math.random()*N);
- var minAngle = (chosen/N)*radians;
- var maxAngle = minAngle + arclen;
- animateChoice((minAngle+maxAngle)/2);
- g.setColor(colours[chosen%colours.length]);
- for (var i = segmentMax-segmentStep; i >= 0; i -= segmentStep)
- arc(i, perimMax, minAngle, maxAngle);
- arc(0, perimMax, minAngle, maxAngle);
- for (var r = 1; r < segmentMax; r += circleStep)
- g.fillCircle(centreX,centreY,r);
- g.fillCircle(centreX,centreY,segmentMax);
-}
-
-// draw the current value of N in the middle of the screen, with
-// up/down arrows
-function drawN() {
- g.setColor(g.theme.fg);
- g.setFont("Vector",fontSize);
- g.drawString(N,centreX-g.stringWidth(N)/2+4,centreY-fontSize/2);
- if (N < maxN)
- g.fillPoly([centreX-6,centreY-fontSize/2-7, centreX+6,centreY-fontSize/2-7, centreX, centreY-fontSize/2-14]);
- if (N > minN)
- g.fillPoly([centreX-6,centreY+fontSize/2+5, centreX+6,centreY+fontSize/2+5, centreX, centreY+fontSize/2+12]);
-}
-
-// update number of segments, with min/max limit, "arclen" update,
-// and screen reset
-function setN(n) {
- N = n;
- if (N < minN) N = minN;
- if (N > maxN) N = maxN;
- arclen = radians/N - gapAngle;
- drawPerimeter();
-}
-
-// save N to choozi.txt
-function writeN() {
- var file = require("Storage").open("choozi.txt","w");
- file.write(N);
-}
-
-// load N from choozi.txt
-function readN() {
- var file = require("Storage").open("choozi.txt","r");
- var n = file.readLine();
- if (n !== undefined) setN(parseInt(n));
- else setN(defaultN);
-}
-
-shuffle(colours); // is this really best?
-Bangle.setLCDTimeout(0); // keep screen on
-readN();
-drawN();
-
-setWatch(() => {
- writeN();
- drawPerimeter();
- choose();
-}, BTN1, {repeat:true});
-
-Bangle.on('touch', function(zone,e) {
- if(e.x>+88){
- setN(N-1);
- drawN();
- }else{
- setN(N+1);
- drawN();
- }
-});
diff --git a/apps/choozi/bangle1-choozi-screenshot1.png b/apps/choozi/bangle1-choozi-screenshot1.png
index 104024958..ee422ed10 100644
Binary files a/apps/choozi/bangle1-choozi-screenshot1.png and b/apps/choozi/bangle1-choozi-screenshot1.png differ
diff --git a/apps/choozi/bangle1-choozi-screenshot2.png b/apps/choozi/bangle1-choozi-screenshot2.png
index f3b6868bf..20edf4c78 100644
Binary files a/apps/choozi/bangle1-choozi-screenshot2.png and b/apps/choozi/bangle1-choozi-screenshot2.png differ
diff --git a/apps/choozi/metadata.json b/apps/choozi/metadata.json
index 79af76fa2..c42abe079 100644
--- a/apps/choozi/metadata.json
+++ b/apps/choozi/metadata.json
@@ -1,7 +1,7 @@
{
"id": "choozi",
"name": "Choozi",
- "version": "0.03",
+ "version": "0.04",
"description": "Choose people or things at random using Bangle.js.",
"icon": "app.png",
"tags": "tool",
@@ -10,8 +10,10 @@
"allow_emulator": true,
"screenshots": [{"url":"bangle1-choozi-screenshot1.png"},{"url":"bangle1-choozi-screenshot2.png"}],
"storage": [
- {"name":"choozi.app.js","url":"app.js","supports": ["BANGLEJS"]},
- {"name":"choozi.app.js","url":"appb2.js","supports": ["BANGLEJS2"]},
+ {"name":"choozi.app.js","url":"app.js"},
{"name":"choozi.img","url":"app-icon.js","evaluate":true}
+ ],
+ "data": [
+ {"name":"choozi.save"}
]
}
diff --git a/apps/circlesclock/ChangeLog b/apps/circlesclock/ChangeLog
index c398a89b6..83abde6df 100644
--- a/apps/circlesclock/ChangeLog
+++ b/apps/circlesclock/ChangeLog
@@ -26,3 +26,16 @@
0.12: Allow configuration of update interval
0.13: Load step goal from Bangle health app as fallback
Memory optimizations
+0.14: Support to show big weather info
+0.15: Use Bangle.setUI({remove:...}) to allow loading the launcher without a full reset on 2v16
+0.16: Fix const error
+ Use widget_utils if available
+0.17: Load circles from clkinfo
+0.18: Improved clkinfo handling and using it for the weather circle
+0.19: Remove old code and fixing clkinfo handling (fix HRM and other items that change)
+ Remove settings for what is displayed and instead allow circles to be changed by swiping
+0.20: Add much faster circle rendering (250ms -> 40ms)
+ Add fast load capability
+0.21: Remade all icons without a palette for dark theme
+ Now re-adds widgets if they were hidden when fast-loading
+0.22: Fixed crash if item has no image and cutting long overflowing text
diff --git a/apps/circlesclock/README.md b/apps/circlesclock/README.md
index aa429d5ec..7f6a2585c 100644
--- a/apps/circlesclock/README.md
+++ b/apps/circlesclock/README.md
@@ -5,28 +5,41 @@ A clock with three or four circles for different data at the bottom in a probabl
By default the time, date and day of week is shown.
It can show the following information (this can be configured):
+
* Steps
* Steps distance
* Heart rate (automatically updates when screen is on and unlocked)
* Battery (including charging status and battery low warning)
- * Weather (requires [weather app](https://banglejs.com/apps/#weather))
+ * Weather (requires [OWM weather provider](https://banglejs.com/apps/?id=owmweather))
* Humidity or wind speed as circle progress
* Temperature inside circle
* Condition as icon below circle
- * Time and progress until next sunrise or sunset (requires [my location app](https://banglejs.com/apps/#mylocation))
- * Temperature, air pressure or altitude from internal pressure sensor
+ * Big weather icon next to clock
+ * Altitude from internal pressure sensor
+ * Active alarms (if `Alarm` app installed)
+ * Sunrise or sunset (if `Sunrise Clockinfo` app installed)
+To change what is shown:
-The color of each circle can be configured. The following colors are available:
+* Unlock the watch
+* Tap on the circle to change (a border is drawn around it)
+* Swipe up/down to change the guage within the given group
+* Swipe left/right to change the group (eg. between standard Bangle.js and Alarms/etc)
+
+Data is provided by ['Clock Info'](http://www.espruino.com/Bangle.js+Clock+Info)
+so any apps that implement this feature can add extra information to be displayed.
+
+The color of each circle can be configured from `Settings -> Apps -> Circles Clock`. The following colors are available:
* Basic colors (red, green, blue, yellow, magenta, cyan, black, white)
* Color depending on value (green -> red, red -> green)
-
## Screenshots




+
+
## Ideas
* Show compass heading
@@ -35,4 +48,5 @@ The color of each circle can be configured. The following colors are available:
Marco ([myxor](https://github.com/myxor))
## Icons
-Icons taken from [materialdesignicons](https://materialdesignicons.com) under Apache License 2.0
+Most of the icons are taken from [materialdesignicons](https://materialdesignicons.com) under Apache License 2.0 except the big weather icons which are from
+[icons8](https://icons8.com/icon/set/weather/small--static--black)
diff --git a/apps/circlesclock/app.js b/apps/circlesclock/app.js
index 83a0aa027..30d6a48f4 100644
--- a/apps/circlesclock/app.js
+++ b/apps/circlesclock/app.js
@@ -1,5 +1,3 @@
-const locale = require("locale");
-const storage = require("Storage");
Graphics.prototype.setFontRobotoRegular50NumericOnly = function(scale) {
// Actual height 39 (40 - 2)
this.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAAB8AAAAAAAfAAAAAAAPwAAAAAAB8AAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAA4AAAAAAB+AAAAAAD/gAAAAAD/4AAAAAH/4AAAAAP/wAAAAAP/gAAAAAf/gAAAAAf/AAAAAA/+AAAAAB/+AAAAAB/8AAAAAD/4AAAAAH/4AAAAAD/wAAAAAA/wAAAAAAPgAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///wAAAB////gAAA////8AAA/////gAAP////8AAH8AAA/gAB8AAAD4AA+AAAAfAAPAAAADwADwAAAA8AA8AAAAPAAPAAAADwADwAAAA8AA8AAAAPAAPgAAAHwAB8AAAD4AAfwAAD+AAD/////AAA/////wAAH////4AAAf///4AAAB///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAPgAAAAAADwAAAAAAB8AAAAAAAfAAAAAAAHgAAAAAAD4AAAAAAA+AAAAAAAPAAAAAAAH/////wAB/////8AA//////AAP/////wAD/////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAfgAADwAAP4AAB8AAH+AAA/AAD/gAAfwAB/AAAf8AAfAAAP/AAPgAAH7wAD4AAD88AA8AAB+PAAPAAA/DwADwAAfg8AA8AAPwPAAPAAH4DwADwAH8A8AA+AD+APAAPwB/ADwAB/D/gA8AAf//gAPAAD//wADwAAf/wAA8AAD/4AAPAAAHwAADwAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAADgAAAHwAA+AAAD8AAP4AAB/AAD/AAA/wAA/wAAf4AAD+AAHwAAAPgAD4APAB8AA+ADwAPAAPAA8ADwADwAPAA8AA8ADwAPAAPAA8ADwADwAfAA8AA8AH4APAAPgD+AHwAB8B/wD4AAf7/+B+AAD//v//AAA//x//wAAD/4P/4AAAf8B/4AAAAYAH4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAHwAAAAAAH8AAAAAAD/AAAAAAD/wAAAAAD/8AAAAAB/vAAAAAB/jwAAAAA/g8AAAAA/wPAAAAAfwDwAAAAf4A8AAAAf4APAAAAP8ADwAAAP8AA8AAAH8AAPAAAD/////8AA//////AAP/////wAD/////8AA//////AAAAAAPAAAAAAADwAAAAAAA8AAAAAAAPAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAB/APwAAH//wD+AAD//8A/wAA///AH+AAP//wAPgAD/B4AB8AA8A+AAfAAPAPAADwADwDwAA8AA8A8AAPAAPAPAADwADwD4AA8AA8A+AAPAAPAPwAHwADwD8AD4AA8AfwD+AAPAH///AADwA///wAA8AH//4AAPAAf/4AAAAAB/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//AAAAAD//+AAAAD///4AAAD////AAAB////4AAA/78D/AAAfw8AH4AAPweAA+AAD4PgAHwAB8DwAA8AAfA8AAPAAHgPAADwAD4DwAA8AA+A8AAPAAPAPgAHwADwD4AB8AA8AfgA+AAPAH+B/gAAAA///wAAAAH//4AAAAA//8AAAAAH/8AAAAAAP4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAA8AAAAAAAPAAAAAAADwAAAAAAA8AAAABAAPAAAABwADwAAAB8AA8AAAB/AAPAAAB/wADwAAD/8AA8AAD/8AAPAAD/4AADwAD/4AAA8AD/4AAAPAH/wAAADwH/wAAAA8H/wAAAAPH/wAAAAD3/gAAAAA//gAAAAAP/gAAAAAD/gAAAAAA/AAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwA/4AAAH/Af/AAAH/8P/4AAD//n//AAA//7//4AAfx/+A+AAHwD+AHwAD4AfgB8AA8AHwAPAAPAA8ADwADwAPAA8AA8ADwAPAAPAA8ADwADwAfAA8AA+AH4AfAAHwD+AHwAB/D/4D4AAP/+/n+AAD//n//AAAf/w//gAAB/wH/wAAAHwA/4AAAAAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAD/8AAAAAD//wAAAAB//+AAAAA///wAAAAf4H+APAAH4AfgDwAD8AB8A8AA+AAfAPAAPAADwDwADwAA8B8AA8AAPAfAAPAADwHgADwAA8D4AA+AAeB+AAHwAHg/AAB+ADwfgAAP8D4/4AAD////8AAAf///8AAAB///+AAAAP//+AAAAAP/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAOAAAB8AAHwAAAfgAD8AAAH4AA/AAAB8AAHwAAAOAAA4AAAAAAAAAAAAAAAAAAAAAAAAAA"), 46, atob("DRUcHBwcHBwcHBwcDA=="), 50+(scale<<8)+(1<<16));
@@ -12,54 +10,47 @@ Graphics.prototype.setFontRobotoRegular21 = function(scale) {
return this;
};
-const SETTINGS_FILE = "circlesclock.json";
+{
+let clock_info = require("clock_info");
+let locale = require("locale");
+let storage = require("Storage");
+
+let SETTINGS_FILE = "circlesclock.json";
let settings = Object.assign(
storage.readJSON("circlesclock.default.json", true) || {},
storage.readJSON(SETTINGS_FILE, true) || {}
);
-
+ //TODO deprecate this (and perhaps use in the clkinfo module)
// Load step goal from health app and pedometer widget as fallback
if (settings.stepGoal == undefined) {
let d = storage.readJSON("health.json", true) || {};
settings.stepGoal = d != undefined && d.settings != undefined ? d.settings.stepGoal : undefined;
-
- if (settings.stepGoal == undefined) {
+
+ if (settings.stepGoal == undefined) {
d = storage.readJSON("wpedom.json", true) || {};
settings.stepGoal = d != undefined && d.settings != undefined ? d.settings.goal : 10000;
}
}
-/*
- * Read location from myLocation app
- */
-function getLocation() {
- return storage.readJSON("mylocation.json", 1) || undefined;
-}
-let location = getLocation();
-
+let drawTimeout;
const showWidgets = settings.showWidgets || false;
const circleCount = settings.circleCount || 3;
+const showBigWeather = settings.showBigWeather || false;
-let hrtValue;
+let hrtValue; //TODO deprecate this
let now = Math.round(new Date().getTime() / 1000);
-
// layout values:
-const colorFg = g.theme.dark ? '#fff' : '#000';
-const colorBg = g.theme.dark ? '#000' : '#fff';
-const colorGrey = '#808080';
-const colorRed = '#ff0000';
-const colorGreen = '#008000';
-const colorBlue = '#0000ff';
-const colorYellow = '#ffff00';
-const widgetOffset = showWidgets ? 24 : 0;
-const dowOffset = circleCount == 3 ? 20 : 22; // dow offset relative to date
-const h = g.getHeight() - widgetOffset;
-const w = g.getWidth();
-const hOffset = (circleCount == 3 ? 34 : 30) - widgetOffset;
-const h1 = Math.round(1 * h / 5 - hOffset);
-const h2 = Math.round(3 * h / 5 - hOffset);
-const h3 = Math.round(8 * h / 8 - hOffset - 3); // circle y position
+let colorFg = g.theme.dark ? '#fff' : '#000';
+let colorBg = g.theme.dark ? '#000' : '#fff';
+let widgetOffset = showWidgets ? 24 : 0;
+let dowOffset = circleCount == 3 ? 20 : 22; // dow offset relative to date
+let h = g.getHeight() - widgetOffset;
+let w = g.getWidth();
+let hOffset = (circleCount == 3 ? 34 : 30) - widgetOffset;
+let h1 = Math.round(1 * h / 5 - hOffset);
+let h2 = Math.round(3 * h / 5 - hOffset);
+let h3 = Math.round(8 * h / 8 - hOffset - 3); // circle middle y position
/*
* circle x positions
@@ -73,483 +64,133 @@ const h3 = Math.round(8 * h / 8 - hOffset - 3); // circle y position
* | (1) (2) (3) (4) |
* => circles start at 1,3,5,7 / 8
*/
-const parts = circleCount * 2;
-const circlePosX = [
+let parts = circleCount * 2;
+let circlePosX = [
Math.round(1 * w / parts), // circle1
Math.round(3 * w / parts), // circle2
Math.round(5 * w / parts), // circle3
Math.round(7 * w / parts), // circle4
];
-const radiusOuter = circleCount == 3 ? 25 : 20;
-const radiusInner = circleCount == 3 ? 20 : 15;
-const circleFontSmall = circleCount == 3 ? "Vector:14" : "Vector:10";
-const circleFont = circleCount == 3 ? "Vector:15" : "Vector:11";
-const circleFontBig = circleCount == 3 ? "Vector:16" : "Vector:12";
-const iconOffset = circleCount == 3 ? 6 : 8;
-const defaultCircleTypes = ["steps", "hr", "battery", "weather"];
+let radiusOuter = circleCount == 3 ? 25 : 20;
+let radiusBorder = radiusOuter+3; // absolute border of circles
+let radiusInner = circleCount == 3 ? 20 : 15;
+let circleFontSmall = circleCount == 3 ? "Vector:14" : "Vector:10";
+let circleFont = circleCount == 3 ? "Vector:15" : "Vector:11";
+let circleFontBig = circleCount == 3 ? "Vector:16" : "Vector:12";
+let iconOffset = circleCount == 3 ? 6 : 8;
-function hideWidgets() {
- /*
- * we are not drawing the widgets as we are taking over the whole screen
- * so we will blank out the draw() functions of each widget and change the
- * area to the top bar doesn't get cleared.
- */
- if (WIDGETS && typeof WIDGETS === "object") {
- for (let wd of WIDGETS) {
- wd.draw = () => {};
- wd.area = "";
- }
- }
-}
-function draw() {
- g.clear(true);
- if (!showWidgets) {
- hideWidgets();
- } else {
- Bangle.drawWidgets();
- }
+let draw = function() {
+ let R = Bangle.appRect;
+ g.reset().clearRect(R.x,R.y, R.x2, h3-(radiusBorder+1));
g.setColor(colorBg);
g.fillRect(0, widgetOffset, w, h2 + 22);
// time
g.setFontRobotoRegular50NumericOnly();
- g.setFontAlign(0, -1);
g.setColor(colorFg);
- g.drawString(locale.time(new Date(), 1), w / 2, h1 + 6);
+ if (!showBigWeather) {
+ g.setFontAlign(0, -1);
+ g.drawString(locale.time(new Date(), 1), w / 2, h1 + 6);
+ }
+ else {
+ g.setFontAlign(-1, -1);
+ g.drawString(locale.time(new Date(), 1), 2, h1 + 6);
+ }
now = Math.round(new Date().getTime() / 1000);
// date & dow
g.setFontRobotoRegular21();
- g.setFontAlign(0, 0);
- g.drawString(locale.date(new Date()), w / 2, h2);
- g.drawString(locale.dow(new Date()), w / 2, h2 + dowOffset);
-
- drawCircle(1);
- drawCircle(2);
- drawCircle(3);
- if (circleCount >= 4) drawCircle(4);
-}
-
-function drawCircle(index) {
- let type = settings['circle' + index];
- if (!type) type = defaultCircleTypes[index - 1];
- const w = getCircleXPosition(type);
-
- switch (type) {
- case "steps":
- drawSteps(w);
- break;
- case "stepsDist":
- drawStepsDistance(w);
- break;
- case "hr":
- drawHeartRate(w);
- break;
- case "battery":
- drawBattery(w);
- break;
- case "weather":
- drawWeather(w);
- break;
- case "sunprogress":
- case "sunProgress":
- drawSunProgress(w);
- break;
- case "temperature":
- drawTemperature(w);
- break;
- case "pressure":
- drawPressure(w);
- break;
- case "altitude":
- drawAltitude(w);
- break;
- case "empty":
- // we draw nothing here
- return;
- }
-}
-
-// serves as cache for quicker lookup of circle positions
-let circlePositionsCache = [];
-/*
- * Looks in the following order if a circle with the given type is somewhere visible/configured
- * 1. circlePositionsCache
- * 2. settings
- * 3. defaultCircleTypes
- *
- * In case 2 and 3 the circlePositionsCache will be updated
- */
-function getCirclePosition(type) {
- if (circlePositionsCache[type] >= 0) {
- return circlePositionsCache[type];
- }
- for (let i = 1; i <= circleCount; i++) {
- const setting = settings['circle' + i];
- if (setting == type) {
- circlePositionsCache[type] = i - 1;
- return i - 1;
- }
- }
- for (let i = 0; i < defaultCircleTypes.length; i++) {
- if (type == defaultCircleTypes[i] && (!settings || settings['circle' + (i + 1)] == undefined)) {
- circlePositionsCache[type] = i;
- return i;
- }
- }
- return undefined;
-}
-
-function getCircleXPosition(type) {
- const circlePos = getCirclePosition(type);
- if (circlePos != undefined) {
- return circlePosX[circlePos];
- }
- return undefined;
-}
-
-function isCircleEnabled(type) {
- return getCirclePosition(type) != undefined;
-}
-
-function getCircleColor(type) {
- const pos = getCirclePosition(type);
- const color = settings["circle" + (pos + 1) + "color"];
- if (color && color != "") return color;
-}
-
-function getCircleIconColor(type, color, percent) {
- const pos = getCirclePosition(type);
- const colorizeIcon = settings["circle" + (pos + 1) + "colorizeIcon"] == true;
- if (colorizeIcon) {
- return getGradientColor(color, percent);
+ if (!showBigWeather) {
+ g.setFontAlign(0, 0);
+ g.drawString(locale.date(new Date()), w / 2, h2);
+ g.drawString(locale.dow(new Date()), w / 2, h2 + dowOffset);
} else {
- return "";
+ g.setFontAlign(-1, 0);
+ g.drawString(locale.date(new Date()), 2, h2);
+ g.drawString(locale.dow(new Date()), 2, h2 + dowOffset, 1);
}
+
+ // weather
+ if (showBigWeather) {
+ let weather = getWeather();
+ let tempString = weather ? locale.temp(weather.temp - 273.15) : undefined;
+ g.setFontAlign(1, 0);
+ if (tempString) g.drawString(tempString, w, h2);
+
+ let code = weather ? weather.code : -1;
+ let icon = getWeatherIconByCode(code, true);
+ if (icon) g.drawImage(icon, w - 48, h1, {scale:0.75});
+ }
+
+ queueDraw();
}
-function getGradientColor(color, percent) {
+let getCircleColor = function(index) {
+ let color = settings["circle" + index + "color"];
+ if (color && color != "") return color;
+ return g.theme.fg;
+}
+
+let getGradientColor = function(color, percent) {
if (isNaN(percent)) percent = 0;
if (percent > 1) percent = 1;
- const colorList = [
+ let colorList = [
'#00FF00', '#80FF00', '#FFFF00', '#FF8000', '#FF0000'
];
if (color == "fg") {
color = colorFg;
}
if (color == "green-red") {
- const colorIndex = Math.round(colorList.length * percent);
+ let colorIndex = Math.round(colorList.length * percent);
return colorList[Math.min(colorIndex, colorList.length) - 1] || "#00ff00";
}
if (color == "red-green") {
- const colorIndex = colorList.length - Math.round(colorList.length * percent);
+ let colorIndex = colorList.length - Math.round(colorList.length * percent);
return colorList[Math.min(colorIndex, colorList.length)] || "#ff0000";
}
return color;
}
-function getImage(graphic, color) {
- if (!color || color == "") {
- return graphic;
+let getCircleIconColor = function(index, color, percent) {
+ let colorizeIcon = settings["circle" + index + "colorizeIcon"] == true;
+ if (colorizeIcon) {
+ return getGradientColor(color, percent);
} else {
- return {
- width: 16,
- height: 16,
- bpp: 1,
- transparent: 0,
- buffer: E.toArrayBuffer(graphic),
- palette: new Uint16Array([colorBg, g.toColor(color)])
- };
+ return g.theme.fg;
}
}
-function drawSteps(w) {
- if (!w) w = getCircleXPosition("steps");
- const steps = getSteps();
-
- drawCircleBackground(w);
-
- const color = getCircleColor("steps");
-
- let percent;
- const stepGoal = settings.stepGoal;
- if (stepGoal > 0) {
- percent = steps / stepGoal;
- if (stepGoal < steps) percent = 1;
- drawGauge(w, h3, percent, color);
- }
-
+let drawEmpty = function(img, w, color) {
+ drawGauge(w, h3, 0, color);
drawInnerCircleAndTriangle(w);
-
- writeCircleText(w, shortValue(steps));
-
- g.drawImage(getImage(atob("EBCBAAAACAAcAB4AHgAeABwwADgGeAZ4AHgAMAAAAHAAIAAA"), getCircleIconColor("steps", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset);
+ writeCircleText(w, "?");
+ if(img)
+ g.setColor(getGradientColor(color, 0))
+ .drawImage(img, w - iconOffset, h3 + radiusOuter - iconOffset, {scale: 16/24});
}
-function drawStepsDistance(w) {
- if (!w) w = getCircleXPosition("stepsDistance");
- const steps = getSteps();
- const stepDistance = settings.stepLength;
- const stepsDistance = Math.round(steps * stepDistance);
-
+let drawCircle = function(index, item, data) {
+ var w = circlePosX[index-1];
drawCircleBackground(w);
-
- const color = getCircleColor("stepsDistance");
-
- let percent;
- const stepDistanceGoal = settings.stepDistanceGoal;
- if (stepDistanceGoal > 0) {
- percent = stepsDistance / stepDistanceGoal;
- if (stepDistanceGoal < stepsDistance) percent = 1;
- drawGauge(w, h3, percent, color);
- }
-
- drawInnerCircleAndTriangle(w);
-
- writeCircleText(w, shortValue(stepsDistance));
-
- g.drawImage(getImage(atob("EBCBAAAACAAcAB4AHgAeABwwADgGeAZ4AHgAMAAAAHAAIAAA"), getCircleIconColor("stepsDistance", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset);
-}
-
-function drawHeartRate(w) {
- if (!w) w = getCircleXPosition("hr");
-
- const heartIcon = atob("EBCBAAAAAAAeeD/8P/x//n/+P/w//B/4D/AH4APAAYAAAAAA");
-
- drawCircleBackground(w);
-
- const color = getCircleColor("hr");
-
- let percent;
- if (hrtValue != undefined) {
- const minHR = settings.minHR;
- const maxHR = settings.maxHR;
- percent = (hrtValue - minHR) / (maxHR - minHR);
- if (isNaN(percent)) percent = 0;
- drawGauge(w, h3, percent, color);
- }
-
- drawInnerCircleAndTriangle(w);
-
- writeCircleText(w, hrtValue != undefined ? hrtValue : "-");
-
- g.drawImage(getImage(heartIcon, getCircleIconColor("hr", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset);
-}
-
-function drawBattery(w) {
- if (!w) w = getCircleXPosition("battery");
- const battery = E.getBattery();
-
- const powerIcon = atob("EBCBAAAAA8ADwA/wD/AP8A/wD/AP8A/wD/AP8A/wD/AH4AAA");
-
- drawCircleBackground(w);
-
- let color = getCircleColor("battery");
-
- let percent;
- if (battery > 0) {
- percent = battery / 100;
- drawGauge(w, h3, percent, color);
- }
-
- drawInnerCircleAndTriangle(w);
-
- if (Bangle.isCharging()) {
- color = colorGreen;
- } else {
- if (settings.batteryWarn != undefined && battery <= settings.batteryWarn) {
- color = colorRed;
- }
- }
- writeCircleText(w, battery + '%');
-
- g.drawImage(getImage(powerIcon, getCircleIconColor("battery", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset);
-}
-
-function drawWeather(w) {
- if (!w) w = getCircleXPosition("weather");
- const weather = getWeather();
- const tempString = weather ? locale.temp(weather.temp - 273.15) : undefined;
- const code = weather ? weather.code : -1;
-
- drawCircleBackground(w);
-
- const color = getCircleColor("weather");
- let percent;
- const data = settings.weatherCircleData;
- switch (data) {
- case "humidity":
- const humidity = weather ? weather.hum : undefined;
- if (humidity >= 0) {
- percent = humidity / 100;
- drawGauge(w, h3, percent, color);
- }
- break;
- case "wind":
- if (weather) {
- const wind = locale.speed(weather.wind).match(/^(\D*\d*)(.*)$/);
- if (wind[1] >= 0) {
- if (wind[2] == "kmh") {
- wind[1] = windAsBeaufort(wind[1]);
- }
- // wind goes from 0 to 12 (see https://en.wikipedia.org/wiki/Beaufort_scale)
- percent = wind[1] / 12;
- drawGauge(w, h3, percent, color);
- }
- }
- break;
- case "empty":
- break;
- }
-
- drawInnerCircleAndTriangle(w);
-
- writeCircleText(w, tempString ? tempString : "?");
-
- if (code > 0) {
- const icon = getWeatherIconByCode(code);
- if (icon) g.drawImage(getImage(icon, getCircleIconColor("weather", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset);
- } else {
- g.drawString("?", w, h3 + radiusOuter);
- }
-}
-
-
-function drawSunProgress(w) {
- if (!w) w = getCircleXPosition("sunprogress");
- const percent = getSunProgress();
-
- // sunset icons:
- const sunSetDown = atob("EBCBAAAAAAABgAAAAAATyAZoBCB//gAAAAAGYAPAAYAAAAAA");
- const sunSetUp = atob("EBCBAAAAAAABgAAAAAATyAZoBCB//gAAAAABgAPABmAAAAAA");
-
- drawCircleBackground(w);
-
- const color = getCircleColor("sunprogress");
-
+ const color = getCircleColor(index);
+ //drawEmpty(info? info.img : null, w, color);
+ var img = data.img;
+ var percent = 1; //fill up if no range
+ var txt = ""+data.text;
+ if (txt.endsWith(" bpm")) txt=txt.slice(0,-4); // hack for heart rate - remove the 'bpm' text
+ if(item.hasRange) percent = (data.v-data.min) / (data.max-data.min);
+ if(data.short) txt = data.short;
+ //long text can overflow and we do not draw there anymore..
+ if(txt.length>6) txt = txt.slice(0,5)+"\n"+txt.slice(5,10)
drawGauge(w, h3, percent, color);
-
drawInnerCircleAndTriangle(w);
-
- let icon = sunSetDown;
- let text = "?";
- const times = getSunData();
- if (times != undefined) {
- const sunRise = Math.round(times.sunrise.getTime() / 1000);
- const sunSet = Math.round(times.sunset.getTime() / 1000);
- if (!isDay()) {
- // night
- if (now > sunRise) {
- // after sunRise
- const upcomingSunRise = sunRise + 60 * 60 * 24;
- text = formatSeconds(upcomingSunRise - now);
- } else {
- text = formatSeconds(sunRise - now);
- }
- icon = sunSetUp;
- } else {
- // day, approx sunrise tomorrow:
- text = formatSeconds(sunSet - now);
- icon = sunSetDown;
- }
- }
-
- writeCircleText(w, text);
-
- g.drawImage(getImage(icon, getCircleIconColor("sunprogress", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset);
-}
-
-function drawTemperature(w) {
- if (!w) w = getCircleXPosition("temperature");
-
- getPressureValue("temperature").then((temperature) => {
- drawCircleBackground(w);
-
- const color = getCircleColor("temperature");
-
- let percent;
- if (temperature) {
- const min = -40;
- const max = 85;
- percent = (temperature - min) / (max - min);
- drawGauge(w, h3, percent, color);
- }
-
- drawInnerCircleAndTriangle(w);
-
- if (temperature)
- writeCircleText(w, locale.temp(temperature));
-
- g.drawImage(getImage(atob("EBCBAAAAAYADwAJAAkADwAPAA8ADwAfgB+AH4AfgA8ABgAAA"), getCircleIconColor("temperature", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset);
-
- });
-}
-
-function drawPressure(w) {
- if (!w) w = getCircleXPosition("pressure");
-
- getPressureValue("pressure").then((pressure) => {
- drawCircleBackground(w);
-
- const color = getCircleColor("pressure");
-
- let percent;
- if (pressure && pressure > 0) {
- const minPressure = 950;
- const maxPressure = 1050;
- percent = (pressure - minPressure) / (maxPressure - minPressure);
- drawGauge(w, h3, percent, color);
- }
-
- drawInnerCircleAndTriangle(w);
-
- if (pressure)
- writeCircleText(w, Math.round(pressure));
-
- g.drawImage(getImage(atob("EBCBAAAAAYADwAJAAkADwAPAA8ADwAfgB+AH4AfgA8ABgAAA"), getCircleIconColor("pressure", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset);
-
- });
-}
-
-function drawAltitude(w) {
- if (!w) w = getCircleXPosition("altitude");
-
- getPressureValue("altitude").then((altitude) => {
- drawCircleBackground(w);
-
- const color = getCircleColor("altitude");
-
- let percent;
- if (altitude) {
- const min = 0;
- const max = 10000;
- percent = (altitude - min) / (max - min);
- drawGauge(w, h3, percent, color);
- }
-
- drawInnerCircleAndTriangle(w);
-
- if (altitude)
- writeCircleText(w, locale.distance(Math.round(altitude)));
-
- g.drawImage(getImage(atob("EBCBAAAAAYADwAJAAkADwAPAA8ADwAfgB+AH4AfgA8ABgAAA"), getCircleIconColor("altitude", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset);
-
- });
-}
-
-/*
- * wind goes from 0 to 12 (see https://en.wikipedia.org/wiki/Beaufort_scale)
- */
-function windAsBeaufort(windInKmh) {
- const beaufort = [2, 6, 12, 20, 29, 39, 50, 62, 75, 89, 103, 118];
- let l = 0;
- while (l < beaufort.length && beaufort[l] < windInKmh) {
- l++;
- }
- return l;
+ writeCircleText(w, txt);
+ if(!img) return; //or get it from the clkinfo?
+ g.setColor(getCircleIconColor(index, color, percent))
+ .drawImage(img, w - iconOffset, h3 + radiusOuter - iconOffset, {scale: 16/24});
}
@@ -557,19 +198,21 @@ function windAsBeaufort(windInKmh) {
* Choose weather icon to display based on weather conditition code
* https://openweathermap.org/weather-conditions#Weather-Condition-Codes-2
*/
-function getWeatherIconByCode(code) {
- const codeGroup = Math.round(code / 100);
+let getWeatherIconByCode = function(code, big) {
+ let codeGroup = Math.round(code / 100);
+ if (big == undefined) big = false;
// weather icons:
- const weatherCloudy = atob("EBCBAAAAAAAAAAfgD/Af8H/4//7///////9//z/+AAAAAAAA");
- const weatherSunny = atob("EBCBAAAAAYAQCBAIA8AH4A/wb/YP8A/gB+ARiBAIAYABgAAA");
- const weatherMoon = atob("EBCBAAAAAYAP8B/4P/w//D/8f/5//j/8P/w//B/4D/ABgAAA");
- const weatherPartlyCloudy = atob("EBCBAAAAAAAYQAMAD8AIQBhoW+AOYBwwOBBgHGAGP/wf+AAA");
- const weatherRainy = atob("EBCBAAAAAYAH4AwwOBBgGEAOQAJBgjPOEkgGYAZgA8ABgAAA");
- const weatherPartlyRainy = atob("EBCBAAAAEEAQAAeADMAYaFvoTmAMMDgQIBxhhiGGG9wDwAGA");
- const weatherSnowy = atob("EBCBAAAAAAADwAGAEYg73C50BCAEIC50O9wRiAGAA8AAAAAA");
- const weatherFoggy = atob("EBCBAAAAAAADwAZgDDA4EGAcQAZAAgAAf74AAAAAd/4AAAAA");
- const weatherStormy = atob("EBCBAAAAAYAH4AwwOBBgGEAOQMJAgjmOGcgAgACAAAAAAAAA");
+ let weatherCloudy = big ? atob("QECBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAAAAAAf/4AAAAAAAD//4AAAAAAA///wAAAAAAH+B/gAAAAAA/AA/AAAAAAH4AB+AAAAAAfAAD8AAAAAD4AAH/wAAAAfAAAP/wAAAB4AAAf/gAAAHgAAB//AAAB+AAACB+AAAfwAAAAB8AAH/AAAAAD4AA/8AAAAAHgAD8AAAAAAeAAfAAAAAAA8AB4AAAAAADwAPgAAAAAAPAA8AAAAAAA8APwAAAAAADwB/AAAAAAAPAP8AAAAAAB8B+AAAAAAAH4PgAAAAAAAHx8AAAAAAAAPngAAAAAAAAeeAAAAAAAAB7wAAAAAAAAD/AAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD/AAAAAAAAAP8AAAAAAAAA94AAAAAAAAHngAAAAAAAAefAAAAAAAAD4+AAAAAAAAfB+AAAAAAAH4D/////////AH////////4AP////////AAH///////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") : atob("EBCBAAAAAAAAAAfgD/Af8H/4//7///////9//z/+AAAAAAAA");
+ let weatherSunny = big ? atob("QECBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAMAA8AAwAAAB4ADwAHgAAAHwAPAA+AAAAPgA8AHwAAAAfADwA+AAAAA+AfgHwAAAAB8P/w+AAAAAD7//3wAAAAAH///+AAAAAAP+B/wAAAAAAfgB+AAAAAAD4AB8AAAAAAPAADwAAAAAB8AAPgAAAAAHgAAeAAAAAAeAAB4AAAAADwAADwAAAP//AAAP//AA//8AAA//8AD//wAAD//wAP//AAAP//AAAA8AAA8AAAAAB4AAHgAAAAAHgAAeAAAAAAfAAD4AAAAAA8AAPAAAAAAD4AB8AAAAAAH4AfgAAAAAA/4H/AAAAAAH///+AAAAAA+//98AAAAAHw//D4AAAAA+AfgHwAAAAHwA8APgAAAA+ADwAfAAAAHwAPAA+AAAAeAA8AB4AAAAwADwADAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") : atob("EBCBAAAAAYAQCBAIA8AH4A/wb/YP8A/gB+ARiBAIAYABgAAA");
+ let weatherMoon = big ? atob("QECBAAAGAAAADwAAAA+AAAAPAAAAD8AAAA8AAAAP4AAADwAAAAfwDwD/8AAAB/gPAP/wAAAH+A8A//AAAAf8DwD/8AAAB/4AAA8AAAAHvgAADwAAAAeeAAAPAAAAB54AAA8AAAAHjwAAAAAAAA+PDgAAAAAADw8PgAAAAAAfDw/AAAAAAB4PD+AAAAAAPg8D8AAAAAB8HwH4AAAAAfg+APwAAPAH8H4Af/AA///g/gA//AD//8H4AB/+AH//AfAAH/8Af/wD4AAIH4A/gAPAAAAHwB/AB8AAAAPgD/AfgAAAAeAH//+AAAAB4AP//4AAAADwAP//AAAAAPAAH/8AAAAA8AAAAAAAAADwAAAAAAAAAPAHAAAAAAAA8A/gAAAAAAHwH4AAAAAAAfg+AAAAAAAAfHwAAAAAAAA+eAAAAAAAAB54AAAAAAAAHvAAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD/AAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD3gAAAAAAAAeeAAAAAAAAB58AAAAAAAAPj4AAAAAAAB8H4AAAAAAAfgP////////8Af////////gA////////8AAf//////+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") : atob("EBCBAAAAAYAP8B/4P/w//D/8f/5//j/8P/w//B/4D/ABgAAA");
+ let weatherPartlyCloudy = big ? atob("QECBAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAcADwAOAAAAB4APAB4AAAAHwA8APgAAAAPgH4B8AAAAAfD/8PgAAAAA+//98AAAAAB////gQAAAAD/gf8DgAAAAH4AfgfAAAAA+AA/B+AAAADwAP8D8AAAAfAB/gH/wAAB4APwAP/wAAHgB+AAf/gAA8AHwAB//AP/wA+AACB+A//AHwAAAB8D/8AeAAAAD4P/wB4AAAAHgAPAfgAAAAeAAeH8AAAAA8AB5/wAAAADwAH3/AAAAAPAAP/AAAAAA8AA/wAAAAADwBB+AAAAAAPAOD4AAAAAB8B4HAAAAAAH4PgMAAAAAAHx8AAAAAAAAPngAAAAAAAAeeAAAAAAAAB7wAAAAAAAAD/AAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD/AAAAAAAAAP8AAAAAAAAA94AAAAAAAAHngAAAAAAAAefAAAAAAAAD4+AAAAAAAAfB+AAAAAAAH4D/////////AH////////4AP////////AAH///////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") : atob("EBCBAAAAAAAYQAMAD8AIQBhoW+AOYBwwOBBgHGAGP/wf+AAA");
+ let weatherRainy = big ? atob("QECBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAAAAAAf/4AAAAAAAD//4AAAAAAA///wAAAAAAH+B/gAAAAAA/AA/AAAAAAH4AB+AAAAAAfAAD8AAAAAD4AAH/wAAAAfAAAP/wAAAB4AAAf/gAAAHgAAB//AAAB+AAACB+AAAfwAAAAB8AAH/AAAAAD4AA/8AAAAAHgAD8AAAAAAeAAfAAAAAAA8AB4AAAAAADwAPgAAAAAAPAA8AAAAAAA8APwAAAAAADwB/AAAAAAAPAP8AAAAAAB8B+ADwAPAAH4PgAPAA8AAHx8AA8ADwAAPngADwAPAAAeeAAAAAAAAB7wAAAAAAAAD/AAAAAAAAAP8AAAAAAAAA/wAA8PDw8AD/AADw8PDwAP8AAPDw8PAA94AA8PDw8AHngAAAAAAAAefAAAAAAAAD4+AAAAAAAAfB+AAAAAAAH4D/8PDw8PD/AH/w8PDw8P4AP/Dw8PDw/AAH8PDw8PDgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8ADwAAAAAADwAPAAAAAAAPAA8AAAAAAA8ADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") : atob("EBCBAAAAAYAH4AwwOBBgGEAOQAJBgjPOEkgGYAZgA8ABgAAA");
+ let weatherPartlyRainy = big ? atob("QECBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAAAAAAf/4AAAAAAAD//4AAAAAAA///wAAAAAAH+B/gAAAAAA/AA/AAAAAAH4AB+AAAAAAfAAD8AAAAAD4AAH/wAAAAfAAAP/wAAAB4AAAf/gAAAHgAAB//AAAB+AAACB+AAAfwAAAAB8AAH/AAAAAD4AA/8AAAAAHgAD8AAAAAAeAAfAAAAAAA8AB4AAAAAADwAPgAAAAAAPAA8AAAAAAA8APwAAAAAADwB/AAAAAAAPAP8AAAAAAB8B+AAAAPAAH4PgAAAA8AAHx8AAAADwAAPngAAAAPAAAeeAAAAA8AAB7wAAAADwAAD/AAAAAPAAAP8AAAAA8AAA/wAAAPDwAAD/AAAA8PAAAP8AAADw8AAA94AAAPDwAAHngAAA8PAAAefAAADw8AAD4+AAAPDwAAfB+AAA8PAAH4D///Dw8P//AH//8PDw//4AP//w8PD//AAH//Dw8P/gAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") : atob("EBCBAAAAEEAQAAeADMAYaFvoTmAMMDgQIBxhhiGGG9wDwAGA");
+ let weatherSnowy = big ? atob("QECBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAAAAAAf/4AAAAAAAD//4AAAAAAA///wAAAAAAH+B/gAAAAAA/AA/AAAAAAH4AB+AAAAAAfAAD8AAAAAD4AAH/wAAAAfAAAP/wAAAB4AAAf/gAAAHgAAB//AAAB+AAACB+AAAfwAAAAB8AAH/AAAAAD4AA/8AAAAAHgAD8AAAAAAeAAfAAAAAAA8AB4AAAAAADwAPgAAAAAAPAA8AAAAAAA8APwAAAAAADwB/AAAAAAAPAP8AAAAAAB8B+AAAAA8AH4PgAAAADwAHx8AAAAAPAAPngAAAAA8AAeeAAPAA//AB7wAA8AD/8AD/AADwAP/wAP8AAPAA//AA/wAP/wAPAAD/AA//AA8AAP8AD/8ADwAA94AP/wAPAAHngADwAAAAAefAAPAAAAAD4+AA8AAAAAfB+ADwAAAAH4D/8AAPAP//AH/wAA8A//4AP/AADwD//AAH8AAPAP/gAAAAAP/wAAAAAAAA//AAAAAAAAD/8AAAAAAAAP/wAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") : atob("EBCBAAAAAAADwAGAEYg73C50BCAEIC50O9wRiAGAA8AAAAAA");
+ let weatherFoggy = big ? atob("QECBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAMAA8AAwAAAB4ADwAHgAAAHwAPAA+AAAAPgA8AHwAAAAfADwA+AAAAA+AfgHwAAAAB8P/w+AAAAAD7//3wAAAAAH///+AAAAAAP+B/wAAAAAAfgB+AAAAAAD4AB8AAAAAAPAADwAAAAAB8AAPgAAAAAHgAAeAAAAAAeAAB4AAAAADwAADwAAAAAAAAAP//AAAAAAAA//8AAAAAAAD//wAAAAAAAP//AA///wAA8AAAD///AAHgAAAP//8AAeAAAA///wAD4AAAAAAAAAPAAAAAAAAAB8AAAAAAAAAfgAAAAAAAAH/AAAAA///w/+AAAAD///D98AAAAP//8PD4AAAA///wgHwAAAAAAAAAPgAAAAAAAAAfAAAAAAAAAA+AAAAAAAAAB4AAD///DwADAAAP//8PAAAAAA///w8AAAAAD///DwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") : atob("EBCBAAAAAAADwAZgDDA4EGAcQAZAAgAAf74AAAAAd/4AAAAA");
+ let weatherStormy = big ? atob("QECBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAAAAAAf/4AAAAAAAD//4AAAAAAA///wAAAAAAH+B/gAAAAAA/AA/AAAAAAH4AB+AAAAAAfAAD8AAAAAD4AAH/wAAAAfAAAP/wAAAB4AAAf/gAAAHgAAB//AAAB+AAACB+AAAfwAAAAB8AAH/AAAAAD4AA/8AAAAAHgAD8AAAAAAeAAfAAAAAAA8AB4AAAAAADwAPgAAAAAAPAA8AAAAAAA8APwAAAAAADwB/AAAAAAAPAP8AAAAAAB8B+AAAAAAAH4PgAAAAAAAHx8AAAAAAAAPngAAAAAAAAeeAAAAA/wAB7wAAAAH+AAD/AAAAAf4AAP8AAAAD/AAA/wAAAAP4AAD/AAAAB/gAAP8AAAAH8AAA94AAAA/wAAHngAAAD+AAAefAAAAfwAAD4+AAAB/AAAfB+AAAP4AAH4D///g//w//AH//+H/+D/4AP//wf/wf/AAH//D//D/gAAAAAAD4AAAAAAAAAfAAAAAAAAAB8AAAAAAAAAPgAAAAAAAAA8AAAAAAAAAHwAAAAAAAAAeAAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") : atob("EBCBAAAAAYAH4AwwOBBgGEAOQMJAgjmOGcgAgACAAAAAAAAA");
+ let unknown = big ? atob("QECBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP8AAAAAAAAH/+AAAAAAAB//+AAAAAAAP//8AAAAAAB/gf4AAAAAAPwAPwAAAAAB+AAfgAAAAAPwAA+AAAAAA+B+B8AAAAAHwf+D4AAAAAfD/8HgAAAAB4P/4eAAAAAPh8Ph8AAAAA+HgfDwAAAAD/8A8PAAAAAP/wDw8AAAAA//APDwAAAAB/4A8PAAAAAAAAHw8AAAAAAAB+HwAAAAAAAfweAAAAAAAH+B4AAAAAAA/wPgAAAAAAH8B8AAAAAAA/APgAAAAAAD4B+AAAAAAAfAfwAAAAAAB4H+AAAAAAAPh/gAAAAAAA8H8AAAAAAADw/AAAAAAAAPDwAAAAAAAA//AAAAAAAAD/8AAAAAAAAP/wAAAAAAAAf+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/gAAAAAAAA//AAAAAAAAD/8AAAAAAAAP/wAAAAAAAA8PAAAAAAAADw8AAAAAAAAPDwAAAAAAAA8PAAAAAAAAD/8AAAAAAAAP/wAAAAAAAA//AAAAAAAAB/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") : undefined;
switch (codeGroup) {
case 2:
@@ -591,7 +234,6 @@ function getWeatherIconByCode(code) {
default:
return weatherRainy;
}
- break;
case 6:
return weatherSnowy;
case 7:
@@ -599,7 +241,9 @@ function getWeatherIconByCode(code) {
case 8:
switch (code) {
case 800:
- return isDay() ? weatherSunny : weatherMoon;
+ var hr = (new Date()).getHours();
+ var isDay = (hr>6) && (hr<=18); // fixme we don't want to include ALL of suncalc just to choose one icon
+ return isDay ? weatherSunny : weatherMoon;
case 801:
return weatherPartlyCloudy;
case 802:
@@ -607,86 +251,24 @@ function getWeatherIconByCode(code) {
default:
return weatherCloudy;
}
- break;
- default:
- return undefined;
- }
-}
-
-
-function isDay() {
- const times = getSunData();
- if (times == undefined) return true;
- const sunRise = Math.round(times.sunrise.getTime() / 1000);
- const sunSet = Math.round(times.sunset.getTime() / 1000);
-
- return (now > sunRise && now < sunSet);
-}
-
-function formatSeconds(s) {
- if (s > 60 * 60) { // hours
- return Math.round(s / (60 * 60)) + "h";
- }
- if (s > 60) { // minutes
- return Math.round(s / 60) + "m";
- }
- return "<1m";
-}
-
-function getSunData() {
- if (location != undefined && location.lat != undefined) {
- const SunCalc = require("https://raw.githubusercontent.com/mourner/suncalc/master/suncalc.js");
- // get today's sunlight times for lat/lon
- return SunCalc ? SunCalc.getTimes(new Date(), location.lat, location.lon) : undefined;
- }
- return undefined;
-}
-
-/*
- * Calculated progress of the sun between sunrise and sunset in percent
- *
- * Taken from rebble app and modified
- */
-function getSunProgress() {
- const times = getSunData();
- if (times == undefined) return 0;
- const sunRise = Math.round(times.sunrise.getTime() / 1000);
- const sunSet = Math.round(times.sunset.getTime() / 1000);
-
- if (isDay()) {
- // during day
- const dayLength = sunSet - sunRise;
- if (now > sunRise) {
- return (now - sunRise) / dayLength;
- } else {
- return (sunRise - now) / dayLength;
- }
- } else {
- // during night
- if (now < sunRise) {
- const prevSunSet = sunSet - 60 * 60 * 24;
- return 1 - (sunRise - now) / (sunRise - prevSunSet);
- } else {
- const upcomingSunRise = sunRise + 60 * 60 * 24;
- return (upcomingSunRise - now) / (upcomingSunRise - sunSet);
- }
+ default:
+ return unknown;
}
}
/*
* Draws the background and the grey circle
*/
-function drawCircleBackground(w) {
- g.clearRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3);
+let drawCircleBackground = function(w) {
// Draw rectangle background:
g.setColor(colorBg);
- g.fillRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3);
+ g.fillRect(w - radiusBorder, h3 - radiusBorder, w + radiusBorder, g.getHeight()-1);
// Draw grey background circle:
- g.setColor(colorGrey);
+ g.setColor('#808080'); // grey
g.fillCircle(w, h3, radiusOuter);
}
-function drawInnerCircleAndTriangle(w) {
+let drawInnerCircleAndTriangle = function(w) {
// Draw inner circle
g.setColor(colorBg);
g.fillCircle(w, h3, radiusInner);
@@ -694,38 +276,39 @@ function drawInnerCircleAndTriangle(w) {
g.fillPoly([w, h3, w - 15, h3 + radiusOuter + 5, w + 15, h3 + radiusOuter + 5]);
}
-function radians(a) {
- return a * Math.PI / 180;
-}
-
/*
* This draws the actual gauge consisting out of lots of little filled circles
*/
-function drawGauge(cx, cy, percent, color) {
- const offset = 15;
- const end = 360 - offset;
- const radius = radiusInner + (circleCount == 3 ? 3 : 2);
- const size = radiusOuter - radiusInner - 2;
+let drawGauge = function(cx, cy, percent, color) {
+ let offset = 15;
+ let end = 360 - offset;
+ let radius = radiusOuter+1;
if (percent <= 0) return; // no gauge needed
if (percent > 1) percent = 1;
- const startRotation = -offset;
- const endRotation = startRotation - ((end - offset) * percent);
+ let startRotation = -offset;
+ let endRotation = startRotation - ((end - offset) * percent);
color = getGradientColor(color, percent);
g.setColor(color);
-
- for (let i = startRotation; i > endRotation - size; i -= size) {
- x = cx + radius * Math.sin(radians(i));
- y = cy + radius * Math.cos(radians(i));
- g.fillCircle(x, y, size);
- }
+ // convert to radians
+ startRotation *= Math.PI / 180;
+ let amt = Math.PI / 10;
+ endRotation = (endRotation * Math.PI / 180) - amt;
+ // all we need to draw is an arc, because we'll fill the center
+ let poly = [cx,cy];
+ for (let r = startRotation; r > endRotation; r -= amt)
+ poly.push(
+ cx + radius * Math.sin(r),
+ cy + radius * Math.cos(r)
+ );
+ g.fillPoly(poly);
}
-function writeCircleText(w, content) {
+let writeCircleText = function(w, content) {
if (content == undefined) return;
- const font = String(content).length > 4 ? circleFontSmall : String(content).length > 3 ? circleFont : circleFontBig;
+ let font = String(content).length > 4 ? circleFontSmall : String(content).length > 3 ? circleFont : circleFontBig;
g.setFont(font);
g.setFontAlign(0, 0);
@@ -733,118 +316,55 @@ function writeCircleText(w, content) {
g.drawString(content, w, h3);
}
-function shortValue(v) {
- if (isNaN(v)) return '-';
- if (v <= 999) return v;
- if (v >= 1000 && v < 10000) {
- v = Math.floor(v / 100) * 100;
- return (v / 1000).toFixed(1).replace(/\.0$/, '') + 'k';
- }
- if (v >= 10000) {
- v = Math.floor(v / 1000) * 1000;
- return (v / 1000).toFixed(1).replace(/\.0$/, '') + 'k';
- }
-}
-
-function getSteps() {
- if (Bangle.getHealthStatus) {
- return Bangle.getHealthStatus("day").steps;
- }
- if (WIDGETS && WIDGETS.wpedom !== undefined) {
- return WIDGETS.wpedom.getSteps();
- }
- return 0;
-}
-
-function getWeather() {
- const jsonWeather = storage.readJSON('weather.json');
+let getWeather=function() {
+ let jsonWeather = storage.readJSON('weather.json');
return jsonWeather && jsonWeather.weather ? jsonWeather.weather : undefined;
}
-function enableHRMSensor() {
- Bangle.setHRMPower(1, "circleclock");
- if (hrtValue == undefined) {
- hrtValue = '...';
- drawHeartRate();
+g.clear(1); // clear the whole screen
+
+Bangle.setUI({
+ mode : "clock",
+ remove : function() {
+ // Called to unload all of the clock app (allowing for 'fast load')
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = undefined;
+ clockInfoMenu.forEach(c => c.remove());
+ delete Graphics.prototype.setFontRobotoRegular50NumericOnly;
+ delete Graphics.prototype.setFontRobotoRegular21;
+ if (!showWidgets) require("widget_utils").show();
}
-}
+});
-let pressureLocked = false;
-let pressureCache;
-
-function getPressureValue(type) {
- return new Promise((resolve) => {
- if (Bangle.getPressure) {
- if (!pressureLocked) {
- pressureLocked = true;
- if (pressureCache && pressureCache[type]) {
- resolve(pressureCache[type]);
- }
- Bangle.getPressure().then(function(d) {
- pressureLocked = false;
- if (d) {
- pressureCache = d;
- if (d[type]) {
- resolve(d[type]);
- }
- }
- }).catch(() => {});
- } else {
- if (pressureCache && pressureCache[type]) {
- resolve(pressureCache[type]);
- }
- }
- }
+let clockInfoDraw = (itm, info, options) => {
+ //print("Draw",itm.name,options);
+ drawCircle(options.circlePosition, itm, info);
+ if (options.focus) g.reset().drawRect(options.x, options.y, options.x+options.w-2, options.y+options.h-1)
+};
+let clockInfoItems = require("clock_info").load();
+let clockInfoMenu = [];
+for(var i=0;i= (settings.confidence)) {
- hrtValue = hrm.bpm;
- if (Bangle.isLCDOn()) {
- drawHeartRate();
- }
- }
- // Let us wait before we overwrite "good" HRM values:
- if (Bangle.isLCDOn()) {
- if (timerHrm) clearTimeout(timerHrm);
- timerHrm = setTimeout(() => {
- hrtValue = '...';
- drawHeartRate();
- }, settings.hrmValidity * 1000);
- }
- }
-});
-
-Bangle.on('charging', function(charging) {
- if (isCircleEnabled("battery")) drawBattery();
-});
-
-if (isCircleEnabled("hr")) {
- enableHRMSensor();
+ }, queueMillis - (Date.now() % queueMillis));
}
-Bangle.setUI("clock");
-Bangle.loadWidgets();
-
-// schedule a draw for the next minute
-setTimeout(function() {
- // draw in interval
- setInterval(draw, settings.updateInterval * 1000);
-}, 60000 - (Date.now() % 60000));
-
draw();
+}
diff --git a/apps/circlesclock/default.json b/apps/circlesclock/default.json
index ea00dc347..ad409b992 100644
--- a/apps/circlesclock/default.json
+++ b/apps/circlesclock/default.json
@@ -1,18 +1,8 @@
{
- "minHR": 40,
- "maxHR": 200,
- "confidence": 0,
- "stepGoal": 10000,
- "stepDistanceGoal": 8000,
- "stepLength": 0.8,
"batteryWarn": 30,
"showWidgets": false,
"weatherCircleData": "humidity",
"circleCount": 3,
- "circle1": "hr",
- "circle2": "steps",
- "circle3": "battery",
- "circle4": "weather",
"circle1color": "green-red",
"circle2color": "#0000ff",
"circle3color": "red-green",
@@ -21,6 +11,15 @@
"circle2colorizeIcon": true,
"circle3colorizeIcon": true,
"circle4colorizeIcon": false,
- "hrmValidity": 60,
- "updateInterval": 60
+ "updateInterval": 60,
+ "showBigWeather": false,
+
+ "minHR": 40,
+ "maxHR": 200,
+ "confidence": 0,
+ "stepGoal": 10000,
+ "stepDistanceGoal": 8000,
+ "stepLength": 0.8,
+ "hrmValidity": 60
+
}
diff --git a/apps/circlesclock/metadata.json b/apps/circlesclock/metadata.json
index 837fcaa88..1b94c00b3 100644
--- a/apps/circlesclock/metadata.json
+++ b/apps/circlesclock/metadata.json
@@ -1,7 +1,7 @@
{ "id": "circlesclock",
"name": "Circles clock",
"shortName":"Circles clock",
- "version":"0.13",
+ "version":"0.22",
"description": "A clock with three or four circles for different data at the bottom in a probably familiar style",
"icon": "app.png",
"screenshots": [{"url":"screenshot-dark.png"}, {"url":"screenshot-light.png"}, {"url":"screenshot-dark-4.png"}, {"url":"screenshot-light-4.png"}],
diff --git a/apps/circlesclock/screenshot-light-with-big-weather.png b/apps/circlesclock/screenshot-light-with-big-weather.png
new file mode 100644
index 000000000..d1d569247
Binary files /dev/null and b/apps/circlesclock/screenshot-light-with-big-weather.png differ
diff --git a/apps/circlesclock/settings.js b/apps/circlesclock/settings.js
index fb23f8d5e..5c5ea4f27 100644
--- a/apps/circlesclock/settings.js
+++ b/apps/circlesclock/settings.js
@@ -1,6 +1,7 @@
(function(back) {
const SETTINGS_FILE = "circlesclock.json";
const storage = require('Storage');
+ const clock_info = require("clock_info");
let settings = Object.assign(
storage.readJSON("circlesclock.default.json", true) || {},
storage.readJSON(SETTINGS_FILE, true) || {}
@@ -11,9 +12,6 @@
storage.write(SETTINGS_FILE, settings);
}
- const valuesCircleTypes = ["empty", "steps", "stepsDist", "hr", "battery", "weather", "sunprogress", "temperature", "pressure", "altitude"];
- const namesCircleTypes = ["empty", "steps", "distance", "heart", "battery", "weather", "sun", "temperature", "pressure", "altitude"];
-
const valuesColors = ["", "#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff",
"#00ffff", "#fff", "#000", "green-red", "red-green", "fg"];
const namesColors = ["default", "red", "green", "blue", "yellow", "magenta",
@@ -36,8 +34,6 @@
/*LANG*/'circle 2': ()=>showCircleMenu(2),
/*LANG*/'circle 3': ()=>showCircleMenu(3),
/*LANG*/'circle 4': ()=>showCircleMenu(4),
- /*LANG*/'heartrate': ()=>showHRMenu(),
- /*LANG*/'steps': ()=>showStepMenu(),
/*LANG*/'battery warn': {
value: settings.batteryWarn,
min: 10,
@@ -68,96 +64,22 @@
return x + 's';
},
onchange: x => save('updateInterval', x),
+ },
+ //TODO deprecated local icons, may disappear in future
+ /*LANG*/'legacy weather icons': {
+ value: !!settings.legacyWeatherIcons,
+ format: () => (settings.legacyWeatherIcons ? 'Yes' : 'No'),
+ onchange: x => save('legacyWeatherIcons', x),
+ },
+ /*LANG*/'show big weather': {
+ value: !!settings.showBigWeather,
+ format: () => (settings.showBigWeather ? 'Yes' : 'No'),
+ onchange: x => save('showBigWeather', x),
}
};
E.showMenu(menu);
}
- function showHRMenu() {
- let menu = {
- '': { 'title': /*LANG*/'Heartrate' },
- /*LANG*/'< Back': ()=>showMainMenu(),
- /*LANG*/'minimum': {
- value: settings.minHR,
- min: 0,
- max : 250,
- step: 5,
- format: x => {
- return x + " bpm";
- },
- onchange: x => save('minHR', x),
- },
- /*LANG*/'maximum': {
- value: settings.maxHR,
- min: 20,
- max : 250,
- step: 5,
- format: x => {
- return x + " bpm";
- },
- onchange: x => save('maxHR', x),
- },
- /*LANG*/'min. confidence': {
- value: settings.confidence,
- min: 0,
- max : 100,
- step: 10,
- format: x => {
- return x + "%";
- },
- onchange: x => save('confidence', x),
- },
- /*LANG*/'valid period': {
- value: settings.hrmValidity,
- min: 10,
- max : 1800,
- step: 10,
- format: x => {
- return x + "s";
- },
- onchange: x => save('hrmValidity', x),
- },
- };
- E.showMenu(menu);
- }
-
- function showStepMenu() {
- let menu = {
- '': { 'title': /*LANG*/'Steps' },
- /*LANG*/'< Back': ()=>showMainMenu(),
- /*LANG*/'goal': {
- value: settings.stepGoal,
- min: 1000,
- max : 50000,
- step: 500,
- format: x => {
- return x;
- },
- onchange: x => save('stepGoal', x),
- },
- /*LANG*/'distance goal': {
- value: settings.stepDistanceGoal,
- min: 1000,
- max : 50000,
- step: 500,
- format: x => {
- return x;
- },
- onchange: x => save('stepDistanceGoal', x),
- },
- /*LANG*/'step length': {
- value: settings.stepLength,
- min: 0.1,
- max : 1.5,
- step: 0.01,
- format: x => {
- return x;
- },
- onchange: x => save('stepLength', x),
- }
- };
- E.showMenu(menu);
- }
function showCircleMenu(circleId) {
const circleName = "circle" + circleId;
const colorKey = circleName + "color";
@@ -166,12 +88,6 @@
const menu = {
'': { 'title': /*LANG*/'Circle ' + circleId },
/*LANG*/'< Back': ()=>showMainMenu(),
- /*LANG*/'data': {
- value: valuesCircleTypes.indexOf(settings[circleName]),
- min: 0, max: valuesCircleTypes.length - 1,
- format: v => namesCircleTypes[v],
- onchange: x => save(circleName, valuesCircleTypes[x]),
- },
/*LANG*/'color': {
value: valuesColors.indexOf(settings[colorKey]) || 0,
min: 0, max: valuesColors.length - 1,
@@ -187,6 +103,5 @@
E.showMenu(menu);
}
-
showMainMenu();
});
diff --git a/apps/nato/changelog.txt b/apps/clkinfocal/ChangeLog
similarity index 100%
rename from apps/nato/changelog.txt
rename to apps/clkinfocal/ChangeLog
diff --git a/apps/clkinfocal/app.png b/apps/clkinfocal/app.png
new file mode 100644
index 000000000..ed79cd884
Binary files /dev/null and b/apps/clkinfocal/app.png differ
diff --git a/apps/clkinfocal/clkinfo.js b/apps/clkinfocal/clkinfo.js
new file mode 100644
index 000000000..a7949cda4
--- /dev/null
+++ b/apps/clkinfocal/clkinfo.js
@@ -0,0 +1,32 @@
+(function() {
+ require("Font4x8Numeric").add(Graphics);
+ return {
+ name: "Bangle",
+ items: [
+ { name : "Date",
+ get : () => {
+ let d = new Date();
+ let g = Graphics.createArrayBuffer(24,24,1,{msb:true});
+ g.drawImage(atob("FhgBDADAMAMP/////////////////////8AADwAAPAAA8AADwAAPAAA8AADwAAPAAA8AADwAAPAAA8AADwAAP///////"),1,0);
+ g.setFont("6x15").setFontAlign(0,0).drawString(d.getDate(),11,17);
+ return {
+ text : require("locale").dow(d,1).toUpperCase(),
+ img : g.asImage("string")
+ };
+ },
+ show : function() {
+ this.interval = setTimeout(()=>{
+ this.emit("redraw");
+ this.interval = setInterval(()=>{
+ this.emit("redraw");
+ }, 86400000);
+ }, 86400000 - (Date.now() % 86400000));
+ },
+ hide : function() {
+ clearInterval(this.interval);
+ this.interval = undefined;
+ }
+ }
+ ]
+ };
+})
diff --git a/apps/clkinfocal/metadata.json b/apps/clkinfocal/metadata.json
new file mode 100644
index 000000000..6d6dd63fc
--- /dev/null
+++ b/apps/clkinfocal/metadata.json
@@ -0,0 +1,12 @@
+{ "id": "clkinfocal",
+ "name": "Calendar Clockinfo",
+ "version":"0.01",
+ "description": "For clocks that display 'clockinfo' (messages that can be cycled through using the clock_info module) this displays the day of the month in the icon, and the weekday",
+ "icon": "app.png",
+ "type": "clkinfo",
+ "tags": "clkinfo,calendar",
+ "supports" : ["BANGLEJS2"],
+ "storage": [
+ {"name":"clkinfocal.clkinfo.js","url":"clkinfo.js"}
+ ]
+}
diff --git a/apps/clkinfofw/ChangeLog b/apps/clkinfofw/ChangeLog
new file mode 100644
index 000000000..10810802b
--- /dev/null
+++ b/apps/clkinfofw/ChangeLog
@@ -0,0 +1,2 @@
+0.01: First release
+0.02: Update clock_info to avoid a redraw and image allocation
diff --git a/apps/clkinfofw/app.png b/apps/clkinfofw/app.png
new file mode 100644
index 000000000..c6575b73b
Binary files /dev/null and b/apps/clkinfofw/app.png differ
diff --git a/apps/clkinfofw/clkinfo.js b/apps/clkinfofw/clkinfo.js
new file mode 100644
index 000000000..2b3cb32ba
--- /dev/null
+++ b/apps/clkinfofw/clkinfo.js
@@ -0,0 +1,17 @@
+(function() {
+ return {
+ name: "Bangle",
+ items: [
+ { name : "FW",
+ get : () => {
+ return {
+ text : process.env.VERSION,
+ img : atob("GBjC////AADve773VWmmmmlVVW22nnlVVbLL445VVwAAAADVWAAAAAAlrAAAAAA6sAAAAAAOWAAAAAAlrAD//wA6sANVVcAOWANVVcAlrANVVcA6rANVVcA6WANVVcAlsANVVcAOrAD//wA6WAAAAAAlsAAAAAAOrAAAAAA6WAAAAAAlVwAAAADVVbLL445VVW22nnlVVWmmmmlV")
+ };
+ },
+ show : function() {},
+ hide : function() {}
+ }
+ ]
+ };
+})
diff --git a/apps/clkinfofw/metadata.json b/apps/clkinfofw/metadata.json
new file mode 100644
index 000000000..720a5baa5
--- /dev/null
+++ b/apps/clkinfofw/metadata.json
@@ -0,0 +1,13 @@
+{ "id": "clkinfofw",
+ "name": "Firmware Clockinfo",
+ "version":"0.02",
+ "description": "For clocks that display 'clockinfo', this displays the firmware version string",
+ "icon": "app.png",
+ "type": "clkinfo",
+ "screenshots": [{"url":"screenshot.png"}],
+ "tags": "clkinfo,firmware",
+ "supports" : ["BANGLEJS2"],
+ "storage": [
+ {"name":"clkinfofw.clkinfo.js","url":"clkinfo.js"}
+ ]
+}
diff --git a/apps/clkinfofw/screenshot.png b/apps/clkinfofw/screenshot.png
new file mode 100644
index 000000000..da185bd2e
Binary files /dev/null and b/apps/clkinfofw/screenshot.png differ
diff --git a/apps/clkinfosunrise/ChangeLog b/apps/clkinfosunrise/ChangeLog
new file mode 100644
index 000000000..86e7a7fa8
--- /dev/null
+++ b/apps/clkinfosunrise/ChangeLog
@@ -0,0 +1,4 @@
+0.01: New App!
+0.02: Use 'modules/suncalc.js' to avoid it being copied 8 times for different apps
+ Add a 'time' clockinfo that also displays a percentage of day left
+0.03: Change 3rd mode to show the time to next sunrise/sunset time (not actual time)
diff --git a/apps/clkinfosunrise/app.png b/apps/clkinfosunrise/app.png
new file mode 100644
index 000000000..a1d53946d
Binary files /dev/null and b/apps/clkinfosunrise/app.png differ
diff --git a/apps/clkinfosunrise/clkinfo.js b/apps/clkinfosunrise/clkinfo.js
new file mode 100644
index 000000000..22c507f34
--- /dev/null
+++ b/apps/clkinfosunrise/clkinfo.js
@@ -0,0 +1,76 @@
+(function() {
+ // get today's sunlight times for lat/lon
+ var sunrise, sunset, date;
+ var SunCalc = require("suncalc"); // from modules folder
+ const locale = require("locale");
+
+ function calculate() {
+ var location = require("Storage").readJSON("mylocation.json",1)||{};
+ location.lat = location.lat||51.5072;
+ location.lon = location.lon||0.1276; // London
+ date = new Date(Date.now());
+ var times = SunCalc.getTimes(date, location.lat, location.lon);
+ sunrise = times.sunrise;
+ sunset = times.sunset;
+ /* do we want to re-calculate this every day? Or we just assume
+ that 'show' will get called once a day? */
+ }
+
+ function show() {
+ this.interval = setTimeout(()=>{
+ this.emit("redraw");
+ this.interval = setInterval(()=>{
+ this.emit("redraw");
+ }, 60000);
+ }, 60000 - (Date.now() % 60000));
+ }
+ function hide() {
+ clearInterval(this.interval);
+ this.interval = undefined;
+ }
+
+ return {
+ name: "Bangle",
+ items: [
+ { name : "Sunrise",
+ get : () => { calculate();
+ return { text : locale.time(sunrise,1),
+ img : atob("GBiBAAAAAAAAAAAAAAAYAAA8AAB+AAD/AAAAAAAAAAAAAAAYAAAYAAQYIA4AcAYAYAA8AAB+AAD/AAH/gD///D///AAAAAAAAAAAAA==") }},
+ show : show, hide : hide
+ }, { name : "Sunset",
+ get : () => { calculate();
+ return { text : locale.time(sunset,1),
+ img : atob("GBiBAAAAAAAAAAAAAAB+AAA8AAAYAAAYAAAAAAAAAAAAAAAYAAAYAAQYIA4AcAYAYAA8AAB+AAD/AAH/gD///D///AAAAAAAAAAAAA==") }},
+ show : show, hide : hide
+ }, { name : "Sunrise/set", // Time in day (uses v/min/max to show percentage through day)
+ hasRange : true,
+ get : () => {
+ calculate();
+ let day = true;
+ let d = date.getTime();
+ let dayLength = sunset.getTime()-sunrise.getTime();
+ let timeUntil, timeTotal;
+ if (d < sunrise.getTime()) {
+ day = false; // early morning
+ timePast = sunrise.getTime()-d;
+ timeTotal = 86400000-dayLength;
+ } else if (d > sunset.getTime()) {
+ day = false; // evening
+ timePast = d-sunset.getTime();
+ timeTotal = 86400000-dayLength;
+ } else { // day!
+ timePast = d-sunrise.getTime();
+ timeTotal = dayLength;
+ }
+ let v = Math.round(100 * timePast / timeTotal);
+ let minutesTo = (timeTotal-timePast)/60000;
+ return { text : (minutesTo>90) ? (Math.round(minutesTo/60)+"h") : (Math.round(minutesTo)+"m"),
+ v : v, min : 0, max : 100,
+ img : day ? atob("GBiBAAAYAAAYAAAYAAgAEBwAOAx+MAD/AAH/gAP/wAf/4Af/4Of/5+f/5wf/4Af/4AP/wAH/gAD/AAx+MBwAOAgAEAAYAAAYAAAYAA==") : atob("GBiBAAfwAA/8AAP/AAH/gAD/wAB/wAB/4AA/8AA/8AA/8AAf8AAf8AAf8AAf8AA/8AA/8AA/4AB/4AB/wAD/wAH/gAf/AA/8AAfwAA==")
+ }
+ },
+ show : show, hide : hide
+ }
+ ]
+ };
+})
diff --git a/apps/clkinfosunrise/metadata.json b/apps/clkinfosunrise/metadata.json
new file mode 100644
index 000000000..d130c6453
--- /dev/null
+++ b/apps/clkinfosunrise/metadata.json
@@ -0,0 +1,12 @@
+{ "id": "clkinfosunrise",
+ "name": "Sunrise Clockinfo",
+ "version":"0.03",
+ "description": "For clocks that display 'clockinfo' (messages that can be cycled through using the clock_info module) this displays sunrise and sunset based on the location from the 'My Location' app",
+ "icon": "app.png",
+ "type": "clkinfo",
+ "tags": "clkinfo,sunrise",
+ "supports" : ["BANGLEJS2"],
+ "storage": [
+ {"name":"sunrise.clkinfo.js","url":"clkinfo.js"}
+ ]
+}
diff --git a/apps/clockcal/ChangeLog b/apps/clockcal/ChangeLog
index 20a46b5b7..27d4fc7f4 100644
--- a/apps/clockcal/ChangeLog
+++ b/apps/clockcal/ChangeLog
@@ -2,3 +2,5 @@
0.02: Added scrollable calendar and swipe gestures
0.03: Configurable drag gestures
0.04: Use default Bangle formatter for booleans
+0.05: Improved colors (connected vs disconnected)
+0.06: Tell clock widgets to hide.
diff --git a/apps/clockcal/app.js b/apps/clockcal/app.js
index 5e8c7f796..58ddd7ef5 100644
--- a/apps/clockcal/app.js
+++ b/apps/clockcal/app.js
@@ -1,3 +1,4 @@
+Bangle.setUI("clock");
Bangle.loadWidgets();
var s = Object.assign({
@@ -123,7 +124,7 @@ function drawMinutes() {
var d = new Date();
var hours = s.MODE24 ? d.getHours().toString().padStart(2, ' ') : ((d.getHours() + 24) % 12 || 12).toString().padStart(2, ' ');
var minutes = d.getMinutes().toString().padStart(2, '0');
- var textColor = NRF.getSecurityStatus().connected ? '#fff' : '#f00';
+ var textColor = NRF.getSecurityStatus().connected ? '#99f' : '#fff';
var size = 50;
var clock_x = (w - 20) / 2;
if (dimSeconds) {
@@ -307,4 +308,4 @@ NRF.on('disconnect', BTevent);
dimSeconds = Bangle.isLocked();
drawWatch();
-Bangle.setUI("clock");
+
diff --git a/apps/clockcal/metadata.json b/apps/clockcal/metadata.json
index 6d547a7a3..872211495 100644
--- a/apps/clockcal/metadata.json
+++ b/apps/clockcal/metadata.json
@@ -1,7 +1,7 @@
{
"id": "clockcal",
"name": "Clock & Calendar",
- "version": "0.04",
+ "version": "0.06",
"description": "Clock with Calendar",
"readme":"README.md",
"icon": "app.png",
diff --git a/apps/color_catalog/Changelog b/apps/color_catalog/ChangeLog
similarity index 100%
rename from apps/color_catalog/Changelog
rename to apps/color_catalog/ChangeLog
diff --git a/apps/colorful_clock/ChangeLog b/apps/colorful_clock/ChangeLog
new file mode 100644
index 000000000..54ee389e3
--- /dev/null
+++ b/apps/colorful_clock/ChangeLog
@@ -0,0 +1,3 @@
+...
+0.03: First update with ChangeLog Added
+0.04: Tell clock widgets to hide.
diff --git a/apps/colorful_clock/app.js b/apps/colorful_clock/app.js
index afc6b321f..ba6272e9b 100644
--- a/apps/colorful_clock/app.js
+++ b/apps/colorful_clock/app.js
@@ -3,6 +3,8 @@
let outerRadius = Math.min(CenterX,CenterY) * 0.9;
+ Bangle.setUI('clock');
+
Bangle.loadWidgets();
/**** updateClockFaceSize ****/
@@ -241,7 +243,3 @@
refreshDisplay();
}
});
-
- Bangle.loadWidgets();
-
- Bangle.setUI('clock');
diff --git a/apps/colorful_clock/metadata.json b/apps/colorful_clock/metadata.json
index 5b6dbe87e..237acf81c 100644
--- a/apps/colorful_clock/metadata.json
+++ b/apps/colorful_clock/metadata.json
@@ -1,7 +1,7 @@
{ "id": "colorful_clock",
"name": "Colorful Analog Clock",
"shortName":"Colorful Clock",
- "version":"0.03",
+ "version":"0.04",
"description": "a colorful analog clock",
"icon": "app-icon.png",
"type": "clock",
diff --git a/apps/compass/ChangeLog b/apps/compass/ChangeLog
index deb1072f5..cb1c6d463 100644
--- a/apps/compass/ChangeLog
+++ b/apps/compass/ChangeLog
@@ -5,3 +5,4 @@
0.05: Fix bearing not clearing correctly (visible in single or double digit bearings)
0.06: Add button for force compass calibration
0.07: Use 360-heading to output the correct heading value (fix #1866)
+0.08: Added adjustment for Bangle.js magnetometer heading fix
diff --git a/apps/compass/compass.js b/apps/compass/compass.js
index dd398ffa6..9a7aec2fc 100644
--- a/apps/compass/compass.js
+++ b/apps/compass/compass.js
@@ -20,7 +20,7 @@ ag.setColor(1).fillCircle(AGM,AGM,AGM-1,AGM-1);
ag.setColor(0).fillCircle(AGM,AGM,AGM-11,AGM-11);
function arrow(r,c) {
- r=r*Math.PI/180;
+ r=(360-r)*Math.PI/180;
var p = Math.PI/2;
ag.setColor(c).fillPoly([
AGM+AGH*Math.sin(r), AGM-AGH*Math.cos(r),
@@ -34,7 +34,7 @@ var oldHeading = 0;
Bangle.on('mag', function(m) {
if (!Bangle.isLCDOn()) return;
g.reset();
- if (isNaN(m.heading)) {
+ if (isNaN(m.heading)) {
if (!wasUncalibrated) {
g.clearRect(0,24,W,48);
g.setFontAlign(0,-1).setFont("6x8");
@@ -49,7 +49,7 @@ Bangle.on('mag', function(m) {
g.setFontAlign(0,0).setFont("6x8",3);
var y = 36;
g.clearRect(M-40,24,M+40,48);
- g.drawString(Math.round(360-m.heading),M,y,true);
+ g.drawString(Math.round(m.heading),M,y,true);
}
diff --git a/apps/compass/metadata.json b/apps/compass/metadata.json
index a3995a123..1a614e1f8 100644
--- a/apps/compass/metadata.json
+++ b/apps/compass/metadata.json
@@ -1,7 +1,7 @@
{
"id": "compass",
"name": "Compass",
- "version": "0.07",
+ "version": "0.08",
"description": "Simple compass that points North",
"icon": "compass.png",
"screenshots": [{"url":"screenshot_compass.png"}],
diff --git a/apps/configurable_clock/ChangeLog b/apps/configurable_clock/ChangeLog
new file mode 100644
index 000000000..9d55c1a91
--- /dev/null
+++ b/apps/configurable_clock/ChangeLog
@@ -0,0 +1,3 @@
+...
+0.02: First update with ChangeLog Added
+0.03: Tell clock widgets to hide.
diff --git a/apps/configurable_clock/app.js b/apps/configurable_clock/app.js
index 157d57741..45c86c7e9 100644
--- a/apps/configurable_clock/app.js
+++ b/apps/configurable_clock/app.js
@@ -5,6 +5,7 @@
let ScreenWidth = g.getWidth(), CenterX;
let ScreenHeight = g.getHeight(), CenterY, outerRadius;
+ Bangle.setUI('clock');
Bangle.loadWidgets();
/**** updateClockFaceSize ****/
@@ -1377,4 +1378,3 @@
}
});
- Bangle.setUI('clock');
diff --git a/apps/configurable_clock/metadata.json b/apps/configurable_clock/metadata.json
index 28feae7e4..687a5b212 100644
--- a/apps/configurable_clock/metadata.json
+++ b/apps/configurable_clock/metadata.json
@@ -1,7 +1,7 @@
{ "id": "configurable_clock",
"name": "Configurable Analog Clock",
"shortName":"Configurable Clock",
- "version":"0.02",
+ "version":"0.03",
"description": "an analog clock with several kinds of faces, hands and colors to choose from",
"icon": "app-icon.png",
"type": "clock",
diff --git a/apps/contourclock/ChangeLog b/apps/contourclock/ChangeLog
index d415a604d..387340d5b 100644
--- a/apps/contourclock/ChangeLog
+++ b/apps/contourclock/ChangeLog
@@ -7,3 +7,4 @@
0.25: Fixed a bug that would let widgets change the color of the clock.
0.26: Time formatted to locale
0.27: Fixed the timing code, which sometimes did not update for one minute
+0.28: More config options for cleaner look, enabled fast loading
diff --git a/apps/contourclock/README.md b/apps/contourclock/README.md
new file mode 100644
index 000000000..3341439da
--- /dev/null
+++ b/apps/contourclock/README.md
@@ -0,0 +1,6 @@
+# New Features:
+- Fast load! (only works if your launcher uses widgets)
+- widgets, date and weekday are individually configurable
+- you can hide widgets, date and weekday for a cleaner look when the watch is locked
+
+Contact me for bug reports or feature requests: ContourClock@gmx.de
diff --git a/apps/contourclock/app.js b/apps/contourclock/app.js
index d5c97edfa..8efa406c6 100644
--- a/apps/contourclock/app.js
+++ b/apps/contourclock/app.js
@@ -1,35 +1,64 @@
-var digits = [];
-var drawTimeout;
-var fontName="";
-var settings = require('Storage').readJSON("contourclock.json", true) || {};
-if (settings.fontIndex==undefined) {
- settings.fontIndex=0;
- require('Storage').writeJSON("myapp.json", settings);
-}
+{
+ let digits = [];
+ let drawTimeout;
+ let fontName="";
+ let settings = require('Storage').readJSON("contourclock.json", true) || {};
+ if (settings.fontIndex==undefined) {
+ settings.fontIndex=0;
+ settings.widgets=true;
+ settings.hide=false;
+ settings.weekday=true;
+ settings.hideWhenLocked=false;
+ settings.date=true; require('Storage').writeJSON("myapp.json", settings);
+ }
-function queueDraw() {
- setTimeout(function() {
+ let queueDraw = function() {
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = setTimeout(function() {
+ drawTimeout = undefined;
+ draw();
+ queueDraw();
+ }, 60000 - (Date.now() % 60000));
+ };
+
+ let draw = function() {
+ var date = new Date();
+ // Draw day of the week
+ g.reset();
+ if ((!settings.hideWhenLocked) || (!Bangle.isLocked())) {
+ // Draw day of the week
+ g.setFont("Teletext10x18Ascii");
+ g.clearRect(0,138,g.getWidth()-1,176);
+ if (settings.weekday) g.setFontAlign(0,1).drawString(require("locale").dow(date).toUpperCase(),g.getWidth()/2,g.getHeight()-18);
+ // Draw Date
+ if (settings.date) g.setFontAlign(0,1).drawString(require('locale').date(new Date(),1),g.getWidth()/2,g.getHeight());
+ }
+ require('contourclock').drawClock(settings.fontIndex);
+ };
+
+ require("FontTeletext10x18Ascii").add(Graphics);
+ g.clear();
+
+ draw();
+ if (settings.hideWhenLocked) Bangle.on('lock', function (locked) {
+ if (!locked) require("widget_utils").show();
+ else {
+ g.clear();
+ if (settings.hide) require("widget_utils").swipeOn();
+ else require("widget_utils").hide();
+ }
draw();
- queueDraw();
- }, 60000 - (Date.now() % 60000));
+ });
+ Bangle.setUI({mode:"clock", remove:function() {
+ if (drawTimeout) clearTimeout(drawTimeout);
+ if (settings.widgets && settings.hide) require("widget_utils").show();
+ g.reset();
+ g.clear();
+ }});
+ if (settings.widgets) {
+ Bangle.loadWidgets();
+ if (settings.hide) require("widget_utils").swipeOn();
+ else Bangle.drawWidgets();
+ }
+ queueDraw();
}
-
-function draw() {
- var date = new Date();
- // Draw day of the week
- g.reset();
- g.setFont("Teletext10x18Ascii");
- g.clearRect(0,138,g.getWidth()-1,176);
- g.setFontAlign(0,1).drawString(require("locale").dow(date).toUpperCase(),g.getWidth()/2,g.getHeight()-18);
- // Draw Date
- g.setFontAlign(0,1).drawString(require('locale').date(new Date(),1),g.getWidth()/2,g.getHeight());
- require('contourclock').drawClock(settings.fontIndex);
-}
-
-require("FontTeletext10x18Ascii").add(Graphics);
-Bangle.setUI("clock");
-g.clear();
-Bangle.loadWidgets();
-Bangle.drawWidgets();
-queueDraw();
-draw();
diff --git a/apps/contourclock/contourclock.settings.js b/apps/contourclock/contourclock.settings.js
index a12538fc5..f2a75d9b5 100644
--- a/apps/contourclock/contourclock.settings.js
+++ b/apps/contourclock/contourclock.settings.js
@@ -1,43 +1,73 @@
(function(back) {
- Bangle.removeAllListeners('drag');
Bangle.setUI("");
var settings = require('Storage').readJSON('contourclock.json', true) || {};
if (settings.fontIndex==undefined) {
- settings.fontIndex=0;
+ settings.fontIndex=0;
+ settings.widgets=true;
+ settings.hide=false;
+ settings.weekday=true;
+ settings.date=true;
+ settings.hideWhenLocked=false;
require('Storage').writeJSON("myapp.json", settings);
}
- savedIndex=settings.fontIndex;
- saveListener = setWatch(function() { //save changes and return to settings menu
- require('Storage').writeJSON('contourclock.json', settings);
- Bangle.removeAllListeners('swipe');
- Bangle.removeAllListeners('lock');
- clearWatch(saveListener);
- g.clear();
- back();
- }, BTN, { repeat:false, edge:'falling' });
- lockListener = Bangle.on('lock', function () { //discard changes and return to clock
- settings.fontIndex=savedIndex;
- require('Storage').writeJSON('contourclock.json', settings);
- Bangle.removeAllListeners('swipe');
- Bangle.removeAllListeners('lock');
- clearWatch(saveListener);
- g.clear();
- load();
- });
- swipeListener = Bangle.on('swipe', function (direction) {
- var fontName = require('contourclock').drawClock(settings.fontIndex+direction);
- if (fontName) {
- settings.fontIndex+=direction;
- g.clearRect(0,0,g.getWidth()-1,16);
- g.setFont('6x8:2x2').setFontAlign(0,-1).drawString(fontName,g.getWidth()/2,0);
- } else {
- require('contourclock').drawClock(settings.fontIndex);
- }
- });
- g.reset();
- g.clear();
- g.setFont('6x8:2x2').setFontAlign(0,-1);
- g.drawString(require('contourclock').drawClock(settings.fontIndex),g.getWidth()/2,0);
- g.drawString('Swipe - change',g.getWidth()/2,g.getHeight()-36);
- g.drawString('BTN - save',g.getWidth()/2,g.getHeight()-18);
+ function mainMenu() {
+ E.showMenu({
+ "" : { "title" : "ContourClock" },
+ "< Back" : () => back(),
+ 'Widgets': {
+ value: (settings.widgets !== undefined ? settings.widgets : true),
+ onchange : v => {settings.widgets=v; require('Storage').writeJSON('contourclock.json', settings);}
+ },
+ 'hide Widgets': {
+ value: (settings.hide !== undefined ? settings.hide : false),
+ onchange : v => {settings.hide=v; require('Storage').writeJSON('contourclock.json', settings);}
+ },
+ 'Weekday': {
+ value: (settings.weekday !== undefined ? settings.weekday : true),
+ onchange : v => {settings.weekday=v; require('Storage').writeJSON('contourclock.json', settings);}
+ },
+ 'Date': {
+ value: (settings.date !== undefined ? settings.date : true),
+ onchange : v => {settings.date=v; require('Storage').writeJSON('contourclock.json', settings);}
+ },
+ 'Hide when locked': {
+ value: (settings.hideWhenLocked !== undefined ? settings.hideWhenLocked : false),
+ onchange : v => {settings.hideWhenLocked=v; require('Storage').writeJSON('contourclock.json', settings);}
+ },
+ 'set Font': () => fontMenu()
+ });
+ }
+ function fontMenu() {
+ Bangle.setUI("");
+ savedIndex=settings.fontIndex;
+ saveListener = setWatch(function() { //save changes and return to settings menu
+ require('Storage').writeJSON('contourclock.json', settings);
+ Bangle.removeAllListeners('swipe');
+ Bangle.removeAllListeners('lock');
+ mainMenu();
+ }, BTN, { repeat:false, edge:'falling' });
+ lockListener = Bangle.on('lock', function () { //discard changes and return to clock
+ settings.fontIndex=savedIndex;
+ require('Storage').writeJSON('contourclock.json', settings);
+ Bangle.removeAllListeners('swipe');
+ Bangle.removeAllListeners('lock');
+ mainMenu();
+ });
+ swipeListener = Bangle.on('swipe', function (direction) {
+ var fontName = require('contourclock').drawClock(settings.fontIndex+direction);
+ if (fontName) {
+ settings.fontIndex+=direction;
+ g.clearRect(0,g.getHeight()-36,g.getWidth()-1,g.getHeight()-36+16);
+ g.setFont('6x8:2x2').setFontAlign(0,-1).drawString(fontName,g.getWidth()/2,g.getHeight()-36);
+ } else {
+ require('contourclock').drawClock(settings.fontIndex);
+ }
+ });
+ g.reset();
+ g.clearRect(0,24,g.getWidth()-1,g.getHeight()-1);
+ g.setFont('6x8:2x2').setFontAlign(0,-1);
+ g.drawString(require('contourclock').drawClock(settings.fontIndex),g.getWidth()/2,g.getHeight()-36);
+ g.drawString('Button to save',g.getWidth()/2,g.getHeight()-18);
+ }
+ mainMenu();
})
diff --git a/apps/contourclock/metadata.json b/apps/contourclock/metadata.json
index eb0dd39fb..6b2b51991 100644
--- a/apps/contourclock/metadata.json
+++ b/apps/contourclock/metadata.json
@@ -1,9 +1,10 @@
{ "id": "contourclock",
"name": "Contour Clock",
"shortName" : "Contour Clock",
- "version":"0.27",
+ "version":"0.28",
"icon": "app.png",
- "description": "A Minimalist clockface with large Digits. Now with more fonts!",
+ "readme": "README.md",
+ "description": "A Minimalist clockface with large Digits.",
"screenshots" : [{"url":"cc-screenshot-1.png"},{"url":"cc-screenshot-2.png"}],
"tags": "clock",
"custom": "custom.html",
diff --git a/apps/counter/ChangeLog b/apps/counter/ChangeLog
index f3f1c4eac..8402b3467 100644
--- a/apps/counter/ChangeLog
+++ b/apps/counter/ChangeLog
@@ -1,3 +1,4 @@
0.01: New App!
0.02: Added decrement and touch functions
0.03: Set color - ensures widgets don't end up coloring the counter's text
+0.04: Adopted for BangleJS 2
diff --git a/apps/counter/counter.js b/apps/counter/counter.js
index 3e0687944..0054ada6d 100644
--- a/apps/counter/counter.js
+++ b/apps/counter/counter.js
@@ -1,45 +1,104 @@
var counter = 0;
+const BANGLEJS2 = process.env.HWVERSION == 2;
+
+if (BANGLEJS2) {
+ var drag;
+ var y = 45;
+ var x = 5;
+} else {
+ var y = 100;
+ var x = 25;
+}
function updateScreen() {
- g.clearRect(0, 50, 250, 150);
- g.setColor(0xFFFF);
+ if (BANGLEJS2) {
+ g.clearRect(0, 50, 250, 130);
+ } else {
+ g.clearRect(0, 50, 250, 150);
+ }
+ g.setBgColor(g.theme.bg).setColor(g.theme.fg);
g.setFont("Vector",40).setFontAlign(0,0);
g.drawString(Math.floor(counter), g.getWidth()/2, 100);
- g.drawString('-', 45, 100);
- g.drawString('+', 185, 100);
+ if (!BANGLEJS2) {
+ g.drawString('-', 45, 100);
+ g.drawString('+', 185, 100);
+ }
}
-// add a count by using BTN1 or BTN5
-setWatch(() => {
- counter += 1;
- updateScreen();
-}, BTN1, {repeat:true});
+if (BANGLEJS2) {
+ setWatch(() => {
+ counter = 0;
+ updateScreen();
+ }, BTN1, {repeat:true});
+ Bangle.on("drag", e => {
+ if (!drag) { // start dragging
+ drag = {x: e.x, y: e.y};
+ } else if (!e.b) { // released
+ const dx = e.x-drag.x, dy = e.y-drag.y;
+ drag = null;
+ if (Math.abs(dx)>Math.abs(dy)+10) {
+ // horizontal
+ if (dx < dy) {
+ //console.log("left " + dx + " " + dy);
+ } else {
+ //console.log("right " + dx + " " + dy);
+ }
+ } else if (Math.abs(dy)>Math.abs(dx)+10) {
+ // vertical
+ if (dx < dy) {
+ //console.log("down " + dx + " " + dy);
+ if (counter > 0) counter -= 1;
+ updateScreen();
+ } else {
+ //console.log("up " + dx + " " + dy);
+ counter += 1;
+ updateScreen();
+ }
+ } else {
+ //console.log("tap " + e.x + " " + e.y);
+ }
+ }
+ });
+ } else {
-setWatch(() => {
- counter += 1;
- updateScreen();
-}, BTN5, {repeat:true});
+ // add a count by using BTN1 or BTN5
+ setWatch(() => {
+ counter += 1;
+ updateScreen();
+ }, BTN1, {repeat:true});
+
+ setWatch(() => {
+ counter += 1;
+ updateScreen();
+ }, BTN5, {repeat:true});
+
+ // subtract a count by using BTN3 or BTN4
+ setWatch(() => {
+ if (counter > 0) counter -= 1;
+ updateScreen();
+ }, BTN4, {repeat:true});
+
+ setWatch(() => {
+ if (counter > 0) counter -= 1;
+ updateScreen();
+ }, BTN3, {repeat:true});
+
+ // reset by using BTN2
+ setWatch(() => {
+ counter = 0;
+ updateScreen();
+ }, BTN2, {repeat:true});
+}
-// subtract a count by using BTN3 or BTN4
-setWatch(() => {
- counter -= 1;
- updateScreen();
-}, BTN4, {repeat:true});
-
-setWatch(() => {
- counter -= 1;
- updateScreen();
-}, BTN3, {repeat:true});
-
-// reset by using BTN2
-setWatch(() => {
- counter = 0;
- updateScreen();
-}, BTN2, {repeat:true});
g.clear(1).setFont("6x8");
-g.drawString('Tap right or BTN1 to increase\nTap left or BTN3 to decrease\nPress BTN2 to reset.', 25, 200);
+g.setBgColor(g.theme.bg).setColor(g.theme.fg);
+if (BANGLEJS2) {
+ g.drawString('Swipe up to increase\nSwipe down to decrease\nPress button to reset.', x, 100 + y);
+} else {
+ g.drawString('Tap right or BTN1 to increase\nTap left or BTN3 to decrease\nPress BTN2 to reset.', x, 100 + y);
+}
Bangle.loadWidgets();
Bangle.drawWidgets();
diff --git a/apps/counter/metadata.json b/apps/counter/metadata.json
index e455fda95..daba58d39 100644
--- a/apps/counter/metadata.json
+++ b/apps/counter/metadata.json
@@ -1,11 +1,11 @@
{
"id": "counter",
"name": "Counter",
- "version": "0.03",
+ "version": "0.04",
"description": "Simple counter",
"icon": "counter_icon.png",
"tags": "tool",
- "supports": ["BANGLEJS"],
+ "supports": ["BANGLEJS", "BANGLEJS2"],
"screenshots": [{"url":"bangle1-counter-screenshot.png"}],
"allow_emulator": true,
"storage": [
diff --git a/apps/crowclk/ChangeLog b/apps/crowclk/ChangeLog
index 4f48bdd14..1c4f6f43b 100644
--- a/apps/crowclk/ChangeLog
+++ b/apps/crowclk/ChangeLog
@@ -1,3 +1,4 @@
0.01: New App!
0.02: Removed "wake LCD on face-up"-feature: A watch-face should not set things like "wake LCD on face-up".
0.03: Fix the clock for dark mode.
+0.04: Tell clock widgets to hide.
diff --git a/apps/crowclk/crow_clock.js b/apps/crowclk/crow_clock.js
index eee1653cb..7e608ef19 100644
--- a/apps/crowclk/crow_clock.js
+++ b/apps/crowclk/crow_clock.js
@@ -136,9 +136,9 @@ Bangle.on('lcdPower', (on) => {
g.clear();
+// Show launcher when button pressed
+Bangle.setUI("clock");
Bangle.loadWidgets();
Bangle.drawWidgets();
startTimers();
-// Show launcher when button pressed
-Bangle.setUI("clock");
diff --git a/apps/crowclk/metadata.json b/apps/crowclk/metadata.json
index 6985cf11a..265a0398b 100644
--- a/apps/crowclk/metadata.json
+++ b/apps/crowclk/metadata.json
@@ -1,7 +1,7 @@
{
"id": "crowclk",
"name": "Crow Clock",
- "version": "0.03",
+ "version": "0.04",
"description": "A simple clock based on Bold Clock that has MST3K's Crow T. Robot for a face",
"icon": "crow_clock.png",
"screenshots": [{"url":"screenshot_crow.png"}],
diff --git a/apps/daisy/ChangeLog b/apps/daisy/ChangeLog
index 829ff3d13..61a09a18d 100644
--- a/apps/daisy/ChangeLog
+++ b/apps/daisy/ChangeLog
@@ -5,3 +5,5 @@
0.05: changed text to uppercase, just looks better, removed colons on text
0.06: better contrast for light theme, use fg color instead of dithered for ring
0.07: Use default Bangle formatter for booleans
+0.08: fix idle timer always getting set to true
+0.09: Use 'modules/suncalc.js' to avoid it being copied 8 times for different apps
diff --git a/apps/daisy/app.js b/apps/daisy/app.js
index 7c513726f..c99b19228 100644
--- a/apps/daisy/app.js
+++ b/apps/daisy/app.js
@@ -1,4 +1,4 @@
-var SunCalc = require("https://raw.githubusercontent.com/mourner/suncalc/master/suncalc.js");
+var SunCalc = require("suncalc"); // from modules folder
const storage = require('Storage');
const locale = require("locale");
const SETTINGS_FILE = "daisy.json";
@@ -70,7 +70,7 @@ function getSteps() {
try {
return Bangle.getHealthStatus("day").steps;
} catch (e) {
- if (WIDGETS.wpedom !== undefined)
+ if (WIDGETS.wpedom !== undefined)
return WIDGETS.wpedom.getSteps();
else
return 0;
@@ -83,7 +83,7 @@ function loadSettings() {
settings = require("Storage").readJSON(SETTINGS_FILE,1)||{};
settings.gy = settings.gy||'#020';
settings.fg = settings.fg||'#0f0';
- settings.idle_check = settings.idle_check||true;
+ settings.idle_check = (settings.idle_check === undefined ? true : settings.idle_check);
assignPalettes();
}
@@ -151,7 +151,7 @@ function prevInfo() {
function clearInfo() {
g.setColor(g.theme.bg);
//g.setColor(g.theme.fg);
- g.fillRect((w/2) - infoWidth, infoLine - infoHeight, (w/2) + infoWidth, infoLine + infoHeight);
+ g.fillRect((w/2) - infoWidth, infoLine - infoHeight, (w/2) + infoWidth, infoLine + infoHeight);
}
function drawInfo() {
@@ -202,7 +202,7 @@ function drawClock() {
var mm = da[4].substr(3,2);
var steps = getSteps();
var p_steps = Math.round(100*(steps/10000));
-
+
g.reset();
g.setColor(g.theme.bg);
g.fillRect(0, 0, w, h);
@@ -218,7 +218,7 @@ function drawClock() {
g.drawString(mm, (w/2) + 1, h/2);
drawInfo();
-
+
// recalc sunrise / sunset every hour
if (drawCount % 60 == 0)
updateSunRiseSunSet(new Date(), location.lat, location.lon);
@@ -254,7 +254,7 @@ function resetHrm() {
Bangle.on('HRM', function(hrm) {
hrmCurrent = hrm.bpm;
hrmConfidence = hrm.confidence;
- log_debug("HRM=" + hrm.bpm + " (" + hrm.confidence + ")");
+ log_debug("HRM=" + hrm.bpm + " (" + hrm.confidence + ")");
if (infoMode == "ID_HRM" ) drawHrm();
});
@@ -360,7 +360,7 @@ function getGaugeImage(p) {
palette : pal2,
buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESFQOoFacFQiSCCwArTgCESQSyEUlTZTboyCnQiSCYQiSCYQiSCZQgdAVxwqYQgSwMVwOoFbMFWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbheqFcTcHbT7cDFY0CbT7cDqArxhWqwArfgFVqgrHFUDcBFY0qFcdVFY2oFcMFFY2qFclAFYugFcMBFYsCFctQFYuAFcMAFYsKFctUFYoqigEVFeEqFctVFYmoFccFFYmqFc1AcIdQFccBFf4rbGAoAhKQYr/Fa8FFc9UFYYqkgEVFf4r/FYwDDAEZTDFf4r/Ff4rbqorooArBqArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlgorCioroAYIr/Ff4r/FbYDDAEZTDFf4r/FYtAFclVFYUBFc9QFf4rZAgoAgKQor/FbFUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHIA="))
};
-
+
// p90
if (p >= 90 && p < 100) return {
width : 176, height : 176, bpp : 2,
@@ -410,7 +410,7 @@ function BUTTON(name,x,y,w,h,c,f,tx) {
// if pressed the callback
BUTTON.prototype.check = function(x,y) {
//console.log(this.name + ":check() x=" + x + " y=" + y +"\n");
-
+
if (x>= this.x && x<= (this.x + this.w) && y>= this.y && y<= (this.y + this.h)) {
log_debug(this.name + ":callback\n");
this.callback();
@@ -472,7 +472,7 @@ function checkIdle() {
warned = false;
return;
}
-
+
let hour = (new Date()).getHours();
let active = (hour >= 9 && hour < 21);
//let active = true;
@@ -501,7 +501,7 @@ function buzzer(n) {
if (n-- < 1) return;
Bangle.buzz(250);
-
+
if (buzzTimeout) clearTimeout(buzzTimeout);
buzzTimeout = setTimeout(function() {
buzzTimeout = undefined;
diff --git a/apps/daisy/metadata.json b/apps/daisy/metadata.json
index 802ba6834..0bad50151 100644
--- a/apps/daisy/metadata.json
+++ b/apps/daisy/metadata.json
@@ -1,6 +1,6 @@
{ "id": "daisy",
"name": "Daisy",
- "version":"0.07",
+ "version":"0.09",
"dependencies": {"mylocation":"app"},
"description": "A beautiful digital clock with large ring guage, idle timer and a cyclic information line that includes, day, date, steps, battery, sunrise and sunset times",
"icon": "app.png",
diff --git a/apps/deko/Building_Typeface.ttf b/apps/deko/Building_Typeface.ttf
new file mode 100644
index 000000000..d5a3933ab
Binary files /dev/null and b/apps/deko/Building_Typeface.ttf differ
diff --git a/apps/deko/ChangeLog b/apps/deko/ChangeLog
new file mode 100644
index 000000000..9db0e26c5
--- /dev/null
+++ b/apps/deko/ChangeLog
@@ -0,0 +1 @@
+0.01: first release
diff --git a/apps/deko/README.md b/apps/deko/README.md
new file mode 100644
index 000000000..91e83bd23
--- /dev/null
+++ b/apps/deko/README.md
@@ -0,0 +1,10 @@
+# Deko Clock
+
+A simple clock with an Art Deko font
+
+The font was obtained from https://dafonttop.com/building.font and is free for personal use
+
+
+
+
+Written by: [Hugh Barney](https://github.com/hughbarney) For support and discussion please post in the [Bangle JS Forum](http://forum.espruino.com/microcosms/1424/)
diff --git a/apps/deko/app-icon.js b/apps/deko/app-icon.js
new file mode 100644
index 000000000..06f93e2ef
--- /dev/null
+++ b/apps/deko/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwIdah/wAof//4ECgYFB4AFBg4FB8AFBj/wh/4AoM/wEB/gFBvwCEBAU/AQP4gfAj8AgPwAoMPwED8AFBg/AAYIBDA4ngg4TB4EBApkPKgJSBJQIFTMgIFCJIIFDKoIFEvgFBGoMAnw7DP4IFEh+BAoItBg+DNIQwBMIaeCKoKxCPoIzCEgKVHUIqtFXIrFFaIrdFdIwAV"))
diff --git a/apps/deko/app.js b/apps/deko/app.js
new file mode 100644
index 000000000..8ae2c1d31
--- /dev/null
+++ b/apps/deko/app.js
@@ -0,0 +1,64 @@
+Graphics.prototype.setFontBuildingTypeface = function(scale) {
+ // Actual height 100 (102 - 3)
+ this.setFontCustom(
+ atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAH/gAAAAAAAAAAAAAAAAAH//gAAAAAAAAAAAAAAAAD///gAAAAAAAAAAAAAAAD////gAAAAAAAAAAAAAAD/////gAAAAAAAAAAAAAB//////gAAAAAAAAAAAAB///////gAAAAAAAAAAAB////////gAAAAAAAAAAA////////4AAAAAAAAAAA////////4AAAAAAAAAAAf///////8AAAAAAAAAAAf///////8AAAAAAAAAAAf///////8AAAAAAAAAAAP///////+AAAAAAAAAAAP///////+AAAAAAAAAAAP///////+AAAAAAAAAAAH////////AAAAAAAAAAAAH///////AAAAAAAAAAAAAH//////AAAAAAAAAAAAAAH/////gAAAAAAAAAAAAAAH////gAAAAAAAAAAAAAAAH///wAAAAAAAAAAAAAAAAH//wAAAAAAAAAAAAAAAAAH/wAAAAAAAAAAAAAAAAAAH4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////////////AAAAAAAD/////////////wAAAAAAP/////////////4AAAAAAP/////////////8AAAAAAf/////////////+AAAAAAf/////////////+AAAAAA///////////////AAAAAA///////////////AAAAAA///////////////AAAAAA/4AAAAAAAAAAAH/AAAAAA/4AAAAAAAAAAAH/AAAAAA/4AAAAAAAAAAAH/AAAAAA/4AAAAAAAAAAAH/AAAAAA/4AAAAAAAAAAAH/AAAAAA/4AAAAAAAAAAAH/AAAAAA/4AAAAAAAAAAAH/AAAAAA/4AAAAAAAAAAAH/AAAAAA/4AAAAAAAAAAAH/AAAAAA/4AAAAAAAAAAAH/AAAAAA/4AAAAAAAAAAAH/AAAAAA/4AAAAAAAAAAAH/AAAAAA///////////////AAAAAA///////////////AAAAAA///////////////AAAAAA///////////////AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAP/////////////8AAAAAAH/////////////4AAAAAAD/////////////wAAAAAAA/////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP4AAAAAAAAAAAAAAAAAAAf4AAAAAAAAAAAAAAAAAAA/4AAAAAAAAAAAAAAAAAAB/4AAAAAAAAAAAAAAAAAAD/4AAAAAAAAAAAAAAAAAAH/4AAAAAAAAAAAAAAAAAAf/4AAAAAAAAAAAAAAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/wAAf///////+AAAAAAB//wAB////////+AAAAAAH//wAD////////+AAAAAAH//wAH////////+AAAAAAP//wAP////////+AAAAAAf//wAP////////+AAAAAAf//wAP////////+AAAAAAf//wAf////////+AAAAAAf//wAf////////+AAAAAAf8AAAf8AAAAAAP+AAAAAAf8AAAf8AAAAAAP+AAAAAAf8AAAf8AAAAAAP+AAAAAAf8AAAf8AAAAAAP+AAAAAAf8AAAf8AAAAAAP+AAAAAAf8AAAf8AAAAAAP+AAAAAAf8AAAf8AAAAAAP+AAAAAAf8AAAf8AAAAAAP+AAAAAAf8AAAf8AAAAAAP+AAAAAAf8AAAf8AAAAAAP+AAAAAAf8AAAf8AAAAAAP+AAAAAAf8AAAf8AAAAAAP+AAAAAAf/////8AAAA///+AAAAAAf/////8AAAA///+AAAAAAf/////8AAAA///+AAAAAAf/////8AAAA///+AAAAAAP/////8AAAA///+AAAAAAH/////8AAAA///+AAAAAAH/////8AAAA///+AAAAAAB/////8AAAA///+AAAAAAAf////8AAAA///+AAAAAAAAAAAAAAAAA///+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf//gAAAAAAAf//+AAAAAAf//wAAAAAAA///+AAAAAAf//wAAAAAAA///+AAAAAAf//wAAAAAAA///+AAAAAAf//wAAAAAAA///+AAAAAAf//wAAAAAAA///+AAAAAAf//wAAAAAAA///+AAAAAAf//wAAAAAAA///+AAAAAAf//wAAAAAAA///+AAAAAAf//wAAAAAAA///+AAAAAAf8AAAAAAAAAAAP+AAAAAAf8AAAAAAAAAAAP+AAAAAAf8AAAAAAAAAAAP+AAAAAAf8AAAAEAAAAAAP+AAAAAAf8AAAB8AAAAAAP+AAAAAAf8AAAP8AAAAAAP+AAAAAAf8AAD/8AAAAAAP+AAAAAAf8AA//8AAAAAAP+AAAAAAf8AP//8AAAAAAP+AAAAAAf8B///8AAAAAAP+AAAAAAf8f///8AAAAAAP+AAAAAAf/////8AAAAAAP+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf///8P////////+AAAAAAf///gH////////+AAAAAAf//4AD////////+AAAAAAf/+AAB////////+AAAAAAf/gAAA////////+AAAAAAfwAAAAD///////+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/gAAAAAAAAAAAAAAAAAB//gAAAAAAAAAAAAAAAAAH//gAAAAAAAAAAAAAAAAA///gAAAAAAAAAAAAAAAAH///gAAAAAAAAAAAAAAAAf///gAAAAAAAAAAAAAAAD////gAAAAAAAAAAAAAAAf////gAAAAAAAAAAAAAAB/////gAAAAAAAAAAAAAAP///z/gAAAAAAAAAAAAAB///+D/gAAAAAAAAAAAAAH///wD/gAAAAAAAAAAAAA///+AD/gAAAAAAAAAAAAH///wAD/gAAAAAAAAAAAAf//+AAD/gAAAAAAAAAAAD///wAAD/gAAAAAAAAAAAf///AAAD/gAAAAAAAAAAB///4AAAD/gAAAAAAAAAAP///AAAAD/gAAAAAAAAAB///4AAAAD/gAAAAAAAAAH///AAAAAD/gAAAAAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAAAAAAAAAAD/gAAAAAAAAAAAAAAAAAAD/gAAAAAAAAAAAAAAAAAAD/gAAAAAAAAAAAAAAAAAAD/gAAAAAAAAAAAAAAAAAAD/gAAAAAAAAAAAAAAAAAAD/gAAAAAAAAAAAAAAAAAAD/gAAAAAAAAAAAAAAAAAAD/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//+AAAAAAAf/////8AAAA///wAAAAAAf/////8AAAA///4AAAAAAf/////8AAAA///8AAAAAAf/////8AAAA///+AAAAAAf/////8AAAA///+AAAAAAf/////8AAAA///+AAAAAAf/////8AAAA////AAAAAAf/////8AAAA////AAAAAAf/////8AAAA////AAAAAAf8AAAf8AAAAAAH/AAAAAAf8AAAf8AAAAAAH/AAAAAAf8AAAf8AAAAAAH/AAAAAAf8AAAf8AAAAAAH/AAAAAAf8AAAf8AAAAAAH/AAAAAAf8AAAf8AAAAAAH/AAAAAAf8AAAf8AAAAAAH/AAAAAAf8AAAf8AAAAAAH/AAAAAAf8AAAf8AAAAAAH/AAAAAAf8AAAf8AAAAAAH/AAAAAAf8AAAf8AAAAAAH/AAAAAAf//wAf8AAAAAAH/AAAAAAf//wAf/////////AAAAAAf//wAf/////////AAAAAAf//wAf/////////AAAAAAf//wAf////////+AAAAAAf//wAP////////+AAAAAAf//wAP////////8AAAAAAf//wAH////////8AAAAAAf//wAD////////wAAAAAAf//wAA////////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////////////AAAAAAAD/////////////wAAAAAAP/////////////4AAAAAAP/////////////8AAAAAAf/////////////+AAAAAAf/////////////+AAAAAA///////////////AAAAAA///////////////AAAAAA///////////////AAAAAA/4AAAf8AAAAAAH/AAAAAA/4AAAf8AAAAAAH/AAAAAA/4AAAf8AAAAAAH/AAAAAA/4AAAf8AAAAAAH/AAAAAA/4AAAf8AAAAAAH/AAAAAA/4AAAf8AAAAAAH/AAAAAA/4AAAf8AAAAAAH/AAAAAA/4AAAf8AAAAAAH/AAAAAA/4AAAf8AAAAAAH/AAAAAA/4AAAf8AAAAAAH/AAAAAA/4AAAf8AAAAAAH/AAAAAA///wAf8AAAAAAH/AAAAAA///wAf/////////AAAAAA///wAf/////////AAAAAA///wAf/////////AAAAAAf//wAf/////////AAAAAAf//wAP////////+AAAAAAP//wAP////////+AAAAAAH//wAH////////8AAAAAAB//wAD////////4AAAAAAAf/wAB////////wAAAAAAAAAAAAf///////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf//wAAAAAAAAAAAAAAAAAf//wAAAAAAAAAAAAAAAAAf//wAAAAAAAAAAeAAAAAAf//wAAAAAAAAAP+AAAAAAf//wAAAAAAAAD/+AAAAAAf//wAAAAAAAA//+AAAAAAf//wAAAAAAAf//+AAAAAAf//wAAAAAAH///+AAAAAAf//wAAAAAD////+AAAAAAf8AAAAAAA/////+AAAAAAf8AAAAAAP/////+AAAAAAf8AAAAAH//////8AAAAAAf8AAAAB///////AAAAAAAf8AAAAf//////wAAAAAAAf8AAAP//////4AAAAAAAAf8AAD//////+AAAAAAAAAf8AB///////gAAAAAAAAAf8Af//////4AAAAAAAAAAf8H//////+AAAAAAAAAAAf////////gAAAAAAAAAAAf///////wAAAAAAAAAAAAf//////8AAAAAAAAAAAAAf//////AAAAAAAAAAAAAAf/////wAAAAAAAAAAAAAAf////8AAAAAAAAAAAAAAAf////AAAAAAAAAAAAAAAAf///wAAAAAAAAAAAAAAAAf//4AAAAAAAAAAAAAAAAAf/+AAAAAAAAAAAAAAAAAAf/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///gf//////+AAAAAAAB////4////////wAAAAAAH////9////////4AAAAAAP/////////////8AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAA///////////////AAAAAA///////////////AAAAAA//////4AAAAAAH/AAAAAA/4AAA/4AAAAAAH/AAAAAA/4AAA/4AAAAAAH/AAAAAA/4AAA/4AAAAAAH/AAAAAA/4AAA/4AAAAAAH/AAAAAA/4AAA/4AAAAAAH/AAAAAA/4AAA/4AAAAAAH/AAAAAA/4AAA/4AAAAAAH/AAAAAA/4AAA/4AAAAAAH/AAAAAA/4AAA/4AAAAAAH/AAAAAA/4AAA/4AAAAAAH/AAAAAA/4AAA/4AAAAAAH/AAAAAA///////////////AAAAAA///////////////AAAAAA///////////////AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAP/////////////+AAAAAAP/////////////8AAAAAAH////9////////4AAAAAAB////4////////gAAAAAAAH///gP//////+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///////+AAAAAAAAAAAAD////////gAD//AAAAAAAP////////wAD//wAAAAAAP////////4AD//4AAAAAAf////////8AD//8AAAAAAf////////8AD//+AAAAAA/////////+AD//+AAAAAA/////////+AD///AAAAAA/////////+AD///AAAAAA/4AAAAAAP+AD///AAAAAA/4AAAAAAP+AAAH/AAAAAA/4AAAAAAP+AAAH/AAAAAA/4AAAAAAP+AAAH/AAAAAA/4AAAAAAP+AAAH/AAAAAA/4AAAAAAP+AAAH/AAAAAA/4AAAAAAP+AAAH/AAAAAA/4AAAAAAP+AAAH/AAAAAA/4AAAAAAP+AAAH/AAAAAA/4AAAAAAP+AAAH/AAAAAA/4AAAAAAP+AAAH/AAAAAA/4AAAAAAP+AAAH/AAAAAA/4AAAAAAP+AAAH/AAAAAA///////////////AAAAAA///////////////AAAAAA///////////////AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAP/////////////8AAAAAAP/////////////4AAAAAAD/////////////wAAAAAAA/////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/wAAAf+AAAAAAAAAAAAAD/wAAAf+AAAAAAAAAAAAAD/wAAAf+AAAAAAAAAAAAAD/wAAAf+AAAAAAAAAAAAAD/wAAAf+AAAAAAAAAAAAAD/wAAAf+AAAAAAAAAAAAAD/wAAAf+AAAAAAAAAAAAAD/wAAAf+AAAAAAAAAAAAAD/wAAAf+AAAAAAAAAAAAAD/wAAAf+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='),
+ 46,
+ atob("FCYpGigoKigoJykoFA=="),
+ 126+(scale<<8)+(1<<16)
+ );
+ return this;
+};
+
+
+
+// timeout used to update every minute
+var drawTimeout;
+
+// schedule a draw for the next minute
+function queueDraw() {
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = setTimeout(function() {
+ drawTimeout = undefined;
+ draw();
+ }, 60000 - (Date.now() % 60000));
+}
+
+
+function draw() {
+ var x = g.getWidth()/2;
+ var y = g.getHeight()/2;
+ g.reset();
+ var date = new Date();
+ var timeStr = require("locale").time(date,1);
+ var dateStr = require("locale").date(date).toUpperCase();
+ // draw time
+ g.setFontAlign(0,0).setFont("BuildingTypeface");
+ g.clearRect(0, 24, g.getWidth(), y+35); // clear the background
+ g.drawString(timeStr,x,y);
+ // draw date
+ y += 60;
+ g.setFontAlign(0,0).setFont("6x8",2);
+ g.clearRect(0,y-8,g.getWidth(),y+8); // clear the background
+ g.drawString(dateStr,x,y);
+ // queue draw in one minute
+ queueDraw();
+}
+
+// Clear the screen once, at startup
+g.clear();
+// draw immediately at first, queue update
+draw();
+// Stop updates when LCD is off, restart when on
+Bangle.on('lcdPower',on=>{
+ if (on) {
+ draw(); // draw immediately, queue redraw
+ } else { // stop draw timer
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = undefined;
+ }
+});
+// Show launcher when middle button pressed
+Bangle.setUI("clock");
+// Load widgets
+Bangle.loadWidgets();
+Bangle.drawWidgets();
diff --git a/apps/deko/app.png b/apps/deko/app.png
new file mode 100644
index 000000000..6f11e7019
Binary files /dev/null and b/apps/deko/app.png differ
diff --git a/apps/deko/metadata.json b/apps/deko/metadata.json
new file mode 100644
index 000000000..9bdd15429
--- /dev/null
+++ b/apps/deko/metadata.json
@@ -0,0 +1,16 @@
+{
+ "id": "deko",
+ "name": "Deko Clock",
+ "version": "0.01",
+ "description": "Clock with Art Deko font",
+ "readme": "README.md",
+ "icon": "app.png",
+ "screenshots": [{"url":"screenshot.png"}],
+ "type": "clock",
+ "tags": "clock",
+ "supports": ["BANGLEJS","BANGLEJS2"],
+ "storage": [
+ {"name":"deko.app.js","url":"app.js"},
+ {"name":"deko.img","url":"app-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/deko/screenshot.png b/apps/deko/screenshot.png
new file mode 100644
index 000000000..91ce2ea38
Binary files /dev/null and b/apps/deko/screenshot.png differ
diff --git a/apps/demoapp/metadata.json b/apps/demoapp/metadata.json
index df6554ef5..2fb30f718 100644
--- a/apps/demoapp/metadata.json
+++ b/apps/demoapp/metadata.json
@@ -12,6 +12,5 @@
"storage": [
{"name":"demoapp.app.js","url":"app.js"},
{"name":"demoapp.img","url":"app-icon.js","evaluate":true}
- ],
- "sortorder": -9
+ ]
}
diff --git a/apps/distortclk/ChangeLog b/apps/distortclk/ChangeLog
new file mode 100644
index 000000000..4c7291526
--- /dev/null
+++ b/apps/distortclk/ChangeLog
@@ -0,0 +1,2 @@
+0.01: New face!
+0.02: Improved clock
diff --git a/apps/distortclk/README.md b/apps/distortclk/README.md
new file mode 100644
index 000000000..8c7c433c1
--- /dev/null
+++ b/apps/distortclk/README.md
@@ -0,0 +1,17 @@
+# Distort Watchface
+Was playing around with custom fonts and made something with it
+Made for Bangle.js 2
+
+
+
+## Features
+
+Has a dark mode
+
+## Requests
+
+If you have any issues or would like to suggest a feature, click here to send a message -> [here](https://github.com/elykittytee/BangleApps/issues/new?title=Poketch%20Clock%20Bug).
+
+## Creator
+
+Eleanor Tayam
diff --git a/apps/distortclk/app-icon.js b/apps/distortclk/app-icon.js
new file mode 100644
index 000000000..c375de96e
--- /dev/null
+++ b/apps/distortclk/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("j0ewkBiIAxHIQMJiBJEIxAaCAIQfHDgIUFDwwNCHYgVFiAVBHYgIDEghKCCIQGCFYoaDAYgORGIJ2DBwYIBHgQOPgAOIPIYOGAgQOFFgh7DHZQeDBwhoFQgh3JEAgOFFoqkHYRzgOfx4bCJ4gNGSIaJEABA7EAGA"))
diff --git a/apps/distortclk/app.js b/apps/distortclk/app.js
new file mode 100644
index 000000000..a9fdd1ef2
--- /dev/null
+++ b/apps/distortclk/app.js
@@ -0,0 +1,65 @@
+Graphics.prototype.setFontSixCaps = function(scale) {
+ // Actual height 60 (59 - 0)
+ this.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADMzMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADMzMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0VniarM3u4AAAAAAAAAAAAAAAAAAAAAAAAAAABEZoiqzN3u////////8AAAAAAAAAAAAAAAAAAAJFZ4iqzN3u////////////////8AAAAAAAAAAARGZ4mrzd7v////////////////////////8AAAAAAAqszd7v////////////////////////////7u3MoAAAAAAA//////////////////////////7t3MqohmRCAAAAAAAAAA//////////////////7t3MqohmRAAAAAAAAAAAAAAAAAAA/////////+7dzKqYdlQwAAAAAAAAAAAAAAAAAAAAAAAAAA/+7dzKqIZkQQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZkQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWazMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMqVAAAAAAAAAa7//////////////////////////////////+oQAAAAAAC/////////////////////////////////////+wAAAAAAf//////////////////////////////////////3AAAAAA3///7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u///9AAAAAA///GREREREREREREREREREREREREREREREREbP//AAAAAA//8wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///AAAAAA///GREREREREREREREREREREREREREREREREbP//AAAAAA3///7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u///9AAAAAAf//////////////////////////////////////3AAAAAAC/////////////////////////////////////+wAAAAAAAa7//////////////////////////////////+oQAAAAAAAAWazMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMqVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAqqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACqoAAAAAAA//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/8AAAAAAA//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/8AAAAAAA//3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3/8AAAAAAA//////////////////////////////////////8AAAAAAA//////////////////////////////////////8AAAAAAA//////////////////////////////////////8AAAAAAA3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADu4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAes3d3d3d0AAAAAAAAAAAAAAAAAAAAAAAADVorAAAAAAAA8////////8AAAAAAAAAAAAAAAAAAAAlaKze///wAAAAAAHf////////8AAAAAAAAAAAAAAAFGis3v///////wAAAAAAn/////////8AAAAAAAAAAARom83v///////////wAAAAAA7//+3d3d3d0AAAAAAEV5rN7//////////////v/wAAAAAA//xTAAAAAAAAA1eKze//////////////7cqXVP/wAAAAAA//UAAAAAFGis3v/////////////+3Kl2QAAAAP/wAAAAAA//6XZoq83v/////////////+26hkEAAAAAAAAP/wAAAAAA3//////////////////tyoZSAAAAAAAAAAAAAP/wAAAAAAb//////////////tuoZAAAAAAAAAAAAAAAAAAP/wAAAAAACv/////////tuXUwAAAAAAAAAAAAAAAAAAAAAP/wAAAAAAAI3////9yoZAAAAAAAAAAAAAAAAAAAAAAAAAAP/wAAAAAAAAJ4qodRAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqgAAAAAAAAWazMzMzMzMzAAAAAAAAAAAAAzMzMzMzMzMqVAAAAAAAAAa7//////////wAAAAAAAAAAAA///////////+oQAAAAAADP///////////wAAAAAAAAAAAA/////////////AAAAAAAf////////////wAAAAiIgAAAAA/////////////3AAAAAA3///7u7u7u7u7gAAAA//8AAAAA7u7u7u7u7u///9AAAAAA//11RERERERERAAAAA//8AAAAAREREREREREV9//AAAAAA//QAAAAAAAAAAAAAAB//8QAAAAAAAAAAAAAAAE//AAAAAA//11RERERERERERERa//+lREREREREREREREV9//AAAAAA3///7u7u7u7u7u7u7/////7u7u7u7u7u7u7u///9AAAAAAf//////////////////////////////////////3AAAAAAC/////////////////+q//////////////////+wAAAAAAAa7///////////////0i3////////////////+oQAAAAAAAAWazMzMzMzMzMzMyoIAKKzMzMzMzMzMzMzMqVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkRGZniIqqoAAAAAAAAAAAAAAAAAAAAAAkRGZniIqqrMzd3u7v//////8AAAAAAAAAAAAAAAqqrMzd3u7v////////////////////8AAAAAAAAAAAAAAA//////////////////////////////8AAAAAAAAAAAAAAA//////////////////////////////8AAAAAAAAAAAAAAA///////////////+7t3czKqpiHZm//8AAAAAAAAAAAAAAA//7u3dzMuqqIhmZUQwAAAAAAAAAA//8AAAAAAAAAAAAAAAZmREAAAAAAAAAARERERERERERERE//9EREREREQAAAAAAAAAAAAAAAAAAAAA7u7u7u7u7u7u7u///u7u7u7u4AAAAAAAAAAAAAAAAAAAAA////////////////////////8AAAAAAAAAAAAAAAAAAAAA////////////////////////8AAAAAAAAAAAAAAAAAAAAA////////////////////////8AAAAAAAAAAAAAAAAAAAAAzMzMzMzMzMzMzMzMzMzMzMzMwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARERERERERERERERERERAAABEREREREREREREAAAAAAAAAA7u7u7u7u7u7u7u7u7u7gAADu7u7u7u7u7u7u2TAAAAAAAA///////////////////wAAD//////////////+YAAAAAAA///////////////////wAAD///////////////4wAAAAAA///////////////////wAAD///////////////+gAAAAAA//zMzMzMzMzMzMzM3//AAADMzMzMzMzMzMzM7//wAAAAAA//AAAAAAAAAAAAAG//cAAAAAAAAAAAAAAAAAKf/wAAAAAA//AAAAAAAAAAAAAN//UAAAAAAAAAAAAAAAAAB//wAAAAAA//AAAAAAAAAAAAAP//6qqqqqqqqqqqqqqqqqv//wAAAAAA//AAAAAAAAAAAAAP///////////////////////AAAAAAA//AAAAAAAAAAAAAN//////////////////////9AAAAAAA//AAAAAAAAAAAAAF7/////////////////////gAAAAAAA//AAAAAAAAAAAAAAWu//////////////////7GAAAAAAAAZmAAAAAAAAAAAAAAADZmZmZmZmZmZmZmZmZmQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADREREREREREREREREREREREREREREREREMAAAAAAAAAADne7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7ZMAAAAAAABd////////////////////////////////////1QAAAAAALv/////////////////////////////////////iAAAAAAr//////////////////////////////////////6AAAAAA///szMzMzMzMzMzMzP//zMzMzMzMzMzMzMzM3///AAAAAA//ogAAAAAAAAAAAAC//5AAAAAAAAAAAAAAAACf//AAAAAA//cAAAAAAAAAAAAAD//zAAAAAAAAAAAAAAAABf//AAAAAA//+6qqqqqqqqAAAAD//8qqqqqqqqqqqqqqqqrv//AAAAAAz///////////AAAAD//////////////////////8AAAAAAT///////////AAAADv/////////////////////0AAAAAACP//////////AAAAB/////////////////////+AAAAAAAAGzv////////AAAAAGzv/////////////////sYAAAAAAAAABGZmZmZmZmAAAAAABGZmZmZmZmZmZmZmZmZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZmAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQAAAAAAA//AAAAAAAAAAAAAAAAAAAAAAAAACRGZ4iqrM3e4AAAAAAA//AAAAAAAAAAAAAAACRGZ4iqrM3e7v////////8AAAAAAA//AAAAACRGZ4iqrM3e7v//////////////////8AAAAAAA//iqrM3e7v////////////////////////////8AAAAAAA//////////////////////////////////7u3cwAAAAAAA///////////////////////+7t3MyqmIZmRCAAAAAAAAAA/////////////u3dzLqoiGZUQgAAAAAAAAAAAAAAAAAAAA//7t3cyqqIhmVEEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZlRAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWazMzMzMqphjAAAAAAAAAARniqrMzMzMzMqVAAAAAAAAAa7//////////+yWEAAAAVi97////////////+oQAAAAAAC///////////////2VAGrf////////////////+wAAAAAAf////////////////+vP///////////////////3AAAAAA3///7u7u7u7/////////////////7u7u7u7u///9AAAAAA///GRERERERmis3////////typhmREREREREbP//AAAAAA//8wAAAAAAAAAAJ8/////8hgAAAAAAAAAAAAA///AAAAAA///GRERERERWis3////////typhmREREREREbP//AAAAAA3///7u7u7u7/////////////////7u7u7u7u///9AAAAAAf////////////////+vP///////////////////3AAAAAAC///////////////2VAGrf////////////////+wAAAAAAAa7//////////+yWIAAAAVi97////////////+oQAAAAAAAAWazMzMzMqphjAAAAAAAAAARniqrMzMzMzMqVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1ZmZmZmZmZmZmZmZmQgAAAAAAZmZmZmZmYwAAAAAAAAAFvv////////////////7rUAAAAA/////////rUAAAAAAAB+////////////////////9gAAAA//////////9wAAAAAAP//////////////////////gAAAA///////////zAAAAAAv//////////////////////wAAAA///////////7AAAAAA///7qqqqqqqqqqqqqqqq3//wAAAAqqqqqqqqrP//AAAAAA//9wAAAAAAAAAAAAAAAAT//wAAAAAAAAAAAAAH//AAAAAA//9wAAAAAAAAAAAAAAAAn//AAAAAAAAAAAAAAZ//AAAAAA///8zMzMzMzMzMzMzMzM///MzMzMzMzMzMzMzf//AAAAAAv//////////////////////////////////////7AAAAAAP//////////////////////////////////////zAAAAAABu////////////////////////////////////5gAAAAAAAEre7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7aQAAAAAAAAAAUREREREREREREREREREREREREREREREREQQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMzMwAAAAAAAAAAAzMzAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//8AAAAAAAAAAA///wAAAAAAAAAAAAAAAAAAAAAAAAAAAP//8AAAAAAAAAAA///wAAAAAAAAAAAAAAAAAAAAAAAAAAAP//8AAAAAAAAAAA///wAAAAAAAAAAAAAAAAAAAAAAAAAAAP//8AAAAAAAAAAA///wAAAAAAAAAAAAAAAAAAAAAAAAAAAMzMwAAAAAAAAAAAzMzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="), 46, atob("CAwODQ4PDw8QDA8QCA=="), 69+(scale<<8)+(4<<16));
+ return this;
+};
+
+const offset = 25;
+const width = g.getWidth();
+const height = g.getHeight();
+
+var drawTimeout;
+var fgTime = 0xf800;
+var bgTime = 0x3333ff;
+var dayDate = 0x000;
+
+function queueDraw() {
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = setTimeout(function() {
+ drawTimeout = undefined;
+ draw();
+ }, 60000 - (Date.now() % 60000));
+}
+
+function time() {
+ require("Font4x5").add(Graphics);
+ var d = new Date();
+ var day = d.getDate();
+ var time = require("locale").time(d,1);
+ var date = require("locale").date(d);
+ var mo = require("date_utils").month(d.getMonth()+1,0);
+
+ g.setFontAlign(0,0);
+ g.setFontSixCaps(2).setColor(fgTime).drawString(time, width/2, height/2+10);
+
+ g.setFont("4x5",2);
+ g.setFontAlign(0,0);
+ g.setColor(dayDate).drawString(mo,width-55, height-16);
+ g.drawString(day,width-10, height-16);
+}
+
+function draw() {
+ g.setColor(bgTime).fillRect(0,40,width,height-offset);
+ time();
+ queueDraw();
+}
+
+//program start
+g.clear(); // Clear the screen once, at startup
+
+if (g.theme.dark==true){
+ dayDate = 0xffff;
+}
+else {
+ dayDate=0x000;
+}
+
+draw(); // draw immediately at first
+
+
+
+// Show launcher when middle button pressed
+Bangle.setUI("clock");
+// Load widgets
+Bangle.loadWidgets();
+Bangle.drawWidgets();
diff --git a/apps/distortclk/app.png b/apps/distortclk/app.png
new file mode 100644
index 000000000..b82a0913e
Binary files /dev/null and b/apps/distortclk/app.png differ
diff --git a/apps/distortclk/metadata.json b/apps/distortclk/metadata.json
new file mode 100644
index 000000000..125dac590
--- /dev/null
+++ b/apps/distortclk/metadata.json
@@ -0,0 +1,17 @@
+{
+ "id": "distortclk",
+ "name": "Distort Clock",
+ "shortName":"Distort Clock",
+ "version": "0.02",
+ "description": "A clockface",
+ "icon": "app.png",
+ "type": "clock",
+ "tags": "clock",
+ "supports": ["BANGLEJS2"],
+ "allow_emulator": true,
+ "readme":"README.md",
+ "storage": [
+ {"name":"distortclk.app.js","url":"app.js"},
+ {"name":"distortclk.img","url":"app-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/distortclk/screenshot.png b/apps/distortclk/screenshot.png
new file mode 100644
index 000000000..3207b4e1e
Binary files /dev/null and b/apps/distortclk/screenshot.png differ
diff --git a/apps/dotmatrixclock/ChangeLog b/apps/dotmatrixclock/ChangeLog
index 7ab9e14a9..12edf33a3 100644
--- a/apps/dotmatrixclock/ChangeLog
+++ b/apps/dotmatrixclock/ChangeLog
@@ -1 +1,2 @@
0.01: Create dotmatrix clock app
+0.02: Added adjustment for Bangle.js magnetometer heading fix
diff --git a/apps/dotmatrixclock/app.js b/apps/dotmatrixclock/app.js
index ba34d4885..493a3c43f 100644
--- a/apps/dotmatrixclock/app.js
+++ b/apps/dotmatrixclock/app.js
@@ -186,7 +186,7 @@ function drawCompass(lastHeading) {
'NW'
];
const cps = Bangle.getCompass();
- let angle = cps.heading;
+ let angle = 360-cps.heading;
let heading = angle?
directions[Math.round(((angle %= 360) < 0 ? angle + 360 : angle) / 45) % 8]:
"-- ";
@@ -351,4 +351,4 @@ Bangle.on('faceUp', (up) => {
setSensors(1);
resetDisplayTimeout();
}
-});
\ No newline at end of file
+});
diff --git a/apps/dotmatrixclock/metadata.json b/apps/dotmatrixclock/metadata.json
index 3425dc1b2..fdfb5271f 100644
--- a/apps/dotmatrixclock/metadata.json
+++ b/apps/dotmatrixclock/metadata.json
@@ -1,7 +1,7 @@
{
"id": "dotmatrixclock",
"name": "Dotmatrix Clock",
- "version": "0.01",
+ "version": "0.02",
"description": "A clear white-on-blue dotmatrix simulated clock",
"icon": "dotmatrixclock.png",
"type": "clock",
diff --git a/apps/dragboard/ChangeLog b/apps/dragboard/ChangeLog
index 48a1ffb03..265094e87 100644
--- a/apps/dragboard/ChangeLog
+++ b/apps/dragboard/ChangeLog
@@ -3,3 +3,4 @@
0.03: Made the code shorter and somewhat more readable by writing some functions. Also made it work as a library where it returns the text once finished. The keyboard is now made to exit correctly when the 'back' event is called. The keyboard now uses theme colors correctly, although it still looks best with dark theme. The numbers row is now solidly green - except for highlights.
0.04: Now displays the opened text string at launch.
0.05: Now scrolls text when string gets longer than screen width.
+0.06: The code is now more reliable and the input snappier. Widgets will be drawn if present.
diff --git a/apps/dragboard/lib.js b/apps/dragboard/lib.js
index b9b19f982..220f075d7 100644
--- a/apps/dragboard/lib.js
+++ b/apps/dragboard/lib.js
@@ -1,12 +1,9 @@
-//Keep banglejs screen on for 100 sec at 0.55 power level for development purposes
-//Bangle.setLCDTimeout(30);
-//Bangle.setLCDPower(1);
-
exports.input = function(options) {
options = options||{};
var text = options.text;
if ("string"!=typeof text) text="";
-
+
+ var R = Bangle.appRect;
var BGCOLOR = g.theme.bg;
var HLCOLOR = g.theme.fg;
var ABCCOLOR = g.toColor(1,0,0);//'#FF0000';
@@ -17,35 +14,38 @@ exports.input = function(options) {
var SMALLFONTWIDTH = parseInt(SMALLFONT.charAt(0)*parseInt(SMALLFONT.charAt(-1)));
var ABC = 'abcdefghijklmnopqrstuvwxyz'.toUpperCase();
- var ABCPADDING = (g.getWidth()-6*ABC.length)/2;
+ var ABCPADDING = ((R.x+R.w)-6*ABC.length)/2;
var NUM = ' 1234567890!?,.- ';
var NUMHIDDEN = ' 1234567890!?,.- ';
- var NUMPADDING = (g.getWidth()-6*NUM.length)/2;
+ var NUMPADDING = ((R.x+R.w)-6*NUM.length)/2;
var rectHeight = 40;
-
var delSpaceLast;
function drawAbcRow() {
g.clear();
+ try { // Draw widgets if they are present in the current app.
+ if (WIDGETS) Bangle.drawWidgets();
+ } catch (_) {}
g.setFont(SMALLFONT);
g.setColor(ABCCOLOR);
- g.drawString(ABC, ABCPADDING, g.getHeight()/2);
- g.fillRect(0, g.getHeight()-26, g.getWidth(), g.getHeight());
+ g.setFontAlign(-1, -1, 0);
+ g.drawString(ABC, ABCPADDING, (R.y+R.h)/2);
+ g.fillRect(0, (R.y+R.h)-26, (R.x+R.w), (R.y+R.h));
}
function drawNumRow() {
g.setFont(SMALLFONT);
g.setColor(NUMCOLOR);
- g.drawString(NUM, NUMPADDING, g.getHeight()/4);
+ g.setFontAlign(-1, -1, 0);
+ g.drawString(NUM, NUMPADDING, (R.y+R.h)/4);
- g.fillRect(NUMPADDING, g.getHeight()-rectHeight*4/3, g.getWidth()-NUMPADDING, g.getHeight()-rectHeight*2/3);
+ g.fillRect(NUMPADDING, (R.y+R.h)-rectHeight*4/3, (R.x+R.w)-NUMPADDING, (R.y+R.h)-rectHeight*2/3);
}
function updateTopString() {
- "ram"
g.setColor(BGCOLOR);
g.fillRect(0,4+20,176,13+20);
g.setColor(0.2,0,0);
@@ -54,13 +54,10 @@ exports.input = function(options) {
g.setColor(0.7,0,0);
g.fillRect(rectLen+5,4+20,rectLen+10,13+20);
g.setColor(1,1,1);
+ g.setFontAlign(-1, -1, 0);
g.drawString(text.length<=27? text.substr(-27, 27) : '<- '+text.substr(-24,24), 5, 5+20);
}
- drawAbcRow();
- drawNumRow();
- updateTopString();
-
var abcHL;
var abcHLPrev = -10;
var numHL;
@@ -68,194 +65,182 @@ exports.input = function(options) {
var type = '';
var typePrev = '';
var largeCharOffset = 6;
-
+
function resetChars(char, HLPrev, typePadding, heightDivisor, rowColor) {
- "ram"
+ "ram";
// Small character in list
g.setColor(rowColor);
g.setFont(SMALLFONT);
- g.drawString(char, typePadding + HLPrev*6, g.getHeight()/heightDivisor);
+ g.setFontAlign(-1, -1, 0);
+ g.drawString(char, typePadding + HLPrev*6, (R.y+R.h)/heightDivisor);
// Large character
g.setColor(BGCOLOR);
- g.fillRect(0,g.getHeight()/3,176,g.getHeight()/3+24);
- //g.drawString(charSet.charAt(HLPrev), typePadding + HLPrev*6 -largeCharOffset, g.getHeight()/3);; //Old implementation where I find the shape and place of letter to remove instead of just a rectangle.
+ g.fillRect(0,(R.y+R.h)/3,176,(R.y+R.h)/3+24);
+ //g.drawString(charSet.charAt(HLPrev), typePadding + HLPrev*6 -largeCharOffset, (R.y+R.h)/3);; //Old implementation where I find the shape and place of letter to remove instead of just a rectangle.
// mark in the list
}
function showChars(char, HL, typePadding, heightDivisor) {
- "ram"
+ "ram";
// mark in the list
g.setColor(HLCOLOR);
g.setFont(SMALLFONT);
- if (char != 'del' && char != 'space') g.drawString(char, typePadding + HL*6, g.getHeight()/heightDivisor);
+ g.setFontAlign(-1, -1, 0);
+ if (char != 'del' && char != 'space') g.drawString(char, typePadding + HL*6, (R.y+R.h)/heightDivisor);
// show new large character
g.setFont(BIGFONT);
- g.drawString(char, typePadding + HL*6 -largeCharOffset, g.getHeight()/3);
+ g.drawString(char, typePadding + HL*6 -largeCharOffset, (R.y+R.h)/3);
g.setFont(SMALLFONT);
}
-
+
+ function initDraw() {
+ //var R = Bangle.appRect; // To make sure it's properly updated. Not sure if this is needed.
+ drawAbcRow();
+ drawNumRow();
+ updateTopString();
+ }
+ initDraw();
+ //setTimeout(initDraw, 0); // So Bangle.appRect reads the correct environment. It would draw off to the side sometimes otherwise.
+
function changeCase(abcHL) {
g.setColor(BGCOLOR);
- g.drawString(ABC, ABCPADDING, g.getHeight()/2);
+ g.setFontAlign(-1, -1, 0);
+ g.drawString(ABC, ABCPADDING, (R.y+R.h)/2);
if (ABC.charAt(abcHL) == ABC.charAt(abcHL).toUpperCase()) ABC = ABC.toLowerCase();
else ABC = ABC.toUpperCase();
g.setColor(ABCCOLOR);
- g.drawString(ABC, ABCPADDING, g.getHeight()/2);
+ g.drawString(ABC, ABCPADDING, (R.y+R.h)/2);
}
return new Promise((resolve,reject) => {
- // Interpret touch input
+ // Interpret touch input
Bangle.setUI({
- mode: 'custom',
- back: ()=>{
- Bangle.setUI();
- g.clearRect(Bangle.appRect);
- resolve(text);
- },
- drag: function(event) {
+ mode: 'custom',
+ back: ()=>{
+ Bangle.setUI();
+ g.clearRect(Bangle.appRect);
+ resolve(text);
+ },
+ drag: function(event) {
+ "ram";
+ // ABCDEFGHIJKLMNOPQRSTUVWXYZ
+ // Choose character by draging along red rectangle at bottom of screen
+ if (event.y >= ( (R.y+R.h) - 12 )) {
+ // Translate x-position to character
+ if (event.x < ABCPADDING) { abcHL = 0; }
+ else if (event.x >= 176-ABCPADDING) { abcHL = 25; }
+ else { abcHL = Math.floor((event.x-ABCPADDING)/6); }
- // ABCDEFGHIJKLMNOPQRSTUVWXYZ
- // Choose character by draging along red rectangle at bottom of screen
- if (event.y >= ( g.getHeight() - 12 )) {
- // Translate x-position to character
- if (event.x < ABCPADDING) { abcHL = 0; }
- else if (event.x >= 176-ABCPADDING) { abcHL = 25; }
- else { abcHL = Math.floor((event.x-ABCPADDING)/6); }
+ // Datastream for development purposes
+ //print(event.x, event.y, event.b, ABC.charAt(abcHL), ABC.charAt(abcHLPrev));
- // Datastream for development purposes
- //print(event.x, event.y, event.b, ABC.charAt(abcHL), ABC.charAt(abcHLPrev));
+ // Unmark previous character and mark the current one...
+ // Handling switching between letters and numbers/punctuation
+ if (typePrev != 'abc') resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
- // Unmark previous character and mark the current one...
- // Handling switching between letters and numbers/punctuation
- if (typePrev != 'abc') resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
-
- if (abcHL != abcHLPrev) {
- resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
- showChars(ABC.charAt(abcHL), abcHL, ABCPADDING, 2);
+ if (abcHL != abcHLPrev) {
+ resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
+ showChars(ABC.charAt(abcHL), abcHL, ABCPADDING, 2);
}
- // Print string at top of screen
- if (event.b == 0) {
- text = text + ABC.charAt(abcHL);
- updateTopString();
-
- // Autoswitching letter case
- if (ABC.charAt(abcHL) == ABC.charAt(abcHL).toUpperCase()) changeCase(abcHL);
- }
- // Update previous character to current one
- abcHLPrev = abcHL;
- typePrev = 'abc';
- }
-
-
-
-
-
-
-
-
-
- // 12345678901234567890
- // Choose number or puctuation by draging on green rectangle
- else if ((event.y < ( g.getHeight() - 12 )) && (event.y > ( g.getHeight() - 52 ))) {
- // Translate x-position to character
- if (event.x < NUMPADDING) { numHL = 0; }
- else if (event.x > 176-NUMPADDING) { numHL = NUM.length-1; }
- else { numHL = Math.floor((event.x-NUMPADDING)/6); }
-
- // Datastream for development purposes
- //print(event.x, event.y, event.b, NUM.charAt(numHL), NUM.charAt(numHLPrev));
-
- // Unmark previous character and mark the current one...
- // Handling switching between letters and numbers/punctuation
- if (typePrev != 'num') resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
-
- if (numHL != numHLPrev) {
- resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
- showChars(NUM.charAt(numHL), numHL, NUMPADDING, 4);
- }
- // Print string at top of screen
- if (event.b == 0) {
- g.setColor(HLCOLOR);
- // Backspace if releasing before list of numbers/punctuation
- if (event.x < NUMPADDING) {
- // show delete sign
- showChars('del', 0, g.getWidth()/2 +6 -27 , 4);
- delSpaceLast = 1;
- text = text.slice(0, -1);
- updateTopString();
- //print(text);
- }
- // Append space if releasing after list of numbers/punctuation
- else if (event.x > g.getWidth()-NUMPADDING) {
- //show space sign
- showChars('space', 0, g.getWidth()/2 +6 -6*3*5/2 , 4);
- delSpaceLast = 1;
- text = text + ' ';
- updateTopString();
- //print(text);
- }
- // Append selected number/punctuation
- else {
- text = text + NUMHIDDEN.charAt(numHL);
+ // Print string at top of screen
+ if (event.b == 0) {
+ text = text + ABC.charAt(abcHL);
updateTopString();
// Autoswitching letter case
- if ((text.charAt(text.length-1) == '.') || (text.charAt(text.length-1) == '!')) changeCase();
+ if (ABC.charAt(abcHL) == ABC.charAt(abcHL).toUpperCase()) changeCase(abcHL);
}
+ // Update previous character to current one
+ abcHLPrev = abcHL;
+ typePrev = 'abc';
}
- // Update previous character to current one
- numHLPrev = numHL;
- typePrev = 'num';
- }
-
-
-
-
-
-
-
-
- // Make a space or backspace by swiping right or left on screen above green rectangle
- else if (event.y > 20+4) {
- if (event.b == 0) {
- g.setColor(HLCOLOR);
- if (event.x < g.getWidth()/2) {
- resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
+
+ // 12345678901234567890
+ // Choose number or puctuation by draging on green rectangle
+ else if ((event.y < ( (R.y+R.h) - 12 )) && (event.y > ( (R.y+R.h) - 52 ))) {
+ // Translate x-position to character
+ if (event.x < NUMPADDING) { numHL = 0; }
+ else if (event.x > 176-NUMPADDING) { numHL = NUM.length-1; }
+ else { numHL = Math.floor((event.x-NUMPADDING)/6); }
+
+ // Datastream for development purposes
+ //print(event.x, event.y, event.b, NUM.charAt(numHL), NUM.charAt(numHLPrev));
+
+ // Unmark previous character and mark the current one...
+ // Handling switching between letters and numbers/punctuation
+ if (typePrev != 'num') resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
+
+ if (numHL != numHLPrev) {
resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
-
- // show delete sign
- showChars('del', 0, g.getWidth()/2 +6 -27 , 4);
- delSpaceLast = 1;
-
- // Backspace and draw string upper right corner
- text = text.slice(0, -1);
- updateTopString();
- if (text.length==0) changeCase(abcHL);
- //print(text, 'undid');
+ showChars(NUM.charAt(numHL), numHL, NUMPADDING, 4);
}
- else {
- resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
- resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
+ // Print string at top of screen
+ if (event.b == 0) {
+ g.setColor(HLCOLOR);
+ // Backspace if releasing before list of numbers/punctuation
+ if (event.x < NUMPADDING) {
+ // show delete sign
+ showChars('del', 0, (R.x+R.w)/2 +6 -27 , 4);
+ delSpaceLast = 1;
+ text = text.slice(0, -1);
+ updateTopString();
+ //print(text);
+ }
+ // Append space if releasing after list of numbers/punctuation
+ else if (event.x > (R.x+R.w)-NUMPADDING) {
+ //show space sign
+ showChars('space', 0, (R.x+R.w)/2 +6 -6*3*5/2 , 4);
+ delSpaceLast = 1;
+ text = text + ' ';
+ updateTopString();
+ //print(text);
+ }
+ // Append selected number/punctuation
+ else {
+ text = text + NUMHIDDEN.charAt(numHL);
+ updateTopString();
- //show space sign
- showChars('space', 0, g.getWidth()/2 +6 -6*3*5/2 , 4);
- delSpaceLast = 1;
+ // Autoswitching letter case
+ if ((text.charAt(text.length-1) == '.') || (text.charAt(text.length-1) == '!')) changeCase();
+ }
+ }
+ // Update previous character to current one
+ numHLPrev = numHL;
+ typePrev = 'num';
+ }
- // Append space and draw string upper right corner
- text = text + NUMHIDDEN.charAt(0);
- updateTopString();
- //print(text, 'made space');
+ // Make a space or backspace by swiping right or left on screen above green rectangle
+ else if (event.y > 20+4) {
+ if (event.b == 0) {
+ g.setColor(HLCOLOR);
+ if (event.x < (R.x+R.w)/2) {
+ resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
+ resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
+
+ // show delete sign
+ showChars('del', 0, (R.x+R.w)/2 +6 -27 , 4);
+ delSpaceLast = 1;
+
+ // Backspace and draw string upper right corner
+ text = text.slice(0, -1);
+ updateTopString();
+ if (text.length==0) changeCase(abcHL);
+ //print(text, 'undid');
+ }
+ else {
+ resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
+ resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
+
+ //show space sign
+ showChars('space', 0, (R.x+R.w)/2 +6 -6*3*5/2 , 4);
+ delSpaceLast = 1;
+
+ // Append space and draw string upper right corner
+ text = text + NUMHIDDEN.charAt(0);
+ updateTopString();
+ //print(text, 'made space');
+ }
}
}
}
- }
- });
-});
-/* return new Promise((resolve,reject) => {
- Bangle.setUI({mode:"custom", back:()=>{
- Bangle.setUI();
- g.clearRect(Bangle.appRect);
- Bangle.setUI();
- resolve(text);
- }});
- }); */
-
+ });
+ });
};
diff --git a/apps/dragboard/metadata.json b/apps/dragboard/metadata.json
index f9c73ddde..64b6dbe18 100644
--- a/apps/dragboard/metadata.json
+++ b/apps/dragboard/metadata.json
@@ -1,6 +1,6 @@
{ "id": "dragboard",
"name": "Dragboard",
- "version":"0.05",
+ "version":"0.06",
"description": "A library for text input via swiping keyboard",
"icon": "app.png",
"type":"textinput",
diff --git a/apps/drinkcounter/ChangeLog b/apps/drinkcounter/ChangeLog
new file mode 100644
index 000000000..d8d174c4c
--- /dev/null
+++ b/apps/drinkcounter/ChangeLog
@@ -0,0 +1,4 @@
+0.10: Initial release - still work in progress
+0.15: Added settings and calculations
+0.20: Added status saving
+0.25: Adopted for Bangle.js 1 - kind of
\ No newline at end of file
diff --git a/apps/drinkcounter/README.md b/apps/drinkcounter/README.md
new file mode 100644
index 000000000..5638ee066
--- /dev/null
+++ b/apps/drinkcounter/README.md
@@ -0,0 +1,15 @@
+# Drink Counter
+
+Counts drinks you had for science. Calculates BAC.
+
+## Usage
+
+Swipe left/right to select drink. Swipe up/down to add/remove drinks.
+
+## Important notes
+
+No warranty whatsoever. Use at your own risk. Calculations might be wrong. Do not drink and drive - even if BAC is low.
+
+## Creator
+
+Hank - contact at http://forum.espruino.com
diff --git a/apps/drinkcounter/app.js b/apps/drinkcounter/app.js
new file mode 100644
index 000000000..323d9fb41
--- /dev/null
+++ b/apps/drinkcounter/app.js
@@ -0,0 +1,291 @@
+g.reset().clear();
+Bangle.loadWidgets();
+Bangle.drawWidgets();
+require("Font8x16").add(Graphics);
+
+const BANGLEJS2 = process.env.HWVERSION == 2;
+const SETTINGSFILE = "drinkcounter.json";
+setting = require("Storage").readJSON("setting.json",1);
+E.setTimeZone(setting.timezone); // timezone = 1 for MEZ, = 2 for MESZ
+var _12hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"]||false;
+var ampm = "AM";
+let drag;
+
+var icoBeer = require("heatshrink").decompress(atob("lEoxH+AG2BAAoecEpAoWC4fXAAIGGAAowTDxAmJE4YGGE5QeJE5QHHE7owJE0pQKE7pQJE86fnE5QJSE5YUHBAIJQYxIpFAAvGBBAJIExYoGDgIACBBApFExonCDYoAOFSAnbFJYnE6vVDYYFHAwakQE4YaFAoQGJEIYoME7QoEE7ogFE/4neTBgntY84n/E+7HUE64mDE8IAFEw4nDTBifIE9gmId7gALE5IGCAooGDE6gASE8yaME7gmOFIgAREqIAhA=="));
+var icoCocktail = require("heatshrink").decompress(atob("lEoxH+AH4AJtgABEkgmiEiXGAAIllAAiXeEAPXAQQDCFBYmTEgYqDFBZNWAIZRME6IfBEAYuEE5J2UwIAaJ5QncFBB3DB4YGCACQnKTQgoXE5bIEE6qfKPAZRFA4MUABgmNPAonBCgQnPExgpFPIgoNEyBSF4wGBFBgmSABCjJTZwoXEzwoHE0AoFE0QnCFAQmhKAonjFAInCE0Qn/E/4n/E/4n/wInDFEAhBEwQoDFLYdCEwooEFTAjHAAwoYIYgAMPDglT"));
+var icoShot = require("heatshrink").decompress(atob("lEoxH+AH4A/AH4A/AH4AqwIAgE+HXADRPME8ZQM5AnSZBQkGAAYngEYonfJA5QQE8zGJFAYfKFBwmKE4iYIE7rpIeYgAJE5woEEpQKHTxhQIIpJaHJxgn/E8zGQZBAnQYxxQRFQYnlFgon5FCYmDE6LjHZRQmPE5AAOE/4njFCTGQKCwmRKAgATE54oWEyAqTDZY"));
+var icoReset = require("heatshrink").decompress(atob("j0egILI8ACBh4DC/4DBh4DCv8f4ED8EPwEPEQMAvEAnkB4EA+AKBCAM8DYOA8EB//HwED/wXBg/wnAOC+EAjkDDoMgg+AJoRFCEIIAB/kHgEB/l8FwP/DYIDBC4MD/ASBgYeCAAw"));
+var drawTimeout;
+var activeDrink = 0;
+var drinks = [0,0,0];
+const maxDrinks = 2; // 3 drinks
+var firstDrinkTime = null;
+var firstDrinkTimeTime = null;
+
+var confBeerSize;
+var confSex;
+var confWeight;
+var confWeightUnit;
+
+
+// Load Status ===============
+var drinkStatus = require("Storage").open("drinkcounter.status.json", "r");
+var test = drinkStatus.read(drinkStatus.getLength());
+if(test!== undefined) {
+ drinkStatus = JSON.parse(test);
+ //console.log("read status: " + test);
+ for (let i = 0; i <= maxDrinks; i++) {
+ drinks[i] = drinkStatus.drinks[i];
+ }
+ firstDrinkTime = Date.parse(drinkStatus.firstDrinkTime);
+ //console.log("read firstDrinkTime: " + firstDrinkTime);
+ if (firstDrinkTime) firstDrinkTimeTime = require("locale").time(new Date(firstDrinkTime), 1);
+ //console.log("read firstDrinkTimeTime: " + firstDrinkTimeTime);
+} else {
+ drinkStatus = {
+ drinks: [0,0,0]
+ };
+ //console.log("no status file - applying default");
+}
+// Load Status ===============
+
+
+var drinksAlcohol = [12,16,5.6]; // in gramm
+// Beer: 0.3L 12g - 0.5L 20g
+// Radler: 0.3L 6g - 0.5L 10g
+// Wine: 0.2L 16g
+// Jäger Shot: 0.02L 5.6g
+
+// sex: Women 60 - Men 70 (Percent)
+// Formula: Alcohol in g /(Body weight in kg x sex) – (0,15 x Hours) = bac per mille
+// Example: 5 Beer (0.3L=12g), 80KG, Male (70%), 5 hours
+// (5 * 12) / (80 / 100 * 70) - (0.15 * 5)
+
+function drawBac(){
+ if (firstDrinkTime) {
+ var sum_drinks = (drinks[0] * drinksAlcohol[0]) + (drinks[1] * drinksAlcohol[1]) + (drinks[2] * drinksAlcohol[2]);
+
+ if (confSex == "male") {
+ sex = 70;
+ } else {
+ sex = 60;
+ }
+ var weight = confWeight;
+
+ if (confWeightUnit == "US Pounds") {
+ weight = weight * 0.45359237;
+ }
+ var currentTime = new Date();
+ var time_diff = Math.floor(((currentTime - firstDrinkTime) % 86400000) / 3600000); // in hours!
+ //console.log("currentTime: " + currentTime)
+ //console.log("firstDrinkTime: " + firstDrinkTime)
+
+ //console.log("timediff: " + time_diff);
+ ebac = Math.round( ((sum_drinks) / (weight / 100 * sex) - (0.15 * time_diff) ) * 100) / 100;
+
+ //console.log("BAC: " + ebac + " weight: " + confWeight + " weightInKilo: " + weight + " Unit: " + confWeightUnit);
+ //console.log("sum_drinks: " + sum_drinks);
+ g.clearRect(0,34 + 20 + 8,176,34 + 20 + 20 + 8); //Clear
+ g.setFontAlign(0,0).setFont("8x16").setColor(g.theme.fg).drawString("BAC: " + ebac, 90, 74);
+ }
+}
+
+
+// Load settings
+function loadMySettings() {
+ // Helper function default setting
+ function def (value, def) {return value !== undefined ? value : def;}
+
+ var settings = require('Storage').readJSON(SETTINGSFILE, true) || {};
+ confBeerSize = def(settings.beerSize, "0.3L");
+ confSex = def(settings.sex, "male");
+ confWeight = def(settings.weight, 80);
+ confWeightUnit = def(settings.weightUnit, "Kilo");
+ //console.log("Read config - weight: " + confWeight);
+}
+
+
+function updateTime(){
+ var d = require("locale").time(new Date(), 1);
+
+ //console.log(d);
+ var time = d.split(":");
+ var hours = time[0];
+ var minutes = time[1];
+ if (_12hour){
+ //do 12 hour stuff
+ if (hours > 12) {
+ ampm = "PM";
+ hours = hours - 12;
+ if (hours < 10) hours = doublenum(hours);
+ } else {
+ ampm = "AM";
+ }
+ } else {
+ ampm = "";
+ }
+ g.setBgColor(g.theme.bg).clearRect(0,24,176,44); //Clear
+ g.setFontAlign(0,0); // center font
+ g.setBgColor(g.theme.bg).setColor(g.theme.fg);
+ g.setFont("8x16").drawString("Time: " + hours + ":" + minutes + " " + ampm,90,34);
+ queueDrawTime();
+}
+
+function queueDrawTime() {
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = setTimeout(function() {
+ drawTimeout = undefined;
+ updateTime();
+ }, 20000 - (Date.now() % 20000));
+}
+
+
+function updateDrinks(){
+ g.setBgColor(g.theme.bg).clearRect(0,145,176,176); //Clear
+ for (let i = 0; i <= maxDrinks; i++) {
+ if (i == activeDrink) {
+ g.setColor(g.theme.fg).fillRect((40 * (i + 1)) - 40 ,145,(40 * (i + 1)),176);
+ g.setColor(g.theme.bg);
+ } else {
+ g.setColor(g.theme.fg);
+ }
+ g.setFont("Vector",20).drawString(drinks[i], (40 * (i + 1)) - 20, 160);
+ g.setColor(g.theme.fg);
+ drinkStatus.drinks[i] = drinks[i];
+ }
+
+ g.setBgColor(g.theme.bg).setColor(g.theme.fg);
+ if (BANGLEJS2) {
+ g.drawImage(icoReset,145,145);
+ }
+
+ drinkStatus.firstDrinkTime = firstDrinkTime;
+ settings_file = require("Storage").open("drinkcounter.status.json", "w");
+ settings_file.write(JSON.stringify(drinkStatus));
+
+ drawBac();
+}
+
+function updateFirstDrinkTime(){
+ if (firstDrinkTime){
+ g.setFont("8x16");
+ g.setFontAlign(0,0).drawString("1st drink @ " + firstDrinkTimeTime, 90, 34 + 20 );
+ }
+}
+
+function addDrink(){
+ if (!firstDrinkTime){
+ firstDrinkTime = new Date();
+ firstDrinkTimeTime = require("locale").time(new Date(), 1);
+ //console.log("init drinking! " + firstDrinkTime);
+ }
+ drinks[activeDrink] = drinks[activeDrink] + 1;
+ updateFirstDrinkTime();
+ updateDrinks();
+}
+
+function removeDrink(){
+ if (drinks[activeDrink] > 0) drinks[activeDrink] = drinks[activeDrink] - 1;
+ updateDrinks();
+
+ if ((!BANGLEJS2) && (drinks[0] == 0) && (drinks[1] == 0) && (drinks[2] == 0)) {
+ resetDrinksFn()
+ }
+}
+
+function previousDrink(){
+ if (activeDrink > 0) activeDrink = activeDrink - 1;
+ updateDrinks();
+}
+
+function nextDrink(){
+ if (activeDrink < maxDrinks) activeDrink = activeDrink + 1;
+ updateDrinks();
+}
+
+function showDrinks() {
+ g.setBgColor(g.theme.bg);
+ g.drawImage(icoBeer,0,100);
+ g.drawImage(icoCocktail,40,100);
+ g.drawImage(icoShot,80,100);
+}
+
+function resetDrinksFn() {
+ g.clearRect(0,34,176,176); //Clear
+ resetDrinks = E.showPrompt("Reset drinks?", {
+ title: "Confirm",
+ buttons: { Yes: true, No: false },
+ });
+ resetDrinks.then((confirm) => {
+ if (confirm) {
+ for (let i = 0; i <= maxDrinks; i++) {
+ drinks[i] = 0;
+ }
+ //console.log("reset to default");
+ }
+ //console.log("reset " + confirm);
+ firstDrinkTime = null;
+ showDrinks();
+ updateDrinks();
+ updateTime();
+ updateFirstDrinkTime();
+ });
+}
+
+
+function initDragEvents() {
+
+if (BANGLEJS2) {
+ Bangle.on("drag", e => {
+ if (!drag) { // start dragging
+ drag = {x: e.x, y: e.y};
+ } else if (!e.b) { // released
+ const dx = e.x-drag.x, dy = e.y-drag.y;
+ drag = null;
+ if (Math.abs(dx)>Math.abs(dy)+10) {
+ // horizontal
+ if (dx < dy) {
+ //console.log("left " + dx + " " + dy);
+ previousDrink();
+ } else {
+ //console.log("right " + dx + " " + dy);
+ nextDrink();
+ }
+ } else if (Math.abs(dy)>Math.abs(dx)+10) {
+ // vertical
+ if (dx < dy) {
+ //console.log("down " + dx + " " + dy);
+ removeDrink();
+ } else {
+ //console.log("up " + dx + " " + dy);
+ addDrink();
+ }
+ } else {
+ //console.log("tap " + e.x + " " + e.y);
+ if (e.x > 145 && e.y > 145) {
+ resetDrinksFn();
+ }
+ }
+ }
+ });
+ } else {
+ setWatch(addDrink, BTN1, { repeat: true, debounce:50 });
+ setWatch(removeDrink, BTN3, { repeat: true, debounce:50 });
+ setWatch(previousDrink, BTN4, { repeat: true, debounce:50 });
+ setWatch(nextDrink, BTN5, { repeat: true, debounce:50 });
+ }
+}
+
+
+loadMySettings();
+showDrinks();
+
+
+if (drawTimeout) clearTimeout(drawTimeout);
+drawTimeout = undefined;
+updateTime();
+queueDrawTime();
+initDragEvents();
+updateDrinks();
+updateFirstDrinkTime();
+
diff --git a/apps/drinkcounter/drinkcounter-icon.js b/apps/drinkcounter/drinkcounter-icon.js
new file mode 100644
index 000000000..e7b95f9ef
--- /dev/null
+++ b/apps/drinkcounter/drinkcounter-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AAWBAAomkFpAweD4fXAAIGGAAo4bExAuJF4YGGF6QmJF5QHHF8o4JF1pgSF7pgRF96/vF5QJSF6YcHBAIJQdyIxFAAvGBBAJIFyYwGEgIACBBAxFFyovCEYoAOGTAvbGKYvE6vVEYYFHAwbEYF4YiFAoQGJFIYwUF7QwEF8ooFF/4v2XBgv1d94v/F/7vsF64uDF9IAFFx4vDXBi/IF+guQR6wvCFSIvOAwQFFAwYvcACQvuXSgvcFywxEACItZAH4A/AH4AlA=="))
diff --git a/apps/drinkcounter/drinkcounter.png b/apps/drinkcounter/drinkcounter.png
new file mode 100644
index 000000000..91a0cd4ad
Binary files /dev/null and b/apps/drinkcounter/drinkcounter.png differ
diff --git a/apps/drinkcounter/metadata.json b/apps/drinkcounter/metadata.json
new file mode 100644
index 000000000..2b8d7fe71
--- /dev/null
+++ b/apps/drinkcounter/metadata.json
@@ -0,0 +1,24 @@
+{
+ "id": "drinkcounter",
+ "name": "Drink Counter",
+ "shortName": "Drink Counter",
+ "version": "0.25",
+ "description": "Counts drinks you had for science. Calculates blood alcohol content (BAC)",
+ "allow_emulator":true,
+ "icon": "drinkcounter.png",
+ "type": "app",
+ "tags": "health",
+ "screenshots": [{"url":"screenshot_drnkcnt.png"}],
+ "supports": ["BANGLEJS", "BANGLEJS2"],
+ "readme": "README.md",
+ "storage": [
+ {"name":"drinkcounter.app.js","url":"app.js"},
+ {"name":"drinkcounter.img","url":"drinkcounter-icon.js","evaluate":true},
+ {"name":"drinkcounter.settings.js","url":"settings.js"}
+ ],
+ "data": [
+ {"name":"drinkcounter.settings.json"},
+ {"name":"drinkcounter.json"},
+ {"name":"drinkcounter.status.json"}
+ ]
+}
\ No newline at end of file
diff --git a/apps/drinkcounter/screenshot_drnkcnt.png b/apps/drinkcounter/screenshot_drnkcnt.png
new file mode 100644
index 000000000..7547eb63f
Binary files /dev/null and b/apps/drinkcounter/screenshot_drnkcnt.png differ
diff --git a/apps/drinkcounter/settings.js b/apps/drinkcounter/settings.js
new file mode 100644
index 000000000..336229b73
--- /dev/null
+++ b/apps/drinkcounter/settings.js
@@ -0,0 +1,58 @@
+(function(back) {
+ var FILE = "drinkcounter.json";
+ var settings = Object.assign({
+ secondsOnUnlock: false,
+ }, require('Storage').readJSON(FILE, true) || {});
+
+ function writeSettings() {
+ require('Storage').writeJSON(FILE, settings);
+ }
+
+ // Helper method which uses int-based menu item for set of string values
+ function stringItems(startvalue, writer, values) {
+ return {
+ value: (startvalue === undefined ? 0 : values.indexOf(startvalue)),
+ format: v => values[v],
+ min: 0,
+ max: values.length - 1,
+ wrap: true,
+ step: 1,
+ onchange: v => {
+ writer(values[v]);
+ writeSettings();
+ }
+ };
+ }
+
+ // Helper method which breaks string set settings down to local settings object
+ function stringInSettings(name, values) {
+ return stringItems(settings[name], v => settings[name] = v, values);
+ }
+
+ var mainmenu = {
+ "": {
+ "title": "Drink counter"
+ },
+ "< Back": () => back(),
+
+ "Beer size": stringInSettings("beerSize", ["0.3L", "0.5L"]),
+
+
+ "Sex": stringInSettings("sex", ["male", "female"]),
+
+ 'Weight': {
+ value: 80|settings.weight,
+ min: 40, max: 500,
+ onchange: v => {
+ settings.weight = v;
+ writeSettings();
+ }
+ },
+ "Weight unit": stringInSettings("weightUnit", ["Kilo", "US Pounds"])
+
+
+ };
+
+ E.showMenu(mainmenu);
+
+});
diff --git a/apps/dtlaunch/ChangeLog b/apps/dtlaunch/ChangeLog
index 16c550334..044b8c35f 100644
--- a/apps/dtlaunch/ChangeLog
+++ b/apps/dtlaunch/ChangeLog
@@ -14,3 +14,13 @@
0.14: Don't move pages when doing exit swipe - Bangle 2.
0.15: 'Swipe to exit'-code is slightly altered to be more reliable - Bangle 2.
0.16: Use default Bangle formatter for booleans
+0.17: Bangle 2: Fast loading on exit to clock face. Added option for exit to
+clock face by timeout.
+0.18: Bangle 2: Move interactions inside setUI. Replace "one click exit" with
+back-functionality through setUI, adding the red back button as well. Hardware
+button to exit is no longer an option.
+0.19: Bangle 2: Utilize new Bangle.load(), Bangle.showClock() functions to
+facilitate 'fast switching' of apps where available.
+0.20: Bangle 2: Revert use of Bangle.load() to classic load() calls since
+widgets would still be loaded when they weren't supposed to.
+
diff --git a/apps/dtlaunch/app-b2.js b/apps/dtlaunch/app-b2.js
index 8cd5790bb..a7a318c18 100644
--- a/apps/dtlaunch/app-b2.js
+++ b/apps/dtlaunch/app-b2.js
@@ -1,61 +1,59 @@
-/* Desktop launcher
-*
-*/
+{ // must be inside our own scope here so that when we are unloaded everything disappears
-var settings = Object.assign({
- showClocks: true,
- showLaunchers: true,
- direct: false,
- oneClickExit:false,
- swipeExit: false
-}, require('Storage').readJSON("dtlaunch.json", true) || {});
+ /* Desktop launcher
+ *
+ */
-if( settings.oneClickExit)
- setWatch(_=> load(), BTN1);
-
-var s = require("Storage");
-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" && settings.showClocks) || (app.type=="launch" && settings.showLaunchers) || !app.type));
+ let settings = Object.assign({
+ showClocks: true,
+ showLaunchers: true,
+ direct: false,
+ swipeExit: false,
+ timeOut: "Off"
+ }, require('Storage').readJSON("dtlaunch.json", true) || {});
-apps.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;
-});
-apps.forEach(app=>{
+ let s = require("Storage");
+ var apps = s.list(/\.info$/).map(app=>{
+ let a=s.readJSON(app,1);
+ return a && {
+ name:a.name, type:a.type, icon:a.icon, sortorder:a.sortorder, src:a.src
+ };}).filter(
+ app=>app && (app.type=="app" || (app.type=="clock" && settings.showClocks) || (app.type=="launch" && settings.showLaunchers) || !app.type));
+
+ apps.sort((a,b)=>{
+ let n=(0|a.sortorder)-(0|b.sortorder);
+ if (n) return n; // do sortorder first
+ if (a.nameb.name) return 1;
+ return 0;
+ });
+ apps.forEach(app=>{
if (app.icon)
app.icon = s.read(app.icon); // should just be a link to a memory area
});
-var Napps = apps.length;
-var Npages = Math.ceil(Napps/4);
-var maxPage = Npages-1;
-var selected = -1;
-var oldselected = -1;
-var page = 0;
-const XOFF = 24;
-const YOFF = 30;
+ let Napps = apps.length;
+ let Npages = Math.ceil(Napps/4);
+ let maxPage = Npages-1;
+ let selected = -1;
+ let oldselected = -1;
+ let page = 0;
+ const XOFF = 24;
+ const YOFF = 30;
-function draw_icon(p,n,selected) {
- var x = (n%2)*72+XOFF;
- var y = n>1?72+YOFF:YOFF;
+ let drawIcon= function(p,n,selected) {
+ let x = (n%2)*72+XOFF;
+ let y = n>1?72+YOFF:YOFF;
(selected?g.setColor(g.theme.fgH):g.setColor(g.theme.bg)).fillRect(x+11,y+3,x+60,y+52);
g.clearRect(x+12,y+4,x+59,y+51);
g.setColor(g.theme.fg);
try{g.drawImage(apps[p*4+n].icon,x+12,y+4);} catch(e){}
g.setFontAlign(0,-1,0).setFont("6x8",1);
- var txt = apps[p*4+n].name.replace(/([a-z])([A-Z])/g, "$1 $2").split(" ");
- var lineY = 0;
- var line = "";
- while (txt.length > 0){
- var c = txt.shift();
-
+ let txt = apps[p*4+n].name.replace(/([a-z])([A-Z])/g, "$1 $2").split(" ");
+ let lineY = 0;
+ let line = "";
+ while (txt.length > 0){
+ let c = txt.shift();
if (c.length + 1 + line.length > 13){
if (line.length > 0){
g.drawString(line.trim(),x+36,y+54+lineY*8);
@@ -67,70 +65,91 @@ function draw_icon(p,n,selected) {
}
}
g.drawString(line.trim(),x+36,y+54+lineY*8);
-}
+ };
-function drawPage(p){
+ let drawPage = function(p){
g.reset();
g.clearRect(0,24,175,175);
- var O = 88+YOFF/2-12*(Npages/2);
- for (var j=0;j{
+ Bangle.loadWidgets();
+ drawPage(0);
+
+ let swipeListenerDt = function(dirLeftRight, dirUpDown){
+ updateTimeoutToClock();
selected = 0;
oldselected=-1;
- if(settings.swipeExit && dirLeftRight==1) load();
+ if(settings.swipeExit && dirLeftRight==1) Bangle.showClock();
if (dirUpDown==-1||dirLeftRight==-1){
- ++page; if (page>maxPage) page=0;
- drawPage(page);
+ ++page; if (page>maxPage) page=0;
+ drawPage(page);
} else if (dirUpDown==1||(dirLeftRight==1 && !settings.swipeExit)){
- --page; if (page<0) page=maxPage;
- drawPage(page);
+ --page; if (page<0) page=maxPage;
+ drawPage(page);
}
-});
+ };
-function isTouched(p,n){
+ let isTouched = function(p,n){
if (n<0 || n>3) return false;
- var x1 = (n%2)*72+XOFF; var y1 = n>1?72+YOFF:YOFF;
- var x2 = x1+71; var y2 = y1+81;
+ let x1 = (n%2)*72+XOFF; let y1 = n>1?72+YOFF:YOFF;
+ let x2 = x1+71; let y2 = y1+81;
return (p.x>x1 && p.y>y1 && p.x{
- var i;
+ let touchListenerDt = function(_,p){
+ updateTimeoutToClock();
+ let i;
for (i=0;i<4;i++){
- if((page*4+i)=0 || settings.direct) {
- if (selected!=i && !settings.direct){
- draw_icon(page,selected,false);
- } else {
- load(apps[page*4+i].src);
- }
- }
- selected=i;
- break;
+ if((page*4+i)=0 || settings.direct) {
+ if (selected!=i && !settings.direct){
+ drawIcon(page,selected,false);
+ } else {
+ load(apps[page*4+i].src);
}
+ }
+ selected=i;
+ break;
}
+ }
}
if ((i==4 || (page*4+i)>Napps) && selected>=0) {
- draw_icon(page,selected,false);
- selected=-1;
+ drawIcon(page,selected,false);
+ selected=-1;
}
-});
+ };
-Bangle.loadWidgets();
-g.clear();
-Bangle.drawWidgets();
-drawPage(0);
+ Bangle.setUI({
+ mode : 'custom',
+ back : Bangle.showClock,
+ swipe : swipeListenerDt,
+ touch : touchListenerDt,
+ remove : ()=>{if (timeoutToClock) clearTimeout(timeoutToClock);}
+ });
+
+ // taken from Icon Launcher with minor alterations
+ let timeoutToClock;
+ const updateTimeoutToClock = function(){
+ if (settings.timeOut!="Off"){
+ let time=parseInt(settings.timeOut); //the "s" will be trimmed by the parseInt
+ if (timeoutToClock) clearTimeout(timeoutToClock);
+ timeoutToClock = setTimeout(Bangle.showClock,time*1000);
+ }
+ };
+ updateTimeoutToClock();
+
+} // end of app scope
diff --git a/apps/dtlaunch/metadata.json b/apps/dtlaunch/metadata.json
index 36728f342..b69a1a5e6 100644
--- a/apps/dtlaunch/metadata.json
+++ b/apps/dtlaunch/metadata.json
@@ -1,7 +1,7 @@
{
"id": "dtlaunch",
"name": "Desktop Launcher",
- "version": "0.16",
+ "version": "0.20",
"description": "Desktop style App Launcher with six (four for Bangle 2) apps per page - fast access if you have lots of apps installed.",
"screenshots": [{"url":"shot1.png"},{"url":"shot2.png"},{"url":"shot3.png"}],
"icon": "icon.png",
diff --git a/apps/dtlaunch/settings-b2.js b/apps/dtlaunch/settings-b2.js
index fac9c0fff..24959df8c 100644
--- a/apps/dtlaunch/settings-b2.js
+++ b/apps/dtlaunch/settings-b2.js
@@ -5,51 +5,56 @@
showClocks: true,
showLaunchers: true,
direct: false,
- oneClickExit:false,
- swipeExit: false
+ swipeExit: false,
+ timeOut: "Off"
}, require('Storage').readJSON(FILE, true) || {});
function writeSettings() {
require('Storage').writeJSON(FILE, settings);
}
+ const timeOutChoices = [/*LANG*/"Off", "10s", "15s", "20s", "30s"];
+
E.showMenu({
"" : { "title" : "Desktop launcher" },
- "< Back" : () => back(),
- 'Show clocks': {
+ /*LANG*/"< Back" : () => back(),
+ /*LANG*/'Show clocks': {
value: settings.showClocks,
onchange: v => {
settings.showClocks = v;
writeSettings();
}
},
- 'Show launchers': {
+ /*LANG*/'Show launchers': {
value: settings.showLaunchers,
onchange: v => {
settings.showLaunchers = v;
writeSettings();
}
},
- 'Direct launch': {
+ /*LANG*/'Direct launch': {
value: settings.direct,
onchange: v => {
settings.direct = v;
writeSettings();
}
},
- 'Swipe Exit': {
+ /*LANG*/'Swipe Exit': {
value: settings.swipeExit,
onchange: v => {
settings.swipeExit = v;
writeSettings();
}
},
- 'One click exit': {
- value: settings.oneClickExit,
+ /*LANG*/'Time Out': { // Adapted from Icon Launcher
+ value: timeOutChoices.indexOf(settings.timeOut),
+ min: 0,
+ max: timeOutChoices.length-1,
+ format: v => timeOutChoices[v],
onchange: v => {
- settings.oneClickExit = v;
+ settings.timeOut = timeOutChoices[v];
writeSettings();
}
}
});
-})
+});
diff --git a/apps/entonclk/ChangeLog b/apps/entonclk/ChangeLog
new file mode 100644
index 000000000..62e2d0c20
--- /dev/null
+++ b/apps/entonclk/ChangeLog
@@ -0,0 +1 @@
+0.1: New App!
\ No newline at end of file
diff --git a/apps/entonclk/README.md b/apps/entonclk/README.md
new file mode 100644
index 000000000..8c788c7a5
--- /dev/null
+++ b/apps/entonclk/README.md
@@ -0,0 +1,9 @@
+Enton - Enhanced Anton Clock
+
+This clock face is based on the 'Anton Clock'.
+
+Things I changed:
+
+- The main font for the time is now Audiowide
+- Removed the written out day name and replaced it with steps and bpm
+- Changed the date string to a (for me) more readable string
\ No newline at end of file
diff --git a/apps/entonclk/app-icon.js b/apps/entonclk/app-icon.js
new file mode 100644
index 000000000..9993b0871
--- /dev/null
+++ b/apps/entonclk/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwkE/4A/AH4A/AH4A/AH4Aw+cikf/mQDCAAIFBAwQDBBYgXCgEDAQIABn4JBkAFBgIKDgQwFmMD+UCmcgl/zEIMzmcQmYKBmYiCAAfxC4QrBl8wBwcgkYsGC4sAiMAF4UxiIGBn8QAgMSC48wgMRiEDBAISCiYcFC48v//yC4PzgJAGiAXIiczPgPzC4JyBmf/AYQXI+KcCj8wmYFCgEjAYQ3G+cjbQIABJIMzAoUin7XIADpSEK4rWGI4MhmRJBn8j+U/d4MimUTkUzIw5dBl4UBMgIXBAgMyLYKOBmQXHiSbCDgMyl8z+UjmJ1BHgJbHCgM/IYQABAgQJBYYYA/AH4AtaQU/mTvBBozWBd44KBkUSkLnBEo8jkcvBI0/CgMiDAIXHHYIXImUzJQJHH+Y+Bn6Z/ABQA=="))
\ No newline at end of file
diff --git a/apps/entonclk/app.js b/apps/entonclk/app.js
new file mode 100644
index 000000000..69fdea479
--- /dev/null
+++ b/apps/entonclk/app.js
@@ -0,0 +1,67 @@
+Graphics.prototype.setFontAudiowide = function() {
+ // Actual height 33 (36 - 4)
+ var widths = atob("CiAsESQjJSQkHyQkDA==");
+ var font = atob("AAAAAAAAAAAAAAAAAAAAAPAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAADgAAAAAAHgAAAAAAfgAAAAAA/gAAAAAD/gAAAAAH/gAAAAAf/AAAAAB/8AAAAAD/4AAAAAP/gAAAAAf/AAAAAB/8AAAAAD/4AAAAAP/gAAAAAf+AAAAAB/8AAAAAH/wAAAAAP/gAAAAA/+AAAAAB/8AAAAAD/wAAAAAD/gAAAAAD+AAAAAAD4AAAAAADwAAAAAADAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAA//+AAAAB///AAAAH///wAAAP///4AAAf///8AAA////+AAA/4AP+AAB/gAD/AAB/AA9/AAD+AB+/gAD+AD+/gAD+AD+/gAD8AH+fgAD8AP8fgAD8AP4fgAD8Af4fgAD8A/wfgAD8A/gfgAD8B/gfgAD8D/AfgAD8D+AfgAD8H+AfgAD8P8AfgAD8P4AfgAD8f4AfgAD8/wAfgAD8/gAfgAD+/gA/gAD+/AA/gAB/eAB/AAB/sAD/AAB/wAH/AAA////+AAAf///8AAAP///4AAAH///wAAAD///gAAAA//+AAAAAP/4AAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD/////gAD/////gAD/////gAD/////gAD/////gAD/////gAD/////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//gAAAAH//gAAAAP//gAD8Af//gAD8A///gAD8B///gAD8B///gAD8B/AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD+D+AfgAD//+AfgAD//+AfgAB//8AfgAA//4AfgAAf/wAfgAAP/gAfgAAB8AAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD+B+A/gAD/////gAB/////AAB/////AAA////+AAAf///8AAAP///4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//4AAAAD//8AAAAD//+AAAAD//+AAAAD//+AAAAD//+AAAAD//+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAD/////gAD/////gAD/////gAD/////gAD/////gAD/////gAD/////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//AAfgAD//wAfgAD//4AfgAD//8AfgAD//8AfgAD//+AfgAD8D+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B/A/gAD8B///gAD8B///gAD8A///AAD8A///AAAAAf/+AAAAAP/4AAAAAD/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB///AAAAH///wAAAf///8AAAf///8AAA////+AAB/////AAB/h+H/AAD/B+B/gAD+B+A/gAD+B+A/gAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B/A/gAD8B///gAD8B///gAD8A///AAAAAf//AAAAAf/+AAAAAH/4AAAAAB/gAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD8AAAAgAD8AAABgAD8AAAHgAD8AAAfgAD8AAA/gAD8AAD/gAD8AAP/gAD8AA//gAD8AB//AAD8AH/8AAD8Af/wAAD8A//AAAD8D/+AAAD8P/4AAAD8f/gAAAD9//AAAAD//8AAAAD//wAAAAD//gAAAAD/+AAAAAD/4AAAAAD/wAAAAAD/AAAAAAD8AAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAH/4AAAAAP/8AAAH+f/+AAAf////AAA/////gAB/////gAB///A/gAD//+AfgAD//+AfgAD+D+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD+D+AfgAD//+AfgAD//+AfgAB///A/gAB/////gAA/////AAAP////AAAD+f/+AAAAAP/8AAAAAH/4AAAAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/AAAAAAf/wAAAAA//4AAAAB//8AAAAB//8AfgAD//+AfgAD/D+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD+B+A/gAD+B+A/gAD/B+B/gAB/////AAB/////AAA////+AAAf///8AAAP///4AAAH///wAAAB///AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAPAAAA/AAfgAAA/AAfgAAA/AAfgAAA/AAfgAAAeAAPAAAAAAAAAAAAAAAAAAAAAAAAAA");
+ var scale = 1; // size multiplier for this font
+ g.setFontCustom(font, 46, widths, 48+(scale<<8)+(1<<16));
+};
+
+function getSteps() {
+ var steps = 0;
+ try{
+ if (WIDGETS.wpedom !== undefined) {
+ steps = WIDGETS.wpedom.getSteps();
+ } else if (WIDGETS.activepedom !== undefined) {
+ steps = WIDGETS.activepedom.getSteps();
+ } else {
+ steps = Bangle.getHealthStatus("day").steps;
+ }
+ } catch(ex) {
+ // In case we failed, we can only show 0 steps.
+ return "?";
+ }
+
+ return Math.round(steps);
+}
+
+{ // must be inside our own scope here so that when we are unloaded everything disappears
+ // we also define functions using 'let fn = function() {..}' for the same reason. function decls are global
+let drawTimeout;
+
+
+// Actually draw the watch face
+let draw = function() {
+ var x = g.getWidth() / 2;
+ var y = g.getHeight() / 2;
+ g.reset().clearRect(Bangle.appRect); // clear whole background (w/o widgets)
+ var date = new Date();
+ var timeStr = require("locale").time(date, 1); // Hour and minute
+ g.setFontAlign(0, 0).setFont("Audiowide").drawString(timeStr, x, y);
+ var dateStr = require("locale").date(date, 1).toUpperCase();
+ g.setFontAlign(0, 0).setFont("6x8", 2).drawString(dateStr, x, y+28);
+ g.setFontAlign(0, 0).setFont("6x8", 2);
+ g.drawString(getSteps(), 50, y+70);
+ g.drawString(Math.round(Bangle.getHealthStatus("last").bpm), g.getWidth() -37, y + 70);
+
+ // queue next draw
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = setTimeout(function() {
+ drawTimeout = undefined;
+ draw();
+ }, 60000 - (Date.now() % 60000));
+};
+
+// Show launcher when middle button pressed
+Bangle.setUI({
+ mode : "clock",
+ remove : function() {
+ // Called to unload all of the clock app
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = undefined;
+ delete Graphics.prototype.setFontAnton;
+ }});
+// Load widgets
+Bangle.loadWidgets();
+draw();
+setTimeout(Bangle.drawWidgets,0);
+}
diff --git a/apps/entonclk/app.png b/apps/entonclk/app.png
new file mode 100644
index 000000000..5b634de5a
Binary files /dev/null and b/apps/entonclk/app.png differ
diff --git a/apps/entonclk/metadata.json b/apps/entonclk/metadata.json
new file mode 100644
index 000000000..7e4947406
--- /dev/null
+++ b/apps/entonclk/metadata.json
@@ -0,0 +1,17 @@
+{
+ "id": "entonclk",
+ "name": "Enton Clock",
+ "version": "0.1",
+ "description": "A simple clock using the Audiowide font. ",
+ "icon": "app.png",
+ "screenshots": [{"url":"screenshot.png"}],
+ "type": "clock",
+ "tags": "clock",
+ "supports": ["BANGLEJS2"],
+ "allow_emulator": true,
+ "readme":"README.md",
+ "storage": [
+ {"name":"entonclk.app.js","url":"app.js"},
+ {"name":"entonclk.img","url":"app-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/entonclk/screenshot.png b/apps/entonclk/screenshot.png
new file mode 100644
index 000000000..0905c6fc8
Binary files /dev/null and b/apps/entonclk/screenshot.png differ
diff --git a/apps/espruinoctrl/README.md b/apps/espruinoctrl/README.md
index a7bca662c..7b2e434e7 100644
--- a/apps/espruinoctrl/README.md
+++ b/apps/espruinoctrl/README.md
@@ -17,7 +17,7 @@ showing available Espruino devices is popped up.
device being connected to. Use this if you want to print data - eg: `print(E.getBattery())`
When done, click 'Upload'. Your changes will be saved to local storage
-so they'll be remembered next time you upload from the same device.s
+so they'll be remembered next time you upload from the same device.
## Usage
diff --git a/apps/espruinoctrl/app-icon.js b/apps/espruinoctrl/app-icon.js
index 70d2dd062..3f9572f72 100644
--- a/apps/espruinoctrl/app-icon.js
+++ b/apps/espruinoctrl/app-icon.js
@@ -1 +1 @@
-require("heatshrink").decompress(atob("mEwhH+AH4A/AH4A/AH4AFwIuuAAIllAAYIGF041IF34AKqwuuAANXF9QuCAANdGHqQgGBwvdGCIud5mjGB4udAAIwPFz3MSR61VFxQwNci4vGeh4uXGAguHGBK3WGA4AIegtXc69dGBxoBGAouWO4IwNe4gwZa4YwLFwikEFzAwLFwwwCFzQwKFw68YGB4AdF5AwmF5IwlF5QwkF5Yw/F8IwEL9WBB4IuuADwuzGxAugFAgliGBYutAH4A/AH4A/ADA="))
+require("heatshrink").decompress(atob("mEw4UA///muVt9TgH+Jf4AQgILKgtABI9VqkVqAgHqoABC48FBYQKGhEVBQNUBY0qyoLJ1WlEZMq1ILJhWqBZMC1QwCBY0PGAYLGn/qGAQLG/4wDBIkggf8GARfF1ED+BhCTQgTBgfAMISaF1WAAYM61SBG0ADB/wLFgNq1EAHoIcDXYVaCYMP+EqC4kVqwTBn/AhDqFqowBn72HqowCBZAwCBZAwCBZAwCBZIwIiowKBYVWC5VUkAvJXYiaDBYS7FTQVUgr2HC4IgHAAYgHAH4AJA=="))
diff --git a/apps/espruinoctrl/metadata.json b/apps/espruinoctrl/metadata.json
index 253307fa0..5107bc6ae 100644
--- a/apps/espruinoctrl/metadata.json
+++ b/apps/espruinoctrl/metadata.json
@@ -5,7 +5,7 @@
"version": "0.01",
"description": "Send commands to other Espruino devices via the Bluetooth UART interface. Customisable commands!",
"icon": "app.png",
- "tags": "",
+ "tags": "tool,bluetooth",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"custom": "custom.html",
diff --git a/apps/espruinoprog/ChangeLog b/apps/espruinoprog/ChangeLog
new file mode 100644
index 000000000..6fdcad1d6
--- /dev/null
+++ b/apps/espruinoprog/ChangeLog
@@ -0,0 +1,4 @@
+0.01: New App!
+0.02: Add 'pre' code that can erase the device
+ Wait more between sending code snippets
+ Now force use of 'Storage' (assume 2v00 or later)
diff --git a/apps/espruinoprog/README.md b/apps/espruinoprog/README.md
new file mode 100644
index 000000000..aef4cccad
--- /dev/null
+++ b/apps/espruinoprog/README.md
@@ -0,0 +1,43 @@
+# Espruino Programmer
+
+Finds Bluetooth devices with a specific name (eg `Puck.js`), connects and uploads code. Great for programming many devices at once!
+
+**WARNING:** This will reprogram **any matching Espruino device within range** while
+the app is running. Unless you are careful to remove other devices from the area or
+turn them off, you could find some of your devices unexpectedly get programmed!
+
+## Customising
+
+Click on the Customise button in the app loader to set up the programmer.
+
+* First you need to choose the kind of devices you want to upload to. This is
+the text that should match the Bluetooth advertising name. So `Puck.js` for Puck.js
+devices, or `Bangle.js` for Bangles.
+* In the next box, you have code to run before the upload of the main code. By default
+the code `require("Storage").list().forEach(f=>require("Storage").erase(f));reset();` will
+erase all files on the device and reset it.
+* Now paste in the code you want to write to the device. This is automatically
+written to flash (`.bootcde`). See https://www.espruino.com/Saving#save-on-send-to-flash-
+for more information.
+* Now enter the code that should be sent **after** programming. This code
+should make the device so it doesn't advertise on Bluetooth with the Bluetooth
+name you entered for the first item. It may also help if it indicates to you that
+the device is programmed properly.
+ * You could turn advertising off with `NRF.sleep()`
+ * You could change the advertising name with `NRF.setAdvertising({},{name:"Ok"});`
+ * On a Bangle, you could turn it off with `Bangle.off()`
+* Finally scroll down and click `Upload`
+* Now you can run the new `Programmer` app on the Bangle.
+
+## Usage
+
+Just run the app, and as soon as it starts it'll start scanning for
+devices to upload to!
+
+To stop scanning, long-press the button to return to the clock.
+
+## Notes
+
+* This assumes the device being written to is at least version 2v00 of Espruino
+* Currently, code is not minified before upload (so you need to supply pre-minified
+ code if you want that)
diff --git a/apps/espruinoprog/app-icon.js b/apps/espruinoprog/app-icon.js
new file mode 100644
index 000000000..532c60eea
--- /dev/null
+++ b/apps/espruinoprog/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEw4cA/4AB7wJB8/5uX+7uUgH41lSKf4AKpMkyQCCggEDAQVtCAMCCNWUx9JufSrmkCJeKqsiytICJtFkWRCJWAEaARCI5BkEoAGBymJ9eSvXkCJZ9JCLI1DyM9uQRLNYWRpRZMR5ARWAwSPCuWR9MuCJZZIgARGPouTCIcSA4OQAoMW7dt2wCEEZECCI1oCJAADrZyBAAcDuQRByOABQkKDAvbtwRBxu24AMFAAcGCIY/B7AQIhpOC3MjKIVsCJe3jYRCwiPEkARBQg227ieDAQO0CJPhCKHJCK1N0ARI28JCIjUDEY4OBzWRfAoRG3ARBygRH3oPBswRB4QjFfAYgCt9pYoJoEkmbCJONCI1ACJGSiQRE7TXDCIuQEYkmCIhpDEYSCFCIj2DCIOTrYRE6ARDAH4AHA"))
diff --git a/apps/espruinoprog/app.js b/apps/espruinoprog/app.js
new file mode 100644
index 000000000..58fac4a0b
--- /dev/null
+++ b/apps/espruinoprog/app.js
@@ -0,0 +1,100 @@
+var uart; // require("ble_uart")
+var device; // BluetoothDevice
+var uploadTimeout; // a timeout used during upload - if we disconnect, kill this
+Bangle.loadWidgets();
+
+var json = require("Storage").readJSON("espruinoprog.json",1);
+/*var json = { // for example
+ namePrefix : "Puck.js ",
+ code : "E.setBootCode('digitalPulse(LED2,1,100);')",
+ post : "LED.set();NRF.sleep()",
+};*/
+
+if ("object" != typeof json) {
+ E.showAlert("JSON not found","Programmer").then(() => load());
+ throw new Error("JSON not found");
+ // stops execution
+}
+
+// Set up terminal
+var R = Bangle.appRect;
+var termg = Graphics.createArrayBuffer(R.w, R.h, 1, {msb:true});
+termg.setFont("6x8");
+var term;
+
+function showTerminal() {
+ E.showMenu(); // clear anything that was drawn
+ if (term) term.print(""); // redraw terminal
+}
+
+function scanAndConnect() {
+ termg.clear();
+ term = require("VT100").connect(termg, {
+ charWidth : 6,
+ charHeight : 8
+ });
+ term.print = str => {
+ for (var i of str) term.char(i);
+ g.reset().drawImage(termg,R.x,R.y);
+ };
+ term.print(`\r\nScanning...\r\n`);
+ NRF.requestDevice({ filters: [{ namePrefix: json.namePrefix }] }).then(function(dev) {
+ term.print(`Found ${dev.name||dev.id.substr(0,17)}\r\n`);
+ device = dev;
+
+ term.print(`Connect to ${dev.name||dev.id.substr(0,17)}...\r\n`);
+ device.removeAllListeners();
+ device.on('gattserverdisconnected', function(reason) {
+ if (!uart) return;
+ term.print(`\r\nDISCONNECTED (${reason})\r\n`);
+ uart = undefined;
+ device = undefined;
+ if (uploadTimeout) clearTimeout(uploadTimeout);
+ uploadTimeout = undefined;
+ setTimeout(scanAndConnect, 1000);
+ });
+ require("ble_uart").connect(device).then(function(u) {
+ uart = u;
+ term.print("Connected...\r\n");
+ uart.removeAllListeners();
+ uart.on('data', function(d) { term.print(d); });
+ term.print("Upload initial...\r\n");
+ uart.write((json.pre||"")+"\n").then(() => {
+ term.print("\r\Done.\r\n");
+ uploadTimeout = setTimeout(function() {
+ uploadTimeout = undefined;
+ term.print("\r\nUpload Code...\r\n");
+ uart.write((json.code||"")+"\n").then(() => {
+ term.print("\r\Done.\r\n");
+ // main upload completed - wait a bit
+ uploadTimeout = setTimeout(function() {
+ uploadTimeout = undefined;
+ term.print("\r\Upload final...\r\n");
+ // now upload the code to run after...
+ uart.write((json.post||"")+"\n").then(() => {
+ term.print("\r\nDone.\r\n");
+ // now wait and disconnect (if not already done!)
+ uploadTimeout = setTimeout(function() {
+ uploadTimeout = undefined;
+ term.print("\r\nDisconnecting...\r\n");
+ if (uart) uart.disconnect();
+ }, 500);
+ });
+ }, 2000);
+ });
+ }, 2000);
+ });
+ });
+ }).catch(err => {
+ if (err.toString().startsWith("No device found")) {
+ // expected - try again
+ scanAndConnect();
+ } else
+ term.print(`\r\ERROR ${err.toString()}\r\n`);
+ });
+}
+
+// now start
+Bangle.drawWidgets();
+showTerminal();
+scanAndConnect();
diff --git a/apps/espruinoprog/app.png b/apps/espruinoprog/app.png
new file mode 100644
index 000000000..b2b435f04
Binary files /dev/null and b/apps/espruinoprog/app.png differ
diff --git a/apps/espruinoprog/custom.html b/apps/espruinoprog/custom.html
new file mode 100644
index 000000000..a12189707
--- /dev/null
+++ b/apps/espruinoprog/custom.html
@@ -0,0 +1,175 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Upload code to devices with names starting with:
+
+ Enter the code to send before upload here:
+
+ Enter your program to upload here:
+
+ Enter the code to send after upload here:
+
+ Then click Upload
+ Click here to reset to defaults.
+
+
+
+
diff --git a/apps/espruinoprog/metadata.json b/apps/espruinoprog/metadata.json
new file mode 100644
index 000000000..7371e005d
--- /dev/null
+++ b/apps/espruinoprog/metadata.json
@@ -0,0 +1,18 @@
+{
+ "id": "espruinoprog",
+ "name": "Espruino Programmer",
+ "shortName": "Programmer",
+ "version": "0.02",
+ "description": "Finds Bluetooth devices with a specific name (eg 'Puck.js'), connects and uploads code. Great for programming many devices at once!",
+ "icon": "app.png",
+ "tags": "tool,bluetooth",
+ "supports": ["BANGLEJS","BANGLEJS2"],
+ "readme": "README.md",
+ "custom": "custom.html",
+ "storage": [
+ {"name":"espruinoprog.app.js","url":"app.js"},
+ {"name":"espruinoprog.img","url":"app-icon.js","evaluate":true}
+ ], "data": [
+ {"name":"espruinoprog.json"}
+ ]
+}
diff --git a/apps/espruinoterm/ChangeLog b/apps/espruinoterm/ChangeLog
new file mode 100644
index 000000000..5560f00bc
--- /dev/null
+++ b/apps/espruinoterm/ChangeLog
@@ -0,0 +1 @@
+0.01: New App!
diff --git a/apps/espruinoterm/README.md b/apps/espruinoterm/README.md
new file mode 100644
index 000000000..df26d59a0
--- /dev/null
+++ b/apps/espruinoterm/README.md
@@ -0,0 +1,22 @@
+# Espruino Terminal
+
+Send commands to other Espruino devices via the Bluetooth UART interface and
+see the result on a terminal.
+
+## Customising
+
+Once installed and you're connected to the Bangle you can click the button next to the app in the app loader
+to change the commands (they will be read from the device).
+
+When done, click `Save to Bangle.js` and your changes will be saved to the same device.
+
+## Usage
+
+* Load the app and after a few seconds you'll see a menu with Espruino devices
+in the vicinity.
+* Tap on the device you want to connect to
+* A terminal will pop up showing `Connecting...` and then `Connected`
+* Now tap on the right (or press the button) to bring up a menu with options for commands, or the option to disconnect.
+
+You can also choose `Custom` in which case a keyboard (using the currently installed text input method) will
+be displayed and you can enter the command you would like to send.
diff --git a/apps/espruinoterm/app-icon.js b/apps/espruinoterm/app-icon.js
new file mode 100644
index 000000000..f566aedf7
--- /dev/null
+++ b/apps/espruinoterm/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwcCpMkyQC/AVW//4AK/oR/COD8LCP4R/CK8DCKNsCKFt2BHPhu2CJ8BCKAjQI4OQNaIUB23bsCPMCJzp/CP4Rf/4AKCKwC/AVIA=="))
diff --git a/apps/espruinoterm/app.js b/apps/espruinoterm/app.js
new file mode 100644
index 000000000..348190db4
--- /dev/null
+++ b/apps/espruinoterm/app.js
@@ -0,0 +1,101 @@
+var uart; // require("ble_uart")
+var device; // BluetoothDevice
+var customCommand = "";
+// Set up terminal
+Bangle.loadWidgets();
+var R = Bangle.appRect;
+var termg = Graphics.createArrayBuffer(R.w, R.h, 1, {msb:true});
+var termVisible = false;
+termg.setFont("6x8");
+term = require("VT100").connect(termg, {
+ charWidth : 6,
+ charHeight : 8
+});
+term.print = str => {
+ for (var i of str) term.char(i);
+ if (termVisible) g.reset().drawImage(termg,R.x,R.y).setFont("6x8").setFontAlign(0,-1,1).drawString("MORE",R.w-1,(R.y+R.y2)/2);
+};
+
+function showConnectMenu() {
+ termVisible = false;
+ var m = { "" : {title:"Devices"} };
+ E.showMessage("Scanning...");
+ NRF.findDevices(devices => {
+ devices.forEach(dev=>{
+ m[dev.name||dev.id.substr(0,17)] = ()=>{
+ connectTo(dev);
+ };
+ });
+ m["< Back"] = () => showConnectMenu();
+ E.showMenu(m);
+ },{filters:[
+ { namePrefix: 'Puck.js' },
+ { namePrefix: 'Pixl.js' },
+ { namePrefix: 'MDBT42Q' },
+ { namePrefix: 'Bangle.js' },
+ { namePrefix: 'Espruino' },
+ { services: [ "6e400001-b5a3-f393-e0a9-e50e24dcca9e" ] }
+ ],active:true,timeout:4000});
+}
+
+function showOptionsMenu() {
+ if (!uart) showConnectMenu();
+ termVisible = false;
+ var menu = {"":{title:/*LANG*/"Options"},
+ "< Back" : () => showTerminal(),
+ };
+ var json = require("Storage").readJSON("espruinoterm.json",1);
+ if (Array.isArray(json)) {
+ json.forEach(j => { menu[j.title] = () => sendCommand(j.cmd); });
+ } else {
+ Object.assign(menu,{
+ "Version" : () => sendCommand("process.env.VERSION"),
+ "Battery" : () => sendCommand("E.getBattery()"),
+ "Flash LED" : () => sendCommand("LED.set();setTimeout(()=>LED.reset(),1000);")
+ });
+ }
+ menu[/*LANG*/"Custom"] = () => { require("textinput").input({text:customCommand}).then(result => {
+ customCommand = result;
+ sendCommand(customCommand);
+ })};
+ menu[/*LANG*/"Disconnect"] = () => { showTerminal(); term.print("\r\nDisconnecting...\r\n"); uart.disconnect(); }
+
+ E.showMenu(menu);
+}
+
+function showTerminal() {
+ E.showMenu();
+ Bangle.setUI({
+ mode : "custom",
+ btn : n=> { showOptionsMenu(); },
+ touch : (n,e) => { if (n==2) showOptionsMenu(); }
+ });
+ termVisible = true;
+ term.print(""); // redraw terminal
+}
+
+function sendCommand(cmd) {
+ showTerminal();
+ uart.write(cmd+"\n");
+}
+
+function connectTo(dev) {
+ device = dev;
+ showTerminal();
+ term.print(`\r\nConnect to ${dev.name||dev.id.substr(0,17)}...\r\n`);
+ device.on('gattserverdisconnected', function(reason) {
+ term.print(`\r\nDISCONNECTED (${reason})\r\n`);
+ uart = undefined;
+ device = undefined;
+ setTimeout(showConnectMenu, 1000);
+ });
+ require("ble_uart").connect(device).then(function(u) {
+ uart = u;
+ term.print("Connected...\r\n");
+ uart.on('data', function(d) { term.print(d); });
+ });
+}
+
+// now start
+Bangle.drawWidgets();
+showConnectMenu();
diff --git a/apps/espruinoterm/app.json b/apps/espruinoterm/app.json
new file mode 100644
index 000000000..72a12e635
--- /dev/null
+++ b/apps/espruinoterm/app.json
@@ -0,0 +1,5 @@
+[
+ {"title":"Version", "cmd":"process.env.VERSION"},
+ {"title":"Battery", "cmd":"E.getBattery()"},
+ {"title":"Flash LED", "cmd":"LED.set();setTimeout(()=>LED.reset(),1000);"}
+]
diff --git a/apps/espruinoterm/app.png b/apps/espruinoterm/app.png
new file mode 100644
index 000000000..e9a8c3758
Binary files /dev/null and b/apps/espruinoterm/app.png differ
diff --git a/apps/espruinoterm/interface.html b/apps/espruinoterm/interface.html
new file mode 100644
index 000000000..660b3a86c
--- /dev/null
+++ b/apps/espruinoterm/interface.html
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+
+ Enter the menu items you'd like to see appear in the app below. When finished, click `Save to Bangle.js` to save the JavaScript back.
+
+
+
+
+ Title
+ Command
+
+
+
+
+
+
+
+ Save to Bangle.js
+
+
+
+
+
+
diff --git a/apps/espruinoterm/metadata.json b/apps/espruinoterm/metadata.json
new file mode 100644
index 000000000..25e6183e1
--- /dev/null
+++ b/apps/espruinoterm/metadata.json
@@ -0,0 +1,20 @@
+{
+ "id": "espruinoterm",
+ "name": "Espruino Terminal",
+ "shortName": "Espruino Term",
+ "version": "0.01",
+ "description": "Send commands to other Espruino devices via the Bluetooth UART interface, and see the result on a VT100 terminal. Customisable commands!",
+ "icon": "app.png",
+ "screenshots": [{"url":"screenshot.png"}],
+ "tags": "tool,bluetooth",
+ "supports": ["BANGLEJS","BANGLEJS2"],
+ "readme": "README.md",
+ "interface": "interface.html",
+ "dependencies": {"textinput":"type"},
+ "storage": [
+ {"name":"espruinoterm.app.js","url":"app.js"},
+ {"name":"espruinoterm.img","url":"app-icon.js","evaluate":true}
+ ],"data": [
+ {"name":"espruinoterm.json","url":"app.json"}
+ ]
+}
diff --git a/apps/espruinoterm/screenshot.png b/apps/espruinoterm/screenshot.png
new file mode 100644
index 000000000..cce881a37
Binary files /dev/null and b/apps/espruinoterm/screenshot.png differ
diff --git a/apps/f9lander/ChangeLog b/apps/f9lander/ChangeLog
index 5560f00bc..a13f2a313 100644
--- a/apps/f9lander/ChangeLog
+++ b/apps/f9lander/ChangeLog
@@ -1 +1,2 @@
0.01: New App!
+0.02: Add lightning
diff --git a/apps/f9lander/app.js b/apps/f9lander/app.js
index 7e52104c0..2f17a5bd5 100644
--- a/apps/f9lander/app.js
+++ b/apps/f9lander/app.js
@@ -46,6 +46,9 @@ var booster = { x : g.getWidth()/4 + Math.random()*g.getWidth()/2,
var exploded = false;
var nExplosions = 0;
var landed = false;
+var lightning = 0;
+
+var settings = require("Storage").readJSON('f9settings.json', 1) || {};
const gravity = 4;
const dt = 0.1;
@@ -61,18 +64,40 @@ function flameImageGen (throttle) {
function drawFalcon(x, y, throttle, angle) {
g.setColor(1, 1, 1).drawImage(falcon9, x, y, {rotate:angle});
- if (throttle>0) {
+ if (throttle>0 || lightning>0) {
var flameImg = flameImageGen(throttle);
var r = falcon9.height/2 + flameImg.height/2-1;
var xoffs = -Math.sin(angle)*r;
var yoffs = Math.cos(angle)*r;
if (Math.random()>0.7) g.setColor(1, 0.5, 0);
else g.setColor(1, 1, 0);
- g.drawImage(flameImg, x+xoffs, y+yoffs, {rotate:angle});
+ if (throttle>0) g.drawImage(flameImg, x+xoffs, y+yoffs, {rotate:angle});
+ if (lightning>1 && lightning<30) {
+ for (var i=0; i<6; ++i) {
+ var r = Math.random()*6;
+ var x = Math.random()*5 - xoffs;
+ var y = Math.random()*5 - yoffs;
+ g.setColor(1, Math.random()*0.5+0.5, 0).fillCircle(booster.x+x, booster.y+y, r);
+ }
+ }
}
}
+function drawLightning() {
+ var c = {x:cloudOffs+50, y:30};
+ var dx = c.x-booster.x;
+ var dy = c.y-booster.y;
+ var m1 = {x:booster.x+0.6*dx+Math.random()*20, y:booster.y+0.6*dy+Math.random()*10};
+ var m2 = {x:booster.x+0.4*dx+Math.random()*20, y:booster.y+0.4*dy+Math.random()*10};
+ g.setColor(1, 1, 1).drawLine(c.x, c.y, m1.x, m1.y).drawLine(m1.x, m1.y, m2.x, m2.y).drawLine(m2.x, m2.y, booster.x, booster.y);
+}
+
function drawBG() {
+ if (lightning==1) {
+ g.setBgColor(1, 1, 1).clear();
+ Bangle.buzz(200);
+ return;
+ }
g.setBgColor(0.2, 0.2, 1).clear();
g.setColor(0, 0, 1).fillRect(0, g.getHeight()-oceanHeight, g.getWidth()-1, g.getHeight()-1);
g.setColor(0.5, 0.5, 1).fillCircle(cloudOffs+34, 30, 15).fillCircle(cloudOffs+60, 35, 20).fillCircle(cloudOffs+75, 20, 10);
@@ -88,6 +113,7 @@ function renderScreen(input) {
drawBG();
showFuel();
drawFalcon(booster.x, booster.y, Math.floor(input.throttle*12), input.angle);
+ if (lightning>1 && lightning<6) drawLightning();
}
function getInputs() {
@@ -97,6 +123,7 @@ function getInputs() {
if (t > 1) t = 1;
if (t < 0) t = 0;
if (booster.fuel<=0) t = 0;
+ if (lightning>0 && lightning<20) t = 0;
return {throttle: t, angle: a};
}
@@ -121,7 +148,6 @@ function gameStep() {
else {
var input = getInputs();
if (booster.y >= targetY) {
-// console.log(booster.x + " " + booster.y + " " + booster.vy + " " + droneX + " " + input.angle);
if (Math.abs(booster.x-droneX-droneShip.width/2)40) && Math.random()>0.98) lightning = 1;
booster.x += booster.vx*dt;
booster.y += booster.vy*dt;
booster.vy += gravity*dt;
diff --git a/apps/f9lander/metadata.json b/apps/f9lander/metadata.json
index 75c6a0164..1db777099 100644
--- a/apps/f9lander/metadata.json
+++ b/apps/f9lander/metadata.json
@@ -1,7 +1,7 @@
{ "id": "f9lander",
"name": "Falcon9 Lander",
"shortName":"F9lander",
- "version":"0.01",
+ "version":"0.02",
"description": "Land a rocket booster",
"icon": "f9lander.png",
"screenshots" : [ { "url":"f9lander_screenshot1.png" }, { "url":"f9lander_screenshot2.png" }, { "url":"f9lander_screenshot3.png" }],
@@ -10,6 +10,7 @@
"supports" : ["BANGLEJS", "BANGLEJS2"],
"storage": [
{"name":"f9lander.app.js","url":"app.js"},
- {"name":"f9lander.img","url":"app-icon.js","evaluate":true}
+ {"name":"f9lander.img","url":"app-icon.js","evaluate":true},
+ {"name":"f9lander.settings.js", "url":"settings.js"}
]
}
diff --git a/apps/f9lander/settings.js b/apps/f9lander/settings.js
new file mode 100644
index 000000000..0f9fba302
--- /dev/null
+++ b/apps/f9lander/settings.js
@@ -0,0 +1,36 @@
+// This file should contain exactly one function, which shows the app's settings
+/**
+ * @param {function} back Use back() to return to settings menu
+ */
+const boolFormat = v => v ? /*LANG*/"On" : /*LANG*/"Off";
+(function(back) {
+ const SETTINGS_FILE = 'f9settings.json'
+ // initialize with default settings...
+ let settings = {
+ 'lightning': false,
+ }
+ // ...and overwrite them with any saved values
+ // This way saved values are preserved if a new version adds more settings
+ const storage = require('Storage')
+ const saved = storage.readJSON(SETTINGS_FILE, 1) || {}
+ for (const key in saved) {
+ settings[key] = saved[key];
+ }
+ // creates a function to safe a specific setting, e.g. save('color')(1)
+ function save(key) {
+ return function (value) {
+ settings[key] = value;
+ storage.write(SETTINGS_FILE, settings);
+ }
+ }
+ const menu = {
+ '': { 'title': 'OpenWind' },
+ '< Back': back,
+ 'Lightning': {
+ value: settings.lightning,
+ format: boolFormat,
+ onchange: save('lightning'),
+ }
+ }
+ E.showMenu(menu);
+})
diff --git a/apps/fastload/ChangeLog b/apps/fastload/ChangeLog
new file mode 100644
index 000000000..53e3c2591
--- /dev/null
+++ b/apps/fastload/ChangeLog
@@ -0,0 +1,3 @@
+0.01: New App!
+0.02: Allow redirection of loads to the launcher
+0.03: Allow hiding the fastloading info screen
diff --git a/apps/fastload/README.md b/apps/fastload/README.md
new file mode 100644
index 000000000..a1feedcf8
--- /dev/null
+++ b/apps/fastload/README.md
@@ -0,0 +1,21 @@
+# Fastload Utils
+
+*EXPERIMENTAL* Use this with caution. When you find something misbehaving please check if the problem actually persists when removing this app.
+
+This allows fast loading of all apps with two conditions:
+* Loaded app contains `Bangle.loadWidgets`. This is needed to prevent problems with apps not expecting widgets to be already loaded.
+* Current app can be removed completely from RAM.
+
+## Settings
+
+* Allows to redirect all loads usually loading the clock to the launcher instead
+* The "Fastloading..." screen can be switched off
+
+## Technical infos
+
+This is still experimental but it uses the same mechanism as `.bootcde` does.
+It checks the app to be loaded for widget use and stores the result of that and a hash of the js in a cache.
+
+# Creator
+
+[halemmerich](https://github.com/halemmerich)
diff --git a/apps/fastload/boot.js b/apps/fastload/boot.js
new file mode 100644
index 000000000..c9271abbf
--- /dev/null
+++ b/apps/fastload/boot.js
@@ -0,0 +1,66 @@
+{
+const SETTINGS = require("Storage").readJSON("fastload.json") || {};
+
+let loadingScreen = function(){
+ g.reset();
+
+ let x = g.getWidth()/2;
+ let y = g.getHeight()/2;
+ g.setColor(g.theme.bg);
+ g.fillRect(x-49, y-19, x+49, y+19);
+ g.setColor(g.theme.fg);
+ g.drawRect(x-50, y-20, x+50, y+20);
+ g.setFont("6x8");
+ g.setFontAlign(0,0);
+ g.drawString("Fastloading...", x, y);
+ g.flip(true);
+};
+
+let cache = require("Storage").readJSON("fastload.cache") || {};
+
+let checkApp = function(n){
+ // no widgets, no problem
+ if (!global.WIDGETS) return true;
+ let app = require("Storage").read(n);
+ if (cache[n] && E.CRC32(app) == cache[n].crc)
+ return cache[n].fast
+ cache[n] = {};
+ cache[n].fast = app.includes("Bangle.loadWidgets");
+ cache[n].crc = E.CRC32(app);
+ require("Storage").writeJSON("fastload.cache", cache);
+ return cache[n].fast;
+}
+
+global._load = load;
+
+let slowload = function(n){
+ global._load(n);
+}
+
+let fastload = function(n){
+ if (!n || checkApp(n)){
+ // Bangle.load can call load, to prevent recursion this must be the system load
+ global.load = slowload;
+ Bangle.load(n);
+ // if fastloading worked, we need to set load back to this method
+ global.load = fastload;
+ }
+ else
+ slowload(n);
+};
+global.load = fastload;
+
+Bangle.load = (o => (name) => {
+ if (Bangle.uiRemove && !SETTINGS.hideLoading) loadingScreen();
+ if (SETTINGS.autoloadLauncher && !name){
+ let orig = Bangle.load;
+ Bangle.load = (n)=>{
+ Bangle.load = orig;
+ fastload(n);
+ }
+ Bangle.showLauncher();
+ Bangle.load = orig;
+ } else
+ o(name);
+})(Bangle.load);
+}
diff --git a/apps/fastload/icon.png b/apps/fastload/icon.png
new file mode 100644
index 000000000..7fe9afe6e
Binary files /dev/null and b/apps/fastload/icon.png differ
diff --git a/apps/fastload/metadata.json b/apps/fastload/metadata.json
new file mode 100644
index 000000000..15adcb7e3
--- /dev/null
+++ b/apps/fastload/metadata.json
@@ -0,0 +1,16 @@
+{ "id": "fastload",
+ "name": "Fastload Utils",
+ "shortName" : "Fastload Utils",
+ "version": "0.03",
+ "icon": "icon.png",
+ "description": "Enable experimental fastloading for more apps",
+ "type":"bootloader",
+ "tags": "system",
+ "supports": ["BANGLEJS2"],
+ "readme": "README.md",
+ "storage": [
+ {"name":"fastload.5.boot.js","url":"boot.js"},
+ {"name":"fastload.settings.js","url":"settings.js"}
+ ],
+ "data": [{"name":"fastload.json"}]
+}
diff --git a/apps/fastload/settings.js b/apps/fastload/settings.js
new file mode 100644
index 000000000..4904e057e
--- /dev/null
+++ b/apps/fastload/settings.js
@@ -0,0 +1,38 @@
+(function(back) {
+ var FILE="fastload.json";
+ var settings;
+
+ function writeSettings(key, value) {
+ var s = require('Storage').readJSON(FILE, true) || {};
+ s[key] = value;
+ require('Storage').writeJSON(FILE, s);
+ readSettings();
+ }
+
+ function readSettings(){
+ settings = require('Storage').readJSON(FILE, true) || {};
+ }
+
+ readSettings();
+
+ function buildMainMenu(){
+ var mainmenu = {
+ '': { 'title': 'Fastload', back: back },
+ 'Force load to launcher': {
+ value: !!settings.autoloadLauncher,
+ onchange: v => {
+ writeSettings("autoloadLauncher",v);
+ }
+ },
+ 'Hide "Fastloading..."': {
+ value: !!settings.hideLoading,
+ onchange: v => {
+ writeSettings("hideLoading",v);
+ }
+ }
+ };
+ return mainmenu;
+ }
+
+ E.showMenu(buildMainMenu());
+})
diff --git a/apps/fclock/ChangeLog b/apps/fclock/ChangeLog
index 30e049f69..7e7307c59 100644
--- a/apps/fclock/ChangeLog
+++ b/apps/fclock/ChangeLog
@@ -1,2 +1,3 @@
0.01: First published version of app
0.02: Move to Bangle.setUI to launcher support
+0.03: Tell clock widgets to hide.
diff --git a/apps/fclock/fclock.app.js b/apps/fclock/fclock.app.js
index afa0c5e2d..838a5578d 100644
--- a/apps/fclock/fclock.app.js
+++ b/apps/fclock/fclock.app.js
@@ -173,6 +173,9 @@ const drawHR = function () {
}
};
+// Show launcher when button pressed
+Bangle.setUI("clock");
+
// clean app screen
g.clear();
Bangle.loadWidgets();
@@ -198,6 +201,3 @@ Bangle.on('HRM', function (d) {
// draw now
drawClock();
-
-// Show launcher when button pressed
-Bangle.setUI("clock");
diff --git a/apps/fclock/metadata.json b/apps/fclock/metadata.json
index da553e110..dffb197a2 100644
--- a/apps/fclock/metadata.json
+++ b/apps/fclock/metadata.json
@@ -2,7 +2,7 @@
"id": "fclock",
"name": "fclock",
"shortName": "F Clock",
- "version": "0.02",
+ "version": "0.03",
"description": "Simple design of a digital clock",
"icon": "app.png",
"type": "clock",
diff --git a/apps/files/ChangeLog b/apps/files/ChangeLog
index 1908f7e5c..4622e6f0f 100644
--- a/apps/files/ChangeLog
+++ b/apps/files/ChangeLog
@@ -3,4 +3,5 @@
0.04: Add functionality to sort apps manually or alphabetically ascending/descending.
0.05: Tweaks to help with memory usage
0.06: Reduce memory usage
-0.07: Allow negative numbers when manual-sorting
\ No newline at end of file
+0.07: Allow negative numbers when manual-sorting
+0.08: Automatic translation of strings.
diff --git a/apps/files/files.js b/apps/files/files.js
index e81e9589f..2f7b5c9a1 100644
--- a/apps/files/files.js
+++ b/apps/files/files.js
@@ -3,20 +3,20 @@ const store = require('Storage');
function showMainMenu() {
const mainmenu = {
'': {
- 'title': 'App Manager',
+ 'title': /*LANG*/'App Manager',
},
'< Back': ()=> {load();},
- 'Sort Apps': () => showSortAppsMenu(),
- 'Manage Apps': ()=> showApps(),
- 'Compact': () => {
- E.showMessage('Compacting...');
+ /*LANG*/'Sort Apps': () => showSortAppsMenu(),
+ /*LANG*/'Manage Apps': ()=> showApps(),
+ /*LANG*/'Compact': () => {
+ E.showMessage(/*LANG*/'Compacting...');
try {
store.compact();
} catch (e) {
}
showMainMenu();
},
- 'Free': {
+ /*LANG*/'Free': {
value: undefined,
format: (v) => {
return store.getFree();
@@ -65,13 +65,13 @@ function eraseData(info) {
});
}
function eraseApp(app, files,data) {
- E.showMessage('Erasing\n' + app.name + '...');
+ E.showMessage(/*LANG*/'Erasing\n' + app.name + '...');
var info = store.readJSON(app.id + ".info", 1)||{};
if (files) eraseFiles(info);
if (data) eraseData(info);
}
function eraseOne(app, files,data){
- E.showPrompt('Erase\n'+app.name+'?').then((v) => {
+ E.showPrompt(/*LANG*/'Erase\n'+app.name+'?').then((v) => {
if (v) {
Bangle.buzz(100, 1);
eraseApp(app, files, data);
@@ -82,7 +82,7 @@ function eraseOne(app, files,data){
});
}
function eraseAll(apps, files,data) {
- E.showPrompt('Erase all?').then((v) => {
+ E.showPrompt(/*LANG*/'Erase all?').then((v) => {
if (v) {
Bangle.buzz(100, 1);
apps.forEach(app => eraseApp(app, files, data));
@@ -99,11 +99,11 @@ function showAppMenu(app) {
'< Back': () => showApps(),
};
if (app.hasData) {
- appmenu['Erase Completely'] = () => eraseOne(app, true, true);
- appmenu['Erase App,Keep Data'] = () => eraseOne(app, true, false);
- appmenu['Only Erase Data'] = () => eraseOne(app, false, true);
+ appmenu[/*LANG*/'Erase Completely'] = () => eraseOne(app, true, true);
+ appmenu[/*LANG*/'Erase App,Keep Data'] = () => eraseOne(app, true, false);
+ appmenu[/*LANG*/'Only Erase Data'] = () => eraseOne(app, false, true);
} else {
- appmenu['Erase'] = () => eraseOne(app, true, false);
+ appmenu[/*LANG*/'Erase'] = () => eraseOne(app, true, false);
}
E.showMenu(appmenu);
}
@@ -111,7 +111,7 @@ function showAppMenu(app) {
function showApps() {
const appsmenu = {
'': {
- 'title': 'Apps',
+ 'title': /*LANG*/'Apps',
},
'< Back': () => showMainMenu(),
};
@@ -128,17 +128,17 @@ function showApps() {
menu[app.name] = () => showAppMenu(app);
return menu;
}, appsmenu);
- appsmenu['Erase All'] = () => {
+ appsmenu[/*LANG*/'Erase All'] = () => {
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),
+ '': {'title': /*LANG*/'Erase All'},
+ /*LANG*/'Erase Everything': () => eraseAll(list, true, true),
+ /*LANG*/'Erase Apps,Keep Data': () => eraseAll(list, true, false),
+ /*LANG*/'Only Erase Data': () => eraseAll(list, false, true),
'< Back': () => showApps(),
});
};
} else {
- appsmenu['...No Apps...'] = {
+ appsmenu[/*LANG*/'...No Apps...'] = {
value: undefined,
format: ()=> '',
onchange: ()=> {}
@@ -150,16 +150,16 @@ function showApps() {
function showSortAppsMenu() {
const sorterMenu = {
'': {
- 'title': 'App Sorter',
+ 'title': /*LANG*/'App Sorter',
},
'< Back': () => showMainMenu(),
- 'Sort: manually': ()=> showSortAppsManually(),
- 'Sort: alph. ASC': () => {
- E.showMessage('Sorting:\nAlphabetically\nascending ...');
+ /*LANG*/'Sort: manually': ()=> showSortAppsManually(),
+ /*LANG*/'Sort: alph. ASC': () => {
+ E.showMessage(/*LANG*/'Sorting:\nAlphabetically\nascending ...');
sortAlphabet(false);
},
'Sort: alph. DESC': () => {
- E.showMessage('Sorting:\nAlphabetically\ndescending ...');
+ E.showMessage(/*LANG*/'Sorting:\nAlphabetically\ndescending ...');
sortAlphabet(true);
}
};
@@ -169,7 +169,7 @@ function showSortAppsMenu() {
function showSortAppsManually() {
const appsSorterMenu = {
'': {
- 'title': 'Sort: manually',
+ 'title': /*LANG*/'Sort: manually',
},
'< Back': () => showSortAppsMenu(),
};
@@ -186,7 +186,7 @@ function showSortAppsManually() {
return menu;
}, appsSorterMenu);
} else {
- appsSorterMenu['...No Apps...'] = {
+ appsSorterMenu[/*LANG*/'...No Apps...'] = {
value: undefined,
format: ()=> '',
onchange: ()=> {}
diff --git a/apps/files/metadata.json b/apps/files/metadata.json
index ac73a7717..a53f914e6 100644
--- a/apps/files/metadata.json
+++ b/apps/files/metadata.json
@@ -1,7 +1,7 @@
{
"id": "files",
"name": "App Manager",
- "version": "0.07",
+ "version": "0.08",
"description": "Show currently installed apps, free space, and allow their deletion from the watch",
"icon": "files.png",
"tags": "tool,system,files",
diff --git a/apps/flappy/ChangeLog b/apps/flappy/ChangeLog
index 349cb9d07..d660f85aa 100644
--- a/apps/flappy/ChangeLog
+++ b/apps/flappy/ChangeLog
@@ -2,3 +2,4 @@
0.03: A few tweaks to improve rendering speed
0.04: Add "ram" keyword to allow 2v06 Espruino builds to cache function that needs to be fast
0.05: Don't use Bangle.setLCDMode, just use offscreen buffer (allows widgets)
+0.06: Bangle.js 2 enhancements - remove offscreen buffer and render direct
diff --git a/apps/flappy/app.js b/apps/flappy/app.js
index e9ca31fa5..70553fe97 100644
--- a/apps/flappy/app.js
+++ b/apps/flappy/app.js
@@ -1,19 +1,20 @@
-b = Graphics.createArrayBuffer(120,120,8);
-var gimg = {
- width:120,
- height:104,
- bpp:8,
- buffer:b.buffer
- };
-
+var Y;
if (process.env.HWVERSION==2) {
- b.flip = function() {
- g.drawImage(gimg,28,50);
- };
+ // we have offscreen graphics, so just go direct
+ b = g;
+ Y = 24; // widgets
} else {
+ b = Graphics.createArrayBuffer(120,120,8);
+ var gimg = {
+ width:120,
+ height:104,
+ bpp:8,
+ buffer:b.buffer
+ };
b.flip = function() {
g.drawImage(gimg,0,24,{scale:2});
};
+ Y = 0; // we offset for widgets anyway
}
var BIRDIMG = E.toArrayBuffer(atob("EQyI/v7+/v7+/gAAAAAAAP7+/v7+/v7+/gYG0tLS0gDXAP7+/v7+/v4A0tLS0tIA19fXAP7+/v4AAAAA0tLS0gDX1wDXAP7+ANfX19cA0tLSANfXANcA/v4A19fX19cA0tLSANfX1wD+/gDS19fX0gDS0tLSAAAAAAD+/gDS0tIA0tLS0gDAwMDAwAD+/gAAAM3Nzc0AwAAAAAAA/v7+/v4Azc3Nzc0AwMDAwAD+/v7+/v4AAM3Nzc0AAAAAAP7+/v7+/v7+AAAAAP7+/v7+/g=="))
@@ -30,14 +31,14 @@ function newBarrier(x) {
barriers.push({
x1 : x-7,
x2 : x+7,
- y : 20+Math.random()*38,
+ y : Y+20+Math.random()*38,
gap : 12+Math.random()*15
});
}
function gameStart() {
running = true;
- birdy = 48/2;
+ birdy = Y + 48/2;
birdvy = 0;
barriers = [];
for (var i=38;ibbot))
gameStop();
});
diff --git a/apps/flappy/metadata.json b/apps/flappy/metadata.json
index 910797066..cb50f0094 100644
--- a/apps/flappy/metadata.json
+++ b/apps/flappy/metadata.json
@@ -1,7 +1,7 @@
{
"id": "flappy",
"name": "Flappy Bird",
- "version": "0.05",
+ "version": "0.06",
"description": "A Flappy Bird game clone",
"icon": "app.png",
"screenshots": [{"url":"screenshot1_flappy.png"},{"url":"screenshot2_flappy.png"}],
diff --git a/apps/football/metadata.json b/apps/football/metadata.json
index 253026c39..43e7ac1bf 100644
--- a/apps/football/metadata.json
+++ b/apps/football/metadata.json
@@ -2,7 +2,7 @@
"id": "football",
"name": "football",
"shortName": "football",
- "version": "1.00",
+ "version": "1.01",
"type": "app",
"description": "Classic football game of the CASIO chronometer",
"icon": "app.png",
diff --git a/apps/ftclock/ChangeLog b/apps/ftclock/ChangeLog
index 83ec21ee6..c30dae69f 100644
--- a/apps/ftclock/ChangeLog
+++ b/apps/ftclock/ChangeLog
@@ -1,3 +1,4 @@
0.01: first release
0.02: RAM efficient version of `fourTwentyTz.js` (as suggested by @gfwilliams).
0.03: `mkFourTwentyTz.js` now handles new timezonedb.com CSV format
+0.04: Tell clock widgets to hide.
diff --git a/apps/ftclock/app.js b/apps/ftclock/app.js
index b12db10f1..4f2cef895 100644
--- a/apps/ftclock/app.js
+++ b/apps/ftclock/app.js
@@ -33,6 +33,8 @@ function draw() {
// Clear the screen once, at startup
g.clear();
+// Show launcher when middle button pressed
+Bangle.setUI("clock");
// Load widgets
Bangle.loadWidgets();
Bangle.drawWidgets();
@@ -47,5 +49,4 @@ Bangle.on('lcdPower',on=>{
drawTimeout = undefined;
}
});
-// Show launcher when middle button pressed
-Bangle.setUI("clock");
+
diff --git a/apps/ftclock/metadata.json b/apps/ftclock/metadata.json
index 876feb1bb..96a4f84b9 100644
--- a/apps/ftclock/metadata.json
+++ b/apps/ftclock/metadata.json
@@ -1,7 +1,7 @@
{
"id": "ftclock",
"name": "Four Twenty Clock",
- "version": "0.03",
+ "version": "0.04",
"description": "A clock that tells when and where it's going to be 4:20 next",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}, {"url":"screenshot1.png"}],
diff --git a/apps/fwupdate/ChangeLog b/apps/fwupdate/ChangeLog
index 06f84a11a..ea0b48eb9 100644
--- a/apps/fwupdate/ChangeLog
+++ b/apps/fwupdate/ChangeLog
@@ -5,3 +5,4 @@
0.03: Improve bootloader update safety. Now sets unsafeFlash:1 to allow flash with 2v11 and later
Add CRC checks for common bootloaders that we know don't work
0.04: Include a precompiled bootloader for easy bootloader updates
+0.05: Rename Bootloader->DFU and add explanation to avoid confusion with Bootloader app
diff --git a/apps/fwupdate/custom.html b/apps/fwupdate/custom.html
index 3f8f50b3f..31eb4a256 100644
--- a/apps/fwupdate/custom.html
+++ b/apps/fwupdate/custom.html
@@ -3,7 +3,7 @@
- This tool allows you to update the bootloader on Bangle.js 2 devices
+
This tool allows you to update the firmware on Bangle.js 2 devices
from within the App Loader.
- Your current firmware version is unknown and bootloader is unknown
+ Your current firmware version is unknown and DFU is unknown
-
If you have an early (KickStarter or developer) Bangle.js device and still have the old 2v10.x bootloader, the Firmware Update
- will fail with a message about the bootloader version. If so, please click here to update to bootloader 2v12 and then click the 'Upload' button that appears.
+
If you have an early (KickStarter or developer) Bangle.js device and still have the old 2v10.x DFU, the Firmware Update
+ will fail with a message about the DFU version. If so, please click here to update to DFU 2v12 and then click the 'Upload' button that appears.
The currently available Espruino firmware releases are:
To update, click a link above and then click the 'Upload' button that appears.
-
Advanced ▼
+
+
What is DFU? ▼
+
+
What is DFU?
+
DFU stands for Device Firmware Update . This is the first
+ bit of code that runs when Bangle.js starts, and it is able to update the
+ Bangle.js firmware. Normally you would update firmware via this Firmware
+ Updater app, but if for some reason Bangle.js will not boot, you can
+ always use DFU to to the update manually .
+
DFU is itself a bootloader, but here we're calling it DFU to avoid confusion
+ with the Bootloader app in the app loader (which prepares Bangle.js for running apps).
+
+
+
Advanced ▼
+
Advanced
Firmware updates via this tool work differently to the NRF Connect method mentioned on
the Bangle.js 2 page . Firmware
- is uploaded to a file on the Bangle. Once complete the Bangle reboots and the bootloader copies
+ is uploaded to a file on the Bangle. Once complete the Bangle reboots and DFU copies
the new firmware into internal Storage.
In addition to the links above, you can upload a hex or zip file directly below. This file should be an .app_hex
- file, *not* the normal .hex (as that contains the bootloader as well).
+ file, *not* the normal
.hex (as that contains the DFU as well).
DANGER! No verification is performed on uploaded ZIP or HEX files - you could
- potentially overwrite your bootloader with the wrong binary and brick your Bangle.
+ potentially overwrite your DFU with the wrong binary and brick your Bangle.
Upload
@@ -73,7 +87,7 @@ function onInit(device) {
document.getElementById("fw-ok").style = "";
}
Puck.eval("E.CRC32(E.memoryArea(0xF7000,0x7000))", crc => {
- console.log("Bootloader CRC = "+crc);
+ console.log("DFU CRC = "+crc);
var version = `unknown (CRC ${crc})`;
var ok = true;
if (crc==1339551013) { version = "2v10.219"; ok = false; }
@@ -83,6 +97,7 @@ function onInit(device) {
if (crc==3508163280 || crc==1418074094) version = "2v12";
if (crc==4056371285) version = "2v13";
if (crc==1038322422) version = "2v14";
+ if (crc==2560806221) version = "2v15";
if (!ok) {
version += `(⚠ update required)`;
}
@@ -298,8 +313,8 @@ function createJS_app(binary, startAddress, endAddress) {
bin32[3] = VERSION; // VERSION! Use this to test ourselves
console.log("CRC 0x"+bin32[2].toString(16));
hexJS = "";//`\x10if (E.CRC32(E.memoryArea(${startAddress},${endAddress-startAddress}))==${bin32[2]}) { print("FIRMWARE UP TO DATE!"); load();}\n`;
- hexJS += `\x10if (E.CRC32(E.memoryArea(0xF7000,0x7000))==1339551013) { print("BOOTLOADER 2v10.219 needs update"); load();}\n`;
- hexJS += `\x10if (E.CRC32(E.memoryArea(0xF7000,0x7000))==1207580954) { print("BOOTLOADER 2v10.236 needs update"); load();}\n`;
+ hexJS += `\x10if (E.CRC32(E.memoryArea(0xF7000,0x7000))==1339551013) { print("DFU 2v10.219 needs update"); load();}\n`;
+ hexJS += `\x10if (E.CRC32(E.memoryArea(0xF7000,0x7000))==1207580954) { print("DFU 2v10.236 needs update"); load();}\n`;
hexJS += '\x10var s = require("Storage");\n';
hexJS += '\x10s.erase(".firmware");\n';
var CHUNKSIZE = 2048;
@@ -319,7 +334,7 @@ function createJS_app(binary, startAddress, endAddress) {
function createJS_bootloader(binary, startAddress, endAddress) {
var crc = CRC32(binary);
console.log("CRC 0x"+crc.toString(16));
- hexJS = `\x10if (E.CRC32(E.memoryArea(${startAddress},${endAddress-startAddress}))==${crc}) { print("BOOTLOADER UP TO DATE!"); load();}\n`;
+ hexJS = `\x10if (E.CRC32(E.memoryArea(${startAddress},${endAddress-startAddress}))==${crc}) { print("DFU UP TO DATE!"); load();}\n`;
hexJS += `\x10var _fw = new Uint8Array(${binary.length})\n`;
var CHUNKSIZE = 1024;
for (var i=0;i
{ drawImage("${fileName}"); }`); // Unfortunately, eval is the only reasonable way to do this
+}
+
+let cachedOptions = Bangle.getOptions(); // We will change the backlight and timeouts later, and need to restore them when displaying the menu
+let backlightSetting = storage.readJSON('setting.json').brightness; // LCD brightness is not included in there for some reason
+
+let angle = 0; // Store the angle of rotation
+let image; // Cache the image here because we access it in multiple places
+
+function drawMenu() {
+ Bangle.removeListener('touch', drawMenu); // We no longer want touching to reload the menu
+ Bangle.setOptions(cachedOptions); // The drawImage function set no timeout, undo that
+ Bangle.setLCDBrightness(backlightSetting); // Restore backlight
+ image = undefined; // Delete the image from memory
+
+ E.showMenu(imageMenu);
+}
+
+function drawImage(fileName) {
+ E.showMenu(); // Remove the menu to prevent it from breaking things
+ setTimeout(() => { Bangle.on('touch', drawMenu); }, 300); // Touch the screen to go back to the image menu (300ms timeout to allow user to lift finger)
+ Bangle.setOptions({ // Disable display power saving while showing the image
+ lockTimeout: 0,
+ lcdPowerTimeout: 0,
+ backlightTimeout: 0
+ });
+ Bangle.setLCDBrightness(1); // Full brightness
+
+ image = eval(storage.read(fileName)); // Sadly, the only reasonable way to do this
+ g.clear().reset().setBgColor(0).setColor("#fff").drawImage(image, 88, 88, { rotate: angle });
+}
+
+setWatch(info => {
+ if (image) {
+ if (angle == 0) angle = Math.PI;
+ else angle = 0;
+ Bangle.buzz();
+
+ g.clear().reset().setBgColor(0).setColor("#fff").drawImage(image, 88, 88, { rotate: angle })
+ }
+}, BTN1, { repeat: true });
+
+// We don't load the widgets because there is no reasonable way to unload them
+drawMenu();
diff --git a/apps/gallery/icon.js b/apps/gallery/icon.js
new file mode 100644
index 000000000..11fee53eb
--- /dev/null
+++ b/apps/gallery/icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwgIOLgf/AAX8Av4FBJgkMAos/CIfMAv4Fe4AF/Apq5EAAw"))
\ No newline at end of file
diff --git a/apps/gallery/icon.png b/apps/gallery/icon.png
new file mode 100644
index 000000000..71835e93d
Binary files /dev/null and b/apps/gallery/icon.png differ
diff --git a/apps/gallery/interface.html b/apps/gallery/interface.html
new file mode 100644
index 000000000..f309270ca
--- /dev/null
+++ b/apps/gallery/interface.html
@@ -0,0 +1,165 @@
+
+
+
+
+
+
+
+
+
+ Existing Images
+
+
+ Convert & Upload Images
+
+
+ Use Compression?
+ Transparency to Color
+ Transparency?
+ Inverted?
+ Crop?
+ Diffusion:
+
+ Brightness:
+
+ Contrast:
+
+ Colours:
+
+
+
+ Upload
+
+
+
+
diff --git a/apps/gallery/metadata.json b/apps/gallery/metadata.json
new file mode 100644
index 000000000..00ac42075
--- /dev/null
+++ b/apps/gallery/metadata.json
@@ -0,0 +1,27 @@
+{
+ "id": "gallery",
+ "name": "Gallery",
+ "version": "0.03",
+ "description": "A gallery that lets you view images uploaded with the IDE (see README)",
+ "readme": "README.md",
+ "icon": "icon.png",
+ "type": "app",
+ "tags": "tools",
+ "supports": [
+ "BANGLEJS2",
+ "BANGLEJS"
+ ],
+ "allow_emulator": true,
+ "interface": "interface.html",
+ "storage": [
+ {
+ "name": "gallery.app.js",
+ "url": "app.js"
+ },
+ {
+ "name": "gallery.img",
+ "url": "icon.js",
+ "evaluate": true
+ }
+ ]
+}
diff --git a/apps/gallifr/ChangeLog b/apps/gallifr/ChangeLog
index 0e1f45042..32c1057b0 100644
--- a/apps/gallifr/ChangeLog
+++ b/apps/gallifr/ChangeLog
@@ -1,2 +1,3 @@
0.01: First released version
0.02: Changed setWatch to Bangle.setUI
+0.03: Tell clock widgets to hide.
diff --git a/apps/gallifr/app.js b/apps/gallifr/app.js
index d327bcdc1..8468eee48 100644
--- a/apps/gallifr/app.js
+++ b/apps/gallifr/app.js
@@ -238,10 +238,12 @@ Bangle.on('lcdPower', (on) => {
}
});
+// Show launcher when button pressed
+Bangle.setUI("clock");
+
g.clear();
startTimers();
Bangle.loadWidgets();
drawAll();
-// Show launcher when button pressed
-Bangle.setUI("clock");
+
diff --git a/apps/gallifr/metadata.json b/apps/gallifr/metadata.json
index 9ce7d7f97..96fe243ed 100644
--- a/apps/gallifr/metadata.json
+++ b/apps/gallifr/metadata.json
@@ -2,7 +2,7 @@
"id": "gallifr",
"name": "Time Traveller's Chronometer",
"shortName": "Time Travel Clock",
- "version": "0.02",
+ "version": "0.03",
"description": "A clock for time travellers. The light pie segment shows the minutes, the black circle, the hour. The dial itself reads 'time' just in case you forget.",
"icon": "gallifr.png",
"screenshots": [{"url":"screenshot_time.png"}],
diff --git a/apps/geissclk/ChangeLog b/apps/geissclk/ChangeLog
index 7458fadee..cd46173f7 100644
--- a/apps/geissclk/ChangeLog
+++ b/apps/geissclk/ChangeLog
@@ -1,3 +1,4 @@
0.01: New App!
0.02: BTN2->launcher, use smaller text to allow "20:00" to fit on screen
0.03: Changed setWatch to Bangle.setUI
+0.04: Tell clock widgets to hide.
diff --git a/apps/geissclk/clock.js b/apps/geissclk/clock.js
index f14ea5f39..5401fb142 100644
--- a/apps/geissclk/clock.js
+++ b/apps/geissclk/clock.js
@@ -142,11 +142,13 @@ Bangle.on('lcdPower',function(on) {
animInterval = setInterval(iterate, 50);
}
});
-g.clear();
+
+// Show launcher when button pressed
+Bangle.setUI("clock");g.clear();
+
Bangle.loadWidgets();
Bangle.drawWidgets();
iterate();
animInterval = setInterval(iterate, 50);
-// Show launcher when button pressed
-Bangle.setUI("clock");
+
diff --git a/apps/geissclk/metadata.json b/apps/geissclk/metadata.json
index 456854dbd..68bd2a970 100644
--- a/apps/geissclk/metadata.json
+++ b/apps/geissclk/metadata.json
@@ -1,7 +1,7 @@
{
"id": "geissclk",
"name": "Geiss Clock",
- "version": "0.03",
+ "version": "0.04",
"description": "7 segment clock with animated background in the style of Ryan Geiss' music visualisation. NOTE: The first run will take ~1 minute to do some precalculation",
"icon": "clock.png",
"type": "clock",
diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog
new file mode 100644
index 000000000..3b0d62009
--- /dev/null
+++ b/apps/gipy/ChangeLog
@@ -0,0 +1,65 @@
+0.01: Initial code
+
+0.05:
+ * We now buzz before reaching a waypoint.
+ * Display is only updated when not locked.
+ * We detect leaving path and finding path again.
+ * We display remaining distance to next point.
+
+0.06:
+ * Special display for points with steep turns.
+ * Buzz on points with steep turns and unlock.
+ * Losing gps is now displayed.
+
+0.07:
+ * We now use orientation to detect current segment
+ when segments overlap going in both directions.
+ * File format is now versioned.
+
+0.08:
+ * Don't use gps course anymore but figure it from previous positions.
+ * Bugfix: path colors are back.
+ * Always buzz when reaching waypoint even if unlocked.
+
+0.09:
+ * We now display interest points.
+ * Menu to choose which file to load.
+
+0.10:
+ * Display performances enhancement.
+ * Waypoints information is embedded in file and extracted from comments on
+ points.
+ * Bugfix in map display (last segment was missing + wrong colors).
+ * Waypoint detections using OSM + sharp angles
+ * New algorith for direction detection
+
+0.11:
+ * Better fonts (more free space, still readable).
+ * Display direction to nearest point when lost.
+ * Display average speed.
+ * Turn off gps when locked and between points
+
+0.12:
+ * Bugfix in speed computation.
+ * Bugfix in current segment detection.
+ * Bugfix : lost direction.
+ * Larger fonts.
+ * Detecting next point correctly when going back.
+
+0.13:
+ * Bugfix in lost direction.
+ * Buzzing 100m ahead instead of 50m.
+ * Detect sharp turns.
+ * Display instant speed.
+ * New instant speed algorithm.
+ * Bugfix for remaining distance when going back.
+
+0.14:
+ * Detect starting distance to compute a good average speed.
+ * Settings
+ * Account for breaks in average speed.
+
+0.15:
+ * Record traveled distance to get a good average speed.
+ * Breaks (low speed) will not count in average speed.
+ * Bugfix in average speed.
diff --git a/apps/gipy/README.md b/apps/gipy/README.md
new file mode 100644
index 000000000..6c9b87c23
--- /dev/null
+++ b/apps/gipy/README.md
@@ -0,0 +1,109 @@
+# Gipy
+
+Gipy allows you to follow gpx traces on your watch.
+
+
+
+
+It is for now meant for bicycling and not hiking
+(it uses your movement to figure out your orientation
+and walking is too slow).
+
+It is untested on Banglejs1. If you can try it, you would be welcome.
+
+This software is not perfect but surprisingly useful.
+
+## Features
+
+It provides the following features :
+
+- display the path with current position from gps
+- detects and buzzes if you leave the path
+- buzzes before sharp turns
+- buzzes before nodes with comments
+(for example when you need to turn in https://mapstogpx.com/)
+- display instant / average speed
+- display distance to next node
+- display additional data from openstreetmap :
+ - water points
+ - toilets
+ - artwork
+ - bakeries
+
+optionally it can also:
+
+- try to turn off gps between crossroads to save battery
+
+## Usage
+
+### Preparing the file
+
+You first need to have a trace file in *gpx* format.
+Usually I download from [komoot](https://www.komoot.com/) or I export
+from google maps using [mapstogpx](https://mapstogpx.com/).
+
+Note that *mapstogpx* has a super nice feature in its advanced settings.
+You can turn on 'next turn info' and be warned by the watch when you need to turn.
+
+Once you have your gpx file you need to convert it to *gpc* which is my custom file format.
+They are smaller than gpx and reduce the number of computations left to be done on the watch.
+
+Just click the disk icon and select your gpx file.
+This will request additional information from openstreetmap.
+Your path will be displayed in svg.
+
+### Starting Gipy
+
+Once you start gipy you will have a menu for selecting your trace (if more than one).
+Choose the one you want and here you go :
+
+
+
+On your screen you can see :
+
+- yourself (the big black dot)
+- the path (the top of the screen is in front of you)
+- if needed a projection of yourself on the path (small black dot)
+- extremities of segments as white dots
+- turning points as doubled white dots
+- some text on the left (from top to bottom) :
+ * current time
+ * left distance till end of current segment
+ * distance from start of path / path length
+ * average speed / instant speed
+- interest points from openstreetmap as color dots :
+ * red : bakery
+ * deep blue : water point
+ * cyan : toilets (often doubles as water point)
+ * green : artwork
+- a *turn* indicator on the top right when you reach a turning point
+- a *gps* indicator (blinking) on the top right if you lose gps signal
+- a *lost* indicator on the top right if you stray too far away from path
+- a black segment extending from you when you are lost, indicating the rough direction of where to go
+
+### Settings
+
+Few settings for now (feel free to suggest me more) :
+
+- keep gps alive : if turned off, will try to save battery by turning the gps off on long segments
+- max speed : used to compute how long to turn the gps off
+
+### Caveats
+
+It is good to use but you should know :
+
+- the gps might take a long time to start initially (see the assisted gps update app).
+- gps signal is noisy : there is therefore a small delay for instant speed. sometimes you may jump somewhere else.
+- your gpx trace has been decimated and approximated : the **REAL PATH** might be **A FEW METERS AWAY**
+- sometimes the watch will tell you that you are lost but you are in fact on the path.
+- battery saving by turning off gps is not very well tested (disabled by default).
+- buzzing does not always work: when there is a high load on the watch, the buzzes might just never happen :-(.
+- buzzes are not strong enough to be always easily noticed.
+- be careful when **GOING DOWNHILL AT VERY HIGH SPEED**. I already missed a few turning points and by the time I realized it,
+I had to go back uphill by quite a distance.
+
+## Creator
+
+Feel free to give me feedback : is it useful for you ? what other features would you like ?
+
+frederic.wagner@imag.fr
diff --git a/apps/gipy/TODO b/apps/gipy/TODO
new file mode 100644
index 000000000..53c3530e2
--- /dev/null
+++ b/apps/gipy/TODO
@@ -0,0 +1,25 @@
+
+* bugs
+
+- when exactly on turn, distance to next point is still often 50m
+ -----> it does not buzz very often on turns
+
+- when going backwards we have a tendencing to get a wrong current_segment
+
+* additional features
+
+- config screen
+ - are we on foot (and should use compass)
+
+- we need to buzz 200m before sharp turns (or even better, 30seconds)
+(and look at more than next point)
+
+- display distance to next water/toilet ?
+- dynamic map rescale
+- display scale (100m)
+
+- compress path ?
+
+* misc
+
+- code is becoming messy
diff --git a/apps/gipy/app-icon.js b/apps/gipy/app-icon.js
new file mode 100644
index 000000000..0fc51609f
--- /dev/null
+++ b/apps/gipy/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwkBiIA/AE8VqoAGCy1RiN3CyYuBi93uIXJIBV3AAIuMBY4XjQ5YXPRAIAEOwIABPBC4LF54wGF6IwFC5jWGIwxIJC4xJFgDuJJAxJFC6TEIJBzEHGCIYPGA5JQC44YPGBBJKY4gwRfQL4DGCL4GGCAXPGAxGBAAJIMGAwWCGCoWGC55HHJB5HIC8pGDSChfXC5AWIL5ynOC45GJC4h3IIyYwCFxwADgB1SC44uSC4guSAH4Ab"))
diff --git a/apps/gipy/app.js b/apps/gipy/app.js
new file mode 100644
index 000000000..ae82e5dfb
--- /dev/null
+++ b/apps/gipy/app.js
@@ -0,0 +1,766 @@
+let simulated = false;
+let file_version = 3;
+let code_key = 47490;
+
+var settings = Object.assign(
+ {
+ keep_gps_alive: true,
+ max_speed: 35,
+ },
+ require("Storage").readJSON("gipy.json", true) || {}
+);
+
+let interests_colors = [
+ 0xf800, // Bakery, red
+ 0x001f, // DrinkingWater, blue
+ 0x07ff, // Toilets, cyan
+ 0x07e0, // Artwork, green
+];
+
+function binary_search(array, x) {
+ let start = 0,
+ end = array.length - 1;
+
+ while (start <= end) {
+ let mid = Math.floor((start + end) / 2);
+ if (array[mid] < x) start = mid + 1;
+ else end = mid - 1;
+ }
+ return start;
+}
+
+class Status {
+ constructor(path) {
+ this.path = path;
+ this.on_path = false; // are we on the path or lost ?
+ this.position = null; // where we are
+ this.adjusted_cos_direction = null; // cos of where we look at
+ this.adjusted_sin_direction = null; // sin of where we look at
+ this.current_segment = null; // which segment is closest
+ this.reaching = null; // which waypoint are we reaching ?
+ this.distance_to_next_point = null; // how far are we from next point ?
+ this.paused_time = 0.0; // how long did we stop (stops don't count in avg speed)
+ this.paused_since = getTime();
+
+ let r = [0];
+ // let's do a reversed prefix computations on all distances:
+ // loop on all segments in reversed order
+ let previous_point = null;
+ for (let i = this.path.len - 1; i >= 0; i--) {
+ let point = this.path.point(i);
+ if (previous_point !== null) {
+ r.unshift(r[0] + point.distance(previous_point));
+ }
+ previous_point = point;
+ }
+ this.remaining_distances = r; // how much distance remains at start of each segment
+ this.starting_time = this.paused_since; // time we start
+ this.advanced_distance = 0.0;
+ this.gps_coordinates_counter = 0; // how many coordinates did we receive
+ this.old_points = [];
+ this.old_times = [];
+ }
+ new_position_reached(position) {
+ // we try to figure out direction by looking at previous points
+ // instead of the gps course which is not very nice.
+ this.gps_coordinates_counter += 1;
+ let now = getTime();
+ this.old_points.push(position);
+ this.old_times.push(now);
+
+ if (this.old_points.length == 1) {
+ return null;
+ }
+
+ let last_point = this.old_points[this.old_points.length - 1];
+ let oldest_point = this.old_points[0];
+
+ // every 7 points we count the distance
+ if (this.gps_coordinates_counter % 7 == 0) {
+ let distance = last_point.distance(oldest_point);
+ if (distance < 150.0) {
+ // to avoid gps glitches
+ this.advanced_distance += distance;
+ }
+ }
+
+ if (this.old_points.length == 8) {
+ let p1 = this.old_points[0]
+ .plus(this.old_points[1])
+ .plus(this.old_points[2])
+ .plus(this.old_points[3])
+ .times(1 / 4);
+ let p2 = this.old_points[4]
+ .plus(this.old_points[5])
+ .plus(this.old_points[6])
+ .plus(this.old_points[7])
+ .times(1 / 4);
+ let t1 = (this.old_times[1] + this.old_times[2]) / 2;
+ let t2 = (this.old_times[5] + this.old_times[6]) / 2;
+ this.instant_speed = p1.distance(p2) / (t2 - t1);
+ this.old_points.shift();
+ this.old_times.shift();
+ } else {
+ this.instant_speed =
+ oldest_point.distance(last_point) / (now - this.old_times[0]);
+
+ // update paused time if we are too slow
+ if (this.instant_speed < 2) {
+ if (this.paused_since === null) {
+ this.paused_since = now;
+ }
+ } else {
+ if (this.paused_since !== null) {
+ this.paused_time += now - this.paused_since;
+ this.paused_since = null;
+ }
+ }
+ }
+ // let's just take angle of segment between newest point and a point a bit before
+ let previous_index = this.old_points.length - 3;
+ if (previous_index < 0) {
+ previous_index = 0;
+ }
+ let diff = position.minus(this.old_points[previous_index]);
+ let angle = Math.atan2(diff.lat, diff.lon);
+ return angle;
+ }
+ update_position(new_position, maybe_direction) {
+ let direction = this.new_position_reached(new_position);
+ if (direction === null) {
+ if (maybe_direction === null) {
+ return;
+ } else {
+ direction = maybe_direction;
+ }
+ }
+
+ this.adjusted_cos_direction = Math.cos(-direction - Math.PI / 2.0);
+ this.adjusted_sin_direction = Math.sin(-direction - Math.PI / 2.0);
+ cos_direction = Math.cos(direction);
+ sin_direction = Math.sin(direction);
+ this.position = new_position;
+
+ // detect segment we are on now
+ let res = this.path.nearest_segment(
+ this.position,
+ Math.max(0, this.current_segment - 1),
+ Math.min(this.current_segment + 2, this.path.len - 1),
+ cos_direction,
+ sin_direction
+ );
+ let orientation = res[0];
+ let next_segment = res[1];
+
+ if (this.is_lost(next_segment)) {
+ // it did not work, try anywhere
+ res = this.path.nearest_segment(
+ this.position,
+ 0,
+ this.path.len - 1,
+ cos_direction,
+ sin_direction
+ );
+ orientation = res[0];
+ next_segment = res[1];
+ }
+ // now check if we strayed away from path or back to it
+ let lost = this.is_lost(next_segment);
+ if (this.on_path == lost) {
+ // if status changes
+ if (lost) {
+ Bangle.buzz(); // we lost path
+ setTimeout(() => Bangle.buzz(), 500);
+ setTimeout(() => Bangle.buzz(), 1000);
+ setTimeout(() => Bangle.buzz(), 1500);
+ }
+ this.on_path = !lost;
+ }
+
+ this.current_segment = next_segment;
+
+ // check if we are nearing the next point on our path and alert the user
+ let next_point = this.current_segment + (1 - orientation);
+ this.distance_to_next_point = Math.ceil(
+ this.position.distance(this.path.point(next_point))
+ );
+
+ // disable gps when far from next point and locked
+ if (Bangle.isLocked() && !settings.keep_gps_alive) {
+ let time_to_next_point =
+ (this.distance_to_next_point * 3.6) / settings.max_speed;
+ if (time_to_next_point > 60) {
+ Bangle.setGPSPower(false, "gipy");
+ setTimeout(function () {
+ Bangle.setGPSPower(true, "gipy");
+ }, time_to_next_point);
+ }
+ }
+ if (this.reaching != next_point && this.distance_to_next_point <= 100) {
+ this.reaching = next_point;
+ let reaching_waypoint = this.path.is_waypoint(next_point);
+ if (reaching_waypoint) {
+ Bangle.buzz();
+ setTimeout(() => Bangle.buzz(), 500);
+ setTimeout(() => Bangle.buzz(), 1000);
+ setTimeout(() => Bangle.buzz(), 1500);
+ if (Bangle.isLocked()) {
+ Bangle.setLocked(false);
+ }
+ }
+ }
+ // re-display
+ this.display(orientation);
+ }
+ remaining_distance(orientation) {
+ let remaining_in_correct_orientation =
+ this.remaining_distances[this.current_segment + 1] +
+ this.position.distance(this.path.point(this.current_segment + 1));
+
+ if (orientation == 0) {
+ return remaining_in_correct_orientation;
+ } else {
+ return this.remaining_distances[0] - remaining_in_correct_orientation;
+ }
+ }
+ is_lost(segment) {
+ let distance_to_nearest = this.position.distance_to_segment(
+ this.path.point(segment),
+ this.path.point(segment + 1)
+ );
+ return distance_to_nearest > 50;
+ }
+ display(orientation) {
+ g.clear();
+ this.display_map();
+
+ this.display_interest_points();
+ this.display_stats(orientation);
+ Bangle.drawWidgets();
+ }
+ display_interest_points() {
+ // this is the algorithm in case we have a lot of interest points
+ // let's draw all points for 5 segments centered on current one
+ let starting_group = Math.floor(Math.max(this.current_segment - 2, 0) / 3);
+ let ending_group = Math.floor(
+ Math.min(this.current_segment + 2, this.path.len - 2) / 3
+ );
+ let starting_bucket = binary_search(
+ this.path.interests_starts,
+ starting_group
+ );
+ let ending_bucket = binary_search(
+ this.path.interests_starts,
+ ending_group + 0.5
+ );
+ // we have 5 points per bucket
+ let end_index = Math.min(
+ this.path.interests_types.length - 1,
+ ending_bucket * 5
+ );
+ for (let i = starting_bucket * 5; i <= end_index; i++) {
+ let index = this.path.interests_on_path[i];
+ let interest_point = this.path.interest_point(index);
+ let color = this.path.interest_color(i);
+ let c = interest_point.coordinates(
+ this.position,
+ this.adjusted_cos_direction,
+ this.adjusted_sin_direction
+ );
+ g.setColor(color).fillCircle(c[0], c[1], 5);
+ }
+ }
+ display_stats(orientation) {
+ let remaining_distance = this.remaining_distance(orientation);
+ let rounded_distance = Math.round(remaining_distance / 100) / 10;
+ let total = Math.round(this.remaining_distances[0] / 100) / 10;
+ let now = new Date();
+ let minutes = now.getMinutes().toString();
+ if (minutes.length < 2) {
+ minutes = "0" + minutes;
+ }
+ let hours = now.getHours().toString();
+ g.setFont("6x8:2")
+ .setFontAlign(-1, -1, 0)
+ .setColor(g.theme.fg)
+ .drawString(hours + ":" + minutes, 0, 30);
+
+ g.setFont("6x8:2").drawString(
+ "" + this.distance_to_next_point + "m",
+ 0,
+ g.getHeight() - 49
+ );
+
+ let point_time = this.old_times[this.old_times.length - 1];
+ let done_in = point_time - this.starting_time - this.paused_time;
+ let approximate_speed = Math.round(
+ (this.advanced_distance * 3.6) / done_in
+ );
+ let approximate_instant_speed = Math.round(this.instant_speed * 3.6);
+
+ g.setFont("6x8:2")
+ .setFontAlign(-1, -1, 0)
+ .drawString(
+ "" + approximate_speed + "km/h (in." + approximate_instant_speed + ")",
+ 0,
+ g.getHeight() - 15
+ );
+
+ g.setFont("6x8:2").drawString(
+ "" + rounded_distance + "/" + total,
+ 0,
+ g.getHeight() - 32
+ );
+
+ if (this.distance_to_next_point <= 100) {
+ if (this.path.is_waypoint(this.reaching)) {
+ g.setColor(0.0, 1.0, 0.0)
+ .setFont("6x15")
+ .drawString("turn", g.getWidth() - 50, 30);
+ }
+ }
+ if (!this.on_path) {
+ g.setColor(1.0, 0.0, 0.0)
+ .setFont("6x15")
+ .drawString("lost", g.getWidth() - 55, 35);
+ }
+ }
+ display_map() {
+ // don't display all segments, only those neighbouring current segment
+ // this is most likely to be the correct display
+ // while lowering the cost a lot
+ //
+ // note that all code is inlined here to speed things up from 400ms to 200ms
+ let start = Math.max(this.current_segment - 4, 0);
+ let end = Math.min(this.current_segment + 6, this.path.len);
+ let pos = this.position;
+ let cos = this.adjusted_cos_direction;
+ let sin = this.adjusted_sin_direction;
+ let points = this.path.points;
+ let cx = pos.lon;
+ let cy = pos.lat;
+ let half_width = g.getWidth() / 2;
+ let half_height = g.getHeight() / 2;
+ let previous_x = null;
+ let previous_y = null;
+ for (let i = start; i < end; i++) {
+ let tx = (points[2 * i] - cx) * 40000.0;
+ let ty = (points[2 * i + 1] - cy) * 40000.0;
+ let rotated_x = tx * cos - ty * sin;
+ let rotated_y = tx * sin + ty * cos;
+ let x = half_width - Math.round(rotated_x); // x is inverted
+ let y = half_height + Math.round(rotated_y);
+ if (previous_x !== null) {
+ if (i == this.current_segment + 1) {
+ g.setColor(0.0, 1.0, 0.0);
+ } else {
+ g.setColor(1.0, 0.0, 0.0);
+ }
+ g.drawLine(previous_x, previous_y, x, y);
+
+ if (this.path.is_waypoint(i - 1)) {
+ g.setColor(g.theme.fg);
+ g.fillCircle(previous_x, previous_y, 6);
+ g.setColor(g.theme.bg);
+ g.fillCircle(previous_x, previous_y, 5);
+ }
+ g.setColor(g.theme.fg);
+ g.fillCircle(previous_x, previous_y, 4);
+ g.setColor(g.theme.bg);
+ g.fillCircle(previous_x, previous_y, 3);
+ }
+
+ previous_x = x;
+ previous_y = y;
+ }
+
+ if (this.path.is_waypoint(end - 1)) {
+ g.setColor(g.theme.fg);
+ g.fillCircle(previous_x, previous_y, 6);
+ g.setColor(g.theme.bg);
+ g.fillCircle(previous_x, previous_y, 5);
+ }
+ g.setColor(g.theme.fg);
+ g.fillCircle(previous_x, previous_y, 4);
+ g.setColor(g.theme.bg);
+ g.fillCircle(previous_x, previous_y, 3);
+
+ // now display ourselves
+ g.setColor(g.theme.fgH);
+ g.fillCircle(half_width, half_height, 5);
+
+ // display old points for direction debug
+ // for (let i = 0; i < this.old_points.length; i++) {
+ // let tx = (this.old_points[i].lon - cx) * 40000.0;
+ // let ty = (this.old_points[i].lat - cy) * 40000.0;
+ // let rotated_x = tx * cos - ty * sin;
+ // let rotated_y = tx * sin + ty * cos;
+ // let x = half_width - Math.round(rotated_x); // x is inverted
+ // let y = half_height + Math.round(rotated_y);
+ // g.setColor((i + 1) / 4.0, 0.0, 0.0);
+ // g.fillCircle(x, y, 3);
+ // }
+
+ // display current-segment's projection for debug
+ let projection = pos.closest_segment_point(
+ this.path.point(this.current_segment),
+ this.path.point(this.current_segment + 1)
+ );
+
+ let tx = (projection.lon - cx) * 40000.0;
+ let ty = (projection.lat - cy) * 40000.0;
+ let rotated_x = tx * cos - ty * sin;
+ let rotated_y = tx * sin + ty * cos;
+ let x = half_width - Math.round(rotated_x); // x is inverted
+ let y = half_height + Math.round(rotated_y);
+ g.setColor(g.theme.fg);
+ g.fillCircle(x, y, 4);
+
+ // display direction to next point if lost
+ if (!this.on_path) {
+ let next_point = this.path.point(this.current_segment + 1);
+ let diff = next_point.minus(this.position);
+ let angle = Math.atan2(diff.lat, diff.lon);
+ let tx = Math.cos(angle) * 50.0;
+ let ty = Math.sin(angle) * 50.0;
+ let rotated_x = tx * cos - ty * sin;
+ let rotated_y = tx * sin + ty * cos;
+ let x = half_width - Math.round(rotated_x); // x is inverted
+ let y = half_height + Math.round(rotated_y);
+ g.setColor(g.theme.fgH).drawLine(half_width, half_height, x, y);
+ }
+ }
+}
+
+function load_gpc(filename) {
+ let buffer = require("Storage").readArrayBuffer(filename);
+ let offset = 0;
+
+ // header
+ let header = Uint16Array(buffer, offset, 5);
+ offset += 5 * 2;
+ let key = header[0];
+ let version = header[1];
+ let points_number = header[2];
+ if (key != code_key || version > file_version) {
+ E.showMessage("Invalid gpc file");
+ load();
+ }
+
+ // path points
+ let points = Float64Array(buffer, offset, points_number * 2);
+ offset += 8 * points_number * 2;
+
+ // path waypoints
+ let waypoints_len = Math.ceil(points_number / 8.0);
+ let waypoints = Uint8Array(buffer, offset, waypoints_len);
+ offset += waypoints_len;
+
+ // interest points
+ let interests_number = header[3];
+ let interests_coordinates = Float64Array(
+ buffer,
+ offset,
+ interests_number * 2
+ );
+ offset += 8 * interests_number * 2;
+ let interests_types = Uint8Array(buffer, offset, interests_number);
+ offset += interests_number;
+
+ // interests on path
+ let interests_on_path_number = header[4];
+ let interests_on_path = Uint16Array(buffer, offset, interests_on_path_number);
+ offset += 2 * interests_on_path_number;
+ let starts_length = Math.ceil(interests_on_path_number / 5.0);
+ let interests_starts = Uint16Array(buffer, offset, starts_length);
+ offset += 2 * starts_length;
+
+ return [
+ points,
+ waypoints,
+ interests_coordinates,
+ interests_types,
+ interests_on_path,
+ interests_starts,
+ ];
+}
+
+class Path {
+ constructor(arrays) {
+ this.points = arrays[0];
+ this.waypoints = arrays[1];
+ this.interests_coordinates = arrays[2];
+ this.interests_types = arrays[3];
+ this.interests_on_path = arrays[4];
+ this.interests_starts = arrays[5];
+ }
+
+ is_waypoint(point_index) {
+ let i = Math.floor(point_index / 8);
+ let subindex = point_index % 8;
+ let r = this.waypoints[i] & (1 << subindex);
+ return r != 0;
+ }
+
+ // execute op on all segments.
+ // start is index of first wanted segment
+ // end is 1 after index of last wanted segment
+ on_segments(op, start, end) {
+ let previous_point = null;
+ for (let i = start; i < end + 1; i++) {
+ let point = new Point(this.points[2 * i], this.points[2 * i + 1]);
+ if (previous_point !== null) {
+ op(previous_point, point, i);
+ }
+ previous_point = point;
+ }
+ }
+
+ // return point at given index
+ point(index) {
+ let lon = this.points[2 * index];
+ let lat = this.points[2 * index + 1];
+ return new Point(lon, lat);
+ }
+
+ interest_point(index) {
+ let lon = this.interests_coordinates[2 * index];
+ let lat = this.interests_coordinates[2 * index + 1];
+ return new Point(lon, lat);
+ }
+
+ interest_color(index) {
+ return interests_colors[this.interests_types[index]];
+ }
+
+ // return index of segment which is nearest from point.
+ // we need a direction because we need there is an ambiguity
+ // for overlapping segments which are taken once to go and once to come back.
+ // (in the other direction).
+ nearest_segment(point, start, end, cos_direction, sin_direction) {
+ // we are going to compute two min distances, one for each direction.
+ let indices = [0, 0];
+ let mins = [Number.MAX_VALUE, Number.MAX_VALUE];
+ this.on_segments(
+ function (p1, p2, i) {
+ // we use the dot product to figure out if oriented correctly
+ // let distance = point.fake_distance_to_segment(p1, p2);
+
+ let projection = point.closest_segment_point(p1, p2);
+ let distance = point.fake_distance(projection);
+
+ // let d = projection.minus(point).times(40000.0);
+ // let rotated_x = d.lon * acos - d.lat * asin;
+ // let rotated_y = d.lon * asin + d.lat * acos;
+ // let x = g.getWidth() / 2 - Math.round(rotated_x); // x is inverted
+ // let y = g.getHeight() / 2 + Math.round(rotated_y);
+ //
+ let diff = p2.minus(p1);
+ let dot = cos_direction * diff.lon + sin_direction * diff.lat;
+ let orientation = +(dot < 0); // index 0 is good orientation
+ // g.setColor(0.0, 0.0 + orientation, 1.0 - orientation).fillCircle(
+ // x,
+ // y,
+ // 10
+ // );
+ if (distance <= mins[orientation]) {
+ mins[orientation] = distance;
+ indices[orientation] = i - 1;
+ }
+ },
+ start,
+ end
+ );
+ // by default correct orientation (0) wins
+ // but if other one is really closer, return other one
+ if (mins[1] < mins[0] / 10.0) {
+ return [1, indices[1]];
+ } else {
+ return [0, indices[0]];
+ }
+ }
+ get len() {
+ return this.points.length / 2;
+ }
+}
+
+class Point {
+ constructor(lon, lat) {
+ this.lon = lon;
+ this.lat = lat;
+ }
+ coordinates(current_position, cos_direction, sin_direction) {
+ let translated = this.minus(current_position).times(40000.0);
+ let rotated_x =
+ translated.lon * cos_direction - translated.lat * sin_direction;
+ let rotated_y =
+ translated.lon * sin_direction + translated.lat * cos_direction;
+ return [
+ g.getWidth() / 2 - Math.round(rotated_x), // x is inverted
+ g.getHeight() / 2 + Math.round(rotated_y),
+ ];
+ }
+ minus(other_point) {
+ let xdiff = this.lon - other_point.lon;
+ let ydiff = this.lat - other_point.lat;
+ return new Point(xdiff, ydiff);
+ }
+ plus(other_point) {
+ return new Point(this.lon + other_point.lon, this.lat + other_point.lat);
+ }
+ length_squared(other_point) {
+ let d = this.minus(other_point);
+ return d.lon * d.lon + d.lat * d.lat;
+ }
+ times(scalar) {
+ return new Point(this.lon * scalar, this.lat * scalar);
+ }
+ dot(other_point) {
+ return this.lon * other_point.lon + this.lat * other_point.lat;
+ }
+ distance(other_point) {
+ //see https://www.movable-type.co.uk/scripts/latlong.html
+ const R = 6371e3; // metres
+ const phi1 = (this.lat * Math.PI) / 180;
+ const phi2 = (other_point.lat * Math.PI) / 180;
+ const deltaphi = ((other_point.lat - this.lat) * Math.PI) / 180;
+ const deltalambda = ((other_point.lon - this.lon) * Math.PI) / 180;
+
+ const a =
+ Math.sin(deltaphi / 2) * Math.sin(deltaphi / 2) +
+ Math.cos(phi1) *
+ Math.cos(phi2) *
+ Math.sin(deltalambda / 2) *
+ Math.sin(deltalambda / 2);
+ const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+
+ return R * c; // in meters
+ }
+ fake_distance(other_point) {
+ return Math.sqrt(this.length_squared(other_point));
+ }
+ closest_segment_point(v, w) {
+ // from : https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment
+ // Return minimum distance between line segment vw and point p
+ let l2 = v.length_squared(w); // i.e. |w-v|^2 - avoid a sqrt
+ if (l2 == 0.0) {
+ return v; // v == w case
+ }
+ // Consider the line extending the segment, parameterized as v + t (w - v).
+ // We find projection of point p onto the line.
+ // It falls where t = [(p-v) . (w-v)] / |w-v|^2
+ // We clamp t from [0,1] to handle points outside the segment vw.
+ let t = Math.max(0, Math.min(1, this.minus(v).dot(w.minus(v)) / l2));
+ return v.plus(w.minus(v).times(t)); // Projection falls on the segment
+ }
+ distance_to_segment(v, w) {
+ let projection = this.closest_segment_point(v, w);
+ return this.distance(projection);
+ }
+ fake_distance_to_segment(v, w) {
+ let projection = this.closest_segment_point(v, w);
+ return this.fake_distance(projection);
+ }
+}
+
+Bangle.loadWidgets();
+
+let fake_gps_point = 0.0;
+function simulate_gps(status) {
+ if (fake_gps_point > status.path.len - 1) {
+ return;
+ }
+ let point_index = Math.floor(fake_gps_point);
+ if (point_index >= status.path.len) {
+ return;
+ }
+ //let p1 = status.path.point(0);
+ //let n = status.path.len;
+ //let p2 = status.path.point(n - 1);
+ let p1 = status.path.point(point_index);
+ let p2 = status.path.point(point_index + 1);
+
+ let alpha = fake_gps_point - point_index;
+ let pos = p1.times(1 - alpha).plus(p2.times(alpha));
+ let old_pos = status.position;
+
+ fake_gps_point += 0.05; // advance simulation
+ status.update_position(pos, null);
+}
+
+function drawMenu() {
+ const menu = {
+ "": { title: "choose trace" },
+ };
+ var files = require("Storage").list(".gpc");
+ for (var i = 0; i < files.length; ++i) {
+ menu[files[i]] = start.bind(null, files[i]);
+ }
+ menu["Exit"] = function () {
+ load();
+ };
+ E.showMenu(menu);
+}
+
+function start(fn) {
+ E.showMenu();
+ console.log("loading", fn);
+
+ // let path = new Path(load_gpx("test.gpx"));
+ let path = new Path(load_gpc(fn));
+ let status = new Status(path);
+
+ if (simulated) {
+ status.position = new Point(status.path.point(0));
+ setInterval(simulate_gps, 500, status);
+ } else {
+ // let's display start while waiting for gps signal
+ let p1 = status.path.point(0);
+ let p2 = status.path.point(1);
+ let diff = p2.minus(p1);
+ let direction = Math.atan2(diff.lat, diff.lon);
+ Bangle.setLocked(false);
+ status.update_position(p1, direction);
+
+ let frame = 0;
+ let set_coordinates = function (data) {
+ frame += 1;
+ // 0,0 coordinates are considered invalid since we sometimes receive them out of nowhere
+ let valid_coordinates =
+ !isNaN(data.lat) &&
+ !isNaN(data.lon) &&
+ (data.lat != 0.0 || data.lon != 0.0);
+ if (valid_coordinates) {
+ status.update_position(new Point(data.lon, data.lat), null);
+ }
+ let gps_status_color;
+ if (frame % 2 == 0 || valid_coordinates) {
+ gps_status_color = g.theme.bg;
+ } else {
+ gps_status_color = g.theme.fg;
+ }
+ g.setColor(gps_status_color)
+ .setFont("6x8:2")
+ .drawString("gps", g.getWidth() - 40, 30);
+ };
+
+ Bangle.setGPSPower(true, "gipy");
+ Bangle.on("GPS", set_coordinates);
+ Bangle.on("lock", function (on) {
+ if (!on) {
+ Bangle.setGPSPower(true, "gipy"); // activate gps when unlocking
+ }
+ });
+ }
+}
+
+let files = require("Storage").list(".gpc");
+if (files.length <= 1) {
+ if (files.length == 0) {
+ load();
+ } else {
+ start(files[0]);
+ }
+} else {
+ drawMenu();
+}
diff --git a/apps/gipy/gipy.png b/apps/gipy/gipy.png
new file mode 100644
index 000000000..e9e472f5c
Binary files /dev/null and b/apps/gipy/gipy.png differ
diff --git a/apps/gipy/interface.html b/apps/gipy/interface.html
new file mode 100644
index 000000000..a1c405ed7
--- /dev/null
+++ b/apps/gipy/interface.html
@@ -0,0 +1,196 @@
+
+
+
+
+
+
+
+ Please select a gpx file to be converted to gpc and loaded.
+
+
+ gpx file :
+
+ gpc filename : .gpc (max 24 characters)
+
+
+ fetch interests from openstreetmap
+
+
+ nice tags could be :
+ shop/bicycle, amenity/bank, shop/supermarket, leisure/picnic_table, tourism/information, amenity/pharmacy
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/gipy/metadata.json b/apps/gipy/metadata.json
new file mode 100644
index 000000000..2d06a7c2d
--- /dev/null
+++ b/apps/gipy/metadata.json
@@ -0,0 +1,23 @@
+{
+ "id": "gipy",
+ "name": "Gipy",
+ "shortName": "Gipy",
+ "version": "0.15",
+ "description": "Follow gpx files",
+ "allow_emulator":false,
+ "icon": "gipy.png",
+ "type": "app",
+ "tags": "tool,outdoors,gps",
+ "screenshots": [],
+ "supports": ["BANGLEJS2"],
+ "readme": "README.md",
+ "interface": "interface.html",
+ "storage": [
+ {"name":"gipy.app.js","url":"app.js"},
+ {"name":"gipy.settings.js","url":"settings.js"},
+ {"name":"gipy.img","url":"app-icon.js","evaluate":true}
+ ],
+ "data": [
+ {"name":"gipy.json"}
+ ]
+}
diff --git a/apps/gipy/pkg/gpconv.d.ts b/apps/gipy/pkg/gpconv.d.ts
new file mode 100644
index 000000000..ecffa7b69
--- /dev/null
+++ b/apps/gipy/pkg/gpconv.d.ts
@@ -0,0 +1,75 @@
+/* tslint:disable */
+/* eslint-disable */
+/**
+* @param {GpcSvg} gpcsvg
+* @returns {Uint8Array}
+*/
+export function get_gpc(gpcsvg: GpcSvg): Uint8Array;
+/**
+* @param {GpcSvg} gpcsvg
+* @returns {Uint8Array}
+*/
+export function get_svg(gpcsvg: GpcSvg): Uint8Array;
+/**
+* @param {string} input_str
+* @returns {Promise}
+*/
+export function convert_gpx_strings_no_osm(input_str: string): Promise;
+/**
+* @param {string} input_str
+* @param {string} key1
+* @param {string} value1
+* @param {string} key2
+* @param {string} value2
+* @param {string} key3
+* @param {string} value3
+* @param {string} key4
+* @param {string} value4
+* @returns {Promise}
+*/
+export function convert_gpx_strings(input_str: string, key1: string, value1: string, key2: string, value2: string, key3: string, value3: string, key4: string, value4: string): Promise;
+/**
+*/
+export class GpcSvg {
+ free(): void;
+}
+
+export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
+
+export interface InitOutput {
+ readonly memory: WebAssembly.Memory;
+ readonly __wbg_gpcsvg_free: (a: number) => void;
+ readonly get_gpc: (a: number, b: number) => void;
+ readonly get_svg: (a: number, b: number) => void;
+ readonly convert_gpx_strings_no_osm: (a: number, b: number) => number;
+ readonly convert_gpx_strings: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number, m: number, n: number, o: number, p: number, q: number, r: number) => number;
+ readonly __wbindgen_malloc: (a: number) => number;
+ readonly __wbindgen_realloc: (a: number, b: number, c: number) => number;
+ readonly __wbindgen_export_2: WebAssembly.Table;
+ readonly _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h0601691a32604cdd: (a: number, b: number, c: number) => void;
+ readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
+ readonly __wbindgen_free: (a: number, b: number) => void;
+ readonly __wbindgen_exn_store: (a: number) => void;
+ readonly wasm_bindgen__convert__closures__invoke2_mut__h25ed812378167476: (a: number, b: number, c: number, d: number) => void;
+}
+
+export type SyncInitInput = BufferSource | WebAssembly.Module;
+/**
+* Instantiates the given `module`, which can either be bytes or
+* a precompiled `WebAssembly.Module`.
+*
+* @param {SyncInitInput} module
+*
+* @returns {InitOutput}
+*/
+export function initSync(module: SyncInitInput): InitOutput;
+
+/**
+* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
+* for everything else, calls `WebAssembly.instantiate` directly.
+*
+* @param {InitInput | Promise} module_or_path
+*
+* @returns {Promise}
+*/
+export default function init (module_or_path?: InitInput | Promise): Promise;
diff --git a/apps/gipy/pkg/gpconv.js b/apps/gipy/pkg/gpconv.js
new file mode 100644
index 000000000..97b37e340
--- /dev/null
+++ b/apps/gipy/pkg/gpconv.js
@@ -0,0 +1,645 @@
+
+let wasm;
+
+const heap = new Array(32).fill(undefined);
+
+heap.push(undefined, null, true, false);
+
+function getObject(idx) { return heap[idx]; }
+
+let heap_next = heap.length;
+
+function dropObject(idx) {
+ if (idx < 36) return;
+ heap[idx] = heap_next;
+ heap_next = idx;
+}
+
+function takeObject(idx) {
+ const ret = getObject(idx);
+ dropObject(idx);
+ return ret;
+}
+
+let WASM_VECTOR_LEN = 0;
+
+let cachedUint8Memory0 = new Uint8Array();
+
+function getUint8Memory0() {
+ if (cachedUint8Memory0.byteLength === 0) {
+ cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer);
+ }
+ return cachedUint8Memory0;
+}
+
+const cachedTextEncoder = new TextEncoder('utf-8');
+
+const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
+ ? function (arg, view) {
+ return cachedTextEncoder.encodeInto(arg, view);
+}
+ : function (arg, view) {
+ const buf = cachedTextEncoder.encode(arg);
+ view.set(buf);
+ return {
+ read: arg.length,
+ written: buf.length
+ };
+});
+
+function passStringToWasm0(arg, malloc, realloc) {
+
+ if (realloc === undefined) {
+ const buf = cachedTextEncoder.encode(arg);
+ const ptr = malloc(buf.length);
+ getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf);
+ WASM_VECTOR_LEN = buf.length;
+ return ptr;
+ }
+
+ let len = arg.length;
+ let ptr = malloc(len);
+
+ const mem = getUint8Memory0();
+
+ let offset = 0;
+
+ for (; offset < len; offset++) {
+ const code = arg.charCodeAt(offset);
+ if (code > 0x7F) break;
+ mem[ptr + offset] = code;
+ }
+
+ if (offset !== len) {
+ if (offset !== 0) {
+ arg = arg.slice(offset);
+ }
+ ptr = realloc(ptr, len, len = offset + arg.length * 3);
+ const view = getUint8Memory0().subarray(ptr + offset, ptr + len);
+ const ret = encodeString(arg, view);
+
+ offset += ret.written;
+ }
+
+ WASM_VECTOR_LEN = offset;
+ return ptr;
+}
+
+function isLikeNone(x) {
+ return x === undefined || x === null;
+}
+
+let cachedInt32Memory0 = new Int32Array();
+
+function getInt32Memory0() {
+ if (cachedInt32Memory0.byteLength === 0) {
+ cachedInt32Memory0 = new Int32Array(wasm.memory.buffer);
+ }
+ return cachedInt32Memory0;
+}
+
+const cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
+
+cachedTextDecoder.decode();
+
+function getStringFromWasm0(ptr, len) {
+ return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
+}
+
+function addHeapObject(obj) {
+ if (heap_next === heap.length) heap.push(heap.length + 1);
+ const idx = heap_next;
+ heap_next = heap[idx];
+
+ heap[idx] = obj;
+ return idx;
+}
+
+function debugString(val) {
+ // primitive types
+ const type = typeof val;
+ if (type == 'number' || type == 'boolean' || val == null) {
+ return `${val}`;
+ }
+ if (type == 'string') {
+ return `"${val}"`;
+ }
+ if (type == 'symbol') {
+ const description = val.description;
+ if (description == null) {
+ return 'Symbol';
+ } else {
+ return `Symbol(${description})`;
+ }
+ }
+ if (type == 'function') {
+ const name = val.name;
+ if (typeof name == 'string' && name.length > 0) {
+ return `Function(${name})`;
+ } else {
+ return 'Function';
+ }
+ }
+ // objects
+ if (Array.isArray(val)) {
+ const length = val.length;
+ let debug = '[';
+ if (length > 0) {
+ debug += debugString(val[0]);
+ }
+ for(let i = 1; i < length; i++) {
+ debug += ', ' + debugString(val[i]);
+ }
+ debug += ']';
+ return debug;
+ }
+ // Test for built-in
+ const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val));
+ let className;
+ if (builtInMatches.length > 1) {
+ className = builtInMatches[1];
+ } else {
+ // Failed to match the standard '[object ClassName]'
+ return toString.call(val);
+ }
+ if (className == 'Object') {
+ // we're a user defined class or Object
+ // JSON.stringify avoids problems with cycles, and is generally much
+ // easier than looping through ownProperties of `val`.
+ try {
+ return 'Object(' + JSON.stringify(val) + ')';
+ } catch (_) {
+ return 'Object';
+ }
+ }
+ // errors
+ if (val instanceof Error) {
+ return `${val.name}: ${val.message}\n${val.stack}`;
+ }
+ // TODO we could test for more things here, like `Set`s and `Map`s.
+ return className;
+}
+
+function makeMutClosure(arg0, arg1, dtor, f) {
+ const state = { a: arg0, b: arg1, cnt: 1, dtor };
+ const real = (...args) => {
+ // First up with a closure we increment the internal reference
+ // count. This ensures that the Rust closure environment won't
+ // be deallocated while we're invoking it.
+ state.cnt++;
+ const a = state.a;
+ state.a = 0;
+ try {
+ return f(a, state.b, ...args);
+ } finally {
+ if (--state.cnt === 0) {
+ wasm.__wbindgen_export_2.get(state.dtor)(a, state.b);
+
+ } else {
+ state.a = a;
+ }
+ }
+ };
+ real.original = state;
+
+ return real;
+}
+function __wbg_adapter_24(arg0, arg1, arg2) {
+ wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h0601691a32604cdd(arg0, arg1, addHeapObject(arg2));
+}
+
+function _assertClass(instance, klass) {
+ if (!(instance instanceof klass)) {
+ throw new Error(`expected instance of ${klass.name}`);
+ }
+ return instance.ptr;
+}
+
+function getArrayU8FromWasm0(ptr, len) {
+ return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len);
+}
+/**
+* @param {GpcSvg} gpcsvg
+* @returns {Uint8Array}
+*/
+export function get_gpc(gpcsvg) {
+ try {
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
+ _assertClass(gpcsvg, GpcSvg);
+ wasm.get_gpc(retptr, gpcsvg.ptr);
+ var r0 = getInt32Memory0()[retptr / 4 + 0];
+ var r1 = getInt32Memory0()[retptr / 4 + 1];
+ var v0 = getArrayU8FromWasm0(r0, r1).slice();
+ wasm.__wbindgen_free(r0, r1 * 1);
+ return v0;
+ } finally {
+ wasm.__wbindgen_add_to_stack_pointer(16);
+ }
+}
+
+/**
+* @param {GpcSvg} gpcsvg
+* @returns {Uint8Array}
+*/
+export function get_svg(gpcsvg) {
+ try {
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
+ _assertClass(gpcsvg, GpcSvg);
+ wasm.get_svg(retptr, gpcsvg.ptr);
+ var r0 = getInt32Memory0()[retptr / 4 + 0];
+ var r1 = getInt32Memory0()[retptr / 4 + 1];
+ var v0 = getArrayU8FromWasm0(r0, r1).slice();
+ wasm.__wbindgen_free(r0, r1 * 1);
+ return v0;
+ } finally {
+ wasm.__wbindgen_add_to_stack_pointer(16);
+ }
+}
+
+/**
+* @param {string} input_str
+* @returns {Promise}
+*/
+export function convert_gpx_strings_no_osm(input_str) {
+ const ptr0 = passStringToWasm0(input_str, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
+ const len0 = WASM_VECTOR_LEN;
+ const ret = wasm.convert_gpx_strings_no_osm(ptr0, len0);
+ return takeObject(ret);
+}
+
+/**
+* @param {string} input_str
+* @param {string} key1
+* @param {string} value1
+* @param {string} key2
+* @param {string} value2
+* @param {string} key3
+* @param {string} value3
+* @param {string} key4
+* @param {string} value4
+* @returns {Promise}
+*/
+export function convert_gpx_strings(input_str, key1, value1, key2, value2, key3, value3, key4, value4) {
+ const ptr0 = passStringToWasm0(input_str, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
+ const len0 = WASM_VECTOR_LEN;
+ const ptr1 = passStringToWasm0(key1, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
+ const len1 = WASM_VECTOR_LEN;
+ const ptr2 = passStringToWasm0(value1, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
+ const len2 = WASM_VECTOR_LEN;
+ const ptr3 = passStringToWasm0(key2, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
+ const len3 = WASM_VECTOR_LEN;
+ const ptr4 = passStringToWasm0(value2, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
+ const len4 = WASM_VECTOR_LEN;
+ const ptr5 = passStringToWasm0(key3, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
+ const len5 = WASM_VECTOR_LEN;
+ const ptr6 = passStringToWasm0(value3, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
+ const len6 = WASM_VECTOR_LEN;
+ const ptr7 = passStringToWasm0(key4, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
+ const len7 = WASM_VECTOR_LEN;
+ const ptr8 = passStringToWasm0(value4, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
+ const len8 = WASM_VECTOR_LEN;
+ const ret = wasm.convert_gpx_strings(ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3, ptr4, len4, ptr5, len5, ptr6, len6, ptr7, len7, ptr8, len8);
+ return takeObject(ret);
+}
+
+function handleError(f, args) {
+ try {
+ return f.apply(this, args);
+ } catch (e) {
+ wasm.__wbindgen_exn_store(addHeapObject(e));
+ }
+}
+function __wbg_adapter_69(arg0, arg1, arg2, arg3) {
+ wasm.wasm_bindgen__convert__closures__invoke2_mut__h25ed812378167476(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
+}
+
+/**
+*/
+export class GpcSvg {
+
+ static __wrap(ptr) {
+ const obj = Object.create(GpcSvg.prototype);
+ obj.ptr = ptr;
+
+ return obj;
+ }
+
+ __destroy_into_raw() {
+ const ptr = this.ptr;
+ this.ptr = 0;
+
+ return ptr;
+ }
+
+ free() {
+ const ptr = this.__destroy_into_raw();
+ wasm.__wbg_gpcsvg_free(ptr);
+ }
+}
+
+async function load(module, imports) {
+ if (typeof Response === 'function' && module instanceof Response) {
+ if (typeof WebAssembly.instantiateStreaming === 'function') {
+ try {
+ return await WebAssembly.instantiateStreaming(module, imports);
+
+ } catch (e) {
+ if (module.headers.get('Content-Type') != 'application/wasm') {
+ console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
+
+ } else {
+ throw e;
+ }
+ }
+ }
+
+ const bytes = await module.arrayBuffer();
+ return await WebAssembly.instantiate(bytes, imports);
+
+ } else {
+ const instance = await WebAssembly.instantiate(module, imports);
+
+ if (instance instanceof WebAssembly.Instance) {
+ return { instance, module };
+
+ } else {
+ return instance;
+ }
+ }
+}
+
+function getImports() {
+ const imports = {};
+ imports.wbg = {};
+ imports.wbg.__wbindgen_object_drop_ref = function(arg0) {
+ takeObject(arg0);
+ };
+ imports.wbg.__wbg_gpcsvg_new = function(arg0) {
+ const ret = GpcSvg.__wrap(arg0);
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbindgen_string_get = function(arg0, arg1) {
+ const obj = getObject(arg1);
+ const ret = typeof(obj) === 'string' ? obj : undefined;
+ var ptr0 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
+ var len0 = WASM_VECTOR_LEN;
+ getInt32Memory0()[arg0 / 4 + 1] = len0;
+ getInt32Memory0()[arg0 / 4 + 0] = ptr0;
+ };
+ imports.wbg.__wbindgen_string_new = function(arg0, arg1) {
+ const ret = getStringFromWasm0(arg0, arg1);
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbindgen_object_clone_ref = function(arg0) {
+ const ret = getObject(arg0);
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbg_fetch_386f87a3ebf5003c = function(arg0) {
+ const ret = fetch(getObject(arg0));
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbindgen_cb_drop = function(arg0) {
+ const obj = takeObject(arg0).original;
+ if (obj.cnt-- == 1) {
+ obj.a = 0;
+ return true;
+ }
+ const ret = false;
+ return ret;
+ };
+ imports.wbg.__wbg_fetch_749a56934f95c96c = function(arg0, arg1) {
+ const ret = getObject(arg0).fetch(getObject(arg1));
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbg_instanceof_Response_eaa426220848a39e = function(arg0) {
+ let result;
+ try {
+ result = getObject(arg0) instanceof Response;
+ } catch {
+ result = false;
+ }
+ const ret = result;
+ return ret;
+ };
+ imports.wbg.__wbg_url_74285ddf2747cb3d = function(arg0, arg1) {
+ const ret = getObject(arg1).url;
+ const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
+ const len0 = WASM_VECTOR_LEN;
+ getInt32Memory0()[arg0 / 4 + 1] = len0;
+ getInt32Memory0()[arg0 / 4 + 0] = ptr0;
+ };
+ imports.wbg.__wbg_status_c4ef3dd591e63435 = function(arg0) {
+ const ret = getObject(arg0).status;
+ return ret;
+ };
+ imports.wbg.__wbg_headers_fd64ad685cf22e5d = function(arg0) {
+ const ret = getObject(arg0).headers;
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbg_text_1169d752cc697903 = function() { return handleError(function (arg0) {
+ const ret = getObject(arg0).text();
+ return addHeapObject(ret);
+ }, arguments) };
+ imports.wbg.__wbg_newwithstrandinit_05d7180788420c40 = function() { return handleError(function (arg0, arg1, arg2) {
+ const ret = new Request(getStringFromWasm0(arg0, arg1), getObject(arg2));
+ return addHeapObject(ret);
+ }, arguments) };
+ imports.wbg.__wbg_new_2d0053ee81e4dd2a = function() { return handleError(function () {
+ const ret = new Headers();
+ return addHeapObject(ret);
+ }, arguments) };
+ imports.wbg.__wbg_append_de37df908812970d = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
+ getObject(arg0).append(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
+ }, arguments) };
+ imports.wbg.__wbindgen_is_object = function(arg0) {
+ const val = getObject(arg0);
+ const ret = typeof(val) === 'object' && val !== null;
+ return ret;
+ };
+ imports.wbg.__wbg_newnoargs_b5b063fc6c2f0376 = function(arg0, arg1) {
+ const ret = new Function(getStringFromWasm0(arg0, arg1));
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbg_next_579e583d33566a86 = function(arg0) {
+ const ret = getObject(arg0).next;
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbindgen_is_function = function(arg0) {
+ const ret = typeof(getObject(arg0)) === 'function';
+ return ret;
+ };
+ imports.wbg.__wbg_value_1ccc36bc03462d71 = function(arg0) {
+ const ret = getObject(arg0).value;
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbg_iterator_6f9d4f28845f426c = function() {
+ const ret = Symbol.iterator;
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbg_new_0b9bfdd97583284e = function() {
+ const ret = new Object();
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbg_self_6d479506f72c6a71 = function() { return handleError(function () {
+ const ret = self.self;
+ return addHeapObject(ret);
+ }, arguments) };
+ imports.wbg.__wbg_window_f2557cc78490aceb = function() { return handleError(function () {
+ const ret = window.window;
+ return addHeapObject(ret);
+ }, arguments) };
+ imports.wbg.__wbg_globalThis_7f206bda628d5286 = function() { return handleError(function () {
+ const ret = globalThis.globalThis;
+ return addHeapObject(ret);
+ }, arguments) };
+ imports.wbg.__wbg_global_ba75c50d1cf384f4 = function() { return handleError(function () {
+ const ret = global.global;
+ return addHeapObject(ret);
+ }, arguments) };
+ imports.wbg.__wbindgen_is_undefined = function(arg0) {
+ const ret = getObject(arg0) === undefined;
+ return ret;
+ };
+ imports.wbg.__wbg_call_97ae9d8645dc388b = function() { return handleError(function (arg0, arg1) {
+ const ret = getObject(arg0).call(getObject(arg1));
+ return addHeapObject(ret);
+ }, arguments) };
+ imports.wbg.__wbg_call_168da88779e35f61 = function() { return handleError(function (arg0, arg1, arg2) {
+ const ret = getObject(arg0).call(getObject(arg1), getObject(arg2));
+ return addHeapObject(ret);
+ }, arguments) };
+ imports.wbg.__wbg_next_aaef7c8aa5e212ac = function() { return handleError(function (arg0) {
+ const ret = getObject(arg0).next();
+ return addHeapObject(ret);
+ }, arguments) };
+ imports.wbg.__wbg_done_1b73b0672e15f234 = function(arg0) {
+ const ret = getObject(arg0).done;
+ return ret;
+ };
+ imports.wbg.__wbg_new_9962f939219f1820 = function(arg0, arg1) {
+ try {
+ var state0 = {a: arg0, b: arg1};
+ var cb0 = (arg0, arg1) => {
+ const a = state0.a;
+ state0.a = 0;
+ try {
+ return __wbg_adapter_69(a, state0.b, arg0, arg1);
+ } finally {
+ state0.a = a;
+ }
+ };
+ const ret = new Promise(cb0);
+ return addHeapObject(ret);
+ } finally {
+ state0.a = state0.b = 0;
+ }
+ };
+ imports.wbg.__wbg_resolve_99fe17964f31ffc0 = function(arg0) {
+ const ret = Promise.resolve(getObject(arg0));
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbg_then_11f7a54d67b4bfad = function(arg0, arg1) {
+ const ret = getObject(arg0).then(getObject(arg1));
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbg_then_cedad20fbbd9418a = function(arg0, arg1, arg2) {
+ const ret = getObject(arg0).then(getObject(arg1), getObject(arg2));
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbg_buffer_3f3d764d4747d564 = function(arg0) {
+ const ret = getObject(arg0).buffer;
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbg_newwithbyteoffsetandlength_d9aa266703cb98be = function(arg0, arg1, arg2) {
+ const ret = new Uint8Array(getObject(arg0), arg1 >>> 0, arg2 >>> 0);
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbg_new_8c3f0052272a457a = function(arg0) {
+ const ret = new Uint8Array(getObject(arg0));
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbg_stringify_d6471d300ded9b68 = function() { return handleError(function (arg0) {
+ const ret = JSON.stringify(getObject(arg0));
+ return addHeapObject(ret);
+ }, arguments) };
+ imports.wbg.__wbg_get_765201544a2b6869 = function() { return handleError(function (arg0, arg1) {
+ const ret = Reflect.get(getObject(arg0), getObject(arg1));
+ return addHeapObject(ret);
+ }, arguments) };
+ imports.wbg.__wbg_has_8359f114ce042f5a = function() { return handleError(function (arg0, arg1) {
+ const ret = Reflect.has(getObject(arg0), getObject(arg1));
+ return ret;
+ }, arguments) };
+ imports.wbg.__wbg_set_bf3f89b92d5a34bf = function() { return handleError(function (arg0, arg1, arg2) {
+ const ret = Reflect.set(getObject(arg0), getObject(arg1), getObject(arg2));
+ return ret;
+ }, arguments) };
+ imports.wbg.__wbindgen_debug_string = function(arg0, arg1) {
+ const ret = debugString(getObject(arg1));
+ const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
+ const len0 = WASM_VECTOR_LEN;
+ getInt32Memory0()[arg0 / 4 + 1] = len0;
+ getInt32Memory0()[arg0 / 4 + 0] = ptr0;
+ };
+ imports.wbg.__wbindgen_throw = function(arg0, arg1) {
+ throw new Error(getStringFromWasm0(arg0, arg1));
+ };
+ imports.wbg.__wbindgen_memory = function() {
+ const ret = wasm.memory;
+ return addHeapObject(ret);
+ };
+ imports.wbg.__wbindgen_closure_wrapper947 = function(arg0, arg1, arg2) {
+ const ret = makeMutClosure(arg0, arg1, 147, __wbg_adapter_24);
+ return addHeapObject(ret);
+ };
+
+ return imports;
+}
+
+function initMemory(imports, maybe_memory) {
+
+}
+
+function finalizeInit(instance, module) {
+ wasm = instance.exports;
+ init.__wbindgen_wasm_module = module;
+ cachedInt32Memory0 = new Int32Array();
+ cachedUint8Memory0 = new Uint8Array();
+
+
+ return wasm;
+}
+
+function initSync(module) {
+ const imports = getImports();
+
+ initMemory(imports);
+
+ if (!(module instanceof WebAssembly.Module)) {
+ module = new WebAssembly.Module(module);
+ }
+
+ const instance = new WebAssembly.Instance(module, imports);
+
+ return finalizeInit(instance, module);
+}
+
+async function init(input) {
+ if (typeof input === 'undefined') {
+ input = new URL('gpconv_bg.wasm', import.meta.url);
+ }
+ const imports = getImports();
+
+ if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) {
+ input = fetch(input);
+ }
+
+ initMemory(imports);
+
+ const { instance, module } = await load(await input, imports);
+
+ return finalizeInit(instance, module);
+}
+
+export { initSync }
+export default init;
diff --git a/apps/gipy/pkg/gpconv_bg.wasm b/apps/gipy/pkg/gpconv_bg.wasm
new file mode 100644
index 000000000..edeb4eb59
Binary files /dev/null and b/apps/gipy/pkg/gpconv_bg.wasm differ
diff --git a/apps/gipy/pkg/gpconv_bg.wasm.d.ts b/apps/gipy/pkg/gpconv_bg.wasm.d.ts
new file mode 100644
index 000000000..6bc5d3719
--- /dev/null
+++ b/apps/gipy/pkg/gpconv_bg.wasm.d.ts
@@ -0,0 +1,16 @@
+/* tslint:disable */
+/* eslint-disable */
+export const memory: WebAssembly.Memory;
+export function __wbg_gpcsvg_free(a: number): void;
+export function get_gpc(a: number, b: number): void;
+export function get_svg(a: number, b: number): void;
+export function convert_gpx_strings_no_osm(a: number, b: number): number;
+export function convert_gpx_strings(a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number, m: number, n: number, o: number, p: number, q: number, r: number): number;
+export function __wbindgen_malloc(a: number): number;
+export function __wbindgen_realloc(a: number, b: number, c: number): number;
+export const __wbindgen_export_2: WebAssembly.Table;
+export function _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h0601691a32604cdd(a: number, b: number, c: number): void;
+export function __wbindgen_add_to_stack_pointer(a: number): number;
+export function __wbindgen_free(a: number, b: number): void;
+export function __wbindgen_exn_store(a: number): void;
+export function wasm_bindgen__convert__closures__invoke2_mut__h25ed812378167476(a: number, b: number, c: number, d: number): void;
diff --git a/apps/gipy/pkg/package.json b/apps/gipy/pkg/package.json
new file mode 100644
index 000000000..dee41f5cc
--- /dev/null
+++ b/apps/gipy/pkg/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "gpconv",
+ "version": "0.1.0",
+ "files": [
+ "gpconv_bg.wasm",
+ "gpconv.js",
+ "gpconv.d.ts"
+ ],
+ "module": "gpconv.js",
+ "types": "gpconv.d.ts",
+ "sideEffects": false
+}
\ No newline at end of file
diff --git a/apps/gipy/screenshot1.png b/apps/gipy/screenshot1.png
new file mode 100644
index 000000000..c7c45fa3b
Binary files /dev/null and b/apps/gipy/screenshot1.png differ
diff --git a/apps/gipy/screenshot2.png b/apps/gipy/screenshot2.png
new file mode 100644
index 000000000..ed61eb795
Binary files /dev/null and b/apps/gipy/screenshot2.png differ
diff --git a/apps/gipy/settings.js b/apps/gipy/settings.js
new file mode 100644
index 000000000..af9cbef22
--- /dev/null
+++ b/apps/gipy/settings.js
@@ -0,0 +1,38 @@
+(function (back) {
+ var FILE = "gipy.json";
+ // Load settings
+ var settings = Object.assign(
+ {
+ keep_gps_alive: false,
+ max_speed: 35,
+ },
+ require("Storage").readJSON(FILE, true) || {}
+ );
+
+ function writeSettings() {
+ require("Storage").writeJSON(FILE, settings);
+ }
+
+ // Show the menu
+ E.showMenu({
+ "": { title: "Gipy" },
+ "< Back": () => back(),
+ "keep gps alive": {
+ value: !!settings.keep_gps_alive, // !! converts undefined to false
+ format: (v) => (v ? "Yes" : "No"),
+ onchange: (v) => {
+ settings.keep_gps_alive = v;
+ writeSettings();
+ },
+ },
+ "max speed": {
+ value: 35 | settings.max_speed, // 0| converts undefined to 0
+ min: 0,
+ max: 130,
+ onchange: (v) => {
+ settings.max_speed = v;
+ writeSettings();
+ },
+ },
+ });
+});
diff --git a/apps/glbasic/ChangeLog b/apps/glbasic/ChangeLog
index 89aee01f8..d97fd44d5 100644
--- a/apps/glbasic/ChangeLog
+++ b/apps/glbasic/ChangeLog
@@ -1,2 +1,3 @@
0.20: New App!
+0.21: Tell clock widgets to hide.
diff --git a/apps/glbasic/glbasic.app.js b/apps/glbasic/glbasic.app.js
index ff7837ada..c1f82f74c 100644
--- a/apps/glbasic/glbasic.app.js
+++ b/apps/glbasic/glbasic.app.js
@@ -178,6 +178,8 @@ function draw() {
////////////////////////////////////////////////////
// Bangle.setBarometerPower(true);
+Bangle.setUI("clock");
+
Bangle.loadWidgets();
draw();
@@ -197,6 +199,5 @@ Bangle.on('lcdPower', on => {
}
});
-Bangle.setUI("clock");
Bangle.drawWidgets();
diff --git a/apps/glbasic/metadata.json b/apps/glbasic/metadata.json
index 7c79097da..6d4c562a3 100644
--- a/apps/glbasic/metadata.json
+++ b/apps/glbasic/metadata.json
@@ -2,7 +2,7 @@
"id": "glbasic",
"name": "GLBasic Clock",
"shortName": "GLBasic",
- "version": "0.20",
+ "version": "0.21",
"description": "A clock with large numbers",
"dependencies": {"widpedom":"app"},
"icon": "icon48.png",
diff --git a/apps/gpsautotime/settings.js b/apps/gpsautotime/settings.js
index be6e3bbec..34a6364fe 100644
--- a/apps/gpsautotime/settings.js
+++ b/apps/gpsautotime/settings.js
@@ -13,7 +13,7 @@
E.showMenu({
"" : { "title" : "GPS auto time" },
"< Back" : () => back(),
- 'Show Widgets': {
+ 'Show Widget': {
value: !!settings.show,
onchange: v => {
settings.show = v;
diff --git a/apps/gpsautotime/widget.js b/apps/gpsautotime/widget.js
index a21c14619..14d6fe140 100644
--- a/apps/gpsautotime/widget.js
+++ b/apps/gpsautotime/widget.js
@@ -9,7 +9,7 @@
delete settings;
Bangle.on('GPS',function(fix) {
- if (fix.fix) {
+ if (fix.fix && fix.time) {
var curTime = fix.time.getTime()/1000;
setTime(curTime);
lastTimeSet = curTime;
diff --git a/apps/gpsinfo/metadata.json b/apps/gpsinfo/metadata.json
index 60bd90c03..002febd86 100644
--- a/apps/gpsinfo/metadata.json
+++ b/apps/gpsinfo/metadata.json
@@ -5,7 +5,7 @@
"description": "An application that displays information about altitude, lat/lon, satellites and time",
"icon": "gps-info.png",
"type": "app",
- "tags": "gps",
+ "tags": "gps,outdoors",
"supports": ["BANGLEJS","BANGLEJS2"],
"storage": [
{"name":"gpsinfo.app.js","url":"gps-info.js"},
diff --git a/apps/gpsnav/ChangeLog b/apps/gpsnav/ChangeLog
index b4eaf5472..840f9ecbc 100644
--- a/apps/gpsnav/ChangeLog
+++ b/apps/gpsnav/ChangeLog
@@ -3,4 +3,6 @@
0.03: Add Waypoint Editor
0.04: Fix great circle formula
0.05: Use locale for speed and distance + fix Vector font sizes
-
+0.06: Move waypoints.json (and editor) to 'waypoints' app
+0.07: Add support for b2
+0.08: Fix not displaying of wpindex = 0, correct compass drawing and nm calculation on b2
diff --git a/apps/gpsnav/README.md b/apps/gpsnav/README.md
index af239b233..2b67799b8 100644
--- a/apps/gpsnav/README.md
+++ b/apps/gpsnav/README.md
@@ -4,9 +4,12 @@ The app is aimed at small boat navigation although it can also be used to mark t
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.
+[Bangle.js 2] Button mappings in brackests. One additional feature:
+On swiping on the main screen you can change the displayed metrics: Right changes to nautical metrics, left to the default locale metrics.
+

-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.
+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) [touch / BTN] and wait for the blue text to turn white. Then use BTN1 and BTN3 [swipe left/right] to select a waypoint. The waypoint choice is fixed by pressing BTN2 [touch / BTN] again. In the screen shot below a waypoint giving the location of Stone Henge has been selected.

@@ -18,7 +21,7 @@ The app lets you mark your current location as follows. There are vacant slots i

-Bearing and distance are both zero as WP1 has currently no GPS location associated with it. To mark the location, press BTN2.
+Bearing and distance are both zero as WP1 has currently no GPS location associated with it. To mark the location, press BTN2 [touch / BTN].

diff --git a/apps/gpsnav/app.js b/apps/gpsnav/app.js
index 8903e07fb..68bd2cbda 100644
--- a/apps/gpsnav/app.js
+++ b/apps/gpsnav/app.js
@@ -36,7 +36,7 @@ function drawCompass(course) {
}
xpos+=15;
}
- if (wpindex!=0) {
+ if (wpindex>=0) {
var bpos = brg - course;
if (bpos>180) bpos -=360;
if (bpos<-180) bpos +=360;
@@ -51,10 +51,10 @@ function drawCompass(course) {
//displayed heading
var heading = 0;
-function newHeading(m,h){
+function newHeading(m,h){
var s = Math.abs(m - h);
var delta = (m>h)?1:-1;
- if (s>=180){s=360-s; delta = -delta;}
+ if (s>=180){s=360-s; delta = -delta;}
if (s<2) return h;
var hd = h + delta*(1 + Math.round(s/5));
if (hd<0) hd+=360;
@@ -125,7 +125,7 @@ function drawN(){
g.setColor(0,0,0);
g.fillRect(10,230,60,239);
g.setColor(1,1,1);
- g.drawString("Sats " + satellites.toString(),10,230);
+ g.drawString("Sats " + satellites.toString(),10,230);
}
var savedfix;
@@ -193,11 +193,11 @@ var SCREENACCESS = {
},
release:function(){
this.withApp=true;
- startdraw();
+ startdraw();
setButtons();
}
-}
-
+}
+
Bangle.on('lcdPower',function(on) {
if (!SCREENACCESS.withApp) return;
if (on) {
@@ -207,7 +207,7 @@ Bangle.on('lcdPower',function(on) {
}
});
-var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"NONE"}];
+var waypoints = require("waypoints").load();
wp=waypoints[0];
function nextwp(inc){
@@ -220,10 +220,10 @@ function nextwp(inc){
}
function doselect(){
- if (selected && wpindex!=0 && waypoints[wpindex].lat===undefined && savedfix.fix) {
+ if (selected && wpindex>=0 && waypoints[wpindex].lat===undefined && savedfix.fix) {
waypoints[wpindex] ={name:"@"+wp.name, lat:savedfix.lat, lon:savedfix.lon};
wp = waypoints[wpindex];
- require("Storage").writeJSON("waypoints.json", waypoints);
+ require("waypoints").save(waypoints);
}
selected=!selected;
drawN();
diff --git a/apps/gpsnav/app.min.js b/apps/gpsnav/app.min.js
index 7771e2b98..a01b251e0 100644
--- a/apps/gpsnav/app.min.js
+++ b/apps/gpsnav/app.min.js
@@ -6,6 +6,6 @@ function drawN(){var a=loc.speed(speed);buf.setColor(1);buf.setFont("6x8",2);buf
0,30);buf.setColor(selected?1:2);buf.drawString(wp.name,140,0);buf.setColor(1);buf.drawString(a,60,0);buf.drawString(loc.distance(dist),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(a){savedfix=a;void 0!==a&&(course=isNaN(a.course)?course:Math.round(a.course),speed=isNaN(a.speed)?speed:a.speed,satellites=a.satellites);candraw&&(void 0!==a&&1==a.fix&&(dist=distance(a,wp),isNaN(dist)&&(dist=0),brg=bearing(a,wp),isNaN(brg)&&(brg=0)),drawN())}var intervalRef;function stopdraw(){candraw=!1;intervalRef&&clearInterval(intervalRef)}
function startTimers(){candraw=!0;intervalRefSec=setInterval(function(){heading=newHeading(course,heading);course!=heading&&drawCompass(heading)},200)}function drawAll(){g.setColor(1,.5,.5);g.fillPoly([120,Yoff+50,110,Yoff+70,130,Yoff+70]);g.setColor(1,1,1);drawN();drawCompass(heading)}function startdraw(){g.clear();Bangle.drawWidgets();startTimers();drawAll()}
-function setButtons(){setWatch(nextwp.bind(null,-1),BTN1,{repeat:!0,edge:"falling"});setWatch(doselect,BTN2,{repeat:!0,edge:"falling"});setWatch(nextwp.bind(null,1),BTN3,{repeat:!0,edge:"falling"})}var SCREENACCESS={withApp:!0,request:function(){this.withApp=!1;stopdraw();clearWatch()},release:function(){this.withApp=!0;startdraw();setButtons()}};Bangle.on("lcdPower",function(a){SCREENACCESS.withApp&&(a?startdraw():stopdraw())});var waypoints=require("Storage").readJSON("waypoints.json")||[{name:"NONE"}];
-wp=waypoints[0];function nextwp(a){selected&&(wpindex+=a,wpindex>=waypoints.length&&(wpindex=0),0>wpindex&&(wpindex=waypoints.length-1),wp=waypoints[wpindex],drawN())}function doselect(){selected&&0!=wpindex&&void 0===waypoints[wpindex].lat&&savedfix.fix&&(waypoints[wpindex]={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();
+function setButtons(){setWatch(nextwp.bind(null,-1),BTN1,{repeat:!0,edge:"falling"});setWatch(doselect,BTN2,{repeat:!0,edge:"falling"});setWatch(nextwp.bind(null,1),BTN3,{repeat:!0,edge:"falling"})}var SCREENACCESS={withApp:!0,request:function(){this.withApp=!1;stopdraw();clearWatch()},release:function(){this.withApp=!0;startdraw();setButtons()}};Bangle.on("lcdPower",function(a){SCREENACCESS.withApp&&(a?startdraw():stopdraw())});var waypoints=require("waypoints").load();
+wp=waypoints[0];function nextwp(a){selected&&(wpindex+=a,wpindex>=waypoints.length&&(wpindex=0),0>wpindex&&(wpindex=waypoints.length-1),wp=waypoints[wpindex],drawN())}function doselect(){selected&&0!=wpindex&&void 0===waypoints[wpindex].lat&&savedfix.fix&&(waypoints[wpindex]={name:"@"+wp.name,lat:savedfix.lat,lon:savedfix.lon},wp=waypoints[wpindex],require("waypoints").save(waypoints));selected=!selected;drawN()}g.clear();Bangle.setLCDBrightness(1);Bangle.loadWidgets();Bangle.drawWidgets();
Bangle.setGPSPower(1);drawAll();startTimers();Bangle.on("GPS",onGPS);setButtons();
diff --git a/apps/gpsnav/app_b2.js b/apps/gpsnav/app_b2.js
new file mode 100644
index 000000000..ee6519c92
--- /dev/null
+++ b/apps/gpsnav/app_b2.js
@@ -0,0 +1,265 @@
+var candraw = true;
+var brg = 0;
+var wpindex = 0;
+var locindex = 0;
+const labels = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"];
+var loc = {
+ speed: [
+ require("locale").speed,
+ (kph) => {
+ return (kph / 1.852).toFixed(1) + "kn ";
+ }
+ ],
+ distance: [
+ require("locale").distance,
+ (m) => {
+ return (m / 1852).toFixed(3) + "nm ";
+ }
+ ]
+};
+
+
+function drawCompass(course) {
+ if (!candraw) return;
+ g.reset().clearRect(0, 24, 175, 71);
+ g.setFont("Vector", 18);
+ var start = course - 90;
+ if (start < 0) start += 360;
+ g.fillRect(14, 67, 162, 71);
+ var xpos = 16;
+ var frag = 15 - start % 15;
+ if (frag < 15) xpos += Math.floor((frag * 4) / 5);
+ else frag = 0;
+ for (var i = frag; i <= 180 - frag; i += 15) {
+ var res = start + i;
+ if (res % 90 == 0) {
+ g.drawString(labels[Math.floor(res / 45) % 8], xpos - 6, 28);
+ g.fillRect(xpos - 2, 47, xpos + 2, 67);
+ } else if (res % 45 == 0) {
+ g.drawString(labels[Math.floor(res / 45) % 8], xpos - 9, 28);
+ g.fillRect(xpos - 2, 52, xpos + 2, 67);
+ } else if (res % 15 == 0) {
+ g.fillRect(xpos, 58, xpos + 1, 67);
+ }
+ xpos += 12;
+ }
+ if (wpindex >= 0) {
+ var bpos = brg - course;
+ if (bpos > 180) bpos -= 360;
+ if (bpos < -180) bpos += 360;
+ bpos = Math.round((bpos * 4) / 5) + 88;
+ if (bpos < 16) bpos = 7;
+ if (bpos > 160) bpos = 169;
+ g.setColor(g.theme.bgH);
+ g.fillCircle(bpos, 63, 8);
+ }
+}
+
+//displayed heading
+var heading = 0;
+
+function newHeading(m, h) {
+ var s = Math.abs(m - h);
+ var delta = (m > h) ? 1 : -1;
+ if (s >= 180) {
+ s = 360 - s;
+ delta = -delta;
+ }
+ if (s < 2) return h;
+ var hd = h + delta * (1 + Math.round(s / 5));
+ if (hd < 0) hd += 360;
+ if (hd > 360) hd -= 360;
+ return hd;
+}
+
+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() {
+ g.reset().clearRect(0, 89, 175, 175);
+ var txt = loc.speed[locindex](speed);
+ g.setFont("6x8", 2);
+ g.drawString("o", 68, 87);
+ g.setFont("6x8", 1);
+ g.drawString(txt.substring(txt.length - 3), 156, 119);
+ g.setFont("Vector", 36);
+ var cs = course.toString().padStart(3, "0");
+ g.drawString(cs, 2, 89);
+ g.drawString(txt.substring(0, txt.length - 3), 92, 89);
+ g.setFont("Vector", 18);
+ var bs = brg.toString().padStart(3, "0");
+ g.drawString("Brg:", 1, 128);
+ g.drawString("Dist:", 1, 148);
+ g.setColor(selected ? g.theme.bgH : g.theme.bg);
+ g.fillRect(90, 127, 175, 143);
+ g.setColor(selected ? g.theme.fgH : g.theme.fg);
+ g.drawString(wp.name, 92, 128);
+ g.setColor(g.theme.fg);
+ g.drawString(bs, 42, 128);
+ g.drawString(loc.distance[locindex](dist), 42, 148);
+ g.setFont("6x8", 0.5);
+ g.drawString("o", 75, 127);
+ g.setFont("6x8", 1);
+ g.setColor(satellites ? g.theme.bg : g.theme.bgH);
+ g.fillRect(0, 167, 75, 175);
+ g.setColor(satellites ? g.theme.fg : g.theme.fgH);
+ g.drawString("Sats:", 1, 168);
+ g.drawString(satellites.toString(), 42, 168);
+}
+
+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 (candraw) {
+ 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 stopdraw() {
+ candraw = false;
+ if (intervalRef) {
+ clearInterval(intervalRef);
+ }
+}
+
+function startTimers() {
+ candraw = true;
+ intervalRefSec = setInterval(function() {
+ heading = newHeading(course, heading);
+ if (course != heading) drawCompass(heading);
+ }, 200);
+}
+
+function drawAll() {
+ g.setColor(1, 0, 0);
+ g.fillPoly([88, 71, 78, 88, 98, 88]);
+ drawN();
+ drawCompass(heading);
+}
+
+function startdraw() {
+ g.clear();
+ Bangle.drawWidgets();
+ startTimers();
+ drawAll();
+}
+
+function setButtons() {
+ Bangle.setUI("leftright", btn => {
+ if (!btn) {
+ doselect();
+ } else {
+ nextwp(btn);
+ }
+ });
+}
+
+var SCREENACCESS = {
+ withApp: true,
+ request: function() {
+ this.withApp = false;
+ stopdraw();
+ clearWatch();
+ },
+ release: function() {
+ this.withApp = true;
+ startdraw();
+ setButtons();
+ }
+};
+
+Bangle.on('lcdPower', function(on) {
+ if (!SCREENACCESS.withApp) return;
+ if (on) {
+ startdraw();
+ } else {
+ stopdraw();
+ }
+});
+
+var waypoints = require("waypoints").load();
+wp = waypoints[0];
+
+function nextwp(inc) {
+ if (selected) {
+ wpindex += inc;
+ if (wpindex >= waypoints.length) wpindex = 0;
+ if (wpindex < 0) wpindex = waypoints.length - 1;
+ wp = waypoints[wpindex];
+ drawN();
+ } else {
+ locindex = inc > 0 ? 1 : 0;
+ drawN();
+ }
+}
+
+function doselect() {
+ if (selected && wpindex >= 0 && waypoints[wpindex].lat === undefined && savedfix.fix) {
+ waypoints[wpindex] = {
+ name: "@" + wp.name,
+ lat: savedfix.lat,
+ lon: savedfix.lon
+ };
+ wp = waypoints[wpindex];
+ require("waypoints").save(waypoints);
+ }
+ selected = !selected;
+ print("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
+setButtons();
diff --git a/apps/gpsnav/metadata.json b/apps/gpsnav/metadata.json
index 5c1830318..bc46a733c 100644
--- a/apps/gpsnav/metadata.json
+++ b/apps/gpsnav/metadata.json
@@ -1,16 +1,17 @@
{
"id": "gpsnav",
"name": "GPS Navigation",
- "version": "0.05",
+ "version": "0.08",
"description": "Displays GPS Course and Speed, + Directions to waypoint and waypoint recording, now with waypoint editor",
+ "screenshots": [{"url":"screenshot-b2.png"}],
"icon": "icon.png",
"tags": "tool,outdoors,gps",
- "supports": ["BANGLEJS"],
+ "supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
- "interface": "waypoints.html",
+ "dependencies" : { "waypoints":"type" },
"storage": [
- {"name":"gpsnav.app.js","url":"app.min.js"},
+ {"name":"gpsnav.app.js","url":"app.min.js","supports":["BANGLEJS"]},
+ {"name":"gpsnav.app.js","url":"app_b2.js","supports":["BANGLEJS2"]},
{"name":"gpsnav.img","url":"app-icon.js","evaluate":true}
- ],
- "data": [{"name":"waypoints.json","url":"waypoints.json"}]
+ ]
}
diff --git a/apps/gpsnav/screenshot-b2.png b/apps/gpsnav/screenshot-b2.png
new file mode 100644
index 000000000..34ad23679
Binary files /dev/null and b/apps/gpsnav/screenshot-b2.png differ
diff --git a/apps/gpsnav/waypoints.html b/apps/gpsnav/waypoints.html
deleted file mode 100644
index d02260732..000000000
--- a/apps/gpsnav/waypoints.html
+++ /dev/null
@@ -1,170 +0,0 @@
-
-
-
-
-
-
-
- List of waypoints
-
-
-
- Name
- Lat.
- Long.
- Actions
-
-
-
-
-
-
-
- Add a new waypoint
-
-
- Reload Upload
-
-
-
-
-
-
diff --git a/apps/gpsnav/waypoints.json b/apps/gpsnav/waypoints.json
deleted file mode 100644
index 98a670c0d..000000000
--- a/apps/gpsnav/waypoints.json
+++ /dev/null
@@ -1,20 +0,0 @@
-[
- {
- "name":"NONE"
- },
- {
- "name":"No10",
- "lat":51.5032,
- "lon":-0.1269
- },
- {
- "name":"Stone",
- "lat":51.1788,
- "lon":-1.8260
- },
- { "name":"WP0" },
- { "name":"WP1" },
- { "name":"WP2" },
- { "name":"WP3" },
- { "name":"WP4" }
-]
\ No newline at end of file
diff --git a/apps/gpstouch/Changelog b/apps/gpstouch/ChangeLog
similarity index 100%
rename from apps/gpstouch/Changelog
rename to apps/gpstouch/ChangeLog
diff --git a/apps/gpstrek/ChangeLog b/apps/gpstrek/ChangeLog
new file mode 100644
index 000000000..849088c64
--- /dev/null
+++ b/apps/gpstrek/ChangeLog
@@ -0,0 +1,16 @@
+0.01: New App!
+0.02: Make selection of background activity more explicit
+0.03: Fix listener for accel always active
+ Use custom UI with swipes instead of leftright
+0.04: Fix compass heading
+0.05: Added adjustment for Bangle.js magnetometer heading fix
+0.06: Fix waypoint menu always selecting last waypoint
+ Fix widget adding listeners more than once
+0.07: Show checkered flag for target markers
+ Single waypoints are now shown in the compass view
+0.08: Better handle state in widget
+ Slightly faster drawing by doing some caching
+ Reconstruct battery voltage by using calibrated batFullVoltage
+ Averaging for smoothing compass headings
+ Save state if route or waypoint has been chosen
+0.09: Workaround a minifier issue allowing to install gpstrek with minification enabled
diff --git a/apps/gpstrek/README.md b/apps/gpstrek/README.md
new file mode 100644
index 000000000..c55f5a8bf
--- /dev/null
+++ b/apps/gpstrek/README.md
@@ -0,0 +1,50 @@
+# GPS Trekking
+
+Helper for tracking the status/progress during hiking. Do NOT depend on this for navigation!
+
+This app is inspired by and uses code from "GPS Navigation" and "Navigation compass".
+
+## Usage
+
+Tapping or button to switch to the next information display, swipe right for the menu.
+
+Choose either a route or a waypoint as basis for the display.
+
+After this selection and availability of a GPS fix the compass will show a checkered flag for your destination and a green dot for possibly available waypoints on the way.
+Waypoints are shown with name if available and distance to waypoint.
+
+As long as no GPS signal is available the compass shows the heading from the build in magnetometer. When a GPS fix becomes available, the compass display shows the GPS course. This can be differentiated by the display of bubble levels on top and sides of the compass.
+If they are on display, the source is the magnetometer and you should keep the bangle level. There is currently no tilt compensation for the compass display.
+
+### Route
+
+Routes can be created from .gpx files containing "trkpt" elements with this script: [createRoute.sh](createRoute.sh)
+
+The resulting file needs to be uploaded to the watch and will be shown in the file selection menu.
+
+The route can be mirrored to switch start and destination.
+
+If the GPS position is closer than 30m to the next waypoint, the route is automatically advanced to the next waypoint.
+
+### Waypoints
+
+You can select a waypoint from the "Waypoints" app as destination.
+
+## Calibration
+
+### Altitude
+
+You can correct the barometric altitude display either by manually setting a known correct value or using the GPS fix elevation as reference. This will only affect the display of altitude values.
+
+### Compass
+
+If the compass fallback starts to show unreliable values, you can reset the calibration in the menu. It starts to show values again after turning 360°.
+
+## Widget
+
+The widget keeps the sensors alive and records some very basic statistics when the app is not started. It shows as the app icon in the widget bar when the background task is active.
+This uses a lot of power so ensure to stop the app if you are not actively using it.
+
+# Creator
+
+[halemmerich](https://github.com/halemmerich)
diff --git a/apps/gpstrek/app-icon.js b/apps/gpstrek/app-icon.js
new file mode 100644
index 000000000..6b2924353
--- /dev/null
+++ b/apps/gpstrek/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwIjggOAApMD4AFJg4FF8AFJh/wApMf/AFJn/8ApN//wFDvfeAof774FD+fPLwYFBMAUB8fHAoUDAoJaCgfD4YFIg+D4JgCAosPAoJgCh6DBAoUfAoJgCjwFBvAFBnwFBvgFBngFBngFBvh3BnwFBvH//8eMgQFBMwX//k//5eB//wh//wAFBAQcDRoU/4EDJQfAbYbfFACYA="))
diff --git a/apps/gpstrek/app.js b/apps/gpstrek/app.js
new file mode 100644
index 000000000..1f46ae2b4
--- /dev/null
+++ b/apps/gpstrek/app.js
@@ -0,0 +1,899 @@
+
+{ //run in own scope for fast switch
+const STORAGE = require("Storage");
+const BAT_FULL = require("Storage").readJSON("setting.json").batFullVoltage || 0.3144;
+
+let init = function(){
+ global.screen = 1;
+ global.drawTimeout = undefined;
+ global.lastDrawnScreen = 0;
+ global.firstDraw = true;
+ global.slices = [];
+ global.maxScreens = 1;
+ global.scheduleDraw = false;
+
+ Bangle.loadWidgets();
+ WIDGETS.gpstrek.start(false);
+ if (!WIDGETS.gpstrek.getState().numberOfSlices) WIDGETS.gpstrek.getState().numberOfSlices = 3;
+};
+
+let cleanup = function(){
+ if (global.drawTimeout) clearTimeout(global.drawTimeout);
+ delete global.screen;
+ delete global.drawTimeout;
+ delete global.lastDrawnScreen;
+ delete global.firstDraw;
+ delete global.slices;
+ delete global.maxScreens;
+};
+
+init();
+scheduleDraw = true;
+
+let parseNumber = function(toParse){
+ if (toParse.includes(".")) return parseFloat(toParse);
+ return parseFloat("" + toParse + ".0");
+};
+
+let parseWaypoint = function(filename, offset, result){
+ result.lat = parseNumber(STORAGE.read(filename, offset, 11));
+ result.lon = parseNumber(STORAGE.read(filename, offset += 11, 12));
+ return offset + 12;
+};
+
+let parseWaypointWithElevation = function (filename, offset, result){
+ offset = parseWaypoint(filename, offset, result);
+ result.alt = parseNumber(STORAGE.read(filename, offset, 6));
+ return offset + 6;
+};
+
+let parseWaypointWithName = function(filename, offset, result){
+ offset = parseWaypoint(filename, offset, result);
+ return parseName(filename, offset, result);
+};
+
+let parseName = function(filename, offset, result){
+ let nameLength = STORAGE.read(filename, offset, 2) - 0;
+ result.name = STORAGE.read(filename, offset += 2, nameLength);
+ return offset + nameLength;
+};
+
+let parseWaypointWithElevationAndName = function(filename, offset, result){
+ offset = parseWaypointWithElevation(filename, offset, result);
+ return parseName(filename, offset, result);
+};
+
+let getEntry = function(filename, offset, result){
+ result.fileOffset = offset;
+ let type = STORAGE.read(filename, offset++, 1);
+ if (type == "") return -1;
+ switch (type){
+ case "A":
+ offset = parseWaypoint(filename, offset, result);
+ break;
+ case "B":
+ offset = parseWaypointWithName(filename, offset, result);
+ break;
+ case "C":
+ offset = parseWaypointWithElevation(filename, offset, result);
+ break;
+ case "D":
+ offset = parseWaypointWithElevationAndName(filename, offset, result);
+ break;
+ default:
+ print("Unknown entry type", type);
+ return -1;
+ }
+ offset++;
+
+ result.fileLength = offset - result.fileOffset;
+ //print(result);
+ return offset;
+};
+
+const labels = ["N","NE","E","SE","S","SW","W","NW"];
+const loc = require("locale");
+
+let matchFontSize = function(graphics, text, height, width){
+ graphics.setFontVector(height);
+ let metrics;
+ let size = 1;
+ while (graphics.stringMetrics(text).width > 0.90 * width){
+ size -= 0.05;
+ graphics.setFont("Vector",Math.floor(height*size));
+ }
+};
+
+let getDoubleLineSlice = function(title1,title2,provider1,provider2,refreshTime){
+ let lastDrawn = Date.now() - Math.random()*refreshTime;
+ let lastValue1 = 0;
+ let lastValue2 = 0;
+ return {
+ refresh: function (){
+ let bigChange1 = (Math.abs(lastValue1 - provider1()) > 1);
+ let bigChange2 = (Math.abs(lastValue2 - provider2()) > 1);
+ let refresh = (Bangle.isLocked()?(refreshTime?refreshTime*5:10000):(refreshTime?refreshTime*2:1000));
+ let old = (Date.now() - lastDrawn) > refresh;
+ return (bigChange1 || bigChange2) && old;
+ },
+ draw: function (graphics, x, y, height, width){
+ lastDrawn = Date.now();
+ if (typeof title1 == "function") title1 = title1();
+ if (typeof title2 == "function") title2 = title2();
+ graphics.clearRect(x,y,x+width,y+height);
+
+ lastValue1 = provider1();
+ matchFontSize(graphics, title1 + lastValue1, Math.floor(height*0.5), width);
+ graphics.setFontAlign(-1,-1);
+ graphics.drawString(title1, x+2, y);
+ graphics.setFontAlign(1,-1);
+ graphics.drawString(lastValue1, x+width, y);
+
+ lastValue2 = provider2();
+ matchFontSize(graphics, title2 + lastValue2, Math.floor(height*0.5), width);
+ graphics.setFontAlign(-1,-1);
+ graphics.drawString(title2, x+2, y+(height*0.5));
+ graphics.setFontAlign(1,-1);
+ graphics.drawString(lastValue2, x+width, y+(height*0.5));
+ }
+ };
+};
+
+let getTargetSlice = function(targetDataSource){
+ let nameIndex = 0;
+ let lastDrawn = Date.now() - Math.random()*3000;
+ return {
+ refresh: function (){
+ return Date.now() - lastDrawn > (Bangle.isLocked()?3000:10000);
+ },
+ draw: function (graphics, x, y, height, width){
+ lastDrawn = Date.now();
+ graphics.clearRect(x,y,x+width,y+height);
+ if (targetDataSource.icon){
+ graphics.drawImage(targetDataSource.icon,x,y + (height - 16)/2);
+ x += 16;
+ width -= 16;
+ }
+
+ if (!targetDataSource.getTarget() || !targetDataSource.getStart()) return;
+
+ let dist = distance(targetDataSource.getStart(),targetDataSource.getTarget());
+ if (isNaN(dist)) dist = Infinity;
+ let bearingString = bearing(targetDataSource.getStart(),targetDataSource.getTarget()) + "°";
+ if (targetDataSource.getTarget().name) {
+ graphics.setFont("Vector",Math.floor(height*0.5));
+ let scrolledName = (targetDataSource.getTarget().name || "").substring(nameIndex);
+ if (graphics.stringMetrics(scrolledName).width > width){
+ nameIndex++;
+ } else {
+ nameIndex = 0;
+ }
+ graphics.drawString(scrolledName, x+2, y);
+
+ let distanceString = loc.distance(dist,2);
+ matchFontSize(graphics, distanceString + bearingString, height*0.5, width);
+ graphics.drawString(bearingString, x+2, y+(height*0.5));
+ graphics.setFontAlign(1,-1);
+ graphics.drawString(distanceString, x + width, y+(height*0.5));
+ } else {
+ graphics.setFont("Vector",Math.floor(height*1));
+ let bearingString = bearing(targetDataSource.getStart(),targetDataSource.getTarget()) + "°";
+ let formattedDist = loc.distance(dist,2);
+ let distNum = (formattedDist.match(/[0-9\.]+/) || [Infinity])[0];
+ let size = 0.8;
+ let distNumMetrics;
+ while (graphics.stringMetrics(bearingString).width + (distNumMetrics = graphics.stringMetrics(distNum)).width > 0.90 * width){
+ size -= 0.05;
+ graphics.setFont("Vector",Math.floor(height*size));
+ }
+ graphics.drawString(bearingString, x+2, y + (height - distNumMetrics.height)/2);
+ graphics.setFontAlign(1,-1);
+ graphics.drawString(distNum, x + width, y + (height - distNumMetrics.height)/2);
+ graphics.setFont("Vector",Math.floor(height*0.25));
+
+ graphics.setFontAlign(-1,1);
+ if (targetDataSource.getProgress){
+ graphics.drawString(targetDataSource.getProgress(), x + 2, y + height);
+ }
+ graphics.setFontAlign(1,1);
+ if (!isNaN(distNum) && distNum != Infinity)
+ graphics.drawString(formattedDist.match(/[a-zA-Z]+/), x + width, y + height);
+ }
+ }
+ };
+};
+
+let drawCompass = function(graphics, x, y, height, width, increment, start){
+ graphics.setFont12x20();
+ graphics.setFontAlign(0,-1);
+ graphics.setColor(graphics.theme.fg);
+ let frag = 0 - start%15;
+ if (frag>0) frag = 0;
+ let xpos = 0 + frag*increment;
+ for (let i=start;i<=720;i+=15){
+ var res = i + frag;
+ if (res%90==0) {
+ graphics.drawString(labels[Math.floor(res/45)%8],xpos,y+2);
+ graphics.fillRect(xpos-2,Math.floor(y+height*0.6),xpos+2,Math.floor(y+height));
+ } else if (res%45==0) {
+ graphics.drawString(labels[Math.floor(res/45)%8],xpos,y+2);
+ graphics.fillRect(xpos-2,Math.floor(y+height*0.75),xpos+2,Math.floor(y+height));
+ } else if (res%15==0) {
+ graphics.fillRect(xpos,Math.floor(y+height*0.9),xpos+1,Math.floor(y+height));
+ }
+ xpos+=increment*15;
+ if (xpos > width + 20) break;
+ }
+};
+
+let getCompassSlice = function(compassDataSource){
+ let lastDrawn = Date.now() - Math.random()*2000;
+ let lastDrawnValue = 0;
+ const buffers = 4;
+ let buf = [];
+ return {
+ refresh : function (){
+ let bigChange = (Math.abs(lastDrawnValue - compassDataSource.getCourse()) > 2);
+ let old = (Bangle.isLocked()?(Date.now() - lastDrawn > 2000):true);
+ return bigChange && old;
+ },
+ draw: function (graphics, x,y,height,width){
+ lastDrawn = Date.now();
+ const max = 180;
+ const increment=width/max;
+
+ graphics.clearRect(x,y,x+width,y+height);
+
+ lastDrawnValue = compassDataSource.getCourse();
+
+ var start = lastDrawnValue - 90;
+ if (isNaN(lastDrawnValue)) start = -90;
+ if (start<0) start+=360;
+ start = start % 360;
+
+ if (WIDGETS.gpstrek.getState().acc && compassDataSource.getCourseType() == "MAG"){
+ drawCompass(graphics,0,y+width*0.05,height-width*0.05,width,increment,start);
+ } else {
+ drawCompass(graphics,0,y,height,width,increment,start);
+ }
+
+
+ if (compassDataSource.getPoints){
+ let points = compassDataSource.getPoints(); //storing this in a variable works around a minifier bug causing a problem in the next line: for(let a of a.getPoints())
+ for (let p of points){
+ g.reset();
+ var bpos = p.bearing - lastDrawnValue;
+ if (bpos>180) bpos -=360;
+ if (bpos<-180) bpos +=360;
+ bpos+=120;
+ let min = 0;
+ let max = 180;
+ if (bpos<=min){
+ bpos = Math.floor(width*0.05);
+ } else if (bpos>=max) {
+ bpos = Math.ceil(width*0.95);
+ } else {
+ bpos=Math.round(bpos*increment);
+ }
+ if (p.color){
+ graphics.setColor(p.color);
+ }
+ if (p.icon){
+ graphics.drawImage(p.icon, bpos,y+height-12, {rotate:0,scale:2});
+ } else {
+ graphics.fillCircle(bpos,y+height-12,Math.floor(width*0.03));
+ }
+ }
+ }
+ if (compassDataSource.getMarkers){
+ let markers = compassDataSource.getMarkers(); //storing this in a variable works around a minifier bug causing a problem in the next line: for(let a of a.getMarkers())
+ for (let m of markers){
+ g.reset();
+ g.setColor(m.fillcolor);
+ let mpos = m.xpos * width;
+ if (m.xpos < 0.05) mpos = Math.floor(width*0.05);
+ if (m.xpos > 0.95) mpos = Math.ceil(width*0.95);
+ g.fillPoly(triangle(mpos,y+height-m.height, m.height, m.width));
+ g.setColor(m.linecolor);
+ g.drawPoly(triangle(mpos,y+height-m.height, m.height, m.width),true);
+ }
+ }
+ graphics.setColor(g.theme.fg);
+ graphics.fillRect(x,y,Math.floor(width*0.05),y+height);
+ graphics.fillRect(Math.ceil(width*0.95),y,width,y+height);
+ if (WIDGETS.gpstrek.getState().acc && compassDataSource.getCourseType() == "MAG") {
+ let xh = E.clip(width*0.5-height/2+(((WIDGETS.gpstrek.getState().acc.x+1)/2)*height),width*0.5 - height/2, width*0.5 + height/2);
+ let yh = E.clip(y+(((WIDGETS.gpstrek.getState().acc.y+1)/2)*height),y,y+height);
+
+ graphics.fillRect(width*0.5 - height/2, y, width*0.5 + height/2, y + Math.floor(width*0.05));
+
+ graphics.setColor(g.theme.bg);
+ graphics.drawLine(width*0.5 - 5, y, width*0.5 - 5, y + Math.floor(width*0.05));
+ graphics.drawLine(width*0.5 + 5, y, width*0.5 + 5, y + Math.floor(width*0.05));
+ graphics.fillRect(xh-1,y,xh+1,y+Math.floor(width*0.05));
+
+ let left = Math.floor(width*0.05);
+ let right = Math.ceil(width*0.95);
+ graphics.drawLine(0,y+height/2-5,left,y+height/2-5);
+ graphics.drawLine(right,y+height/2-5,x+width,y+height/2-5);
+ graphics.drawLine(0,y+height/2+5,left,y+height/2+5);
+ graphics.drawLine(right,y+height/2+5,x+width,y+height/2+5);
+ graphics.fillRect(0,yh-1,left,yh+1);
+ graphics.fillRect(right,yh-1,x+width,yh+1);
+ }
+ graphics.setColor(g.theme.fg);
+ graphics.drawRect(Math.floor(width*0.05),y,Math.ceil(width*0.95),y+height);
+ }
+ };
+};
+
+let radians = function(a) {
+ return a*Math.PI/180;
+};
+
+let degrees = function(a) {
+ let d = a*180/Math.PI;
+ return (d+360)%360;
+};
+
+let bearing = function(a,b){
+ if (!a || !b || !a.lon || !a.lat || !b.lon || !b.lat) return Infinity;
+ let delta = radians(b.lon-a.lon);
+ let alat = radians(a.lat);
+ let blat = radians(b.lat);
+ let y = Math.sin(delta) * Math.cos(blat);
+ let x = Math.cos(alat)*Math.sin(blat) -
+ Math.sin(alat)*Math.cos(blat)*Math.cos(delta);
+ return Math.round(degrees(Math.atan2(y, x)));
+};
+
+let distance = function(a,b){
+ if (!a || !b || !a.lon || !a.lat || !b.lon || !b.lat) return Infinity;
+ let x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2));
+ let y = radians(b.lat-a.lat);
+ return Math.round(Math.sqrt(x*x + y*y) * 6371000);
+};
+
+let getAveragedCompass = function(){
+ return Math.round(WIDGETS.gpstrek.getState().avgComp);
+};
+
+let triangle = function(x, y, width, height){
+ return [
+ Math.round(x),Math.round(y),
+ Math.round(x+width * 0.5), Math.round(y+height),
+ Math.round(x-width * 0.5), Math.round(y+height)
+ ];
+};
+
+let onSwipe = function(dir){
+ if (dir < 0) {
+ nextScreen();
+ } else if (dir > 0) {
+ switchMenu();
+ } else {
+ nextScreen();
+ }
+};
+
+let setButtons = function(){
+ let options = {
+ mode: "custom",
+ swipe: onSwipe,
+ btn: nextScreen,
+ touch: nextScreen
+ };
+ Bangle.setUI(options);
+};
+
+let getApproxFileSize = function(name){
+ let currentStart = STORAGE.getStats().totalBytes;
+ let currentSize = 0;
+ for (let i = currentStart; i > 500; i/=2){
+ let currentDiff = i;
+ //print("Searching", currentDiff);
+ while (STORAGE.read(name, currentSize+currentDiff, 1) == ""){
+ //print("Loop", currentDiff);
+ currentDiff = Math.ceil(currentDiff/2);
+ }
+ i = currentDiff*2;
+ currentSize += currentDiff;
+ }
+ return currentSize;
+};
+
+let parseRouteData = function(filename, progressMonitor){
+ let routeInfo = {};
+
+ routeInfo.filename = filename;
+ routeInfo.refs = [];
+
+ let c = {};
+ let scanOffset = 0;
+ routeInfo.length = 0;
+ routeInfo.count = 0;
+ routeInfo.mirror = false;
+ let lastSeenWaypoint;
+ let lastSeenAlt;
+ let waypoint = {};
+
+ routeInfo.up = 0;
+ routeInfo.down = 0;
+
+ let size = getApproxFileSize(filename);
+
+ while ((scanOffset = getEntry(filename, scanOffset, waypoint)) > 0) {
+ if (routeInfo.count % 5 == 0) progressMonitor(scanOffset, "Loading", size);
+ if (lastSeenWaypoint){
+ routeInfo.length += distance(lastSeenWaypoint, waypoint);
+
+ let diff = waypoint.alt - lastSeenAlt;
+ //print("Distance", routeInfo.length, "alt", lastSeenAlt, waypoint.alt, diff);
+ if (waypoint.alt && lastSeenAlt && diff > 3){
+ if (lastSeenAlt < waypoint.alt){
+ //print("Up", diff);
+ routeInfo.up += diff;
+ } else {
+ //print("Down", diff);
+ routeInfo.down += diff;
+ }
+ }
+ }
+ routeInfo.count++;
+ routeInfo.refs.push(waypoint.fileOffset);
+ lastSeenWaypoint = waypoint;
+ if (!isNaN(waypoint.alt)) lastSeenAlt = waypoint.alt;
+ waypoint = {};
+ }
+
+ set(routeInfo, 0);
+ return routeInfo;
+};
+
+let hasPrev = function(route){
+ if (route.mirror) return route.index < (route.count - 1);
+ return route.index > 0;
+};
+
+let hasNext = function(route){
+ if (route.mirror) return route.index > 0;
+ return route.index < (route.count - 1);
+};
+
+let next = function(route){
+ if (!hasNext(route)) return;
+ if (route.mirror) set(route, --route.index);
+ if (!route.mirror) set(route, ++route.index);
+};
+
+let set = function(route, index){
+ route.currentWaypoint = {};
+ route.index = index;
+ getEntry(route.filename, route.refs[index], route.currentWaypoint);
+};
+
+let prev = function(route){
+ if (!hasPrev(route)) return;
+ if (route.mirror) set(route, ++route.index);
+ if (!route.mirror) set(route, --route.index);
+};
+
+let lastMirror;
+let cachedLast;
+
+let getLast = function(route){
+ let wp = {};
+ if (lastMirror != route.mirror){
+ if (route.mirror) getEntry(route.filename, route.refs[0], wp);
+ if (!route.mirror) getEntry(route.filename, route.refs[route.count - 1], wp);
+ lastMirror = route.mirror;
+ cachedLast = wp;
+ }
+ return cachedLast;
+};
+
+let removeMenu = function(){
+ E.showMenu();
+ switchNav();
+};
+
+let showProgress = function(progress, title, max){
+ //print("Progress",progress,max)
+ let message = title? title: "Loading";
+ if (max){
+ message += " " + E.clip((progress/max*100),0,100).toFixed(0) +"%";
+ } else {
+ let dots = progress % 4;
+ for (let i = 0; i < dots; i++) message += ".";
+ for (let i = dots; i < 4; i++) message += " ";
+ }
+ E.showMessage(message);
+};
+
+let handleLoading = function(c){
+ E.showMenu();
+ WIDGETS.gpstrek.getState().route = parseRouteData(c, showProgress);
+ WIDGETS.gpstrek.getState().waypoint = null;
+ WIDGETS.gpstrek.getState().route.mirror = false;
+ removeMenu();
+};
+
+let showRouteSelector = function(){
+ var menu = {
+ "" : {
+ back : showRouteMenu,
+ }
+ };
+
+ STORAGE.list(/\.trf$/).forEach((file)=>{
+ menu[file] = ()=>{handleLoading(file);};
+ });
+
+ E.showMenu(menu);
+};
+
+let showRouteMenu = function(){
+ var menu = {
+ "" : {
+ "title" : "Route",
+ back : showMenu,
+ },
+ "Select file" : showRouteSelector
+ };
+
+ if (WIDGETS.gpstrek.getState().route){
+ menu.Mirror = {
+ value: WIDGETS.gpstrek.getState() && WIDGETS.gpstrek.getState().route && !!WIDGETS.gpstrek.getState().route.mirror || false,
+ onchange: v=>{
+ WIDGETS.gpstrek.getState().route.mirror = v;
+ }
+ };
+ menu['Select closest waypoint'] = function () {
+ if (WIDGETS.gpstrek.getState().currentPos && WIDGETS.gpstrek.getState().currentPos.lat){
+ setClosestWaypoint(WIDGETS.gpstrek.getState().route, null, showProgress); removeMenu();
+ } else {
+ E.showAlert("No position").then(()=>{E.showMenu(menu);});
+ }
+ };
+ menu['Select closest waypoint (not visited)'] = function () {
+ if (WIDGETS.gpstrek.getState().currentPos && WIDGETS.gpstrek.getState().currentPos.lat){
+ setClosestWaypoint(WIDGETS.gpstrek.getState().route, WIDGETS.gpstrek.getState().route.index, showProgress); removeMenu();
+ } else {
+ E.showAlert("No position").then(()=>{E.showMenu(menu);});
+ }
+ };
+ menu['Select waypoint'] = {
+ value : WIDGETS.gpstrek.getState().route.index,
+ min:1,max:WIDGETS.gpstrek.getState().route.count,step:1,
+ onchange : v => { set(WIDGETS.gpstrek.getState().route, v-1); }
+ };
+ menu['Select waypoint as current position'] = function (){
+ WIDGETS.gpstrek.getState().currentPos.lat = WIDGETS.gpstrek.getState().route.currentWaypoint.lat;
+ WIDGETS.gpstrek.getState().currentPos.lon = WIDGETS.gpstrek.getState().route.currentWaypoint.lon;
+ WIDGETS.gpstrek.getState().currentPos.alt = WIDGETS.gpstrek.getState().route.currentWaypoint.alt;
+ removeMenu();
+ };
+ }
+
+ if (WIDGETS.gpstrek.getState().route && hasPrev(WIDGETS.gpstrek.getState().route))
+ menu['Previous waypoint'] = function() { prev(WIDGETS.gpstrek.getState().route); removeMenu(); };
+ if (WIDGETS.gpstrek.getState().route && hasNext(WIDGETS.gpstrek.getState().route))
+ menu['Next waypoint'] = function() { next(WIDGETS.gpstrek.getState().route); removeMenu(); };
+ E.showMenu(menu);
+};
+
+let showWaypointSelector = function(){
+ let waypoints = require("waypoints").load();
+ var menu = {
+ "" : {
+ back : showWaypointMenu,
+ }
+ };
+
+ waypoints.forEach((wp,c)=>{
+ menu[waypoints[c].name] = function (){
+ WIDGETS.gpstrek.getState().waypoint = waypoints[c];
+ WIDGETS.gpstrek.getState().waypointIndex = c;
+ WIDGETS.gpstrek.getState().route = null;
+ removeMenu();
+ };
+ });
+
+ E.showMenu(menu);
+};
+
+let showCalibrationMenu = function(){
+ let menu = {
+ "" : {
+ "title" : "Calibration",
+ back : showMenu,
+ },
+ "Barometer (GPS)" : ()=>{
+ if (!WIDGETS.gpstrek.getState().currentPos || isNaN(WIDGETS.gpstrek.getState().currentPos.alt)){
+ E.showAlert("No GPS altitude").then(()=>{E.showMenu(menu);});
+ } else {
+ WIDGETS.gpstrek.getState().calibAltDiff = WIDGETS.gpstrek.getState().altitude - WIDGETS.gpstrek.getState().currentPos.alt;
+ E.showAlert("Calibrated Altitude Difference: " + WIDGETS.gpstrek.getState().calibAltDiff.toFixed(0)).then(()=>{removeMenu();});
+ }
+ },
+ "Barometer (Manual)" : {
+ value : Math.round(WIDGETS.gpstrek.getState().currentPos && (WIDGETS.gpstrek.getState().currentPos.alt != undefined && !isNaN(WIDGETS.gpstrek.getState().currentPos.alt)) ? WIDGETS.gpstrek.getState().currentPos.alt: WIDGETS.gpstrek.getState().altitude),
+ min:-2000,max: 10000,step:1,
+ onchange : v => { WIDGETS.gpstrek.getState().calibAltDiff = WIDGETS.gpstrek.getState().altitude - v; }
+ },
+ "Reset Compass" : ()=>{ Bangle.resetCompass(); removeMenu();},
+ };
+ E.showMenu(menu);
+};
+
+let showWaypointMenu = function(){
+ let menu = {
+ "" : {
+ "title" : "Waypoint",
+ back : showMenu,
+ },
+ "Select waypoint" : showWaypointSelector,
+ };
+ E.showMenu(menu);
+};
+
+let showBackgroundMenu = function(){
+ let menu = {
+ "" : {
+ "title" : "Background",
+ back : showMenu,
+ },
+ "Start" : ()=>{ E.showPrompt("Start?").then((v)=>{ if (v) {WIDGETS.gpstrek.start(true); removeMenu();} else {showMenu();}}).catch(()=>{showMenu();});},
+ "Stop" : ()=>{ E.showPrompt("Stop?").then((v)=>{ if (v) {WIDGETS.gpstrek.stop(true); removeMenu();} else {showMenu();}}).catch(()=>{showMenu();});},
+ };
+ E.showMenu(menu);
+};
+
+let showMenu = function(){
+ var mainmenu = {
+ "" : {
+ "title" : "Main",
+ back : removeMenu,
+ },
+ "Route" : showRouteMenu,
+ "Waypoint" : showWaypointMenu,
+ "Background" : showBackgroundMenu,
+ "Calibration": showCalibrationMenu,
+ "Reset" : ()=>{ E.showPrompt("Do Reset?").then((v)=>{ if (v) {WIDGETS.gpstrek.resetState(); removeMenu();} else {E.showMenu(mainmenu);}}).catch(()=>{E.showMenu(mainmenu);});},
+ "Info rows" : {
+ value : WIDGETS.gpstrek.getState().numberOfSlices,
+ min:1,max:6,step:1,
+ onchange : v => { WIDGETS.gpstrek.getState().numberOfSlices = v; }
+ },
+ };
+
+ E.showMenu(mainmenu);
+};
+
+
+let switchMenu = function(){
+ stopDrawing();
+ showMenu();
+};
+
+let stopDrawing = function(){
+ if (drawTimeout) clearTimeout(drawTimeout);
+ scheduleDraw = false;
+};
+
+let drawInTimeout = function(){
+ if (global.drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = setTimeout(()=>{
+ drawTimeout = undefined;
+ draw();
+ },50);
+};
+
+let switchNav = function(){
+ if (!screen) screen = 1;
+ setButtons();
+ scheduleDraw = true;
+ firstDraw = true;
+ drawInTimeout();
+};
+
+let nextScreen = function(){
+ screen++;
+ if (screen > maxScreens){
+ screen = 1;
+ }
+ drawInTimeout();
+};
+
+let setClosestWaypoint = function(route, startindex, progress){
+ if (startindex >= WIDGETS.gpstrek.getState().route.count) startindex = WIDGETS.gpstrek.getState().route.count - 1;
+ if (!WIDGETS.gpstrek.getState().currentPos.lat){
+ set(route, startindex);
+ return;
+ }
+ let minDist = 100000000000000;
+ let minIndex = 0;
+ for (let i = startindex?startindex:0; i < route.count - 1; i++){
+ if (progress && (i % 5 == 0)) progress(i-(startindex?startindex:0), "Searching", route.count);
+ let wp = {};
+ getEntry(route.filename, route.refs[i], wp);
+ let curDist = distance(WIDGETS.gpstrek.getState().currentPos, wp);
+ if (curDist < minDist){
+ minDist = curDist;
+ minIndex = i;
+ } else {
+ if (startindex) break;
+ }
+ }
+ set(route, minIndex);
+};
+
+const finishIcon = atob("CggB//meZmeZ+Z5n/w==");
+
+const compassSliceData = {
+ getCourseType: function(){
+ return (WIDGETS.gpstrek.getState().currentPos && WIDGETS.gpstrek.getState().currentPos.course) ? "GPS" : "MAG";
+ },
+ getCourse: function (){
+ if(compassSliceData.getCourseType() == "GPS") return WIDGETS.gpstrek.getState().currentPos.course;
+ return getAveragedCompass();
+ },
+ getPoints: function (){
+ let points = [];
+ if (WIDGETS.gpstrek.getState().currentPos && WIDGETS.gpstrek.getState().currentPos.lon && WIDGETS.gpstrek.getState().route && WIDGETS.gpstrek.getState().route.currentWaypoint){
+ points.push({bearing:bearing(WIDGETS.gpstrek.getState().currentPos, WIDGETS.gpstrek.getState().route.currentWaypoint), color:"#0f0"});
+ }
+ if (WIDGETS.gpstrek.getState().currentPos && WIDGETS.gpstrek.getState().currentPos.lon && WIDGETS.gpstrek.getState().route){
+ points.push({bearing:bearing(WIDGETS.gpstrek.getState().currentPos, getLast(WIDGETS.gpstrek.getState().route)), icon: finishIcon});
+ }
+ if (WIDGETS.gpstrek.getState().currentPos && WIDGETS.gpstrek.getState().currentPos.lon && WIDGETS.gpstrek.getState().waypoint){
+ points.push({bearing:bearing(WIDGETS.gpstrek.getState().currentPos, WIDGETS.gpstrek.getState().waypoint), icon: finishIcon});
+ }
+ return points;
+ },
+ getMarkers: function (){
+ return [{xpos:0.5, width:10, height:10, linecolor:g.theme.fg, fillcolor:"#f00"}];
+ }
+};
+
+const waypointData = {
+ icon: atob("EBCBAAAAAAAAAAAAcIB+zg/uAe4AwACAAAAAAAAAAAAAAAAA"),
+ getProgress: function() {
+ return (WIDGETS.gpstrek.getState().route.index + 1) + "/" + WIDGETS.gpstrek.getState().route.count;
+ },
+ getTarget: function (){
+ if (distance(WIDGETS.gpstrek.getState().currentPos,WIDGETS.gpstrek.getState().route.currentWaypoint) < 30 && hasNext(WIDGETS.gpstrek.getState().route)){
+ next(WIDGETS.gpstrek.getState().route);
+ Bangle.buzz(1000);
+ }
+ return WIDGETS.gpstrek.getState().route.currentWaypoint;
+ },
+ getStart: function (){
+ return WIDGETS.gpstrek.getState().currentPos;
+ }
+};
+
+const finishData = {
+ icon: atob("EBABAAA/4DmgJmAmYDmgOaAmYD/gMAAwADAAMAAwAAAAAAA="),
+ getTarget: function (){
+ if (WIDGETS.gpstrek.getState().route) return getLast(WIDGETS.gpstrek.getState().route);
+ if (WIDGETS.gpstrek.getState().waypoint) return WIDGETS.gpstrek.getState().waypoint;
+ },
+ getStart: function (){
+ return WIDGETS.gpstrek.getState().currentPos;
+ }
+};
+
+let getSliceHeight = function(number){
+ return Math.floor(Bangle.appRect.h/WIDGETS.gpstrek.getState().numberOfSlices);
+};
+
+let compassSlice = getCompassSlice(compassSliceData);
+let waypointSlice = getTargetSlice(waypointData);
+let finishSlice = getTargetSlice(finishData);
+let eleSlice = getDoubleLineSlice("Up","Down",()=>{
+ return loc.distance(WIDGETS.gpstrek.getState().up,3) + "/" + (WIDGETS.gpstrek.getState().route ? loc.distance(WIDGETS.gpstrek.getState().route.up,3):"---");
+},()=>{
+ return loc.distance(WIDGETS.gpstrek.getState().down,3) + "/" + (WIDGETS.gpstrek.getState().route ? loc.distance(WIDGETS.gpstrek.getState().route.down,3): "---");
+});
+
+let statusSlice = getDoubleLineSlice("Speed","Alt",()=>{
+ let speed = 0;
+ if (WIDGETS.gpstrek.getState().currentPos && WIDGETS.gpstrek.getState().currentPos.speed) speed = WIDGETS.gpstrek.getState().currentPos.speed;
+ return loc.speed(speed,2);
+},()=>{
+ let alt = Infinity;
+ if (!isNaN(WIDGETS.gpstrek.getState().altitude)){
+ alt = isNaN(WIDGETS.gpstrek.getState().calibAltDiff) ? WIDGETS.gpstrek.getState().altitude : (WIDGETS.gpstrek.getState().altitude - WIDGETS.gpstrek.getState().calibAltDiff);
+ }
+ if (WIDGETS.gpstrek.getState().currentPos && WIDGETS.gpstrek.getState().currentPos.alt) alt = WIDGETS.gpstrek.getState().currentPos.alt;
+ if (isNaN(alt)) return "---";
+ return loc.distance(alt,3);
+});
+
+let status2Slice = getDoubleLineSlice("Compass","GPS",()=>{
+ return getAveragedCompass() + "°";
+},()=>{
+ let course = "---°";
+ if (WIDGETS.gpstrek.getState().currentPos && WIDGETS.gpstrek.getState().currentPos.course) course = WIDGETS.gpstrek.getState().currentPos.course + "°";
+ return course;
+},200);
+
+let healthSlice = getDoubleLineSlice("Heart","Steps",()=>{
+ return WIDGETS.gpstrek.getState().bpm || "---";
+},()=>{
+ return !isNaN(WIDGETS.gpstrek.getState().steps)? WIDGETS.gpstrek.getState().steps: "---";
+});
+
+let system2Slice = getDoubleLineSlice("Bat","",()=>{
+ return (Bangle.isCharging()?"+":"") + E.getBattery().toFixed(0)+"% " + (analogRead(D3)*4.2/BAT_FULL).toFixed(2) + "V";
+},()=>{
+ return "";
+});
+
+let systemSlice = getDoubleLineSlice("RAM","Storage",()=>{
+ let ram = process.memory(false);
+ return ((ram.blocksize * ram.free)/1024).toFixed(0)+"kB";
+},()=>{
+ return (STORAGE.getFree()/1024).toFixed(0)+"kB";
+});
+
+let updateSlices = function(){
+ slices = [];
+ slices.push(compassSlice);
+
+ if (WIDGETS.gpstrek.getState().currentPos && WIDGETS.gpstrek.getState().currentPos.lat && WIDGETS.gpstrek.getState().route && WIDGETS.gpstrek.getState().route.currentWaypoint && WIDGETS.gpstrek.getState().route.index < WIDGETS.gpstrek.getState().route.count - 1) {
+ slices.push(waypointSlice);
+ }
+ if (WIDGETS.gpstrek.getState().currentPos && WIDGETS.gpstrek.getState().currentPos.lat && (WIDGETS.gpstrek.getState().route || WIDGETS.gpstrek.getState().waypoint)) {
+ slices.push(finishSlice);
+ }
+ if ((WIDGETS.gpstrek.getState().route && WIDGETS.gpstrek.getState().route.down !== undefined) || WIDGETS.gpstrek.getState().down != undefined) {
+ slices.push(eleSlice);
+ }
+ slices.push(statusSlice);
+ slices.push(status2Slice);
+ slices.push(healthSlice);
+ slices.push(systemSlice);
+ slices.push(system2Slice);
+ maxScreens = Math.ceil(slices.length/WIDGETS.gpstrek.getState().numberOfSlices);
+};
+
+let clear = function() {
+ g.clearRect(Bangle.appRect);
+};
+
+let draw = function(){
+ if (!global.screen) return;
+ let ypos = Bangle.appRect.y;
+
+ let firstSlice = (screen-1)*WIDGETS.gpstrek.getState().numberOfSlices;
+
+ updateSlices();
+
+ let force = lastDrawnScreen != screen || firstDraw;
+ if (force){
+ clear();
+ }
+ if (firstDraw) Bangle.drawWidgets();
+ lastDrawnScreen = screen;
+
+ let sliceHeight = getSliceHeight();
+ for (let slice of slices.slice(firstSlice,firstSlice + WIDGETS.gpstrek.getState().numberOfSlices)) {
+ g.reset();
+ if (!slice.refresh || slice.refresh() || force) slice.draw(g,0,ypos,sliceHeight,g.getWidth());
+ ypos += sliceHeight+1;
+ g.drawLine(0,ypos-1,g.getWidth(),ypos-1);
+ }
+
+ if (scheduleDraw){
+ drawInTimeout();
+ }
+ firstDraw = false;
+};
+
+
+switchNav();
+
+clear();
+}
diff --git a/apps/gpstrek/createRoute.sh b/apps/gpstrek/createRoute.sh
new file mode 100755
index 000000000..729e6af00
--- /dev/null
+++ b/apps/gpstrek/createRoute.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+[ -z "$1" ] && echo Give gpx file name
+
+
+xmlstarlet select -t -m '//_:trkpt' \
+ --if '_:name and _:ele' -o D \
+ --elif '_:ele and not(_:name)' -o C \
+ --elif 'not(_:ele) and _:name' -o B \
+ --else -o A -b \
+ -v 'format-number(@lat,"+00.0000000;-00.0000000")' \
+ -v 'format-number(@lon,"+000.0000000;-000.0000000")' \
+ --if '_:ele' -v 'format-number(_:ele,"+00000;-00000")' -b \
+ --if _:name -v 'format-number(string-length(_:name),"00")' -v '_:name' -b \
+ -n "$1" | iconv -f utf8 -t iso8859-1 > "$(basename "$1" | sed -e "s|.gpx||").trf"
diff --git a/apps/gpstrek/icon.png b/apps/gpstrek/icon.png
new file mode 100644
index 000000000..e1ff2b99d
Binary files /dev/null and b/apps/gpstrek/icon.png differ
diff --git a/apps/gpstrek/metadata.json b/apps/gpstrek/metadata.json
new file mode 100644
index 000000000..2e2e481af
--- /dev/null
+++ b/apps/gpstrek/metadata.json
@@ -0,0 +1,18 @@
+{
+ "id": "gpstrek",
+ "name": "GPS Trekking",
+ "version": "0.09",
+ "description": "Helper for tracking the status/progress during hiking. Do NOT depend on this for navigation!",
+ "icon": "icon.png",
+ "screenshots": [{"url":"screen1.png"},{"url":"screen2.png"},{"url":"screen3.png"},{"url":"screen4.png"}],
+ "tags": "tool,outdoors,gps",
+ "supports": ["BANGLEJS2"],
+ "readme": "README.md",
+ "dependencies" : { "waypoints":"type" },
+ "storage": [
+ {"name":"gpstrek.app.js","url":"app.js"},
+ {"name":"gpstrek.wid.js","url":"widget.js"},
+ {"name":"gpstrek.img","url":"app-icon.js","evaluate":true}
+ ],
+ "data": [{"name":"gpstrek.state.json"}]
+}
diff --git a/apps/gpstrek/screen1.png b/apps/gpstrek/screen1.png
new file mode 100644
index 000000000..3cfd7d31b
Binary files /dev/null and b/apps/gpstrek/screen1.png differ
diff --git a/apps/gpstrek/screen2.png b/apps/gpstrek/screen2.png
new file mode 100644
index 000000000..9a6e14e06
Binary files /dev/null and b/apps/gpstrek/screen2.png differ
diff --git a/apps/gpstrek/screen3.png b/apps/gpstrek/screen3.png
new file mode 100644
index 000000000..a0c7fd8c3
Binary files /dev/null and b/apps/gpstrek/screen3.png differ
diff --git a/apps/gpstrek/screen4.png b/apps/gpstrek/screen4.png
new file mode 100644
index 000000000..7b6812077
Binary files /dev/null and b/apps/gpstrek/screen4.png differ
diff --git a/apps/gpstrek/widget.js b/apps/gpstrek/widget.js
new file mode 100644
index 000000000..363ade8ee
--- /dev/null
+++ b/apps/gpstrek/widget.js
@@ -0,0 +1,200 @@
+(() => {
+const SAMPLES=5;
+function initState(){
+ //cleanup volatile state here
+ state = {};
+ state.compassSamples = new Array(SAMPLES).fill(0);
+ state.lastSample = 0;
+ state.sampleIndex = 0;
+ state.currentPos={};
+ state.steps = 0;
+ state.calibAltDiff = 0;
+ state.numberOfSlices = 3;
+ state.steps = 0;
+ state.up = 0;
+ state.down = 0;
+ state.saved = 0;
+ state.avgComp = 0;
+}
+
+const STORAGE=require('Storage');
+let state = STORAGE.readJSON("gpstrek.state.json");
+if (!state) {
+ state = {};
+ initState();
+}
+let bgChanged = false;
+
+function saveState(){
+ state.saved = Date.now();
+ STORAGE.writeJSON("gpstrek.state.json", state);
+}
+
+function onKill(){
+ if (bgChanged || state.route || state.waypoint){
+ saveState();
+ }
+}
+
+E.on("kill", onKill);
+
+function onPulse(e){
+ state.bpm = e.bpm;
+}
+
+function onGPS(fix) {
+ if(fix.fix) state.currentPos = fix;
+}
+
+let radians = function(a) {
+ return a*Math.PI/180;
+};
+
+let degrees = function(a) {
+ let d = a*180/Math.PI;
+ return (d+360)%360;
+};
+
+function average(samples){
+ let s = 0;
+ let c = 0;
+ for (let h of samples){
+ s += Math.sin(radians(h));
+ c += Math.cos(radians(h));
+ }
+ s /= samples.length;
+ c /= samples.length;
+ let result = degrees(Math.atan(s/c));
+
+ if (c < 0) result += 180;
+ if (s < 0 && c > 0) result += 360;
+
+ result%=360;
+ return result;
+}
+
+function onMag(e) {
+ if (!isNaN(e.heading)){
+ if (Bangle.isLocked() || (Bangle.getGPSFix() && Bangle.getGPSFix().lon))
+ state.avgComp = e.heading;
+ else {
+ state.compassSamples[state.sampleIndex++] = e.heading;
+ state.lastSample = Date.now();
+ if (state.sampleIndex > SAMPLES - 1){
+ state.sampleIndex = 0;
+ let avg = average(state.compassSamples);
+ state.avgComp = average([state.avgComp,avg]);
+ }
+ }
+ }
+}
+
+function onStep(e) {
+ state.steps++;
+}
+
+function onPressure(e) {
+ state.pressure = e.pressure;
+
+ if (!state.altitude){
+ state.altitude = e.altitude;
+ state.up = 0;
+ state.down = 0;
+ }
+ let diff = state.altitude - e.altitude;
+ if (Math.abs(diff) > 3){
+ if (diff > 0){
+ state.up += diff;
+ } else {
+ state.down -= diff;
+ }
+ state.altitude = e.altitude;
+ }
+}
+
+function onAcc (e){
+ state.acc = e;
+}
+
+function update(){
+ if (state.active){
+ start(false);
+ }
+ if (state.active == !(WIDGETS.gpstrek.width)) {
+ if(WIDGETS.gpstrek) WIDGETS.gpstrek.width = state.active?24:0;
+ Bangle.drawWidgets();
+ }
+}
+
+function start(bg){
+ Bangle.removeListener('GPS', onGPS);
+ Bangle.removeListener("HRM", onPulse);
+ Bangle.removeListener("mag", onMag);
+ Bangle.removeListener("step", onStep);
+ Bangle.removeListener("pressure", onPressure);
+ Bangle.removeListener('accel', onAcc);
+ Bangle.on('GPS', onGPS);
+ Bangle.on("HRM", onPulse);
+ Bangle.on("mag", onMag);
+ Bangle.on("step", onStep);
+ Bangle.on("pressure", onPressure);
+ Bangle.on('accel', onAcc);
+
+ Bangle.setGPSPower(1, "gpstrek");
+ Bangle.setHRMPower(1, "gpstrek");
+ Bangle.setCompassPower(1, "gpstrek");
+ Bangle.setBarometerPower(1, "gpstrek");
+ if (bg){
+ if (!state.active) bgChanged = true;
+ state.active = true;
+ update();
+ saveState();
+ }
+}
+
+function stop(bg){
+ if (bg){
+ if (state.active) bgChanged = true;
+ state.active = false;
+ } else if (!state.active) {
+ Bangle.setGPSPower(0, "gpstrek");
+ Bangle.setHRMPower(0, "gpstrek");
+ Bangle.setCompassPower(0, "gpstrek");
+ Bangle.setBarometerPower(0, "gpstrek");
+ Bangle.removeListener('GPS', onGPS);
+ Bangle.removeListener("HRM", onPulse);
+ Bangle.removeListener("mag", onMag);
+ Bangle.removeListener("step", onStep);
+ Bangle.removeListener("pressure", onPressure);
+ Bangle.removeListener('accel', onAcc);
+ E.removeListener("kill", onKill);
+ }
+ update();
+ saveState();
+}
+
+if (state.active){
+ start(false);
+}
+
+WIDGETS["gpstrek"]={
+ area:"tl",
+ width:state.active?24:0,
+ resetState: initState,
+ getState: function() {
+ if (state.saved && Date.now() - state.saved > 60000 || !state){
+ initState();
+ }
+ return state;
+ },
+ start:start,
+ stop:stop,
+ draw:function() {
+ update();
+ if (state.active){
+ g.reset();
+ g.drawImage(atob("GBiBAAAAAAAAAAAYAAAYAAAYAAA8AAA8AAB+AAB+AADbAADbAAGZgAGZgAMYwAMYwAcY4AYYYA5+cA3/sB/D+B4AeBAACAAAAAAAAA=="), this.x, this.y);
+ }
+ }
+};
+})();
diff --git a/apps/groceryaug/ChangeLog b/apps/groceryaug/ChangeLog
new file mode 100644
index 000000000..906046782
--- /dev/null
+++ b/apps/groceryaug/ChangeLog
@@ -0,0 +1,2 @@
+0.01: New App!
+0.02: Refactor code to store grocery list in separate file
diff --git a/apps/groceryaug/README.md b/apps/groceryaug/README.md
new file mode 100644
index 000000000..aa1e62beb
--- /dev/null
+++ b/apps/groceryaug/README.md
@@ -0,0 +1,6 @@
+Modified version of the Grocery App - lets you upload an image with the products you need to shop - Display a list of product and track if you already put them in your cart.
+
+Uses this API to do the OCR: https://rapidapi.com/serendi/api/pen-to-print-handwriting-ocr
+With a free account you get 100 API calls a month.
+
+
diff --git a/apps/groceryaug/app.js b/apps/groceryaug/app.js
new file mode 100644
index 000000000..00408abba
--- /dev/null
+++ b/apps/groceryaug/app.js
@@ -0,0 +1,25 @@
+var filename = 'grocery_list_aug.json';
+var settings = require("Storage").readJSON(filename,1)|| { products: [] };
+
+function updateSettings() {
+ require("Storage").writeJSON(filename, settings);
+ Bangle.buzz();
+}
+
+
+const mainMenu = settings.products.reduce(function(m, p, i){
+const name = p.name;
+ m[name] = {
+ value: p.ok,
+ format: v => v?'[x]':'[ ]',
+ onchange: v => {
+ settings.products[i].ok = v;
+ updateSettings();
+ }
+ };
+ return m;
+}, {
+ '': { 'title': 'Grocery list' }
+});
+mainMenu['< Back'] = ()=>{load();};
+E.showMenu(mainMenu);
diff --git a/apps/groceryaug/groceryaug-icon.js b/apps/groceryaug/groceryaug-icon.js
new file mode 100644
index 000000000..33b649647
--- /dev/null
+++ b/apps/groceryaug/groceryaug-icon.js
@@ -0,0 +1 @@
+E.toArrayBuffer(atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAiAAMAADAAAwAAMAAiAAMAAAAAAAA/8zP/Mz/zM/8z/zM/8zP/Mz/zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA///MzMzMzMzMzM/////8zP//zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA///MzMzMzMzMz//////8zP//zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA///MzMzMzMzMzM/////8zP//zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA///MzMzMzMzMz//////8zP//zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA/////////////MzMzMzMzP//zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAAAA////////////////////////zAAAAARE////////////////////////zERAAARE////////////////////////zERAAERE////////////////////////zEREAERE////////////////////////zEREAAREzMzMzMzMzMzMzMzMzMzMzMzMzERAAABEREREREREREREREREREREREREREQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="))
diff --git a/apps/groceryaug/groceryaug.html b/apps/groceryaug/groceryaug.html
new file mode 100644
index 000000000..6ed07df62
--- /dev/null
+++ b/apps/groceryaug/groceryaug.html
@@ -0,0 +1,145 @@
+
+
+
+
+
+ Enter/change API key
+
+
+
+
save API key
+
API key saved!
+
If you don't have an API key, you can create one here . You get 100 API calls a month for free.
+
+
+
+ Products
+
+
+ Upload
+
+
+
+
+
+
+
+
diff --git a/apps/groceryaug/groceryaug.png b/apps/groceryaug/groceryaug.png
new file mode 100644
index 000000000..895a6bbca
Binary files /dev/null and b/apps/groceryaug/groceryaug.png differ
diff --git a/apps/groceryaug/groceryaug_preview.gif b/apps/groceryaug/groceryaug_preview.gif
new file mode 100644
index 000000000..9b099f86e
Binary files /dev/null and b/apps/groceryaug/groceryaug_preview.gif differ
diff --git a/apps/groceryaug/metadata.json b/apps/groceryaug/metadata.json
new file mode 100644
index 000000000..13f377584
--- /dev/null
+++ b/apps/groceryaug/metadata.json
@@ -0,0 +1,17 @@
+{
+ "id": "groceryaug",
+ "name": "Grocery Augmented",
+ "version": "0.02",
+ "description": "Modified version of the Grocery App - lets you upload an image with the products you need to shop - Display a list of product and track if you already put them in your cart.",
+ "icon": "groceryaug.png",
+ "readme":"README.md",
+ "type": "app",
+ "tags": "tool,outdoors,shopping,list",
+ "supports": ["BANGLEJS", "BANGLEJS2"],
+ "custom": "groceryaug.html",
+ "allow_emulator": true,
+ "storage": [
+ {"name":"groceryaug.app.js","url":"app.js"},
+ {"name":"groceryaug.img","url":"groceryaug-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/ha/ChangeLog b/apps/ha/ChangeLog
index 07afedd21..c0d58e5bc 100644
--- a/apps/ha/ChangeLog
+++ b/apps/ha/ChangeLog
@@ -1 +1,7 @@
-0.01: Release
\ No newline at end of file
+0.01: Release
+0.02: Includeas the ha.lib.js library that can be used by other apps or clocks.
+0.03: Added clkinfo for clocks.
+0.04: Feedback if clkinfo run is called.
+0.05: Clkinfo improvements.
+0.06: Updated clkinfo icon.
+0.07: Update clock_info to avoid a redraw
diff --git a/apps/ha/README.md b/apps/ha/README.md
index 8005421f1..654a262c8 100644
--- a/apps/ha/README.md
+++ b/apps/ha/README.md
@@ -1,13 +1,15 @@
# Home Assistant
This app integrates your BangleJs into the HomeAssistant.
+
# How to use
Click on the left and right side of the screen to select the triggers that you
configured. Click in the middle of the screen to send the trigger to HomeAssistant.

-# First Setup
+
+# Initial Setup
1.) First of all, make sure that HomeAssistant and the HomeAssistant Android App works.
2.) Open your BangleJs Gadgetbridge App, click on the Settings icon of your BangleJs and enable "Allow Intent Access"
@@ -22,6 +24,7 @@ configured. Click in the middle of the screen to send the trigger to HomeAssista
This setup must be done only once -- now you are ready to configure your BangleJS to
control some devices or entities in your HomeAssistant :)
+
# Setup Trigger
1.) Upload the app and all corresponding triggers through the AppStore UI. You must specify
the display name, the trigger as well as an icon.
@@ -38,12 +41,36 @@ The following icons are currently supported:
3.) Don't forget to select the action that should be executed at the bottom of each automation.
+
# Default Trigger
This app also implements two default trigger that can always be used:
- APP_STARTED -- Will be sent whenever the app is started. So you could do some actions already when the app is sarted without the need of any user interaction.
- TRIGGER -- Will be sent whenever some trigger is executed. So you could generically listen to that.
+# How to use the library (ha.lib.js) in my own app/clk
+This app inlcludes a library that can be used by other apps or clocks
+to read all configured intents or to send a trigger. Example code:
+
+```js
+// First of all impport the library
+var ha = require("ha.lib.js");
+
+// You can read all triggers that a user configured simply via
+var triggers = ha.getTriggers();
+
+// Get display name and icon of trigger
+var display = triggers[0].display;
+var icon = triggers[0].getIcon();
+
+// Trigger the first configured trigger
+ha.sendTrigger(triggers[0].trigger);
+
+// Send a custom trigger that is not configured by a user
+ha.sendTrigger("MY_CUSTOM_TRIGGER");
+```
+
+
# FAQ
## Sometimes the trigger is not executed
diff --git a/apps/ha/ha.app.js b/apps/ha/ha.app.js
index 85f926138..d9199fb0e 100644
--- a/apps/ha/ha.app.js
+++ b/apps/ha/ha.app.js
@@ -1,72 +1,10 @@
-var storage = require("Storage");
+/**
+ * This app uses the ha library to send trigger to HomeAssistant.
+ */
+var ha = require("ha.lib.js");
var W = g.getWidth(), H = g.getHeight();
var position=0;
-
-
-// Note: All icons should have 48x48 pixels
-function getIcon(icon){
- if(icon == "light"){
- return {
- width : 48, height : 48, bpp : 1,
- transparent : 0,
- buffer : require("heatshrink").decompress(atob("AAMBwAFE4AFDgYFJjgFBnAFBjwXBvAFBh4jBuAFCAQPwAQMHAQPgEQQCBEgcf/AvDn/8Aof//5GDAoJOBh+BAoOB+EP8YFB4fwgfnAoPnGANHAoPjHYQFBHYQFd44pDg47C4/gh/DIIZNFLIplGgF//wFIgZ9BRIUHRII7Ch4FBUIUOAoKzCjwFEhgCBmDpIVooFFh4oCAA4LFC5b7BAob1BAYI="))
- };
- } else if(icon == "door"){
- return {
- width : 48, height : 48, bpp : 1,
- transparent : 0,
- buffer : require("heatshrink").decompress(atob("AAM4Aok/4AED///Aov4Aon8DgQGBAv4FpnIFKJv4FweAQFFAgQFB8AFDnADC"))
- };
- } else if (icon == "fire"){
- return {
- width : 48, height : 48, bpp : 1,
- transparent : 0,
- buffer : require("heatshrink").decompress(atob("ABsDAokBwAFE4AFE8AFE+AFE/AFJgf8Aon+AocHAokP/8QAokYAoUfAok//88ApF//4kDAo//AgMQAgIFCjgFEjwFCOYIFFHQIFDn/+AoJ/BAoIqBAoN//xCBAoI5BDIPAgP//gFB8AFChYFBgf//EJAogOBAoSgBAoMHAQIFEFgXAAoJEBv4FCNoQFGVYd/wAFEYYIFIvwCBDoV8UwQCBcgUPwDwDfQMBaIYADA"))
- };
- }
-
- // Default is always the HA icon
- return {
- width : 48, height : 48, bpp : 1,
- transparent : 0,
- buffer : require("heatshrink").decompress(atob("AD8BwAFDg/gAocP+AFDj4FEn/8Aod//wFD/1+FAf4j+8AoMD+EPDAUH+OPAoUP+fPAoUfBYk/C4l/EYIwC//8n//FwIFEgYFD4EH+E8nkP8BdBAonjjk44/wj/nzk58/4gAFDF4PgCIMHAoPwhkwh4FB/EEkEfIIWAHwIFC4A+BAoXgg4FDL4IFDL4IFDLIYFkAEQA=="))
- };
-}
-
-// Try to read custom actions, otherwise use default
-var triggers = [
- {display: "Not found.", trigger: "NOP", icon: "ha"},
-];
-
-try{
- triggers = storage.read("ha.trigger.json");
- triggers = JSON.parse(triggers);
-} catch(e) {
- // In case there are no user triggers yet, we show the default...
-}
-
-
-function sendIntent(trigger){
- var retries=3;
-
- while(retries > 0){
- try{
- // Send a startup trigger such that we could also execute
- // an action when the app is started :)
- Bluetooth.println(JSON.stringify({
- t:"intent",
- action:"com.espruino.gadgetbridge.banglejs.HA",
- extra:{
- trigger: trigger
- }})
- );
- retries = -1;
-
- } catch(e){
- retries--;
- }
- }
-}
+var triggers = ha.getTriggers();
function draw() {
@@ -78,7 +16,7 @@ function draw() {
var w = g.stringWidth(trigger.display);
g.setFontAlign(-1,-1);
- var icon = getIcon(trigger.icon);
+ var icon = trigger.getIcon();
g.setColor(g.theme.fg).drawImage(icon, 12, H/5-2);
g.drawString("Home", icon.width + 20, H/5);
g.drawString("Assistant", icon.width + 18, H/5+24);
@@ -112,13 +50,11 @@ Bangle.on('touch', function(btn, e){
}
if(!isRight && !isLeft){
-
- // Send a default intent that we triggered something.
- sendIntent("TRIGGER");
+ ha.sendTrigger("TRIGGER");
// Now send the selected trigger
Bangle.buzz(80, 0.6).then(()=>{
- sendIntent(triggers[position].trigger);
+ ha.sendTrigger(triggers[position].trigger);
setTimeout(()=>{
Bangle.buzz(80, 0.6);
}, 250);
@@ -126,12 +62,14 @@ Bangle.on('touch', function(btn, e){
}
});
+
// Send intent that the we started the app.
-sendIntent("APP_STARTED");
+ha.sendTrigger("APP_STARTED");
// Next load the widgets and draw the app
Bangle.loadWidgets();
Bangle.drawWidgets();
+// Draw app
draw();
setWatch(_=>load(), BTN1);
diff --git a/apps/ha/ha.clkinfo.js b/apps/ha/ha.clkinfo.js
new file mode 100644
index 000000000..09724ba45
--- /dev/null
+++ b/apps/ha/ha.clkinfo.js
@@ -0,0 +1,26 @@
+(function() {
+ var ha = require("ha.lib.js");
+ var triggers = ha.getTriggers();
+
+ var haItems = {
+ name: "Home",
+ img: atob("GBiBAAAAAAAAAAAAAAAYAAA+AAB+AADD4AHb4APD4Afn8A/n+BxmOD0mnA0ksAwAMA+B8A/D8A/n8A/n8A/n8A/n8AAAAAAAAAAAAA=="),
+ items: []
+ };
+
+ triggers.forEach((trigger, i) => {
+ haItems.items.push({
+ name: null,
+ get: () => ({ text: trigger.display, img: trigger.getIcon()}),
+ show: function() {},
+ hide: function () {},
+ run: function() {
+ ha.sendTrigger("TRIGGER_BW");
+ ha.sendTrigger(trigger.trigger);
+ return true;
+ }
+ });
+ });
+
+ return haItems;
+})
diff --git a/apps/ha/ha.lib.js b/apps/ha/ha.lib.js
new file mode 100644
index 000000000..b09cbeab2
--- /dev/null
+++ b/apps/ha/ha.lib.js
@@ -0,0 +1,80 @@
+/**
+ * This library can be used to read all triggers that a user
+ * configured and send a trigger to homeassistant.
+ */
+function _getIcon(trigger){
+ icon = trigger.icon;
+ if(icon == "light"){
+ return {
+ width : 48, height : 48, bpp : 1,
+ transparent : 0,
+ buffer : require("heatshrink").decompress(atob("AAMBwAFE4AFDgYFJjgFBnAFBjwXBvAFBh4jBuAFCAQPwAQMHAQPgEQQCBEgcf/AvDn/8Aof//5GDAoJOBh+BAoOB+EP8YFB4fwgfnAoPnGANHAoPjHYQFBHYQFd44pDg47C4/gh/DIIZNFLIplGgF//wFIgZ9BRIUHRII7Ch4FBUIUOAoKzCjwFEhgCBmDpIVooFFh4oCAA4LFC5b7BAob1BAYI="))
+ };
+ } else if(icon == "door"){
+ return {
+ width : 48, height : 48, bpp : 1,
+ transparent : 0,
+ buffer : require("heatshrink").decompress(atob("AAM4Aok/4AED///Aov4Aon8DgQGBAv4FpnIFKJv4FweAQFFAgQFB8AFDnADC"))
+ };
+ } else if (icon == "fire"){
+ return {
+ width : 48, height : 48, bpp : 1,
+ transparent : 0,
+ buffer : require("heatshrink").decompress(atob("ABsDAokBwAFE4AFE8AFE+AFE/AFJgf8Aon+AocHAokP/8QAokYAoUfAok//88ApF//4kDAo//AgMQAgIFCjgFEjwFCOYIFFHQIFDn/+AoJ/BAoIqBAoN//xCBAoI5BDIPAgP//gFB8AFChYFBgf//EJAogOBAoSgBAoMHAQIFEFgXAAoJEBv4FCNoQFGVYd/wAFEYYIFIvwCBDoV8UwQCBcgUPwDwDfQMBaIYADA"))
+ };
+ }
+
+ // Default is always the HA icon
+ return {
+ width : 48, height : 48, bpp : 1,
+ transparent : 0,
+ buffer : require("heatshrink").decompress(atob("AD8BwAFDg/gAocP+AFDj4FEn/8Aod//wFD/1+FAf4j+8AoMD+EPDAUH+OPAoUP+fPAoUfBYk/C4l/EYIwC//8n//FwIFEgYFD4EH+E8nkP8BdBAonjjk44/wj/nzk58/4gAFDF4PgCIMHAoPwhkwh4FB/EEkEfIIWAHwIFC4A+BAoXgg4FDL4IFDL4IFDLIYFkAEQA=="))
+ };
+}
+
+exports.getTriggers = function(){
+ var triggers = [
+ {display: "Empty", trigger: "NOP", icon: "ha"},
+ ];
+
+ try{
+ triggers = require("Storage").read("ha.trigger.json");
+ triggers = JSON.parse(triggers);
+
+ // We lazy load all icons, otherwise, we have to keep
+ // all the icons n times in memory which can be
+ // problematic for embedded devices. Therefore,
+ // we lazy load icons only if needed using the getIcon
+ // method of each trigger...
+ triggers.forEach(trigger => {
+ trigger.getIcon = function(){
+ return _getIcon(trigger);
+ }
+ })
+ } catch(e) {
+ // In case there are no user triggers yet, we show the default...
+ }
+
+ return triggers;
+}
+
+exports.sendTrigger = function(triggerName){
+ var retries=3;
+
+ while(retries > 0){
+ try{
+ // Now lets send the trigger that we sould send.
+ Bluetooth.println(JSON.stringify({
+ t:"intent",
+ action:"com.espruino.gadgetbridge.banglejs.HA",
+ extra:{
+ trigger: triggerName
+ }})
+ );
+ retries = -1;
+
+ } catch(e){
+ retries--;
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/ha/metadata.json b/apps/ha/metadata.json
index 0f9929d8c..1432e010e 100644
--- a/apps/ha/metadata.json
+++ b/apps/ha/metadata.json
@@ -1,11 +1,11 @@
{
"id": "ha",
"name": "HomeAssistant",
- "version": "0.01",
+ "version": "0.07",
"description": "Integrates your BangleJS into HomeAssistant.",
"icon": "ha.png",
"type": "app",
- "tags": "tool",
+ "tags": "tool,clkinfo",
"readme": "README.md",
"supports": ["BANGLEJS2"],
"custom": "custom.html",
@@ -19,6 +19,8 @@
],
"storage": [
{"name":"ha.app.js","url":"ha.app.js"},
+ {"name":"ha.lib.js","url":"ha.lib.js"},
+ {"name":"ha.clkinfo.js","url":"ha.clkinfo.js"},
{"name":"ha.img","url":"ha.icon.js","evaluate":true}
]
}
diff --git a/apps/hcclock/ChangeLog b/apps/hcclock/ChangeLog
index f70653d58..e2eb18be3 100644
--- a/apps/hcclock/ChangeLog
+++ b/apps/hcclock/ChangeLog
@@ -1,3 +1,5 @@
0.01: Base code
0.02: Saved settings when switching color scheme
-0.03: Added Button 3 opening messages (if app is installed)
\ No newline at end of file
+0.03: Added Button 3 opening messages (if app is installed)
+0.04: Use `messages` library to check for new messages
+0.05: Use `messages` library to open message GUI
\ No newline at end of file
diff --git a/apps/hcclock/hcclock.app.js b/apps/hcclock/hcclock.app.js
index de5163996..f12a4733e 100644
--- a/apps/hcclock/hcclock.app.js
+++ b/apps/hcclock/hcclock.app.js
@@ -3,7 +3,7 @@
// Numbers Rect order (left, top, right, bottom)
// Each number defines a set of rects to draw
-const numbers =
+const numbers =
[
[// Zero
[0, 0, 1, 0.2],
@@ -64,7 +64,7 @@ const numbers =
[0, 0.8, 1, 1],
[0, 0, 0.1, 0.6],
[0.9, 0, 1, 1]
- ]
+ ]
];
const months = [ "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" ];
@@ -103,7 +103,7 @@ function updateTime()
let mo = now.getMonth();
let y = now.getFullYear();
let d = now.getDate();
-
+
if(h != hour)
{
hour = h;
@@ -127,7 +127,7 @@ function updateTime()
day = d;
g.setFont("6x8", 2);
g.setFontAlign(0, -1, 0);
- g.drawString(fmtDate(d,mo,y,hour), 120, 120);
+ g.drawString(fmtDate(d,mo,y,hour), 120, 120);
}
drawMessages();
}
@@ -136,7 +136,7 @@ function drawDigits(x, value)
{
if(!Bangle.isLCDOn()) // No need to draw when LCD Off
return;
-
+
drawChar(Math.floor(value/10), 15, x, 115, x+50);
if(value%10 == 1)
drawChar(value%10, 55, x, 155, x+50);
@@ -228,27 +228,18 @@ function flipColors()
// MESSAGE HANDLING()
//
-let messages_installed = require("Storage").read("messages.app.js") != undefined;
+let messages_installed = require("Storage").read("messages") !== undefined;
function handleMessages()
{
- if(messages_installed && hasMessages() > 0)
- {
- E.showMessage("Loading Messages...");
- load("messages.app.js");
- }
+ if(!hasMessages()) return;
+ E.showMessage("Loading Messages...");
+ require("messages").openGUI();
}
function hasMessages()
{
- if(!messages_installed)
- return false;
-
- var messages = require("Storage").readJSON("messages.json",1)||[];
- if (messages.some(m=>m.new))
- return true;
- else
- return false;
+ return messages_installed && require("messages").status() === 'new';
}
let msg = atob("GBiBAAAAAAAAAAAAAAAAAAAAAB//+DAADDAADDAADDwAPD8A/DOBzDDn/DA//DAHvDAPvjAPvjAPvjAPvh///gf/vAAD+AAB8AAAAA==");
@@ -256,20 +247,21 @@ let had_messages = false;
function drawMessages()
{
- if(!had_messages && hasMessages()) {
+ const has_messages = hasMessages();
+ if(has_messages === had_messages) return;
+ if(has_messages) {
g.setColor(255,255,255);
g.drawImage(msg, 184, 212);
g.setFont("6x8", 2);
g.setFontAlign(0, -1, 0);
g.drawString(">", 224, 216);
- had_messages = true;
- }
- else if (had_messages && !hasMessages())
+ }
+ else
{
g.setColor(0,0,0);
g.fillRect(180, 210, 240, 240);
- had_messages = false;
}
+ had_messages = has_messages;
}
//////////////////////////////////////////
diff --git a/apps/hcclock/metadata.json b/apps/hcclock/metadata.json
index 0d4cbe0cd..407114e25 100644
--- a/apps/hcclock/metadata.json
+++ b/apps/hcclock/metadata.json
@@ -1,7 +1,7 @@
{
"id": "hcclock",
"name": "Hi-Contrast Clock",
- "version": "0.03",
+ "version": "0.05",
"description": "Hi-Contrast Clock : A simple yet very bold clock that aims to be readable in high luninosity environments. Uses big 10x5 pixel digits. Use BTN 1 to switch background and foreground colors.",
"icon": "hcclock-icon.png",
"type": "clock",
diff --git a/apps/health/ChangeLog b/apps/health/ChangeLog
index 62d93e606..fc8f2c950 100644
--- a/apps/health/ChangeLog
+++ b/apps/health/ChangeLog
@@ -14,3 +14,5 @@
0.13: Add support for internationalization
0.14: Move settings
0.15: Fix charts (fix #1366)
+0.16: Code tidyup, add back button in top left of health app graphs
+0.17: Add automatic translation of bar chart labels
diff --git a/apps/health/app.js b/apps/health/app.js
index c0a40bd93..844dd7241 100644
--- a/apps/health/app.js
+++ b/apps/health/app.js
@@ -1,6 +1,4 @@
function menuMain() {
- swipe_enabled = false;
- clearButton();
E.showMenu({
"": { title: /*LANG*/"Health Tracking" },
/*LANG*/"< Back": () => load(),
@@ -12,8 +10,6 @@ function menuMain() {
}
function menuStepCount() {
- swipe_enabled = false;
- clearButton();
E.showMenu({
"": { title:/*LANG*/"Steps" },
/*LANG*/"< Back": () => menuMain(),
@@ -23,8 +19,6 @@ function menuStepCount() {
}
function menuMovement() {
- swipe_enabled = false;
- clearButton();
E.showMenu({
"": { title:/*LANG*/"Movement" },
/*LANG*/"< Back": () => menuMain(),
@@ -34,8 +28,6 @@ function menuMovement() {
}
function menuHRM() {
- swipe_enabled = false;
- clearButton();
E.showMenu({
"": { title:/*LANG*/"Heart Rate" },
/*LANG*/"< Back": () => menuMain(),
@@ -48,22 +40,16 @@ function stepsPerHour() {
E.showMessage(/*LANG*/"Loading...");
var data = new Uint16Array(24);
require("health").readDay(new Date(), h=>data[h.hr]+=h.steps);
- g.clear(1);
- Bangle.drawWidgets();
- g.reset();
setButton(menuStepCount);
- barChart("HOUR", data);
+ barChart(/*LANG*/"HOUR", data);
}
function stepsPerDay() {
E.showMessage(/*LANG*/"Loading...");
var data = new Uint16Array(31);
require("health").readDailySummaries(new Date(), h=>data[h.day]+=h.steps);
- g.clear(1);
- Bangle.drawWidgets();
- g.reset();
setButton(menuStepCount);
- barChart("DAY", data);
+ barChart(/*LANG*/"DAY", data);
}
function hrmPerHour() {
@@ -75,11 +61,8 @@ function hrmPerHour() {
if (h.bpm) cnt[h.hr]++;
});
data.forEach((d,i)=>data[i] = d/cnt[i]);
- g.clear(1);
- Bangle.drawWidgets();
- g.reset();
setButton(menuHRM);
- barChart("HOUR", data);
+ barChart(/*LANG*/"HOUR", data);
}
function hrmPerDay() {
@@ -91,37 +74,27 @@ function hrmPerDay() {
if (h.bpm) cnt[h.day]++;
});
data.forEach((d,i)=>data[i] = d/cnt[i]);
- g.clear(1);
- Bangle.drawWidgets();
- g.reset();
setButton(menuHRM);
- barChart("DAY", data);
+ barChart(/*LANG*/"DAY", data);
}
function movementPerHour() {
E.showMessage(/*LANG*/"Loading...");
var data = new Uint16Array(24);
require("health").readDay(new Date(), h=>data[h.hr]+=h.movement);
- g.clear(1);
- Bangle.drawWidgets();
- g.reset();
setButton(menuMovement);
- barChart("HOUR", data);
+ barChart(/*LANG*/"HOUR", data);
}
function movementPerDay() {
E.showMessage(/*LANG*/"Loading...");
var data = new Uint16Array(31);
require("health").readDailySummaries(new Date(), h=>data[h.day]+=h.movement);
- g.clear(1);
- Bangle.drawWidgets();
- g.reset();
setButton(menuMovement);
- barChart("DAY", data);
+ barChart(/*LANG*/"DAY", data);
}
// Bar Chart Code
-
const w = g.getWidth();
const h = g.getHeight();
@@ -130,13 +103,10 @@ var chart_index;
var chart_max_datum;
var chart_label;
var chart_data;
-var swipe_enabled = false;
-var btn;
// find the max value in the array, using a loop due to array size
function max(arr) {
var m = -Infinity;
-
for(var i=0; i< arr.length; i++)
if(arr[i] > m) m = arr[i];
return m;
@@ -145,10 +115,8 @@ function max(arr) {
// find the end of the data, the array might be for 31 days but only have 2 days of data in it
function get_data_length(arr) {
var nlen = arr.length;
-
for(var i = arr.length - 1; i > 0 && arr[i] == 0; i--)
nlen--;
-
return nlen;
}
@@ -167,15 +135,11 @@ function drawBarChart() {
const bar_width = (w - 2) / 9; // we want 9 bars, bar 5 in the centre
var bar_top;
var bar;
-
- g.setColor(g.theme.bg);
- g.fillRect(0,24,w,h);
+ g.reset().clearRect(0,24,w,h);
for (bar = 1; bar < 10; bar++) {
if (bar == 5) {
- g.setFont('6x8', 2);
- g.setFontAlign(0,-1);
- g.setColor(g.theme.fg);
+ g.setFont('6x8', 2).setFontAlign(0,-1).setColor(g.theme.fg);
g.drawString(chart_label + " " + (chart_index + bar -1) + " " + chart_data[chart_index + bar - 1], g.getWidth()/2, 150);
g.setColor("#00f");
} else {
@@ -189,45 +153,26 @@ function drawBarChart() {
bar_top = bar_bot;
g.fillRect( 1 + (bar - 1)* bar_width, bar_bot, 1 + bar*bar_width, bar_top);
- g.setColor(g.theme.fg);
- g.drawRect( 1 + (bar - 1)* bar_width, bar_bot, 1 + bar*bar_width, bar_top);
+ g.setColor(g.theme.fg).drawRect( 1 + (bar - 1)* bar_width, bar_bot, 1 + bar*bar_width, bar_top);
}
}
-function next_bar() {
- chart_index = Math.min(data_len - 5, chart_index + 1);
-}
-
-function prev_bar() {
- // HOUR data starts at index 0, DAY data starts at index 1
- chart_index = Math.max((chart_label == "DAY") ? -3 : -4, chart_index - 1);
-}
-
-Bangle.on('swipe', dir => {
- if (!swipe_enabled) return;
- if (dir == 1) prev_bar(); else next_bar();
- drawBarChart();
-});
-
-// use setWatch() as Bangle.setUI("updown",..) interacts with swipes
function setButton(fn) {
- // cancel callback, otherwise a slight up down movement will show the E.showMenu()
- Bangle.setUI("updown", undefined);
-
- if (process.env.HWVERSION == 1)
- btn = setWatch(fn, BTN2);
- else
- btn = setWatch(fn, BTN1);
-}
-
-function clearButton() {
- if (btn !== undefined) {
- clearWatch(btn);
- btn = undefined;
- }
+ Bangle.setUI({mode:"custom",
+ back:fn,
+ swipe:(lr,ud) => {
+ if (lr == 1) {
+ // HOUR data starts at index 0, DAY data starts at index 1
+ chart_index = Math.max((chart_label == /*LANG*/"DAY") ? -3 : -4, chart_index - 1);
+ } else if (lr<0) {
+ chart_index = Math.min(data_len - 5, chart_index + 1);
+ } else {
+ return fn();
+ }
+ drawBarChart();
+ }});
}
Bangle.loadWidgets();
Bangle.drawWidgets();
-
menuMain();
diff --git a/apps/health/metadata.json b/apps/health/metadata.json
index a038f67b5..12f6b617f 100644
--- a/apps/health/metadata.json
+++ b/apps/health/metadata.json
@@ -1,7 +1,8 @@
{
"id": "health",
"name": "Health Tracking",
- "version": "0.15",
+ "shortName": "Health",
+ "version": "0.17",
"description": "Logs health data and provides an app to view it",
"icon": "app.png",
"tags": "tool,system,health",
diff --git a/apps/henkinen/ChangeLog b/apps/henkinen/ChangeLog
new file mode 100644
index 000000000..5560f00bc
--- /dev/null
+++ b/apps/henkinen/ChangeLog
@@ -0,0 +1 @@
+0.01: New App!
diff --git a/apps/henkinen/README.md b/apps/henkinen/README.md
new file mode 100644
index 000000000..e17e86121
--- /dev/null
+++ b/apps/henkinen/README.md
@@ -0,0 +1,7 @@
+# Henkinen
+
+By Jukio Kallio
+
+A tiny app helping you to breath and relax.
+
+
diff --git a/apps/henkinen/app-icon.js b/apps/henkinen/app-icon.js
new file mode 100644
index 000000000..7c82a375d
--- /dev/null
+++ b/apps/henkinen/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwkEogA0/4AKCpNPCxYAB+gtTGJQuOGBAWPGAwuQGAwXH+cykc/C6UhgMSkMQiQXKBQsgiYFDmMCMBIIEmAWEDAUDC5nzBwogDMYgXHBoohJC4wuJEQwXG+ALDmUQgMjEYcPC5MhAYXxgAACj4ICVYYXGIwXzCwYABHAUwC5HyEwXwC4pEC+MvC4/xEoUQC4sBHIQlCC4vwIxBIEGYQXFmJKCC45ECfQQXIRoiRGC5EiOxB4EBwQXdI653XU67XX+QJCPAwrC+JKCC4v/gZIIHIUwCAQXGkIDCSIg4C/8SC5PwEwX/mUQgMjAwXzJQQXH+ZICAA8wEYYXGBgoAEEQoXHGBIhFC44OBcgQADmIgFC5H/kAYEmMCBooXDp4KFkMBiUhiCjDAAX0C5RjBmUjPo4XMABQXEMAwALCwgwRFwowRCwwwPFw4xOCpIArA"))
diff --git a/apps/henkinen/app.js b/apps/henkinen/app.js
new file mode 100644
index 000000000..d7c7bd5ed
--- /dev/null
+++ b/apps/henkinen/app.js
@@ -0,0 +1,127 @@
+// Henkinen
+//
+// Bangle.js 2 breathing helper
+// by Jukio Kallio
+// www.jukiokallio.com
+
+require("FontHaxorNarrow7x17").add(Graphics);
+
+// settings
+const breath = {
+ theme: "default",
+ x:0, y:0, w:0, h:0,
+ size: 60,
+
+ bgcolor: g.theme.bg,
+ incolor: g.theme.fg,
+ keepcolor: g.theme.fg,
+ outcolor: g.theme.fg,
+
+ font: "HaxorNarrow7x17", fontsize: 1,
+ textcolor: g.theme.fg,
+ texty: 18,
+
+ in: 4000,
+ keep: 7000,
+ out: 8000
+};
+
+// set some additional settings
+breath.w = g.getWidth(); // size of the background
+breath.h = g.getHeight();
+breath.x = breath.w * 0.5; // position of the circles
+breath.y = breath.h * 0.45;
+breath.texty = breath.y + breath.size + breath.texty; // text position
+
+var wait = 100; // wait time, normally a minute
+var time = 0; // for time keeping
+
+
+// timeout used to update every minute
+var drawTimeout;
+
+// schedule a draw for the next minute
+function queueDraw() {
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = setTimeout(function() {
+ drawTimeout = undefined;
+ draw();
+ }, wait - (Date.now() % wait));
+}
+
+
+// main function
+function draw() {
+ // make date object
+ var date = new Date();
+
+ // update current time
+ time += wait - (Date.now() % wait);
+ if (time > breath.in + breath.keep + breath.out) time = 0; // reset time
+
+ // Reset the state of the graphics library
+ g.reset();
+
+ // Clear the area where we want to draw the time
+ g.setColor(breath.bgcolor);
+ g.fillRect(0, 0, breath.w, breath.h);
+
+ // calculate circle size
+ var circle = 0;
+ if (time < breath.in) {
+ // breath in
+ circle = time / breath.in;
+ g.setColor(breath.incolor);
+
+ } else if (time < breath.in + breath.keep) {
+ // keep breath
+ circle = 1;
+ g.setColor(breath.keepcolor);
+
+ } else if (time < breath.in + breath.keep + breath.out) {
+ // breath out
+ circle = ((breath.in + breath.keep + breath.out) - time) / breath.out;
+ g.setColor(breath.outcolor);
+
+ }
+
+ // draw breath circle
+ g.fillCircle(breath.x, breath.y, breath.size * circle);
+
+ // breath area
+ g.setColor(breath.textcolor);
+ g.drawCircle(breath.x, breath.y, breath.size);
+
+ // draw text
+ g.setFontAlign(0,0).setFont(breath.font, breath.fontsize).setColor(breath.textcolor);
+
+ if (time < breath.in) {
+ // breath in
+ g.drawString("Breath in", breath.x, breath.texty);
+
+ } else if (time < breath.in + breath.keep) {
+ // keep breath
+ g.drawString("Keep it in", breath.x, breath.texty);
+
+ } else if (time < breath.in + breath.keep + breath.out) {
+ // breath out
+ g.drawString("Breath out", breath.x, breath.texty);
+
+ }
+
+ // queue draw
+ queueDraw();
+}
+
+
+// Clear the screen once, at startup
+g.clear();
+// draw immediately at first
+draw();
+
+
+// keep LCD on
+Bangle.setLCDPower(1);
+
+// Show launcher when middle button pressed
+Bangle.setUI("clock");
diff --git a/apps/henkinen/app.png b/apps/henkinen/app.png
new file mode 100644
index 000000000..575ecbcd4
Binary files /dev/null and b/apps/henkinen/app.png differ
diff --git a/apps/henkinen/metadata.json b/apps/henkinen/metadata.json
new file mode 100644
index 000000000..1f1bb77fc
--- /dev/null
+++ b/apps/henkinen/metadata.json
@@ -0,0 +1,15 @@
+{ "id": "henkinen",
+ "name": "Henkinen - Tiny Breathing Helper",
+ "shortName":"Henkinen",
+ "version":"0.01",
+ "description": "A tiny app helping you to breath and relax.",
+ "icon": "app.png",
+ "screenshots": [{"url":"screenshot1.png"}],
+ "tags": "outdoors",
+ "supports" : ["BANGLEJS","BANGLEJS2"],
+ "readme": "README.md",
+ "storage": [
+ {"name":"henkinen.app.js","url":"app.js"},
+ {"name":"henkinen.img","url":"app-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/henkinen/screenshot1.png b/apps/henkinen/screenshot1.png
new file mode 100644
index 000000000..938494673
Binary files /dev/null and b/apps/henkinen/screenshot1.png differ
diff --git a/apps/hidcam/ChangeLog b/apps/hidcam/ChangeLog
index 480d7d448..b0ba92aef 100644
--- a/apps/hidcam/ChangeLog
+++ b/apps/hidcam/ChangeLog
@@ -1,3 +1,4 @@
0.01: Core functionnality
0.02: Offer to enable HID if disabled
0.03: Adds Readme and tags to be used by App Loader
+0.04: Adds Bangle.js 2 support, Buzz and Touch
diff --git a/apps/hidcam/README.md b/apps/hidcam/README.md
index 5e8d40817..fa8e0153b 100644
--- a/apps/hidcam/README.md
+++ b/apps/hidcam/README.md
@@ -7,7 +7,7 @@ Control the camera shutter from your phone using your watch
1. In settings, enable HID for "Keyboard & Media".
2. Pair your watch to your phone.
3. Load your camera app on your phone.
-4. There you go, launch the app on your watch and press button 2 to trigger the shutter !
+4. There you go, launch the app on your watch and press the button (button 2 on Bangle.js 1) to trigger the shutter !
## How does it work ?
diff --git a/apps/hidcam/app.js b/apps/hidcam/app.js
index bb8ddf7e9..639018db3 100644
--- a/apps/hidcam/app.js
+++ b/apps/hidcam/app.js
@@ -1,25 +1,29 @@
var storage = require('Storage');
const settings = storage.readJSON('setting.json',1) || { HID: false };
-
+const isB2 = process.env.HWVERSION === 2;
var sendHid, camShot, profile;
if (settings.HID=="kbmedia") {
profile = 'camShutter';
sendHid = function (code, cb) {
- try {
- NRF.sendHIDReport([1,code], () => {
- NRF.sendHIDReport([1,0], () => {
- if (cb) cb();
- });
- });
- } catch(e) {
- print(e);
- }
+
+ try {
+ NRF.sendHIDReport([1,code], () => {
+ NRF.sendHIDReport([1,0], () => {
+ if (cb) cb();
+ });
+ });
+ } catch(e) {
+ print(e);
+ }
+
};
camShot = function (cb) { sendHid(0x80, cb); };
} else {
+
E.showPrompt("Enable HID?",{title:"HID disabled"}).then(function(enable) {
+
if (enable) {
settings.HID = "kbmedia";
require("Storage").write('setting.json', settings);
@@ -31,10 +35,15 @@ function drawApp() {
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
- g.fillCircle(122,127,60);
- g.drawImage(storage.read("hidcam.img"),100,105);
- const d = g.getWidth() - 18;
-
+ if (!isB2) { // Bangle.js 1
+ g.fillCircle(122,127,60);
+ g.drawImage(storage.read("hidcam.img"),100,105);
+ const d = g.getWidth() - 18;
+ } else {
+ g.fillCircle(90,95,60);
+ g.drawImage(storage.read("hidcam.img"),65,70);
+ const d = g.getWidth() - 18;
+ }
function c(a) {
return {
width: 8,
@@ -46,12 +55,27 @@ function drawApp() {
g.fillRect(180,130, 240, 124);
}
-if (camShot) {
- setWatch(function(e) {
- E.showMessage('camShot !');
- setTimeout(drawApp, 1000);
- camShot(() => {});
- }, BTN2, { edge:"falling",repeat:true,debounce:50});
-
+ if (camShot) {
+ if (!isB2) { // Bangle.js 1
+ setWatch(function(e) {
+ E.showMessage('camShot !');
+ Bangle.buzz(300, 1);
+ setTimeout(drawApp, 1000);
+ camShot(() => {});
+ }, BTN2, { edge:"falling",repeat:true,debounce:50});
+ } else { // Bangle.js 2
+ setWatch(function(e) {
+ E.showMessage('camShot !');
+ Bangle.buzz(300, 1);
+ setTimeout(drawApp, 1000);
+ camShot(() => {});
+ }, BTN, { edge:"falling",repeat:true,debounce:50});
+ Bangle.on('touch', function (wat, tap) {
+ E.showMessage('camShot !');
+ Bangle.buzz(300, 1);
+ setTimeout(drawApp, 1000);
+ camShot(() => {});
+ });
+ }
drawApp();
-}
+ }
diff --git a/apps/hidcam/metadata.json b/apps/hidcam/metadata.json
index b2ef33229..b57d41ed1 100644
--- a/apps/hidcam/metadata.json
+++ b/apps/hidcam/metadata.json
@@ -2,11 +2,11 @@
"id": "hidcam",
"name": "Camera shutter",
"shortName": "Cam shutter",
- "version": "0.03",
+ "version": "0.04",
"description": "Enable HID, connect to your phone, start your camera and trigger the shot on your Bangle",
"icon": "app.png",
"tags": "bluetooth,tool",
- "supports": ["BANGLEJS"],
+ "supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"hidcam.app.js","url":"app.js"},
diff --git a/apps/hidmsicswipe/changelog b/apps/hidmsicswipe/ChangeLog
similarity index 100%
rename from apps/hidmsicswipe/changelog
rename to apps/hidmsicswipe/ChangeLog
diff --git a/apps/homework/ChangeLog b/apps/homework/ChangeLog
new file mode 100644
index 000000000..b9a5425d1
--- /dev/null
+++ b/apps/homework/ChangeLog
@@ -0,0 +1,2 @@
+...
+0.10: First update with ChangeLog Added
diff --git a/apps/homework/metadata.json b/apps/homework/metadata.json
index a46c74dad..d2af99fd4 100644
--- a/apps/homework/metadata.json
+++ b/apps/homework/metadata.json
@@ -2,7 +2,7 @@
{ "id": "homework",
"name": "Homework",
"shortName":"Homework",
- "version":"0.1",
+ "version":"0.10",
"description": "A simple app to manage homework",
"icon": "app.png",
"tags": "tool",
diff --git a/apps/hourstrike/metadata.json b/apps/hourstrike/metadata.json
index 614db54e4..d0ddb511a 100644
--- a/apps/hourstrike/metadata.json
+++ b/apps/hourstrike/metadata.json
@@ -11,7 +11,8 @@
"storage": [
{"name":"hourstrike.app.js","url":"app.js"},
{"name":"hourstrike.boot.js","url":"boot.js"},
- {"name":"hourstrike.img","url":"app-icon.js","evaluate":true},
+ {"name":"hourstrike.img","url":"app-icon.js","evaluate":true}
+ ], "data" : [
{"name":"hourstrike.json","url":"hourstrike.json"}
]
}
diff --git a/apps/hrm/ChangeLog b/apps/hrm/ChangeLog
index 62956e8cd..b55ba8930 100644
--- a/apps/hrm/ChangeLog
+++ b/apps/hrm/ChangeLog
@@ -8,3 +8,4 @@
0.08: Don't force backlight on/watch unlocked on Bangle 2
0.09: Grey out BPM until confidence is over 50%
0.10: Autoscale raw graph to maximum value seen
+0.11: Automatic translation of strings.
diff --git a/apps/hrm/heartrate.js b/apps/hrm/heartrate.js
index 386341e6d..2e5a720e5 100644
--- a/apps/hrm/heartrate.js
+++ b/apps/hrm/heartrate.js
@@ -37,7 +37,7 @@ function updateHrm(){
var px = g.getWidth()/2;
g.setFontAlign(0,-1);
g.clearRect(0,24,g.getWidth(),80);
- g.setFont("6x8").drawString("Confidence "+(hrmInfo.confidence || "--")+"%", px, 70);
+ g.setFont("6x8").drawString(/*LANG*/"Confidence "+(hrmInfo.confidence || "--")+"%", px, 70);
updateScale();
@@ -46,7 +46,7 @@ function updateHrm(){
g.setFontVector(40).setColor(hrmInfo.confidence > 50 ? g.theme.fg : "#888").drawString(str,px,45);
px += g.stringWidth(str)/2;
g.setFont("6x8").setColor(g.theme.fg);
- g.drawString("BPM",px+15,45);
+ g.drawString(/*LANG*/"BPM",px+15,45);
}
function updateScale(){
@@ -101,7 +101,7 @@ Bangle.loadWidgets();
Bangle.drawWidgets();
g.setColor(g.theme.fg);
g.reset().setFont("6x8",2).setFontAlign(0,-1);
-g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2 - 16);
+g.drawString(/*LANG*/"Please wait...",g.getWidth()/2,g.getHeight()/2 - 16);
countDown();
diff --git a/apps/hrm/metadata.json b/apps/hrm/metadata.json
index c5a5f4f4d..f254b5d23 100644
--- a/apps/hrm/metadata.json
+++ b/apps/hrm/metadata.json
@@ -1,7 +1,7 @@
{
"id": "hrm",
"name": "Heart Rate Monitor",
- "version": "0.10",
+ "version": "0.11",
"description": "Measure your heart rate and see live sensor data",
"icon": "heartrate.png",
"tags": "health",
diff --git a/apps/hrmaccevents/ChangeLog b/apps/hrmaccevents/ChangeLog
index 2748e5f62..b8519d272 100644
--- a/apps/hrmaccevents/ChangeLog
+++ b/apps/hrmaccevents/ChangeLog
@@ -1,3 +1,5 @@
0.01: New App!
0.02: Show status info on display
Allow recording to Bangle
+0.03: Allow downloading recorded files
+ Make it work with more BTHRM configs
diff --git a/apps/hrmaccevents/README.md b/apps/hrmaccevents/README.md
index 9c131cfd8..ecd619152 100644
--- a/apps/hrmaccevents/README.md
+++ b/apps/hrmaccevents/README.md
@@ -6,13 +6,28 @@ This app can use [BTHRM](https://banglejs.com/apps/#bthrm) as a reference.
## Steps for usage
* (Optional) Install [BTHRM](https://banglejs.com/apps/#bthrm) as reference (use ECG based sensor for best accuracy).
- * Configure BTHRM to "Both"-Mode. This prevents data beeing lost because BTHRM can replace the HRM-events data with BTHRM data.
+ * Configure BTHRM to "Both"-Mode or use a version >= 0.12. This prevents data beeing lost because BTHRM can replace the HRM-events data with BTHRM data.
* Click "Start" in browser.
* Wait until the "Events" number starts to grow, that means there are events recorded.
* Record for some time, since BTHRM and HRM often need some seconds to start getting useful values. Consider 2000 events a useful minimum.
* (Recording to file) Stop the recording with a long press of the button and download log.csv with the Espruino IDE.
* (Recording to browser) Click "Stop" followed by "Save" and store the resulting file on your device.
+
+## CSV data format
+
+The CSV data contains the following columns:
+
+* Time - Current time (milliseconds since 1970)
+* Acc_x,Acc_y,Acc_z - X,Y,Z acceleration in Gs
+* HRM_b - BPM figure reported by internal HRM algorithm in Bangle.js
+* HRM_c - BPM confidence figure (0..100%) reported by internal HRM algorithm in Bangle.js
+* HRM_r - `e.raw` from the `Bangle.on("HRM-raw"` event. This is the value that gets passed to the HRM algorithm.
+* HRM_f - `e.filt` from the `Bangle.on("HRM-raw"` event. This is the filtered value that comes from the Bangle's HRM algorithm and which is used for peak detection
+* PPG_r - `e.vcPPG` from the `Bangle.on("HRM-raw"` event. This is the PPG value direct from the sensor
+* PPG_o - `e.vcPPGoffs` from the `Bangle.on("HRM-raw"` event. This is the PPG offset used to map `e.vcPPG` to `e.raw` so there are no glitches when the exposure values in the sensor change.
+* BTHRM - BPM figure from external Bluetooth HRM device (this is our reference BPM)
+
## Creator
[halemmerich](https://github.com/halemmerich)
diff --git a/apps/hrmaccevents/custom.html b/apps/hrmaccevents/custom.html
index 02276eb64..a5bf9796f 100644
--- a/apps/hrmaccevents/custom.html
+++ b/apps/hrmaccevents/custom.html
@@ -1,18 +1,42 @@
Bangle.js Accelerometer streaming
+
-
Store on Bangle (file named log.csv, download with IDE)
- Start
- Stop
- Reset
- Save CSV
+
+
+
+ Store on bangle (file named log.csv)
+
+
+
+
+ Start
+ Stop
+ Reset
+ Save CSV
+ Download CSV
-
+
+
@@ -23,6 +23,10 @@
Options:
Wrap draw calls in timeouts (Slower, more RAM use, better interactivity)
+
+ Force use of direct drawing (Even faster, but will produce visible artifacts on not optimized watch faces)
+
+ Do not create combined app flle (slower but more flexible for debugging, incompatible with minification)
Add debug prints to generated code
@@ -30,7 +34,7 @@
Select watchface folder:
or
Select watchface zip file:
-
+
Upload to watch
Save resources file
Save face file
@@ -53,15 +57,15 @@
var expectedFiles = 0;
var rootZip = new JSZip();
var resourcesZip = rootZip.folder("resources");
-
+
function isNativeFormat(){
return document.getElementById("useNative").checked;
}
-
+
function addDebug(){
return document.getElementById("debugprints").checked;
}
-
+
function convertAmazfitTime(time){
var result = {};
if (time.Hours){
@@ -86,7 +90,7 @@
}
return result;
}
-
+
function convertAmazfitDate(date){
var result = {};
if (date.MonthAndDay.Separate.Day) result.Day = convertAmazfitNumber(date.MonthAndDay.Separate.Day, "Day");
@@ -96,11 +100,11 @@
}
return result;
}
-
+
var filesToMove={};
-
+
var zipChangePromise = Promise.resolve();
-
+
function performFileChanges(){
var promise = Promise.resolve();
//rename all files to just numbers without leading zeroes
@@ -109,7 +113,7 @@
var tmp = resultJson[c];
delete resultJson[c];
resultJson[Number(c)] = tmp;
-
+
async function modZip(c){
console.log("Async modification of ", c)
var fileRegex = new RegExp(c + ".*");
@@ -118,27 +122,27 @@
console.log("Filedata is", fileData);
var extension = resourcesZip.file(fileRegex)[0].name.match(/\.[^.]*$/);
var newName = Number(c) + extension;
-
+
console.log("Renaming to", newName);
resourcesZip.remove(c + extension);
resourcesZip.file(newName, fileData);
}
promise = promise.then(modZip(c));
-
+
}
-
-
+
+
console.log("File moves:", filesToMove);
-
+
for (var c in filesToMove){
var tmp = resultJson[c];
console.log("Handle filemove", c, filesToMove[c], tmp);
-
+
var element = resultJson;
var path = filesToMove[c];
-
-
+
+
async function modZip(c){
console.log("Async modification of ", c)
var fileRegex = new RegExp(c + ".*");
@@ -147,13 +151,13 @@
console.log("Filedata is", fileData);
var extension = resourcesZip.file(fileRegex)[0].name.match(/\.[^.]*$/);
var newName = Number(c) + extension;
-
+
console.log("Copying to", newName);
resourcesZip.file(filesToMove[c].join("/") + extension, fileData);
}
promise = promise.then(modZip(c));
-
-
+
+
for (var i = 0; i< path.length; i++){
if (!element[path[i]]) element[path[i]] = {};
if (i == path.length - 1){
@@ -162,7 +166,7 @@
element = element[path[i]];
}
}
-
+
}
promise.then(()=>{
document.getElementById('btnUpload').disabled = true;
@@ -170,7 +174,7 @@
console.log("After moves", resultJson);
return promise;
};
-
+
function convertAmazfitMultistate(multistate, value, minValue, maxValue){
var result = {
MultiState: {
@@ -186,18 +190,18 @@
if (multistate.ImageIndexOff) filesToMove[multistate.ImageIndexOff] = ["status", value, "off"];
return result;
}
-
+
function convertAmazfitStatus(status){
var result = {};
-
+
if (status.Alarm) result.Alarm = convertAmazfitMultistate(status.Alarm,"Alarm");
if (status.Bluetooth) result.Bluetooth = convertAmazfitMultistate(status.Bluetooth,"Bluetooth");
if (status.DoNotDisturb) result.DoNotDisturb = convertAmazfitMultistate(status.DoNotDisturb,"Notifications");
if (status.Lock) result.Lock = convertAmazfitMultistate(status.Lock,"Lock");
-
+
return result;
}
-
+
function convertAmazfitNumber(element, value, minValue, maxValue){
var number = {};
var result = {
@@ -231,10 +235,10 @@
if (maxValue !== undefined) number.MinValue = minValue;
return result;
}
-
+
function moveWeatherIcons(icon){
filesToMove[icon.ImageIndex + 0] = ["weather", "fallback"];
-
+
// Light clouds
filesToMove[icon.ImageIndex + 1] = ["weather", 801];
// Cloudy, possible rain
@@ -280,7 +284,7 @@
// Very heavy shower
filesToMove[icon.ImageIndex + 22] = ["weather", 531];
}
-
+
function convertAmazfitTemperature(temp){
var result = {};
result = convertAmazfitNumber(temp.Number, "WeatherTemperature");
@@ -292,15 +296,15 @@
}
return result;
}
-
+
function convertAmazfitWeather(weather){
var result = {};
-
+
if (weather.Temperature && weather.Temperature.Current){
if (!result.Temperature) result.Temperature = {};
result.Temperature.Current = convertAmazfitTemperature(weather.Temperature.Current);
}
-
+
if (weather.Temperature && weather.Temperature.Today){
if (!result.Temperature) result.Temperature = {};
if (weather.Temperature.Today.Separate){
@@ -325,10 +329,10 @@
}
return result;
}
-
+
function convertAmazfitActivity(activity){
var result = {};
-
+
if (activity.Steps){
result.Steps = convertAmazfitNumber(activity.Steps, "Steps");
}
@@ -337,7 +341,7 @@
}
return result;
}
-
+
function convertAmazfitScale(scale, value, minValue, maxValue){
var result = {};
result.Scale = {
@@ -354,10 +358,10 @@
Y: c.Y
});
}
-
+
return result;
}
-
+
function convertAmazfitStepsProgress(steps){
var result = {};
if (steps.GoalImage){
@@ -376,7 +380,7 @@
}
return result;
}
-
+
function convertAmazfitBattery(battery){
var result = {};
if (battery.Scale){
@@ -387,7 +391,7 @@
}
return result;
}
-
+
function convertAmazfitImage(image){
var result = {
Image: {
@@ -399,11 +403,11 @@
};
return result;
}
-
+
function convertAmazfitColor(color){
return "#" + color.substring(2);
}
-
+
function convertAmazfitHand(hand, rotationValue, minRotationValue, maxRotationValue){
var result = {
Filled: !hand.OnlyBorder,
@@ -416,18 +420,18 @@
MaxRotationValue: maxRotationValue,
MinRotationValue: minRotationValue
};
-
+
result.Vertices = []
for (var c of hand.Shape){
result.Vertices.push(c);
}
return { Poly: result };
}
-
+
function convertAmazfitAnalog(analog, face){
var result = {
};
-
+
if (analog.Hours){
result.Hours = {};
result.Hours.Hand = convertAmazfitHand(analog.Hours, "Hour12Analog", 0, 12);
@@ -462,14 +466,14 @@
}
return result;
}
-
+
function restructureAmazfitFormat(dataString){
console.log("Amazfit data:", dataString);
-
-
+
+
var json = JSON.parse(dataString);
faceJson = json;
-
+
var result = {};
result.Properties = {};
@@ -477,8 +481,8 @@
result.Properties.Redraw.Unlocked = 60000;
result.Properties.Redraw.Locked = 60000;
result.Properties.Redraw.Clear = true;
-
-
+
+
if (json.Background){
result.Background = json.Background;
result.Background.Image.ImagePath = [];
@@ -489,32 +493,32 @@
result.Time = convertAmazfitTime(json.Time);
if (json.AnalogDialFace) result.Time.Plane = 1;
}
-
+
if (json.Date){
result.Date = convertAmazfitDate(json.Date);
if (json.AnalogDialFace) result.Date.Plane = 1;
}
-
+
if (json.Status){
result.Status = convertAmazfitStatus(json.Status);
if (json.AnalogDialFace) result.Status.Plane = 1;
}
-
+
if (json.Weather){
result.Weather = convertAmazfitWeather(json.Weather);
if (json.AnalogDialFace) result.Weather.Plane = 1;
}
-
+
if (json.Activity){
result.Activity = convertAmazfitActivity(json.Activity);
if (json.AnalogDialFace) result.Activity.Plane = 1;
}
-
+
if (json.StepsProgress){
result.StepsProgress = convertAmazfitStepsProgress(json.StepsProgress);
if (json.AnalogDialFace) result.StepsProgress.Plane = 1;
}
-
+
if (json.Battery){
result.Battery = convertAmazfitBattery(json.Battery);
if (json.AnalogDialFace) result.Battery.Plane = 1;
@@ -527,7 +531,7 @@
return result;
}
-
+
function parseFaceJson(jsonString){
if (isNativeFormat()){
return JSON.parse(jsonString);
@@ -535,7 +539,7 @@
return restructureAmazfitFormat(jsonString);
}
}
-
+
function combineProperty(name, source, target){
if (source[name] && target[name]){
if (Array.isArray(target[name])){
@@ -554,7 +558,7 @@
if (typeof element == "string" || typeof element == "number") return [];
for (var c in element){
var next = element[c];
-
+
combineProperty("X",element,next);
combineProperty("Y",element,next);
combineProperty("Width",element,next);
@@ -569,7 +573,7 @@
combineProperty("MaxRotationValue",element,next);
if (typeof element.Plane == "number") next.Plane = element.Plane;
next.Layer = element.Layer ? (element.Layer) : "" + c;
-
+
if (["MultiState","Image","CodedImage","Number","Circle","Poly","Rect","Scale"].includes(c)){
result.push({type:c, value: next});
} else {
@@ -578,15 +582,12 @@
}
return result;
}
-
- function convertToCode(elements, properties, wrapInTimeouts){
+
+ function convertToCode(elements, properties, wrapInTimeouts, forceUseOrigPlane){
var code = "(function (wr, wf) {\n";
- if (!wrapInTimeouts){
- code += "var ct=Date.now();\n";
- }
code += "var lc;\n";
code += "var p = Promise.resolve();\n";
-
+
//get mapped by layer
var counter = 0;
var planes = {};
@@ -595,7 +596,7 @@
var c = elements[i].value;
console.log("Check element", c);
var name = c.Layer;
- var plane = wrapInTimeouts ? 1 : 0;
+ var plane = (wrapInTimeouts && !forceUseOrigPlane) ? 1 : 0;
if (typeof c.Plane == "number"){
plane = c.Plane;
}
@@ -607,72 +608,57 @@
}
if (!planeNumbers.includes(0)) planeNumbers.push(0);
planeNumbers.sort().reverse();
-
+
console.log("Found planes", planes, "with numbers", planeNumbers)
-
- if (wrapInTimeouts && planes == 0) planes = 1;
-
+
code += "p0 = g;\n";
-
+
for (var planeIndex = 0; planeIndex < planeNumbers.length; planeIndex++){
var layers = planes[planeNumbers[planeIndex]];
var plane = planeNumbers[planeIndex];
-
+
var lastSetColor;
var lastSetBgColor;
-
+
if (plane != 0) code += "if (!p" + plane + ") p" + plane + " = Graphics.createArrayBuffer(g.getWidth(),g.getHeight(),4,{msb:true});\n";
-
+
if (properties.Redraw && properties.Redraw.Clear){
- if (wrapInTimeouts && plane != 0){
+ if (wrapInTimeouts && (plane != 0 || forceUseOrigPlane)){
code += "p = p.then(()=>delay(0)).then(()=>{\n";
} else {
code += "p = p.then(()=>{\n";
}
- code += "var ct=Date.now();\n"
if (addDebug()) code += 'print("Clear for redraw of plane ' + p + '");'+"\n";
code += 'startPerfLog("initialDraw_g.clear");'+"\n";
code += "p" + plane + ".clear(true);\n";
code += 'endPerfLog("initialDraw_g.clear");'+ "\n";
-
- code += "drawingTime += Date.now() - ct;\n";
code += "});\n";
}
-
+
var previousPlane = plane + 1;
if (previousPlane < planeNumbers.length){
code += "p = p.then(()=>{\n";
- code += "var ct=Date.now();\n";
-
+
if (addDebug()) code += 'print("Copying of plane ' + previousPlane + ' to display");'+"\n";
//code += "g.drawImage(p" + i + ".asImage());";
code += "p0.drawImage({width: p" + previousPlane + ".getWidth(), height: p" + previousPlane + ".getHeight(), bpp: p" + previousPlane + ".getBPP(), buffer: p" + previousPlane + ".buffer, palette: palette});\n";
-
-
- code += "drawingTime += Date.now() - ct;\n";
code += "});\n";
}
-
+
console.log("Got layers", layers);
for (var layername in layers){
var layerElements = layers[layername];
-
+
console.log("Layer elements", layername, layerElements);
//code for whole layer
-
- if (wrapInTimeouts && plane != 0){
- code += "p = p.then(()=>delay(0)).then(()=>{\n";
- } else {
- code += "p = p.then(()=>{\n";
- }
- code += "var ct=Date.now();\n";
+
if (addDebug()) code += 'print("Starting layer ' + layername + '");' + "\n";
-
+
var checkForLayerChange = false;
var checkcode = "";
-
+
if (!(properties.Redraw && properties.Redraw.Clear)){
- checkcode = 'firstDraw';
+ checkcode = 's.fd';
for (var i = 0; i< layerElements.length; i++){
var layerElement = layerElements[i];
var referencedElement = elements[layerElements[i].index];
@@ -680,38 +666,38 @@
console.log("Check for change:", layerElement, referencedElement);
if (layerElement.element.Value){
if (elementType == "MultiState" && layerElement.element.Value) {
- checkcode += '| isChangedMultistate(wf.Collapsed[' + layerElement.index + '].value)';
+ checkcode += '| isChangedMultistate(wf.c[' + layerElement.index + '].value)';
} else {
- checkcode += '| isChangedNumber(wf.Collapsed[' + layerElement.index + '].value)';
+ checkcode += '| isChangedNumber(wf.c[' + layerElement.index + '].value)';
}
checkForLayerChange = true;
}
}
}
-
-
+
+
//code for elements
for (var i = 0; i< layerElements.length; i++){
var elementIndex = layerElements[i].index;
var c = elements[elementIndex];
console.log("convert to code", c);
-
+
var condition = "";
if (checkcode.length > 0 && checkForLayerChange){
if (condition.length > 0) condition += " && ";
condition = '(' + checkcode + ')';
}
-
+
if (c.value.HideOn && c.value.HideOn.includes("Lock")){
if (condition.length > 0) condition += " && ";
condition = '!Bangle.isLocked()';
}
-
+
if (c.value.Type == "Once"){
if (condition.length > 0) condition += " && ";
- condition += "firstDraw";
+ condition += "s.fd";
}
-
+
var planeName = "p" + plane;
var colorsetting = "";
if (c.value.ForegroundColor && lastSetColor != c.value.ForegroundColor){
@@ -728,25 +714,28 @@
else
colorsetting += planeName + ".setBgColor(\"" + c.value.BackgroundColor + "\");\n";
}
-
- if (addDebug()) code += 'print("Element condition is ' + condition + '");' + "\n";
- code += "" + colorsetting;
- code += (condition.length > 0 ? "if (" + condition + "){\n" : "");
- if (addDebug()) code += 'print("Drawing element ' + elementIndex + ' with type ' + c.type + ' on plane ' + planeName + '");' + "\n";
- code += "draw" + c.type + "(" + planeName + ", wr, wf.Collapsed[" + elementIndex + "].value);\n";
+ if (addDebug()) code += 'print("Element condition is ' + condition + '");' + "\n";
+ code += (condition.length > 0 ? "if (" + condition + "){\n" : "");
+ if (wrapInTimeouts && (plane != 0 || forceUseOrigPlane)){
+ code += "p = p.then(()=>delay(0)).then(()=>{\n";
+ } else {
+ code += "p = p.then(()=>{\n";
+ }
+ code += "" + colorsetting;
+ if (addDebug()) code += 'print("Drawing element ' + elementIndex + ' with type ' + c.type + ' on plane ' + planeName + '");' + "\n";
+ code += "draw" + c.type + "(" + planeName + ", wr, wf.c[" + elementIndex + "].value);\n";
+
+ code += "});\n";
code += (condition.length > 0 ? "}\n" : "");
}
-
- code += "drawingTime += Date.now() - ct;\n";
- code += "});\n";
}
console.log("Current plane is", plane);
-
-
+
+
}
-
+
code += "return p;})";
console.log("Code:", code);
return code
@@ -755,14 +744,14 @@
function postProcess(){
moveData(resultJson);
console.log("Created data file", resourceDataString, resourceDataOffset, resultJson);
-
+
var properties = faceJson.Properties;
- faceJson = { Properties: properties, Collapsed: collapseTree(faceJson,{X:0,Y:0})};
+ faceJson = { Properties: properties, c: collapseTree(faceJson,{X:0,Y:0})};
console.log("After collapsing", faceJson);
- precompiledJs = convertToCode(faceJson.Collapsed, properties, document.getElementById('timeoutwrap').checked);
+ precompiledJs = convertToCode(faceJson.c, properties, document.getElementById('timeoutwrap').checked, document.getElementById('forceOrigPlane').checked);
console.log("After precompiling", precompiledJs);
}
-
+
function convertJsToJson(imgstr){
var E = {};
E.toArrayBuffer = (s)=>s;
@@ -781,7 +770,7 @@
function imageLoaded() {
var options = {};
-
+
options.diffusion = infoJson.diffusion ? infoJson.diffusion : "none";
options.compression = false;
options.alphaToColor = false;
@@ -792,12 +781,12 @@
options.contrast = 0;
options.mode = infoJson.color ? infoJson.color : "1bit";
options.output = "object";
-
+
console.log("Loaded image has path", this.path);
var jsonPath = this.path.split("/");
-
+
var forcedTransparentColorMatch = jsonPath[jsonPath.length-1].match(/.*\.t([^.]+)\..*/)
-
+
var forcedTransparentColor;
if (jsonPath[jsonPath.length-1].includes(".t.")){
options.transparent = true;
@@ -805,13 +794,13 @@
options.transparent = false;
forcedTransparentColor = forcedTransparentColorMatch[1];
}
-
-
+
+
console.log("image has transparency", options.transparent);
console.log("image has forced transparent color", forcedTransparentColor);
jsonPath[jsonPath.length-1] = jsonPath[jsonPath.length-1].replace(/([^.]*)\..*/, "$1");
console.log("Loaded image has json path", jsonPath);
-
+
var canvas = document.getElementById("canvas")
canvas.width = this.width*2;
canvas.height = this.height;
@@ -832,7 +821,7 @@
imgstr = imageconverter.RGBAtoString(rgba, options);
var outputImageData = new ImageData(options.rgbaOut, options.width, options.height);
ctx.putImageData(outputImageData,this.width,0);
-
+
imgstr = convertJsToJson(imgstr);
// checkerboard for transparency on original image
@@ -840,9 +829,9 @@
imageconverter.RGBAtoCheckerboard(imageData.data, {width:this.width,height:this.height});
ctx.putImageData(imageData,0,0);
-
+
var currentElement = resultJson;
-
+
for (var i = 0; i < jsonPath.length; i++){
if (i == jsonPath.length - 1){
var resultingObject = JSON.parse(imgstr);
@@ -854,18 +843,18 @@
currentElement = currentElement[jsonPath[i]];
}
}
-
+
handledFiles++;
console.log("Expected:", expectedFiles, " handled:", handledFiles);
-
+
if (handledFiles == expectedFiles){
if (!isNativeFormat()) {
performFileChanges().then(()=>{
postProcess();
-
+
rootZip.file("face.json", JSON.stringify(faceJson, null, 2));
rootZip.file("info.json", JSON.stringify(infoJson, null, 2));
-
+
document.getElementById('btnSave').disabled = false;
document.getElementById('btnSaveFace').disabled = false;
document.getElementById('btnSaveZip').disabled = false;
@@ -873,21 +862,21 @@
});
} else {
postProcess();
-
+
document.getElementById('btnSave').disabled = false;
document.getElementById('btnSaveFace').disabled = false;
document.getElementById('btnUpload').disabled = false;
}
}
}
-
+
function handleWatchFace(infoFile, faceFile, resourceFiles){
if (isNativeFormat()){
var reader = new FileReader();
reader.path = infoFile.webkitRelativePath;
reader.onload = function(event) {
infoJson = JSON.parse(reader.result);
-
+
handleFaceJson(faceFile, resourceFiles);
};
reader.readAsText(infoFile);
@@ -896,18 +885,18 @@
handleFaceJson(faceFile, resourceFiles);
}
}
-
+
function handleFaceJson(faceFile, resourceFiles){
var reader = new FileReader();
reader.path = faceFile.webkitRelativePath;
reader.onload = function(event) {
faceJson = parseFaceJson(reader.result);
-
+
handleResourceFiles(resourceFiles);
};
reader.readAsText(faceFile);
}
-
+
function handleResourceFiles(files){
for (var current of files){
console.log('Handle resource file ', current);
@@ -930,25 +919,25 @@
reader.readAsDataURL(current);
}
}
-
+
function handleFileSelect(event) {
handledFiles = 0;
expectedFiles = undefined;
-
+
document.getElementById('btnSave').disabled = true;
document.getElementById('btnSaveZip').disabled = true;
document.getElementById('btnSaveFace').disabled = true;
document.getElementById('btnUpload').disabled = true;
-
+
console.log("File select event", event);
if (event.target.files.length == 0) return;
result = "";
resultJson= {};
-
+
var resourceFiles = [];
var faceFile;
var infoFile;
-
+
for (var current of event.target.files){
console.log('Handle file ', current);
if (isNativeFormat()){
@@ -983,10 +972,10 @@
}
}
handleWatchFace(infoFile, faceFile, resourceFiles);
-
+
};
document.getElementById('fileLoader').addEventListener('change', handleFileSelect, false);
-
+
function moveData(json){
console.log("MoveData for", json);
for (var k in json){
@@ -1010,7 +999,11 @@
}
}
}
-
+
+ document.getElementById("timeoutwrap").addEventListener("click", function() {
+ document.getElementById("forceOrigPlane").disabled = !document.getElementById("timeoutwrap").checked;
+ });
+
document.getElementById("btnSave").addEventListener("click", function() {
var h = document.createElement('a');
h.href = 'data:text/json;charset=utf-8,' + encodeURI(JSON.stringify(resultJson));
@@ -1019,25 +1012,48 @@
h.click();
});
document.getElementById("btnUpload").addEventListener("click", function() {
-
+
+ console.log("Fetching app");
+ fetch('app.js').then((r) => {
+ console.log("Got response", r);
+ return r.text();
+ }
+ ).then((imageclockSrc) => {
+ console.log("Got src", imageclockSrc)
+
+ if (!document.getElementById('separateFiles').checked){
+ if (precompiledJs.length > 0){
+ const replacementString = 'eval(require("Storage").read("imageclock.draw.js"))';
+ console.log("Can replace:", imageclockSrc.includes(replacementString));
+ imageclockSrc = imageclockSrc.replace(replacementString, precompiledJs);
+ }
+ imageclockSrc = imageclockSrc.replace('require("Storage").readJSON("imageclock.face.json")', JSON.stringify(faceJson));
+ imageclockSrc = imageclockSrc.replace('require("Storage").readJSON("imageclock.resources.json")', JSON.stringify(resultJson));
+ }
var appDef = {
id : "imageclock",
storage:[
- {name:"imageclock.app.js", url:"app.js"},
- {name:"imageclock.resources.json", content: JSON.stringify(resultJson)},
{name:"imageclock.img", url:"app-icon.js", evaluate:true},
]
};
+ if (document.getElementById('separateFiles').checked){
+ appDef.storage.push({name:"imageclock.app.js", url:"app.js"});
+ if (precompiledJs.length > 0){
+ appDef.storage.push({name:"imageclock.draw.js", content:precompiledJs});
+ }
+ appDef.storage.push({name:"imageclock.face.json", content: JSON.stringify(faceJson)});
+ appDef.storage.push({name:"imageclock.resources.json", content: JSON.stringify(resultJson)});
+ } else {
+ appDef.storage.push({name:"imageclock.app.js", url:"pleaseminifycontent.js", content:imageclockSrc});
+ }
if (resourceDataString.length > 0){
appDef.storage.push({name:"imageclock.resources.data", content: resourceDataString});
}
- appDef.storage.push({name:"imageclock.draw.js", content: precompiledJs.length > 0 ? precompiledJs : "//empty"});
- appDef.storage.push({name:"imageclock.face.json", content: JSON.stringify(faceJson)});
-
console.log("Uploading app:", appDef);
sendCustomizedApp(appDef);
+ });
});
-
+
function handleZipSelect(evt) {
@@ -1049,18 +1065,18 @@
document.getElementById('btnSaveZip').disabled = true;
document.getElementById('btnUpload').disabled = true;
JSZip.loadAsync(f).then(function(zip) {
-
+
console.log("Zip loaded", zip);
result = "";
resultJson= {};
-
+
var resourceFiles = [];
-
+
var promise = zip.file("face.json").async("string").then((data)=>{
console.log("face.json data", data);
faceJson = parseFaceJson(data);
});
-
+
if (isNativeFormat()){
promise = promise.then(zip.file("info.json").async("string").then((data)=>{
console.log("info.json data", data);
@@ -1071,12 +1087,12 @@
"color": "3bit",
"transparent": true
};
-
+
}
-
+
zip.folder("resources").forEach(function (relativePath, file){
console.log("iterating over", relativePath);
-
+
if (!file.dir){
expectedFiles++;
promise = promise.then(file.async("blob").then(function (blob) {
@@ -1092,10 +1108,10 @@
reader.readAsDataURL(blob);
}));
}
-
+
});
-
-
+
+
}, function (e) {
console.log("Error reading " + f.name + ": " + e.message);
});
@@ -1104,11 +1120,11 @@
console.log("Zip select event", evt);
var files = evt.target.files;
-
+
if (files.length > 1){
alert("Only one file allowed");
}
-
+
handleFile(files[0]);
}
@@ -1122,7 +1138,7 @@
});
}
-
+
document.getElementById("btnSaveFace").addEventListener("click", function() {
var h = document.createElement('a');
h.href = 'data:text/json;charset=utf-8,' + encodeURI(JSON.stringify(faceJson));
@@ -1130,14 +1146,14 @@
h.download = "face.json";
h.click();
});
-
+
document.getElementById('zipLoader').addEventListener('change', handleZipSelect, false);
document.getElementById('btnSaveZip').addEventListener('click', handleZipExport, false);
document.getElementById('btnSave').disabled = true;
document.getElementById('btnSaveFace').disabled = true;
document.getElementById('btnSaveZip').disabled = true;
document.getElementById('btnUpload').disabled = true;
-
+
diff --git a/apps/imageclock/metadata.json b/apps/imageclock/metadata.json
index c3ece0184..b291ab01e 100644
--- a/apps/imageclock/metadata.json
+++ b/apps/imageclock/metadata.json
@@ -2,7 +2,7 @@
"id": "imageclock",
"name": "Imageclock",
"shortName": "Imageclock",
- "version": "0.08",
+ "version": "0.13",
"type": "clock",
"description": "BETA!!! File formats still subject to change --- This app is a highly customizable watchface. To use it, you need to select a watchface. You can build the watchfaces yourself without programming anything. All you need to do is write some json and create image files.",
"icon": "app.png",
diff --git a/apps/imgclock/ChangeLog b/apps/imgclock/ChangeLog
index 01a6a4248..0895bb66d 100644
--- a/apps/imgclock/ChangeLog
+++ b/apps/imgclock/ChangeLog
@@ -7,3 +7,5 @@
0.06: Support 12 hour time
0.07: Don't cut off wide date formats
0.08: Use Bangle.setUI for button/launcher handling
+0.09: Bangle.js 2 compatibility
+0.10: Tell clock widgets to hide.
diff --git a/apps/imgclock/app.js b/apps/imgclock/app.js
index 0e4435638..7d74bee82 100644
--- a/apps/imgclock/app.js
+++ b/apps/imgclock/app.js
@@ -10,8 +10,8 @@ var IX = inf.x, IY = inf.y, IBPP = inf.bpp;
var IW = 174, IH = 45, OY = 24;
var bgwidth = img.charCodeAt(0);
var bgoptions;
-if (bgwidth<240)
- bgoptions = { scale : 240/bgwidth };
+if (bgwidth{
draw();
}
});
-// Show launcher when button pressed
-Bangle.setUI("clock");
+
diff --git a/apps/imgclock/b2_122240.png b/apps/imgclock/b2_122240.png
new file mode 100644
index 000000000..1a3f4daaa
Binary files /dev/null and b/apps/imgclock/b2_122240.png differ
diff --git a/apps/imgclock/b2_122271.png b/apps/imgclock/b2_122271.png
new file mode 100644
index 000000000..31733fb2c
Binary files /dev/null and b/apps/imgclock/b2_122271.png differ
diff --git a/apps/imgclock/b2_explode.png b/apps/imgclock/b2_explode.png
new file mode 100644
index 000000000..5252bbcd2
Binary files /dev/null and b/apps/imgclock/b2_explode.png differ
diff --git a/apps/imgclock/b2_thisisfine.png b/apps/imgclock/b2_thisisfine.png
new file mode 100644
index 000000000..1b7daaf60
Binary files /dev/null and b/apps/imgclock/b2_thisisfine.png differ
diff --git a/apps/imgclock/custom.html b/apps/imgclock/custom.html
index 2511f8a54..68d059b80 100644
--- a/apps/imgclock/custom.html
+++ b/apps/imgclock/custom.html
@@ -5,20 +5,59 @@
-
+
diff --git a/apps/imgclock/metadata.json b/apps/imgclock/metadata.json
index 799d11acc..94dff5f17 100644
--- a/apps/imgclock/metadata.json
+++ b/apps/imgclock/metadata.json
@@ -2,18 +2,19 @@
"id": "imgclock",
"name": "Image background clock",
"shortName": "Image Clock",
- "version": "0.08",
+ "version": "0.10",
"description": "A clock with an image as a background",
"icon": "app.png",
"type": "clock",
"tags": "clock",
- "supports": ["BANGLEJS"],
+ "supports": ["BANGLEJS","BANGLEJS2"],
"custom": "custom.html",
+ "customConnect": true,
"storage": [
{"name":"imgclock.app.js","url":"app.js"},
{"name":"imgclock.img","url":"app-icon.js","evaluate":true},
{"name":"imgclock.face.img"},
{"name":"imgclock.face.json"},
- {"name":"imgclock.face.bg","content":""}
+ {"name":"imgclock.face.bg","content":"X"}
]
}
diff --git a/apps/impwclock/ChangeLog b/apps/impwclock/ChangeLog
index 6555fcc8f..0af7c99d6 100644
--- a/apps/impwclock/ChangeLog
+++ b/apps/impwclock/ChangeLog
@@ -3,3 +3,4 @@
0.03: Move to Bangle.setUI to launcher support
0.04: Tweaks for compatibility with BangleJS2
0.05: Time-word now readable on Bangle.js 2
+0.06: Tell clock widgets to hide.
diff --git a/apps/impwclock/clock-impword.js b/apps/impwclock/clock-impword.js
index c42dbda44..04421017b 100644
--- a/apps/impwclock/clock-impword.js
+++ b/apps/impwclock/clock-impword.js
@@ -154,6 +154,9 @@ Bangle.on('lcdPower', function(on) {
if (on) drawWordClock();
});
+// Show launcher when button pressed
+Bangle.setUI("clock");
+
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
@@ -172,5 +175,4 @@ Bangle.on('touch',e=>{
}
});
-// Show launcher when button pressed
-Bangle.setUI("clock");
+
diff --git a/apps/impwclock/metadata.json b/apps/impwclock/metadata.json
index 733dbb957..1b92ea3ae 100644
--- a/apps/impwclock/metadata.json
+++ b/apps/impwclock/metadata.json
@@ -1,7 +1,7 @@
{
"id": "impwclock",
"name": "Imprecise Word Clock",
- "version": "0.05",
+ "version": "0.06",
"description": "Imprecise word clock for vacations, weekends, and those who never need accurate time.",
"icon": "clock-impword.png",
"type": "clock",
diff --git a/apps/info/ChangeLog b/apps/info/ChangeLog
index 07afedd21..093dd4606 100644
--- a/apps/info/ChangeLog
+++ b/apps/info/ChangeLog
@@ -1 +1,3 @@
-0.01: Release
\ No newline at end of file
+0.01: Release
+0.02: Recfactoring and show weather data
+0.03: Show sizes for used, free and trash through storage.getStats
\ No newline at end of file
diff --git a/apps/info/info.app.js b/apps/info/info.app.js
index c61a88045..ade3f3ebb 100644
--- a/apps/info/info.app.js
+++ b/apps/info/info.app.js
@@ -1,27 +1,99 @@
-var s = require("Storage");
+const storage = require("Storage");
const locale = require('locale');
var ENV = process.env;
var W = g.getWidth(), H = g.getHeight();
var screen = 0;
-const maxScreen = 2;
+
+
+var screens = [
+ {
+ name: "General",
+ items: [
+ {name: "Steps", fun: () => getSteps()},
+ {name: "HRM", fun: () => getBpm()},
+ {name: "", fun: () => ""},
+ {name: "Temp.", fun: () => getWeatherTemp()},
+ {name: "Humidity", fun: () => getWeatherHumidity()},
+ {name: "Wind", fun: () => getWeatherWind()},
+ ]
+ },
+ {
+ name: "Hardware",
+ items: [
+ {name: "Battery", fun: () => E.getBattery() + "%"},
+ {name: "Charge?", fun: () => Bangle.isCharging() ? "Yes" : "No"},
+ {name: "TempInt.", fun: () => locale.temp(parseInt(E.getTemperature()))},
+ {name: "Bluetooth", fun: () => NRF.getSecurityStatus().connected ? "Conn" : "NoConn"},
+ {name: "GPS", fun: () => Bangle.isGPSOn() ? "On" : "Off"},
+ {name: "Compass", fun: () => Bangle.isCompassOn() ? "On" : "Off"},
+ ]
+ },
+ {
+ name: "Software",
+ items: [
+ {name: "Firmw.", fun: () => ENV.VERSION},
+ {name: "Git", fun: () => ENV.GIT_COMMIT},
+ {name: "Boot.", fun: () => getVersion("boot.info")},
+ {name: "Settings.", fun: () => getVersion("setting.info")},
+ ]
+ },
+ {
+ name: "Storage [kB]",
+ items: [
+ {name: "Total", fun: () => storage.getStats().totalBytes>>10},
+ {name: "Free", fun: () => storage.getStats().freeBytes>>10},
+ {name: "Trash", fun: () => storage.getStats().trashBytes>>10},
+ {name: "", fun: () => ""},
+ {name: "#File", fun: () => storage.getStats().fileCount},
+ {name: "#Trash", fun: () => storage.getStats().trashCount},
+ ]
+ },
+];
+
+
+function getWeatherTemp(){
+ try {
+ var weather = storage.readJSON('weather.json').weather;
+ return locale.temp(weather.temp-273.15);
+ } catch(ex) { }
+
+ return "?";
+}
+
+
+function getWeatherHumidity(){
+ try {
+ var weather = storage.readJSON('weather.json').weather;
+ return weather.hum = weather.hum + "%";
+ } catch(ex) { }
+
+ return "?";
+}
+
+
+function getWeatherWind(){
+ try {
+ var weather = storage.readJSON('weather.json').weather;
+ var speed = locale.speed(weather.wind).replace("mph", "");
+ return Math.round(speed * 1.609344) + "kph";
+ } catch(ex) { }
+
+ return "?";
+}
+
function getVersion(file) {
- var j = s.readJSON(file,1);
+ var j = storage.readJSON(file,1);
var v = ("object"==typeof j)?j.version:false;
return v?((v?"v"+v:"Unknown")):"NO ";
}
-function drawData(name, value, y){
- g.drawString(name, 5, y);
- g.drawString(value, 100, y);
-}
-
function getSteps(){
try{
return Bangle.getHealthStatus("day").steps;
} catch(e) {
- return ">= 2v12";
+ return ">2v12";
}
}
@@ -29,53 +101,36 @@ function getBpm(){
try{
return Math.round(Bangle.getHealthStatus("day").bpm) + "bpm";
} catch(e) {
- return ">= 2v12";
+ return ">2v12";
}
}
+function drawData(name, value, y){
+ g.drawString(name, 10, y);
+ g.drawString(value, 100, y);
+}
+
function drawInfo() {
g.reset().clearRect(Bangle.appRect);
var h=18, y = h;//-h;
// Header
- g.setFont("Vector", h+2).setFontAlign(0,-1);
- g.drawString("--==|| INFO ||==--", W/2, 0);
+ g.drawLine(0,25,W,25);
+ g.drawLine(0,26,W,26);
+
+ // Info body depending on screen
g.setFont("Vector",h).setFontAlign(-1,-1);
+ screens[screen].items.forEach(function (item, index){
+ drawData(item.name, item.fun(), y+=h);
+ });
- // Dynamic data
- if(screen == 0){
- drawData("Steps", getSteps(), y+=h);
- drawData("HRM", getBpm(), y+=h);
- drawData("Battery", E.getBattery() + "%", y+=h);
- drawData("Voltage", E.getAnalogVRef().toFixed(2) + "V", y+=h);
- drawData("IntTemp.", locale.temp(parseInt(E.getTemperature())), y+=h);
- }
-
- if(screen == 1){
- drawData("Charging?", Bangle.isCharging() ? "Yes" : "No", y+=h);
- drawData("Bluetooth", NRF.getSecurityStatus().connected ? "Conn." : "Disconn.", y+=h);
- drawData("GPS", Bangle.isGPSOn() ? "On" : "Off", y+=h);
- drawData("Compass", Bangle.isCompassOn() ? "On" : "Off", y+=h);
- drawData("HRM", Bangle.isHRMOn() ? "On" : "Off", y+=h);
- }
-
- // Static data
- if(screen == 2){
- drawData("Firmw.", ENV.VERSION, y+=h);
- drawData("Boot.", getVersion("boot.info"), y+=h);
- drawData("Settings", getVersion("setting.info"), y+=h);
- drawData("Storage", "", y+=h);
- drawData(" Total", ENV.STORAGE>>10, y+=h);
- drawData(" Free", require("Storage").getFree()>>10, y+=h);
- }
-
- if(Bangle.isLocked()){
- g.setFont("Vector",h-2).setFontAlign(-1,-1);
- g.drawString("Locked", 0, H-h+2);
- }
-
+ // Bottom
+ g.drawLine(0,H-h-3,W,H-h-3);
+ g.drawLine(0,H-h-2,W,H-h-2);
+ g.setFont("Vector",h-2).setFontAlign(-1,-1);
+ g.drawString(screens[screen].name, 2, H-h+2);
g.setFont("Vector",h-2).setFontAlign(1,-1);
- g.drawString((screen+1) + "/3", W, H-h+2);
+ g.drawString((screen+1) + "/" + screens.length, W, H-h+2);
}
drawInfo();
@@ -88,14 +143,15 @@ Bangle.on('touch', function(btn, e){
var isRight = e.x > right;
if(isRight){
- screen = (screen + 1) % (maxScreen+1);
+ screen = (screen + 1) % screens.length;
}
if(isLeft){
screen -= 1;
- screen = screen < 0 ? maxScreen : screen;
+ screen = screen < 0 ? screens.length-1 : screen;
}
+ Bangle.buzz(40, 0.6);
drawInfo();
});
@@ -104,5 +160,4 @@ Bangle.on('lock', function(isLocked) {
});
Bangle.loadWidgets();
-for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
-// Bangle.drawWidgets();
\ No newline at end of file
+Bangle.drawWidgets();
\ No newline at end of file
diff --git a/apps/info/metadata.json b/apps/info/metadata.json
index f05f0e134..ac56cd5c3 100644
--- a/apps/info/metadata.json
+++ b/apps/info/metadata.json
@@ -1,7 +1,7 @@
{
"id": "info",
"name": "Info",
- "version": "0.01",
+ "version": "0.03",
"description": "An application that displays information such as battery level, steps etc.",
"icon": "info.png",
"type": "app",
@@ -11,7 +11,8 @@
"screenshots": [
{"url":"screenshot_1.png"},
{"url":"screenshot_2.png"},
- {"url":"screenshot_3.png"}],
+ {"url":"screenshot_3.png"},
+ {"url":"screenshot_4.png"}],
"storage": [
{"name":"info.app.js","url":"info.app.js"},
{"name":"info.img","url":"info.icon.js","evaluate":true}
diff --git a/apps/info/screenshot_1.png b/apps/info/screenshot_1.png
index 97d42a896..6661c122c 100644
Binary files a/apps/info/screenshot_1.png and b/apps/info/screenshot_1.png differ
diff --git a/apps/info/screenshot_2.png b/apps/info/screenshot_2.png
index 2d25dd4e6..3d91fcabe 100644
Binary files a/apps/info/screenshot_2.png and b/apps/info/screenshot_2.png differ
diff --git a/apps/info/screenshot_3.png b/apps/info/screenshot_3.png
index 782e4a195..86bbb67cf 100644
Binary files a/apps/info/screenshot_3.png and b/apps/info/screenshot_3.png differ
diff --git a/apps/info/screenshot_4.png b/apps/info/screenshot_4.png
new file mode 100644
index 000000000..b8b59b1ef
Binary files /dev/null and b/apps/info/screenshot_4.png differ
diff --git a/apps/infoclk/ChangeLog b/apps/infoclk/ChangeLog
new file mode 100644
index 000000000..4744f872a
--- /dev/null
+++ b/apps/infoclk/ChangeLog
@@ -0,0 +1,3 @@
+0.01: New app!
+0.02-0.07: Bug fixes
+0.08: Submitted to the app loader
\ No newline at end of file
diff --git a/apps/infoclk/README.md b/apps/infoclk/README.md
new file mode 100644
index 000000000..1dd563bec
--- /dev/null
+++ b/apps/infoclk/README.md
@@ -0,0 +1,33 @@
+# Informational clock
+
+A configurable clock with extra info and shortcuts when unlocked, but large time when locked
+
+## Information
+
+The clock has two different screen arrangements, depending on whether the watch is locked or unlocked. The most commonly viewed piece of information is the time, so when the watch is locked it optimizes for the time being visible at a glance without the backlight. The hours and minutes take up nearly the entire top half of the display, with the date and seconds taking up nearly the entire bottom half. The day progress bar is between them if enabled, unless configured to be on the bottom row. The bottom row can be configured to display a weather summary, step count, step count and heart rate, the daily progress bar, or nothing.
+
+When the watch is unlocked, it can be assumed that the backlight is on and the user is actively looking at the watch, so instead we can optimize for information density. The bottom half of the display becomes shortcuts, and the top half of the display becomes 4 rows of information (date and time, step count and heart rate, 2 line weather summary) + an optional daily progress bar. (The daily progress bar can be independently enabled when locked and unlocked.)
+
+Most things are self-explanatory, but the day progress bar might not be. The day progress bar is intended to show approximately how far through the day you are, in the form of a progress bar. You might want to configure it to show how far you are through your waking hours, or you might want to use it to show how far you are through your work or school day.
+
+## Shortcuts
+
+There are generally a few apps that the user uses far more frequently than the others. For example, they might use a timer, alarm clock, and calculator every day, while everything else (such as the settings app) gets used only occasionally. This clock has space for 8 apps in the bottom half of the screen only one tap away, avoiding the need to wait for the launcher to open and then scroll through it. Tapping the top of the watch opens the launcher, eliminating the need for the button (which still opens the launcher due to bangle.js conventions). There is also handling for left, right, and vertical swipes. A vertical swipe by default opens the messages app, mimicking mobile operating systems which use a swipe down to view the notification shade.
+
+## Configurability
+
+Displaying the seconds allows for more precise timing, but waking up the CPU to refresh the display more often consumes battery. The user can enable or disable them completely, but can also configure them to be enabled or disabled automatically based on some hueristics:
+
+* They can be hidden while the display is locked, if the user expects to unlock their watch when they need the seconds.
+* They can be hidden when the battery is too low, to make the last portion of the battery last a little bit longer.
+* They can be hidden during a period of time such as when the user is asleep and therefore unlikely to need very much precision.
+
+The date format can be changed.
+
+As described earlier, the contents of the bottom row when locked can be changed.
+
+The 8 tap-based shortcuts on the bottom and the 3 swipe-based shortcuts can be changed to nothing, the launcher, or any app on the watch.
+
+The start and end time of the day progress bar can be changed. It can be enabled or disabled separately when the watch is locked and unlocked. The color can be changed. The time when it resets from full to empty can be changed.
+
+When the battery is below a defined point, the watch's color can change to another chosen color to help the user notice that the battery is low.
\ No newline at end of file
diff --git a/apps/infoclk/app.js b/apps/infoclk/app.js
new file mode 100644
index 000000000..3d51191df
--- /dev/null
+++ b/apps/infoclk/app.js
@@ -0,0 +1,405 @@
+const SETTINGS_FILE = "infoclk.json";
+const FONT = require('infoclk-font.js');
+
+const storage = require("Storage");
+const locale = require("locale");
+const weather = require('weather');
+
+let config = Object.assign({
+ seconds: {
+ // Displaying the seconds can reduce battery life because the CPU must wake up more often to update the display.
+ // The seconds will be shown unless one of these conditions is enabled here, and currently true.
+ hideLocked: false, // Hide the seconds when the display is locked.
+ hideBattery: 20, // Hide the seconds when the battery is at or below a defined percentage.
+ hideTime: true, // Hide the seconds when between a certain period of time. Useful for when you are sleeping and don't need the seconds
+ hideStart: 2200, // The time when the seconds are hidden: first 2 digits are hours on a 24 hour clock, last 2 are minutes
+ hideEnd: 700, // The time when the seconds are shown again
+ hideAlways: false, // Always hide (never show) the seconds
+ },
+
+ date: {
+ // Settings related to the display of the date
+ mmdd: true, // If true, display the month first. If false, display the date first.
+ separator: '-', // The character that goes between the month and date
+ monthName: false, // If false, display the month as a number. If true, display the name.
+ monthFullName: false, // If displaying the name: If false, display an abbreviation. If true, display a full name.
+ dayFullName: false, // If false, display the day of the week's abbreviation. If true, display the full name.
+ },
+
+ bottomLocked: {
+ display: 'weather' // What to display in the bottom row when locked:
+ // 'weather': The current temperature and weather description
+ // 'steps': Step count
+ // 'health': Step count and bpm
+ // 'progress': Day progress bar
+ // false: Nothing
+ },
+
+ shortcuts: [
+ //8 shortcuts, displayed in the bottom half of the screen (2 rows of 4 shortcuts) when unlocked
+ // false = no shortcut
+ // '#LAUNCHER' = open the launcher
+ // any other string = name of app to open
+ 'stlap', 'keytimer', 'pomoplus', 'alarm',
+ 'rpnsci', 'calendar', 'torch', 'weather'
+ ],
+
+ swipe: {
+ // 3 shortcuts to launch upon swiping:
+ // false = no shortcut
+ // '#LAUNCHER' = open the launcher
+ // any other string = name of app to open
+ up: 'messages', // Swipe up or swipe down, due to limitation of event handler
+ left: '#LAUNCHER',
+ right: '#LAUNCHER',
+ },
+
+ dayProgress: {
+ // A progress bar representing how far through the day you are
+ enabledLocked: true, // Whether this bar is enabled when the watch is locked
+ enabledUnlocked: false, // Whether the bar is enabled when the watch is unlocked
+ color: [0, 0, 1], // The color of the bar
+ start: 700, // The time of day that the bar starts filling
+ end: 2200, // The time of day that the bar becomes full
+ reset: 300 // The time of day when the progress bar resets from full to empty
+ },
+
+ lowBattColor: {
+ // The text can change color to indicate that the battery is low
+ level: 20, // The percentage where this happens
+ color: [1, 0, 0] // The color that the text changes to
+ }
+}, storage.readJSON(SETTINGS_FILE));
+
+// Return whether the given time (as a date object) is between start and end (as a number where the first 2 digits are hours on a 24 hour clock and the last 2 are minutes), with end time wrapping to next day if necessary
+function timeInRange(start, time, end) {
+
+ // Convert the given date object to a time number
+ let timeNumber = time.getHours() * 100 + time.getMinutes();
+
+ // Normalize to prevent the numbers from wrapping around at midnight
+ if (end <= start) {
+ end += 2400;
+ if (timeNumber < start) timeNumber += 2400;
+ }
+
+ return start <= timeNumber && timeNumber <= end;
+}
+
+// Return whether settings should be displayed based on the user's configuration
+function shouldDisplaySeconds(now) {
+ return !(
+ (config.seconds.hideAlways) ||
+ (config.seconds.hideLocked && Bangle.isLocked()) ||
+ (E.getBattery() <= config.seconds.hideBattery) ||
+ (config.seconds.hideTime && timeInRange(config.seconds.hideStart, now, config.seconds.hideEnd))
+ );
+}
+
+// Determine the font size needed to fit a string of the given length widthin maxWidth number of pixels, clamped between minSize and maxSize
+function getFontSize(length, maxWidth, minSize, maxSize) {
+ let size = Math.floor(maxWidth / length); //Number of pixels of width available to character
+ size *= (20 / 12); //Convert to height, assuming 20 pixels of height for every 12 of width
+
+ // Clamp to within range
+ if (size < minSize) return minSize;
+ else if (size > maxSize) return maxSize;
+ else return Math.floor(size);
+}
+
+// Get the current day of the week according to user settings
+function getDayString(now) {
+ if (config.date.dayFullName) return ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][now.getDay()];
+ else return ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][now.getDay()];
+}
+
+// Pad a number with zeros to be the given number of digits
+function pad(number, digits) {
+ let result = '' + number;
+ while (result.length < digits) result = '0' + result;
+ return result;
+}
+
+// Get the current date formatted according to the user settings
+function getDateString(now) {
+ let month;
+ if (!config.date.monthName) month = pad(now.getMonth() + 1, 2);
+ else if (config.date.monthFullName) month = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'][now.getMonth()];
+ else month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][now.getMonth()];
+
+ if (config.date.mmdd) return `${month}${config.date.separator}${pad(now.getDate(), 2)}`;
+ else return `${pad(now.getDate(), 2)}${config.date.separator}${month}`;
+}
+
+// Get a floating point number from 0 to 1 representing how far between the user-defined start and end points we are
+function getDayProgress(now) {
+ let start = config.dayProgress.start;
+ let current = now.getHours() * 100 + now.getMinutes();
+ let end = config.dayProgress.end;
+ let reset = config.dayProgress.reset;
+
+ // Normalize
+ if (end <= start) end += 2400;
+ if (current < start) current += 2400;
+ if (reset < start) reset += 2400;
+
+ // Convert an hhmm number into a floating-point hours
+ function toDecimalHours(time) {
+ let hours = Math.floor(time / 100);
+ let minutes = time % 100;
+
+ return hours + (minutes / 60);
+ }
+
+ start = toDecimalHours(start);
+ current = toDecimalHours(current);
+ end = toDecimalHours(end);
+ reset = toDecimalHours(reset);
+
+ let progress = (current - start) / (end - start);
+
+ if (progress < 0 || progress > 1) {
+ if (current < reset) return 1;
+ else return 0;
+ } else {
+ return progress;
+ }
+}
+
+// Get a Gadgetbridge weather string
+function getWeatherString() {
+ let current = weather.get();
+ if (current) return locale.temp(current.temp - 273.15) + ', ' + current.txt;
+ else return 'Weather unknown!';
+}
+
+// Get a second weather row showing humidity, wind speed, and wind direction
+function getWeatherRow2() {
+ let current = weather.get();
+ if (current) return `${current.hum}%, ${locale.speed(current.wind)} ${current.wrose}`;
+ else return 'Check Gadgetbridge';
+}
+
+// Get a step string
+function getStepsString() {
+ return '' + Bangle.getHealthStatus('day').steps + ' steps';
+}
+
+// Get a health string including daily steps and recent bpm
+function getHealthString() {
+ return `${Bangle.getHealthStatus('day').steps} steps ${Bangle.getHealthStatus('last').bpm} bpm`;
+}
+
+// Set the next timeout to draw the screen
+let drawTimeout;
+function setNextDrawTimeout() {
+ if (drawTimeout) {
+ clearTimeout(drawTimeout);
+ drawTimeout = undefined;
+ }
+
+ let time;
+ let now = new Date();
+ if (shouldDisplaySeconds(now)) time = 1000 - (now.getTime() % 1000);
+ else time = 60000 - (now.getTime() % 60000);
+
+ drawTimeout = setTimeout(draw, time);
+}
+
+
+const DIGIT_WIDTH = 40; // How much width is allocated for each digit, 37 pixels + 3 pixels of space (which will go off of the screen on the right edge)
+const COLON_WIDTH = 19; // How much width is allocated for the colon, 16 pixels + 3 pixels of space
+const HHMM_TOP = 27; // 24 pixels for widgets + 3 pixels of space
+const DIGIT_HEIGHT = 64; // How tall the digits are
+
+const SECONDS_TOP = HHMM_TOP + DIGIT_HEIGHT + 3; // The top edge of the seconds, top of hours and minutes + digit height + space
+const SECONDS_LEFT = 2 * DIGIT_WIDTH + COLON_WIDTH; // The left edge of the seconds: displayed after 2 digits and the colon
+const DATE_LETTER_HEIGHT = DIGIT_HEIGHT / 2; // Each letter of the day of week and date will be half the height of the time digits
+
+const DATE_CENTER_X = SECONDS_LEFT / 2; // Day of week and date will be centered between left edge of screen and where seconds start
+const DOW_CENTER_Y = SECONDS_TOP + (DATE_LETTER_HEIGHT / 2); // Day of week will be the top row
+const DATE_CENTER_Y = DOW_CENTER_Y + DATE_LETTER_HEIGHT; // Date will be the bottom row
+const DOW_DATE_CENTER_Y = SECONDS_TOP + (DIGIT_HEIGHT / 2); // When displaying both on one row, center it
+const BOTTOM_CENTER_Y = ((SECONDS_TOP + DIGIT_HEIGHT + 3) + g.getHeight()) / 2;
+
+// Draw the clock
+function draw() {
+ //Prepare to draw
+ g.reset()
+ .setFontAlign(0, 0);
+
+ if (E.getBattery() <= config.lowBattColor.level) {
+ let color = config.lowBattColor.color;
+ g.setColor(color[0], color[1], color[2]);
+ }
+ now = new Date();
+
+ if (Bangle.isLocked()) { //When the watch is locked
+ g.clearRect(0, 24, g.getWidth(), g.getHeight());
+
+ //Draw the hours and minutes
+ let x = 0;
+
+ for (let digit of locale.time(now, 1)) { //apparently this is how you get an hh:mm time string adjusting for the user's 12/24 hour preference
+ if (digit != ' ') g.drawImage(FONT[digit], x, HHMM_TOP);
+ if (digit == ':') x += COLON_WIDTH;
+ else x += DIGIT_WIDTH;
+ }
+ if (storage.readJSON('setting.json')['12hour']) g.drawImage(FONT[(now.getHours() < 12) ? 'am' : 'pm'], 0, HHMM_TOP);
+
+ //Draw the seconds if necessary
+ if (shouldDisplaySeconds(now)) {
+ let tens = Math.floor(now.getSeconds() / 10);
+ let ones = now.getSeconds() % 10;
+ g.drawImage(FONT[tens], SECONDS_LEFT, SECONDS_TOP)
+ .drawImage(FONT[ones], SECONDS_LEFT + DIGIT_WIDTH, SECONDS_TOP);
+
+ // Draw the day of week and date assuming the seconds are displayed
+
+ g.setFont('Vector', getFontSize(getDayString(now).length, SECONDS_LEFT, 6, DATE_LETTER_HEIGHT))
+ .drawString(getDayString(now), DATE_CENTER_X, DOW_CENTER_Y)
+ .setFont('Vector', getFontSize(getDateString(now).length, SECONDS_LEFT, 6, DATE_LETTER_HEIGHT))
+ .drawString(getDateString(now), DATE_CENTER_X, DATE_CENTER_Y);
+
+ } else {
+ //Draw the day of week and date without the seconds
+
+ let string = getDayString(now) + ' ' + getDateString(now);
+ g.setFont('Vector', getFontSize(string.length, g.getWidth(), 6, DATE_LETTER_HEIGHT))
+ .drawString(string, g.getWidth() / 2, DOW_DATE_CENTER_Y);
+ }
+
+ // Draw the bottom area
+ if (config.bottomLocked.display == 'progress') {
+ let color = config.dayProgress.color;
+ g.setColor(color[0], color[1], color[2])
+ .fillRect(0, SECONDS_TOP + DIGIT_HEIGHT + 3, g.getWidth() * getDayProgress(now), g.getHeight());
+ } else {
+ let bottomString;
+
+ if (config.bottomLocked.display == 'weather') bottomString = getWeatherString();
+ else if (config.bottomLocked.display == 'steps') bottomString = getStepsString();
+ else if (config.bottomLocked.display == 'health') bottomString = getHealthString();
+ else bottomString = ' ';
+
+ g.setFont('Vector', getFontSize(bottomString.length, 176, 6, g.getHeight() - (SECONDS_TOP + DIGIT_HEIGHT + 3)))
+ .drawString(bottomString, g.getWidth() / 2, BOTTOM_CENTER_Y);
+ }
+
+ // Draw the day progress bar between the rows if necessary
+ if (config.dayProgress.enabledLocked && config.bottomLocked.display != 'progress') {
+ let color = config.dayProgress.color;
+ g.setColor(color[0], color[1], color[2])
+ .fillRect(0, HHMM_TOP + DIGIT_HEIGHT, g.getWidth() * getDayProgress(now), SECONDS_TOP);
+ }
+ } else {
+
+ //If the watch is unlocked
+ g.clearRect(0, 24, g.getWidth(), g.getHeight() / 2);
+ rows = [
+ `${getDayString(now)} ${getDateString(now)} ${locale.time(now, 1)}`,
+ getHealthString(),
+ getWeatherString(),
+ getWeatherRow2()
+ ];
+ if (shouldDisplaySeconds(now)) rows[0] += ':' + pad(now.getSeconds(), 2);
+ if (storage.readJSON('setting.json')['12hour']) rows[0] += ((now.getHours() < 12) ? ' AM' : ' PM');
+
+ let maxHeight = ((g.getHeight() / 2) - HHMM_TOP) / (config.dayProgress.enabledUnlocked ? (rows.length + 1) : rows.length);
+
+ let y = HHMM_TOP + maxHeight / 2;
+ for (let row of rows) {
+ let size = getFontSize(row.length, g.getWidth(), 6, maxHeight);
+ g.setFont('Vector', size)
+ .drawString(row, g.getWidth() / 2, y);
+ y += maxHeight;
+ }
+
+ if (config.dayProgress.enabledUnlocked) {
+ let color = config.dayProgress.color;
+ g.setColor(color[0], color[1], color[2])
+ .fillRect(0, y - maxHeight / 2, 176 * getDayProgress(now), y + maxHeight / 2);
+ }
+ }
+
+ setNextDrawTimeout();
+}
+
+// Draw the icons. This is done separately from the main draw routine to avoid having to scale and draw a bunch of images repeatedly.
+function drawIcons() {
+ g.reset().clearRect(0, 24, g.getWidth(), g.getHeight());
+ for (let i = 0; i < 8; i++) {
+ let x = [0, 44, 88, 132, 0, 44, 88, 132][i];
+ let y = [88, 88, 88, 88, 132, 132, 132, 132][i];
+ let appId = config.shortcuts[i];
+ let appInfo = storage.readJSON(appId + '.info', 1);
+ if (!appInfo) continue;
+ icon = storage.read(appInfo.icon);
+ g.drawImage(icon, x, y, {
+ scale: 0.916666666667
+ });
+ }
+}
+
+weather.on("update", draw);
+Bangle.on("step", draw);
+Bangle.on('lock', locked => {
+ //If the watch is unlocked, draw the icons
+ if (!locked) drawIcons();
+ draw();
+});
+
+// Show launcher when middle button pressed
+Bangle.setUI("clock");
+// Load widgets
+Bangle.loadWidgets();
+Bangle.drawWidgets();
+
+// Launch an app given the current ID. Handles special cases:
+// false: Do nothing
+// '#LAUNCHER': Open the launcher
+// nonexistent app: Do nothing
+function launch(appId) {
+ if (appId == false) return;
+ else if (appId == '#LAUNCHER') {
+ Bangle.buzz();
+ Bangle.showLauncher();
+ } else {
+ let appInfo = storage.readJSON(appId + '.info', 1);
+ if (appInfo) {
+ Bangle.buzz();
+ load(appInfo.src);
+ }
+ }
+}
+
+//Set up touch to launch the selected app
+Bangle.on('touch', function (button, xy) {
+ let x = Math.floor(xy.x / 44);
+ if (x < 0) x = 0;
+ else if (x > 3) x = 3;
+
+ let y = Math.floor(xy.y / 44);
+ if (y < 0) y = -1;
+ else if (y > 3) y = 1;
+ else y -= 2;
+
+ if (y < 0) {
+ Bangle.buzz();
+ Bangle.showLauncher();
+ } else {
+ let i = 4 * y + x;
+ launch(config.shortcuts[i]);
+ }
+});
+
+//Set up swipe handler
+Bangle.on('swipe', function (direction) {
+ if (direction == -1) launch(config.swipe.left);
+ else if (direction == 0) launch(config.swipe.up);
+ else launch(config.swipe.right);
+});
+
+if (!Bangle.isLocked()) drawIcons();
+
+draw();
\ No newline at end of file
diff --git a/apps/infoclk/font.js b/apps/infoclk/font.js
new file mode 100644
index 000000000..6063958e7
--- /dev/null
+++ b/apps/infoclk/font.js
@@ -0,0 +1,23 @@
+const heatshrink = require("heatshrink")
+
+function decompress(string) {
+ return heatshrink.decompress(atob(string))
+}
+
+exports = {
+ '0': decompress("ktAwIEB////EAj4EB+EDAYP/8E/AgWDAYX+CIX/+IDC//PBoYIDAAvwgEHAgOAgAnB/kAgIvCgEPAgJCBv5CCHwXAI4X+KAYk/En4kmAA4qBAAP7BAePAYX4BofBAYX8F4Q+BEwRHBIQI5BA"),
+ '1': decompress("ktAwIGDj/4AgX/4ADBg/+BAU/+ADBgP/wAEBh/8BoV/8ADBgf/En4k/En4k/EgQ="),
+ '2': decompress("ktAwMA/4AB/EHAgXwn4EC8IDC/+PAYX+v4EC+YND74NDBAYAE4A0Bg/+HIU/+ADBgP/wAEBh/8BoV/8ADBgf/BAUf/AECElQdBPA2HAYX8OYfHBAYRD8Z3Dj6TG/kPPYZm4EiwAHO4f7BAfPfI/xBoaTEPAfgQwY"),
+ '3': decompress("ktAwMA/4AB/EHAgXwn4EC8IDC/+PAYX+v4EC+YND74NDBAYAE4A0Bg/+HIU/+ADBgP/wAEBh/8BoV/8ADBgf/BAUf/AECElJWIAEpu/EhpgS34DC/IID54DC/l/AgXDAYX4j57DA"),
+ '4': decompress("ktAwMA//AgEf//+BYP///wgEHAgOAgE///8gEBBAPggEPAgIWBv///EAgYIBEn4kXABf9AgfnAgY4BAAP4BAfDAYX+EwfwIQRRCJIJRBJIRRBJIQICj5RBJIRRBJIJRCNwJRBNwQk/Ei4A=="),
+ '5': decompress("ktAwIEB/4AB/EfAgXDAYX+n4EC+YDC/+fAYX9BAfvAgYAJ+AwBgP/wAEBh/8H4V/8ADBgf/BAUf/AEC//AAYMH/wICn4kpPYUPAgXgv4EC4JfDg4DC/iFD8ANDwaTDCQfwEoZ2/EhrXNAAm/AYX5BAfPQoaTD4ahDj57DA=="),
+ '6': decompress("ktAwIEB/4AB/EfAgXDAYX+n4EC+YDC/+fAYX9BAfvAgYAJ+AwBgP/wAEBh/8H4V/8ADBgf/BAUf/AEC//AAYMH/wICn4kpPYUPAgXgv6AG/6JD/gID84ED358NJIIsCKIQ0BKIRJCFgJJCSYcHAgJuBXYJuBKIQkpAA58D/YIDx6PDBofBQoYvCHwImCI4KUCwA="),
+ '7': decompress("ktAwMA/4AB/EHAgXwn4EC8IDC/+PAYX+v4EC+YND74NDBAYAE4A0Bg/+HIU/+ADBgP/wAEBh/8BoV/8ADBgf/BAUf/AECEn4k/En4kVA"),
+ '8': decompress("ktAwIEB////EAj4EB+EDAYP/8E/AgWDAYX+CIX/+IDC//PBoYIDAAvwgEHAgOAgAnB/kAgIvCgEPAgJCBv5CCHwXAI4X+KAYkpAFpu/EhwAHFQIAB/YIDx4DC/AND4IDC/ieD4AmCI4JCBHIIA=="),
+ '9': decompress("ktAwIEB////EAj4EB+EDAYP/8E/AgWDAYX+CIX/+IDC//PBoYIDAAvwgEHAgOAgAnB/kAgIvCgEPAgJCBv5CCHwXAI4X+KAYkpABf9AgfnAgaFD/AID4Z8DEwfwIQRRCJIJRBJIRRBJIQICj5RBJIRRBJIJRCNwJRBNwQkoPhoAE34DC/L0H/iwBQAv4WAJ7CA=="),
+
+ ':': decompress("iFAwITQg/gj/4n/8v/+AIP/ABQPDCoIZBDoJTfH94A=="),
+
+ 'am': decompress("jFAwIEBngCEvwCH/4CFwEBAQkD//AgfnAQcH4fgAQsPwPwAQf/+Ef//4AQn8n0AvgCCHQN+vkAnwCC/EAj4CF+EAh4CCNIoLFC4v8gE/AQv+gF/AQpwB/4CDwICG+/D94CD8/v+fn54CC+P/x4CF+H/IgICFvwCEngCD"),
+ 'pm': decompress("jFAwMAn///l///+/4AE+EAh4CaEYoABFgX8BwMAAUwAFIIv4gEfAQX8OYICF/0Av4CF/8AKQICCwICG+/D94CD8/v+fn54CC+P/x4CF+H/IgICFvwCEngCDA")
+}
\ No newline at end of file
diff --git a/apps/infoclk/font/am.png b/apps/infoclk/font/am.png
new file mode 100644
index 000000000..a76ad25fc
Binary files /dev/null and b/apps/infoclk/font/am.png differ
diff --git a/apps/infoclk/font/colon.png b/apps/infoclk/font/colon.png
new file mode 100644
index 000000000..878770dde
Binary files /dev/null and b/apps/infoclk/font/colon.png differ
diff --git a/apps/infoclk/font/digit0.png b/apps/infoclk/font/digit0.png
new file mode 100644
index 000000000..5470154ee
Binary files /dev/null and b/apps/infoclk/font/digit0.png differ
diff --git a/apps/infoclk/font/digit1.png b/apps/infoclk/font/digit1.png
new file mode 100644
index 000000000..26a35fd1b
Binary files /dev/null and b/apps/infoclk/font/digit1.png differ
diff --git a/apps/infoclk/font/digit2.png b/apps/infoclk/font/digit2.png
new file mode 100644
index 000000000..92974daf3
Binary files /dev/null and b/apps/infoclk/font/digit2.png differ
diff --git a/apps/infoclk/font/digit3.png b/apps/infoclk/font/digit3.png
new file mode 100644
index 000000000..6751067c6
Binary files /dev/null and b/apps/infoclk/font/digit3.png differ
diff --git a/apps/infoclk/font/digit4.png b/apps/infoclk/font/digit4.png
new file mode 100644
index 000000000..fdb0c5f8d
Binary files /dev/null and b/apps/infoclk/font/digit4.png differ
diff --git a/apps/infoclk/font/digit5.png b/apps/infoclk/font/digit5.png
new file mode 100644
index 000000000..5647ad00a
Binary files /dev/null and b/apps/infoclk/font/digit5.png differ
diff --git a/apps/infoclk/font/digit6.png b/apps/infoclk/font/digit6.png
new file mode 100644
index 000000000..56c446881
Binary files /dev/null and b/apps/infoclk/font/digit6.png differ
diff --git a/apps/infoclk/font/digit7.png b/apps/infoclk/font/digit7.png
new file mode 100644
index 000000000..1fb6a6423
Binary files /dev/null and b/apps/infoclk/font/digit7.png differ
diff --git a/apps/infoclk/font/digit8.png b/apps/infoclk/font/digit8.png
new file mode 100644
index 000000000..a373205f1
Binary files /dev/null and b/apps/infoclk/font/digit8.png differ
diff --git a/apps/infoclk/font/digit9.png b/apps/infoclk/font/digit9.png
new file mode 100644
index 000000000..990a3a43b
Binary files /dev/null and b/apps/infoclk/font/digit9.png differ
diff --git a/apps/infoclk/font/pm.png b/apps/infoclk/font/pm.png
new file mode 100644
index 000000000..a3db97eb8
Binary files /dev/null and b/apps/infoclk/font/pm.png differ
diff --git a/apps/infoclk/icon.js b/apps/infoclk/icon.js
new file mode 100644
index 000000000..ae230d8f4
--- /dev/null
+++ b/apps/infoclk/icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwgOAA4YFS/4AKEf5BlABcAjAgBjAfBAuhH/Apo"))
\ No newline at end of file
diff --git a/apps/infoclk/icon.png b/apps/infoclk/icon.png
new file mode 100644
index 000000000..24423fbd6
Binary files /dev/null and b/apps/infoclk/icon.png differ
diff --git a/apps/infoclk/metadata.json b/apps/infoclk/metadata.json
new file mode 100644
index 000000000..bb6dea3a4
--- /dev/null
+++ b/apps/infoclk/metadata.json
@@ -0,0 +1,38 @@
+{
+ "id": "infoclk",
+ "name": "Informational clock",
+ "version": "0.08",
+ "description": "A configurable clock with extra info and shortcuts when unlocked, but large time when locked",
+ "readme": "README.md",
+ "icon": "icon.png",
+ "type": "clock",
+ "tags": "clock",
+ "supports": [
+ "BANGLEJS2"
+ ],
+ "allow_emulator": true,
+ "storage": [
+ {
+ "name": "infoclk.app.js",
+ "url": "app.js"
+ },
+ {
+ "name": "infoclk.settings.js",
+ "url": "settings.js"
+ },
+ {
+ "name": "infoclk-font.js",
+ "url": "font.js"
+ },
+ {
+ "name": "infoclk.img",
+ "url": "icon.js",
+ "evaluate": true
+ }
+ ],
+ "data": [
+ {
+ "name": "infoclk.json"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/apps/infoclk/settings.js b/apps/infoclk/settings.js
new file mode 100644
index 000000000..0bc3d4b15
--- /dev/null
+++ b/apps/infoclk/settings.js
@@ -0,0 +1,571 @@
+(function (back) {
+ const SETTINGS_FILE = "infoclk.json";
+ const storage = require('Storage');
+
+ let config = Object.assign({
+ seconds: {
+ // Displaying the seconds can reduce battery life because the CPU must wake up more often to update the display.
+ // The seconds will be shown unless one of these conditions is enabled here, and currently true.
+ hideLocked: false, // Hide the seconds when the display is locked.
+ hideBattery: 20, // Hide the seconds when the battery is at or below a defined percentage.
+ hideTime: true, // Hide the seconds when between a certain period of time. Useful for when you are sleeping and don't need the seconds
+ hideStart: 2200, // The time when the seconds are hidden: first 2 digits are hours on a 24 hour clock, last 2 are minutes
+ hideEnd: 700, // The time when the seconds are shown again
+ hideAlways: false, // Always hide (never show) the seconds
+ },
+
+ date: {
+ // Settings related to the display of the date
+ mmdd: true, // If true, display the month first. If false, display the date first.
+ separator: '-', // The character that goes between the month and date
+ monthName: false, // If false, display the month as a number. If true, display the name.
+ monthFullName: false, // If displaying the name: If false, display an abbreviation. If true, display a full name.
+ dayFullName: false, // If false, display the day of the week's abbreviation. If true, display the full name.
+ },
+
+ bottomLocked: {
+ display: 'weather' // What to display in the bottom row when locked:
+ // 'weather': The current temperature and weather description
+ // 'steps': Step count
+ // 'health': Step count and bpm
+ // 'progress': Day progress bar
+ // false: Nothing
+ },
+
+ shortcuts: [
+ //8 shortcuts, displayed in the bottom half of the screen (2 rows of 4 shortcuts) when unlocked
+ // false = no shortcut
+ // '#LAUNCHER' = open the launcher
+ // any other string = name of app to open
+ 'stlap', 'keytimer', 'pomoplus', 'alarm',
+ 'rpnsci', 'calendar', 'torch', 'weather'
+ ],
+
+ swipe: {
+ // 3 shortcuts to launch upon swiping:
+ // false = no shortcut
+ // '#LAUNCHER' = open the launcher
+ // any other string = name of app to open
+ up: 'messages', // Swipe up or swipe down, due to limitation of event handler
+ left: '#LAUNCHER',
+ right: '#LAUNCHER',
+ },
+
+ dayProgress: {
+ // A progress bar representing how far through the day you are
+ enabledLocked: true, // Whether this bar is enabled when the watch is locked
+ enabledUnlocked: false, // Whether the bar is enabled when the watch is unlocked
+ color: [0, 0, 1], // The color of the bar
+ start: 700, // The time of day that the bar starts filling
+ end: 2200, // The time of day that the bar becomes full
+ reset: 300 // The time of day when the progress bar resets from full to empty
+ },
+
+ lowBattColor: {
+ // The text can change color to indicate that the battery is low
+ level: 20, // The percentage where this happens
+ color: [1, 0, 0] // The color that the text changes to
+ }
+ }, storage.readJSON(SETTINGS_FILE));
+
+ function saveSettings() {
+ storage.writeJSON(SETTINGS_FILE, config);
+ }
+
+ function hourToString(hour) {
+ if (storage.readJSON('setting.json')['12hour']) {
+ if (hour == 0) return '12 AM';
+ else if (hour < 12) return `${hour} AM`;
+ else if (hour == 12) return '12 PM';
+ else return `${hour - 12} PM`;
+ } else return '' + hour;
+ }
+
+ // The menu for configuring when the seconds are shown
+ function showSecondsMenu() {
+ E.showMenu({
+ '': {
+ 'title': 'Seconds display',
+ 'back': showMainMenu
+ },
+ 'Show seconds': {
+ value: !config.seconds.hideAlways,
+ onchange: value => {
+ config.seconds.hideAlways = !value;
+ saveSettings();
+ }
+ },
+ '...unless locked': {
+ value: config.seconds.hideLocked,
+ onchange: value => {
+ config.seconds.hideLocked = value;
+ saveSettings();
+ }
+ },
+ '...unless battery below': {
+ value: config.seconds.hideBattery,
+ min: 0,
+ max: 100,
+ format: value => `${value}%`,
+ onchange: value => {
+ config.seconds.hideBattery = value;
+ saveSettings();
+ }
+ },
+ '...unless between these 2 times...': () => {
+ E.showMenu({
+ '': {
+ 'title': 'Hide seconds between',
+ 'back': showSecondsMenu
+ },
+ 'Enabled': {
+ value: config.seconds.hideTime,
+ onchange: value => {
+ config.seconds.hideTime = value;
+ saveSettings();
+ }
+ },
+ 'Start hour': {
+ value: Math.floor(config.seconds.hideStart / 100),
+ format: hourToString,
+ min: 0,
+ max: 23,
+ wrap: true,
+ onchange: hour => {
+ minute = config.seconds.hideStart % 100;
+ config.seconds.hideStart = (100 * hour) + minute;
+ saveSettings();
+ }
+ },
+ 'Start minute': {
+ value: config.seconds.hideStart % 100,
+ min: 0,
+ max: 59,
+ wrap: true,
+ onchange: minute => {
+ hour = Math.floor(config.seconds.hideStart / 100);
+ config.seconds.hideStart = (100 * hour) + minute;
+ saveSettings();
+ }
+ },
+ 'End hour': {
+ value: Math.floor(config.seconds.hideEnd / 100),
+ format: hourToString,
+ min: 0,
+ max: 23,
+ wrap: true,
+ onchange: hour => {
+ minute = config.seconds.hideEnd % 100;
+ config.seconds.hideEnd = (100 * hour) + minute;
+ saveSettings();
+ }
+ },
+ 'End minute': {
+ value: config.seconds.hideEnd % 100,
+ min: 0,
+ max: 59,
+ wrap: true,
+ onchange: minute => {
+ hour = Math.floor(config.seconds.hideEnd / 100);
+ config.seconds.hideEnd = (100 * hour) + minute;
+ saveSettings();
+ }
+ }
+ });
+ }
+ });
+ }
+
+ // Available month/date separators
+ const SEPARATORS = [
+ { name: 'Slash', char: '/' },
+ { name: 'Dash', char: '-' },
+ { name: 'Space', char: ' ' },
+ { name: 'Comma', char: ',' },
+ { name: 'None', char: '' }
+ ];
+
+ // Available bottom row display options
+ const BOTTOM_ROW_OPTIONS = [
+ { name: 'Weather', val: 'weather' },
+ { name: 'Step count', val: 'steps' },
+ { name: 'Steps + BPM', val: 'health' },
+ { name: 'Day progresss bar', val: 'progress' },
+ { name: 'Nothing', val: false }
+ ];
+
+ // The menu for configuring which apps have shortcut icons
+ function showShortcutMenu() {
+ //Builds the shortcut options
+ let shortcutOptions = [
+ { name: 'Nothing', val: false },
+ { name: 'Launcher', val: '#LAUNCHER' },
+ ];
+
+ let infoFiles = storage.list(/\.info$/).sort((a, b) => {
+ if (a.name < b.name) return -1;
+ else if (a.name > b.name) return 1;
+ else return 0;
+ });
+ for (let infoFile of infoFiles) {
+ let appInfo = storage.readJSON(infoFile);
+ if (appInfo.src) shortcutOptions.push({
+ name: appInfo.name,
+ val: appInfo.id
+ });
+ }
+
+ E.showMenu({
+ '': {
+ 'title': 'Shortcuts',
+ 'back': showMainMenu
+ },
+ 'Top first': {
+ value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[0]),
+ format: value => shortcutOptions[value].name,
+ min: 0,
+ max: shortcutOptions.length - 1,
+ wrap: false,
+ onchange: value => {
+ config.shortcuts[0] = shortcutOptions[value].val;
+ saveSettings();
+ }
+ },
+ 'Top second': {
+ value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[1]),
+ format: value => shortcutOptions[value].name,
+ min: 0,
+ max: shortcutOptions.length - 1,
+ wrap: false,
+ onchange: value => {
+ config.shortcuts[1] = shortcutOptions[value].val;
+ saveSettings();
+ }
+ },
+ 'Top third': {
+ value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[2]),
+ format: value => shortcutOptions[value].name,
+ min: 0,
+ max: shortcutOptions.length - 1,
+ wrap: false,
+ onchange: value => {
+ config.shortcuts[2] = shortcutOptions[value].val;
+ saveSettings();
+ }
+ },
+ 'Top fourth': {
+ value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[3]),
+ format: value => shortcutOptions[value].name,
+ min: 0,
+ max: shortcutOptions.length - 1,
+ wrap: false,
+ onchange: value => {
+ config.shortcuts[3] = shortcutOptions[value].val;
+ saveSettings();
+ }
+ },
+ 'Bottom first': {
+ value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[4]),
+ format: value => shortcutOptions[value].name,
+ min: 0,
+ max: shortcutOptions.length - 1,
+ wrap: false,
+ onchange: value => {
+ config.shortcuts[4] = shortcutOptions[value].val;
+ saveSettings();
+ }
+ },
+ 'Bottom second': {
+ value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[5]),
+ format: value => shortcutOptions[value].name,
+ min: 0,
+ max: shortcutOptions.length - 1,
+ wrap: false,
+ onchange: value => {
+ config.shortcuts[5] = shortcutOptions[value].val;
+ saveSettings();
+ }
+ },
+ 'Bottom third': {
+ value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[6]),
+ format: value => shortcutOptions[value].name,
+ min: 0,
+ max: shortcutOptions.length - 1,
+ wrap: false,
+ onchange: value => {
+ config.shortcuts[6] = shortcutOptions[value].val;
+ saveSettings();
+ }
+ },
+ 'Bottom fourth': {
+ value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[7]),
+ format: value => shortcutOptions[value].name,
+ min: 0,
+ max: shortcutOptions.length - 1,
+ wrap: false,
+ onchange: value => {
+ config.shortcuts[7] = shortcutOptions[value].val;
+ saveSettings();
+ }
+ },
+ 'Swipe up': {
+ value: shortcutOptions.map(item => item.val).indexOf(config.swipe.up),
+ format: value => shortcutOptions[value].name,
+ min: 0,
+ max: shortcutOptions.length - 1,
+ wrap: false,
+ onchange: value => {
+ config.swipe.up = shortcutOptions[value].val;
+ saveSettings();
+ }
+ },
+ 'Swipe left': {
+ value: shortcutOptions.map(item => item.val).indexOf(config.swipe.left),
+ format: value => shortcutOptions[value].name,
+ min: 0,
+ max: shortcutOptions.length - 1,
+ wrap: false,
+ onchange: value => {
+ config.swipe.left = shortcutOptions[value].val;
+ saveSettings();
+ }
+ },
+ 'Swipe right': {
+ value: shortcutOptions.map(item => item.val).indexOf(config.swipe.right),
+ format: value => shortcutOptions[value].name,
+ min: 0,
+ max: shortcutOptions.length - 1,
+ wrap: false,
+ onchange: value => {
+ config.swipe.right = shortcutOptions[value].val;
+ saveSettings();
+ }
+ },
+ });
+ }
+
+ const COLOR_OPTIONS = [
+ { name: 'Black', val: [0, 0, 0] },
+ { name: 'Blue', val: [0, 0, 1] },
+ { name: 'Green', val: [0, 1, 0] },
+ { name: 'Cyan', val: [0, 1, 1] },
+ { name: 'Red', val: [1, 0, 0] },
+ { name: 'Magenta', val: [1, 0, 1] },
+ { name: 'Yellow', val: [1, 1, 0] },
+ { name: 'White', val: [1, 1, 1] }
+ ];
+
+ // Workaround for being unable to use == on arrays: convert them into strings
+ function colorString(color) {
+ return `${color[0]} ${color[1]} ${color[2]}`;
+ }
+
+ //Shows the top level menu
+ function showMainMenu() {
+ E.showMenu({
+ '': {
+ 'title': 'Informational Clock',
+ 'back': back
+ },
+ 'Seconds display': showSecondsMenu,
+ 'Day of week format': {
+ value: config.date.dayFullName,
+ format: value => value ? 'Full name' : 'Abbreviation',
+ onchange: value => {
+ config.date.dayFullName = value;
+ saveSettings();
+ }
+ },
+ 'Date format': () => {
+ E.showMenu({
+ '': {
+ 'title': 'Date format',
+ 'back': showMainMenu,
+ },
+ 'Order': {
+ value: config.date.mmdd,
+ format: value => value ? 'Month first' : 'Date first',
+ onchange: value => {
+ config.date.mmdd = value;
+ saveSettings();
+ }
+ },
+ 'Separator': {
+ value: SEPARATORS.map(item => item.char).indexOf(config.date.separator),
+ format: value => SEPARATORS[value].name,
+ min: 0,
+ max: SEPARATORS.length - 1,
+ wrap: true,
+ onchange: value => {
+ config.date.separator = SEPARATORS[value].char;
+ saveSettings();
+ }
+ },
+ 'Month format': {
+ // 0 = number only
+ // 1 = abbreviation
+ // 2 = full name
+ value: config.date.monthName ? (config.date.monthFullName ? 2 : 1) : 0,
+ format: value => ['Number', 'Abbreviation', 'Full name'][value],
+ min: 0,
+ max: 2,
+ wrap: true,
+ onchange: value => {
+ if (value == 0) config.date.monthName = false;
+ else {
+ config.date.monthName = true;
+ config.date.monthFullName = (value == 2);
+ }
+ saveSettings();
+ }
+ }
+ });
+ },
+ 'Bottom row': {
+ value: BOTTOM_ROW_OPTIONS.map(item => item.val).indexOf(config.bottomLocked.display),
+ format: value => BOTTOM_ROW_OPTIONS[value].name,
+ min: 0,
+ max: BOTTOM_ROW_OPTIONS.length - 1,
+ wrap: true,
+ onchange: value => {
+ config.bottomLocked.display = BOTTOM_ROW_OPTIONS[value].val;
+ saveSettings();
+ }
+ },
+ 'Shortcuts': showShortcutMenu,
+ 'Day progress': () => {
+ E.showMenu({
+ '': {
+ 'title': 'Day progress',
+ 'back': showMainMenu
+ },
+ 'Enable while locked': {
+ value: config.dayProgress.enabledLocked,
+ onchange: value => {
+ config.dayProgress.enableLocked = value;
+ saveSettings();
+ }
+ },
+ 'Enable while unlocked': {
+ value: config.dayProgress.enabledUnlocked,
+ onchange: value => {
+ config.dayProgress.enabledUnlocked = value;
+ saveSettings();
+ }
+ },
+ 'Color': {
+ value: COLOR_OPTIONS.map(item => colorString(item.val)).indexOf(colorString(config.dayProgress.color)),
+ format: value => COLOR_OPTIONS[value].name,
+ min: 0,
+ max: COLOR_OPTIONS.length - 1,
+ wrap: false,
+ onchange: value => {
+ config.dayProgress.color = COLOR_OPTIONS[value].val;
+ saveSettings();
+ }
+ },
+ 'Start hour': {
+ value: Math.floor(config.dayProgress.start / 100),
+ format: hourToString,
+ min: 0,
+ max: 23,
+ wrap: true,
+ onchange: hour => {
+ minute = config.dayProgress.start % 100;
+ config.dayProgress.start = (100 * hour) + minute;
+ saveSettings();
+ }
+ },
+ 'Start minute': {
+ value: config.dayProgress.start % 100,
+ min: 0,
+ max: 59,
+ wrap: true,
+ onchange: minute => {
+ hour = Math.floor(config.dayProgress.start / 100);
+ config.dayProgress.start = (100 * hour) + minute;
+ saveSettings();
+ }
+ },
+ 'End hour': {
+ value: Math.floor(config.dayProgress.end / 100),
+ format: hourToString,
+ min: 0,
+ max: 23,
+ wrap: true,
+ onchange: hour => {
+ minute = config.dayProgress.end % 100;
+ config.dayProgress.end = (100 * hour) + minute;
+ saveSettings();
+ }
+ },
+ 'End minute': {
+ value: config.dayProgress.end % 100,
+ min: 0,
+ max: 59,
+ wrap: true,
+ onchange: minute => {
+ hour = Math.floor(config.dayProgress.end / 100);
+ config.dayProgress.end = (100 * hour) + minute;
+ saveSettings();
+ }
+ },
+ 'Reset hour': {
+ value: Math.floor(config.dayProgress.reset / 100),
+ format: hourToString,
+ min: 0,
+ max: 23,
+ wrap: true,
+ onchange: hour => {
+ minute = config.dayProgress.reset % 100;
+ config.dayProgress.reset = (100 * hour) + minute;
+ saveSettings();
+ }
+ },
+ 'Reset minute': {
+ value: config.dayProgress.reset % 100,
+ min: 0,
+ max: 59,
+ wrap: true,
+ onchange: minute => {
+ hour = Math.floor(config.dayProgress.reset / 100);
+ config.dayProgress.reset = (100 * hour) + minute;
+ saveSettings();
+ }
+ }
+ });
+ },
+ 'Low battery color': () => {
+ E.showMenu({
+ '': {
+ 'title': 'Low battery color',
+ back: showMainMenu
+ },
+ 'Low battery threshold': {
+ value: config.lowBattColor.level,
+ min: 0,
+ max: 100,
+ format: value => `${value}%`,
+ onchange: value => {
+ config.lowBattColor.level = value;
+ saveSettings();
+ }
+ },
+ 'Color': {
+ value: COLOR_OPTIONS.map(item => colorString(item.val)).indexOf(colorString(config.lowBattColor.color)),
+ format: value => COLOR_OPTIONS[value].name,
+ min: 0,
+ max: COLOR_OPTIONS.length - 1,
+ wrap: false,
+ onchange: value => {
+ config.lowBattColor.color = COLOR_OPTIONS[value].val;
+ saveSettings();
+ }
+ }
+ });
+ },
+ });
+ }
+
+ showMainMenu();
+});
\ No newline at end of file
diff --git a/apps/invader/ChangeLog b/apps/invader/ChangeLog
index 5560f00bc..6c5a33e59 100644
--- a/apps/invader/ChangeLog
+++ b/apps/invader/ChangeLog
@@ -1 +1,2 @@
0.01: New App!
+0.11: Changes...
diff --git a/apps/ios/ChangeLog b/apps/ios/ChangeLog
index b6a386bcb..8ab99b4db 100644
--- a/apps/ios/ChangeLog
+++ b/apps/ios/ChangeLog
@@ -7,3 +7,7 @@
0.07: Added more details from music (instead of Undefined), added more app identifiers
0.08: Added more app identifiers, added 'cannot display' in case a message goes empty because of replacements
0.09: Enable 'ams' on new firmwares (ams/ancs can now be enabled individually) (fix #1365)
+0.10: Added more bundleIds
+0.11: Added letters with caron to unicodeRemap, to properly display messages in Czech language
+0.12: Use new message library
+0.13: Making ANCS message receive more resilient (#2402)
diff --git a/apps/ios/app.js b/apps/ios/app.js
index b210886fd..2a9e8f322 100644
--- a/apps/ios/app.js
+++ b/apps/ios/app.js
@@ -1,2 +1,2 @@
// Config app not implemented yet
-setTimeout(()=>load("messages.app.js"),10);
+setTimeout(()=>require("messages").openGUI(),10);
diff --git a/apps/ios/boot.js b/apps/ios/boot.js
index 5ea7550eb..8952a047e 100644
--- a/apps/ios/boot.js
+++ b/apps/ios/boot.js
@@ -26,7 +26,7 @@ E.on('ANCS',msg=>{
// not a remove - we need to get the message info first
function ancsHandler() {
var msg = Bangle.ancsMessageQueue[0];
- NRF.ancsGetNotificationInfo( msg.uid ).then( info => {
+ NRF.ancsGetNotificationInfo( msg.uid ).then( info => { // success
if(msg.preExisting === true){
info.new = false;
@@ -38,6 +38,10 @@ E.on('ANCS',msg=>{
Bangle.ancsMessageQueue.shift();
if (Bangle.ancsMessageQueue.length)
ancsHandler();
+ }, err=>{ // failure
+ console.log("ancsGetNotificationInfo failed",err);
+ if (Bangle.ancsMessageQueue.length)
+ ancsHandler();
});
}
Bangle.ancsMessageQueue.push(msg);
@@ -63,6 +67,7 @@ E.on('notify',msg=>{
"name" : string,
*/
var appNames = {
+ "ch.publisheria.bring": "Bring",
"com.apple.facetime": "FaceTime",
"com.apple.mobilecal": "Calendar",
"com.apple.mobilemail": "Mail",
@@ -73,6 +78,9 @@ E.on('notify',msg=>{
"com.apple.podcasts": "Podcasts",
"com.apple.reminders": "Reminders",
"com.apple.shortcuts": "Shortcuts",
+ "com.apple.TestFlight": "TestFlight",
+ "com.apple.ScreenTimeNotifications": "ScreenTime",
+ "com.apple.wifid.usernotification": "WiFi",
"com.atebits.Tweetie2": "Twitter",
"com.burbn.instagram" : "Instagram",
"com.facebook.Facebook": "Facebook",
@@ -99,19 +107,22 @@ E.on('notify',msg=>{
"com.toyopagroup.picaboo": "Snapchat",
"com.ubercab.UberClient": "Uber",
"com.ubercab.UberEats": "UberEats",
+ "com.unitedinternet.mmc.mobile.gmx.iosmailer": "GMX",
+ "com.valvesoftware.Steam": "Steam",
"com.vilcsak.bitcoin2": "Coinbase",
"com.wordfeud.free": "WordFeud",
+ "com.yourcompany.PPClient": "PayPal",
"com.zhiliaoapp.musically": "TikTok",
+ "de.no26.Number26": "N26",
"io.robbie.HomeAssistant": "Home Assistant",
+ "net.superblock.Pushover": "Pushover",
"net.weks.prowl": "Prowl",
"net.whatsapp.WhatsApp": "WhatsApp",
- "net.superblock.Pushover": "Pushover",
"nl.ah.Appie": "Albert Heijn",
"nl.postnl.TrackNTrace": "PostNL",
"org.whispersystems.signal": "Signal",
"ph.telegra.Telegraph": "Telegram",
"tv.twitch": "Twitch",
-
// could also use NRF.ancsGetAppInfo(msg.appId) here
};
var unicodeRemap = {
@@ -120,18 +131,34 @@ E.on('notify',msg=>{
'261':"a",
'262':"C",
'263':"c",
+ '268':"C",
+ '269':"c",
+ '270':"D",
+ '271':"d",
'280':"E",
'281':"e",
+ '282':"E",
+ '283':"e",
'321':"L",
'322':"l",
'323':"N",
'324':"n",
+ '327':"N",
+ '328':"n",
+ '344':"R",
+ '345':"r",
'346':"S",
'347':"s",
+ '352':"S",
+ '353':"s",
+ '356':"T",
+ '357':"t",
'377':"Z",
'378':"z",
'379':"Z",
'380':"z",
+ '381':"Z",
+ '382':"z",
};
var replacer = ""; //(n)=>print('Unknown unicode '+n.toString(16));
//if (appNames[msg.appId]) msg.a
@@ -173,7 +200,11 @@ Bangle.messageResponse = (msg,response) => {
// error/warn here?
};
// remove all messages on disconnect
-NRF.on("disconnect", () => require("messages").clearAll());
+NRF.on("disconnect", () => {
+ require("messages").clearAll();
+ // Remove any messages from the ANCS queue
+ Bangle.ancsMessageQueue = [];
+});
/*
// For testing...
diff --git a/apps/ios/metadata.json b/apps/ios/metadata.json
index eb75a6dbc..42e0060d0 100644
--- a/apps/ios/metadata.json
+++ b/apps/ios/metadata.json
@@ -1,11 +1,11 @@
{
"id": "ios",
"name": "iOS Integration",
- "version": "0.09",
+ "version": "0.13",
"description": "Display notifications/music/etc from iOS devices",
"icon": "app.png",
"tags": "tool,system,ios,apple,messages,notifications",
- "dependencies": {"messages":"app"},
+ "dependencies": {"messages":"module"},
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"storage": [
diff --git a/apps/isoclock/ChangeLog b/apps/isoclock/ChangeLog
index 809091ce4..7b57ecfa9 100644
--- a/apps/isoclock/ChangeLog
+++ b/apps/isoclock/ChangeLog
@@ -1,2 +1,3 @@
0.01: Created app based on digiclock with some small tweaks.
0.02: Swap to Bangle.setUI for launcher/buttons
+0.03: Tell clock widgets to hide.
diff --git a/apps/isoclock/checkout b/apps/isoclock/checkout
new file mode 100644
index 000000000..e69de29bb
diff --git a/apps/isoclock/isoclock.js b/apps/isoclock/isoclock.js
index 59f28e66e..7526660b9 100644
--- a/apps/isoclock/isoclock.js
+++ b/apps/isoclock/isoclock.js
@@ -89,8 +89,8 @@ Bangle.on('lcdPower',on=>{
}
});
-Bangle.loadWidgets();
-Bangle.drawWidgets();
-
// Show launcher when button pressed
Bangle.setUI("clock");
+
+Bangle.loadWidgets();
+Bangle.drawWidgets();
diff --git a/apps/isoclock/metadata.json b/apps/isoclock/metadata.json
index 313153dde..488afcb41 100644
--- a/apps/isoclock/metadata.json
+++ b/apps/isoclock/metadata.json
@@ -2,7 +2,7 @@
"id": "isoclock",
"name": "ISO Compliant Clock Face",
"shortName": "ISO Clock",
- "version": "0.02",
+ "version": "0.03",
"description": "Tweaked fork of digiclock for ISO date and time",
"icon": "isoclock.png",
"type": "clock",
diff --git a/apps/kanawatch/ChangeLog b/apps/kanawatch/ChangeLog
index 7b83706bf..f2c991fd0 100644
--- a/apps/kanawatch/ChangeLog
+++ b/apps/kanawatch/ChangeLog
@@ -1 +1,5 @@
0.01: First release
+0.02: Improve battery life, sprite resolution, fix launcher issue and unaligned text bug
+0.03: Reduce code size, refresh once a minute and faster refresh
+0.04: Show a random kana every minute to improve learning
+0.05: Tell clock widgets to hide.
diff --git a/apps/kanawatch/README.md b/apps/kanawatch/README.md
index 1fdf1927c..e213949dc 100644
--- a/apps/kanawatch/README.md
+++ b/apps/kanawatch/README.md
@@ -3,10 +3,17 @@
A simple watchface design with hiragana and katakana
cards for learning.
+## Changelog
+
+0.01: First release
+0.02: Improve battery life, sprite resolution, fix launcher issue and unaligned text bug
+0.03: Reduce code size, refresh once a minute and faster refresh
+0.04: Show a random kana every minute to improve learning
+
## Author
Written by pancake in 2022, powered by insomnia
## Screenshots
-
+
diff --git a/apps/kanawatch/app.js b/apps/kanawatch/app.js
index ada6aa6df..088dab785 100644
--- a/apps/kanawatch/app.js
+++ b/apps/kanawatch/app.js
@@ -7,641 +7,107 @@ const w = g.getWidth();
/// /////////////////////////////////////////
const katakana = {};
const hiragana = {};
-katakana.A = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAjAEBfv4B/+yeAXwAOgBAAPAAAEHAAABzAAAAPgAAADgAAAAwAAAAMAAAAGAAAABgAAAAYAAAAMAAAADAAAABgAAAAYAAAAMAAAAGAAAADAAAABgAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.A = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAACAAAAAwAAAAIAAAACAAAABgAAAAZ4AAGf4AAA/gAAAAQAAAAEAAAABBAAAAQwAAAN/wAADiGAADxAwABswEAAhYBgAQUAYAMHAEACBgDABh4AwAZ2AYAD4gcAAQAcAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.I = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAwAAAAGAAAADwAAAA0AAAAYAAAAUgAAAGAAAAFAAAADgAAAA8AAAA2AAAAZgAAAYYAAAMGAAAMFgAAGAYAAGAGAACABgAAAAYAAAAGAAAABgAAAAYAAAAGAAAABgAAAAYAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.I = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAwAAgAEAAEABAAAgAQAAMAGAABAAgAAYAIAAGACAAAwAQAAMAEAADABiAAQAIgAAADQAAAAcAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.U = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAQAAAAHAAAAAwAAAAICAAACIAAAAgIABQa3AAP7q4ADQANAAwADAAMABgADAAYAAwAGAAMADAADAAwAAwAYAAMAGAABADAAAABgAAAAwAAAAMAAAAGAAAACQAAADAAAABgAAAAwAAAAoAAAAAAAAAAAAAAAgAAA=')
-};
-hiragana.U = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAIAAAABwAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAAA4YAAA4CAAAAAgAAAAIAAAACAAAAAgAAAAYAAAAGAAAABAAAAAQAAAAIAAAACAAAABAAAAAQAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.E = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAJXAAe+20ADRQAAAAOAAAABgAAAAQAAAAMAAAABAAAAAwAAAAEAACABAEAgJbvgP9qSsByAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.E = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAADgAAAAOAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAdwAAAcYAAB8MAAAIGAAAADAAAABgAAAAwAAAAYAAAAMAAAAGAAAADIAAAB4gAAA4EAAAMAgAACAOGAAAB/wAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.O = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAAAAAADwAAAAOAAAADAAAAAwAAAAMAAAAjAABAAydAbff/wH/XAUAwDwAAAB0AAAA7AAAAMwAAAHMAAADjAAABkwAAA4MAAAZDAAAMEwAAGEMAAGQzAADAHwABAA8AAAAHAAAABAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.O = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAgAAAAMAAAADACAAAwAYAAMADAADIAQAA/AGAF+AAAAyAAAAAgAAAAIAAAACAAAAAg/gAAJwOAADgBgABgAMAAoADAAyAAwAIgAMAEIAGABCADAAJgBgAD4AgAAMAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.HA = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABIAAAAcGAAADgwAAB4HAAA4A4AAMgHAAHAA4ADAAXAAwAA4AYAAHAMAABwGAAAMGAAACDAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.HA = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAABAACAAYAAwAGAAMABgACAAYAAgAHwAIAD4AGAfYABAAGAAQABgAEAAYABAAGAAQABgAEAAYABAAGAAQABgAEAAYABAOGAAQEfgAFCA8ABggPwAYG+GAGAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.HI = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAAABgAAAAXAAAABwAAAAYAgAAGAMAABgDgABYD0AAWF4gABvwAAAfAAAAGAAAABgAAAAYAAAAGAAAABgAAAAYAAAAGAAAABgAgAAal8AAD//gAAJQAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.HI = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwEAAA8BAAB2AYAABACAAAwAQAAIAEAAGAJgABACIAAwAjAAIAIYACACGABABAwAQAQEAEAEAABADAAAAAgAAEAYAABAEAAAYDAAADDgAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.HU = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJAAAALwAYt/vAD/0DwAcABwAACAcAAAAGAAAADIAAAAwAAAAYAAAAOAAAAGAAAADgAAAAwAAAA0AAAAaAAAAOAAAAHAAAAHAAAAHAAAAGgAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.HU = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAGAAAAAwAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAACAAAAAQAAAACAgAAAgEAAAMBgAABAYAgAwDAMAMAgBgCAAAYHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.HE = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAANwAAAGHAAADA4AABwDgAIwBOADcABwQeAAHgDAAA8AIAADwAAAAeAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.HE = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZAAAAIMAAAMAgAAGAGAADAAwAAAADAAAAAYAAAADgAAAAMAAAABwAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.HO = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAAAAAAEAAAADQAAAAYAAAASAAAABgAAAAIAACAGK4A273dAHoYAAAAGAAAAAgAAAIIQAAECGAABAgwAAgYGAAIGAwAGAgGADAIBwBiCAaAYRgDAMDIAgAAeAAAADgAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.HO = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAQB+AAEAgAABAAQAAQAGAAIABgACAAQAAgAHwAIAD4ACAfQABAAEAAQABAAEAAQABAAGAAQABgAEAAYABAAGAAQBdgAHAg4ABwAHgAIB+OACAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.KA = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAAAAAAUAAAADwAAAAcAAAAGAAAABgAAAAYFABAOvwAfv9eAD6wHAAQMBwAADAYAABwGAAAYBgAAGAYAADAOAAAwDAAAYgwAAMgcAADEmAAFgzgAAwHwAA4B4ABYAcAAMABAAEAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.KA = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAwAAAAIAAAACAAAABAAAAAQAgAAMAEAAD8AwAHggGAHQIBgAECAMADAgDAAgIAQAIGAEAEBAAABAwAAAwIAAAYGAAAGBgAAADwAAAAcAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.KI = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAAAAACwAAAAeAAAADgAAAAYAAAATBgAAAz8AAAP5AAxfQAAH8YAAA4GAAAABgHAAAYf4AAD+pAAF8AAMPsAAC/hgAAPAYAABAGAAAABwAAAAYAAAAHAAAAAwAAAAOAAAADAAAAAYAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.KI = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAQAAAAGAAAAAgAAAAIAAAADDAAAAfwAAAeAAAA4gAAAwIAAAABAAAAAZwAAADwAAAHwAAAOGAAAAAgAAAAMAAAADAAAAAQAAAAAAAAAAAAAAAAAAEAAAABgAAAAPmAAAAfwAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.KU = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAAAAAAQAAAAHAAAAA4AAAAMAAAAHBwAAB/+AAA0XAAAaBkAAGA4AADAOAABgHAAAwBwAAYA4AAMAMAAGAHAAAADgAAABwAAAA0AAAAaAAAAOAAAAHAAAADIAAADgAAACgAAABgAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.KU = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAQAAAAMAAAADAAAABgAAAAQAAAAIAAAAGAAAABAAAAAgAAAAQAAAAEAAAACAAAAAQAAAAEAAAAAgAAAAEAAAABgAAAAIAAAADAAAAAYAAAAGAAAAAwAAAAMAAAABgAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.KE = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAAAAABwAAAAOAAAADgAAABwAAAAYAQAAGAAgABgF8AA79/gAb7gAAGQcQADAHgABgBgAAYAwAAZAMAAMAHAADABgAAgAwAAAAMAAAAGAAAALAAAABwAAAAYAAAAYAAAAMgAAAGAAAACAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.KE = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAABAAAAAYAAIAGAAGABgABgAYAAYAGAAEAB+ABAB/gAQHmAAEABgADAAYAAgAGAAIABgACAAYAAgAGAAIABAACAAQAAgAEAAKABAADgAwAAYAIAAGACAAAgBAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.KO = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAwCtwAH//8AA+oGAAEABgAAAAYAAAAGAAAABgAAAAQAAAAsAAAADAAIAFwADv//AAf1CQACAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.KO = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/8AAAADwAAAB8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAYAAAAD8EAAAH/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.MA = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAFcAIG/3ga/0h4H6gA4AcAAcACAAOAAAAHAAAYDAAAFjgAAAPgAAAB4AAAAOAAAABwAAAAMAAAADAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.MA = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAEAAAABBAAAAf8AAD+AAAOBAAAAAQAAAAGAAAABgAAAAZwAAAHwAAB/gAAAAYAAAAGAAAABgAAAAYAAAAGAAAARgAAAR4AAAIHgAACDPAAARg4AABAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.MI = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAegAAAC+gAAAB8AAAAHgAAAAYAAAAQAAAAgAAAegAAAB+AAAAD4AAAAPAAAABwAAAAMAAAAAAAAAAAAAAAAAAAUAAAAF8AAAAHwAAAAPgAAAA8AAAAHwAAAAeAAAADAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.MI = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAA+YAAAEMAAAADAAAABgAAAAQAAAAMAAAACAAAABgAAAAQAAAAIAIAAGAGAADgBgAO/wQAEIH8ACEAH4AiABnAJgAQQBgAIAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.MU = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAACgAAAAcAAAADgAAAA4AAAAcAAAAHAAAABkAAAAYAAAAMQAAADEAAABwwAAAYGAAAGAwAADAHAAAwA4AAIAOAAWBfwBBX9OAf/oDgH+gAYA6AAGAAEAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.MU = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAYAAAACAAAAAgAwAAIAGAACIAwAA/gEAB+ABAB2AAAAAgAAAAYAAAAGAAAABgAAAAYAAAAGAEAABgBAAGQAQAA0AEAAFABAAAwAQAAEAMAARgCAAGWHgAA8fgAAGAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.ME = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAAAAAAAcAAAABgAAAAcAAAAHAAAADgAAAAwAAABcAABgGAAAfDgAAAewAAAB8AAAAPAAAAD8AAABzgAAA44AAAcHAAAGAwAADAAAACgAAABwAAAAoAAAAcAAAAMAAAAMAAAABAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.ME = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABgAAAAYAABAGAAAIBAAACAwAAAgP4AAMeDgABZgMAAYQBgAOMAYAGiACADJgAgAjQAIAQcACAEGABgBBgAQARsAIAHwAEAAQAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.MO = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAAABAAUAASXfgAD7EQAAQwAAAAOAAAABAAAAAwAAAAEAUBADd/wNfaRID1EAAAIDAAAAAwAAAAEAAAADAAAAAQAAAAMAAAABiQAAAf+AAABKQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.MO = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAACAAAAAgAAAAIAAAACAAAAB+AAAA/wAAB0AAAABAAAAAQAAAAEAAAABAAAAAQAAAAEYAAAf+AAABwAAAAMAAAACAIAAAgCAAAIAgAACAIAAAgCAAAEBAAABgwAAAP4AAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.NA = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAAAAAAPAAAAA4AAAAOAAAADAAAAAwBAAAMQAAADAaBAT9/wf/vbcD6DAAAQAxAAAAMAAAADAAAAAwAAAAMAAAAGAAAABgAAAAYAAAAMAAAAGAAAABgAAAAwAAAAYAAAAMAAAAEAAAABAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.NA = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAEAAAADAAAAAgAAAAJgAAAH4AAA/gAAAMQAIAAIABAACAAYABgACAAQAAAAMAAAACAIAAAgCAAAAAgAAAAIAAAACAAAAAgAAAPIAAAEOAAABB4AAAQTgAAD4MAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.NI = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgALAANb/8AB/6pAAMAAAAAACAAAAAAAAAAAAAAAAAAAAAAABAAAIAAAJvAKN//4D/1EGAdAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.NI = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAGAAAABgA/AAYBwAAEAAAABAAAAAQAAAAMAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAAEAAAIAgAADAH/gAwAAAAMAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.NU = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAFgAML38AB/9OAAOgHAAAQBgABAA4AAAAMAADoHAAAPRgAAAfYAAAB4gAAAPQAAADeAAABjwAABwcAAI4DgAA4AcAAcADAAcAAQAaAAAAWAAAACAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.NU = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAABAwAAAIMAAACDf4AAg4DAAIYAYACeACAAZAAgAMQAIAFMACACSAAgBDgAIAwwOGAIMEbACHBDgAjYPsAPCABgBggAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.NE = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAGAAAAA4AAAAHAAAAA4AAAAGAAAABgAAAAJYABAv/AAf+nwAD4DoAAQB4AAAA8AAAB8AAAAeAAAAPQAAAHzgAAHMeAAHDB4ADgwOAHgMBwHADAMKgAwAgAAMAAAADAAAAAwAAAAMAAAADAAAAAwAAAAEAAAAAAAAAAAAA=')
-};
-hiragana.NE = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAgAAAAMAAAADAAAAAwAAAAIAAAADAHgAA8GIAA+CCAAzBAwAAhAMAAIgDAAGQAwABoAMAAsADAASAAwAFgAMAC4ACAAyAugAcgIYAGYCHABGAecABgABgAYAAIAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.NO = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAGAAAABwAAAAeAAAAOAAAADgAAAAwAAAAcAAAANAAAADAAAABwAAAAYAAAAOAAAAHAAAABgAAAAwAAAAYAAAAMAAAAGAAAAGQAAADAAAADgAAAAkAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.NO = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAD44AADEBAABBAIABgQBAAwMAYAICACAEBgAgAAQAIAgMACAICAAgCBgAYAwwAGAEIADABmAAgAPAAQADgAYAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.RA = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAEAAQAIAANTvgAD/u0AAOAAAAAACAAAAAAABAACAAgAt4APf/vAB/UDQAIAJwAAAA4AAAAOAAAAHAAAABgAAAA4AAAAcAAAAOAAAAGgAAADQAAABoAAAA4AAAAcAAAAaAAAAMgAAAEIAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.RA = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAQAAAACAAAAAwAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAIAAAACAAAAAgAAAAYAAAAEAAAABAAAAAQAAAAEA+AADBwQAA3gCAAPgAgADAAIAAAAGAAAADAAAABgAAAAwAAAAgAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.RI = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAQBwAAHgOAAAsDgAAGAwAABgMAAAYDAAAGAwAABgMAAAYDAAAGAwAABgMAAAYDAAAGAwAABgMAAAYDAAACAwAAAAYAAAAGAAAADAAAAEwAAAA0AAAAcAAAAOAAAAOAAAAOAAAAGAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.RI = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAABAAAAAYAAAAGAAAABAQAAAQGAAAEAgAABAIAAAwCAAAIAgAACAIAAAoCAAAOAgAADAIAAAQCAAAEAgAAAAYAAAAGAAAABAAAAAQAAAAEAAAACAAAAAgAAAAAAAAAEAAAACAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.RU = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAABAAAAAeAAAADgAAAA4AAAcGAAADhgAAA4YAAAMGABAGDAAwBkYAYAYGAMAMDAOADAYHAAwGDgAYBjgAMgbwADAHyABgD4AAwA4AAYAEAAMAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.RU = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAO4AAA8MAAAAGAAAADAAAAAgAAAAQAAAAIAAAAGAAAABAAAAAgAAAAQAAAAIYMAAE4BgAB4AIAA4ACAAMAAgAAAAYAAAAEAAATCAAAEZAAAB/AAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.RE = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAACgAAAAeAAAADgAAAAwAAAAMAAAADAAAAAwAAYAMAAMADAAGAAwAGAAsADgADABgAAwBwAAMBwAADA4AAAw8AAAM8AAAD8AAAA+AAAAGAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.RE = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAGAAAYDgAAfBIAANgiAAMQwgAAMYIAAHMCAAB2AgAAnAIAAJgCAAEwAgAAcAIAAvACAAewAggHMAIwBDADwAAwAAAAMAAAABAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.RO = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAYABk3/AAP/a4ADQAYAAwAGAAMABgABAAwAAwAMAAMADAABAAwAAQEMAAEASAADEt4AA/++QAGgAAADAAQAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.RO = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAF8wAAAwYAAAAMAAAACAAAABAAAAAwAAAAYAAAAEAAAACAAAABAAAAAg/gAARgGAAPgAgAHgAMADgADAAQAAwAAAAYAAAAOAAAAGAAAAGAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.SA = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAFAAAAA4AABAHAAAdBgAABwYAAAYGAAAGBgAABgYABAYGrAYu//4H/aomA4YGAAAGBgAABgYAAAYGAAAGBgAABgwAAAYMAAACGAAAABgAAAAwAAAAYAAAAOAAAAGAAAADgAAABgAAAAgAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.SA = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAGAAAABgAAAAIAAAADAAAAAQgAAAG8AAAB4AAAB8AAAPhgAAAAIAAAABAAAAAYAAAADAAAABwAAAAGAAAAAgAAAAAAAAAAAAAAAAAAAAAAAEAAAAAwAAAAH/AAAAHwAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.SI = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAA0AAAAHgAAAAcAAAAjgAAAAYABAAAAAwEAAAYB4AAMAHgAGAA4ADAAWABgAAgAyAAAAYAAAAcAAAAOAAAAOAAAIHAAAUHgAABnwAAAPwAAAB4AAAAIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.SI = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAQAAAAGAAAABgAAAAYAAAAGAAAABgAAAAYAAAAGAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAIAQAADA4AAAf4AAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.SU = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAEAAGC/gAE/9cAAOgOAAAgHAAAABwAAAA4AAAAcAAAAGAAAAHgAAAB2AAAA44AAAYHAAAcA4AAOAHAALAA4AHAAOAGgABgDAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.SU = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAADAAAAAQAAAAEAAAABAAAAAQAAAAE/gAAf/4AH4QAAHAEAAAABAAAAAQAAAGkAAABFAAAARQAAAEcAAABDAAAAYwAAAB8AAAAGAAAABgAAAAQAAAAMAAAAGAAAABAAAABgAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.SE = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAA4AAAAHgAAAA4AAAAMAAAADABAAAwG4CAN/vAw36DgH+wDgA6MBwACDA4AAAwZAAAMUAAADMAAAAyAAAAMAAAADAAAAAwAAAAMAAAADAGAAA//gAAL94AAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.SE = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAMAAAgCAAAMAgAADAIAAAwCAAAMAgAADAf8AAx+AAAPhgAAPAQAA8wEAAMMBAAADAwAAAwcAAAEGAAABAAAAAQAAAACAAAAA8OAAAB/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.SO = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAQAAAALAAIAA4ADAAeAAcAHAADIBwAAYA4AAHAOAAAwDAAAIBwAAAAYAAAAMgAAAHAAAADAAAABwAAAAYAAAAMAAAAOAAAAHAAAADgAAADgAAADgAAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.SO = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAYAAAAzAAAHxgAAAwwAAAAYAAAAEAAAACAAAABAAAAAgAAAAQDwAAIDwAAEGQAACOIAABeEAAAMCAAAAAAAAAAQAAAAEAAAABAAAAAYAAAADAAAAAYAAAADwAAAAMAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.TA = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAAAAAAOAAAABwAAAAcAAAAGBYAAB/fAAA1DgAAMA4AAGAcAABgHAAA4DAABdwwAAMPcAAGA+AADADkABgB8AAQAzAAAAMAAAAGAAAADAAAADgAAABwAAAA4AAAAYAAAAcAAAAMAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.TA = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAACAAAABgAAAAYAAAAEAAAADHgAAA/gAAH8AAAAmAAAABAAAAAQAAAAMAAAACAfgABg4AAAQAAAAEAAAADAAAAAgAAAAYAAAAEAAAADAAAAAwDjgAIAP8ACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.TI = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAAAAAAAOAAAAH4AAAPwAAAvgAADe4AAL4OAABADAAABAwAQAAMV4ECv//B7/0IwPQmAAAgDAAAAQwAAAAMAAAADAAAABgAAAAYAAAAMAAAAGAAAACgAAAAwAAAAwAAAAsAAAAMAAAACAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.TI = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAABAAAAAIAAAAGAAAABgAAAAQAAAAEAAAABHAAAB/AAAH4AAAACAAAAAgAAAAQAAAAEAAAABAAAAAQAAAAIPcAACMBgAAsAIAAcACAAGAAgAAAAIAAAAGAAAADAAAABgAAABgAAABgAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.TU = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAACAAAAAyBgAgHAOAGA4DwAwOA8AGBgcABwYHAAcADgADAA4AAQAcAAAAGAAAALgAAABwAAAAYAAAAMAAAAOAAAADAAAADgAAABwAAABwAAABwAAADRAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.TU = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/HAAB4AYADwACAPwAAwBgAAMAAAADAAAAAgAAAAYAAAAEAAAADAAAADAAAADgAAADAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.TE = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAAAAAAACAACAn4AC3/vAAHoAAACQAAAAAAIAAAIAAAgAFfQG3+/8B/YwBAGAOAAAADgAAABgAAAAcAAAAGAAAADAAAABQAAAAYAAAAOAAAADAAAABgAAAAwAAAAwAAAAYAAAAKAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.TE = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAPAAAAbgAAA5gAABwgAADwYAAHgEAAAgCAAAABAAAAAQAAAAAAAAACAAAAAgAAAAIAAAACAAAAAQAAAAEAAAAAgAAAAOAAAABwAAAAHAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.TO = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAAAAAA4AAAAHgAAAA4AAAAMAAAADAAAAAwAAAAMAAAADAAAAAwAAAAMAAAAD6AAAAzwAAAMPAAADA4AAAwHAAAMAwAADAEAAAwAAAAMAAAADAAAAAwAAAAMAAAAGAAAABwAAAAMAAAAAAAAAAAAAAAIAAAAAAAA=')
-};
-hiragana.TO = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAGAAAAAgAAAAMAYAABAHAAAQHAAAGDAAAAhgAAAIwAAABwAAAAYAAAAMAAAAEAAAACAAAABAAAAAQAAAAAAAAACAAAAAAAAAAGAAAAAf/wAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.WA = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAAAAAACAAANACsAB7//gAPtI4AJgAOAAYADAAMABwABgAYACYAGAAOABgABgA4AAwAcAAGAHAABADgAAAAwAAAAcAAAAOAAAAHAAAADgAAADgAAADgAAAFwAAACgAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.WA = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAgAAAAMAAAADAAAAAwAAAAMAAAADAAAAA8AAAAfAAAAfgAAAIwAAAAIDnAAGCAYACiACAArAAwATAAMAJgADAD4AAgByAAYARgAEAAYACAAGACAABgAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.WI = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAQAAAAPAAAAA4AAAAMAAAADAAAAAwAAAAsEABhLvgAP//cAB5MAAAGDAAADAwAAAQMAAAMDAAADAwcBg19/wf/7UsD1AwAAIAMAAAADAAAAAwAAAAMAAAADAAAAAwAAAAMAAAADAAAAAQAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.WI = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAcAAAAbAAAB4wAAAMIAAAACAAAABgAAAAQAAAAEAAAADAAAAA3+AAAeAwAAeAGAAZAAgAMQAMAEMADACCAAwBBgAMAwQACAIMDxgBCBGwARAQYADgCcAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.WE = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAFBK4AB//+AAPUHgABADgAAARwAAAOwAAABwAAAAMAAAADAAAAAwAAAAMAAAALAAAgAyVgPv//+B/qIrgIAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.WE = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAA8AAAB3AAAHhgAAAAwAAAAYAAAAMAAAAGAAAADAAAABhwAAAzDAAAaAYAAOAGAADADAAACRgAAANgAAAGAAAADAAAABgAAAAgAAAAwAcAAYAxwAfggGAOGwAwDA4AAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.WO = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAABAAAgAAwAO37/AB//bwAMgAwAAABYAAgAHAAAABgAGFb4AA//uAAHQDAAAAQwAAAAYAAAAOAAAADAAAABgAAAAwAAAAsAAAAGAAAADAAAABgAAABoAAAAwAAAA4AAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.WO = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAMAAAACOAAAB/AAAPwAAAAMAAAACAAAABAAAAAwAgAAIAcAAHMMAADBOAAAAeAAAAGAAAADgAAADIAAABCAAAAgAAAAIAAAACAAAAAgAAAAGBwAAAP8AAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.YA = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAEAAAAD4AAAAOAAAADAAAAAwAIAAGADAABgX4AAa/fAAX6OAZfwHAD9MDgAcDBgAEAwwAAAmYAAABogAAAYAAAAGAAAABgAAAAcAAAADAAAAAwAAAAOAAAADgAAAA4AAAAGAAAABgAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.YA = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAgAAAAGAAAAAgAAAAMAAAQAAAAEAAAABAHGAAQOAwAGcAEAA4ADAA4AAwA7AAYA4QA4AAEAAAAAgAAAAIAAAADAAAAAQAAAAGAAAAAgAAAAMAAAADAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.YU = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAABAC4AAd//AAH2hoAAQAyAAAAMAAAALAAAAAwAAAAMAAAADAAABAwQEABe+Bt//9wP+kAIBwAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.YU = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAAgAAAAIAACADAAAwD3AAIDIIACBCDAAgggQAIwIGACICBgBkAgYASAIEAEACBABQBgwAcB4YAGAGcABgB8AAYAQAACAIAAAACAAAABAAAAAQAAAAIAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-katakana.YO = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAIAlgAD//8AAfUGAACABgAAAAYAAAAMAABABgABBLwAAf/8AAF0DAAAgAwAAAAMAAAADAAAQAwAAgAMAANN3AAD/3wAANAIAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.YO = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAEAAAABgAAAAMAAAACAAAAAgAAAAIAAAACDAAAA3wAAAOAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAPCAAAEPgAABA8AAAQHwAAADPAAA/g8AAAADgAAAAYAAAAAAAAAAAAAAAAA=')
-};
-katakana.N = {
- width: 32,
- height: 32,
- bpp: 1,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAEAAAABgAAAAOAAAABwAAIAMgAGADgADAAwABgAAAAwAAAAwAAABYAAABOAAAAHAAAAHAAAADgAAADkAAABwAACB4AAAx4AAAP4QAAB8AAAAOAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
-hiragana.N = {
- width: 32,
- height: 32,
- bpp: 1,
- transparent: 0,
- buffer: atob('AAAAAAAAAAAAAAAAAAAAAAAAgAAAAIAAAACAAAABgAAAAQAAAAMAAAACAAAABAAAAAQAAAAIAAAAGAAAABAAAAAkAAAALgAAAFIAIADiAAAAwwBAAYMAQAEBAIADAQGAAgGDAAYAzgAEAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
-};
+function image(x,y,b) {
+ return {
+ bpp:1, width:x,height:y,
+ buffer:require('heatshrink').decompress(atob(b))
+ };
+}
+katakana['A'] = image(56, 51, "v//AAfwAon//AGF/wGT/gGM/A3F/BDEn/wJQoGCj4RB//gAxUB//AAwcDAwsH/+AAwcP/4tCAwMf/wGEn/8Awl/JYYGBKQkf/I9DAwJgBGwQGDGwRlBAwJsE+42DAwPzGwYGB+J7EQIIvDQIIFEAw5DEAwRDDgCIEAxCPBKIcAR4IhER4hnCLAg9BLAgoBAwgoBcQiCBMwj0BHogGBHogGBfoooEQQREFEIgGBAokAhAGFA=");
+katakana['I'] = image(54, 55, "AAkEAws+AokB/wGEg//Awk//gTE//gAwcPCYt/CYkDCYsfCYv//A0F4A0ECYg0BCYggBCYn/KwhBBGgl/EAgtBEAgMBEAZOBEAgMBEAYZB/+ABggTDBgQnDAoIaDJoIaDFgIABDQQFC74aBBgX8v4aBEwWBDQQgB/EHDQQ6BwEfGoX/+AJBDQMDWAKMBDQMPAQIaDiBFCPAgaDU4hrDDQiuDDX4acSAIaCA=");
+katakana['U'] = image(52, 55, "AAMP/gGE//ABlH/AAnvAon+Bk5EDv/vIgcHBkHPBgZwBBgn/Bi8B/+PBgcf/AMFw/wBgYEDgED/6qEv4MEKYK3F8AFDj7EED4LREv/4CQn/wASEFginBDAgfEDAIfDn67BC4YABH4QXBCQcHZoQkEEoYMCHAYlBFYZEBLwk/MgpQEAAw");
+katakana['E'] = image(58, 45, "h//AAfwgYGE/0AAwn/wE/AwngDgv4DjhDCv/wJQkf/gGEg//AwkB//AA4gc/Dn4cjbAv/34GF94GF/YGF/wcjwA=");
+katakana['O'] = image(57, 54, "AAcf+AGEh/8AwkH/wGEgf/AwkB/+AA4n/4AGEv/gAwk/GIsf/A4P/4AE+F/Awn4n4GE/kfAwn+h4cFg4GFwYGF4IGFKwYFBMQpxFAwJxEAwJxEAwJxEAwJxEK4JxEAwKqEMoQGE/o4En/8HAl//iqEAwKqEv/+VQgNBVQgNBcYgNBcYhLBcYhSCHAQKBAwI4CAwY4CD4IGBHASxBAYI4CAwY4CYwIGBHAQGBD4I4CBIJfCHASmDHAV/PYQ4Cj5QCHAUPLwQ4CgQGCOIgABOIgABHAIGEAAY=");
+katakana['KA'] = image(54, 54, "AAMP/AGEv/gAocB/+AAwcH/wTEj4arg//AAf+j4GE/F/AwnhAon/w4aZHAMP/hTEn/wKYn/4BTDgf/KYgQCDQYQCBIQQDBIQQCBIc/DQouCDQQuCEghJBEhITBH4RTBLoRTEBIJTGCAUPNwoTCDQQWBDoIuCj4TCJIX/CYQ/BZQInBH4U//0HwBTBGgPwXAXwh4PBXAXAv4PCZIIgBEYTJBn5SBDQXABAIzBCYJcCDQXwgbOCAwIDBQgI4CgEOJwIADkAGFA");
+katakana['KI'] = image(58, 55, "AAU+Awv/4AGEn/wAwkP/gGEgf/Dkk/CAc//4ABwAGBj4GC8ATBAAf4h4GE/woBAAmAAwvgFAYcIwAcD/BFDFARFD/kBIoYACv5FBAAcfRL94DgkfHgf/95EBD4RgDD4MHLwf8AogAd+CPFGwiJCS4XHJgSGB8CJEkCJJUwYABg5pDD4amTNwKmXYbgcDLoY=");
+katakana['KU'] = image(55, 55, "AAMHwAGEh/8Awkf/AGEv/wAwn/4AFDgf/EQkH/whF/4ACAwM/AoQQCBgY5BgIGDHIMHAwY5Bh4GD8AhEIAQFDIAIhBBIJACEIJpEj45CNIV/NgRpBDQIrBEoPgDQJlBEoQaDEoV/RwUP/wPBQ4Uf/gPBQ4QsBKAKSD8BvCSQXDDQYYBNYIaCGYIqBDQU//kPXoYYBj5QCEIPgj60DKoMcWga7FKoYABKogaDbojPBbojMDGob/ECYJBCbgYaDE4IaEPoIaDEAI1EbYQZECYgtBCZQGCLol/KwxxEAwJqEgIMFgIZEgA=");
+katakana['KE'] = image(60, 54, "AAMcAwsD/4HFn/wBxl/8AGEg/+BxkP/gOF//ABxcB/+AA4kf/BCGAAZOBv4HEIQIOGAwgOBh4OFGYIOFn4OFEgoOBAwvgh52BKgYDBOwJUDv5nBBwY6BAYM/BwIKBJgJjBBQSbCWoQVBRgK1D/4oDBwJJBWos/WIS1CgIVCJoRGBWowCCj61HYgpRCdIjEGLgTLEIwTLEfAv/GYqtBEghyBGYjoCAwwkDAwQVEYwYjEHQt/CopeBQgQOEIIgOBPgxeFgZ7FA");
+katakana['KO'] = image(49, 46, "v//AAYFF34FE74FE94FE+4FE/IFE/gFE/w0Dgf/AocB/+AAwf/4BHE8AFDn/wAocf/AFDh/8AocHGH4w6YZf7Aon9YYoFEejBhEAAIA=");
+katakana['SA'] = image(58, 53, "AAcD/wDBg4DC//AgEB/+AgE/+AKBv/ggEP/gGBj/4DgP/DnU//4A34CQ+DAIcEDAIcDDAQDDDAYDCDAYDD/4cDIgJADAAUfIAQACh4jCAAUHD4QACJwIfBAAQtBEYgGBI4QUDFQkP/4qEVYQvEAAIxCEIK5CBwV/AwsfAwocCAwYcCJogcBNIp3F");
+katakana['SI'] = image(56, 52, "gFwAwt+Awv/8AGF/gFDgP//4GGCocDAwIVDBoX/wAHCn4VFg4GB4AxEAwsfAworBEQYABv4GFj4DCjgrCBQYRFn/4JQfAIgIGD+F/JQcD/gGBMARQCOwcH/wNBCoUP/0PAwIrBj/8OwQGBn4fBGIIGCAQIlB+BcBAQKvDBIQRB8AfBIQUH4AXBP4RXBGgJmERoJsFAwv//yaFbYghBQIYaCeAi9FPQTZGdxKFCFASECFAZPBEIgNCJQaZEAwhDDAwRJDTAYGEQAiQBPIgAGA");
+katakana['SU'] = image(60, 51, "gH/AAYGBh4GD/AOG4AOF/gONDo+ABxAACgY7CAAd/+AGEg4OG//gAwkP/wGEgJCCAAcfKIQzEIQIzEIQozOj4zFEgIzFn4kHGYv/M4okIGYt/IQqXBFghuBHYs/bAY6DCwrJECod/HgYVB8ZLEcoMfLQYECCwYVB+BTBCwT7CCwYrBAYIKCCoQDC8BXBEIQSBNoQVBBYP4EAIoCOQPHCoYTB/xdBIwQ8B+6SET4N/dYn/4aCFFgKRFgC+EgPghivEAoI");
+katakana['SE'] = image(57, 53, "gEH/wGEgf/AwkB/+AA4n/4AGEv/gAwk/+AGEj/4AwkP/g4JjA4EBQQ4D/4DD4E/AwIuBv/vAoP/FwILCAAIuBv4GEBgn//wFEAwITEh//CgfwAwMfCIRGB/4BB/5xBAgJTBIQQGBwP/75CBAwOAD4JCBAwRmDDIKYBOIQGDOIQGDOIQbBAwSqBAwiqBAwiqBDYg4Cv4GCHAUfAwQ4Cg4GCHAUBbwbjnHAgADcYYADUYQxEEYq6CVwbDBdQi6CZQYqBAAZcCAwY1BEYi5DAAQ8CegfgA=");
+katakana['SO'] = image(52, 52, "gGAAol8AYUD/Ef4AGCn/3/wFCg/+v/wAwV/8//Bgk//AMD8f/FoQMBj/8Bgfg//gBgcPFoYMBFocP/kHFof/4AtDBgMDFoYMBFoYMBgIIBgADBwAtDj4dBHQQMCFoYqCHQQqCFoc/BIIPCCwQtDKYIpBB4IwDIAQwCh45CBIVAFgSmDFIaaDOIYfCVgYfBRYYfCTASTCUoY1BQgZPCD4l/D4kfH4g4BH4YYBH4gFBGQd//4yDBYIyDn4SEJQIlEBgRXEHAg+BFYZRGZYQADBYgAG");
+katakana['TA'] = image(55, 56, "AAMHwAGEh/8Awkf/AGEv/gAwn/4AFDgf/EQkH/4oF/4ACAwM/AoX+FAQGCHIMBCYY5BEIIAC+AhFIAIhDHIQFDF4IhBJQMHF4JDDNIUfHIRpCv5sCn/wDQJsCDwIaBEIIKBwEf/9gOAQaB/gbBFAIPB+YsC/AaB54RBFAIaBAIOAEoJvBOgPh/+DNAJWB+//DQPBQIZyBM4f4LQSQC8EPKAIpBFAMPPgKKCgEcYIZwBiAGDbohwEZ4bdEFILxFf4ghBXwLjEDQhLBCYoaEE4IaDdIQaDBgLBCDIRQENYYTIewRkEAwJCFHYicBOIkAEAhDBS4IAJ");
+katakana['TI'] = image(57, 54, "AAkGAwsfwAGE//gAocP//wBgn//gEBgIFBAAIeBAof/wAYBAwkHAof+gEDAwf4E4YAB4AGBv4TDAAM/AwoxDKQhABLQwiCAAV/MIglBMIglBHwRwDNARbF//3Awv7Awv9Awv+Awv/MQQAD34GF74GFKAUHOIYABSAJxGaYp4Uv54FP40/P4oGHQwQGKKgt/AwrUEMIQGEVYIGLg4bMFII+Fv5TGNAsPQgsHTIoAG");
+katakana['TU'] = image(54, 53, "AAMBwAGEj4FEgf8AYPwgFgn/4BIP/g+Av/ggEP/n/gP/4EAv/v/wQBFQP/z/4CAMAg/+DAMfEIICBDAN/FgN/8YYBBAIaBw4hDDQIVBAYMAn/wDAIhCCwIhDCwIBBwAIBHAIYBEIQYDBAIuBwAjBFQghCJgQhEAIIhDEYQPBh5HBM4IhDQQQhCwYeBCwMBCoSPB/0CIQQhBAQKWDvytBCYTBDv5tBZYYTCAAQTCAAYTFHAITEj4TF/4TEh4TFv4TEg//JgIMDMYIMEO4ImD/53BAAM/AwIsEEAgFBEAZNBIIgTCFocfJwo6BPgpHEgZAEgEOAogAGA==");
+katakana['TE'] = image(57, 51, "h//AAfwg4GE/kDAwn+gIGE/8AAwuAv4GE4E/Awngj4GFNWJNF/gGF/5UF/+/AwvfAwvvAwv3Awv7GJn8IQV/4BJEv59Fn/wAwkf/DJFEAYABg/+AwjJBAxbQBwAGFH4gGBH4gGIIwgGNG4IGEg//LYjyBAwiyBAxc/EQoGGFIJTLdYJvEgF+fIsYAwo=");
+katakana['TO'] = image(42, 54, "//AAgU/+AECh/8AgUD/4U/CgYPDn//wAUC/4VCCgIlDAgIKCCgIKCCgP//wUD//gCgQKCn/zBQQ+BDYP8CgMBEAQBBj4KBKYIKC54yBBQP7KYIKCG4QKB35YBBQIUCGQPjNAUD+BXDnB9Dgy8/CicAA=");
+katakana['MA'] = image(57, 50, "/4AE/l/A4s/AwvfAwoAN/YGF/oxGHokf/wGLh4GN/4GSg4GChgGDwARBAw3gAwv4Awo7BAwn/4ACBAwIKB+AGDgJtBAwcAUgOPAwYLB94GDgaFCAwTBDAwcfAwoyBAwgyBAwgyCAwgcBAwgyBNgL0ENgIADn6oHDijhFW4wcB4AGDKwPwBwl/fwzUJDgZOFgAGGngGFhADCA");
+katakana['MI'] = image(52, 53, "gPwAwkf/wFDgf///gAwU/AwIVCBgX//AME//8gEHAoQGCBgYGCv4GDFIMPBggoE4A2CCoIuCAweAAwc/BghYBMwswNw0PNwkBGAIbEG4gMCOoYMCOoQMDAwRnE4BYDKYQTEKYRuCKYY8GgCjDAAV+LAtgcTMDbYhTCHobICBwbBDBghZDZwmAZoYGCAogGBCYgiBEIidCBwQ2DS4QMCVYT2CSAb2DBoLpFn72EdJAA==");
+katakana['MU'] = image(59, 54, "AAMDwAHFv/AAwkf/gVF/4VG8AGEh4VHFgoVPFdZBdRogVBgP4CokBFogVBn/wTIkHEwYrCv4ODCoMP/wVDFIP/JYQVCBwgVBGYLICCoTIDCoQCBBwQhCn5RCCoR/DNoZCDDIRRDCoQODg4+CIQYvGCoZCCCoZRDAQV//4SBRAM//4ABwEfAgQAB/ARBAAkPAwvxAwv+Dgv/8YGF/gkD/xCB543DH4P5AoaBBewsAvgGFhgGFAAQ=");
+katakana['ME'] = image(55, 54, "AAcB8AGEgf/AwkP/wGEj/8Awk/+AGEv4iF//AFAuAAwcHFAsPFA34AYNwFAQvBgICCFAUHCAIoDDwQoDn4DBKIf/MYIoCDwIGB/5RBAwWDKIYGB456Dv//75RDAwP/JQQmBAwJ6Dj4GBOYYGCOYcP/5zEg//OYgGNDYw3BAwgvBAwaABAwgaBOARZC/wGDOoP8MQI1D+AGDFwPAAwJaBDAQNCJIc/AQJsBTYL3COQc/4ATBXoYdCSgU8J4SNCmCNCNQqoDAwQuBAwgFDFAITEAwK1DAAKZEAAIMFAA4=");
+katakana['MO'] = image(55, 49, "j//AAfAv4GFAon/wIGFgYFE/0HAwn8h4GE/AvF8A4Bv4DCAAQzBAocB/+AAwYxBCYkH/wGEh/8MIv4Awk/+AGEGyJfFAFP9AwpOBNuikeAwxfEHoLpFNoZACAwZABIgIACJYYABIAYGCIAYwCHIoABA=");
+katakana['NA'] = image(57, 55, "AAV/8AGEn/wAwkf/AGEh/8AwkH/wGEgf/AwkB/+AA4n/4A4rGoIAE/IGF/wGF/9/Awu/AwvfAwvvAwv3AwpQCOOqqEWLV/H4pGGn5GFAw0fJosfJooGGn4GGKgq6BLQoGEg4GFh4GFPoIpEDYIwFv5MFLQ4GFg6EFgaZFAAw");
+katakana['NI'] = image(56, 43, "h//AAf4A25+/AH4AuWggA5A=");
+katakana['NU'] = image(55, 51, "g//AAcAh4GFj4FD/0An4GD/kAv4GD/EADQnwgIGE8EDAwnAAwuAIIgvBAAcPF4IADn4vBAAd/8AGEFAIDBAQIsBFAMDCAIoDh4eBj4oCj4GBFAd/CIJRBgBZCAQIlD/+HQIIGD54oCNwZKDPQZPDOYRdDOYqmBOYi0BOYjCBBogGGYQSAEAwimDGATdDAwQTBH4JFBLIP8AwYTB+AqBAwITB4AGBE4bADBIJyBUIJ6CVgXgJAQzBg+BAoJkCgxcBCYRIEPArlEH4YGDO4ibBeQs+AokAsAGF");
+katakana['NE'] = image(61, 55, "AAX/4AGEg/+Bws/+AGEgP/wAHEh/8Cwt/8AGEgf/Bwsf/AMEAAYnBj4GDHwQOEDAMHA4hVBn4WFJIIADHwMPA4hgCAwZkFCQKCGBwpHBPQwOFFAJyGBwt/BwozBBwpwDGYiYEEgP+iAkF4IPDCoP8j7WCUAXhbwYVB/4RBU4n4QISfD54vBS4f+FASPD+AEB+AFB/IjBFIPnA4LzCGAfAeYIjBGAP4eYQCBwZuBeYUH/EfIwJRCAoIDBg6ACnCmDR4oqBDIKfEHgKuFS4g5CBwo8CWwqOCAAQ8DcYg8Vn48FAAo=");
+katakana['NO'] = image(47, 52, "AAcHAokP/gFDj/4Aod/+AFD//gAgUB//AAoUD/4oE/woJn4oLEQYoBwAoIh4oEj4oFJZ8HERU/EQhFEDgIiDH4JFDh4iEH4t/NAYcFHII/Dj4cEv4/DCwIcDCwIcDCwI5DCwhEBHIYQBKwf/GYYhBCwc/FoYKBFoYEBFoQKCE4RrBE4YFCHwQyBHAYnBJ4YFBcBN/AgcAPgYABA=");
+katakana['HA'] = image(62, 52, "AAP/wEH/gGCgf/gE/+AHCh4MB//AA4QMBCIQeD4ARCDwv4Dwt/8AeEgI4BDwkH/weFj4eEAgIeF8AeEAgQeEAgQeEAgQeEAgQeGMggeCMggeCQYiACQYYbCDwgbCIogbCIoZZDIoYTCMggTCEwn/CYJFDBYZFDBYYmDv4LBEwYDDg4aCh5JCDQYiDaIQWBNAQ5CMAYLDcgYmCCwgqCGIYTBFwL7EJIIWEAgPgh4WDNAPACwgMBCwiHB/wWEFwV/CwZVB/YWEDgPHXgYuBDwLbDKQPwh60CGwWAngGDgAFBkAHEsAFEAAQA==");
+katakana['HI'] = image(47, 51, "//AAgUB/+AAoUD/4QDg/+AocP/gFDj/4Aoc/+AFDv/gFw8BwIuDj+DFwf/FwcP/4uD///FwQKB/wuBJwIFBFwM/AoP8//PAgP/+IDCAAJdBAAXwg4FDEoQKCIIIgCLoQFBKYV//5qDB4aMuF1YFDFwIRDUIQAC+YFE8YFE44FEw4FEUgn+Aon8WwhKBXggA=");
+katakana['HU'] = image(49, 50, "/4AEv4FE34FE74FE94FE+4FE/YFE/oFE/w0Dg//AocD/+AAoUB//AI4ngAod/+AFDn4FEj/4Aon8AocPAokHHgg2BHhYFDHgJCLJBZCEAopIFAoxIEAoxOEApc/AojSBbwplEAoZxBAocPAojICBQhBCGYIFDBYRZCa4P/NYQuCPoYFBSoZGFZYsPAgYABA=");
+katakana['HE'] = image(61, 43, "AAMH8AHF/4HFh//wAOF/wOG/AHEv4eFg//DwoOBDwgOCDwk//YeEgf/x4eEn/8n4eDgP/4AeEj/8DAIeCBwPgLgkfDYIeECYQeDh4LBIwIeC//wDIIeCBYJdCDwV/BwIwBDwIOBCQYeBn4pCDwRIBIAQeCMIJPD/AOB4CED4BhBMwf/MISbD/kHPovwj4ODDwV/UYhYBKQJ2DRoIGDHQINEcARCCWYgGEDwIOFgb+FDwL2EDwQGFIQoeCBw0YA40AA==");
+katakana['HO'] = image(61, 54, "AAV/8AGEgf/Bwsf/AHF//AAwkH/wOFn/wAwkB/+AA4kP/g8Rg//AAngv4HFCYIAE/EfA4vAAwv+Eo3wn4HFwAGFJwZ5UgfAPIJzDn/x/+PEgR/BAoJzDP4N/8JzD//D/6KDFYI8BCwYrCCAItBPQOH/wWDCgIQBCwf/4P/wIWCCQIBDWgYBCZ4KJBE4LPDEYInBh5sBBgKLBNgQ0CJoIWB4ACCBgIiBBwP8EYU/TQLXBHQQECFAI8BCwIqB8DzCDYMPAgQbCMoI3BF4IRB44OBWwQUBv4TBJIV//InBHgQCBw4OBHgUH/EfNgKOCj0A3BsCQwNgeaSdCABA=");
+katakana['N'] = image(54, 50, "ggGFngFEgP+AwkPAws/AwkB/4GEh4GFn4Gaj///gNF/AGF4BEJAwITBgOAAwQTBh4GCnwJCCgVwLgRwMHAgTBHAgTGv4TEgYTFMIITEMAsHMBY0B+ClFCYiPFEAITEv//OIQMCTg3gBgggEDIIgDGYIgDMIJVDDAIABIIILCFoYYCJwZ0BHQgsBBgZnBBggnCKgYhBMIi3FgAFFgAA==");
+katakana['WA'] = image(51, 50, "/4Ay4A3E/AFCh4GBAoUBAoPgAwU///8AoUHBgOAD4nwAoUf//+AoUDGRYSBGQYSCGQd/94yDh/9GQZFB34yDn/zGQcPAgYSCG4YSBC4YSNv4SKJYJwDLwISEn5QDS4QSDDAJjDDAJ2DGIJ2DUYQ+DQYKcFFYYXBDASOCGIQFDGIQRCDwTaCG4YFBEgbHHN4hiFg6HEA=");
+katakana['WO'] = image(50, 52, "/4AE34FE94FE/YFE/wYYGocB/+AAwd/8AFDn/4AocP/gFDgf/KovADAnwDB43B45EE+IFE/F/KAkfBgmHAonhAonwDAn8h4MEN5X/N4l/N4k/KwkfRwgoBDwcHOohoBOoYFBEgY2BEgYFBEgYFBJIYXBFQYpBFQZ3CAoIWCKoQQCGwQLDHgR8CAoQdCAoQvCOYYFFn5gENgKREbYgAGA");
+katakana['RA'] = image(51, 50, "n//AAcHAongAon8j4GEwYFE+F/Aof+h4ME4IFE/BYr+4FE/wFE//fAon7BgpYE//vAon9CQo3Ev/gAocP/gFDgP/wASX+ASJgYSFXwJ2ECQivBDAoSEWIs//wFDbYIrDAoI+DAoIYDQ4IYCFIIABDALlDGIJhBewS/EJQQYCG4YkED4QFDD4JJF4AFDA");
+katakana['RI'] = image(43, 53, "AAf/7/4AgMf/f/AgMD/9/8AFBv/v/gEBh/9/+AgEB/+/+AKBn/3/wEBg/+//AFX4q3v4qDh/8FQQPBz4PDAYQvBEYQvCEYI/CGYRPBB4cfIYQpBB4cH/5TCDwJjD/4kCn4EBCgN/AgIUBDoP/FIJHBAAIyCDIYjBIYYaBQ4QaBJoZHDAAoA=");
+katakana['RU'] = image(61, 53, "AAUH/wHFn/wAgUB/+B/+AA4UP/gBBCgd/8ABBAwUD/4BBBwcf/ABBA4f/4ABBHQg8FHQI8/HksYHgwYBHgkPF4I8EvwlCHwOAg4gBEYI8CCIQjBHgITBCIP+HgU/CwIRBDAIgB4AMCAgMfEAIMBDAIOCBgQYCIwQMCPYJTBAQI8BBwUHEoN/8P/IYN/+AvBj4LBBwOAj/7BwZGB/4ABBwXAAQIODM4QOFHgIOC/4OBh4OCAYJGBv4OCn4OBHgJKBAYJkBIQISBaIYhCCwIOBSoTqBJQISBeYUHd4U+bYUwcAYAKA");
+katakana['RE'] = image(51, 51, "//AAocf/AFDgf/CQl/8AFDh/8AocB/+AAwc/+AFDg/+GX4ECgwyEgPgGQk+GQkP+IyDC4IyE//3GQc//gyDh//GQYYB8YyD//4GQc//wyDDAOBGQUH//gGQRvB/BlD/4DBGQU/CwIyCj4YBMoQkBBIIyBBAIYBGQIkBDAIDBGgIiD+AFBGoIyBv4eCGQIABJwQvBAAJnDEgTLCEgY8CIYLLDEgZVCAoZuBb4iaBfAj+EgE4AokAA");
+katakana['RO'] = image(50, 47, "/4AEn4FE94FE/YFE/wYF34YS4A1BgIYB+A8Cv/v/gFCj4YBAoUHDH4Y/DEbglDBQ8CAAYA==");
+katakana['YU'] = image(59, 46, "gP/AAX+A4M/A4fggEHAwf8BwIGD/4GBj4VFgYVGv4HDwEAh4GD+A+Eg46CAAf/4AGEj/4Coo6CCqJFBCot/KAIADh5QCQAhQBCrM/Myk/M3JQGh5QFMyIRBAH6NB");
+katakana['YO'] = image(50, 49, "v//AAefAonnAon5Aon+DDA1DgP/wA8E8AFDj/4AocHDFZjfDCJjxDD5WE/+/AonvAon7PgoYX/g3DAAQ");
+hiragana['A'] = image(52, 50, "gEB/wGEn/AAocD/gMcg//AAfgv4FD/wMYFIRNa54HDgYyCBgYsEBgX/+AGBHQYpBCQQaCh4JBJQPwgIdBBAP/wASB4H/j/8MIP8j5fBBIP/4P8gf+j/7/hVBj/jA4PH/C/Bn4RBv8Aj/3/Ef55FB/9/wI+D+/wj40BHwIWBL4QJB+BFBwAmB/4MBD4M/94MBD4JAB/4cBNYN/BgM//AsB/n/z4bBQgOHX4QVB/B3B/CQCAQTSC8BFCB4Q4CB4UAgIIBRQOAXojREn/gaIgAC");
+hiragana['I'] = image(58, 50, "v/gAgUggEf/AGCnkAg/+AwU/gEB/+AAwQZBDgcP/gcECQIcFCQIJCCol/4AGBgYLBj/wCokHCAIABFAIQCCon/DgQECn4cDCoItCAAI+BDggVCLoZeB+BgCCocPPQZUBwZdDJAQcEGAIcEGAIcEGQPDDghIBDggyBDggyBx4cBjxIC8aaCCAIyBLAMDM4IyBSARnC//HUIk/+IyBCASdBLAJKCGQOf/kDJQV/GQRKCJ4XgEYRPC/CoCDgOHNwl/8P/84jCDgM//5HCDgMHAwIjBgP8DwIsBQgYVBSQgVBaYZnCTIgtBbQhDCUAYkCfwYOCGIgAHA");
+hiragana['U'] = image(46, 50, "h//Aoc////8AFBAgIABgEDAofACwIAB/wWD//4CwgdBCIeAFQUfCwIADCwIAMj//+AEBv4tDAgQLBHAYFBAgf/8YFE54FECwRTB/wkCAoP7IAd/OgR2CKwcBQ4kH/hMEJYQcC4AWIh4WEn4tJg6EEj6EEVgIQDE4l/CAbABCAZqBBQgQDBQIQCXwIyCYYTIFeIhlCBQjxCLIQWBMgbdFvzYJ");
+hiragana['E'] = image(55, 50, "gF//4GE/4AB+AFBgIGC/+AgEDAwYNBg4FC/wGBh4GC/gGF/ArFFIQAD4BRVn42FLAIGEJQYGBLAhEBLAhEBLAf/8ArDBIIyEj5fCRYZYEEgJYEN4JNFDQouFDQKcBFwYGFMIIGDLQRJFAwgaBOYQuC8Y2DFwODAwcP/0HXAc//EPcQnAj5LCPAU/MwR4Cv5ECPAQ9CLoUBd4auE/guBVwf5PARaC+5qCAwXnJwSXB//HI4QGCw5ACAwUHNIn+gj/HAAg");
+hiragana['O'] = image(54, 50, "gEB/0AggGCg/4gE8AwUf8EA/gGCv+AB4QaDv/wDQn/CwIaCgP/4AaDgf/wAaCgPn/4PBAAXv/0HAwef/kfAoX+n/4v4GCAgPxCYfg/4jBAAWBGwQ1BgEDJoJQCJoJRBLYcPCAJrCgEcKAaGEHgSGDF4QPCJYYxCHoYMBn5YDBgoGBDIP8FQKiBDwabBFoIzCv/gEAJQCMwWfKAIbBh58BDQMH/l/4IaCh/xTgIaCn/P/BrD/8/4CGD/i3BDQfz/gaDv/P+AaCCAIaEHQQaDv/hGoV4h//g4VB8JnBa4ePZYRkBBwKNCbwPwCYR/C44CB4BtBfgSaD8ACBYQQWBAAYA==");
+hiragana['KA'] = image(55, 49, "gEH/AGEh/wAwkf8AGEn/AAwl/wEAhgGC/4CBngCBgP+AQP8AwMDAYIyDAYUPAwQ2CAwY2Cj/4gP/AAP4j/wgYGC/gGBg4GC/0/8EPAwsfCgd/4E/Awt/FIf/LgJmBE4IGCMwMf8JjBHwIPB4IDBgZmBv+DAYMHMwP/BQRfBOwIKCL4J2BOIQvBAgJxCGQIEBHAKPCCwIYDCwQBBQoRGBviIDIQJRC4AdCXAYdCKIcHboQ/CboY4BboghBboZKCFAYhBjAoDh/8nzME+CfBF4V/RgP/EgKVBwYGBFAMH/zIBFAQeBAwIoDboRRD4DrBJQUHAQJsDAAwA=");
+hiragana['KI'] = image(48, 50, "AAMB+AFDh4FL/AFDg4FIn//AAX4ArpHC/xNEAov/LQgFCDgYAlF4UfPx8/g/8CoQbBKgQhCAoMDFAkHAoeAh4FEDgQAB4E/FgIUBwE/HwQdBn/gAoM+AoPAAoMMAohFCAqIpCgI7C4BEBI4oICAoZfE4C9BAob2EAoISCaQgACA=");
+hiragana['KU'] = image(33, 45, "AAsB4ADC+ADC/wDBgf/wADMg//CYIDDh4DDD4UfAY/8AY34AZRDCh4DCg4DCgYbCgI/CgH/BgU/BgREBBgIQB8AMCFIRNDLoJ2Cv42DJwQdDFQIdDFQQdDFQIdDHYRkDgYhCgADDnwDChyzE");
+hiragana['KE'] = image(50, 49, "AAUB/0Ag/gAwN/wAICgEfBIIIBB4P4BAYPCh/wDAcD/gYE/4FBDAU/4AYEGIgOCDAQOBh//AAP+v+DAoX/7/AAof3+E/AoX9/gYD/9/gYFD/4YE/5QCGIJQDHYRvCJQU/N4JKCKAYYCKAQYWmAYEjwYEx6lDh/zUocDMgIYDv6cBKgUf/4yBBAMH/4eC4EBNQUfAQN/DYMPE4TjCAQQkCYgSJBDYLEBn7QCAQIbCE4UDDYP/PIV/CgLpD4EPP4UH+AkBAoIACCgIADh6LCAAMDAoYA==");
+hiragana['KO'] = image(52, 50, "h//AAX+gAFD//gBgn/BgvwBiWAAon4GwUBDIQACCQQFCn//4AFCg4lBCQc/DwYfBKQJdEDwYAB8CIihAFEgJJDIgQFEg5KEMgITEj/8D4hwED4JqEOIIfEv5eEg4fEFg0PHIwsEBigmFCYkOv65CJYPnbgn+ZgIAD8IMFewvgCYjRBE4IMDegQABIoUfAoK7HA==");
+hiragana['SA'] = image(51, 50, "AAMB/gFE/+AAwcf+AFDgf+DIl/4AFDg4fEgAfLgIfCj//AFQzCn/gLJYMELI5mEh6GGBgUHGAP4CAQ3COYILCBgUDIgYZBAoYmBn5REDwPgQQPgDAIVBj4fBJ4d+CQI1CgeAXhgSDKoYSEQQp1GQQpFBawXwD4IGBg42BaQngBgRlDBgmABgjzBRYZDCPIYvCv//MQoACA==");
+hiragana['SI'] = image(45, 50, "v/AAgUD/wKDj/wAof/wAECg/8BQc/8AbD/4bE/AbEFgcHFgk/FgcBFgkPDYhIgFgIKDFh8eFgn+FgcH/4sDv+/FgUD/osDn/vFgQ2BFgcf+YsD/+fFgUP/gsDv/HFgSKBLId/8IsCHgIXBSod/EIIKBwIhCv/4h4WBAQOAv/+IIP8AQIAC4AYBAAIkBn4KDJQIKDCwYpBCwRWCAoJhDAoK1DAAg=");
+hiragana['SU'] = image(52, 50, "AAUf8AFDgP+BjH/AYP/AAnvAon+BjJAUgf9BgZFB/4MDn4kEg4MFGIwMED4QME+E/+AyC/x0DFgPABwIMC/gMGDIn8gYMFv/4EwcP/+AKYf/BgRACBgYRB/4mCgF/AwJ6DBgoTCRohNDTZE/VAkP/gFDE4PAUQhGCI4YeEUIgYBD4gMBEpI4GgIFEAAo");
+hiragana['SE'] = image(56, 50, "AAcP/ADB//AAwP8AwkHA34FBAAn+A1JalmAGFvinFv4GF//PXghEBAwfBAwoNGEQP/+AGDn4GFh//8AGDg5PCgF/AYP/wAGEgj/CAwQADAw4mCAwZCCAAQ8BFQgGBAAQGBj4GFJQIGEJQIGEgYGFGIIGCIQQVDHQgACA");
+hiragana['SO'] = image(53, 50, "gP/AAXggEPAweAgF/AoX+gEDBgfwgEfCYoFD/EAg4MFAAQMCAAQwBBhQpBJQozBAAU/IAIACIYJUBAAV//gsJD4IsEn4sEOAn+NIn/+4FEAA39AwvvAwqQDAAP7UYhmCx5bDuBVB4BCDg5bEJ4JoEgJ1EEQKCESwIFEg5vEEA4TFh4TFv4TGYgiLBCYrFG/5dDd4YHCOQKkBDQjbDDQQwDWgR5DAwSGEEAgAEA==");
+hiragana['TA'] = image(52, 50, "gEP+AGE/4Mjgf/AAXAgE/AoX8BjUAgP+GYkf8AFDBhHnEIQMBEQQhBn/jFAWAgYMD/AMH/gMF4f/F4UH/kQGYd/KIIACg4VBBgmAQ4gMFUJcB/8DDQZgBv6iD/wuEn/gKIJGDEIl/4KCDC4KPE/+BBgYXBBgY5BAIImCj4MBTIKFB/wMBAAKSB8EPAwXnUYIMDCwLYD95RBEAIZCFQN/AwPBKISpBwEGQAgAGA==");
+hiragana['TI'] = image(51, 49, "gED/wGEv/AAocP/AFDgP/CQk/8AFDg/8Bgn/wAFDj/wBQYAqJ4M/LBZrMJYZ+Ch5aDv/f/4bCBQIABCoMDHAYTBv4+Ej4MEg4DB4IMCAoIcCwE/TwU/+ASBEQI8BVQJLCv/gS4cP/kBMgYWBjyoEgLbJEYYSCQQkHCQg2EHASCEv4SBgYOBOQ70BQoYrBEQIABFYR/DJASRED4YFCBgJDDA=");
+hiragana['TU'] = image(59, 45, "AAUP/4FFAAIGCAoX//EAg4GD//ACYYAB/kBAwgOBn4OFDgoOBAYX+BYP8j4GBwEAAgPDGwQ+C/F/BgIABCwOMLQl/+AGEg/+NIv/8BwF/gGEKwIqDAAM/HAYzDEhkfEgsDEgxJGh5JFHQPACqQrBCpkfCopXBCogcBCog5BK4jSCAwxtDDYK8EZIQcCAoQcDCYTjCJgQGCEYT0DIAYGGEgQGDEgRcEv5UEA=");
+hiragana['TE'] = image(57, 50, "/4AFv4GF34GF74GF94GF+4GF/YGF/oGF/w7Cn//4BCDAwOAAwpQEj4ZDAxP8AyUPAwwiFg4GMgZFFAw0BLQqlBNAkAv4GG8AGEn/wKgv4KhZGGHALeGH4oxNh4xFOJBjGEYt/VQwVFg//BwhOBAAI7Dv4GBHYYcBCwgcB/5CEDgQyFGYgrCUwkPKAwAC");
+hiragana['TO'] = image(46, 49, "gEH/AFDj/wAod/4AECgP/Cwn8C0cICwcDBoIWC/4NBCwMfEgV/4f/BoIWBv//LAMH/4AB8AWBAoWAgE/BQYlBDYUAh4FBHwQPEEIJQDFYJhCgYwCLQQqCDYQKDDYIKDn5xEEAYQB/x8JDYkDCAkPYIk/JoQWTAol/AocZQwR6B8aNCAAOPAgf+TIZqBAongT4QfCBYY9BW4R1BA=");
+hiragana['NA'] = image(55, 50, "AAd/wEAn4CBgH/BIXAgEB/wJEgf8AQIJCg/4AQIJBgEP+ACBBIMAj/gAQYsBEoIoCGwf/GwkB/8P/4AC4f+j4GDw/4n4GDj/wv4FC/0/8AMD/l/4IGD/H/wYGD+P/g4vELARtCMQRtDMQQKDL4YKCMQQKDMQQKDR4QKCTIYKCFYQ2bOoI2C4BgCGwWASAQ2BGQKJC8DNBBAIAB+DNBPYf4ZoKrDAgPwT4K7BAwRdBB4K3BVYIqCVYY6BAwKrB/0DVY3+v/hAwf8n4SBdIXwnxEBAwXgnBEBAwShBO4IbBSYSVCOYQAHA");
+hiragana['NI'] = image(57, 50, "AAMPwAGE//gAocf//wgFwgEH////kH/AZBAwP+gf+Bof/wP/gEDAwWAAIMBAwc/FgIGDj4sBv4GBE4P8HAIdBE4IqBAwYgBKAIGCKAYKBAwN/EYIGDn4jBAwZfBDAQfBLIPAAwZZBDgItENYN/CAIfBIAIGCLIRfDLIXwAwc/RQJmCHAPv/0PEoI4B+f/AwcH/P/w50D/l/wZ0CgP+j/BK4Q4Bg/gJoQ4BwIGBIwU/4EwAQI4CIYICCAYY/EJQMHHATcCbAQKEHARGBGgQqBCIc/D4IGDaITCDT4PAAQJfCQQRYDeQQGDSIIGEYYIGEE4IGEDgYFCcAQ+CGQZsCABAA=");
+hiragana['NU'] = image(58, 50, "gEP/AGEgf//wHE/4ABAwc/AwIPDh4OC8AGBg4GCEwUBAwX8Dod/EgoHC4AsF+BJFjAGDg4iEFgRfF/+AAwk/IwQjDFIgjDvAjDMYJlCgRHB4ABBFIUf/ABBFIXH/0HCoUf+BcBLwQpBCogpBCYIVDv+ACohNBn/wCoRxBCohNCMoIVBOIQVBAIJNCCAIVCEYIQBCoOAb4QtDCAQtC/gjCdIIXCN4QwBC4SVBDQIXBEYUP/gXBI4QEBHwPD/8ODgR/CwZNCCYN/8P/5/4GQOf+DtBKgXv/jtBKgX5/0PAwJxB/0/DAL8CvkDJYP/IYMMgFgg//fot/VYQACgYGFAAoA==");
+hiragana['NE'] = image(67, 45, "AAXwA43/4AHFn/8A4sPCA0B//+CAt///gA4kfCA0H/4QGA4IyFn4IBGQg5BIYsD//nCAt//F/CAkf/wzBCAYFBwH//BaE8ArBwBzFCAgNBLoQQCHIPADYIQD/6dBCAk/OQIQEHIQQEHIQkCCARaBO4YUCSYQQDHIQQFHIQQERQgQCLQQQEHIKBDCAPAn5fDCAP8gbNECAaJDCAbVECAPgvj+Gg72GdoqYFCAgHFKIoQDDA0AKIjODDA0ARYQAEhwHGAAIA==");
+hiragana['NO'] = image(54, 50, "h4GFn+AAocB/0IAwcH/F//4AB+Ef8IFC//A/+PAwcD/0fAoX8h/wDQk/4ITDAgMDAwcH/hGC/EAj/wIwXggF/4AGB/+AJIIFBGQJJCDQoWBDQf/wZlBDQIWBh41Dx5kE/0/Mgn4IgIGD8f8MgYaBL4IaEPQJrD/6RCGoRkCKAR/BKAgaBKAoaFNYoWCKIIaC8BKCDQWAIYQaCgJCCDQRyDDQRXDEoOBK4ahBW4K+CAgKcBDgLcBMwIwC/1/4JHBCYP5CoQwC4aND/atBRofDAgPgdQaSBHgX4hxXBHQXAhAOBAwKXCAAJlBbIIAH");
+hiragana['HA'] = image(50, 50, "AAMH/gFDgP/Bgl/4AFDj/wDBsH/4AD/oFE/9/AwoARJVXhAon4JQn+j4MEw4YLn4YEJTIfCAooYCAoX4DgQwCwBdEBgMDHoYMB//3Bgd/8AUC4A7BJQP//kHBwQGB4JYBFoX8KgMP/gGBz/+h//AIPjGAXA//wAoXwh/4DgX4gP8IgQnCF4QFBgOAEIKIEv6SCAA4A==");
+hiragana['HI'] = image(59, 50, "gP/AAOAA4U/AwPwAwUHAwP+CwYVC4AGCj4GB/AGCgYOCCod/AwPgGokH/g8GHQY8CHQYVCHQg8CwEfCAYEBgYQDAgV/JYYEBh5LDj/4GoJKEGoJLCAwP4JYZ9C/BLCNwSGDQgSGDOoaGDAwg6BEYQHDh//EomDAIP+ToaQBEIIvCKoJyCJgPH/yDCEIIVB4BNBMwIgB+CZCn/n4f+h5jBAQMw/+BOgKyCCoN/PIICBS4I0BCoQJBJQJqCBIP5NQfgD4KACn5tDGQSDEwADBTIJaBGQKZEDISvCToR8BeAQDBAQLbCb4RSCAAcHcQYACvwGFg45BAAj/DAAw=");
+hiragana['HU'] = image(55, 50, "gED/gGEg/4AwkP+EAhwGCj/ggF+AwU/4EB/wGCv+Ag4GD/4kBAwM//4AB84GBv4GC54GBAoX/x/+gIGDh/+gYFC/0P/kHAwX8AwMPAwX4j5cCGwJOBAwJIDj5jBv4QCAwIpBNoU/+AiBNoIGCJYJtBAwPhFwPANQXjAwOAgEEv+P/A2C/H+CoI2BTIIhBwY2Bh/xwH+UgUf+CwBUgSgBBYKkCn/gh/gToI1B4Ef4AvCBIM/4ZmCIAN/44oBSgKdCFAJ3CLAY0BUgQoBGgIGBEIUPAwSID+AGBQIZHBJQRECd4Q9DI4QvBJwQ2Cj4sBGATRBJwLcDFgTcDC4QGEEILqEAwIbDIARoCBgQAGA=");
+hiragana['HE'] = image(55, 50, "AAUf+AGEn/gAwl/4AECBQP/wAYC4EB/4YDwED/wYDwEH/gGCCIMP/AFBgIRBGwcDCIN/GwUH/EP/4bCDAP/AAI2C+4GCHwMfAoX/JgM/AwYjBv4GI8YGCFoN/wIGBgYCBFwIiBHYJfBNAPAn/8IwIGBwAaBh/wAwOD//4R4IfBg//+B2BDoJKB+AoBg/+JQPjOwMP/n/z/nQIMf/IOB76BBn/3/gVBMgN/94nBOQX/7/gAwKbBOwSOCHoJMCEIMH/v/CAJxBh/7/hcCF4X4KYLEC5/wj5KBEIOfGwJRCL4PzF4V/JIQvBCYJJCH4JxB4AGB/xCCFQIJDDoIMBBIRNBAQJdCIwKUCeAb5CPgQACSgIFDSgIFEAAg=");
+hiragana['HO'] = image(51, 50, "AAN+AokP+AFDgf+Bgl/4ASE/ASVv//AAX8h4FD/+BAonwn4FD/0HBgnAAogoBgP/HAk/8AFDg5LEgASM/gSFwADBFQIAC8E4Iof+/5FE5/wAof5/0fAwc/8YFD8f8PAYEB54MDJ4SRDJ4KRDj/gNYaoCLAYWBLAYWCLAQWCDYJvDgYSCCwV/NYQWBGQc/+AyDg4yBj4MBgYSBAQP4OwPwbIglBQAgpBBgZiBBgYYBBgY1CU4S0DFoIRCAAo=");
+hiragana['MA'] = image(55, 49, "gEP+AGEj/gAwk/4EAkAGCv+AgAPD/8AgYdCgP+EgkD/gdB/AGBg4DBv4GCj/w/wGCv////8AwQFB//4AwMBAwXwEQMDAwXgAwMHAwXAAwMPAwWAG4QvBLgQGBL4X/AwRfBKgIGCL4X8n/gLARUBn5YDMwM8NQaLBQYIoCAQSIDAQRZBRYaBDRYQhBFAIJCKIYyCDwKoBToZkBOAIJBPYKLCGwMH/h2CAwMfKoKKCI4PgSIYYB4afDJQMP/gpB+AhBMgIjB/AhC4EfAwIhCEoIGCwJdBaIIZBMgSkCjhMBgakBG4LICUgKDBAwQuBPgRKCjgGE4EQAwgEBAAIbBRAQACQgIDB");
+hiragana['MI'] = image(50, 50, "h+AAocD/gFDgP/CQl/4AFDn/gv//AAOP/E/AoXj/0HAoX4/+BAoX+DAuf+EfAoXn/gYD/P/gYEBG48f+AFDg5QMMYkf8BvE/BvE/wYE/4YEKAIYYgZSCDAMBJgQYCCgYDBFoYDBj4tCDAJlDDAMBGYYYBNYYYBn4xCg/4h6ECPgIHBPgfBDwaVBQgYvBToYYCFYauBaIIwB5/wcAfz/0PAoX8cAn/IgQFC55dBAoXxFILtC/grBGgL5BYIoAGA==");
+hiragana['MU'] = image(58, 50, "AAV/4AGEj/wAwkH/gGEgP/Aod+Dgv/wAcEj/gDgkH/AcEgP+Dgt/Dg3wn4mBHwYGBDAIyCAwP/8AGBAoQODh4GC/4sBgYGD/AcCAAO/IQQcC4IkCDgI7Bj5YBg//w/8EAIjCwIEBv/gMQPgLAMPFYP//h1BgZpC/4LCNwIxB4YoBFoIxB/AjBNIMH/v+n5UB/4qBn/fIoIJBv+PLYUPQwPhOIUD/gvBGYMH/3/BAX/457CBAP/84GBDgIlB/YGBCYJwB/qECDgKREwBCC34YBDgfvLYP+HIM/+YYCIwM/MoIYB/hGBMoQEBz4nBKQfDAwODGQXwKQQMB/P4j4GBAQP+ngtBUgIRBg6aBRwKiBwOAf4TNBAobjCAogAEA");
+hiragana['ME'] = image(57, 50, "gEP+AGEg/4AwkD/gGEgP+Dgv/Awt/wAGEn/Agf/BIUf8EP/40CHAMf/4tBAYP4AQImBCIP8n4GB4EH//+AwXgEwP/v4CB/EBAYIPBg4jBAwX8BYJFBCQRKDFYIGBJQJxBIgUfAQIrBAYMPCAIfBBQR8CAwR8DMAZ8Cv4GCGIQGDGIU/AwR8BAwKqCWoU/FoS1Cj4tCHASEBWogGBUAQKBAwItBHARpB8BlBBQKuCAQIKBO4SqCBQX8AwX4h/9/wGC/kP/n/DYSlCv+P/ArB4K+B4/4SIV+j/jWIX8n0P+JSBDoMOMwJWBAwOCMwM//ZOCMwI4C75nB/5bC45nBv+DAwPhTgXAb4PAoCfCQQifBYoYAHA");
+hiragana['MO'] = image(60, 50, "AAX//4GEv4HFj4GB/wGCg4GB//4AwMBAwX/4AcEDwcPAwYWBgYGDCwQVC54tCCoX8F4PgFYP4CYI+BgE//0P/gaB/ARB4F/4ApBwAVBg4OBj/8EgITB4AiB4InBBwQgBCAIOCPQPjD4MPJ4MH/0/+ALBwARB84kBBwQ0Bv/gBwc/+5bBj5tEHAR8Bn5lBBwInBBxY2CBwcDWIQOEGwIODJwIOFIoRKC4CNCBQP3AgKwCDIIOBKIQKB8/8IQJgBj4OB8E/MAfD/ytBEgX8J4KeBZwWDIgJCBCoP4ZgIzCAYIqBeYRQB8DnCK4gGBGoIDBwAyBF4IKCCQWBAwIVBEoPgF4RFBg/4F4Q2BAAQOBTwIADHoQADbIQAIA");
+hiragana['YA'] = image(54, 50, "gEf+AGEv/AAocB/4MEg/8DUv///Aj//wEDAwIcBAwMP//8BgIGBn//+IFBAwICB54GCDQQAC/0HAgXAn45BD4IDBn45Bv4MBAYPgGYJKCFAIbB8EAgf+DQRbEv/4LYYaBOQU/4EPCwIhCCYJrCgf8CYkP+BlBCYQaBv6GDOwQaECYIaEKwIaD4JWDgP+CYIaCg/4NQYTB8Z+BFwef+4aCMgN/74aCn/z/zXCIAOH/IaCh5CB44aBJoU+a4QyBwFwDQLGBCAOBX4adBGIJMBRIQaBUYI4CDQJnDFYJ7EDQKzCDQYECAA4");
+hiragana['YU'] = image(52, 49, "AAMf+AFDgP+Bgk/8AFDgYMM/gkD/4AC+EBAof/BkA5FhEAg45Cg/AgF/AQMBBIMP/4DB//gE4Xwn5dBn4GB74IBgY0Fv4FD8AfBAoYfB/gbBIAIiBg///A7B/+A/4rBCQIxBBAISB/ghBCQeBEoIMBCQI0BBgQSCDIYSB54MBgIlB+AMCj0H/0PBgIABHQQMBOgP4BgZBBBwTDCMYIMDKIIMRWQQmDAwUMYYqyBAoaxBN4IMEV4QMCcggMBWwbZCAweA");
+hiragana['YO'] = image(55, 50, "AAMHAwsP+AGEn/gAwl/4AFDgP/BgkD/whF/AGEj4oFEIsA/+AEIgoFg/8EIooFJQ3/JRcHJSgoGJQxEEg//FIkfAws/Cgv/AwUGJQX/HwMP8AoB74GBj/gh/+IoU/4BzBBQJBCJQIKBNQRzBv+AWoIIDJAP4SoMBIgIkBOYMDHoKTBAIIRBXgQBBB4IfBEIQYBFALgCCwMP/iVCJAXwJ4QfDcAX/4JRBSoRvBEIZ2DcAQGCFQIhBPoIYBcAQGBDAJqBCgQ6Bg7rIAAY=");
+hiragana['RA'] = image(48, 50, "gEP4AFDj//wAFE/gFE/4TCn4FBBgQFCBgQRC//gBgN/BYUP/EBAog3BGIIFCgH/BAIFCh4FEgQFEBoXwAqsfAoIuBAoROBEwIFBIwP+AoPnLIWALwZfBNQf/+AFE/AFBEIM/AoR6Bh/8OoIzBg4FBRgQFCL4UD/wlBAoikCAoM/W4QFBj5dCAoMGAohpDg4FEHYJ1EAog5DDgJWCb4Y/Cg7RDaARFCAoZFBAobiEeoruCAoQtCAoI+DAAgA=");
+hiragana['RI'] = image(40, 49, "ngEDn/AAg9/4Ef/AEBwF//4EBwP//4HBw4EB4F/x4EB8F/z4EB+H/n4EDAQIjBCwUPAgUAAgX+gEH/n//gEDHIMDAg3wAgP+AgvgAhBeBAhmAAiJ3BAhf8AgRUBAhBXBAAJtBAgSgCVgRcBAAJXCEwIEDj5SCBoJDCBAKSBBASSBXwKICAgQmCAgIcCv4SCAgI0DeAY=");
+hiragana['RU'] = image(51, 50, "gf/AAXAgF/AoX8gEPBgeAgIFD/EAn4MEg4FD8EACQoACn4lBAAUf/4FDDYOAAoQuBHwIACv/wDwgkEh/+DwoFDDw5ECDwRLDMwg5BLIZMBNgh/FGgIeB+AVB4AeBEYJmBBAJQBDgPBOocf/AoCVIU/Kwc/+5WDg/+Kwl/5/wh4mBh/4/A2CFgMOAoJDC8GBMgUHGAJQCCQKpCBgISBgf+SQMPCQN/4H/4YSBGIIwBCgMBDoTMCn/AEIROCLoKFEAIJvBTwZvCTAarFNIQFCXASyCYoYxBAoYAEA=");
+hiragana['RE'] = image(56, 50, "gEf8AGF+AGigP/wAGDg//GYQGBh//C4M/AYICB/AGDv///gGC+P/AwQKB+YGB/wNC+//w4GDBYMDAwn4AwQ3BFQIGF8AGF4AGFgAGEAYMDHwIGBAYIGDn5XBAwhlBAwd/Axh6CAwSPBAwMHAxEDAwqdBAwidDAw5IBOoQGDU4QGDUAIGE//fAwufCgrmCh4iCAwk4nwGE/EcAwbSBjAGFegReCUgIGJOYIUEQIYGCIYOAAwPgAwIAIA=");
+hiragana['RO'] = image(50, 50, "AAf4gEB/4AC8EAv4FC/kAj4MDwEHAofwDAgSBDAoACn/+AocfAokP/4FDE4OAApED//AAohJBAAI5BAocAIQIFEHghFCD4QFCBoU/KIQMBNQZ9BOAhOCQYYFE/B8CE4QFBM4JGB4YuDj/7AocD/xIE/+fP4c/84FDh/8QoZyBj5mE4aFDn5yEDAIFDGIIFDIgIXDDwKREv4eEv4eBiAFCDwMH+A8BIQLnEEgLnDSooqBQYQFCDgQ2DAoolCJAgAD");
+hiragana['WA'] = image(51, 50, "AAV/4AFDh/4AocB/4DBj/ggE/AQMD/0Ag/8DgWAgH/AQMP+ASB//AgISBAoIDC4Ef///+ASBh4FB/4SBgYFC+E/4IFC/8H/F///9//g/8f/3/x/+j/nAQPwv/j/H/wf+I4N/KAJlBv+P9/4MoMP/f9/xlBAIIqBwAUBn/vFwIdBg40BNIIOBIIR7B+BbC8B7BKoX4uAyCAwM+GQX5//f8IyCn/z/hHCK4N/4/8h/8/4EB/4lBF4P/z5wB8f+RYJjBPoPAFwO/BQP4IQX/wJkCTAUfVYf4gf4BgS4BbQRiCcgbSCAAILEcALkCAAM/DoYeCC4ZLBfoIeD/ASEDAhoBAoYlBDwcAg/ABggAEA=");
+hiragana['N'] = image(54, 50, "AAVgAYUP8EHwAGCv/Av4RD/8D/wFCgf8g/8DQf4j/4AwU/8E/+AaDwF//4VBgIfB/4GCD4MPAwcf+YFB/4jBn4FC/4jBAof/4AYC//n/+DBYeD/wZC/f/FgIrCGIQsCKYU/444CKYP/z4xCvxOBv+/8EBQQP4B4KFCCoJeCNIYPBQgQKBj53CAYSbBCYQDBHgJbCTYUDOQZHBM4QTBTYX/GQQxBP4Y8BDQRGBTYY4Eh5MDHgZTDAojdEbAYGEHgIGEv7/DHgIhFfAh1EEIg8GEIg8GTYYhDHhYAF");
/// /////////////////////////////////////////
-let kana = katakana.KI;
+let kana = katakana.KA;
let scroll = 0;
-function drawWheel () {
- if (scroll > 20 || scroll < -20) {
- scroll = 0;
- next();
- }
-}
let hiramode = false;
let curkana = 'KA';
function next () {
@@ -658,6 +124,19 @@ function next () {
}
curkana = 'KA';
kana = hiramode ? hiragana[curkana] : katakana[curkana];
+ updateWatch(ohhmm);
+}
+
+function randKana() {
+ try {
+ const keys = Object.keys(katakana);
+ const total = keys.length;
+ let index = 0 | (Math.random() * total);
+ curkana = keys[index];
+ kana = hiramode ? hiragana[curkana] : katakana[curkana];
+ } catch (e) {
+ randKana();
+ }
}
function prev () {
@@ -669,7 +148,6 @@ function prev () {
curkana = oldk;
kana = katakana[curkana];
return;
- } else {
}
}
oldk = k;
@@ -677,37 +155,18 @@ function prev () {
}
curkana = oldk;
kana = katakana[curkana];
+ updateWatch(ohhmm);
}
const kanacolors = {
A: []
};
-const clocktop = false;
function updateWatch (hhmm) {
- if (!hhmm) {
- hhmm = ohhmm;
- }
+ g.setFontAlign(-1, -1, 0);
g.setBgColor(0, 0, 0);
g.setColor(0, 0, 0);
- if (false) {
- g.fillRect(0, 0, g.getWidth(), g.getHeight());
- g.setColor(0.3, 0.3, 0.3);
- g.setColor(1, 0, 0);
-
- g.fillRect(stripe_pos, 0, stripe_pos + stripe_width, h);
-
- g.fillRect(stripe2_pos, 0, stripe2_pos + stripe_width, h);
-
- for (i = 0; i < h; i += 8) {
- g.setColor(0.15, 0.15, 0.15);
- g.fillRect(0, i, g.getWidth(), i + 3);
- g.setColor(0.4, 0.4, 0.4);
- g.fillRect(stripe_pos, i, stripe_pos + stripe_width, i + 3);
- g.fillRect(stripe2_pos, i, stripe2_pos + stripe_width, i + 3);
- }
- } else {
var whitecolor = false;
if (curkana.indexOf('A') != -1) {
g.setColor(1, 0, 0);
@@ -723,27 +182,15 @@ function updateWatch (hhmm) {
g.setColor(0, 1, 1);
}
g.fillRect(0, 0, w, h);
- }
- // GOOD FONT SIZE g.setFont("Vector", 62);
- g.setFont('Vector', 50);
- const bignumbers = false;
- if (bignumbers) {
- g.setColor(1, 1, 1);
- g.drawString(hhmm, 12, 12);
- g.setColor(0, 0, 0);
- g.drawString(hhmm, 10, 10);
- } else {
+ g.setFont('Vector', 50);
if (whitecolor) {
g.setColor(0, 0, 0);
} else {
g.setColor(0.5, 0.5, 0.5);
}
- if (clocktop) {
- x = 26; y = 26;
- } else {
- x = 26; y = h - 42;
- }
+ x = 26;
+ y = h - 42;
g.drawString(hhmm, x - 3, y - 3);
if (whitecolor) {
g.setColor(1, 1, 1);
@@ -751,63 +198,60 @@ function updateWatch (hhmm) {
g.setColor(0, 0, 0);
}
g.drawString(hhmm, x, y - 1);
- }
- // drawKana(hira_a, 0, 60);
- drawKana(hiragana.KA, g.getWidth() / 6, 60);
+
+ drawKana(4 + (g.getWidth() / 6), 60);
+ drawMonthDay();
Bangle.drawWidgets();
}
-function drawKana (img, x, y) {
+
+function drawMonthDay() {
+ g.setFont('Vector', 20);
+ g.setColor(1,1,1);
+ g.setFontAlign(-1, -1, 0);
+ g.drawString(month, 4, 112);
+ g.setFontAlign(1, -1, 0);
+ g.drawString(day, w, 112);
+}
+
+function getPhoneme(k) {
+ switch (k) {
+ case "TU": return "TSU";
+ case "TI": return "CHI";
+ case "SI": return "SHI";
+ case "HU": return "FU";
+ }
+ return k;
+}
+
+function drawKana (x, y) {
g.setColor(0, 0, 0);
-
- // g.fillRect(0,0,g.getWidth(), h);
- if (clocktop) {
- g.fillRect(0, h / 2.5, g.getWidth(), h);
- } else {
- g.fillRect(0, 0, g.getWidth(), 6 * (h / 8) + 1);
- }
-
- if (false) {
- g.drawImage(hira_a, x, y);
- g.setColor(1, 1, 1);
- g.setFont('Vector', 30);
- g.drawString(curkana, x + 32, y + 4);
- } else {
- if (clocktop) {
- g.setColor(1, 1, 1);
- g.drawImage(kana, x + 8, y + 12, { scale: 3.4 });
- g.setColor(1, 1, 1);
- g.setFont('Vector', 30);
- g.drawString(curkana, 0, y + 16);
- g.drawString(hiramode ? 'H' : 'K', w - 20, y + 16);
- } else {
- g.setColor(1, 1, 1);
- g.drawImage(kana, x + 8, 26, { scale: 3.4 });
- g.setColor(1, 1, 1);
- g.setFont('Vector', 30);
- g.drawString(curkana, 4, 32);
- g.drawString(hiramode ? 'H' : 'K', w - 20, 32);
- }
- }
+ g.fillRect(0, 0, g.getWidth(), 6 * (h / 8) + 1);
+ g.setColor(1, 1, 1);
+ g.drawImage(kana, x + 20, 40, { scale: 1.6 });
+ g.setColor(1, 1, 1);
+ g.setFont('Vector', 24);
+ g.drawString(getPhoneme(curkana), 4, 32);
+ g.drawString(hiramode ? 'H' : 'K', w - 20, 32);
}
var ohhmm = '';
function tickWatch () {
const now = Date();
+ month = now.getMonth() + 1;
+ day = now.getDate();
function zpad (n) {
return (n < 10) ? '0' + n : n;
}
const hhmm = zpad(now.getHours()) + ':' + zpad(now.getMinutes());
if (hhmm !== ohhmm) {
+ randKana();
updateWatch(hhmm);
+ ohhmm = hhmm;
}
}
Bangle.on('touch', function (tap, top) {
- if (top.y < h / 3) {
- // clocktop = !clocktop;
- return;
- }
if (top.x < w / 4) {
prev();
} else if (top.x > (w - (w / 4))) {
@@ -816,10 +260,14 @@ Bangle.on('touch', function (tap, top) {
hiramode = !hiramode;
}
kana = hiramode ? hiragana[curkana] : katakana[curkana];
- tickWatch();
+ updateWatch(ohhmm);
});
+g.clear(true);
+// show launcher when button pressed
+Bangle.setUI('clock');
Bangle.loadWidgets();
tickWatch();
-setInterval(tickWatch, 1000);
+setInterval(tickWatch, 1000 * 60);
+
diff --git a/apps/kanawatch/fontmaker.zip b/apps/kanawatch/fontmaker.zip
new file mode 100644
index 000000000..39c7d5d53
Binary files /dev/null and b/apps/kanawatch/fontmaker.zip differ
diff --git a/apps/kanawatch/metadata.json b/apps/kanawatch/metadata.json
index 09bfc2d36..b14703979 100644
--- a/apps/kanawatch/metadata.json
+++ b/apps/kanawatch/metadata.json
@@ -2,7 +2,7 @@
"id": "kanawatch",
"name": "Kanawatch",
"shortName": "Kanawatch",
- "version": "0.01",
+ "version": "0.05",
"type": "clock",
"description": "Learn Hiragana and Katakana",
"icon": "app.png",
@@ -25,7 +25,7 @@
],
"screenshots": [
{
- "url": "screenshot.jpg"
+ "url": "screenshot.png"
}
]
}
diff --git a/apps/kanawatch/screenshot.jpg b/apps/kanawatch/screenshot.jpg
deleted file mode 100644
index ac7447ee8..000000000
Binary files a/apps/kanawatch/screenshot.jpg and /dev/null differ
diff --git a/apps/kanawatch/screenshot.png b/apps/kanawatch/screenshot.png
new file mode 100644
index 000000000..b1ed879aa
Binary files /dev/null and b/apps/kanawatch/screenshot.png differ
diff --git a/apps/kbmorse/ChangeLog b/apps/kbmorse/ChangeLog
index f62348ec8..c85361374 100644
--- a/apps/kbmorse/ChangeLog
+++ b/apps/kbmorse/ChangeLog
@@ -1 +1,2 @@
-0.01: New Keyboard!
\ No newline at end of file
+0.01: New Keyboard!
+0.02: Temporarily fix because of firmware bug.
diff --git a/apps/kbmorse/lib.js b/apps/kbmorse/lib.js
index 8bc177a46..997f2cb16 100644
--- a/apps/kbmorse/lib.js
+++ b/apps/kbmorse/lib.js
@@ -82,6 +82,36 @@ exports.input = function(options) {
}
return new Promise((resolve, reject) => {
+ const Layout = require("Layout");
+ let layout = new Layout({
+ type: "h", c: [
+ {
+ type: "v", width: Bangle.appRect.w-8, bgCol: g.theme.bg, c: [
+ {id: "dots", type: "txt", font: "6x8:2", label: "", fillx: 1, bgCol: g.theme.bg},
+ {filly: 1, bgCol: g.theme.bg},
+ {
+ type: "h", fillx: 1, c: [
+ {id: "del", type: "txt", font: "6x8", label: "
+ ({type: "txt", font: "6x8", height: Math.floor(Bangle.appRect.h/3), r: 1, label: l})
+ )
+ }
+ ]
+ });
function update() {
let dots = [], dashes = [];
@@ -157,36 +187,6 @@ exports.input = function(options) {
}
}
- const Layout = require("Layout");
- let layout = new Layout({
- type: "h", c: [
- {
- type: "v", width: Bangle.appRect.w-8, bgCol: g.theme.bg, c: [
- {id: "dots", type: "txt", font: "6x8:2", label: "", fillx: 1, bgCol: g.theme.bg},
- {filly: 1, bgCol: g.theme.bg},
- {
- type: "h", fillx: 1, c: [
- {id: "del", type: "txt", font: "6x8", label: "
- ({type: "txt", font: "6x8", height: Math.floor(Bangle.appRect.h/3), r: 1, label: l})
- )
- }
- ]
- });
g.reset().clear();
update();
@@ -244,4 +244,4 @@ exports.input = function(options) {
};
Bangle.on("swipe", Bangle.swipeHandler);
});
-};
\ No newline at end of file
+};
diff --git a/apps/kbmorse/metadata.json b/apps/kbmorse/metadata.json
index f9c5354f1..9111d514d 100644
--- a/apps/kbmorse/metadata.json
+++ b/apps/kbmorse/metadata.json
@@ -1,7 +1,7 @@
{
"id": "kbmorse",
"name": "Morse keyboard",
- "version": "0.01",
+ "version": "0.02",
"description": "A library for text input as morse code",
"icon": "app.png",
"type": "textinput",
diff --git a/apps/kbmulti/ChangeLog b/apps/kbmulti/ChangeLog
index 26647b548..4ef8f7bda 100644
--- a/apps/kbmulti/ChangeLog
+++ b/apps/kbmulti/ChangeLog
@@ -1,3 +1,5 @@
0.01: New keyboard
0.02: Introduce setting "Show help button?". Make setting firstLaunch invisible by removing corresponding code from settings.js. Add marker that shows when character selection timeout has run out. Display opened text on launch when editing existing text string. Perfect horizontal alignment of buttons. Tweak help message letter casing.
0.03: Use default Bangle formatter for booleans
+0.04: Allow moving the cursor
+0.05: Switch swipe directions for Caps Lock and moving cursor.
diff --git a/apps/kbmulti/README.md b/apps/kbmulti/README.md
index 4c83d378e..b6754711d 100644
--- a/apps/kbmulti/README.md
+++ b/apps/kbmulti/README.md
@@ -2,7 +2,7 @@
A library that provides the ability to input text in a style familiar to anyone who had a mobile phone before they went all touchscreen.
-Swipe right for Space, left for Backspace, and up/down for Caps lock. Tap the '?' button in the app if you need a reminder!
+Swipe right for Space, left for Backspace, down for Caps lock switch, and up for cursor moving mode. Swipe left and right to move the cursor in moving mode. Tap the '?' button in the app if you need a reminder!
At time of writing, only the [Noteify app](http://microco.sm/out/Ffe9i) uses a keyboard.
diff --git a/apps/kbmulti/lib.js b/apps/kbmulti/lib.js
index 5ccab4204..9b642a132 100644
--- a/apps/kbmulti/lib.js
+++ b/apps/kbmulti/lib.js
@@ -17,18 +17,50 @@ exports.input = function(options) {
"4":"GHI4","5":"JKL5","6":"MNO6",
"7":"PQRS7","8":"TUV80","9":"WXYZ9",
};
- var helpMessage = 'Swipe:\nRight: Space\nLeft:Backspace\nUp/Down: Caps lock\n';
+ var helpMessage = 'Swipe:\nRight: Space\nLeft:Backspace\nUp: Move mode\nDown:Caps lock';
var charTimeout; // timeout after a key is pressed
var charCurrent; // current character (index in letters)
var charIndex; // index in letters[charCurrent]
+ var textIndex = text.length;
+ var textWidth = settings.showHelpBtn ? 10 : 14;
var caps = true;
var layout;
- var btnWidth = g.getWidth()/3
+ var btnWidth = g.getWidth()/3;
+
+ function getMoveChar(){
+ return "\x00\x0B\x11\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00@\x1F\xE1\x00\x10\x00\x10\x01\x0F\xF0\x04\x01\x00";
+ }
+
+ function getMoreChar(){
+ return "\x00\x0B\x11\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xDB\x1B`\x00\x00\x00";
+ }
+
+
+ function getCursorChar(){
+ return "\x00\x0B\x11\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xAA\xAA\x80"; }
function displayText(hideMarker) {
layout.clear(layout.text);
- layout.text.label = text.slice(settings.showHelpBtn ? -11 : -13) + (hideMarker ? " " : "_");
+
+ let charsBeforeCursor = textIndex;
+ let charsAfterCursor = Math.min(text.length - textIndex, (textWidth)/2);
+
+
+ let start = textIndex - Math.ceil(textWidth - charsAfterCursor);
+ let startMore = false;
+ if (start > 0) {start++; startMore = true}
+ if (start < 0) start = 0;
+ let cursor = textIndex + 1;
+
+ let end = cursor + Math.floor(start + textWidth - cursor);
+ if (end <= text.length) {end--; if (startMore) end--;}
+ if (end > text.length) end = text.length;
+
+ let pre = (start > 0 ? getMoreChar() : "") + text.slice(start, cursor);
+ let post = text.slice(cursor, end) + (end < text.length - 1 ? getMoreChar() : "");
+
+ layout.text.label = pre + (hideMarker ? " " : (moveMode? getMoveChar():getCursorChar())) + post;
layout.render(layout.text);
}
@@ -41,8 +73,11 @@ exports.input = function(options) {
function backspace() {
deactivateTimeout(charTimeout);
- text = text.slice(0, -1);
- newCharacter();
+ if (textIndex > -1){
+ text = text.slice(0, textIndex) + text.slice(textIndex + 1);
+ if (textIndex > -1) textIndex --;
+ newCharacter();
+ }
}
function setCaps() {
@@ -55,6 +90,7 @@ exports.input = function(options) {
function newCharacter(ch) {
displayText();
+ if (ch && textIndex < text.length) textIndex ++;
charCurrent = ch;
charIndex = 0;
}
@@ -69,7 +105,11 @@ exports.input = function(options) {
newCharacter(key);
}
var newLetter = letters[charCurrent][charIndex];
- text += (caps ? newLetter.toUpperCase() : newLetter.toLowerCase());
+ let pre = text.slice(0, textIndex);
+ let post = text.slice(textIndex, text.length);
+
+ text = pre + (caps ? newLetter.toUpperCase() : newLetter.toLowerCase()) + post;
+
// set a timeout
charTimeout = setTimeout(function() {
charTimeout = undefined;
@@ -78,14 +118,29 @@ exports.input = function(options) {
displayText(charTimeout);
}
+ var moveMode = false;
+
function onSwipe(dirLeftRight, dirUpDown) {
- if (dirUpDown) {
+ if (dirUpDown == -1) {
+ moveMode = !moveMode;
+ displayText(false);
+ } else if (dirUpDown == 1) {
setCaps();
} else if (dirLeftRight == 1) {
- text += ' ';
- newCharacter();
+ if (!moveMode){
+ text = text.slice(0, textIndex + 1) + " " + text.slice(++textIndex);
+ newCharacter();
+ } else {
+ if (textIndex < text.length) textIndex++;
+ displayText(false);
+ }
} else if (dirLeftRight == -1) {
- backspace();
+ if (!moveMode){
+ backspace();
+ } else {
+ if (textIndex > -1) textIndex--;
+ displayText(false);
+ }
}
}
diff --git a/apps/kbmulti/metadata.json b/apps/kbmulti/metadata.json
index 30ffa6f9e..510454f79 100644
--- a/apps/kbmulti/metadata.json
+++ b/apps/kbmulti/metadata.json
@@ -1,6 +1,6 @@
{ "id": "kbmulti",
"name": "Multitap keyboard",
- "version":"0.03",
+ "version":"0.05",
"description": "A library for text input via multitap/T9 style keypad",
"icon": "app.png",
"type":"textinput",
diff --git a/apps/kbswipe/ChangeLog b/apps/kbswipe/ChangeLog
index f0dc54b69..a7b2d44c2 100644
--- a/apps/kbswipe/ChangeLog
+++ b/apps/kbswipe/ChangeLog
@@ -2,3 +2,6 @@
0.02: Now keeps user input trace intact by changing how the screen is updated.
0.03: Positioning of marker now takes the height of the widget field into account.
0.04: Fix issue if going back without typing.
+0.05: Keep drag-function in ram, hopefully improving performance and input reliability somewhat.
+0.06: Support input of numbers and uppercase characters.
+0.07: Support input of symbols.
diff --git a/apps/kbswipe/README.md b/apps/kbswipe/README.md
index 3f5575777..105d7cd9b 100644
--- a/apps/kbswipe/README.md
+++ b/apps/kbswipe/README.md
@@ -4,6 +4,10 @@ A library that provides the ability to input text by swiping PalmOS Graffiti-sty
To get a legend of available characters, just tap the screen.
+To switch between the input of alphabetic, numeric and symbol characters tap the widget which displays either "123", "ABC" or "?:$".
+
+To switch between lowercase and uppercase characters do an up swipe.
+

## Usage
diff --git a/apps/kbswipe/lib.js b/apps/kbswipe/lib.js
index 417ac98d9..ea6d78255 100644
--- a/apps/kbswipe/lib.js
+++ b/apps/kbswipe/lib.js
@@ -1,47 +1,101 @@
+exports.INPUT_MODE_ALPHA = 0;
+exports.INPUT_MODE_NUM = 1;
+exports.INPUT_MODE_SYM = 2;
+
/* To make your own strokes, type:
Bangle.on('stroke',print)
on the left of the IDE, then do a stroke and copy out the Uint8Array line
*/
-exports.getStrokes = function(cb) {
- cb("a", new Uint8Array([58, 159, 58, 155, 62, 144, 69, 127, 77, 106, 86, 90, 94, 77, 101, 68, 108, 62, 114, 59, 121, 59, 133, 61, 146, 70, 158, 88, 169, 107, 176, 124, 180, 135, 183, 144, 185, 152]));
- cb("b", new Uint8Array([51, 47, 51, 77, 56, 123, 60, 151, 65, 163, 68, 164, 68, 144, 67, 108, 67, 76, 72, 43, 104, 51, 121, 74, 110, 87, 109, 95, 131, 117, 131, 140, 109, 152, 88, 157]));
- cb("c", new Uint8Array([153, 62, 150, 62, 145, 62, 136, 62, 123, 62, 106, 65, 85, 70, 65, 75, 50, 82, 42, 93, 37, 106, 36, 119, 36, 130, 40, 140, 49, 147, 61, 153, 72, 156, 85, 157, 106, 158, 116, 158]));
- cb("d", new Uint8Array([57, 178, 57, 176, 55, 171, 52, 163, 50, 154, 49, 146, 47, 135, 45, 121, 44, 108, 44, 97, 44, 85, 44, 75, 44, 66, 44, 58, 44, 48, 44, 38, 46, 31, 48, 26, 58, 21, 75, 20, 99, 26, 120, 35, 136, 51, 144, 70, 144, 88, 137, 110, 124, 131, 106, 145, 88, 153]));
- cb("e", new Uint8Array([150, 72, 141, 69, 114, 68, 79, 69, 48, 77, 32, 81, 31, 85, 46, 91, 73, 95, 107, 100, 114, 103, 83, 117, 58, 134, 66, 143, 105, 148, 133, 148, 144, 148]));
- cb("f", new Uint8Array([157, 52, 155, 52, 148, 52, 137, 52, 124, 52, 110, 52, 96, 52, 83, 52, 74, 52, 67, 52, 61, 52, 57, 52, 55, 52, 52, 52, 52, 54, 52, 58, 52, 64, 54, 75, 58, 97, 59, 117, 60, 130]));
- cb("g", new Uint8Array([160, 66, 153, 62, 129, 58, 90, 56, 58, 57, 38, 65, 31, 86, 43, 125, 69, 152, 116, 166, 145, 154, 146, 134, 112, 116, 85, 108, 97, 106, 140, 106, 164, 106]));
- cb("h", new Uint8Array([58, 50, 58, 55, 58, 64, 58, 80, 58, 102, 58, 122, 58, 139, 58, 153, 58, 164, 58, 171, 58, 177, 58, 179, 58, 181, 58, 180, 58, 173, 58, 163, 59, 154, 61, 138, 64, 114, 68, 95, 72, 84, 80, 79, 91, 79, 107, 82, 123, 93, 137, 111, 145, 130, 149, 147, 150, 154, 150, 159]));
- cb("i", new Uint8Array([89, 48, 89, 49, 89, 51, 89, 55, 89, 60, 89, 68, 89, 78, 89, 91, 89, 103, 89, 114, 89, 124, 89, 132, 89, 138, 89, 144, 89, 148, 89, 151, 89, 154, 89, 156, 89, 157, 89, 158]));
- cb("j", new Uint8Array([130, 57, 130, 61, 130, 73, 130, 91, 130, 113, 130, 133, 130, 147, 130, 156, 130, 161, 130, 164, 130, 166, 129, 168, 127, 168, 120, 168, 110, 168, 91, 167, 81, 167, 68, 167]));
- cb("k", new Uint8Array([149, 63, 147, 68, 143, 76, 136, 89, 126, 106, 114, 123, 100, 136, 86, 147, 72, 153, 57, 155, 45, 152, 36, 145, 29, 131, 26, 117, 26, 104, 27, 93, 30, 86, 35, 80, 45, 77, 62, 80, 88, 96, 113, 116, 130, 131, 140, 142, 145, 149, 148, 153]));
- cb("l", new Uint8Array([42, 55, 42, 59, 42, 69, 44, 87, 44, 107, 44, 128, 44, 143, 44, 156, 44, 163, 44, 167, 44, 169, 45, 170, 49, 170, 59, 169, 76, 167, 100, 164, 119, 162, 139, 160, 163, 159]));
- cb("m", new Uint8Array([49, 165, 48, 162, 46, 156, 44, 148, 42, 138, 42, 126, 42, 113, 43, 101, 45, 91, 47, 82, 49, 75, 51, 71, 54, 70, 57, 70, 61, 74, 69, 81, 75, 91, 84, 104, 94, 121, 101, 132, 103, 137, 106, 130, 110, 114, 116, 92, 125, 75, 134, 65, 139, 62, 144, 66, 148, 83, 151, 108, 155, 132, 157, 149]));
- cb("n", new Uint8Array([50, 165, 50, 160, 50, 153, 50, 140, 50, 122, 50, 103, 50, 83, 50, 65, 50, 52, 50, 45, 50, 43, 52, 52, 57, 67, 66, 90, 78, 112, 93, 131, 104, 143, 116, 152, 127, 159, 135, 160, 141, 150, 148, 125, 154, 96, 158, 71, 161, 56, 162, 49]));
- cb("o", new Uint8Array([107, 58, 104, 58, 97, 61, 87, 68, 75, 77, 65, 88, 58, 103, 54, 116, 53, 126, 55, 135, 61, 143, 75, 149, 91, 150, 106, 148, 119, 141, 137, 125, 143, 115, 146, 104, 146, 89, 142, 78, 130, 70, 116, 65, 104, 62]));
- cb("p", new Uint8Array([52, 59, 52, 64, 54, 73, 58, 88, 61, 104, 65, 119, 67, 130, 69, 138, 71, 145, 71, 147, 71, 148, 71, 143, 70, 133, 68, 120, 67, 108, 67, 97, 67, 89, 68, 79, 72, 67, 83, 60, 99, 58, 118, 58, 136, 63, 146, 70, 148, 77, 145, 84, 136, 91, 121, 95, 106, 97, 93, 97, 82, 97]));
- cb("q", new Uint8Array([95, 59, 93, 59, 88, 59, 79, 59, 68, 61, 57, 67, 50, 77, 48, 89, 48, 103, 50, 117, 55, 130, 65, 140, 76, 145, 85, 146, 94, 144, 101, 140, 105, 136, 106, 127, 106, 113, 100, 98, 92, 86, 86, 79, 84, 75, 84, 72, 91, 69, 106, 67, 126, 67, 144, 67, 158, 67, 168, 67, 173, 67, 177, 67]));
- cb("r", new Uint8Array([53, 49, 53, 62, 53, 91, 53, 127, 53, 146, 53, 147, 53, 128, 53, 94, 53, 69, 62, 44, 82, 42, 94, 50, 92, 68, 82, 85, 77, 93, 80, 102, 95, 119, 114, 134, 129, 145, 137, 150]));
- cb("s", new Uint8Array([159, 72, 157, 70, 155, 68, 151, 66, 145, 63, 134, 60, 121, 58, 108, 56, 96, 55, 83, 55, 73, 55, 64, 56, 57, 60, 52, 65, 49, 71, 49, 76, 50, 81, 55, 87, 71, 94, 94, 100, 116, 104, 131, 108, 141, 114, 145, 124, 142, 135, 124, 146, 97, 153, 70, 157, 52, 158]));
- cb("t", new Uint8Array([45, 55, 48, 55, 55, 55, 72, 55, 96, 55, 120, 55, 136, 55, 147, 55, 152, 55, 155, 55, 157, 55, 158, 56, 158, 60, 156, 70, 154, 86, 151, 102, 150, 114, 148, 125, 148, 138, 148, 146]));
- cb("u", new Uint8Array([35, 52, 35, 59, 35, 73, 35, 90, 36, 114, 38, 133, 42, 146, 49, 153, 60, 157, 73, 158, 86, 156, 100, 152, 112, 144, 121, 131, 127, 114, 132, 97, 134, 85, 135, 73, 136, 61, 136, 56]));
- cb("v", new Uint8Array([36, 55, 37, 59, 40, 68, 45, 83, 51, 100, 58, 118, 64, 132, 69, 142, 71, 149, 73, 156, 76, 158, 77, 160, 77, 159, 80, 151, 82, 137, 84, 122, 86, 111, 90, 91, 91, 78, 91, 68, 91, 63, 92, 61, 97, 61, 111, 61, 132, 61, 150, 61, 162, 61]));
- cb("w", new Uint8Array([33, 58, 34, 81, 39, 127, 44, 151, 48, 161, 52, 162, 57, 154, 61, 136, 65, 115, 70, 95, 76, 95, 93, 121, 110, 146, 119, 151, 130, 129, 138, 84, 140, 56, 140, 45]));
- cb("x", new Uint8Array([56, 63, 56, 67, 57, 74, 60, 89, 66, 109, 74, 129, 85, 145, 96, 158, 107, 164, 117, 167, 128, 164, 141, 155, 151, 140, 159, 122, 166, 105, 168, 89, 170, 81, 170, 73, 169, 66, 161, 63, 141, 68, 110, 83, 77, 110, 55, 134, 47, 145]));
- cb("y", new Uint8Array([42, 56, 42, 70, 48, 97, 62, 109, 85, 106, 109, 90, 126, 65, 134, 47, 137, 45, 137, 75, 127, 125, 98, 141, 70, 133, 65, 126, 92, 137, 132, 156, 149, 166]));
- cb("z", new Uint8Array([29, 62, 35, 62, 43, 62, 63, 62, 87, 62, 110, 62, 125, 62, 134, 62, 138, 62, 136, 63, 122, 68, 103, 77, 85, 91, 70, 107, 59, 120, 50, 132, 47, 138, 43, 143, 41, 148, 42, 151, 53, 155, 80, 157, 116, 158, 146, 158, 163, 158]));
+exports.getStrokes = function(mode, cb) {
+ if (mode === exports.INPUT_MODE_ALPHA) {
+ cb("a", new Uint8Array([58, 159, 58, 155, 62, 144, 69, 127, 77, 106, 86, 90, 94, 77, 101, 68, 108, 62, 114, 59, 121, 59, 133, 61, 146, 70, 158, 88, 169, 107, 176, 124, 180, 135, 183, 144, 185, 152]));
+ cb("b", new Uint8Array([51, 47, 51, 77, 56, 123, 60, 151, 65, 163, 68, 164, 68, 144, 67, 108, 67, 76, 72, 43, 104, 51, 121, 74, 110, 87, 109, 95, 131, 117, 131, 140, 109, 152, 88, 157]));
+ cb("c", new Uint8Array([153, 62, 150, 62, 145, 62, 136, 62, 123, 62, 106, 65, 85, 70, 65, 75, 50, 82, 42, 93, 37, 106, 36, 119, 36, 130, 40, 140, 49, 147, 61, 153, 72, 156, 85, 157, 106, 158, 116, 158]));
+ cb("d", new Uint8Array([57, 178, 57, 176, 55, 171, 52, 163, 50, 154, 49, 146, 47, 135, 45, 121, 44, 108, 44, 97, 44, 85, 44, 75, 44, 66, 44, 58, 44, 48, 44, 38, 46, 31, 48, 26, 58, 21, 75, 20, 99, 26, 120, 35, 136, 51, 144, 70, 144, 88, 137, 110, 124, 131, 106, 145, 88, 153]));
+ cb("e", new Uint8Array([150, 72, 141, 69, 114, 68, 79, 69, 48, 77, 32, 81, 31, 85, 46, 91, 73, 95, 107, 100, 114, 103, 83, 117, 58, 134, 66, 143, 105, 148, 133, 148, 144, 148]));
+ cb("f", new Uint8Array([157, 52, 155, 52, 148, 52, 137, 52, 124, 52, 110, 52, 96, 52, 83, 52, 74, 52, 67, 52, 61, 52, 57, 52, 55, 52, 52, 52, 52, 54, 52, 58, 52, 64, 54, 75, 58, 97, 59, 117, 60, 130]));
+ cb("g", new Uint8Array([160, 66, 153, 62, 129, 58, 90, 56, 58, 57, 38, 65, 31, 86, 43, 125, 69, 152, 116, 166, 145, 154, 146, 134, 112, 116, 85, 108, 97, 106, 140, 106, 164, 106]));
+ cb("h", new Uint8Array([58, 50, 58, 55, 58, 64, 58, 80, 58, 102, 58, 122, 58, 139, 58, 153, 58, 164, 58, 171, 58, 177, 58, 179, 58, 181, 58, 180, 58, 173, 58, 163, 59, 154, 61, 138, 64, 114, 68, 95, 72, 84, 80, 79, 91, 79, 107, 82, 123, 93, 137, 111, 145, 130, 149, 147, 150, 154, 150, 159]));
+ cb("i", new Uint8Array([89, 48, 89, 49, 89, 51, 89, 55, 89, 60, 89, 68, 89, 78, 89, 91, 89, 103, 89, 114, 89, 124, 89, 132, 89, 138, 89, 144, 89, 148, 89, 151, 89, 154, 89, 156, 89, 157, 89, 158]));
+ cb("j", new Uint8Array([130, 57, 130, 61, 130, 73, 130, 91, 130, 113, 130, 133, 130, 147, 130, 156, 130, 161, 130, 164, 130, 166, 129, 168, 127, 168, 120, 168, 110, 168, 91, 167, 81, 167, 68, 167]));
+ cb("k", new Uint8Array([149, 63, 147, 68, 143, 76, 136, 89, 126, 106, 114, 123, 100, 136, 86, 147, 72, 153, 57, 155, 45, 152, 36, 145, 29, 131, 26, 117, 26, 104, 27, 93, 30, 86, 35, 80, 45, 77, 62, 80, 88, 96, 113, 116, 130, 131, 140, 142, 145, 149, 148, 153]));
+ cb("l", new Uint8Array([42, 55, 42, 59, 42, 69, 44, 87, 44, 107, 44, 128, 44, 143, 44, 156, 44, 163, 44, 167, 44, 169, 45, 170, 49, 170, 59, 169, 76, 167, 100, 164, 119, 162, 139, 160, 163, 159]));
+ cb("m", new Uint8Array([49, 165, 48, 162, 46, 156, 44, 148, 42, 138, 42, 126, 42, 113, 43, 101, 45, 91, 47, 82, 49, 75, 51, 71, 54, 70, 57, 70, 61, 74, 69, 81, 75, 91, 84, 104, 94, 121, 101, 132, 103, 137, 106, 130, 110, 114, 116, 92, 125, 75, 134, 65, 139, 62, 144, 66, 148, 83, 151, 108, 155, 132, 157, 149]));
+ cb("n", new Uint8Array([50, 165, 50, 160, 50, 153, 50, 140, 50, 122, 50, 103, 50, 83, 50, 65, 50, 52, 50, 45, 50, 43, 52, 52, 57, 67, 66, 90, 78, 112, 93, 131, 104, 143, 116, 152, 127, 159, 135, 160, 141, 150, 148, 125, 154, 96, 158, 71, 161, 56, 162, 49]));
+ cb("o", new Uint8Array([107, 58, 104, 58, 97, 61, 87, 68, 75, 77, 65, 88, 58, 103, 54, 116, 53, 126, 55, 135, 61, 143, 75, 149, 91, 150, 106, 148, 119, 141, 137, 125, 143, 115, 146, 104, 146, 89, 142, 78, 130, 70, 116, 65, 104, 62]));
+ cb("p", new Uint8Array([29, 47, 29, 55, 29, 75, 29, 110, 29, 145, 29, 165, 29, 172, 29, 164, 30, 149, 37, 120, 50, 91, 61, 74, 72, 65, 85, 61, 103, 61, 118, 63, 126, 69, 129, 76, 130, 87, 126, 98, 112, 108, 97, 114, 87, 116]));
+ cb("q", new Uint8Array([95, 59, 93, 59, 88, 59, 79, 59, 68, 61, 57, 67, 50, 77, 48, 89, 48, 103, 50, 117, 55, 130, 65, 140, 76, 145, 85, 146, 94, 144, 101, 140, 105, 136, 106, 127, 106, 113, 100, 98, 92, 86, 86, 79, 84, 75, 84, 72, 91, 69, 106, 67, 126, 67, 144, 67, 158, 67, 168, 67, 173, 67, 177, 67]));
+ cb("r", new Uint8Array([53, 49, 53, 62, 53, 91, 53, 127, 53, 146, 53, 147, 53, 128, 53, 94, 53, 69, 62, 44, 82, 42, 94, 50, 92, 68, 82, 85, 77, 93, 80, 102, 95, 119, 114, 134, 129, 145, 137, 150]));
+ cb("s", new Uint8Array([159, 72, 157, 70, 155, 68, 151, 66, 145, 63, 134, 60, 121, 58, 108, 56, 96, 55, 83, 55, 73, 55, 64, 56, 57, 60, 52, 65, 49, 71, 49, 76, 50, 81, 55, 87, 71, 94, 94, 100, 116, 104, 131, 108, 141, 114, 145, 124, 142, 135, 124, 146, 97, 153, 70, 157, 52, 158]));
+ cb("t", new Uint8Array([45, 55, 48, 55, 55, 55, 72, 55, 96, 55, 120, 55, 136, 55, 147, 55, 152, 55, 155, 55, 157, 55, 158, 56, 158, 60, 156, 70, 154, 86, 151, 102, 150, 114, 148, 125, 148, 138, 148, 146]));
+ cb("u", new Uint8Array([35, 52, 35, 59, 35, 73, 35, 90, 36, 114, 38, 133, 42, 146, 49, 153, 60, 157, 73, 158, 86, 156, 100, 152, 112, 144, 121, 131, 127, 114, 132, 97, 134, 85, 135, 73, 136, 61, 136, 56]));
+ cb("v", new Uint8Array([36, 55, 37, 59, 40, 68, 45, 83, 51, 100, 58, 118, 64, 132, 69, 142, 71, 149, 73, 156, 76, 158, 77, 160, 77, 159, 80, 151, 82, 137, 84, 122, 86, 111, 90, 91, 91, 78, 91, 68, 91, 63, 92, 61, 97, 61, 111, 61, 132, 61, 150, 61, 162, 61]));
+ cb("w", new Uint8Array([25, 46, 25, 82, 25, 119, 33, 143, 43, 153, 60, 147, 73, 118, 75, 91, 76, 88, 85, 109, 96, 134, 107, 143, 118, 137, 129, 112, 134, 81, 134, 64, 134, 55]));
+ cb("x", new Uint8Array([56, 63, 56, 67, 57, 74, 60, 89, 66, 109, 74, 129, 85, 145, 96, 158, 107, 164, 117, 167, 128, 164, 141, 155, 151, 140, 159, 122, 166, 105, 168, 89, 170, 81, 170, 73, 169, 66, 161, 63, 141, 68, 110, 83, 77, 110, 55, 134, 47, 145]));
+ cb("y", new Uint8Array([30, 41, 30, 46, 30, 52, 30, 63, 30, 79, 33, 92, 38, 100, 47, 104, 54, 107, 66, 105, 79, 94, 88, 82, 92, 74, 94, 77, 96, 98, 96, 131, 94, 151, 91, 164, 85, 171, 75, 171, 71, 162, 74, 146, 84, 130, 95, 119, 106, 113]));
+ cb("z", new Uint8Array([29, 62, 35, 62, 43, 62, 63, 62, 87, 62, 110, 62, 125, 62, 134, 62, 138, 62, 136, 63, 122, 68, 103, 77, 85, 91, 70, 107, 59, 120, 50, 132, 47, 138, 43, 143, 41, 148, 42, 151, 53, 155, 80, 157, 116, 158, 146, 158, 163, 158]));
+ cb("SHIFT", new Uint8Array([100, 160, 100, 50]));
+ } else if (mode === exports.INPUT_MODE_NUM) {
+ cb("0", new Uint8Array([82, 50, 76, 50, 67, 50, 59, 50, 50, 51, 43, 57, 38, 68, 34, 83, 33, 95, 33, 108, 34, 121, 42, 136, 57, 148, 72, 155, 85, 157, 98, 155, 110, 149, 120, 139, 128, 127, 134, 119, 137, 114, 138, 107, 138, 98, 138, 88, 138, 77, 137, 71, 134, 65, 128, 60, 123, 58]));
+ cb("1", new Uint8Array([100, 50, 100, 160]));
+ cb("2", new Uint8Array([40, 79, 46, 74, 56, 66, 68, 58, 77, 49, 87, 45, 100, 45, 111, 46, 119, 50, 128, 58, 133, 71, 130, 88, 120, 106, 98, 128, 69, 150, 50, 162, 42, 167, 43, 168, 58, 169, 78, 170, 93, 170, 103, 170, 109, 170]));
+ cb("3", new Uint8Array([47, 65, 51, 60, 57, 56, 65, 51, 74, 47, 84, 45, 93, 45, 102, 45, 109, 46, 122, 51, 129, 58, 130, 65, 127, 74, 120, 85, 112, 92, 107, 96, 112, 101, 117, 105, 125, 113, 128, 123, 127, 134, 122, 145, 108, 156, 91, 161, 70, 163, 55, 163]));
+ cb("4", new Uint8Array([37, 58, 37, 60, 37, 64, 37, 69, 37, 75, 37, 86, 37, 96, 37, 105, 37, 112, 37, 117, 37, 122, 37, 126, 37, 128, 38, 129, 40, 129, 45, 129, 48, 129, 53, 129, 67, 129, 85, 129, 104, 129, 119, 129, 129, 129, 136, 129]));
+ cb("5", new Uint8Array([142, 60, 119, 60, 79, 60, 45, 60, 37, 64, 37, 86, 37, 103, 47, 107, 66, 106, 81, 103, 97, 103, 116, 103, 129, 108, 131, 130, 122, 152, 101, 168, 85, 172, 70, 172, 59, 172]));
+ cb("6", new Uint8Array([136, 54, 135, 49, 129, 47, 114, 47, 89, 54, 66, 66, 50, 81, 39, 95, 35, 109, 34, 128, 38, 145, 52, 158, 81, 164, 114, 157, 133, 139, 136, 125, 132, 118, 120, 115, 102, 117, 85, 123]));
+ cb("7", new Uint8Array([47, 38, 48, 38, 53, 38, 66, 38, 85, 38, 103, 38, 117, 38, 125, 38, 129, 38, 134, 41, 135, 47, 135, 54, 135, 66, 131, 93, 124, 126, 116, 149, 109, 161, 105, 168]));
+ cb("8", new Uint8Array([122, 61, 102, 61, 83, 61, 60, 61, 47, 62, 45, 78, 58, 99, 84, 112, 105, 122, 118, 134, 121, 149, 113, 165, 86, 171, 59, 171, 47, 164, 45, 144, 50, 132, 57, 125, 67, 117, 78, 109, 87, 102, 96, 94, 105, 86, 113, 85]));
+ cb("9", new Uint8Array([122, 58, 117, 55, 112, 51, 104, 51, 95, 51, 86, 51, 77, 51, 68, 51, 60, 51, 54, 56, 47, 64, 46, 77, 46, 89, 46, 96, 51, 103, 64, 109, 74, 110, 83, 110, 94, 107, 106, 102, 116, 94, 124, 84, 127, 79, 128, 78, 128, 94, 128, 123, 128, 161, 128, 175]));
+ } else if (mode === exports.INPUT_MODE_SYM) {
+ cb("?", new Uint8Array([36, 69, 39, 68, 44, 65, 52, 60, 61, 56, 70, 51, 78, 47, 87, 46, 96, 46, 108, 46, 121, 49, 128, 56, 129, 63, 126, 76, 119, 91, 108, 105, 103, 114, 98, 118, 93, 124, 91, 131, 91, 143, 91, 155, 91, 163]));
+ cb(".", new Uint8Array([105, 158, 97, 157, 80, 150, 60, 140, 44, 127, 34, 110, 31, 97, 31, 84, 35, 74, 48, 59, 78, 55, 115, 57, 145, 70, 159, 89, 162, 112, 160, 138, 153, 153, 144, 164, 125, 170, 103, 171]));
+ cb(",", new Uint8Array([140, 44, 139, 45, 138, 46, 137, 47, 135, 49, 132, 51, 127, 55, 123, 58, 117, 62, 112, 67, 105, 71, 100, 77, 93, 82, 86, 86, 80, 91, 74, 96, 69, 101, 64, 105, 60, 108, 57, 112, 53, 115, 51, 117, 49, 119, 48, 121, 47, 122, 46, 122, 46, 123]));
+ cb("'", new Uint8Array([100, 50, 100, 160]));
+ cb("-", new Uint8Array([34, 63, 36, 63, 40, 63, 46, 63, 54, 63, 63, 63, 72, 63, 82, 63, 92, 63, 103, 63, 113, 63, 124, 63, 132, 63, 139, 63, 143, 63, 145, 63, 147, 63, 149, 63, 152, 63]));
+ cb("_", new Uint8Array([34, 84, 36, 84, 40, 84, 47, 84, 56, 84, 67, 84, 81, 84, 95, 84, 108, 84, 120, 84, 131, 84, 139, 84, 146, 84, 149, 84, 151, 84, 154, 84, 155, 83, 154, 81, 150, 78, 143, 74, 130, 71, 111, 68, 90, 65, 73, 64, 60, 64, 51, 64, 46, 64]));
+ cb("\"", new Uint8Array([24, 168, 24, 158, 28, 132, 33, 102, 37, 82, 41, 66, 46, 54, 50, 47, 54, 46, 60, 49, 67, 64, 73, 88, 80, 114, 87, 138, 95, 149, 109, 145, 123, 128, 130, 108, 135, 87, 136, 70, 136, 57, 136, 50]));
+ cb(":", new Uint8Array([24, 62, 24, 63, 24, 68, 26, 73, 27, 80, 29, 88, 31, 94, 33, 101, 35, 108, 37, 114, 39, 121, 39, 127, 39, 131, 39, 134, 39, 135, 39, 133, 39, 130, 41, 125, 45, 114, 48, 100, 51, 89, 52, 81, 52, 74, 52, 70, 52, 67, 52, 63, 52, 60, 52, 57]));
+ cb(";", new Uint8Array([142, 58, 139, 59, 136, 61, 131, 65, 124, 71, 116, 79, 105, 87, 94, 98, 82, 109, 70, 121, 58, 132, 49, 141, 40, 149, 33, 156, 28, 160, 24, 164, 23, 166, 22, 164, 25, 156, 33, 138, 47, 111, 66, 81, 82, 58, 95, 41, 103, 30]));
+ cb("(", new Uint8Array([72, 51, 70, 51, 68, 51, 66, 54, 63, 56, 61, 59, 58, 61, 56, 65, 54, 70, 51, 74, 49, 79, 47, 83, 45, 87, 44, 92, 44, 94, 44, 96, 44, 99, 44, 101, 44, 104, 44, 107, 44, 114, 44, 120, 46, 127, 49, 135, 52, 141, 56, 145]));
+ cb(")", new Uint8Array([18, 42, 21, 43, 24, 45, 28, 47, 32, 50, 37, 53, 40, 58, 44, 62, 46, 69, 48, 76, 50, 81, 52, 85, 53, 90, 53, 94, 53, 98, 53, 103, 53, 106, 53, 111, 53, 119, 53, 129, 52, 137, 50, 142, 47, 146]));
+ cb("[", new Uint8Array([121, 138, 118, 143, 114, 146, 110, 149, 105, 152, 98, 152, 91, 152, 83, 152, 77, 152, 67, 151, 59, 146, 52, 138, 47, 131, 47, 124, 48, 118, 57, 115, 64, 115, 67, 113, 64, 106, 59, 95, 53, 85, 48, 80, 47, 74, 47, 64, 53, 57, 65, 56, 83, 56, 99, 61]));
+ cb("]", new Uint8Array([36, 136, 42, 140, 54, 145, 70, 149, 84, 151, 98, 149, 109, 143, 113, 135, 113, 127, 104, 115, 87, 105, 75, 103, 76, 98, 87, 84, 96, 67, 100, 54, 97, 48, 90, 45, 76, 45, 60, 47, 44, 52]));
+ cb("<", new Uint8Array([154, 122, 151, 122, 149, 121, 147, 118, 144, 116, 139, 114, 133, 112, 126, 110, 118, 107, 108, 105, 97, 102, 86, 97, 75, 93, 64, 90, 56, 88, 49, 85, 46, 84, 41, 82, 40, 80, 47, 76, 63, 69, 86, 59, 106, 50, 121, 44, 128, 40]));
+ cb(">", new Uint8Array([28, 115, 31, 115, 38, 113, 48, 110, 57, 107, 68, 103, 79, 98, 90, 94, 98, 92, 104, 90, 111, 88, 117, 85, 122, 83, 125, 81, 127, 80, 129, 80, 132, 80, 130, 78, 126, 75, 120, 72, 110, 69, 98, 66, 85, 63, 72, 60, 59, 57, 45, 53, 36, 49, 30, 46]));
+ cb("@", new Uint8Array([82, 50, 76, 50, 67, 50, 59, 50, 50, 51, 43, 57, 38, 68, 34, 83, 33, 95, 33, 108, 34, 121, 42, 136, 57, 148, 72, 155, 85, 157, 98, 155, 110, 149, 120, 139, 128, 127, 134, 119, 137, 114, 138, 107, 138, 98, 138, 88, 138, 77, 137, 71, 134, 65, 128, 60, 123, 58]));
+ cb("#", new Uint8Array([23, 70, 23, 76, 26, 85, 30, 97, 36, 112, 40, 129, 45, 142, 49, 152, 53, 158, 59, 161, 67, 155, 78, 130, 84, 98, 88, 76, 90, 68, 96, 62, 102, 61, 108, 61, 119, 67, 126, 80, 131, 101, 135, 129, 136, 152]));
+ cb("$", new Uint8Array([159, 72, 157, 70, 155, 68, 151, 66, 145, 63, 134, 60, 121, 58, 108, 56, 96, 55, 83, 55, 73, 55, 64, 56, 57, 60, 52, 65, 49, 71, 49, 76, 50, 81, 55, 87, 71, 94, 94, 100, 116, 104, 131, 108, 141, 114, 145, 124, 142, 135, 124, 146, 97, 153, 70, 157, 52, 158]));
+ cb("%", new Uint8Array([31, 39, 39, 54, 51, 78, 60, 97, 62, 107, 59, 118, 47, 118, 44, 109, 46, 92, 56, 73, 69, 62, 92, 61, 115, 70, 125, 90, 126, 110, 125, 122, 118, 127, 111, 127, 105, 124, 105, 115, 105, 97, 109, 75, 117, 56, 124, 45]));
+ cb("^", new Uint8Array([28, 175, 28, 168, 33, 156, 37, 142, 41, 128, 46, 111, 51, 95, 58, 82, 62, 75, 68, 68, 74, 57, 81, 49, 88, 44, 93, 44, 102, 56, 113, 79, 118, 95, 123, 110, 131, 130, 135, 146, 136, 158]));
+ cb("&", new Uint8Array([122, 61, 102, 61, 83, 61, 60, 61, 47, 62, 45, 78, 58, 99, 84, 112, 105, 122, 118, 134, 121, 149, 113, 165, 86, 171, 59, 171, 47, 164, 45, 144, 50, 132, 57, 125, 67, 117, 78, 109, 87, 102, 96, 94, 105, 86, 113, 85]));
+ cb("*", new Uint8Array([35, 61, 41, 62, 53, 68, 72, 78, 91, 91, 103, 99, 113, 103, 119, 106, 124, 107, 131, 107, 139, 107, 150, 107, 161, 104, 166, 97, 166, 89, 165, 78, 162, 70, 158, 61, 151, 54, 144, 51, 132, 51, 115, 57, 98, 66, 82, 78, 65, 89, 52, 100, 44, 109]));
+ cb("!", new Uint8Array([100, 160, 100, 50]));
+ cb("~", new Uint8Array([133, 40, 133, 48, 133, 65, 133, 87, 133, 105, 132, 116, 128, 125, 124, 133, 120, 140, 114, 146, 107, 148, 101, 147, 91, 139, 82, 126, 74, 108, 70, 91, 70, 82, 70, 75, 70, 65, 68, 57, 62, 51, 57, 50, 49, 57, 41, 76, 36, 96, 33, 114, 33, 132]));
+ cb("+", new Uint8Array([151, 41, 146, 46, 133, 55, 116, 71, 101, 87, 87, 98, 74, 105, 63, 109, 54, 110, 43, 106, 36, 94, 36, 80, 36, 68, 42, 60, 60, 58, 91, 64, 115, 77, 129, 88, 139, 99, 144, 106]));
+ cb("=", new Uint8Array([34, 46, 47, 46, 70, 46, 87, 46, 96, 46, 101, 46, 104, 46, 102, 50, 96, 58, 80, 78, 62, 100, 49, 117, 40, 127, 43, 132, 61, 132, 84, 132, 99, 132]));
+ cb("\\", new Uint8Array([25, 38, 26, 40, 30, 43, 35, 48, 43, 54, 54, 63, 65, 74, 76, 85, 87, 96, 98, 108, 108, 121, 116, 131, 123, 138, 127, 144, 131, 148, 134, 152, 136, 155]));
+ cb("|", new Uint8Array([66, 146, 66, 144, 66, 140, 66, 134, 66, 125, 66, 114, 66, 102, 66, 92, 66, 83, 66, 77, 66, 71, 66, 67, 66, 62, 66, 58, 66, 53, 66, 49, 66, 48, 66, 46, 64, 42, 61, 41, 58, 42, 54, 47, 51, 55, 46, 67, 40, 81, 37, 93, 34, 102, 30, 109, 28, 116]));
+ cb("/", new Uint8Array([24, 173, 26, 171, 30, 166, 36, 158, 44, 148, 53, 137, 63, 126, 73, 115, 82, 104, 91, 95, 99, 87, 105, 80, 112, 74, 117, 70, 122, 65, 125, 61, 127, 60, 129, 57, 133, 53, 136, 50, 137, 47]));
+ }
cb("\b", new Uint8Array([183, 103, 182, 103, 180, 103, 176, 103, 169, 103, 159, 103, 147, 103, 133, 103, 116, 103, 101, 103, 85, 103, 73, 103, 61, 103, 52, 103, 38, 103, 34, 103, 29, 103, 27, 103, 26, 103, 25, 103, 24, 103]));
- cb(" ", new Uint8Array([39, 118, 40, 118, 41, 118, 44, 118, 47, 118, 52, 118, 58, 118, 66, 118, 74, 118, 84, 118, 94, 118, 104, 117, 114, 116, 123, 116, 130, 116, 144, 116, 149, 116, 154, 116, 158, 116, 161, 116, 163, 116]));
+ if (mode === exports.INPUT_MODE_ALPHA || mode === exports.INPUT_MODE_NUM) {
+ cb(" ", new Uint8Array([39, 118, 40, 118, 41, 118, 44, 118, 47, 118, 52, 118, 58, 118, 66, 118, 74, 118, 84, 118, 94, 118, 104, 117, 114, 116, 123, 116, 130, 116, 144, 116, 149, 116, 154, 116, 158, 116, 161, 116, 163, 116]));
+ }
};
exports.input = function(options) {
options = options||{};
+ let input_mode = exports.INPUT_MODE_ALPHA;
var text = options.text;
if ("string"!=typeof text) text="";
-Bangle.strokes = {};
-exports.getStrokes( (id,s) => Bangle.strokes[id] = Unistroke.new(s) );
+ function setupStrokes() {
+ Bangle.strokes = {};
+ exports.getStrokes(input_mode, (id,s) => Bangle.strokes[id] = Unistroke.new(s) );
+ }
+ setupStrokes();
var flashToggle = false;
const R = Bangle.appRect;
@@ -49,6 +103,9 @@ exports.getStrokes( (id,s) => Bangle.strokes[id] = Unistroke.new(s) );
var Rx2;
var Ry1;
var Ry2;
+ let flashInterval;
+ let shift = false;
+ let lastDrag;
function findMarker(strArr) {
if (strArr.length == 0) {
@@ -101,51 +158,117 @@ exports.getStrokes( (id,s) => Bangle.strokes[id] = Unistroke.new(s) );
*/
function show() {
+ if (flashInterval) clearInterval(flashInterval);
+ flashInterval = undefined;
+
g.reset();
- g.clearRect(R).setColor("#f00");
- var n=0;
- exports.getStrokes((id,s) => {
- var x = n%6;
- var y = (n-x)/6;
+ g.setFont("6x8");
+ g.clearRect(R);
+ let n=0;
+ exports.getStrokes(input_mode, (id,s) => {
+ let x = n%6;
+ let y = (n-x)/6;
s = g.transformVertices(s, {scale:0.16, x:R.x+x*30-4, y:R.y+y*30-4});
g.fillRect(s[0]-1,s[1]-2,s[0]+1,s[1]+1);
- g.drawPoly(s);
+ g.setColor("#f00").drawPoly(s);
+ switch(id) {
+ case 'SHIFT':
+ g.setBgColor(0).setColor("#00f").drawImage(atob("CgqBAfP4fh8D4fh+H4fh+HA="), R.x+x*30+20, R.y+y*30+20);
+ break;
+ case '\b':
+ case '\n':
+ case ' ':
+ break;
+ default:
+ g.setColor("#00f").drawString(shift ? id.toUpperCase() : id, R.x+x*30+20, R.y+y*30+20);
+ }
n++;
});
}
+ function isInside(rect, e) {
+ return e.x>=rect.x && e.x=rect.y && e.y<=rect.y+rect.h;
+ }
+
+ function isStrokeInside(rect, stroke) {
+ for(let i=0; i < stroke.length; i+=2) {
+ if (!isInside(rect, {x: stroke[i], y: stroke[i+1]})) {
+ return false;
+ }
+ }
+ return true;
+ }
+
function strokeHandler(o) {
//print(o);
if (!flashInterval)
flashInterval = setInterval(() => {
flashToggle = !flashToggle;
- draw();
+ draw(false);
}, 1000);
- if (o.stroke!==undefined) {
+ if (o.stroke!==undefined && o.xy.length >= 6 && isStrokeInside(R, o.xy)) {
var ch = o.stroke;
if (ch=="\b") text = text.slice(0,-1);
- else text += ch;
- g.clearRect(R);
+ else if (ch==="SHIFT") { shift=!shift; Bangle.drawWidgets(); }
+ else text += shift ? ch.toUpperCase() : ch;
}
+ lastDrag = undefined;
+ g.clearRect(R);
flashToggle = true;
- draw();
+ draw(false);
}
+
+ // Switches between alphabetic and numeric input
+ function cycleInput() {
+ input_mode++;
+ if (input_mode > exports.INPUT_MODE_SYM) input_mode = 0;
+ shift = false;
+ setupStrokes();
+ show();
+ Bangle.drawWidgets();
+ }
+
Bangle.on('stroke',strokeHandler);
g.reset().clearRect(R);
show();
draw(false);
- var flashInterval;
+
+ // Create Widget to switch between alphabetic and numeric input
+ WIDGETS.kbswipe={
+ area:"tl",
+ width: 36, // 3 chars, 6*2 px/char
+ draw: function() {
+ g.reset();
+ g.setFont("6x8:2x3");
+ g.setColor("#f00");
+ if (input_mode === exports.INPUT_MODE_ALPHA) {
+ g.drawString(shift ? "ABC" : "abc", this.x, this.y);
+ } else if (input_mode === exports.INPUT_MODE_NUM) {
+ g.drawString("123", this.x, this.y);
+ } else if (input_mode === exports.INPUT_MODE_SYM) {
+ g.drawString("?:$", this.x, this.y);
+ }
+ }
+ };
return new Promise((resolve,reject) => {
- var l;//last event
Bangle.setUI({mode:"custom", drag:e=>{
- if (l) g.reset().setColor("#f00").drawLine(l.x,l.y,e.x,e.y);
- l = e.b ? e : 0;
- },touch:() => {
- if (flashInterval) clearInterval(flashInterval);
- flashInterval = undefined;
- show();
+ "ram";
+ if (isInside(R, e)) {
+ if (lastDrag) g.reset().setColor("#f00").drawLine(lastDrag.x,lastDrag.y,e.x,e.y);
+ lastDrag = e.b ? e : 0;
+ }
+ },touch:(n,e) => {
+ if (WIDGETS.kbswipe && isInside({x: WIDGETS.kbswipe.x, y: WIDGETS.kbswipe.y, w: WIDGETS.kbswipe.width, h: 24}, e)) {
+ // touch inside widget
+ cycleInput();
+ } else if (isInside(R, e)) {
+ // touch inside app area
+ show();
+ }
}, back:()=>{
+ delete WIDGETS.kbswipe;
Bangle.removeListener("stroke", strokeHandler);
if (flashInterval) clearInterval(flashInterval);
Bangle.setUI();
diff --git a/apps/kbswipe/metadata.json b/apps/kbswipe/metadata.json
index d4026c815..6b597a371 100644
--- a/apps/kbswipe/metadata.json
+++ b/apps/kbswipe/metadata.json
@@ -1,6 +1,6 @@
{ "id": "kbswipe",
"name": "Swipe keyboard",
- "version":"0.04",
+ "version":"0.07",
"description": "A library for text input via PalmOS style swipe gestures (beta!)",
"icon": "app.png",
"type":"textinput",
diff --git a/apps/kbtouch/metadata.json b/apps/kbtouch/metadata.json
index f6d6d5228..89d121d63 100644
--- a/apps/kbtouch/metadata.json
+++ b/apps/kbtouch/metadata.json
@@ -6,10 +6,11 @@
"type":"textinput",
"tags": "keyboard",
"supports" : ["BANGLEJS2"],
- "screenshots": [{"url":"screenshot.png"}],
+ "screenshots": [{"url":"screenshot.png"}],
"readme": "README.md",
"storage": [
{"name":"textinput","url":"lib.js"},
{"name":"kbtouch.settings.js","url":"settings.js"}
- ]
+ ],
+ "sortorder":-1
}
diff --git a/apps/keytimer/ChangeLog b/apps/keytimer/ChangeLog
new file mode 100644
index 000000000..c819919ed
--- /dev/null
+++ b/apps/keytimer/ChangeLog
@@ -0,0 +1,2 @@
+0.01: New app!
+0.02: Submitted to the app loader
\ No newline at end of file
diff --git a/apps/keytimer/app.js b/apps/keytimer/app.js
new file mode 100644
index 000000000..7d235f9a8
--- /dev/null
+++ b/apps/keytimer/app.js
@@ -0,0 +1,27 @@
+Bangle.keytimer_ACTIVE = true;
+const common = require("keytimer-com.js");
+const storage = require("Storage");
+
+const keypad = require("keytimer-keys.js");
+const timerView = require("keytimer-tview.js");
+
+Bangle.loadWidgets();
+Bangle.drawWidgets();
+
+//Save our state when the app is closed
+E.on('kill', () => {
+ storage.writeJSON(common.STATE_PATH, common.state);
+});
+
+//Handle touch here. I would implement these separately in each view, but I can't figure out how to clear the event listeners.
+Bangle.on('touch', (button, xy) => {
+ if (common.state.wasRunning) timerView.touch(button, xy);
+ else keypad.touch(button, xy);
+});
+
+Bangle.on('swipe', dir => {
+ if (!common.state.wasRunning) keypad.swipe(dir);
+});
+
+if (common.state.wasRunning) timerView.show(common);
+else keypad.show(common);
diff --git a/apps/keytimer/boot.js b/apps/keytimer/boot.js
new file mode 100644
index 000000000..f202bcbdf
--- /dev/null
+++ b/apps/keytimer/boot.js
@@ -0,0 +1,11 @@
+const keytimer_common = require("keytimer-com.js");
+
+//Only start the timeout if the timer is running
+if (keytimer_common.state.running) {
+ setTimeout(() => {
+ //Check now to avoid race condition
+ if (Bangle.keytimer_ACTIVE === undefined) {
+ load('keytimer-ring.js');
+ }
+ }, keytimer_common.getTimeLeft());
+}
\ No newline at end of file
diff --git a/apps/keytimer/common.js b/apps/keytimer/common.js
new file mode 100644
index 000000000..8c702de66
--- /dev/null
+++ b/apps/keytimer/common.js
@@ -0,0 +1,42 @@
+const storage = require("Storage");
+const heatshrink = require("heatshrink");
+
+exports.STATE_PATH = "keytimer.state.json";
+
+exports.BUTTON_ICONS = {
+ play: heatshrink.decompress(atob("jEYwMAkAGBnACBnwCBn+AAQPgAQPwAQP8AQP/AQXAAQPwAQP8AQP+AQgICBwQUCEAn4FggyBHAQ+CIgQ")),
+ pause: heatshrink.decompress(atob("jEYwMA/4BBAX4CEA")),
+ reset: heatshrink.decompress(atob("jEYwMA/4BB/+BAQPDAQPnAQIAKv///0///8j///EP//wAQQICBwQUCEhgyCHAQ+CIgI="))
+};
+
+//Store the minimal amount of information to be able to reconstruct the state of the timer at any given time.
+//This is necessary because it is necessary to write to flash to let the timer run in the background, so minimizing the writes is necessary.
+exports.STATE_DEFAULT = {
+ wasRunning: false, //If the timer ever was running. Used to determine whether to display a reset button
+ running: false, //Whether the timer is currently running
+ startTime: 0, //When the timer was last started. Difference between this and now is how long timer has run continuously.
+ pausedTime: 0, //When the timer was last paused. Used for expiration and displaying timer while paused.
+ elapsedTime: 0, //How much time the timer had spent running before the current start time. Update on pause or user skipping stages.
+ setTime: 0, //How long the user wants the timer to run for
+ inputString: '0' //The string of numbers the user typed in.
+};
+exports.state = storage.readJSON(exports.STATE_PATH);
+if (!exports.state) {
+ exports.state = exports.STATE_DEFAULT;
+}
+
+//Get the number of milliseconds until the timer expires
+exports.getTimeLeft = function () {
+ if (!exports.state.wasRunning) {
+ //If the timer never ran, the time left is just the set time
+ return exports.setTime
+ } else if (exports.state.running) {
+ //If the timer is running, the time left is current time - start time + preexisting time
+ var runningTime = (new Date()).getTime() - exports.state.startTime + exports.state.elapsedTime;
+ } else {
+ //If the timer is not running, the same as above but use when the timer was paused instead of now.
+ var runningTime = exports.state.pausedTime - exports.state.startTime + exports.state.elapsedTime;
+ }
+
+ return exports.state.setTime - runningTime;
+}
\ No newline at end of file
diff --git a/apps/keytimer/icon.js b/apps/keytimer/icon.js
new file mode 100644
index 000000000..a47eb21f8
--- /dev/null
+++ b/apps/keytimer/icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwcAkmSpICOggRPpEACJ9AgESCJxMBhu27dtARVgCIMBCJpxDmwRL7ARDgwRL4CWECJaoFjYRJ2ARFgYRJwDNGCJFsb46SIRgQAFSRAQHSRCMEAAqSGRgoAFRhaSKRgySKRg6SIRhCSIRhCSICBqSCRhSSGRhY2FkARPhMkCJ9JkiONgECCIOQCJsSCIOSCJuSCIVACBcECIdICJYOBCIVJRhYRFSRSMBCIiSKBwgCCSRCMCCIqSIRgYCFRhYCFSQyMEAQqSGBw6SIRgySKRgtO4iSJBAmT23bOIqSCRgvtCINsSQ4aEndtCINt2KSGIggOBCIW2JQlARgZECCIhKEpBEGCIpKEA=="))
\ No newline at end of file
diff --git a/apps/keytimer/icon.png b/apps/keytimer/icon.png
new file mode 100644
index 000000000..7dcf44b88
Binary files /dev/null and b/apps/keytimer/icon.png differ
diff --git a/apps/keytimer/img/pause.png b/apps/keytimer/img/pause.png
new file mode 100644
index 000000000..ad31dadcf
Binary files /dev/null and b/apps/keytimer/img/pause.png differ
diff --git a/apps/keytimer/img/play.png b/apps/keytimer/img/play.png
new file mode 100644
index 000000000..6c20c24c5
Binary files /dev/null and b/apps/keytimer/img/play.png differ
diff --git a/apps/keytimer/img/reset.png b/apps/keytimer/img/reset.png
new file mode 100644
index 000000000..7a317d097
Binary files /dev/null and b/apps/keytimer/img/reset.png differ
diff --git a/apps/keytimer/keypad.js b/apps/keytimer/keypad.js
new file mode 100644
index 000000000..a5edeb2f2
--- /dev/null
+++ b/apps/keytimer/keypad.js
@@ -0,0 +1,136 @@
+let common;
+
+function inputStringToTime(inputString) {
+ let number = parseInt(inputString);
+ let hours = Math.floor(number / 10000);
+ let minutes = Math.floor((number % 10000) / 100);
+ let seconds = number % 100;
+
+ return 3600000 * hours +
+ 60000 * minutes +
+ 1000 * seconds;
+}
+
+function pad(number) {
+ return ('00' + parseInt(number)).slice(-2);
+}
+
+function inputStringToDisplayString(inputString) {
+ let number = parseInt(inputString);
+ let hours = Math.floor(number / 10000);
+ let minutes = Math.floor((number % 10000) / 100);
+ let seconds = number % 100;
+
+ if (hours == 0 && minutes == 0) return '' + seconds;
+ else if (hours == 0) return `${pad(minutes)}:${pad(seconds)}`;
+ else return `${hours}:${pad(minutes)}:${pad(seconds)}`;
+}
+
+class NumberButton {
+ constructor(number) {
+ this.label = '' + number;
+ }
+
+ onclick() {
+ if (common.state.inputString == '0') common.state.inputString = this.label;
+ else common.state.inputString += this.label;
+ common.state.setTime = inputStringToTime(common.state.inputString);
+ feedback(true);
+ updateDisplay();
+ }
+}
+
+let ClearButton = {
+ label: 'Clr',
+ onclick: () => {
+ common.state.inputString = '0';
+ common.state.setTime = 0;
+ updateDisplay();
+ feedback(true);
+ }
+};
+
+let StartButton = {
+ label: 'Go',
+ onclick: () => {
+ common.state.startTime = (new Date()).getTime();
+ common.state.elapsedTime = 0;
+ common.state.wasRunning = true;
+ common.state.running = true;
+ feedback(true);
+ require('keytimer-tview.js').show(common);
+ }
+};
+
+const BUTTONS = [
+ [new NumberButton(7), new NumberButton(8), new NumberButton(9), ClearButton],
+ [new NumberButton(4), new NumberButton(5), new NumberButton(6), new NumberButton(0)],
+ [new NumberButton(1), new NumberButton(2), new NumberButton(3), StartButton]
+];
+
+function feedback(acceptable) {
+ if (acceptable) Bangle.buzz(50, 0.5);
+ else Bangle.buzz(200, 1);
+}
+
+function drawButtons() {
+ g.reset().clearRect(0, 44, 175, 175).setFont("Vector", 15).setFontAlign(0, 0);
+ //Draw lines
+ for (let x = 44; x <= 176; x += 44) {
+ g.drawLine(x, 44, x, 175);
+ }
+ for (let y = 44; y <= 176; y += 44) {
+ g.drawLine(0, y, 175, y);
+ }
+ for (let row = 0; row < 3; row++) {
+ for (let col = 0; col < 4; col++) {
+ g.drawString(BUTTONS[row][col].label, 22 + 44 * col, 66 + 44 * row);
+ }
+ }
+}
+
+function getFontSize(length) {
+ let size = Math.floor(176 / length); //Characters of width needed per pixel
+ size *= (20 / 12); //Convert to height
+ // Clamp to between 6 and 20
+ if (size < 6) return 6;
+ else if (size > 20) return 20;
+ else return Math.floor(size);
+}
+
+function updateDisplay() {
+ let displayString = inputStringToDisplayString(common.state.inputString);
+ g.clearRect(0, 24, 175, 43).setColor(storage.readJSON('setting.json').theme.fg2).setFontAlign(1, -1).setFont("Vector", getFontSize(displayString.length)).drawString(displayString, 176, 24);
+}
+
+exports.show = function (callerCommon) {
+ common = callerCommon;
+ g.reset();
+ drawButtons();
+ updateDisplay();
+};
+
+exports.touch = function (button, xy) {
+ let row = Math.floor((xy.y - 44) / 44);
+ let col = Math.floor(xy.x / 44);
+ if (row < 0) return;
+ if (row > 2) row = 2;
+ if (col < 0) col = 0;
+ if (col > 3) col = 3;
+
+ BUTTONS[row][col].onclick();
+};
+
+exports.swipe = function (dir) {
+ if (dir == -1) {
+ if (common.state.inputString.length == 1) common.state.inputString = '0';
+ else common.state.inputString = common.state.inputString.substring(0, common.state.inputString.length - 1);
+
+ common.state.setTime = inputStringToTime(common.state.inputString);
+
+ feedback(true);
+ updateDisplay();
+ } else if (dir == 0) {
+ EnterButton.onclick();
+ }
+};
\ No newline at end of file
diff --git a/apps/keytimer/metadata.json b/apps/keytimer/metadata.json
new file mode 100644
index 000000000..a982594f1
--- /dev/null
+++ b/apps/keytimer/metadata.json
@@ -0,0 +1,44 @@
+{
+ "id": "keytimer",
+ "name": "Keypad Timer",
+ "version": "0.02",
+ "description": "A timer with a keypad that runs in the background",
+ "icon": "icon.png",
+ "type": "app",
+ "tags": "tools",
+ "supports": [
+ "BANGLEJS2"
+ ],
+ "allow_emulator": true,
+ "storage": [
+ {
+ "name": "keytimer.app.js",
+ "url": "app.js"
+ },
+ {
+ "name": "keytimer.img",
+ "url": "icon.js",
+ "evaluate": true
+ },
+ {
+ "name": "keytimer.boot.js",
+ "url": "boot.js"
+ },
+ {
+ "name": "keytimer-com.js",
+ "url": "common.js"
+ },
+ {
+ "name": "keytimer-ring.js",
+ "url": "ring.js"
+ },
+ {
+ "name": "keytimer-keys.js",
+ "url": "keypad.js"
+ },
+ {
+ "name": "keytimer-tview.js",
+ "url": "timerview.js"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/apps/keytimer/ring.js b/apps/keytimer/ring.js
new file mode 100644
index 000000000..c42c11394
--- /dev/null
+++ b/apps/keytimer/ring.js
@@ -0,0 +1,28 @@
+const common = require('keytimer-com.js');
+
+Bangle.loadWidgets()
+Bangle.drawWidgets()
+
+Bangle.setLocked(false);
+Bangle.setLCDPower(true);
+
+let brightness = 0;
+
+setInterval(() => {
+ Bangle.buzz(200);
+ Bangle.setLCDBrightness(1 - brightness);
+ brightness = 1 - brightness;
+}, 400);
+Bangle.buzz(200);
+
+function stopTimer() {
+ common.state.wasRunning = false;
+ common.state.running = false;
+ require("Storage").writeJSON(common.STATE_PATH, common.state);
+}
+
+E.showAlert("Timer expired!").then(() => {
+ stopTimer();
+ load();
+});
+E.on('kill', stopTimer);
\ No newline at end of file
diff --git a/apps/keytimer/timerview.js b/apps/keytimer/timerview.js
new file mode 100644
index 000000000..48c896ba0
--- /dev/null
+++ b/apps/keytimer/timerview.js
@@ -0,0 +1,107 @@
+let common;
+
+function drawButtons() {
+ //Draw the backdrop
+ const BAR_TOP = g.getHeight() - 24;
+ g.setColor(0, 0, 1).setFontAlign(0, -1)
+ .clearRect(0, BAR_TOP, g.getWidth(), g.getHeight())
+ .fillRect(0, BAR_TOP, g.getWidth(), g.getHeight())
+ .setColor(1, 1, 1)
+ .drawLine(g.getWidth() / 2, BAR_TOP, g.getWidth() / 2, g.getHeight())
+
+ //Draw the buttons
+ .drawImage(common.BUTTON_ICONS.reset, g.getWidth() / 4, BAR_TOP);
+ if (common.state.running) {
+ g.drawImage(common.BUTTON_ICONS.pause, g.getWidth() * 3 / 4, BAR_TOP);
+ } else {
+ g.drawImage(common.BUTTON_ICONS.play, g.getWidth() * 3 / 4, BAR_TOP);
+ }
+}
+
+function drawTimer() {
+ let timeLeft = common.getTimeLeft();
+ g.reset()
+ .setFontAlign(0, 0)
+ .setFont("Vector", 36)
+ .clearRect(0, 24, 176, 152)
+
+ //Draw the timer
+ .drawString((() => {
+ let hours = timeLeft / 3600000;
+ let minutes = (timeLeft % 3600000) / 60000;
+ let seconds = (timeLeft % 60000) / 1000;
+
+ function pad(number) {
+ return ('00' + parseInt(number)).slice(-2);
+ }
+
+ if (hours >= 1) return `${parseInt(hours)}:${pad(minutes)}:${pad(seconds)}`;
+ else return `${parseInt(minutes)}:${pad(seconds)}`;
+ })(), g.getWidth() / 2, g.getHeight() / 2)
+
+ if (timeLeft <= 0) load('keytimer-ring.js');
+}
+
+let timerInterval;
+
+function setupTimerInterval() {
+ if (timerInterval !== undefined) {
+ clearInterval(timerInterval);
+ }
+ setTimeout(() => {
+ timerInterval = setInterval(drawTimer, 1000);
+ drawTimer();
+ }, common.timeLeft % 1000);
+}
+
+exports.show = function (callerCommon) {
+ common = callerCommon;
+ drawButtons();
+ drawTimer();
+ if (common.state.running) {
+ setupTimerInterval();
+ }
+}
+
+function clearTimerInterval() {
+ if (timerInterval !== undefined) {
+ clearInterval(timerInterval);
+ timerInterval = undefined;
+ }
+}
+
+exports.touch = (button, xy) => {
+ if (xy.y < 152) return;
+
+ if (button == 1) {
+ //Reset the timer
+ let setTime = common.state.setTime;
+ let inputString = common.state.inputString;
+ common.state = common.STATE_DEFAULT;
+ common.state.setTime = setTime;
+ common.state.inputString = inputString;
+ clearTimerInterval();
+ require('keytimer-keys.js').show(common);
+ } else {
+ if (common.state.running) {
+ //Record the exact moment that we paused
+ let now = (new Date()).getTime();
+ common.state.pausedTime = now;
+
+ //Stop the timer
+ common.state.running = false;
+ clearTimerInterval();
+ drawTimer();
+ drawButtons();
+ } else {
+ //Start the timer and record when we started
+ let now = (new Date()).getTime();
+ common.state.elapsedTime += common.state.pausedTime - common.state.startTime;
+ common.state.startTime = now;
+ common.state.running = true;
+ drawTimer();
+ setupTimerInterval();
+ drawButtons();
+ }
+ }
+};
\ No newline at end of file
diff --git a/apps/kitchen/ChangeLog b/apps/kitchen/ChangeLog
index 3767a9548..4e8c49c50 100644
--- a/apps/kitchen/ChangeLog
+++ b/apps/kitchen/ChangeLog
@@ -11,3 +11,4 @@
0.11: Detect when waypoints.json is not present, error E-WPT
0.12: Added stepo2 as a replacement for stepo and digi
0.13: Added long press BTN2 toggle gpsrec status in GPS clock
+0.14: Move waypoints.json (and editor) to 'waypoints' app
diff --git a/apps/kitchen/README.md b/apps/kitchen/README.md
index 102881d15..3049d9c6d 100644
--- a/apps/kitchen/README.md
+++ b/apps/kitchen/README.md
@@ -60,7 +60,7 @@ The following buttons depend on which face is currently in use

- now replaced by Stepo2 but still available if you install manually
-- Requires one of the pedominter widgets to be installed
+- Requires one of the pedominter widgets to be installed
- Displays the time in large font
- Display current step count in a doughnut gauge
- Show step count in the middle of the doughnut gauge
@@ -208,14 +208,8 @@ which will obviously limit this.
### Waypoint Editor
-Clicking on the download icon of gpsnav in the app loader invokes the
-waypoint editor. The editor downloads and displays the current
-`waypoints.json` file. Clicking the `Edit` button beside an entry
-causes the entry to be deleted from the list and displayed in the
-edit boxes. It can be restored - by clicking the `Add waypoint`
-button. A new markable entry is created by using the `Add name`
-button. The edited `waypoints.json` file is uploaded to the Bangle by
-clicking the `Upload` button.
+Clicking on the download icon of `Waypoints` in the app loader invokes the
+waypoint editor. See the `Waypoints` app for more information.
### Calibration of the Compass
diff --git a/apps/kitchen/kitchen.app.js b/apps/kitchen/kitchen.app.js
index 5564b2807..2c2cebaef 100644
--- a/apps/kitchen/kitchen.app.js
+++ b/apps/kitchen/kitchen.app.js
@@ -23,7 +23,7 @@ function nextFace(){
iface += 1
iface = iface % FACES.length;
face = FACES[iface]();
-
+
g.clear();
g.reset();
face.init(gpsObj, swObj, hrmObj, tripObject);
@@ -64,7 +64,7 @@ function buttonReleased(btn) {
clearInterval(pressTimer);
pressTimer = undefined;
}
-
+
if ( dur >= 1.5 ) {
switch(btn) {
case 1:
@@ -165,11 +165,11 @@ GPS.prototype.getLastFix = function() {
GPS.prototype.determineGPSState = function() {
this.log_debug("determineGPSState");
gpsPowerState = Bangle.isGPSOn();
-
+
//this.log_debug("last_fix.fix " + this.last_fix.fix);
//this.log_debug("gpsPowerState " + this.gpsPowerState);
//this.log_debug("last_fix.satellites " + this.last_fix.satellites);
-
+
if (!gpsPowerState) {
this.gpsState = this.GPS_OFF;
this.resetLastFix();
@@ -178,9 +178,9 @@ GPS.prototype.determineGPSState = function() {
} else {
this.gpsState = this.GPS_SATS;
}
-
+
this.log_debug("gpsState=" + this.gpsState);
-
+
if (this.gpsState !== this.GPS_OFF) {
if (this.listenerCount === 0) {
Bangle.on('GPS', processFix);
@@ -196,9 +196,9 @@ GPS.prototype.determineGPSState = function() {
}
};
-GPS.prototype.getGPSTime = function() {
+GPS.prototype.getGPSTime = function() {
var time;
-
+
if (this.last_fix !== undefined && this.last_fix.time !== undefined && this.last_fix.time.toUTCString !== undefined &&
(this.gpsState == this.GPS_SATS || this.gpsState == this.GPS_RUNNING)) {
time = this.last_fix.time.toUTCString().split(" ");
@@ -216,7 +216,7 @@ GPS.prototype.toggleGPSPower = function() {
this.gpsPowerState = Bangle.isGPSOn();
this.gpsPowerState = !this.gpsPowerState;
Bangle.setGPSPower((this.gpsPowerState ? 1 : 0), 'kitchen');
-
+
this.resetLastFix();
this.determineGPSState();
@@ -247,11 +247,11 @@ GPS.prototype.processFix = function(fix) {
//this.log_debug("GPS:processFix()");
//this.log_debug(fix);
this.last_fix.time = fix.time;
-
+
if (this.gpsState == this.GPS_TIME) {
this.gpsState = this.GPS_SATS;
}
-
+
if (fix.fix) {
//this.log_debug("Got fix - setting state to GPS_RUNNING");
this.gpsState = this.GPS_RUNNING;
@@ -271,10 +271,10 @@ GPS.prototype.formatTime = function(now) {
GPS.prototype.timeSince = function(t) {
var hms = t.split(":");
var now = new Date();
-
+
var sn = 3600*(now.getHours()) + 60*(now.getMinutes()) + 1*(now.getSeconds());
var st = 3600*(hms[0]) + 60*(hms[1]) + 1*(hms[2]);
-
+
return (sn - st);
};
@@ -313,7 +313,7 @@ GPS.prototype.getWPdistance = function() {
GPS.prototype.getWPbearing = function() {
//log_debug(this.last_fix);
//log_debug(this.wp_current);
-
+
if (this.wp_current.name === "E-WPT" || this.wp_current.name === "NONE" || this.wp_current.lat === undefined || this.wp_current.lat === 0)
return 0;
else
@@ -321,7 +321,7 @@ GPS.prototype.getWPbearing = function() {
}
GPS.prototype.loadFirstWaypoint = function() {
- var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"E-WPT"}];
+ var waypoints = require("waypoints").load();
this.wp_index = 0;
this.wp_current = waypoints[this.wp_index];
log_debug(this.wp_current);
@@ -345,10 +345,10 @@ GPS.prototype.markWaypoint = function() {
return;
log_debug("GPS::markWaypoint()");
-
- var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"E-WPT"}];
+
+ var waypoints = require("waypoints").load();
this.wp_current = waypoints[this.wp_index];
-
+
if (this.waypointHasLocation()) {
waypoints[this.wp_index] = {name:this.wp_current.name, lat:0, lon:0};
} else {
@@ -356,12 +356,12 @@ GPS.prototype.markWaypoint = function() {
}
this.wp_current = waypoints[this.wp_index];
- require("Storage").writeJSON("waypoints.json", waypoints);
+ require("waypoints").save(waypoints);
log_debug("GPS::markWaypoint() written");
}
GPS.prototype.nextWaypoint = function(inc) {
- var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"E-WPT"}];
+ var waypoints = require("waypoints").load();
this.wp_index+=inc;
if (this.wp_index>=waypoints.length) this.wp_index=0;
if (this.wp_index<0) this.wp_index = waypoints.length-1;
@@ -520,7 +520,7 @@ function STOPWATCH() {
this.redrawLaps = true;
this.redrawTime = true;
}
-
+
STOPWATCH.prototype.log_debug = function(o) {
//console.log(o);
}
@@ -531,7 +531,7 @@ STOPWATCH.prototype.timeToText = function(t) {
let secs = Math.floor(t/1000)%60;
let text;
- if (hrs === 0)
+ if (hrs === 0)
text = ("0"+mins).substr(-2) + ":" + ("0"+secs).substr(-2);
else
text = (""+hrs) + ":" + ("0"+mins).substr(-2) + ":" + ("0"+secs).substr(-2);
@@ -551,7 +551,7 @@ STOPWATCH.prototype.stopStart = function() {
if (this.running)
this.tStart = Date.now() + this.tStart - this.tCurrent;
-
+
this.tTotal = Date.now() + this.tTotal - this.tCurrent;
this.tCurrent = Date.now();
this.redrawButtons = true;
@@ -623,7 +623,7 @@ STOPWATCH.prototype.drawLaptimes = function() {
g.setFont("Vector",24);
g.setFontAlign(-1,-1);
g.clearRect(4, 205, 239, 229); // clear the last line of the lap times
-
+
let laps = 0;
for (let i in this.lapTimes) {
g.drawString(this.lapTimes.length-i + ": " + this.timeToText(this.lapTimes[i]), 4, this.timeY + 40 + i*24);
@@ -645,7 +645,7 @@ STOPWATCH.prototype.drawTime = function() {
g.setFont("Vector",38);
g.setFontAlign(0,0);
g.clearRect(0, this.timeY-21, 200, this.timeY+21);
- g.setColor(0xFFC0);
+ g.setColor(0xFFC0);
g.drawString(txtTotal, xTotal, this.timeY);
// current lap time
@@ -691,7 +691,7 @@ function HRM() {
this.bpm = 0;
this.confidence = 0;
}
-
+
HRM.prototype.log_debug = function(o) {
//console.log(o);
}
@@ -782,7 +782,7 @@ Debug Object
function DEBUG() {
this.logfile = require("Storage").open("debug.log","a");
}
-
+
DEBUG.prototype.log = function(msg) {
let timestamp = new Date().toString().split(" ")[4];
let line = timestamp + ", " + msg + "\n";
diff --git a/apps/kitchen/metadata.json b/apps/kitchen/metadata.json
index ab2e7183c..9c9f7b2ec 100644
--- a/apps/kitchen/metadata.json
+++ b/apps/kitchen/metadata.json
@@ -1,14 +1,14 @@
{
"id": "kitchen",
"name": "Kitchen Combo",
- "version": "0.13",
+ "version": "0.14",
"description": "Combination of the Stepo, Walkersclock, Arrow and Waypointer apps into a multiclock format. 'Everything but the kitchen sink'",
"icon": "kitchen.png",
"type": "clock",
"tags": "tool,outdoors,gps",
"supports": ["BANGLEJS"],
"readme": "README.md",
- "interface": "waypoints.html",
+ "dependencies" : { "waypoints":"type" },
"storage": [
{"name":"kitchen.app.js","url":"kitchen.app.js"},
{"name":"stepo2.kit.js","url":"stepo2.kit.js"},
@@ -16,6 +16,5 @@
{"name":"gps.kit.js","url":"gps.kit.js"},
{"name":"compass.kit.js","url":"compass.kit.js"},
{"name":"kitchen.img","url":"kitchen.icon.js","evaluate":true}
- ],
- "data": [{"name":"waypoints.json","url":"waypoints.json"}]
+ ]
}
diff --git a/apps/kitchen/waypoints.html b/apps/kitchen/waypoints.html
deleted file mode 100644
index d02260732..000000000
--- a/apps/kitchen/waypoints.html
+++ /dev/null
@@ -1,170 +0,0 @@
-
-
-
-
-
-
-
- List of waypoints
-
-
-
- Name
- Lat.
- Long.
- Actions
-
-
-
-
-
-
-
- Add a new waypoint
-
-
-
-
- Add Name Only
-
-
- Add Waypoint
-
-
-
-
- Reload Upload
-
-
-
-
-
-
diff --git a/apps/kitchen/waypoints.json b/apps/kitchen/waypoints.json
deleted file mode 100644
index 98a670c0d..000000000
--- a/apps/kitchen/waypoints.json
+++ /dev/null
@@ -1,20 +0,0 @@
-[
- {
- "name":"NONE"
- },
- {
- "name":"No10",
- "lat":51.5032,
- "lon":-0.1269
- },
- {
- "name":"Stone",
- "lat":51.1788,
- "lon":-1.8260
- },
- { "name":"WP0" },
- { "name":"WP1" },
- { "name":"WP2" },
- { "name":"WP3" },
- { "name":"WP4" }
-]
\ No newline at end of file
diff --git a/apps/lato/README.md b/apps/lato/README.md
new file mode 100644
index 000000000..556ee6fbc
--- /dev/null
+++ b/apps/lato/README.md
@@ -0,0 +1,54 @@
+# Lato
+
+A simple clock with the Lato font, with fast load and clock_info
+
+
+
+
+
+This clock is a Lato version of Simplest++. Simplest++ provided the
+smallest example of a clock that supports 'fast load' and 'clock
+info'. Lato takes this one step further and adds the lovely Lato
+font. The clock is derived from Simplest++ and inspired by the
+Pastel Clock.
+
+## Usage
+
+* When the screen is unlocked, tap at the bottom of the csreen on the information text.
+ It should change color showing it is selected.
+
+* Swipe up or down to cycle through the info screens that can be displayed
+ when you have finished tap again towards the centre of the screen to unselect.
+
+* Swipe left or right to change the type of info screens displayed (by default
+ there is only one type of data so this will have no effect)
+
+* Settings are saved automatically and reloaded along with the clock.
+
+## About Clock Info's
+
+* The clock info modules enable all clocks to add the display of information to the clock face.
+
+* The default clock_info module provides a display of battery %, Steps, Heart Rate and Altitude.
+
+* Installing the [Sunrise ClockInfo](https://banglejs.com/apps/?id=clkinfosunrise) adds Sunrise and Sunset times into the list of info's.
+
+
+## References
+
+* [What is Fast Load and how does it work](http://www.espruino.com/Bangle.js+Fast+Load)
+
+* [Clock Info Tutorial](http://www.espruino.com/Bangle.js+Clock+Info)
+
+* [How to load modules through the IDE](https://github.com/espruino/BangleApps/blob/master/modules/README.md)
+
+
+## With Thanks
+
+* Gordon for support
+* David Peer for his work on BW Clock
+
+
+Written by: [Hugh Barney](https://github.com/hughbarney) For support
+and discussion please post in the [Bangle JS
+Forum](http://forum.espruino.com/microcosms/1424/)
diff --git a/apps/lato/app.js b/apps/lato/app.js
new file mode 100644
index 000000000..6045d7f17
--- /dev/null
+++ b/apps/lato/app.js
@@ -0,0 +1,138 @@
+/**
+ *
+ * Lato Clock
+ *
+ * The entire clock code is contained within the block below this
+ * supports 'fast load'
+ *
+ * To add support for clock_info_supprt we add the code marked at [1] and [2]
+ *
+ */
+
+Graphics.prototype.setFontLato = function(scale) {
+ // Actual height 50 (54 - 5)
+ this.setFontCustom(
+ E.toString(require('heatshrink').decompress(atob('ADkD8AHFg/wA4sP/AHVD44HHgPALD0OA40+F43+H4wHGn/8A4v/L4sH/5PFj//CxkD/6eFCw9/GooWHh//wAWLgP/TgoWHn5rFCw41BMYqCHaRDKGgzLYKAJgFv//LIhQBAAI7DWgIABU4adBAAJTDn4HCVAaOCQQhvDAYQuBDYaxBgJEDh4HBgYzDPgUDIYYECA5DUDgIHBg4HEEgIHfF44/EA45HDL4xvHP46PHT5CvHX47PDGYcDb4zvHf5AA/AA9wA4yoDZYq/DXAgHDXYQHEXYQHEj4HHXYQHDn6UCA4d/e4sAXYYHCd4gHCXwbADA86DFA/4HGAGA3Db40HA4UDe40Hc4YHCh7nDA4UfA4X/A4U/A4b/Cv7vGX4UB/A+CZ4YaCgf9A4sH+IHCHwfjA4JWDj/DA4s/wYHFv4kCA4f+A4pKBA4sD/AHCG4R9BA4YCBj/gA4s/4AECN4R5BA4f/gf/Mgn///+A4wZBA4d//6JBA4c/VATHEVASUEEwIHEAAbnDAGbyCAAg+DgKwDA4S4DLQSlCSYQHCn4HDFAV/bAX/4ADFCYgbCh4zHZ4SlBR4iSEA46XCe4QHCDgJWCngHOnwHGvwHRG4iFBI4ppBA4f4OIRnCN4MD9+AO4f///v8CHCDoP/54CBS4f/44CBU4f/wYHBX4f/EQLHDh6gB/6jDZQaTDAEUcA40/A4xODYoYHGgYHGh4HGNIIuG74uGz4uGj4uF/gHFh/4A4sf+AHFn/AA4q0BA4kBVgIHEFwIHFFwIHFj7jBA4guBA4rjCA4YuCA4guCA4r0BAATgBA75SEa4wHvAEEBA40DUYIAEg4HDgZ0Bh67BXAQHCZYJMBA4UHA4KPCA4SXEAgQHL4IEBgIHC/AMCgP4CQUDFgIHoIQY3DA4wCEDggHFO4YHB/iHDCQX+gE/S4IHCOIP/U4IHCv6CBA4k/A4K1CEQKpBEIIHDh//HILSDTQK+CAAd/f64Amn4GFgLxCAAZfBSIIADN4heDP4YeDR4Z5CEwN/U4IABg4NBj6ADEwLHDIoQtBVgQuCHoIHDFwIHBe4QLB/14A4kH/i1BeQQuB/AHFn/wA4pLBA4guBwAHELoMAA4o9BA4Q3BgYFBJ4gCCA4pqBvxvDf4T2Bh4HCIIc/R4MCKISfBS4aQCU4gHDX4ioBY4paBNwQAD/6uDAAUOf4wAjO4QHNdQYHYmAHGW4gUEA4kPA4z7BA4v/A4qYBY4QHCh63CA4c/V4QHDV4Y6DV4YHCDwYHDDwYHDv7ODA4MBZwgHBcwL1DA4MfdogHBDwgHB+LtFgf3DwhMCDwgHCDwhcFA4geEA4IeFA4IeFd5AArj77EsCgB/gGCg5QBOQkf/6oB/77D//DA4JrCv//44HB4DkC//n/E/MgIcCRIMPA4X8RIUHegQCBFoL8DA4cBA4QaBv4HGvwHBTgMHHQM+HgIHhF44HFJ5RfGN45/Bz6NBP4SPHT4XnT4ivHX47PHgCQCb4bvIAHxdBMgRfD/58CKgf/WgIADP4JlFR4J1ET4QHCiACBQwQEBuC/CDIIHBX4QtBn+Aa4sfZ4bvCh+Ah4HGUAUHA4d/AgIHEa4QHDwJyCA4eDKIQHDx5pCA4bPDG4c/RIRPCjwuCA4aJBUwZnCRAcBP4SgE/+D/7+ET4ImDA4jIEX4KvFh7HGgbXGgF+f6oAggZeBSgShEb4RYCagQHGh5iDA5QXEE443HADoA=='))),
+ 46,
+ atob("DhglJSUlJSUlJSUlEA=="),
+ 64+(scale<<8)+(1<<16)
+ );
+ return this;
+}
+
+
+Graphics.prototype.setFontLatoSmall = function(scale) {
+ // Actual height 25 (24 - 0)
+ this.setFontCustom(
+ E.toString(require('heatshrink').decompress(atob('ADE//lwj/+nEP/kcDCGAgEfAQIAFgfwgEB/AZIggCB4YCBsO8gEz/0Av/8gP/DgP+jAiBhvggO/+ED//gh/9wEH+HAgEYsEAhhMJkEB8E4gf4h0B70HgPDgOA4P//f///9//4mEYjk4h0PnkDgZEBwH8FhMfAQXAiEYDoMMjE8g0MDwOOnBgBvEAnwCBgFwAQJsBgHn8ACBGIPjg0B4ODgPA4OA4FxNoIvBgEHAQIAGVIMBTAM4/6bB8PAv/gsE4+BmCJQMHhgvB50D4F2gHgLwMwh7eCFwM+JwJhBZwgAHGwMAvwNJe4QCBv4TBVYPB/EB/J0Bj1AgECC4rZC8/AgfxDAPgn//4BsCABECEQMBkCkDCgaBBGQICBhJhCwAgIUgVgAQMwAQJ4CIoMB/4pB/6uCD4QYLABMHJgMDJgT0CO48GSogxMAA0cAQMOGIQMFVoMBAQMHAQMPEoM/agcBMoJIDGYTWDTIMHXQMH4H4g+Aj0DwEHJgIBBDAPAJgNgnEAuEPXgIeBSoQCBj49BABYjBjA+BhgCBgwCBga3B/+AAQPAv5EBZZIAK4CKB8D5B+ACB/CQB9iwB8ywB8Y9B8OA8Hg4E/8FgKwMwPILkLhA/BWgM8gY0CuACBnEMAAMGAAMDg5jB4eDMYPjGIO/4EPx6dBeAYACAoTZCZATOBgPMAQPGZQPDAQOBwDEBSYKPBSoyLDOgIAJjixB/4gB/+B4FxwFgmHAmEYsEYhk4CYMcjhjB/0BwP+D4N8FZSFBgEHLQMPCoMPPgMPGIMe4FgjwzBjwzBhwuBgkPToMDFwQCCBAIAFe4prBgTgBg6gBh6EBj8AsE+gEwKAMYIYMNUgMHVQJPCXIwADEIMH5/gg///EHj0OcAeDDQPBGYNgAIM4+Fwh/8vEH841B8ICBABZBCh4RBg57Bg8HGIMBx4vB58A4FvgFwv0AngCBGIJdBAQIMBFY8CgGAcoPggPACAJPBCQ0ICYI2B2CaB+A4BExBUCcwSTBgZaBgOwAQPcBgPGAQPjB4KGBJQIkBdIJ/MAAczAQMZDwMMDwMGsA0BmAxBzAPB5gCBswYKAB7QBI4ICBjhNBgwuBge4B4PYAQN8AQMcAQMOUoYAJDoMAVIUYhk4hkPjkGh6pBxxcB/wPBbIJTBD4s/LQN/foN4jyYBV4LVCmF+nEwv+MjFw80MuEjLIMw4cYmFhx/8mHH/0wseBzC2BxkGZAMB8bLBv40Bh6VBAAb0BPoIMCc4MfI4QLB/+Agf44ED+FggPgO4P8F4M/xgdBNoQYCg/wAwIJCh4xEPAv/+Ef//4h///kGAAMDAAMBwOBwHAAANgAAM4uFwj/8nEH+/4gPx/gmBvDHGgILBgZdBg//8BuB/CpBjgCBg8BNAOA8AEBsC0CcoL4BnD4BjkHaoIQBA4NAUwIAIMYo3B/0DH4J6BAIKmBGIydBjAxEhwxDf4SPBUgKhCOQQAHN4P/gICBwACBSocwAAMYAAMMAAKuKJQIsJAAJjDGIrGBMIJkBGAJhBMgIwBgAwBJQJ9Be47KEEoOAFYPwgPgvB8CY41wSo5hBhkHgZjBwOHLwIlBuF/wEQn45IMZR7DMYIwBAQIyBMgICBeQRjBMggYBv//8DNB+D5CTxcAY4QYFCpgyDPgIGB8ACBQIMAvEPGgJjB/hjBHRpKCDAP8PgRjGXAIIBEIMD7wCB47HB4HgAQM8YQMPC4IEBSwkgGgoxFVwSWHV6QAEVxEHEYR4Cj4aCRwQJGCYIWCAQUPIYIOCnwCBvgxKcCsHUIgoDh5AEgDyCjwCBCwiVKABH8C4P/XgI3BDoN8gPAhwCBY4PgAgNwgHgVgMwVgMYh0AjkHgEOCYIEB8ACBnhRBOwIrCGgN+H5I9BCQJ8FF4bcBhjcBgzcBgb1BgPBPgPxAQM/RgMPAQJSBFgkfQoM/R4N//xKCwE4CYM4gFwjkAnBjCGYx8ICoP4g+BVAXzwF/8C1B4CLBAA75FY4RjGwBXBF4VgR4M4m+Ah/5UYP4nkB/BPBDQIqCNQIABZ4Q9BIAPwMYM4Y4MOGYMHGYOBwJjB8IzBvPgjEf8EMg6ZBE4J8BF4ZKBAYKkCsACBNoRjFg//QQIYOcQIAGTYPwfILHBfgTbEQYKBBAQL9BY4ICBg4dCCoICCg/AVwPAJQKCBE4IxDJQRuCBYUfGAMD/DLCEIXwAwK7BgEPCYQMBv4cDDASuBAQIoBg5CCPgoqCv4GBj/8AwJKBDIIGCGIU/BgQfBBYIrCn4UBj5CCGIMDLQU/AwxhCBIIcD8ClBwEHKwimDAANAY4NgVIPwGIPwgfBDwP5EgMfNQQtEJoUH74CBwfgh+A/B8Bjx8BgcBFwOAP4TbCnhgCdAStCvgJCPIJUCAQPAB4Q3CHoUPAQKuGMQYABMYUwMAMYXIMMgP8g0B+xIB8cBwfhwHD4HAs/AsE34EwdYMYJoMMg8AgxjCGAoADv///8/AQOYBAPMAQNkAQMQfhDdCgZ5CnwCBg5tBQAgXCBITLBC4JmBgIxC543B84CBEYQAKkC4EewQWBgIsCAQTEGBIQ1BABcMAQMGMQRvFZIK9CEAa9BDAwMDjh7CD4oANLYMw/gpBvwfBsYsBmOAg0Y4EDhlggPmIAN/O4MfDAIALTwPgcAIuBuBMBmBWBmBWBjBvBhhPBg8BGIP3OQIbBgE/PAQAEgY6Bgf/AQPvwEDwHgEAIuB4CIBsAxEjiBBgykCAAouCv5NBn18gE4hxKGEAJKBXoXA8E///4j//Phf+PoS5B/pjB804gFjJQMxwwxB4YxBsJ8BmPAgP4GIN8d4QABoDnEUoICCwACB4DPBE4IzBZ4IZCgUAh0T4EP3/wh//jEGmeMgcI40BwwdB4wdBu4dBn+O8Efw/gPgPgEYKXHPgqzBMAICESoLKBAQLlBLQTXBn4YBgaMCAAhcBgHjewNx//wnAYCAAliAQMzx///PHB4IYBbQIAHOoP+g6VCgCkCXoLwBAQN/HIN5ZQN4BgMwfIMQLYRJDAAd/GgL5PHAPAgZjBgB8CAQRACcAUcKIeAgIYBAQPgSoYYIj4uDDAY+JZwauBKILEDY4xrCEAVwDAhHBagR8GB4IIBn5pBnzKBnCVBjApBhgpBGIJ8BLYJjBFgN/ZoMfAQMHaZJKBagJpBHwNgO4Nghh8BFIIxFg4sBDAJeBAQUfQo8DZoKVBAoPnDAOAVwOAFwPAjAxEAQMMMwJ/B/AbBdpSuIg6rEGIKuHcAQACg+GAQPDMYPwJQMYSoOOJQPHJQNhHoM4AQMIngeDgg0FPgJWB+BWDSoxFBA4LjDVwIFBDALKBN4R5BFIZbJAQKIBDAgAEhBpCDQMDdgV/AQMP8ADBDAUeTIQCBTYMBE4IYCcoILBHgQ5CnwCBj4hBgIYBeAcBBIKcBBAY3CSIUfIgQ6Cn4YEGoUPPgQLBGIS2BAASVCY4OB8EB+IbBn4CBh4YBgYCCLoMH7wCBw4YBwAYBgEQEwdAJRHwS4N+BQMPQYRUBJoUHGgJpCj7MEfITgDwFwKQN4SoN8fIP2MYPzfIPhwAkBJQPgsCuBmAYBOghUBgHB4OB///+P/7/8HgMGYAMDkDJEABEDDYP9AQLOCABJcCmYCBjPHx8cDAP8j4CBGwIaIH4MAOQICDL4LUDRIUHQwYDBLYMATwMAUgIbDAA8gS4NwnEAvAnBv8MgH+4kA/MygP4zsB+FOwfwl1D8EUk/ghkX4EEh/AgUHWAL8BgOBwEDIQUAnEGgUYh8BxEPwF0j9Aj0fkEPn0Qh2+hEOvkEgk8gRvBgZtCXRYANg8f/kDz/+gPD/4WNdwTcBgP/LgPgT4PgRoNgRQM/BgLXB4F8WoLWB4AEBDAOAagQAEMQMARQIrBweAeYPAEgPgAoMwjkYjEMAAMGAAIXBgcB4KjBbgPAIQT9EAA8wbwJ4BPgICBgeOHQQPB4KhBsBTBnBsBj/wgEd7kAHgIqJPQInBwMggPwyEAn1IBIPkBoJjBgF/EoP//EB/VAgfxkED8GQgPADALhOj/4v/v/k/EoIALjhsBz6SB//Dwf88HBx04sHHhkwsPHjEw8f8jk//kEh+8EYpKBAYJeBgCxBAAcIAQMOPgQVCDgsH/ACBx4SBEYMGnEwg1/zECv/mgdwucB2EcwGYg1ApkDkE2gOQjeBzEE4HMgazC4ACBmA8Bh88OYLjBAAsIK4MMSAMGtARBkhQByzGCUoS1MD4MBFYMD44FBxIWBj4OBn6KBniHBhCECABcwAQMYAQJcBJAICBM4IrBIISyCsACBnxOEh4MCAA6uJGoICBjEC//2gd//cB2PAVwNgVwM4kE3n0QjP77EEvHsIwMcVx50CL554CiBWEgYPBgbHBgYwBgOGBgPDAQNhVoQCBg7KIgw5BgY5BgOBCAPAFINgsED/+wgP/7CxBF4IYMAA8MK4MOnAaBPATABgP7B4N5I4VADAhzBDAdMDAMmDAP/B4N/DAMBbZKVE8ACETYQAGn//8Ef//wh//e4LzBWY8YJIQCDDALdB/6jGg4CCHAMHEwMHdgKeBLoQXB/40Bv///gvIg4PBwYCB8ICB8BIIbQgZCFYMDCYMBO4QAMgSzBgegAwPwAQKzBAA8QJoUggEcAwMPNIgjBDA8PIYMPIAMGKgMDhBJBwgPB9y6CAQMOJ5cOQgMDzwGBGoWcFgPhOAM9AQMHOwTAGLo0cLocILoMMLoMeLoM8PYl4Qgi2BUIPgU4PQgeB5ACB40BbwI4BHYY+DgjGCOwMHgkGgf+g75Bh4NBMwUcAQI9CHQVwAQPxw0B8PHgPA4+A4FnOAM+fYMOfY0DjACBwxFByIJB5gaBv/B8Ed8YeBgZQBg7LCVwkeX4M8hqBBjmAAQNgiDxCfYICBM4J2DgaQBgbIBgOOgPHwcA8fBwFx4A4BUAICBLQLDDEoR6C/weB/kgg/4jA3Bh0/xkDn8GgHegcAl4qBgf4FQM/FQIYBEIN/AQMPGgTbCGIRtBNwISEG4JZCfoMAj/GgHfLgPvNgPnfINh/lgmEfOQg7BT4QxCbAQxCGhkDGgMDz/GgOfGgPPGgNnGgM5GgMMGiaGBcIcfQwQFB/8B4P/gHH+IuDmfAsEN/Ewh1/jEGPgQYBTYjBBMAgxCeYYxBCQUf4JvBwPA/+A8bHBgfwsEB8EwgH8jDgCg8D/0BDAJmFRgIoBGIkAGIIgBCQY3Cn4LBv/AmKSBnpjBiXwjEPv0MgbgCgJmCDAUD/DEESoQxCTwsPBAMfAoM/C4l9BAJsBgJsHCIJfBn//IQMf/EMAAMGAAMDAAMBwOBwHAAANgAAMwAAIgBAIIAFVobABFYLCBg/AjkA8ACBnEOgEco5dB0ZjB6OAgO4DoPcuAVBnEAuAVBuECgBaBABEf//4h///nH//+sZaBnJaBxxZB4ZaGAAJzDgABBABaMCGIqMChYxBhoxBxgxB5wwBswwBmQxDgABBABf///Av//8G/GgPYDYPMJoNmGgMzGgMZGgOGGgPBMwaJLFw/nMYPzzAuBxjwHSoWcMYguJ4ACBsACBnIuBxwCB4ZgCIhgABgoVBwwCB4xKCBYMDAQMBCQUgAQI7CmYVBjICBxgbCCoWAAYNAAQI7CuACBiISBwB8FGIQYBgJgCGQcYAQLPBg///0DDYMBEISFB4CFDAALpDhCeBgCeBwEHFYIEBuACBj0HBQIhBEoI8Bn6JKj///EP//8IIUA+BJBnkAh0PwEGgPgSQN4GgMeYIMDJoIWBMoM8LQZ8EY5UfI4QyBv43BngyBnEB404gFzjjvB50Ajl2OYMbUIJzD8AEBXAPgCoPgg+BKIP/FYQ9Bh62DAAcB/AjB/64CDAPA/ApBjilFwQxB4ZwBsewgEzzhKBxxKBw6OBCoMORYKJBn4rCNIMA/h8V4G4gFw7kAnB8C8x8BuZYBjJKBwxKB4J8niZ8BjoxBxgxB4x8CmEAmBKFo58m475CVwIxDgx8BgZ8EzhKB5x8YAAUYhhbBLIMDcIMA9wCBnwCBBYIeBKYMOEgMOJYIYCgRFBABJsEwF///AvhBBdIU4gfwjkD3EOMQMGg5sBg8DPoK/B94VBvxpBUwPgj+B+EfNgMMNhzmD//8gP//8QOIOMBwPnAQNxKQMYoEAhkgO4cHAQh9BMAOAcwOAj7jIAAIxB/EAGgK6BPIILBKILgBGIMcGIMGmBEBUYMDzAdB5ACBDAMDOYJdBh4CBMYKxKv/+WII0BRIQxBnBjCGIMHAQMBGIMA5wCB8YCBuB8BuAFB/AxB/CVB/BjBVBQxBwBKB+AYB/gfBhxjCzhgDgFgAQMxAQM5BIMcQYMcBAM+GIJdBAQV/+AxDaILCEKIMBBwU+YwcBD4PhZAPxAQP54APB4F8gFAYYMBAQMADwRICRgIAGTwPwNgP4NgLtB4CaBsEYgEwhkAjEGJQMHJQMDIIPnJwKVCn7cCFw7gCTAQCB/BsCEwMMgcDg8BwfBwHD+HAue4sEfx1wh+D+EBwJkCFgw3Bh/AoOH8Ex40wjnDjEPsOMgOw40Aj1mLQLdBdpkAvBzBvxMBn52BmOAi0Y4E7hlgnOGmEY+cwhE/SoMPcAIAJgY0BNQMGsP4g1xxkHmHGgcYscBxgxBs+ZwEZSoMAv7YCABEeTgMfwFjPgNzxlgmJKBPgQ0BxkDnnMgeP/5DBPgIoKGgMw/kHPgMDzhKEgx8BkZ8C81gjl/YgMfPgIAJg6xBY4J8CjnjjEfJQMIY4MG7AxB98zJoSSBPgTKLhjKBh0/JQMw4CfBsBUBmEA404gFzAQMfKAMHKAMH94CBmIYIjAYBhgYBxwSB4w3BIwIADv4CCBIN9SoNwjlAmEHkEYgfQhkBdoMA7kDwAVB4FgRY4eBJQuHJQNjJQM5JQMOJQVjJQM5JQMPJQMD8aLHQoICBTYM/SQIYCjHDgHssOA8yVBGIUh5lwgF+P4MfGgIAFgJNBgP/gER/fAjIYBjhKBhhKBg0xw0DzAxBt1jwED+JEB/CcFAAMPJoMP/EDx/egPODAJKCY4oxCh1zjkCj+MY4gABF4MAVIU5//whn//ECv5aBABE3//Amf/8AYCO4UCAQMDAQUf/8Bx//wHnDAKNBgE4AQMcAQMEGIU//0Dz4YBOg0/AQN/8EQnhNBnEcg3Yg0D9g2B80BwFzgHAvnA8BQB8C3BcASeHAAUHJQMTCQM4CIMYAQMMQwMDK4MBzACB5wYB44YBFYcf+AoHBAMHNIMH78Aw5oBsawBnOAmEO4CXBsEMQwMOcYP+HAICBgACCAAsfJYI3Cj98T4MHKgJ8Bmx8BP4IxDiBoBPgP4FwICBgYCBAA1/AQQuBvvwg1wFgMwVwMYgcBxgxB8wxBmeAPgKrDAQMPAQIAFgJ/BSQMAmP34E54FwVwMYVwMMGISuBGIPOgeA4f/wAuBAQM/AQKuKgOHVwPnVwJ8CKQLYBGIMOGISuBgKuPYYMAgwCBgZgCHoMI4kAh1nHQJ9BgeZSoIYLABM/xACBRIM8boM4n0AjE7EgLYBXYPAgdwMYPwuEA/p2B/4CBs4CBQoxpC//AWgPwiAsBJgJ8BJwKNBJwoCBDAPgDARWJj/4dILdBg4uBBQLwCmEOLYICBhh+Bg0CQYQYBwAYEAA9/EIKCCz4uBJoOABQPAsDgBbwMwjgxBFIMYDAJzBj4YBABBjBNgJmBh1//h8BhwsBZY3AVINgnACBhH/OYIYBFA0IVwQaBgaRCv0BOAPHwA4B4eAn+DwAMBwAhBwYnBgZnBXYIbBHgTZF///+YCB/EDwInBwD5BwB+B4B5BsDiBnCzBj5/BewUAAQRrCAYRQDCIMP4EjwP4+PAn5IBg/wkAMBnEfwEcv8Agl8DYKGBgEQgA='))),
+ 32,
+ atob("BQkKDw8UEgYICAoPBQkFCQ8PDw8PDw8PDw8GBg8PDwoVERAREw8OEhMICxENFxMUDxQQDQ8SERkQEBAICQgPCggNDgwODQgNDgYGDQYVDg4ODgoLCQ4NEw0NDAgICA8AAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAADAAFCQ8PDw8IDQgUCQwPABQICg8ICAgOEQcICAoMEhISChERERERERcRDw8PDwgICAgUExQUFBQUDxQSEhISEA8PDQ0NDQ0NFAwNDQ0NBgYGBg4ODg4ODg4PDg4ODg4NDg0="),
+ 25+(scale<<8)+(1<<16)
+ );
+ return this;
+}
+
+
+
+{
+ // must be inside our own scope here so that when we are unloaded everything disappears
+ // we also define functions using 'let fn = function() {..}' for the same reason. function decls are global
+
+ let draw = function() {
+ var date = new Date();
+ var timeStr = require("locale").time(date,1);
+ var h = g.getHeight();
+ var w = g.getWidth();
+
+ g.reset();
+ g.setColor(g.theme.bg);
+ g.fillRect(Bangle.appRect);
+
+ //g.setFont('Vector', w/3);
+ g.setFontLato();
+ g.setFontAlign(0, 0);
+ g.setColor(g.theme.fg);
+ g.drawString(timeStr, w/2, h/2);
+ clockInfoMenu.redraw(); // clock_info_support
+
+ // schedule a draw for the next minute
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = setTimeout(function() {
+ drawTimeout = undefined;
+ draw();
+ }, 60000 - (Date.now() % 60000));
+ };
+
+ /**
+ * clock_info_support
+ * this is the callback function that get invoked by clockInfoMenu.redraw();
+ *
+ * We will display the image and text on the same line and centre the combined
+ * length of the image+text
+ *
+ *
+ */
+ let clockInfoDraw = (itm, info, options) => {
+ //g.reset().setFont('Vector',24).setBgColor(options.bg).setColor(options.fg);
+ g.reset().setFontLatoSmall();
+ g.setBgColor(options.bg).setColor(options.fg);
+
+ //use info.text.toString(), steps does not have length defined
+ var text_w = g.stringWidth(info.text.toString());
+ // gap between image and text
+ var gap = 10;
+ // width of the image and text combined
+ var w = gap + (info.img ? 24 :0) + text_w;
+ // different fg color if we tapped on the menu
+ if (options.focus) g.setColor(options.hl);
+
+ // clear the whole info line, allow additional 2 pixels in case LatoFont overflows area
+ g.clearRect(0, options.y -2, g.getWidth(), options.y+ 23 + 2);
+
+ // draw the image if we have one
+ if (info.img) {
+ // image start
+ var x = (g.getWidth() / 2) - (w/2);
+ g.drawImage(info.img, x, options.y);
+ // draw the text to the side of the image (left/centre alignment)
+ g.setFontAlign(-1,0).drawString(info.text, x + 23 + gap, options.y+12);
+ } else {
+ // text only option, not tested yet
+ g.setFontAlign(0,0).drawString(info.text, g.getWidth() / 2, options.y+12);
+ }
+
+ };
+
+ // clock_info_support
+ // retrieve all the clock_info modules that are installed
+ let clockInfoItems = require("clock_info").load();
+
+ // clock_info_support
+ // setup the way we wish to interact with the menu
+ // the hl property defines the color the of the info when the menu is selected after tapping on it
+ let clockInfoMenu = require("clock_info").addInteractive(clockInfoItems, { x:64, y:132, w:50, h:40, draw : clockInfoDraw, bg : g.theme.bg, fg : g.theme.fg, hl : "#0ff"} );
+
+ // timeout used to update every minute
+ var drawTimeout;
+ g.clear();
+
+ // Show launcher when middle button pressed, add updown button handlers
+ Bangle.setUI({
+ mode : "clock",
+ remove : function() {
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = undefined;
+ // remove info menu
+ clockInfoMenu.remove();
+ delete clockInfoMenu;
+ // delete the custom fonts
+ delete Graphics.prototype.setFontLato;
+ delete Graphics.prototype.setFontLatoSmall;
+ }
+ });
+
+ // Load widgets
+ Bangle.loadWidgets();
+ draw();
+ setTimeout(Bangle.drawWidgets,0);
+} // end of clock
diff --git a/apps/lato/app.png b/apps/lato/app.png
new file mode 100644
index 000000000..02a4031a3
Binary files /dev/null and b/apps/lato/app.png differ
diff --git a/apps/lato/icon.js b/apps/lato/icon.js
new file mode 100644
index 000000000..746f010dc
--- /dev/null
+++ b/apps/lato/icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEw4UA///1NygH+zn/Jf4AJgdVAAnABZ8BBYtABbc1BYtcBYcVBYtUBbcC1QAEwALPgYLFQYoLWgAHBytWAYK0F1Wpv/9tQLH0v//9aBY+XBYPWBY3qz/1r/21YLGv/Vq/9BY3Vv6NB/tXBaMVBYamEBZ1fHYP1BY01r5TB+ruEBYVXNYPVBY9VBYNVBY0FqoiBqtQBY4ACBb0NBYdwBbsBBYdABYoA/AAg="))
diff --git a/apps/lato/metadata.json b/apps/lato/metadata.json
new file mode 100644
index 000000000..0b5e4a0f3
--- /dev/null
+++ b/apps/lato/metadata.json
@@ -0,0 +1,16 @@
+{
+ "id": "lato",
+ "name": "Lato",
+ "version": "0.01",
+ "description": "A Lato Font clock with fast load and clock_info",
+ "readme": "README.md",
+ "icon": "app.png",
+ "screenshots": [{"url":"screenshot3.png"}],
+ "type": "clock",
+ "tags": "clock",
+ "supports": ["BANGLEJS2"],
+ "storage": [
+ {"name":"lato.app.js","url":"app.js"},
+ {"name":"lato.img","url":"icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/lato/screenshot1.png b/apps/lato/screenshot1.png
new file mode 100644
index 000000000..14c8d6d04
Binary files /dev/null and b/apps/lato/screenshot1.png differ
diff --git a/apps/lato/screenshot2.png b/apps/lato/screenshot2.png
new file mode 100644
index 000000000..f40495c79
Binary files /dev/null and b/apps/lato/screenshot2.png differ
diff --git a/apps/lato/screenshot3.png b/apps/lato/screenshot3.png
new file mode 100644
index 000000000..1cf135a60
Binary files /dev/null and b/apps/lato/screenshot3.png differ
diff --git a/apps/launch/ChangeLog b/apps/launch/ChangeLog
index 44866b9f3..0aff8c49f 100644
--- a/apps/launch/ChangeLog
+++ b/apps/launch/ChangeLog
@@ -14,3 +14,9 @@
Add /*LANG*/ tags for internationalisation
0.13: Add fullscreen mode
0.14: Use default Bangle formatter for booleans
+0.15: Support for unload and quick return to the clock on 2v16
+0.16: Use a cache of app.info files to speed up loading the launcher
+0.17: Don't display 'Loading...' now the watch has its own loading screen
+0.18: Add 'back' icon in top-left to go back to clock
+0.19: Fix regression after back button added (returnToClock was called twice!)
+0.20: Use Bangle.showClock for changing to clock
diff --git a/apps/launch/app.js b/apps/launch/app.js
index 556e61bfd..36f3aaf4b 100644
--- a/apps/launch/app.js
+++ b/apps/launch/app.js
@@ -1,61 +1,59 @@
-var s = require("Storage");
-var scaleval = 1;
-var vectorval = 20;
-var font = g.getFonts().includes("12x20") ? "12x20" : "6x8:2";
+{ // must be inside our own scope here so that when we are unloaded everything disappears
+let s = require("Storage");
+// handle customised launcher
+let scaleval = 1;
+let vectorval = 20;
+let font = g.getFonts().includes("12x20") ? "12x20" : "6x8:2";
let settings = Object.assign({
showClocks: true,
fullscreen: false
}, s.readJSON("launch.json", true) || {});
-
-if ("vectorsize" in settings) {
- vectorval = parseInt(settings.vectorsize);
-}
+if ("vectorsize" in settings)
+ vectorval = parseInt(settings.vectorsize);
if ("font" in settings){
- if(settings.font == "Vector"){
- scaleval = vectorval/20;
- font = "Vector"+(vectorval).toString();
- }
- else{
- font = settings.font;
- scaleval = (font.split("x")[1])/20;
- }
+ if(settings.font == "Vector"){
+ scaleval = vectorval/20;
+ font = "Vector"+(vectorval).toString();
+ } else{
+ font = settings.font;
+ scaleval = (font.split("x")[1])/20;
+ }
}
-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" && settings.showClocks) || !app.type));
-apps.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;
-});
-apps.forEach(app=>{
- if (app.icon)
- app.icon = s.read(app.icon); // should just be a link to a memory area
-});
-// FIXME: check not needed after 2v11
-if (g.wrapString) {
- g.setFont(font);
- apps.forEach(app=>app.name = g.wrapString(app.name, g.getWidth()-64).join("\n"));
+// cache app list so launcher loads more quickly
+let launchCache = s.readJSON("launch.cache.json", true)||{};
+let launchHash = require("Storage").hash(/\.info/);
+if (launchCache.hash!=launchHash) {
+ launchCache = {
+ hash : launchHash,
+ apps : s.list(/\.info$/)
+ .map(app=>{var a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src};})
+ .filter(app=>app && (app.type=="app" || (app.type=="clock" && settings.showClocks) || !app.type))
+ .sort((a,b)=>{
+ var n=(0|a.sortorder)-(0|b.sortorder);
+ if (n) return n; // do sortorder first
+ if (a.nameb.name) return 1;
+ return 0;
+ }) };
+ s.writeJSON("launch.cache.json", launchCache);
}
-
-function drawApp(i, r) {
- var app = apps[i];
- if (!app) return;
- g.clearRect((r.x),(r.y),(r.x+r.w-1), (r.y+r.h-1));
- g.setFont(font).setFontAlign(-1,0).drawString(app.name,64*scaleval,r.y+(32*scaleval));
- if (app.icon) try {g.drawImage(app.icon,8*scaleval, r.y+(8*scaleval), {scale: scaleval});} catch(e){}
-}
-
-g.clear();
-
-if (!settings.fullscreen) {
+let apps = launchCache.apps;
+// Now apps list is loaded - render
+if (!settings.fullscreen)
Bangle.loadWidgets();
- Bangle.drawWidgets();
-}
E.showScroller({
h : 64*scaleval, c : apps.length,
- draw : drawApp,
+ draw : (i, r) => {
+ var app = apps[i];
+ if (!app) return;
+ g.clearRect((r.x),(r.y),(r.x+r.w-1), (r.y+r.h-1));
+ g.setFont(font).setFontAlign(-1,0).drawString(app.name,64*scaleval,r.y+(32*scaleval));
+ if (app.icon) {
+ if (!app.img) app.img = s.read(app.icon); // load icon if it wasn't loaded
+ try {g.drawImage(app.img,8*scaleval, r.y+(8*scaleval), {scale: scaleval});} catch(e){}
+ }
+ },
select : i => {
var app = apps[i];
if (!app) return;
@@ -63,24 +61,28 @@ E.showScroller({
E.showMessage(/*LANG*/"App Source\nNot found");
setTimeout(drawMenu, 2000);
} else {
- E.showMessage(/*LANG*/"Loading...");
load(app.src);
}
+ },
+ back : Bangle.showClock, // button press or tap in top left shows clock now
+ remove : () => {
+ // cleanup the timeout to not leave anything behind after being removed from ram
+ if (lockTimeout) clearTimeout(lockTimeout);
+ Bangle.removeListener("lock", lockHandler);
}
});
-
-// on bangle.js 2, the screen is used for navigating, so the single button goes back
-// on bangle.js 1, the buttons are used for navigating
-if (process.env.HWVERSION==2) {
- setWatch(_=>load(), BTN1, {edge:"falling"});
-}
+g.flip(); // force a render before widgets have finished drawing
// 10s of inactivity goes back to clock
Bangle.setLocked(false); // unlock initially
-var lockTimeout;
-Bangle.on("lock", locked => {
+let lockTimeout;
+let lockHandler = function(locked) {
if (lockTimeout) clearTimeout(lockTimeout);
lockTimeout = undefined;
if (locked)
- lockTimeout = setTimeout(_=>load(), 10000);
-});
+ lockTimeout = setTimeout(Bangle.showClock, 10000);
+}
+Bangle.on("lock", lockHandler);
+if (!settings.fullscreen) // finally draw widgets
+ Bangle.drawWidgets();
+}
diff --git a/apps/launch/metadata.json b/apps/launch/metadata.json
index 19ca74e73..85fcdd02f 100644
--- a/apps/launch/metadata.json
+++ b/apps/launch/metadata.json
@@ -2,17 +2,18 @@
"id": "launch",
"name": "Launcher",
"shortName": "Launcher",
- "version": "0.14",
+ "version": "0.20",
"description": "This is needed to display a menu allowing you to choose your own applications. You can replace this with a customised launcher.",
"readme": "README.md",
"icon": "app.png",
"type": "launch",
+ "default": true,
"tags": "tool,system,launcher",
"supports": ["BANGLEJS","BANGLEJS2"],
"storage": [
{"name":"launch.app.js","url":"app.js"},
{"name":"launch.settings.js","url":"settings.js"}
],
- "data": [{"name":"launch.json"}],
+ "data": [{"name":"launch.json"},{"name":"launch.cache.json"}],
"sortorder": -10
}
diff --git a/apps/lcars/ChangeLog b/apps/lcars/ChangeLog
index 9a8ac4008..f97ddf540 100644
--- a/apps/lcars/ChangeLog
+++ b/apps/lcars/ChangeLog
@@ -21,3 +21,4 @@
0.21: Add custom theming.
0.22: Fix alarm and add build in function for step counting.
0.23: Add warning for low flash memory
+0.24: Add ability to disable alarm functionality
\ No newline at end of file
diff --git a/apps/lcars/lcars.app.js b/apps/lcars/lcars.app.js
index e81c0d6f3..06a89a957 100644
--- a/apps/lcars/lcars.app.js
+++ b/apps/lcars/lcars.app.js
@@ -12,6 +12,7 @@ let settings = {
themeColor1BG: "#FF9900",
themeColor2BG: "#FF00DC",
themeColor3BG: "#0094FF",
+ disableAlarms: false,
};
let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings;
for (const key in saved_settings) {
@@ -722,12 +723,12 @@ Bangle.on('touch', function(btn, e){
}
if(lcarsViewPos == 0){
- if(is_upper){
+ if(is_upper && !settings.disableAlarms){
feedback();
increaseAlarm();
drawState();
return;
- } if(is_lower){
+ } if(is_lower && !settings.disableAlarms){
feedback();
decreaseAlarm();
drawState();
diff --git a/apps/lcars/lcars.settings.js b/apps/lcars/lcars.settings.js
index b64feb30e..e4b9b0a78 100644
--- a/apps/lcars/lcars.settings.js
+++ b/apps/lcars/lcars.settings.js
@@ -13,6 +13,7 @@
themeColor1BG: "#FF9900",
themeColor2BG: "#FF00DC",
themeColor3BG: "#0094FF",
+ disableAlarms: false,
};
let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings;
for (const key in saved_settings) {
@@ -102,6 +103,14 @@
settings.themeColor3BG = bg_code[v];
save();
},
- }
+ },
+ 'Disable alarm functionality': {
+ value: settings.disableAlarms,
+ format: () => (settings.disableAlarms ? 'Yes' : 'No'),
+ onchange: () => {
+ settings.disableAlarms = !settings.disableAlarms;
+ save();
+ },
+ },
});
})
diff --git a/apps/lcars/metadata.json b/apps/lcars/metadata.json
index 62a1c67db..6533ddd52 100644
--- a/apps/lcars/metadata.json
+++ b/apps/lcars/metadata.json
@@ -3,7 +3,7 @@
"name": "LCARS Clock",
"shortName":"LCARS",
"icon": "lcars.png",
- "version":"0.23",
+ "version":"0.24",
"readme": "README.md",
"supports": ["BANGLEJS2"],
"description": "Library Computer Access Retrieval System (LCARS) clock.",
diff --git a/apps/lcdclock/ChangeLog b/apps/lcdclock/ChangeLog
new file mode 100644
index 000000000..5560f00bc
--- /dev/null
+++ b/apps/lcdclock/ChangeLog
@@ -0,0 +1 @@
+0.01: New App!
diff --git a/apps/lcdclock/app-icon.js b/apps/lcdclock/app-icon.js
new file mode 100644
index 000000000..ed3161c41
--- /dev/null
+++ b/apps/lcdclock/app-icon.js
@@ -0,0 +1 @@
+atob("MDABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf///T//+f///T///Q///Qf//Sf//Qf//Pf//Qf//AP//Yf//f///c///f///f///fiD/f8f/fur/f9f/fir/f9f/f67/f9f/fy7/f8f/fz//f+//AAAAAAAAAAAAAAAAf///////TD/u///vQB/EX/9PUV/d3/9fSd/F3/9HWd/d3/9Xf9/d//9Hf///////f///////f///8Hg/fmefwPAffmecz/Offmecz/Offmecz/OffmAcwHOffngc8DPffn+c/zOffn+c/zOffn+c/zOffn+f8DAff///8Hg/f///8Pw/f///////f//////+P//////8AAAAAAAAAAAAAAAAAAAAAAAA")
diff --git a/apps/lcdclock/app.js b/apps/lcdclock/app.js
new file mode 100644
index 000000000..2bc23247c
--- /dev/null
+++ b/apps/lcdclock/app.js
@@ -0,0 +1,84 @@
+Graphics.prototype.setFont7Seg = function() {
+ return this.setFontCustom(atob("AAAAAAAAAAAACAQCAAAAAAIAd0BgMBdwAAAAAAAADuAAAB0RiMRcAAAAAiMRiLuAAAcAQCAQdwAADgiMRiIOAAAd0RiMRBwAAAAgEAgDuAAAd0RiMRdwAADgiMRiLuAAAABsAAAd0QiEQdwAADuCIRCIOAAAd0BgMBAAAAAOCIRCLuAAAd0RiMRAAAADuiEQiAAAAAd0BgMBBwAADuCAQCDuAAAdwAAAAAAAAAAAIBALuAAAdwQCAQdwAADuAIBAIAAAAd0AgEAcEAgEAdwAd0AgEAdwAADugMBgLuAAAd0QiEQcAAADgiEQiDuAAAd0AgEAAAAADgiMRiIOAAAAEAgEAdwAADuAIBALuAAAdwBAIBdwAADuAIBAIOAIBALuADuCAQCDuAAAcAQCAQdwAAAOiMRiLgAAAA=="), 32, atob("BwAAAAAAAAAAAAAAAAcCAAcHBwcHBwcHBwcEAAAAAAAABwcHBwcHBwcHBwcHCgcHBwcHBwcHBwoHBwc="), 9);
+}
+
+
+{ // must be inside our own scope here so that when we are unloaded everything disappears
+ // we also define functions using 'let fn = function() {..}' for the same reason. function decls are global
+let drawTimeout;
+
+// Actually draw the watch face
+let draw = function() {
+ var x = R.x + R.w/2;
+ var y = R.y + R.h/2;
+ g.reset().setColor(g.theme.bg).setBgColor(g.theme.fg);
+ g.clearRect(R.x,barY+2,R.x2,R.y2-8);
+ var date = new Date();
+ var timeStr = require("locale").time(date, 1); // Hour and minute
+ g.setFontAlign(0, 0).setFont("7Seg:5").drawString(timeStr, x, y+39);
+ // Show date and day of week
+ g.setFontAlign(0, 0).setFont("7Seg:2");
+ g.setFontAlign(-1, 0).drawString(require("locale").meridian(date).toUpperCase(), R.x+6, y);
+ g.setFontAlign(0, 0).drawString(require("locale").dow(date, 1).toUpperCase(), x, y);
+ g.setFontAlign(1, 0).drawString(date.getDate(), R.x2 - 6, y);
+
+ // queue next draw
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = setTimeout(function() {
+ drawTimeout = undefined;
+ draw();
+ }, 60000 - (Date.now() % 60000));
+};
+
+// Show launcher when middle button pressed
+Bangle.setUI({
+ mode : "clock",
+ remove : function() {
+ // Called to unload all of the clock app
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = undefined;
+ delete Graphics.prototype.setFont7Seg;
+ // remove info menu
+ clockInfoMenu.remove();
+ delete clockInfoMenu;
+ clockInfoMenu2.remove();
+ delete clockInfoMenu2;
+ // reset theme
+ g.setTheme(oldTheme);
+ }});
+// Load widgets
+Bangle.loadWidgets();
+var R = Bangle.appRect;
+R.x+=1;
+R.y+=1;
+R.x2-=1;
+R.y2-=1;
+R.w-=2;
+R.h-=2;
+var midX = R.x+R.w/2;
+var barY = 80;
+// Clear the screen once, at startup
+let oldTheme = g.theme;
+g.setTheme({bg:"#000",fg:"#fff",dark:true}).clear(1);
+g.fillRect({x:R.x, y:R.y, w:R.w, h:R.h, r:8}).clearRect(R.x,barY,R.w,barY+1).clearRect(midX,R.y,midX+1,barY);
+draw();
+setTimeout(Bangle.drawWidgets,0);
+
+let clockInfoDraw = (itm, info, options) => {
+ let texty = options.y+41;
+ g.reset().setFont("7Seg").setColor(g.theme.bg).setBgColor(g.theme.fg);
+ if (options.focus) g.setBgColor("#FF0");
+ g.clearRect({x:options.x,y:options.y,w:options.w,h:options.h,r:8});
+
+ if (info.img) g.drawImage(info.img, options.x+2, options.y+2);
+ var title = clockInfoItems[options.menuA].name;
+ var text = info.text.toString().toUpperCase();
+ if (title!="Bangle") g.setFontAlign(1,0).drawString(title.toUpperCase(), options.x+options.w-2, options.y+14);
+ if (g.setFont("7Seg:2").stringWidth(text)+8>options.w) g.setFont("7Seg");
+ g.setFontAlign(0,0).drawString(text, options.x+options.w/2, options.y+40);
+
+};
+let clockInfoItems = require("clock_info").load();
+let clockInfoMenu = require("clock_info").addInteractive(clockInfoItems, { x:R.x, y:R.y, w:midX-2, h:barY-R.y-2, draw : clockInfoDraw});
+let clockInfoMenu2 = require("clock_info").addInteractive(clockInfoItems, { x:midX+2, y:R.y, w:midX-3, h:barY-R.y-2, draw : clockInfoDraw});
+}
diff --git a/apps/lcdclock/app.png b/apps/lcdclock/app.png
new file mode 100644
index 000000000..6a117b525
Binary files /dev/null and b/apps/lcdclock/app.png differ
diff --git a/apps/lcdclock/metadata.json b/apps/lcdclock/metadata.json
new file mode 100644
index 000000000..d7d09b106
--- /dev/null
+++ b/apps/lcdclock/metadata.json
@@ -0,0 +1,14 @@
+{ "id": "lcdclock",
+ "name": "LCD Clock",
+ "version":"0.01",
+ "description": "A Casio-style clock, with ClockInfo areas at the top and bottom. Tap them and swipe up/down to toggle between different information",
+ "icon": "app.png",
+ "screenshots": [{"url":"screenshot.png"}],
+ "type": "clock",
+ "tags": "clock,clkinfo",
+ "supports" : ["BANGLEJS2"],
+ "storage": [
+ {"name":"lcdclock.app.js","url":"app.js"},
+ {"name":"lcdclock.img","url":"app-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/lcdclock/screenshot.png b/apps/lcdclock/screenshot.png
new file mode 100644
index 000000000..b0bb5934a
Binary files /dev/null and b/apps/lcdclock/screenshot.png differ
diff --git a/apps/lightswitch/ChangeLog b/apps/lightswitch/ChangeLog
index 4c89bae76..c4aeb2c1e 100644
--- a/apps/lightswitch/ChangeLog
+++ b/apps/lightswitch/ChangeLog
@@ -3,3 +3,4 @@
0.03: Settings page now uses built-in min/max/wrap (fix #1607)
0.04: Add masking widget input to other apps (using espruino/Espruino#2151), add a oversize option to increase the touch area.
0.05: Prevent drawing into app area.
+0.06: Fix issue where .draw was being called by reference (not allowing widgets to be hidden)
diff --git a/apps/lightswitch/metadata.json b/apps/lightswitch/metadata.json
index b8da2f759..d1a8d6e2a 100644
--- a/apps/lightswitch/metadata.json
+++ b/apps/lightswitch/metadata.json
@@ -2,7 +2,7 @@
"id": "lightswitch",
"name": "Light Switch Widget",
"shortName": "Light Switch",
- "version": "0.05",
+ "version": "0.06",
"description": "A fast way to switch LCD backlight on/off, change the brightness and show the lock status. All in one widget.",
"icon": "images/app.png",
"screenshots": [
diff --git a/apps/lightswitch/widget.js b/apps/lightswitch/widget.js
index d9d4d421d..9eb488aca 100644
--- a/apps/lightswitch/widget.js
+++ b/apps/lightswitch/widget.js
@@ -224,28 +224,20 @@
// main widget function //
// display and setup/reset function
- draw: function(locked) {
+ draw: function() {
// setup shortcut to this widget
var w = WIDGETS.lightswitch;
- // set lcd brightness on unlocking
- // all other cases are catched by the boot file
- if (locked === false) Bangle.setLCDBrightness(w.isOn ? w.value : 0);
-
// read lock status
- locked = Bangle.isLocked();
+ var locked = Bangle.isLocked();
// remove listeners to prevent uncertainties
- Bangle.removeListener("lock", w.draw);
Bangle.removeListener("touch", w.touchListener);
Bangle.removeListener("tap", require("lightswitch.js").tapListener);
// draw widget icon
w.drawIcon(locked);
- // add lock listener
- Bangle.on("lock", w.draw);
-
// add touch listener to control the light depending on settings at first position
if (w.touchOn === "always" || !global.__FILE__ ||
w.touchOn.includes(__FILE__) ||
@@ -259,7 +251,15 @@
w = undefined;
}
});
+
+ Bangle.on("lock", locked => {
+ var w = WIDGETS.lightswitch;
+ // set lcd brightness on unlocking
+ // all other cases are catched by the boot file
+ if (locked === false) Bangle.setLCDBrightness(w.isOn ? w.value : 0);
+ w.draw()
+ });
// clear variable
- settings = undefined;
+ delete settings;
})()
diff --git a/apps/limelight/ChangeLog b/apps/limelight/ChangeLog
index 9db0e26c5..8fe3a0b2c 100644
--- a/apps/limelight/ChangeLog
+++ b/apps/limelight/ChangeLog
@@ -1 +1,2 @@
0.01: first release
+0.02: Tell clock widgets to hide.
diff --git a/apps/limelight/limelight.app.js b/apps/limelight/limelight.app.js
index 20d79deeb..84ded1039 100644
--- a/apps/limelight/limelight.app.js
+++ b/apps/limelight/limelight.app.js
@@ -10,6 +10,8 @@
*
*/
+Bangle.setUI('clock');
+
g.clear();
const SETTINGS_FILE = "limelight.json";
@@ -259,5 +261,4 @@ Bangle.on('lcdPower',on=>{
}
});
-Bangle.setUI('clock');
draw();
diff --git a/apps/limelight/metadata.json b/apps/limelight/metadata.json
index 7c3736e1a..e484a2825 100644
--- a/apps/limelight/metadata.json
+++ b/apps/limelight/metadata.json
@@ -1,7 +1,7 @@
{
"id": "limelight",
"name": "Limelight",
- "version": "0.01",
+ "version": "0.02",
"description": "Simple analogue clock (with configurable fonts) based on the work of @Andreas_Rozek (Simple_Clock)",
"icon": "limelight.png",
"readme":"README.md",
diff --git a/apps/linuxclock/ChangeLog b/apps/linuxclock/ChangeLog
new file mode 100644
index 000000000..1c4f7d79b
--- /dev/null
+++ b/apps/linuxclock/ChangeLog
@@ -0,0 +1,3 @@
+0.01: New App.
+0.02: Performance improvements.
+0.03: Update clock_info to avoid a redraw
diff --git a/apps/linuxclock/README.md b/apps/linuxclock/README.md
new file mode 100644
index 000000000..934ed2902
--- /dev/null
+++ b/apps/linuxclock/README.md
@@ -0,0 +1,13 @@
+# A Linux inspired clock
+
+
+A linux inspired clock which also loads and shows clock_infos .
+Simply click left/right to execute another command ;)
+With up/down you can select an individual entry and with a click at the
+center of the screen you can trigger an action if its supported (e.g. HomeAssistant).
+
+# Thanks
+Icons from by Freepik - Flaticon
+
+## Creator
+- [David Peer](https://github.com/peerdavid).
\ No newline at end of file
diff --git a/apps/linuxclock/app-icon.js b/apps/linuxclock/app-icon.js
new file mode 100644
index 000000000..8a767a209
--- /dev/null
+++ b/apps/linuxclock/app-icon.js
@@ -0,0 +1 @@
+atob("JiaEAAAAAAAAAAAAD///AAAAAAAAAAAAAAAAAAAAD/////AAAAAAAAAAAAAAAAAAAA//////AAAAAAAAAAAAAAAAAAD///////AAAAAAAAAAAAAAAAAA///////wAAAAAAAAAAAAAAAAAPD/8AD/8AAAAAAAAAAAAAAAAADwD/AA//AAAAAAAAAAAAAAAAAADw/w8P/wAAAAAAAAAAAAAAAAAP////D/8AAAAAAAAAAAAAAAAAD/8AD///8AAAAAAAAAAAAAAAAA/wAAD///AAAAAAAAAAAAAAAAAP8AAP///wAAAAAAAAAAAAAAAADw8P8AD/8AAAAAAAAAAAAAAAAP8AAAAA//8AAAAAAAAAAAAAAADwAAAAAA//AAAAAAAAAAAAAAAP8AAAAAAP//AAAAAAAAAAAAAA/wAAAAAAAP//AAAAAAAAAAAAAP8AAAAAAAD///AAAAAAAAAAAA/wAAAAAAAA///wAAAAAAAAAAAP8AAAAAAAAA///wAAAAAAAAAA/wAAAAAAAAAP//8AAAAAAAAAAP8AAAAAAAAAD///8AAAAAAAAAD/AAAAAAAAAAD///AAAAAAAAAP8AAAAAAAAAAA///wAAAAAAAAD/AAAAAAAAAAAP//8AAAAAAAAA//AAAAAAAAAA////AAAAAAAAAP/wAAAAAAAAD////wAAAAAAAA8A/wAAAAAAAA////8AAAAAAA/wAA/wAAAAAAAPD///8AAAAA/wAAAP/wAAAAAADw//APAAAAAPAAAAAP/wAAAAAA8AAAD/AAAADwAAAAD/8AAAAAD/AAAAD/AAAA8AAAAAD/AAAAAP/wAAAADwAAAPAAAAAADwAAAP//AAAAD/AAAAD/AAAAAA///////wAAD/8AAAAAD///AAAP//////8AAP8AAAAAAAAAD//w/wAAAAAP8P8AAAAAAAAAAAAAD/AAAAAAAP/wAAAAAA")
\ No newline at end of file
diff --git a/apps/linuxclock/app.js b/apps/linuxclock/app.js
new file mode 100644
index 000000000..9470b803c
--- /dev/null
+++ b/apps/linuxclock/app.js
@@ -0,0 +1,386 @@
+
+/************************************************
+ * Includes
+ */
+ const clock_info = require("clock_info");
+ const storage = require('Storage');
+ const locale = require('locale');
+
+/*
+ * Some vars
+ */
+var W = g.getWidth();
+var H = g.getHeight();
+
+ /************************************************
+ * Settings
+ */
+ const SETTINGS_FILE = "linuxclock.setting.json";
+ let settings = {
+ menuPosX: 0,
+ menuPosY: 0,
+ };
+
+ let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings;
+ for (const key in saved_settings) {
+ settings[key] = saved_settings[key]
+ }
+
+
+ /************************************************
+ * Assets
+ */
+ Graphics.prototype.setFontUbuntuMono = function(scale) {
+ // Actual height 24 (27 - 4)
+ this.setFontCustom(
+ atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD+A4AP/ngA/+eAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAA/gAAD+AAAAAAAAAAAAD+AAAP4AAA8AAAAAAAAAAAAAAAAAAAAAAAMGAAAwfgAD/+AB//gAP/YAA/BgAAMH4AA3/gAP/8AD/+AAPwYAADBgAAAAAAAAAAAAAEAAfA4AD+DgAf4GABxwYA+HB8D4OHwBg4YAGDzgAcH8AAgPwAAAcAAAAAAA8AAAH8BgA9wOADBjwAOccAA/3gAA94AAAPeAAD3+AAcc4AHhhgA4H+ADAfwAAAeAAAAAAAAPgADx/AAf/eAD/wYAMHBgAw+GADneYAP4/AAfB8AAA/4AADzgAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/gAAD+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAB//AAP//AD4A+AeAA8DwAB4OAADwQAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAYOAABw8AAeB4ADwD4A+AD//wAH/8AAD/AAAAAAAAAAAAAAAAAAAAAAAAAA4AAABiAAAHcAAAPwAAP8AAA/wAAAPwAAB3AAAGIAAAYAAAAAAAAAAAAAAAAAABgAAAGAAAAYAAABgAAAGAAAP/wAA//AAAGAAAAYAAABgAAAGAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAABOAAAewAAB/AAAH4AAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAYAAABgAAAGAAAAYAAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAB4AAAHgAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAB8AAA/wAAf4AAP8AAH+AAD/AAB/gAAPwAAA4AAAAAAAAAAAAAAAAAAB/AAA//gAH//AA8AeADhg4AMPBgAw8GADhg4APAHgAf/8AA//gAAfwAAAAAAAAAAAAAAAAGAAAAYAYADgBgAcAGAB//4AP//gA//+AAAAYAAABgAAAGAAAAIAAAAAAAAAAAAAAAGADgA4A+ADgH4AMA9gAwHGADA4YAOHBgA/4GAB/AYAD4BgAAAGAAAAAAAAAAAAAAABgA4AOADgA4AGADBgYAMGBgAw4GADjw4AP/ngAfv8AA8fgAAAYAAAAAAAAAAAAA4AAAPgAAD+AAAeYAADhgAA8GAAHgYAA8BgAD//4AP//gAABgAAAGAAAAAAAAAAAAAAAAAADgA/4OAD/gYAP+BgAw4GADDgYAMGDgAweeADA/wAMB+AAABgAAAAAAAAAAAAPAAAH/gAB//AAP4eABzA4AGMBgA4wGADjgYAMOHgAwf8ADA/gAAB4AAAAAAAAAAAAAAAAwAAADAAAAMADgAwB+ADA/4AMP8AAz8AADfAAAPwAAA8AAADgAAAAAAAAAAAAAAHAAD5/AAf38AD344AMHBgAwYGADBwYAOHBgA9+OAB/fwAD5/AAABwAAAAAAAAAAAHgAAA/gYAH+BgA4cGADAw4AMDDgAwMcADgzwAPD+AAf/wAA/+AAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADweAAPB4AA8HgADweAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAADgA8HMADwfwAPB+AA8HwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAA8AAADwAAAPgAAB+AAAGYAAA5wAADDAAAcOAABw4AAGBgAAYGAAAAAAAAAAAAMYAAAxgAADGAAAMYAAAxgAADGAAAMYAAAxgAADGAAAMYAAAxgAADGAAAAAAAAAAAAGBgAAYHAABwYAAHDgAAMMAAA5wAABmAAAH4AAAfgAAA8AAADwAAAGAAAAAAAAAAAAAAAAAAAAAOAAAAwAMADAZ4AMHngAw8eADngAAP8AAAfgAAAYAAAAAAAAAAAAAAAAAAP+AAH/+AA//+AHgB8A4PhwDD/jgMP/GAxwcYDmAxgPYDGAf/8YA//wAAAAAAAAGAAAD4AAD/gAB/wAA/+AAP4YAA8BgADwGAAP8YAAP/gAAH/gAAD/gAAA+AAAAYAAAAAA//+AD//4AP//gAwYGADBgYAMGBgAwYGADjw4AP/DgAfv8AA8fgAAA8AAAAAAAAAAAAfwAAH/wAA//gAHgPAA8AOADgA4AMABgAwAGADAAYAOABgA4AOABAAwAAAAAAAAAAD//4AP//gA//+ADAAYAMABgAwAGADgA4AOADgAcAcAA//gAB/8AAB/AAAAAAAAAAAAAAAAD//4AP//gA//+ADBgYAMGBgAwYGADBgYAMGBgAwYGADBgYAIABgAAAAAAAAAAAAAAA//+AD//4AP//gAwYAADBgAAMGAAAwYAADBgAAMGAAAwYAADAAAAAAAAAAAAAAH8AAB/8AAP/4AB4DwAPADgA4AOADAAYAMABgAwAGADgf4AOD/gAQP+AAAAAAAAAAA//+AD//4AP//gAAYAAABgAAAGAAAAYAAABgAAAGAAA//+AD//4AP//gAAAAAAAAAAAAAAAwAGADAAYAMABgAwAGAD//4AP//gAwAGADAAYAMABgAwAGAAAAAAAAAAAAAAAAAAQAAADAAwAOADAAYAMABgAwAGADAAYAMADgA//8AD//wAP/8AAAAAAAAAAAAAAAAAAAAD//4AP//gAAcAAADwAAAfgAAHvAAA8eAAHg+AA8A8ADgB4AIADgAAACAAAAAAAAAAA//+AD//4AP//gAAAGAAAAYAAABgAAAGAAAAYAAABgAAAGAAAAYAAAAAAAAAAAP/4AP//gA/+AAD+AAAB/AAAA+AAAD4AAB/AAA/gAAD/4AAP//gAH/+AAAAAAAAAAA//+AD//4AP//gAfAAAA+AAAA+AAAA/AAAA/AAAA/AA//+AD//4AP//gAAAAAAAAAAB/8AAP/4AB+PwAOADgA4AOADAAYAMABgA4AOADgA4AH4/AAP/4AAf/AAAAAAAAAAAAAAAAP//gA//+AD//4AMBgAAwGAADAYAAODgAA4OAAB/wAAD+AAAHwAAAAAAAAAAAAD/wAA//wAH4fgA4APADgAcAMAA8AwAD4DgAfgPAD3Afh+MA//wwA/8CAAAAAAAAAAP//gA//+AD//4AMDAAAwMAADAwAAMDgAA4fgAD/vgAH+PgAPwOAAAAYAAAAAAAAAAAAAQAD4DAAfwOAD/AYAOOBgAwYGADBwYAMDDgA4OOADgfwAEB/AAABwAAAAAAAAAAAwAAADAAAAMAAAAwAAADAAAAP//gA//+ADAAAAMAAAAwAAADAAAAMAAAAAAAAAAAAAP/8AA//4AD//wAAADgAAAGAAAAYAAABgAAAGAAAA4AP//AA//4AD//AAAAAAAwAAAD4AAAP8AAAP/AAAD/gAAB/gAAAeAAAB4AAA/gAA/4AA/8AAP+AAA+AAADAAAAAAAAA//wAD//4AAf/gAAD+AAB/AAAPgAAA+AAAB/AAAA/gAB/+AD//4AP/4AAAAAAAAAAAIABgA4AeAD4H4AH5+AAH/gAAH4AAAfgAAH/gAB+fgAPgfgA4AeACAAYAAAAAAgAAADgAAAPgAAAfgAAAfgAAAfgAAAf+AAB/4AAfgAAH4AAB+AAAPgAAA4AAACAAAAAAAAAAAGADAB4AMAfgAwD+ADA+YAMHhgAx8GADPAYAP4BgA+AGADwAYAMABgAAAAAAAAAAAAAAAAAAAAAAAA////D///8MAAAwwAADDAAAMMAAAwwAADAAAAAAAAAAAAAAAAAAAAAAAA4AAAD8AAAH+AAAH/AAAD/gAAB/wAAAf4AAAP8AAAHwAAADAAAAAAAAAAAAAAAAAAAAAAAAwAADDAAAMMAAAwwAADDAAAMP///w////AAAAAAAAAAAAAAAAAAAAAAAAAGAAAA4AAAPgAAD4AAA+AAADwAAAPAAAA+AAAA+AAAA+AAAA4AAABgAAAAAAAAAAMAAAAwAAADAAAAMAAAAwAAADAAAAMAAAAwAAADAAAAMAAAAwAAADAAAAMAAAAwAAAAAAAAAAAAAAAAAAAAAAAOAAAA8AAAB4AAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAD8AAMPwAAxzgADGGAAMYYAAxhgADmGAAPYYAAf/gAA/+AAAAAAAAAAAAAAAAAAAA///gD//+AP//4AA4BgADAGAAMAYAA4DgADgeAAH/wAAP+AAAfwAAAAAAAAAAAAPgAAD/gAAf/AABwcAAOA4AA4DgADAGAAMAYAAwBgADAGAAOA4AAABgAAAAAAAAAAAH8AAA/4AAH/wAA4HgADgOAAMAYAAwBgADgGAH//4A///gD//+AAAAAAAAAAAAAAAAA/AAAP+AAB/8AAOZ4AA5jgADGGAAMYYAAxhgADmGAAH44AAfjgAAeAAAAAAAAAAAAAAAAAMAAAAwAAAf/+AH//4A///gDjAAAMMAAAwwAADDAAAMMAAA4AAABAAAAAAAAAAH8AAA/4cAH/xwA8DjADgOMAMAYwAwBjADAOcAP//wA//+AD//wAAAAAAAAAAAAAAAAAAA///gD//+AP//4AAwAAADAAAAMAAAA4AAAD4AAAH/4AAP/gAAAAAAAAAAAAAAADAAAAMAAAAwAADjAAAPP/gA8//ABj/+AAAA4AAABgAAAGAAAA4AAABAAAAAAAAAAAAAAAAAAAcAMABwAwADADAAMAMAAw8wAHDz//8PP//gY//8AAAAAAAAAAAAAAAAAAAAAAAA///gD//+AP//4AADwAAAfgAADvAAAeeAADw8AAOB4AAwDgACAGAAAAAAAAAADAAAAMAAAAwAAADAAAAP//gA///AD//+AAAA4AAABgAAAGAAAA4AAABAAAAAAAAAAAA//gAD/+AAOAAAAwAAADgAAAP8AAA/wAADAAAAMAAAA4AAAD/+AAH/4AAAAAAAAAAAAAAAA//gAD/+AAP/4AAwAAADAAAAMAAAA4AAAD4AAAH/4AAP/gAAAAAAAAAAAAAAAAfwAAD/gAAf/AADweAAOA4AAwBgADAGAAOA4AA8HgAB/8AAD/gAAH8AAAAAAAAAAAAAAAAD//8AP//wA///ADAOAAMAYAAwBgADgOAAPB4AAf/AAA/4AAB/AAAAAAAAAAAAB/AAAP+AAB/8AAPB4AA4DgADAGAAMAYAAwDgAD//8AP//wA///AAAAAAAAAAAAAAAAAAAAAAAAAf/gAD/+AAP/4AAwAAADAAAAMAAAAwAAADAAAAMAAAAAAAAAAAAAAAAAAAAAAA4OAAHw4AA/hgADOGAAMcYAAxxgADDGAAMO4AA4fAABB8AAAAAAAAAAAAAAAAAAAAAwAAADAAAD//AAP//AA//+AAMA4AAwBgADAGAAMAYAAwDgADAEAAAAAAAAAAAAAAAAP/gAA//AAAA+AAAA4AAABgAAAGAAAAYAA//gAD/+AAP/4AAAAAAAAAAAAAAAA4AAAD8AAAP8AAAH+AAAD+AAAB4AAAHgAAD8AAB/AAA/wAAD8AAAOAAAAAAAADAAAAP+AAA//gAAH+AAAD4AAB+AAAfAAAB8AAAD+AAAB+AAAP4AA//gAD/AAAMAAAAAAAACAGAAMA4AA8HgAB54AAD/AAAH4AAAPgAAD/AAAeeAADw+AAMA4AAgBgAAAAAAAAAAAgADAD4AMAP4AwAf8DAAH8cAAD/gAAD8AAB/AAB/wAA/4AAD8AAAMAAAAAAAAAAAAAAAAAAwDgADAeAAMH4AAw9gADPmAAN4YAA/BgAD4GAAPAYAAwBgAAAAAAAAAAAAAAAAAAAAAYAAABgAAAPAAD///Af/f+D/wf8MAAAwwAADDAAAMMAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///w////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAADDAAAMMAAAwwAADD/w/8H/3/gP//8AAPAAAAYAAABgAAAAAAAAAAAAAAAAADAAAA8AAADgAAAMAAAA4AAADgAAAHAAAAcAAAAwAAAHAAAA8AAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=='),
+ 32,
+ atob("Dg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4c"),
+ 28+(scale<<8)+(1<<16)
+ );
+ return this;
+ }
+
+
+
+ /************************************************
+ * Menu
+ */
+ var dateMenu = {
+ name: "date",
+ img: null,
+ items: [
+ { name: "time",
+ get: () => ({ text: getTime(), img: null}),
+ show: function() {},
+ hide: function () {}
+ },
+ { name: "day",
+ get: () => ({ text: getDay(), img: null}),
+ show: function() {},
+ hide: function () {}
+ },
+ { name: "date",
+ get: () => ({ text: getDate(), img: null}),
+ show: function() {},
+ hide: function () {}
+ },
+ { name: "week",
+ get: () => ({ text: weekOfYear(), img: null}),
+ show: function() {},
+ hide: function () {}
+ },
+ ]
+ };
+
+ var menu = clock_info.load();
+ menu = menu.concat(dateMenu);
+
+ // Set draw functions for each item
+ menu.forEach((menuItm, x) => {
+ menuItm.items.forEach((item, y) => {
+ function drawItem() {
+ item.hide();
+
+ var info = item.get();
+ drawText(item.name, info.text, (y%4)+1);
+ }
+
+ item.on('redraw', drawItem);
+ })
+ });
+
+
+ // Ensure that our settings are still in range (e.g. app uninstall). Otherwise reset the position it.
+ if(settings.menuPosX >= menu.length || settings.menuPosY > menu[settings.menuPosX].items.length ){
+ settings.menuPosX = 0;
+ settings.menuPosY = 0;
+ }
+
+function canRunMenuItem(){
+ var menuEntry = menu[settings.menuPosX];
+ var item = menuEntry.items[settings.menuPosY];
+ return item.run !== undefined;
+}
+
+
+function runMenuItem(){
+ var menuEntry = menu[settings.menuPosX];
+ var item = menuEntry.items[settings.menuPosY];
+ try{
+ var ret = item.run();
+ if(ret){
+ Bangle.buzz(300, 0.6);
+ }
+ } catch (ex) {
+ // Simply ignore it...
+ }
+}
+
+/************************************************
+* Helper
+*/
+function getTime(){
+ var date = new Date();
+ return twoD(date.getHours())+ ":" + twoD(date.getMinutes());
+}
+
+function getDate(){
+ var date = new Date();
+ return twoD(date.getDate()) + "." + twoD(date.getMonth());
+}
+
+function getDay(){
+ var date = new Date();
+ return locale.dow(date, true);
+}
+
+function weekOfYear() {
+ var date = new Date();
+ date.setHours(0, 0, 0, 0);
+ // Thursday in current week decides the year.
+ date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7);
+ // January 4 is always in week 1.
+ var week1 = new Date(date.getFullYear(), 0, 4);
+ // Adjust to Thursday in week 1 and count number of weeks from date to week1.
+ return 1 + Math.round(((date.getTime() - week1.getTime()) / 86400000
+ - 3 + (week1.getDay() + 6) % 7) / 7);
+}
+
+
+
+/************************************************
+* Draw
+*/
+function draw() {
+ queueDraw();
+
+ g.setFontUbuntuMono();
+ g.setFontAlign(-1, -1);
+
+ g.clearRect(0,24,W,H);
+
+ drawMainScreen();
+}
+
+
+
+function drawMainScreen(){
+ // Get menu item based on x
+ var menuItem = menu[settings.menuPosX];
+ var cmd = menuItem.name.slice(0,5).toLowerCase();
+ drawCmd(cmd);
+
+ // Draw menu items depending on our y value
+ drawMenuItems(menuItem);
+
+ // And draw the cursor
+ drawCursor();
+}
+
+function drawMenuItems(menuItem) {
+ var start = parseInt(settings.menuPosY / 4) * 4;
+ for (var i = start; i < start + 4; i++) {
+ if (i >= menuItem.items.length) {
+ continue;
+ }
+ lock_input++;
+ menuItem.items[i].show();
+ }
+}
+
+function drawCursor(){
+ g.setFontUbuntuMono();
+ g.setFontAlign(-1, -1);
+ g.setColor(g.theme.fg);
+
+ g.clearRect(0, 27 + 28, 15, H);
+ if(!Bangle.isLocked()){
+ g.drawString(">", -2, ((settings.menuPosY % 4) + 1) * 27 + 28);
+ }
+}
+
+function drawText(key, value, line){
+ var x = 15;
+ var y = line * 27 + 28;
+
+ g.setFontUbuntuMono();
+ g.setFontAlign(-1, -1);
+ g.setColor(g.theme.fg);
+
+ if(key){
+ key = (key.toLowerCase() + " ").slice(0, 4) + "|";
+ } else {
+ key = ""
+ }
+
+ value = String(value).replace("\n", " ");
+ g.drawString(key + value, x, y);
+
+ lock_input -= 1;
+}
+
+
+function drawCmd(cmd){
+ var c = 0;
+ var x = 10;
+ var y = 28;
+
+ g.setColor("#0f0");
+ g.drawString("bjs", x+c, y);
+ c += g.stringWidth("bjs");
+
+ g.setColor(g.theme.fg);
+ g.drawString(":", x+c, y);
+ c += g.stringWidth(":");
+
+ g.setColor("#0ff");
+ g.drawString("$ ", x+c, y);
+ c += g.stringWidth("$ ");
+
+ g.setColor(g.theme.fg);
+ g.drawString(cmd, x+c, y);
+}
+
+function twoD(str){
+ return ("0" + str).slice(-2)
+}
+
+
+/************************************************
+* Listener
+*/
+// timeout used to update every minute
+var drawTimeout;
+
+// schedule a draw for the next minute
+function queueDraw() {
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = setTimeout(function() {
+ drawTimeout = undefined;
+ draw();
+ }, 60000 - (Date.now() % 60000));
+}
+
+
+// Stop updates when LCD is off, restart when on
+Bangle.on('lcdPower',on=>{
+ if (on) {
+ draw(); // draw immediately, queue redraw
+ } else { // stop draw timer
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = undefined;
+ }
+});
+
+
+Bangle.on('lock', function(isLocked) {
+ drawCursor();
+});
+
+
+Bangle.on('charging',function(charging) {
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = undefined;
+
+ settings.menuPosX=0;
+ settings.menuPosY=0;
+
+ draw();
+});
+
+var lock_input = 0;
+
+Bangle.on('touch', function(btn, e){
+ if(lock_input > 0){
+ return;
+ }
+ lock_input = 0;
+
+ var left = parseInt(g.getWidth() * 0.22);
+ var right = g.getWidth() - left;
+ var upper = parseInt(g.getHeight() * 0.22) + 20;
+ var lower = g.getHeight() - upper;
+
+ var is_upper = e.y < upper;
+ var is_lower = e.y > lower;
+ var is_left = e.x < left && !is_upper && !is_lower;
+ var is_right = e.x > right && !is_upper && !is_lower;
+ var is_center = !is_upper && !is_lower && !is_left && !is_right;
+
+ var oldYScreen = parseInt(settings.menuPosY/4);
+ if(is_lower){
+ if(settings.menuPosY >= menu[settings.menuPosX].items.length-1){
+ return;
+ }
+
+ Bangle.buzz(40, 0.6);
+ settings.menuPosY++;
+ if(parseInt(settings.menuPosY/4) == oldYScreen){
+ drawCursor();
+ return;
+ }
+ }
+
+ if(is_upper){
+ if(e.y < 20){ // Reserved for widget clicks
+ return;
+ }
+
+ if(settings.menuPosY <= 0){
+ return;
+ }
+ Bangle.buzz(40, 0.6);
+ settings.menuPosY--;
+ settings.menuPosY = settings.menuPosY < 0 ? 0 : settings.menuPosY;
+
+ if(parseInt(settings.menuPosY/4) == oldYScreen){
+ drawCursor();
+ return;
+ }
+ }
+
+ if(is_right){
+ Bangle.buzz(40, 0.6);
+ settings.menuPosX = (settings.menuPosX+1) % menu.length;
+ settings.menuPosY = 0;
+ }
+
+ if(is_left){
+ Bangle.buzz(40, 0.6);
+ settings.menuPosY = 0;
+ settings.menuPosX = settings.menuPosX-1;
+ settings.menuPosX = settings.menuPosX < 0 ? menu.length-1 : settings.menuPosX;
+ }
+
+ if(is_center){
+ if(!canRunMenuItem()){
+ return;
+ }
+ runMenuItem();
+ }
+
+ draw();
+});
+
+E.on("kill", function(){
+ try{
+ storage.write(SETTINGS_FILE, settings);
+ } catch(ex){
+ // If this fails, we still kill the app...
+ }
+});
+
+
+/************************************************
+* Startup Clock
+*/
+// Show launcher when middle button pressed
+Bangle.setUI("clock");
+
+// Load and draw widgets
+Bangle.loadWidgets();
+Bangle.drawWidgets();
+
+// Draw first time
+draw();
diff --git a/apps/linuxclock/app.png b/apps/linuxclock/app.png
new file mode 100644
index 000000000..3a09cd575
Binary files /dev/null and b/apps/linuxclock/app.png differ
diff --git a/apps/linuxclock/metadata.json b/apps/linuxclock/metadata.json
new file mode 100644
index 000000000..06ef66498
--- /dev/null
+++ b/apps/linuxclock/metadata.json
@@ -0,0 +1,18 @@
+{
+ "id": "linuxclock",
+ "name": "Linux Clock",
+ "version": "0.03",
+ "description": "A Linux inspired clock.",
+ "readme": "README.md",
+ "icon": "app.png",
+ "screenshots": [{"url":"screenshot.png"}, {"url":"screenshot_2.png"}],
+ "type": "clock",
+ "tags": "clock",
+ "supports": ["BANGLEJS2"],
+ "allow_emulator": true,
+ "storage": [
+ {"name":"linuxclock.app.js","url":"app.js"},
+ {"name":"linuxclock.img","url":"app-icon.js","evaluate":true},
+ {"name":"linuxclock.settings.js","url":"settings.js"}
+ ]
+}
diff --git a/apps/linuxclock/screenshot.png b/apps/linuxclock/screenshot.png
new file mode 100644
index 000000000..4bc7f9967
Binary files /dev/null and b/apps/linuxclock/screenshot.png differ
diff --git a/apps/linuxclock/screenshot_2.png b/apps/linuxclock/screenshot_2.png
new file mode 100644
index 000000000..abeba7a92
Binary files /dev/null and b/apps/linuxclock/screenshot_2.png differ
diff --git a/apps/linuxclock/settings.js b/apps/linuxclock/settings.js
new file mode 100644
index 000000000..116253fda
--- /dev/null
+++ b/apps/linuxclock/settings.js
@@ -0,0 +1,50 @@
+(function(back) {
+ const SETTINGS_FILE = "bwclk.setting.json";
+
+ // initialize with default settings...
+ const storage = require('Storage')
+ let settings = {
+ screen: "Normal",
+ showLock: true,
+ hideColon: false,
+ };
+ let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings;
+ for (const key in saved_settings) {
+ settings[key] = saved_settings[key]
+ }
+
+ function save() {
+ storage.write(SETTINGS_FILE, settings)
+ }
+
+ var screenOptions = ["Normal", "Dynamic", "Full"];
+ E.showMenu({
+ '': { 'title': 'BW Clock' },
+ '< Back': back,
+ 'Screen': {
+ value: 0 | screenOptions.indexOf(settings.screen),
+ min: 0, max: 2,
+ format: v => screenOptions[v],
+ onchange: v => {
+ settings.screen = screenOptions[v];
+ save();
+ },
+ },
+ 'Show Lock': {
+ value: settings.showLock,
+ format: () => (settings.showLock ? 'Yes' : 'No'),
+ onchange: () => {
+ settings.showLock = !settings.showLock;
+ save();
+ },
+ },
+ 'Hide Colon': {
+ value: settings.hideColon,
+ format: () => (settings.hideColon ? 'Yes' : 'No'),
+ onchange: () => {
+ settings.hideColon = !settings.hideColon;
+ save();
+ },
+ }
+ });
+ })
diff --git a/apps/locale/locales.js b/apps/locale/locales.js
index bfb8fdceb..7b3146e15 100644
--- a/apps/locale/locales.js
+++ b/apps/locale/locales.js
@@ -97,6 +97,25 @@ var locales = {
day: "Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday",
// No translation for english...
},
+ "en_IE": {
+ lang: "en_IE",
+ decimal_point: ".",
+ thousands_sep: ",",
+ currency_symbol: "€",
+ int_curr_symbol: "EUR",
+ currency_first: true,
+ speed: 'kmh',
+ distance: { "0": "m", "1": "km" },
+ temperature: '°C',
+ ampm: { 0: "am", 1: "pm" },
+ timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" },
+ datePattern: { 0: "%d %b %Y", 1: "%d/%m/%Y" }, // 28 Feb 2020" // "28/03/2020"(short)
+ abmonth: "Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec",
+ month: "January,February,March,April,May,June,July,August,September,October,November,December",
+ abday: "Sun,Mon,Tue,Wed,Thu,Fri,Sat",
+ day: "Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday",
+ // No translation for english...
+ },
"en_NAV": { // navigation units nautical miles and knots
lang: "en_NAV",
decimal_point: ".",
diff --git a/apps/macwatch2/ChangeLog b/apps/macwatch2/ChangeLog
index a60193ba7..12559d732 100644
--- a/apps/macwatch2/ChangeLog
+++ b/apps/macwatch2/ChangeLog
@@ -1,3 +1,6 @@
0.01: Created first version of the app with numeric date, only works in light mode
0.02: New icon, shimmied date right a bit
0.03: Incorporated improvements from Peer David for accuracy, fix dark mode, widgets run in background
+0.04: Changed clock to use 12/24 hour format based on locale
+0.05: Tell clock widgets to hide.
+0.06: Widgets can now be made visible by swiping down (#2196)
diff --git a/apps/macwatch2/app.js b/apps/macwatch2/app.js
index 3b78d5baf..36917a988 100644
--- a/apps/macwatch2/app.js
+++ b/apps/macwatch2/app.js
@@ -20,7 +20,7 @@ function queueDraw() {
function draw() {
queueDraw();
-
+
// Fix theme to "light"
g.setTheme({bg:"#fff", fg:"#000", dark:false}).clear();
g.reset();
@@ -30,20 +30,17 @@ function draw() {
g.setFontAlign(0, -1, 0);
g.setColor(0,0,0);
var d = new Date();
- var da = d.toString().split(" ");
- hh = da[4].substr(0,2);
- mi = da[4].substr(3,2);
+ var dt = require("locale").time(d, 1);
+ var hh = dt.split(":")[0];
+ var mm = dt.split(":")[1];
+ g.drawString(hh, 52, 65, true);
+ g.drawString(mm, 132, 65, true);
+ g.drawString(':', 93,65);
dd = ("0"+(new Date()).getDate()).substr(-2);
mo = ("0"+((new Date()).getMonth()+1)).substr(-2);
yy = ("0"+((new Date()).getFullYear())).substr(-2);
- g.drawString(hh, 52, 65, true);
- g.drawString(mi, 132, 65, true);
- g.drawString(':', 93,65);
g.setFontCustom(font, 48, 8, 521);
g.drawString(dd + ':' + mo + ':' + yy, 88, 120, true);
-
- // Hide widgets
- for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
}
@@ -57,8 +54,9 @@ Bangle.on('lcdPower',on=>{
}
});
+Bangle.setUI("clock");
// Load widgets but hide them
Bangle.loadWidgets();
+require("widget_utils").swipeOn(); // hide widgets, make them visible with a swipe
draw();
-Bangle.setUI("clock");
diff --git a/apps/macwatch2/metadata.json b/apps/macwatch2/metadata.json
index 09ec01e06..701c82102 100644
--- a/apps/macwatch2/metadata.json
+++ b/apps/macwatch2/metadata.json
@@ -2,7 +2,7 @@
"name": "MacWatch2",
"shortName":"MacWatch2",
"icon": "app.png",
- "version":"0.03",
+ "version":"0.06",
"description": "Classic Mac Finder clock",
"type": "clock",
"tags": "clock",
diff --git a/apps/matrixclock/ChangeLog b/apps/matrixclock/ChangeLog
index 52f705301..02f7d109b 100644
--- a/apps/matrixclock/ChangeLog
+++ b/apps/matrixclock/ChangeLog
@@ -1,4 +1,7 @@
0.01: Initial Release
0.02: Support for Bangle 2
0.03: Keep the date from being overwritten, use correct colour from theme for clearing
-0.04: Removed "wake LCD on face-up"-feature: A watch-face should not set things like "wake LCD on face-up".
+0.04: Removed "wake LCD on face-up"-feature: A watch-face should not set things like "wake LCD on face-up".
+0.05: Added support to other color themes (other then black)
+0.06: Added support for 24 hour clock enabled from settings
+0.07: Tell clock widgets to hide.
diff --git a/apps/matrixclock/README.md b/apps/matrixclock/README.md
index 010524b60..01aef6544 100644
--- a/apps/matrixclock/README.md
+++ b/apps/matrixclock/README.md
@@ -2,6 +2,25 @@

+## Settings
+Please use the setting->App->Matrix Clock Menu to change the settings
+
+| Setting | Description |
+|-------------|--------------------------------------------------------------------------------------------------------------------|
+| Color | By default set to **'theme'** to follow the theme colors. Selector also offers a selection of other colour schemes |
+| Time Format | Choose between 12 hour and 24 hour time format |
+| Intensity | Changes the number of matrix streams that are falling |
+
+## Colour Themes
+
+Some of the colours schemes that are available from the settings screen
+
+|  |  |  |
+|-------------------------------|-------------------------------|-----|
+| green on black | white on black | white on gray |
+
+
+
## Requests
Please reach out to adrian@adriankirk.com if you have feature requests or notice bugs.
diff --git a/apps/matrixclock/matrix_black_on_white.jpg b/apps/matrixclock/matrix_black_on_white.jpg
new file mode 100644
index 000000000..545545c65
Binary files /dev/null and b/apps/matrixclock/matrix_black_on_white.jpg differ
diff --git a/apps/matrixclock/matrix_green_on_black.jpg b/apps/matrixclock/matrix_green_on_black.jpg
new file mode 100644
index 000000000..7caa38bec
Binary files /dev/null and b/apps/matrixclock/matrix_green_on_black.jpg differ
diff --git a/apps/matrixclock/matrix_white_on_gray.jpg b/apps/matrixclock/matrix_white_on_gray.jpg
new file mode 100644
index 000000000..dc9d2f3ba
Binary files /dev/null and b/apps/matrixclock/matrix_white_on_gray.jpg differ
diff --git a/apps/matrixclock/matrixclock.js b/apps/matrixclock/matrixclock.js
index 2e4ba1ac4..9618c3a47 100644
--- a/apps/matrixclock/matrixclock.js
+++ b/apps/matrixclock/matrixclock.js
@@ -3,24 +3,107 @@
*
* Matrix Clock
*
- * A simple clock inspired by the movie.
- * Text shards move down the screen as a background to the
+ * A simple clock inspired by the movie.
+ * Text shards move down the screen as a background to the
* time and date
**/
const Locale = require('locale');
-const SHARD_COLOR =[0,1.0,0];
+const PREFERENCE_FILE = "matrixclock.settings.json";
+const settings = Object.assign({color: "theme", time_format: '12 hour', intensity: 'light'},
+ require('Storage').readJSON(PREFERENCE_FILE, true) || {});
+
+var format_time;
+if(settings.time_format == '24 hour'){
+ format_time = (t) => format_time_24_hour(t);
+} else {
+ format_time = (t) => format_time_12_hour(t);
+}
+
+const colors = {
+ 'gray' :[0.5,0.5,0.5],
+ 'green': [0,1.0,0],
+ 'red' : [1.0,0.0,0.0],
+ 'blue' : [0.0,0.0,1.0],
+ 'black': [0.0,0.0,0.0],
+ 'purple': [1.0,0.0,1.0],
+ 'white': [1.0,1.0,1.0],
+ 'yellow': [1.0,1.0,0.0]
+};
+
+const color_schemes = {
+ 'black on white': ['white','black'],
+ 'green on white' : ['white','green'],
+ 'green on black' : ['black','green'],
+ 'red on black' : ['black', 'red'],
+ 'red on white' : ['white', 'red'],
+ 'white on gray' : ['gray', 'white'],
+ 'white on red' : ['red', 'white'],
+ 'white on blue': ['blue','white'],
+ 'white on purple': ['purple', 'white']
+};
+
+function int2Color(color_int){
+ var blue_int = color_int & 31;
+ var blue = (blue_int)/31.0;
+
+ var green_int = (color_int >> 5) & 31;
+ var green = (green_int)/31.0;
+
+ var red_int = (color_int >> 11) & 31;
+ var red = red_int/ 31.0;
+ return [red,green,blue];
+}
+
+var fg_color = colors.black;
+var bg_color = colors.white;
+
+// now lets deal with the settings
+if(settings.color === "theme"){
+ bg_color = int2Color(g.theme.bg);
+ if(g.theme.bg === 0) {
+ fg_color = colors.green;
+ } else {
+ fg_color = int2Color(g.theme.fg);
+ }
+} else {
+ var color_scheme = color_schemes[settings.color];
+ bg_color = colors[color_scheme[0]];
+ fg_color = colors[color_scheme[1]];
+ g.setBgColor(bg_color[0],bg_color[1],bg_color[2]);
+}
+if(fg_color === undefined)
+ fg_color = colors.black;
+
+if(bg_color === undefined)
+ bg_color = colors.white;
+
+const intensity_schemes = {
+ 'light': 3,
+ 'medium': 4,
+ 'high': 5
+};
+
+var noShards = intensity_schemes.light;
+if(settings.intensity !== undefined){
+ noShards = intensity_schemes[settings.intensity];
+}
+if(noShards === undefined){
+ noShards = intensity_schemes.light;
+}
+
const SHARD_FONT_SIZE = 12;
const SHARD_Y_START = 30;
+
const w = g.getWidth();
/**
-* The text shard object is responsible for creating the
-* shards of text that move down the screen. As the
-* shard moves down the screen the latest character added
-* is brightest with characters being coloured darker and darker
-* going back to the eldest
-*/
+ * The text shard object is responsible for creating the
+ * shards of text that move down the screen. As the
+ * shard moves down the screen the latest character added
+ * is brightest with characters being coloured darker and darker
+ * going back to the eldest
+ */
class TextShard {
constructor(x,y,length){
@@ -34,44 +117,46 @@ class TextShard {
this.txt = [];
}
/**
- * The add method call adds another random character to
- * the chain
- */
+ * The add method call adds another random character to
+ * the chain
+ */
add(){
this.txt.push(randomChar());
}
/**
- * The show method displays the latest shard image to the
- * screen with the following rules:
- * - latest addition is brightest, oldest is darker
- * - display up to defined length of characters only
- * of the shard to save cpu
- */
+ * The show method displays the latest shard image to the
+ * screen with the following rules:
+ * - latest addition is brightest, oldest is darker
+ * - display up to defined length of characters only
+ * of the shard to save cpu
+ */
show(){
g.setFontAlign(-1,-1,0);
for(var i=0; i this.length - 2){
color_strength = 0;
- }
- g.setColor(color_strength*SHARD_COLOR[0],
- color_strength*SHARD_COLOR[1],
- color_strength*SHARD_COLOR[2]);
+ }
+ var bg_color_strength = 1 - color_strength;
+ g.setColor(Math.abs(color_strength*fg_color[0] - bg_color_strength*bg_color[0]),
+ Math.abs(color_strength*fg_color[1] - bg_color_strength*bg_color[1]),
+ Math.abs(color_strength*fg_color[2] - bg_color_strength*bg_color[2])
+ );
g.setFont("Vector",SHARD_FONT_SIZE);
- g.drawString(this.txt[idx], this.x, this.y + idx*SHARD_FONT_SIZE);
+ g.drawString(this.txt[idx], this.x, this.y + idx*SHARD_FONT_SIZE);
}
}
/**
- * Method tests to see if any part of the shard chain is still
- * visible on the screen
- */
+ * Method tests to see if any part of the shard chain is still
+ * visible on the screen
+ */
isVisible(){
- return (this.y + (this.txt.length - this.length - 2)*SHARD_FONT_SIZE < g.getHeight());
+ return (this.y + (this.txt.length - this.length - 2)*SHARD_FONT_SIZE < g.getHeight());
}
/**
- * resets the shard back to the top of the screen
- */
+ * resets the shard back to the top of the screen
+ */
reset(){
this.y = SHARD_Y_START;
this.txt = [];
@@ -79,8 +164,8 @@ class TextShard {
}
/**
-* random character chooser to be called by the shard when adding characters
-*/
+ * random character chooser to be called by the shard when adding characters
+ */
const CHAR_CODE_START = 33;
const CHAR_CODE_LAST = 126;
const CHAR_CODE_LENGTH = CHAR_CODE_LAST - CHAR_CODE_START;
@@ -90,11 +175,10 @@ function randomChar(){
// Now set up the shards
// we are going to have a limited no of shards (to save cpu)
-// but randomize the x value and length every reset to make it look as if there
+// but randomize the x value and length every reset to make it look as if there
// are more
var shards = [];
-const NO_SHARDS = 3;
-const channel_width = g.getWidth()/NO_SHARDS;
+const channel_width = g.getWidth()/noShards;
function shard_x(i){
return i*channel_width + Math.random() * channel_width;
@@ -104,7 +188,7 @@ function shard_length(){
return Math.floor(Math.random()*5) + 3;
}
-for(var i=0; i 99 || value < 0)
- throw "must be between in range 0-99";
- if(value < 10)
- return "0" + value.toString();
- else
- return value.toString();
+ var value = (num | 0);
+ if(value > 99 || value < 0)
+ throw "must be between in range 0-99";
+ if(value < 10)
+ return "0" + value.toString();
+ else
+ return value.toString();
}
// The interval reference for updating the clock
@@ -215,12 +304,12 @@ function startTimers(){
clearTimers();
if (Bangle.isLCDOn()) {
intervalRef = setInterval(() => {
- if (!shouldRedraw()) {
- //console.log("draw clock callback - skipped redraw");
- } else {
- draw_clock();
- }
- }, 100
+ if (!shouldRedraw()) {
+ //console.log("draw clock callback - skipped redraw");
+ } else {
+ draw_clock();
+ }
+ }, 100
);
draw_clock();
} else {
@@ -239,11 +328,9 @@ Bangle.on('lcdPower', (on) => {
}
});
+Bangle.setUI("clock");
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
startTimers();
-Bangle.setUI("clock");
-
-
diff --git a/apps/matrixclock/matrixclock.settings.js b/apps/matrixclock/matrixclock.settings.js
new file mode 100644
index 000000000..1f22a045f
--- /dev/null
+++ b/apps/matrixclock/matrixclock.settings.js
@@ -0,0 +1,52 @@
+(function(back) {
+ const PREFERENCE_FILE = "matrixclock.settings.json";
+ var settings = Object.assign({color : "theme", time_format: '12 hour', intensity: "light"},
+ require('Storage').readJSON(PREFERENCE_FILE, true) || {});
+
+ console.log("loaded:" + JSON.stringify(settings));
+
+ function writeSettings() {
+ console.log("saving:" + JSON.stringify(settings));
+ require('Storage').writeJSON(PREFERENCE_FILE, settings);
+ }
+
+ // Helper method which uses int-based menu item for set of string values
+ function stringItems(startvalue, writer, values) {
+ return {
+ value: (startvalue === undefined ? 0 : values.indexOf(startvalue)),
+ format: v => values[v],
+ min: 0,
+ max: values.length - 1,
+ wrap: true,
+ step: 1,
+ onchange: v => {
+ writer(values[v]);
+ writeSettings();
+ }
+ };
+ }
+
+ // Helper method which breaks string set settings down to local settings object
+ function stringInSettings(name, values) {
+ return stringItems(settings[name], v => settings[name] = v, values);
+ }
+
+ // Show the menu
+ E.showMenu({
+ "" : { "title" : "Matrix Clock" },
+ "< Back" : () => back(),
+ "Colour": stringInSettings("color", ['theme',
+ 'black on white',
+ 'green on white',
+ 'green on black',
+ 'red on white',
+ 'white on gray',
+ 'white on red',
+ 'white on blue'
+ ]),
+ "Time Format": stringInSettings("time_format", ['12 hour','24 hour']),
+ "Intensity": stringInSettings("intensity", ['light',
+ 'medium',
+ 'high'])
+ });
+})
\ No newline at end of file
diff --git a/apps/matrixclock/metadata.json b/apps/matrixclock/metadata.json
index 122cee3a1..718b878e5 100644
--- a/apps/matrixclock/metadata.json
+++ b/apps/matrixclock/metadata.json
@@ -1,10 +1,10 @@
{
"id": "matrixclock",
"name": "Matrix Clock",
- "version": "0.04",
+ "version": "0.07",
"description": "inspired by The Matrix, a clock of the same style",
"icon": "matrixclock.png",
- "screenshots": [{"url":"screenshot_matrix.png"}],
+ "screenshots": [{"url":"matrix_green_on_black.jpg"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS","BANGLEJS2"],
@@ -12,6 +12,8 @@
"allow_emulator": true,
"storage": [
{"name":"matrixclock.app.js","url":"matrixclock.js"},
+ { "name":"matrixclock.settings.js","url":"matrixclock.settings.js"},
{"name":"matrixclock.img","url":"matrixclock-icon.js","evaluate":true}
- ]
+ ],
+ "data": [{"name": "matrixclock.settings.json"}]
}
diff --git a/apps/matrixclock/screenshot_matrix.png b/apps/matrixclock/screenshot_matrix.png
deleted file mode 100644
index 3d843848c..000000000
Binary files a/apps/matrixclock/screenshot_matrix.png and /dev/null differ
diff --git a/apps/mclock/ChangeLog b/apps/mclock/ChangeLog
index 05b422406..e3b164942 100644
--- a/apps/mclock/ChangeLog
+++ b/apps/mclock/ChangeLog
@@ -5,3 +5,4 @@
Fix issue where first digit could get stuck going from "2x:xx" to " x:xx" (fix #365)
0.06: Support 12 hour time
0.07: Use Bangle.setUI for button/launcher handling
+0.08: Tell clock widgets to hide.
diff --git a/apps/mclock/clock-morphing.js b/apps/mclock/clock-morphing.js
index f1254860b..bd133206e 100644
--- a/apps/mclock/clock-morphing.js
+++ b/apps/mclock/clock-morphing.js
@@ -209,6 +209,9 @@ Bangle.on('lcdPower',function(on) {
}
});
+// Show launcher when button pressed
+Bangle.setUI("clock");
+
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
@@ -216,5 +219,3 @@ Bangle.drawWidgets();
timeInterval = setInterval(showTime, 1000);
showTime();
-// Show launcher when button pressed
-Bangle.setUI("clock");
diff --git a/apps/mclock/metadata.json b/apps/mclock/metadata.json
index 513f823a1..a7d56f752 100644
--- a/apps/mclock/metadata.json
+++ b/apps/mclock/metadata.json
@@ -1,7 +1,7 @@
{
"id": "mclock",
"name": "Morphing Clock",
- "version": "0.07",
+ "version": "0.08",
"description": "7 segment clock that morphs between minutes and hours",
"icon": "clock-morphing.png",
"type": "clock",
diff --git a/apps/medicalinfo/ChangeLog b/apps/medicalinfo/ChangeLog
new file mode 100644
index 000000000..e8739a121
--- /dev/null
+++ b/apps/medicalinfo/ChangeLog
@@ -0,0 +1 @@
+0.01: Initial Medical Information application!
diff --git a/apps/medicalinfo/README.md b/apps/medicalinfo/README.md
new file mode 100644
index 000000000..6dd19d4c6
--- /dev/null
+++ b/apps/medicalinfo/README.md
@@ -0,0 +1,27 @@
+# Medical Information
+
+This app displays basic medical information, and provides a common way to set up the `medicalinfo.json` file, which other apps can use if required.
+
+## Medical information JSON file
+
+When the app is loaded from the app loader, a file named `medicalinfo.json` is loaded along with the javascript etc.
+The file has the following contents:
+
+```
+{
+ "bloodType": "",
+ "height": "",
+ "weight": "",
+ "medicalAlert": [ "" ]
+}
+```
+
+## Medical information editor
+
+Clicking on the download icon of `Medical Information` in the app loader invokes the editor.
+The editor downloads and displays the current `medicalinfo.json` file, which can then be edited.
+The edited `medicalinfo.json` file is uploaded to the Bangle by clicking the `Upload` button.
+
+## Creator
+
+James Taylor ([jt-nti](https://github.com/jt-nti))
diff --git a/apps/medicalinfo/app-icon.js b/apps/medicalinfo/app-icon.js
new file mode 100644
index 000000000..1ae7916fb
--- /dev/null
+++ b/apps/medicalinfo/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwg+7kUiCykCC4MgFykgDIIXUAQgAMiMRiREBC4YABkILBCxEBC4pHCC4kQFxIXEAAgXCGBERif/+QXHl//mIXJj//+YXHn//+IXL/8yCwsjBIIXNABIX/C63d7oDB+czmaPPC7hHR/oWBAAPfC65HRC7qnXX/4XDABAXkIIQAFI5wXXL/5f/L/5fvC9sTC5cxC5IAOC48BCxsQC44wOCxAArA"))
diff --git a/apps/medicalinfo/app.js b/apps/medicalinfo/app.js
new file mode 100644
index 000000000..9c4941744
--- /dev/null
+++ b/apps/medicalinfo/app.js
@@ -0,0 +1,61 @@
+const medicalinfo = require('medicalinfo').load();
+// const medicalinfo = {
+// bloodType: "O+",
+// height: "166cm",
+// weight: "73kg"
+// };
+
+function hasAlert(info) {
+ return (Array.isArray(info.medicalAlert)) && (info.medicalAlert[0]);
+}
+
+// No space for widgets!
+// TODO: no padlock widget visible so prevent screen locking?
+
+g.clear();
+const bodyFont = g.getFonts().includes("12x20") ? "12x20" : "6x8:2";
+g.setFont(bodyFont);
+
+const title = hasAlert(medicalinfo) ? "MEDICAL ALERT" : "Medical Information";
+var lines = [];
+
+lines = g.wrapString(title, g.getWidth() - 10);
+var titleCnt = lines.length;
+if (titleCnt) lines.push(""); // add blank line after title
+
+if (hasAlert(medicalinfo)) {
+ medicalinfo.medicalAlert.forEach(function (details) {
+ lines = lines.concat(g.wrapString(details, g.getWidth() - 10));
+ });
+ lines.push(""); // add blank line after medical alert
+}
+
+if (medicalinfo.bloodType) {
+ lines = lines.concat(g.wrapString("Blood group: " + medicalinfo.bloodType, g.getWidth() - 10));
+}
+if (medicalinfo.height) {
+ lines = lines.concat(g.wrapString("Height: " + medicalinfo.height, g.getWidth() - 10));
+}
+if (medicalinfo.weight) {
+ lines = lines.concat(g.wrapString("Weight: " + medicalinfo.weight, g.getWidth() - 10));
+}
+
+lines.push("");
+
+// TODO: display instructions for updating medical info if there is none!
+
+E.showScroller({
+ h: g.getFontHeight(), // height of each menu item in pixels
+ c: lines.length, // number of menu items
+ // a function to draw a menu item
+ draw: function (idx, r) {
+ // FIXME: in 2v13 onwards, clearRect(r) will work fine. There's a bug in 2v12
+ g.setBgColor(idx < titleCnt ? g.theme.bg2 : g.theme.bg).
+ setColor(idx < titleCnt ? g.theme.fg2 : g.theme.fg).
+ clearRect(r.x, r.y, r.x + r.w, r.y + r.h);
+ g.setFont(bodyFont).drawString(lines[idx], r.x, r.y);
+ }
+});
+
+// Show launcher when button pressed
+setWatch(() => load(), process.env.HWVERSION === 2 ? BTN : BTN3, { repeat: false, edge: "falling" });
diff --git a/apps/medicalinfo/app.png b/apps/medicalinfo/app.png
new file mode 100644
index 000000000..16204ea89
Binary files /dev/null and b/apps/medicalinfo/app.png differ
diff --git a/apps/medicalinfo/interface.html b/apps/medicalinfo/interface.html
new file mode 100644
index 000000000..9376be32f
--- /dev/null
+++ b/apps/medicalinfo/interface.html
@@ -0,0 +1,135 @@
+
+
+
+
+
+
+
+
+
+ Reload from watch
+ Upload to watch
+ Download
+
+
+
+
+
+
+
diff --git a/apps/medicalinfo/lib.js b/apps/medicalinfo/lib.js
new file mode 100644
index 000000000..683005359
--- /dev/null
+++ b/apps/medicalinfo/lib.js
@@ -0,0 +1,21 @@
+const storage = require('Storage');
+
+exports.load = function () {
+ const medicalinfo = storage.readJSON('medicalinfo.json') || {
+ bloodType: "",
+ height: "",
+ weight: "",
+ medicalAlert: [""]
+ };
+
+ // Don't return anything unexpected
+ const expectedMedicalinfo = [
+ "bloodType",
+ "height",
+ "weight",
+ "medicalAlert"
+ ].filter(key => key in medicalinfo)
+ .reduce((obj, key) => (obj[key] = medicalinfo[key], obj), {});
+
+ return expectedMedicalinfo;
+};
diff --git a/apps/medicalinfo/medicalinfo.json b/apps/medicalinfo/medicalinfo.json
new file mode 100644
index 000000000..8b49725cb
--- /dev/null
+++ b/apps/medicalinfo/medicalinfo.json
@@ -0,0 +1,6 @@
+{
+ "bloodType": "",
+ "height": "",
+ "weight": "",
+ "medicalAlert": [ "" ]
+}
diff --git a/apps/medicalinfo/metadata.json b/apps/medicalinfo/metadata.json
new file mode 100644
index 000000000..f1a0c145f
--- /dev/null
+++ b/apps/medicalinfo/metadata.json
@@ -0,0 +1,20 @@
+{ "id": "medicalinfo",
+ "name": "Medical Information",
+ "version":"0.01",
+ "description": "Provides 'medicalinfo.json' used by various health apps, as well as a way to edit it from the App Loader",
+ "icon": "app.png",
+ "tags": "health,medical",
+ "type": "app",
+ "supports" : ["BANGLEJS","BANGLEJS2"],
+ "readme": "README.md",
+ "screenshots": [{"url":"screenshot_light.png"}],
+ "interface": "interface.html",
+ "storage": [
+ {"name":"medicalinfo.app.js","url":"app.js"},
+ {"name":"medicalinfo.img","url":"app-icon.js","evaluate":true},
+ {"name":"medicalinfo","url":"lib.js"}
+ ],
+ "data": [
+ {"name":"medicalinfo.json","url":"medicalinfo.json"}
+ ]
+}
diff --git a/apps/medicalinfo/screenshot_light.png b/apps/medicalinfo/screenshot_light.png
new file mode 100644
index 000000000..42970f9fc
Binary files /dev/null and b/apps/medicalinfo/screenshot_light.png differ
diff --git a/apps/messagegui/ChangeLog b/apps/messagegui/ChangeLog
new file mode 100644
index 000000000..228a952de
--- /dev/null
+++ b/apps/messagegui/ChangeLog
@@ -0,0 +1,88 @@
+0.01: New App!
+0.02: Add 'messages' library
+0.03: Fixes for Bangle.js 1
+0.04: Add require("messages").clearAll()
+0.05: Handling of message actions (ok/clear)
+0.06: New messages now go at the start (fix #898)
+ Answering true/false now exits the messages app if no new messages
+ Back now marks a message as read
+ Clicking top-left opens a menu which allows you to delete a message or mark unread
+0.07: Added settings menu with option to choose vibrate pattern and frequency (fix #909)
+0.08: Fix rendering of long messages (fix #969)
+ buzz on new message (fix #999)
+0.09: Message now disappears after 60s if no action taken and clock loads (fix 922)
+ Fix phone icon (#1014)
+0.10: Respect the 'new' attribute if it was set from iOS integrations
+0.11: Open app when touching the widget (Bangle.js 2 only)
+0.12: Extra app-specific notification icons
+ New animated notification icon (instead of large blinking 'MESSAGES')
+ Added screenshots
+0.13: Add /*LANG*/ comments for internationalisation
+ Add 'Delete All' option to message options
+ Now update correctly when 'require("messages").clearAll()' is called
+0.14: Hide widget when all unread notifications are dismissed from phone
+0.15: Don't buzz when Quiet Mode is active
+0.16: Fix text wrapping so it fits the screen even if title is big (fix #1147)
+0.17: Fix: Get dynamic dimensions of notify icon, fixed notification font
+0.18: Use app-specific icon colors
+ Spread message action buttons out
+ Back button now goes back to list of messages
+ If showMessage called with no message (eg all messages deleted) now return to the clock (fix #1267)
+0.19: Use a larger font for message text if it'll fit
+0.20: Allow tapping on the body to show a scrollable view of the message and title in a bigger font (fix #1405, #1031)
+0.21: Improve list readability on dark theme
+0.22: Add Home Assistant icon
+ Allow repeat to be switched Off, so there is no buzzing repetition.
+ Also gave the widget a pixel more room to the right
+0.23: Change message colors to match current theme instead of using green
+ Now attempt to use Large/Big/Medium fonts, and allow minimum font size to be configured
+0.24: Remove left-over debug statement
+0.25: Fix widget memory usage issues if message received and watch repeatedly calls Bangle.drawWidgets (fix #1550)
+0.26: Setting to auto-open music
+0.27: Add 'mark all read' option to popup menu (fix #1624)
+0.28: Option to auto-unlock the watch when a new message arrives
+0.29: Fix message list overwrites on Bangle.js 1 (fix #1642)
+0.30: Add new Icons (Youtube, Twitch, MS TODO, Teams, Snapchat, Signal, Post & DHL, Nina, Lieferando, Kalender, Discord, Corona Warn, Bibel)
+0.31: Option to disable icon flashing
+0.32: Added an option to allow quiet mode to override message auto-open
+0.33: Timeout from the message list screen if the message being displayed is removed and there is a timer going
+0.34: Don't buzz for 'map' update messages
+0.35: Reset graphics colors before rendering a message (possibly fix #1752)
+0.36: Ensure a new message plus an almost immediate deletion of that message doesn't load the messages app (fix #1362)
+0.37: Now use the setUI 'back' icon in the top left rather than specific buttons/menu items
+0.38: Add telegram foss handling
+0.39: Set default color for message icons according to theme
+0.40: Use default Bangle formatter for booleans
+0.41: Add notification icons in the widget
+0.42: Fix messages ignoring "Vibrate: Off" setting
+0.43: Add new Icons (Airbnb, warnwetter)
+0.44: Separate buzz pattern for incoming calls
+0.45: Added new app colors and icons
+0.46: Add 'Vibrate Timer' option to set how long to vibrate for, and fix Repeat:off
+ Fix message removal from widget bar (previously caused exception as .hide has been removed)
+0.47: Add new Icons (Nextbike, Mattermost, etc.)
+0.48: When getting new message from the clock, only buzz once the messages app is loaded
+0.49: Change messages icon (to fit within 24px) and ensure widget renders icons centrally
+0.50: Add `getMessages` and `status` functions to library
+ Option to disable auto-open of messages
+ Option to make message icons monochrome (not colored)
+ messages widget buzz now returns a promise
+0.51: Emit "message events"
+ Setting to hide widget
+ Add custom event handlers to prevent default app form loading
+ Move WIDGETS.messages.buzz() to require("messages").buzz()
+0.52: Fix require("messages").buzz() regression
+ Fix background color in messages list after one unread message is shown
+0.53: Messages now uses Bangle.load() to load messages app faster (if possible)
+0.54: Move icons out to messageicons module
+0.55: Rename to messagegui, move global message handling library to message module
+ Move widget to widmessage
+0.56: Fix handling of music messages
+0.57: Fix "unread Timeout" = off (previously defaulted to 60s)
+0.58: Fast load messages without writing to flash
+ Don't write messages to flash until the app closes
+0.59: Ensure we do write messages if messages app can't be fast loaded (see #2373)
+0.60: Fix saving of removal messages if UI not open
+0.61: Fix regression where loading into messages app stops back from working (#2398)
+0.62: Remove '.show' field, tidyup and fix .open if fast load not enabled
+0.63: Fix messages app loading on clock without fast load
diff --git a/apps/messagegui/README.md b/apps/messagegui/README.md
new file mode 100644
index 000000000..699588e1b
--- /dev/null
+++ b/apps/messagegui/README.md
@@ -0,0 +1,68 @@
+# Messages app
+
+Default app to handle the display of messages and message notifications. It allows
+them to be listed, viewed, and responded to.
+It is installed automatically if you install `Android Integration` or `iOS Integration`.
+
+It is a replacement for the old `notify`/`gadgetbridge` apps.
+
+
+## Settings
+
+You can change settings by going to the global `Settings` app, then `App Settings`
+and `Messages`:
+
+* `Vibrate` - This is the pattern of buzzes that should be made when a new message is received
+* `Vibrate for calls` - This is the pattern of buzzes that should be made when an incoming call is received
+* `Repeat` - How often should buzzes repeat - the default of 4 means the Bangle will buzz every 4 seconds
+* `Vibrate Timer` - When a new message is received when in a non-clock app, we display the message icon and
+buzz every `Repeat` seconds. This is how long we continue to do that.
+* `Unread Timer` - When a new message is received when showing the clock we go into the Messages app.
+If there is no user input for this amount of time then the app will exit and return
+to the clock where a ringing bell will be shown in the Widget bar.
+* `Min Font` - The minimum font size used when displaying messages on the screen. A bigger font
+is chosen if there isn't much message text, but this specifies the smallest the font should get before
+it starts getting clipped.
+* `Auto-Open Music` - Should the app automatically open when the phone starts playing music?
+* `Unlock Watch` - Should the app unlock the watch when a new message arrives, so you can touch the buttons at the bottom of the app?
+
+## New Messages
+
+When a new message is received:
+
+* If you're in an app, the Bangle will buzz and a message icon appears in the Widget bar. You can tap this icon to view the message.
+* If you're in a clock, the Messages app will automatically start and show the message
+
+When a message is shown, you'll see a screen showing the message title and text.
+
+* The 'back-arrow' button (or physical button on Bangle.js 2) goes back to Messages, marking the current message as read.
+* The top-left icon shows more options, for instance deleting the message of marking unread
+* On Bangle.js 2 you can tap on the message body to view a scrollable version of the title and text (or can use the top-left icon + `View Message`)
+* If shown, the 'tick' button:
+ * **Android** opens the notification on the phone
+ * **iOS** responds positively to the notification (accept call/etc)
+* If shown, the 'cross' button:
+ * **Android** dismisses the notification on the phone
+ * **iOS** responds negatively to the notification (dismiss call/etc)
+
+## Images
+_1. Screenshot of a notification_
+
+
+
+
+## Requests
+
+Please file any issues on https://github.com/espruino/BangleApps/issues/new?title=messages%20app
+
+## Creator
+
+Gordon Williams
+
+## Contributors
+
+[Jeroen Peters](https://github.com/jeroenpeters1986)
+
+## Attributions
+
+Icons used in this app are from https://icons8.com
diff --git a/apps/messages/app-icon.js b/apps/messagegui/app-icon.js
similarity index 100%
rename from apps/messages/app-icon.js
rename to apps/messagegui/app-icon.js
diff --git a/apps/messagegui/app-newmessage.js b/apps/messagegui/app-newmessage.js
new file mode 100644
index 000000000..73d9a79c1
--- /dev/null
+++ b/apps/messagegui/app-newmessage.js
@@ -0,0 +1,5 @@
+/* Called when we have a new message when we're in the clock...
+BUZZ_ON_NEW_MESSAGE is set so when messagegui.app.js loads it knows
+that it should buzz */
+global.BUZZ_ON_NEW_MESSAGE = true;
+eval(require("Storage").read("messagegui.app.js"));
diff --git a/apps/messages/app.js b/apps/messagegui/app.js
similarity index 83%
rename from apps/messages/app.js
rename to apps/messagegui/app.js
index d4540b797..b158310a1 100644
--- a/apps/messages/app.js
+++ b/apps/messagegui/app.js
@@ -19,7 +19,6 @@ require("messages").pushMessage({"t":"add","id":1,"src":"Maps","title":"0 yd - H
// call
require("messages").pushMessage({"t":"add","id":"call","src":"Phone","title":"Bob","body":"12421312",positive:true,negative:true})
*/
-
var Layout = require("Layout");
var settings = require('Storage').readJSON("messages.settings.json", true) || {};
var fontSmall = "6x8";
@@ -48,14 +47,21 @@ we should start a timeout for settings.unreadTimeout to return
to the clock. */
var unreadTimeout;
/// List of all our messages
-var MESSAGES = require("Storage").readJSON("messages.json",1)||[];
-if (!Array.isArray(MESSAGES)) MESSAGES=[];
-var onMessagesModified = function(msg) {
+var MESSAGES = require("messages").getMessages();
+if (Bangle.MESSAGES) {
+ // fast loading messages
+ Bangle.MESSAGES.forEach(m => require("messages").apply(m, MESSAGES));
+ delete Bangle.MESSAGES;
+}
+
+var onMessagesModified = function(type,msg) {
+ if (msg.handled) return;
+ msg.handled = true;
+ require("messages").apply(msg, MESSAGES);
// TODO: if new, show this new one
if (msg && msg.id!=="music" && msg.new && active!="map" &&
!((require('Storage').readJSON('setting.json', 1) || {}).quiet)) {
- if (WIDGETS["messages"]) WIDGETS["messages"].buzz();
- else Bangle.buzz();
+ require("messages").buzz(msg.src);
}
if (msg && msg.id=="music") {
if (msg.state && msg.state!="play") openMusic = false; // no longer playing music to go back to
@@ -63,14 +69,16 @@ var onMessagesModified = function(msg) {
}
showMessage(msg&&msg.id);
};
+Bangle.on("message", onMessagesModified);
+
function saveMessages() {
- require("Storage").writeJSON("messages.json",MESSAGES)
+ require("messages").write(MESSAGES);
}
+E.on("kill", saveMessages);
function showMapMessage(msg) {
active = "map";
- var m;
- var distance, street, target, eta;
+ var m, distance, street, target, eta;
m=msg.title.match(/(.*) - (.*)/);
if (m) {
distance = m[1];
@@ -99,16 +107,18 @@ function showMapMessage(msg) {
layout.render();
function back() { // mark as not new and return to menu
msg.new = false;
- saveMessages();
layout = undefined;
checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1,openMusic:0});
}
Bangle.setUI({mode:"updown", back: back}, back); // any input takes us back
}
-var updateLabelsInterval;
+let updateLabelsInterval;
+
function showMusicMessage(msg) {
active = "music";
+ // defaults, so e.g. msg.xyz.length doesn't error. `msg` should contain up to date info
+ msg = Object.assign({artist: "", album: "", track: "Music"}, msg);
openMusic = msg.state=="play";
var trackScrollOffset = 0;
var artistScrollOffset = 0;
@@ -132,7 +142,6 @@ function showMusicMessage(msg) {
openMusic = false;
var wasNew = msg.new;
msg.new = false;
- saveMessages();
layout = undefined;
if (wasNew) checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:0,openMusic:0});
else checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0});
@@ -215,24 +224,20 @@ function showMessageSettings(msg) {
},
/*LANG*/"Delete" : () => {
MESSAGES = MESSAGES.filter(m=>m.id!=msg.id);
- saveMessages();
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0});
},
/*LANG*/"Mark Unread" : () => {
msg.new = true;
- saveMessages();
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0});
},
/*LANG*/"Mark all read" : () => {
MESSAGES.forEach(msg => msg.new = false);
- saveMessages();
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0});
},
/*LANG*/"Delete all messages" : () => {
E.showPrompt(/*LANG*/"Are you sure?", {title:/*LANG*/"Delete All Messages"}).then(isYes => {
if (isYes) {
MESSAGES = [];
- saveMessages();
}
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0});
});
@@ -286,7 +291,8 @@ function showMessage(msgid) {
}
}
function goBack() {
- msg.new = false; saveMessages(); // read mail
+ layout = undefined;
+ msg.new = false; // read mail
cancelReloadTimeout(); // don't auto-reload to clock now
checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:0,openMusic:openMusic});
}
@@ -294,7 +300,7 @@ function showMessage(msgid) {
];
if (msg.positive) {
buttons.push({type:"btn", src:atob("GRSBAAAAAYAAAcAAAeAAAfAAAfAAAfAAAfAAAfAAAfBgAfA4AfAeAfAPgfAD4fAA+fAAP/AAD/AAA/AAAPAAADAAAA=="), cb:()=>{
- msg.new = false; saveMessages();
+ msg.new = false;
cancelReloadTimeout(); // don't auto-reload to clock now
Bangle.messageResponse(msg,true);
checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1,openMusic:openMusic});
@@ -303,7 +309,7 @@ function showMessage(msgid) {
if (msg.negative) {
if (buttons.length) buttons.push({width:32}); // nasty hack...
buttons.push({type:"btn", src:atob("FhaBADAAMeAB78AP/4B/fwP4/h/B/P4D//AH/4AP/AAf4AB/gAP/AB/+AP/8B/P4P4fx/A/v4B//AD94AHjAAMA="), cb:()=>{
- msg.new = false; saveMessages();
+ msg.new = false;
cancelReloadTimeout(); // don't auto-reload to clock now
Bangle.messageResponse(msg,false);
checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1,openMusic:openMusic});
@@ -317,10 +323,14 @@ function showMessage(msgid) {
{type:"txt", font:fontSmall, label:msg.src||/*LANG*/"Message", bgCol:g.theme.bg2, col: g.theme.fg2, fillx:1, pad:2, halign:1 },
title?{type:"txt", font:titleFont, label:title, bgCol:g.theme.bg2, col: g.theme.fg2, fillx:1, pad:2 }:{},
]},
- { type:"btn", src:require("messages").getMessageImage(msg), col:require("messages").getMessageImageCol(msg, g.theme.fg2), pad: 3, cb:()=>{
- cancelReloadTimeout(); // don't auto-reload to clock now
- showMessageSettings(msg);
- }},
+ { type:"btn",
+ src:require("messageicons").getImage(msg),
+ col:require("messageicons").getColor(msg, {settings:settings, default:g.theme.fg2}),
+ pad: 3, cb:()=>{
+ cancelReloadTimeout(); // don't auto-reload to clock now
+ showMessageSettings(msg);
+ }
+ },
]},
{type:"txt", font:bodyFont, label:body, fillx:1, filly:1, pad:2, cb:()=>{
// allow tapping to show a larger version
@@ -337,6 +347,7 @@ function showMessage(msgid) {
clockIfNoMsg : bool
clockIfAllRead : bool
showMsgIfUnread : bool
+ openMusic : bool // open music if it's playing
}
*/
function checkMessages(options) {
@@ -353,10 +364,21 @@ function checkMessages(options) {
// we have >0 messages
var newMessages = MESSAGES.filter(m=>m.new&&m.id!="music");
// If we have a new message, show it
- if (options.showMsgIfUnread && newMessages.length)
- return showMessage(newMessages[0].id);
- // no new messages: show playing music? (only if we have playing music to show)
- if (options.openMusic && MESSAGES.some(m=>m.id=="music" && m.track && m.state=="play"))
+ if (options.showMsgIfUnread && newMessages.length) {
+ delete newMessages[0].show; // stop us getting stuck here if we're called a second time
+ showMessage(newMessages[0].id);
+ // buzz after showMessage, so being busy during layout doesn't affect the buzz pattern
+ if (global.BUZZ_ON_NEW_MESSAGE) {
+ // this is set if we entered the messages app by loading `messagegui.new.js`
+ // ... but only buzz the first time we view a new message
+ global.BUZZ_ON_NEW_MESSAGE = false;
+ // messages.buzz respects quiet mode - no need to check here
+ require("messages").buzz(newMessages[0].src);
+ }
+ return;
+ }
+ // no new messages: show playing music? Only if we have playing music, or state=="show" (set by messagesmusic)
+ if (options.openMusic && MESSAGES.some(m=>m.id=="music" && ((m.track && m.state=="play") || m.state=="show")))
return showMessage('music');
// no new messages - go to clock?
if (options.clockIfAllRead && newMessages.length==0)
@@ -369,18 +391,19 @@ function checkMessages(options) {
draw : function(idx, r) {"ram"
var msg = MESSAGES[idx];
if (msg && msg.new) g.setBgColor(g.theme.bgH).setColor(g.theme.fgH);
- else g.setColor(g.theme.fg);
+ else g.setBgColor(g.theme.bg).setColor(g.theme.fg);
g.clearRect(r.x,r.y,r.x+r.w, r.y+r.h);
if (!msg) return;
var x = r.x+2, title = msg.title, body = msg.body;
- var img = require("messages").getMessageImage(msg);
+ var img = require("messageicons").getImage(msg);
if (msg.id=="music") {
title = msg.artist || /*LANG*/"Music";
body = msg.track;
}
if (img) {
- var fg = g.getColor();
- g.setColor(require("messages").getMessageImageCol(msg,fg)).drawImage(img, x+24, r.y+24, {rotate:0}) // force centering
+ var fg = g.getColor(),
+ col = require("messageicons").getColor(msg, {settings:settings, default:fg});
+ g.setColor(col).drawImage(img, x+24, r.y+24, {rotate:0}) // force centering
.setColor(fg); // only color the icon
x += 50;
}
@@ -414,14 +437,16 @@ function cancelReloadTimeout() {
g.clear();
Bangle.loadWidgets();
+require("messages").toggleWidget(false);
Bangle.drawWidgets();
setTimeout(() => {
- var unreadTimeoutMillis = (settings.unreadTimeout || 60) * 1000;
- if (unreadTimeoutMillis) {
- unreadTimeout = setTimeout(load, unreadTimeoutMillis);
- }
- // only openMusic on launch if music is new
- var newMusic = MESSAGES.some(m => m.id === "music" && m.new);
- checkMessages({ clockIfNoMsg: 0, clockIfAllRead: 0, showMsgIfUnread: 1, openMusic: newMusic && settings.openMusic });
+ if (!isFinite(settings.unreadTimeout)) settings.unreadTimeout=60;
+ if (settings.unreadTimeout)
+ unreadTimeout = setTimeout(load, settings.unreadTimeout*1000);
+ // only openMusic on launch if music is new, or state=="show" (set by messagesmusic)
+ var musicMsg = MESSAGES.find(m => m.id === "music");
+ checkMessages({
+ clockIfNoMsg: 0, clockIfAllRead: 0, showMsgIfUnread: 1,
+ openMusic: ((musicMsg&&musicMsg.new) && settings.openMusic) || (musicMsg&&musicMsg.state=="show") });
}, 10); // if checkMessages wants to 'load', do that
diff --git a/apps/messagegui/app.png b/apps/messagegui/app.png
new file mode 100644
index 000000000..c9177692e
Binary files /dev/null and b/apps/messagegui/app.png differ
diff --git a/apps/messagegui/boot.js b/apps/messagegui/boot.js
new file mode 100644
index 000000000..ce7f1b99c
--- /dev/null
+++ b/apps/messagegui/boot.js
@@ -0,0 +1 @@
+Bangle.on("message", (type, msg) => require("messagegui").listener(type, msg));
diff --git a/apps/messagegui/lib.js b/apps/messagegui/lib.js
new file mode 100644
index 000000000..a9436a77b
--- /dev/null
+++ b/apps/messagegui/lib.js
@@ -0,0 +1,101 @@
+// Will calling Bangle.load reset everything? if false, we fast load
+function loadWillReset() {
+ return Bangle.load === load || !Bangle.uiRemove;
+ /* FIXME: Maybe we need a better way of deciding if an app will
+ be fast loaded than just hard-coding a Bangle.uiRemove check.
+ Bangle.load could return a bool (as the load doesn't happen immediately). */
+}
+
+/**
+ * Listener set up in boot.js, calls into here to keep boot.js short
+ */
+exports.listener = function(type, msg) {
+ // Default handler: Launch the GUI for all unhandled messages (except music if disabled in settings)
+ if (msg.handled || (global.__FILE__ && __FILE__.startsWith('messagegui.'))) return; // already handled or app open
+
+ // if no new messages now, make sure we don't load the messages app
+ if (exports.messageTimeout && !msg.new && require("messages").status(msg) !== "new") {
+ clearTimeout(exports.messageTimeout);
+ delete exports.messageTimeout;
+ }
+ if (msg.t==="remove") {
+ // we won't open the UI for removed messages, so make sure to delete it from flash
+ if (Bangle.MESSAGES) {
+ // we were waiting for exports.messageTimeout
+ require("messages").apply(msg, Bangle.MESSAGES);
+ if (!Bangle.MESSAGES.length) delete Bangle.MESSAGES;
+ }
+ return require("messages").save(msg); // always write removal to flash
+ }
+
+ const appSettings = require("Storage").readJSON("messages.settings.json", 1) || {};
+ let loadMessages = (Bangle.CLOCK || event.important); // should we load the messages app?
+ if (type==="music") {
+ if (Bangle.CLOCK && msg.state && msg.title && appSettings.openMusic) loadMessages = true;
+ else return;
+ }
+ // Write the message to Bangle.MESSAGES. We'll deal with it in messageTimeout below
+ if (!Bangle.MESSAGES) Bangle.MESSAGES = [];
+ require("messages").apply(msg, Bangle.MESSAGES);
+ if (!Bangle.MESSAGES.length) delete Bangle.MESSAGES;
+ const saveToFlash = () => {
+ // save messages from RAM to flash if we decide not to launch app
+ // We apply all of Bangle.MESSAGES here in one write
+ if (!Bangle.MESSAGES || !Bangle.MESSAGES.length) return;
+ let messages = require("messages").getMessages(msg);
+ (Bangle.MESSAGES || []).forEach(m => require("messages").apply(m, messages));
+ require("messages").write(messages);
+ delete Bangle.MESSAGES;
+ }
+ msg.handled = true;
+ if ((msg.t!=="add" || !msg.new) && (type!=="music")) // music always has t:"modify"
+ return saveToFlash();
+
+ const quiet = (require("Storage").readJSON("setting.json", 1) || {}).quiet;
+ const unlockWatch = appSettings.unlockWatch;
+ // don't auto-open messages in quiet mode if quietNoAutOpn is true
+ if ((quiet && appSettings.quietNoAutOpn) || appSettings.noAutOpn)
+ loadMessages = false;
+ // after a delay load the app, to ensure we have all the messages
+ if (exports.messageTimeout) clearTimeout(exports.messageTimeout);
+ exports.messageTimeout = setTimeout(function() {
+ delete exports.messageTimeout;
+ if (!Bangle.MESSAGES) return; // message was removed during the delay
+ if (type!=="music") {
+ if (!loadMessages) {
+ // not opening the app, just buzz
+ saveToFlash();
+ return require("messages").buzz(msg.src);
+ }
+ if (!quiet && unlockWatch) {
+ Bangle.setLocked(false);
+ Bangle.setLCDPower(1); // turn screen on
+ }
+ }
+ // if loading the gui would reload everything, we must save our messages
+ if (loadWillReset()) saveToFlash();
+ exports.open(msg);
+ }, 500);
+};
+
+/**
+ * Launch GUI app with given message
+ * @param {object} msg
+ */
+exports.open = function(msg) {
+ if (msg && msg.id) {
+ // force a display by setting it as new and ensuring it ends up at the beginning of messages list
+ msg.new = 1;
+ if (loadWillReset()) {
+ // no fast loading: store message to load in flash - `msg` will be put in first
+ require("messages").save(msg, {force: 1});
+ } else {
+ // fast load - putting it at the end of Bangle.MESSAGES ensures it goes at the start of messages list
+ if (!Bangle.MESSAGES) Bangle.MESSAGES=[];
+ Bangle.MESSAGES = Bangle.MESSAGES.filter(m => m.id!=msg.id)
+ Bangle.MESSAGES.push(msg); // putting at the
+ }
+ }
+
+ Bangle.load((msg && msg.new && msg.id!=="music") ? "messagegui.new.js" : "messagegui.app.js");
+};
diff --git a/apps/messagegui/metadata.json b/apps/messagegui/metadata.json
new file mode 100644
index 000000000..1a7a6c750
--- /dev/null
+++ b/apps/messagegui/metadata.json
@@ -0,0 +1,24 @@
+{
+ "id": "messagegui",
+ "name": "Message UI",
+ "shortName": "Messages",
+ "version": "0.63",
+ "description": "Default app to display notifications from iOS and Gadgetbridge/Android",
+ "icon": "app.png",
+ "type": "app",
+ "tags": "tool,system",
+ "supports": ["BANGLEJS","BANGLEJS2"],
+ "dependencies" : { "messageicons":"module" },
+ "provides_modules": ["messagegui"],
+ "default": true,
+ "readme": "README.md",
+ "storage": [
+ {"name":"messagegui","url":"lib.js"},
+ {"name":"messagegui.app.js","url":"app.js"},
+ {"name":"messagegui.new.js","url":"app-newmessage.js"},
+ {"name":"messagegui.boot.js","url":"boot.js"},
+ {"name":"messagegui.img","url":"app-icon.js","evaluate":true}
+ ],
+ "screenshots": [{"url":"screenshot.png"}],
+ "sortorder": -9
+}
diff --git a/apps/messages/screenshot.png b/apps/messagegui/screenshot.png
similarity index 100%
rename from apps/messages/screenshot.png
rename to apps/messagegui/screenshot.png
diff --git a/apps/messageicons/ChangeLog b/apps/messageicons/ChangeLog
new file mode 100644
index 000000000..c923b169f
--- /dev/null
+++ b/apps/messageicons/ChangeLog
@@ -0,0 +1,5 @@
+0.01: Moved message icons from messages into standalone library
+0.02: Added several new icons and colors
+0.03: Fix icons broken in 0v02 (#2386)
+ Store all icons in a separate binary file (much faster lookup)
+
diff --git a/apps/messageicons/app.png b/apps/messageicons/app.png
new file mode 100644
index 000000000..1e47a39c6
Binary files /dev/null and b/apps/messageicons/app.png differ
diff --git a/apps/messageicons/icons.img b/apps/messageicons/icons.img
new file mode 100644
index 000000000..104168357
Binary files /dev/null and b/apps/messageicons/icons.img differ
diff --git a/apps/messageicons/icons/1password.png b/apps/messageicons/icons/1password.png
new file mode 100644
index 000000000..7e28c0c93
Binary files /dev/null and b/apps/messageicons/icons/1password.png differ
diff --git a/apps/messageicons/icons/airbnb.png b/apps/messageicons/icons/airbnb.png
new file mode 100644
index 000000000..f691469bc
Binary files /dev/null and b/apps/messageicons/icons/airbnb.png differ
diff --git a/apps/messageicons/icons/alarm.png b/apps/messageicons/icons/alarm.png
new file mode 100644
index 000000000..22a5b6cc4
Binary files /dev/null and b/apps/messageicons/icons/alarm.png differ
diff --git a/apps/messageicons/icons/amazon.png b/apps/messageicons/icons/amazon.png
new file mode 100644
index 000000000..9d446cb6a
Binary files /dev/null and b/apps/messageicons/icons/amazon.png differ
diff --git a/apps/messageicons/icons/bag.png b/apps/messageicons/icons/bag.png
new file mode 100644
index 000000000..70dab4221
Binary files /dev/null and b/apps/messageicons/icons/bag.png differ
diff --git a/apps/messageicons/icons/bank.png b/apps/messageicons/icons/bank.png
new file mode 100644
index 000000000..fa1500a41
Binary files /dev/null and b/apps/messageicons/icons/bank.png differ
diff --git a/apps/messageicons/icons/beeper.png b/apps/messageicons/icons/beeper.png
new file mode 100644
index 000000000..bea9138ec
Binary files /dev/null and b/apps/messageicons/icons/beeper.png differ
diff --git a/apps/messageicons/icons/bibel.png b/apps/messageicons/icons/bibel.png
new file mode 100644
index 000000000..053fcf178
Binary files /dev/null and b/apps/messageicons/icons/bibel.png differ
diff --git a/apps/messageicons/icons/bitcoin.png b/apps/messageicons/icons/bitcoin.png
new file mode 100644
index 000000000..85deecc36
Binary files /dev/null and b/apps/messageicons/icons/bitcoin.png differ
diff --git a/apps/messageicons/icons/bolt.png b/apps/messageicons/icons/bolt.png
new file mode 100644
index 000000000..215b9d052
Binary files /dev/null and b/apps/messageicons/icons/bolt.png differ
diff --git a/apps/messageicons/icons/bring.png b/apps/messageicons/icons/bring.png
new file mode 100644
index 000000000..673d1b7be
Binary files /dev/null and b/apps/messageicons/icons/bring.png differ
diff --git a/apps/messageicons/icons/cafe.png b/apps/messageicons/icons/cafe.png
new file mode 100644
index 000000000..26a3bb114
Binary files /dev/null and b/apps/messageicons/icons/cafe.png differ
diff --git a/apps/messageicons/icons/calendar.png b/apps/messageicons/icons/calendar.png
new file mode 100644
index 000000000..286952af5
Binary files /dev/null and b/apps/messageicons/icons/calendar.png differ
diff --git a/apps/messageicons/icons/cart.png b/apps/messageicons/icons/cart.png
new file mode 100644
index 000000000..dec53ef00
Binary files /dev/null and b/apps/messageicons/icons/cart.png differ
diff --git a/apps/messageicons/icons/cashapp.png b/apps/messageicons/icons/cashapp.png
new file mode 100644
index 000000000..23e897c82
Binary files /dev/null and b/apps/messageicons/icons/cashapp.png differ
diff --git a/apps/messageicons/icons/cbc.png b/apps/messageicons/icons/cbc.png
new file mode 100644
index 000000000..96e3ddd1b
Binary files /dev/null and b/apps/messageicons/icons/cbc.png differ
diff --git a/apps/messageicons/icons/chrome.png b/apps/messageicons/icons/chrome.png
new file mode 100644
index 000000000..b477c57ff
Binary files /dev/null and b/apps/messageicons/icons/chrome.png differ
diff --git a/apps/messageicons/icons/coronavirus.png b/apps/messageicons/icons/coronavirus.png
new file mode 100644
index 000000000..98b967954
Binary files /dev/null and b/apps/messageicons/icons/coronavirus.png differ
diff --git a/apps/messageicons/icons/crave.png b/apps/messageicons/icons/crave.png
new file mode 100644
index 000000000..ee6f0778a
Binary files /dev/null and b/apps/messageicons/icons/crave.png differ
diff --git a/apps/messageicons/icons/default.png b/apps/messageicons/icons/default.png
new file mode 100644
index 000000000..1f85079df
Binary files /dev/null and b/apps/messageicons/icons/default.png differ
diff --git a/apps/messageicons/icons/delivery.png b/apps/messageicons/icons/delivery.png
new file mode 100644
index 000000000..78ca0e190
Binary files /dev/null and b/apps/messageicons/icons/delivery.png differ
diff --git a/apps/messageicons/icons/desjardins.png b/apps/messageicons/icons/desjardins.png
new file mode 100644
index 000000000..c54899aab
Binary files /dev/null and b/apps/messageicons/icons/desjardins.png differ
diff --git a/apps/messageicons/icons/discord.png b/apps/messageicons/icons/discord.png
new file mode 100644
index 000000000..a8c4e2d39
Binary files /dev/null and b/apps/messageicons/icons/discord.png differ
diff --git a/apps/messageicons/icons/dollars.png b/apps/messageicons/icons/dollars.png
new file mode 100644
index 000000000..e5c1d2e68
Binary files /dev/null and b/apps/messageicons/icons/dollars.png differ
diff --git a/apps/messageicons/icons/dropbox.png b/apps/messageicons/icons/dropbox.png
new file mode 100644
index 000000000..ad6dd84a8
Binary files /dev/null and b/apps/messageicons/icons/dropbox.png differ
diff --git a/apps/messageicons/icons/etar.png b/apps/messageicons/icons/etar.png
new file mode 100644
index 000000000..24f0cc587
Binary files /dev/null and b/apps/messageicons/icons/etar.png differ
diff --git a/apps/messageicons/icons/facebook messenger.png b/apps/messageicons/icons/facebook messenger.png
new file mode 100644
index 000000000..286b5bc29
Binary files /dev/null and b/apps/messageicons/icons/facebook messenger.png differ
diff --git a/apps/messageicons/icons/facebook.png b/apps/messageicons/icons/facebook.png
new file mode 100644
index 000000000..5ba18eca3
Binary files /dev/null and b/apps/messageicons/icons/facebook.png differ
diff --git a/apps/messageicons/icons/fdroid.png b/apps/messageicons/icons/fdroid.png
new file mode 100644
index 000000000..4b5c6761e
Binary files /dev/null and b/apps/messageicons/icons/fdroid.png differ
diff --git a/apps/messageicons/icons/firefox.png b/apps/messageicons/icons/firefox.png
new file mode 100644
index 000000000..2dcae9270
Binary files /dev/null and b/apps/messageicons/icons/firefox.png differ
diff --git a/apps/messageicons/icons/generate.js b/apps/messageicons/icons/generate.js
new file mode 100755
index 000000000..e857032af
--- /dev/null
+++ b/apps/messageicons/icons/generate.js
@@ -0,0 +1,143 @@
+#!/usr/bin/node
+
+// Creates lib.js from icons
+// npm install png-js
+
+// default icon must come first in icon_names
+
+var imageconverter = require("../../../webtools/imageconverter.js");
+var icons = JSON.parse(require("fs").readFileSync(__dirname+"/icon_names.json"));
+const imgOptions = {
+ mode : "1bit",
+ inverted : true,
+ transparent : true,
+ output: "raw"
+};
+var PNG = require('png-js');
+var IMAGE_BYTES = 76;
+
+var iconTests = [];
+var iconImages = []; // array of converted icons
+var iconIndices = {}; // maps filename -> index in iconImages
+
+var promises = [];
+
+icons.forEach(icon => {
+ var index = iconIndices[icon.icon];
+ if (index===undefined) { // need a new icon
+ index = iconImages.length;
+ iconIndices[icon.icon] = index;
+ iconImages.push(""); // placeholder
+ // create image
+ console.log("Loading "+icon.icon);
+ var png = new PNG(require("fs").readFileSync(__dirname+"/"+icon.icon));
+ if (png.width!=24 || png.height!=24) {
+ console.warn(icon.icon+" should be 24x24px");
+ }
+
+ promises.push(new Promise(r => {
+ png.decode(function (pixels) {
+ var rgba = new Uint8Array(pixels);
+ var isTransparent = false;
+ for (var i=0;i {
+ // Yay, more JS. Why is it so hard to get the bytes???
+ iconData.set(Array.prototype.slice.call(Buffer.from(img,"binary")), idx*IMAGE_BYTES)
+ });
+
+ console.log("Saving images");
+ require("fs").writeFileSync(__dirname+"/../icons.img", Buffer.from(iconData,"binary"));
+
+ console.log("Saving library");
+ require("fs").writeFileSync(__dirname+"/../lib.js", `exports.getImage = function(msg) {
+ if (msg.img) return atob(msg.img);
+ let s = (("string"=== typeof msg) ? msg : (msg.src || "")).toLowerCase();
+ if (msg.id=="music") s="music";
+ let match = ${JSON.stringify(","+icons.map(icon=>icon.app+"|"+icon.index).join(",")+",")}.match(new RegExp(\`,\${s}\\\\|(\\\\d+)\`))
+ return require("Storage").read("messageicons.img", (match===null)?0:match[1]*${IMAGE_BYTES}, ${IMAGE_BYTES});
+};
+
+exports.getColor = function(msg,options) {
+ options = options||{};
+ var st = options.settings || require('Storage').readJSON("messages.settings.json", 1) || {};
+ if (options.default===undefined) options.default=g.theme.fg;
+ if (st.iconColorMode == 'mono') return options.default;
+ const s = (("string"=== typeof msg) ? msg : (msg.src || "")).toLowerCase();
+ return {
+ // generic colors, using B2-safe colors
+ // DO NOT USE BLACK OR WHITE HERE, just leave the declaration out and then the theme's fg color will be used
+ "airbnb": "#ff385c", // https://news.airbnb.com/media-assets/category/brand/
+ "mail": "#ff0",
+ "music": "#f0f",
+ "phone": "#0f0",
+ "sms message": "#0ff",
+ // brands, according to https://www.schemecolor.com/?s (picking one for multicolored logos)
+ // all dithered on B2, but we only use the color for the icons. (Could maybe pick the closest 3-bit color for B2?)
+ "bibel": "#54342c",
+ "bring": "#455a64",
+ "discord": "#5865f2", // https://discord.com/branding
+ "etar": "#36a18b",
+ "facebook": "#1877f2", // https://www.facebook.com/brand/resources/facebookapp/logo
+ "gmail": "#ea4335",
+ "gmx": "#1c449b",
+ "google": "#4285F4",
+ "google home": "#fbbc05",
+// "home assistant": "#41bdf5", // ha-blue is #41bdf5, but that's the background
+ "instagram": "#ff0069", // https://about.instagram.com/brand/gradient
+ "lieferando": "#ff8000",
+ "linkedin": "#0a66c2", // https://brand.linkedin.com/
+ "messenger": "#0078ff",
+ "mastodon": "#563acc", // https://www.joinmastodon.org/branding
+ "mattermost": "#00f",
+ "n26": "#36a18b",
+ "nextbike": "#00f",
+ "newpipe": "#f00",
+ "nina": "#e57004",
+ "opentasks": "#409f8f",
+ "outlook mail": "#0078d4", // https://developer.microsoft.com/en-us/fluentui#/styles/web/colors/products
+ "paypal": "#003087",
+ "pocket": "#ef4154f", // https://blog.getpocket.com/press/
+ "post & dhl": "#f2c101",
+ "reddit": "#ff4500", // https://www.redditinc.com/brand
+ "signal": "#3a76f0", // https://github.com/signalapp/Signal-Desktop/blob/main/images/signal-logo.svg
+ "skype": "#0078d4", // https://developer.microsoft.com/en-us/fluentui#/styles/web/colors/products
+ "slack": "#e51670",
+ "snapchat": "#ff0",
+ "steam": "#171a21",
+ "teams": "#6264a7", // https://developer.microsoft.com/en-us/fluentui#/styles/web/colors/products
+ "telegram": "#0088cc",
+ "telegram foss": "#0088cc",
+ "to do": "#3999e5",
+ "twitch": "#9146ff", // https://brand.twitch.tv/
+ "twitter": "#1d9bf0", // https://about.twitter.com/en/who-we-are/brand-toolkit
+ "vlc": "#ff8800",
+ "whatsapp": "#4fce5d",
+ "wordfeud": "#e7d3c7",
+ "youtube": "#f00", // https://www.youtube.com/howyoutubeworks/resources/brand-resources/#logos-icons-and-colors
+ }[s]||options.default;
+};
+ `);
+});
diff --git a/apps/messageicons/icons/github.png b/apps/messageicons/icons/github.png
new file mode 100644
index 000000000..813cbb2c9
Binary files /dev/null and b/apps/messageicons/icons/github.png differ
diff --git a/apps/messageicons/icons/gitlab.png b/apps/messageicons/icons/gitlab.png
new file mode 100644
index 000000000..3e7280f59
Binary files /dev/null and b/apps/messageicons/icons/gitlab.png differ
diff --git a/apps/messageicons/icons/gmx.png b/apps/messageicons/icons/gmx.png
new file mode 100644
index 000000000..185c90aa3
Binary files /dev/null and b/apps/messageicons/icons/gmx.png differ
diff --git a/apps/messageicons/icons/google chat.png b/apps/messageicons/icons/google chat.png
new file mode 100644
index 000000000..6d8eb7741
Binary files /dev/null and b/apps/messageicons/icons/google chat.png differ
diff --git a/apps/messageicons/icons/google drive.png b/apps/messageicons/icons/google drive.png
new file mode 100644
index 000000000..97da419a8
Binary files /dev/null and b/apps/messageicons/icons/google drive.png differ
diff --git a/apps/messageicons/icons/google home.png b/apps/messageicons/icons/google home.png
new file mode 100644
index 000000000..f6ebaa77f
Binary files /dev/null and b/apps/messageicons/icons/google home.png differ
diff --git a/apps/messageicons/icons/google keep.png b/apps/messageicons/icons/google keep.png
new file mode 100644
index 000000000..f7d1f97c6
Binary files /dev/null and b/apps/messageicons/icons/google keep.png differ
diff --git a/apps/messageicons/icons/google opinion rewards.png b/apps/messageicons/icons/google opinion rewards.png
new file mode 100644
index 000000000..479ec0c5f
Binary files /dev/null and b/apps/messageicons/icons/google opinion rewards.png differ
diff --git a/apps/messageicons/icons/google photos.png b/apps/messageicons/icons/google photos.png
new file mode 100644
index 000000000..aecf00dbe
Binary files /dev/null and b/apps/messageicons/icons/google photos.png differ
diff --git a/apps/messageicons/icons/google play store.png b/apps/messageicons/icons/google play store.png
new file mode 100644
index 000000000..166094907
Binary files /dev/null and b/apps/messageicons/icons/google play store.png differ
diff --git a/apps/messageicons/icons/google.png b/apps/messageicons/icons/google.png
new file mode 100644
index 000000000..62797fefb
Binary files /dev/null and b/apps/messageicons/icons/google.png differ
diff --git a/apps/messageicons/icons/home assistant.png b/apps/messageicons/icons/home assistant.png
new file mode 100644
index 000000000..d08932ae8
Binary files /dev/null and b/apps/messageicons/icons/home assistant.png differ
diff --git a/apps/messageicons/icons/icon_names.json b/apps/messageicons/icons/icon_names.json
new file mode 100644
index 000000000..0085731cc
--- /dev/null
+++ b/apps/messageicons/icons/icon_names.json
@@ -0,0 +1,111 @@
+[
+ { "app":"default", "icon":"default.png" },
+ { "app":"airbnb", "icon":"airbnb.png" },
+ { "app":"alarm", "icon":"alarm.png" },
+ { "app":"alarmclockreceiver", "icon":"alarm.png" },
+ { "app":"amazon shopping", "icon":"amazon.png" },
+ { "app":"bibel", "icon":"bibel.png" },
+ { "app":"bitwarden", "icon":"security.png" },
+ { "app":"1password", "icon":"security.png" },
+ { "app":"lastpass", "icon":"security.png" },
+ { "app":"dashlane", "icon":"security.png" },
+ { "app":"bring", "icon":"bring.png" },
+ { "app":"calendar", "icon":"etar.png" },
+ { "app":"etar", "icon":"etar.png" },
+ { "app":"chat", "icon":"google chat.png" },
+ { "app":"chrome", "icon":"chrome.png" },
+ { "app":"corona-warn", "icon":"coronavirus.png" },
+ { "app":"bmo", "icon":"bank.png" },
+ { "app":"desjardins", "icon":"bank.png" },
+ { "app":"rbc mobile", "icon":"bank.png" },
+ { "app":"nbc", "icon":"bank.png" },
+ { "app":"rabobank", "icon":"bank.png" },
+ { "app":"scotiabank", "icon":"bank.png" },
+ { "app":"td (canada)", "icon":"bank.png" },
+ { "app":"discord", "icon":"discord.png" },
+ { "app":"drive", "icon":"google drive.png" },
+ { "app":"element", "icon":"matrix element.png" },
+ { "app":"facebook", "icon":"facebook.png" },
+ { "app":"messenger", "icon":"facebook messenger.png" },
+ { "app":"firefox", "icon":"firefox.png" },
+ { "app":"firefox beta", "icon":"firefox.png" },
+ { "app":"firefox nightly", "icon":"firefox.png" },
+ { "app":"f-droid", "icon":"security.png" },
+ { "app":"neo store", "icon":"security.png" },
+ { "app":"aurora droid", "icon":"security.png" },
+ { "app":"github", "icon":"github.png" },
+ { "app":"gitlab", "icon":"gitlab.png" },
+ { "app":"gmx", "icon":"gmx.png" },
+ { "app":"google", "icon":"google.png" },
+ { "app":"google home", "icon":"google home.png" },
+ { "app":"google play store", "icon":"google play store.png" },
+ { "app":"home assistant", "icon":"home assistant.png" },
+ { "app":"instagram", "icon":"instagram.png" },
+ { "app":"kalender", "icon":"kalender.png" },
+ { "app":"keep notes", "icon":"google keep.png" },
+ { "app":"lieferando", "icon":"lieferando.png" },
+ { "app":"linkedin", "icon":"linkedin.png" },
+ { "app":"maps", "icon":"map.png" },
+ { "app":"organic maps", "icon":"map.png" },
+ { "app":"osmand", "icon":"map.png" },
+ { "app":"mastodon", "icon":"mastodon.png" },
+ { "app":"fedilab", "icon":"mastodon.png" },
+ { "app":"tooot", "icon":"mastodon.png" },
+ { "app":"tusky", "icon":"mastodon.png" },
+ { "app":"mattermost", "icon":"mattermost.png" },
+ { "app":"n26", "icon":"n26.png" },
+ { "app":"netflix", "icon":"netflix.png" },
+ { "app":"news", "icon":"news.png" },
+ { "app":"cbc news", "icon":"news.png" },
+ { "app":"rc info", "icon":"news.png" },
+ { "app":"reuters", "icon":"news.png" },
+ { "app":"ap news", "icon":"news.png" },
+ { "app":"la presse", "icon":"news.png" },
+ { "app":"nbc news", "icon":"news.png" },
+ { "app":"nextbike", "icon":"nextbike.png" },
+ { "app":"nina", "icon":"nina.png" },
+ { "app":"outlook mail", "icon":"outlook.png" },
+ { "app":"paypal", "icon":"paypal.png" },
+ { "app":"phone", "icon":"phone.png" },
+ { "app":"plex", "icon":"plex.png" },
+ { "app":"pocket", "icon":"pocket.png" },
+ { "app":"post & dhl", "icon":"delivery.png" },
+ { "app":"proton mail", "icon":"protonmail.png" },
+ { "app":"reddit", "icon":"reddit.png" },
+ { "app":"sync pro", "icon":"reddit.png" },
+ { "app":"sync dev", "icon":"reddit.png" },
+ { "app":"boost", "icon":"reddit.png" },
+ { "app":"infinity", "icon":"reddit.png" },
+ { "app":"slide", "icon":"reddit.png" },
+ { "app":"signal", "icon":"signal.png" },
+ { "app":"skype", "icon":"skype.png" },
+ { "app":"slack", "icon":"slack.png" },
+ { "app":"snapchat", "icon":"snapchat.png" },
+ { "app":"starbucks", "icon":"cafe.png" },
+ { "app":"steam", "icon":"steam.png" },
+ { "app":"teams", "icon":"teams.png" },
+ { "app":"telegram", "icon":"telegram.png" },
+ { "app":"telegram foss", "icon":"telegram.png" },
+ { "app":"threema", "icon":"threema.png" },
+ { "app":"tiktok", "icon":"tiktok.png" },
+ { "app":"to do", "icon":"task.png" },
+ { "app":"opentasks", "icon":"task.png" },
+ { "app":"tasks", "icon":"task.png" },
+ { "app":"transit", "icon":"transit.png" },
+ { "app":"twitch", "icon":"twitch.png" },
+ { "app":"twitter", "icon":"twitter.png" },
+ { "app":"uber", "icon":"taxi.png" },
+ { "app":"lyft", "icon":"taxi.png" },
+ { "app":"vlc", "icon":"vlc.png" },
+ { "app":"warnapp", "icon":"warnapp.png" },
+ { "app":"whatsapp", "icon":"whatsapp.png" },
+ { "app":"wordfeud", "icon":"wordfeud.png" },
+ { "app":"youtube", "icon":"youtube.png" },
+ { "app":"newpipe", "icon":"youtube.png" },
+ { "app":"zoom", "icon":"videoconf.png" },
+ { "app":"meet", "icon":"videoconf.png" },
+ { "app":"music", "icon":"music.png" },
+ { "app":"sms message", "icon":"default.png" },
+ { "app":"mail", "icon":"default.png" },
+ { "app":"gmail", "icon":"default.png" }
+]
diff --git a/apps/messageicons/icons/instagram.png b/apps/messageicons/icons/instagram.png
new file mode 100644
index 000000000..9bccd20af
Binary files /dev/null and b/apps/messageicons/icons/instagram.png differ
diff --git a/apps/messageicons/icons/kalender.png b/apps/messageicons/icons/kalender.png
new file mode 100644
index 000000000..dd807dd9e
Binary files /dev/null and b/apps/messageicons/icons/kalender.png differ
diff --git a/apps/messageicons/icons/kde connect.png b/apps/messageicons/icons/kde connect.png
new file mode 100644
index 000000000..f13298b1d
Binary files /dev/null and b/apps/messageicons/icons/kde connect.png differ
diff --git a/apps/messageicons/icons/lieferando.png b/apps/messageicons/icons/lieferando.png
new file mode 100644
index 000000000..7a31bc9e1
Binary files /dev/null and b/apps/messageicons/icons/lieferando.png differ
diff --git a/apps/messageicons/icons/linkedin.png b/apps/messageicons/icons/linkedin.png
new file mode 100644
index 000000000..016e29ca8
Binary files /dev/null and b/apps/messageicons/icons/linkedin.png differ
diff --git a/apps/messageicons/icons/mail.png b/apps/messageicons/icons/mail.png
new file mode 100644
index 000000000..8c29a4895
Binary files /dev/null and b/apps/messageicons/icons/mail.png differ
diff --git a/apps/messageicons/icons/map.png b/apps/messageicons/icons/map.png
new file mode 100644
index 000000000..215f3f971
Binary files /dev/null and b/apps/messageicons/icons/map.png differ
diff --git a/apps/messageicons/icons/mastodon.png b/apps/messageicons/icons/mastodon.png
new file mode 100644
index 000000000..82fe737a3
Binary files /dev/null and b/apps/messageicons/icons/mastodon.png differ
diff --git a/apps/messageicons/icons/matrix element.png b/apps/messageicons/icons/matrix element.png
new file mode 100644
index 000000000..9ae89dc37
Binary files /dev/null and b/apps/messageicons/icons/matrix element.png differ
diff --git a/apps/messageicons/icons/mattermost.png b/apps/messageicons/icons/mattermost.png
new file mode 100644
index 000000000..2d5f168ca
Binary files /dev/null and b/apps/messageicons/icons/mattermost.png differ
diff --git a/apps/messageicons/icons/mcdonalds.png b/apps/messageicons/icons/mcdonalds.png
new file mode 100644
index 000000000..efe4088a4
Binary files /dev/null and b/apps/messageicons/icons/mcdonalds.png differ
diff --git a/apps/messageicons/icons/message.png b/apps/messageicons/icons/message.png
new file mode 100644
index 000000000..a93cb3f4c
Binary files /dev/null and b/apps/messageicons/icons/message.png differ
diff --git a/apps/messageicons/icons/music.png b/apps/messageicons/icons/music.png
new file mode 100644
index 000000000..62f7acfee
Binary files /dev/null and b/apps/messageicons/icons/music.png differ
diff --git a/apps/messageicons/icons/n26.png b/apps/messageicons/icons/n26.png
new file mode 100644
index 000000000..aa441ab8b
Binary files /dev/null and b/apps/messageicons/icons/n26.png differ
diff --git a/apps/messageicons/icons/netflix.png b/apps/messageicons/icons/netflix.png
new file mode 100644
index 000000000..d956a103b
Binary files /dev/null and b/apps/messageicons/icons/netflix.png differ
diff --git a/apps/messageicons/icons/news.png b/apps/messageicons/icons/news.png
new file mode 100644
index 000000000..8e75513e9
Binary files /dev/null and b/apps/messageicons/icons/news.png differ
diff --git a/apps/messageicons/icons/nextbike.png b/apps/messageicons/icons/nextbike.png
new file mode 100644
index 000000000..467bed8ac
Binary files /dev/null and b/apps/messageicons/icons/nextbike.png differ
diff --git a/apps/messageicons/icons/nina.png b/apps/messageicons/icons/nina.png
new file mode 100644
index 000000000..2669b6401
Binary files /dev/null and b/apps/messageicons/icons/nina.png differ
diff --git a/apps/messageicons/icons/notification.png b/apps/messageicons/icons/notification.png
new file mode 100644
index 000000000..c29a6025c
Binary files /dev/null and b/apps/messageicons/icons/notification.png differ
diff --git a/apps/messageicons/icons/onedrive.png b/apps/messageicons/icons/onedrive.png
new file mode 100644
index 000000000..ff2b08304
Binary files /dev/null and b/apps/messageicons/icons/onedrive.png differ
diff --git a/apps/messageicons/icons/outlook.png b/apps/messageicons/icons/outlook.png
new file mode 100644
index 000000000..5519ccd4c
Binary files /dev/null and b/apps/messageicons/icons/outlook.png differ
diff --git a/apps/messageicons/icons/paypal.png b/apps/messageicons/icons/paypal.png
new file mode 100644
index 000000000..cd76aae90
Binary files /dev/null and b/apps/messageicons/icons/paypal.png differ
diff --git a/apps/messageicons/icons/phone.png b/apps/messageicons/icons/phone.png
new file mode 100644
index 000000000..376170f7c
Binary files /dev/null and b/apps/messageicons/icons/phone.png differ
diff --git a/apps/messageicons/icons/playstation.png b/apps/messageicons/icons/playstation.png
new file mode 100644
index 000000000..a97a38964
Binary files /dev/null and b/apps/messageicons/icons/playstation.png differ
diff --git a/apps/messageicons/icons/plex.png b/apps/messageicons/icons/plex.png
new file mode 100644
index 000000000..a0840b751
Binary files /dev/null and b/apps/messageicons/icons/plex.png differ
diff --git a/apps/messageicons/icons/pocket.png b/apps/messageicons/icons/pocket.png
new file mode 100644
index 000000000..d34f2e399
Binary files /dev/null and b/apps/messageicons/icons/pocket.png differ
diff --git a/apps/messageicons/icons/podcast.png b/apps/messageicons/icons/podcast.png
new file mode 100644
index 000000000..1be0f22b6
Binary files /dev/null and b/apps/messageicons/icons/podcast.png differ
diff --git a/apps/messageicons/icons/pokeball.png b/apps/messageicons/icons/pokeball.png
new file mode 100644
index 000000000..d023761eb
Binary files /dev/null and b/apps/messageicons/icons/pokeball.png differ
diff --git a/apps/messageicons/icons/protonmail.png b/apps/messageicons/icons/protonmail.png
new file mode 100644
index 000000000..065607c47
Binary files /dev/null and b/apps/messageicons/icons/protonmail.png differ
diff --git a/apps/messageicons/icons/protonvpn.png b/apps/messageicons/icons/protonvpn.png
new file mode 100644
index 000000000..6d837a3e2
Binary files /dev/null and b/apps/messageicons/icons/protonvpn.png differ
diff --git a/apps/messageicons/icons/radio.png b/apps/messageicons/icons/radio.png
new file mode 100644
index 000000000..ea1cbffe1
Binary files /dev/null and b/apps/messageicons/icons/radio.png differ
diff --git a/apps/messageicons/icons/reddit.png b/apps/messageicons/icons/reddit.png
new file mode 100644
index 000000000..96fcce901
Binary files /dev/null and b/apps/messageicons/icons/reddit.png differ
diff --git a/apps/messageicons/icons/restaurant.png b/apps/messageicons/icons/restaurant.png
new file mode 100644
index 000000000..9bf1fcea9
Binary files /dev/null and b/apps/messageicons/icons/restaurant.png differ
diff --git a/apps/messageicons/icons/router.png b/apps/messageicons/icons/router.png
new file mode 100644
index 000000000..4446342f6
Binary files /dev/null and b/apps/messageicons/icons/router.png differ
diff --git a/apps/messageicons/icons/rss.png b/apps/messageicons/icons/rss.png
new file mode 100644
index 000000000..b248b70e9
Binary files /dev/null and b/apps/messageicons/icons/rss.png differ
diff --git a/apps/messageicons/icons/rust.png b/apps/messageicons/icons/rust.png
new file mode 100644
index 000000000..b74eb6ec4
Binary files /dev/null and b/apps/messageicons/icons/rust.png differ
diff --git a/apps/messageicons/icons/security.png b/apps/messageicons/icons/security.png
new file mode 100644
index 000000000..b8cc5c77e
Binary files /dev/null and b/apps/messageicons/icons/security.png differ
diff --git a/apps/messageicons/icons/shopping.png b/apps/messageicons/icons/shopping.png
new file mode 100644
index 000000000..f966188b8
Binary files /dev/null and b/apps/messageicons/icons/shopping.png differ
diff --git a/apps/messageicons/icons/signal.png b/apps/messageicons/icons/signal.png
new file mode 100644
index 000000000..e8508706f
Binary files /dev/null and b/apps/messageicons/icons/signal.png differ
diff --git a/apps/messageicons/icons/skype.png b/apps/messageicons/icons/skype.png
new file mode 100644
index 000000000..867a8feb6
Binary files /dev/null and b/apps/messageicons/icons/skype.png differ
diff --git a/apps/messageicons/icons/slack.png b/apps/messageicons/icons/slack.png
new file mode 100644
index 000000000..7a5a5a71c
Binary files /dev/null and b/apps/messageicons/icons/slack.png differ
diff --git a/apps/messageicons/icons/snapchat.png b/apps/messageicons/icons/snapchat.png
new file mode 100644
index 000000000..42daddbaf
Binary files /dev/null and b/apps/messageicons/icons/snapchat.png differ
diff --git a/apps/messageicons/icons/steam.png b/apps/messageicons/icons/steam.png
new file mode 100644
index 000000000..f6212cdfb
Binary files /dev/null and b/apps/messageicons/icons/steam.png differ
diff --git a/apps/messageicons/icons/syncthing.png b/apps/messageicons/icons/syncthing.png
new file mode 100644
index 000000000..174384aba
Binary files /dev/null and b/apps/messageicons/icons/syncthing.png differ
diff --git a/apps/messageicons/icons/task.png b/apps/messageicons/icons/task.png
new file mode 100644
index 000000000..c43d355c4
Binary files /dev/null and b/apps/messageicons/icons/task.png differ
diff --git a/apps/messageicons/icons/taxi.png b/apps/messageicons/icons/taxi.png
new file mode 100644
index 000000000..b577eef0e
Binary files /dev/null and b/apps/messageicons/icons/taxi.png differ
diff --git a/apps/messageicons/icons/teams.png b/apps/messageicons/icons/teams.png
new file mode 100644
index 000000000..5160b007e
Binary files /dev/null and b/apps/messageicons/icons/teams.png differ
diff --git a/apps/messageicons/icons/telegram.png b/apps/messageicons/icons/telegram.png
new file mode 100644
index 000000000..fe8051006
Binary files /dev/null and b/apps/messageicons/icons/telegram.png differ
diff --git a/apps/messageicons/icons/terminal.png b/apps/messageicons/icons/terminal.png
new file mode 100644
index 000000000..10a35e71f
Binary files /dev/null and b/apps/messageicons/icons/terminal.png differ
diff --git a/apps/messageicons/icons/thermostat.png b/apps/messageicons/icons/thermostat.png
new file mode 100644
index 000000000..8e96f6241
Binary files /dev/null and b/apps/messageicons/icons/thermostat.png differ
diff --git a/apps/messageicons/icons/threema.png b/apps/messageicons/icons/threema.png
new file mode 100644
index 000000000..401660b5c
Binary files /dev/null and b/apps/messageicons/icons/threema.png differ
diff --git a/apps/messageicons/icons/tiktok.png b/apps/messageicons/icons/tiktok.png
new file mode 100644
index 000000000..99afd3dd9
Binary files /dev/null and b/apps/messageicons/icons/tiktok.png differ
diff --git a/apps/messageicons/icons/transit.png b/apps/messageicons/icons/transit.png
new file mode 100644
index 000000000..3ec108194
Binary files /dev/null and b/apps/messageicons/icons/transit.png differ
diff --git a/apps/messageicons/icons/twitch.png b/apps/messageicons/icons/twitch.png
new file mode 100644
index 000000000..cd7d479c1
Binary files /dev/null and b/apps/messageicons/icons/twitch.png differ
diff --git a/apps/messageicons/icons/twitter.png b/apps/messageicons/icons/twitter.png
new file mode 100644
index 000000000..88df293f8
Binary files /dev/null and b/apps/messageicons/icons/twitter.png differ
diff --git a/apps/messageicons/icons/videoconf.png b/apps/messageicons/icons/videoconf.png
new file mode 100644
index 000000000..9b420341a
Binary files /dev/null and b/apps/messageicons/icons/videoconf.png differ
diff --git a/apps/messageicons/icons/vlc.png b/apps/messageicons/icons/vlc.png
new file mode 100644
index 000000000..74949aded
Binary files /dev/null and b/apps/messageicons/icons/vlc.png differ
diff --git a/apps/messageicons/icons/voicemail.png b/apps/messageicons/icons/voicemail.png
new file mode 100644
index 000000000..2c1972a56
Binary files /dev/null and b/apps/messageicons/icons/voicemail.png differ
diff --git a/apps/messageicons/icons/wallet.png b/apps/messageicons/icons/wallet.png
new file mode 100644
index 000000000..536cae2ba
Binary files /dev/null and b/apps/messageicons/icons/wallet.png differ
diff --git a/apps/messageicons/icons/warnapp.png b/apps/messageicons/icons/warnapp.png
new file mode 100644
index 000000000..988485053
Binary files /dev/null and b/apps/messageicons/icons/warnapp.png differ
diff --git a/apps/messageicons/icons/warning.png b/apps/messageicons/icons/warning.png
new file mode 100644
index 000000000..59080713f
Binary files /dev/null and b/apps/messageicons/icons/warning.png differ
diff --git a/apps/messageicons/icons/webhook.png b/apps/messageicons/icons/webhook.png
new file mode 100644
index 000000000..7562fd759
Binary files /dev/null and b/apps/messageicons/icons/webhook.png differ
diff --git a/apps/messageicons/icons/wechat.png b/apps/messageicons/icons/wechat.png
new file mode 100644
index 000000000..55f4bd6a9
Binary files /dev/null and b/apps/messageicons/icons/wechat.png differ
diff --git a/apps/messageicons/icons/whatsapp.png b/apps/messageicons/icons/whatsapp.png
new file mode 100644
index 000000000..d6d89bc0c
Binary files /dev/null and b/apps/messageicons/icons/whatsapp.png differ
diff --git a/apps/messageicons/icons/wordfeud.png b/apps/messageicons/icons/wordfeud.png
new file mode 100644
index 000000000..83963d4d4
Binary files /dev/null and b/apps/messageicons/icons/wordfeud.png differ
diff --git a/apps/messageicons/icons/xbox.png b/apps/messageicons/icons/xbox.png
new file mode 100644
index 000000000..dce76128d
Binary files /dev/null and b/apps/messageicons/icons/xbox.png differ
diff --git a/apps/messageicons/icons/youtube.png b/apps/messageicons/icons/youtube.png
new file mode 100644
index 000000000..93e50ccad
Binary files /dev/null and b/apps/messageicons/icons/youtube.png differ
diff --git a/apps/messageicons/lib.js b/apps/messageicons/lib.js
new file mode 100644
index 000000000..c5be21bb0
--- /dev/null
+++ b/apps/messageicons/lib.js
@@ -0,0 +1,68 @@
+exports.getImage = function(msg) {
+ if (msg.img) return atob(msg.img);
+ let s = (("string"=== typeof msg) ? msg : (msg.src || "")).toLowerCase();
+ if (msg.id=="music") s="music";
+ let match = ",default|0,airbnb|1,alarm|2,alarmclockreceiver|2,amazon shopping|3,bibel|4,bitwarden|5,1password|5,lastpass|5,dashlane|5,bring|6,calendar|7,etar|7,chat|8,chrome|9,corona-warn|10,bmo|11,desjardins|11,rbc mobile|11,nbc|11,rabobank|11,scotiabank|11,td (canada)|11,discord|12,drive|13,element|14,facebook|15,messenger|16,firefox|17,firefox beta|17,firefox nightly|17,f-droid|5,neo store|5,aurora droid|5,github|18,gitlab|19,gmx|20,google|21,google home|22,google play store|23,home assistant|24,instagram|25,kalender|26,keep notes|27,lieferando|28,linkedin|29,maps|30,organic maps|30,osmand|30,mastodon|31,fedilab|31,tooot|31,tusky|31,mattermost|32,n26|33,netflix|34,news|35,cbc news|35,rc info|35,reuters|35,ap news|35,la presse|35,nbc news|35,nextbike|36,nina|37,outlook mail|38,paypal|39,phone|40,plex|41,pocket|42,post & dhl|43,proton mail|44,reddit|45,sync pro|45,sync dev|45,boost|45,infinity|45,slide|45,signal|46,skype|47,slack|48,snapchat|49,starbucks|50,steam|51,teams|52,telegram|53,telegram foss|53,threema|54,tiktok|55,to do|56,opentasks|56,tasks|56,transit|57,twitch|58,twitter|59,uber|60,lyft|60,vlc|61,warnapp|62,whatsapp|63,wordfeud|64,youtube|65,newpipe|65,zoom|66,meet|66,music|67,sms message|0,mail|0,gmail|0,".match(new RegExp(`,${s}\\|(\\d+)`))
+ return require("Storage").read("messageicons.img", (match===null)?0:match[1]*76, 76);
+};
+
+exports.getColor = function(msg,options) {
+ options = options||{};
+ var st = options.settings || require('Storage').readJSON("messages.settings.json", 1) || {};
+ if (options.default===undefined) options.default=g.theme.fg;
+ if (st.iconColorMode == 'mono') return options.default;
+ const s = (("string"=== typeof msg) ? msg : (msg.src || "")).toLowerCase();
+ return {
+ // generic colors, using B2-safe colors
+ // DO NOT USE BLACK OR WHITE HERE, just leave the declaration out and then the theme's fg color will be used
+ "airbnb": "#ff385c", // https://news.airbnb.com/media-assets/category/brand/
+ "mail": "#ff0",
+ "music": "#f0f",
+ "phone": "#0f0",
+ "sms message": "#0ff",
+ // brands, according to https://www.schemecolor.com/?s (picking one for multicolored logos)
+ // all dithered on B2, but we only use the color for the icons. (Could maybe pick the closest 3-bit color for B2?)
+ "bibel": "#54342c",
+ "bring": "#455a64",
+ "discord": "#5865f2", // https://discord.com/branding
+ "etar": "#36a18b",
+ "facebook": "#1877f2", // https://www.facebook.com/brand/resources/facebookapp/logo
+ "gmail": "#ea4335",
+ "gmx": "#1c449b",
+ "google": "#4285F4",
+ "google home": "#fbbc05",
+// "home assistant": "#41bdf5", // ha-blue is #41bdf5, but that's the background
+ "instagram": "#ff0069", // https://about.instagram.com/brand/gradient
+ "lieferando": "#ff8000",
+ "linkedin": "#0a66c2", // https://brand.linkedin.com/
+ "messenger": "#0078ff",
+ "mastodon": "#563acc", // https://www.joinmastodon.org/branding
+ "mattermost": "#00f",
+ "n26": "#36a18b",
+ "nextbike": "#00f",
+ "newpipe": "#f00",
+ "nina": "#e57004",
+ "opentasks": "#409f8f",
+ "outlook mail": "#0078d4", // https://developer.microsoft.com/en-us/fluentui#/styles/web/colors/products
+ "paypal": "#003087",
+ "pocket": "#ef4154f", // https://blog.getpocket.com/press/
+ "post & dhl": "#f2c101",
+ "reddit": "#ff4500", // https://www.redditinc.com/brand
+ "signal": "#3a76f0", // https://github.com/signalapp/Signal-Desktop/blob/main/images/signal-logo.svg
+ "skype": "#0078d4", // https://developer.microsoft.com/en-us/fluentui#/styles/web/colors/products
+ "slack": "#e51670",
+ "snapchat": "#ff0",
+ "steam": "#171a21",
+ "teams": "#6264a7", // https://developer.microsoft.com/en-us/fluentui#/styles/web/colors/products
+ "telegram": "#0088cc",
+ "telegram foss": "#0088cc",
+ "to do": "#3999e5",
+ "twitch": "#9146ff", // https://brand.twitch.tv/
+ "twitter": "#1d9bf0", // https://about.twitter.com/en/who-we-are/brand-toolkit
+ "vlc": "#ff8800",
+ "whatsapp": "#4fce5d",
+ "wordfeud": "#e7d3c7",
+ "youtube": "#f00", // https://www.youtube.com/howyoutubeworks/resources/brand-resources/#logos-icons-and-colors
+ }[s]||options.default;
+};
+
\ No newline at end of file
diff --git a/apps/messageicons/metadata.json b/apps/messageicons/metadata.json
new file mode 100644
index 000000000..079835a0b
--- /dev/null
+++ b/apps/messageicons/metadata.json
@@ -0,0 +1,16 @@
+{
+ "id": "messageicons",
+ "name": "Message Icons",
+ "version": "0.03",
+ "description": "Library containing a list of icons and colors for apps",
+ "icon": "app.png",
+ "type": "module",
+ "tags": "tool,system",
+ "supports": ["BANGLEJS","BANGLEJS2"],
+ "provides_modules" : ["messageicons"],
+ "default": true,
+ "storage": [
+ {"name":"messageicons","url":"lib.js"},
+ {"name":"messageicons.img","url":"icons.img"}
+ ]
+}
diff --git a/apps/messagelist/ChangeLog b/apps/messagelist/ChangeLog
new file mode 100644
index 000000000..759f68777
--- /dev/null
+++ b/apps/messagelist/ChangeLog
@@ -0,0 +1 @@
+0.01: New app!
\ No newline at end of file
diff --git a/apps/messagelist/README.md b/apps/messagelist/README.md
new file mode 100644
index 000000000..776d0d0e6
--- /dev/null
+++ b/apps/messagelist/README.md
@@ -0,0 +1,69 @@
+# Message List
+
+Display messages inline as a single list:
+Displays one message at a time, if it doesn't fit on the screen you can scroll
+up/down. When you reach the bottom, you can scroll on to the next message.
+
+## Installation
+**First** uninstall the default [Message UI](/?id=messagegui) app (`messagegui`,
+not the library!).
+Then install this app.
+
+## Screenshots
+
+### Main menu:
+
+
+### Unread message:
+
+The chevrons are hints for swipe actions:
+- Swipe right to go back
+- Swipe left for the message-actions menu
+- Swipe down to show the previous message: We are currently viewing message 2 of 2,
+ so message 1 is "above" this one.
+
+### Long (read) message:
+
+The button is disabled until you scroll all the way to the bottom.
+
+### Music:
+
+Minimal setup: album name and buttons disabled through settings.
+Swipe for next/previous song, tap to pause/resume.
+
+## Settings
+
+### Interface
+* `Font size` - The font size used when displaying messages/music.
+* `On Tap` - If messages are too large to fit on the screen, tapping the screen scrolls down.
+ This is the action to take when tapping a message after reaching the bottom:
+ - `Message menu`: Open menu with message actions
+ - `Dismiss`: Dismiss message right away
+ - `Back`: Go back to clock/main menu
+ - `Nothing`: Do nothing
+* `Dismiss button` - Show inline button to dismiss message right away
+
+### Behaviour
+* `Vibrate` - The pattern of buzzes when a new message is received.
+* `Vibrate for calls` - The pattern of buzzes for incoming calls.
+* `Vibrate for alarms` - The pattern of buzzes for (phone) alarms.
+* `Repeat` - How often buzzes repeat - the default of 4 means the Bangle will buzz every 4 seconds.
+* `Unread timer` - When a new message is received the Messages app is opened.
+ If there is no user input for this amount of time then the app will exit and return to the clock.
+* `Auto-open` - Automatically open app when a new message arrives.
+* `Respect quiet mode` - Prevent auto-opening during quiet mode.
+
+### Music
+* `Auto-open` - Automatically open app when music starts playing.
+* `Always visible` - Show "music" in the main menu even when nothing is playing.
+* `Buttons` - Show `previous`/`play/pause`/`next` buttons on music screen.
+* `Show album` - Display album names?
+
+
+### Util
+* `Delete all` - Erase all messages.
+
+
+## Attributions
+
+Some icons used in this app are from https://icons8.com
diff --git a/apps/messagelist/TODO.txt b/apps/messagelist/TODO.txt
new file mode 100644
index 000000000..3a6d7b664
--- /dev/null
+++ b/apps/messagelist/TODO.txt
@@ -0,0 +1,17 @@
+## Nice to have:
+* Add labels to B1 music HW buttons
+* Add volume buttons to B2 music screen (when controls are enabled)
+* Draw messages ourselves instead of piling hacks on Layout
+* Make sure all icons are 24x24px: icon sizes affect layout
+* Check/optimize layout for B1, other fonts (scrolling for just 5px is a shame)
+
+## Wishlist:
+* Option to swipe-dismiss (instead of action menu)
+* Maybe refactor showGrid() out into a general-use module?
+
+* Message replies (needs `android` support)
+* Customize replies
+* Custom replies (i.e. `textinput`)
+* Hooks to add custom replies/actions,
+ e.g. external code could add "Send intent" option to Home Assistant messages
+ Maybe just use this for all replies, so we don't hardcode anything in "messages"?
diff --git a/apps/messagelist/app-icon.js b/apps/messagelist/app-icon.js
new file mode 100644
index 000000000..6ed3c1141
--- /dev/null
+++ b/apps/messagelist/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEw4UA///rkcAYP9ohL/ABMBqoAEoALDioLFqgLDBQoABERIkEBZcFBY9QBed61QAC1oLF7wLD24LF24LD7wLF1vqBQOrvQLFA4IuC9QLFD4IuC1QLGGAQOBBYwgBEwQLHvQBBEZHVq4jI7wWBHY5TLNZaDLTZazLffMBBY9ABZsABY4KCgEVBQtUBYYkGEQYA/AAwA="))
diff --git a/apps/messagelist/app.js b/apps/messagelist/app.js
new file mode 100644
index 000000000..ebd5d4217
--- /dev/null
+++ b/apps/messagelist/app.js
@@ -0,0 +1,1208 @@
+/* MESSAGES is a list of:
+ {id:int,
+ src,
+ title,
+ subject,
+ body,
+ sender,
+ tel:string,
+ new:true // not read yet
+ }
+*/
+
+/* For example for maps:
+
+// a message
+{"t":"add","id":1575479849,"src":"Hangouts","title":"A Name","body":"message contents"}
+// maps
+{"t":"add","id":1,"src":"Maps","title":"0 yd - High St","body":"Campton - 11:48 ETA","img":"GhqBAAAMAAAHgAAD8AAB/gAA/8AAf/gAP/8AH//gD/98B//Pg/4B8f8Afv+PP//n3/f5//j+f/wfn/4D5/8Aef+AD//AAf/gAD/wAAf4AAD8AAAeAAADAAA="}
+// call
+{"t":"add","id":"call","src":"Phone","name":"Bob","number":"12421312",positive:true,negative:true}
+*/
+{
+ const B2 = process.env.HWVERSION>1, // Bangle.js 2?
+ RIGHT = 1, LEFT = -1, // swipe directions
+ UP = -1, DOWN = 1; // updown directions
+ const Layout = require("Layout");
+
+ const settings = () => require("messagegui").settings();
+ const fontTiny = "6x8"; // fixed size, don't use this for important things
+ let fontNormal;
+ // setFont() is also called after we close the settings screen
+ const setFont = function() {
+ const fontSize = settings().fontSize;
+ if (fontSize===0) // small
+ fontNormal = g.getFonts().includes("6x15") ? "6x15" : "6x8:2";
+ else if (fontSize===2) // large
+ fontNormal = g.getFonts().includes("6x15") ? "6x15:2" : "6x8:4";
+ else // medium
+ fontNormal = g.getFonts().includes("12x20") ? "12x20" : "6x8:3";
+ };
+ setFont();
+
+ let active, back; // active screen, last active screen
+
+ /// List of all our messages
+ let MESSAGES;
+ const saveMessages = function() {
+ const noSave = ["alarm", "call", "music"]; // assume these are outdated once we close the app
+ noSave.forEach(id => remove({id: id}));
+ require("messages").write(MESSAGES
+ .filter(m => m.id && !noSave.includes(m.id))
+ .map(m => {
+ delete m.show;
+ return m;
+ })
+ );
+ };
+ const uiRemove = function() {
+ if (musicTimeout) clearTimeout(musicTimeout);
+ layout = undefined;
+ Bangle.removeListener("message", onMessage);
+ saveMessages();
+ clearUnreadStuff();
+ delete Bangle.appRect;
+ };
+ const quitApp = () => load(); // TODO: revert to Bangle.showClock after fixing memory leaks
+ try {
+ MESSAGES = require("messages").getMessages();
+ // Apply fast loaded messages
+ (Bangle.MESSAGES || []).forEach(m => require("messages").apply(m, MESSAGES));
+ delete Bangle.MESSAGES;
+ // Write them back to storage when we're done
+ E.on("kill", saveMessages);
+ } catch(e) {
+ g.reset().clear();
+ E.showPrompt(/*LANG*/"Message file corrupt, erase all messages?", {title:/*LANG*/"Delete All Messages"}).then(isYes => {
+ // We are troubleshooting, so do a clean "load" in both cases (instead of Bangle.load)
+ if (isYes) { // OK: erase message file and reload this app
+ require("messages").clearAll();
+ load("messagelist.app.js");
+ } else {
+ load(); // well, this app won't work... let's go back to the clock
+ }
+ });
+ }
+
+ const setUI = function(options, cb) {
+ options = Object.assign({remove: () => uiRemove()}, options);
+ Bangle.setUI(options, cb);
+ Bangle.on("message", onMessage);
+ };
+
+ const remove = function(msg) {
+ if (msg.id==="call") call = undefined;
+ else if (msg.id==="map") map = undefined;
+ else if (msg.id==="alarm") alarm = undefined;
+ else if (msg.id==="music") music = undefined;
+ else MESSAGES = MESSAGES.filter(m => m.id!==msg.id);
+ };
+ const buzz = function(msg) {
+ return require("messages").buzz(msg.src);
+ };
+ const show = function(msg) {
+ delete msg.show; // don't show this again
+ if (msg.id==="call") showCall(msg);
+ else if (msg.id==="map") showMap(msg);
+ else if (msg.id==="alarm") showAlarm(msg);
+ else if (msg.id==="music") showMusic(msg);
+ else showMessage(msg);
+ };
+
+ const onMessage = function(type, msg) {
+ if (msg.handled) return;
+ msg.handled = true;
+ switch(type) {
+ case "call":
+ return onCall(msg);
+ case "music":
+ return onMusic(msg);
+ case "map":
+ return onMap(msg);
+ case "alarm":
+ return onAlarm(msg);
+ case "text":
+ return onText(msg);
+ case "clearAll":
+ MESSAGES = [];
+ if (["messages", "menu"].includes(active)) showMenu();
+ break;
+ default:
+ E.showAlert(/*LANG*/"Unknown message type:"+"\n"+type).then(goBack);
+ }
+ };
+ Bangle.on("message", onMessage);
+
+ const onCall = function(msg) {
+ if (msg.t==="remove") {
+ call = undefined;
+ return exitScreen("call");
+ }
+ // incoming call: show it
+ call = msg;
+ buzz(call);
+ showCall();
+ };
+ const onAlarm = function(msg) {
+ if (msg.t==="remove") {
+ alarm = undefined;
+ return exitScreen("alarm");
+ }
+ alarm = msg;
+ buzz(alarm);
+ showAlarm();
+ };
+ let musicTimeout;
+ const onMusic = function(msg) {
+ const hadMusic = !!music;
+ if (musicTimeout) clearTimeout(musicTimeout);
+ musicTimeout = undefined;
+ if (msg.t==="remove") {
+ music = undefined;
+ if (active==="main" && hadMusic) return showMain(); // refresh menu: remove "Music" entry (if not always visible)
+ else return exitScreen("music");
+ }
+
+ music = Object.assign({}, music, msg);
+
+ // auto-close after being paused
+ if (music.state!=="play") musicTimeout = setTimeout(function() {
+ musicTimeout = undefined;
+ if (active==="music" && (!music || music.state!=="play")) quitApp();
+ }, 60*1000); // paused for 1 minute
+ // auto-close after "playing" way beyond song duration (because "stop" messages don't seem to exist)
+ else musicTimeout = setTimeout(function() {
+ musicTimeout = undefined;
+ if (active==="music" && (!music || music.state==="play")) quitApp();
+ }, 2*Math.max(music.dur || 0, 5*60)*1000); // playing: assume ended after twice song duration, or at least 10 minutes
+
+ if (active==="music") showMusic(); // update music screen
+ else if (active==="main" && !hadMusic) {
+ if (settings().openMusic && music.state==="play" && music.track) showMusic();
+ else showMain(); // refresh menu: add "Music" entry
+ }
+ };
+ const onMap = function(msg) {
+ const hadMap = !!map;
+ if (msg.t==="remove") {
+ map = undefined;
+ if (back==="map") back = undefined;
+ if (active==="main" && hadMap) return showMain(); // refresh menu: remove "Map" entry
+ else return exitScreen("map");
+ }
+ map = msg;
+ if (["map", "music"].includes(active)) showMap(); // update map screen, or switch away from music (not other screens)
+ else if (active==="main" && !hadMap) showMain(); // refresh menu: add "Map" entry
+ };
+ const onText = function(msg) {
+ require("messages").apply(msg, MESSAGES);
+ const mIdx = MESSAGES.findIndex(m => m.id===msg.id);
+ if (!MESSAGES[mIdx]) if (back==="messages") back = undefined;
+ if (active==="main") showMain(); // update message count
+ if (MESSAGES.length===0) exitScreen("messages"); // removed last message
+ else if (active==="messages") showMessage(messageNum);
+ if (msg.new) buzz(msg);
+ if (active!=="call") {// don't switch away from incoming call
+ if (active!=="messages" || messageNum===mIdx) showMessage(mIdx);
+ }
+ if (active==="messages") drawFooter(); // update footer with new number of messages
+ };
+
+ const getImage = function(msg, def) {
+ // app icons, provided by `messages` app
+ return require("messageicons").getImage(msg);
+ };
+ const getImageColor = function(msg, def) {
+ // app colors, provided by `messages` app
+ return require("messageicons").getColor(msg, {default: def});
+ };
+ const getIcon = function(icon) {
+ return require("messagegui").getIcon(icon);
+ };
+ const getIconColor = function(icon) {
+ return require("messagegui").getColor(icon);
+ };
+
+ /*
+ * icons should be 24x24px with 1bpp colors and transparancy
+ */
+ const getMessageImage = function(msg) {
+ if (msg.img) return atob(msg.img);
+ if (msg.id==="music") return getIcon("Music");
+ if (msg.id==="back") return getIcon("Back");
+ const s = (msg.src || "").toLowerCase();
+
+ return getImage(s, "notification");
+ };
+
+ const showMap = function() {
+ setActive("map");
+ delete map.new;
+ let m, distance, street, target, eta;
+ m = map.title.match(/(.*) - (.*)/);
+ if (m) {
+ distance = m[1];
+ street = m[2];
+ } else {
+ street = map.title;
+ }
+ m = map.body.match(/(.*) - (.*)/);
+ if (m) {
+ target = m[1];
+ eta = m[2];
+ } else {
+ target = map.body;
+ }
+ let layout = new Layout({
+ type: "v", c: [
+ {type: "txt", font: fontNormal, label: target, bgCol: g.theme.bg2, col: g.theme.fg2, fillx: 1, pad: 2},
+ {
+ type: "h", bgCol: g.theme.bg2, col: g.theme.fg2, fillx: 1, c: [
+ {type: "txt", font: "6x8", label: "Towards"},
+ {type: "txt", font: fontNormal, label: street},
+ ]
+ },
+ {
+ type: "h", fillx: 1, filly: 1, c: [
+ map.img ? {type: "img", src: () => atob(map.img), scale: 2} : {},
+ {
+ type: "v", fillx: 1, c: [
+ {type: "txt", font: fontNormal, label: distance || ""},
+ ]
+ },
+ ]
+ },
+ {type: "txt", font: "6x8:2", label: eta}
+ ]
+ });
+ layout.render();
+ // go back on any input
+ setUI({
+ mode: "custom",
+ back: goBack,
+ btn: b => {
+ if (B2 || b===2) goBack();
+ },
+ swipe: dir => {
+ if (dir===RIGHT) showMain();
+ },
+ });
+ };
+
+ const toggleMusic = function() {
+ const mc = cmd => {
+ if (Bangle.musicControl) Bangle.musicControl(cmd);
+ };
+ if (!music) {
+ music = {state: "play"};
+ mc("play");
+ } else if (music.state==="play") {
+ music.state = "pause";
+ mc("pause");
+ } else {
+ music.state = "play";
+ mc("play");
+ }
+ if (layout && layout.musicIcon) {
+ // musicIcon/musicToggle .src returns icon based on current music.state
+ layout.update(layout.musicIcon);
+ if (layout.musicToggle) layout.update(layout.musicToggle);
+ layout.render();
+ }
+ };
+
+ const doMusic = function(action) {
+ if (!Bangle.musicControl) return;
+ Bangle.buzz(50);
+ if (action==="toggle") toggleMusic();
+ else Bangle.musicControl(action);
+ };
+ const showMusic = function() {
+ if (active!==music) setActive("music");
+ if (!music) music = {track: "", artist: "", album: "", state: "pause"};
+ delete music.new;
+ const w = Bangle.appRect.w-50; // title/album need to leave room for icon
+ let artist, album;
+ if (music.album && settings().showAlbum) {
+ // max 2 lines for artist/album
+ artist = g.setFont(fontNormal).wrapString(music.artist, w).slice(0, 2).join("\n");
+ album = g.wrapString(music.album, w).slice(0, 2).join("\n");
+ } else {
+ // no album: artist gets 3 lines
+ artist = g.setFont(fontNormal).wrapString(music.artist, w).slice(0, 3).join("\n");
+ album = "";
+ }
+ // place (subtitle) on a new line
+ let track = music.track.replace(/ \(/, "\n(");
+ track = g.wrapString(track, Bangle.appRect.w).slice(0, 5).join("\n");
+ // "unknown" n/c/dur can show up as -1
+ let num, dur;
+ if ("n" in music && music.n>0) {
+ num = "#"+music.n;
+ if ("c" in music && music.c>0) {
+ num += "/"+music.c;
+ }
+ num = {type: "txt", font: fontTiny, bgCol: g.theme.bg, label: num};
+ }
+ if ("dur" in music && music.dur>0) {
+ dur = Math.floor(music.dur/60)+":"+(music.dur%60).toString().padStart(2, "0");
+ dur = {type: "txt", font: fontTiny, bgCol: g.theme.bg, label: dur};
+ }
+ let info;
+ if (num && dur) info = {type: "h", fillx: 1, c: [{fillx: 1}, dur, {fillx: 1}, num, {fillx: 1},]};
+ else if (num) info = num;
+ else if (dur) info = dur;
+ else info = {};
+
+ layout = new Layout({
+ type: "v", c: [
+ {
+ type: "h", fillx: 1, bgCol: g.theme.bg2, col: g.theme.fg2, c: [
+ {
+ id: "musicIcon", type: "img", pad: 10, bgCol: g.theme.bg2, col: g.theme.fg2
+ , src: () => getIcon((music.state==="play") ? "music" : "pause")
+ },
+ {
+ type: "v", fillx: 1, c: [
+ {type: "txt", font: fontNormal, col: g.theme.fg2, bgCol: g.theme.bg2, label: artist, pad: 2, id: "artist"},
+ album ? {type: "txt", font: fontNormal, col: g.theme.fg2, bgCol: g.theme.bg2, label: album, pad: 2, id: "album"} : {},
+ ]
+ }
+ ]
+ },
+ {type: "txt", halign: 0, font: fontNormal, bgCol: g.theme.bg, label: track, fillx: 1, filly: 1, pad: 2, id: "track"},
+ settings().musicButtons ? {
+ type: "h", fillx: 1, c: [
+ B2 ? {} : {width: 4},
+ {
+ type: "btn", id: "previous", cb: () => doMusic("previous")
+ , src: () => getIcon("previous")
+ },
+ {fillx: 1},
+ {
+ type: "btn", id: "musicToggle", cb: () => doMusic("toggle")
+ , src: () => getIcon((music.state==="play") ? "pause" : "play")
+ },
+ {fillx: 1},
+ {
+ type: "btn", id: "next", cb: () => doMusic("next")
+ , src: () => getIcon("next")
+ },
+ B2 ? {} : {width: 4},
+ ]
+ } : {},
+ info,
+ ]
+ });
+ layout.render();
+ let options = {mode: "updown"};
+ // B1 with buttons: left hand side of screen is used for "previous"
+ if (B2 || !settings().musicButtons) options.back = goBack;
+ setUI(options, ud => {
+ if (ud) Bangle.musicControl(ud>0 ? "volumedown" : "volumeup");
+ else {
+ if (B2 || settings().musicButtons) goBack(); // B1 left-hand touch is "previous", so we need a way to go back
+ else doMusic("toggle");
+ }
+ });
+
+ Bangle.swipeHandler = dir => {
+ if (dir!==0) doMusic(dir===RIGHT ? "previous" : "next");
+ };
+ Bangle.on("swipe", Bangle.swipeHandler);
+
+ if (Bangle.touchHandler) Bangle.removeListener("touch", Bangle.touchHandler);
+ if (settings().musicButtons) {
+ // visible buttons
+ // left = previous, middle = toggle, right = next
+ if (B2) Bangle.touchHandler = (_side, xy) => {
+ // accept touches on the whole bottom and pick the closest button
+ if (xy.y2*Bangle.appRect.w/3) doMusic("next");
+ else doMusic("toggle");
+ };
+ else Bangle.touchHandler = (side) => {
+ if (side===1) doMusic("previous");
+ if (side===2) doMusic("next");
+ if (side===3) doMusic("toggle");
+ };
+ } else {
+ // no buttons: touch = toggle
+ // B2 setUI sets touchHandler, override that (we only want up/down swipes from the UI)
+ Bangle.touchHandler = (side, e) => {
+ // B1: side 1 (left) = back, B2: only toggle for e outside widget area
+ if ((!B2 && side>1) || (B2 && e.y>Bangle.appRect.y)) doMusic("toggle");
+ };
+ }
+ Bangle.on("touch", Bangle.touchHandler);
+ };
+
+ let layout;
+
+ const clearStuff = function() {
+ delete Bangle.appRect;
+ layout = undefined;
+ setUI();
+ g.reset().clearRect(Bangle.appRect);
+ };
+ const setActive = function(screen, args) {
+ clearStuff();
+ if (active && screen!==active) back = active;
+ if (screen==="messages") messageNum = args;
+ active = screen;
+ };
+ /**
+ * Go back to previous screen, preserving history
+ */
+ const goBack = function() {
+ if (back==="call" && call) showCall();
+ else if (back==="map" && map) showMap();
+ else if (back==="music" && music) showMusic();
+ else if (back==="messages" && MESSAGES.length) showMessage();
+ else if (back) showMain(); // previous screen was "main", or no longer valid
+ else quitApp(); // no previous screen: go back to clock
+ };
+ /**
+ * Leave screen, and make sure goBack() won't take us there anymore;
+ * @param {string} screen
+ */
+ const exitScreen = function(screen) {
+ if (back===screen) back = (active==="main") ? undefined : "main";
+ if (active===screen) {
+ active = undefined;
+ goBack();
+ }
+ };
+ const showMain = function() {
+ setActive("main");
+ let grid = {"": {title:/*LANG*/"Messages", align: 0, back: load}};
+ if (call) grid[/*LANG*/"Incoming Call"] = {icon: "Phone", cb: showCall};
+ if (alarm) grid[/*LANG*/"Alarm"] = {icon: "Alarm", cb: showAlarm};
+ const unread = MESSAGES.filter(m => m.new).length;
+ if (unread) {
+ grid[unread+" "+/*LANG*/"New"] = {icon: "Unread", cb: () => showMessage(MESSAGES.findIndex(m => m.new))};
+ grid[/*LANG*/"All"+` (${MESSAGES.length})`] = {icon: "Notification", cb: showMessage};
+ } else {
+ const allLabel = MESSAGES.length+" "+(MESSAGES.length===1 ?/*LANG*/"Message" :/*LANG*/"Messages");
+ if (MESSAGES.length) grid[allLabel] = {icon: "Notification", cb: showMessage};
+ else grid[/*LANG*/"No Messages"] = {icon: "Neg", cb: load};
+ }
+ if (unread {
+ E.showPrompt(/*LANG*/"Are you sure?", {title:/*LANG*/"Dismiss Read Messages"}).then(isYes => {
+ if (isYes) {
+ MESSAGES.filter(m => !m.new).forEach(msg => {
+ Bangle.messageResponse(msg, false);
+ remove(msg);
+ });
+ }
+ showMain();
+ });
+ }
+ };
+ }
+ if (map) grid[/*LANG*/"Map"] = {icon: "Map", cb: showMap};
+ if (music || settings().alwaysShowMusic) grid[/*LANG*/"Music"] = {icon: "Music", cb: showMusic};
+ grid[/*LANG*/"settings"] = {icon: "settings", cb: showSettings};
+ showGrid(grid);
+ };
+ const clamp = function(val, min, max) {
+ if (valmax) return max;
+ return val;
+ };
+ /**
+ * Show grid of labeled buttons,
+ *
+ * items:
+ * {
+ * cb: callback,
+ * img: button image,
+ * icon: icon name, // string, use getIcon(icon) instead of img
+ * col: icon color, // optional: defaults to getColor(icon)
+ * }
+ * "" item is options:
+ * {
+ * title: string,
+ * back: callback,
+ * rows/cols: (optional) fit to this many columns/rows, omit for automatic fit
+ * align: bottom row alignment if items don't fit perfectly into a grid
+ * -1: left
+ * 1: right
+ * 0: left, but move final button to the right
+ * undefined: spread (can be unaligned with rest of grid!)
+ * }
+ * @param items
+ */
+ const showGrid = function(items) {
+ clearStuff();
+ const options = items[""] || {},
+ back = options.back || items["< Back"];
+ const keys = Object.keys(items).filter(k => k!=="" && k!=="< Back");
+ let cols;
+ if (options.cols) {
+ cols = options.cols;
+ } else if (options.rows) {
+ cols = Math.ceil(keys.length/options.rows);
+ } else {
+ const rows = Math.round(Math.sqrt(keys.length));
+ cols = Math.ceil(keys.length/rows);
+ }
+
+ let l = {type: "v", c: []};
+ if (options.title) {
+ l.c.push({id: "title", type: "txt", label: options.title, font: (B2 ? "12x20" : "6x8:2"), fillx: 1});
+ }
+ const w = Bangle.appRect.w/cols, // set explicit width, because labels can stick out
+ bgs = [g.theme.bgH, g.theme.bg2], // background colors used for buttons
+ newRow = () => ({type: "h", filly: 1, c: []});
+ let row = newRow(),
+ cbs = [[]]; // callbacks for Bangle.js 2 touchHandler below
+ keys.forEach(key => {
+ const item = items[key],
+ label = g.setFont(fontTiny).wrapString(key, w).join("\n");
+ let color = "col" in item ? item.col : getIconColor(item.icon || "Unknown");
+ if (color && bgs.includes(g.setColor(color).getColor())) color = undefined; // make sure button is not invisible
+ row.c.push({
+ type: "v", pad: 2, width: w, c: [
+ {
+ type: "btn",
+ src: item.img || (() => getIcon(item.icon || "Unknown")),
+ col: color,
+ cb: B2
+ ? undefined // We handle B2 touches below
+ : () => setTimeout(item.cb), // prevent MEMORY error from running cb() inside the Layout touchHandler
+ },
+ {height: 2},
+ {type: "txt", label: label, font: fontTiny},
+ ]
+ });
+ if (B2) cbs[cbs.length-1].push(item.cb);
+ if (row.c.length>=cols) {
+ l.c.push(row);
+ row = newRow();
+ if (B2) cbs.push([]);
+ }
+ });
+ if (row.c.length) {
+ if (options.align!==undefined) {
+ const filler = {width: w*(cols-row.c.length)};
+ if (options.align=== -1) row.c.unshift(filler); // left
+ else if (options.align===1) row.c.push(filler); // right
+ else if (options.align===0) row.c.splice(row.c.length-1, 0, filler); // left, but final item on right
+ }
+ l.c.push(row);
+ }
+ layout = new Layout(l, {back: back});
+ layout.render();
+
+ if (B2) {
+ // override touchHandler: no need to hit buttons exactly, just pick the nearest
+ if (Bangle.touchHandler) Bangle.removeListener("touch", Bangle.touchHandler);
+ Bangle.touchHandler = (side, xy) => {
+ if (xy.y<=Bangle.appRect.y) return; // widgetbar: ignore
+ let rows = l.c.length,
+ y = Bangle.appRect.y, h = Bangle.appRect.h;
+ if (options.title) {
+ rows--;
+ y += layout.title.h;
+ h -= layout.title.h;
+ }
+ const r = clamp(Math.floor(rows*(xy.y-y)/h), 0, rows-1); // row (0-indexed)
+ let c; // column (0-indexed)
+ if (rcbs[r].length-2) return; // gap before final item
+ } else { // spread
+ c = clamp(Math.floor(cbs[r].length*(xy.x-Bangle.appRect.x)/Bangle.appRect.w), 0, cols-1);
+ }
+ }
+ if (r {
+ setFont();
+ showMain();
+ });
+ };
+ const showCall = function() {
+ setActive("call");
+ delete call.new;
+ Bangle.setLocked(false);
+ Bangle.setLCDPower(1);
+
+ const w = g.getWidth()-48,
+ lines = g.setFont(fontNormal).wrapString(call.title, w),
+ title = (lines.length>2) ? lines.slice(0, 2).join("\n")+"..." : lines.join("\n");
+ const respond = function(accept) {
+ Bangle.buzz(50);
+ Bangle.messageResponse(call, accept);
+ remove(call);
+ call = undefined;
+ goBack();
+ };
+ let options = {};
+ if (!B2) {
+ options.btns = [
+ {
+ label:/*LANG*/"accept",
+ cb: () => respond(true),
+ }, {
+ label:/*LANG*/"ignore",
+ cb: goBack,
+ }, {
+ label:/*LANG*/"reject",
+ cb: () => respond(false),
+ }
+ ];
+ }
+
+ layout = new Layout({
+ type: "v", c: [
+ {
+ type: "h", fillx: 1, bgCol: g.theme.bg2, col: g.theme.fg2, c: [
+ {type: "img", pad: 10, src: () => getIcon("phone"), col: getIconColor("phone")},
+ {
+ type: "v", fillx: 1, c: [
+ {type: "txt", font: fontTiny, label: call.src ||/*LANG*/"Incoming Call", bgCol: g.theme.bg2, col: g.theme.fg2, fillx: 1, pad: 2, halign: 1},
+ title ? {type: "txt", font: fontNormal, label: title, bgCol: g.theme.bg2, col: g.theme.fg2, fillx: 1, pad: 2} : {},
+ ]
+ },
+ ]
+ },
+ {type: "txt", font: fontNormal, label: call.body, fillx: 1, filly: 1, pad: 2, wrap: true},
+ {
+ type: "h", fillx: 1, c: [
+ // button callbacks won't actually be used: setUI below overrides the touchHandler set by Layout
+ {type: B2 ? "btn" : "img", src: () => getIcon("Neg"), cb: () => respond(false)},
+ {fillx: 1},
+ {type: B2 ? "btn" : "img", src: () => getIcon("Pos"), cb: () => respond(true)},
+ ]
+ }
+ ]
+ }, options);
+ layout.render();
+ setUI({
+ mode: "custom",
+ back: goBack,
+ touch: (side, xy) => {
+ if (B2 && xy.y {
+ if (B2 || b===2) goBack();
+ else if (b===1) respond(true);
+ else respond(false);
+ },
+ swipe: dir => {
+ if (dir===RIGHT) showMain();
+ },
+ });
+ };
+ const showAlarm = function() {
+ // dismissing alarms doesn't seem to work, so this is simple */
+ setActive("alarm");
+ delete alarm.new;
+ Bangle.setLocked(false);
+ Bangle.setLCDPower(1);
+
+ const w = g.getWidth()-48,
+ lines = g.setFont(fontNormal).wrapString(alarm.title, w),
+ title = (lines.length>2) ? lines.slice(0, 2).join("\n")+"..." : lines.join("\n");
+ layout = new Layout({
+ type: "v", c: [
+ {
+ type: "h", fillx: 1, bgCol: g.theme.bg2, col: g.theme.fg2, c: [
+ alarm.body ? {type: "img", pad: 10, src: () => getIcon("alarm"), col: getIconColor("alarm")} : {},
+ {type: "txt", font: fontNormal, label: title ||/*LANG*/"Alarm", bgCol: g.theme.bg2, col: g.theme.fg2, fillx: 1, pad: 2, halign: 1},
+ ]
+ },
+ alarm.body
+ ? {type: "txt", font: fontNormal, label: alarm.body, fillx: 1, filly: 1, pad: 2, wrap: true}
+ : {type: "img", pad: 10, scale: 3, src: () => getIcon("alarm"), col: getIconColor("alarm")},
+ ]
+ });
+ layout.render();
+ setUI({
+ mode: "custom",
+ back: goBack,
+ btn: b => {
+ if (B2 || b===2) goBack();
+ },
+ swipe: dir => {
+ if (dir===RIGHT) showMain();
+ },
+ });
+ };
+ /**
+ * Send message response, and delete it from list
+ * @param {string|boolean} reply Response text, false to dismiss (true to open on phone)
+ */
+ const respondToMessage = function(reply) {
+ const msg = MESSAGES[messageNum];
+ if (msg) {
+ Bangle.messageResponse(msg, reply);
+ if (reply===false) remove(msg);
+ }
+ if (MESSAGES.length<1) goBack(); // no more messages
+ else showMessage((msg && reply===false) ? messageNum : messageNum+1); // show next message
+ };
+ const showMessageActions = function() {
+ let title = MESSAGES[messageNum].title || "";
+ if (g.setFont(fontNormal).stringMetrics(title).width>Bangle.appRect.w-(B2 ? 0 : 20)) {
+ title = g.wrapString("..."+title, Bangle.appRect.w-(B2 ? 0 : 20))[0].substring(3)+"...";
+ }
+ clearStuff();
+ let grid = {
+ "": {
+ title: title ||/*LANG*/"Message",
+ back: () => showMessage(messageNum),
+ cols: 3, // fit all replies on first row, dismiss on bottom
+ }
+ };
+ // Text replies don't work (yet)
+ // grid[/*LANG*/"OK"] = {icon: "Ok", col: "#0f0", cb: () => respondToMessage("\u{1F44D}")}; // "Thumbs up" emoji
+ // grid[/*LANG*/"Nak"] = {icon: "Nak", col: "#f00", cb: () => respondToMessage("\u{1F44E}")}; // "Thumbs down" emoji
+ // grid[/*LANG*/"No Phone"] = {icon: "NoPhone", col: "#f0f", cb: () => respondToMessage("\u{1F4F5}")}; // "No Mobile Phones" emoji
+
+ grid[/*LANG*/"Dismiss"] = {icon: "Trash", col: "#ff0", cb: () => respondToMessage(false)};
+ showGrid(grid);
+ };
+ /**
+ * Show message
+ *
+ * @param {number} [num=0] Message to show
+ * @param {boolean} [bottom=false] Scroll message to bottom right away
+ */
+ let buzzing = false, moving = false, switching = false;
+ let h, fh, offset;
+
+ /**
+ * draw (sticky) footer
+ */
+ const drawFooter = function() {
+ // left hint: swipe from left for main menu
+ g.reset().clearRect(Bangle.appRect.x, Bangle.appRect.y2-fh, Bangle.appRect.x2, Bangle.appRect.y2)
+ .setFont(fontTiny)
+ .setFontAlign(-1, 1) // bottom left
+ .drawString(
+ "\0"+atob("CAiBACBA/EIiAnwA")+ // back
+ "\0"+atob("CAiBAEgkEgkSJEgA"), // >>
+ Bangle.appRect.x+(B2 ? 1 : 28), Bangle.appRect.y2
+ );
+ // center message count+hints: swipe up/down for next/prev message
+ const footer = ` ${messageNum+1}/${MESSAGES.length} `,
+ fw = g.stringWidth(footer);
+ g.setFontAlign(0, 1); // bottom center
+ if (B2 && messageNum>0 && offset<=0)
+ g.drawString("\0"+atob("CAiBAABBIhRJIhQI"), Bangle.appRect.x+Bangle.appRect.w/2-fw/2, Bangle.appRect.y2); // ^ swipe to prev
+ g.drawString(footer, Bangle.appRect.x+Bangle.appRect.w/2, Bangle.appRect.y2);
+ if (B2 && messageNum=h-(Bangle.appRect.h-fh))
+ g.drawString("\0"+atob("CAiBABAoRJIoRIIA"), Bangle.appRect.x+Bangle.appRect.w/2+fw/2, Bangle.appRect.y2); // v swipe to next
+ // right hint: swipe from right for message actions
+ g.setFontAlign(1, 1) // bottom right
+ .drawString(
+ "\0"+atob("CAiBABIkSJBIJBIA")+ // <<
+ "\0"+atob("CAiBAP8AAP8AAP8A"), // = ("hamburger menu")
+ Bangle.appRect.x2-(B2 ? 1 : 28), Bangle.appRect.y2
+ );
+ };
+ const showMessage = function(num, bottom) {
+ if (num<0) num = 0;
+ if (!num) num = 0; // no number: show first
+ if (num>=MESSAGES.length) num = MESSAGES.length-1;
+ setActive("messages", num);
+ if (!MESSAGES.length) {
+ // I /think/ this should never happen...
+ return E.showPrompt(/*LANG*/"No Messages", {
+ title:/*LANG*/"Messages",
+ img: require("heatshrink").decompress(atob("kkk4UBrkc/4AC/tEqtACQkBqtUDg0VqAIGgoZFDYQIIM1sD1QAD4AIBhnqA4WrmAIBhc6BAWs8AIBhXOBAWz0AIC2YIC5wID1gkB1c6BAYFBEQPqBAYXBEQOqBAnDAIQaEnkAngaEEAPDFgo+IKA5iIOhCGIAFb7RqAIGgtUBA0VqobFgNVA")),
+ buttons: {/*LANG*/"Ok": 1}
+ }).then(showMain);
+ }
+ Bangle.setLocked(false);
+ Bangle.setLCDPower(1);
+ // only clear msg.new on user input
+ const msg = MESSAGES[messageNum]; // message
+ fh = 10; // footer height
+ offset = 0;
+ let oldOffset = 0;
+ const move = (dy) => {
+ offset = Math.max(0, Math.min(h-(Bangle.appRect.h-fh), offset+dy)); // clip at message height
+ dy = oldOffset-offset; // real dy
+ // move all elements to new offset
+ const offsetRecurser = function(l) {
+ if (l.y) l.y += dy;
+ if (l.c) l.c.forEach(offsetRecurser);
+ };
+ offsetRecurser(layout.l);
+ oldOffset = offset;
+ draw();
+ };
+ const draw = () => {
+ g.reset()
+ .clearRect(Bangle.appRect.x, Bangle.appRect.y, Bangle.appRect.x2, Bangle.appRect.y2-fh)
+ .setClipRect(Bangle.appRect.x, Bangle.appRect.y, Bangle.appRect.x2, Bangle.appRect.y2-fh);
+ g.reset = () => g.setColor(g.theme.fg).setBgColor(g.theme.bg); // stop Layout resetting ClipRect
+ layout.render();
+ if (layout.button && h>Bangle.appRect.h-fh && offset(Bangle.appRect.h-fh)) {
+ const sbh = (Bangle.appRect.h-fh)/h*(Bangle.appRect.h-fh), // scrollbar height
+ y1 = Bangle.appRect.y+offset/h*(Bangle.appRect.h-fh), y2 = y1+sbh;
+ g.setColor(g.theme.bg).drawLine(Bangle.appRect.x2, Bangle.appRect.y, Bangle.appRect.x2, Bangle.appRect.y2-fh);
+ g.setColor(g.theme.fg).drawLine(Bangle.appRect.x2, y1, Bangle.appRect.x2, y2);
+ }
+ drawFooter();
+ };
+ const buzzOnce = () => {
+ if (buzzing) return;
+ buzzing = true;
+ Bangle.buzz(50).then(() => setTimeout(() => {buzzing = false;}, 500));
+ };
+
+ layout = getMessageLayout(msg);
+ h = layout.l.h; // message height
+ if (bottom) move(h); // scrolling backwards: jump to bottom of message
+ else draw();
+ const PAGE_SIZE = Bangle.appRect.h-fh;
+ const // shared B1/B2 handlers
+ back = () => {
+ delete msg.new; // we mark messages as read on any input
+ goBack();
+ },
+ swipe = dir => {
+ delete msg.new;
+ if (dir===RIGHT) showMain();
+ else if (dir===LEFT) showMessageActions();
+ },
+ touch = (side, xy) => {
+ delete msg.new;
+ if (h<=Bangle.appRect.h-fh || offset>=h-(Bangle.appRect.h-fh)) { // already at bottom
+ // B2: check for button-press
+ // setUI overrides Layout listeners, so we need to check for button presses ourselves
+ if (B2 && layout.button) {
+ const b = layout.button;
+ // the button is at the bottom of the screen, so we accept touches all the way down
+ if (xy.x>=b.x && xy.y>=b.y && xy.x<=b.x+b.w /*&& xy.y<=b.y+b.h*/) return b.cb();
+ }
+ if (B2 && xy.yBangle.appRect.h-fh && offset {
+ delete msg.new;
+ if (!switching) {
+ const dy = -e.dy;
+ if (dy>0) { // up
+ if (h>Bangle.appRect.h-fh && offset0) {
+ moving = true; // prevent scrolling right into prev message
+ move(dy);
+ } else if (messageNum>0) { // already at top: show prev
+ if (!moving) { // don't scroll right through to previous message
+ Bangle.buzz(30);
+ switching = true; // don't process any more drag events until we lift our finger
+ showMessage(messageNum-1, true);
+ }
+ } else { // already at top of first message
+ buzzOnce();
+ }
+ }
+ }
+ if (!e.b) {
+ // touch end: we can swipe to another message (if we reached the top/bottom) or move the new message
+ moving = false;
+ switching = false;
+ }
+ },
+ touch: touch,
+ });
+ } else { // Bangle.js 1
+ setUI({
+ mode: "updown",
+ back: back,
+ }, dir => {
+ delete msg.new;
+ if (dir===DOWN) {
+ if (h>Bangle.appRect.h-fh && offset0) {
+ move(-PAGE_SIZE);
+ } else if (messageNum>0) { // top reached: show previous
+ Bangle.buzz(30);
+ showMessage(messageNum-1, true);
+ } else {
+ buzzOnce(); // already at top of first message
+ }
+ } else { // button
+ showMessageActions();
+ }
+ });
+ Bangle.swipeHandler = swipe;
+ Bangle.on("swipe", Bangle.swipeHandler);
+ Bangle.touchHandler = touch;
+ Bangle.on("touch", Bangle.touchHandler);
+ } // Bangle.js 1/2
+ };
+ /**
+ * Determine message layout information: size, fonts, and wrapped title/body texts
+ *
+ * @param msg
+ * @returns {{h: number, w: number,
+ * src: (string),
+ * title: (string), titleFont: (string),
+ * body: (string), bodyFont: (string)}}
+ */
+ const getMessageLayoutInfo = function(msg) {
+ // header: [icon][title]
+ // [ src]
+ //
+ // But: no title? -> use src as title
+ let w, src = msg.src || "",
+ title = msg.title || "",
+ body = msg.body || "",
+ h = 0, // total height
+ th = 0, // title height
+ ih = 46; // icon height: // icon(24) + internal padding(20) + icon<->src spacer(2)
+ if (!title) {
+ title = src;
+ src = "";
+ }
+
+ // top bar
+ if (title) {
+ w = Bangle.appRect.w-59; // icon(24) + padding:left(5) + padding:btn-txt(5) + internal btn padding(20) + padding:right(5)
+ title = g.setFont(fontNormal).wrapString(title, w).join("\n");
+ th += 2+g.stringMetrics(title).height; // 2px padding
+ }
+ if (src) {
+ w = 59; // icon(24) + padding:left(5) + padding:btn-txt(5) + internal btn padding(20) + padding:right(5)
+ src = g.setFont(fontTiny).wrapString(src, w).join("\n");
+ ih += g.stringMetrics(src).height;
+ }
+
+ h = Math.max(ih, th); // maximum of icon/title
+
+ // body
+ w = Bangle.appRect.w-4; // padding(2x2)
+ body = g.setFont(fontNormal).wrapString(msg.body, w).join("\n");
+ h += 4+g.stringMetrics(body).height; // padding(2x2)
+
+ if (settings().button) h += 44; // icon(24) + padding(2x2) + internal btn padding(16)
+
+ w = Bangle.appRect.w;
+ // always expand to -<(10x)footer>
+ h = Math.max(h, Bangle.appRect.h-10);
+
+ return {
+ src: src,
+ title: title,
+ body: body,
+ h: h,
+ w: w,
+ };
+ };
+
+ const getMessageLayout = function(msg) {
+ // Crafted so that on B2, with "medium" font, a message with
+ // icon + src + 2-line title + 2-line body + button
+ // fits exactly, i.e. no need for scrolling
+ const info = getMessageLayoutInfo(msg);
+ const hCol = msg.new ? g.theme.fgH : g.theme.fg2,
+ hBg = msg.new ? g.theme.bgH : g.theme.bg2;
+
+ // lie to Layout library about available space
+ Bangle.appRect = Object.assign({}, Bangle.appRect,
+ {w: info.w, h: info.h, x2: Bangle.appRect.x+info.w-1, y2: Bangle.appRect.y+info.h-1});
+
+ // make sure icon is not invisible
+ let imageCol = getImageColor(msg);
+ if (g.setColor(imageCol).getColor()==hBg) imageCol = hCol;
+
+ layout = new Layout({
+ type: "v", c: [
+ {
+ type: "h", fillx: 1, bgCol: hBg, col: hCol, c: [
+ {width: 3},
+ {
+ type: "v", c: [
+ {type: "img", /*pad: 2,*/ src: () => getMessageImage(msg), col: imageCol},
+ {height: 2},
+ info.src ? {type: "txt", font: fontTiny, label: info.src, bgCol: hBg, col: hCol} : {},
+ ]
+ },
+ info.title ? {type: "txt", font: fontNormal, label: info.title, bgCol: hBg, col: hCol, fillx: 1, pad: 2} : {},
+ {width: 3},
+ ]
+ },
+ {type: "txt", font: fontNormal, label: info.body, fillx: 1, filly: 1, pad: 2},
+ {filly: 1},
+ settings().button ? {
+ type: "h", c: [
+ B2 ? {} : {fillx: 1}, // Bangle.js 1: touching right side = press button
+ {id: "button", type: "btn", pad: 2, src: () => getIcon("trash"), cb: () => respondToMessage(false)},
+ ]
+ } : {},
+ ]
+ });
+ layout.update();
+ delete Bangle.appRect;
+ return layout;
+ };
+
+ /** this is a timeout if the app has started and is showing a single message
+ but the user hasn't seen it (e.g. no user input) - in which case
+ we should start a timeout for settings().unreadTimeout to return
+ to the clock. */
+ let unreadTimeout;
+ /**
+ * Stop auto-unload timeout and buzzing, remove listeners for this function
+ */
+ const clearUnreadStuff = function() {
+ require("messages").stopBuzz();
+ if (unreadTimeout) clearTimeout(unreadTimeout);
+ unreadTimeout = undefined;
+ ["touch", "drag", "swipe"].forEach(l => Bangle.removeListener(l, clearUnreadStuff));
+ watches.forEach(w => clearWatch(w));
+ watches = [];
+ };
+
+ let messageNum, // currently visible message
+ watches = [], // button watches
+ savedMusic = false; // did we find a stored "music" message when loading?
+// special messages
+ let call, music, map, alarm;
+ /**
+ * Find special messages, and remove them from MESSAGES
+ */
+ const findSpecials = function() {
+ let idx = MESSAGES.findIndex(m => m.id==="call");
+ if (idx>=0) call = MESSAGES.splice(idx, 1)[0];
+ idx = MESSAGES.findIndex(m => m.id==="music");
+ if (idx>=0) {
+ music = MESSAGES.splice(idx, 1)[0];
+ savedMusic = true;
+ }
+ idx = MESSAGES.findIndex(m => m.id==="map");
+ if (idx>=0) map = MESSAGES.splice(idx, 1)[0];
+ idx = MESSAGES.findIndex(m => m.src && m.src.toLowerCase().startsWith("alarm"));
+ if (idx>=0) alarm = MESSAGES.splice(idx, 1)[0];
+ };
+ if (MESSAGES!==undefined) { // only if loading MESSAGES worked
+ g.reset().clear();
+ Bangle.loadWidgets();
+ require("messages").toggleWidget(false);
+ Bangle.drawWidgets();
+ findSpecials(); // sets global vars for special messages
+ // any message we asked to show?
+ const showIdx = MESSAGES.findIndex(m => m.show);
+ // any new text messages?
+ const newIdx = MESSAGES.findIndex(m => m.new);
+
+ // figure out why the app was loaded
+ if (showIdx>=0) show(showIdx);
+ else if (call && call.new) showCall();
+ else if (alarm && alarm.new) showAlarm();
+ else if (map && map.new) showMap();
+ else if (music && music.new && settings().openMusic) {
+ if (settings().alwaysShowMusic===undefined) {
+ // if not explicitly disabled, enable this the first time we see music
+ let s = settings();
+ s.alwaysShowMusic = true;
+ require("Storage").writeJSON("messages.settings.json", s);
+ }
+ showMusic();
+ }
+ // check for new message last: Maybe we already showed it, but timed out before
+ // if that happened, and we're loading for e.g. music now, we want to show the music screen
+ else if (newIdx>=0) {
+ showMessage(newIdx);
+ // auto-loaded for message(s): auto-close after timeout
+ let unreadTimeoutSecs = settings().unreadTimeout;
+ if (unreadTimeoutSecs===undefined) unreadTimeoutSecs = 60;
+ if (unreadTimeoutSecs) {
+ unreadTimeout = setTimeout(load, unreadTimeoutSecs*1000);
+ }
+ } else if (MESSAGES.length) { // not autoloaded, but we have messages to show
+ back = "main"; // prevent "back" from loading clock
+ showMessage();
+ } else showMain();
+
+ // stop buzzing, auto-close timeout on input
+ ["touch", "drag", "swipe"].forEach(l => Bangle.on(l, clearUnreadStuff));
+ (B2 ? [BTN1] : [BTN1, BTN2, BTN3]).forEach(b => watches.push(setWatch(clearUnreadStuff, b, false)));
+ }
+}
\ No newline at end of file
diff --git a/apps/messagelist/app.png b/apps/messagelist/app.png
new file mode 100644
index 000000000..6eae4bb96
Binary files /dev/null and b/apps/messagelist/app.png differ
diff --git a/apps/messagelist/boot.js b/apps/messagelist/boot.js
new file mode 100644
index 000000000..994a2cfed
--- /dev/null
+++ b/apps/messagelist/boot.js
@@ -0,0 +1,3 @@
+(function() {
+ Bangle.on("message", require("messagegui").messageListener);
+})();
\ No newline at end of file
diff --git a/apps/messagelist/lib.js b/apps/messagelist/lib.js
new file mode 100644
index 000000000..33b6d9d69
--- /dev/null
+++ b/apps/messagelist/lib.js
@@ -0,0 +1,246 @@
+// Handle incoming messages while the app is not loaded
+// The messages app overrides Bangle.messageListener
+// (placed in separate file, so we don't read this all at boot time)
+exports.messageListener = function(type, msg) {
+ if (msg.handled || (global.__FILE__ && __FILE__.startsWith("messagelist."))) return; // already handled/app open
+ // clean up, in case previous message didn't load the app after all
+ if (exports.loadTimeout) clearTimeout(exports.loadTimeout);
+ delete exports.loadTimeout;
+ delete exports.buzz;
+ const quiet = () => (require("Storage").readJSON("setting.json", 1) || {}).quiet;
+ /**
+ * Quietly load the app for music/map, if not already loading
+ */
+ function loadQuietly(msg) {
+ if (exports.loadTimeout) return; // already loading
+ exports.loadTimeout = setTimeout(function() {
+ Bangle.load("messagelist.app.js");
+ }, 500);
+ }
+ function loadNormal(msg) {
+ if (exports.loadTimeout) clearTimeout(exports.loadTimeout); // restart timeout
+ exports.loadTimeout = setTimeout(function() {
+ delete exports.loadTimeout;
+ // check there are still new messages (for #1362)
+ let messages = require("messages").getMessages(msg);
+ (Bangle.MESSAGES || []).forEach(m => require("messages").apply(m, messages));
+ if (!messages.some(m => m.new)) return; // don't use `status()`: also load for new music!
+ // if we're in a clock, or it's important, open app
+ if (Bangle.CLOCK || msg.important) {
+ if (exports.buzz) require("messages").buzz(msg.src);
+ Bangle.load("messagelist.app.js");
+ }
+ }, 500);
+ }
+
+ /**
+ * Mark message as handled, and save it for the app
+ */
+ const handled = () => {
+ if (!Bangle.MESSAGES) Bangle.MESSAGES = [];
+ require("messages").apply(msg, Bangle.MESSAGES);
+ if (!Bangle.MESSAGES.length) delete Bangle.MESSAGES;
+ if (msg.t==="remove") require("messages").save(msg);
+ else msg.handled = true;
+ };
+ /**
+ * Write messages to flash after all, when not laoding the app
+ */
+ const saveToFlash = () => {
+ (Bangle.MESSAGES||[]).forEach(m=>require("messages").save(m));
+ delete Bangle.MESSAGES;
+ }
+
+ switch(type) {
+ case "music":
+ if (!Bangle.CLOCK) return;
+ // only load app if we are playing, and we know which song
+ if (msg.state!=="play" || !msg.title) return;
+ if (exports.openMusic===undefined) {
+ // only read settings for first music message
+ exports.openMusic = !!(exports.settings().openMusic);
+ }
+ if (!exports.openMusic) return; // we don't care about music
+ if (quiet()) return;
+ msg.new = true;
+ handled();
+ return loadQuietly();
+
+ case "map":
+ handled();
+ if (msg.t!=="remove" && Bangle.CLOCK) loadQuietly();
+ else saveToFlash();
+ return;
+
+ case "text":
+ handled();
+ if (exports.settings().autoOpen===false) return saveToFlash();
+ if (quiet()) return saveToFlash();
+ if (msg.t!=="add" || !msg.new || !(Bangle.CLOCK || msg.important)) {
+ // not important enough to load the app
+ if (msg.t==="add" && msg.new) require("messages").buzz(msg);
+ return saveToFlash();
+ }
+ if (msg.t==="add" && msg.new) exports.buzz = true;
+ return loadNormal(msg);
+
+ case "alarm":
+ if (quiet()<2) return saveToFlash();
+ // fall through
+ case "call":
+ handled();
+ exports.buzz = true;
+ return loadNormal(msg);
+
+ // case "clearAll": do nothing
+ }
+};
+
+exports.settings = function() {
+ return Object.assign({
+ // Interface //
+ fontSize: 1,
+ onTap: 0, // [Message menu, Dismiss, Back, Nothing]
+ button: true,
+
+ // Behaviour //
+ vibrate: ":",
+ vibrateCalls: ":",
+ vibrateAlarms: ":",
+ repeat: 4,
+ vibrateTimeout: 60,
+ unreadTimeout: 60,
+ autoOpen: true,
+
+ // Music //
+ openMusic: true,
+ // no default: alwaysShowMusic (auto-enabled by app when music happens)
+ showAlbum: true,
+ musicButtons: false,
+
+ // Widget //
+ flash: true,
+ // showRead: false,
+
+ // Utils //
+ },
+ // fall back to default app settings if not set for messagelist
+ (require("Storage").readJSON("messages.settings.json", true) || {}),
+ (require("Storage").readJSON("messagelist.settings.json", true) || {}));
+};
+
+/**
+ * @param {string} icon Icon name
+ * @returns string Icon image string, for use with g.drawImage()
+ */
+exports.getIcon = function(icon) {
+ // TODO: icons should be 24x24px with 1bpp colors
+ switch(icon.toLowerCase()) {
+ // generic icons:
+ case "alert":
+ return atob("GBgBAAAAAP8AA//AD8PwHwD4HBg4ODwcODwccDwOcDwOYDwGYDwGYBgGYBgGcBgOcAAOOBgcODwcHDw4Hxj4D8PwA//AAP8AAAAA");
+ case "alarm":
+ case "alarmclockreceiver":
+ return atob("GBjBAP////8AAAAAAAACAEAHAOAefng5/5wTgcgHAOAOGHAMGDAYGBgYGBgYGBgYGBgYDhgYBxgMATAOAHAHAOADgcAB/4AAfgAAAAAAAAA=");
+ case "back": // TODO: 22x22
+ return atob("FhYBAAAAEAAAwAAHAAA//wH//wf//g///BwB+DAB4EAHwAAPAAA8AADwAAPAAB4AAHgAB+AH/wA/+AD/wAH8AA==");
+ case "calendar":
+ return atob("GBiBAAAAAAAAAAAAAA//8B//+BgAGBgAGBgAGB//+B//+B//+B9m2B//+B//+Btm2B//+B//+Btm+B//+B//+A//8AAAAAAAAAAAAA==");
+ case "mail": // TODO: 28x18
+ case "sms message":
+ case "notification":
+ return atob("HBKBAD///8H///iP//8cf//j4//8f5//j/x/8//j/H//H4//4PB//EYj/44HH/Hw+P4//8fH//44///xH///g////A==");
+ case "map": // TODO: 25x25,
+ return atob("GRmBAAAAAAAAAAAAAAIAYAHx/wH//+D/+fhz75w/P/4f//8P//uH///D///h3f/w4P+4eO/8PHZ+HJ/nDu//g///wH+HwAYAIAAAAAAAAAAAAAA=");
+ case "menu":
+ return atob("GBiBAAAAAAAAAAAAAAAAAP///////wAAAAAAAAAAAAAAAAAAAP///////wAAAAAAAAAAAAAAAAAAAP///////wAAAAAAAAAAAAAAAA==");
+ case "music": // TODO: 22x22
+ return atob("FhaBAH//+/////////////h/+AH/4Af/gB/+H3/7/f/v9/+/3/7+f/vB/w8H+Dwf4PD/x/////////////3//+A=");
+ case "nak": // TODO: 22x25
+ return atob("FhmBAA//wH//j//+P//8///7///v//+///7//////////////v//////////z//+D8AAPwAAfgAB+AAD4AAPgAAeAAB4AAHAAA==");
+ case "neg": // TODO: 22x22
+ return atob("FhaBADAAMeAB78AP/4B/fwP4/h/B/P4D//AH/4AP/AAf4AB/gAP/AB/+AP/8B/P4P4fx/A/v4B//AD94AHjAAMA=");
+ case "next":
+ return atob("GBiBAAAAAAAAAAAAAAwAcB8A+B+A+B/g+B/4+B/8+B//+B//+B//+B//+B//+B//+B/8+B/4+B/g+B+A+B8A+AwAcAAAAAAAAAAAAA==");
+ case "nophone": // TODO: 30x30
+ return atob("Hh6BAAAAAAGAAAAHAAAADgAAABwADwA4Af8AcA/8AOB/+AHH/+ADv/8AB//wAA/HAAAeAAACOAAADHAAAHjgAAPhwAAfg4AAfgcAAfwOAA/wHAA/wDgA/gBwA/gA4AfAAcAfAAOAGAAHAAAADgAAABgAAAAA");
+ case "ok": // TODO: 22x25
+ return atob("FhmBAAHAAAeAAB4AAPgAA+AAH4AAfgAD8AAPwAD//+//////////////7//////////////v//+///7///v//8///gf/+A//wA==");
+ case "pause":
+ return atob("GBiBAAAAAAAAAAAAAAOBwAfD4AfD4AfD4AfD4AfD4AfD4AfD4AfD4AfD4AfD4AfD4AfD4AfD4AfD4AfD4AfD4AOBwAAAAAAAAAAAAA==");
+ case "phone": // TODO: 23x23
+ case "call":
+ return atob("FxeBABgAAPgAAfAAB/AAD+AAH+AAP8AAP4AAfgAA/AAA+AAA+AAA+AAB+AAB+AAB+OAB//AB//gB//gA//AA/8AAf4AAPAA=");
+ case "play":
+ return atob("GBiBAAAAAAAAAAAAAAcAAA+AAA/gAA/4AA/8AA//AA//wA//4A//8A//8A//4A//wA//AA/8AA/4AA/gAA+AAAcAAAAAAAAAAAAAAA==");
+ case "pos": // TODO: 25x20
+ return atob("GRSBAAAAAYAAAcAAAeAAAfAAAfAAAfAAAfAAAfAAAfBgAfA4AfAeAfAPgfAD4fAA+fAAP/AAD/AAA/AAAPAAADAAAA==");
+ case "previous":
+ return atob("GBiBAAAAAAAAAAAAAA4AMB8A+B8B+B8H+B8f+B8/+B//+B//+B//+B//+B//+B//+B8/+B8f+B8H+B8B+B8A+A4AMAAAAAAAAAAAAA==");
+ case "settings": // TODO: 20x20
+ return atob("FBSBAAAAAA8AAPABzzgf/4H/+A//APnwfw/n4H5+B+fw/g+fAP/wH/+B//gc84APAADwAAAA");
+ case "to do":
+ return atob("GBgBAAAAAAAAAAAwAAB4AAD8AAH+AAP/DAf/Hg//Px/+f7/8///4///wf//gP//AH/+AD/8AB/4AA/wAAfgAAPAAAGAAAAAAAAAA");
+ case "trash":
+ return atob("GBiBAAAAAAAAAAB+AA//8A//8AYAYAYAYAZmYAZmYAZmYAZmYAZmYAZmYAZmYAZmYAZmYAZmYAZmYAYAYAYAYAf/4AP/wAAAAAAAAA==");
+ case "unknown": // TODO: 30x30
+ return atob("Hh6BAAAAAAAAAAAAAAAAAAPwAAA/8AAB/+AAD//AAD4fAAHwPgAHwPgAAAPgAAAfAAAA/AAAD+AAAH8AAAHwAAAPgAAAPgAAAPgAAAAAAAAAAAAAAAAAAHAAAAPgAAAPgAAAPgAAAHAAAAAAAAAAAAAAAAAA");
+ case "unread": // TODO: 29x24
+ return atob("HRiBAAAAH4AAAf4AAB/4AAHz4AAfn4AA/Pz/5+fj/z8/j/n5/j/P//j/Pn3j+PPPx+P8fx+Pw/x+AF/B4A78RiP3xwOPvHw+Pcf/+Ox//+NH//+If//+B///+A==");
+ default: //should never happen
+ return exports.getIcon("unknown");
+ }
+};
+/**
+ * @param {string} icon Icon
+ * @returns {string} Color to use with g.setColor()
+ */
+exports.getColor = function(icon) {
+ switch(icon.toLowerCase()) {
+ // generic colors, using B2-safe colors
+ case "alert":
+ return "#ff0";
+ case "alarm":
+ return "#fff";
+ case "calendar":
+ return "#f00";
+ case "mail":
+ return "#ff0";
+ case "map":
+ return "#f0f";
+ case "music":
+ return "#f0f";
+ case "neg":
+ return "#f00";
+ case "notification":
+ return "#0ff";
+ case "phone":
+ case "call":
+ return "#0f0";
+ case "settings":
+ return "#000";
+ case "sms message":
+ return "#0ff";
+ case "trash":
+ return "#f00";
+ case "unknown":
+ return g.theme.fg;
+ case "unread":
+ return "#ff0";
+ default:
+ return g.theme.fg;
+ }
+};
+
+/**
+ * Launch GUI app with given message
+ * @param {object} msg
+ */
+exports.open = function(msg) {
+ if (msg && msg.id && !msg.show) {
+ // store which message to load
+ msg.show = 1;
+ }
+
+ Bangle.load((msg && msg.new && msg.id!=="music") ? "messagelist.new.js" : "messagelist.app.js");
+};
diff --git a/apps/messagelist/metadata.json b/apps/messagelist/metadata.json
new file mode 100644
index 000000000..7947e2db4
--- /dev/null
+++ b/apps/messagelist/metadata.json
@@ -0,0 +1,28 @@
+{
+ "id": "messagelist",
+ "name": "Message List",
+ "version": "0.01",
+ "description": "Display notifications from iOS and Gadgetbridge/Android as a list",
+ "icon": "app.png",
+ "type": "app",
+ "tags": "tool,system",
+ "screenshots": [
+ {"url": "screenshot0.png"},
+ {"url": "screenshot1.png"},
+ {"url": "screenshot2.png"},
+ {"url": "screenshot3.png"}
+ ],
+ "supports": ["BANGLEJS","BANGLEJS2"],
+ "dependencies" : { "messageicons":"module" },
+ "provides_modules": ["messagegui"],
+ "readme": "README.md",
+ "storage": [
+ {"name":"messagelist.boot.js","url":"boot.js"},
+ {"name":"messagegui","url":"lib.js"},
+ {"name":"messagelist.app.js","url":"app.js"},
+ {"name":"messagelist.settings.js","url":"settings.js"},
+ {"name":"messagelist.img","url":"app-icon.js","evaluate":true}
+ ],
+ "data": [{"name":"messagelist.settings.json"}],
+ "sortorder": -9
+}
diff --git a/apps/messagelist/screenshot0.png b/apps/messagelist/screenshot0.png
new file mode 100644
index 000000000..b6f37c053
Binary files /dev/null and b/apps/messagelist/screenshot0.png differ
diff --git a/apps/messagelist/screenshot1.png b/apps/messagelist/screenshot1.png
new file mode 100644
index 000000000..f4d4db9fa
Binary files /dev/null and b/apps/messagelist/screenshot1.png differ
diff --git a/apps/messagelist/screenshot2.png b/apps/messagelist/screenshot2.png
new file mode 100644
index 000000000..67c192a1c
Binary files /dev/null and b/apps/messagelist/screenshot2.png differ
diff --git a/apps/messagelist/screenshot3.png b/apps/messagelist/screenshot3.png
new file mode 100644
index 000000000..02fed81a7
Binary files /dev/null and b/apps/messagelist/screenshot3.png differ
diff --git a/apps/messagelist/settings.js b/apps/messagelist/settings.js
new file mode 100644
index 000000000..cd2767336
--- /dev/null
+++ b/apps/messagelist/settings.js
@@ -0,0 +1,139 @@
+(function(back) {
+ let settings = require("messagegui").settings();
+ const inApp = (global.__FILE__ && __FILE__.startsWith("messagelist."));
+
+ function updateSetting(setting, value) {
+ settings[setting] = value;
+ let file;
+ switch(setting) {
+ case "flash":
+ case "showRead":
+ case "iconColorMode":
+ case "maxMessages":
+ case "maxUnreadTimeout":
+ case "openMusic":
+ case "repeat":
+ case "unlockWatch":
+ case "unreadTimeout":
+ case "vibrate":
+ case "vibrateCalls":
+ case "vibrateTimeout":
+ // Default app has this setting: update that file
+ file = "messages";
+ break;
+ default:
+ // write to our own settings file
+ file = "messagelist";
+ }
+ file += ".settings.json";
+ let saved = require("Storage").readJSON(file, true) || {};
+ saved[setting] = value;
+ require("Storage").writeJSON(file, saved);
+ }
+
+ function toggler(setting) {
+ return {
+ value: !!settings[setting],
+ onchange: v => updateSetting(setting, v)
+ };
+ }
+
+ function showIfMenu() {
+ const tapOptions = [/*LANG*/"Message menu",/*LANG*/"Dismiss",/*LANG*/"Back",/*LANG*/"Nothing"];
+ E.showMenu({
+ "": {"title": /*LANG*/"Interface"},
+ "< Back": () => showMainMenu(),
+ /*LANG*/"Font size": {
+ value: 0|settings.fontSize,
+ min: 0, max: 2,
+ format: v => [/*LANG*/"Small",/*LANG*/"Medium",/*LANG*/"Large",/*LANG*/"Huge"][v],
+ onchange: v => updateSetting("fontSize", v)
+ },
+ /*LANG*/"On Tap": {
+ value: settings.onTap,
+ min: 0, max: tapOptions.length-1, wrap: true,
+ format: v => tapOptions[v],
+ onchange: v => updateSetting("onTap", v)
+ },
+ /*LANG*/"Dismiss button": toggler("button"),
+ });
+ }
+
+ function showBMenu() {
+ E.showMenu({
+ "": {"title": /*LANG*/"Behaviour"},
+ "< Back": () => showMainMenu(),
+ /*LANG*/"Vibrate": require("buzz_menu").pattern(settings.vibrate, v => updateSetting("vibrate", v)),
+ /*LANG*/"Vibrate for calls": require("buzz_menu").pattern(settings.vibrateCalls, v => updateSetting("vibrateCalls", v)),
+ /*LANG*/"Vibrate for alarms": require("buzz_menu").pattern(settings.vibrateAlarms, v => updateSetting("vibrateAlarms", v)),
+ /*LANG*/"Repeat": {
+ value: settings.repeat,
+ min: 0, max: 10,
+ format: v => v ? v+"s" :/*LANG*/"Off",
+ onchange: v => updateSetting("repeat", v)
+ },
+ /*LANG*/"Vibrate timer": {
+ value: settings.vibrateTimeout,
+ min: 0, max: 240, step: 10,
+ format: v => v ? v+"s" :/*LANG*/"Forever",
+ onchange: v => updateSetting("vibrateTimeout", v)
+ },
+ /*LANG*/"Unread timer": {
+ value: settings.unreadTimeout,
+ min: 0, max: 240, step: 10,
+ format: v => v ? v+"s" :/*LANG*/"Off",
+ onchange: v => updateSetting("unreadTimeout", v)
+ },
+ /*LANG*/"Auto-open": toggler("autoOpen"),
+ });
+ }
+
+ function showMusicMenu() {
+ E.showMenu({
+ "": {"title": /*LANG*/"Music"},
+ "< Back": () => showMainMenu(),
+ /*LANG*/"Auto-open": toggler("openMusic"),
+ /*LANG*/"Always visible": toggler("alwaysShowMusic"),
+ /*LANG*/"Buttons": toggler("musicButtons"),
+ /*LANG*/"Show album": toggler("showAlbum"),
+ });
+ }
+
+ function showWidMenu() {
+ E.showMenu({
+ "": {"title": /*LANG*/"Widget"},
+ "< Back": () => showMainMenu(),
+ /*LANG*/"Flash icon": toggler("flash"),
+ // /*LANG*/"Show Read": toggler("showRead"),
+ });
+ }
+
+ function showUtilsMenu() {
+ let m = E.showMenu({
+ "": {"title": /*LANG*/"Utilities"},
+ "< Back": () => showMainMenu(),
+ /*LANG*/"Delete all": () => {
+ E.showPrompt(/*LANG*/"Are you sure?",
+ {title:/*LANG*/"Delete All Messages"})
+ .then(isYes => {
+ if (isYes) require("messages").write([]);
+ showUtilsMenu();
+ });
+ }
+ });
+ }
+
+ function showMainMenu() {
+ E.showMenu({
+ "": {"title": inApp ?/*LANG*/"Settings" :/*LANG*/"Messages"},
+ "< Back": back,
+ /*LANG*/"Interface": () => showIfMenu(),
+ /*LANG*/"Behaviour": () => showBMenu(),
+ /*LANG*/"Music": () => showMusicMenu(),
+ /*LANG*/"Widget": () => showWidMenu(),
+ /*LANG*/"Utils": () => showUtilsMenu(),
+ });
+ }
+
+ showMainMenu();
+});
diff --git a/apps/messages/ChangeLog b/apps/messages/ChangeLog
index 77334c54d..7d3414c1a 100644
--- a/apps/messages/ChangeLog
+++ b/apps/messages/ChangeLog
@@ -1,55 +1,4 @@
-0.01: New App!
-0.02: Add 'messages' library
-0.03: Fixes for Bangle.js 1
-0.04: Add require("messages").clearAll()
-0.05: Handling of message actions (ok/clear)
-0.06: New messages now go at the start (fix #898)
- Answering true/false now exits the messages app if no new messages
- Back now marks a message as read
- Clicking top-left opens a menu which allows you to delete a message or mark unread
-0.07: Added settings menu with option to choose vibrate pattern and frequency (fix #909)
-0.08: Fix rendering of long messages (fix #969)
- buzz on new message (fix #999)
-0.09: Message now disappears after 60s if no action taken and clock loads (fix 922)
- Fix phone icon (#1014)
-0.10: Respect the 'new' attribute if it was set from iOS integrations
-0.11: Open app when touching the widget (Bangle.js 2 only)
-0.12: Extra app-specific notification icons
- New animated notification icon (instead of large blinking 'MESSAGES')
- Added screenshots
-0.13: Add /*LANG*/ comments for internationalisation
- Add 'Delete All' option to message options
- Now update correctly when 'require("messages").clearAll()' is called
-0.14: Hide widget when all unread notifications are dismissed from phone
-0.15: Don't buzz when Quiet Mode is active
-0.16: Fix text wrapping so it fits the screen even if title is big (fix #1147)
-0.17: Fix: Get dynamic dimensions of notify icon, fixed notification font
-0.18: Use app-specific icon colors
- Spread message action buttons out
- Back button now goes back to list of messages
- If showMessage called with no message (eg all messages deleted) now return to the clock (fix #1267)
-0.19: Use a larger font for message text if it'll fit
-0.20: Allow tapping on the body to show a scrollable view of the message and title in a bigger font (fix #1405, #1031)
-0.21: Improve list readability on dark theme
-0.22: Add Home Assistant icon
- Allow repeat to be switched Off, so there is no buzzing repetition.
- Also gave the widget a pixel more room to the right
-0.23: Change message colors to match current theme instead of using green
- Now attempt to use Large/Big/Medium fonts, and allow minimum font size to be configured
-0.24: Remove left-over debug statement
-0.25: Fix widget memory usage issues if message received and watch repeatedly calls Bangle.drawWidgets (fix #1550)
-0.26: Setting to auto-open music
-0.27: Add 'mark all read' option to popup menu (fix #1624)
-0.28: Option to auto-unlock the watch when a new message arrives
-0.29: Fix message list overwrites on Bangle.js 1 (fix #1642)
-0.30: Add new Icons (Youtube, Twitch, MS TODO, Teams, Snapchat, Signal, Post & DHL, Nina, Lieferando, Kalender, Discord, Corona Warn, Bibel)
-0.31: Option to disable icon flashing
-0.32: Added an option to allow quiet mode to override message auto-open
-0.33: Timeout from the message list screen if the message being displayed is removed and there is a timer going
-0.34: Don't buzz for 'map' update messages
-0.35: Reset graphics colors before rendering a message (possibly fix #1752)
-0.36: Ensure a new message plus an almost immediate deletion of that message doesn't load the messages app (fix #1362)
-0.37: Now use the setUI 'back' icon in the top left rather than specific buttons/menu items
-0.38: Add telegram foss handling
-0.39: Set default color for message icons according to theme
-0.40: Use default Bangle formatter for booleans
+0.55: Moved messages library into standalone library
+0.56: Fix handling of music messages
+0.57: Optimize saving empty message list
+0.58: show/hide "messages" widget directly, instead of through library stub
diff --git a/apps/messages/README.md b/apps/messages/README.md
index da2701f35..83524d7c8 100644
--- a/apps/messages/README.md
+++ b/apps/messages/README.md
@@ -1,61 +1,59 @@
-# Messages app
+# Messages library
-This app handles the display of messages and message notifications. It stores
-a list of currently received messages and allows them to be listed, viewed,
-and responded to.
+This library handles the passing of messages. It can storess a list of messages
+and allows them to be retrieved by other apps.
-It is a replacement for the old `notify`/`gadgetbridge` apps.
+## Example
-## Settings
+Assuming you are using GadgetBridge and "overlay notifications":
-You can change settings by going to the global `Settings` app, then `App Settings`
-and `Messages`:
+1. Gadgetbridge sends an event to your watch for an incoming message
+2. The `android` app parses the message, and calls `require("messages").pushMessage({/** the message */})`
+3. `require("messages")` calls `Bangle.emit("message", "text", {/** the message */})`
+4. Overlay Notifications shows the message in an overlay, and marks it as `handled`
+5. The default UI app (Message UI, `messagegui`) sees the event is marked as `handled`, so does nothing.
+6. The default widget (`widmessages`) does nothing with `handled`, and shows a notification icon.
+7. You tap the notification, in order to open the full GUI: Overlay Notifications
+ calls `require("messages").openGUI({/** the message */})`
+8. `openGUI` calls `require("messagegui").open(/** copy of the message */)`.
+9. The `messagegui` library loads the Message UI app.
-* `Vibrate` - This is the pattern of buzzes that should be made when a new message is received
-* `Repeat` - How often should buzzes repeat - the default of 4 means the Bangle will buzz every 4 seconds
-* `Unread Timer` - When a new message is received we go into the Messages app.
-If there is no user input for this amount of time then the app will exit and return
-to the clock where a ringing bell will be shown in the Widget bar.
-* `Min Font` - The minimum font size used when displaying messages on the screen. A bigger font
-is chosen if there isn't much message text, but this specifies the smallest the font should get before
-it starts getting clipped.
-* `Auto-Open Music` - Should the app automatically open when the phone starts playing music?
-* `Unlock Watch` - Should the app unlock the watch when a new message arrives, so you can touch the buttons at the bottom of the app?
-* `Flash Icon` - Toggle flashing of the widget icon.
-## New Messages
-When a new message is received:
+## Events
-* If you're in an app, the Bangle will buzz and a 'new message' icon appears in the Widget bar. You can tap this bar to view the message.
-* If you're in a clock, the Messages app will automatically start and show the message
+When a new message arrives, a `"message"` event is emitted, you can listen for
+it like this:
-When a message is shown, you'll see a screen showing the message title and text.
+```js
+myMessageListener = Bangle.on("message", (type, message)=>{
+ if (message.handled) return; // another app already handled this message
+ // is one of "text", "call", "alarm", "map", or "music"
+ // see `messages/lib.js` for possible formats
+ // message.t could be "add", "modify" or "remove"
+ E.showMessage(`${message.title}\n${message.body}`, `${message.t} ${type} message`);
+ // You can prevent the default `message` app from loading by setting `message.handled = true`:
+ message.handled = true;
+});
+```
-* The 'back-arrow' button (or physical button on Bangle.js 2) goes back to Messages, marking the current message as read.
-* The top-left icon shows more options, for instance deleting the message of marking unread
-* On Bangle.js 2 you can tap on the message body to view a scrollable version of the title and text (or can use the top-left icon + `View Message`)
-* If shown, the 'tick' button:
- * **Android** opens the notification on the phone
- * **iOS** responds positively to the notification (accept call/etc)
-* If shown, the 'cross' button:
- * **Android** dismisses the notification on the phone
- * **iOS** responds negatively to the notification (dismiss call/etc)
+Apps can launch the full GUI by calling `require("messages").openGUI()`, if you
+want to write your own GUI, it should include boot code that listens for
+`"messageGUI"` events:
-## Images
-_1. Screenshot of a notification_
-
-
-
-_2. What the notify icon looks like (it's touchable on Bangle.js2!)_
-
-
+```js
+Bangle.on("messageGUI", message=>{
+ if (message.handled) return; // another app already opened it's GUI
+ message.handled = true; // prevent other apps form launching
+ Bangle.load("my_message_gui.app.js");
+})
+```
## Requests
-Please file any issues on https://github.com/espruino/BangleApps/issues/new?title=messages%20app
+Please file any issues on https://github.com/espruino/BangleApps/issues/new?title=[messages]%20library
## Creator
diff --git a/apps/messages/TEST_ME.txt b/apps/messages/TEST_ME.txt
new file mode 100644
index 000000000..8ce50d8b6
--- /dev/null
+++ b/apps/messages/TEST_ME.txt
@@ -0,0 +1,7 @@
+We need automated tests for this. Specifically:
+
+
+* send notification in clock with fast load -> messagesgui appears
+* send notification in clock without fast load -> messagesgui appears
+* send notification and delete notification quick -> messagesgui doesn't load
+* music?
diff --git a/apps/messages/lib.js b/apps/messages/lib.js
index 3f801e101..f301a91cd 100644
--- a/apps/messages/lib.js
+++ b/apps/messages/lib.js
@@ -1,187 +1,234 @@
-function openMusic() {
- // only read settings file for first music message
- if ("undefined"==typeof exports._openMusic) {
- exports._openMusic = !!((require('Storage').readJSON("messages.settings.json", true) || {}).openMusic);
- }
- return exports._openMusic;
+exports.music = {};
+/**
+ * Emit "message" event with appropriate type from Bangle
+ * @param {object} msg
+ */
+function emit(msg) {
+ let type = "text";
+ if (["call", "music", "map"].includes(msg.id)) type = msg.id;
+ if (msg.src && msg.src.toLowerCase().startsWith("alarm")) type = "alarm";
+ Bangle.emit("message", type, msg);
}
+
/* Push a new message onto messages queue, event is:
{t:"add",id:int, src,title,subject,body,sender,tel, important:bool, new:bool}
{t:"add",id:int, id:"music", state, artist, track, etc} // add new
- {t:"remove-",id:int} // remove
+ {t:"remove",id:int} // remove
{t:"modify",id:int, title:string} // modified
*/
exports.pushMessage = function(event) {
- var messages, inApp = "undefined"!=typeof MESSAGES;
- if (inApp)
- messages = MESSAGES; // we're in an app that has already loaded messages
- else // no app - load messages
- messages = require("Storage").readJSON("messages.json",1)||[];
// now modify/delete as appropriate
- var mIdx = messages.findIndex(m=>m.id==event.id);
- if (event.t=="remove") {
- if (mIdx>=0) messages.splice(mIdx, 1); // remove item
- mIdx=-1;
+ if (event.t==="remove") {
+ if (event.id==="music") exports.music = {};
} else { // add/modify
- if (event.t=="add"){
- if(event.new === undefined ) { // If 'new' has not been set yet, set it
- event.new=true; // Assume it should be new
- }
+ if (event.t==="add") {
+ if (event.new===undefined) event.new = true; // Assume it should be new
+ } else if (event.t==="modify") {
+ const old = exports.getMessages().find(m => m.id===event.id);
+ if (old) event = Object.assign(old, event);
}
- if (mIdx<0) {
- mIdx=0;
- messages.unshift(event); // add new messages to the beginning
- }
- else Object.assign(messages[mIdx], event);
- if (event.id=="music" && messages[mIdx].state=="play") {
- messages[mIdx].new = true; // new track, or playback (re)started
- }
- }
- require("Storage").writeJSON("messages.json",messages);
- // if in app, process immediately
- if (inApp) return onMessagesModified(mIdx<0 ? {id:event.id} : messages[mIdx]);
- // if we've removed the last new message, hide the widget
- if (event.t=="remove" && !messages.some(m=>m.new)) {
- if (global.WIDGETS && WIDGETS.messages) WIDGETS.messages.hide();
- // if no new messages now, make sure we don't load the messages app
- if (exports.messageTimeout && !messages.some(m=>m.new))
- clearTimeout(exports.messageTimeout);
- }
- // ok, saved now
- if (event.id=="music" && Bangle.CLOCK && messages[mIdx].new && openMusic()) {
- // just load the app to display music: no buzzing
- load("messages.app.js");
- } else if (event.t!="add") {
- // we only care if it's new
- return;
- } else if(event.new == false) {
- return;
- }
- // otherwise load messages/show widget
- var loadMessages = Bangle.CLOCK || event.important;
- var quiet = (require('Storage').readJSON('setting.json',1)||{}).quiet;
- var appSettings = require('Storage').readJSON('messages.settings.json',1)||{};
- var unlockWatch = appSettings.unlockWatch;
- var quietNoAutOpn = appSettings.quietNoAutOpn;
- delete appSettings;
- // don't auto-open messages in quiet mode if quietNoAutOpn is true
- if(quiet && quietNoAutOpn) {
- loadMessages = false;
- }
- // first, buzz
- if (!quiet && loadMessages && global.WIDGETS && WIDGETS.messages){
- WIDGETS.messages.buzz();
- if(unlockWatch != false){
- Bangle.setLocked(false);
- Bangle.setLCDPower(1); // turn screen on
- }
- }
- // after a delay load the app, to ensure we have all the messages
- if (exports.messageTimeout) clearTimeout(exports.messageTimeout);
- exports.messageTimeout = setTimeout(function() {
- exports.messageTimeout = undefined;
- // if we're in a clock or it's important, go straight to messages app
- if (loadMessages){
- return load("messages.app.js");
- }
- if (!quiet && (!global.WIDGETS || !WIDGETS.messages)) return Bangle.buzz(); // no widgets - just buzz to let someone know
- WIDGETS.messages.show();
- }, 500);
-}
-/// Remove all messages
-exports.clearAll = function(event) {
- var messages, inApp = "undefined"!=typeof MESSAGES;
- if (inApp) {
- MESSAGES = [];
- messages = MESSAGES; // we're in an app that has already loaded messages
- } else // no app - empty messages
- messages = [];
- // Save all messages
- require("Storage").writeJSON("messages.json",messages);
- // update app if in app
- if (inApp) return onMessagesModified();
- // if we have a widget, update it
- if (global.WIDGETS && WIDGETS.messages)
- WIDGETS.messages.hide();
-}
-exports.getMessageImage = function(msg) {
- /*
- * icons should be 24x24px with 1bpp colors and 'Transparency to Color'
- * http://www.espruino.com/Image+Converter
- */
- if (msg.img) return atob(msg.img);
- var s = (msg.src||"").toLowerCase();
- if (s=="alarm" || s =="alarmclockreceiver") return atob("GBjBAP////8AAAAAAAACAEAHAOAefng5/5wTgcgHAOAOGHAMGDAYGBgYGBgYGBgYGBgYDhgYBxgMATAOAHAHAOADgcAB/4AAfgAAAAAAAAA=");
- if (s=="bibel") return atob("GBgBAAAAA//wD//4D//4H//4H/f4H/f4H+P4H4D4H4D4H/f4H/f4H/f4H/f4H/f4H//4H//4H//4GAAAEAAAEAAACAAAB//4AAAA");
- if (s=="calendar") return atob("GBiBAAAAAAAAAAAAAA//8B//+BgAGBgAGBgAGB//+B//+B//+B9m2B//+B//+Btm2B//+B//+Btm+B//+B//+A//8AAAAAAAAAAAAA==");
- if (s=="corona-warn") return atob("GBgBAAAAABwAAP+AAf/gA//wB/PwD/PgDzvAHzuAP8EAP8AAPAAAPMAAP8AAH8AAHzsADzuAB/PAB/PgA//wAP/gAH+AAAwAAAAA");
- if (s=="discord") return atob("GBgBAAAAAAAAAAAAAIEABwDgDP8wH//4H//4P//8P//8P//8Pjx8fhh+fzz+f//+f//+e//ePH48HwD4AgBAAAAAAAAAAAAAAAAA");
- if (s=="facebook" || s=="messenger") return atob("GBiBAAAAAAAAAAAYAAD/AAP/wAf/4A/48A/g8B/g+B/j+B/n+D/n/D8A/B8A+B+B+B/n+A/n8A/n8Afn4APnwADnAAAAAAAAAAAAAA==");
- if (s=="google home") return atob("GBiCAAAAAAAAAAAAAAAAAAAAAoAAAAAACqAAAAAAKqwAAAAAqroAAAACquqAAAAKq+qgAAAqr/qoAACqv/6qAAKq//+qgA6r///qsAqr///6sAqv///6sAqv///6sAqv///6sA6v///6sA6v///qsA6qqqqqsA6qqqqqsA6qqqqqsAP7///vwAAAAAAAAAAAAAAAAA==");
- if (s=="hangouts") return atob("FBaBAAH4AH/gD/8B//g//8P//H5n58Y+fGPnxj5+d+fmfj//4//8H//B//gH/4A/8AA+AAHAABgAAAA=");
- if (s=="home assistant") return atob("FhaBAAAAAADAAAeAAD8AAf4AD/3AfP8D7fwft/D/P8ec572zbzbNsOEhw+AfD8D8P4fw/z/D/P8P8/w/z/AAAAA=");
- if (s=="instagram") return atob("GBiBAAAAAAAAAAAAAAAAAAP/wAYAYAwAMAgAkAh+EAjDEAiBEAiBEAiBEAiBEAjDEAh+EAgAEAwAMAYAYAP/wAAAAAAAAAAAAAAAAA==");
- if (s=="kalender") return atob("GBgBBgBgBQCgff++RQCiRgBiQAACf//+QAACQAACR//iRJkiRIEiR//iRNsiRIEiRJkiR//iRIEiRIEiR//iQAACQAACf//+AAAA");
- if (s=="lieferando") return atob("GBgBABgAAH5wAP9wAf/4A//4B//4D//4H//4P/88fV8+fV4//V4//Vw/HVw4HVw4HBg4HBg4HBg4HDg4Hjw4Hj84Hj44Hj44Hj44");
- if (s=="nina") return atob("GBgBAAAABAAQCAAICAAIEAAEEgAkJAgSJBwSKRxKSj4pUn8lVP+VVP+VUgAlSgApKQBKJAASJAASEgAkEAAECAAICAAIBAAQAAAA");
- if (s=="outlook mail") return atob("HBwBAAAAAAAAAAAIAAAfwAAP/gAB/+AAP/5/A//v/D/+/8P/7/g+Pv8Dye/gPd74w5znHDnOB8Oc4Pw8nv/Dwe/8Pj7/w//v/D/+/8P/7/gf/gAA/+AAAfwAAACAAAAAAAAAAAA=");
- if (s=="phone") return atob("FxeBABgAAPgAAfAAB/AAD+AAH+AAP8AAP4AAfgAA/AAA+AAA+AAA+AAB+AAB+AAB+OAB//AB//gB//gA//AA/8AAf4AAPAA=");
- if (s=="post & dhl") return atob("GBgBAPgAE/5wMwZ8NgN8NgP4NgP4HgP4HgPwDwfgD//AB/+AAf8AAAAABs7AHcdgG4MwAAAAGESAFESAEkSAEnyAEkSAFESAGETw");
- if (s=="signal") return atob("GBgBAAAAAGwAAQGAAhggCP8QE//AB//oJ//kL//wD//0D//wT//wD//wL//0J//kB//oA//ICf8ABfxgBYBAADoABMAABAAAAAAA");
- if (s=="skype") return atob("GhoBB8AAB//AA//+Af//wH//+D///w/8D+P8Afz/DD8/j4/H4fP5/A/+f4B/n/gP5//B+fj8fj4/H8+DB/PwA/x/A/8P///B///gP//4B//8AD/+AAA+AA==");
- if (s=="slack") return atob("GBiBAAAAAAAAAABAAAHvAAHvAADvAAAPAB/PMB/veD/veB/mcAAAABzH8B3v+B3v+B3n8AHgAAHuAAHvAAHvAADGAAAAAAAAAAAAAA==");
- if (s=="snapchat") return atob("GBgBAAAAAAAAAH4AAf+AAf+AA//AA//AA//AA//AA//AH//4D//wB//gA//AB//gD//wH//4f//+P//8D//wAf+AAH4AAAAAAAAA");
- if (s=="teams") return atob("GBgBAAAAAAAAAAQAAB4AAD8IAA8cP/M+f/scf/gIeDgAfvvefvvffvvffvvffvvff/vff/veP/PeAA/cAH/AAD+AAD8AAAQAAAAA");
- if (s=="telegram" || s=="telegram foss") return atob("GBiBAAAAAAAAAAAAAAAAAwAAHwAA/wAD/wAf3gD/Pgf+fh/4/v/z/P/H/D8P/Acf/AM//AF/+AF/+AH/+ADz+ADh+ADAcAAAMAAAAA==");
- if (s=="threema") return atob("GBjB/4Yx//8AAAAAAAAAAAAAfgAB/4AD/8AH/+AH/+AP//AP2/APw/APw/AHw+AH/+AH/8AH/4AH/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=");
- if (s=="to do") return atob("GBgBAAAAAAAAAAAwAAB4AAD8AAH+AAP/DAf/Hg//Px/+f7/8///4///wf//gP//AH/+AD/8AB/4AA/wAAfgAAPAAAGAAAAAAAAAA");
- if (s=="twitch") return atob("GBgBH//+P//+P//+eAAGeAAGeAAGeDGGeDOGeDOGeDOGeDOGeDOGeDOGeAAOeAAOeAAcf4/4f5/wf7/gf//Af/+AA/AAA+AAAcAA");
- if (s=="twitter") return atob("GhYBAABgAAB+JgA/8cAf/ngH/5+B/8P8f+D///h///4f//+D///g///wD//8B//+AP//gD//wAP/8AB/+AB/+AH//AAf/AAAYAAA");
- if (s=="whatsapp") return atob("GBiBAAB+AAP/wAf/4A//8B//+D///H9//n5//nw//vw///x///5///4///8e//+EP3/APn/wPn/+/j///H//+H//8H//4H//wMB+AA==");
- if (s=="wordfeud") return atob("GBgCWqqqqqqlf//////9v//////+v/////++v/////++v8///Lu+v8///L++v8///P/+v8v//P/+v9v//P/+v+fx/P/+v+Pk+P/+v/PN+f/+v/POuv/+v/Ofdv/+v/NvM//+v/I/Y//+v/k/k//+v/i/w//+v/7/6//+v//////+v//////+f//////9Wqqqqqql");
- if (s=="youtube") return atob("GBgBAAAAAAAAAAAAAAAAAf8AH//4P//4P//8P//8P5/8P4/8f4P8f4P8P4/8P5/8P//8P//8P//4H//4Af8AAAAAAAAAAAAAAAAA");
- if (msg.id=="music") return atob("FhaBAH//+/////////////h/+AH/4Af/gB/+H3/7/f/v9/+/3/7+f/vB/w8H+Dwf4PD/x/////////////3//+A=");
- // if (s=="sms message" || s=="mail" || s=="gmail") // .. default icon (below)
- return atob("HBKBAD///8H///iP//8cf//j4//8f5//j/x/8//j/H//H4//4PB//EYj/44HH/Hw+P4//8fH//44///xH///g////A==");
+ // combine musicinfo and musicstate events
+ if (event.id==="music") {
+ if (event.state==="play") event.new = true; // new track, or playback (re)started
+ event = Object.assign(exports.music, event);
+ }
+ }
+ // reset state (just in case)
+ delete event.handled;
+ delete event.saved;
+ emit(event);
};
-exports.getMessageImageCol = function(msg,def) {
- return {
- // generic colors, using B2-safe colors
- "alarm": "#fff",
- "mail": "#ff0",
- "music": "#f0f",
- "phone": "#0f0",
- "sms message": "#0ff",
- // brands, according to https://www.schemecolor.com/?s (picking one for multicolored logos)
- // all dithered on B2, but we only use the color for the icons. (Could maybe pick the closest 3-bit color for B2?)
- "bibel": "#54342c",
- "discord": "#738adb",
- "facebook": "#4267b2",
- "gmail": "#ea4335",
- "google home": "#fbbc05",
- "hangouts": "#1ba261",
- "home assistant": "#fff", // ha-blue is #41bdf5, but that's the background
- "instagram": "#dd2a7b",
- "liferando": "#ee5c00",
- "messenger": "#0078ff",
- "nina": "#e57004",
- "outlook mail": "#0072c6",
- "post & dhl": "#f2c101",
- "signal": "#00f",
- "skype": "#00aff0",
- "slack": "#e51670",
- "snapchat": "#ff0",
- "teams": "#464eb8",
- "telegram": "#0088cc",
- "telegram foss": "#0088cc",
- "threema": "#000",
- "to do": "#3999e5",
- "twitch": "#6441A4",
- "twitter": "#1da1f2",
- "whatsapp": "#4fce5d",
- "wordfeud": "#e7d3c7",
- "youtube": "#f00",
- }[(msg.src||"").toLowerCase()]||(def !== undefined?def:g.theme.fg);
+/**
+ * Save a single message to flash
+ * Also sets msg.saved=true
+ *
+ * @param {object} msg
+ * @param {object} [options={}] Options:
+ * {boolean} [force=false] Force save even if msg.saved is already set
+ */
+exports.save = function(msg, options) {
+ if (!options) options = {};
+ if (msg.saved && !options.force) return; //already saved
+ let messages = exports.getMessages();
+ exports.apply(msg, messages);
+ exports.write(messages);
+ msg.saved = true;
+};
+
+/**
+ * Apply incoming event to array of messages
+ *
+ * @param {object} event Event to apply
+ * @param {array} messages Array of messages, *will be modified in-place*
+ * @return {array} Modified messages array
+ */
+exports.apply = function(event, messages) {
+ if (!event || !event.id) return messages;
+ const mIdx = messages.findIndex(m => m.id===event.id);
+ if (event.t==="remove") {
+ if (mIdx<0) return messages; // already gone -> nothing to do
+ messages.splice(mIdx, 1);
+ } else if (event.t==="add") {
+ if (mIdx>=0) messages.splice(mIdx, 1); // duplicate ID! erase previous version
+ messages.unshift(event); // add at the beginning
+ } else if (event.t==="modify") {
+ if (mIdx>=0) messages[mIdx] = Object.assign(messages[mIdx], event);
+ else messages.unshift(event);
+ }
+ return messages;
+};
+
+/**
+ * Accept a call (or other acceptable event)
+ * @param {object} msg
+ */
+exports.accept = function(msg) {
+ if (msg.positive) Bangle.messageResponse(msg, true);
+};
+
+/**
+ * Dismiss a message (if applicable), and erase it from flash
+ * Emits a "message" event with t="remove", only if message existed
+ *
+ * @param {object} msg
+ */
+exports.dismiss = function(msg) {
+ if (msg.negative) Bangle.messageResponse(msg, false);
+ let messages = exports.getMessages();
+ const mIdx = messages.findIndex(m=>m.id===msg.id);
+ if (mIdx<0) return;
+ messages.splice(mIdx, 1);
+ exports.write(messages);
+ if (msg.t==="remove") return; // already removed, don't re-emit
+ msg.t = "remove";
+ emit(msg); // emit t="remove", so e.g. widgets know to update
+};
+
+/**
+ * Emit a "type=openGUI" event, to open GUI app
+ *
+ * @param {object} [msg={}] Message the app should show
+ */
+exports.openGUI = function(msg) {
+ if (!require("Storage").read("messagegui")) return; // "messagegui" module is missing!
+ // Mark the event as unhandled for GUI, but leave passed arguments intact
+ let copy = Object.assign({}, msg);
+ delete copy.handled;
+ require("messagegui").open(copy);
+};
+
+/**
+ * Show/hide the messages widget
+ *
+ * @param {boolean} show
+ */
+exports.toggleWidget = function(show) {
+ if (!global.WIDGETS || !WIDGETS["messages"]) return; // widget is missing!
+ const method = WIDGETS["messages"][show ? "show" : "hide"];
+ /* if (typeof(method)!=="function") return; // widget must always have show()+hide(), fail hard rather than hide problems */
+ method.apply(WIDGETS["messages"]);
+};
+
+/**
+ * Replace all stored messages
+ * @param {array} messages Messages to save
+ */
+exports.write = function(messages) {
+ if (!messages.length) require("Storage").erase("messages.json");
+ else require("Storage").writeJSON("messages.json", messages.map(m => {
+ // we never want to save saved/handled status to file;
+ delete m.saved;
+ delete m.handled;
+ return m;
+ }));
+};
+/**
+ * Erase all messages
+ */
+exports.clearAll = function() {
+ exports.write([]);
+ Bangle.emit("message", "clearAll", {});
+}
+
+/**
+ * Get saved messages
+ *
+ * Optionally pass in a message to apply to the list, this is for event handlers:
+ * By passing the message from the event, you can make sure the list is up-to-date,
+ * even if the message has not been saved (yet)
+ *
+ * Example:
+ * Bangle.on("message", (type, msg) => {
+ * console.log("All messages:", require("messages").getMessages(msg));
+ * });
+ *
+ * @param {object} [withMessage] Apply this event to messages
+ * @returns {array} All messages
+ */
+exports.getMessages = function(withMessage) {
+ let messages = require("Storage").readJSON("messages.json", true);
+ messages = Array.isArray(messages) ? messages : []; // make sure we always return an array
+ if (withMessage && withMessage.id) exports.apply(withMessage, messages);
+ return messages;
+};
+
+/**
+ * Check if there are any messages
+ *
+ * @param {object} [withMessage] Apply this event to messages, see getMessages
+ * @returns {string} "new"/"old"/"none"
+ */
+exports.status = function(withMessage) {
+ try {
+ let status = "none";
+ for(const m of exports.getMessages(withMessage)) {
+ if (["music", "map"].includes(m.id)) continue;
+ if (m.new) return "new";
+ status = "old";
+ }
+ return status;
+ } catch(e) {
+ return "none"; // don't bother callers with errors
+ }
+};
+
+/**
+ * Start buzzing for new message
+ * @param {string} msgSrc Message src to buzz for
+ * @return {Promise} Resolves when initial buzz finishes (there might be repeat buzzes later)
+ */
+exports.buzz = function(msgSrc) {
+ exports.stopBuzz(); // cancel any previous buzz timeouts
+ if ((require("Storage").readJSON("setting.json", 1) || {}).quiet) return Promise.resolve(); // never buzz during Quiet Mode
+ const msgSettings = require("Storage").readJSON("messages.settings.json", true) || {};
+ let pattern;
+ if (msgSrc && msgSrc.toLowerCase()==="phone") {
+ // special vibration pattern for incoming calls
+ pattern = msgSettings.vibrateCalls;
+ } else {
+ pattern = msgSettings.vibrate;
+ }
+ if (pattern===undefined) { pattern = ":"; } // pattern may be "", so we can't use || ":" here
+ if (!pattern) return Promise.resolve();
+
+ let repeat = msgSettings.repeat;
+ if (repeat===undefined) repeat = 4; // repeat may be zero
+ if (repeat) {
+ exports.buzzTimeout = setTimeout(() => require("buzz").pattern(pattern), repeat*1000);
+ let vibrateTimeout = msgSettings.vibrateTimeout;
+ if (vibrateTimeout===undefined) vibrateTimeout = 60;
+ if (vibrateTimeout && !exports.stopTimeout) exports.stopTimeout = setTimeout(exports.stopBuzz, vibrateTimeout*1000);
+ }
+ return require("buzz").pattern(pattern);
+};
+/**
+ * Stop buzzing
+ */
+exports.stopBuzz = function() {
+ if (exports.buzzTimeout) clearTimeout(exports.buzzTimeout);
+ delete exports.buzzTimeout;
+ if (exports.stopTimeout) clearTimeout(exports.stopTimeout);
+ delete exports.stopTimeout;
};
diff --git a/apps/messages/metadata.json b/apps/messages/metadata.json
index b30d31705..9c7c8b49e 100644
--- a/apps/messages/metadata.json
+++ b/apps/messages/metadata.json
@@ -1,21 +1,22 @@
{
"id": "messages",
"name": "Messages",
- "version": "0.40",
- "description": "App to display notifications from iOS and Gadgetbridge/Android",
+ "version": "0.58",
+ "description": "Library to handle, load and store message events received from Android/iOS",
"icon": "app.png",
- "type": "app",
+ "type": "module",
"tags": "tool,system",
"supports": ["BANGLEJS","BANGLEJS2"],
+ "provides_modules" : ["messages"],
+ "dependencies" : {
+ "messagegui":"module",
+ "message":"widget"
+ },
+ "default": true,
"readme": "README.md",
"storage": [
- {"name":"messages.app.js","url":"app.js"},
- {"name":"messages.settings.js","url":"settings.js"},
- {"name":"messages.img","url":"app-icon.js","evaluate":true},
- {"name":"messages.wid.js","url":"widget.js"},
- {"name":"messages","url":"lib.js"}
+ {"name":"messages","url":"lib.js"},
+ {"name":"messages.settings.js","url":"settings.js"}
],
- "data": [{"name":"messages.json"},{"name":"messages.settings.json"}],
- "screenshots": [{"url":"screenshot.png"},{"url":"screenshot-notify.gif"}],
- "sortorder": -9
+ "data": [{"name":"messages.json"},{"name":"messages.settings.json"}]
}
diff --git a/apps/messages/screenshot-notify.gif b/apps/messages/screenshot-notify.gif
deleted file mode 100644
index 3d0ed0b32..000000000
Binary files a/apps/messages/screenshot-notify.gif and /dev/null differ
diff --git a/apps/messages/settings.js b/apps/messages/settings.js
index b708213be..09c9db455 100644
--- a/apps/messages/settings.js
+++ b/apps/messages/settings.js
@@ -1,9 +1,15 @@
(function(back) {
+ const iconColorModes = ['color', 'mono'];
+
function settings() {
let settings = require('Storage').readJSON("messages.settings.json", true) || {};
if (settings.vibrate===undefined) settings.vibrate=":";
+ if (settings.vibrateCalls===undefined) settings.vibrateCalls=":";
if (settings.repeat===undefined) settings.repeat=4;
+ if (settings.vibrateTimeout===undefined) settings.vibrateTimeout=60;
if (settings.unreadTimeout===undefined) settings.unreadTimeout=60;
+ if (settings.maxMessages===undefined) settings.maxMessages=3;
+ if (settings.iconColorMode === undefined) settings.iconColorMode = iconColorModes[0];
settings.unlockWatch=!!settings.unlockWatch;
settings.openMusic=!!settings.openMusic;
settings.maxUnreadTimeout=240;
@@ -20,12 +26,19 @@
"" : { "title" : /*LANG*/"Messages" },
"< Back" : back,
/*LANG*/'Vibrate': require("buzz_menu").pattern(settings().vibrate, v => updateSetting("vibrate", v)),
+ /*LANG*/'Vibrate for calls': require("buzz_menu").pattern(settings().vibrateCalls, v => updateSetting("vibrateCalls", v)),
/*LANG*/'Repeat': {
value: settings().repeat,
min: 0, max: 10,
format: v => v?v+"s":/*LANG*/"Off",
onchange: v => updateSetting("repeat", v)
},
+ /*LANG*/'Vibrate timer': {
+ value: settings().vibrateTimeout,
+ min: 0, max: settings().maxUnreadTimeout, step : 10,
+ format: v => v?v+"s":/*LANG*/"Off",
+ onchange: v => updateSetting("vibrateTimeout", v)
+ },
/*LANG*/'Unread timer': {
value: settings().unreadTimeout,
min: 0, max: settings().maxUnreadTimeout, step : 10,
@@ -54,6 +67,22 @@
value: !!settings().quietNoAutOpn,
onchange: v => updateSetting("quietNoAutOpn", v)
},
+ /*LANG*/'Disable auto-open': {
+ value: !!settings().noAutOpn,
+ onchange: v => updateSetting("noAutOpn", v)
+ },
+ /*LANG*/'Widget messages': {
+ value:0|settings().maxMessages,
+ min: 0, max: 5,
+ format: v => v ? v :/*LANG*/"Hide",
+ onchange: v => updateSetting("maxMessages", v)
+ },
+ /*LANG*/'Icon color mode': {
+ value: Math.max(0,iconColorModes.indexOf(settings().iconColorMode)),
+ min: 0, max: iconColorModes.length - 1,
+ format: v => iconColorModes[v],
+ onchange: v => updateSetting("iconColorMode", iconColorModes[v])
+ }
};
E.showMenu(mainmenu);
-})
+});
diff --git a/apps/messages/widget.js b/apps/messages/widget.js
deleted file mode 100644
index 25573220f..000000000
--- a/apps/messages/widget.js
+++ /dev/null
@@ -1,49 +0,0 @@
-WIDGETS["messages"]={area:"tl", width:0, iconwidth:24,
-draw:function(recall) {
- // If we had a setTimeout queued from the last time we were called, remove it
- if (WIDGETS["messages"].i) {
- clearTimeout(WIDGETS["messages"].i);
- delete WIDGETS["messages"].i;
- }
- Bangle.removeListener('touch', this.touch);
- if (!this.width) return;
- var c = (Date.now()-this.t)/1000;
- let settings = require('Storage').readJSON("messages.settings.json", true) || {};
- if (settings.flash===undefined) settings.flash = true;
- if (recall !== true || settings.flash) {
- g.reset().clearRect(this.x, this.y, this.x+this.width, this.y+23);
- g.drawImage(settings.flash && (c&1) ? atob("GBiBAAAAAAAAAAAAAAAAAAAAAB//+DAADDAADDAADDwAPD8A/DOBzDDn/DA//DAHvDAPvjAPvjAPvjAPvh///gf/vAAD+AAB8AAAAA==") : atob("GBiBAAAAAAAAAAAAAAAAAAAAAB//+D///D///A//8CP/xDj/HD48DD+B8D/D+D/3vD/vvj/vvj/vvj/vvh/v/gfnvAAD+AAB8AAAAA=="), this.x, this.y-1);
- }
- if (settings.repeat===undefined) settings.repeat = 4;
- if (c<120 && (Date.now()-this.l)>settings.repeat*1000) {
- this.l = Date.now();
- WIDGETS["messages"].buzz(); // buzz every 4 seconds
- }
- WIDGETS["messages"].i=setTimeout(()=>WIDGETS["messages"].draw(true), 1000);
- if (process.env.HWVERSION>1) Bangle.on('touch', this.touch);
-},show:function(quiet) {
- WIDGETS["messages"].t=Date.now(); // first time
- WIDGETS["messages"].l=Date.now()-10000; // last buzz
- if (quiet) WIDGETS["messages"].t -= 500000; // if quiet, set last time in the past so there is no buzzing
- WIDGETS["messages"].width=this.iconwidth;
- Bangle.drawWidgets();
-},hide:function() {
- delete WIDGETS["messages"].t;
- delete WIDGETS["messages"].l;
- WIDGETS["messages"].width=0;
- Bangle.drawWidgets();
-},buzz:function() {
- if ((require('Storage').readJSON('setting.json',1)||{}).quiet) return; // never buzz during Quiet Mode
- require("buzz").pattern((require('Storage').readJSON("messages.settings.json", true) || {}).vibrate || ":");
-},touch:function(b,c) {
- var w=WIDGETS["messages"];
- if (!w||!w.width||c.xw.x+w.width||c.yw.y+w.iconwidth) return;
- load("messages.app.js");
-}};
-/* We might have returned here if we were in the Messages app for a
-message but then the watch was never viewed. In that case we don't
-want to buzz but should still show that there are unread messages. */
-if (global.MESSAGES===undefined) (function() {
- var messages = require("Storage").readJSON("messages.json",1)||[];
- if (messages.some(m=>m.new&&m.id!="music")) WIDGETS["messages"].show(true);
-})();
diff --git a/apps/messages_light/ChangeLog b/apps/messages_light/ChangeLog
new file mode 100644
index 000000000..328e2a120
--- /dev/null
+++ b/apps/messages_light/ChangeLog
@@ -0,0 +1,7 @@
+1.0: New App!
+1.1: fix app opening when a remove notification arrives
+1.2: message_light overrides require() by sending requests to "message" to a proxy library which overrides pushMessage
+ settings now points to message settings
+ implemented use of the "messageicons" library
+ removed lib no longer used
+1.3: icon changed
\ No newline at end of file
diff --git a/apps/messages_light/README.md b/apps/messages_light/README.md
new file mode 100644
index 000000000..00fe39bd0
--- /dev/null
+++ b/apps/messages_light/README.md
@@ -0,0 +1,11 @@
+# Messages app
+
+This app handles the display of messages and message notifications.
+
+It is a GUI replacement for the `messages` apps.
+
+
+## Creator
+
+Rarder44
+
diff --git a/apps/messages_light/app-icon.js b/apps/messages_light/app-icon.js
new file mode 100644
index 000000000..7d1da35c9
--- /dev/null
+++ b/apps/messages_light/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEw4UA/4ACBIMQwhL/ABMBqoAEoALDioLFqgLDBQoABERIkEBZcFBY9QBYVe1QAB1YLGrSlC/YLGrYHCr4Lrr9drpLC1oLEAAN5rxKB/ILHEYV5EY4LIHYoLorRaBqoPCBYlfUoXrBYwGBrdeDIILIvXVBZFa1I+CBY/5BZIHBBwOq1ILGrXVvf//oLGq+trLLFBYVVvQxCBY9XJIQLCgILDHoVVoALHAAQLCgALHBQUAioKFqgLDEgwiDAH4AGA"))
\ No newline at end of file
diff --git a/apps/messages_light/app-icon.png b/apps/messages_light/app-icon.png
new file mode 100644
index 000000000..c9b4b62ac
Binary files /dev/null and b/apps/messages_light/app-icon.png differ
diff --git a/apps/messages_light/app.png b/apps/messages_light/app.png
new file mode 100644
index 000000000..1f738504d
Binary files /dev/null and b/apps/messages_light/app.png differ
diff --git a/apps/messages_light/full-size-app.png b/apps/messages_light/full-size-app.png
new file mode 100644
index 000000000..2df7915ed
Binary files /dev/null and b/apps/messages_light/full-size-app.png differ
diff --git a/apps/messages_light/messages_light.app.js b/apps/messages_light/messages_light.app.js
new file mode 100644
index 000000000..5d5363d38
--- /dev/null
+++ b/apps/messages_light/messages_light.app.js
@@ -0,0 +1,496 @@
+/* MESSAGES is a list of:
+ {id:int,
+ src,
+ title,
+ subject,
+ body,
+ sender,
+ tel:string,
+ new:true // not read yet
+ }
+*/
+
+let LOG=function(){
+ //print.apply(null, arguments);
+}
+
+
+
+
+let settings= (()=>{
+ let tmp={};
+ tmp.NewEventFileName="messages_light.NewEvent.json";
+
+ tmp.fontSmall = "6x8";
+ tmp.fontMedium = g.getFonts().includes("Vector")?"Vector:16":"6x8:2";
+ tmp.fontBig = g.getFonts().includes("12x20")?"12x20":"6x8:2";
+ tmp.fontLarge = g.getFonts().includes("6x15")?"6x15:2":"6x8:4";
+
+
+ tmp.colHeadBg = g.theme.dark ? "#141":"#4f4";
+ tmp.colBg = g.theme.dark ? "#000":"#fff";
+ tmp.colLock = g.theme.dark ? "#ff0000":"#ff0000";
+
+ tmp.quiet=((require('Storage').readJSON('setting.json', 1) || {}).quiet)
+
+ return tmp;
+})();
+let EventQueue=[]; //in posizione 0, c'è quello attualmente visualizzato
+let callInProgress=false;
+
+
+
+
+//TODO: RICORDARSI DI FARE IL DELETE
+var manageEvent = function(event) {
+
+ event.new=true;
+
+
+ LOG("manageEvent");
+ if( event.id=="call")
+ {
+ showCall(event);
+ return;
+ }
+ switch(event.t)
+ {
+ case "add":
+ EventQueue.unshift(event);
+
+ if(!callInProgress)
+ showMessage(event);
+ break;
+
+ case "modify":
+ //cerco l'evento nella lista, se lo trovo, lo modifico, altrimenti lo pusho
+ let find=false;
+ EventQueue.forEach(element => {
+ if(element.id == event.id)
+ {
+ find=true;
+ Object.assign(element,event);
+ }
+ });
+ if(!find) //se non l'ho trovato, lo aggiungo in fondo
+ EventQueue.unshift(event);
+
+ if(!callInProgress)
+ showMessage(event);
+ break;
+
+ case "remove":
+
+ //se non c'è niente nella queue e non c'è una chiamata in corso
+ if( EventQueue.length==0 && !callInProgress)
+ next();
+
+ //se l'id è uguale a quello attualmente visualizzato ( e non siamo in chiamata )
+ if(!callInProgress && EventQueue[0] !== undefined && EventQueue[0].id == event.id)
+ next(); //passo al messaggio successivo ( per la rimozione ci penserà la next )
+
+ else{
+ //altrimenti rimuovo tutti gli elementi con quell'id( creando un nuovo array )
+ let newEventQueue=[];
+ EventQueue.forEach(element => {
+ if(element.id != event.id)
+ newEventQueue.push(element);
+ });
+ EventQueue=newEventQueue;
+ }
+
+
+
+
+ break;
+ case "musicstate":
+ case "musicinfo":
+
+ break;
+ }
+};
+
+
+
+
+
+
+let showMessage = function(msg){
+ LOG("showMessage");
+ LOG(msg);
+ g.setBgColor(settings.colBg);
+
+
+ if(typeof msg.CanScrollDown==="undefined")
+ msg.CanScrollDown=false;
+ if(typeof msg.CanScrollUp==="undefined")
+ msg.CanScrollUp=false;
+
+
+
+
+
+ // Normal text message display
+ let title=msg.title, titleFont = settings.fontLarge, lines;
+ if (title) {
+ let w = g.getWidth()-48;
+ if (g.setFont(titleFont).stringWidth(title) > w)
+ titleFont = settings.fontMedium;
+ if (g.setFont(titleFont).stringWidth(title) > w) {
+ lines = g.wrapString(title, w);
+ title = (lines.length>2) ? lines.slice(0,2).join("\n")+"..." : lines.join("\n");
+ }
+ }
+
+
+
+ let Layout = require("Layout");
+ layout = new Layout({ type:"v", c: [
+ {type:"h", fillx:1, bgCol:settings.colHeadBg, c: [
+ { type:"btn", src:require("messageicons").getImage(msg), col:require("messageicons").getColor(msg), pad: 3},
+ { type:"v", fillx:1, c: [
+ {type:"txt", font:settings.fontSmall, label:msg.src||/*LANG*/"Message", bgCol:settings.colHeadBg, fillx:1, pad:2, halign:1 },
+ title?{type:"txt", font:titleFont, label:title, bgCol:settings.colHeadBg, fillx:1, pad:2 }:{},
+ ]},
+ ]},
+ {type:"v",fillx:1,filly:1,pad:2 ,halign:-1,c:[]},
+
+
+
+
+ ]});
+
+
+ if (!settings.quiet && msg.new)
+ {
+ msg.new=false;
+ Bangle.buzz();
+ }
+
+
+ g.clearRect(Bangle.appRect);
+ layout.render();
+
+ PrintMessageStrings(msg);
+ Bangle.setLCDPower(1);
+
+ DrawLock();
+
+};
+let DrawLock=function()
+{
+ let w=8,h=8;
+ let x = g.getWidth()-w;
+ let y = 0;
+ if(Bangle.isLocked())
+ g.setBgColor(settings.colLock);
+ else
+ g.setBgColor(settings.colHeadBg);
+ g.clearRect(x,y,x+w,y+h);
+};
+
+
+
+
+
+
+let showCall = function(msg)
+{
+ LOG("showCall");
+ LOG(msg);
+ // se anche prima era una call PrevMessage==msg.id
+ //non so perchè prima era cosi
+ if( msg.t=="remove")
+ {
+ LOG("hide call screen");
+ next(); //dont shift
+ return;
+ }
+
+ callInProgress=true;
+
+
+
+ //se è una chiamata ( o una nuova chiamata, diversa dalla precedente )
+ //la visualizzo
+
+ let title=msg.title, titleFont = settings.fontLarge, lines;
+ if (title) {
+ let w = g.getWidth()-48;
+ if (g.setFont(titleFont).stringWidth(title) > w)
+ titleFont = settings.fontMedium;
+ if (g.setFont(titleFont).stringWidth(title) > w) {
+ lines = g.wrapString(title, w);
+ title = (lines.length>2) ? lines.slice(0,2).join("\n")+"..." : lines.join("\n");
+ }
+ }
+ let Layout = require("Layout");
+ layout = new Layout({ type:"v", c: [
+ {type:"h", fillx:1, bgCol:settings.colHeadBg, c: [
+ { type:"btn", src:require("messageicons").getImage(msg), col:require("messageicons").getColor(msg), pad: 3},
+ { type:"v", fillx:1, c: [
+ {type:"txt", font:settings.fontSmall, label:msg.src||/*LANG*/"Message", bgCol:settings.colHeadBg, fillx:1, pad:2, halign:1 },
+ title?{type:"txt", font:titleFont, label:title, bgCol:settings.colHeadBg, fillx:1, pad:2 }:{},
+ ]},
+ ]},
+ {type:"txt", font:settings.fontMedium, label:msg.body, fillx:1,filly:1,pad:2 ,halign:0}
+ ]});
+
+
+ StopBuzzCall();
+ if ( !settings.quiet ) {
+ if(msg.new)
+ {
+ msg.new=false;
+ CallBuzzTimer = setInterval(function() {
+ Bangle.buzz(500);
+ }, 1000);
+
+ Bangle.buzz(500);
+ }
+ }
+ g.clearRect(Bangle.appRect);
+ layout.render();
+ PrintMessageStrings(msg);
+ Bangle.setLCDPower(1);
+ DrawLock();
+};
+
+
+
+
+
+
+
+
+
+let next=function(){
+ LOG("next");
+ StopBuzzCall();
+
+
+ //se c'è una chiamata, non shifto
+ if(!callInProgress)
+ EventQueue.shift(); //passa al messaggio successivo, se presente - tolgo il primo
+
+ callInProgress=false;
+ if( EventQueue.length == 0)
+ {
+ LOG("no element in queue - closing")
+ setTimeout(_ => load());
+ return;
+ }
+
+
+ showMessage(EventQueue[0]);
+
+};
+
+
+
+
+
+
+
+
+
+
+
+let showMapMessage=function(msg) {
+
+ g.clearRect(Bangle.appRect);
+ PrintMessageStrings({body:"Not implemented!"});
+
+}
+
+
+
+
+
+let CallBuzzTimer=null;
+let StopBuzzCall=function()
+{
+ if (CallBuzzTimer){
+ clearInterval(CallBuzzTimer);
+ CallBuzzTimer=null;
+ }
+}
+let DrawTriangleUp=function()
+{
+ g.fillPoly([169,46,164,56,174,56]);
+}
+let DrawTriangleDown=function()
+{
+ g.fillPoly([169,170,164,160,174,160]);
+}
+
+
+
+
+let ScrollUp=function()
+{
+ msg= EventQueue[0];
+
+ if(typeof msg.FirstLine==="undefined")
+ msg.FirstLine=0;
+ if(typeof msg.CanScrollUp==="undefined")
+ msg.CanScrollUp=false;
+
+ if(!msg.CanScrollUp) return;
+
+ msg.FirstLine = msg.FirstLine>0?msg.FirstLine-1:0;
+
+ PrintMessageStrings(msg);
+}
+let ScrollDown=function()
+{
+ msg= EventQueue[0];
+ if(typeof msg.FirstLine==="undefined")
+ msg.FirstLine=0;
+ if(typeof msg.CanScrollDown==="undefined")
+ msg.CanScrollDown=false;
+
+ if(!msg.CanScrollDown) return;
+
+ msg.FirstLine = msg.FirstLine+1;
+ PrintMessageStrings(msg);
+}
+
+
+
+
+
+
+let PrintMessageStrings=function(msg)
+{
+ let MyWrapString = function (str,maxWidth)
+ {
+ str=str.replace("\r\n","\n").replace("\r","\n");
+ return g.wrapString(str,maxWidth);
+ }
+
+
+ if(typeof msg.FirstLine==="undefined") msg.FirstLine=0;
+
+ let bodyFont = typeof msg.bodyFont==="undefined"? settings.fontMedium : msg.bodyFont;
+ let Padding=2;
+ if(typeof msg.lines==="undefined")
+ {
+ g.setFont(bodyFont);
+ msg.lines = MyWrapString(msg.body,g.getWidth()-(Padding*2))
+ if ( msg.lines.length<=2)
+ {
+ bodyFont= g.getFonts().includes("Vector")?"Vector:20":"6x8:3";
+ g.setFont(bodyFont);
+ msg.lines = MyWrapString(msg.body,g.getWidth()-(Padding*2))
+ msg.bodyFont = bodyFont;
+ }
+ }
+
+
+
+ //prendo le linee da stampare
+ let NumLines=8;
+ let linesToPrint = (msg.lines.length>NumLines) ? msg.lines.slice(msg.FirstLine,msg.FirstLine+NumLines):msg.lines;
+
+
+ let yText=45;
+
+ //invalido l'area e disegno il testo
+ g.setBgColor(settings.colBg);
+ g.clearRect(0,yText,176,176);
+ let xText=Padding;
+ yText+=Padding;
+ g.setFont(bodyFont);
+ let HText=g.getFontHeight();
+
+ yText=((176-yText)/2)-(linesToPrint.length * HText / 2) + yText;
+
+ if( linesToPrint.length<=2)
+ {
+ g.setFontAlign(0,-1);
+ xText = g.getWidth()/2;
+ }
+ else
+ g.setFontAlign(-1,-1);
+
+
+ linesToPrint.forEach((line, i)=>{
+ g.drawString(line,xText,yText+HText*i);
+ });
+
+ //disegno le freccie
+ if(msg.FirstLine!=0)
+ {
+ msg.CanScrollUp=true;
+ DrawTriangleUp();
+ }
+ else
+ msg.CanScrollUp=false;
+
+ if(msg.FirstLine+linesToPrint.length < msg.lines.length)
+ {
+ msg.CanScrollDown=true;
+ DrawTriangleDown();
+ }
+ else
+ msg.CanScrollDown=false;
+
+
+}
+
+
+
+
+let doubleTapUnlock=function(data) {
+ if( data.double) //solo se in double
+ {
+ Bangle.setLocked(false);
+ Bangle.setLCDPower(1);
+ }
+}
+let toushScroll=function(button, xy) {
+ let height=176; //g.getHeight(); -> 176 B2
+ height/=2;
+
+ if(xy.y next(), BTN1,{repeat: true});
+
+ //il tap è il tocco con l'accellerometro!
+ Bangle.on('tap', doubleTapUnlock);
+ Bangle.on('touch', toushScroll);
+
+ //quando apro quest'app, do per scontato che c'è un messaggio da leggere posto in un file particolare ( NewMessage.json )
+ let eventToShow = require('Storage').readJSON(settings.NewEventFileName, true);
+ require("Storage").erase(settings.NewEventFileName)
+ if( eventToShow!==undefined)
+ manageEvent(eventToShow);
+ else
+ {
+ LOG("file not found!");
+ setTimeout(_ => load(), 0);
+ }
+};
+
+
+
+
+main();
\ No newline at end of file
diff --git a/apps/messages_light/messages_light.boot.js b/apps/messages_light/messages_light.boot.js
new file mode 100644
index 000000000..741d08b96
--- /dev/null
+++ b/apps/messages_light/messages_light.boot.js
@@ -0,0 +1,33 @@
+/*
+//OLD CODE -> backup purpose
+
+let messageBootManager=function(type,event){
+ //se l'app non è aperta
+ if ("undefined"==typeof manageEvent)
+ {
+ if(event.t=="remove") return; //l'app non è aperta, non c'è nessun messaggio da rimuovere dalla queue -> non lancio l'app
+
+ //la apro
+ require("Storage").writeJSON("messages_light.NewEvent.json",{"event":event,"type":type});
+ load("messages_light.app.js");
+ }
+ else
+ {
+ //altrimenti gli dico di gestire il messaggio
+ manageEvent(type,event);
+ }
+}
+Bangle.on("message", messageBootManager);
+Bangle.on("call", messageBootManager);*/
+
+
+
+//override require to filter require("message")
+global.require_real=global.require;
+global.require = (_require => file => {
+ if (file==="messages") file = "messagesProxy";
+ //else if (file==="messages_REAL") file = "messages"; //backdoor to real message
+
+ return _require(file);
+})(require);
+
diff --git a/apps/messages_light/messages_light.messagesProxy.js b/apps/messages_light/messages_light.messagesProxy.js
new file mode 100644
index 000000000..723397057
--- /dev/null
+++ b/apps/messages_light/messages_light.messagesProxy.js
@@ -0,0 +1,30 @@
+
+//gestisco il messaggio a modo mio
+exports.pushMessage = function(event) {
+
+ //TODO: now i can't handle the music, so i call the real message app
+ if( event.id=="music") return require_real("messages").pushMessage(event);
+
+ //se l'app non è aperta
+ if ("undefined"==typeof manageEvent)
+ {
+ if(event.t=="remove") return; //l'app non è aperta, non c'è nessun messaggio da rimuovere dalla queue -> non lancio l'app
+
+ //la apro
+ require_real("Storage").writeJSON("messages_light.NewEvent.json",event);
+ load("messages_light.app.js");
+ }
+ else
+ {
+ //altrimenti gli dico di gestire il messaggio
+ manageEvent(event);
+ }
+}
+
+
+//Call original message library
+exports.clearAll = function() { return require_real("messages").clearAll()}
+exports.getMessages = function() { return require_real("messages").getMessages()}
+exports.status = function() { return require_real("messages").status()}
+exports.buzz = function() { return require_real("messages").buzz(msgSrc)}
+exports.stopBuzz = function() { return require_real("messages").stopBuzz()}
\ No newline at end of file
diff --git a/apps/messages_light/messages_light.settings.js b/apps/messages_light/messages_light.settings.js
new file mode 100644
index 000000000..b7197c70a
--- /dev/null
+++ b/apps/messages_light/messages_light.settings.js
@@ -0,0 +1 @@
+eval(require("Storage").read("messages.settings.js"));
diff --git a/apps/messages_light/metadata.json b/apps/messages_light/metadata.json
new file mode 100644
index 000000000..3515a75c2
--- /dev/null
+++ b/apps/messages_light/metadata.json
@@ -0,0 +1,21 @@
+{
+ "id": "messages_light",
+ "name": "Messages Light",
+ "version": "1.3",
+ "description": "A light implementation of messages App (display notifications from iOS and Gadgetbridge/Android)",
+ "icon": "app.png",
+ "type": "app",
+ "tags": "tool,system",
+ "supports": ["BANGLEJS","BANGLEJS2"],
+ "dependencies" : { "messageicons":"module","messages":"app" },
+ "readme": "README.md",
+ "storage": [
+ {"name":"messages_light.app.js","url":"messages_light.app.js"},
+ {"name":"messages_light.settings.js","url":"messages_light.settings.js"},
+ {"name":"messages_light.img","url":"app-icon.js","evaluate":true},
+ {"name":"messagesProxy","url":"messages_light.messagesProxy.js"},
+ {"name":"messages_light.boot.js","url":"messages_light.boot.js"}
+ ],
+ "data": [{"name":"messages_light.settings.json"},{"name":"messages_light.NewMessage.json"}],
+ "screenshots": [{"url":"screenshot-notify.png"} ,{"url":"screenshot-long-text1.png"},{"url":"screenshot-long-text2.png"}, {"url":"screenshot-call.png"} ]
+}
diff --git a/apps/messages_light/screenshot-call.png b/apps/messages_light/screenshot-call.png
new file mode 100644
index 000000000..703faad6f
Binary files /dev/null and b/apps/messages_light/screenshot-call.png differ
diff --git a/apps/messages_light/screenshot-long-text1.png b/apps/messages_light/screenshot-long-text1.png
new file mode 100644
index 000000000..147b0cd5c
Binary files /dev/null and b/apps/messages_light/screenshot-long-text1.png differ
diff --git a/apps/messages_light/screenshot-long-text2.png b/apps/messages_light/screenshot-long-text2.png
new file mode 100644
index 000000000..5408f2059
Binary files /dev/null and b/apps/messages_light/screenshot-long-text2.png differ
diff --git a/apps/messages_light/screenshot-notify.png b/apps/messages_light/screenshot-notify.png
new file mode 100644
index 000000000..8896b803a
Binary files /dev/null and b/apps/messages_light/screenshot-notify.png differ
diff --git a/apps/messagesmusic/ChangeLog b/apps/messagesmusic/ChangeLog
index 5560f00bc..cd1c49b60 100644
--- a/apps/messagesmusic/ChangeLog
+++ b/apps/messagesmusic/ChangeLog
@@ -1 +1,6 @@
0.01: New App!
+0.02: Remove one line of code that didn't do anything other than in some instances hinder the function of the app.
+0.03: Use the new messages library
+0.04: Fix dependency on messages library
+ Fix loading message UI
+0.05: Ensure we don't clear artist info
diff --git a/apps/messagesmusic/README.md b/apps/messagesmusic/README.md
index 7aa9209df..9a50de93e 100644
--- a/apps/messagesmusic/README.md
+++ b/apps/messagesmusic/README.md
@@ -1,15 +1,9 @@
Hacky app that uses Messages app and it's library to push a message that triggers the music controls. It's nearly not an app, and yet it moves.
-This app require Messages setting 'Auto-open Music' to be 'Yes'. If it isn't, the app will change it to 'Yes' and let it stay that way.
-
Making the music controls accessible this way lets one start a music stream on the phone in some situations even though the message app didn't receive a music message from gadgetbridge to begin with. (I think.)
It is suggested to use Messages Music along side the app Quick Launch.
-Messages Music v0.01 has been verified to work with Messages v0.31 on Bangle.js 2 fw2v13.
-
-Music Messages should work with forks of the original Messages app. At least as long as functions pushMessage() in the library and showMusicMessage() in app.js hasn't been changed too much.
-
Messages app is created by Gordon Williams with contributions from [Jeroen Peters](https://github.com/jeroenpeters1986).
The icon used for this app is from [https://icons8.com](https://icons8.com).
diff --git a/apps/messagesmusic/app.js b/apps/messagesmusic/app.js
index a6f7e075e..68e88c2d8 100644
--- a/apps/messagesmusic/app.js
+++ b/apps/messagesmusic/app.js
@@ -1,15 +1,2 @@
-let showMusic = () => {
- Bangle.CLOCK = 1; // To pass condition in messages library
- require('messages').pushMessage({"t":"add","artist":" ","album":" ","track":" ","dur":0,"c":-1,"n":-1,"id":"music","title":"Music","state":"play","new":true});
- Bangle.CLOCK = undefined;
-};
-
-var settings = require('Storage').readJSON('messages.settings.json', true) || {}; //read settings if they exist else set to empty dict
-if (!settings.openMusic) {
- settings.openMusic = true; // This app/hack works as intended only if this setting is true
- require('Storage').writeJSON('messages.settings.json', settings);
- E.showMessage("First run:\n\nMessages setting\n\n 'Auto-Open Music'\n\n set to 'Yes'");
- setTimeout(()=>{showMusic();}, 5000);
-} else {
- showMusic();
-}
+// don't define artist/etc here so we don't wipe them out of memory if they were stored from before
+setTimeout(()=>require('messages').openGUI({"t":"add","id":"music","state":"show","new":true}));
diff --git a/apps/messagesmusic/metadata.json b/apps/messagesmusic/metadata.json
index edc6835ed..eef528f55 100644
--- a/apps/messagesmusic/metadata.json
+++ b/apps/messagesmusic/metadata.json
@@ -1,7 +1,8 @@
{
"id": "messagesmusic",
"name":"Messages Music",
- "version":"0.01",
+ "shortName": "Music",
+ "version":"0.05",
"description": "Uses Messages library to push a music message which in turn displays Messages app music controls",
"icon":"app.png",
"type": "app",
@@ -13,6 +14,5 @@
{"name":"messagesmusic.app.js","url":"app.js"},
{"name":"messagesmusic.img","url":"app-icon.js","evaluate":true}
],
- "dependencies": {"messages":"app"}
-
+ "dependencies":{"messages":"module"}
}
diff --git a/apps/miclock/ChangeLog b/apps/miclock/ChangeLog
index e92bad2e3..d1ac3e388 100644
--- a/apps/miclock/ChangeLog
+++ b/apps/miclock/ChangeLog
@@ -2,3 +2,4 @@
0.03: Localization
0.04: move jshint to the top
0.05: Use Bangle.setUI for button/launcher handling
+0.06: Tell clock widgets to hide.
diff --git a/apps/miclock/clock-mixed.js b/apps/miclock/clock-mixed.js
index b3d6bea8d..cb3235406 100644
--- a/apps/miclock/clock-mixed.js
+++ b/apps/miclock/clock-mixed.js
@@ -77,11 +77,13 @@ Bangle.on('lcdPower', function(on) {
drawMixedClock();
});
+// Show launcher when button pressed
+Bangle.setUI("clock");
+
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
setInterval(drawMixedClock, 5E3);
drawMixedClock();
-// Show launcher when button pressed
-Bangle.setUI("clock");
+
diff --git a/apps/miclock/metadata.json b/apps/miclock/metadata.json
index 6eece46b0..2c216dc33 100644
--- a/apps/miclock/metadata.json
+++ b/apps/miclock/metadata.json
@@ -1,7 +1,7 @@
{
"id": "miclock",
"name": "Mixed Clock",
- "version": "0.05",
+ "version": "0.06",
"description": "A mix of analog and digital Clock",
"icon": "clock-mixed.png",
"type": "clock",
diff --git a/apps/minimal_clock/ChangeLog b/apps/minimal_clock/ChangeLog
new file mode 100644
index 000000000..54ee389e3
--- /dev/null
+++ b/apps/minimal_clock/ChangeLog
@@ -0,0 +1,3 @@
+...
+0.03: First update with ChangeLog Added
+0.04: Tell clock widgets to hide.
diff --git a/apps/minimal_clock/app.js b/apps/minimal_clock/app.js
index d78790347..47eca3c66 100644
--- a/apps/minimal_clock/app.js
+++ b/apps/minimal_clock/app.js
@@ -3,6 +3,7 @@
let outerRadius = Math.min(CenterX,CenterY) * 0.9;
+ Bangle.setUI('clock');
Bangle.loadWidgets();
/**** updateClockFaceSize ****/
@@ -225,6 +226,5 @@
}
});
- Bangle.loadWidgets();
- Bangle.setUI('clock');
+ Bangle.loadWidgets();
diff --git a/apps/minimal_clock/metadata.json b/apps/minimal_clock/metadata.json
index 1702d97a9..3089780ce 100644
--- a/apps/minimal_clock/metadata.json
+++ b/apps/minimal_clock/metadata.json
@@ -1,7 +1,7 @@
{ "id": "minimal_clock",
"name": "Minimal Analog Clock",
"shortName":"Minimal Clock",
- "version":"0.03",
+ "version":"0.04",
"description": "a minimal analog clock - just with some hands and no clock face",
"icon": "app-icon.png",
"type": "clock",
diff --git a/apps/minionclk/ChangeLog b/apps/minionclk/ChangeLog
index a8b6efc81..5949a786d 100644
--- a/apps/minionclk/ChangeLog
+++ b/apps/minionclk/ChangeLog
@@ -3,3 +3,4 @@
0.03: Fixed rendering for Espruino v2.06
0.04: Fixed overlapped rendering of dates
0.05: Use Bangle.setUI for button/launcher handling
+0.06: Tell clock widgets to hide.
diff --git a/apps/minionclk/app b/apps/minionclk/app
new file mode 100644
index 000000000..e69de29bb
diff --git a/apps/minionclk/app.js b/apps/minionclk/app.js
index 9648e3d89..c61f8d3bf 100644
--- a/apps/minionclk/app.js
+++ b/apps/minionclk/app.js
@@ -78,8 +78,10 @@ Bangle.on('lcdPower', (on) => {
}
});
+// Show launcher when button pressed
+Bangle.setUI("clock");
+
Bangle.loadWidgets();
startDrawing();
-// Show launcher when button pressed
-Bangle.setUI("clock");
+
diff --git a/apps/minionclk/metadata.json b/apps/minionclk/metadata.json
index 44fc2a82d..4df2ddc6b 100644
--- a/apps/minionclk/metadata.json
+++ b/apps/minionclk/metadata.json
@@ -1,7 +1,7 @@
{
"id": "minionclk",
"name": "Minion clock",
- "version": "0.05",
+ "version": "0.06",
"description": "Minion themed clock.",
"icon": "minionclk.png",
"type": "clock",
diff --git a/apps/mitherm/ChangeLog b/apps/mitherm/ChangeLog
new file mode 100644
index 000000000..630459c15
--- /dev/null
+++ b/apps/mitherm/ChangeLog
@@ -0,0 +1 @@
+0.01: Create mitherm app with support for pvvx firmware only
diff --git a/apps/mitherm/README.md b/apps/mitherm/README.md
new file mode 100644
index 000000000..cdf3daa61
--- /dev/null
+++ b/apps/mitherm/README.md
@@ -0,0 +1,22 @@
+Reads BLE advertisement data from Xiaomi temperature/humidity sensors running the
+`pvvx` custom firmware (https://github.com/pvvx/ATC_MiThermometer).
+
+## Features
+
+* Display temperature
+* Display humidity
+* Display battery state of sensor
+* Auto-refresh every 5 minutes
+* Manual refresh on demand
+* Add aliases for MAC addresses to easily recognise devices
+
+## Planned features
+
+* Supprt for other advertising formats:
+ * atc1441 format
+ * BTHome
+ * Xiaomi Mijia format
+* Configurable auto-refresh interval
+* Configurable scan length (currently 30s)
+* Alerts when temperature outside defined limits (with a widget or bootcode to
+ work when app is inactive)
diff --git a/apps/mitherm/app-icon.js b/apps/mitherm/app-icon.js
new file mode 100644
index 000000000..2e8737704
--- /dev/null
+++ b/apps/mitherm/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwhC/AH4Ac5gWVhnM4AWVAAIYTCwQABCywYRIoYADJJwWHDB4RD5sz7hJPFIlP//0MRxFE6f/AAM9JJgWE4gWCAANMDBZcEn4XE+ZiKFwhcBCYPdDYRiEGAoXDLgf97vfMQwXILggXFMQYXHLgoXB6czMQoXHLgQXJMQQXG4YWEI44ABngXGh4XHF4v/+DAGC6DXGC5BHGC509F4IXTdwIABV4gXOIwIABJAoX/C6p3Xa4a/UABAXfgczABswC/4XmAH4A/ABY"))
diff --git a/apps/mitherm/app.js b/apps/mitherm/app.js
new file mode 100644
index 000000000..b7abdb2fc
--- /dev/null
+++ b/apps/mitherm/app.js
@@ -0,0 +1,172 @@
+var filterTemperature = [{
+ serviceData: {
+ "181a": {}
+ }
+}];
+var results = {};
+var macs = [];
+
+var aliases = require("Storage").readJSON("mitherm.json", true);
+if (!aliases) aliases = {};
+
+var lastSeen = {};
+var current = 0;
+var scanning = false;
+var timeoutDraw;
+var timeoutScan;
+
+
+const scan = function() {
+ if (!scanning) { // Don't start scanning if already doing so.
+ scanning = true;
+ if (timeoutScan) clearTimeout(timeoutScan);
+ timeoutScan = setTimeout(scan, 300000); // Scan again in 5 minutes.
+ drawScanState(scanning);
+ NRF.findDevices(function(devices) {
+ onDevices(devices);
+ }, {
+ filters: filterTemperature,
+ timeout: 30000 // Scan for 30s
+ });
+ }
+};
+
+
+const onDevices = function(devices) {
+ let now = Date.now();
+ for (let i = 0; i < devices.length; i++) {
+ let device = devices[i];
+
+ let processedData = extractData(device.data);
+ console.log({
+ rssi: device.rssi,
+ data: processedData
+ });
+ if (!macs.includes(processedData.MAC)) {
+ macs.push(processedData.MAC);
+ }
+ results[processedData.MAC] = processedData;
+ lastSeen[processedData.MAC] = now;
+ }
+ console.log("Scan complete.");
+ scanning = false;
+ writeOutput();
+};
+
+
+const extractData = function(thedata) {
+ let data = DataView(thedata);
+ let MAC = [];
+ for (let i = 9; i > 3; i--) {
+ MAC.push(data.getUint8(i, true).toString(16).padStart(2, "0"));
+ }
+ out = {
+ size: data.getUint8(0, true),
+ uid: data.getUint8(1, true),
+ UUID: data.getUint16(2, true),
+ MAC: MAC.join(":"),
+ temperature: data.getInt16(10, true) * 0.01,
+ humidity: data.getUint16(12, true) * 0.01,
+ battery_mv: data.getUint16(14, true),
+ battery_level: data.getUint8(16, true),
+ };
+ return out;
+};
+
+
+const writeOutput = function() {
+ let now = Date.now();
+ if (timeoutDraw) clearTimeout(timeoutDraw);
+ timeoutDraw = setTimeout(writeOutput, 60000); // Refresh in 1 minute.
+ g.clear(true);
+ Bangle.drawWidgets();
+ g.reset();
+ drawScanState(scanning);
+
+ if (macs.length == 0) return;
+
+ processedData = results[macs[current]];
+ g.setFont12x20(2);
+ g.drawString(`${processedData.temperature.toFixed(2)}°C`, 10, 30);
+ g.drawString(`${processedData.humidity.toFixed(2)} %`, 10, 70);
+
+ g.setFont6x15();
+ g.drawString(`${((now - lastSeen[macs[current]]) / 60000).toFixed(0)} min ago`, 10, 130);
+ g.drawString(`${processedData.battery_level} % battery`, 80, 130);
+ g.drawString(` ${processedData.MAC in aliases ? aliases[processedData.MAC] : processedData.MAC}: ${current + 1} / ${macs.length}`, 10, 150);
+};
+
+
+const scrollDevices = function(directionLR) {
+ // Swipe left or right to move between devices.
+ current -= directionLR; // inverted feels a more familiar gesture.
+ if (current + 1 > macs.length)
+ current = 0;
+ if (current < 0)
+ current = macs.length - 1;
+ writeOutput();
+};
+
+const drawScanState = function(state) {
+ if (state)
+ g.fillRect(160, 160, 170, 170);
+ else
+ g.clearRect(160, 160, 170, 170);
+};
+
+const setAlias = function(mac, alias) {
+ if (alias === "") {
+ delete aliases[mac];
+ }
+ else {
+ aliases[mac] = alias;
+ require("Storage").writeJSON("mitherm.json", aliases);
+ }
+};
+
+const changeAlias = function(mac) {
+ g.clear();
+ require("textinput").input((mac in aliases) ? aliases[mac] : "").then(function(text) {
+ setAlias(mac, text);
+ setUI();
+ writeOutput();
+ });
+};
+
+
+const setUI = function() {
+ Bangle.setUI({
+ mode: "custom",
+ swipe: scrollDevices,
+ btn: function() {
+ E.showMenu(actionsMenu);
+ }
+ });
+};
+
+
+const actionsMenu = {
+ "": {
+ "title": "-- Actions --",
+ "back": function() {
+ E.showMenu();
+ },
+ "remove": function() {
+ setUI();
+ writeOutput();
+ },
+ },
+ "Scan now": function() {
+ scan();
+ E.showMenu();
+ },
+ "Edit alias": function() {
+ changeAlias(macs[current]);
+ },
+};
+
+setUI();
+Bangle.loadWidgets();
+g.setClipRect(Bangle.appRect);
+scan();
+writeOutput();
diff --git a/apps/mitherm/app.png b/apps/mitherm/app.png
new file mode 100644
index 000000000..81d6bb24f
Binary files /dev/null and b/apps/mitherm/app.png differ
diff --git a/apps/mitherm/metadata.json b/apps/mitherm/metadata.json
new file mode 100644
index 000000000..a8da6fd26
--- /dev/null
+++ b/apps/mitherm/metadata.json
@@ -0,0 +1,15 @@
+{
+ "id": "mitherm",
+ "name": "Xiaomi Mijia Temperature and Humidity display",
+ "shortName": "MiTherm",
+ "version": "0.01",
+ "description": "Reads and displays data from Xiaomi temperature/humidity sensors running custom firmware",
+ "icon": "app.png",
+ "tags": "xiaomi,mi,ble,bluetooth,thermometer,humidity",
+ "readme": "README.md",
+ "supports": ["BANGLEJS", "BANGLEJS2"],
+ "storage": [
+ {"name":"mitherm.app.js","url":"app.js"},
+ {"name":"mitherm.img","url":"app-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/mylocation/ChangeLog b/apps/mylocation/ChangeLog
index c14e64ba9..afe1810e9 100644
--- a/apps/mylocation/ChangeLog
+++ b/apps/mylocation/ChangeLog
@@ -5,3 +5,5 @@
0.05: Fixed issue with back option
0.06: renamed source files to match standard
0.07: Move mylocation app into 'Settings -> Apps'
+0.08: Allow setting location from webinterface in the AppLoader
+0.09: Fix web interface so app can be installed (replaced custom with interface html)
diff --git a/apps/mylocation/README.md b/apps/mylocation/README.md
index a6a16ce83..11a644262 100644
--- a/apps/mylocation/README.md
+++ b/apps/mylocation/README.md
@@ -1,10 +1,15 @@
# My Location
- *Sets and stores GPS lat and lon of your preferred city*
+*Sets and stores GPS lat and lon of your preferred city*
-To access, go to `Settings -> Apps -> My Location`
+To access, you have two options:
-* Select one of the preset Cities or setup through the GPS
+**In the App Loader** once My Location is installed, click on the 'Save' icon
+next to it - and you can choose your location on a map.
+
+**On Bangle.js** go to `Settings -> Apps -> My Location`
+
+* Select one of the preset Cities, setup through the GPS or use the webinterface from the AppLoader
* Other Apps can read this information to do calculations based on location
* When the City shows ??? it means the location has been set through the GPS
diff --git a/apps/mylocation/interface.html b/apps/mylocation/interface.html
new file mode 100644
index 000000000..79a122bf7
--- /dev/null
+++ b/apps/mylocation/interface.html
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Click the map to select a location
+ Save
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/mylocation/metadata.json b/apps/mylocation/metadata.json
index 4ab9aa37e..1c2974030 100644
--- a/apps/mylocation/metadata.json
+++ b/apps/mylocation/metadata.json
@@ -4,11 +4,12 @@
"icon": "app.png",
"type": "settings",
"screenshots": [{"url":"screenshot_1.png"}],
- "version":"0.07",
- "description": "Sets and stores the lat and long of your preferred City or it can be set from the GPS. mylocation.json can be used by other apps that need your main location lat and lon. See README",
+ "version":"0.09",
+ "description": "Sets and stores the latitude and longitude of your preferred City. It can be set from GPS or webinterface. `mylocation.json` can be used by other apps that need your main location. See README for details.",
"readme": "README.md",
"tags": "tool,utility",
"supports": ["BANGLEJS", "BANGLEJS2"],
+ "interface": "interface.html",
"storage": [
{"name":"mylocation.settings.js","url":"settings.js"}
],
diff --git a/apps/mysticclock/ChangeLog b/apps/mysticclock/ChangeLog
index b486a29a1..cd91abe00 100644
--- a/apps/mysticclock/ChangeLog
+++ b/apps/mysticclock/ChangeLog
@@ -1,2 +1,3 @@
1.00: First published version.
1.01: Use Bangle.setUI for Launcher/buttons
+1.02: Tell clock widgets to hide.
diff --git a/apps/mysticclock/metadata.json b/apps/mysticclock/metadata.json
index 571a55ecd..bd2df2f8d 100644
--- a/apps/mysticclock/metadata.json
+++ b/apps/mysticclock/metadata.json
@@ -1,7 +1,7 @@
{
"id": "mysticclock",
"name": "Mystic Clock",
- "version": "1.01",
+ "version": "1.02",
"description": "A retro-inspired watchface featuring time, date, and an interactive data display line.",
"icon": "mystic-clock.png",
"type": "clock",
diff --git a/apps/mysticclock/mystic-clock-app.js b/apps/mysticclock/mystic-clock-app.js
index 2d95633fe..d7f4ab1c3 100644
--- a/apps/mysticclock/mystic-clock-app.js
+++ b/apps/mysticclock/mystic-clock-app.js
@@ -189,6 +189,13 @@ Bangle.on('touch', (button) => {
if (button === 3 && Bangle.isLCDOn()) Bangle.setLCDPower(false);
});
+// Show launcher when button pressed
+Bangle.setUI("clockupdown", btn=>{
+ if (btn<0) prevInfo();
+ if (btn>0) nextInfo();
+ drawAll();
+});
+
// clean app screen
g.clear();
Bangle.loadWidgets();
@@ -200,9 +207,3 @@ if (Bangle.isLCDOn()) {
drawAll(); // draw immediately
}
-// Show launcher when button pressed
-Bangle.setUI("clockupdown", btn=>{
- if (btn<0) prevInfo();
- if (btn>0) nextInfo();
- drawAll();
-});
diff --git a/apps/nato/ChangeLog b/apps/nato/ChangeLog
new file mode 100644
index 000000000..5560f00bc
--- /dev/null
+++ b/apps/nato/ChangeLog
@@ -0,0 +1 @@
+0.01: New App!
diff --git a/apps/ncfrun/metadata.json b/apps/ncfrun/metadata.json
deleted file mode 100644
index 831ae3d4e..000000000
--- a/apps/ncfrun/metadata.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "id": "ncfrun",
- "name": "NCEU 5K Fun Run",
- "version": "0.01",
- "description": "Display a map of the NodeConf EU 2019 5K Fun Run route and your location on it",
- "icon": "nceu-funrun.png",
- "tags": "health",
- "supports": ["BANGLEJS"],
- "storage": [
- {"name":"ncfrun.app.js","url":"nceu-funrun.js"},
- {"name":"ncfrun.img","url":"nceu-funrun-icon.js","evaluate":true}
- ]
-}
diff --git a/apps/ncfrun/nceu-funrun-icon.js b/apps/ncfrun/nceu-funrun-icon.js
deleted file mode 100644
index a13452a8b..000000000
--- a/apps/ncfrun/nceu-funrun-icon.js
+++ /dev/null
@@ -1 +0,0 @@
-require("heatshrink").decompress(atob("mEwwgurglEC6tDmYYUgkzAANAFygXKKYIADBwgXDkg8LBwwXMoQXEH4hHNC4s0O6BfECAKhDHYKnOghCB3cga6dEnYYBaScC2cznewC6W7OQU7BYyIFAAhFBAAYwGC5RFBC5QAJlY0FSIQAMkUjGgrTJRYoXFPQIXGLg8iAAJFDDgIXGgYXJGAWweQJHOC4jtBC6cidgQXUUQQXBogACDYR3HmQXHAAYzKU4IACC48kJBwFBgg7EMZYwDJAReDoh5PC4QARJAoARJAYXTJChtDoSgNAAaeEAAU0C5wqCC4q5LOYYvWgjOEaJ4AGoZGQPY6OPFw0yF34uFRlYXCFykAoQuVeIQWUAB4A="))
diff --git a/apps/ncfrun/nceu-funrun.js b/apps/ncfrun/nceu-funrun.js
deleted file mode 100644
index 30e587188..000000000
--- a/apps/ncfrun/nceu-funrun.js
+++ /dev/null
@@ -1,140 +0,0 @@
-var coordScale = 0.6068;
-var coords = new Int32Array([-807016,6918514,-807057,6918544,-807135,6918582,-807238,6918630,-807289,6918646,-807308,6918663,-807376,6918755,-807413,6918852,-807454,6919002,-807482,6919080,-807509,6919158,-807523,6919221,-807538,6919256,-807578,6919336,-807628,6919447,-807634,6919485,-807640,6919505,-807671,6919531,-807703,6919558,-807760,6919613,-807752,6919623,-807772,6919643,-807802,6919665,-807807,6919670,-807811,6919685,-807919,6919656,-807919,6919645,-807890,6919584,-807858,6919533,-807897,6919503,-807951,6919463,-807929,6919430,-807916,6919412,-807907,6919382,-807901,6919347,-807893,6919322,-807878,6919292,-807858,6919274,-807890,6919232,-807909,6919217,-807938,6919206,-807988,6919180,-807940,6919127,-807921,6919100,-807908,6919072,-807903,6919039,-807899,6919006,-807911,6918947,-807907,6918936,-807898,6918905,-807881,6918911,-807874,6918843,-807870,6918821,-807854,6918775,-807811,6918684,-807768,6918593,-807767,6918593,-807729,6918516,-807726,6918505,-807726,6918498,-807739,6918481,-807718,6918465,-807697,6918443,-807616,6918355,-807518,6918263,-807459,6918191,-807492,6918162,-807494,6918147,-807499,6918142,-807500,6918142,-807622,6918041,-807558,6917962,-807520,6917901,-807475,6917933,-807402,6917995,-807381,6918024,-807361,6918068,-807323,6918028,-807262,6918061,-807263,6918061,-807159,6918116,-807148,6918056,-807028,6918063,-807030,6918063,-806979,6918068,-806892,6918090,-806760,6918115,-806628,6918140,-806556,6918162,-806545,6918175,-806531,6918173,-806477,6918169,-806424,6918180,-806425,6918180,-806367,6918195,-806339,6918197,-806309,6918191,-806282,6918182,-806248,6918160,-806225,6918136,-806204,6918107,-806190,6918076,-806169,6917968,-806167,6917953,-806157,6917925,-806140,6917896,-806087,6917839,-806071,6917824,-805969,6917904,-805867,6917983,-805765,6918063,-805659,6918096,-805677,6918131,-805676,6918131,-805717,6918212,-805757,6918294,-805798,6918397,-805827,6918459,-805877,6918557,-805930,6918608,-805965,6918619,-806037,6918646,-806149,6918676,-806196,6918685,-806324,6918703,-806480,6918735,-806528,6918738,-806644,6918712,-806792,6918667,-806846,6918659,-806914,6918654,-806945,6918661,-806971,6918676,-806993,6918689,-806992,6918692,-807065,6918753,-807086,6918786,-807094,6918788,-807102,6918795,-807104,6918793,-807107,6918799,-807102,6918802,-807112,6918812,-807106,6918815,-807115,6918826,-807120,6918823,-807132,6918841,-807141,6918850,-807151,6918841,-807170,6918832,-807193,6918813,-807222,6918775,-807246,6918718,-807250,6918694,-807264,6918637,-807238,6918630,-807148,6918587,-807057,6918544,-806948,6918463]);
-
-var min = {"x":-807988,"y":6917824};
-var max = {"x":-805659,"y":6919685};
-var gcoords = new Uint8Array(coords.length);
-var coordDistance = new Uint16Array(coords.length/2);
-
-var PT_DISTANCE = 30; // distance to a point before we consider it complete
-
-function toScr(p) {
- return {
- x : 10 + (p.x-min.x)*100/(max.x-min.x),
- y : 230 - (p.y-min.y)*100/(max.y-min.y)
- };
-}
-
-var last;
-var totalDistance = 0;
-for (var i=0;i {
}
});
+// Show launcher when button pressed
+Bangle.setUI("clock");
+
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
drawHands(true);
-
-// Show launcher when button pressed
-Bangle.setUI("clock");
diff --git a/apps/ncrclk/metadata.json b/apps/ncrclk/metadata.json
index b50b554e1..fdab77450 100644
--- a/apps/ncrclk/metadata.json
+++ b/apps/ncrclk/metadata.json
@@ -2,7 +2,7 @@
"id": "ncrclk",
"name": "NCR Clock",
"shortName": "NCR Clock",
- "version": "0.02",
+ "version": "0.03",
"description": "NodeConf Remote clock",
"icon": "app.png",
"type": "clock",
diff --git a/apps/ncstart/ChangeLog b/apps/ncstart/ChangeLog
deleted file mode 100644
index 152fdc9d1..000000000
--- a/apps/ncstart/ChangeLog
+++ /dev/null
@@ -1,9 +0,0 @@
-0.02: Modified for use with new bootloader and firmware
- 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
-0.05: Don't overwrite existing settings on app update
-0.06: Allow welcome to run after a fresh install
diff --git a/apps/ncstart/boot.js b/apps/ncstart/boot.js
deleted file mode 100644
index 62ac962f6..000000000
--- a/apps/ncstart/boot.js
+++ /dev/null
@@ -1,9 +0,0 @@
-(function() {
- let s = require('Storage').readJSON('ncstart.json', 1) || {};
- if (!s.welcomed) {
- setTimeout(() => {
- require('Storage').write('ncstart.json', {welcomed: true})
- load('ncstart.app.js')
- })
- }
-})()
diff --git a/apps/ncstart/metadata.json b/apps/ncstart/metadata.json
deleted file mode 100644
index d2b3e2196..000000000
--- a/apps/ncstart/metadata.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "id": "ncstart",
- "name": "NCEU Startup",
- "version": "0.06",
- "description": "NodeConfEU 2019 'First Start' Sequence",
- "icon": "start.png",
- "tags": "start,welcome",
- "supports": ["BANGLEJS"],
- "storage": [
- {"name":"ncstart.app.js","url":"start.js"},
- {"name":"ncstart.boot.js","url":"boot.js"},
- {"name":"ncstart.settings.js","url":"settings.js"},
- {"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"}]
-}
diff --git a/apps/ncstart/settings.js b/apps/ncstart/settings.js
deleted file mode 100644
index 560fad8ba..000000000
--- a/apps/ncstart/settings.js
+++ /dev/null
@@ -1,14 +0,0 @@
-(function(back) {
- 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.json', {welcomed: !v}),
- },
- 'Run Now': () => load('ncstart.app.js'),
- '< Back': back,
- })
-})
diff --git a/apps/ncstart/start-bangle.js b/apps/ncstart/start-bangle.js
deleted file mode 100644
index 26f38ae14..000000000
--- a/apps/ncstart/start-bangle.js
+++ /dev/null
@@ -1 +0,0 @@
-require("heatshrink").decompress(atob("s8wxH+AH4AQ/4AJJX5mmM/5m/AH5m/M34A/M35l/M35mqM/5m/AH5m/M34A/MqQKQJm5laOh7kNM35MGbiQxLM9osWIiZnGDI5m/VTBm/MsrOGM35maB4xm/MsoZFORZm/Fq5mDAAwUKBhAHBDJYLGAY4rOPShmRF44TIIoqlJCIxmKEZLMSBxY1GE5RTIJpwYSP5hmQZxodKLBKpIDBQZHMxS4MM1IKCMzKNQHJJmtFwbbUMy4AIM35mcJR5mbLCo1GZrxLOLZ6BMH5wOHMyAYRSRLOWGRY+MAxRmODCZeNMyLNMAA4TIBgpmPFA4YMHBZnPFIp/cADa0cC9Zm2J5YkKMtgsIGjZRTCYLMsFow0dDqJluGAgzhEJwxiAGpYLMn70hAA5N/M34A/M35mzJn5m/AH5nNJf5m/AH5m/M34A/M35m/MpgA="))
diff --git a/apps/ncstart/start-icon.js b/apps/ncstart/start-icon.js
deleted file mode 100644
index 0302cadbc..000000000
--- a/apps/ncstart/start-icon.js
+++ /dev/null
@@ -1 +0,0 @@
-require("heatshrink").decompress(atob("mEwxH+AHMPADQv/F+YxZYtb1wFto7SEbwwQBIsen0/ADU+jxfOjwtbAAYwDWZVWF79WfBAvEq4vfq4vIGQgviR44AEFz4vEGRQvnGA4v/F79YX9IHEq4aKh//jwvRrBcHG4ovL/4ABB5gAFRAwvVGIQveoAAIF4oABq0/CZIACF8BiBrAvTGIoaKF5AABIpVXd44AFJBQvKh4vOGBIvVL54vdX5iPhqztLoFYFpYvSh8/FxgABFpYvQRRgveoEP/8eFqAvbACi/CeA4IDP6IvUGIYGEF+EMADwvJR4ovmdoovnFoowDF8QsIF4dZF79ZF5RpCj1AFztAjy7JAAgwdFwbAFFwwAmF/4vhGFrxLFkoAvA="))
diff --git a/apps/ncstart/start-nceu.js b/apps/ncstart/start-nceu.js
deleted file mode 100644
index 89a9850cc..000000000
--- a/apps/ncstart/start-nceu.js
+++ /dev/null
@@ -1 +0,0 @@
-require("heatshrink").decompress(atob("o9HxH+AEOAwAkiIkIADIv5CEI/4/IJHbNLbPA4Iv1+JHREIwkmk2EJBBE2IoUnIwJHBCx5GoBA2DIgQACBw5G3aQQADwRG+wEmagQCBvxGufoQpDFxOCI4YNIDgxNeD4gDHCY+EwgMKBQIjGJDJlHA4YlKvzRHDRZHZDJQkMBZojVECb+OHJgkOZ6w6KCJAHJCgY1dK5wPDCg4GICYjDZBY9+vxGMArItLeRgWDwOEwmBJA5Ggv2GlMMwJGTwRFBI5JGfv2HlIACwRGRwBFDAAIUGIz+FIYMMI4R0CIxzSCRwhaMIBy2FAAaMBhmHI4QjIRqwUFIxxFJOgLTDlMGRqJHFwF+CpAWDIxgwJBgN+aoSMEIyAGBweDXBg6FABIWLAgOCw+GMhRGKByI9IIxYtQIywaJC5YTTIzwRGOyQqTIzLGNCTJGgXqIRTIzILIIzQvUI5a4EBgh6TDI7dKZJo7IAwQLFIzAjKIhwQGChBvMEhojLIqIjGBaZGPEbppOEerrLBYpGVEZrVOBpJjJIzCHNcpoqPI6gaUIywfSCLJGgXBYSZIzwRFCxoSGFSJGYCA4XLCRArQIywOJYxDPLFqA3OwFPp4HCy4lKHogAIM5uulukMIxGNy1MAAWW2JENFBJIMv8B0ksAAQQDIx2AptMpoCCChZGQGROYIocslsBIyGVIQNOp5HByhaMIxj9IAAWMIYUtRwiNPaIKNCpgUGIB4FNAAMXRq/+yhDBAAOUtJGlgKOCAAOvCJRGH2OVp1OypFGI0BHB0jUBzCMCIyAABtJEHI0RICIgYRMJBBGMCg4GICYgnPCBhHPBwQSIA5IUDGpxWOJBwgLfpgkOIhwVOEBj9WIipsKA4YiKgMBERojIIqphHAgYjKy+n1VpTJYjIADZlGEpOVlwABhTJKRL4oHFxIIEIgUKlula44/hShwIG1RFB02lJQJVII2zTC0iNBhVpI24vGgOmlpIBl2WagwWIJGFp1UKhRFGImI0FGouAaIoPIJGQMWJG5E7H5BE/I4pF/JA4kiA"))
diff --git a/apps/ncstart/start-nfr.js b/apps/ncstart/start-nfr.js
deleted file mode 100644
index 2a0ad70ea..000000000
--- a/apps/ncstart/start-nfr.js
+++ /dev/null
@@ -1 +0,0 @@
-require("heatshrink").decompress(atob("5EuxH+AAkPABIQFABIaKDiIA/AH6qaVpwbbAH4A/YzysMDbYA/AH7GfVhbHgChrr0MT5FoTDobOQijH/Y/6aYcqzH/Y/5EeZDLHmFxTH/Y/4TVY84uJY/7H7TibHuC4rH5XmiHRZC7HpDAjSQF5QpJCgYGJY6Y8MFR4bJBSrITY9RNJb6LFNY5ALFP6CsVY55PQTzDH6MhrGPY7opYY7IZFAgqfRY9xzIWx7GQY6QsYFTTHQZDLHlL44ONDxIfJdKS3PA54qSCRL1MWpDIRY8yLNCg5FICB7ZMHZwrKaB4bQEpTHZH5bHgRhiZNbCTZSY5qBNHiDHZZCbHsOZjHPRSTHYOZbHyZDLH/Y8pQRY+zIYY8xPLG6bHsDJjHuUiTbQdTjHfQBjHYVaLHyUqbHoKJC2KCBgRDBA7HeThbHvZETHdVxKKPTkzISfJLHpZELHeOZLGOY9g8OY+TIgY74OJLqDHqFZIMJY/7HuFxRcPYJbHeXi7HUKAqGYCSgdRAH4A/XC7IdY/4A/Y9rIZY/4A/Y/7H/AH7H2ZDDH/AH7HvZC7H/SMrH/Io7IZDCoVIBgwNFBSA7JBRoZOJ5jboY6IOBY9oWKDpYLFApZkNH6YIHJ5BMNY97IZY6yvTTJCGRBwQRIExYVKB4zH+ZDDHpBQ7HgH5Q+QY/7IYY9KDJY6QeKY6xDOY/7H7BhRiPCRQGHA4SsRCJDH4ZDJqUfpQiIBR6UNDRISQOJ52TY9DIYNSyvSIZLfOAxoaIY/7HVZC4SQQBSJUC57HTDIw9QGZzH/Y7xmINyTHTAAwfKHyzH/OBTH3CRg1LYxAUFD5Y+QY9RXLLxQaWY6yIYY6g5SH57kHY9StRcbZPQQJivRC6AKJEBpGHBxrH/DcbHUEpQKQBojSPH5gpIXx7HjVp4caJkbjRGv4AkA=="))
diff --git a/apps/ncstart/start-nodew.js b/apps/ncstart/start-nodew.js
deleted file mode 100644
index 287d49a1b..000000000
--- a/apps/ncstart/start-nodew.js
+++ /dev/null
@@ -1 +0,0 @@
-require("heatshrink").decompress(atob("5E1xH+AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A6hEICD4A/AH7FlY6TJ/AH7G1Y6bI/AH7FyY6rJ/AH7GyY6zI/AH7H/Y/4A/Y3DHXg5g/AH4Ak5jH/AH4A/Y9VXq5l/AH4Ag47HjZAJm/AH7GgY5S9KZBjHDZH4A/Y9S5KBxrH/AH7GkY5DGOCIrHJZA4pGBiQAPKpAQLHKQmPADRSQY6LFQCZLHPQJjIeQqwKVHTLHoEpDISY4rIGJRbH/IjBYXC67GCY5LGRY7CCaY8hEPV87Ha4zHEYyoXGY6SOKY9IQJIhRDUY+YdEY64YCAgTIBY6CDPMBxRIDpAvMIaZALV5z/NFRrHH5glGYyqOFY4LIJDJoHKaZolPMZIRMUZAyLHxotLLJzHJ4zGBY8TICY6KXJO6wdQWiJCHGRp+QJaTINYoRbQY6bICZINRY8RJQDhowPY8RMYY5YABgR9US6MHgIwGJ5QMLE44GIURY4NBSoyKIZQRObhrIMg7HkgQvIJBapSBzrBPCBhdJY5w+NeBgAFzO93rIFY8AFBxmGwydKFxSFOMJR6JFZhXLIKbHVPhbHPZALJBZA7HcAgLFBY5qFYY+KsLY/DICY4rIWC4kC/2MY6CGJOZjWPRBy8KY95MHY62ZAoLICY64/G/zGIMRxcQdB7HWBZqeQEZxcIY7e9Y4ZMHY/AwJIByrOY7JzLCJAbNY8jITCozHVURqDRDrY/MGSDHWPhTHOZAbHFZCgxHY4gxGIJbrLT6AQIDiRLQOp7ITGBbHcZB4wKY5o+IOxwWLBoQdUY54zMCJTTQBQ6GSZAjHGZCJCLY5IA/AH4AWY5L7LBpzHDNH4A/ZEDHIXQoALaZLH/AH7HsZB4WHgTHCM34A/AELHjY34A/AErHhAH4A/AEzH/AH4A/Y8xe/AH7IwCsgA/AH7IiCkYA/AH7IjCcQA/AH7JkCMAA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AHIA="))
diff --git a/apps/ncstart/start-tf.js b/apps/ncstart/start-tf.js
deleted file mode 100644
index d09185caf..000000000
--- a/apps/ncstart/start-tf.js
+++ /dev/null
@@ -1 +0,0 @@
-require("heatshrink").decompress(atob("4M7ghC/AH4A/AEcBiMQAgY+3iIACIAQhcAgYjdTzZgfYH5giYHo5BIIQGDAQYLHgMQBoZZFYAoYENGREDGgI6BAYYMDBYrXDA4RTDAobnFLgbNEYF5LFVAIGCAghsEBQYMFYAoMEC4TAxAAg5DLoqiCZQZ2CAQa2DYAiGFA4bAvXYj6DAIZkEVYizHLYhyDNoaGCL9zAEHw5aGOIwHJYAqBFL96zCHxJsEKwcBYAx5GYA4SCFobAuMohoDAQ4UCKgYCEYBR7FYGqsDIIwLFYAzUFYAhbEL+IyGIojAFBYbAEKYZYHCgghEYGoGFWoQGFJIYHBgRZDbQpsFYGiNuIP4AedooA/L/4A5gJf/AH4A/AH4A/AH4A/AH4A/AH4A/ACsN7oAJG+oJC6AoaL5QmbG7PQTLrA/YH7A/YH7AhFgxbrYA4JQFkTArWwzAlEQhnCA4bPDBQwLDHwwJH5oGBEAydNAwQHDEgggDcaJBDEY4JDJoYSEH5A7GBAbAPHg5aHCQoiSEYpNHBIxpIBIQHGTpwzGGQJlMESZCIFowTNCQLAVSZAXFGQnNL5AiNBJAeBBIYsDNJIJHTpwyMCQhGEQQwsFYo7kGAoQJDEQQyDS5ChDCgxRJGRJ4DUIpAFYBQiFJgwUGBIr3IIAy5EaJgyNcgoTGcZZCFWwxMLboxqLYBoUJGw5GIYBaSGFoo3GLApAOYCAUJQYycLYBCSHDIoUJYBfdYDwKDEwQFCBAbAfIwwHEFA6rKTpLAPEoQCDYEBgIFgzAMHwzAOXpDAkMIxALYD4wEAozAOaA7AKMIxAJVZjAWApLAOBxasGEYYZCIAyrOYCQlBYC4jGhpHCYBBpJAYSrSTp4FELQZmFYBoRDBQjAMGwvcHQYiEdBDANHgpTFApbALaYgWERpISGHYoJFYCo8JVBKAHFg5COAoY2JBI65IYBofHOYZmIYBxgGNIr4KSxJJHYCQfGCQhmIYBwjFPZDVKQg53IYCI8IBIgFIERgjEPZLVJEhHcAwUMYCo8IYBIfGAH4A/AHw"))
diff --git a/apps/ncstart/start.js b/apps/ncstart/start.js
deleted file mode 100644
index d2d713cb2..000000000
--- a/apps/ncstart/start.js
+++ /dev/null
@@ -1,120 +0,0 @@
-g.setFontAlign(1, 1, 0);
-const d = g.getWidth() - 18;
-function c(a) {
- return {
- width: 8,
- height: a.length,
- bpp: 1,
- buffer: (new Uint8Array(a)).buffer
- };
-}
-
-function welcome() {
- var welcomes = [
- 'Welcome',
- 'Failte',
- 'Bienvenue',
- 'Willkommen',
- 'Bienvenido'
- ];
- function next() {
- var n = welcomes.shift();
- E.showMessage(n);
- g.drawImage(c([0,8,12,14,255,14,12,8]),d,116);
- welcomes.push(n);
- }
- return new Promise((res) => {
- next();
- var i = setInterval(next, 2000);
- setWatch(() => {
- clearInterval(i);
- clearWatch();
- E.showMessage('Loading...');
- res();
- }, BTN2, {repeat:false});
- });
-}
-
-function logos() {
- var logos = [
- ['nfr', 20, 90, ()=>{}],
- ['nceu', 20, 90, ()=>{
- g.setFont("6x8", 2);
- g.setColor(0,0,1);
- g.drawString('Welcome To', 160, 110);
- g.drawString('NodeConfEU', 160, 130);
- g.drawString('2019', 200, 150);
- }],
- ['bangle', 70, 90, ()=>{}],
- ['nodew', 20, 90, ()=>{}],
- ['tf', 24, 90, ()=>{}],
- ];
- function next() {
- var n = logos.shift();
- var img = require("Storage").read("nc-"+n[0]+".img");
- g.clear();
- g.drawImage(img, n[1], n[2]);
- n[3]();
- g.drawImage(c([0,8,12,14,255,14,12,8]),d,116);
- logos.push(n);
- }
- return new Promise((res) => {
- next();
- var i = setInterval(next, 2000);
- setWatch(() => {
- clearInterval(i);
- clearWatch();
- res();
- }, BTN2, {repeat:false});
- });
-}
-
-function info() {
- var slides = [
- () => E.showMessage('Visit\nnodewatch.dev\nfor info'),
- () => E.showMessage('Visit\nbanglejs.com/apps\nfor apps'),
- () => E.showMessage('Remember\nto charge\nyour watch!'),
- () => {
- g.clear();
- g.setFont('6x8',2);
- g.setColor(1,1,1);
- g.drawImage(c([0,8,12,14,255,14,12,8]),d,40);
- g.drawImage(c([0,8,12,14,255,14,12,8]),d,194);
- g.drawImage(c([0,8,12,14,255,14,12,8]),d,116);
- g.drawString('Menu Up', d - 50, 42);
- g.drawString('Select', d - 40, 118);
- g.drawString('Menu Down', d - 60, 196);
- },
- () => {
- g.clear();
- E.showMessage('Hold\nto return\nto clock');
- g.drawImage(c([0,8,12,14,255,14,12,8]),d,194);
- },
- () => {
- g.clear();
- E.showMessage('Hold both\nto reboot');
- g.drawImage(c([0,8,12,14,255,14,12,8]),d,40);
- g.drawImage(c([0,8,12,14,255,14,12,8]),d,116);
- },
- () => E.showMessage('Open Settings\nto enable\nBluetooth')
- ];
- function next() {
- var n = slides.shift();
- n();
- slides.push(n);
- }
- return new Promise((res) => {
- next();
- var i = setInterval(next, 2000);
- setWatch(()=>{
- clearInterval(i);
- clearWatch();
- res();
- }, BTN2, {repeat:false});
- });
-}
-
-welcome()
- .then(logos)
- .then(info)
- .then(load);
diff --git a/apps/ncstart/start.png b/apps/ncstart/start.png
deleted file mode 100644
index 9df0974c8..000000000
Binary files a/apps/ncstart/start.png and /dev/null differ
diff --git a/apps/nixie/nixie.info b/apps/nixie/nixie.info
deleted file mode 100644
index 66f5ff2a5..000000000
--- a/apps/nixie/nixie.info
+++ /dev/null
@@ -1,10 +0,0 @@
-{
-"id":"jvNixie",
-"name":"Nixie Clock",
-"type":"clock",
-"src":"nixie.app.js",
-"icon": "nixie.img",
-"sortorder":1,
-"version":"1.1",
-"files":"nixie.info,nixie.app.js,nixie.img, m_vatch.js"
-}
diff --git a/apps/noteify/ChangeLog b/apps/noteify/ChangeLog
index d7bc46dcd..a37a66731 100644
--- a/apps/noteify/ChangeLog
+++ b/apps/noteify/ChangeLog
@@ -1,2 +1,3 @@
0.01: Initial version
0.02: Use default Bangle formatter for booleans
+0.03: Drop duplicate alarm widget
diff --git a/apps/noteify/README.md b/apps/noteify/README.md
index c846709de..dbdceb399 100644
--- a/apps/noteify/README.md
+++ b/apps/noteify/README.md
@@ -1,6 +1,6 @@
# WARNING
-This app uses the [Scheduler library](https://banglejs.com/apps/?id=sched) and requires a keyboard such as [Swipe keyboard](https://banglejs.com/apps/?id=kbswipe).
+This app uses the [Scheduler library](https://banglejs.com/apps/?id=sched) and requires a [keyboard library](https://banglejs.com/apps/?c=textinput#).
## Usage
diff --git a/apps/noteify/interface.html b/apps/noteify/interface.html
index 027c98860..4d7974ad9 100644
--- a/apps/noteify/interface.html
+++ b/apps/noteify/interface.html
@@ -18,6 +18,11 @@
var notesElement = document.getElementById("notes");
var notes = {};
+function disableFormInput() {
+ document.querySelectorAll(".form-input").forEach(el => el.disabled = true);
+ document.querySelectorAll(".btn").forEach(el => el.disabled = true);
+}
+
function getData() {
// show loading window
Util.showModal("Loading...");
@@ -53,8 +58,10 @@ function getData() {
buttonSave.classList.add('btn-default');
buttonSave.onclick = function() {
notes[i].note = textarea.value;
- Util.writeStorage("noteify.json", JSON.stringify(notes));
- location.reload();
+ disableFormInput();
+ Util.writeStorage("noteify.json", JSON.stringify(notes), () => {
+ location.reload(); // reload so we see current data
+ });
}
divColumn2.appendChild(buttonSave);
@@ -64,8 +71,10 @@ function getData() {
buttonDelete.onclick = function() {
notes[i].note = textarea.value;
notes.splice(i, 1);
- Util.writeStorage("noteify.json", JSON.stringify(notes));
- location.reload(); // reload so we see current data
+ disableFormInput();
+ Util.writeStorage("noteify.json", JSON.stringify(notes), () => {
+ location.reload(); // reload so we see current data
+ });
}
divColumn2.appendChild(buttonDelete);
divColumn.appendChild(divColumn2);
@@ -77,8 +86,10 @@ function getData() {
document.getElementById("btnAdd").addEventListener("click", function() {
const note = document.getElementById("note-new").value;
notes.push({"note": note});
- Util.writeStorage("noteify.json", JSON.stringify(notes));
- location.reload(); // reload so we see current data
+ disableFormInput();
+ Util.writeStorage("noteify.json", JSON.stringify(notes), () => {
+ location.reload(); // reload so we see current data
+ });
});
});
}
diff --git a/apps/noteify/metadata.json b/apps/noteify/metadata.json
index eb6dc695a..850628c46 100644
--- a/apps/noteify/metadata.json
+++ b/apps/noteify/metadata.json
@@ -1,16 +1,15 @@
{
"id": "noteify",
"name": "Noteify",
- "version": "0.02",
+ "version": "0.03",
"description": "Write notes using an onscreen keyboard and use them as custom messages for alarms or timers.",
"icon": "app.png",
"tags": "tool,alarm",
- "supports": ["BANGLEJS2"],
+ "supports": ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"noteify.app.js","url":"app.js"},
- {"name":"noteify.img","url":"app-icon.js","evaluate":true},
- {"name":"noteify.wid.js","url":"widget.js"}
+ {"name":"noteify.img","url":"app-icon.js","evaluate":true}
],
"data": [{"name":"noteify.json"}],
"dependencies": {"scheduler":"type","textinput":"type"},
diff --git a/apps/noteify/widget.js b/apps/noteify/widget.js
deleted file mode 100644
index 052ac9ebd..000000000
--- a/apps/noteify/widget.js
+++ /dev/null
@@ -1,8 +0,0 @@
-WIDGETS["alarm"]={area:"tl",width:0,draw:function() {
- if (this.width) g.reset().drawImage(atob("GBgBAAAAAAAAABgADhhwDDwwGP8YGf+YMf+MM//MM//MA//AA//AA//AA//AA//AA//AB//gD//wD//wAAAAADwAABgAAAAAAAAA"),this.x,this.y);
- },reload:function() {
- // don't include library here as we're trying to use as little RAM as possible
- WIDGETS["alarm"].width = (require('Storage').readJSON('sched.json',1)||[]).some(alarm=>alarm.on&&(alarm.hidden!==false)) ? 24 : 0;
- }
-};
-WIDGETS["alarm"].reload();
diff --git a/apps/novaclock/ChangeLog b/apps/novaclock/ChangeLog
new file mode 100644
index 000000000..8b05ff9ec
--- /dev/null
+++ b/apps/novaclock/ChangeLog
@@ -0,0 +1,3 @@
+...
+0.10: First update with ChangeLog Added
+0.11: Tell clock widgets to hide.
diff --git a/apps/novaclock/app.js b/apps/novaclock/app.js
index e5bd37b06..52bee0dbd 100644
--- a/apps/novaclock/app.js
+++ b/apps/novaclock/app.js
@@ -249,13 +249,13 @@ var open = false;
var timemode = true;
var clockmode;
var novaYPos = -7;
+Bangle.setUI("clock");
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
g.drawImage(nova(), -10, -10, {
scale: 2.2
});
-Bangle.setUI("clock");
g.drawImage(star(), 5, -5, {scale:0.8});
g.drawImage(star(), -10, 120, {scale:0.8});
diff --git a/apps/novaclock/metadata.json b/apps/novaclock/metadata.json
index c1dad60a1..69b7627f8 100644
--- a/apps/novaclock/metadata.json
+++ b/apps/novaclock/metadata.json
@@ -3,7 +3,7 @@
"shortName":"Nova Clock",
"icon": "app.png",
"type": "clock",
- "version":"0.1",
+ "version":"0.11",
"description": "A clock inspired by the Kirby series",
"tags": "clock",
"supports": ["BANGLEJS2"],
diff --git a/apps/numberchaser/app-icon.js b/apps/numberchaser/app-icon.js
new file mode 100644
index 000000000..a4bb5054d
--- /dev/null
+++ b/apps/numberchaser/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwkA/4AC+c/AoYAR+QTTj4DBkYXS+YUB+cSl4YS+P/mUiL4RLQ+chiUziPyAAIXPmJEC+Ui+UhO6QABj8iVKocEACUTC60j+YXWPoZHTkcyC6sibYaPpC63ziXzkYXUkXyO6nykMykIXUl8xmcykQ2BSZ4XBkTXB+LdBMgPyDRnzj8vmf/AIMyDgMjAoIALiQSCVYI0CD4QAL+MTIILFCI4TJOmMRkUzn40CGQRLBMRipBiIABkR2DTAIAQmURF4KcBZScxn5qBACgWWbwUhMCT7CmU/WQQAR+awBF6jdBVggXSPCwXXVAPyJCkimUieKinBI6sxAQIvUeAMyMQIXT+MjI6iNC+RIT+bAB+cSYiZdBMQMzSCkf+ZIUGAMiYKsjkTbVkMSl4A=="))
diff --git a/apps/numberchaser/app.js b/apps/numberchaser/app.js
new file mode 100644
index 000000000..f68119fb2
--- /dev/null
+++ b/apps/numberchaser/app.js
@@ -0,0 +1,104 @@
+var randomNumber;
+var guessNumber = 1;
+
+function mathRandomInt(a, b) {
+ if (a > b) {
+ // Swap a and b to ensure a is smaller.
+ var c = a;
+ a = b;
+ b = c;
+ }
+ return Math.floor(Math.random() * (b - a + 1) + a);
+}
+
+/**
+ * Describe this function...
+ */
+function game() {
+
+ g.drawString('',0,20,true);
+ E.showMenu(numMenu);
+ console.log(randomNumber);
+}
+
+var numMenu = {
+ "" : {
+ "title" : "Number Chaser",
+ },
+ "Guess Number" : {
+ value : guessNumber,
+ min:1,max:100,step:1,
+ onchange : v => { guessNumber=v; }
+ },
+ "OK" : function () {
+ g.clear();
+ if (guessNumber == randomNumber) {
+ //if guess is correct
+ g.setFont("Vector",13);g.setFontAlign(-1,-1);
+ status = "You won! ";
+ gameOver();
+ } else {
+ //if guess is incorrect
+ g.setFont("Vector",13);g.setFontAlign(-1,-1);
+ if (guessNumber > randomNumber) {
+ //Decreases number if guess is greater
+ randomNumber = randomNumber - 1;
+ status = "Too high!";
+ } else if (guessNumber < randomNumber) {
+ //Increases number if guess is lower
+ status = "Too low!";
+ randomNumber = randomNumber + 1;
+ }
+ if (randomNumber < 0 || randomNumber > 100) {
+ //You lose when the number is out of the 1 to 100 range
+ g.setFont("Vector",13);g.setFontAlign(-1,-1);
+ g.drawString('You have lost\nNumber is out\nof range.',10,10,true);
+ status = "You lost!";
+ } else {
+ g.drawString(status+"\nTry again!",10,10);
+ Bangle.on('tap', function() {
+ delay(3000).then(() => game());
+ }
+ );
+ }
+ }
+ }
+};
+
+function gameOver()
+{
+ E.showPrompt(status+'Play again?',{title:""+'Number Chaser'}).then(function(a) {
+ if (a) {
+ randomNumber = mathRandomInt(1, 100);
+ game();
+ } else {
+ load();
+ }
+ }
+ );
+}
+
+function delay(time) {
+ return new Promise(resolve => setTimeout(resolve, time));
+}
+
+function instructions()
+{
+ g.setFont("Vector",13);g.setFontAlign(-1,-1);
+ g.drawString('Guess the number\nbetween 1 and 100.\nGuess too high, it\ndecreases by 1.\nToo low, it increases\nby 1.\nIf the number\ngoes below 0 or\nabove 100, it\nis out of range\nand you have\nlost.',10,10,true);
+ randomNumber = mathRandomInt(1, 100);
+ delay(10000).then(() => game());
+}
+
+
+g.clear();
+E.showPrompt('Do you need instructions?',{title:""+'Number Chaser'}).then(function(a)
+ { if (a) {
+ instructions();
+ } else
+ {
+ randomNumber = mathRandomInt(1, 100);
+ game();
+ }
+ }
+);
diff --git a/apps/numberchaser/metadata.json b/apps/numberchaser/metadata.json
new file mode 100644
index 000000000..f9b6ff4b2
--- /dev/null
+++ b/apps/numberchaser/metadata.json
@@ -0,0 +1,13 @@
+{ "id": "numberchaser",
+ "name": "Number Chaser",
+ "shortName":"Number Chaser",
+ "version":"0.01",
+ "description": "A number guessing game, but the number goes up or down based on if you're guessing too high or too low.",
+ "icon": "numberchaser.png",
+ "tags": "game,fun",
+ "supports": ["BANGLEJS","BANGLEJS2"],
+ "storage": [
+ {"name":"numberchaser.app.js","url":"app.js"},
+ {"name":"numberchaser.img","url":"app-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/numberchaser/numberchaser.png b/apps/numberchaser/numberchaser.png
new file mode 100644
index 000000000..2042cc22b
Binary files /dev/null and b/apps/numberchaser/numberchaser.png differ
diff --git a/apps/openhaystack/ChangeLog b/apps/openhaystack/ChangeLog
new file mode 100644
index 000000000..5560f00bc
--- /dev/null
+++ b/apps/openhaystack/ChangeLog
@@ -0,0 +1 @@
+0.01: New App!
diff --git a/apps/openhaystack/README.md b/apps/openhaystack/README.md
new file mode 100644
index 000000000..e2d5e2212
--- /dev/null
+++ b/apps/openhaystack/README.md
@@ -0,0 +1,18 @@
+# OpenHaystack (AirTag)
+
+Copy a base64 key from https://github.com/seemoo-lab/openhaystack and make your Bangle.js trackable as if it's an AirTag
+
+Based on https://github.com/seemoo-lab/openhaystack/issues/59
+
+## Usage
+
+* Follow the steps on https://github.com/seemoo-lab/openhaystack#how-to-use-openhaystack to install OpenHaystack and get a unique base64 code
+* Click the ≡ icon next to `OpenHaystack (AirTag)`
+* Paste in the base64 code
+* Click `Upload`
+
+## Note
+
+This code changes your Bangle's MAC address, so while it still advertises with
+the same `Bangle.js abcd` name, devices that were previously paired with it
+won't automatically reconnect it until you re-pair.
diff --git a/apps/openhaystack/custom.html b/apps/openhaystack/custom.html
new file mode 100644
index 000000000..f56e94a98
--- /dev/null
+++ b/apps/openhaystack/custom.html
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+ Follow the steps on https://github.com/seemoo-lab/openhaystack to install OpenHaystack and get a unique base64 code
+ Then paste the key in below and click Upload
+
+ Base64 key:
+
+
+ Click Upload
+
+
+
+
+
+
+
diff --git a/apps/openhaystack/icon.png b/apps/openhaystack/icon.png
new file mode 100644
index 000000000..f5e4f7f3b
Binary files /dev/null and b/apps/openhaystack/icon.png differ
diff --git a/apps/openhaystack/metadata.json b/apps/openhaystack/metadata.json
new file mode 100644
index 000000000..5573529f7
--- /dev/null
+++ b/apps/openhaystack/metadata.json
@@ -0,0 +1,14 @@
+{ "id": "openhaystack",
+ "name": "OpenHaystack (AirTag)",
+ "icon": "icon.png",
+ "version":"0.01",
+ "description": "Copy a base64 key from https://github.com/seemoo-lab/openhaystack and make your Bangle.js trackable as if it's an AirTag",
+ "tags": "openhaystack,bluetooth,ble,tracking,airtag",
+ "type": "bootloader",
+ "custom": "custom.html",
+ "readme": "README.md",
+ "supports": ["BANGLEJS","BANGLEJS2"],
+ "storage": [
+ {"name":"openhaystack.boot.js"}
+ ]
+}
diff --git a/apps/openstmap/ChangeLog b/apps/openstmap/ChangeLog
index 6cb9d061e..7f788c139 100644
--- a/apps/openstmap/ChangeLog
+++ b/apps/openstmap/ChangeLog
@@ -10,3 +10,9 @@
0.10: Improve scale factor calculation to fix scaling issues (#984)
0.11: Add slight offset to OSM data to align it properly (fix #984)
Fix alignment of satellite info text
+0.12: switch to using normal OpenStreetMap tiles (opentopomap was too slow)
+0.13: Use a single image file with 'frames' of data (drastically reduces file count, possibility of >1 map on device)
+0.14: Added ability to upload multiple sets of map tiles
+ Support for zooming in on map
+ Satellite count moved to widget bar to leave more room for the map
+0.15: Make track drawing an option (default off)
diff --git a/apps/openstmap/README.md b/apps/openstmap/README.md
new file mode 100644
index 000000000..f19b13bd1
--- /dev/null
+++ b/apps/openstmap/README.md
@@ -0,0 +1,53 @@
+# OpenStreetMap
+
+This app allows you to upload and use OpenSteetMap map tiles onto your
+Bangle. There's an uploader, the app, and also a library that
+allows you to use the maps in your Bangle.js applications.
+
+## Uploader
+
+Once you've installed OpenStreepMap on your Bangle, find it
+in the App Loader and click the Disk icon next to it.
+
+A window will pop up showing what maps you have loaded.
+
+To add a map:
+
+* Click `Add Map`
+* Scroll and zoom to the area of interest or use the Search button in the top left
+* Now choose the size you want to upload (Small/Medium/etc)
+* On Bangle.js 1 you can choose if you want a 3 bits per pixel map (this is lower
+quality but uploads faster and takes less space). On Bangle.js 2 you only have a 3bpp
+display so can only use 3bpp.
+* Click `Get Map`, and a preview will be displayed. If you need to adjust the area you
+can change settings, move the map around, and click `Get Map` again.
+* When you're ready, click `Upload`
+
+## Bangle.js App
+
+The Bangle.js app allows you to view a map - it also turns the GPS on and marks
+the path that you've been travelling (if enabled).
+
+* Drag on the screen to move the map
+* Press the button to bring up a menu, where you can zoom, go to GPS location
+, put the map back in its default location, or choose whether to draw the currently
+recording GPS track (from the `Recorder` app).
+
+**Note:** If enabled, drawing the currently recorded GPS track can take a second
+or two (which happens after you've finished scrolling the screen with your finger).
+
+
+## Library
+
+See the documentation in the library itself for full usage info:
+https://github.com/espruino/BangleApps/blob/master/apps/openstmap/openstmap.js
+
+Or check the app itself: https://github.com/espruino/BangleApps/blob/master/apps/openstmap/app.js
+
+But in the most simple form:
+
+```
+var m = require("openstmap");
+// m.lat/lon are now the center of the loaded map
+m.draw(); // draw centered on the middle of the loaded map
+```
diff --git a/apps/openstmap/app.js b/apps/openstmap/app.js
index 62597ca20..89e2d2ddb 100644
--- a/apps/openstmap/app.js
+++ b/apps/openstmap/app.js
@@ -1,20 +1,31 @@
var m = require("openstmap");
var HASWIDGETS = true;
-var y1,y2;
+var R;
var fix = {};
+var mapVisible = false;
+var hasScrolled = false;
+var settings = require("Storage").readJSON("openstmap.json",1)||{};
+// Redraw the whole page
function redraw() {
- g.setClipRect(0,y1,g.getWidth()-1,y2);
+ g.setClipRect(R.x,R.y,R.x2,R.y2);
m.draw();
drawMarker();
- if (WIDGETS["gpsrec"] && WIDGETS["gpsrec"].plotTrack) {
- g.flip(); // force immediate draw on double-buffered screens - track will update later
- g.setColor(0.75,0.2,0);
- WIDGETS["gpsrec"].plotTrack(m);
+ // if track drawing is enabled...
+ if (settings.drawTrack) {
+ if (HASWIDGETS && WIDGETS["gpsrec"] && WIDGETS["gpsrec"].plotTrack) {
+ g.setColor("#f00").flip(); // force immediate draw on double-buffered screens - track will update later
+ WIDGETS["gpsrec"].plotTrack(m);
+ }
+ if (HASWIDGETS && WIDGETS["recorder"] && WIDGETS["recorder"].plotTrack) {
+ g.setColor("#f00").flip(); // force immediate draw on double-buffered screens - track will update later
+ WIDGETS["recorder"].plotTrack(m);
+ }
}
g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
}
+// Draw the marker for where we are
function drawMarker() {
if (!fix.fix) return;
var p = m.latLonToXY(fix.lat, fix.lon);
@@ -22,50 +33,70 @@ function drawMarker() {
g.fillRect(p.x-2, p.y-2, p.x+2, p.y+2);
}
-var fix;
Bangle.on('GPS',function(f) {
fix=f;
- g.reset().clearRect(0,y1,g.getWidth()-1,y1+8).setFont("6x8").setFontAlign(0,0);
- var txt = fix.satellites+" satellites";
- if (!fix.fix)
- txt += " - NO FIX";
- g.drawString(txt,g.getWidth()/2,y1 + 4);
- drawMarker();
+ if (HASWIDGETS) WIDGETS["sats"].draw(WIDGETS["sats"]);
+ if (mapVisible) drawMarker();
});
Bangle.setGPSPower(1, "app");
if (HASWIDGETS) {
Bangle.loadWidgets();
+ WIDGETS["sats"] = { area:"tl", width:48, draw:w=>{
+ var txt = (0|fix.satellites)+" Sats";
+ if (!fix.fix) txt += "\nNO FIX";
+ g.reset().setFont("6x8").setFontAlign(0,0)
+ .drawString(txt,w.x+24,w.y+12);
+ }
+ };
Bangle.drawWidgets();
- y1 = 24;
- var hasBottomRow = Object.keys(WIDGETS).some(w=>WIDGETS[w].area[0]=="b");
- y2 = g.getHeight() - (hasBottomRow ? 24 : 1);
-} else {
- y1=0;
- y2=g.getHeight()-1;
}
+R = Bangle.appRect;
-redraw();
-
-function recenter() {
- if (!fix.fix) return;
- m.lat = fix.lat;
- m.lon = fix.lon;
+function showMap() {
+ mapVisible = true;
+ g.reset().clearRect(R);
redraw();
+ Bangle.setUI({mode:"custom",drag:e=>{
+ if (e.b) {
+ g.setClipRect(R.x,R.y,R.x2,R.y2);
+ g.scroll(e.dx,e.dy);
+ m.scroll(e.dx,e.dy);
+ g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
+ hasScrolled = true;
+ } else if (hasScrolled) {
+ hasScrolled = false;
+ redraw();
+ }
+ }, btn: btn=>{
+ mapVisible = false;
+ var menu = {"":{title:"Map"},
+ "< Back": ()=> showMap(),
+ /*LANG*/"Zoom In": () =>{
+ m.scale /= 2;
+ showMap();
+ },
+ /*LANG*/"Zoom Out": () =>{
+ m.scale *= 2;
+ showMap();
+ },
+ /*LANG*/"Draw Track": {
+ value : !!settings.drawTrack,
+ onchange : v => { settings.drawTrack=v; require("Storage").writeJSON("openstmap.json",settings); }
+ },
+ /*LANG*/"Center Map": () =>{
+ m.lat = m.map.lat;
+ m.lon = m.map.lon;
+ m.scale = m.map.scale;
+ showMap();
+ }};
+ if (fix.fix) menu[/*LANG*/"Center GPS"]=() =>{
+ m.lat = fix.lat;
+ m.lon = fix.lon;
+ showMap();
+ };
+ E.showMenu(menu);
+ }});
}
-setWatch(recenter, global.BTN2?BTN2:BTN1, {repeat:true});
-
-var hasScrolled = false;
-Bangle.on('drag',e=>{
- if (e.b) {
- g.setClipRect(0,y1,g.getWidth()-1,y2);
- g.scroll(e.dx,e.dy);
- m.scroll(e.dx,e.dy);
- g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
- hasScrolled = true;
- } else if (hasScrolled) {
- hasScrolled = false;
- redraw();
- }
-});
+showMap();
diff --git a/apps/openstmap/custom.html b/apps/openstmap/interface.html
similarity index 51%
rename from apps/openstmap/custom.html
rename to apps/openstmap/interface.html
index 6e79a6e9a..0bf2268a4 100644
--- a/apps/openstmap/custom.html
+++ b/apps/openstmap/interface.html
@@ -9,7 +9,8 @@
padding: 0;
margin: 0;
}
- html, body, #map {
+ html, body, #map, #mapsLoaded, #mapContainer {
+ position: relative;
height: 100%;
width: 100%;
}
@@ -27,24 +28,46 @@
width: 256px;
height: 256px;
}
+ .tile-title {
+ font-weight:bold;
+ font-size: 125%;
+ }
+ .tile-map {
+ width: 128px;
+ height: 128px;
+ }
-
+
-
-
3 bit
-
Get Map
-
-
Upload
- Cancel
+
+
+
+
+
3 bit
+
+
+ Small (4x4)
+ Medium (5x5)
+ Large (7x7)
+ XL (10x10)
+ XXL (15x15)
+
+
+
Get Map Map List
+
+
Upload
+ Cancel
+
+
-
+
-
-
+
+
diff --git a/apps/openstmap/metadata.json b/apps/openstmap/metadata.json
index 2dc9bd427..819dc4122 100644
--- a/apps/openstmap/metadata.json
+++ b/apps/openstmap/metadata.json
@@ -2,17 +2,21 @@
"id": "openstmap",
"name": "OpenStreetMap",
"shortName": "OpenStMap",
- "version": "0.11",
+ "version": "0.15",
"description": "Loads map tiles from OpenStreetMap onto your Bangle.js and displays a map of where you are. Once installed this also adds map functionality to `GPS Recorder` and `Recorder` apps",
+ "readme": "README.md",
"icon": "app.png",
"tags": "outdoors,gps,osm",
"supports": ["BANGLEJS","BANGLEJS2"],
"screenshots": [{"url":"screenshot.png"}],
- "custom": "custom.html",
- "customConnect": true,
+ "interface": "interface.html",
"storage": [
{"name":"openstmap","url":"openstmap.js"},
{"name":"openstmap.app.js","url":"app.js"},
{"name":"openstmap.img","url":"app-icon.js","evaluate":true}
+ ], "data": [
+ {"name":"openstmap.json"},
+ {"wildcard":"openstmap.*.json"},
+ {"wildcard":"openstmap.*.img"}
]
}
diff --git a/apps/openstmap/openstmap.js b/apps/openstmap/openstmap.js
index d995aca25..692344357 100644
--- a/apps/openstmap/openstmap.js
+++ b/apps/openstmap/openstmap.js
@@ -20,34 +20,59 @@ function center() {
m.draw();
}
+// you can even change the scale - eg 'm/scale *= 2'
+
*/
-var map = require("Storage").readJSON("openstmap.json");
-map.center = Bangle.project({lat:map.lat,lon:map.lon});
-exports.map = map;
-exports.lat = map.lat; // actual position of middle of screen
-exports.lon = map.lon; // actual position of middle of screen
var m = exports;
+m.maps = require("Storage").list(/openstmap\.\d+\.json/).map(f=>{
+ let map = require("Storage").readJSON(f);
+ map.center = Bangle.project({lat:map.lat,lon:map.lon});
+ return map;
+});
+// we base our start position on the middle of the first map
+m.map = m.maps[0];
+m.scale = m.map.scale; // current scale (based on first map)
+m.lat = m.map.lat; // position of middle of screen
+m.lon = m.map.lon; // position of middle of screen
exports.draw = function() {
- var s = require("Storage");
var cx = g.getWidth()/2;
var cy = g.getHeight()/2;
var p = Bangle.project({lat:m.lat,lon:m.lon});
- var ix = (p.x-map.center.x)/map.scale + (map.imgx/2) - cx;
- var iy = (map.center.y-p.y)/map.scale + (map.imgy/2) - cy;
- //console.log(ix,iy);
- var tx = 0|(ix/map.tilesize);
- var ty = 0|(iy/map.tilesize);
- var ox = (tx*map.tilesize)-ix;
- var oy = (ty*map.tilesize)-iy;
- for (var x=ox,ttx=tx;x
{
+ var d = map.scale/m.scale;
+ var ix = (p.x-map.center.x)/m.scale + (map.imgx*d/2) - cx;
+ var iy = (map.center.y-p.y)/m.scale + (map.imgy*d/2) - cy;
+ var o = {};
+ var s = map.tilesize;
+ if (d!=1) { // if the two are different, add scaling
+ s *= d;
+ o.scale = d;
}
- }
+ //console.log(ix,iy);
+ var tx = 0|(ix/s);
+ var ty = 0|(iy/s);
+ var ox = (tx*s)-ix;
+ var oy = (ty*s)-iy;
+ var img = require("Storage").read(map.fn);
+ // fix out of range so we don't have to iterate over them
+ if (tx<0) {
+ ox+=s*-tx;
+ tx=0;
+ }
+ if (ty<0) {
+ oy+=s*-ty;
+ ty=0;
+ }
+ var mx = g.getWidth();
+ var my = g.getHeight();
+ for (var x=ox,ttx=tx; x {
+ if (!waiting){
+ waiting = true;
+ require("owmweather").pull(completion);
+ }
+ }, 5000);
+ }
+ setInterval(() => {
+ if (!waiting && NRF.getSecurityStatus().connected){
+ waiting = true;
+ require("owmweather").pull(completion);
+ }
+ }, settings.refresh * 1000 * 60);
+ }
+}
diff --git a/apps/owmweather/default.json b/apps/owmweather/default.json
new file mode 100644
index 000000000..9d8998867
--- /dev/null
+++ b/apps/owmweather/default.json
@@ -0,0 +1 @@
+{"enabled":false,"refresh":180}
diff --git a/apps/owmweather/interface.html b/apps/owmweather/interface.html
new file mode 100644
index 000000000..3f9467a83
--- /dev/null
+++ b/apps/owmweather/interface.html
@@ -0,0 +1,63 @@
+
+
+
+
+
+ Set OpenWeatherMap (OWM) API key
+ Save key
+
+ Where to get your personal API key?
+ Go to https://home.openweathermap.org/users/sign_up and sign up for a free account.
+ After registration you can login and optain your personal API key.
+
+
+
+
+
+
+
diff --git a/apps/owmweather/lib.js b/apps/owmweather/lib.js
new file mode 100644
index 000000000..6ba52b498
--- /dev/null
+++ b/apps/owmweather/lib.js
@@ -0,0 +1,53 @@
+function parseWeather(response) {
+ let owmData = JSON.parse(response);
+
+ let isOwmData = owmData.coord && owmData.weather && owmData.main;
+
+ if (isOwmData) {
+ let json = require("Storage").readJSON('weather.json') || {};
+ let weather = {};
+ weather.time = Date.now();
+ weather.hum = owmData.main.humidity;
+ weather.temp = owmData.main.temp;
+ weather.code = owmData.weather[0].id;
+ weather.wdir = owmData.wind.deg;
+ weather.wind = owmData.wind.speed;
+ weather.loc = owmData.name;
+ weather.txt = owmData.weather[0].main;
+
+ if (weather.wdir != null) {
+ let deg = weather.wdir;
+ while (deg < 0 || deg > 360) {
+ deg = (deg + 360) % 360;
+ }
+ weather.wrose = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw', 'n'][Math.floor((deg + 22.5) / 45)];
+ }
+
+ json.weather = weather;
+ require("Storage").writeJSON('weather.json', json);
+ require("weather").emit("update", json.weather);
+ return undefined;
+ } else {
+ return /*LANG*/"Not OWM data";
+ }
+}
+
+exports.pull = function(completionCallback) {
+ let location = require("Storage").readJSON("mylocation.json", 1) || {
+ "lat": 51.50,
+ "lon": 0.12,
+ "location": "London"
+ };
+ let settings = require("Storage").readJSON("owmweather.json", 1);
+ let uri = "https://api.openweathermap.org/data/2.5/weather?lat=" + location.lat.toFixed(2) + "&lon=" + location.lon.toFixed(2) + "&exclude=hourly,daily&appid=" + settings.apikey;
+ if (Bangle.http){
+ Bangle.http(uri, {timeout:10000}).then(event => {
+ let result = parseWeather(event.resp);
+ if (completionCallback) completionCallback(result);
+ }).catch((e)=>{
+ if (completionCallback) completionCallback(e);
+ });
+ } else {
+ if (completionCallback) completionCallback(/*LANG*/"No http method found");
+ }
+};
diff --git a/apps/owmweather/metadata.json b/apps/owmweather/metadata.json
new file mode 100644
index 000000000..56f9afca7
--- /dev/null
+++ b/apps/owmweather/metadata.json
@@ -0,0 +1,22 @@
+{ "id": "owmweather",
+ "name": "OpenWeatherMap weather provider",
+ "shortName":"OWM Weather",
+ "version":"0.02",
+ "description": "Pulls weather from OpenWeatherMap (OWM) API",
+ "icon": "app.png",
+ "type": "bootloader",
+ "tags": "boot,tool,weather",
+ "supports" : ["BANGLEJS2"],
+ "interface": "interface.html",
+ "readme": "README.md",
+ "data": [
+ {"name":"owmweather.json"},
+ {"name":"weather.json"}
+ ],
+ "storage": [
+ {"name":"owmweather.default.json","url":"default.json"},
+ {"name":"owmweather.boot.js","url":"boot.js"},
+ {"name":"owmweather","url":"lib.js"},
+ {"name":"owmweather.settings.js","url":"settings.js"}
+ ]
+}
diff --git a/apps/owmweather/settings.js b/apps/owmweather/settings.js
new file mode 100644
index 000000000..a4d21dd7c
--- /dev/null
+++ b/apps/owmweather/settings.js
@@ -0,0 +1,84 @@
+(function(back) {
+ function writeSettings(key, value) {
+ var s = require('Storage').readJSON(FILE, true) || {};
+ s[key] = value;
+ require('Storage').writeJSON(FILE, s);
+ readSettings();
+ }
+
+ function readSettings(){
+ settings = Object.assign(
+ require('Storage').readJSON("owmweather.default.json", true) || {},
+ require('Storage').readJSON(FILE, true) || {}
+ );
+ }
+
+ var FILE="owmweather.json";
+ var settings;
+ readSettings();
+
+ function buildMainMenu(){
+ var mainmenu = {
+ '': { 'title': 'OWM weather' },
+ '< Back': back,
+ "Enabled": {
+ value: !!settings.enabled,
+ onchange: v => {
+ writeSettings("enabled", v);
+ }
+ },
+ "Refresh every": {
+ value: settings.refresh / 60,
+ min: 1,
+ max: 48,
+ step: 1,
+ format: v=>v+"h",
+ onchange: v => {
+ writeSettings("refresh",Math.round(v * 60));
+ }
+ },
+ "Force refresh": ()=>{
+ if (!settings.apikey){
+ E.showAlert("API key is needed","Hint").then(()=>{
+ E.showMenu(buildMainMenu());
+ });
+ } else {
+ E.showMessage("Reloading weather");
+ require("owmweather").pull((e)=>{
+ if (e) {
+ E.showAlert(e,"Error").then(()=>{
+ E.showMenu(buildMainMenu());
+ });
+ } else {
+ E.showAlert("Success").then(()=>{
+ E.showMenu(buildMainMenu());
+ });
+ }
+ });
+ }
+ }
+ };
+
+ mainmenu["API key"] = function (){
+ if (require("textinput")){
+ require("textinput").input({text:settings.apikey}).then(result => {
+ if (result != "") {
+ print("Result is", result);
+ settings.apikey = result;
+ writeSettings("apikey",result);
+ }
+ E.showMenu(buildMainMenu());
+ });
+ } else {
+ E.showPrompt("Install a text input lib"),then(()=>{
+ E.showMenu(buildMainMenu());
+ });
+ }
+ };
+
+
+ return mainmenu;
+ }
+
+ E.showMenu(buildMainMenu());
+});
diff --git a/apps/palikkainen/ChangeLog b/apps/palikkainen/ChangeLog
new file mode 100644
index 000000000..5560f00bc
--- /dev/null
+++ b/apps/palikkainen/ChangeLog
@@ -0,0 +1 @@
+0.01: New App!
diff --git a/apps/palikkainen/README.md b/apps/palikkainen/README.md
new file mode 100644
index 000000000..81d857209
--- /dev/null
+++ b/apps/palikkainen/README.md
@@ -0,0 +1,7 @@
+# Palikkainen
+
+By Jukio Kallio
+
+A minimal watch face consisting of blocks. Minutes fills the blocks, and after 12 hours it starts to empty them.
+
+
diff --git a/apps/palikkainen/app-icon.js b/apps/palikkainen/app-icon.js
new file mode 100644
index 000000000..a99602121
--- /dev/null
+++ b/apps/palikkainen/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwkBiIA0/4AKCpMfCxYAB+ItTGJQuOGBAWPGAwuQGAwXvCyJgFC+PwgAAEh4X/C/6//A4gX/C/6//A4QX/C/6/vC6sfCyPxC+ZgSCwgwRFwowRCwwwPFw4xOCpIArA"))
diff --git a/apps/palikkainen/app.js b/apps/palikkainen/app.js
new file mode 100644
index 000000000..42013af69
--- /dev/null
+++ b/apps/palikkainen/app.js
@@ -0,0 +1,184 @@
+// Palikkainen
+//
+// Bangle.js 2 watch face
+// by Jukio Kallio
+// www.jukiokallio.com
+
+require("Font6x8").add(Graphics);
+
+// settings
+const watch = {
+ x:0, y:0, w:0, h:0,
+ bgcolor:g.theme.bg,
+ fgcolor:g.theme.fg,
+ font: "6x8", fontsize: 1,
+ finland:true, // change if you want Finnish style date, or US style
+};
+
+// set some additional settings
+watch.w = g.getWidth(); // size of the background
+watch.h = g.getHeight();
+watch.x = watch.w * 0.5; // position of the circles
+watch.y = watch.h * 0.45;
+
+const dateWeekday = { 0: "SUN", 1: "MON", 2: "TUE", 3: "WED", 4:"THU", 5:"FRI", 6:"SAT" }; // weekdays
+
+var wait = 60000; // wait time, normally a minute
+
+
+// timeout used to update every minute
+var drawTimeout;
+
+// schedule a draw for the next minute
+function queueDraw() {
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = setTimeout(function() {
+ drawTimeout = undefined;
+ draw();
+ }, wait - (Date.now() % wait));
+}
+
+
+// main function
+function draw() {
+ // make date object
+ var date = new Date();
+
+ // work out the date string
+ var dateDay = date.getDate();
+ var dateMonth = date.getMonth() + 1;
+ var dateYear = date.getFullYear();
+ var dateStr = dateWeekday[date.getDay()] + " " + dateMonth + "." + dateDay + "." + dateYear;
+ if (watch.finland) dateStr = dateWeekday[date.getDay()] + " " + dateDay + "." + dateMonth + "." + dateYear; // the true way of showing date
+
+ // Reset the state of the graphics library
+ g.reset();
+
+ // Clear the area where we want to draw the time
+ g.setColor(watch.bgcolor);
+ g.fillRect(0, 0, watch.w, watch.h);
+
+ // setup watch face
+ const block = {
+ w: watch.w / 2 - 6,
+ h: 18,
+ pad: 4,
+ };
+
+ // get hours and minutes
+ var hour = date.getHours();
+ var minute = date.getMinutes();
+
+ // calculate size of the block face
+ var facew = block.w * 2 + block.pad;
+ var faceh = (block.h + block.pad) * 6;
+
+
+ // loop through first 12 hours and draw blocks accordingly
+ g.setColor(watch.fgcolor); // set foreground color
+
+ for (var i = 0; i < 12; i++) {
+ // where to draw
+ var x = watch.x - facew / 2; // starting position
+ var y = watch.y + faceh / 2 - block.h - block.pad / 2; // draw blocks from bottom up
+ if (i > 5) {
+ // second column
+ x += block.w + block.pad;
+ y -= (block.h + block.pad) * (i - 6);
+ } else {
+ // first column
+ x += 0;
+ y -= (block.h + block.pad) * i;
+ }
+
+ if (i < hour) {
+ // draw full hour block
+ g.fillRect(x, y, x + block.w, y + block.h);
+ } else if (i == hour) {
+ // draw minutes
+ g.fillRect(x, y, x + block.w * (minute / 60), y + block.h);
+
+ // minute reading help
+ for (var m = 1; m < 12; m++) {
+ // set color
+ if (m * 5 < minute) g.setColor(watch.bgcolor); else g.setColor(watch.fgcolor);
+
+ var mlineh = 1; // minute line height
+ if (m == 3 || m == 6 || m == 9) mlineh = 3; // minute line height at 15, 30 and 45 minutes
+
+ g.drawLine(x + (block.w / 12 * m), y + block.h / 2 - mlineh, x + (block.w / 12 * m), y + block.h / 2 + mlineh);
+ }
+ }
+ }
+
+
+ // loop through second 12 hours and draw blocks accordingly
+ if (hour >= 12) {
+ g.setColor(watch.bgcolor); // set foreground color
+
+ for (var i2 = 0; i2 < 12; i2++) {
+ // where to draw
+ var x2 = watch.x - facew / 2; // starting position
+ var y2 = watch.y + faceh / 2 - block.h - block.pad / 2; // draw blocks from bottom up
+ if (i2 > 5) {
+ // second column
+ x2 += block.w + block.pad;
+ y2 -= (block.h + block.pad) * (i2 - 6);
+ } else {
+ // first column
+ x2 += 0;
+ y2 -= (block.h + block.pad) * i2;
+ }
+
+ if (i2 < hour % 12) {
+ // draw full hour block
+ g.fillRect(x2, y2, x2 + block.w, y2 + block.h);
+ } else if (i2 == hour % 12) {
+ // draw minutes
+ g.fillRect(x2, y2, x2 + block.w * (minute / 60), y2 + block.h);
+
+ // minute reading help
+ for (var m2 = 1; m2 < 12; m2++) {
+ // set color
+ if (m2 * 5 < minute) g.setColor(watch.fgcolor); else g.setColor(watch.bgcolor);
+
+ var mlineh2 = 1; // minute line height
+ if (m2 == 3 || m2 == 6 || m2 == 9) mlineh2 = 3; // minute line height at 15, 30 and 45 minutes
+
+ g.drawLine(x2 + (block.w / 12 * m2), y2 + block.h / 2 - mlineh2, x2 + (block.w / 12 * m2), y2 + block.h / 2 + mlineh2);
+ }
+ }
+ }
+ }
+
+
+ // draw date
+ var datey = 11;
+ g.setFontAlign(0,-1).setFont(watch.font, watch.fontsize).setColor(watch.fgcolor);
+ g.drawString(dateStr, watch.x, watch.y + faceh / 2 + datey);
+
+
+ // queue draw
+ queueDraw();
+}
+
+
+// Clear the screen once, at startup
+g.clear();
+// draw immediately at first
+draw();
+
+
+// Stop updates when LCD is off, restart when on
+Bangle.on('lcdPower',on=>{
+ if (on) {
+ draw(); // draw immediately, queue redraw
+ } else { // stop draw timer
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = undefined;
+ }
+});
+
+
+// Show launcher when middle button pressed
+Bangle.setUI("clock");
diff --git a/apps/palikkainen/app.png b/apps/palikkainen/app.png
new file mode 100644
index 000000000..142d429e9
Binary files /dev/null and b/apps/palikkainen/app.png differ
diff --git a/apps/palikkainen/metadata.json b/apps/palikkainen/metadata.json
new file mode 100644
index 000000000..4ed8be817
--- /dev/null
+++ b/apps/palikkainen/metadata.json
@@ -0,0 +1,16 @@
+{ "id": "palikkainen",
+ "name": "Palikkainen - A blocky watch face",
+ "shortName":"Palikkainen",
+ "version":"0.01",
+ "description": "A minimal watch face consisting of blocks.",
+ "icon": "app.png",
+ "screenshots": [{"url":"screenshot1.png"}],
+ "type": "clock",
+ "tags": "clock",
+ "supports" : ["BANGLEJS","BANGLEJS2"],
+ "readme": "README.md",
+ "storage": [
+ {"name":"palikkainen.app.js","url":"app.js"},
+ {"name":"palikkainen.img","url":"app-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/palikkainen/screenshot1.png b/apps/palikkainen/screenshot1.png
new file mode 100644
index 000000000..43a630d59
Binary files /dev/null and b/apps/palikkainen/screenshot1.png differ
diff --git a/apps/pastel/ChangeLog b/apps/pastel/ChangeLog
index f4640426b..02cef7774 100644
--- a/apps/pastel/ChangeLog
+++ b/apps/pastel/ChangeLog
@@ -18,3 +18,5 @@
added setting to enable/disable idle timer warning
0.16: make check_idle boolean setting work properly with new B2 menu
0.17: Use default Bangle formatter for booleans
+0.18: fix idle option always getting defaulted to true
+0.19: Use 'modules/suncalc.js' to avoid it being copied 8 times for different apps
diff --git a/apps/pastel/metadata.json b/apps/pastel/metadata.json
index 1fe176d5f..cf4bbbe9b 100644
--- a/apps/pastel/metadata.json
+++ b/apps/pastel/metadata.json
@@ -2,7 +2,7 @@
"id": "pastel",
"name": "Pastel Clock",
"shortName": "Pastel",
- "version": "0.17",
+ "version": "0.19",
"description": "A Configurable clock with custom fonts, background and weather display. Has a cyclic information line that includes, day, date, battery, sunrise and sunset times",
"icon": "pastel.png",
"dependencies": {"mylocation":"app","weather":"app"},
diff --git a/apps/pastel/pastel.app.js b/apps/pastel/pastel.app.js
index 605b78ad0..bc41588d8 100644
--- a/apps/pastel/pastel.app.js
+++ b/apps/pastel/pastel.app.js
@@ -1,4 +1,4 @@
-var SunCalc = require("https://raw.githubusercontent.com/mourner/suncalc/master/suncalc.js");
+var SunCalc = require("suncalc"); // from modules folder
require("f_latosmall").add(Graphics);
const storage = require('Storage');
const locale = require("locale");
@@ -34,7 +34,7 @@ function loadSettings() {
settings = require("Storage").readJSON(SETTINGS_FILE,1)||{};
settings.grid = settings.grid||false;
settings.font = settings.font||"Lato";
- settings.idle_check = settings.idle_check||true;
+ settings.idle_check = (settings.idle_check === undefined ? true : settings.idle_check);
}
// requires the myLocation app
@@ -85,7 +85,7 @@ function getSteps() {
try {
return Bangle.getHealthStatus("day").steps;
} catch (e) {
- if (WIDGETS.wpedom !== undefined)
+ if (WIDGETS.wpedom !== undefined)
return WIDGETS.wpedom.getSteps();
else
return '???';
@@ -181,12 +181,12 @@ function drawClock() {
var d = new Date();
var da = d.toString().split(" ");
var time = da[4].substr(0,5);
-
+
var hh = da[4].substr(0,2);
var mm = da[4].substr(3,2);
var day = da[0];
var month_day = da[1] + " " + da[2];
-
+
// fix hh for 12hr clock
var h2 = "0" + parseInt(hh) % 12 || 12;
if (parseInt(hh) > 12)
@@ -215,12 +215,12 @@ function drawClock() {
g.reset();
g.setColor(g.theme.bg);
g.fillRect(Bangle.appRect);
-
+
// draw a grid like graph paper
if (settings.grid && process.env.HWVERSION !=1) {
g.setColor("#0f0");
for (var gx=20; gx <= w; gx += 20)
- g.drawLine(gx, 30, gx, h - 24);
+ g.drawLine(gx, 30, gx, h - 24);
for (var gy=30; gy <= h - 24; gy += 20)
g.drawLine(0, gy, w, gy);
}
@@ -238,7 +238,7 @@ function drawClock() {
g.drawString( (w_wind.split(' ').slice(0, 2).join(' ')), (w/2) + 6, 24 + ((y - 24)/2));
// display first 2 words of the wind string eg '4 mph'
}
-
+
if (settings.font == "Architect")
g.setFontArchitect();
else if (settings.font == "GochiHand")
@@ -253,7 +253,7 @@ function drawClock() {
g.setFontSpecialElite();
else
g.setFontLato();
-
+
g.setFontAlign(1,-1); // right aligned
g.drawString(hh, x - 6, y);
g.setFontAlign(-1,-1); // left aligned
@@ -310,7 +310,7 @@ function BUTTON(name,x,y,w,h,c,f,tx) {
// if pressed the callback
BUTTON.prototype.check = function(x,y) {
//console.log(this.name + ":check() x=" + x + " y=" + y +"\n");
-
+
if (x>= this.x && x<= (this.x + this.w) && y>= this.y && y<= (this.y + this.h)) {
log_debug(this.name + ":callback\n");
this.callback();
@@ -366,7 +366,7 @@ function checkIdle() {
warned = false;
return;
}
-
+
let hour = (new Date()).getHours();
let active = (hour >= 9 && hour < 21);
//let active = true;
@@ -397,7 +397,7 @@ function buzzer(n) {
if (n-- < 1) return;
Bangle.buzz(250);
-
+
if (buzzTimeout) clearTimeout(buzzTimeout);
buzzTimeout = setTimeout(function() {
buzzTimeout = undefined;
diff --git a/apps/pebble/ChangeLog b/apps/pebble/ChangeLog
index 274c34a34..9c21e09bc 100644
--- a/apps/pebble/ChangeLog
+++ b/apps/pebble/ChangeLog
@@ -8,3 +8,6 @@
0.08: Add theme options and optional lock symbol
0.09: Add support for internationalization (LANG placeholders + "locale" module)
Get steps from built-in step counter (widpedom no more needed, fix #1697)
+0.10: Tell clock widgets to hide.
+0.11: Swipe down to see widgets
+ Support for fast loading
diff --git a/apps/pebble/metadata.json b/apps/pebble/metadata.json
index f3c1fcc12..0ccb8e2af 100644
--- a/apps/pebble/metadata.json
+++ b/apps/pebble/metadata.json
@@ -2,7 +2,7 @@
"id": "pebble",
"name": "Pebble Clock",
"shortName": "Pebble",
- "version": "0.09",
+ "version": "0.11",
"description": "A pebble style clock to keep the rebellion going",
"readme": "README.md",
"icon": "pebble.png",
diff --git a/apps/pebble/pebble.app.js b/apps/pebble/pebble.app.js
index 774b24c3f..48acbae87 100644
--- a/apps/pebble/pebble.app.js
+++ b/apps/pebble/pebble.app.js
@@ -1,22 +1,24 @@
Graphics.prototype.setFontLECO1976Regular42 = function(scale) {
// Actual height 42 (41 - 0)
g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/AAAAAAAAH/AAAAAAAAH/AAAAAAAAH/AAAAAAAAH/AAAAAAAAH/AAAAAAAAH/AAAAAAAAH/AAAAAAAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAAAA/AAAAAAAAH/AAAAAAAA//AAAAAAAP//AAAAAAB///AAAAAAP///AAAAAB////AAAAAf////AAAAD////4AAAAf////AAAAH////4AAAA////+AAAAA////wAAAAA///+AAAAAA///gAAAAAA//8AAAAAAA//gAAAAAAA/4AAAAAAAA/AAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAAAAAAAH/AAAAAAAAH/AAAAAAAAH/AAAAAAAAH/AAAAAAAAH/AAAAAAAAH/AAAAAAAAH/AAAAAAAAH/AAAAAAAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//h////AAA//h////AAA//h////AAA//h////AAA//h////AAA//h////AAA//h////AAA//h////AAA//h////AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA////wH/AAA////wH/AAA////wH/AAA////wH/AAA////wH/AAA////wH/AAA////wH/AAA////wH/AAA////gD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4AAAH/AAA/4B/gH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////wAAAAA////wAAAAA////wAAAAA////wAAAAA////wAAAAA////wAAAAA////wAAAAA////wAAAAA////wAAAAAAAB/wAAAAAAAB/wAAAAAAAB/wAAAAAAAB/wAAAAAAAB/wAAAAAAAB/wAAAAAAAB/wAAAAAAAB/wAAAAAAAB/wAAAAAAAB/wAAAAAAAB/wAAAAAAAB/wAAAAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////x//AAA////x//AAA////x//AAA////x//AAA////x//AAA////x//AAA////x//AAA////x//AAA////x//AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B////AAA/4B////AAA/4B////AAA/4B////AAA/4B////AAA/4B////AAA/4B////AAA/4B////AAA/wB////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B////AAA/4B////AAA/4B////AAA/4B////AAA/4B////AAA/4B////AAA/4B////AAA/4B////AAA/wB////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//gAAAAAAA//gAAAAAAA//gAAAAAAA//gAAAAAAA//gAAAAAAA//gAAAAAAA//gAAAAAAA//gAAAAAAA//gAAAAAAA/4AAAAAAAA/4AAAAAAAA/4AAAAAAAA/4AAAAAAAA/4AAAAAAAA/4AAAAAAAA/4AAAAAAAA/4AAAAAAAA/4AAAAAAAA/4AAAAAAAA/4AAAAAAAA/4AAAAAAAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////wH/AAA////wH/AAA////wH/AAA////wH/AAA////wH/AAA////wH/AAA////wH/AAA////wH/AAA////wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA/4B/wH/AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAA///////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+AAH/AAAAP+AAH/AAAAP+AAH/AAAAP+AAH/AAAAP+AAH/AAAAP+AAH/AAAAP+AAH/AAAAP+AAH/AAAAH+AAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), 46, atob("ERkmHyYmJiYmJCYmEQ=="), 60+(scale<<8)+(1<<16));
-}
+};
Graphics.prototype.setFontLECO1976Regular22 = function(scale) {
// Actual height 22 (21 - 0)
g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/nA/+cD/5wP/nAAAAAAAAPwAA/gAD+AAPwAAAAAD+AAP4AA/gAAAAAAAAAAAAAcOAP//A//8D//wP//AHDgAcOAP//A//8D//wP//AHDgAAAAAAAAH/jgf+OB/44H/jj8OP/w4//Dj/8OPxw/4HD/gcP+Bw/4AAAAAAAP+AA/8AD/wQOHHA4c8D//wP/8A//gAD4AAfAAH/8A//wP//A84cDjhwIP/AA/8AB/wAAAAAAAD//wP//A//8D//wOHHA4ccDhxwOHHA4f8Dh/wOH/A4f8ABwAAAAAAAAD8AAP4AA/gAD8AAAAAAAAAAAEAAD+AB//A///v/D//gB/wABwAAAAAADgAA/wAf/4P8///wf/4AP8AAOAAAAAAAAAyAAHcAAPwAD/gAP/AA/8AA/AAH8AAMwAAAAAAAAAAAAADgAAOAAA4AAf8AD/wAP/AA/8AAOAAA4AADgAAAAAAAAAAD8AAfwAB/AAD8AAAAAAAADgAAOAAA4AADgAAOAAA4AADgAAAAAAAAAADgAAOAAA4AADgAAAAAAAAABwAB/AA/8A//gP/gA/wADwAAIAAAAAAD//wP//A//8D//wOAHA4AcDgBwOAHA//8D//wP//A//8AAAAAAAA4AcDgBwOAHA//8D//wP//A//8AABwAAHAAAcAAAAAAAA+f8D5/wPn/A+f8DhxwOHHA4ccDhxwP/HA/8cD/xwP/HAAAAAAAAOAHA4AcDhxwOHHA4ccDhxwOHHA4ccD//wP//A//8D//wAAAAAAAD/wAP/AA/8AD/wAAHAAAcAABwAAHAA//8D//wP//A//8AAAAAAAA/98D/3wP/fA/98DhxwOHHA4ccDhxwOH/A4f8Dh/wOH/AAAAAAAAP//A//8D//wP//A4ccDhxwOHHA4ccDh/wOH/A4f8Dh/wAAAAAAAD4AAPgAA+AADgAAOAAA4AADgAAP//A//8D//wP//AAAAAAAAP//A//8D//wP//A4ccDhxwOHHA4ccD//wP//A//8D//wAAAAAAAD/xwP/HA/8cD/xwOHHA4ccDhxwOHHA//8D//wP//A//8AAAAAAAAOA4A4DgDgOAOA4AAAAAAAAOA/A4H8DgfwOA/AAAAAAAAB4AAPwAA/AAD8AAf4ABzgAPPAA8cAHh4AAAAAAAAAAAAHHAAccABxwAHHAAccABxwAHHAAccABxwAHHAAAAAAAAAOHAA4cADzwAPPAAf4AB/gAD8AAPwAAeAAB4AAAAAAAAA+AAD4AAPgAA+ecDh9wOH3A4fcDhwAP/AA/8AD/wAP/AAAAAAAAAP//4///j//+P//44ADjn/OOf845/zjnHOP8c4//zj//OP/84AAAAAAAP//A//8D//wP//A4cADhwAOHAA4cAD//wP//A//8D//wAAAAAAAD//wP//A//8D//wOHHA4ccDhxwOHHA//8D//wP9/A/j8AAAAAAAA//8D//wP//A//8DgBwOAHA4AcDgBwOAHA4AcDgBwOAHAAAAAAAAP//A//8D//wP//A4AcDgBwOAHA8A8D//wH/+AP/wAf+AAAAAAAAD//wP//A//8D//wOHHA4ccDhxwOHHA4ccDhxwOAHA4AcAAAAAAAA//8D//wP//A//8DhwAOHAA4cADhwAOHAA4cADgAAOAAAAAAD//wP//A//8D//wOAHA4ccDhxwOHHA4f8Dh/wOH/A4f8AAAAAAAA//8D//wP//A//8ABwAAHAAAcAABwAP//A//8D//wP//AAAAAAAAP//A//8D//wP//AAAAAAAAOAHA4AcDgBwOAHA4AcDgBwOAHA//8D//wP//A//8AAAAAAAA//8D//wP//A//8AHwAA/AAP8AB/wAPn/A8f8DB/wIH/AAAAAAAAP//A//8D//wP//AAAcAABwAAHAAAcAABwAAHAAAAAAAAP//A//8D//wP//Af8AAP+AAH/AAD8AAHwAD/AB/wAf8AP+AA//8D//wP//AAAAAAAAP//A//8D//wP//AfwAAfwAAfwAAfwAAfwP//A//8D//wAAAAAAAAAAAP//A//8D//wP//A4AcDgBwOAHA4AcD//wP//A//8D//wAAAAAAAD//wP//A//8D//wOHAA4cADhwAOHAA/8AD/wAP/AA/8AAAAAP//A//8D//wP//A4AcDgBwOAHA4AcD//+P//4///j//+AAA4AADgAAAP//A//8D//wP//A4eADh+AOH8A4f4D/3wP/HA/8MD/wQAAAAAAAD/xwP/HA/8cD/xwOHHA4ccDhxwOHHA4f8Dh/wOH/A4f8AAAAAAAA4AADgAAOAAA//8D//wP//A//8DgAAOAAA4AADgAAAAAA//8D//wP//A//8AABwAAHAAAcAABwP//A//8D//wP//AAAADAAAPgAA/wAD/4AB/8AA/8AAfwAB/AA/8Af+AP/AA/wAD4AAMAAA4AAD+AAP/gA//8AH/wAB/AAf8Af/wP/4A/4AD/gAP/4AH/8AB/wAB/AB/8D//wP/gA/gADgAAIABA4AcDwDwPw/Afn4Af+AA/wAD/AA//AH5+A/D8DwDwOAHAgAEAAAAP/AA/8AD/wAP/AAAf8AB/wAH/AAf8D/wAP/AA/8AD/wAAAAAAAADh/wOH/A4f8Dh/wOHHA4ccDhxwOHHA/8cD/xwP/HA/8cAAAAAAAAf//9///3///f//9wAA3AADcAAMAAAOAAA/gAD/wAH/8AB/8AA/wAAPAAAEAAAAHAADcAANwAB3///f//9///wAA"), 32, atob("BwYLDg4UDwYJCQwMBgkGCQ4MDg4ODg4NDg4GBgwMDA4PDg4ODg4NDg4GDQ4MEg8ODQ8ODgwODhQODg4ICQg="), 22+(scale<<8)+(1<<16));
-}
+};
+{
const SETTINGS_FILE = "pebble.json";
let settings;
let theme;
+let drawTimeout;
-function loadSettings() {
+let loadSettings = function() {
settings = require("Storage").readJSON(SETTINGS_FILE,1)|| {'bg': '#0f0', 'color': 'Green', 'theme':'System', 'showlock':false};
-}
+};
-var img = require("heatshrink").decompress(atob("oFAwkEogA/AH4A/AH4A/AH4A/AE8AAAoeXoAfeDQUBmcyD7A+Dh///8QD649CiAfaHwUvD4sEHy0DDYIfEICg+Cn4fHICY+DD4nxcgojOHwgfEIAYfRCIQaDD4ZAFD5r7DH4//kAfRCIZ/GAAnwD5p9DX44fTHgYSBf4ofVDAQEBl4fFUAgfOXoQzBgIfFBAIfPP4RAEAoYAB+cRiK/SG4h/WIBAfXIA7CBAAswD55AHn6fUIBMCD65AHl4gCmcziAfQQJqfQQJpiDgk0IDXxQLRAEECaBM+QgRYRYgUIA0CD4ggSQJiDCiAKBICszAAswD55AHABKBVD7BAFABIqBD5pAFABPxD55AOD6BADiIAJQAyxLABwf/gaAPAH4A/AH4ARA=="));
+const img = require("heatshrink").decompress(atob("oFAwkEogA/AH4A/AH4A/AH4A/AE8AAAoeXoAfeDQUBmcyD7A+Dh///8QD649CiAfaHwUvD4sEHy0DDYIfEICg+Cn4fHICY+DD4nxcgojOHwgfEIAYfRCIQaDD4ZAFD5r7DH4//kAfRCIZ/GAAnwD5p9DX44fTHgYSBf4ofVDAQEBl4fFUAgfOXoQzBgIfFBAIfPP4RAEAoYAB+cRiK/SG4h/WIBAfXIA7CBAAswD55AHn6fUIBMCD65AHl4gCmcziAfQQJqfQQJpiDgk0IDXxQLRAEECaBM+QgRYRYgUIA0CD4ggSQJiDCiAKBICszAAswD55AHABKBVD7BAFABIqBD5pAFABPxD55AOD6BADiIAJQAyxLABwf/gaAPAH4A/AH4ARA=="));
const h = g.getHeight();
const w = g.getWidth();
@@ -26,7 +28,7 @@ const h3 = 7*h/8;
let batteryWarning = false;
-function draw() {
+let draw = function() {
let locale = require("locale");
let date = new Date();
let dayOfWeek = locale.dow(date, 1).toUpperCase();
@@ -81,10 +83,16 @@ function draw() {
drawCalendar(((w/2) - 42)/2, 14, 42, 4, dayOfMonth);
drawLock();
-}
+ // queue next draw
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = setTimeout(function() {
+ drawTimeout = undefined;
+ draw();
+ }, 60000 - (Date.now() % 60000));
+};
// at x,y width:wi thicknes:th
-function drawCalendar(x,y,wi,th,str) {
+let drawCalendar = function(x,y,wi,th,str) {
g.setColor(theme.fg);
g.fillRect(x, y, x + wi, y + wi);
g.setColor(theme.bg);
@@ -100,9 +108,9 @@ function drawCalendar(x,y,wi,th,str) {
g.setFontLECO1976Regular22();
g.setFontAlign(0, 0);
g.drawString(str, x + wi/2, y + wi/2 + th);
-}
+};
-function loadThemeColors() {
+let loadThemeColors = function() {
theme = {fg: g.theme.fg, bg: g.theme.bg, day: g.toColor(0,0,0)};
if (settings.theme === "Dark") {
theme.fg = g.toColor(1,1,1);
@@ -114,9 +122,9 @@ function loadThemeColors() {
// day and steps
if (settings.color == 'Blue' || settings.color == 'Red')
theme.day = g.toColor(1,1,1); // white on blue or red best contrast
-}
+};
-function drawLock(){
+let drawLock = function(){
if (settings.showlock) {
if (Bangle.isLocked()){
g.setColor(theme.day);
@@ -127,26 +135,28 @@ function drawLock(){
g.fillRect(0, 0, 20, 20);
}
}
-}
+};
-Bangle.on('lock', function(on) {
- drawLock();
-});
+Bangle.on('lock', drawLock);
+
+// Show launcher when middle button pressed
+Bangle.setUI({
+ mode : "clock",
+ remove : function() {
+ // Called to unload all of the clock app
+ Bangle.removeListener('lock', drawLock);
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = undefined;
+ delete Graphics.prototype.setFontLECO1976Regular22;
+ delete Graphics.prototype.setFontLECO1976Regular42;
+ require("widget_utils").show(); // re-show widgets
+ }});
g.clear();
Bangle.loadWidgets();
-
-// We are not drawing the widgets as we are taking over the whole screen
-// so we will blank out the draw() functions of each widget and change the
-// area to the top bar doesn't get cleared.
-for (let wd of WIDGETS) {
- wd.draw=()=>{};
- wd.area="";
-}
+require("widget_utils").swipeOn(); // hide widgets, make them visible with a swipe
loadSettings();
loadThemeColors();
-setInterval(draw, 15000); // refresh every 15s
draw();
-
-Bangle.setUI("clock");
+}
diff --git a/apps/pebbled/ChangeLog b/apps/pebbled/ChangeLog
index 9db0e26c5..d2f71f908 100644
--- a/apps/pebbled/ChangeLog
+++ b/apps/pebbled/ChangeLog
@@ -1 +1,4 @@
0.01: first release
+0.02: Tell clock widgets to hide.
+0.03: Swipe down to see widgets
+ Support for fast loading
diff --git a/apps/pebbled/metadata.json b/apps/pebbled/metadata.json
index c16025f6f..9e71a914b 100644
--- a/apps/pebbled/metadata.json
+++ b/apps/pebbled/metadata.json
@@ -2,7 +2,7 @@
"id": "pebbled",
"name": "Pebble Clock with distance",
"shortName": "Pebble + distance",
- "version": "0.01",
+ "version": "0.03",
"description": "Fork of Pebble Clock with distance in KM. Both step count and the distance are on the main screen. Default step length = 0.75m (can be changed in settings).",
"readme": "README.md",
"icon": "pebbled.png",
diff --git a/apps/pebbled/pebbled.app.js b/apps/pebbled/pebbled.app.js
index bbe98823f..627a7651c 100644
--- a/apps/pebbled/pebbled.app.js
+++ b/apps/pebbled/pebbled.app.js
@@ -8,14 +8,16 @@ Graphics.prototype.setFontLECO1976Regular22 = function(scale) {
g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/nA/+cD/5wP/nAAAAAAAAPwAA/gAD+AAPwAAAAAD+AAP4AA/gAAAAAAAAAAAAAcOAP//A//8D//wP//AHDgAcOAP//A//8D//wP//AHDgAAAAAAAAH/jgf+OB/44H/jj8OP/w4//Dj/8OPxw/4HD/gcP+Bw/4AAAAAAAP+AA/8AD/wQOHHA4c8D//wP/8A//gAD4AAfAAH/8A//wP//A84cDjhwIP/AA/8AB/wAAAAAAAD//wP//A//8D//wOHHA4ccDhxwOHHA4f8Dh/wOH/A4f8ABwAAAAAAAAD8AAP4AA/gAD8AAAAAAAAAAAEAAD+AB//A///v/D//gB/wABwAAAAAADgAA/wAf/4P8///wf/4AP8AAOAAAAAAAAAyAAHcAAPwAD/gAP/AA/8AA/AAH8AAMwAAAAAAAAAAAAADgAAOAAA4AAf8AD/wAP/AA/8AAOAAA4AADgAAAAAAAAAAD8AAfwAB/AAD8AAAAAAAADgAAOAAA4AADgAAOAAA4AADgAAAAAAAAAADgAAOAAA4AADgAAAAAAAAABwAB/AA/8A//gP/gA/wADwAAIAAAAAAD//wP//A//8D//wOAHA4AcDgBwOAHA//8D//wP//A//8AAAAAAAA4AcDgBwOAHA//8D//wP//A//8AABwAAHAAAcAAAAAAAA+f8D5/wPn/A+f8DhxwOHHA4ccDhxwP/HA/8cD/xwP/HAAAAAAAAOAHA4AcDhxwOHHA4ccDhxwOHHA4ccD//wP//A//8D//wAAAAAAAD/wAP/AA/8AD/wAAHAAAcAABwAAHAA//8D//wP//A//8AAAAAAAA/98D/3wP/fA/98DhxwOHHA4ccDhxwOH/A4f8Dh/wOH/AAAAAAAAP//A//8D//wP//A4ccDhxwOHHA4ccDh/wOH/A4f8Dh/wAAAAAAAD4AAPgAA+AADgAAOAAA4AADgAAP//A//8D//wP//AAAAAAAAP//A//8D//wP//A4ccDhxwOHHA4ccD//wP//A//8D//wAAAAAAAD/xwP/HA/8cD/xwOHHA4ccDhxwOHHA//8D//wP//A//8AAAAAAAAOA4A4DgDgOAOA4AAAAAAAAOA/A4H8DgfwOA/AAAAAAAAB4AAPwAA/AAD8AAf4ABzgAPPAA8cAHh4AAAAAAAAAAAAHHAAccABxwAHHAAccABxwAHHAAccABxwAHHAAAAAAAAAOHAA4cADzwAPPAAf4AB/gAD8AAPwAAeAAB4AAAAAAAAA+AAD4AAPgAA+ecDh9wOH3A4fcDhwAP/AA/8AD/wAP/AAAAAAAAAP//4///j//+P//44ADjn/OOf845/zjnHOP8c4//zj//OP/84AAAAAAAP//A//8D//wP//A4cADhwAOHAA4cAD//wP//A//8D//wAAAAAAAD//wP//A//8D//wOHHA4ccDhxwOHHA//8D//wP9/A/j8AAAAAAAA//8D//wP//A//8DgBwOAHA4AcDgBwOAHA4AcDgBwOAHAAAAAAAAP//A//8D//wP//A4AcDgBwOAHA8A8D//wH/+AP/wAf+AAAAAAAAD//wP//A//8D//wOHHA4ccDhxwOHHA4ccDhxwOAHA4AcAAAAAAAA//8D//wP//A//8DhwAOHAA4cADhwAOHAA4cADgAAOAAAAAAD//wP//A//8D//wOAHA4ccDhxwOHHA4f8Dh/wOH/A4f8AAAAAAAA//8D//wP//A//8ABwAAHAAAcAABwAP//A//8D//wP//AAAAAAAAP//A//8D//wP//AAAAAAAAOAHA4AcDgBwOAHA4AcDgBwOAHA//8D//wP//A//8AAAAAAAA//8D//wP//A//8AHwAA/AAP8AB/wAPn/A8f8DB/wIH/AAAAAAAAP//A//8D//wP//AAAcAABwAAHAAAcAABwAAHAAAAAAAAP//A//8D//wP//Af8AAP+AAH/AAD8AAHwAD/AB/wAf8AP+AA//8D//wP//AAAAAAAAP//A//8D//wP//AfwAAfwAAfwAAfwAAfwP//A//8D//wAAAAAAAAAAAP//A//8D//wP//A4AcDgBwOAHA4AcD//wP//A//8D//wAAAAAAAD//wP//A//8D//wOHAA4cADhwAOHAA/8AD/wAP/AA/8AAAAAP//A//8D//wP//A4AcDgBwOAHA4AcD//+P//4///j//+AAA4AADgAAAP//A//8D//wP//A4eADh+AOH8A4f4D/3wP/HA/8MD/wQAAAAAAAD/xwP/HA/8cD/xwOHHA4ccDhxwOHHA4f8Dh/wOH/A4f8AAAAAAAA4AADgAAOAAA//8D//wP//A//8DgAAOAAA4AADgAAAAAA//8D//wP//A//8AABwAAHAAAcAABwP//A//8D//wP//AAAADAAAPgAA/wAD/4AB/8AA/8AAfwAB/AA/8Af+AP/AA/wAD4AAMAAA4AAD+AAP/gA//8AH/wAB/AAf8Af/wP/4A/4AD/gAP/4AH/8AB/wAB/AB/8D//wP/gA/gADgAAIABA4AcDwDwPw/Afn4Af+AA/wAD/AA//AH5+A/D8DwDwOAHAgAEAAAAP/AA/8AD/wAP/AAAf8AB/wAH/AAf8D/wAP/AA/8AD/wAAAAAAAADh/wOH/A4f8Dh/wOHHA4ccDhxwOHHA/8cD/xwP/HA/8cAAAAAAAAf//9///3///f//9wAA3AADcAAMAAAOAAA/gAD/wAH/8AB/8AA/wAAPAAAEAAAAHAADcAANwAB3///f//9///wAA"), 32, atob("BwYLDg4UDwYJCQwMBgkGCQ4MDg4ODg4NDg4GBgwMDA4PDg4ODg4NDg4GDQ4MEg8ODQ8ODgwODhQODg4ICQg="), 22+(scale<<8)+(1<<16));
};
+{
const SETTINGS_FILE = "pebbleDistance.json";
let settings;
+let drawTimeout;
-function loadSettings() {
+let loadSettings = function() {
settings = require("Storage").readJSON(SETTINGS_FILE,1)|| {'bg': '#0f0', 'color': 'Green', 'avStep': 0.75};
-}
+};
-var img = require("heatshrink").decompress(atob("oFAwkEogA/AH4A/AH4A/AH4A/AE8AAAoeXoAfeDQUBmcyD7A+Dh///8QD649CiAfaHwUvD4sEHy0DDYIfEICg+Cn4fHICY+DD4nxcgojOHwgfEIAYfRCIQaDD4ZAFD5r7DH4//kAfRCIZ/GAAnwD5p9DX44fTHgYSBf4ofVDAQEBl4fFUAgfOXoQzBgIfFBAIfPP4RAEAoYAB+cRiK/SG4h/WIBAfXIA7CBAAswD55AHn6fUIBMCD65AHl4gCmcziAfQQJqfQQJpiDgk0IDXxQLRAEECaBM+QgRYRYgUIA0CD4ggSQJiDCiAKBICszAAswD55AHABKBVD7BAFABIqBD5pAFABPxD55AOD6BADiIAJQAyxLABwf/gaAPAH4A/AH4ARA=="));
+const img = require("heatshrink").decompress(atob("oFAwkEogA/AH4A/AH4A/AH4A/AE8AAAoeXoAfeDQUBmcyD7A+Dh///8QD649CiAfaHwUvD4sEHy0DDYIfEICg+Cn4fHICY+DD4nxcgojOHwgfEIAYfRCIQaDD4ZAFD5r7DH4//kAfRCIZ/GAAnwD5p9DX44fTHgYSBf4ofVDAQEBl4fFUAgfOXoQzBgIfFBAIfPP4RAEAoYAB+cRiK/SG4h/WIBAfXIA7CBAAswD55AHn6fUIBMCD65AHl4gCmcziAfQQJqfQQJpiDgk0IDXxQLRAEECaBM+QgRYRYgUIA0CD4ggSQJiDCiAKBICszAAswD55AHABKBVD7BAFABIqBD5pAFABPxD55AOD6BADiIAJQAyxLABwf/gaAPAH4A/AH4ARA=="));
const h = g.getHeight();
const w = g.getWidth();
@@ -25,12 +27,12 @@ const h3 = 7*h/8 - 10;
let batteryWarning = false;
-function draw() {
+let draw = function() {
let date = new Date();
let da = date.toString().split(" ");
let timeStr = da[4].substr(0,5);
const t = 6;
- const stps = getSteps();
+ let stps = Bangle.getHealthStatus("day").steps;
// turn the warning on once we have dipped below 15%
if (E.getBattery() < 15)
@@ -80,17 +82,24 @@ function draw() {
g.setColor(settings.bg);
g.drawImage(img, w/2 + ((w/2) - 64)/2, -2, { scale: 1 });
drawCalendar(((w/2) - 42)/2, 11, 42, 4, da[2]);
-
- // distance
+
+ // distance
if (settings.color == 'Blue' || settings.color == 'Red')
g.setColor('#fff'); // white on blue or red best contrast
else
g.setColor('#000'); // otherwise black regardless of theme
g.drawString((stps / 1000 * settings.avStep).toFixed(2) + ' KM', w/2, ha + 107);
-}
+
+ // queue next draw
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = setTimeout(function() {
+ drawTimeout = undefined;
+ draw();
+ }, 60000 - (Date.now() % 60000));
+};
// at x,y width:wi thicknes:th
-function drawCalendar(x,y,wi,th,str) {
+let drawCalendar = function(x,y,wi,th,str) {
g.setColor(g.theme.fg);
g.fillRect(x, y, x + wi, y + wi);
g.setColor(g.theme.bg);
@@ -106,24 +115,23 @@ function drawCalendar(x,y,wi,th,str) {
g.setFontLECO1976Regular22();
g.setFontAlign(0, 0);
g.drawString(str, x + wi/2, y + wi/2 + th);
-}
+};
-function getSteps() {
- if (WIDGETS.wpedom !== undefined) {
- return WIDGETS.wpedom.getSteps();
- }
- return '0';
-}
+// Show launcher when middle button pressed
+Bangle.setUI({
+ mode : "clock",
+ remove : function() {
+ // Called to unload all of the clock app
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = undefined;
+ delete Graphics.prototype.setFontLECO1976Regular22;
+ delete Graphics.prototype.setFontLECO1976Regular42;
+ require("widget_utils").show(); // re-show widgets
+ }});
g.clear();
Bangle.loadWidgets();
-/*
- * we are not drawing the widgets as we are taking over the whole screen
- * so we will blank out the draw() functions of each widget and change the
- * area to the top bar doesn't get cleared.
- */
-for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
+require("widget_utils").swipeOn(); // hide widgets, make them visible with a swipe
loadSettings();
-setInterval(draw, 15000); // refresh every 15s
draw();
-Bangle.setUI("clock");
+}
diff --git a/apps/pisteinen/ChangeLog b/apps/pisteinen/ChangeLog
new file mode 100644
index 000000000..5560f00bc
--- /dev/null
+++ b/apps/pisteinen/ChangeLog
@@ -0,0 +1 @@
+0.01: New App!
diff --git a/apps/pisteinen/README.md b/apps/pisteinen/README.md
new file mode 100644
index 000000000..20e8bf9a1
--- /dev/null
+++ b/apps/pisteinen/README.md
@@ -0,0 +1,7 @@
+# Pisteinen
+
+By Jukio Kallio
+
+A Minimal digital watch face consisting of dots. Big dots for hours, small dots for minutes.
+
+
diff --git a/apps/pisteinen/app-icon.js b/apps/pisteinen/app-icon.js
new file mode 100644
index 000000000..d8ad05c50
--- /dev/null
+++ b/apps/pisteinen/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwkDmYA0/4AKCpM/CxYAB+YtTGJQuOGBAWPGAwuQGAwXvCyJgFC+UhiQDNC43ygEAl4DLC4/xBYMfAZYXfI653XX/6//X/6//O5gBKU5gGBAZAXfI66//C7s/CyPzC+ZgSCwgwRFwowRCwwwPFw4xOCpIArA=="))
diff --git a/apps/pisteinen/app.js b/apps/pisteinen/app.js
new file mode 100644
index 000000000..a455875ec
--- /dev/null
+++ b/apps/pisteinen/app.js
@@ -0,0 +1,121 @@
+// Pisteinen
+//
+// Bangle.js 2 watch face
+// by Jukio Kallio
+// www.jukiokallio.com
+
+
+// settings
+const watch = {
+ x:0, y:0, w:0, h:0,
+ bgcolor:g.theme.bg,
+ fgcolor:g.theme.fg,
+};
+
+// set some additional settings
+watch.w = g.getWidth(); // size of the background
+watch.h = g.getHeight();
+watch.x = watch.w * 0.5; // position of the circles
+watch.y = watch.h * 0.5;
+
+var wait = 60000; // wait time, normally a minute
+
+
+// timeout used to update every minute
+var drawTimeout;
+
+// schedule a draw for the next minute
+function queueDraw() {
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = setTimeout(function() {
+ drawTimeout = undefined;
+ draw();
+ }, wait - (Date.now() % wait));
+}
+
+
+// main function
+function draw() {
+ // make date object
+ var date = new Date();
+
+ // Reset the state of the graphics library
+ g.reset();
+
+ // Clear the area where we want to draw the time
+ g.setColor(watch.bgcolor);
+ g.fillRect(0, 0, watch.w, watch.h);
+
+ // setup watch face
+ const hball = {
+ size: 9,
+ pad: 9,
+ };
+ const mball = {
+ size: 3,
+ pad: 4,
+ pad2: 2,
+ };
+
+ // get hours and minutes
+ var hour = date.getHours();
+ var minute = date.getMinutes();
+
+ // calculate size of the hour face
+ var hfacew = (hball.size * 2 + hball.pad) * 6 - hball.pad;
+ var hfaceh = (hball.size * 2 + hball.pad) * 4 - hball.pad;
+ var mfacew = (mball.size * 2 + mball.pad) * 15 - mball.pad + mball.pad2 * 2;
+ var mfaceh = (mball.size * 2 + mball.pad) * 4 - mball.pad;
+ var faceh = hfaceh + mfaceh + hball.pad + mball.pad;
+
+ g.setColor(watch.fgcolor); // set foreground color
+
+ // draw hour balls
+ for (var i = 0; i < 24; i++) {
+ var x = ((hball.size * 2 + hball.pad) * (i % 6)) + (watch.x - hfacew / 2) + hball.size;
+ var y = watch.y - faceh / 2 + hball.size;
+ if (i >= 6) y += hball.size * 2 + hball.pad;
+ if (i >= 12) y += hball.size * 2 + hball.pad;
+ if (i >= 18) y += hball.size * 2 + hball.pad;
+
+ if (i < hour) g.fillCircle(x, y, hball.size); else g.drawCircle(x, y, hball.size);
+ }
+
+ // draw minute balls
+ for (var j = 0; j < 60; j++) {
+ var x2 = ((mball.size * 2 + mball.pad) * (j % 15)) + (watch.x - mfacew / 2) + mball.size;
+ if (j % 15 >= 5) x2 += mball.pad2;
+ if (j % 15 >= 10) x2 += mball.pad2;
+ var y2 = watch.y - faceh / 2 + hfaceh + mball.size + hball.pad + mball.pad;
+ if (j >= 15) y2 += mball.size * 2 + mball.pad;
+ if (j >= 30) y2 += mball.size * 2 + mball.pad;
+ if (j >= 45) y2 += mball.size * 2 + mball.pad;
+
+ if (j < minute) g.fillCircle(x2, y2, mball.size); else g.drawCircle(x2, y2, mball.size);
+ }
+
+
+ // queue draw
+ queueDraw();
+}
+
+
+// Clear the screen once, at startup
+g.clear();
+// draw immediately at first
+draw();
+
+
+// Stop updates when LCD is off, restart when on
+Bangle.on('lcdPower',on=>{
+ if (on) {
+ draw(); // draw immediately, queue redraw
+ } else { // stop draw timer
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = undefined;
+ }
+});
+
+
+// Show launcher when middle button pressed
+Bangle.setUI("clock");
diff --git a/apps/pisteinen/app.png b/apps/pisteinen/app.png
new file mode 100644
index 000000000..a6c441423
Binary files /dev/null and b/apps/pisteinen/app.png differ
diff --git a/apps/pisteinen/metadata.json b/apps/pisteinen/metadata.json
new file mode 100644
index 000000000..f1137e589
--- /dev/null
+++ b/apps/pisteinen/metadata.json
@@ -0,0 +1,16 @@
+{ "id": "pisteinen",
+ "name": "Pisteinen - Dotted watch face",
+ "shortName":"Pisteinen",
+ "version":"0.01",
+ "description": "A minimal digital watch face made with dots.",
+ "icon": "app.png",
+ "screenshots": [{"url":"screenshot1.png"}],
+ "type": "clock",
+ "tags": "clock",
+ "supports" : ["BANGLEJS","BANGLEJS2"],
+ "readme": "README.md",
+ "storage": [
+ {"name":"pisteinen.app.js","url":"app.js"},
+ {"name":"pisteinen.img","url":"app-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/pisteinen/screenshot1.png b/apps/pisteinen/screenshot1.png
new file mode 100644
index 000000000..556c004c0
Binary files /dev/null and b/apps/pisteinen/screenshot1.png differ
diff --git a/apps/podadrem/ChangeLog b/apps/podadrem/ChangeLog
new file mode 100644
index 000000000..3c68f15ac
--- /dev/null
+++ b/apps/podadrem/ChangeLog
@@ -0,0 +1,9 @@
+0.01: Inital release.
+0.02: Misc fixes. Add Search and play.
+0.03: Simplify "Search and play" function after some bugfixes to Podcast
+Addict.
+0.04: New layout.
+0.05: Add widget field, tweak layout.
+0.06: Add compatibility with Fastload Utils.
+0.07: Remove just the specific listeners to not interfere with Quick Launch
+when fastloading.
diff --git a/apps/podadrem/README.md b/apps/podadrem/README.md
new file mode 100644
index 000000000..3760e6b5b
--- /dev/null
+++ b/apps/podadrem/README.md
@@ -0,0 +1,21 @@
+Requires Gadgetbridge 71.0 or later. Allow intents in Gadgetbridge in order for this app to work.
+
+Touch input:
+
+Press the different ui elements to control Podcast Addict and open menus.
+Press left or right arrow to move backward/forward in current playlist.
+
+Swipe input:
+
+Swipe left/right to jump backward/forward within the current podcast episode.
+Swipe up/down to change the volume.
+
+It's possible to start a podcast by searching with the remote. It's also possible to change the playback speed.
+
+The swipe logic was inspired by the implementation in [rigrig](https://git.tubul.net/rigrig/)'s Scrolling Messages.
+
+Podcast Addict Remote was created by [thyttan](https://github.com/thyttan/).
+
+Podcast Addict is developed by [Xavier Guillemane](https://twitter.com/xguillem) and can be installed via the [Google Play Store](https://play.google.com/store/apps/details?id=com.bambuna.podcastaddict&hl=en_US&gl=US).
+
+The Podcast Addict icon is used with permission.
diff --git a/apps/podadrem/app-icon.js b/apps/podadrem/app-icon.js
new file mode 100644
index 000000000..fc4406666
--- /dev/null
+++ b/apps/podadrem/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwgHEhvdABnQDwwVNAAYtTGI4WSGAgWTGAYXUGAJGUGAQXXCyoXKmf/AAPznogQn4WCAAQYP6YWFDB4WFJQhFSA4gwMIYogEGBffLg0zKAYwKRgwTDBQP9Ix09n7DCpowBJBKNEBwIXBAQIsBMwgXKIQReCDoRgJOwYQDLQU/poMBC5B2DIAUzLwIKBnoXBPBAXEIQQVDA4IXNCIQXaWAgXNI4kzNQoXLO4wXLU4a+CU4gXR7ovBcIoXIBobMFPAgXILQKPDmgxCR5omDc4QAHC5ITCC6hgCC6hICC6owBC6phBC6zcFAAMzeogALdQjdBC6AZCeYfTmczAwfQhocOAAwXYgAXVgAXVFwMAJCgXCDCYWDJKYWEGKAtEA=="))
diff --git a/apps/podadrem/app.js b/apps/podadrem/app.js
new file mode 100644
index 000000000..9c9ed8b04
--- /dev/null
+++ b/apps/podadrem/app.js
@@ -0,0 +1,326 @@
+{
+/*
+ Bluetooth.println(JSON.stringify({t:"intent", action:"", flags:["flag1", "flag2",...], categories:["category1","category2",...], mimetype:"", data:"", package:"", class:"", target:"", extra:{someKey:"someValueOrString"}}));
+
+ Podcast Addict is developed by Xavier Guillemane and can be downloaded on Google Play Store: https://play.google.com/store/apps/details?id=com.bambuna.podcastaddict&hl=en_US&gl=US
+
+ How to use intents to control Podcast Addict: https://podcastaddict.com/faq/130
+*/
+
+let R;
+let widgetUtils = require("widget_utils");
+let backToMenu = false;
+let dark = g.theme.dark; // bool
+
+// The main layout of the app
+let gfx = function() {
+ widgetUtils.hide();
+ R = Bangle.appRect;
+ marigin = 8;
+ // g.drawString(str, x, y, solid)
+ g.clearRect(R);
+ g.reset();
+
+ if (dark) {g.setColor(0xFD20);} else {g.setColor(0xF800);} // Orange on dark theme, RED on light theme.
+ g.setFont("4x6:2");
+ g.setFontAlign(1, 0, 0);
+ g.drawString("->", R.x2 - marigin, R.y + R.h/2);
+
+ g.setFontAlign(-1, 0, 0);
+ g.drawString("<-", R.x + marigin, R.y + R.h/2);
+
+ g.setFontAlign(-1, 0, 1);
+ g.drawString("<-", R.x + R.w/2, R.y + marigin);
+
+ g.setFontAlign(1, 0, 1);
+ g.drawString("->", R.x + R.w/2, R.y2 - marigin);
+
+ g.setFontAlign(0, 0, 0);
+ g.drawString("Play\nPause", R.x + R.w/2, R.y + R.h/2);
+
+ g.setFontAlign(-1, -1, 0);
+ g.drawString("Menu", R.x + 2*marigin, R.y + 2*marigin);
+
+ g.setFontAlign(-1, 1, 0);
+ g.drawString("Wake", R.x + 2*marigin, R.y + R.h - 2*marigin);
+
+ g.setFontAlign(1, -1, 0);
+ g.drawString("Srch", R.x + R.w - 2*marigin, R.y + 2*marigin);
+
+ g.setFontAlign(1, 1, 0);
+ g.drawString("Speed", R.x + R.w - 2*marigin, R.y + R.h - 2*marigin);
+};
+
+// Touch handler for main layout
+let touchHandler = function(_, xy) {
+ x = xy.x;
+ y = xy.y;
+ len = (R.wb-1 instead of a>=b.
+ if ((R.x-1{
+ Bangle.removeListener("touch", touchHandler);
+ Bangle.removeListener("swipe", swipeHandler);
+ clearWatch(buttonHandler);
+ widgetUtils.show();
+ }
+ },
+ ud => {
+ if (ud) Bangle.musicControl(ud>0 ? "volumedown" : "volumeup");
+ }
+ );
+ Bangle.on("touch", touchHandler);
+ Bangle.on("swipe", swipeHandler);
+ let buttonHandler = setWatch(()=>{load();}, BTN, {edge:'falling'});
+};
+
+/*
+The functions for interacting with Android and the Podcast Addict app
+*/
+
+let pkg = "com.bambuna.podcastaddict";
+let standardCls = pkg + ".receiver.PodcastAddictPlayerReceiver";
+let updateCls = pkg + ".receiver.PodcastAddictBroadcastReceiver";
+let speed = 1.0;
+
+let simpleSearch = "";
+
+let simpleSearchTerm = function() { // input a simple search term without tags, overrides search with tags (artist and track)
+ require("textinput").input({
+ text: simpleSearch
+ }).then(result => {
+ simpleSearch = result;
+ }).then(() => {
+ E.showMenu(searchMenu);
+ });
+};
+
+let searchPlayWOTags = function() { //make a search and play using entered terms
+ searchString = simpleSearch;
+ Bluetooth.println(JSON.stringify({
+ t: "intent",
+ action: "android.media.action.MEDIA_PLAY_FROM_SEARCH",
+ package: pkg,
+ target: "activity",
+ extra: {
+ query: searchString
+ },
+ flags: ["FLAG_ACTIVITY_NEW_TASK"]
+ }));
+};
+
+let gadgetbridgeWake = function() {
+ Bluetooth.println(JSON.stringify({
+ t: "intent",
+ target: "activity",
+ flags: ["FLAG_ACTIVITY_NEW_TASK", "FLAG_ACTIVITY_CLEAR_TASK", "FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS", "FLAG_ACTIVITY_NO_ANIMATION"],
+ package: "gadgetbridge",
+ class: "nodomain.freeyourgadget.gadgetbridge.activities.WakeActivity"
+ }));
+};
+
+// For stringing together the action for Podcast Addict to perform
+let actFn = function(actName, activOrServ) {
+ return "com.bambuna.podcastaddict." + (activOrServ == "service" ? "service." : "") + actName;
+};
+
+// Send the intent message to Gadgetbridge
+let btMsg = function(activOrServ, cls, actName, xtra) {
+
+ Bluetooth.println(JSON.stringify({
+ t: "intent",
+ action: actFn(actName, activOrServ),
+ package: pkg,
+ class: cls,
+ target: "broadcastreceiver",
+ extra: xtra
+ }));
+};
+
+// Get back to the main layout
+let backToGfx = function() {
+ E.showMenu();
+ g.clear();
+ g.reset();
+ setUI();
+ gfx();
+ backToMenu = false;
+};
+
+// Podcast Addict Menu
+let paMenu = {
+ "": {
+ title: " ",
+ back: backToGfx
+ },
+ "Controls": () => {
+ E.showMenu(controlMenu);
+ },
+ "Speed Controls": () => {
+ E.showMenu(speedMenu);
+ },
+ "Search and play": () => {
+ E.showMenu(searchMenu);
+ },
+ "Navigate and play": () => {
+ E.showMenu(navigationMenu);
+ },
+ "Wake the android": () => {
+ gadgetbridgeWake();
+ gadgetbridgeWake();
+ },
+ "Exit PA Remote": ()=>{load();}
+};
+
+
+let controlMenu = {
+ "": {
+ title: " ",
+ back: () => {if (backToMenu) E.showMenu(paMenu);
+ if (!backToMenu) backToGfx();
+ }
+ },
+ "Toggle Play/Pause": () => {
+ btMsg("service", standardCls, "player.toggle");
+ },
+ "Jump Backward": () => {
+ btMsg("service", standardCls, "player.jumpbackward");
+ },
+ "Jump Forward": () => {
+ btMsg("service", standardCls, "player.jumpforward");
+ },
+ "Previous": () => {
+ btMsg("service", standardCls, "player.previoustrack");
+ },
+ "Next": () => {
+ btMsg("service", standardCls, "player.nexttrack");
+ },
+ "Play": () => {
+ btMsg("service", standardCls, "player.play");
+ },
+ "Pause": () => {
+ btMsg("service", standardCls, "player.pause");
+ },
+ "Stop": () => {
+ btMsg("service", standardCls, "player.stop");
+ },
+ "Update": () => {
+ btMsg("service", updateCls, "update");
+ },
+ "Messages Music Controls": () => {
+ load("messagesmusic.app.js");
+ },
+};
+
+let speedMenu = {
+ "": {
+ title: " ",
+ back: () => {if (backToMenu) E.showMenu(paMenu);
+ if (!backToMenu) backToGfx();
+ }
+ },
+ "Regular Speed": () => {
+ speed = 1.0;
+ btMsg("service", standardCls, "player.1xspeed");
+ },
+ "1.5x Regular Speed": () => {
+ speed = 1.5;
+ btMsg("service", standardCls, "player.1.5xspeed");
+ },
+ "2x Regular Speed": () => {
+ speed = 2.0;
+ btMsg("service", standardCls, "player.2xspeed");
+ },
+ //"Faster" : ()=>{speed+=0.1; speed=((speed>5.0)?5.0:speed); btMsg("service",standardCls,"player.customspeed",{arg1:speed});},
+ //"Slower" : ()=>{speed-=0.1; speed=((speed<0.1)?0.1:speed); btMsg("service",standardCls,"player.customspeed",{arg1:speed});},
+};
+
+let searchMenu = {
+ "": {
+ title: " ",
+
+ back: () => {if (backToMenu) E.showMenu(paMenu);
+ if (!backToMenu) backToGfx();}
+
+ },
+ "Search term": () => {
+ simpleSearchTerm();
+ },
+ "Execute search and play": () => {
+ btMsg("service", standardCls, "player.play");
+ setTimeout(() => {
+ searchPlayWOTags();
+ setTimeout(() => {
+ btMsg("service", standardCls, "player.play");
+ }, 200);
+ }, 1500);
+ },
+ "Simpler search and play" : searchPlayWOTags,
+};
+
+let navigationMenu = {
+ "": {
+ title: " ",
+ back: () => {if (backToMenu) E.showMenu(paMenu);
+ if (!backToMenu) backToGfx();}
+ },
+ "Open Main Screen": () => {
+ btMsg("activity", standardCls, "openmainscreen");
+ },
+ "Open Player Screen": () => {
+ btMsg("activity", standardCls, "openplayer");
+ },
+};
+
+Bangle.loadWidgets();
+setUI();
+widgetUtils.hide();
+gfx();
+}
diff --git a/apps/podadrem/app.png b/apps/podadrem/app.png
new file mode 100644
index 000000000..b9cdf4fed
Binary files /dev/null and b/apps/podadrem/app.png differ
diff --git a/apps/podadrem/metadata.json b/apps/podadrem/metadata.json
new file mode 100644
index 000000000..c58b9241d
--- /dev/null
+++ b/apps/podadrem/metadata.json
@@ -0,0 +1,18 @@
+{
+ "id": "podadrem",
+ "name": "Podcast Addict Remote",
+ "shortName": "PA Remote",
+ "version": "0.07",
+ "description": "Control Podcast Addict on your android device.",
+ "readme": "README.md",
+ "type": "app",
+ "tags": "remote,podcast,podcasts,radio,player,intent,intents,gadgetbridge,podadrem,pa remote",
+ "icon": "app.png",
+ "screenshots" : [ {"url":"screenshot1.png"}, {"url":"screenshot2.png"} ],
+ "supports": ["BANGLEJS2"],
+ "dependencies": { "textinput":"type"},
+ "storage": [
+ {"name":"podadrem.app.js","url":"app.js"},
+ {"name":"podadrem.img","url":"app-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/podadrem/screenshot1.png b/apps/podadrem/screenshot1.png
new file mode 100644
index 000000000..50f3f17f1
Binary files /dev/null and b/apps/podadrem/screenshot1.png differ
diff --git a/apps/podadrem/screenshot2.png b/apps/podadrem/screenshot2.png
new file mode 100644
index 000000000..e20c808a2
Binary files /dev/null and b/apps/podadrem/screenshot2.png differ
diff --git a/apps/poikkipuinen/ChangeLog b/apps/poikkipuinen/ChangeLog
new file mode 100644
index 000000000..5560f00bc
--- /dev/null
+++ b/apps/poikkipuinen/ChangeLog
@@ -0,0 +1 @@
+0.01: New App!
diff --git a/apps/poikkipuinen/README.md b/apps/poikkipuinen/README.md
new file mode 100644
index 000000000..12f8d5d7e
--- /dev/null
+++ b/apps/poikkipuinen/README.md
@@ -0,0 +1,7 @@
+# Poikkipuinen
+
+By Jukio Kallio
+
+A Minimal digital watch face. Follows the theme colors.
+
+
diff --git a/apps/poikkipuinen/app-icon.js b/apps/poikkipuinen/app-icon.js
new file mode 100644
index 000000000..d7ddba399
--- /dev/null
+++ b/apps/poikkipuinen/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwkEogA0/4AKCpNPCxYAB+gtTGJQuOGBAWPGAwuQGAwXamQULkYXGBQUgn4WJ+cCMAwXNiQXV+MBC6swh4XU+cAn4XU+IUBC6kgj4XUIwKnV+EDC6sQl4XU+UBd6q8BC6q8BC6i8CC6i8CC6a8DC6a8DC6a8DC6S8EC6S8EC6S8EC6K8FC6K8FC6C8BIwwXOXgwXQXgwXQkIWHd6IXPp4GBmQWJAAMjAQP0C4wAPC7hgDABwWEGCIuFGCIWGGB4uHGJwVJAFY="))
diff --git a/apps/poikkipuinen/app.js b/apps/poikkipuinen/app.js
new file mode 100644
index 000000000..0bf09c5e5
--- /dev/null
+++ b/apps/poikkipuinen/app.js
@@ -0,0 +1,158 @@
+// Poikkipuinen
+//
+// Bangle.js 2 watch face
+// by Jukio Kallio
+// www.jukiokallio.com
+
+require("Font5x9Numeric7Seg").add(Graphics);
+require("FontSinclair").add(Graphics);
+
+// settings
+const watch = {
+ x:0, y:0, w:0, h:0,
+ bgcolor:g.theme.bg,
+ fgcolor:g.theme.fg,
+ font: "5x9Numeric7Seg", fontsize: 1,
+ font2: "Sinclair", font2size: 1,
+ finland:true, // change if you want Finnish style date, or US style
+};
+
+
+// set some additional settings
+watch.w = g.getWidth(); // size of the background
+watch.h = g.getHeight();
+watch.x = watch.w * 0.5; // position of the circles
+watch.y = watch.h * 0.41;
+
+const dateWeekday = { 0: "SUN", 1: "MON", 2: "TUE", 3: "WED", 4:"THU", 5:"FRI", 6:"SAT" }; // weekdays
+
+var wait = 60000; // wait time, normally a minute
+
+
+// timeout used to update every minute
+var drawTimeout;
+
+// schedule a draw for the next minute
+function queueDraw() {
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = setTimeout(function() {
+ drawTimeout = undefined;
+ draw();
+ }, wait - (Date.now() % wait));
+}
+
+
+// main function
+function draw() {
+ // make date object
+ var date = new Date();
+
+ // work out the date string
+ var dateDay = date.getDate();
+ var dateMonth = date.getMonth() + 1;
+ var dateYear = date.getFullYear();
+ var dateStr = dateMonth + "." + dateDay + "." + dateYear;
+ if (watch.finland) dateStr = dateDay + "." + dateMonth + "." + dateYear; // the true way of showing date
+ var dateStr2 = dateWeekday[date.getDay()];
+
+ // Reset the state of the graphics library
+ g.reset();
+
+ // Clear the area where we want to draw the time
+ g.setColor(watch.bgcolor);
+ g.fillRect(0, 0, watch.w, watch.h);
+
+ // set foreground color
+ g.setColor(watch.fgcolor);
+ g.setFontAlign(1,-1).setFont(watch.font, watch.fontsize);
+
+ // watch face size
+ var facew, faceh; // halves of the size for easier calculation
+ facew = 50;
+ faceh = 59;
+
+ // save hour and minute y positions
+ var houry, minutey;
+
+ // draw hour meter
+ g.drawLine(watch.x - facew, watch.y - faceh, watch.x - facew, watch.y + faceh);
+ var lines = 13;
+ var lineh = faceh * 2 / (lines - 2);
+ for (var i = 1; i < lines; i++) {
+ var w = 3;
+ var y = faceh - lineh * (i - 1);
+
+ if (i % 3 == 0) {
+ // longer line and numbers every 3
+ w = 5;
+ g.drawString(i, watch.x - facew - 2, y + watch.y);
+ }
+
+ g.drawLine(watch.x - facew, y + watch.y, watch.x - facew + w, y + watch.y);
+
+ // get hour y position
+ var hour = date.getHours() % 12; // modulate away the 24h
+ if (hour == 0) hour = 12; // fix a problem with 0-23 hours
+ //var hourMin = date.getMinutes() / 60; // move hour line by minutes
+ var hourMin = Math.floor(date.getMinutes() / 15) / 4; // move hour line by 15-minutes
+ if (hour == 12) hourMin = 0; // don't do minute moving if 12 (line ends there)
+ if (i == hour) houry = y - (lineh * hourMin);
+ }
+
+ // draw minute meter
+ g.drawLine(watch.x + facew, watch.y - faceh, watch.x + facew, watch.y + faceh);
+ g.setFontAlign(-1,-1);
+ lines = 60;
+ lineh = faceh * 2 / (lines - 1);
+ for (i = 0; i < lines; i++) {
+ var mw = 3;
+ var my = faceh - lineh * i;
+
+ if (i % 15 == 0 && i != 0) {
+ // longer line and numbers every 3
+ mw = 5;
+ g.drawString(i, watch.x + facew + 4, my + watch.y);
+ }
+
+ //if (i % 2 == 0 || i == 15 || i == 45)
+ g.drawLine(watch.x + facew, my + watch.y, watch.x + facew - mw, my + watch.y);
+
+ // get minute y position
+ if (i == date.getMinutes()) minutey = my;
+ }
+
+ // draw the time
+ var timexpad = 8;
+ g.drawLine(watch.x - facew + timexpad, watch.y + houry, watch.x + facew - timexpad, watch.y + minutey);
+
+ // draw date
+ var datey = 14;
+ g.setFontAlign(0,-1);
+ g.drawString(dateStr, watch.x, watch.y + faceh + datey);
+ g.setFontAlign(0,-1).setFont(watch.font2, watch.font2size);
+ g.drawString(dateStr2, watch.x, watch.y + faceh + datey*2);
+
+ // queue draw
+ queueDraw();
+}
+
+
+// Clear the screen once, at startup
+g.clear();
+// draw immediately at first
+draw();
+
+
+// Stop updates when LCD is off, restart when on
+Bangle.on('lcdPower',on=>{
+ if (on) {
+ draw(); // draw immediately, queue redraw
+ } else { // stop draw timer
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = undefined;
+ }
+});
+
+
+// Show launcher when middle button pressed
+Bangle.setUI("clock");
diff --git a/apps/poikkipuinen/app.png b/apps/poikkipuinen/app.png
new file mode 100644
index 000000000..fa506c886
Binary files /dev/null and b/apps/poikkipuinen/app.png differ
diff --git a/apps/poikkipuinen/metadata.json b/apps/poikkipuinen/metadata.json
new file mode 100644
index 000000000..ec95ab7ce
--- /dev/null
+++ b/apps/poikkipuinen/metadata.json
@@ -0,0 +1,16 @@
+{ "id": "poikkipuinen",
+ "name": "Poikkipuinen - Minimal watch face",
+ "shortName":"Poikkipuinen",
+ "version":"0.01",
+ "description": "A minimal digital watch face.",
+ "icon": "app.png",
+ "screenshots": [{"url":"screenshot1.png"}],
+ "type": "clock",
+ "tags": "clock",
+ "supports" : ["BANGLEJS","BANGLEJS2"],
+ "readme": "README.md",
+ "storage": [
+ {"name":"poikkipuinen.app.js","url":"app.js"},
+ {"name":"poikkipuinen.img","url":"app-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/poikkipuinen/screenshot1.png b/apps/poikkipuinen/screenshot1.png
new file mode 100644
index 000000000..23fcc348c
Binary files /dev/null and b/apps/poikkipuinen/screenshot1.png differ
diff --git a/apps/pokeclk/ChangeLog b/apps/pokeclk/ChangeLog
index 8e506ce50..5838e596d 100644
--- a/apps/pokeclk/ChangeLog
+++ b/apps/pokeclk/ChangeLog
@@ -1,2 +1,3 @@
0.01: New face :)
0.02: Color image compressed
+0.03: Improved clock
diff --git a/apps/pokeclk/app.js b/apps/pokeclk/app.js
index 17a487bc0..7e495f7d2 100644
--- a/apps/pokeclk/app.js
+++ b/apps/pokeclk/app.js
@@ -5,7 +5,7 @@ const width = g.getWidth();
const height = g.getHeight();
const font = "Vector:12";
-const locale = require("locale");
+var drawTimeout;
var img = {
width : 176, height : 149, bpp : 4,
@@ -20,34 +20,12 @@ var night= {
buffer : (atob("ERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERABEREREREREAEREREREREREREREREREREREREREREREREREREREREREREREQABERERERHwABEREREREREREREREREREREREREREREREREREREREREREREREQ/xERERER/wERERERERERERERERERERERERERERERERERERERERERERERERH//xERERH//xERERERERERERERERERERERERERERERERERERERERERERERERH//xEREf/xERERERERERERERERERERERERERERERERERERERERERERERERERH///////ERERERERERERERERERERERERERERERERERERERERERERERERERER////////8RERERERERERERERERERERERERERERERERERERERERERERERERH/////////ERERERERERERERERERERERERERERERERERERERERERERERERER////D///DxERERERERERERERERERERERERERERERERERERERERERERERERH////w///w8RERERERERERERERERERERERERERERERERERERERER//ERERER//AA///w/w8REREREREREREREREREREREREREREREREREREREREf////EREf/wAP/wAA8PERERERERERERERERERERERERERERERERERERERERH////xERH/8AD/////DxERERERERERERERERERERERERERERERERERERERER////8REf//////////ERERERERERERERERERERERERERERERERERERERERERERH/8R/////////xERERERERERERERERERERERERERERERERERERERER////////Ef////////////////////////////////////////////////////////////////////////////////////////////////////////////8RERERH////////////xERERERERERERERERERERERERERERERERERERERERERERERH///////////EREREREREREREREREREREREREREREREREREREREREf/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////w=="))
};
-var time= "10:20";
-
-function time() { //numbers
- // work out how to display the current time
- const d = new Date();
- const h = d.getHours(),
- m = d.getMinutes();
- const time = h + ":" + ("0" + m).substr(-2);
- const day = Date.now();
- const mo = d.getMonth()+1;
- const damo = d.getDate();
-
- var dayMonth = mo+"-"+damo;
-
- // time
- require("Font4x5").add(Graphics);
- isDark();
- g.setFontAlign(0,0);
- //g.setFont("6x8:4x5");
- g.setFont("4x5",7);
- g.drawString(time, width/2, height/2);
- // date
- require("Font4x5").add(Graphics);
- g.setFontAlign(1,1);
- //g.setFont("4x6",2);
- g.setFont("4x5",3);
- g.drawString(dayMonth, width/2+60, height/2+40);
-
+function queueDraw() {
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = setTimeout(function() {
+ drawTimeout = undefined;
+ draw();
+ }, 60000 - (Date.now() % 60000));
}
function isDark(){
@@ -59,6 +37,22 @@ function isDark(){
}
}
+function time() {
+ var d = new Date();
+ var day = d.getDate();
+ var time = require("locale").time(d,1);
+ var date = require("locale").date(d);
+ var mo = require("date_utils").month(d.getMonth()+1,1);
+
+ require("Font4x5").add(Graphics); // time
+ isDark();
+ g.setFontAlign(0,0);
+ g.setFont("4x5",7.5).drawString(time, width/2, height/2);
+
+ g.setFontAlign(1,1);
+ g.setFont("4x5",3).drawString(mo+" "+day, width-15, height-35);
+}
+
function draw() { //poketch background
if (g.theme.dark==true){
g.drawImage(night, 0, 25, {scale:2}); //poketch is life
@@ -67,20 +61,13 @@ function draw() { //poketch background
g.drawImage(img, 0, 25); //poketch is life
}
time();
+ queueDraw();
}
//program start
g.clear();
draw();
-var secondInterval = setInterval(draw, 1000); // Stop updates when LCD is off, restart when on
-Bangle.on('lcdPower',on=>{
- if (secondInterval) clearInterval(secondInterval);
- secondInterval = undefined;
- if (on) {
- secondInterval = setInterval(draw, 1000);
- draw(); // draw immediately
- }
-});
+
// Show launcher when middle button pressed
Bangle.setUI("clock");
// Load widgets
diff --git a/apps/pokeclk/metadata.json b/apps/pokeclk/metadata.json
index 433077efe..c022868ec 100644
--- a/apps/pokeclk/metadata.json
+++ b/apps/pokeclk/metadata.json
@@ -2,7 +2,7 @@
"id": "pokeclk",
"name": "Poketch Clock",
"shortName":"Poketch Clock",
- "version": "0.02",
+ "version": "0.03",
"description": "A clock based on the Poketch electronic device found in Sinnoh",
"icon": "app.png",
"type": "clock",
diff --git a/apps/pomoplus/ChangeLog b/apps/pomoplus/ChangeLog
new file mode 100644
index 000000000..1a137aad0
--- /dev/null
+++ b/apps/pomoplus/ChangeLog
@@ -0,0 +1,3 @@
+0.01: New app!
+0.02-0.04: Bug fixes
+0.05: Submitted to the app loader
\ No newline at end of file
diff --git a/apps/pomoplus/app.js b/apps/pomoplus/app.js
new file mode 100644
index 000000000..73af5c935
--- /dev/null
+++ b/apps/pomoplus/app.js
@@ -0,0 +1,157 @@
+Bangle.POMOPLUS_ACTIVE = true; //Prevent the boot code from running. To avoid having to reload on every interaction, we'll control the vibrations from here when the user is in the app.
+
+const storage = require("Storage");
+const common = require("pomoplus-com.js");
+
+//Expire the state if necessary
+if (
+ common.settings.pausedTimerExpireTime != 0 &&
+ !common.state.running &&
+ (new Date()).getTime() - common.state.pausedTime > common.settings.pausedTimerExpireTime
+) {
+ common.state = common.STATE_DEFAULT;
+}
+
+function drawButtons() {
+ //Draw the backdrop
+ const BAR_TOP = g.getHeight() - 24;
+ g.setColor(0, 0, 1).setFontAlign(0, -1)
+ .clearRect(0, BAR_TOP, g.getWidth(), g.getHeight())
+ .fillRect(0, BAR_TOP, g.getWidth(), g.getHeight())
+ .setColor(1, 1, 1);
+
+ if (!common.state.wasRunning) { //If the timer was never started, only show a play button
+ g.drawImage(common.BUTTON_ICONS.play, g.getWidth() / 2, BAR_TOP);
+ } else {
+ g.drawLine(g.getWidth() / 2, BAR_TOP, g.getWidth() / 2, g.getHeight());
+ if (common.state.running) {
+ g.drawImage(common.BUTTON_ICONS.pause, g.getWidth() / 4, BAR_TOP)
+ .drawImage(common.BUTTON_ICONS.skip, g.getWidth() * 3 / 4, BAR_TOP);
+ } else {
+ g.drawImage(common.BUTTON_ICONS.reset, g.getWidth() / 4, BAR_TOP)
+ .drawImage(common.BUTTON_ICONS.play, g.getWidth() * 3 / 4, BAR_TOP);
+ }
+ }
+}
+
+function drawTimerAndMessage() {
+ g.reset()
+ .setFontAlign(0, 0)
+ .setFont("Vector", 36)
+ .clearRect(0, 24, 176, 152)
+
+ //Draw the timer
+ .drawString((() => {
+ let timeLeft = common.getTimeLeft();
+ let hours = timeLeft / 3600000;
+ let minutes = (timeLeft % 3600000) / 60000;
+ let seconds = (timeLeft % 60000) / 1000;
+
+ function pad(number) {
+ return ('00' + parseInt(number)).slice(-2);
+ }
+
+ if (hours >= 1) return `${parseInt(hours)}:${pad(minutes)}:${pad(seconds)}`;
+ else return `${parseInt(minutes)}:${pad(seconds)}`;
+ })(), g.getWidth() / 2, g.getHeight() / 2)
+
+ //Draw the phase label
+ .setFont("Vector", 12)
+ .drawString(((currentPhase, numShortBreaks) => {
+ if (!common.state.wasRunning) return "Not started";
+ else if (currentPhase == common.PHASE_WORKING) return `Work ${numShortBreaks + 1}/${common.settings.numShortBreaks + 1}`
+ else if (currentPhase == common.PHASE_SHORT_BREAK) return `Short break ${numShortBreaks + 1}/${common.settings.numShortBreaks}`;
+ else return "Long break!";
+ })(common.state.phase, common.state.numShortBreaks),
+ g.getWidth() / 2, g.getHeight() / 2 + 18);
+
+ //Update phase with vibation if needed
+ if (common.getTimeLeft() <= 0) {
+ common.nextPhase(true);
+ }
+}
+
+drawButtons();
+Bangle.on("touch", (button, xy) => {
+ //If we support full touch and we're not touching the keys, ignore.
+ //If we don't support full touch, we can't tell so just assume we are.
+ if (xy !== undefined && xy.y <= g.getHeight() - 24) return;
+
+ if (!common.state.wasRunning) {
+ //If we were never running, there is only one button: the start button
+ let now = (new Date()).getTime();
+ common.state = {
+ wasRunning: true,
+ running: true,
+ startTime: now,
+ pausedTime: now,
+ elapsedTime: 0,
+ phase: common.PHASE_WORKING,
+ numShortBreaks: 0
+ };
+ setupTimerInterval();
+ drawButtons();
+
+ } else if (common.state.running) {
+ //If we are running, there are two buttons: pause and skip
+ if (button == 1) {
+ //Record the exact moment that we paused
+ let now = (new Date()).getTime();
+ common.state.pausedTime = now;
+
+ //Stop the timer
+ common.state.running = false;
+ clearInterval(timerInterval);
+ timerInterval = undefined;
+ drawTimerAndMessage();
+ drawButtons();
+
+ } else {
+ common.nextPhase(false);
+ }
+
+ } else {
+ //If we are stopped, there are two buttons: Reset and continue
+ if (button == 1) {
+ //Reset the timer
+ common.state = common.STATE_DEFAULT;
+ drawTimerAndMessage();
+ drawButtons();
+
+ } else {
+ //Start the timer and record old elapsed time and when we started
+ let now = (new Date()).getTime();
+ common.state.elapsedTime += common.state.pausedTime - common.state.startTime;
+ common.state.startTime = now;
+ common.state.running = true;
+ drawTimerAndMessage();
+ setupTimerInterval();
+ drawButtons();
+ }
+ }
+});
+
+let timerInterval;
+
+function setupTimerInterval() {
+ if (timerInterval !== undefined) {
+ clearInterval(timerInterval);
+ }
+ setTimeout(() => {
+ timerInterval = setInterval(drawTimerAndMessage, 1000);
+ drawTimerAndMessage();
+ }, common.timeLeft % 1000);
+}
+
+drawTimerAndMessage();
+if (common.state.running) {
+ setupTimerInterval();
+}
+
+//Save our state when the app is closed
+E.on('kill', () => {
+ storage.writeJSON(common.STATE_PATH, common.state);
+});
+
+Bangle.loadWidgets();
+Bangle.drawWidgets();
\ No newline at end of file
diff --git a/apps/pomoplus/boot.js b/apps/pomoplus/boot.js
new file mode 100644
index 000000000..edc233853
--- /dev/null
+++ b/apps/pomoplus/boot.js
@@ -0,0 +1,19 @@
+const POMOPLUS_storage = require("Storage");
+const POMOPLUS_common = require("pomoplus-com.js");
+
+function setNextTimeout() {
+ setTimeout(() => {
+ //Make sure that the pomoplus app isn't in the foreground. The pomoplus app handles the vibrations when it is in the foreground in order to avoid having to reload every time the user changes state. That means that when the app is in the foreground, we shouldn't do anything here.
+ //We do this after the timer rather than before because the timer will start before the app executes.
+ if (Bangle.POMOPLUS_ACTIVE === undefined) {
+ POMOPLUS_common.nextPhase(true);
+ setNextTimeout();
+ POMOPLUS_storage.writeJSON(POMOPLUS_common.STATE_PATH, POMOPLUS_common.state)
+ }
+ }, POMOPLUS_common.getTimeLeft());
+}
+
+//Only start the timeout if the timer is running
+if (POMOPLUS_common.state.running) {
+ setNextTimeout();
+}
\ No newline at end of file
diff --git a/apps/pomoplus/common.js b/apps/pomoplus/common.js
new file mode 100644
index 000000000..b1cd42de8
--- /dev/null
+++ b/apps/pomoplus/common.js
@@ -0,0 +1,118 @@
+const storage = require("Storage");
+const heatshrink = require("heatshrink");
+
+exports.STATE_PATH = "pomoplus.state.json";
+exports.SETTINGS_PATH = "pomoplus.json";
+
+exports.PHASE_WORKING = 0;
+exports.PHASE_SHORT_BREAK = 1;
+exports.PHASE_LONG_BREAK = 2;
+
+exports.BUTTON_ICONS = {
+ play: heatshrink.decompress(atob("jEYwMAkAGBnACBnwCBn+AAQPgAQPwAQP8AQP/AQXAAQPwAQP8AQP+AQgICBwQUCEAn4FggyBHAQ+CIgQ")),
+ pause: heatshrink.decompress(atob("jEYwMA/4BBAX4CEA")),
+ reset: heatshrink.decompress(atob("jEYwMA/4BB/+BAQPDAQPnAQIAKv///0///8j///EP//wAQQICBwQUCEhgyCHAQ+CIgI=")),
+ skip: heatshrink.decompress(atob("jEYwMAwEIgHAhkA8EOgHwh8A/EPwH8h/A/0P8H/h/w/+P/H/5/8//v/3/AAoICBwQUCDQIgCEwQsCGQQ4CHwRECA"))
+};
+
+exports.settings = storage.readJSON(exports.SETTINGS_PATH);
+if (!exports.settings) {
+ exports.settings = {
+ workTime: 1500000, //Work for 25 minutes
+ shortBreak: 300000, //5 minute short break
+ longBreak: 900000, //15 minute long break
+ numShortBreaks: 3, //3 short breaks for every long break
+ pausedTimerExpireTime: 21600000, //If the timer was left paused for >6 hours, reset it on next launch
+ widget: false //If a widget is added in the future, whether the user wants it
+ };
+}
+
+//Store the minimal amount of information to be able to reconstruct the state of the timer at any given time.
+//This is necessary because it is necessary to write to flash to let the timer run in the background, so minimizing the writes is necessary.
+exports.STATE_DEFAULT = {
+ wasRunning: false, //If the timer ever was running. Used to determine whether to display a reset button
+ running: false, //Whether the timer is currently running
+ startTime: 0, //When the timer was last started. Difference between this and now is how long timer has run continuously.
+ pausedTime: 0, //When the timer was last paused. Used for expiration and displaying timer while paused.
+ elapsedTime: 0, //How much time the timer had spent running before the current start time. Update on pause or user skipping stages.
+ phase: exports.PHASE_WORKING, //What phase the timer is currently in
+ numShortBreaks: 0 //Number of short breaks that have occured so far
+};
+exports.state = storage.readJSON(exports.STATE_PATH);
+if (!exports.state) {
+ exports.state = exports.STATE_DEFAULT;
+}
+
+//Get the number of milliseconds until the next phase change
+exports.getTimeLeft = function () {
+ if (!exports.state.wasRunning) {
+ //If the timer never ran, the time left is just the amount of work time.
+ return exports.settings.workTime;
+ } else if (exports.state.running) {
+ //If the timer is running, the time left is current time - start time + preexisting time
+ var runningTime = (new Date()).getTime() - exports.state.startTime + exports.state.elapsedTime;
+ } else {
+ //If the timer is not running, the same as above but use when the timer was paused instead of now.
+ var runningTime = exports.state.pausedTime - exports.state.startTime + exports.state.elapsedTime;
+ }
+
+ if (exports.state.phase == exports.PHASE_WORKING) {
+ return exports.settings.workTime - runningTime;
+ } else if (exports.state.phase == exports.PHASE_SHORT_BREAK) {
+ return exports.settings.shortBreak - runningTime;
+ } else {
+ return exports.settings.longBreak - runningTime;
+ }
+}
+
+//Get the next phase to change to
+exports.getNextPhase = function () {
+ if (exports.state.phase == exports.PHASE_WORKING) {
+ if (exports.state.numShortBreaks < exports.settings.numShortBreaks) {
+ return exports.PHASE_SHORT_BREAK;
+ } else {
+ return exports.PHASE_LONG_BREAK;
+ }
+ } else {
+ return exports.PHASE_WORKING;
+ }
+}
+
+//Change to the next phase and update numShortBreaks, and optionally vibrate. DOES NOT WRITE STATE CHANGE TO STORAGE!
+exports.nextPhase = function (vibrate) {
+ a = {
+ startTime: 0, //When the timer was last started. Difference between this and now is how long timer has run continuously.
+ pausedTime: 0, //When the timer was last paused. Used for expiration and displaying timer while paused.
+ elapsedTime: 0, //How much time the timer had spent running before the current start time. Update on pause or user skipping stages.
+ phase: exports.PHASE_WORKING, //What phase the timer is currently in
+ numShortBreaks: 0 //Number of short breaks that have occured so far
+ }
+ let now = (new Date()).getTime();
+ exports.state.startTime = now; //The timer is being reset, so say it starts now.
+ exports.state.pausedTime = now; //This prevents a paused timer from having the start time moved to the future and therefore having been run for negative time.
+ exports.state.elapsedTime = 0; //Because we are resetting the timer, we no longer need to care about whether it was paused previously.
+
+ let oldPhase = exports.state.phase; //Cache the old phase because we need to remember it when counting the number of short breaks
+ exports.state.phase = exports.getNextPhase();
+
+ if (oldPhase == exports.PHASE_SHORT_BREAK) {
+ //If we just left a short break, increase the number of short breaks
+ exports.state.numShortBreaks++;
+ } else if (oldPhase == exports.PHASE_LONG_BREAK) {
+ //If we just left a long break, set the number of short breaks to zero
+ exports.state.numShortBreaks = 0;
+ }
+
+ if (vibrate) {
+ if (exports.state.phase == exports.PHASE_WORKING) {
+ Bangle.buzz(750, 1);
+ } else if (exports.state.phase == exports.PHASE_SHORT_BREAK) {
+ Bangle.buzz();
+ setTimeout(Bangle.buzz, 400);
+ } else {
+ Bangle.buzz();
+ setTimeout(Bangle.buzz, 400);
+ setTimeout(Bangle.buzz, 600);
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/pomoplus/icon.js b/apps/pomoplus/icon.js
new file mode 100644
index 000000000..e4ecc7d1c
--- /dev/null
+++ b/apps/pomoplus/icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwcBkmSpICG5AIHCLMmoQOKycJAoUyiQRLAgIOBAQQyKsmSpAROpgEBhmQzIHBC4JTIDIxcHDQYgCBQUSphEGpMJkwcEwgLByBoFCIMyCIgpDL4RQEBwWQ5ICBDoRKCBAIFBNYeSjJHDKYYaCR4YLBiYgDKYo4DEwQgECIpiECISqFkJlCCIILETwYRGDo1CsiJECIiPCdIaqCSoabFCgYRHAQ5iBCJ8hcAgRNKwOQgARLU4IRCvwRMa4QRPfwQR5YooR/cAYOGgAADvwEDCI8H/4AG/Ek5IRXGpMkzJZNoQGByYRNiQJCsgRLyAJDpgRQpIRLwgJEWxARBkIJFUg4RChIJGQA4RJNw4RKLg0kCJQ4DVoUACIY"))
\ No newline at end of file
diff --git a/apps/pomoplus/icon.png b/apps/pomoplus/icon.png
new file mode 100644
index 000000000..60d8023db
Binary files /dev/null and b/apps/pomoplus/icon.png differ
diff --git a/apps/pomoplus/img/pause.png b/apps/pomoplus/img/pause.png
new file mode 100644
index 000000000..ad31dadcf
Binary files /dev/null and b/apps/pomoplus/img/pause.png differ
diff --git a/apps/pomoplus/img/play.png b/apps/pomoplus/img/play.png
new file mode 100644
index 000000000..6c20c24c5
Binary files /dev/null and b/apps/pomoplus/img/play.png differ
diff --git a/apps/pomoplus/img/reset.png b/apps/pomoplus/img/reset.png
new file mode 100644
index 000000000..7a317d097
Binary files /dev/null and b/apps/pomoplus/img/reset.png differ
diff --git a/apps/pomoplus/img/skip.png b/apps/pomoplus/img/skip.png
new file mode 100644
index 000000000..375318069
Binary files /dev/null and b/apps/pomoplus/img/skip.png differ
diff --git a/apps/pomoplus/metadata.json b/apps/pomoplus/metadata.json
new file mode 100644
index 000000000..068eeed91
--- /dev/null
+++ b/apps/pomoplus/metadata.json
@@ -0,0 +1,37 @@
+{
+ "id": "pomoplus",
+ "name": "Pomodoro Plus",
+ "version": "0.05",
+ "description": "A configurable pomodoro timer that runs in the background.",
+ "icon": "icon.png",
+ "type": "app",
+ "tags": "pomodoro,cooking,tools",
+ "supports": [
+ "BANGLEJS",
+ "BANGLEJS2"
+ ],
+ "allow_emulator": true,
+ "storage": [
+ {
+ "name": "pomoplus.app.js",
+ "url": "app.js"
+ },
+ {
+ "name": "pomoplus.img",
+ "url": "icon.js",
+ "evaluate": true
+ },
+ {
+ "name": "pomoplus.boot.js",
+ "url": "boot.js"
+ },
+ {
+ "name": "pomoplus-com.js",
+ "url": "common.js"
+ },
+ {
+ "name": "pomoplus.settings.js",
+ "url": "settings.js"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/apps/pomoplus/settings.js b/apps/pomoplus/settings.js
new file mode 100644
index 000000000..1ff52340a
--- /dev/null
+++ b/apps/pomoplus/settings.js
@@ -0,0 +1,94 @@
+const SETTINGS_PATH = 'pomoplus.json';
+const storage = require("Storage");
+
+(function (back) {
+ let settings = storage.readJSON(SETTINGS_PATH);
+ if (!settings) {
+ settings = {
+ workTime: 1500000, //Work for 25 minutes
+ shortBreak: 300000, //5 minute short break
+ longBreak: 900000, //15 minute long break
+ numShortBreaks: 3, //3 short breaks for every long break
+ pausedTimerExpireTime: 21600000, //If the timer was left paused for >6 hours, reset it on next launch
+ widget: false //If a widget is added in the future, whether the user wants it
+ };
+ }
+
+ function save() {
+ storage.writeJSON(SETTINGS_PATH, settings);
+ }
+
+ const menu = {
+ '': { 'title': 'Pomodoro Plus' },
+ '< Back': back,
+ 'Work time': {
+ value: settings.workTime,
+ step: 60000, //1 minute
+ min: 60000,
+ // max: 10800000,
+ // wrap: true,
+ onchange: function (value) {
+ settings.workTime = value;
+ save();
+ },
+ format: function (value) {
+ return '' + (value / 60000) + 'm'
+ }
+ },
+ 'Short break time': {
+ value: settings.shortBreak,
+ step: 60000,
+ min: 60000,
+ // max: 10800000,
+ // wrap: true,
+ onchange: function (value) {
+ settings.shortBreak = value;
+ save();
+ },
+ format: function (value) {
+ return '' + (value / 60000) + 'm'
+ }
+ },
+ '# Short breaks': {
+ value: settings.numShortBreaks,
+ step: 1,
+ min: 0,
+ // max: 10800000,
+ // wrap: true,
+ onchange: function (value) {
+ settings.numShortBreaks = value;
+ save();
+ }
+ },
+ 'Long break time': {
+ value: settings.longBreak,
+ step: 60000,
+ min: 60000,
+ // max: 10800000,
+ // wrap: true,
+ onchange: function (value) {
+ settings.longBreak = value;
+ save();
+ },
+ format: function (value) {
+ return '' + (value / 60000) + 'm'
+ }
+ },
+ 'Timer expiration': {
+ value: settings.pausedTimerExpireTime,
+ step: 900000, //15 minutes
+ min: 0,
+ // max: 10800000,
+ // wrap: true,
+ onchange: function (value) {
+ settings.pausedTimerExpireTime = value;
+ save();
+ },
+ format: function (value) {
+ if (value == 0) return "Off"
+ else return `${Math.floor(value / 3600000)}h ${(value % 3600000) / 60000}m`
+ }
+ },
+ };
+ E.showMenu(menu)
+})
\ No newline at end of file
diff --git a/apps/pooqroman/ChangeLog b/apps/pooqroman/ChangeLog
index c4f3171d3..b21b34b58 100644
--- a/apps/pooqroman/ChangeLog
+++ b/apps/pooqroman/ChangeLog
@@ -1,3 +1,4 @@
0.01: Initial check-in.
0.02: Make internal menu time out + small fixes.
0.03: Autolight feature.
+0.04: Added adjustment for Bangle.js magnetometer heading fix
diff --git a/apps/pooqroman/app.js b/apps/pooqroman/app.js
index fcb2437e1..7bd749ac4 100644
--- a/apps/pooqroman/app.js
+++ b/apps/pooqroman/app.js
@@ -70,7 +70,7 @@ class Options {
delay
);
}
-
+
bless(k) {
Object.defineProperty(this, k, {
get: () => this.backing[k],
@@ -103,7 +103,7 @@ class Options {
if (this.bored) clearTimeout(this.bored);
this.bored = setTimeout(_ => this.showMenu(), 15000);
}
-
+
reset() {
this.backing = {__proto__: this.constructor.defaults};
this.writeBack(0);
@@ -145,7 +145,7 @@ class RomanOptions extends Options {
Defaults: _ => {this.reset(); this.interact();}
};
}
-
+
interact() {this.showMenu(this.menu);}
}
@@ -337,7 +337,7 @@ const events = {
// colour: colour, dramatic?: bool, event?: any}
fixed: [{time: Number.POSITIVE_INFINITY}], // indexed by ms absolute
wall: [{time: Number.POSITIVE_INFINITY}], // indexed by nominal ms + TZ ms
-
+
clean: function(now, l) {
let o = now.getTimezoneOffset() * 60000;
let tf = now.getTime() + l, tw = tf - o;
@@ -345,7 +345,7 @@ const events = {
while (this.wall[0].time <= tw) this.wall.shift();
while (this.fixed[0].time <= tf) this.fixed.shift();
},
-
+
scan: function(now, from, to, f) {
result = Infinity;
let o = now.getTimezoneOffset() * 60000;
@@ -482,7 +482,7 @@ class Sidebar {
compassI,
this.x + 4 + imageWidth(compassI) / 2,
this.y + 4 + imageHeight(compassI) / 2,
- a ? {rotate: c.heading / 180 * Math.PI} : undefined
+ a ? {rotate: (360-c.heading) / 180 * Math.PI} : undefined
);
this.y += 4 + imageHeight(compassI);
}
@@ -535,13 +535,13 @@ class Roman {
static pos(p, r) {
let h = r * rectW / 2;
let v = r * rectH / 2;
- p = (p + 1) % 12;
+ p = (p + 1) % 12;
return p <= 2 ? [faceCX + h * (p - 1), faceCY - v]
: p < 6 ? [faceCX + h, faceCY + v / 2 * (p - 4)]
: p <= 8 ? [faceCX - h * (p - 7), faceCY + v]
: [faceCX - h, faceCY - v / 2 * (p - 10)];
}
-
+
alert(e, date, now, past) {
const g = this.g;
g.setColor(e.colour);
@@ -564,7 +564,7 @@ class Roman {
}
return Infinity;
}
-
+
render(d, rate) {
const g = this.g;
const state = this.state || (g.clear(true), this.state = {});
@@ -625,7 +625,7 @@ class Roman {
for (let h = keyHour; h < keyHour + 12; h++) {
g.drawString(
numeral(h % 24, options),
- faceX + layout[h % 12 * 2],
+ faceX + layout[h % 12 * 2],
faceY + layout[h % 12 * 2 + 1]
);
}
@@ -643,7 +643,7 @@ class Roman {
(e, t, p) => this.alert(e, t, d, p)
);
if (rate > requestedRate) rate = requestedRate;
-
+
// Hands
// Here we are using incremental hands for hours and minutes.
// If we quantised, we could use hand-crafted bitmaps, though.
@@ -668,7 +668,7 @@ class Clock {
this.rates = {};
this.options.on('done', () => this.start());
-
+
this.listeners = {
charging: _ => {face.doIcons('charging'); this.active();},
lock: _ => {face.doIcons('locked'); this.active();},
@@ -723,7 +723,7 @@ class Clock {
this.face.reset(); // Cancel any ongoing background rendering
return this;
}
-
+
active() {
const prev = this.rate;
const now = Date.now();
diff --git a/apps/pooqroman/metadata.json b/apps/pooqroman/metadata.json
index 8cdbea728..0294e22a0 100644
--- a/apps/pooqroman/metadata.json
+++ b/apps/pooqroman/metadata.json
@@ -1,7 +1,7 @@
{ "id": "pooqroman",
"name": "pooq Roman watch face",
"shortName":"pooq Roman",
- "version":"0.03",
+ "version":"0.04",
"description": "A classic watch face with a certain dynamicity. Most amusing in 24h mode. Slide up to show more hands, down for less(!). By design does not support standard widgets, sorry!",
"icon": "app.png",
"type": "clock",
diff --git a/apps/powermanager/ChangeLog b/apps/powermanager/ChangeLog
index f0b60a45a..a83e8c676 100644
--- a/apps/powermanager/ChangeLog
+++ b/apps/powermanager/ChangeLog
@@ -1,3 +1,5 @@
0.01: New App!
0.02: Allow forcing monotonic battery voltage/percentage
0.03: Use default Bangle formatter for booleans
+0.04: Remove calibration with current voltage (Calibrate->Auto) as it is now handled by settings app
+ Allow automatic calibration on every charge longer than 3 hours
diff --git a/apps/powermanager/README.md b/apps/powermanager/README.md
index 434ec814e..88b3c370a 100644
--- a/apps/powermanager/README.md
+++ b/apps/powermanager/README.md
@@ -3,8 +3,9 @@
Manages settings for charging.
Features:
* Warning threshold to be able to disconnect the charger at a given percentage
-* Set the battery calibration offset.
+* Set the battery calibration offset
* Force monotonic battery percentage or voltage
+* Automatic calibration on charging uninterrupted longer than 3 hours (reloads of the watch reset the timer).
## Internals
diff --git a/apps/powermanager/boot.js b/apps/powermanager/boot.js
index 077e24413..2bc2aaa35 100644
--- a/apps/powermanager/boot.js
+++ b/apps/powermanager/boot.js
@@ -5,7 +5,6 @@
);
if (settings.warnEnabled){
- print("Charge warning enabled");
var chargingInterval;
function handleCharging(charging){
@@ -48,4 +47,13 @@
return v;
};
}
+
+ if (settings.autoCalibration){
+ let chargeStart;
+ Bangle.on("charging", (charging)=>{
+ if (charging) chargeStart = Date.now();
+ if (chargeStart && !charging && (Date.now() - chargeStart > 1000*60*60*3)) require("powermanager").setCalibration();
+ if (!charging) chargeStart = undefined;
+ });
+ }
})();
diff --git a/apps/powermanager/lib.js b/apps/powermanager/lib.js
new file mode 100644
index 000000000..f4a7e3378
--- /dev/null
+++ b/apps/powermanager/lib.js
@@ -0,0 +1,6 @@
+// set battery calibration value by either applying the given value or setting the currently read battery voltage
+exports.setCalibration = function(calibration){
+ let s = require('Storage').readJSON("setting.json", true) || {};
+ s.batFullVoltage = calibration?calibration:((analogRead(D3) + analogRead(D3) + analogRead(D3) + analogRead(D3)) / 4);
+ require('Storage').writeJSON("setting.json", s);
+}
diff --git a/apps/powermanager/metadata.json b/apps/powermanager/metadata.json
index dd1727657..0777feee3 100644
--- a/apps/powermanager/metadata.json
+++ b/apps/powermanager/metadata.json
@@ -2,7 +2,7 @@
"id": "powermanager",
"name": "Power Manager",
"shortName": "Power Manager",
- "version": "0.03",
+ "version": "0.04",
"description": "Allow configuration of warnings and thresholds for battery charging and display.",
"icon": "app.png",
"type": "bootloader",
@@ -12,6 +12,7 @@
"storage": [
{"name":"powermanager.boot.js","url":"boot.js"},
{"name":"powermanager.settings.js","url":"settings.js"},
+ {"name":"powermanager","url":"lib.js"},
{"name":"powermanager.default.json","url":"default.json"}
]
}
diff --git a/apps/powermanager/settings.js b/apps/powermanager/settings.js
index 7cc683024..9eeb29e00 100644
--- a/apps/powermanager/settings.js
+++ b/apps/powermanager/settings.js
@@ -23,7 +23,6 @@
'': {
'title': 'Power Manager'
},
- '< Back': back,
'Monotonic percentage': {
value: !!settings.forceMonoPercentage,
onchange: v => {
@@ -44,29 +43,29 @@
}
};
-
function roundToDigits(number, stepsize) {
return Math.round(number / stepsize) * stepsize;
}
- function getCurrentVoltageDirect() {
- return (analogRead(D3) + analogRead(D3) + analogRead(D3) + analogRead(D3)) / 4;
- }
-
var stepsize = 0.0002;
- var full = 0.32;
+ var full = 0.3144;
function getInitialCalibrationOffset() {
return roundToDigits(systemsettings.batFullVoltage - full, stepsize) || 0;
}
-
var submenu_calibrate = {
'': {
- title: "Calibrate"
+ title: "Calibrate",
+ back: function() {
+ E.showMenu(mainmenu);
+ },
},
- '< Back': function() {
- E.showMenu(mainmenu);
+ 'Autodetect': {
+ value: !!settings.autoCalibration,
+ onchange: v => {
+ writeSettings("autoCalibration", v);
+ }
},
'Offset': {
min: -0.05,
@@ -75,25 +74,9 @@
value: getInitialCalibrationOffset(),
format: v => roundToDigits(v, stepsize).toFixed((""+stepsize).length - 2),
onchange: v => {
- print(typeof v);
- systemsettings.batFullVoltage = v + full;
- require("Storage").writeJSON("setting.json", systemsettings);
+ require("powermanager").setCalibration(v + full);
}
},
- 'Auto': function() {
- var newVoltage = getCurrentVoltageDirect();
- E.showAlert("Please charge fully before auto setting").then(() => {
- E.showPrompt("Set current charge as full").then((r) => {
- if (r) {
- systemsettings.batFullVoltage = newVoltage;
- require("Storage").writeJSON("setting.json", systemsettings);
- //reset value shown in menu to the newly set one
- submenu_calibrate.Offset.value = getInitialCalibrationOffset();
- E.showMenu(mainmenu);
- }
- });
- });
- },
'Clear': function() {
E.showPrompt("Clear charging offset?").then((r) => {
if (r) {
@@ -109,10 +92,10 @@
var submenu_chargewarn = {
'': {
- title: "Charge warning"
- },
- '< Back': function() {
- E.showMenu(mainmenu);
+ title: "Charge warning",
+ back: function() {
+ E.showMenu(mainmenu);
+ },
},
'Enabled': {
value: !!settings.warnEnabled,
diff --git a/apps/powersave/ChangeLog b/apps/powersave/ChangeLog
new file mode 100644
index 000000000..28d913cc8
--- /dev/null
+++ b/apps/powersave/ChangeLog
@@ -0,0 +1,3 @@
+0.01: Initial release
+0.02: Removed accelerometer poll interval adjustment, fixed a few issues with detecting the current app
+0.03: Fix a couple of silly mistakes
\ No newline at end of file
diff --git a/apps/powersave/README.md b/apps/powersave/README.md
new file mode 100644
index 000000000..51ba044e1
--- /dev/null
+++ b/apps/powersave/README.md
@@ -0,0 +1,25 @@
+# Power Saver
+
+Save your watch's battery power by halting foreground app execution while the screen is off.
+
+## Features
+- Stops foreground app processes
+- Background processes still run
+- Clears screen
+- Foreground app is returned to when screen is turned back on (app state is not preserved)
+
+## Controls
+- Automatically activates when screen times out, timing can be adjusted using normal timeout settings
+- Deactivates when screen is turned back on
+
+## Warnings
+- This is not compatible with apps that need to run in the foreground even while the screen is off, such as most stopwatch apps and some health trackers.
+- If you check your watch super often (like multiple times per minute), this may end of costing you more power than it saves since the app you are using will have to restart everytime you check it.
+
+## Requests
+
+[Contact information is on my website](https://kyleplo.com/#contact)
+
+## Creator
+
+[kyleplo](https://kyleplo.com)
\ No newline at end of file
diff --git a/apps/powersave/boot.js b/apps/powersave/boot.js
new file mode 100644
index 000000000..f37fbc536
--- /dev/null
+++ b/apps/powersave/boot.js
@@ -0,0 +1,20 @@
+var Storage = Storage || require("Storage");
+Bangle.on("lock", locked => {
+ if(locked){
+ load("powersave.screen.js");
+ }else{
+ const data = JSON.parse(Storage.read("powersave.json") || Storage.read("setting.json"));
+ load(data.app || data.clock);
+ }
+});
+E.on("init", () => {
+ if("__FILE__" in global && __FILE__ !== "powersave.screen.js"){
+ Storage.write("powersave.json", {
+ app: __FILE__
+ });
+ }else if(!("__FILE__" in global)){
+ Storage.write("powersave.json", {
+ app: null
+ });
+ }
+});
\ No newline at end of file
diff --git a/apps/powersave/metadata.json b/apps/powersave/metadata.json
new file mode 100644
index 000000000..705384058
--- /dev/null
+++ b/apps/powersave/metadata.json
@@ -0,0 +1,18 @@
+{
+ "id": "powersave",
+ "name": "Power Save",
+ "version": "0.03",
+ "description": "Halts foreground app execution while screen is off while still allowing background processes.",
+ "readme": "README.md",
+ "icon": "powersave.png",
+ "type": "bootloader",
+ "tags": "tool",
+ "supports": ["BANGLEJS2"],
+ "storage": [
+ {"name":"powersave.boot.js","url":"boot.js"},
+ {"name":"powersave.screen.js","url":"screen.js"}
+ ],
+ "data": [
+ {"name": "powersave.json"}
+ ]
+}
\ No newline at end of file
diff --git a/apps/powersave/powersave.png b/apps/powersave/powersave.png
new file mode 100644
index 000000000..fa0399b73
Binary files /dev/null and b/apps/powersave/powersave.png differ
diff --git a/apps/powersave/screen.js b/apps/powersave/screen.js
new file mode 100644
index 000000000..c920b205d
--- /dev/null
+++ b/apps/powersave/screen.js
@@ -0,0 +1,7 @@
+g.clear();
+Bangle.setLCDBrightness(0);
+if(!Bangle.isLocked()){
+ var Storage = Storage || require("Storage");
+ const data = JSON.parse(Storage.read("powersave.json") || Storage.read("setting.json"));
+ load(data.app || data.clock);
+}
\ No newline at end of file
diff --git a/apps/presentation_timer/ChangeLog b/apps/presentation_timer/ChangeLog
new file mode 100644
index 000000000..2ed460931
--- /dev/null
+++ b/apps/presentation_timer/ChangeLog
@@ -0,0 +1,2 @@
+0.01: first release
+0.02: added interface for configuration from app loader
diff --git a/apps/presentation_timer/README.md b/apps/presentation_timer/README.md
new file mode 100644
index 000000000..4539fc2f9
--- /dev/null
+++ b/apps/presentation_timer/README.md
@@ -0,0 +1,47 @@
+# Presentation Timer
+
+*Forked from Stopwatch Touch*
+
+Simple application to keep track of slides and
+time during a presentation. Useful for conferences,
+lectures or any presentation with a somewhat strict timing.
+
+The interface is pretty simple, it shows a stopwatch
+and the number of the current slide (based on the time),
+when the time for the last slide is approaching,
+the button becomes red, when it passed,
+the time will go on for another half a minute and stop automatically.
+
+You can set personalized timings from the web interface
+by uploading a CSV to the bangle (floppy disk button in the app loader).
+
+Each line in the file (`presentation_timer.csv`)
+contains the time in minutes at which the slide
+is supposed to finish and the slide number,
+separated by a semicolon.
+For instance the line `1.5;1` means that slide 1
+is lasting until 1 minutes 30 seconds (yes it's decimal),
+after another slide will start.
+The only requirement is that timings are increasing,
+so slides number don't have to be consecutive,
+some can be skipped and they can even be short texts.
+
+At the moment the app is just quick and dirty
+but it should do its job.
+
+## Screenshots
+
+
+
+
+
+
+## Example configuration file
+
+_presentation_timer.csv_
+```csv
+1.5;1
+2;2
+2.5;3
+3;4
+```
diff --git a/apps/presentation_timer/interface.html b/apps/presentation_timer/interface.html
new file mode 100644
index 000000000..137ea8475
--- /dev/null
+++ b/apps/presentation_timer/interface.html
@@ -0,0 +1,136 @@
+
+
+
+
+
+
+
+
+
+ Reload from watch
+ Upload to watch
+ Download
+
+
+
+
+
+
+
diff --git a/apps/presentation_timer/metadata.json b/apps/presentation_timer/metadata.json
new file mode 100644
index 000000000..8790d6208
--- /dev/null
+++ b/apps/presentation_timer/metadata.json
@@ -0,0 +1,17 @@
+{
+ "id": "presentation_timer",
+ "name": "Presentation Timer",
+ "version": "0.02",
+ "description": "A touch based presentation timer for Bangle JS 2",
+ "icon": "presentation_timer.png",
+ "screenshots": [{"url":"screenshot1.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"},{"url":"screenshot4.png"}],
+ "tags": "tools,app",
+ "supports": ["BANGLEJS2"],
+ "readme": "README.md",
+ "interface": "interface.html",
+ "storage": [
+ {"name":"presentation_timer.app.js","url":"presentation_timer.app.js"},
+ {"name":"presentation_timer.img","url":"presentation_timer.icon.js","evaluate":true}
+ ],
+ "data": [{ "name": "presentation_timer.csv" }]
+}
diff --git a/apps/presentation_timer/presentation_timer.app.js b/apps/presentation_timer/presentation_timer.app.js
new file mode 100644
index 000000000..1d0e5945d
--- /dev/null
+++ b/apps/presentation_timer/presentation_timer.app.js
@@ -0,0 +1,272 @@
+let w = g.getWidth();
+let h = g.getHeight();
+let tTotal = Date.now();
+let tStart = tTotal;
+let tCurrent = tTotal;
+let running = false;
+let timeY = 2*h/5;
+let displayInterval;
+let redrawButtons = true;
+const iconScale = g.getWidth() / 178; // scale up/down based on Bangle 2 size
+
+// 24 pixel images, scale to watch
+// 1 bit optimal, image string, no E.toArrayBuffer()
+const pause_img = atob("GBiBAf////////////////wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP////////////////w==");
+const play_img = atob("GBjBAP//AAAAAAAAAAAIAAAOAAAPgAAP4AAP+AAP/AAP/wAP/8AP//AP//gP//gP//AP/8AP/wAP/AAP+AAP4AAPgAAOAAAIAAAAAAAAAAA=");
+const reset_img = atob("GBiBAf////////////AAD+AAB+f/5+f/5+f/5+cA5+cA5+cA5+cA5+cA5+cA5+cA5+cA5+f/5+f/5+f/5+AAB/AAD////////////w==");
+
+const margin = 0.5; //half a minute tolerance
+
+//dummy default values
+var slides = [
+ [1.5, 1],
+ [2, 2],
+ [2.5, 3],
+ [3,4]
+];
+
+function log_debug(o) {
+ //console.log(o);
+}
+
+//first must be a number
+function readSlides() {
+ let csv = require("Storage").read("presentation_timer.csv");
+ if(!csv) return;
+ let lines = csv.split("\n").filter(e=>e);
+ log_debug("Loading "+lines.length+" slides");
+ slides = lines.map(line=>{let s=line.split(";");return [+s[0],s[1]];});
+}
+
+
+function timeToText(t) {
+ let hrs = Math.floor(t/3600000);
+ let mins = Math.floor(t/60000)%60;
+ let secs = Math.floor(t/1000)%60;
+ let tnth = Math.floor(t/100)%10;
+ let text;
+
+ if (hrs === 0)
+ text = ("0"+mins).substr(-2) + ":" + ("0"+secs).substr(-2) + "." + tnth;
+ else
+ text = ("0"+hrs) + ":" + ("0"+mins).substr(-2) + ":" + ("0"+secs).substr(-2);
+
+ //log_debug(text);
+ return text;
+}
+
+function drawButtons() {
+ log_debug("drawButtons()");
+ if (!running && tCurrent == tTotal) {
+ bigPlayPauseBtn.draw();
+ } else if (!running && tCurrent != tTotal) {
+ resetBtn.draw();
+ smallPlayPauseBtn.draw();
+ } else {
+ bigPlayPauseBtn.draw();
+ }
+
+ redrawButtons = false;
+}
+
+//not efficient but damn easy
+function findSlide(time) {
+ time /= 60000;
+ //change colour for the last 30 seconds
+ if(time > slides[slides.length-1][0] - margin && bigPlayPauseBtn.color!="#f00") {
+ bigPlayPauseBtn.color="#f00";
+ drawButtons();
+ }
+ for(let i=0; i time)
+ return slides[i][1];
+ }
+ //stop automatically
+ if(time > slides[slides.length-1][0] + margin) {
+ bigPlayPauseBtn.color="#0ff"; //restore
+ stopTimer();
+ }
+ return /*LANG*/"end!";
+}
+
+function drawTime() {
+ log_debug("drawTime()");
+ let Tt = tCurrent-tTotal;
+ let Ttxt = timeToText(Tt);
+
+ Ttxt += "\n"+findSlide(Tt);
+ // total time
+ g.setFont("Vector",38); // check
+ g.setFontAlign(0,0);
+ g.clearRect(0, timeY - 42, w, timeY + 42);
+ g.setColor(g.theme.fg);
+ g.drawString(Ttxt, w/2, timeY);
+}
+
+function draw() {
+ let last = tCurrent;
+ if (running) tCurrent = Date.now();
+ g.setColor(g.theme.fg);
+ if (redrawButtons) drawButtons();
+ drawTime();
+}
+
+function startTimer() {
+ log_debug("startTimer()");
+ draw();
+ displayInterval = setInterval(draw, 100);
+}
+
+function stopTimer() {
+ log_debug("stopTimer()");
+ if (displayInterval) {
+ clearInterval(displayInterval);
+ displayInterval = undefined;
+ }
+}
+
+// BTN stop start
+function stopStart() {
+ log_debug("stopStart()");
+
+ if (running)
+ stopTimer();
+
+ running = !running;
+ Bangle.buzz();
+
+ if (running)
+ tStart = Date.now() + tStart- tCurrent;
+ tTotal = Date.now() + tTotal - tCurrent;
+ tCurrent = Date.now();
+
+ setButtonImages();
+ redrawButtons = true;
+ if (running) {
+ startTimer();
+ } else {
+ draw();
+ }
+}
+
+function setButtonImages() {
+ if (running) {
+ bigPlayPauseBtn.setImage(pause_img);
+ smallPlayPauseBtn.setImage(pause_img);
+ resetBtn.setImage(reset_img);
+ } else {
+ bigPlayPauseBtn.setImage(play_img);
+ smallPlayPauseBtn.setImage(play_img);
+ resetBtn.setImage(reset_img);
+ }
+}
+
+// lap or reset
+function lapReset() {
+ log_debug("lapReset()");
+ if (!running && tStart != tCurrent) {
+ redrawButtons = true;
+ Bangle.buzz();
+ tStart = tCurrent = tTotal = Date.now();
+ g.clearRect(0,24,w,h);
+ draw();
+ }
+}
+
+// simple on screen button class
+function BUTTON(name,x,y,w,h,c,f,i) {
+ this.name = name;
+ this.x = x;
+ this.y = y;
+ this.w = w;
+ this.h = h;
+ this.color = c;
+ this.callback = f;
+ this.img = i;
+}
+
+BUTTON.prototype.setImage = function(i) {
+ this.img = i;
+}
+
+// if pressed the callback
+BUTTON.prototype.check = function(x,y) {
+ //console.log(this.name + ":check() x=" + x + " y=" + y +"\n");
+
+ if (x>= this.x && x<= (this.x + this.w) && y>= this.y && y<= (this.y + this.h)) {
+ log_debug(this.name + ":callback\n");
+ this.callback();
+ return true;
+ }
+ return false;
+};
+
+BUTTON.prototype.draw = function() {
+ g.setColor(this.color);
+ g.fillRect(this.x, this.y, this.x + this.w, this.y + this.h);
+ g.setColor("#000"); // the icons and boxes are drawn black
+ if (this.img != undefined) {
+ let iw = iconScale * 24; // the images were loaded as 24 pixels, we will scale
+ let ix = this.x + ((this.w - iw) /2);
+ let iy = this.y + ((this.h - iw) /2);
+ log_debug("g.drawImage(" + ix + "," + iy + "{scale: " + iconScale + "})");
+ g.drawImage(this.img, ix, iy, {scale: iconScale});
+ }
+ g.drawRect(this.x, this.y, this.x + this.w, this.y + this.h);
+};
+
+
+var bigPlayPauseBtn = new BUTTON("big",0, 3*h/4 ,w, h/4, "#0ff", stopStart, play_img);
+var smallPlayPauseBtn = new BUTTON("small",w/2, 3*h/4 ,w/2, h/4, "#0ff", stopStart, play_img);
+var resetBtn = new BUTTON("rst",0, 3*h/4, w/2, h/4, "#ff0", lapReset, pause_img);
+
+bigPlayPauseBtn.setImage(play_img);
+smallPlayPauseBtn.setImage(play_img);
+resetBtn.setImage(pause_img);
+
+
+Bangle.on('touch', function(button, xy) {
+ var x = xy.x;
+ var y = xy.y;
+
+ // adjust for outside the dimension of the screen
+ // http://forum.espruino.com/conversations/371867/#comment16406025
+ if (y > h) y = h;
+ if (y < 0) y = 0;
+ if (x > w) x = w;
+ if (x < 0) x = 0;
+
+ // not running, and reset
+ if (!running && tCurrent == tTotal && bigPlayPauseBtn.check(x, y)) return;
+
+ // paused and hit play
+ if (!running && tCurrent != tTotal && smallPlayPauseBtn.check(x, y)) return;
+
+ // paused and press reset
+ if (!running && tCurrent != tTotal && resetBtn.check(x, y)) return;
+
+ // must be running
+ if (running && bigPlayPauseBtn.check(x, y)) return;
+});
+
+// Stop updates when LCD is off, restart when on
+Bangle.on('lcdPower',on=>{
+ if (on) {
+ draw(); // draw immediately, queue redraw
+ } else { // stop draw timer
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = undefined;
+ }
+});
+
+// Clear the screen once, at startup
+g.setTheme({bg:"#000",fg:"#fff",dark:true}).clear();
+// above not working, hence using next 2 lines
+g.setColor("#000");
+g.fillRect(0,0,w,h);
+
+Bangle.loadWidgets();
+Bangle.drawWidgets();
+readSlides();
+draw();
+setWatch(() => load(), BTN, { repeat: false, edge: "falling" });
diff --git a/apps/presentation_timer/presentation_timer.icon.js b/apps/presentation_timer/presentation_timer.icon.js
new file mode 100644
index 000000000..f18768b2b
--- /dev/null
+++ b/apps/presentation_timer/presentation_timer.icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwxH+AC1WwIZXACmBF7FWAH4Ae/0WAFiQBF9+sAFgv/AAvXAAgvmFgoyWF6IuLGCIvPFpoxRF5wlIwIKJF8lWwIvjQpIvKGBgv8cpWBF5QwLF/4vrEJQvNGBQv/F5FWSCq/XGAVWB5DviEgRiJF8gxDF9q+SF5owWEJYv9GCggMF5wwSD5ovPGCAeOF6AwODp4vRGJYbRF6YAbF/4v/F8eBAYYECAYYvRACFWqwEGwNWwIeSF7IEFAD5VBGhpekAo6QiEYo1LR0QpGBgyOhAxCQfKIIhFGxpegA44+HF85gRA=="))
diff --git a/apps/presentation_timer/presentation_timer.png b/apps/presentation_timer/presentation_timer.png
new file mode 100644
index 000000000..7db9866d7
Binary files /dev/null and b/apps/presentation_timer/presentation_timer.png differ
diff --git a/apps/presentation_timer/screenshot1.png b/apps/presentation_timer/screenshot1.png
new file mode 100644
index 000000000..d26720d7e
Binary files /dev/null and b/apps/presentation_timer/screenshot1.png differ
diff --git a/apps/presentation_timer/screenshot2.png b/apps/presentation_timer/screenshot2.png
new file mode 100644
index 000000000..cbd6f0bd1
Binary files /dev/null and b/apps/presentation_timer/screenshot2.png differ
diff --git a/apps/presentation_timer/screenshot3.png b/apps/presentation_timer/screenshot3.png
new file mode 100644
index 000000000..40b375b37
Binary files /dev/null and b/apps/presentation_timer/screenshot3.png differ
diff --git a/apps/presentation_timer/screenshot4.png b/apps/presentation_timer/screenshot4.png
new file mode 100644
index 000000000..7c43cf91f
Binary files /dev/null and b/apps/presentation_timer/screenshot4.png differ
diff --git a/apps/presentor/metadata.json b/apps/presentor/metadata.json
index e5b5e289f..2d0a22102 100644
--- a/apps/presentor/metadata.json
+++ b/apps/presentor/metadata.json
@@ -12,7 +12,8 @@
"allow_emulator": true,
"storage": [
{"name":"presentor.app.js","url":"app.js"},
- {"name":"presentor.img","url":"app-icon.js","evaluate":true},
+ {"name":"presentor.img","url":"app-icon.js","evaluate":true}
+ ], "data": [
{"name":"presentor.json","url":"settings.json"}
]
}
diff --git a/apps/primetime/README.md b/apps/primetime/README.md
new file mode 100644
index 000000000..a07c19f52
--- /dev/null
+++ b/apps/primetime/README.md
@@ -0,0 +1,10 @@
+# App Name
+
+Watchface that displays time and the prime factors of the "military time" (i.e. 21:05 => 2105, shows prime factors of 2105 which are 5 & 421). Displays "Prime Time!" if prime.
+
+
+
+
+## Creator
+
+Adapted from simplestclock by [Eve Bury](https://www.github.com/eveeeon)
diff --git a/apps/primetime/app.png b/apps/primetime/app.png
new file mode 100644
index 000000000..5024727fb
Binary files /dev/null and b/apps/primetime/app.png differ
diff --git a/apps/primetime/metadata.json b/apps/primetime/metadata.json
new file mode 100644
index 000000000..d796d290c
--- /dev/null
+++ b/apps/primetime/metadata.json
@@ -0,0 +1,15 @@
+{ "id": "primetime",
+ "name": "Prime Time Clock",
+ "version": "0.01",
+ "type": "clock",
+ "description": "A clock that tells you the primes of the time",
+ "icon": "app.png",
+ "screenshots": [{"url":"screenshot.png"}],
+ "tags": "clock",
+ "supports": ["BANGLEJS2"],
+ "readme": "README.md",
+ "storage": [
+ {"name":"primetime.app.js","url":"primetime.js"},
+ {"name":"primetime.img","url":"primetime-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/primetime/primetime-icon.js b/apps/primetime/primetime-icon.js
new file mode 100644
index 000000000..57969a68b
--- /dev/null
+++ b/apps/primetime/primetime-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwgVVABVADJMBBf4L/Bf4LMgtQgIHCitAqoHBoEv+EHwALBv/S//4BYO//svwELoP//X/+gLB2E93+Ah9B9f+//QBYMVvv3C4XvvwLDl/0q+AgsB998qt4F4XgHYIXB/1+6ALC//93/4F4I7CI4QLBAIMLoF/6ABBBYNVqgBBgprCAIKz0qkAooLHgP8gXvvALH/EL7e4BY+tz/+vovH3PR1++L9YL/BYdVABQ="))
diff --git a/apps/primetime/primetime.js b/apps/primetime/primetime.js
new file mode 100644
index 000000000..bba63bc48
--- /dev/null
+++ b/apps/primetime/primetime.js
@@ -0,0 +1,89 @@
+const h = g.getHeight();
+const w = g.getWidth();
+
+
+
+// creates a list of prime factors of n and outputs them as a string, if n is prime outputs "Prime Time!"
+function primeFactors(n) {
+ const factors = [];
+ let divisor = 2;
+
+ while (n >= 2) {
+ if (n % divisor == 0) {
+ factors.push(divisor);
+ n = n / divisor;
+ } else {
+ divisor++;
+ }
+ }
+ if (factors.length === 1) {
+ return "Prime Time!";
+ }
+ else
+ return factors.toString();
+}
+
+
+// converts time HR:MIN to integer HRMIN e.g. 15:35 => 1535
+function timeToInt(t) {
+ var arr = t.split(':');
+ var intTime = parseInt(arr[0])*100+parseInt(arr[1]);
+
+ return intTime;
+}
+
+
+
+function draw() {
+ var date = new Date();
+ var timeStr = require("locale").time(date,1);
+ var primeStr = primeFactors(timeToInt(timeStr));
+
+ g.reset();
+ g.setColor(0,0,0);
+ g.fillRect(Bangle.appRect);
+
+ g.setFont("6x8", w/30);
+ g.setFontAlign(0, 0);
+ g.setColor(100,100,100);
+ g.drawString(timeStr, w/2, h/2);
+ g.setFont("6x8", w/60);
+ g.drawString(primeStr, w/2, 3*h/4);
+ queueDraw();
+}
+
+// timeout used to update every minute
+var drawTimeout;
+
+// schedule a draw for the next minute
+function queueDraw() {
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = setTimeout(function() {
+ drawTimeout = undefined;
+ draw();
+ }, 60000 - (Date.now() % 60000));
+}
+
+// Stop updates when LCD is off, restart when on
+Bangle.on('lcdPower',on=>{
+ if (on) {
+ draw(); // draw immediately, queue redraw
+ } else { // stop draw timer
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = undefined;
+ }
+});
+
+g.clear();
+
+// Show launcher when middle button pressed
+// Bangle.setUI("clock");
+// use clockupdown as it tests for issue #1249
+Bangle.setUI("clockupdown", btn=> {
+ draw();
+});
+
+// Load widgets
+Bangle.loadWidgets();
+Bangle.drawWidgets();
+draw();
diff --git a/apps/primetime/screenshot.png b/apps/primetime/screenshot.png
new file mode 100644
index 000000000..cb625a9b6
Binary files /dev/null and b/apps/primetime/screenshot.png differ
diff --git a/apps/primetimelato/ChangeLog b/apps/primetimelato/ChangeLog
new file mode 100644
index 000000000..7781e93a1
--- /dev/null
+++ b/apps/primetimelato/ChangeLog
@@ -0,0 +1,4 @@
+0.01: first release
+0.02: added option to buzz on prime, with settings
+0.03: added option to debug settings and test fw 2.15.93 load speed ups
+0.04: changed icon
diff --git a/apps/primetimelato/README.md b/apps/primetimelato/README.md
new file mode 100644
index 000000000..0ffd5a3fa
--- /dev/null
+++ b/apps/primetimelato/README.md
@@ -0,0 +1,19 @@
+# Prime Lato (clock)
+
+A watchface that displays time and its prime factors in the Lato font.
+For example when the time is 21:05, the prime factors are 5,421.
+Displays 'Prime Time!' when the time is a prime number.
+
+There is a settings option added in the Settings App. If 'Buzz on
+Prime' is ticked then the buzzer will sound when 'Prime Time!' is
+detected. Note the buzzer is limited to between 8am and 8pm so it
+should not go off when you want to sleep.
+
+
+
+
+Written by: [Hugh Barney](https://github.com/hughbarney)
+
+Adapted from primetime by [Eve Bury](https://www.github.com/eveeeon)
+
+For support and discussion please post in the [Bangle JS Forum](http://forum.espruino.com/microcosms/1424/)
diff --git a/apps/primetimelato/app.js b/apps/primetimelato/app.js
new file mode 100644
index 000000000..b4b9d5bb9
--- /dev/null
+++ b/apps/primetimelato/app.js
@@ -0,0 +1,156 @@
+const h = g.getHeight();
+const w = g.getWidth();
+const SETTINGS_FILE = "primetimelato.json";
+let settings;
+let setStr = 'U';
+
+Graphics.prototype.setFontLato = function(scale) {
+ // Actual height 41 (43 - 3)
+ this.setFontCustom(
+ E.toString(require('heatshrink').decompress(atob('ACEfAokB/AGEn+AAocDBgsfBgkB+A6Yg4FEgYgF/4FEv/gHIhAEh/+DwgYEgP/4AeJn4eF/hDEDwxrE/4eFKAgeFgJDERQ5DEXJI0Eh//IIZlB/4pDAoP/HgQYBAAIaBPARDDv4JBj5WBh5ZCv4CBPATPCeQcPwDYECAIMGPId4gEeSIYMHDIYMCExZAGh6ICn5QEK4RnGOgqBGaoKOECYSiDn78FAFgxFR4bcCKISOBgF+RwYCBTgQMCOIQMCj5eCBgIfDBgQfCBgSbDWoSHC/61CAwYMUAAYzCAAZNCBkYAfgYmBgJ7CTYsPTYQMBgJnBgYMCn4MBv64CBgMPXYSBBD4cfBgQIBgf3RwK7CvybCGQXPBgIfBGQIMBeoU/DoIMCF4IMDgP8BgQmBn8AEwb+BBgQIBIQJAC4BYBIgTNBDYIMBg///xRDn//OoIMBcYISBBgUHNATpCgjCngIEDFIM+AoV8TYKYCMIJQBAQLCCgZcBYQIJBX4UHCwSJBYQaSCMYQDCYQabEBgngW4V4WoQBBOgIMN+AoCEwY+BGYZfBYwQIBv+AG4X+j/8K4TCB+bECM4P+YgcD//B/67Ch/4h//K4SSBv5IBAAdAWqMYAokDD4t+AokfBgi0DAAX/Bgkf7wSE/IME/6PBCQfBBgcD/AME/ypCCQXABgYSBBgg+BBgYSBBgatBBggSBBgYSBBgcB/4ACZ4QGDZ4IMLFARAEAAJaEBjQAUhAnFMgIABuD5CVwWAboUDLwZ0DNYKLBM4IMBh50C55rBCoWACAIMC+EPFIN4EQQMBg4MSHgYzEBgIzBIAUAvhND+EH8DZCBAN/bIfwMQP/OgIMBLgalCBgKlDg//V4kfCQIAqWgYzC/gFDIAJgBZoS3CAwV//4TDh/+j5UCCQOAn4SD8DPCCQSbCCQR/BNAV/3i1CB4PzAYLCBgP8AYIMCv+HBgcP+AMDCQMHEwb2BYQLPDgYMBIAKoBOYLPCwDNBZ4UQBQPhJ4J0EDYJbCZ4R7EAoZiDSoaUDADBNBFQj5EKwQMEGAoMEOgQFCnAMEQIYFBgaOCBgTRBBgc/AYIMCaIQMCgb2CBgX/JQIMCDAQMCh/8JoYYBJoiNDBgIYDBgIYDBgPzUwkfUwisBOokfDAYMCQIq/ERwwAcn4pCgfwg42D//B/6hBCAP+KwYQBMQKbBgF//9+g5EBh4YB4CfC/EHDwK1Dn7PD8A0BgF4gEeAIUHBgQBBBi4mEGYpAEAIMP4BNELQpnGOgM/ZYaBFGQMPYos/JAIAuj4xEKgJrBfoX//hEE/4TDCQJSCCoN/gZfBjCBCj+AgaOCAIiKBg4OCgKKBvgbCWYMDToK1CgE8JIQMC4ZCBBgU4HYTNCz4JBEwV7KoQzCUIYvBLYZNBn60CLQPfCQcDM4LHCEALHDZ4TaCCYaODHYK8fh6FDEwKSCF4Uf4COCBgJsBn4MDDIJPDVgYAZA='))),
+ 46,
+ atob("CxMeHh4eHh4eHh4eDQ=="),
+ 52+(scale<<8)+(1<<16)
+ );
+ return this;
+};
+
+Graphics.prototype.setFontLatoSmall = function(scale) {
+ // Actual height 21 (20 - 0)
+ this.setFontCustom(
+ E.toString(require('heatshrink').decompress(atob('AB0B/+ch/88EAgQPHg/AgE+A4cPwEAvwTHoEAscQgc/wE//EP/0Au0wgEz/ED/+A//gg9jgEDiEAAIIACgcBwF+h0H+EwmPBwOf/4AB8Ng4cDg84hkfwFAvA/EgZfBneAwOEjkMkPAuccgPzwAbBCQJeBuZBBKYNxxkOhkgsFjgUD+B5BNox+Bgf4g/Y8F/wcDjkYjFw4HB40Gg8wjkfQgJLBj6bB84fBjCQDU5AAFj/wg//+A1B9xEGhkAg18gPx//+bgJmBAAckAQOgDQa+BgI2BjQ0HsBoBJogSBZgX/DQIrBG4IUFAAs7CQKVBFJ0GB4kEAQNwYQYACEIKaBRYX4RoQGCCoIADMwMf/kB8HwdQPA4EGGAMwmEBwPAjkHwfACgMAv4iBAAxICuEMSQNguEDgf//Ef/5ZEmCDDAAkBgHgnEOgfA8Eeg0B2EwjOBweMh04sE/wZlBfYomCjkPEIM8VwNgsI+BhkYjHg4HnFoPv8EOQYIAFQwMHJAN8gEOW4PjAgMYQwUPcIN/cI4ACkCNBgP8hkPsFgsY+BHoMYD4PHjkGj/gkAxBAAoHBh/wgPxV4J8B7EwnnBwPGhkMnHggLUBg/gXI6oDCgLiBsE+gb0BjD7B74bDni1CAAk8gP3+EP/ZTBfYMYfYI+Bw0Mh148E+HwPj+A6DAAIpBj6HB/0EhkwPwPPgcE+EYt4ZB8EDDwMP4EAiAeDgUCgE4nEB4INBAAcBKQMcjuA8BhBAAh2BgY5BjwUBJAMMDwPGJoMYf4ImFAAoUCswhBEgMZEgPMDgL7BmYpBzAUDABnBwEDhhTBFIiIBh4PBuDKCCwR6BgITBDAKSB88Dh84jAKB/geBHAQcBj/wgPhcIMDgOBhkYn0w4exw0wwkhxkhxHBhl/hFj7BKBo0Mg0wnnjgF+LIkEbQc/gEH8EBHgM/sEH4bGBjEB/HAgf2JIKvBCgICBJ4MOaYv//kP//gsHDga/BjEw4HBw0GjkwnHhwH9/kH54hBnggDkB1D//AjydBPIIyBFIMDgcAFIILB4EGg0AFIILBCgLLHj4kB//+CgUwTwOAhi8DFIccZANghkD4Pgh/+D4L7IFIpuBmHBwOGFIMwsFhwcDaAJTCwECSRasBJgIjBgDXBgxMBEYMBwUAhAdEn+Ag//gF8vkDwHgPoJoIXgnhw0Dh/gjF/Mo8D//4NASDBIgICBPQJLBIgJZBwEAF4IwBgB9BIYPwE44pHBYoiHg4EBvAUBwAZBExMAv//FIQbCKYU8AgJABnPggeHwE4jyKBnC8CgC8LEQZ2PNBA4BgIgCDYMHCwMfJIYNCnwMBB4N4gEPBYY+CNAJyHU4U/QoIxCFwQNBjwCBnACBHgpoGAAz4BB4PAj5OB8E4hwDBsEDPwMYSQXAgx+BmE4TwIUBPAOAh65Bj5wBAAxTBwI+BhgiBsHAgYiBjChB4OAg8cDwJVBvgfHB4KCBvl8HQmBwEMXoNgKY/ghwUBPAP+SoPhwF8ghOH//+U4UwsEBwbnBKYXwgcPRQPngF+jzMBuAbBOYnAn8GgP4nEc8HA4aSBjkwmHhwPDzkGNwMgNwYwBDoMAQgKoBcYIqBfYhoBChQAFv/4YIPwewIgBboIoFSIJnBgEHAgPwj6nBDIQVEAwMH8BBBAQQ6BdIUeCAc/BgR4BHwI6BCYJTCEIQCBj4CBh4CCE4QVBDQP+dYYLBnwcBBIMBAQoUBg/Agf8KwIZBHYIoBNIQSBfYUcIQP4nkB8YZBDwMPLoK5BJIXz8EPg+A8EeU4KmCXgYFBngCBDwL2BLAJGBMwIHCB4JKBgF4BwIWBiBGCgQpBuEwg+BwF8hkPsFg+cDj0YjPg4HcgwhBmBXBCgJnDAALmB/+f//8JYMkIoIMCHYJqCAQUfBYICCUYU4EgkIJINAgETFIgPEgAhBHoJtEgb3DQoacBSAQAGkBLDGYMAGYMCQ4gYBgiLCAQMYAQIzBDQQAFsbZBMYMxzEBxlAhlmgFikCIBwEPLowABn//4P/aIMwCgJUBjBpB4FgWYISBMIP/OYYAEg4MBv/Ag4UBmEYEAPAhjlBsEwgLyCAAcBIYMf+EB4IUFHwcIj//8C5BLA4cB4EBBgMdjkA5BTBocAmQ+ByBGBx0AjlgDISoCfwJ4BQ4P4jKwB5kAgwTDsEP3+A//mg03mEwzOBwnMU4Ngv8zgfh+EYPAKEEFIIuB4BnBEoMAPgTcEHgMAh/4NQ8gBwOf/kcPoKWEyEAhvP//uv7UBQgiSETgJ4BAgLJBnPAgeHcweAGALDGfYMP/4XBAAxmBSoP4FYQgBNAsPcIN/8CBCmBADCgV/dwP/BAIpIYgQpHLoIpCdwT7Mj08EIL7BDoMwfYOA4EOhwxBT4KUFHwXwZ4ITB4EMaQNgmEDDoMYH4LeBgZtBgZdFHwscCgI+HwOAUoPgv5eIPp8QCgcD4YUBFIOYKYPGgFjKYMfwEIvAUCgi8Dj5qBcwJoGGQQADn7JCH4J9BbRHAKYoAEuBLBVIMHAQMPEIMPBoIUBJYIMCvhoDAAQZFDgkeBQUBDwIGBEYUBXgIKCgYUBNAM/wA+CSQi8CnE4gH3B4PwFIINBIIMPSQPh8AUCAAMBLQR3Bn4KBn4SBv4fDDgJlBgI2BTw0AU4XBFIMfgEx7EB3nAh+GgF4XgOBF4JRCGAP/8f+/+YbAINBkArGbwIQB/5BBAArwBgLSBhP/v/H/6QBBAIACjhrDKwVgdAwHBRQThBgIbD'))),
+ 32,
+ atob("BAcIDAwRDwUGBggMBAcECAwMDAwMDAwMDAwFBQwMDAgRDg4OEAwMDxAGCQ4LExARDREOCwwPDhUODQ0GCAYMCAYLDAoMCwcLDAUFCwURDAwMDAgJCAwLEAsLCgYGBgwA"),
+ 21+(scale<<8)+(1<<16)
+ );
+ return this;
+};
+
+function loadSettings() {
+ settings = require("Storage").readJSON(SETTINGS_FILE,1)||{};
+ settings.buzz_on_prime = (settings.buzz_on_prime === undefined ? false : settings.buzz_on_prime);
+ settings.debug = (settings.debug === undefined ? false : settings.debug);
+
+ switch(settings.buzz_on_prime) {
+ case true:
+ setStr = 'T';
+ break;
+
+ case false:
+ setStr = 'F';
+ break;
+
+ case undefined:
+ default:
+ setStr = 'U';
+ break;
+ }
+}
+
+// creates a list of prime factors of n and outputs them as a string, if n is prime outputs "Prime Time!"
+function primeFactors(n) {
+ const factors = [];
+ let divisor = 2;
+
+ while (n >= 2) {
+ if (n % divisor == 0) {
+ factors.push(divisor);
+ n = n / divisor;
+ } else {
+ divisor++;
+ }
+ }
+ if (factors.length === 1) {
+ return "Prime Time!";
+ }
+ else
+ return factors.toString();
+}
+
+
+// converts time HR:MIN to integer HRMIN e.g. 15:35 => 1535
+function timeToInt(t) {
+ var arr = t.split(':');
+ var intTime = parseInt(arr[0])*100+parseInt(arr[1]);
+
+ return intTime;
+}
+
+function draw() {
+ var date = new Date();
+ var timeStr = require("locale").time(date,1);
+ var intTime = timeToInt(timeStr);
+ var primeStr = primeFactors(intTime);
+
+ g.reset();
+ g.setColor(0,0,0);
+ g.fillRect(Bangle.appRect);
+
+ g.setColor(100,100,100);
+
+ if (settings.debug) {
+ g.setFontLatoSmall();
+ g.setFontAlign(0, 0);
+ g.drawString(setStr, w/2, h/4);
+ }
+
+ g.setFontLato();
+ g.setFontAlign(0, 0);
+ g.drawString(timeStr, w/2, h/2);
+
+ g.setFontLatoSmall();
+ g.drawString(primeStr, w/2, 3*h/4);
+
+ // Buzz if Prime Time and between 8am and 8pm
+ if (settings.buzz_on_prime && primeStr == "Prime Time!" && intTime >= 800 && intTime <= 2000)
+ buzzer(2);
+ queueDraw();
+}
+
+// timeout for multi-buzzer
+var buzzTimeout;
+
+// n buzzes
+function buzzer(n) {
+ if (n-- < 1) return;
+ Bangle.buzz(250);
+
+ if (buzzTimeout) clearTimeout(buzzTimeout);
+ buzzTimeout = setTimeout(function() {
+ buzzTimeout = undefined;
+ buzzer(n);
+ }, 500);
+}
+
+// timeout used to update every minute
+var drawTimeout;
+
+// schedule a draw for the next minute
+function queueDraw() {
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = setTimeout(function() {
+ drawTimeout = undefined;
+ draw();
+ }, 60000 - (Date.now() % 60000));
+}
+
+// Stop updates when LCD is off, restart when on
+Bangle.on('lcdPower',on=>{
+ if (on) {
+ draw(); // draw immediately, queue redraw
+ } else { // stop draw timer
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = undefined;
+ }
+});
+
+
+loadSettings();
+g.clear();
+// Show launcher when middle button pressed
+Bangle.setUI("clock");
+
+// Load widgets
+Bangle.loadWidgets();
+Bangle.drawWidgets();
+draw();
diff --git a/apps/primetimelato/app.png b/apps/primetimelato/app.png
new file mode 100644
index 000000000..e5762b97c
Binary files /dev/null and b/apps/primetimelato/app.png differ
diff --git a/apps/primetimelato/icon.js b/apps/primetimelato/icon.js
new file mode 100644
index 000000000..7c8d5380b
--- /dev/null
+++ b/apps/primetimelato/icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEw4X/AAIHB8cYttrJf4AR1gKJgdYBZMCBZdcBZMNsALKuALJhNABZMFwALJmvAAwkOqvAmtAkwSF83+uEV4EMOIpZBznWII5NB7mXGo5BB7Z0HkpBB6x0HFYXVNA4rC6pcFAANXDQhSFqgaEBZGYaBLfIaAUBBZUJNQ4jCm+cHZPcBZFXgdwzELBg1W/PAy/rBY3VPAOVTY863kAnaPHAH4A/ADAA=="))
diff --git a/apps/primetimelato/metadata.json b/apps/primetimelato/metadata.json
new file mode 100644
index 000000000..400220b10
--- /dev/null
+++ b/apps/primetimelato/metadata.json
@@ -0,0 +1,18 @@
+{ "id": "primetimelato",
+ "name": "Prime Lato",
+ "version": "0.04",
+ "type": "clock",
+ "description": "A clock that tells you the primes of the time in the Lato font",
+ "icon": "app.png",
+ "screenshots": [{"url":"screenshot.png"}],
+ "tags": "clock",
+ "supports": ["BANGLEJS2"],
+ "readme": "README.md",
+ "storage": [
+ {"name":"primetimelato.app.js","url":"app.js"},
+ {"name":"primetimelato.img","url":"icon.js","evaluate":true},
+ {"name":"primetimelato.settings.js","url":"settings.js"}
+
+ ],
+ "data": [{"name":"primetimelato.json"}]
+}
diff --git a/apps/primetimelato/screenshot.png b/apps/primetimelato/screenshot.png
new file mode 100644
index 000000000..7f6e7cc0d
Binary files /dev/null and b/apps/primetimelato/screenshot.png differ
diff --git a/apps/primetimelato/settings.js b/apps/primetimelato/settings.js
new file mode 100644
index 000000000..069c976c8
--- /dev/null
+++ b/apps/primetimelato/settings.js
@@ -0,0 +1,45 @@
+(function(back) {
+ const SETTINGS_FILE = "primetimelato.json";
+
+ // initialize with default settings...
+ let s = {
+ 'buzz_on_prime': true,
+ 'debug': false
+ }
+
+ // ...and overwrite them with any saved values
+ // This way saved values are preserved if a new version adds more settings
+ const storage = require('Storage')
+ let settings = storage.readJSON(SETTINGS_FILE, 1) || {}
+ const saved = settings || {}
+ for (const key in saved) {
+ s[key] = saved[key]
+ }
+
+ function save() {
+ settings = s;
+ storage.write(SETTINGS_FILE, settings);
+ }
+
+ E.showMenu({
+ '': { 'title': 'Prime Time Lato' },
+ '< Back': back,
+ 'Buzz on Prime': {
+ value: !!s.buzz_on_prime,
+ onchange: v => {
+ s.buzz_on_prime = v;
+ save();
+ },
+ },
+
+ 'Debug': {
+ value: !!s.debug,
+ onchange: v => {
+ s.debug = v;
+ save();
+ },
+ }
+
+
+ })
+})
diff --git a/apps/ptlaunch/ChangeLog b/apps/ptlaunch/ChangeLog
index eec3610ed..5871b1fdc 100644
--- a/apps/ptlaunch/ChangeLog
+++ b/apps/ptlaunch/ChangeLog
@@ -6,3 +6,4 @@
0.12: Improve pattern detection code readability by PaddeK http://forum.espruino.com/profiles/117930/
0.13: Improve pattern rendering by HughB http://forum.espruino.com/profiles/167235/
0.14: Update setUI to work with new Bangle.js 2v13 menu style
+0.15: Update to support clocks in custom setUI mode
diff --git a/apps/ptlaunch/boot.js b/apps/ptlaunch/boot.js
index 748d564f3..885962761 100644
--- a/apps/ptlaunch/boot.js
+++ b/apps/ptlaunch/boot.js
@@ -76,13 +76,8 @@
var sui = Bangle.setUI;
Bangle.setUI = function (mode, cb) {
sui(mode, cb);
- if ("object"==typeof mode) mode = mode.mode;
- if (!mode) {
- Bangle.removeListener("drag", dragHandler);
- storedPatterns = {};
- return;
- }
- if (!mode.startsWith("clock")) {
+ if (typeof mode === "object") mode = (mode.clock ? "clock" : "") + mode.mode;
+ if (!mode || !mode.startsWith("clock")) {
storedPatterns = {};
Bangle.removeListener("drag", dragHandler);
return;
diff --git a/apps/ptlaunch/metadata.json b/apps/ptlaunch/metadata.json
index 0b6dce3d1..6f8a9e16f 100644
--- a/apps/ptlaunch/metadata.json
+++ b/apps/ptlaunch/metadata.json
@@ -2,7 +2,7 @@
"id": "ptlaunch",
"name": "Pattern Launcher",
"shortName": "Pattern Launcher",
- "version": "0.14",
+ "version": "0.15",
"description": "Directly launch apps from the clock screen with custom patterns.",
"icon": "app.png",
"screenshots": [{"url":"manage_patterns_light.png"}],
diff --git a/apps/qcenter/ChangeLog b/apps/qcenter/ChangeLog
new file mode 100644
index 000000000..900b9017c
--- /dev/null
+++ b/apps/qcenter/ChangeLog
@@ -0,0 +1,2 @@
+0.01: New App!
+0.02: Fix fast loading on swipe to clock
diff --git a/apps/qcenter/README.md b/apps/qcenter/README.md
new file mode 100644
index 000000000..4931b9c7f
--- /dev/null
+++ b/apps/qcenter/README.md
@@ -0,0 +1,20 @@
+# Quick Center
+
+An app with a status bar showing various information and up to six shortcuts for your favorite apps!
+Designed for use with any kind of quick launcher, such as Quick Launch or Pattern Launcher.
+
+
+
+## Usage
+
+Pin your apps with settings, then launch them with your favorite quick launcher to access them quickly.
+If you don't have any apps pinned, the settings and about apps will be shown as an example.
+
+## Features
+
+Battery and GPS status display (for now)
+Up to six shortcuts to your favorite apps
+
+## Upcoming features
+- Quick switches for toggleable features such as Bluetooth or HID mode
+- Customizable status information
\ No newline at end of file
diff --git a/apps/qcenter/app-icon.js b/apps/qcenter/app-icon.js
new file mode 100644
index 000000000..bfc94d10a
--- /dev/null
+++ b/apps/qcenter/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEw4UB6cA/4ACBYNVAElQHAsFBYZFHCxIYEoALHgILNOxILChWqAAmgBYNUBZMVBYIAIBc0C1WAlWoAgQL/O96D/Qf4LZqoLJqoLMoAKHgILNqALHgoLBGBAKCDA4WDAEQA="))
\ No newline at end of file
diff --git a/apps/qcenter/app.js b/apps/qcenter/app.js
new file mode 100644
index 000000000..be28db3b6
--- /dev/null
+++ b/apps/qcenter/app.js
@@ -0,0 +1,129 @@
+{
+require("Font8x12").add(Graphics);
+
+// load pinned apps from config
+let settings = require("Storage").readJSON("qcenter.json", 1) || {};
+let pinnedApps = settings.pinnedApps || [];
+let exitGesture = settings.exitGesture || "swipeup";
+
+// if empty load a default set of apps as an example
+if (pinnedApps.length == 0) {
+ pinnedApps = [
+ { src: "setting.app.js", icon: "setting.img" },
+ { src: "about.app.js", icon: "about.img" },
+ ];
+}
+
+// button drawing from Layout.js, edited to have completely custom button size with icon
+let drawButton = function(l) {
+ let x = l.x + (0 | l.pad),
+ y = l.y + (0 | l.pad),
+ w = l.w - (l.pad << 1),
+ h = l.h - (l.pad << 1);
+ let poly = [
+ x,
+ y + 4,
+ x + 4,
+ y,
+ x + w - 5,
+ y,
+ x + w - 1,
+ y + 4,
+ x + w - 1,
+ y + h - 5,
+ x + w - 5,
+ y + h - 1,
+ x + 4,
+ y + h - 1,
+ x,
+ y + h - 5,
+ x,
+ y + 4,
+ ],
+ bg = l.selected ? g.theme.bgH : g.theme.bg2;
+ g.setColor(bg)
+ .fillPoly(poly)
+ .setColor(l.selected ? g.theme.fgH : g.theme.fg2)
+ .drawPoly(poly);
+ if (l.src)
+ g.setBgColor(bg).drawImage(
+ "function" == typeof l.src ? l.src() : l.src,
+ l.x + l.w / 2,
+ l.y + l.h / 2,
+ { scale: l.scale || undefined, rotate: Math.PI * 0.5 * (l.r || 0) }
+ );
+}
+
+// function to split array into group of 3, for button placement
+let groupBy3 = function(data) {
+ let result = [];
+ for (let i = 0; i < data.length; i += 3) result.push(data.slice(i, i + 3));
+ return result;
+}
+
+// generate object with buttons for apps by group of 3
+let appButtons = groupBy3(pinnedApps).map((appGroup, i) => {
+ return appGroup.map((app, j) => {
+ return {
+ type: "custom",
+ render: drawButton,
+ width: 50,
+ height: 50,
+ pad: 5,
+ src: require("Storage").read(app.icon),
+ scale: 0.75,
+ cb: (l) => load(app.src),
+ };
+ });
+});
+
+// create basic layout content with status info and sensor status on top
+let layoutContent = [
+ {
+ type: "h",
+ pad: 5,
+ fillx: 1,
+ c: [
+ { type: "txt", font: "8x12", pad: 3, scale: 2, label: E.getBattery() + "%" },
+ { type: "txt", font: "8x12", pad: 3, scale: 2, label: "GPS: " + (Bangle.isGPSOn() ? "ON" : "OFF") },
+ ],
+ },
+];
+
+// create rows for buttons and add them to layoutContent
+appButtons.forEach((appGroup) => {
+ layoutContent.push({
+ type: "h",
+ pad: 2,
+ c: appGroup,
+ });
+});
+
+// create layout with content
+
+Bangle.loadWidgets();
+
+let Layout = require("Layout");
+let layout = new Layout({
+ type: "v",
+ c: layoutContent
+}, {
+ remove: ()=>{
+ Bangle.removeListener("swipe", onSwipe);
+ delete Graphics.prototype.setFont8x12;
+ }
+});
+g.clear();
+layout.render();
+Bangle.drawWidgets();
+
+// swipe event listener for exit gesture
+let onSwipe = function (lr, ud) {
+ if(exitGesture == "swipeup" && ud == -1) Bangle.showClock();
+ if(exitGesture == "swipedown" && ud == 1) Bangle.showClock();
+ if(exitGesture == "swipeleft" && lr == -1) Bangle.showClock();
+ if(exitGesture == "swiperight" && lr == 1) Bangle.showClock();
+}
+
+Bangle.on("swipe", onSwipe);
+}
diff --git a/apps/qcenter/app.png b/apps/qcenter/app.png
new file mode 100644
index 000000000..27ec75f1c
Binary files /dev/null and b/apps/qcenter/app.png differ
diff --git a/apps/qcenter/metadata.json b/apps/qcenter/metadata.json
new file mode 100644
index 000000000..a325de10f
--- /dev/null
+++ b/apps/qcenter/metadata.json
@@ -0,0 +1,18 @@
+{
+ "id": "qcenter",
+ "name": "Quick Center",
+ "shortName": "QCenter",
+ "version": "0.02",
+ "description": "An app for quickly launching your favourite apps, inspired by the control centres of other watches.",
+ "icon": "app.png",
+ "tags": "",
+ "supports": ["BANGLEJS2"],
+ "readme": "README.md",
+ "screenshots": [{ "url": "screenshot.png" }],
+ "storage": [
+ { "name": "qcenter.app.js", "url": "app.js" },
+ { "name": "qcenter.settings.js", "url": "settings.js" },
+ { "name": "qcenter.img", "url": "app-icon.js", "evaluate": true }
+ ],
+ "data": [{"name":"qcenter.json"}]
+}
diff --git a/apps/qcenter/screenshot.png b/apps/qcenter/screenshot.png
new file mode 100644
index 000000000..8c0a335aa
Binary files /dev/null and b/apps/qcenter/screenshot.png differ
diff --git a/apps/qcenter/settings.js b/apps/qcenter/settings.js
new file mode 100644
index 000000000..2c97f8a5f
--- /dev/null
+++ b/apps/qcenter/settings.js
@@ -0,0 +1,133 @@
+// make sure to enclose the function in parentheses
+(function (back) {
+ let settings = require("Storage").readJSON("qcenter.json", 1) || {};
+ var apps = require("Storage")
+ .list(/\.info$/)
+ .map((app) => {
+ var a = require("Storage").readJSON(app, 1);
+ return (
+ a && {
+ name: a.name,
+ type: a.type,
+ sortorder: a.sortorder,
+ src: a.src,
+ icon: a.icon,
+ }
+ );
+ })
+ .filter(
+ (app) =>
+ app &&
+ (app.type == "app" ||
+ app.type == "launch" ||
+ app.type == "clock" ||
+ !app.type)
+ );
+ apps.sort((a, b) => {
+ var n = (0 | a.sortorder) - (0 | b.sortorder);
+ if (n) return n; // do sortorder first
+ if (a.name < b.name) return -1;
+ if (a.name > b.name) return 1;
+ return 0;
+ });
+
+ function save(key, value) {
+ settings[key] = value;
+ require("Storage").write("qcenter.json", settings);
+ }
+
+ var pinnedApps = settings.pinnedApps || [];
+ var exitGesture = settings.exitGesture || "swipeup";
+
+ function showMainMenu() {
+ var mainmenu = {
+ "": { title: "Quick Center", back: back},
+ };
+
+ // Set exit gesture
+ mainmenu["Exit Gesture: " + exitGesture] = function () {
+ E.showMenu(exitGestureMenu);
+ };
+
+ //List all pinned apps, redirecting to menu with options to unpin and reorder
+ pinnedApps.forEach((app, i) => {
+ mainmenu[app.name] = function () {
+ E.showMenu({
+ "": { title: app.name, back: showMainMenu },
+ "Unpin": () => {
+ pinnedApps.splice(i, 1);
+ save("pinnedApps", pinnedApps);
+ showMainMenu();
+ },
+ "Move Up": () => {
+ if (i > 0) {
+ pinnedApps.splice(i - 1, 0, pinnedApps.splice(i, 1)[0]);
+ save("pinnedApps", pinnedApps);
+ showMainMenu();
+ }
+ },
+ "Move Down": () => {
+ if (i < pinnedApps.length - 1) {
+ pinnedApps.splice(i + 1, 0, pinnedApps.splice(i, 1)[0]);
+ save("pinnedApps", pinnedApps);
+ showMainMenu();
+ }
+ },
+ });
+ };
+ });
+
+ // Show pin app menu, or show alert if max amount of apps are pinned
+ mainmenu["Pin App"] = function () {
+ if (pinnedApps.length < 6) {
+ E.showMenu(pinAppMenu);
+ } else {
+ E.showAlert("Max apps pinned").then(showMainMenu);
+ }
+ };
+
+ return E.showMenu(mainmenu);
+ }
+
+ // menu for adding apps to the quick launch menu, listing all apps
+ var pinAppMenu = {
+ "": { title: "Add App", back: showMainMenu }
+ };
+ apps.forEach((a) => {
+ pinAppMenu[a.name] = function () {
+ // strip unncecessary properties
+ delete a.type;
+ delete a.sortorder;
+ pinnedApps.push(a);
+ save("pinnedApps", pinnedApps);
+ showMainMenu();
+ };
+ });
+
+ // menu for setting exit gesture
+ var exitGestureMenu = {
+ "": { title: "Exit Gesture", back: showMainMenu }
+ };
+ exitGestureMenu["Swipe Up"] = function () {
+ exitGesture = "swipeup";
+ save("exitGesture", "swipeup");
+ showMainMenu();
+ };
+ exitGestureMenu["Swipe Down"] = function () {
+ exitGesture = "swipedown";
+ save("exitGesture", "swipedown");
+ showMainMenu();
+ };
+ exitGestureMenu["Swipe Left"] = function () {
+ exitGesture = "swipeleft";
+ save("exitGesture", "swipeleft");
+ showMainMenu();
+ };
+ exitGestureMenu["Swipe Right"] = function () {
+ exitGesture = "swiperight";
+ save("exitGesture", "swiperight");
+ showMainMenu();
+ };
+
+ showMainMenu();
+});
diff --git a/apps/qrcode/custom.html b/apps/qrcode/custom.html
index 9955ea6c9..a3362f101 100644
--- a/apps/qrcode/custom.html
+++ b/apps/qrcode/custom.html
@@ -106,8 +106,8 @@
-
-
+
+
+
+
+