diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 000000000..345bce54f
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,12 @@
+# These are supported funding model platforms
+
+github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
+patreon: espruino
+open_collective: # Replace with a single Open Collective username
+ko_fi: # Replace with a single Ko-fi username
+tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
+community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
+liberapay: # Replace with a single Liberapay username
+issuehunt: # Replace with a single IssueHunt username
+otechie: # Replace with a single Otechie username
+custom: ['http://www.espruino.com/Donate']# Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml
index 1eb009153..a3469e7bb 100644
--- a/.github/workflows/nodejs.yml
+++ b/.github/workflows/nodejs.yml
@@ -1,4 +1,4 @@
-name: Node CI
+name: build
on: [push, pull_request]
@@ -6,29 +6,25 @@ jobs:
build:
runs-on: ubuntu-latest
- strategy:
- matrix:
- node-version: [16.x]
-
steps:
- name: Checkout repository and submodules
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
with:
submodules: recursive
- - name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v1
+ - name: Use Node.js 16.x
+ uses: actions/setup-node@v3
with:
- node-version: ${{ matrix.node-version }}
- - name: install testing dependencies
- run: npm i
- - name: test all apps and widgets
- run: npm run test
- - name: install typescript dependencies
+ node-version: 16.x
+ - name: Install testing dependencies
+ run: npm ci
+ - name: Test all apps and widgets
+ run: npm test
+ - name: Install typescript dependencies
working-directory: ./typescript
run: npm ci
- - name: build types
+ - name: Build types
working-directory: ./typescript
run: npm run build:types
- - name: build all TS apps and widgets
+ - name: Build all TS apps and widgets
working-directory: ./typescript
- run: npm run build
\ No newline at end of file
+ run: npm run build
diff --git a/.gitignore b/.gitignore
index 231851dd6..a9398e871 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,5 @@
.htaccess
node_modules
-package-lock.json
.DS_Store
*.js.bak
appdates.csv
diff --git a/README.md b/README.md
index b3da9f685..ea485da86 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
Bangle.js App Loader (and Apps)
================================
-[](https://app.travis-ci.com/github/espruino/BangleApps)
+[](https://github.com/espruino/BangleApps/actions/workflows/nodejs.yml)
* Try the **release version** at [banglejs.com/apps](https://banglejs.com/apps)
* Try the **development version** at [espruino.github.io](https://espruino.github.io/BangleApps/)
@@ -191,7 +191,7 @@ widget bar at the top of the screen they can add themselves to the global
```
WIDGETS["mywidget"]={
- area:"tl", // tl (top left), tr (top right)
+ area:"tl", // tl (top left), tr (top right), bl (bottom left), br (bottom right)
sortorder:0, // (Optional) determines order of widgets in the same corner
width: 24, // how wide is the widget? You can change this and call Bangle.drawWidgets() to re-layout
draw:draw // called to draw the widget
diff --git a/apps/2ofthemclk/ChangeLog b/apps/2ofthemclk/ChangeLog
new file mode 100644
index 000000000..2286a7f70
--- /dev/null
+++ b/apps/2ofthemclk/ChangeLog
@@ -0,0 +1 @@
+0.01: New App!
\ No newline at end of file
diff --git a/apps/2ofthemclk/README.md b/apps/2ofthemclk/README.md
new file mode 100644
index 000000000..7ac2cf779
--- /dev/null
+++ b/apps/2ofthemclk/README.md
@@ -0,0 +1,11 @@
+# two of them clock
+
+You can now wear teh memez on your wrist.
+
+
+
+Also serves as an example of displaying seconds only when unlocked or charging and only refreshing on the minute otherwise.
+Widgets not supported
+
+## Creator
+- [Kilrah](https://github.com/kilrah)
diff --git a/apps/2ofthemclk/app-icon.js b/apps/2ofthemclk/app-icon.js
new file mode 100644
index 000000000..9bfb8a550
--- /dev/null
+++ b/apps/2ofthemclk/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwgZC/AH4ADkAPOgVJkgEBAQQAJiQRByEJgmQCJWSpMEAQMkyQJCpASHhAOBpAmBJJgjBCIUJCRg4CCIJxFMQ2SoARCkmACI0EBAJHCCIMLj4RFiUBskAgIXBEAU5A4P34CtCiEJsEJ/AHBCgOBAoQAEi0H////HciQsBwywICIXWzkG4A+BEY0gif46dt6/cgnIgkWnHfLIP/MoUWwHbpvC/kAjEEj0HNYQCCkEfGgP/64RB2EAifHLwMAjg1CCIMD/0H/0B8EAh+HgeAkARCE4IjC/4jBYIMPLIcIAYUPB4OBCIQABhu/AoShCHYIRBx6QBDgUw2//8OHPwcJ39//ILBCIU9LgMBSQgsBJAYRBkE/CIIABgRHD3wRFkk/2zBDAYU//3b/oRB8ARBj6ABgEE7YREEYf+oMkSwINCyClCn//z//+4RBgMkgU3EgUcwFJgEeboOXCIP2EYJCDAAVJkkGWoIuBgf2EYQPDkECCIOGd4ffyEJkgFBAAcSoEkwQCBhw+BwQaByVAkGAKwIFBBANLkEQgAyBCIVIkBpBgmSBYOQoApBgcgiQRCAQIyCCgsSjIFBCIcgRgJNCCgQyBpAgDAQT2BCgIOBBAQUCCIpfBCIwCKP4QRNpCSDCLyJBCIbjBTwYRLboJ0BCI4QD"))
diff --git a/apps/2ofthemclk/app.js b/apps/2ofthemclk/app.js
new file mode 100644
index 000000000..78415fba7
--- /dev/null
+++ b/apps/2ofthemclk/app.js
@@ -0,0 +1,90 @@
+const img = require("heatshrink").decompress(atob("2GwgZC/ADEIAQMBgEQAgMChEEAoQA4wACEAHKDBAQYA9X/YACgQFGkBH1HAQFCwEIgkAAQVAH2GAAwtBQwcQgMAwQ/vGQI4CAQRHCwFAkACCQYTRFAEqzBAopHCgMEAQiSHAEsQAYT1ChBEEPoMQBAMQAoQRCX9BEDOgR6BHwkgwVBQw4aEIlKGCPoWCI4REBBAMEQYUCH856CNwJ9CO4YCBHwOSgCGBRIREBJQICBAAOAH0UQGQJ0CHYTCDAQOQpKABZAICCZAREBQccgIgJHBoJrBXIQFBIISDFwALBKwTEkd4RrBPobIBHAMkIgICBQYoODSoIfBRIYmEACjjBMoIgBwAsEF4YFDkmSAoUBBATgDDQIjEADSqBEwI4BgJxCPoUEiVIkmCpI7BoLCBpBTEC4MChAjEfy6hCL4J3EAQMJHwLCDBwJEBQYMkiQODI4JcBQAICDEwKMCRK4dBYoJrCHYMgyB3BHYWShKGCBweAJoLXDQAJiBZAQAVC4ZZBU4KADPQR3BQYLCByA+BBAWCJoabCUIR6BcwQIFaIQAPC4KABwC8CoC/DPQSDDoICBHwREBJQabBHwRHBNYSqDZYQAMCIQCBQYT7DAQKADQwVJAQY7BAoJEBHAJZCQwYmBEANANATXBI4IvBPpJQFiEAPoYCBWwg7CBYIFCAQIFGcAYCBX4ImBIgQaBBwLDPwCDCoMAEYS/BfwJECoJEDAQuCSQLOBBwJ9DQYY7BHwaDDABYRBQwIgBDQMSVoMSHwK5BIgKDDIIyACKwMEyDLChKGEZwckIgWCIhwXBYQY7BEwJHBHZICHSoSeCMogCCQYYOBYho+BTwQmBQYQvCQAUSIJyACZAKDCMobsDEANAH5hTDTYYmDF4ICBQaICCLgSeBEAKACJohKCRgJBIwCeBC4IdBEALsBQAYCCHx4XBLIRlBkkQRgRBBdgMEJoSDMgJEBKwRcCU4J9SAQYaCGoI4BAoUQVobFBJQL4BGoICBAA4UDL4QsCiRBVAQjdBiS8CAQMShLRDfAUQH44+BC4sgyAjBHzC/CGoKnCySqCagJuCiEIQYNAQZIRCyAXBQALFXQxBHEaIQIBQYZNBH4sSQYVAKwIgBIgQ+ZMQYFBHwMBVQiGCGoILBIAsEgBNBTwWCZASAdMQkSVQaDBKAOCXIVBZALLBIIY+BwBWBQYbCfQYbCBEwRHBRIRxBQwMCQgiYDCIOShBcCQbxEBEYJHChIIBAQMQQYK5CwFAH4JHBiCYCQYg+eUgaDBAQYLCeoR6BAQUBQYcCQYJNCCgRBgQAZrBBAZ0BYoUQAQMAQYaJBKwLaDDQgCdQA6MCBYI4BiEJIgKDCYoVAHwIOBCgRBggTvChLCDRIMEGQTFBAQKDCgAOBhI+kyVIj4mBgkCRgzFGgRHBQYRKBKAJcCIkP/+UIJpB3BJoMQAoOQoBBBgCYDQclJgF+YQZEIkkQKAKDBgGAQAKSBQYNBIMfgh0BHY2SXgI1BQANIgjLCQYKPBLJZBcgPBggIDoI+BHYL4CgUBBAI/BwDIBKYUSIMsAnApFfYQABHwOQoEEiACCZYKPBQctIGwSDEfYIAEX4L7BiQLCQYTFmpEDwAuBgSAByVAIIoABhMAYoKDBH0xBHGoJ6BABMkYoJTBAoTFqgf+H5QABkGQYQOAQdNPGQUcvxBMgGAoMEiVBkmQExYOBILkfwP4jhAKgmShEEYRxNBCIJEBiVJkBKSo4zDv//8CDLFgTdBkmCExWCC4gFBIIM5IK8fwDHMEx0EDBEB/5BQoED+EAgbFBQZIjBAYK2CVozRBAoMSLRUPDoJBPvEAvw1FEZJNBgI4DAQa2CAoKeKgf+g/kIJ84//+IIX4AYMHgfx4Ech0HjgqFiUJDol/8AFBkANBUJDsC8ECIJuT/H/MQ0H8f+n//x/AFQwmFyE4j4FBL4PHKwwAB/gmBJoNBTAYCIyP4CgJBG//wn/j+CvIgLFFgFwIIMAnAUHHwU4AQMIQZmf/5BIIgjyJII0OAQMPTA8B/4CBBYjILv//cZAIBMQRBNyAHCn41B/AXEMQMP/AcHhJBIk//KwIAJQwRBRKwN/4BBC8Fx4Hj/5BBJoJBNpEDCgQAWII8AMgP4j+PAgIADYQMPOIsBQZEfwE4IMEfHooAD/EDwE/dIqDI/g4QEYJBLg//+ARBIJKDBAA8EII1HcAZBN/kBEwxBFMQSDKDQSzEuACBIIySDIJsHgEOQZkcIJeOn4XCGQhHBiRBGOgZALuAaBIJeSg4vCSoJBHwP48F/45xDvxBKDoKCMTwQAHEAeRaIccIIuP/5rBg///x0DgeB44EBIIpKBv//8EPfAwAGQZeTDQN/DoU///x44+Bn6tCj/+cwJBCwAFCIImRQAMPIIIaBH5UHgf+IJf4jggDg59C8EOAoKhBj47DEwYCBhIgE/4RBn4gEIJKnBIJkfXwL4C/kBJQJBB8fwCwSJDAAK8BIIrmBIIIdBBgP/DQYAGBZDmIF4XgIIMD+E4XgIKBSQJuFHAMBIIn4/4UBAQf/45oCAAb1BVQRBJyCtCNwQCBuE48eABAQgCFYJHCAAMcgBBEyE/OIUfWwIVBLIxiBWwOOYpZZBhwJBgPAgF+JQU/IIopDMoK8CEAeAagUHgf/4BBCGw0B/5BNDAIODHwN+AgNxLgI1B8CDJUgoLDMoZ6BGwxoBj6VBIJORLYXAfwMHHAPAjgjBHYMHB4RBFEAwLFh6kBgJBDZAhBSNwMH/l/HwP/QYKtEIIk/KwIgEiE4BYM4YoP48DXBEwM/8Y7Dn//IJ74BhxBBh4IDAAPHHwIgBIIUB47RCgiDDHwIvCUIIvCL4P/YoqJEIJn/+I+FAAPwZwMBQwK5EIIxxCx0AU4Q0HQYjmLyAOBIJXgZwILBZwIgEBYJBDkGAg68BgDFBIIMB/BBIQZhBEbod/IIZfDAARBEgYFBkCDDg6/EHwMBYQhHDeQJBLwYwC4ED/wMBh4ICEwYOCIIg4DQYYvCa4JfB/CGBUIQmCIIU4IJmBDAP+O4SkCIIhHDIIs4IJH/xyDCMoP/46YD+IpBQZ2AHwPxX5H4AQofHIIcEIIQgBHwKGCHYQABuBZCn6MDII4gBIIahC//wC4KhBJYZBBMQomBgIgCQYMAUgQPDgLIBTYbgGQZJBCx40BRIQmCZAYAEFIyDDEALCC/8cgLmCQYhBQBwRBDHwLIHAAakGh0BIIQHCIIX/wLaDI4IFD8FwSQhBGpBBC8ZBB/kOHYSkCIIccv4CBDokcuADBQYjjDMQP8QY4IBI4ZBHSQZiEfwaDD+BfCFgIgHkCDEgEPIIUAbRDOKIIOQAofjHoWOAwJ6BYov8uLFHAALFCQYL1Bh/4L4Uf+A7Hv5BPL4UAv0HfwcBAgQADgEONgxBCAgI7BUIYADLIrjFIIyYDnEfQALpDPoStCNYTODcxCDBeohBFboKPBAQLpBIJRnD/ED/wUB+IgDjkOn7RDQYTwHIIQgC8FwIIOP45BEgCGBFgJ3DIJQoDDYPwC4OPHAIICJoStHIIsgCgPgagMfGoV/Rgf8v4eBIKK8Cx/AHAUD+E/JQM4BAXjU4yDFC4LXCQwIQEcYRfIB4cgA4pWCEYImCIIKtBwBEBJQaDKgE4GoPHGQ6hDIBIABIIk/gfxfYl+BQNx4DIDQZNJII6hBwLCDUghALQY3jGgZZCL4OPEwKPEjggGhJBEn4gBHwaDFDQ4gGIIcOYooAB+ChD/gLEj+AIJVwa4SbBACpfCIIkPEQLIEAA/HVQ7FDkkOQYP+n/gEwRBZcYI+LR4ofEgRBDkBBB8EB//gHR1wAomCIIgODX4YAJR4TFGQYihB/hBBYipiCA4hfBnBALF4SDCOgcSQYhBCAQPj+P4HRcfMQkEL4QOEOgPAgF/IJMDAYWAnDFIQYRQBg/jx4jBABUDIJAIEGIRxC8YmBIAnAAgaDBAAYgCAQSSEgeOXxn8AohBDyQHCHAYRFI4ILBPoMfx4PBwPAYpBlBn6kDABscAogdDyVIA4IgBMQICBn7sB444B45KBIgPxYoU/EIZBHa4JBEuA/HBAV+AwUBIIlJgg4C/yGD8F/YgjLFwP4IJTCB/4OEIJFxEALUDII0gh4wC/ACCEwIABX4MfIwgOCageCIImQnEcLgMcHxDCCv4vBWwIABhJBFkkBF4SGBGQI7Dd4JAGAAR6BgAgFyUHgBlCGQYAHBwMcAwYdFpIIBWwaADAAWOn4FD+ILFgEEIK8AnAEDgRBGQYLCD8AjC/w+BYoIAC/jXDAAXAQY9ADoZBMAAhBHgDjBAQSzBRIYAFeoQADGQKDHgH8gBcCMoI9KgbjCwRBGkBfBUgX8uEHj+BX4f8v0AJAxBJpBBBL4JBBQZ8SII0EgIsDAQUDX4fHRgMAj5BF/0AcwyDBAAQUBIJ5fHQYKhBO4j1BXgXwnEcNwRBHkAjGo62DIIPjMoLFJ+EBL45BD/+BHwfgZYLpCjkP/DFIEY8/GYYXBfYYAJIJMkBgLmBEAQ0DIITCGIIcBU42TX4kBMoIAMhJBLEAg0DJQM4j/wYoXHB4IFBQZGBIIgAOiA+HAQSbBj6eEIQfx/8AuEHRIUAhwOBIJGRIKcCIJWQg7FDX4wsBv5EBJouAU5E/KYYAOHxBBDn5BFgb7DgEcPoJNGIJMAIIsggFAHw0B4EEIJeSQYx9C/+OnAIE/BNCCgIjJHYL7FpCMGgP4HxSkD//+QY/x4AJDh7IBJoPgiQjKiAVCBAVIYhCDOgeAII4IFwPH/+P/DpMHYRQDIJDgIAQtATYx6BjgHEIIP4SoRlNI45BVpDyBAAf8boLgBX4IADg8f/0BHyBrDIA0CLJ5BEg4FDh/AEQrPB5JBbgBBPYocOgEfwAFDII3+HyICCDgsHgESCJFBBApBEg8cAgMgQY0H//yIKl4IQvgIJICGHYUAgPH/gXBglwQY0cdJ4CFo5fF8C/KIIy8Dg/kIIMn+CnHHyQCCC4IdFwTFPn+OC4U/GoUjII0AghBbnAdKoAIGxwaBv/4JQZBHHyYCCp/gd4fACJOQYo0kh//gfwgIICng/FgPBIK1AIIcAhIRKNY+R/EfwAXDn+AEIccuPJIK1INAIeBuBBJkGQYo4aBkECBAZBBnCEEHygmDgFx4BrBBxFBkkAIJACBiQFDkaDCh/AjgLEASsCgEgHAQLFyCDBYpCVIP4RBdyDCJOgNBgiDLAQkncwQABjg+XaJpNBfYMQQaH+H4MB4HgIMgsBiUJgGAQZ8kxxBBh0AghBjQAICBoDFBQZ8kx/Aj/+gKAjkmCoKABAQMAhAXPyZBB+DCkAQMJkkCgEgwSDQyVIkA+kpMEiEIgACBkCDRAVB9BgMEYYMAQwJB4QYNAQYUEgTLBQfGAQIQABwA+2AQNIkiDCgEIZYMIIO8JQIkBgjC3AQNBgkQIISDByBB3HAKDEoKA3AQOCgEgIAWChJB6hCDEhBB5iFAQf6DDgMAgSD7YYcEiVJkBB3kGAIIaDBYvMgH4cIkGCaIpBypBBEAAI7zAQtBH4kBgkSYocgIOdIQQrFBoIOEI4YCuoBAEoKDBpMEBwTLzQY5BBHeICFIAqDByAODkBBywRAEgUBgi/zAQmQoBBDiFAPosEIOaDFhI7zAQpAEgkSH26DIBw4JBhEEiFIkGQAQRBmpA/EAAVAkGAhEAwUAA=="));
+
+var battery = E.getBattery();
+
+// Positions on screen
+const timeX = 88, timeY = 52;
+const dateX = 5, dateY = 180;
+const battX = 172, battY = 175;
+
+// Draw on every second if unlocked or charging, minute otherwise, start at with seconds on load
+var drawTimeout;
+var drawInterval = 1000;
+
+// schedule a draw for the next interval
+function queueDraw() {
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = setTimeout(function() {
+ drawTimeout = undefined;
+ draw();
+ }, drawInterval - (Date.now() % drawInterval));
+}
+
+// Update display and timeout on lock/unlock and charge state change
+Bangle.on('lock',on=>{
+ draw();
+});
+
+Bangle.on('charging',charging=>{
+ draw();
+});
+
+function draw() {
+ // work out how to display the current time
+ var d = new Date();
+ var h = d.getHours(), m = d.getMinutes();
+ var time = (" "+h).substr(-2) + ":" + ("0"+m).substr(-2);
+ var seconds = ("0"+d.getSeconds()).substr(-2);
+
+ // g.clear(); // Unneeded if background image takes the whole screen
+
+ // Draw background
+ g.drawImage(img);
+ g.setColor(1, 1, 1);
+
+ // draw the current time
+ g.setFontAlign(1,1); // align right bottom
+ g.setFont("6x15",3);
+ g.drawString(time, timeX, timeY, false);
+
+ // Draw battery %
+ g.setFont("6x15",1);
+ var battStr = "";
+ if(Bangle.isCharging()) {
+ battStr = "+";
+ }
+ g.drawString(battStr + battery + "%", battX, battY, false);
+
+ // Draw date
+ g.setFontAlign(-1,1); // align left bottom
+ g.setFont("6x15",2);
+ var dateStr = require("locale").date(d)+" ";
+ g.drawString(dateStr, dateX, dateY, false);
+
+ // draw the seconds only if unlocked, set next timeout
+ if(!Bangle.isLocked() || Bangle.isCharging()) {
+ drawInterval = 1000;
+ g.setFont("6x15",2);
+ g.drawString(seconds, timeX+2, timeY-4, false);
+ }
+ else
+ drawInterval = 60000;
+
+ // Schedule next draw
+ queueDraw();
+ // console.log("Draw " + time + ":" + seconds);
+}
+
+function refreshBattery() {
+ battery = E.getBattery();
+}
+
+// Only update displayed battery level every minute as it fluctuates a lot
+var batteryInterval = setInterval(refreshBattery, 60000);
+
+Bangle.setUI("clock");
+Bangle.setLocked(false);
+// Clear the screen once, at startup
+g.clear();
+// draw immediately at first
+draw();
diff --git a/apps/2ofthemclk/app.png b/apps/2ofthemclk/app.png
new file mode 100644
index 000000000..d304f27d9
Binary files /dev/null and b/apps/2ofthemclk/app.png differ
diff --git a/apps/2ofthemclk/bg.png b/apps/2ofthemclk/bg.png
new file mode 100644
index 000000000..5f65ca3c7
Binary files /dev/null and b/apps/2ofthemclk/bg.png differ
diff --git a/apps/2ofthemclk/metadata.json b/apps/2ofthemclk/metadata.json
new file mode 100644
index 000000000..fa02b3e2f
--- /dev/null
+++ b/apps/2ofthemclk/metadata.json
@@ -0,0 +1,17 @@
+{
+ "id": "2ofthemclk",
+ "name": "two of them clock",
+ "version": "0.01",
+ "description": "You can now wear teh memez on your wrist.",
+ "readme": "README.md",
+ "icon": "app.png",
+ "screenshots": [{"url":"screenshot.png"}],
+ "type": "clock",
+ "tags": "clock",
+ "supports": ["BANGLEJS2"],
+ "allow_emulator": true,
+ "storage": [
+ {"name":"2ofthemclk.app.js","url":"app.js"},
+ {"name":"2ofthemclk.img","url":"app-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/2ofthemclk/screenshot.png b/apps/2ofthemclk/screenshot.png
new file mode 100644
index 000000000..b9a80a2c5
Binary files /dev/null and b/apps/2ofthemclk/screenshot.png differ
diff --git a/apps/_example_widget/widget.js b/apps/_example_widget/widget.js
index f7aed6991..226aea589 100644
--- a/apps/_example_widget/widget.js
+++ b/apps/_example_widget/widget.js
@@ -9,7 +9,7 @@ currently-running apps */
// add your widget
WIDGETS["mywidget"]={
- area:"tl", // tl (top left), tr (top right), bl (bottom left), br (bottom right)
+ area:"tl", // tl (top left), tr (top right), bl (bottom left), br (bottom right), be aware that not all apps support widgets at the bottom of the screen
width: 28, // how wide is the widget? You can change this and call Bangle.drawWidgets() to re-layout
draw:draw // called to draw the widget
};
diff --git a/apps/activityreminder/ChangeLog b/apps/activityreminder/ChangeLog
index d4b5100a2..da897b899 100644
--- a/apps/activityreminder/ChangeLog
+++ b/apps/activityreminder/ChangeLog
@@ -3,3 +3,5 @@
0.03: Do not alarm while charging
0.04: Obey system quiet mode
0.05: Battery optimisation, add the pause option, bug fixes
+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
\ No newline at end of file
diff --git a/apps/activityreminder/README.md b/apps/activityreminder/README.md
index 25e2c8d35..0c79b4141 100644
--- a/apps/activityreminder/README.md
+++ b/apps/activityreminder/README.md
@@ -11,4 +11,5 @@ Different settings can be personalized:
- Dismiss delay: Delay added before the next alert if the alert is dismissed. From 5 to 60 min
- Pause delay: Same as Dismiss delay but longer (usefull for meetings and such). From 30 to 240 min
- Min steps: Minimal amount of steps to count as an activity
+- Temp Threshold: Temperature threshold to determine if the watch is worn
diff --git a/apps/activityreminder/app.js b/apps/activityreminder/app.js
index f3d72976e..c2b626fb3 100644
--- a/apps/activityreminder/app.js
+++ b/apps/activityreminder/app.js
@@ -1,42 +1,46 @@
-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();
+(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();
+
+ 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);
}
- if (v == 2) {
- activityreminder_data.dismissDate = new Date();
+
+ function run() {
+ if (activityreminder.mustAlert(activityreminder_data, activityreminder_settings)) {
+ drawAlert();
+ } else {
+ eval(storage.read("activityreminder.settings.js"))(() => load());
+ }
}
- 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());
- }
-}
-
-
-const activityreminder = require("activityreminder");
-const storage = require("Storage");
-g.clear();
-Bangle.loadWidgets();
-Bangle.drawWidgets();
-const activityreminder_settings = activityreminder.loadSettings();
-const activityreminder_data = activityreminder.loadData();
-run();
+ g.clear();
+ Bangle.loadWidgets();
+ Bangle.drawWidgets();
+ run();
+
+})();
\ No newline at end of file
diff --git a/apps/activityreminder/boot.js b/apps/activityreminder/boot.js
index 7c094f521..f97cf274d 100644
--- a/apps/activityreminder/boot.js
+++ b/apps/activityreminder/boot.js
@@ -1,45 +1,70 @@
-function run() {
- if (isNotWorn()) return;
- let now = new Date();
- let h = now.getHours();
- let health = Bangle.getHealthStatus("day");
-
- if (h >= activityreminder_settings.startHour && h < activityreminder_settings.endHour) {
- 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 10 secs after the stepsDate + minSteps
- cancel all other timers of this app
- */
- }
-
- if(activityreminder.mustAlert(activityreminder_data, activityreminder_settings)){
- load('activityreminder.app.js');
- }
- }
-
-}
-
-function isNotWorn() {
- // todo in a futur release check temperature and mouvement in a futur release
- return Bangle.isCharging();
-}
-
-const activityreminder = require("activityreminder");
-const activityreminder_settings = activityreminder.loadSettings();
-if (activityreminder_settings.enabled) {
- const activityreminder_data = activityreminder.loadData();
- if(activityreminder_data.firstLoad){
- activityreminder_data.firstLoad =false;
+(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;
activityreminder.saveData(activityreminder_data);
}
- setInterval(run, 60000);
- /* todo in a futur release
- increase setInterval time to something that is still sensible (5 mins ?)
- add settimer to trigger like 10 secs after the stepsDate + minSteps
- cancel all other timers of this app
- */
-}
+
+ 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
+ */
+ }
+})();
diff --git a/apps/activityreminder/lib.js b/apps/activityreminder/lib.js
index 5b7959827..704d35641 100644
--- a/apps/activityreminder/lib.js
+++ b/apps/activityreminder/lib.js
@@ -1,5 +1,3 @@
-const storage = require("Storage");
-
exports.loadSettings = function () {
return Object.assign({
enabled: true,
@@ -8,21 +6,22 @@ exports.loadSettings = function () {
maxInnactivityMin: 30,
dismissDelayMin: 15,
pauseDelayMin: 120,
- minSteps: 50
- }, storage.readJSON("activityreminder.s.json", true) || {});
+ minSteps: 50,
+ tempThreshold: 27
+ }, require("Storage").readJSON("activityreminder.s.json", true) || {});
};
exports.writeSettings = function (settings) {
- storage.writeJSON("activityreminder.s.json", settings);
+ require("Storage").writeJSON("activityreminder.s.json", settings);
};
exports.saveData = function (data) {
- storage.writeJSON("activityreminder.data.json", data);
+ require("Storage").writeJSON("activityreminder.data.json", data);
};
exports.loadData = function () {
let health = Bangle.getHealthStatus("day");
- const data = Object.assign({
+ let data = Object.assign({
firstLoad: true,
stepsDate: new Date(),
stepsOnDate: health.steps,
@@ -30,7 +29,7 @@ exports.loadData = function () {
dismissDate: new Date(1970),
pauseDate: new Date(1970),
},
- storage.readJSON("activityreminder.data.json") || {});
+ require("Storage").readJSON("activityreminder.data.json") || {});
if(typeof(data.stepsDate) == "string")
data.stepsDate = new Date(data.stepsDate);
diff --git a/apps/activityreminder/metadata.json b/apps/activityreminder/metadata.json
index 15f10f2ed..fb9423e7c 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.05",
+ "version":"0.07",
"icon": "app.png",
"type": "app",
"tags": "tool,activity",
diff --git a/apps/activityreminder/settings.js b/apps/activityreminder/settings.js
index 9dff61f48..ce7cdc913 100644
--- a/apps/activityreminder/settings.js
+++ b/apps/activityreminder/settings.js
@@ -1,76 +1,85 @@
(function (back) {
- // Load settings
- const activityreminder = require("activityreminder");
- const 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,
- format: v => v ? "Yes" : "No",
- 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 => {
- return x + " min";
- }
- },
- 'Dismiss delay': {
- value: settings.dismissDelayMin,
- min: 5, max: 60,
- onchange: v => {
- settings.dismissDelayMin = v;
- activityreminder.writeSettings(settings);
- },
- format: x => {
- return x + " min";
- }
- },
- 'Pause delay': {
- value: settings.pauseDelayMin,
- min: 30, max: 240,
- onchange: v => {
- settings.pauseDelayMin = v;
- activityreminder.writeSettings(settings);
- },
- format: x => {
- return x + " min";
- }
- },
- 'Min steps': {
- value: settings.minSteps,
- min: 10, max: 500,
- onchange: v => {
- settings.minSteps = v;
- activityreminder.writeSettings(settings);
- }
- }
- });
+ // Show the menu
+ E.showMenu({
+ "": { "title": "Activity Reminder" },
+ "< Back": () => back(),
+ 'Enable': {
+ value: settings.enabled,
+ format: v => v ? "Yes" : "No",
+ 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 => {
+ return x + " min";
+ }
+ },
+ 'Dismiss delay': {
+ value: settings.dismissDelayMin,
+ min: 5, max: 60,
+ onchange: v => {
+ settings.dismissDelayMin = v;
+ activityreminder.writeSettings(settings);
+ },
+ format: x => {
+ return x + " min";
+ }
+ },
+ 'Pause delay': {
+ value: settings.pauseDelayMin,
+ min: 30, max: 240, step: 5,
+ onchange: v => {
+ settings.pauseDelayMin = v;
+ activityreminder.writeSettings(settings);
+ },
+ format: x => {
+ return x + " min";
+ }
+ },
+ '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);
+ }
+ }
+ });
})
diff --git a/apps/agenda/ChangeLog b/apps/agenda/ChangeLog
new file mode 100644
index 000000000..56dfffa0d
--- /dev/null
+++ b/apps/agenda/ChangeLog
@@ -0,0 +1 @@
+0.01: Basic agenda with events from GB
diff --git a/apps/agenda/README.md b/apps/agenda/README.md
new file mode 100644
index 000000000..a546e0a89
--- /dev/null
+++ b/apps/agenda/README.md
@@ -0,0 +1,3 @@
+# Agenda
+
+Basic agenda reading the events synchronised from GadgetBridge
diff --git a/apps/agenda/agenda-icon.js b/apps/agenda/agenda-icon.js
new file mode 100644
index 000000000..891543955
--- /dev/null
+++ b/apps/agenda/agenda-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwg1yhGIxAPMBwIPFhH//GAC5n/C4oHBC5/IGwoXBHQQAKC4OIFAWOxHv9GO9wAKI4XoC4foEIIWLC4IABC4gIBFxnuE4IqBC4gARC4ZzNAAwXaxe7ACO4C625C4m4xIJBzAeCxGbCAOIFgQOBC4pOBxe4AYIPBAYQKCAYYXE3GL/ADBx/oxb3BC4X+xG4xwOBC4uP/YDB54MBf4Po3eM/4XBx/+C4pTBGIIkBLgOYAYIvB9GJBwI6BL45zCL4aCCL4h3GU64ALdYS1CI55bBAAgXFO4mMO4QDBDIO/////YxBU53IxIVB/GfDAWYa5wtC/GPAYWIL4wXBL4oSBC4jcBC4m4QIWYSwWIIQIAG/CnMMAIAC/JLCMIIvMIwZHFJAJfLC5yPHAYIRDAoy/KCIi7BMon4d4+Od4IXBxAZBEQLtB/+YxIXDL4SLCL4WPzAXCNgRFBLIKnKLIrcEI4gXNAAp3CxGZAAzCBC5KnCKAIAICxBlBC4IAJxG/C4/4wAXLhBgD/IcD3AXMGAIqDDgRGNGAoXDFxxhEI4W4FxwwCaoYWBFx4YDAAQWRAEQ"))
diff --git a/apps/agenda/agenda.js b/apps/agenda/agenda.js
new file mode 100644
index 000000000..f39e31c75
--- /dev/null
+++ b/apps/agenda/agenda.js
@@ -0,0 +1,127 @@
+/* CALENDAR is a list of:
+ {id:int,
+ type,
+ timestamp,
+ durationInSeconds,
+ title,
+ description,
+ location,
+ allDay: bool,
+ }
+*/
+
+Bangle.loadWidgets();
+Bangle.drawWidgets();
+
+var FILE = "android.calendar.json";
+
+var Locale = require("locale");
+
+var fontSmall = "6x8";
+var fontMedium = g.getFonts().includes("6x15")?"6x15":"6x8:2";
+var fontBig = g.getFonts().includes("12x20")?"12x20":"6x8:2";
+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)||[];
+
+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 formatDateShort(date) {
+ return Locale.date(date).replace(/\d\d\d\d/,"")+Locale.time(date,1);
+}
+
+var lines = [];
+function showEvent(ev) {
+ var bodyFont = fontBig;
+ if(!ev) return;
+ g.setFont(bodyFont);
+ //var lines = [];
+ 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));
+ var includeDay = true;
+ if (titleCnt) lines.push(""); // add blank line after title
+ if(start.getDay() == end.getDay() && start.getMonth() == end.getMonth())
+ includeDay = false;
+ if(includeDay) {
+ lines = lines.concat(
+ /*LANG*/"Start:",
+ g.wrapString(formatDateLong(start, includeDay), g.getWidth()-10),
+ /*LANG*/"End:",
+ g.wrapString(formatDateLong(end, includeDay), 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));
+ }
+ 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));
+ lines = lines.concat(["",/*LANG*/"< Back"]);
+ 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=lines.length-2)
+ showList();
+ },
+ back : () => showList()
+ });
+}
+
+function showList() {
+ if(CALENDAR.length == 0) {
+ E.showMessage("No events");
+ return;
+ }
+ E.showScroller({
+ h : 52,
+ c : Math.max(CALENDAR.length,3), // workaround for 2v10.219 firmware (min 3 not needed for 2v11)
+ draw : function(idx, r) {"ram"
+ var ev = CALENDAR[idx];
+ 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 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;
+ if (title) g.setFontAlign(-1,-1).setFont(fontBig)
+ .setColor(isPast ? "#888" : g.theme.fg).drawString(title, x,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);
+ }
+ //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
+ },
+ select : idx => showEvent(CALENDAR[idx]),
+ back : () => load()
+ });
+}
+showList();
diff --git a/apps/agenda/agenda.png b/apps/agenda/agenda.png
new file mode 100644
index 000000000..c850b0e5d
Binary files /dev/null and b/apps/agenda/agenda.png differ
diff --git a/apps/agenda/metadata.json b/apps/agenda/metadata.json
new file mode 100644
index 000000000..ce8438686
--- /dev/null
+++ b/apps/agenda/metadata.json
@@ -0,0 +1,17 @@
+{
+ "id": "agenda",
+ "name": "Agenda",
+ "version": "0.02",
+ "description": "Simple agenda",
+ "icon": "agenda.png",
+ "screenshots": [{"url":"screenshot_agenda_overview.png"}, {"url":"screenshot_agenda_event1.png"}, {"url":"screenshot_agenda_event2.png"}],
+ "tags": "agenda",
+ "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.img","url":"agenda-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/agenda/screenshot_agenda_event1.png b/apps/agenda/screenshot_agenda_event1.png
new file mode 100644
index 000000000..581da286b
Binary files /dev/null and b/apps/agenda/screenshot_agenda_event1.png differ
diff --git a/apps/agenda/screenshot_agenda_event2.png b/apps/agenda/screenshot_agenda_event2.png
new file mode 100644
index 000000000..f5edcaae8
Binary files /dev/null and b/apps/agenda/screenshot_agenda_event2.png differ
diff --git a/apps/agenda/screenshot_agenda_overview.png b/apps/agenda/screenshot_agenda_overview.png
new file mode 100644
index 000000000..a2030d05f
Binary files /dev/null and b/apps/agenda/screenshot_agenda_overview.png differ
diff --git a/apps/agenda/settings.js b/apps/agenda/settings.js
new file mode 100644
index 000000000..fe9dab2d8
--- /dev/null
+++ b/apps/agenda/settings.js
@@ -0,0 +1,37 @@
+(function(back) {
+ function gbSend(message) {
+ Bluetooth.println("");
+ Bluetooth.println(JSON.stringify(message));
+ }
+ var CALENDAR = require("Storage").readJSON("android.calendar.json",true)||[];
+ var mainmenu = {
+ "" : { "title" : "Agenda" },
+ "< Back" : back,
+ /*LANG*/"Connected" : { value : NRF.getSecurityStatus().connected?/*LANG*/"Yes":/*LANG*/"No" },
+ /*LANG*/"Force calendar sync" : () => {
+ if(NRF.getSecurityStatus().connected) {
+ E.showPrompt(/*LANG*/"Do you want to also clear the internal database first?", {
+ buttons: {/*LANG*/"Yes": 1, /*LANG*/"No": 2, /*LANG*/"Cancel": 3}
+ }).then((v)=>{
+ switch(v) {
+ case 1:
+ require("Storage").writeJSON("android.calendar.json",[]);
+ CALENDAR = [];
+ /* falls through */
+ case 2:
+ gbSend({t:"force_calendar_sync", ids: CALENDAR.map(e=>e.id)});
+ E.showAlert(/*LANG*/"Request sent to the phone").then(()=>E.showMenu(mainmenu));
+ break;
+ case 3:
+ default:
+ E.showMenu(mainmenu);
+ return;
+ }
+ });
+ } else {
+ E.showAlert(/*LANG*/"You are not connected").then(()=>E.showMenu(mainmenu));
+ }
+ },
+ };
+ E.showMenu(mainmenu);
+})
diff --git a/apps/alarm/ChangeLog b/apps/alarm/ChangeLog
index b00055334..1be0dcdd5 100644
--- a/apps/alarm/ChangeLog
+++ b/apps/alarm/ChangeLog
@@ -29,3 +29,5 @@
0.27: New UI!
0.28: Fix bug with alarms not firing when configured to fire only once
0.29: Fix wrong 'dow' handling in new timer if first day of week is Monday
+0.30: Fix "Enable All"
+0.31: Add seconds to timers
diff --git a/apps/alarm/app.js b/apps/alarm/app.js
index fe0f67dbb..0a7bb8f24 100644
--- a/apps/alarm/app.js
+++ b/apps/alarm/app.js
@@ -86,7 +86,8 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex) {
const menu = {
"": { "title": isNew ? /*LANG*/"New Alarm" : /*LANG*/"Edit Alarm" },
"< Back": () => {
- saveAlarm(alarm, alarmIndex, time);
+ prepareAlarmForSave(alarm, alarmIndex, time);
+ saveAndReload();
showMainMenu();
},
/*LANG*/"Hour": {
@@ -144,7 +145,7 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex) {
E.showMenu(menu);
}
-function saveAlarm(alarm, alarmIndex, time) {
+function prepareAlarmForSave(alarm, alarmIndex, time) {
alarm.t = require("time_utils").encodeTime(time);
alarm.last = alarm.t < require("time_utils").getCurrentTimeMillis() ? new Date().getDate() : 0;
@@ -153,8 +154,6 @@ function saveAlarm(alarm, alarmIndex, time) {
} else {
alarms[alarmIndex] = alarm;
}
-
- saveAndReload();
}
function saveAndReload() {
@@ -251,7 +250,8 @@ function showEditTimerMenu(selectedTimer, timerIndex) {
const menu = {
"": { "title": isNew ? /*LANG*/"New Timer" : /*LANG*/"Edit Timer" },
"< Back": () => {
- saveTimer(timer, timerIndex, time);
+ prepareTimerForSave(timer, timerIndex, time);
+ saveAndReload();
showMainMenu();
},
/*LANG*/"Hours": {
@@ -268,11 +268,20 @@ function showEditTimerMenu(selectedTimer, timerIndex) {
wrap: true,
onchange: v => time.m = v
},
+ /*LANG*/"Seconds": {
+ value: time.s,
+ min: 0,
+ max: 59,
+ step: 1,
+ wrap: true,
+ onchange: v => time.s = v
+ },
/*LANG*/"Enabled": {
value: timer.on,
onchange: v => timer.on = v
},
/*LANG*/"Vibrate": require("buzz_menu").pattern(timer.vibrate, v => timer.vibrate = v),
+ /*LANG*/"Cancel": () => showMainMenu()
};
if (!isNew) {
@@ -293,7 +302,7 @@ function showEditTimerMenu(selectedTimer, timerIndex) {
E.showMenu(menu);
}
-function saveTimer(timer, timerIndex, time) {
+function prepareTimerForSave(timer, timerIndex, time) {
timer.timer = require("time_utils").encodeTime(time);
timer.t = require("time_utils").getCurrentTimeMillis() + timer.timer;
timer.last = 0;
@@ -303,8 +312,6 @@ function saveTimer(timer, timerIndex, time) {
} else {
alarms[timerIndex] = timer;
}
-
- saveAndReload();
}
function showAdvancedMenu() {
@@ -327,7 +334,16 @@ function enableAll(on) {
} else {
E.showPrompt(/*LANG*/"Are you sure?", { title: on ? /*LANG*/"Enable All" : /*LANG*/"Disable All" }).then((confirm) => {
if (confirm) {
- alarms.forEach(alarm => alarm.on = on);
+ alarms.forEach((alarm, i) => {
+ alarm.on = on;
+ if (on) {
+ if (alarm.timer) {
+ prepareTimerForSave(alarm, i, require("time_utils").decodeTime(alarm.timer))
+ } else {
+ prepareAlarmForSave(alarm, i, require("time_utils").decodeTime(alarm.t))
+ }
+ }
+ });
saveAndReload();
showMainMenu();
} else {
diff --git a/apps/alarm/metadata.json b/apps/alarm/metadata.json
index cac837b5e..54472a12c 100644
--- a/apps/alarm/metadata.json
+++ b/apps/alarm/metadata.json
@@ -2,7 +2,7 @@
"id": "alarm",
"name": "Alarms & Timers",
"shortName": "Alarms",
- "version": "0.29",
+ "version": "0.31",
"description": "Set alarms and timers on your Bangle",
"icon": "app.png",
"tags": "tool,alarm,widget",
diff --git a/apps/android/boot.js b/apps/android/boot.js
index efd7e7e46..9cdc019a6 100644
--- a/apps/android/boot.js
+++ b/apps/android/boot.js
@@ -90,6 +90,35 @@
sched.setAlarms(alarms);
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);
+ if (!cal || !Array.isArray(cal)) cal = [];
+ var i = cal.findIndex(e=>e.id==event.id);
+ if(i<0)
+ cal.push(event);
+ else
+ cal[i] = event;
+ require("Storage").writeJSON("android.calendar.json", cal);
+ },
+ "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;
+ cal = cal.filter(e=>e.id!=event.id);
+ require("Storage").writeJSON("android.calendar.json", cal);
+ },
+ //triggered by GB, send all ids
+ "force_calendar_sync_start" : function() {
+ 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)});
+ }
};
var h = HANDLERS[event.t];
if (h) h(); else console.log("GB Unknown",event);
diff --git a/apps/android/metadata.json b/apps/android/metadata.json
index bf37b8407..27b77cf2f 100644
--- a/apps/android/metadata.json
+++ b/apps/android/metadata.json
@@ -2,7 +2,7 @@
"id": "android",
"name": "Android Integration",
"shortName": "Android",
- "version": "0.10",
+ "version": "0.11",
"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",
@@ -15,6 +15,6 @@
{"name":"android.img","url":"app-icon.js","evaluate":true},
{"name":"android.boot.js","url":"boot.js"}
],
- "data": [{"name":"android.settings.json"}],
+ "data": [{"name":"android.settings.json"}, {"name":"android.calendar.json"}],
"sortorder": -8
}
diff --git a/apps/barclock/ChangeLog b/apps/barclock/ChangeLog
index 5df032c4d..ba44ecef8 100644
--- a/apps/barclock/ChangeLog
+++ b/apps/barclock/ChangeLog
@@ -9,3 +9,6 @@
0.09: Fix time/date disappearing after fullscreen notification
0.10: Use ClockFace library
0.11: Use ClockFace.is12Hour
+0.12: Add settings to hide date,widgets
+0.13: Add font setting
+0.14: Use ClockFace_menu.addItems
diff --git a/apps/barclock/README.md b/apps/barclock/README.md
index 4b92313c5..ff66a5cbb 100644
--- a/apps/barclock/README.md
+++ b/apps/barclock/README.md
@@ -4,3 +4,7 @@ A simple digital clock showing seconds as a horizontal bar.
| 24hr style | 12hr style |
| --- | --- |
|  |  |
+
+## Settings
+* `Show date`: display date at the bottom of screen
+* `Font`: choose between bitmap or vector fonts
\ No newline at end of file
diff --git a/apps/barclock/clock-bar.js b/apps/barclock/clock-bar.js
index 987d41cc6..61ce07dfb 100644
--- a/apps/barclock/clock-bar.js
+++ b/apps/barclock/clock-bar.js
@@ -51,24 +51,25 @@ function dateText(date) {
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}, // size updated below
+ {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},
- {height: 40},
- {id: "date", type: "txt", font: "10%", valign: 1},
+ 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) {
+ if (this.is12Hour && locale.hasMeridian) {
// Maximum font size = ( - ) / (5chars * 6px)
thickness = Math.floor((Bangle.appRect.w-24)/(5*6));
} else {
@@ -76,13 +77,23 @@ const ClockFace = require("ClockFace"),
thickness = Math.floor(Bangle.appRect.w/(5*6));
}
this.layout.bar.height = thickness+1;
- this.layout.time.font = "6x8:"+thickness;
+ 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();
},
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.layout.date.label = dateText(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();
diff --git a/apps/barclock/metadata.json b/apps/barclock/metadata.json
index 7bc61096d..0c227dc52 100644
--- a/apps/barclock/metadata.json
+++ b/apps/barclock/metadata.json
@@ -1,7 +1,7 @@
{
"id": "barclock",
"name": "Bar Clock",
- "version": "0.11",
+ "version": "0.14",
"description": "A simple digital clock showing seconds as a bar",
"icon": "clock-bar.png",
"screenshots": [{"url":"screenshot.png"},{"url":"screenshot_pm.png"}],
@@ -12,6 +12,10 @@
"allow_emulator": true,
"storage": [
{"name":"barclock.app.js","url":"clock-bar.js"},
+ {"name":"barclock.settings.js","url":"settings.js"},
{"name":"barclock.img","url":"clock-bar-icon.js","evaluate":true}
+ ],
+ "data": [
+ {"name":"barclock.settings.json"}
]
}
diff --git a/apps/barclock/settings.js b/apps/barclock/settings.js
new file mode 100644
index 000000000..dfe25581c
--- /dev/null
+++ b/apps/barclock/settings.js
@@ -0,0 +1,26 @@
+(function(back) {
+ let s = require("Storage").readJSON("barclock.settings.json", true) || {};
+
+ function save(key, value) {
+ s[key] = value;
+ require("Storage").writeJSON("barclock.settings.json", s);
+ }
+
+ const fonts = [/*LANG*/"Bitmap",/*LANG*/"Vector"];
+ let menu = {
+ "": {"title": /*LANG*/"Bar Clock"},
+ /*LANG*/"< Back": back,
+ /*LANG*/"Font": {
+ value: s.font|0,
+ min: 0, max: 1, wrap: true,
+ format: v => fonts[v],
+ onchange: v => save("font", v),
+ },
+ };
+ require("ClockFace_menu").addItems(menu, save, {
+ showDate: s.showDate,
+ loadWidgets: s.loadWidgets,
+ });
+
+ E.showMenu(menu);
+});
diff --git a/apps/barcode/ChangeLog b/apps/barcode/ChangeLog
new file mode 100644
index 000000000..4f99f15ac
--- /dev/null
+++ b/apps/barcode/ChangeLog
@@ -0,0 +1,9 @@
+0.01: Please forgive me
+0.02: Now tells time!
+0.03: Interaction
+0.04: Shows day of week
+0.05: Shows day of month
+0.06: Updates every 5 minutes when locked, or when unlock occurs. Also shows nr of steps.
+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...)
diff --git a/apps/barcode/README.md b/apps/barcode/README.md
new file mode 100644
index 000000000..17b365d45
--- /dev/null
+++ b/apps/barcode/README.md
@@ -0,0 +1,23 @@
+# Barcode clockwatchface
+
+A scannable EAN-8 compatible clockwatchface for your Bangle 2
+
+The format of the bars are
+
+`||HHmm||MMwc||`
+
+* Left section: HHmm
+ * H: Hours
+ * m: Minutes
+* Right section: MM9c
+ * M: Day of month
+ * w: Day of week
+ * c: Calculated EAN-8 digit checksum
+
+Apart from that
+
+* The upper left section displays total number of steps per day
+* The upper right section displays total number of steps from last boot ("stepuptime")
+* The face updates every 5 minutes or on demant by pressing the hardware button
+
+This clockwathface is aware of theme choice, so it will adapt to Light/Dark themes.
diff --git a/apps/barcode/barcode.app.js b/apps/barcode/barcode.app.js
new file mode 100644
index 000000000..89419f33c
--- /dev/null
+++ b/apps/barcode/barcode.app.js
@@ -0,0 +1,428 @@
+/* Sizes */
+let checkBarWidth = 10;
+let checkBarHeight = 140;
+
+let digitBarWidth = 14;
+let digitBarHeight = 100;
+
+let textBarWidth = 56;
+let textBarHeight = 20;
+
+let textWidth = 14;
+let textHeight = 20;
+
+/* Offsets */
+var startOffsetX = 17;
+var startOffsetY = 30;
+
+let startBarOffsetX = startOffsetX;
+let startBarOffsetY = startOffsetY;
+
+let upperTextBarLeftOffsetX = startBarOffsetX + checkBarWidth;
+let upperTextBarLeftOffsetY = startOffsetY;
+
+let midBarOffsetX = upperTextBarLeftOffsetX + textBarWidth;
+let midBarOffsetY = startOffsetY;
+
+let upperTextBarRightOffsetX = midBarOffsetX + checkBarWidth;
+let upperTextBarRightOffsetY = startOffsetY;
+
+let endBarOffsetX = upperTextBarRightOffsetX + textBarWidth;
+let endBarOffsetY = startOffsetY;
+
+let leftBarsStartX = startBarOffsetX + checkBarWidth;
+let leftBarsStartY = upperTextBarLeftOffsetY + textBarHeight;
+
+let rightBarsStartX = midBarOffsetX + checkBarWidth;
+let rightBarsStartY = upperTextBarRightOffsetY + textBarHeight;
+
+/* Utilities */
+let stepCount = require("Storage").readJSON("stepCount",1);
+if(stepCount === undefined) stepCount = 0;
+let intCaster = num => Number(num);
+
+var drawTimeout;
+
+function renderWatch(l) {
+ g.setFont("4x6",2);
+
+ // work out how to display the current time
+
+ var d = new Date();
+ var h = d.getHours(), m = d.getMinutes();
+ var time = h + ":" + ("0"+m).substr(-2);
+ //var month = ("0" + (d.getMonth()+1)).slice(-2);
+ var dayOfMonth = ('0' + d.getDate()).slice(-2);
+ var dayOfWeek = d.getDay() || 7;
+ var concatTime = ("0"+h).substr(-2) + ("0"+m).substr(-2) + dayOfMonth + dayOfWeek;
+
+ const chars = String(concatTime).split("").map((concatTime) => {
+ return Number(concatTime);
+ });
+ const checkSum = calculateChecksum(chars);
+ concatTime += checkSum;
+
+ drawCheckBar(startBarOffsetX, startBarOffsetY);
+
+ drawLDigit(chars[0], 0, leftBarsStartY);
+ drawLDigit(chars[1], 1, leftBarsStartY);
+ drawLDigit(chars[2], 2, leftBarsStartY);
+ drawLDigit(chars[3], 3, leftBarsStartY);
+
+ g.drawString(getStepCount(), startOffsetX + checkBarWidth + 3, startOffsetY + 4);
+ g.drawString(concatTime.substring(0,4), startOffsetX + checkBarWidth + 3, startOffsetY + textBarHeight + digitBarHeight + 6);
+
+ drawCheckBar(midBarOffsetX, midBarOffsetY);
+
+ drawRDigit(chars[4], 0, rightBarsStartY);
+ drawRDigit(chars[5], 1, rightBarsStartY);
+ drawRDigit(chars[6], 2, rightBarsStartY);
+ drawRDigit(checkSum, 3, rightBarsStartY);
+
+ g.drawString(Bangle.getStepCount(), midBarOffsetX + checkBarWidth + 3, startOffsetY + 4);
+ g.drawString(concatTime.substring(4), midBarOffsetX + checkBarWidth + 3, startOffsetY + textBarHeight + digitBarHeight + 6);
+
+ drawCheckBar(endBarOffsetX, endBarOffsetY);
+
+ // schedule a draw for the next minute
+ if (drawTimeout) {
+ clearTimeout(drawTimeout);
+ }
+ drawTimeout = setTimeout(function() {
+ drawTimeout = undefined;
+ layout.render(layout.watch);
+ }, (1000 * 60 * 5) - (Date.now() % (1000 * 60 * 5)));
+}
+
+function drawLDigit(digit, index, offsetY) {
+ switch(digit) {
+ case 0:
+ drawLZeroWithOffset(leftBarsStartX+(digitBarWidth*index), offsetY);
+ break;
+ case 1:
+ drawLOneWithOffset(leftBarsStartX+(digitBarWidth*index), offsetY);
+ break;
+ case 2:
+ drawLTwoWithOffset(leftBarsStartX+(digitBarWidth*index), offsetY);
+ break;
+ case 3:
+ drawLThreeWithOffset(leftBarsStartX+(digitBarWidth*index), offsetY);
+ break;
+ case 4:
+ drawLFourWithOffset(leftBarsStartX+(digitBarWidth*index), offsetY);
+ break;
+ case 5:
+ drawLFiveWithOffset(leftBarsStartX+(digitBarWidth*index), offsetY);
+ break;
+ case 6:
+ drawLSixWithOffset(leftBarsStartX+(digitBarWidth*index), offsetY);
+ break;
+ case 7:
+ drawLSevenWithOffset(leftBarsStartX+(digitBarWidth*index), offsetY);
+ break;
+ case 8:
+ drawLEightWithOffset(leftBarsStartX+(digitBarWidth*index), offsetY);
+ break;
+ case 9:
+ drawLNineWithOffset(leftBarsStartX+(digitBarWidth*index), offsetY);
+ break;
+ }
+}
+
+function drawRDigit(digit, index, offsetY) {
+ switch(digit) {
+ case 0:
+ drawRZeroWithOffset(rightBarsStartX+(digitBarWidth*index), offsetY);
+ break;
+ case 1:
+ drawROneWithOffset(rightBarsStartX+(digitBarWidth*index), offsetY);
+ break;
+ case 2:
+ drawRTwoWithOffset(rightBarsStartX+(digitBarWidth*index), offsetY);
+ break;
+ case 3:
+ drawRThreeWithOffset(rightBarsStartX+(digitBarWidth*index), offsetY);
+ break;
+ case 4:
+ drawRFourWithOffset(rightBarsStartX+(digitBarWidth*index), offsetY);
+ break;
+ case 5:
+ drawRFiveWithOffset(rightBarsStartX+(digitBarWidth*index), offsetY);
+ break;
+ case 6:
+ drawRSixWithOffset(rightBarsStartX+(digitBarWidth*index), offsetY);
+ break;
+ case 7:
+ drawRSevenWithOffset(rightBarsStartX+(digitBarWidth*index), offsetY);
+ break;
+ case 8:
+ drawREightWithOffset(rightBarsStartX+(digitBarWidth*index), offsetY);
+ break;
+ case 9:
+ drawRNineWithOffset(rightBarsStartX+(digitBarWidth*index), offsetY);
+ break;
+ }
+}
+
+/*
+LEAN
+
+01234567890123
+ xxxx xx
+ xx xxxx
+ xxxxxxxx xx
+ xx xxxx
+ xxxx xx
+ xx xxxxxxxx
+ xxxxxx xxxx
+ xxxx xxxxxx
+ xx xxxx
+ xxxx xx
+*/
+function drawLOneWithOffset(offset, offsetY) {
+ let barOneX = 4;
+ let barTwoX = 12;
+ g.fillRect(barOneX+offset,offsetY+0,barOneX+3+offset,offsetY+digitBarHeight);
+ g.fillRect(barTwoX+offset,offsetY+0,barTwoX+1+offset,offsetY+digitBarHeight);
+ //g.drawString("1",offset+3,offsetY+digitHeight+5);
+}
+
+function drawLTwoWithOffset(offset, offsetY) {
+ let barOneX = 4;
+ let barTwoX = 10;
+ g.fillRect(barOneX+offset,offsetY+0,barOneX+1+offset,offsetY+digitBarHeight);
+ g.fillRect(barTwoX+offset,offsetY+0,barTwoX+3+offset,offsetY+digitBarHeight);
+ //g.drawString("2",offset+3,offsetY+digitHeight+5);
+}
+
+function drawLThreeWithOffset(offset, offsetY) {
+ let barOneX = 2;
+ let barTwoX = 12;
+ g.fillRect(barOneX+offset,offsetY+0,barOneX+7+offset,offsetY+digitBarHeight);
+ g.fillRect(barTwoX+offset,offsetY+0,barTwoX+1+offset,offsetY+digitBarHeight);
+ //g.drawString("3",offset+3,offsetY+digitHeight+5);
+}
+
+function drawLFourWithOffset(offset, offsetY) {
+ let barOneX = 2;
+ let barTwoX = 10;
+ g.fillRect(barOneX+offset,offsetY+0,barOneX+1+offset,offsetY+digitBarHeight);
+ g.fillRect(barTwoX+offset,offsetY+0,barTwoX+3+offset,offsetY+digitBarHeight);
+ //g.drawString("4",offset+3,offsetY+digitHeight+5);
+}
+
+function drawLFiveWithOffset(offset, offsetY) {
+ let barOneX = 2;
+ let barTwoX = 12;
+ g.fillRect(barOneX+offset,offsetY+0,barOneX+3+offset,offsetY+digitBarHeight);
+ g.fillRect(barTwoX+offset,offsetY+0,barTwoX+1+offset,offsetY+digitBarHeight);
+ //g.drawString("5",offset+3,offsetY+digitHeight+5);
+}
+
+function drawLSixWithOffset(offset, offsetY) {
+ let barOneX = 2;
+ let barTwoX = 6;
+ g.fillRect(barOneX+offset,offsetY+0,barOneX+1+offset,offsetY+digitBarHeight);
+ g.fillRect(barTwoX+offset,offsetY+0,barTwoX+7+offset,offsetY+digitBarHeight);
+ //g.drawString("6",offset+3,offsetY+digitHeight+5);
+}
+
+function drawLSevenWithOffset(offset, offsetY) {
+ let barOneX = 2;
+ let barTwoX = 10;
+ g.fillRect(barOneX+offset,offsetY+0,barOneX+5+offset,offsetY+digitBarHeight);
+ g.fillRect(barTwoX+offset,offsetY+0,barTwoX+3+offset,offsetY+digitBarHeight);
+ //g.drawString("7",offset+3,offsetY+digitHeight+5);
+}
+
+function drawLEightWithOffset(offset, offsetY) {
+ let barOneX = 2;
+ let barTwoX = 8;
+ g.fillRect(barOneX+offset,offsetY+0,barOneX+3+offset,offsetY+digitBarHeight);
+ g.fillRect(barTwoX+offset,offsetY+0,barTwoX+5+offset,offsetY+digitBarHeight);
+ //g.drawString("8",offset+3,offsetY+digitHeight+5);
+}
+
+function drawLNineWithOffset(offset, offsetY) {
+ let barOneX = 6;
+ let barTwoX = 10;
+ g.fillRect(barOneX+offset,offsetY+0,barOneX+1+offset,offsetY+digitBarHeight);
+ g.fillRect(barTwoX+offset,offsetY+0,barTwoX+3+offset,offsetY+digitBarHeight);
+ //g.drawString("9",offset+3,offsetY+digitHeight+5);
+}
+
+function drawLZeroWithOffset(offset, offsetY) {
+ let barOneX = 6;
+ let barTwoX = 12;
+ g.fillRect(barOneX+offset,offsetY+0,barOneX+3+offset,offsetY+digitBarHeight);
+ g.fillRect(barTwoX+offset,offsetY+0,barTwoX+1+offset,offsetY+digitBarHeight);
+ //g.drawString("0",offset+3,offsetY+digitHeight+5);
+}
+
+
+
+/*
+REAN
+
+01234567890123
+xxxx xxxx
+xxxx xxxx
+xx xx
+xx xxxxxx
+xx xxxxxx
+xx xx
+xx xx
+xx xx
+xxxxxx xx
+xxxxxx xx
+
+*/
+function drawROneWithOffset(offset, offsetY) {
+ let barOneX = 0;
+ let barTwoX = 8;
+ g.fillRect(offset+barOneX,offsetY+0,offset+barOneX+3,offsetY+digitBarHeight);
+ g.fillRect(offset+barTwoX,offsetY+0,offset+barTwoX+3,offsetY+digitBarHeight);
+ //g.drawString("1",offset+2,offsetY+textHeight+5);
+}
+
+function drawRTwoWithOffset(offset, offsetY) {
+ let barOneX = 0;
+ let barTwoX = 6;
+ g.fillRect(offset+barOneX,offsetY+0,offset+barOneX+3,offsetY+digitBarHeight);
+ g.fillRect(offset+barTwoX,offsetY+0,offset+barTwoX+3,offsetY+digitBarHeight);
+ //g.drawString("2",offset+2,offsetY+textHeight+5);
+}
+
+function drawRThreeWithOffset(offset, offsetY) {
+ let barOneX = 0;
+ let barTwoX = 10;
+ g.fillRect(barOneX+offset,offsetY+0,barOneX+1+offset,offsetY+digitBarHeight);
+ g.fillRect(barTwoX+offset,offsetY+0,barTwoX+1+offset,offsetY+digitBarHeight);
+ //g.drawString("3",offset+2,offsetY+textHeight+5);
+}
+
+function drawRFourWithOffset(offset, offsetY) {
+ let barOneX = 0;
+ let barTwoX = 4;
+ g.fillRect(barOneX+offset,offsetY+0,barOneX+1+offset,offsetY+digitBarHeight);
+ g.fillRect(barTwoX+offset,offsetY+0,barTwoX+5+offset,offsetY+digitBarHeight);
+ //g.drawString("4",offset+2,offsetY+textHeight+5);
+}
+
+function drawRFiveWithOffset(offset, offsetY) {
+ let barOneX = 0;
+ let barTwoX = 6;
+ g.fillRect(barOneX+offset,offsetY+0,barOneX+1+offset,offsetY+digitBarHeight);
+ g.fillRect(barTwoX+offset,offsetY+0,barTwoX+5+offset,offsetY+digitBarHeight);
+ //g.drawString("5",offset+2,offsetY+textHeight+5);
+}
+
+function drawRSixWithOffset(offset, offsetY) {
+ let barOneX = 0;
+ let barTwoX = 4;
+ g.fillRect(barOneX+offset,offsetY+0,barOneX+1+offset,offsetY+digitBarHeight);
+ g.fillRect(barTwoX+offset,offsetY+0,barTwoX+1+offset,offsetY+digitBarHeight);
+ //g.drawString("6",offset+2,offsetY+textHeight+5);
+}
+
+function drawRSevenWithOffset(offset, offsetY) {
+ let barOneX = 0;
+ let barTwoX = 8;
+ g.fillRect(barOneX+offset,offsetY+0,barOneX+1+offset,offsetY+digitBarHeight);
+ g.fillRect(barTwoX+offset,offsetY+0,barTwoX+1+offset,offsetY+digitBarHeight);
+ //g.drawString("7",offset+2,offsetY+textHeight+5);
+}
+
+function drawREightWithOffset(offset, offsetY) {
+ let barOneX = 0;
+ let barTwoX = 6;
+ g.fillRect(offset+barOneX,offsetY+0,offset+barOneX+1,offsetY+digitBarHeight);
+ g.fillRect(offset+barTwoX,offsetY+0,offset+barTwoX+1,offsetY+digitBarHeight);
+ //g.drawString("8",offset+2,offsetY+textHeight+5);
+}
+
+function drawRNineWithOffset(offset, offsetY) {
+ let barOneX = 0;
+ let barTwoX = 8;
+ g.fillRect(barOneX+offset,offsetY+0,barOneX+5+offset,offsetY+digitBarHeight);
+ g.fillRect(barTwoX+offset,offsetY+0,barTwoX+1+offset,offsetY+digitBarHeight);
+ //g.drawString("9",offset+2,offsetY+textHeight+5);
+}
+
+function drawRZeroWithOffset(offset, offsetY) {
+ let barOneX = 0;
+ let barTwoX = 10;
+ g.fillRect(barOneX+offset,offsetY+0,barOneX+5+offset,offsetY+digitBarHeight);
+ g.fillRect(barTwoX+offset,offsetY+0,barTwoX+1+offset,offsetY+digitBarHeight);
+ //g.drawString("0",offset+2,offsetY+textHeight+5);
+}
+
+function drawCheckBar(offsetX, offsetY) {
+ const barOneX = offsetX+2;
+ const barOneWidth = 1;
+ const barTwoX = offsetX+6;
+ const barTwoWidth = 1;
+ g.fillRect(barOneX,offsetY,barOneX+barOneWidth,offsetY+checkBarHeight);
+ g.fillRect(barTwoX,offsetY,barTwoX+barTwoWidth,offsetY+checkBarHeight);
+}
+
+function calculateChecksum(digits) {
+ let oddSum = digits[6] + digits[4] + digits[2] + digits[0];
+ let evenSum = digits[5] + digits[3] + digits[1];
+
+ let checkSum = (10 - ((3 * oddSum + evenSum) % 10)) % 10;
+
+ return checkSum;
+}
+
+function storeStepCount() {
+ stepCount = Bangle.getStepCount();
+ require("Storage").writeJSON("stepCount",stepCount);
+}
+
+function getStepCount() {
+ let accumulatedSteps = Bangle.getStepCount();
+ if(accumulatedSteps <= stepCount) {
+ return 0;
+ }
+ return accumulatedSteps - stepCount;
+}
+
+function resetAtMidnight() {
+ let now = new Date();
+ let night = new Date(
+ now.getFullYear(),
+ now.getMonth(),
+ now.getDate(), // the next day, ...
+ 23, 58, 0 // ...at 00:00:00 hours
+);
+ let msToMidnight = night.getTime() - now.getTime();
+
+ setTimeout(function() {
+ storeStepCount(); // <-- This is the function being called at midnight.
+ resetAtMidnight(); // Then, reset again next midnight.
+ }, msToMidnight);
+}
+
+resetAtMidnight();
+
+// The layout, referencing the custom renderer
+var Layout = require("Layout");
+var layout = new Layout( {
+ type:"v", c: [
+ {type:"custom", render:renderWatch, id:"watch", bgCol:g.theme.bg, fillx:1, filly:1 }
+ ]
+});
+
+// Clear the screen once, at startup
+g.clear();
+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/barcode.clock.png b/apps/barcode/barcode.clock.png
new file mode 100644
index 000000000..7d249cdeb
Binary files /dev/null and b/apps/barcode/barcode.clock.png differ
diff --git a/apps/barcode/barcode.icon.js b/apps/barcode/barcode.icon.js
new file mode 100644
index 000000000..969943e0e
--- /dev/null
+++ b/apps/barcode/barcode.icon.js
@@ -0,0 +1 @@
+E.toArrayBuffer(atob("MDAE///+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifrV6mlp+rXYiFXZr8k4q8r+if/////+if7+///u//79ie7/7//u///+if/////+ifnP6t+378r+ienvyf/K/7v+if/////+ifrfx6/I78j+ibe/+e/W75n+if/////+iee/+t/Z77f+iervyv/r/7v+if//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////"))
diff --git a/apps/barcode/barcode.icon.png b/apps/barcode/barcode.icon.png
new file mode 100644
index 000000000..43fa77a6f
Binary files /dev/null and b/apps/barcode/barcode.icon.png differ
diff --git a/apps/barcode/metadata.json b/apps/barcode/metadata.json
new file mode 100644
index 000000000..cef267b2b
--- /dev/null
+++ b/apps/barcode/metadata.json
@@ -0,0 +1,16 @@
+{ "id": "barcode",
+ "name": "Barcode clock",
+ "shortName":"Barcode clock",
+ "icon": "barcode.icon.png",
+ "version":"0.09",
+ "description": "EAN-8 compatible barcode clock.",
+ "tags": "barcode,ean,ean-8,watchface,clock,clockface",
+ "type": "clock",
+ "supports" : ["BANGLEJS2"],
+ "storage": [
+ {"name":"barcode.app.js","url":"barcode.app.js"},
+ {"name":"barcode.img","url":"barcode.icon.js","evaluate":true}
+ ],
+ "readme":"README.md",
+ "screenshots": [{"url":"barcode.clock.png"}]
+}
diff --git a/apps/bigdclock/ChangeLog b/apps/bigdclock/ChangeLog
new file mode 100644
index 000000000..09cc978fb
--- /dev/null
+++ b/apps/bigdclock/ChangeLog
@@ -0,0 +1,5 @@
+0.01: Initial version
+0.02: setTimeout bug fix; no leading zero on date; lightmode; 12 hour format; cleanup
+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()
diff --git a/apps/bigdclock/README.md b/apps/bigdclock/README.md
new file mode 100644
index 000000000..71f4362fa
--- /dev/null
+++ b/apps/bigdclock/README.md
@@ -0,0 +1,14 @@
+# Big Digit Clock
+
+There are a number of big digit clocks available for the Bangle, but this is
+the first which shows all the essential information that a clock needs to show
+in a manner that is easy to read by those with poor eyesight.
+
+The clock shows the time-of-day, the day-of-week and the day-of-month, as well
+as an easy-to-see icon showing the current charge on the battery.
+
+
+
+## Creator
+
+Created by [Deirdre O'Byrne](https://github.com/deirdreobyrne)
diff --git a/apps/bigdclock/bigdclock.app.js b/apps/bigdclock/bigdclock.app.js
new file mode 100644
index 000000000..c013c6188
--- /dev/null
+++ b/apps/bigdclock/bigdclock.app.js
@@ -0,0 +1,91 @@
+//
+
+Graphics.prototype.setFontOpenSans = function(scale) {
+ // Actual height 48 (50 - 3)
+ this.setFontCustom(
+ atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAP8AAAAAAAAB/wAAAAAAAAH/gAAAAAAAAf+AAAAAAAAB/4AAAAAAAAH/gAAAAAAAAf8AAAAAAAAA/wAAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAPAAAAAAAAAH8AAAAAAAAD/wAAAAAAAA//AAAAAAAAf/8AAAAAAAP//wAAAAAAH///AAAAAAB///4AAAAAA///8AAAAAAf///AAAAAAH///gAAAAAD///wAAAAAB///4AAAAAAf//+AAAAAAP///AAAAAAH///gAAAAAA///4AAAAAAD//8AAAAAAAP/+AAAAAAAA//gAAAAAAAD/wAAAAAAAAP4AAAAAAAAA8AAAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/gAAAAAAAP///AAAAAAH////gAAAAD/////gAAAAf/////gAAAH//////AAAA//////+AAAD//////8AAAf//////4AAD//4AH//gAAP/gAAAf/AAA/4AAAAf8AAH/AAAAA/4AAf4AAAAB/gAB/AAAAAD+AAH8AAAAAP4AAfwAAAAA/gAB/AAAAAD+AAH8AAAAAP4AAf4AAAAB/gAB/gAAAAH+AAD/gAAAB/wAAP/gAAAf/AAA//4AAf/8AAB///////gAAD//////8AAAH//////wAAAP/////+AAAAf/////wAAAA/////8AAAAAf////AAAAAAf///gAAAAAAB//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAAAAAAAHgAAAAAAAAA/AAAAAAAAAH+AAAAAAAAA/4AAAAAAAAD/AAAAAAAAAf8AAAAAAAAD/gAAAAAAAAf8AAAAAAAAB/gAAAAAAAAP8AAAAAAAAB/wAAAAAAAAP///////AAA///////8AAD///////wAAP///////AAA///////8AAD///////wAAP///////AAA///////8AAD///////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAD8AAAOAAAAAfwAAB+AAAAD/AAAH8AAAAf8AAA/wAAAH/wAAH/AAAA//AAAf4AAAH/8AAD/gAAA//wAAP8AAAH//AAA/gAAA//8AAD+AAAH//wAAf4AAA/9/AAB/AAAH/n8AAH8AAA/8fwAAfwAAH/h/AAB/AAA/8H8AAH8AAH/gfwAAfwAA/8B/AAB/gAH/gH8AAH+AB/8AfwAAP8Af/gB/AAA////8AH8AAD////gAfwAAH///8AB/AAAf///gAH8AAA///8AAfwAAB///gAB/AAAD//4AAH8AAAH/+AAAfwAAAH/gAAB/AAAAAAAAAH8AAAAAAAAAfwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAYAAAAB/gAAB4AAAAD+AAAPwAAAAP4AAA/gAAAAfwAAH+AAAAB/AAAf4AAAAH8AAD/AB+AAfwAAP8AH4AA/gAA/gAfgAD+AAH+AB+AAP4AAfwAH4AA/gAB/AAfgAD+AAH8AB+AAP4AAfwAH4AA/gAB/AA/wAD+AAH8AD/AAP4AAfwAP8AA/gAB/AA/wAD+AAH+AH/AAf4AAf4Af+AB/AAA/wH/8AP8AAD////4D/wAAP//+////AAAf//7///4AAB///P///gAAD//8f//8AAAP//h///gAAAf/8D//+AAAA//gH//wAAAA/4AP/8AAAAAAAAP/AAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAAAA/wAAAAAAAAH/AAAAAAAAB/8AAAAAAAAP/wAAAAAAAD//AAAAAAAAf/8AAAAAAAH//wAAAAAAA/+/AAAAAAAP/z8AAAAAAB/8PwAAAAAAf/g/AAAAAAD/4D8AAAAAAf/APwAAAAAH/4A/AAAAAA/+AD8AAAAAP/wAPwAAAAB/8AA/AAAAAf/gAD8AAAAD/4AAPwAAAA//AAA/AAAAD///////wAAP///////AAA///////8AAD///////wAAP///////AAA///////8AAD///////wAAP///////AAAAAAAA/AAAAAAAAAD8AAAAAAAAAPwAAAAAAAAA/AAAAAAAAAD8AAAAAAAAAPwAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAQAB/gAAAD//gAD+AAA////AAP8AAD///8AAfwAAP///wAB/AAA////AAH8AAD///8AAf4AAP///wAA/gAA////AAD+AAD/+H8AAP4AAP4AfwAA/gAA/gB+AAD+AAD+AH4AAP4AAP4AfwAA/gAA/gB/AAD+AAD+AH8AAP4AAP4AfwAB/gAA/gB/AAH+AAD+AH+AA/wAAP4Af8AD/AAA/gB/4A/8AAD+AD////gAAP4AP///+AAA/gAf///wAAD+AB////AAAP4AD///4AAA/gAH///AAAAAAAP//4AAAAAAAf/+AAAAAAAAf/gAAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf//AAAAAAAf///gAAAAAP////gAAAAD/////gAAAAf/////AAAAD/////+AAAA//////8AAAD//////4AAAf/8/4//gAAD/8H+Af/AAAP/AfgAf8AAB/wD+AA/wAAH+APwAB/gAA/wB/AAD+AAD+AH4AAP4AAP4AfgAA/gAB/gB+AAD+AAH8AH4AAP4AAfwAfgAA/gAB/AB/AAH+AAH8AH8AAf4AAfwAf4AD/AAB/AB/4A/8AAH8AH////wAAfwAP///+AAB/AA////4AAH8AB////AAAfwAD///4AAA/AAH///AAAAAAAP//4AAAAAAAP/+AAAAAAAAP/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP4AAAAAAAAA/gAAAAAAAAD+AAAAAAAAAP4AAAAAAAAA/gAAAAAAAAD+AAAAAAAAAP4AAAAADAAA/gAAAAB8AAD+AAAAAfwAAP4AAAAH/AAA/gAAAB/8AAD+AAAAf/wAAP4AAAP//AAA/gAAD//8AAD+AAA///wAAP4AAP//8AAA/gAD///AAAD+AB///wAAAP4Af//4AAAA/gH//+AAAAD+B///gAAAAP4f//wAAAAA/v//8AAAAAD////AAAAAAP///wAAAAAA///4AAAAAAD//+AAAAAAAP//gAAAAAAA//wAAAAAAAD/8AAAAAAAAP/AAAAAAAAA/wAAAAAAAAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAAAAAAAAH/wAAAAH8AB//gAAAB/8AP//gAAAf/8B//+AAAB//4P//8AAAP//w///4AAB///n///gAAH//+////AAA/////gf8AAD/h//4AfwAAP4B//AB/gAB/gD/4AD+AAH8AP/gAP4AAfwAf8AA/gAB/AA/wAB+AAH4AD/AAH4AAfwAP8AAfgAB/AB/4AD+AAH8AH/gAP4AAfwA//AA/gAA/gH/+AH+AAD/h//8AfwAAP//+/4D/AAAf//7///8AAB///H///gAAD//4P//+AAAP//g///wAAAf/8B//+AAAA//gD//wAAAA/4AH/+AAAAAAAAH/wAAAAAAAAH4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAAAD//AAAAAAAA///AAAAAAAH//+AAPwAAB///8AA/gAAP///4AD+AAA////wAP4AAH////AA/gAAf///8AD+AAD/4B/4AP4AAP+AB/gA/gAA/gAD+AD+AAH+AAP4AP4AAfwAAfgA/gAB/AAB+AD+AAH8AAH4AP4AAfwAAfgB/AAB/AAB+AH8AAH8AAH4A/wAAf4AA/AH+AAA/gAD8A/4AAD/gAfwH/gAAP/AD+B/8AAAf/g/w//gAAB//////+AAAD//////wAAAH/////+AAAAP/////wAAAAf////8AAAAA/////AAAAAA////wAAAAAAf//4AAAAAAAH/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAfgAAAAP8AAD/AAAAA/4AAf8AAAAH/gAB/4AAAAf+AAH/gAAAB/4AAf+AAAAH/gAB/4AAAAf+AAH/AAAAA/wAAP8AAAAB+AAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=='),
+ 46,
+ atob("EhklJSUlJSUlJSUlEg=="),
+ 64+(scale<<8)+(1<<16)
+ );
+};
+
+var drawTimeout;
+
+function queueDraw(millis_now) {
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = setTimeout(function () {
+ drawTimeout = undefined;
+ draw();
+ }, 60000 - (millis_now % 60000));
+}
+
+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 is12Hour = (require("Storage").readJSON("setting.json", 1) || {})["12hour"];
+ var dows = require("date_utils").dows(0,1);
+
+ g.reset();
+ g.clear();
+
+ g.setFontOpenSans();
+ g.setFontAlign(0, -1);
+ if (is12Hour) {
+ if (h > 12) h -= 12;
+ if (h == 0) h = 12;
+ g.drawString(h + ":" + ("0"+m).substr(-2), g.getWidth() / 2, 30);
+ } else {
+ g.drawString(("0"+h).substr(-2) + ":" + ("0"+m).substr(-2), g.getWidth() / 2, 30);
+ }
+ g.setFontAlign(1, -1);
+ 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.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);
+ } else {
+ g.setColor(1,0,0);
+ }
+ g.fillRect(12,162,12+width,168);
+ if (level < 100) {
+ g.setColor(g.theme.bg);
+ g.fillRect(12+width+1,162,162,168);
+ }
+
+ g.setColor(0, 1, 0);
+ g.fillRect(0, 90, g.getWidth(), 94);
+
+ // widget redraw
+ Bangle.drawWidgets();
+ queueDraw(date.getTime());
+}
+
+Bangle.on('lcdPower', on => {
+ if (on) {
+ draw(); // draw immediately, queue redraw
+ } else { // stop draw timer
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = undefined;
+ }
+});
+
+Bangle.on('charging', (charging) => {
+ draw();
+});
+
+Bangle.loadWidgets();
+draw();
+
+Bangle.setUI("clock");
diff --git a/apps/bigdclock/bigdclock.icon.js b/apps/bigdclock/bigdclock.icon.js
new file mode 100644
index 000000000..4aaecfa23
--- /dev/null
+++ b/apps/bigdclock/bigdclock.icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwgJC/AAMD4F4AgN4g/D/4FB/E/AoUH/F/AoOAh4FCz4FD4EPAoUHAoOHwAFDx/AAoUfAol/g4RD/w1Cg/B/AFD4fwn4XC4fg8/wAoPH//P7AFE9wFE8YFEEwcf4+BwAFBiACBAoUwAQPAAQMgAQNAArIjFF4sYgEBAoUIAoIRChi3B8AFBg8Ah/wAoIVBjH8ZAXguF+AoSDBn7WEh4FEg4"))
diff --git a/apps/bigdclock/bigdclock.png b/apps/bigdclock/bigdclock.png
new file mode 100644
index 000000000..4da1a9010
Binary files /dev/null and b/apps/bigdclock/bigdclock.png differ
diff --git a/apps/bigdclock/metadata.json b/apps/bigdclock/metadata.json
new file mode 100644
index 000000000..7359bcf20
--- /dev/null
+++ b/apps/bigdclock/metadata.json
@@ -0,0 +1,17 @@
+{ "id": "bigdclock",
+ "name": "Big digit clock containing just the essentials",
+ "shortName":"Big digit clk",
+ "version":"0.05",
+ "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",
+ "tags": "clock",
+ "allow_emulator":true,
+ "supports" : ["BANGLEJS2"],
+ "readme": "README.md",
+ "screenshots": [ { "url":"screenshot.png" } ],
+ "storage": [
+ {"name":"bigdclock.app.js","url":"bigdclock.app.js"},
+ {"name":"bigdclock.img","url":"bigdclock.icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/bigdclock/screenshot.png b/apps/bigdclock/screenshot.png
new file mode 100644
index 000000000..8a12b266e
Binary files /dev/null and b/apps/bigdclock/screenshot.png differ
diff --git a/apps/calendar/ChangeLog b/apps/calendar/ChangeLog
index ea8934f84..873f90de6 100644
--- a/apps/calendar/ChangeLog
+++ b/apps/calendar/ChangeLog
@@ -7,3 +7,4 @@
0.07: Fix off-by-one-error on previous month
0.08: Do not register as watch, manually start clock on button
read start of week from system settings
+0.09: Fix scope of let variables
diff --git a/apps/calendar/calendar.js b/apps/calendar/calendar.js
index fc7e93cf5..f4676fc22 100644
--- a/apps/calendar/calendar.js
+++ b/apps/calendar/calendar.js
@@ -16,6 +16,12 @@ const white = "#ffffff";
const red = "#d41706";
const blue = "#0000ff";
const yellow = "#ffff00";
+let bgColor = color4;
+let bgColorMonth = color1;
+let bgColorDow = color2;
+let bgColorWeekend = color3;
+let fgOtherMonth = gray1;
+let fgSameMonth = white;
let settings = require('Storage').readJSON("calendar.json", true) || {};
let startOnSun = ((require("Storage").readJSON("setting.json", true) || {}).firstDayOfWeek || 0) === 0;
@@ -27,19 +33,12 @@ if (settings.ndColors === undefined)
}
if (settings.ndColors === true) {
- let bgColor = white;
- let bgColorMonth = blue;
- let bgColorDow = black;
- let bgColorWeekend = yellow;
- let fgOtherMonth = blue;
- let fgSameMonth = black;
-} else {
- let bgColor = color4;
- let bgColorMonth = color1;
- let bgColorDow = color2;
- let bgColorWeekend = color3;
- let fgOtherMonth = gray1;
- let fgSameMonth = white;
+ bgColor = white;
+ bgColorMonth = blue;
+ bgColorDow = black;
+ bgColorWeekend = yellow;
+ fgOtherMonth = blue;
+ fgSameMonth = black;
}
function getDowLbls(locale) {
diff --git a/apps/calendar/metadata.json b/apps/calendar/metadata.json
index 5f968b364..65a54c097 100644
--- a/apps/calendar/metadata.json
+++ b/apps/calendar/metadata.json
@@ -1,7 +1,7 @@
{
"id": "calendar",
"name": "Calendar",
- "version": "0.08",
+ "version": "0.09",
"description": "Simple calendar",
"icon": "calendar.png",
"screenshots": [{"url":"screenshot_calendar.png"}],
diff --git a/apps/calibration/ChangeLog b/apps/calibration/ChangeLog
new file mode 100644
index 000000000..0e22605af
--- /dev/null
+++ b/apps/calibration/ChangeLog
@@ -0,0 +1,2 @@
+1.00: New App!
+1.01: Use fractional numbers and scale the points to keep working consistently on whole screen
diff --git a/apps/calibration/README.md b/apps/calibration/README.md
index 37f637d21..ed1a29d9e 100644
--- a/apps/calibration/README.md
+++ b/apps/calibration/README.md
@@ -6,6 +6,7 @@ A simple calibration app for the touchscreen
Once lauched touch the cross that appear on the screen to make
another spawn elsewhere.
-each new touch on the screen will help to calibrate the offset
-of your finger on the screen. After five or more input, press
-the button to save the calibration and close the application.
\ No newline at end of file
+Each new touch on the screen will help to calibrate the offset
+of your finger on the screen. After four or more inputs, press
+the button to save the calibration and close the application. Quality
+of the calibration gets better with every touch on a cross.
diff --git a/apps/calibration/app.js b/apps/calibration/app.js
index d3823de63..049430d45 100644
--- a/apps/calibration/app.js
+++ b/apps/calibration/app.js
@@ -1,40 +1,60 @@
class BanglejsApp {
constructor() {
+ this.maxSamples = 16;
+ this.target = {
+ xMin: Math.floor(0.1 * g.getWidth()),
+ xMax: Math.floor(0.9 * g.getWidth()),
+ yMin: Math.floor(0.1 * g.getHeight()),
+ yMax: Math.floor(0.9 * g.getHeight()),
+ };
this.x = 0;
this.y = 0;
+ this.step = 0;
this.settings = {
- xoffset: 0,
- yoffset: 0,
+ xoffset: [0],
+ yoffset: [0],
+ xMaxActual: [this.target.xMax],
+ yMaxActual: [this.target.yMax],
};
}
load_settings() {
let settings = require('Storage').readJSON('calibration.json', true) || {active: false};
- // do nothing if the calibration is deactivated
- if (settings.active === true) {
- // cancel the calibration offset
- Bangle.on('touch', function(button, xy) {
- xy.x += settings.xoffset;
- xy.y += settings.yoffset;
- });
- }
- if (!settings.xoffset) settings.xoffset = 0;
- if (!settings.yoffset) settings.yoffset = 0;
-
console.log('loaded settings:');
console.log(settings);
return settings;
}
- save_settings() {
- this.settings.active = true;
- this.settings.reload = false;
- require('Storage').writeJSON('calibration.json', this.settings);
+ getMedian(array){
+ array.sort();
+ let i = Math.floor(array.length/2);
+ if ( array.length % 2 && array.length > 1 ){
+ return (array[i]+array[i+1])/2;
+ } else {
+ return array[i];
+ }
+ }
- console.log('saved settings:');
- console.log(this.settings);
+ getMedianSettings(){
+ let medianSettings = {
+ xoffset: this.getMedian(this.settings.xoffset),
+ yoffset: this.getMedian(this.settings.yoffset)
+ };
+
+ medianSettings.xscale = this.target.xMax / (medianSettings.xoffset + this.getMedian(this.settings.xMaxActual));
+ medianSettings.yscale = this.target.yMax / (medianSettings.yoffset + this.getMedian(this.settings.yMaxActual));
+ return medianSettings;
+ }
+
+ save_settings() {
+ let settingsToSave = this.getMedianSettings();
+ settingsToSave.active = true;
+ settingsToSave.reload = false;
+ require('Storage').writeJSON('calibration.json', settingsToSave);
+
+ console.log('saved settings:', settingsToSave);
}
explain() {
@@ -46,29 +66,78 @@ class BanglejsApp {
}
drawTarget() {
- this.x = 16 + Math.floor(Math.random() * (g.getWidth() - 32));
- this.y = 40 + Math.floor(Math.random() * (g.getHeight() - 80));
+ switch (this.step){
+ case 0:
+ this.x = this.target.xMin;
+ this.y = this.target.yMin;
+ break;
+ case 1:
+ this.x = this.target.xMax;
+ this.y = this.target.yMin;
+ break;
+ case 2:
+ this.x = this.target.xMin;
+ this.y = this.target.yMax;
+ break;
+ case 3:
+ this.x = this.target.xMax;
+ this.y = this.target.yMax;
+ break;
+ }
- g.clearRect(0, 24, g.getWidth(), g.getHeight() - 24);
+ g.clearRect(0, 0, g.getWidth(), g.getHeight());
+ g.setColor(g.theme.fg);
g.drawLine(this.x, this.y - 5, this.x, this.y + 5);
g.drawLine(this.x - 5, this.y, this.x + 5, this.y);
g.setFont('Vector', 10);
- g.drawString('current offset: ' + this.settings.xoffset + ', ' + this.settings.yoffset, 0, 24);
+ let medianSettings = this.getMedianSettings();
+ g.drawString('current offset: ' + medianSettings.xoffset.toFixed(3) + ', ' + medianSettings.yoffset.toFixed(3), 2, (g.getHeight()/2)-6);
+ g.drawString('current scale: ' + medianSettings.xscale.toFixed(3) + ', ' + medianSettings.yscale.toFixed(3), 2, (g.getHeight()/2)+6);
}
setOffset(xy) {
- this.settings.xoffset = Math.round((this.settings.xoffset + (this.x - Math.floor((this.x + xy.x)/2)))/2);
- this.settings.yoffset = Math.round((this.settings.yoffset + (this.y - Math.floor((this.y + xy.y)/2)))/2);
+ switch (this.step){
+ case 0:
+ this.settings.xoffset.push(this.x - xy.x);
+ this.settings.yoffset.push(this.y - xy.y);
+ break;
+ case 1:
+ this.settings.xMaxActual.push(xy.x);
+ this.settings.yoffset.push(this.y - xy.y);
+ break;
+ case 2:
+ this.settings.xoffset.push(this.x - xy.x);
+ this.settings.yMaxActual.push(xy.y);
+ break;
+ case 3:
+ this.settings.xMaxActual.push(xy.x);
+ this.settings.yMaxActual.push(xy.y);
+ break;
+ }
+
+ for (let c in this.settings){
+ if (this.settings[c].length > this.maxSamples) this.settings[c] = this.settings[c].slice(1, this.maxSamples);
+ }
+ }
+
+ nextStep() {
+ this.step++;
+ if ( this.step == 4 ) this.step = 0;
}
}
E.srand(Date.now());
-Bangle.loadWidgets();
-Bangle.drawWidgets();
calibration = new BanglejsApp();
calibration.load_settings();
+Bangle.disableCalibration = true;
+
+function touchHandler (btn, xy){
+ if (xy) calibration.setOffset(xy);
+ calibration.nextStep();
+ calibration.drawTarget();
+}
let modes = {
mode : 'custom',
@@ -76,10 +145,7 @@ let modes = {
calibration.save_settings(this.settings);
load();
},
- touch : function(btn, xy) {
- calibration.setOffset(xy);
- calibration.drawTarget();
- },
+ touch : touchHandler,
};
Bangle.setUI(modes);
calibration.drawTarget();
diff --git a/apps/calibration/boot.js b/apps/calibration/boot.js
index 237fb2e0d..03b17a03a 100644
--- a/apps/calibration/boot.js
+++ b/apps/calibration/boot.js
@@ -1,7 +1,7 @@
let cal_settings = require('Storage').readJSON("calibration.json", true) || {active: false};
Bangle.on('touch', function(button, xy) {
// do nothing if the calibration is deactivated
- if (cal_settings.active === false) return;
+ if (cal_settings.active === false || Bangle.disableCalibration) return;
// reload the calibration offset at each touch event /!\ bad for the flash memory
if (cal_settings.reload === true) {
@@ -9,6 +9,6 @@ Bangle.on('touch', function(button, xy) {
}
// apply the calibration offset
- xy.x += cal_settings.xoffset;
- xy.y += cal_settings.yoffset;
+ xy.x = E.clip(Math.round((xy.x + (cal_settings.xoffset || 0)) * (cal_settings.xscale || 1)),0,g.getWidth());
+ xy.y = E.clip(Math.round((xy.y + (cal_settings.yoffset || 0)) * (cal_settings.yscale || 1)),0,g.getHeight());
});
diff --git a/apps/calibration/metadata.json b/apps/calibration/metadata.json
index 122a2c175..b7a719e1c 100644
--- a/apps/calibration/metadata.json
+++ b/apps/calibration/metadata.json
@@ -2,7 +2,7 @@
"name": "Touchscreen Calibration",
"shortName":"Calibration",
"icon": "calibration.png",
- "version":"1.00",
+ "version":"1.01",
"description": "A simple calibration app for the touchscreen",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
diff --git a/apps/cassioWatch/ChangeLog b/apps/cassioWatch/ChangeLog
new file mode 100644
index 000000000..419810021
--- /dev/null
+++ b/apps/cassioWatch/ChangeLog
@@ -0,0 +1,11 @@
+0.0: Main App.
+0.1: Performance Fixes.
+0.2: Correct Screen Clear and Draw.
+0.3: Minor Fixes.
+0.4: Clear Old Time on Screen Before Draw new Time
+0.5: Update Date and Time to use Native date Funcions
+0.6: Add Settings Page
+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
diff --git a/apps/cassioWatch/README.md b/apps/cassioWatch/README.md
new file mode 100644
index 000000000..aaeb3f122
--- /dev/null
+++ b/apps/cassioWatch/README.md
@@ -0,0 +1,11 @@
+# cassioWatch
+
+ 
+
+Clock with Space Cassio Watch Style.
+
+It displays current temperature,day,steps,battery.heartbeat and weather.
+
+
+**To-do**:
+Align and change size of some elements.
diff --git a/apps/cassioWatch/app.js b/apps/cassioWatch/app.js
new file mode 100644
index 000000000..6bbb9e823
--- /dev/null
+++ b/apps/cassioWatch/app.js
@@ -0,0 +1,175 @@
+const storage = require('Storage');
+
+require("Font6x12").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);
+}
+
+function getBackgroundImage() {
+ 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));
+}
+
+
+function clearIntervals() {
+ if (rocketInterval) clearInterval(rocketInterval);
+ rocketInterval = undefined;
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = undefined;
+}
+
+function drawClock() {
+ 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(), "%", 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";
+}
+
+
+function draw() {
+ queueDraw();
+
+ g.reset();
+ g.clear();
+ 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.drawString(getTemperature(), 155, 132);
+ g.drawString(Math.round(Bangle.getHealthStatus("last").bpm), 109, 98);
+ g.drawString(getSteps(), 158, 98);
+
+ g.setFontAlign(-1,-1);
+ drawClock();
+ drawRocket();
+ drawBattery();
+
+ // Hide widgets
+ for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
+}
+
+Bangle.on("lcdPower", (on) => {
+ if (on) {
+ draw();
+ } else {
+ clearIntervals();
+ }
+});
+
+
+Bangle.on("lock", (locked) => {
+ clearIntervals();
+ draw();
+ if (!locked) {
+ rocketInterval = setInterval(drawRocket, rocketSpeed);
+ }
+});
+
+
+// Load widgets, but don't show them
+Bangle.loadWidgets();
+Bangle.setUI("clock");
+
+g.reset();
+g.clear();
+draw();
\ No newline at end of file
diff --git a/apps/cassioWatch/app.png b/apps/cassioWatch/app.png
new file mode 100644
index 000000000..3f9bbb36e
Binary files /dev/null and b/apps/cassioWatch/app.png differ
diff --git a/apps/cassioWatch/icon.js b/apps/cassioWatch/icon.js
new file mode 100644
index 000000000..4e4428f88
--- /dev/null
+++ b/apps/cassioWatch/icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("lkswkGswAHtGIxGGBhATJAAYXNCYoWOFIwWNChQWKBYWYCxIqTxGJFgwnDnACBkUiCwuYFQo9ECYIAClAsJxIUElAUDwQWEyxAHBwITBmczmUiCwprHCgMjmUhiIYBA4JCGIAeCwQUBCYIXBiUyFggVCFQsziMjmdCkcxiZbBiJCEFQZUBmND93uolEochFgpWEIAUUCgIACp00iZVBzIVEAoJABmUeConu8cRiNEoMZNwIrCzEiiUxpwVF901IwNN6JuBtGJlAVBkchCg3umkSiMNCoIAEQIJWFkgCB8UYinUjIVFxEhKwszDAUU7tRCg2hilTH4xVB6kRzAUGBQIVECYVEorEDAAeBptBiUenw7BCYQACieCCosd6MYwUVBwNUAQYvBeYOJCYVoxVeoK5BAAq0C8MjiM4LALFCilNCAQpCN4lBmUoFQQVCxTlBiMieI3uqUoagIVDRAeCkclCgvlkciFYdmsxwDlESmTdE8lRmSCECosiFgMhqgUDkZABBwWYCoNpIIYXBmchiKLBkYqBNggVBNwIsECwMikQUBlEiBgWJCoRCEEQITDNIJrEIAQABZYOYnIWBFoQUGIAZCCTYYWCAAQUExIUDFggNDAA5AEFg4AIIAhvGEYU4ChgWHAAoUIWYpdFFRIWHChxEICZoWFFBIA=="))
diff --git a/apps/cassioWatch/metadata.json b/apps/cassioWatch/metadata.json
new file mode 100644
index 000000000..dabdc2c93
--- /dev/null
+++ b/apps/cassioWatch/metadata.json
@@ -0,0 +1,18 @@
+{
+ "id": "cassioWatch",
+ "name": "Cassio Watch",
+ "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",
+ "type": "clock",
+ "tags": "clock, weather, cassio, retro",
+ "supports": ["BANGLEJS2"],
+ "allow_emulator": true,
+ "readme": "README.md",
+ "storage": [
+ { "name": "cassioWatch.app.js", "url": "app.js" },
+ {"name":"cassioWatch.settings.js","url":"settings.js"},
+ { "name": "cassioWatch.img", "url": "icon.js", "evaluate": true }
+ ]
+}
diff --git a/apps/cassioWatch/screens/screen_day.png b/apps/cassioWatch/screens/screen_day.png
new file mode 100644
index 000000000..ba150b4f7
Binary files /dev/null and b/apps/cassioWatch/screens/screen_day.png differ
diff --git a/apps/cassioWatch/screens/screen_night.png b/apps/cassioWatch/screens/screen_night.png
new file mode 100644
index 000000000..4055e0943
Binary files /dev/null and b/apps/cassioWatch/screens/screen_night.png differ
diff --git a/apps/cassioWatch/settings.js b/apps/cassioWatch/settings.js
new file mode 100644
index 000000000..b07c6c58f
--- /dev/null
+++ b/apps/cassioWatch/settings.js
@@ -0,0 +1,24 @@
+(function(back) {
+ var FILE = "cassioWatch.settings.json";
+ var settings = Object.assign({
+ rocketSpeed: 700,
+ }, require('Storage').readJSON(FILE, true) || {});
+
+ function writeSettings() {
+ require('Storage').writeJSON(FILE, settings);
+ }
+
+
+ E.showMenu({
+ "" : { "title" : "Cassio Watch" },
+ "< Back" : () => back(),
+ 'Rocket Speed': {
+ value: 0|settings.rocketSpeed,
+ min: 100, max: 60000,
+ onchange: v => {
+ settings.rocketSpeed = v;
+ writeSettings();
+ }
+ },
+ });
+ })
\ No newline at end of file
diff --git a/apps/cogclock/15x32.png b/apps/cogclock/15x32.png
new file mode 100644
index 000000000..0af326e71
Binary files /dev/null and b/apps/cogclock/15x32.png differ
diff --git a/apps/cogclock/ChangeLog b/apps/cogclock/ChangeLog
new file mode 100644
index 000000000..f4bfe77a5
--- /dev/null
+++ b/apps/cogclock/ChangeLog
@@ -0,0 +1,3 @@
+0.01: New clock
+0.02: Use ClockFace library, add settings
+0.03: Use ClockFace_menu.addSettingsFile
diff --git a/apps/cogclock/app.js b/apps/cogclock/app.js
new file mode 100644
index 000000000..d24031684
--- /dev/null
+++ b/apps/cogclock/app.js
@@ -0,0 +1,111 @@
+Graphics.prototype.setFont15x32N = function() {
+ this.setFontCustom(atob(
+ // 15x32.png, converted using http://ebfc.mattbrailsford.com/
+ "/////oAAAAKAAAACgAAAAoAAAAKAAAACgf//AoEAAQKB//8CgAAAAoAAAAKAAAACgAAAAoAAAAL////+/wAB/oEAAQKBAAECgf//AoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAAC////AgAAAQIAAAH+/w///oEIAAKBCAACgQgAAoEIAAKBCAACgQg/AoEIIQKB+CECgAAhAoAAIQKAACECgAAhAoAAIQL//+H+/w/h/oEIIQKBCCECgQghAoEIIQKBCCECgQghAoEIIQKB+D8CgAAAAoAAAAKAAAACgAAAAoAAAAL////+///gAIAAIACAACAAgAAgAIAAIAD/+CAAAAggAAAIIAAACD/+//gAAoAAAAKAAAACgAAAAoAAAAL////+///h/oAAIQKAACECgAAhAoAAIQKAACECgfghAoEIIQKBCD8CgQgAAoEIAAKBCAACgQgAAoEIAAL/D//+/////oAAAAKAAAACgAAAAoAAAAKAAAACgfg/AoEIIQKBCD8CgQgAAoEIAAKBCAACgQgAAoEIAAL/D//+/wAAAIEAAACBAAAAgQAAAIEAAACBAAAAgQAAAIH///6AAAACgAAAAoAAAAKAAAACgAAAAoAAAAL////+/////oAAAAKAAAACgAAAAoAAAAKAAAACgfg/AoEIIQKB+D8CgAAAAoAAAAKAAAACgAAAAoAAAAL////+///h/oAAIQKAACECgAAhAoAAIQKAACECgfghAoEIIQKB+D8CgAAAAoAAAAKAAAACgAAAAoAAAAL////+"
+ ), "0".charCodeAt(0), 15, 32);
+};
+
+/**
+ * Add coordinates for nth tooth to vertices
+ * @param {array} poly Array to add points to
+ * @param {number} n Tooth number
+ */
+function addTooth(poly, n) {
+ const
+ tau = Math.PI*2, arc = tau/clock.teeth,
+ e = arc*clock.edge, p = arc*clock.point, s = (arc-(e+p))/2; // edge,point,slopes
+ const sin = Math.sin, cos = Math.cos,
+ x = clock.x, y = clock.y,
+ r2 = clock.r2, r3 = clock.r3;
+ let r = (n-1)*arc+e/2; // rads
+ poly.push(x+r2*sin(r), y-r2*cos(r));
+ r += s;
+ poly.push(x+r3*sin(r), y-r3*cos(r));
+ r += p;
+ poly.push(x+r3*sin(r), y-r3*cos(r));
+ r += s;
+ poly.push(x+r2*sin(r), y-r2*cos(r));
+}
+
+/**
+ * @param {number} n Tooth number to fill (1-based)
+ * @param col Fill color
+ */
+function fillTooth(n, col) {
+ if (!n) return; // easiest to check here
+ let poly = [];
+ addTooth(poly, n);
+ g.setColor(col).fillPoly(poly)
+ .setColor(g.theme.fg2).drawPoly(poly); // fillPoly colored over the outline
+}
+
+const ClockFace = require("ClockFace");
+const clock = new ClockFace({
+ precision: 1,
+ settingsFile: "cogclock.settings.json",
+ init: function() {
+ this.r1 = 84; // inner radius
+ this.r3 = Math.min(Bangle.appRect.w/2, Bangle.appRect.h/2); // outer radius
+ this.r2 = (this.r1*3+this.r3*2)/5;
+ this.teeth = 12;
+ this.edge = 0.45;
+ this.point = 0.35; // as fraction of arc
+ this.x = Bangle.appRect.x+Bangle.appRect.w/2;
+ this.y = Bangle.appRect.y+Bangle.appRect.h/2;
+ },
+ draw: function(d) {
+ const x = this.x, y = this.y;
+ g.setColor(g.theme.bg2).fillCircle(x, y, this.r2) // fill cog
+ .setColor(g.theme.bg).fillCircle(x, y, this.r1) // clear center
+ .setColor(g.theme.fg2).drawCircle(x, y, this.r1); // draw inner border
+ let poly = []; // complete teeth outline
+ for(let t = 1; t<=this.teeth; t++) {
+ fillTooth(t, g.theme.bg2);
+ addTooth(poly, t);
+ }
+ g.drawPoly(poly, true); // draw outer border
+ if (!this.showDate) {
+ // draw top/bottom rectangles (instead of year/date)
+ g.reset()
+ .fillRect(x-30, y-60, x+29, y-33).clearRect(x-28, y-58, x+27, y-33)
+ .fillRect(x-30, y+60, x+29, y+30).clearRect(x-28, y+58, x+27, y+30);
+ }
+ this.tooth = 0;
+ this.update(d, {s: 1, m: 1, h: 1, d: 1});
+ },
+ update: function(d, c) {
+ g.reset();
+ const pad2 = num => (num<10 ? "0" : "")+num,
+ year = d.getFullYear(),
+ date = pad2(d.getDate())+pad2(d.getMonth()),
+ time = pad2(d.getHours())+pad2(d.getMinutes()),
+ tooth = Math.round(d.getSeconds()/60*this.teeth);
+ const x = this.x, y = this.y;
+ if (c.m) {
+ g.setFont("15x32N:2").setFontAlign(0, 0) // center middle
+ .drawString(time, x, y, true);
+ }
+ if (this.showDate) {
+ if (c.d) {
+ g.setFont("15x32N").setFontAlign(0, -1) // center top
+ .drawString(year, x, y+32, true)
+ .setFont("15x32N").setFontAlign(0, 1) // center bottom
+ .drawString(date, x, y-32, true);
+ }
+ }
+
+ if (tooth!==this.tooth) {
+ if (tooth>this.tooth) {
+ for(let t = this.tooth; t<=tooth; t++) { // fill missing teeth
+ fillTooth(t, g.theme.fg2);
+ }
+ } else {
+ for(let t = this.tooth; t>tooth; t--) { // erase extraneous teeth
+ fillTooth(t, g.theme.bg2);
+ }
+ }
+ }
+ this.tooth = tooth;
+ }
+});
+clock.start();
diff --git a/apps/cogclock/icon.js b/apps/cogclock/icon.js
new file mode 100644
index 000000000..899cfc7c1
--- /dev/null
+++ b/apps/cogclock/icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwhC/ACcikQXpCQUCC4MgBAgqMCQIXEAgQXNBwIDCAggXOABAXLHwQAHMYQXmmczI5oiCBwUjCwIABmQXDEgJ0KCwMwCwMDDAgyGLYoWBgAXBgAYBMZIXEkYWBC4YYBGAh7FFwgHCC4YEBPRIwCFwYXFGAaqHC56oIIwgXFJAbUJLwpgHI4qPDIwpIFR4wWDLwa6BAAQHDVIYYCC/gYCC453MPIR3HU5gADd5bXHC4rvJMAYAECwJeCd5MjGAjVDC4ZHGNARIFGAgNDFw5IJUogwFC4gwBDAhGBBghIFBQhhBbYguEPAweCDAgACCwZACNg5LFXQYsIC5QAFdg4XcCxJHNBwYTEC6A+BJYQEEC5YYBMYhbCCxo0GCaIXbAHgA="))
\ No newline at end of file
diff --git a/apps/cogclock/icon.png b/apps/cogclock/icon.png
new file mode 100644
index 000000000..8520fcf5d
Binary files /dev/null and b/apps/cogclock/icon.png differ
diff --git a/apps/cogclock/metadata.json b/apps/cogclock/metadata.json
new file mode 100644
index 000000000..29000b589
--- /dev/null
+++ b/apps/cogclock/metadata.json
@@ -0,0 +1,20 @@
+{
+ "id": "cogclock",
+ "name": "Cog Clock",
+ "version": "0.03",
+ "description": "A cross-shaped clock inside a cog",
+ "icon": "icon.png",
+ "screenshots": [{"url":"screenshot.png"}],
+ "type": "clock",
+ "tags": "clock",
+ "supports": ["BANGLEJS"],
+ "allow_emulator": true,
+ "storage": [
+ {"name":"cogclock.app.js","url":"app.js"},
+ {"name":"cogclock.settings.js","url":"settings.js"},
+ {"name":"cogclock.img","url":"icon.js","evaluate":true}
+ ],
+ "data": [
+ {"name": "cogclock.settings.json"}
+ ]
+}
diff --git a/apps/cogclock/screenshot.png b/apps/cogclock/screenshot.png
new file mode 100644
index 000000000..f49709aef
Binary files /dev/null and b/apps/cogclock/screenshot.png differ
diff --git a/apps/cogclock/settings.js b/apps/cogclock/settings.js
new file mode 100644
index 000000000..a91b033d0
--- /dev/null
+++ b/apps/cogclock/settings.js
@@ -0,0 +1,10 @@
+(function(back) {
+ let menu = {
+ "": {"title": /*LANG*/"Cog Clock"},
+ /*LANG*/"< Back": back,
+ };
+ require("ClockFace_menu").addSettingsFile(menu, "cogclock.settings.json", [
+ "showDate", "loadWidgets"
+ ]);
+ E.showMenu(menu);
+});
diff --git a/apps/compass/ChangeLog b/apps/compass/ChangeLog
index d1adafc4c..deb1072f5 100644
--- a/apps/compass/ChangeLog
+++ b/apps/compass/ChangeLog
@@ -4,3 +4,4 @@
0.04: Fix for Bangle.js 2 and themes
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)
diff --git a/apps/compass/compass.js b/apps/compass/compass.js
index 4730111ac..dd398ffa6 100644
--- a/apps/compass/compass.js
+++ b/apps/compass/compass.js
@@ -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(m.heading),M,y,true);
+ g.drawString(Math.round(360-m.heading),M,y,true);
}
diff --git a/apps/compass/metadata.json b/apps/compass/metadata.json
index 3e3b37f72..a3995a123 100644
--- a/apps/compass/metadata.json
+++ b/apps/compass/metadata.json
@@ -1,7 +1,7 @@
{
"id": "compass",
"name": "Compass",
- "version": "0.06",
+ "version": "0.07",
"description": "Simple compass that points North",
"icon": "compass.png",
"screenshots": [{"url":"screenshot_compass.png"}],
diff --git a/apps/dtlaunch/ChangeLog b/apps/dtlaunch/ChangeLog
index 09804b82e..a360e4875 100644
--- a/apps/dtlaunch/ChangeLog
+++ b/apps/dtlaunch/ChangeLog
@@ -11,4 +11,5 @@
0.11: Fix bangle.js 1 white icons not displaying
0.12: On Bangle 2 change to swiping up/down to move between pages as to match page indicator. Swiping from left to right now loads the clock.
0.13: Added swipeExit setting so that left-right to exit is an option
-0.14: Don't move pages when doing exit swipe.
+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.
diff --git a/apps/dtlaunch/README.md b/apps/dtlaunch/README.md
index 55c9f53b8..1835bc842 100644
--- a/apps/dtlaunch/README.md
+++ b/apps/dtlaunch/README.md
@@ -27,7 +27,7 @@ Bangle 2:
## Controls- Bangle 2
-**Touch** - icon to select, scond touch launches app
+**Touch** - icon to select, second touch launches app
**Swipe Left/Up** - move to next page of app icons
diff --git a/apps/dtlaunch/app-b2.js b/apps/dtlaunch/app-b2.js
index 46194ec5d..8cd5790bb 100644
--- a/apps/dtlaunch/app-b2.js
+++ b/apps/dtlaunch/app-b2.js
@@ -89,7 +89,7 @@ function drawPage(p){
Bangle.on("swipe",(dirLeftRight, dirUpDown)=>{
selected = 0;
oldselected=-1;
- if(settings.swipeExit && dirLeftRight==1) showClock();
+ if(settings.swipeExit && dirLeftRight==1) load();
if (dirUpDown==-1||dirLeftRight==-1){
++page; if (page>maxPage) page=0;
drawPage(page);
@@ -99,12 +99,6 @@ Bangle.on("swipe",(dirLeftRight, dirUpDown)=>{
}
});
-function showClock(){
- var app = require("Storage").readJSON('setting.json', 1).clock;
- if (app) load(app);
- else E.showMessage("clock\nnot found");
-}
-
function isTouched(p,n){
if (n<0 || n>3) return false;
var x1 = (n%2)*72+XOFF; var y1 = n>1?72+YOFF:YOFF;
diff --git a/apps/dtlaunch/metadata.json b/apps/dtlaunch/metadata.json
index 4a0b8067c..9711a6964 100644
--- a/apps/dtlaunch/metadata.json
+++ b/apps/dtlaunch/metadata.json
@@ -1,7 +1,7 @@
{
"id": "dtlaunch",
"name": "Desktop Launcher",
- "version": "0.14",
+ "version": "0.15",
"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/espruinoctrl/metadata.json b/apps/espruinoctrl/metadata.json
index 5798c7842..253307fa0 100644
--- a/apps/espruinoctrl/metadata.json
+++ b/apps/espruinoctrl/metadata.json
@@ -6,7 +6,7 @@
"description": "Send commands to other Espruino devices via the Bluetooth UART interface. Customisable commands!",
"icon": "app.png",
"tags": "",
- "supports": ["BANGLEJS"],
+ "supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"custom": "custom.html",
"storage": [
diff --git a/apps/ffcniftya/ChangeLog b/apps/ffcniftya/ChangeLog
index cb520193b..6d2f50119 100644
--- a/apps/ffcniftya/ChangeLog
+++ b/apps/ffcniftya/ChangeLog
@@ -2,3 +2,5 @@
0.02: Shows the current week number (ISO8601), can be disabled via settings
0.03: Call setUI before loading widgets
Improve settings page
+0.04: Use ClockFace library
+
diff --git a/apps/ffcniftya/app.js b/apps/ffcniftya/app.js
index 4000a1578..2c1a54f6e 100644
--- a/apps/ffcniftya/app.js
+++ b/apps/ffcniftya/app.js
@@ -1,22 +1,3 @@
-const locale = require("locale");
-const is12Hour = Object.assign({ "12hour": false }, require("Storage").readJSON("setting.json", true))["12hour"];
-const showWeekNum = Object.assign({ showWeekNum: true }, require('Storage').readJSON("ffcniftya.json", true))["showWeekNum"];
-
-/* Clock *********************************************/
-const scale = g.getWidth() / 176;
-
-const widget = 24;
-
-const viewport = {
- width: g.getWidth(),
- height: g.getHeight(),
-}
-
-const center = {
- x: viewport.width / 2,
- y: Math.round(((viewport.height - widget) / 2) + widget),
-}
-
// copied from: https://gist.github.com/IamSilviu/5899269#gistcomment-3035480
function ISO8601_week_no(date) {
var tdt = new Date(date.valueOf());
@@ -30,77 +11,49 @@ function ISO8601_week_no(date) {
return 1 + Math.ceil((firstThursday - tdt) / 604800000);
}
-function d02(value) {
- return ('0' + value).substr(-2);
+function format(value) {
+ return ("0" + value).substr(-2);
}
-function draw() {
- g.reset();
- g.clearRect(0, widget, viewport.width, viewport.height);
- const now = new Date();
+const ClockFace = require("ClockFace");
+const clock = new ClockFace({
+ init: function () {
+ const appRect = Bangle.appRect;
- const hour = d02(now.getHours() - (is12Hour && now.getHours() > 12 ? 12 : 0));
- const minutes = d02(now.getMinutes());
- const day = d02(now.getDate());
- const month = d02(now.getMonth() + 1);
- const year = now.getFullYear(now);
- const weekNum = d02(ISO8601_week_no(now));
- const monthName = locale.month(now, 3);
- const dayName = locale.dow(now, 3);
+ this.viewport = {
+ width: appRect.w,
+ height: appRect.h
+ };
- const centerTimeScaleX = center.x + 32 * scale;
- g.setFontAlign(1, 0).setFont("Vector", 90 * scale);
- g.drawString(hour, centerTimeScaleX, center.y - 31 * scale);
- g.drawString(minutes, centerTimeScaleX, center.y + 46 * scale);
+ this.center = {
+ x: this.viewport.width / 2,
+ y: Math.round((this.viewport.height / 2) + appRect.y)
+ };
- g.fillRect(center.x + 30 * scale, center.y - 72 * scale, center.x + 32 * scale, center.y + 74 * scale);
+ this.scale = g.getWidth() / this.viewport.width;
+ this.centerTimeScaleX = this.center.x + 32 * this.scale;
+ this.centerDatesScaleX = this.center.x + 40 * this.scale;
+ },
+ draw: function (date) {
+ const hour = date.getHours() - (this.is12Hour && date.getHours() > 12 ? 12 : 0);
+ const month = date.getMonth() + 1;
+ const monthName = require("date_utils").month(month, 1);
+ const dayName = require("date_utils").dow(date.getDay(), 1);
- const centerDatesScaleX = center.x + 40 * scale;
- g.setFontAlign(-1, 0).setFont("Vector", 16 * scale);
- g.drawString(year, centerDatesScaleX, center.y - 62 * scale);
- g.drawString(month, centerDatesScaleX, center.y - 44 * scale);
- g.drawString(day, centerDatesScaleX, center.y - 26 * scale);
- if (showWeekNum) g.drawString(weekNum, centerDatesScaleX, center.y + 15 * scale);
- g.drawString(monthName, centerDatesScaleX, center.y + 48 * scale);
- g.drawString(dayName, centerDatesScaleX, center.y + 66 * scale);
-}
+ g.setFontAlign(1, 0).setFont("Vector", 90 * this.scale);
+ g.drawString(format(hour), this.centerTimeScaleX, this.center.y - 31 * this.scale);
+ g.drawString(format(date.getMinutes()), this.centerTimeScaleX, this.center.y + 46 * this.scale);
+ g.fillRect(this.center.x + 30 * this.scale, this.center.y - 72 * this.scale, this.center.x + 32 * this.scale, this.center.y + 74 * this.scale);
-/* Minute Ticker *************************************/
-
-let tickTimer;
-
-function clearTickTimer() {
- if (tickTimer) {
- clearTimeout(tickTimer);
- tickTimer = undefined;
- }
-}
-
-function queueNextTick() {
- clearTickTimer();
- tickTimer = setTimeout(tick, 60000 - (Date.now() % 60000));
-}
-
-function tick() {
- draw();
- queueNextTick();
-}
-
-/* Init **********************************************/
-
-// Clear the screen once, at startup
-g.clear();
-tick();
-
-Bangle.on('lcdPower', (on) => {
- if (on) {
- tick();
- } else {
- clearTickTimer();
- }
+ g.setFontAlign(-1, 0).setFont("Vector", 16 * this.scale);
+ g.drawString(date.getFullYear(date), this.centerDatesScaleX, this.center.y - 62 * this.scale);
+ g.drawString(format(month), this.centerDatesScaleX, this.center.y - 44 * this.scale);
+ g.drawString(format(date.getDate()), this.centerDatesScaleX, this.center.y - 26 * this.scale);
+ if (this.showWeekNum) g.drawString(format(ISO8601_week_no(date)), this.centerDatesScaleX, this.center.y + 15 * this.scale);
+ g.drawString(monthName, this.centerDatesScaleX, this.center.y + 48 * this.scale);
+ g.drawString(dayName, this.centerDatesScaleX, this.center.y + 66 * this.scale);
+ },
+ settingsFile: "ffcniftya.json"
});
-
-Bangle.setUI("clock");
-Bangle.loadWidgets();
-Bangle.drawWidgets();
+clock.start();
\ No newline at end of file
diff --git a/apps/ffcniftya/metadata.json b/apps/ffcniftya/metadata.json
index 91b426cd0..015c56119 100644
--- a/apps/ffcniftya/metadata.json
+++ b/apps/ffcniftya/metadata.json
@@ -1,7 +1,7 @@
{
"id": "ffcniftya",
"name": "Nifty-A Clock",
- "version": "0.03",
+ "version": "0.04",
"description": "A nifty clock with time and date",
"icon": "app.png",
"screenshots": [{"url":"screenshot_nifty.png"}],
diff --git a/apps/ffcniftyb/ChangeLog b/apps/ffcniftyb/ChangeLog
index 9fc7e3c5c..83b11eb78 100644
--- a/apps/ffcniftyb/ChangeLog
+++ b/apps/ffcniftyb/ChangeLog
@@ -3,3 +3,4 @@
0.03: Call setUI before loading widgets
Fix bug with black being unselectable
Improve settings page
+0.04: Use ClockFace library
diff --git a/apps/ffcniftyb/app.js b/apps/ffcniftyb/app.js
index 65c74dbd7..540924fa5 100644
--- a/apps/ffcniftyb/app.js
+++ b/apps/ffcniftyb/app.js
@@ -1,20 +1,10 @@
-const is12Hour = Object.assign({ "12hour": false }, require("Storage").readJSON("setting.json", true))["12hour"];
-const color = Object.assign({ color: 63488 }, require("Storage").readJSON("ffcniftyb.json", true)).color; // Default to RED
+var scale;
+var screen;
+var center;
+var buf;
+var img;
-/* Clock *********************************************/
-const scale = g.getWidth() / 176;
-
-const screen = {
- width: g.getWidth(),
- height: g.getHeight() - 24,
-};
-
-const center = {
- x: screen.width / 2,
- y: screen.height / 2,
-};
-
-function d02(value) {
+function format(value) {
return ("0" + value).substr(-2);
}
@@ -22,91 +12,69 @@ function renderEllipse(g) {
g.fillEllipse(center.x - 5 * scale, center.y - 70 * scale, center.x + 160 * scale, center.y + 90 * scale);
}
-function renderText(g) {
- const now = new Date();
+function renderText(g, date) {
+ const hour = date.getHours() - (this.is12Hour && date.getHours() > 12 ? 12 : 0);
+ const month = date.getMonth() + 1;
- const hour = d02(now.getHours() - (is12Hour && now.getHours() > 12 ? 12 : 0));
- const minutes = d02(now.getMinutes());
- const day = d02(now.getDate());
- const month = d02(now.getMonth() + 1);
- const year = now.getFullYear();
-
- const month2 = require("locale").month(now, 3);
- const day2 = require("locale").dow(now, 3);
+ const monthName = require("date_utils").month(month, 1);
+ const dayName = require("date_utils").dow(date.getDay(), 1);
g.setFontAlign(1, 0).setFont("Vector", 90 * scale);
- g.drawString(hour, center.x + 32 * scale, center.y - 31 * scale);
- g.drawString(minutes, center.x + 32 * scale, center.y + 46 * scale);
+ g.drawString(format(hour), center.x + 32 * scale, center.y - 31 * scale);
+ g.drawString(format(date.getMinutes()), center.x + 32 * scale, center.y + 46 * scale);
g.setFontAlign(1, 0).setFont("Vector", 16 * scale);
- g.drawString(year, center.x + 80 * scale, center.y - 42 * scale);
- g.drawString(month, center.x + 80 * scale, center.y - 26 * scale);
- g.drawString(day, center.x + 80 * scale, center.y - 10 * scale);
- g.drawString(month2, center.x + 80 * scale, center.y + 44 * scale);
- g.drawString(day2, center.x + 80 * scale, center.y + 60 * scale);
+ g.drawString(date.getFullYear(), center.x + 80 * scale, center.y - 42 * scale);
+ g.drawString(format(month), center.x + 80 * scale, center.y - 26 * scale);
+ g.drawString(format(date.getDate()), center.x + 80 * scale, center.y - 10 * scale);
+ g.drawString(monthName, center.x + 80 * scale, center.y + 44 * scale);
+ g.drawString(dayName, center.x + 80 * scale, center.y + 60 * scale);
}
-const buf = Graphics.createArrayBuffer(screen.width, screen.height, 1, {
- msb: true
+const ClockFace = require("ClockFace");
+const clock = new ClockFace({
+ init: function () {
+ const appRect = Bangle.appRect;
+
+ screen = {
+ width: appRect.w,
+ height: appRect.h
+ };
+
+ center = {
+ x: screen.width / 2,
+ y: screen.height / 2
+ };
+
+ buf = Graphics.createArrayBuffer(screen.width, screen.height, 1, { msb: true });
+
+ scale = g.getWidth() / screen.width;
+
+ img = {
+ width: screen.width,
+ height: screen.height,
+ transparent: 0,
+ bpp: 1,
+ buffer: buf.buffer
+ };
+
+ // default to RED (see settings.js)
+ // don't use || to default because 0 is a valid color
+ this.color = this.color === undefined ? 63488 : this.color;
+ },
+ draw: function (date) {
+ // render outside text with ellipse
+ buf.clear();
+ renderText(buf.setColor(1), date);
+ renderEllipse(buf.setColor(0));
+ g.setColor(this.color).drawImage(img, 0, 24);
+
+ // render ellipse with inside text
+ buf.clear();
+ renderEllipse(buf.setColor(1));
+ renderText(buf.setColor(0), date);
+ g.setColor(this.color).drawImage(img, 0, 24);
+ },
+ settingsFile: "ffcniftyb.json"
});
-
-function draw() {
-
- const img = {
- width: screen.width,
- height: screen.height,
- transparent: 0,
- bpp: 1,
- buffer: buf.buffer
- };
-
- // cleat screen area
- g.clearRect(0, 24, g.getWidth(), g.getHeight());
-
- // render outside text with ellipse
- buf.clear();
- renderText(buf.setColor(1));
- renderEllipse(buf.setColor(0));
- g.setColor(color).drawImage(img, 0, 24);
-
- // render ellipse with inside text
- buf.clear();
- renderEllipse(buf.setColor(1));
- renderText(buf.setColor(0));
- g.setColor(color).drawImage(img, 0, 24);
-}
-
-
-/* Minute Ticker *************************************/
-
-let ticker;
-
-function stopTick() {
- if (ticker) {
- clearTimeout(ticker);
- ticker = undefined;
- }
-}
-
-function startTick(run) {
- stopTick();
- run();
- ticker = setTimeout(() => startTick(run), 60000 - (Date.now() % 60000));
-}
-
-/* Init **********************************************/
-
-g.clear();
-startTick(draw);
-
-Bangle.on("lcdPower", (on) => {
- if (on) {
- startTick(draw);
- } else {
- stopTick();
- }
-});
-
-Bangle.setUI("clock");
-Bangle.loadWidgets();
-Bangle.drawWidgets();
+clock.start();
\ No newline at end of file
diff --git a/apps/ffcniftyb/metadata.json b/apps/ffcniftyb/metadata.json
index 3d26c27ea..019ae6eb3 100644
--- a/apps/ffcniftyb/metadata.json
+++ b/apps/ffcniftyb/metadata.json
@@ -1,7 +1,7 @@
{
"id": "ffcniftyb",
"name": "Nifty-B Clock",
- "version": "0.03",
+ "version": "0.04",
"description": "A nifty clock (series B) with time, date and colour configuration",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
diff --git a/apps/football/ChangeLog b/apps/football/ChangeLog
new file mode 100644
index 000000000..66b9882cc
--- /dev/null
+++ b/apps/football/ChangeLog
@@ -0,0 +1,2 @@
+1.00: Initial implementation
+1.01: Bug fixes and performance and visual improvements
diff --git a/apps/football/README.md b/apps/football/README.md
new file mode 100644
index 000000000..f751b927e
--- /dev/null
+++ b/apps/football/README.md
@@ -0,0 +1,3 @@
+# Classic Football Chronometer Game
+
+Context: https://www.anaitgames.com/analisis/analisis-casio-football-14
diff --git a/apps/football/app-icon.js b/apps/football/app-icon.js
new file mode 100644
index 000000000..7eec578c6
--- /dev/null
+++ b/apps/football/app-icon.js
@@ -0,0 +1 @@
+atob("MDABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAAAA/wAAAAAA/wAAAAAA/wAAAAAA/wAAAAAA/wAAAAAA/wAAAAAA/wAAAAAADwAAAAAADwAAAAAADwAAAAAADwAAAAAA//AAAAAA//AAAAAA//AAAAAA//AAAAAADw8AD/AADw8AD/AADw8AD/AADw8AD/AP/wAAD/AP/wAAD/AP/wAAD/AP/wAAD/AA8PAAAAAA8PAAAAAA8PAAAAAA8PAAAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
diff --git a/apps/football/app.js b/apps/football/app.js
new file mode 100644
index 000000000..d12f07e2b
--- /dev/null
+++ b/apps/football/app.js
@@ -0,0 +1,474 @@
+// globals. TODO: move into an object
+const digit = [];
+let part = 0;
+let endInc = 0;
+var endGame = false;
+let goalFrame = 0;
+var stopped = true;
+let score0 = 0;
+let score1 = 0;
+let inc = 0;
+let msinc = 0;
+let seq0 = 0;
+let seq1 = 0;
+let goaler = -1;
+const w = g.getWidth();
+let owner = -1;
+
+const dash = {
+ width: 75,
+ height: 128,
+ bpp: 1,
+ buffer: require('heatshrink').decompress(atob('AH4A/AH4A/AH4A/AH4A/AB0D/4AB+AJEBAX/BAk/CQ8PCQ4kDCQoIDCQgkDCQgkECQgIE4ASHFxH8JRgSEEgYSEPJAkEAH4A/AH4A/AH4A/AH4A/ACg='))
+};
+
+function loadDigits () {
+ digit[0] = {
+ width: 80,
+ height: 128,
+ bpp: 1,
+ buffer: require('heatshrink').decompress(atob('AH4AGn//AAngBIMfBIvABIMPBIuABIMHBIoIBg0DBAn+gYSBgIJE/kHBIOABIn4h4JB4F/BIfwj4JB8BQEAoIJBBoJOEv4JBEIJOEIwMHGoIJDIIIJBJIJOEBIQOCJwYJDOIR9DBISFCSIYJCTISlDBIXwBIZoBBP4J/BP4J/BNX+gED//gBIc/BIMB//ABIcf/gDB/+ABIcP/AhCBAYuBFoU+BIkDFoUcBIkBFoUIBIkAFogA/AAZPJMZJ3JRZKfIWZLHJbZL5bBP4J/BP4J/BKPgBIc/BIfABIcfBIeA/4AB/EPBIcBBIX8AwIJB/0DBIQECBIIOCAAQYBBIIiCAAQsBBIPwGwIAC4F/BIPgJQIACAoIJBBoIJDDIIJBJwZQDBIJODKAcAgxODKAZxBJwgABPYROEKASFDAAiRCJwhQCTYYAkA'))
+ };
+
+ digit[1] = {
+ width: 80,
+ height: 128,
+ bpp: 1,
+ buffer: require('heatshrink').decompress(atob('AH4A2wAIHgIJIgYJIg4JIh4JIj4JIn4JIv4JI/4JHgIJIgYJIg4JIh4JIj4JIn4J/BP4J/BP4Jqj//BA0Ah/+BI8H/gJHgf4BI8B+AJHgHgBJFABJAA/J55jIO5KLJT5KzJY5L5nBP4J/BP4J/AAcfBJEPBJEHBJEDBJEBBJEABJN/BJE/BJEfBJEPBJEHBJEDBJEBBJEABJIA/AAwA='))
+ };
+
+ digit[2] = {
+ width: 80,
+ height: 128,
+ bpp: 1,
+ buffer: require('heatshrink').decompress(atob('AH4AGj//AAnwBIMPBIvgBIMHBIvABIMDBIuABIMBBIsBGQQIE/0DBIV/BIf8g4JCn4JD/EPKA/wj4JCKAngn4JCKAnAv4JCKAmA/4JCKAgEBQYZOEBIgADFgIJHIAKlJBI5oBBP4J/BP4J/BOcfBJEP/wJHg/8Aof/AAP+gf4BAUBBIX/gPwBIUDBIeA8AiDBIfAoA2DBIYSDJQQACEwZeCAAQ6DgF/BJATJE5I7IghPFBIUOMYomDO4g6EwCLDJwgiDAAhyFTohKEToheEBP4J/BP4J/BOHwBJHgBJHAv////8BImABAP//wJEAIIACBIf+BImABIX8g4JD4AJC/EPBIZACgfwj4JDKgUD8E/BIZoCgZODKAkDJwZQEgcBBIhQCgROEKAhOEKAhOEKAhOEKAgAm'))
+ };
+
+ digit[3] = {
+ width: 80,
+ height: 128,
+ bpp: 1,
+ buffer: require('heatshrink').decompress(atob('AH4AGj//AAnwBIMPBIvgBIMHBIvABIMDBIuABIMBBIsBGQQIE/0DBIV/BIf8g4JCn4JD/EPKA/wj4JCKAngn4JCKAnAv4JCKAmA/4JCKAgEBQYZOEBIgADFgIJHIAKlJBI5oBBI58BBP4J/BP4J/BL8/BJEf/wJHh/8BI8H/AFD/4AB/0D+AICgIJDgPgBIUDBIQ5B4AiDBIeAwA2DBIYSDJQIJDEwZeCAAQ6DOQQACJwgTJE5I7JJ5JjEgIUDO4kDFAgJC/kDIwipNj4JIn7HIbZL5TBP4J/BP4J/BJs/BJEfBJEPBIgjB//8g4JDgIIB//+gYJDAgIACBwIJCDAIACwAJDFgIAC4F/IAgAC8E/KggAC+EfIgoAB/EPBIQIDKAROFKAZOGKAROGKAQJI4BOGKAQJI+CfHAEAA=='))
+ };
+ digit[4] = {
+ width: 80,
+ height: 128,
+ bpp: 1,
+ buffer: require('heatshrink').decompress(atob('AH4AswEBBJOABAoHBgPABIsDBIPgBIsHBIPwBIsPBIP4BIsfBIP8BIs/BIP+BIt/BIP/BIv/BIRQEAwQCBKAkDBIZQEg4JDKAkPBIZQEj4JIn4J/BP4J/BP4JjgAJFj//AYN/8AJDh/+C4QJEg/8C4XAv////+gYjCh+ABAIABgPwC4Q9BAAWAEYUCgYJD4FAFgYJDIAoJDEwRUDAARoGAAROCCZYnJHZJPJMZAABO46hCRYwAFT4YAFWYYAFY4YAFbYYJ/BP4J/BP4Jnj4JIh4JIg4JIgYJIgIJIgAJJv4JIn4JIj4JIh4JIg4JIgYJIgIJIgAJJAH4AGA='))
+ };
+
+ digit[5] = {
+ width: 80,
+ height: 128,
+ bpp: 1,
+ buffer: require('heatshrink').decompress(atob('AGUP/4AE/gJBg4JF/AJBgYJF+AJBgIJF8AJBwAJF4AJB4F/BImABIPgn4JEIoXwj4ID/wJC/BQEJwUA/hQEJwUA/xQEJwUA/5QEJwQJBKAhOCBIJQEJwQJBKAiVDFggAEIAgJFKgYJFNAYJ/BP4J/BP4Jmv/8BI8//AJHj/wBI8P8E//4ABBIcH4F/BIWABIUDwAIC//ABIUBgIJD8AeDgYJDGwkHBIZKEh4JDLwkfBIZyECZInJHZJPFkChEMYdwUIh3DFAiLDgKvIgbDIJQKvIUIgJFUIZ8FBP4J/BP4J/BL8PBJEHBJEDBJEBBIl//4ABwAJEBAQHBv4JCDAIAC8E/BIQsBAAXwj4JCIAIAC/EPBIRUBAAX8g4JCNAIAC/0DBI//gIJCJwZQCBIQIEKANAJwpQCJwxQCJwxQCJwxQCBJYAwA='))
+ };
+
+ digit[6] = {
+ width: 80,
+ height: 128,
+ bpp: 1,
+ buffer: require('heatshrink').decompress(atob('AGU//4AE8AJBj4JF4AJBh4JFwAJBg4JFBAMGgYIE/wSCgIJE/gJCwAJE/AJC4F/BIfwBIXgKAhOCg/wKAhOCg/4KAhOD/hQEOwUH/xQDJwRiCKAZOCBIRQDJwQJCGwQAEBIJKCBIxeCBP4J/BP4J/BOED//gBI0B//ABI0A/+ABI9/CoIAB/gJDnwpBAAP+BIccHoIACBIcIh4JDFgkfBIZAEBIhUEv4JDNAgJE/ATNn4nIHZBPGKARjFgIUCO4sDFASLFg48COQsPKARyGUILHGn6hBBIJyGco4J/BP4J/BP4Jm8AJDn4JD4AJDj4JDwAJDh4JDgP/AAP8AwIJB/0DBIQECBIIOCAAQYBBIP4EQIACwAJC+A2BAAXAv4JB8BKBAAQFBBIINBBIYZBBIIhBAAYtBBIJODKAcAgxODKAZnBJwhQCOIYAEPoROEKASZDAAilCJwhQCTYYAuA=='))
+ };
+
+ digit[7] = {
+ width: 80,
+ height: 128,
+ bpp: 4,
+ buffer : require("heatshrink").decompress(atob("AH4A/AEtVADdQE5Nf/4AayAnJgoma/J4LKDR2KKDZODUMadChKhiJwefE5RQXJwbxLKCxOEE5hQVJwgnNKCZOFE5pQTJwonOKCJOGE5xQRD4Q8EE5xQPJw4nPgFZzIAMqCdFE6IARJwgnhJwonhJwonhe5In/E/4n/E/4n/E/4n/E/4n/E/4n/E/4n/E/4n/E/4n/E/4nlr4mE/NQE78FE4n1Ez5QGE0JQEJ0RQETsBQFJ0gABrJOkAH4A/AH4A/ADNZqAmkgv/yAnkr///JQjJwIABypOkAAP5J0oABUMJODKAShgEwhQiE/4n/E/4n/E/4n/E/4n/E/4n/E/4n/E/4n/E/4n/E/4n/AA+fE80JE8xQGE8JQFE8JQFE8RQEE8RQEE8ZQDE8ZQDE8hQCE8hQCE8pQBE8pQBE80JE80AE84A/AH4A/AH4A/AAQA=="))
+ };
+
+ digit[8] = {
+ width: 80,
+ height: 128,
+ bpp: 1,
+ buffer: require('heatshrink').decompress(atob('AGUf/4AE+AJBh4JF8AJBg4JF4AJBgYJFwAIBgIJFgOAgeABAn+A4MD4F/BIf8g4JB8E/BIf4h4JB+BQEAoIJBBoJOEn4JBEIJOEv4JBGoJOEAIIHBKAgEBBIRQDDAQJCKAYsCBISFCSIYJCTISlDBIX4BIZoBBP4J/BP4J/BNkB//wBIcf/4DB//gBIcP/wDBv/ABIcH/ghCwH/AAP+gYtCj4pBAAUBFoUOHoIACwAtCgkHBIfAoA2DBIZAEJQIACKgheBAARoGBKInJHZBPJMZJ3JRZKfJWZDHJfM4J/BP4J/BP4JP+AJDj4JD8AJDh4JD4AJDg4JDwH/AAP+AwQCBgIJCAgQJBBwQACDAIJB/giBAAXAv4JB/A2BAAXgn4JB+BKBAAQFBBIINBBIYZBBIIhBBIYtBBIJODKAYJBJwhQCwECJwhQCwBxCAAh9CJwhQCTIYAEUoROEKASbDAFwA='))
+ };
+
+ digit[9] = {
+ width: 80,
+ height: 128,
+ bpp: 1,
+ buffer: require('heatshrink').decompress(atob('AGUP/4AE/gJBg4JF/AJBgYJF+AJBgIJF8AJBwAJF4FAgHAv4JEwHAgHgn4JEgIJB+EfBAf+gYJB/BQE/kHBIIDBJwkPBIIXBJwkfBIIrBJwk/BIRQEJYIJCKAgOBBIXgIwYiBBIR7CQ4YJCR4SbDBISjCV4YJC/wJDFYIJ/BP4J/BP4Jjv/8BIcP/+AgE//AJDg//C4XwBIcDEYUP8E//4ABgIjCg/Av4JCwAjCgeABAQ5BuAJBgMBBIfgkAsDBIY2EIAIACJQhUBAAReENAIACOQgTJE5I7JJ5KhBMYwABO44ABRY4AFT4YAFWYYAFY4YAFbYYJ/BP4J/BP4Jnh4JIg4JIgYJIgIJEv//AAOABIgICA4N/BIQYBAAXgn4JCFgIAC+EfBIRABAAX4h4JCKgIAC/kHBIRoBAAX+gYJCn4JD/8BBIRODKAQJCBAhQBoBOFKAROGKAROGKAROGKAQJLAGA='))
+ };
+}
+
+// sprites
+
+const left0 = {
+ width: 8,
+ height: 10,
+ bpp: 1,
+ transparent: 0,
+ buffer: atob('ADAwEDgUcCgEAA==')
+};
+
+const left1 = {
+ width: 8,
+ height: 10,
+ bpp: 1,
+ transparent: 0,
+ buffer: atob('ADAwEDh0ECAoAA==')
+};
+
+const left2 = {
+ width: 8,
+ height: 10,
+ bpp: 1,
+ transparent: 0,
+ buffer: atob('ADAwEBg4EBg0AA==')
+};
+
+const left4 = {
+ width: 8,
+ height: 10,
+ bpp: 1,
+ transparent: 0,
+ buffer: atob('ABgYVDgQEChEAA==')
+};
+
+const right0 = {
+ width: 8,
+ height: 10,
+ bpp: 1,
+ transparent: 0,
+ buffer: atob('AAwMCBwoDhQgAA==')
+};
+
+const right1 = {
+ width: 8,
+ height: 10,
+ bpp: 1,
+ transparent: 0,
+ buffer: atob('AAwMCBwuCAQUAA==')
+};
+
+const right2 = {
+ width: 8,
+ height: 10,
+ bpp: 1,
+ transparent: 0,
+ buffer: atob('AAwMCBgcCBgsAA==')
+};
+
+const right4 = left4;
+
+const ball0 = {
+ width: 8,
+ height: 10,
+ bpp: 1,
+ transparent: 0,
+ buffer: atob('AAAAAAAwMAAAAA==')
+};
+
+const ball1 = {
+ width: 8,
+ height: 10,
+ bpp: 1,
+ transparent: 0,
+ buffer: atob('AAAAAAAAGBgAAA==')
+};
+const gol01 = {
+ width: 8,
+ height: 10,
+ bpp: 1,
+ transparent: 0,
+ buffer: atob('ABhkhIS0sIAAAA==')
+};
+
+const gol11 = {
+ width: 8,
+ height: 10,
+ bpp: 1,
+ buffer: require('heatshrink').decompress(atob('gEYk0hkMthsBBAI='))
+
+};
+
+loadDigits();
+
+
+function printNumber (n, x, y, options) {
+ if (n > 9 || n < -1) {
+ console.log(n);
+ return; // error
+ }
+ if (digit.length === 0) {
+ g.setColor(1, 1, 1);
+ if (options.scale == 0.2) {
+ g.setFont12x20(1);
+ } else {
+ g.setFont12x20(2.5);
+ }
+ g.drawString('' + n, x, y);
+ return;
+ }
+ const img = (n == -1) ? dash : digit[n];
+ if (img) {
+ // g.setColor(0,0,0);
+ // g.fillRect(x,y,x+32*options.scale,64*options.scale);
+ g.setColor(1, 1, 1);
+ g.drawImage(img, x, y, options);
+ }
+}
+
+g.setBgColor(0, 0, 0);
+g.clear();
+g.setColor(1, 1, 1);
+function onStop () {
+ if (goalFrame > 0) {
+ return;
+ }
+ stopped = !stopped;
+ if (stopped) {
+ // Bangle.beep();
+ if (msinc == 0) {
+ // GOOL
+ if (owner == 0) {
+ score0++;
+ goaler = 0;
+ } else if (owner == 1) {
+ score1++;
+ goaler = 1;
+ }
+ goalFrame = 5;
+ }
+ let newOwner = 0;
+ if (msinc % 2) {
+ newOwner = 1;
+ } else {
+ newOwner = 0;
+ }
+ if (newOwner) {
+ seq0--;
+ seq1++;
+ } else {
+ seq0++;
+ seq1--;
+ }
+ if (seq0 < 0) seq0 = 0;
+ if (seq0 > 3) seq0 = 3;
+ if (seq1 < 0) seq1 = 0;
+ if (seq1 > 3) seq1 = 3;
+ owner = newOwner;
+ }
+ refresh();
+ refresh_ms();
+}
+
+function onButtonPress() {
+ console.log('on.tap');
+ setWatch(() => {
+ onButtonPress();
+}, BTN1);
+ Bangle.beep();
+ if (endGame) {
+ score0 = 0;
+ score1 = 0;
+ seq0 = 0;
+ seq1 = 0;
+ part = 0;
+ inc = 0;
+ msinc = 0;
+ stopped = true;
+ endGame = false;
+ } else {
+ if (inc == 0) {
+ // autogame();
+ stopped = false;
+ } else {
+ onStop();
+ }
+ }
+}
+
+setWatch(() => {
+ onButtonPress();
+}, BTN1);
+/*Bangle.on('tap', function () {
+ onButtonPress();
+});
+*/
+g.setFont12x20(3);
+
+function refresh () {
+ g.clear();
+ if (inc > 59) {
+ inc = 0;
+ part++;
+ }
+ if (part >= 2 && inc > 30) {
+ part = 2;
+ Bangle.buzz();
+ endGame = true;
+ endInc = inc;
+ inc = 0;
+ }
+ if (inc > 44) {
+ inc = 0;
+ if (part < 2) {
+ part++;
+ }
+ if (part >= 2) {
+ if (score0 != score1) {
+ Bangle.buzz();
+ endGame = true;
+ endInc = inc;
+ inc = 0;
+ }
+ }
+ // end of 1st or 2nd part of the game?
+ }
+ let two = (inc < 10) ? '0' + inc : '' + inc;
+ if (endGame) {
+ g.setColor(0, 0, 0);
+ g.fillRect(0, 64, w, h);
+ if (inc % 2) {
+ two = (endInc < 10) ? '0' + endInc : '' + endInc;
+ printNumber(-1, 2, 64 + 16, { scale: 0.4 });
+ printNumber(part, 34, 64 + 16, { scale: 0.4 });
+ printNumber(two[0], 74, 64 + 16, { scale: 0.4 });
+ printNumber(two[1], 74 + 32, 64 + 16, { scale: 0.4 });
+ }
+ } else {
+ // seconds
+ printNumber(0, 2, 64 + 16, { scale: 0.4 });
+ printNumber(part, 34, 64 + 16, { scale: 0.4 });
+ printNumber(two[0], 74, 64 + 16, { scale: 0.4 });
+ printNumber(two[1], 74 + 32, 64 + 16, { scale: 0.4 });
+ }
+ refresh_ms();
+ refresh_score();
+ refresh_pixels();
+}
+
+function refresh_pixels () {
+ let frame4 = inc % 2;
+ if (goalFrame > 0) {
+ frame4 = goalFrame % 2;
+ if (goaler == 0) {
+ g.drawImage(frame4 ? right4 : right0, 20, 10, { scale: 5 });
+ g.setColor(1, 1, 1);
+ g.drawImage(gol11, w - 50, 10, { scale: 5 });
+ } else if (goaler == 1) {
+ g.drawImage(frame4 ? left0 : left4, w - 50, 10, { scale: 5 });
+ g.setColor(1, 1, 1);
+ g.drawImage(gol01, 30, 10, { scale: 5 });
+ }
+ return;
+ }
+ if (endGame) {
+ if (score0 > score1) {
+ g.drawImage(frame4 ? right1 : right0, 5 + (seq0 * 10), 10, { scale: 5 });
+ } else if (score0 < score1) {
+ g.drawImage(frame4 ? left0 : left1, w - 30 - (seq1 * 10), 10, { scale: 5 });
+ }
+ return;
+ }
+ g.drawImage(frame4 ? right1 : right0, 5 + (seq0 * 10), 10, { scale: 5 });
+ g.drawImage(frame4 ? left0 : left1, w - 30 - (seq1 * 10), 10, { scale: 5 });
+ let bx = (owner == 0) ? w / 3 : w / 2;
+ bx += 2;
+ g.drawImage(frame4 ? ball0 : ball1, bx, 10, { scale: 5 });
+ const liney = 60;
+ if (owner) {
+ g.drawLine(w-8, liney, 2*(w/3), liney);
+ } else {
+ g.drawLine(8, liney, w/3, liney);
+ }
+}
+let dots = 0;
+function refresh_dots () {
+ if (endGame) {
+ dots = 0;
+ } else {
+ dots++;
+ }
+ if (dots % 2) {
+ g.setColor(1, 1, 1);
+ } else {
+ g.setColor(0, 0, 0);
+ }
+ const x = 67;
+ let y = 100;
+ g.fillRect(x, y, x + 4, y + 4);
+ y += 16;
+ g.fillRect(x, y, x + 4, y + 4);
+}
+
+const h = g.getHeight();
+
+function refresh_ms () {
+ if (endGame) {
+ if (inc % 2) {
+ printNumber(-1, 80 + 64 - 4, 64 + 16 + 8 + 16, { scale: 0.2 });
+ printNumber(-1, 80 + 64 + 16 - 4, 64 + 32 + 8, { scale: 0.2 });
+ }
+ return;
+ }
+ // nanoseconds
+ if (msinc > 59) {
+ msinc = 0;
+ }
+ g.setColor(0, 0, 0);
+ g.fillRect(80 + 64, h / 2, 80 + 64 + 32, g.getHeight());
+ const two = (msinc < 10) ? '0' + msinc : '' + msinc;
+ printNumber(two[0], 80 + 64 - 4, 64 + 16 + 8 + 16, { scale: 0.2 });
+ printNumber(two[1], 80 + 64 + 16 - 4, 64 + 32 + 8, { scale: 0.2 });
+}
+
+function refresh_score () {
+ g.setColor(0, 0, 0);
+ g.fillRect(0, h - 32, w, h);
+ let two = (score0 < 10) ? '0' + score0 : '' + score0;
+ printNumber(two[0], 64 - 16, 32 + 64 + 16 + 8 + 16, { scale: 0.2 });
+ printNumber(two[1], 64, 32 + 64 + 32 + 8, { scale: 0.2 });
+ two = (score1 < 10) ? '0' + score1 : '' + score1;
+ printNumber(two[0], 32 + 64, 32 + 64 + 16 + 8 + 16, { scale: 0.2 });
+ printNumber(two[1], 32 + 64 + 16, 32 + 64 + 32 + 8, { scale: 0.2 });
+}
+refresh();
+
+setInterval(function () {
+ if (!stopped || endGame) {
+ inc++;
+ }
+ if (goalFrame > 0) {
+ goalFrame--;
+ }
+ refresh();
+}, 1000);
+
+setInterval(function () {
+ refresh_dots();
+}, 500);
+
+setInterval(function () {
+ if (!stopped) {
+ msinc++;
+ if (msinc > 59) {
+ msinc = 0;
+ }
+ }
+}, 10);
+
+setInterval(function () {
+ if (!stopped) {
+ refresh_ms();
+ }
+}, 250);
+
+function autogame () {
+ if (endGame) {
+ return;
+ }
+ onStop();
+ if (stopped) {
+ setTimeout(autogame, 500);
+ } else {
+ setTimeout(autogame, 315 + 10 * (Math.random() * 5));
+ }
+}
+
+Bangle.setOptions({ lockTimeout: 0, backlightTimeout: 0 });
+// autogame();
+
diff --git a/apps/football/app.png b/apps/football/app.png
new file mode 100644
index 000000000..80d7cea15
Binary files /dev/null and b/apps/football/app.png differ
diff --git a/apps/football/media/ball0.png b/apps/football/media/ball0.png
new file mode 100644
index 000000000..5b890c180
Binary files /dev/null and b/apps/football/media/ball0.png differ
diff --git a/apps/football/media/ball1.png b/apps/football/media/ball1.png
new file mode 100644
index 000000000..c72e56189
Binary files /dev/null and b/apps/football/media/ball1.png differ
diff --git a/apps/football/media/dash.png b/apps/football/media/dash.png
new file mode 100644
index 000000000..6a9b0c4ac
Binary files /dev/null and b/apps/football/media/dash.png differ
diff --git a/apps/football/media/digit0.png b/apps/football/media/digit0.png
new file mode 100644
index 000000000..33856cc5e
Binary files /dev/null and b/apps/football/media/digit0.png differ
diff --git a/apps/football/media/digit1.png b/apps/football/media/digit1.png
new file mode 100644
index 000000000..53b914ded
Binary files /dev/null and b/apps/football/media/digit1.png differ
diff --git a/apps/football/media/digit2.png b/apps/football/media/digit2.png
new file mode 100644
index 000000000..7a7787b05
Binary files /dev/null and b/apps/football/media/digit2.png differ
diff --git a/apps/football/media/digit3.png b/apps/football/media/digit3.png
new file mode 100644
index 000000000..a197d5993
Binary files /dev/null and b/apps/football/media/digit3.png differ
diff --git a/apps/football/media/digit4.png b/apps/football/media/digit4.png
new file mode 100644
index 000000000..f2810a0b6
Binary files /dev/null and b/apps/football/media/digit4.png differ
diff --git a/apps/football/media/digit5.png b/apps/football/media/digit5.png
new file mode 100644
index 000000000..d8027c362
Binary files /dev/null and b/apps/football/media/digit5.png differ
diff --git a/apps/football/media/digit6.png b/apps/football/media/digit6.png
new file mode 100644
index 000000000..bd7980045
Binary files /dev/null and b/apps/football/media/digit6.png differ
diff --git a/apps/football/media/digit7.png b/apps/football/media/digit7.png
new file mode 100644
index 000000000..9ef0df11a
Binary files /dev/null and b/apps/football/media/digit7.png differ
diff --git a/apps/football/media/digit8.png b/apps/football/media/digit8.png
new file mode 100644
index 000000000..6916a301a
Binary files /dev/null and b/apps/football/media/digit8.png differ
diff --git a/apps/football/media/digit9.png b/apps/football/media/digit9.png
new file mode 100644
index 000000000..d8d327523
Binary files /dev/null and b/apps/football/media/digit9.png differ
diff --git a/apps/football/media/digits.png b/apps/football/media/digits.png
new file mode 100644
index 000000000..68ace56af
Binary files /dev/null and b/apps/football/media/digits.png differ
diff --git a/apps/football/media/digits128.png b/apps/football/media/digits128.png
new file mode 100644
index 000000000..f363a7e8e
Binary files /dev/null and b/apps/football/media/digits128.png differ
diff --git a/apps/football/media/digits64.png b/apps/football/media/digits64.png
new file mode 100644
index 000000000..445c14dfa
Binary files /dev/null and b/apps/football/media/digits64.png differ
diff --git a/apps/football/media/gol00.png b/apps/football/media/gol00.png
new file mode 100644
index 000000000..3b16aa967
Binary files /dev/null and b/apps/football/media/gol00.png differ
diff --git a/apps/football/media/gol01.png b/apps/football/media/gol01.png
new file mode 100644
index 000000000..3b16aa967
Binary files /dev/null and b/apps/football/media/gol01.png differ
diff --git a/apps/football/media/gol10.png b/apps/football/media/gol10.png
new file mode 100644
index 000000000..178b6fe3d
Binary files /dev/null and b/apps/football/media/gol10.png differ
diff --git a/apps/football/media/gol11.png b/apps/football/media/gol11.png
new file mode 100644
index 000000000..732fa815d
Binary files /dev/null and b/apps/football/media/gol11.png differ
diff --git a/apps/football/media/left0.png b/apps/football/media/left0.png
new file mode 100644
index 000000000..20599cbb7
Binary files /dev/null and b/apps/football/media/left0.png differ
diff --git a/apps/football/media/left1.png b/apps/football/media/left1.png
new file mode 100644
index 000000000..b6ffc22f9
Binary files /dev/null and b/apps/football/media/left1.png differ
diff --git a/apps/football/media/left2.png b/apps/football/media/left2.png
new file mode 100644
index 000000000..11ff8885f
Binary files /dev/null and b/apps/football/media/left2.png differ
diff --git a/apps/football/media/left4.png b/apps/football/media/left4.png
new file mode 100644
index 000000000..a7301a4f8
Binary files /dev/null and b/apps/football/media/left4.png differ
diff --git a/apps/football/media/right0.png b/apps/football/media/right0.png
new file mode 100644
index 000000000..ac418ad7b
Binary files /dev/null and b/apps/football/media/right0.png differ
diff --git a/apps/football/media/right1.png b/apps/football/media/right1.png
new file mode 100644
index 000000000..31554cbc3
Binary files /dev/null and b/apps/football/media/right1.png differ
diff --git a/apps/football/media/right2.png b/apps/football/media/right2.png
new file mode 100644
index 000000000..8c8d0ece4
Binary files /dev/null and b/apps/football/media/right2.png differ
diff --git a/apps/football/media/right4.png b/apps/football/media/right4.png
new file mode 100644
index 000000000..83a78b52c
Binary files /dev/null and b/apps/football/media/right4.png differ
diff --git a/apps/football/metadata.json b/apps/football/metadata.json
new file mode 100644
index 000000000..253026c39
--- /dev/null
+++ b/apps/football/metadata.json
@@ -0,0 +1,31 @@
+{
+ "id": "football",
+ "name": "football",
+ "shortName": "football",
+ "version": "1.00",
+ "type": "app",
+ "description": "Classic football game of the CASIO chronometer",
+ "icon": "app.png",
+ "allow_emulator": true,
+ "tags": "games",
+ "supports": [
+ "BANGLEJS2"
+ ],
+ "readme": "README.md",
+ "storage": [
+ {
+ "name": "football.app.js",
+ "url": "app.js"
+ },
+ {
+ "name": "football.img",
+ "url": "app-icon.js",
+ "evaluate": true
+ }
+ ],
+ "screenshots": [
+ {
+ "url": "screenshot.png"
+ }
+ ]
+}
diff --git a/apps/football/screenshot.png b/apps/football/screenshot.png
new file mode 100644
index 000000000..5742fe9e1
Binary files /dev/null and b/apps/football/screenshot.png differ
diff --git a/apps/fwupdate/custom.html b/apps/fwupdate/custom.html
index b3cd7e12d..3f8f50b3f 100644
--- a/apps/fwupdate/custom.html
+++ b/apps/fwupdate/custom.html
@@ -81,6 +81,8 @@ function onInit(device) {
if (crc==3435933210) version = "2v11.52";
if (crc==46757280) version = "2v11.58";
if (crc==3508163280 || crc==1418074094) version = "2v12";
+ if (crc==4056371285) version = "2v13";
+ if (crc==1038322422) version = "2v14";
if (!ok) {
version += `(⚠ update required)`;
}
diff --git a/apps/health/interface.html b/apps/health/interface.html
index 0791acd24..a708e2645 100644
--- a/apps/health/interface.html
+++ b/apps/health/interface.html
@@ -113,7 +113,7 @@ function getMonthList() {
Util.showModal("Deleting...");
Util.eraseStorage(filename,()=>{
Util.hideModal();
- getTrackList();
+ getMonthList();
});
}
if (task=="downloadcsv") {
diff --git a/apps/homework/app.js b/apps/homework/app.js
index 4ba786690..3d9be31c9 100644
--- a/apps/homework/app.js
+++ b/apps/homework/app.js
@@ -14,7 +14,7 @@ var nhwmn = { // New homework Menu
function newHomeworkMenu() {
E.showMessage("Getting subjects...");
- var rawsubjects = require("Storage").read("subjects.txt"); // This code reads out the subjects list and removes the newline character at the end
+ var rawsubjects = require("Storage").read("homework.subjects.txt"); // This code reads out the subjects list and removes the newline character at the end
var splitsubjects = rawsubjects.split(",");
var lastItem = splitsubjects[splitsubjects.length - 1];
var thiscurrentsubject;
diff --git a/apps/homework/metadata.json b/apps/homework/metadata.json
index 2ba1e918f..a46c74dad 100644
--- a/apps/homework/metadata.json
+++ b/apps/homework/metadata.json
@@ -9,6 +9,10 @@
"supports" : ["BANGLEJS2"],
"readme": "README.md",
"custom": "subjects.html",
+ "data": [
+ {"name":"homework.txt" },
+ {"name":"homework.subjects.txt" }
+ ],
"storage": [
{"name":"homework.app.js","url":"app.js"},
{"name":"homework.img","url":"app-icon.js","evaluate":true}
diff --git a/apps/homework/subjects.html b/apps/homework/subjects.html
index d3bf7a400..92c3023dd 100644
--- a/apps/homework/subjects.html
+++ b/apps/homework/subjects.html
@@ -20,9 +20,10 @@
// send finished app (in addition to contents of app.json)
sendCustomizedApp({
storage:[
- {name:"subjects.txt"},
+ {name:"homework.subjects.txt", url:"subjects.txt", content:app},
]
});
+ console.log("Sent homework.subjects.txt!");
});
diff --git a/apps/hwid_a_battery_widget/ChangeLog b/apps/hwid_a_battery_widget/ChangeLog
new file mode 100644
index 000000000..9d5ee1264
--- /dev/null
+++ b/apps/hwid_a_battery_widget/ChangeLog
@@ -0,0 +1,7 @@
+0.01: Release for Bangle 2 (2021/11/18)
+0.02: Internal id update to wid_* as per Gordon's request (2021/11/21)
+0.03: Support dark themes
+0.04: Increase screen update rate when charging
+0.05: Deleting Background - making Font larger
+0.06: Fixing refresh issues
+0.07
\ No newline at end of file
diff --git a/apps/hwid_a_battery_widget/README.md b/apps/hwid_a_battery_widget/README.md
new file mode 100644
index 000000000..638272b3f
--- /dev/null
+++ b/apps/hwid_a_battery_widget/README.md
@@ -0,0 +1,15 @@
+# A Battery Widget (with percentage)
+
+Show the current battery level and charging status in the top right of the clock, with charge percentage
+
+* Works with Bangle 2
+* Simple design, no settings
+ * Red when the batterly level is below 30%
+ * Blue when charging
+* 40 pixels wide
+
+
+
+## Creator
+[@alainsaas](https://github.com/alainsaas)
+Mod by Hank
diff --git a/apps/hwid_a_battery_widget/h_battery_widget-pic.jpg b/apps/hwid_a_battery_widget/h_battery_widget-pic.jpg
new file mode 100644
index 000000000..9d9a4decc
Binary files /dev/null and b/apps/hwid_a_battery_widget/h_battery_widget-pic.jpg differ
diff --git a/apps/hwid_a_battery_widget/metadata.json b/apps/hwid_a_battery_widget/metadata.json
new file mode 100644
index 000000000..38fd503a2
--- /dev/null
+++ b/apps/hwid_a_battery_widget/metadata.json
@@ -0,0 +1,15 @@
+{
+ "id": "hwid_a_battery_widget",
+ "name": "A Battery Widget (with percentage) - Hanks Mod",
+ "shortName":"H Battery Widget",
+ "icon": "widget.png",
+ "version":"0.07",
+ "type": "widget",
+ "supports": ["BANGLEJS", "BANGLEJS2"],
+ "readme": "README.md",
+ "description": "Simple and slim battery widget with charge status and percentage",
+ "tags": "widget,battery",
+ "storage": [
+ {"name":"hwid_a_battery_widget.wid.js","url":"widget.js"}
+ ]
+}
diff --git a/apps/hwid_a_battery_widget/widget.js b/apps/hwid_a_battery_widget/widget.js
new file mode 100644
index 000000000..708e8cbbf
--- /dev/null
+++ b/apps/hwid_a_battery_widget/widget.js
@@ -0,0 +1,54 @@
+(function(){
+ const intervalLow = 60000; // update time when not charging
+ const intervalHigh = 2000; // update time when charging
+ var old_l;
+
+ let COLORS = {
+ 'white': g.theme.dark ? "#000" : "#fff",
+ 'black': g.theme.dark ? "#fff" : "#000",
+ 'charging': "#08f",
+ 'high': g.theme.dark ? "#fff" : "#000",
+ 'low': "#f00",
+ };
+
+ const levelColor = (l) => {
+ if (Bangle.isCharging()) return COLORS.charging;
+ if (l >= 30) return COLORS.high;
+ return COLORS.low;
+ };
+
+ function draw() {
+ var s = 29;
+ var x = this.x, y = this.y;
+ const l = E.getBattery();
+ let xl = x+4+l*(s-12)/100;
+ if (l != old_l){ // Delete the old value from screen
+ old_l = l;
+ let xl_old = x+4+old_l*(s-12)/100;
+ g.setColor(COLORS.white);
+ // g.fillRect(x+2,y+5,x+s-6,y+18);
+ g.fillRect(x,y,xl+4,y+16+3); //Clear
+ g.setFontAlign(0,0);
+ g.setFont('Vector',16);
+ g.drawString(old_l, x + 14, y + 10);
+ g.fillRect(x+4,y+14+3,xl_old,y+16+3); // charging bar
+ }
+
+ g.setColor(levelColor(l));
+ g.fillRect(x+4,y+14+3,xl,y+16+3); // charging bar
+ g.fillRect((x+4+100*(s-12)/100)-1,y+14+3,x+4+100*(s-12)/100,y+16+3); // charging bar "full mark"
+ // Show percentage
+ g.setColor(COLORS.black);
+ g.setFontAlign(0,0);
+ g.setFont('Vector',16);
+ g.drawString(l, x + 14, y + 10);
+
+ if (Bangle.isCharging()) changeInterval(id, intervalHigh);
+ else changeInterval(id, intervalLow);
+ }
+
+ Bangle.on('charging',function(charging) { draw(); });
+ var id = setInterval(()=>WIDGETS["wid_a_battery_widget"].draw(), intervalLow);
+
+ WIDGETS["wid_a_battery_widget"]={area:"tr",width:30,draw:draw};
+})();
diff --git a/apps/hwid_a_battery_widget/widget.png b/apps/hwid_a_battery_widget/widget.png
new file mode 100644
index 000000000..b9c9594b1
Binary files /dev/null and b/apps/hwid_a_battery_widget/widget.png differ
diff --git a/apps/hworldclock/ChangeLog b/apps/hworldclock/ChangeLog
new file mode 100644
index 000000000..72b25a7c6
--- /dev/null
+++ b/apps/hworldclock/ChangeLog
@@ -0,0 +1,7 @@
+0.15: Initial release - be patient as this is the first try :)
+0.16: Fix timing
+0.17: Fix hours
+0.18: Code cleanup and major changes with seconds timing. New feature: if watch is locked, seconds get refreshed every 10 seconds.
+0.19: Fix PM Hours
+0.20: Add theme support
+0.21: Add Settings
\ No newline at end of file
diff --git a/apps/hworldclock/README.md b/apps/hworldclock/README.md
new file mode 100644
index 000000000..0f4f9296c
--- /dev/null
+++ b/apps/hworldclock/README.md
@@ -0,0 +1,31 @@
+# Hanks World Clock - See the time in four locations
+
+In addition to the main clock and date in your current location, you can add up to three other locations. Great for travel or remote working.
+Additionally we show the sunset/sunrise and seconds for the current location and the day name is shown in your locale.
+If watch is locked, seconds get refreshed every 10 seconds.
+
+
+
+## Usage
+
+Provide names and the UTC offsets for up to three other timezones in the app store. These are stored in a json file on your watch. UTC offsets can be decimal (e.g., 5.5 for India).
+
+The clock does not handle summer time / daylight saving time changes automatically. If one of your three locations changes its UTC offset, you can simply change the setting in the app store and update. Currently the clock only supports 24 hour time format for the additional time zones.
+
+
+## Requests
+
+Please use [the Espruino Forum](http://forum.espruino.com/microcosms/1424/) if you have feature requests or notice bugs.
+
+## Creator
+
+Created by Hank.
+
+Based on the great work of
+=================
+World Clock - 4 time zones
+Made by [Scott Hale](https://www.github.com/computermacgyver), based upon the [Simple Clock](https://github.com/espruino/BangleApps/tree/master/apps/sclock).
+===== a n d =====
+Sun Clock
+[Sun Clock](https://github.com/espruino/BangleApps/tree/master/apps/sunclock)
+=================
diff --git a/apps/hworldclock/app.js b/apps/hworldclock/app.js
new file mode 100644
index 000000000..d4c677d26
--- /dev/null
+++ b/apps/hworldclock/app.js
@@ -0,0 +1,389 @@
+// ------- Settings file
+const SETTINGSFILE = "hworldclock.json";
+var secondsMode;
+var showSunInfo;
+var colorWhenDark;
+// ------- Settings file
+
+const big = g.getWidth()>200;
+// Font for primary time and date
+const primaryTimeFontSize = big?6:5;
+const primaryDateFontSize = big?3:2;
+require("Font5x9Numeric7Seg").add(Graphics);
+require("FontTeletext10x18Ascii").add(Graphics);
+
+// Font for single secondary time
+const secondaryTimeFontSize = 4;
+const secondaryTimeZoneFontSize = 2;
+
+// Font / columns for multiple secondary times
+const secondaryRowColFontSize = 2;
+const xcol1 = 10;
+const xcol2 = g.getWidth() - xcol1;
+
+const font = "6x8";
+
+/* TODO: we could totally use 'Layout' here and
+avoid a whole bunch of hard-coded offsets */
+
+const xyCenter = g.getWidth() / 2;
+const xyCenterSeconds = xyCenter + (big ? 85 : 68);
+const yAmPm = xyCenter - (big ? 70 : 48);
+const yposTime = big ? 70 : 55;
+const yposTime2 = yposTime + (big ? 100 : 60);
+const yposDate = big ? 135 : 95;
+const yposWorld = big ? 170 : 120;
+
+const OFFSET_TIME_ZONE = 0;
+const OFFSET_HOURS = 1;
+
+var PosInterval = 0;
+
+var offsets = require("Storage").readJSON("hworldclock.settings.json") || [];
+
+//=======Sun
+setting = require("Storage").readJSON("setting.json",1);
+E.setTimeZone(setting.timezone); // timezone = 1 for MEZ, = 2 for MESZ
+SunCalc = require("hsuncalc.js");
+const LOCATION_FILE = "mylocation.json";
+var rise = "07:00";
+var set = "20:00";
+var pos = {altitude: 20, azimuth: 135};
+var noonpos = {altitude: 37, azimuth: 180};
+//=======Sun
+
+var ampm = "AM";
+
+// TESTING CODE
+// Used to test offset array values during development.
+// Uncomment to override secondary offsets value
+/*
+const mockOffsets = {
+ zeroOffsets: [],
+ oneOffset: [["UTC", 0]],
+ twoOffsets: [
+ ["Tokyo", 9],
+ ["UTC", 0],
+ ],
+ fourOffsets: [
+ ["Tokyo", 9],
+ ["UTC", 0],
+ ["Denver", -7],
+ ["Miami", -5],
+ ],
+};*/
+
+
+// Example hworldclock.settings.json
+// [["London","0"],["NY","-5"],["Denver","-6"]]
+
+
+// Uncomment one at a time to test various offsets array scenarios
+//offsets = mockOffsets.zeroOffsets; // should render nothing below primary time
+//offsets = mockOffsets.oneOffset; // should render larger in two rows
+//offsets = mockOffsets.twoOffsets; // should render two in columns
+//offsets = mockOffsets.fourOffsets; // should render in columns
+
+// END TESTING CODE
+
+
+// Load settings
+function loadMySettings() {
+ // Helper function default setting
+ function def (value, def) {return value !== undefined ? value : def;}
+
+ var settings = require('Storage').readJSON(SETTINGSFILE, true) || {};
+ secondsMode = def(settings.secondsMode, "when unlocked");
+ showSunInfo = def(settings.showSunInfo, true);
+ colorWhenDark = def(settings.colorWhenDark, "green");
+}
+
+
+// Check settings for what type our clock should be
+var _12hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"]||false;
+
+// timeout used to update every minute
+var drawTimeout;
+var drawTimeoutSeconds;
+var secondsTimeout;
+
+g.setBgColor(g.theme.bg);
+
+// schedule a draw for the next minute
+function queueDraw() {
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = setTimeout(function() {
+ drawTimeout = undefined;
+ draw();
+ }, 60000 - (Date.now() % 60000));
+}
+
+// schedule a draw for the next second
+function queueDrawSeconds() {
+ if (drawTimeoutSeconds) clearTimeout(drawTimeoutSeconds);
+ drawTimeoutSeconds = setTimeout(function() {
+ drawTimeoutSeconds = undefined;
+ drawSeconds();
+ //console.log("TO: " + secondsTimeout);
+ }, secondsTimeout - (Date.now() % secondsTimeout));
+}
+
+function doublenum(x) {
+ return x < 10 ? "0" + x : "" + x;
+}
+
+function getCurrentTimeFromOffset(dt, offset) {
+ return new Date(dt.getTime() + offset * 60 * 60 * 1000);
+}
+
+function updatePos() {
+ coord = require("Storage").readJSON(LOCATION_FILE,1)|| {"lat":53.3,"lon":10.1,"location":"Pattensen"};
+ pos = SunCalc.getPosition(Date.now(), coord.lat, coord.lon);
+ times = SunCalc.getTimes(Date.now(), coord.lat, coord.lon);
+ rise = times.sunrise.toString().split(" ")[4].substr(0,5);
+ set = times.sunset.toString().split(" ")[4].substr(0,5);
+ noonpos = SunCalc.getPosition(times.solarNoon, coord.lat, coord.lon);
+}
+
+
+function drawSeconds() {
+ // get date
+ var d = new Date();
+ var da = d.toString().split(" ");
+
+ // default draw styles
+ g.reset();
+ g.setBgColor(g.theme.bg);
+
+ // drawSting centered
+ g.setFontAlign(0, 0);
+
+ // draw time
+ var time = da[4].split(":");
+ var seconds = time[2];
+
+ g.setFont("5x9Numeric7Seg",primaryTimeFontSize - 3);
+ if (g.theme.dark) {
+ if (colorWhenDark == "green") {
+ g.setColor("#22ff05");
+ } else {
+ g.setColor(g.theme.fg);
+ }
+ } else {
+ g.setColor(g.theme.fg);
+ }
+ //console.log("---");
+ //console.log(seconds);
+ if (Bangle.isLocked() && secondsMode != "always") seconds = seconds.slice(0, -1) + ':::'; // we use :: as the font does not have an x
+ //console.log(seconds);
+ g.drawString(`${seconds}`, xyCenterSeconds, yposTime+14, true);
+ queueDrawSeconds();
+
+}
+
+function draw() {
+ // get date
+ var d = new Date();
+ var da = d.toString().split(" ");
+
+ // default draw styles
+ g.reset();
+ g.setBgColor(g.theme.bg);
+
+ // drawSting centered
+ g.setFontAlign(0, 0);
+
+ // draw time
+ var time = da[4].split(":");
+ var hours = time[0],
+ 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";
+ }
+ }
+
+ //g.setFont(font, primaryTimeFontSize);
+ g.setFont("5x9Numeric7Seg",primaryTimeFontSize);
+ if (g.theme.dark) {
+ if (colorWhenDark == "green") {
+ g.setColor("#22ff05");
+ } else {
+ g.setColor(g.theme.fg);
+ }
+ } else {
+ g.setColor(g.theme.fg);
+ }
+ g.drawString(`${hours}:${minutes}`, xyCenter-10, yposTime, true);
+
+ // am / PM ?
+ if (_12hour){
+ //do 12 hour stuff
+ //var ampm = require("locale").medidian(new Date()); Not working
+ g.setFont("Vector", 17);
+ g.drawString(ampm, xyCenterSeconds, yAmPm, true);
+ }
+
+ if (secondsMode != "none") drawSeconds(); // To make sure...
+
+ // draw Day, name of month, Date
+ //DATE
+ var localDate = require("locale").date(new Date(), 1);
+ localDate = localDate.substring(0, localDate.length - 5);
+ g.setFont("Vector", 17);
+ g.drawString(require("locale").dow(new Date(), 1).toUpperCase() + ", " + localDate, xyCenter, yposDate, true);
+
+ g.setFont(font, primaryDateFontSize);
+ // set gmt to UTC+0
+ var gmt = new Date(d.getTime() + d.getTimezoneOffset() * 60 * 1000);
+
+ // Loop through offset(s) and render
+ offsets.forEach((offset, index) => {
+ dx = getCurrentTimeFromOffset(gmt, offset[OFFSET_HOURS]);
+ hours = doublenum(dx.getHours());
+ minutes = doublenum(dx.getMinutes());
+
+
+ if (offsets.length === 1) {
+ var date = [require("locale").dow(new Date(), 1), require("locale").date(new Date(), 1)];
+ // For a single secondary timezone, draw it bigger and drop time zone to second line
+ const xOffset = 30;
+ g.setFont(font, secondaryTimeFontSize);
+ g.drawString(`${hours}:${minutes}`, xyCenter, yposTime2, true);
+ g.setFont(font, secondaryTimeZoneFontSize);
+ g.drawString(offset[OFFSET_TIME_ZONE], xyCenter, yposTime2 + 30, true);
+
+ // draw Day, name of month, Date
+ g.setFont(font, secondaryTimeZoneFontSize);
+ g.drawString(date, xyCenter, yposDate, true);
+ } else if (index < 3) {
+ // For > 1 extra timezones, render as columns / rows
+ g.setFont(font, secondaryRowColFontSize);
+ g.setFontAlign(-1, 0);
+ g.drawString(
+ offset[OFFSET_TIME_ZONE],
+ xcol1,
+ yposWorld + index * 15,
+ true
+ );
+ g.setFontAlign(1, 0);
+ g.drawString(`${hours}:${minutes}`, xcol2, yposWorld + index * 15, true);
+ }
+ });
+
+ if (showSunInfo) {
+ g.setFontAlign(-1, 0);
+ g.setFont("Vector",12);
+ g.drawString(`^${rise}`, 10, 3 + yposWorld + 3 * 15, true); // draw riseset
+ g.setFontAlign(1, 0);
+ g.drawString(`v${set}`, xcol2, 3 + yposWorld + 3 * 15, true); // draw riseset
+ }
+ //debug settings
+ //g.setFontAlign(1, 0);
+ //g.drawString(secondsMode, xcol2, 3 + yposWorld + 3 * 15, true);
+ //g.drawString(showSunInfo, xcol2, 3 + yposWorld + 3 * 15, true);
+ //g.drawString(colorWhenDark, xcol2, 3 + yposWorld + 3 * 15, true);
+
+
+ queueDraw();
+
+ if (secondsMode != "none") queueDrawSeconds();
+}
+
+// clean app screen
+g.clear();
+
+// Init the settings of the app
+loadMySettings();
+
+// Show launcher when button pressed
+Bangle.setUI("clock");
+Bangle.loadWidgets();
+Bangle.drawWidgets();
+
+
+// draw immediately at first, queue update
+draw();
+
+
+if (!Bangle.isLocked()) { // Initial state
+ if (showSunInfo) {
+ if (PosInterval != 0) clearInterval(PosInterval);
+ PosInterval = setInterval(updatePos, 60*10E3); // refesh every 10 mins
+ }
+
+ secondsTimeout = 1000;
+ if (secondsMode != "none") {
+ if (drawTimeoutSeconds) clearTimeout(drawTimeoutSeconds);
+ drawTimeoutSeconds = undefined;
+ }
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = undefined;
+
+ draw(); // draw immediately, queue redraw
+ if (showSunInfo) updatePos();
+ }else{
+ if (secondsMode == "always") secondsTimeout = 1000;
+ if (secondsMode == "when unlocked") secondsTimeout = 10 * 1000;
+
+ if (secondsMode != "none") {
+ if (drawTimeoutSeconds) clearTimeout(drawTimeoutSeconds);
+ drawTimeoutSeconds = undefined;
+ }
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = undefined;
+
+ if (showSunInfo) {
+ if (PosInterval != 0) clearInterval(PosInterval);
+ PosInterval = setInterval(updatePos, 60*60E3); // refesh every 60 mins
+ }
+ draw(); // draw immediately, queue redraw
+ if (showSunInfo) updatePos();
+ }
+
+
+
+
+
+Bangle.on('lock',on=>{
+ if (!on) { // UNlocked
+ if (showSunInfo) {
+ if (PosInterval != 0) clearInterval(PosInterval);
+ PosInterval = setInterval(updatePos, 60*10E3); // refesh every 10 mins
+ }
+
+ secondsTimeout = 1000;
+ if (secondsMode != "none") {
+ if (drawTimeoutSeconds) clearTimeout(drawTimeoutSeconds);
+ drawTimeoutSeconds = undefined;
+ }
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = undefined;
+
+ draw(); // draw immediately, queue redraw
+ if (showSunInfo) updatePos();
+ }else{ // locked
+
+ if (secondsMode == "always") secondsTimeout = 1000;
+ if (secondsMode == "when unlocked") secondsTimeout = 10 * 1000;
+
+ if (secondsMode != "none") {
+ if (drawTimeoutSeconds) clearTimeout(drawTimeoutSeconds);
+ drawTimeoutSeconds = undefined;
+ }
+ if (drawTimeout) clearTimeout(drawTimeout);
+ drawTimeout = undefined;
+
+ if (PosInterval != 0) clearInterval(PosInterval);
+ PosInterval = setInterval(updatePos, 60*60E3); // refesh every 60 mins
+ draw(); // draw immediately, queue redraw
+ if (showSunInfo) updatePos();
+ }
+ });
\ No newline at end of file
diff --git a/apps/hworldclock/app.png b/apps/hworldclock/app.png
new file mode 100644
index 000000000..506fa45b9
Binary files /dev/null and b/apps/hworldclock/app.png differ
diff --git a/apps/hworldclock/custom.html b/apps/hworldclock/custom.html
new file mode 100644
index 000000000..896d999f5
--- /dev/null
+++ b/apps/hworldclock/custom.html
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+ You can add up to 3 timezones. Please give a name and UTC offset in hours.
+ If you want less than 3, clear the checkbox to the left.
+
+
+
+ Enabled? |
+ Name |
+ UTC Offset |
+
+
+
+ Click
+
+
+
+
+
+
diff --git a/apps/hworldclock/hsuncalc.js b/apps/hworldclock/hsuncalc.js
new file mode 100644
index 000000000..b1af0a0d9
--- /dev/null
+++ b/apps/hworldclock/hsuncalc.js
@@ -0,0 +1,298 @@
+/* Module suncalc.js
+ (c) 2011-2015, Vladimir Agafonkin
+ SunCalc is a JavaScript library for calculating sun/moon position and light phases.
+ https://github.com/mourner/suncalc
+
+PB: Usage:
+E.setTimeZone(2); // 1 = MEZ, 2 = MESZ
+SunCalc = require("suncalc.js");
+pos = SunCalc.getPosition(Date.now(), 53.3, 10.1);
+times = SunCalc.getTimes(Date.now(), 53.3, 10.1);
+rise = times.sunrise; // Date object
+rise_str = rise.getHours() + ':' + rise.getMinutes(); //hh:mm
+*/
+var exports={};
+
+// 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 new Date((j + 0.5 - J1970) * dayMs); } // PB: onece removed + 0.5; included it again 4 Jan 2021
+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)
+ };
+}
+
+// calculates sun position for a given date and latitude/longitude
+
+exports.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: Math.round((azimuth(H, phi, c.dec) / rad + 180) % 360), // PB: converted to deg
+ altitude: Math.round( altitude(H, phi, c.dec) / rad) // PB: converted to deg
+ };
+};
+
+
+// sun times configuration (angle, morning name, evening name)
+
+var times = [
+ [-0.833, 'sunrise', 'sunset' ]
+];
+
+// 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
+
+exports.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: fromJulian(Jnoon),
+ nadir: 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]] = fromJulian(Jrise);
+ result[time[2]] = fromJulian(Jset);
+ }
+
+ 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
+ };
+}
+
+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.
+
+getMoonIllumination = function (date) {
+
+ var d = toDays(date || new Date()),
+ s = sunCoords(d),
+ m = moonCoords(d),
+
+ sdist = 149598000, // distance from Earth to Sun in km
+
+ phi = acos(sin(s.dec) * sin(m.dec) + cos(s.dec) * cos(m.dec) * cos(s.ra - m.ra)),
+ inc = atan(sdist * sin(phi), m.dist - sdist * cos(phi)),
+ angle = atan(cos(s.dec) * sin(s.ra - m.ra), sin(s.dec) * cos(m.dec) -
+ cos(s.dec) * sin(m.dec) * cos(s.ra - m.ra));
+
+ return {
+ fraction: (1 + cos(inc)) / 2,
+ phase: 0.5 + 0.5 * inc * (angle < 0 ? -1 : 1) / Math.PI,
+ angle: angle
+ };
+};
+
+
+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
+
+getMoonTimes = function (date, lat, lng, inUTC) {
+ var t = new Date(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;
+};
\ No newline at end of file
diff --git a/apps/hworldclock/hworldclock-icon.js b/apps/hworldclock/hworldclock-icon.js
new file mode 100644
index 000000000..6e05d254c
--- /dev/null
+++ b/apps/hworldclock/hworldclock-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwgJC/ABEE+EA4EAj9E8HF//gn/gwP///wt/MgF//8gh/8gYLBwEP+EHAofghgFD4EOj//gEPA4ILBGgIxB/wFBgwFB/lsgCKBj/4oxHBvAFBJoV8gP4TQX+gJUBAAN/Aok+AoVgAoXogAfBjkA8AfBAoXAAoUYY4cAiCDEAooA/ABg"))
diff --git a/apps/hworldclock/hworldclock.png b/apps/hworldclock/hworldclock.png
new file mode 100644
index 000000000..565e0dc6b
Binary files /dev/null and b/apps/hworldclock/hworldclock.png differ
diff --git a/apps/hworldclock/metadata.json b/apps/hworldclock/metadata.json
new file mode 100644
index 000000000..3d9be3bc7
--- /dev/null
+++ b/apps/hworldclock/metadata.json
@@ -0,0 +1,25 @@
+{
+ "id": "hworldclock",
+ "name": "Hanks World Clock",
+ "shortName": "Hanks World Clock",
+ "version": "0.21",
+ "description": "Current time zone plus up to three others",
+ "allow_emulator":true,
+ "icon": "app.png",
+ "screenshots": [{"url":"screenshot_hworld.png"}],
+ "type": "clock",
+ "tags": "clock",
+ "supports": ["BANGLEJS","BANGLEJS2"],
+ "readme": "README.md",
+ "custom": "custom.html",
+ "storage": [
+ {"name":"hworldclock.app.js","url":"app.js"},
+ {"name":"hworldclock.img","url":"hworldclock-icon.js","evaluate":true},
+ {"name":"hworldclock.settings.js","url":"settings.js"},
+ {"name":"hsuncalc.js","url":"hsuncalc.js"}
+ ],
+ "data": [
+ {"name":"hworldclock.settings.json"},
+ {"name":"hworldclock.json"}
+ ]
+}
\ No newline at end of file
diff --git a/apps/hworldclock/screenshot_hworld.png b/apps/hworldclock/screenshot_hworld.png
new file mode 100644
index 000000000..565e0dc6b
Binary files /dev/null and b/apps/hworldclock/screenshot_hworld.png differ
diff --git a/apps/hworldclock/settings.js b/apps/hworldclock/settings.js
new file mode 100644
index 000000000..60092d21e
--- /dev/null
+++ b/apps/hworldclock/settings.js
@@ -0,0 +1,59 @@
+// Settings menu for the enhanced Anton clock
+
+(function(back) {
+ var FILE = "hworldclock.json";
+ // Load settings
+ 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": "Hanks World Clock"
+ },
+ "< Back": () => back(),
+ "Seconds": stringInSettings("secondsMode", ["always", "when unlocked", "none"]),
+ "Color w. dark": stringInSettings("colorWhenDark", ["green", "default"]),
+ "Show SunInfo": {
+ value: (settings.showSunInfo !== undefined ? settings.showSunInfo : true),
+ format: v => v ? "On" : "Off",
+ onchange: v => {
+ settings.showSunInfo = v;
+ writeSettings();
+ }
+ }
+ };
+
+
+
+ // Actually display the menu
+ E.showMenu(mainmenu);
+
+});
+
+// end of file
diff --git a/apps/iconlaunch/app.js b/apps/iconlaunch/app.js
index 4eeaff589..59e9eb0e3 100644
--- a/apps/iconlaunch/app.js
+++ b/apps/iconlaunch/app.js
@@ -158,6 +158,8 @@ const itemsN = Math.ceil(apps.length / appsN);
Bangle.setUI({
mode: "custom",
drag: (e) => {
+ g.setColor(g.theme.fg);
+ g.setBgColor(g.theme.bg);
let dy = e.dy;
if (scroll + R.h - dy > itemsN * itemSize) {
dy = scroll + R.h - itemsN * itemSize;
diff --git a/apps/imageclock/ChangeLog b/apps/imageclock/ChangeLog
new file mode 100644
index 000000000..5b99b5848
--- /dev/null
+++ b/apps/imageclock/ChangeLog
@@ -0,0 +1,9 @@
+0.01: New App
+0.02: Allow drawing polys
+0.03: Allow partly importing Amazfit decompiler formatted watchfaces
+0.04: Allow writing all image data to separate file to save some RAM
+ Allow hiding elements on lock
+0.05: Add precompilation to js for performance
+0.06: Watchfaces can be refreshed partly
+0.07: Allow wrapping drawing in timeouts to get faster reactions
+ Show/Hide widgets with swipe up or down
diff --git a/apps/imageclock/README.md b/apps/imageclock/README.md
new file mode 100644
index 000000000..c05c09a16
--- /dev/null
+++ b/apps/imageclock/README.md
@@ -0,0 +1,297 @@
+# Imageclock
+
+This app is a highly customizable watchface. To use it, you need to select
+a watchface from another source. There is a native format as described here. You can also load decompiled watchfaces for Amazfit BIP fitness trackers.
+
+# Usage
+
+## Install a watchface
+
+Choose the folder which contains the watchface, then clock "Upload to watch".
+
+## Usage on the watch
+
+Slide up/down to hide/show widgets.
+Press button to start launcher.
+
+# Design watch faces
+
+## Folder structure
+
+* watchfacename
+ * resources/
+ * face.json
+ * info.json
+
+
+### resources
+
+This folder contains image files. It can have subfolders. These files will
+be read and converted into a resource bundle used by the clock
+
+Folder types:
+
+* Number
+ * Contains files named 0.ext to 9.ext and minus.ext
+* CodedImage
+ * Contains files named with only digits for codes, i.e. 721.ext
+ * Contains a file named fallback.ext in case no code matches
+ * Codes are evaluated as follows: 721 -> if not found check 720 -> if not found check 700 -> if found use
+* MultiState
+ * Notifications: sound.ext, silent.ext, vibrate.ext
+ * other status icons: on.ext, off.ext
+* Scale
+ * Contains the components of the scale, named 0.ext to y.ext, y beeing the last element of the scale
+
+### face.json
+
+This file contains the description of the watch face elements.
+
+#### Object types:
+
+##### Properties
+```
+Properties: {
+ "Redraw": {
+ "Unlocked": 5000,
+ "Locked": 60000,
+ "Default": "Always",
+ "Events": ["HRM"],
+ "Clear": true
+ },
+ "Events": ["lock","HRM"]
+}
+```
+
+Possible values for `Default` are `Always`, `Change`.
+
+##### Images
+
+```
+"Image": {
+ "X": 0,
+ "Y": 0,
+ "Scale": 1,
+ "RotationValue": "Seconds",
+ "MinRotationValue": 0,
+ "MaxRotationValue": 60,
+ "ImagePath": [ "path", "in", "resources", "file" ]
+}
+```
+
+`RotationValue` references one of the implemented numerical values.
+Mandatory:
+* `ImagePath`
+
+```
+"Image": {
+ "X": 0,
+ "Y": 0,
+ "Value": "BatteryPercentage",
+ "Steps": 7,
+ "ImagePath": [ "path", "in", "resources", "file" ]
+}
+```
+If `Value` and `Steps`are given, the value is scaled and `Steps` number of images starting at 0 are used to display the value.
+
+##### Coded Images
+```
+"CodedImage": {
+ "X": 0,
+ "Y": 0,
+ "Value": "WeatherCode",
+ "ImagePath": [ "path", "in", "resources", "file" ]
+}
+```
+The `Value` field is one of the implemented numerical values.
+
+##### Number
+
+Can be aligned to bottom left, top left, bottom right, top right and center. Will currently force all numbers to
+be integer.
+
+```
+"Number": {
+ "X": 123,
+ "Y": 123,
+ "Alignment": "BottomRight",
+ "Value": "Temperature",
+ "Spacing": 1,
+ "ImagePath": [ "path", "to", "numbers", "folder" ]
+}
+```
+The `Value` field is one of the implemented numerical values.
+`Alignment` is either `BottomRight` or `TopLeft`
+
+Mandatory:
+* `ImagePath`
+* `Value`
+
+##### Scale
+
+```
+"Scale": {
+ "X": 123,
+ "Y": 123,
+ "Value": "Temperature",
+ "MinValue": "-20",
+ "MaxValue": "50",
+ "ImagePath": [ "path", "to", "scale", "folder" ],
+ "Segments": [
+ { "X": 5, "Y": 5},
+ { "X": 10, "Y": 10 }
+ ]
+}
+```
+The `Value` field is one of the implemented numerical values.
+`MaxValue` and `MinValue` set the start and endpoints of the scale.
+
+Mandatory:
+* `ImagePath`
+* `Value`
+
+##### MultiState
+
+```
+"MultiState": {
+ "X": 0,
+ "Y": 0,
+ "Value": "Lock",
+ "ImagePath": ["icons", "status", "lock"]
+}
+```
+The `Value` field is one of the implemented multi state values.
+
+Mandatory:
+* `ImagePath`
+* `Value`
+
+##### Poly
+
+```
+"Poly":{
+ "Filled": true,
+ "RotationValue": "Second",
+ "MinRotationValue": "0",
+ "MaxRotationValue": "60",
+ "ForegroundColor": "#00f",
+ "BackgroundColor": "#008",
+ "Vertices":[
+ {"X":-1, "Y":13},
+ {"X":0, "Y":15},
+ {"X":1, "Y":13},
+ {"X":2, "Y":0},
+ {"X":1, "Y":-75},
+ {"X":0, "Y":-80},
+ {"X":-1, "Y":-75},
+ {"X":-2, "Y":0}
+ ]
+}
+```
+The `RotationValue` field is one of the implemented numeric values.
+
+##### Rect
+
+```
+"Rect":{
+ "X": 10,
+ "Y": 20,
+ "Width": 30,
+ "Height": 40,
+ "Filled": true,
+ "ForegroundColor": "#00f",
+ "BackgroundColor": "#008"
+}
+```
+The `RotationValue` field is one of the implemented numeric values.
+
+##### Nesting
+```
+"Container": {
+ "X": 10,
+ "Y": 10,
+ "OtherContainer": {
+ "X": 5,
+ "Y": 5,
+ "SomeElement": {
+ "X": 2,
+ "Y": 2,
+
+ }
+ }
+}
+```
+`SomeElement` will be drawn at X- and Y-position 2+5+10=17, because positions are relative to parent element.
+Container names can be everything but other object names.
+
+#### Implemented data sources
+
+##### Numerical
+
+* Hour
+* Hour12Analog
+* Hour12
+* HourTens
+* HourOnes
+* Minute
+* MinuteAnalog
+* MinuteTens
+* MinuteOnes
+* Second
+* SecondAnalog
+* SecondTens
+* SecondOnes
+* WeekDay
+* WeekDayMondayFirst
+* Day
+* DayTens
+* DayOnes
+* Month
+* MonthTens
+* MonthOnes
+* Pulse
+* Steps
+* Temperature
+* Pressure
+* Altitude
+* BatteryPercentage
+* BatteryVoltage
+* StepsGoal
+* WeatherCode
+* WeatherTemperature
+
+##### Multistate
+
+* on/off
+ * Lock
+ * Charge
+ * Alarm
+ * Bluetooth
+ * BluetoothPeripheral
+ * HRM
+ * Barometer
+ * Compass
+ * GPS
+ * StepsGoal
+ * WeatherTemperatureNegative
+* on/off/vibrate
+ * Notifications
+* celsius/fahrenheit/unknown
+ * WeatherTemperatureUnit
+
+### info.json
+
+This file contains information for the conversion process, it will not be
+stored on the watch
+
+# TODO
+
+* Handle events and redraws better
+* Performance improvements
+ * Mark elements with how often they need to be redrawn
+* Finalize the file format
+* Localization
+
+# Creator
+
+[halemmerich](https://github.com/halemmerich)
diff --git a/apps/imageclock/app-icon.js b/apps/imageclock/app-icon.js
new file mode 100644
index 000000000..06f93e2ef
--- /dev/null
+++ b/apps/imageclock/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/imageclock/app.js b/apps/imageclock/app.js
new file mode 100644
index 000000000..7b933b710
--- /dev/null
+++ b/apps/imageclock/app.js
@@ -0,0 +1,774 @@
+var watchface = require("Storage").readJSON("imageclock.face.json");
+var watchfaceResources = require("Storage").readJSON("imageclock.resources.json");
+var precompiledJs = eval(require("Storage").read("imageclock.draw.js"));
+var settings = require('Storage').readJSON("imageclock.json", true) || {};
+
+var performanceLog = {};
+
+var startPerfLog = () => {};
+var endPerfLog = () => {};
+var printPerfLog = () => print("Deactivated");
+var resetPerfLog = () => {performanceLog = {};};
+
+var colormap={
+"#000":0,
+"#00f":1,
+"#0f0":2,
+"#0ff":3,
+"#f00":4,
+"#f0f":5,
+"#ff0":6,
+"#fff":7
+};
+
+var palette = new Uint16Array([
+0x0000, //black #000
+0x001f, //blue #00f
+0x07e0, //green #0f0
+0x07ff, //cyan #0ff
+0xf800, //red #f00
+0xf81f, //magenta #f0f
+0xffe0, //yellow #ff0
+0xffff, //white #fff
+0xffff, //white
+0xffff, //white
+0xffff, //white
+0xffff, //white
+0xffff, //white
+0xffff, //white
+0xffff, //white
+0xffff, //white
+])
+
+var p0 = g;
+var p1;
+
+if (settings.perflog){
+ startPerfLog = function(name){
+ var time = getTime();
+ if (!performanceLog.start) performanceLog.start={};
+ performanceLog.start[name] = time;
+ };
+ endPerfLog = function (name){
+ var time = getTime();
+ if (!performanceLog.last) performanceLog.last={};
+ var duration = time - performanceLog.start[name];
+ performanceLog.last[name] = duration;
+ if (!performanceLog.cum) performanceLog.cum={};
+ if (!performanceLog.cum[name]) performanceLog.cum[name] = 0;
+ performanceLog.cum[name] += duration;
+ if (!performanceLog.count) performanceLog.count={};
+ if (!performanceLog.count[name]) performanceLog.count[name] = 0;
+ performanceLog.count[name]++;
+ };
+
+ printPerfLog = function(){
+ var result = "";
+ var keys = [];
+ for (var c in performanceLog.cum){
+ keys.push(c);
+ }
+ keys.sort();
+ for (var k of keys){
+ print(k, "last:", (performanceLog.last[k] * 1000).toFixed(0), "average:", (performanceLog.cum[k]/performanceLog.count[k]*1000).toFixed(0), "count:", performanceLog.count[k], "total:", (performanceLog.cum[k] * 1000).toFixed(0));
+ }
+ };
+}
+
+function delay(t) {
+ return new Promise(function (resolve) {
+ setTimeout(resolve, t);
+ });
+}
+
+function prepareImg(resource){
+ startPerfLog("prepareImg");
+ //print("prepareImg: ", resource);
+
+ if (resource.dataOffset !== undefined){
+ resource.buffer = E.toArrayBuffer(require("Storage").read("imageclock.resources.data", resource.dataOffset, resource.dataLength));
+ delete resource.dataOffset;
+ delete resource.dataLength;
+ if (resource.paletteData){
+ result.palette = new Uint16Array(resource.paletteData);
+ delete resource.paletteData;
+ }
+ }
+ endPerfLog("prepareImg");
+ return resource;
+}
+
+function getByPath(object, path, lastElem){
+ startPerfLog("getByPath");
+ //print("getByPath", path,lastElem);
+ var current = object;
+ if (path.length) {
+ for (var c of path){
+ if (!current[c]) return undefined;
+ current = current[c];
+ }
+ }
+ if (lastElem!==undefined){
+ if (!current["" + lastElem]) return undefined;
+ //print("Found by lastElem", lastElem);
+ current = current["" + lastElem];
+ }
+ endPerfLog("getByPath");
+ if (typeof current == "function"){
+ //print("current was function");
+ return undefined;
+ }
+ return current;
+}
+
+function splitNumberToDigits(num){
+ return String(num).split('').map(item => Number(item));
+}
+
+function isChangedNumber(element){
+ return element.lastDrawnValue != getValue(element.Value);
+}
+
+function isChangedMultistate(element){
+ return element.lastDrawnValue != getMultistate(element.Value);
+}
+
+function drawNumber(graphics, resources, element){
+ startPerfLog("drawNumber");
+ var number = getValue(element.Value);
+ var spacing = element.Spacing ? element.Spacing : 0;
+ var unit = element.Unit;
+
+ var imageIndexMinus = element.ImageIndexMinus;
+ var imageIndexUnit = element.ImageIndexUnit;
+ var numberOfDigits = element.Digits;
+
+
+ //print("drawNumber: ", number, element);
+ if (number) number = number.toFixed(0);
+
+ var isNegative;
+ var digits;
+ if (number == undefined){
+ isNegative = true;
+ digits = [];
+ numberOfDigits = 0;
+ } else {
+ isNegative = number < 0;
+ if (isNegative) number *= -1;
+ digits = splitNumberToDigits(number);
+ }
+
+ //print("digits: ", digits);
+ if (!numberOfDigits) numberOfDigits = digits.length;
+ var firstDigitX = element.X;
+ var firstDigitY = element.Y;
+ var imageIndex = element.ImageIndex ? element.ImageIndex : 0;
+
+ var firstImage;
+ if (imageIndex){
+ firstImage = getByPath(resources, [], "" + (0 + imageIndex));
+ } else {
+ firstImage = getByPath(resources, element.ImagePath, 0);
+ }
+
+ var minusImage;
+ if (imageIndexMinus){
+ minusImage = getByPath(resources, [], "" + (0 + imageIndexMinus));
+ } else {
+ minusImage = getByPath(resources, element.ImagePath, "minus");
+ }
+
+ var unitImage;
+ //print("Get image for unit", imageIndexUnit);
+ if (imageIndexUnit !== undefined){
+ unitImage = getByPath(resources, [], "" + (0 + imageIndexUnit));
+ //print("Unit image is", unitImage);
+ } else if (element.Unit){
+ unitImage = getByPath(resources, element.ImagePath, getMultistate(element.Unit, "unknown"));
+ }
+
+ var numberWidth = (numberOfDigits * firstImage.width) + (Math.max((numberOfDigits - 1),0) * spacing);
+ if (isNegative && minusImage){
+ //print("Adding to width", minusImage);
+ numberWidth += minusImage.width + spacing;
+ }
+ if (unitImage){
+ //print("Adding to width", unitImage);
+ numberWidth += unitImage.width + spacing;
+ }
+ //print("numberWidth:", numberWidth);
+
+ if (element.Alignment == "Center") {
+ firstDigitX = Math.round(element.X - (numberWidth/2)) + 1;
+ firstDigitY = Math.round(element.Y - (firstImage.height/2)) + 1;
+ } else if (element.Alignment == "BottomRight"){
+ firstDigitX = element.X - numberWidth + 1;
+ firstDigitY = element.Y - firstImage.height + 1;
+ } else if (element.Alignment == "TopRight") {
+ firstDigitX = element.X - numberWidth + 1;
+ firstDigitY = element.Y;
+ } else if (element.Alignment == "BottomLeft") {
+ firstDigitX = element.X;
+ firstDigitY = element.Y - firstImage.height + 1;
+ }
+
+ var currentX = firstDigitX;
+ if (isNegative && minusImage){
+ //print("Draw minus at", currentX);
+ if (imageIndexMinus){
+ drawElement(graphics, resources, {X:currentX,Y:firstDigitY}, element, "" + (0 + imageIndexMinus));
+ } else {
+ drawElement(graphics, resources, {X:currentX,Y:firstDigitY}, element, "minus");
+ }
+ currentX += minusImage.width + spacing;
+ }
+ for (var d = 0; d < numberOfDigits; d++){
+ var currentDigit;
+ var difference = numberOfDigits - digits.length;
+ if (d >= difference){
+ currentDigit = digits[d-difference];
+ } else {
+ currentDigit = 0;
+ }
+ //print("Digit " + currentDigit + " " + currentX);
+ drawElement(graphics, resources, {X:currentX,Y:firstDigitY}, element, currentDigit + imageIndex);
+ currentX += firstImage.width + spacing;
+ }
+ if (imageIndexUnit){
+ //print("Draw unit at", currentX);
+ drawElement(graphics, resources, {X:currentX,Y:firstDigitY}, element, "" + (0 + imageIndexUnit));
+ } else if (element.Unit){
+ drawElement(graphics, resources, {X:currentX,Y:firstDigitY}, element, getMultistate(element.Unit,"unknown"));
+ }
+ element.lastDrawnValue = number;
+
+ endPerfLog("drawNumber");
+}
+
+function drawElement(graphics, resources, pos, element, lastElem){
+ startPerfLog("drawElement");
+ var cacheKey = "_"+(lastElem?lastElem:"nole");
+ if (!element.cachedImage) element.cachedImage={};
+ if (!element.cachedImage[cacheKey]){
+ var resource = getByPath(resources, element.ImagePath, lastElem);
+ if (resource){
+ prepareImg(resource);
+ //print("lastElem", typeof resource)
+ if (resource) {
+ element.cachedImage[cacheKey] = resource;
+ //print("cache res ",typeof element.cachedImage[cacheKey]);
+ } else {
+ element.cachedImage[cacheKey] = null;
+ //print("cache null",typeof element.cachedImage[cacheKey]);
+ //print("Could not create image from", resource);
+ }
+ } else {
+ //print("Could not get resource from", element, lastElem);
+ }
+ }
+
+ //print("cache ",typeof element.cachedImage[cacheKey], element.ImagePath, lastElem);
+ if(element.cachedImage[cacheKey]){
+ //print("drawElement ",pos, path, lastElem);
+ //print("resource ", resource,pos, path, lastElem);
+ //print("drawImage from drawElement", image, pos);
+ var options={};
+ if (element.RotationValue){
+ options.rotate = radians(element);
+ }
+ if (element.Scale){
+ options.scale = element.ScaleValue;
+ }
+ //print("options", options);
+ //print("Memory before drawing", process.memory(false));
+ startPerfLog("drawElement_g.drawImage");
+ try{
+ graphics.drawImage(element.cachedImage[cacheKey] ,(pos.X ? pos.X : 0), (pos.Y ? pos.Y : 0), options);} catch (e) {
+ //print("Error", e, element.cachedImage[cacheKey]);
+ }
+ endPerfLog("drawElement_g.drawImage");
+ }
+ endPerfLog("drawElement");
+}
+
+function getValue(value, defaultValue){
+ if (typeof value == "string"){
+ return numbers[value]();
+ }
+ if (value == undefined) return defaultValue;
+ return value;
+}
+
+function getMultistate(name, defaultValue){
+ if (typeof name == "string"){
+ return multistates[name]();
+ } else {
+ if (name == undefined) return defaultValue;
+ }
+ return undefined;
+}
+
+function drawScale(graphics, resources, scale){
+ startPerfLog("drawScale");
+ //print("drawScale", scale);
+ var segments = scale.Segments;
+ var imageIndex = scale.ImageIndex !== undefined ? scale.ImageIndex : 0;
+
+ var value = scaledown(scale.Value, scale.MinValue, scale.MaxValue);
+
+ //print("Value is ", value, "(", maxValue, ",", minValue, ")");
+
+ var segmentsToDraw = Math.ceil(value * segments.length);
+
+ for (var i = 0; i < segmentsToDraw; i++){
+ drawElement(graphics, resources, segments[i], scale, imageIndex + i);
+ }
+ scale.lastDrawnValue = segmentsToDraw;
+
+ endPerfLog("drawScale");
+}
+
+function drawImage(graphics, resources, image, name){
+ startPerfLog("drawImage");
+ //print("drawImage", image.X, image.Y, name);
+ if (image.Value && image.Steps){
+ var steps = Math.floor(scaledown(image.Value, image.MinValue, image.MaxValue) * (image.Steps - 1));
+ //print("Step", steps, "of", image.Steps);
+ drawElement(graphics, resources, image, image, "" + steps);
+ } else if (image.ImageIndex !== undefined) {
+ drawElement(graphics, resources, image, image, image.ImageIndex);
+ } else {
+ drawElement(graphics, resources, image, image, name ? "" + name: undefined);
+ }
+
+ endPerfLog("drawImage");
+}
+
+function drawCodedImage(graphics, resources, image){
+ startPerfLog("drawCodedImage");
+ var code = getValue(image.Value);
+ //print("drawCodedImage", image, code);
+
+ if (image.ImagePath) {
+ var factor = 1;
+ var currentCode = code;
+ while (code / factor > 1){
+ currentCode = Math.floor(currentCode/factor)*factor;
+ //print("currentCode", currentCode);
+ if (getByPath(resources, image.ImagePath, currentCode)){
+ break;
+ }
+ factor *= 10;
+ }
+ if (code / factor > 1){
+ //print("found match");
+ drawImage(graphics, resources, image, currentCode);
+ } else {
+ //print("fallback");
+ drawImage(graphics, resources, image, "fallback");
+ }
+ }
+ image.lastDrawnValue = code;
+
+ startPerfLog("drawCodedImage");
+}
+
+function getWeatherCode(){
+ var jsonWeather = require("Storage").readJSON('weather.json');
+ var weather = (jsonWeather && jsonWeather.weather) ? jsonWeather.weather : undefined;
+
+ if (weather && weather.code){
+ return weather.code;
+ }
+ return undefined;
+}
+
+function getWeatherTemperature(){
+ var jsonWeather = require("Storage").readJSON('weather.json');
+ var weather = (jsonWeather && jsonWeather.weather) ? jsonWeather.weather : undefined;
+
+ var result = { unit: "unknown"};
+ if (weather && weather.temp){
+ //print("Weather is", weather);
+ var temp = require('locale').temp(weather.temp-273.15);
+ result.value = Number(temp.match(/[\d\-]*/)[0]);
+ var unit;
+ if (temp.includes("C")){
+ result.unit = "celsius";
+ } else if (temp.includes("F")){
+ result.unit = "fahrenheit";
+ }
+ }
+ return result;
+}
+
+function scaledown(value, min, max){
+ //print("scaledown", value, min, max);
+ var scaled = E.clip(getValue(value),getValue(min,0),getValue(max,1));
+ scaled -= getValue(min,0);
+ scaled /= getValue(max,1);
+ return scaled;
+}
+
+function radians(rotation){
+ var value = scaledown(rotation.RotationValue, rotation.MinRotationValue, rotation.MaxRotationValue);
+ value -= rotation.RotationOffset ? rotation.RotationOffset : 0;
+ value *= 360;
+ value *= Math.PI / 180;
+ return value;
+}
+
+function drawPoly(graphics, resources, element){
+ startPerfLog("drawPoly");
+ var vertices = [];
+
+ startPerfLog("drawPoly_transform");
+ for (var c of element.Vertices){
+ vertices.push(c.X);
+ vertices.push(c.Y);
+ }
+ var transform = { x: element.X ? element.X : 0,
+ y: element.Y ? element.Y : 0
+ };
+ if (element.RotationValue){
+ transform.rotate = radians(element);
+ }
+ vertices = graphics.transformVertices(vertices, transform);
+
+ endPerfLog("drawPoly_transform");
+
+ if (element.Filled){
+ startPerfLog("drawPoly_g.fillPoly");
+ graphics.fillPoly(vertices,true);
+ endPerfLog("drawPoly_g.fillPoly");
+ } else {
+ startPerfLog("drawPoly_g.drawPoly");
+ graphics.drawPoly(vertices,true);
+ endPerfLog("drawPoly_g.drawPoly");
+ }
+
+ endPerfLog("drawPoly");
+}
+
+function drawRect(graphics, resources, element){
+ startPerfLog("drawRect");
+ var vertices = [];
+
+ if (element.Filled){
+ startPerfLog("drawRect_g.fillRect");
+ graphics.fillRect(element.X, element.Y, element.X + element.Width, element.Y + element.Height);
+ endPerfLog("drawRect_g.fillRect");
+ } else {
+ startPerfLog("drawRect_g.fillRect");
+ graphics.drawRect(element.X, element.Y, element.X + element.Width, element.Y + element.Height);
+ endPerfLog("drawRect_g.fillRect");
+ }
+ endPerfLog("drawRect");
+}
+
+function drawCircle(graphics, resources, element){
+ startPerfLog("drawCircle");
+
+ if (element.Filled){
+ startPerfLog("drawCircle_g.fillCircle");
+ graphics.fillCircle(element.X, element.Y, element.Radius);
+ endPerfLog("drawCircle_g.fillCircle");
+ } else {
+ startPerfLog("drawCircle_g.drawCircle");
+ graphics.drawCircle(element.X, element.Y, element.Radius);
+ endPerfLog("drawCircle_g.drawCircle");
+ }
+ endPerfLog("drawCircle");
+}
+
+var numbers = {};
+numbers.Hour = () => { return new Date().getHours(); };
+numbers.HourTens = () => { return Math.floor(new Date().getHours()/10); };
+numbers.HourOnes = () => { return Math.floor(new Date().getHours()%10); };
+numbers.Hour12 = () => { return new Date().getHours()%12; };
+numbers.Hour12Analog = () => { var date = new Date(); return date.getHours()%12 + (date.getMinutes()/59); };
+numbers.Hour12Tens = () => { return Math.floor((new Date().getHours()%12)/10); };
+numbers.Hour12Ones = () => { return Math.floor((new Date().getHours()%12)%10); };
+numbers.Minute = () => { return new Date().getMinutes(); };
+numbers.MinuteAnalog = () => { var date = new Date(); return date.getMinutes() + (date.getSeconds()/59); };
+numbers.MinuteTens = () => { return Math.floor(new Date().getMinutes()/10); };
+numbers.MinuteOnes = () => { return Math.floor(new Date().getMinutes()%10); };
+numbers.Second = () => { return new Date().getSeconds(); };
+numbers.SecondAnalog = () => { var date = new Date(); return date.getSeconds() + (date.getMilliseconds()/999); };
+numbers.SecondTens = () => { return Math.floor(new Date().getSeconds()/10); };
+numbers.SecondOnes = () => { return Math.floor(new Date().getSeconds()%10); };
+numbers.WeekDay = () => { return new Date().getDay(); };
+numbers.WeekDayMondayFirst = () => { var day = (new Date().getDay() - 1); if (day < 0) day = 7 + day; return day; };
+numbers.Day = () => { return new Date().getDate(); };
+numbers.DayTens = () => { return Math.floor(new Date().getDate()/10); };
+numbers.DayOnes = () => { return Math.floor(new Date().getDate()%10); };
+numbers.Month = () => { return new Date().getMonth() + 1; };
+numbers.MonthTens = () => { return Math.floor((new Date().getMonth() + 1)/10); };
+numbers.MonthOnes = () => { return Math.floor((new Date().getMonth() + 1)%10); };
+numbers.Pulse = () => { return pulse; };
+numbers.Steps = () => { return Bangle.getHealthStatus ? Bangle.getHealthStatus("day").steps : undefined; };
+numbers.StepsGoal = () => { return settings.stepsgoal || 10000; };
+numbers.Temperature = () => { return temp; };
+numbers.Pressure = () => { return press; };
+numbers.Altitude = () => { return alt; };
+numbers.BatteryPercentage = E.getBattery;
+numbers.BatteryVoltage = NRF.getBattery;
+numbers.WeatherCode = getWeatherCode;
+numbers.WeatherTemperature = () => { return getWeatherTemperature().value; };
+
+var multistates = {};
+multistates.Lock = () => { return Bangle.isLocked() ? "on" : "off"; };
+multistates.Charge = () => { return Bangle.isCharging() ? "on" : "off"; };
+multistates.Notifications = () => { return ((require("Storage").readJSON("setting.json", 1) || {}).quiet|0) ? "off" : "vibrate"; };
+multistates.Alarm = () => { return (require('Storage').readJSON('alarm.json',1)||[]).some(alarm=>alarm.on) ? "on" : "off"; };
+multistates.Bluetooth = () => { return NRF.getSecurityStatus().connected ? "on" : "off"; };
+//TODO: Implement peripheral connection status
+multistates.BluetoothPeripheral = () => { return NRF.getSecurityStatus().connected ? "on" : "off"; };
+multistates.HRM = () => { return Bangle.isHRMOn ? "on" : "off"; };
+multistates.Barometer = () => { return Bangle.isBarometerOn() ? "on" : "off"; };
+multistates.Compass = () => { return Bangle.isCompassOn() ? "on" : "off"; };
+multistates.GPS = () => { return Bangle.isGPSOn() ? "on" : "off"; };
+multistates.WeatherTemperatureNegative = () => { return getWeatherTemperature().value ? getWeatherTemperature().value : 0 < 0; };
+multistates.WeatherTemperatureUnit = () => { return getWeatherTemperature().unit; };
+multistates.StepsGoal = () => { return (numbers.Steps() >= (settings.stepsgoal || 10000)) ? "on": "off"; };
+
+function drawMultiState(graphics, resources, element){
+ startPerfLog("drawMultiState");
+ //print("drawMultiState", element);
+ var value = multistates[element.Value]();
+ //print("drawImage from drawMultiState", element, value);
+ drawImage(graphics, resources, element, value);
+ element.lastDrawnValue = value;
+ endPerfLog("drawMultiState");
+}
+
+var pulse,alt,temp,press;
+
+
+var requestedDraws = 0;
+var isDrawing = false;
+
+var drawingTime;
+
+var start;
+
+function initialDraw(resources, face){
+ //print("Free memory", process.memory(false).free);
+ requestedDraws++;
+ if (!isDrawing){
+ //print(new Date().toISOString(), "Can draw,", requestedDraws, "draws requested so far");
+ isDrawing = true;
+ resetPerfLog();
+ requestedDraws = 0;
+ //print(new Date().toISOString(), "Drawing start");
+ startPerfLog("initialDraw");
+ //start = Date.now();
+ drawingTime = 0;
+ //print("Precompiled");
+ var promise = precompiledJs(watchfaceResources, watchface);
+
+ promise.then(()=>{
+ var currentDrawingTime = Date.now();
+ if (showWidgets){
+ //print("Draw widgets");
+ Bangle.drawWidgets();
+ g.setColor(g.theme.fg);
+ g.drawLine(0,24,g.getWidth(),24);
+ }
+ lastDrawTime = Date.now() - start;
+ drawingTime += Date.now() - currentDrawingTime;
+ //print(new Date().toISOString(), "Drawing done in", lastDrawTime.toFixed(0), "active:", drawingTime.toFixed(0));
+ isDrawing=false;
+ firstDraw=false;
+ requestRefresh = false;
+ endPerfLog("initialDraw");
+ }).catch((e)=>{
+ print("Error during drawing", e);
+ });
+
+ if (requestedDraws > 0){
+ //print(new Date().toISOString(), "Had deferred drawing left, drawing again");
+ requestedDraws = 0;
+ setTimeout(()=>{initialDraw(resources, face);}, 10);
+ }
+ } //else {
+ //print("queued draw");
+ //}
+}
+
+function handleHrm(e){
+ if (e.confidence > 70){
+ pulse = e.bpm;
+ if (!redrawEvents || redrawEvents.includes("HRM") && !Bangle.isLocked()){
+ //print("Redrawing on HRM");
+ initialDraw(watchfaceResources, watchface);
+ }
+ }
+}
+
+function handlePressure(e){
+ alt = e.altitude;
+ temp = e.temperature;
+ press = e.pressure;
+ if (!redrawEvents || redrawEvents.includes("pressure") && !Bangle.isLocked()){
+ //print("Redrawing on pressure");
+ initialDraw(watchfaceResources, watchface);
+ }
+}
+
+function handleCharging(e){
+ if (!redrawEvents || redrawEvents.includes("charging") && !Bangle.isLocked()){
+ //print("Redrawing on charging");
+ initialDraw(watchfaceResources, watchface);
+ }
+}
+
+
+function getMatchedWaitingTime(time){
+ var result = time - (Date.now() % time);
+ //print("Matched timeout", time, result);
+ return result;
+}
+
+
+
+function setMatchedInterval(callable, time, intervalHandler, delay){
+ //print("Setting matched interval for", time);
+ var matchedTime = getMatchedWaitingTime(time + delay);
+ setTimeout(()=>{
+ var interval = setInterval(callable, time);
+ if (intervalHandler) intervalHandler(interval);
+ callable();
+ }, matchedTime);
+}
+
+var unlockedDrawInterval;
+var lockedDrawInterval;
+
+var lastDrawTime = 0;
+var firstDraw = true;
+
+var lockedRedraw = getByPath(watchface, ["Properties","Redraw","Locked"]) || 60000;
+var unlockedRedraw = getByPath(watchface, ["Properties","Redraw","Unlocked"]) || 1000;
+var defaultRedraw = getByPath(watchface, ["Properties","Redraw","Default"]) || "Always";
+var redrawEvents = getByPath(watchface, ["Properties","Redraw","Events"]);
+var clearOnRedraw = getByPath(watchface, ["Properties","Redraw","Clear"]);
+var events = getByPath(watchface, ["Properties","Events"]);
+
+//print("events", events);
+//print("redrawEvents", redrawEvents);
+
+function handleLock(isLocked, forceRedraw){
+ //print("isLocked", Bangle.isLocked());
+ if (lockedDrawInterval) clearInterval(lockedDrawInterval);
+ if (unlockedDrawInterval) clearInterval(unlockedDrawInterval);
+ if (!isLocked){
+ if (forceRedraw || !redrawEvents || (redrawEvents.includes("unlock"))){
+ //print("Redrawing on unlock", isLocked);
+ initialDraw(watchfaceResources, watchface);
+ }
+ setMatchedInterval(()=>{
+ //print("Redrawing on unlocked interval");
+ initialDraw(watchfaceResources, watchface);
+ },unlockedRedraw, (v)=>{
+ unlockedDrawInterval = v;
+ }, lastDrawTime);
+ if (!events || events.includes("HRM")) Bangle.setHRMPower(1, "imageclock");
+ if (!events || events.includes("pressure")) Bangle.setBarometerPower(1, 'imageclock');
+ } else {
+ if (forceRedraw || !redrawEvents || (redrawEvents.includes("lock"))){
+ //print("Redrawing on lock", isLocked);
+ initialDraw(watchfaceResources, watchface);
+ }
+ setMatchedInterval(()=>{
+ //print("Redrawing on locked interval");
+ initialDraw(watchfaceResources, watchface);
+ },lockedRedraw, (v)=>{
+ lockedDrawInterval = v;
+ }, lastDrawTime);
+ Bangle.setHRMPower(0, "imageclock");
+ Bangle.setBarometerPower(0, 'imageclock');
+ }
+}
+
+
+var showWidgets = false;
+var showWidgetsChanged = false;
+var currentDragDistance = 0;
+
+Bangle.setUI("clock");
+Bangle.on('drag', (e)=>{
+ currentDragDistance += e.dy;
+ if (Math.abs(currentDragDistance) < 10) return;
+ dragDown = currentDragDistance > 0;
+ currentDragDistance = 0;
+ if (!showWidgets && dragDown){
+ //print("Enable widgets");
+ if (WIDGETS && typeof WIDGETS === "object") {
+ for (let w in WIDGETS) {
+ var wd = WIDGETS[w];
+ wd.draw = originalWidgetDraw[w];
+ wd.area = originalWidgetArea[w];
+ }
+ }
+ showWidgetsChanged = true;
+ }
+ if (showWidgets && !dragDown){
+ //print("Disable widgets");
+ clearWidgetsDraw();
+ firstDraw = true;
+ showWidgetsChanged = true;
+ }
+ if (showWidgetsChanged){
+ showWidgetsChanged = false;
+ //print("Draw after widget change");
+ showWidgets = dragDown;
+ initialDraw();
+ }
+ }
+);
+
+if (!events || events.includes("pressure")){
+ Bangle.on('pressure', handlePressure);
+ try{
+ Bangle.setBarometerPower(1, 'imageclock');
+ } catch (e){
+ print("Error during barometer power up", e);
+ }
+}
+if (!events || events.includes("HRM")) {
+ Bangle.on('HRM', handleHrm);
+ Bangle.setHRMPower(1, "imageclock");
+}
+if (!events || events.includes("lock")) {
+ Bangle.on('lock', handleLock);
+}
+if (!events || events.includes("charging")) {
+ Bangle.on('charging', handleCharging);
+}
+
+var originalWidgetDraw = {};
+var originalWidgetArea = {};
+
+function clearWidgetsDraw(){
+ //print("Clear widget draw calls");
+ if (WIDGETS && typeof WIDGETS === "object") {
+ originalWidgetDraw = {};
+ originalWidgetArea = {};
+ for (let w in WIDGETS) {
+ var wd = WIDGETS[w];
+ originalWidgetDraw[w] = wd.draw;
+ originalWidgetArea[w] = wd.area;
+ wd.draw = () => {};
+ wd.area = "";
+ }
+ }
+}
+
+setTimeout(()=>{
+ Bangle.loadWidgets();
+ clearWidgetsDraw();
+}, 0);
+
+handleLock(Bangle.isLocked());
diff --git a/apps/imageclock/app.png b/apps/imageclock/app.png
new file mode 100644
index 000000000..cf057046b
Binary files /dev/null and b/apps/imageclock/app.png differ
diff --git a/apps/imageclock/custom.html b/apps/imageclock/custom.html
new file mode 100644
index 000000000..af7a1835f
--- /dev/null
+++ b/apps/imageclock/custom.html
@@ -0,0 +1,1144 @@
+
+
+
+
+
+
+
+
+
+
+
+ Upload watchface:
+
+
+ Select format:
+
+
+
+
+
+
+
+ Options:
+
+
+
+
+
+
+ Select watchface folder:
+ or
+ Select watchface zip file:
+
+
+
+
+
+
+ Download Demo Watchface here:
+ digitalretro.zip
+ simpleanalog.zip
+
+
+
+
+
+
diff --git a/apps/imageclock/demomode.js b/apps/imageclock/demomode.js
new file mode 100644
index 000000000..8c9d19195
--- /dev/null
+++ b/apps/imageclock/demomode.js
@@ -0,0 +1,29 @@
+var demostate = 0;
+function demoMode(){
+ lockedRedraw = 2000;
+ unlockedRedraw = 2000;
+ for (var c in multistates){
+ multistates[c] = ()=>{return ["on","off"][demostate%2];};
+ if (c == "WeatherTemperatureUnit") multistates[c] = ()=>{return ["celsius","fahrenheit"][demostate%2];};
+ if (c == "Notifications") multistates[c] = ()=>{return ["on","off","vibrate"][demostate%3];};
+ }
+ for (var c in numbers){
+ if (c.contains("Minute")) numbers[c] = ()=>{return Math.floor((Math.random() * 9) + 1);};
+ if (c.contains("Second")) numbers[c] = ()=>{return Math.floor((Math.random() * 9) + 1);};
+ if (c.contains("Hour")) numbers[c] = ()=>{return Math.floor((Math.random() * 9) + 1);};
+ }
+ for (var c in numbers){
+ if (c.contains("Ones")) numbers[c] = ()=>{return Math.floor((Math.random() * 9) + 1);};
+ if (c.contains("Tens")) numbers[c] = ()=>{return Math.floor((Math.random() * 9) + 1);};
+ }
+ numbers.Pulse = ()=>{return Math.floor((Math.random() * 60) + 40);};
+ numbers.Steps = ()=>{return Math.floor((Math.random() * 10000) + 10);};
+ numbers.Temperature = ()=>{return Math.floor((Math.random() * 15) + 10);};
+ numbers.Pressure = ()=>{return Math.floor((Math.random() * 1000) + 10);};
+ numbers.Altitude = ()=>{return Math.floor((Math.random() * 1000) + 10);};
+ numbers.WeatherCode = ()=>{return Math.floor((Math.random() * 800) + 100);};
+ numbers.WeatherTemperature = ()=>{return Math.floor((Math.random() * 10) + 0);};
+}
+demoMode();
+handleLock(false);
+setInterval(()=>{demostate++;},1000);
diff --git a/apps/imageclock/digitalretro.zip b/apps/imageclock/digitalretro.zip
new file mode 100644
index 000000000..96ab90b6d
Binary files /dev/null and b/apps/imageclock/digitalretro.zip differ
diff --git a/apps/imageclock/metadata.json b/apps/imageclock/metadata.json
new file mode 100644
index 000000000..a2594653e
--- /dev/null
+++ b/apps/imageclock/metadata.json
@@ -0,0 +1,19 @@
+{
+ "id": "imageclock",
+ "name": "Imageclock",
+ "shortName": "Imageclock",
+ "version": "0.07",
+ "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",
+ "tags": "clock",
+ "supports": ["BANGLEJS2"],
+ "custom": "custom.html",
+ "customConnect": false,
+ "readme": "README.md",
+ "storage": [
+ {"name":"imageclock.app.js","url":"app.js"},
+ {"name":"imageclock.settings.js","url":"settings.js"},
+ {"name":"imageclock.img","url":"app-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/imageclock/settings.js b/apps/imageclock/settings.js
new file mode 100644
index 000000000..a86901b9e
--- /dev/null
+++ b/apps/imageclock/settings.js
@@ -0,0 +1,35 @@
+(function(back) {
+ var FILE = "imageclock.json";
+
+ var settings = Object.assign({
+ stepsgoal: 10000,
+ perflog: false
+ }, require('Storage').readJSON(FILE, true) || {});
+
+ function writeSettings() {
+ require('Storage').writeJSON(FILE, settings);
+ }
+
+ E.showMenu({
+ '': { 'title': 'Imageclock' },
+ '< Back': back,
+ 'Steps goal': {
+ value: settings.stepsgoal,
+ min: 0,
+ step: 500,
+ max: 50000,
+ onchange: v => {
+ settings.stepsgoal = v;
+ writeSettings();
+ }
+ },
+ 'Performance log': {
+ value: !!settings.perflog,
+ format: v => settings.perflog ? "On" : "Off",
+ onchange: v => {
+ settings.perflog = v;
+ writeSettings();
+ }
+ }
+ });
+})
diff --git a/apps/imageclock/simpleanalog.zip b/apps/imageclock/simpleanalog.zip
new file mode 100644
index 000000000..1301be055
Binary files /dev/null and b/apps/imageclock/simpleanalog.zip differ
diff --git a/apps/invader/ChangeLog b/apps/invader/ChangeLog
new file mode 100644
index 000000000..5560f00bc
--- /dev/null
+++ b/apps/invader/ChangeLog
@@ -0,0 +1 @@
+0.01: New App!
diff --git a/apps/invader/README.md b/apps/invader/README.md
new file mode 100644
index 000000000..7ed98defb
--- /dev/null
+++ b/apps/invader/README.md
@@ -0,0 +1,23 @@
+# App Name
+
+Invader
+
+## Usage
+
+For fun! - I'm creating this demo to learn JavaScript with the Bangle.js 2
+
+## Features
+
+Shoot the Alien, you have three lives
+
+## Controls
+
+Touch the lower Left or Right hand sides of the screen to move turret left or right, tap upper Right hand part of screen to fire, tap upper Left hand part of screen to restart
+
+## Requests
+
+bkumanchik on Espruino Forums
+
+## Creator
+
+Brian Kumanchik 2022
diff --git a/apps/invader/app-icon.js b/apps/invader/app-icon.js
new file mode 100644
index 000000000..dc7003c84
--- /dev/null
+++ b/apps/invader/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwhHXAH4A/AH4A/AH4AYgAACC/4X/C/YbTFbYv/F/4rTAC4v/F/4vfC5YnPGaYv/F/4vLc7b3TF/4v/F/4v/F/4vTA5YAPE64v/F/4fTa55DXF/4v/AH4A/AH4A/AH4A/AHIA=="))
diff --git a/apps/invader/app.js b/apps/invader/app.js
new file mode 100644
index 000000000..89e7462f6
--- /dev/null
+++ b/apps/invader/app.js
@@ -0,0 +1,452 @@
+// Brian Kumanchik
+// Started 05-25-22
+// My Invader Demo, for Bangle.js 2, written JavaScript - using Espruino Web IDE
+
+
+// note: resolution is 176x176
+
+
+// to do:
+// upload to official app page
+// make invader clock
+
+
+
+// - variables -----------------------------------------
+// invader variables
+var inv_x = 77;
+var inv_y = 20;
+var i_anim_delay = 10; // invader animation (and move) delay
+var inv_frame = 1; // invader start animation frame
+var ix_speed = 6; // march speed
+var i_dir = 1; // 1 = right, 0 = left
+var been_hit = false; // invader hit state
+// - shoot variables
+var inv_shot_x = -32;
+var inv_shot_y = -32;
+var inv_fire_pause = 30;
+var inv_fired = false; // invader fired state
+// - explode variables
+var been_hit = false; // invader hit state
+var bx = -32; // blast x
+var by = -32; // blast y
+var blast_delay = 15; // invader blast delay - pause after explosion
+var boom_play = false;
+
+// turret variables
+var tur_x = 77;
+var tur_y = 148;
+var shot_fired = false; // turret fired state
+var sx = -20; // turret shot starting x - off screen
+var sy = -20; // turret shot starting y - off screen
+var turret_been_hit = false;
+var turret_blast_delay = 25; // keep blast active on screen for 60 frames
+var turret_exp_frame = 1; // turret explode start animation frame
+var turret_anim_delay = 3; // turret explode animation delay
+var explosion_play = false;
+
+// misc variables
+var score = 0; // starting score
+var lives = 3; // starting lives
+var game_state = 0; // game state - 0 = game not started, 1 = game running, 3 = game over
+var ang = 0.1;
+var start_been_pressed = false; // stops double press on restart
+var fire_been_pressed = false; // stops auto fire
+
+// input(screen controller) variables
+var BTNL, BTNR, BTNF, BTNS; // button - left, right, fire, start
+var tap = {};
+// use tapping on screen for left, right, fire, start
+Bangle.on('drag',e=>tap=e);
+BTNL = { read : _=>tap.b && tap.x < 88 && tap.y > 88};
+BTNR = { read : _=>tap.b && tap.x > 88 && tap.y > 88};
+BTNF = { read : _=>tap.b && tap.x > 88 && tap.y < 88};
+BTNS = { read : _=>tap.b && tap.x < 88 && tap.y < 88};
+
+
+// - sprites -------------------------------------------
+// invader sprites
+var invader_a =
+ require("heatshrink").decompress(atob("hcIwkBiIBBAQoECCQQFBgEQAIMBEhUBDoYWDAYI="));
+var invader_b =
+ require("heatshrink").decompress(atob("hcIwkBiIBBAQMQAoQEBgISCAYUQAIQAEB4YEBEAgEDAYIA=="));
+var boom =
+ require("heatshrink").decompress(atob("hcJwkBiMQAIURgMQAgIKBAIICFAIMAAwIWBBAYSIEAgrDiA="));
+var inv_shot =
+ require("heatshrink").decompress(atob("gcFwkBiERiAABAYQ"));
+
+// turret sprites
+var turret =
+ require("heatshrink").decompress(atob("h8IwkBiIABAYYACgAHFiEABggADCAInFgITBAAgOPA=="));
+var tur_exp_a =
+ require("heatshrink").decompress(atob("h8IwkBiMRiACBAAwJEiAABBQgZCAAkAiAJBBoIUBgIABBgQACDIQ9ECQIA=="));
+var tur_exp_b =
+ require("heatshrink").decompress(atob("h8IwkBiIBBAAUBiADCiMQAwQFDCIYXEB4IABgMAEYQXBiEAAQIQBAoIABDAQUCAAIVBA"));
+var shot =
+ require("heatshrink").decompress(atob("gMDwkBAoIA=="));
+
+
+// function to move and animate invader
+function move_anim_inv() {
+ // invader anim code
+ i_anim_delay -= 1;
+ if ((i_anim_delay < 0) && !(been_hit)) {
+ i_anim_delay = 10;
+
+ inv_frame += 1;
+ if (inv_frame > 2) {
+ inv_frame = 1;
+ }
+
+ // move right
+ if (i_dir == 1){
+ inv_x += ix_speed;
+ if (inv_x >= 142) {
+ inv_y += 8; // step down
+ i_dir = -1;
+ }
+ }
+
+ // move left
+ if (i_dir < 1){
+ inv_x -= ix_speed;
+ if (inv_x <= 10) {
+ inv_y += 8; // step down
+ i_dir = 1;
+ }
+ }
+ }
+}
+
+
+// function to make invader fire
+function invader_fire() {
+ inv_fire_pause -= 1;
+
+ if (!(inv_fired)) { // so once invader shot is fired it doesn't follow the invader still
+ inv_shot_x = inv_x + 8;
+ inv_shot_y = inv_y + 18;
+ }
+
+ if (inv_fire_pause < 0) {
+ inv_fired = true;
+ inv_shot_y += 8;
+ }
+}
+
+
+// function to make turret explode (when hit) then start back in center
+function turret_hit() {
+ if (turret_been_hit) {
+ if (!(explosion_play)) {
+ //Bangle.buzz();
+ //Bangle.beep();
+ }
+
+ explosion_play = true;
+ turret_anim_delay -= 1;
+ turret_blast_delay -= 1;
+
+ if (turret_anim_delay < 0) {
+ turret_exp_frame += 1;
+ if (turret_exp_frame > 2) {
+ Bangle.buzz();
+ turret_exp_frame = 1;
+ }
+ turret_anim_delay = 3;
+ }
+
+ if (turret_blast_delay < 0) {
+ turret_blast_delay = 21;
+ turret_been_hit = false;
+ explosion_play = false;
+ tur_x = 77; // reset turret x
+ tur_y = 148; // reset turret y
+ }
+ }
+}
+
+
+// function to make invader explode (when hit) then randomly start somewhere else
+function invader_hit() {
+ if (been_hit) {
+ if (!(boom_play)) {
+ Bangle.buzz();
+ //Bangle.beep();
+ }
+
+ inv_shot_x = -32; // hide shot
+ inv_shot_y = -32; // hide shot
+ inv_fire_pause = 30; // and reset pause
+
+ boom_play = true;
+ blast_delay -= 1;
+
+ if (blast_delay < 0) {
+ blast_delay = 15;
+ boom_play = false;
+ been_hit = false;
+ bx = -32; // move boom off screen (following invader)
+ by = -32;
+ // generate a random rounded number between 10 and 142;
+ inv_x = Math.floor(Math.random() * 142) + 10;
+ inv_y = 20; // move invader back up after being hit
+ i_dir = 1; // reset invader direction
+ }
+ }
+}
+
+
+// - setup stuff ---------------------------------------
+function gameStart() {
+ setInterval(onFrame, 50);
+}
+
+
+// - main loop -------------------------------------------------------------
+function onFrame() {
+
+ // game not started state (title screen) ***************************
+ if(game_state == 0) {
+ g.clear();
+
+
+ if (!(BTNS.read())) {
+ start_been_pressed = false; // stops double press on restart
+ }
+
+
+ // draw text during game over state
+ g.setFont("4x6", 4); // set font and size x 2
+ g.setColor(0,1,0); // set color (black)
+ g.drawString("INVADER", 33, 55);
+
+
+ // just animate invader
+ // invader anim code
+ i_anim_delay -= 1;
+ if(i_anim_delay < 0) {
+ i_anim_delay = 15;
+
+ inv_frame += 1;
+ if (inv_frame > 2) {
+ inv_frame = 1;
+ }
+ }
+
+
+ // draw sprites during game over state
+ // next 2 line for a rotating invader on the title screen
+ //ang += 0.1;
+ //g.drawImage(invader_a, 88, 98, {scale:4, rotate:ang});
+ if(inv_frame == 1) {
+ g.drawImage(invader_a, 88-22, 85, {scale:4});
+ }
+ else if(inv_frame == 2) {
+ g.drawImage(invader_b, 88-22, 85, {scale:4});
+ }
+
+ // reset stuff
+ if(BTNS.read() && !(start_been_pressed)) {
+ turret_been_hit = false;
+ tur_x = 77; // reset turret to center of screen
+ tur_y = 148; // reset turret y
+ inv_x = 77; // reset invader to center of screen
+ inv_y = 20; // reset invader back to top
+ i_dir = 1; // reset invader direction
+ lives = 3; // reset lives
+ score = 0; // reset score
+ explosion_play = false;
+ game_state = 1;
+ turret_blast_delay = 25;
+ }
+
+
+ g.flip();
+ }
+
+
+ // game over state *************************************************
+ if(game_state == 3) {
+ g.clear();
+
+ // draw text during game over state
+ g.setFont("4x6", 2); // set font and size x 2
+ g.setColor(0,0,0); // set color (black)
+ g.drawString("SCORE:" + score ,5, 5);
+ g.drawString("LIVES:" + lives ,117, 5);
+ g.drawString("GAME OVER", 52, 80);
+
+
+ // draw sprites during game over state
+ // - invader frame 2
+ g.drawImage(invader_b, inv_x, inv_y, {scale:2});
+ g.drawImage(tur_exp_b, tur_x, tur_y, {scale:2});
+ g.drawImage(inv_shot, inv_shot_x, inv_shot_y, {scale:2});
+
+
+ // reset stuff
+ if(BTNS.read()) {
+ //turret_been_hit = false;
+ //tur_x = 77; // reset turret to center of screen
+ //tur_y = 148; // reset turret y
+ //inv_x = 77; // reset invader to center of screen
+ //inv_y = 20; // reset invader back to top
+ //i_dir = 1; // reset invader direction
+ //lives = 3; // reset lives
+ //score = 0; // reset score
+ //explosion_play = false;
+ game_state = 0;
+ start_been_pressed = true;
+ //turret_blast_delay = 25;
+ }
+
+
+ g.flip();
+ }
+
+
+ // not game over state (game running) ******************************
+ if(game_state == 1) {
+ Bangle.setLCDPower(1); // optional - this keeps the watch LCD lit up
+ g.clear();
+
+
+ if (!(BTNF.read())) {
+ fire_been_pressed = false; // stops auto fire
+ }
+
+
+ // call function to move and animate invader
+ move_anim_inv();
+
+ // call function to make invader fire
+ invader_fire();
+
+
+ // check input (screen presses)
+ if(BTNL.read() && tur_x >= 12 && !(turret_been_hit)) {
+ tur_x -= 6;
+ }
+ else if(BTNR.read() && tur_x <= 140 && !(turret_been_hit)) {
+ tur_x += 6;
+ }
+ else if(BTNF.read() && !(turret_been_hit) && !(fire_been_pressed) && !(shot_fired)) {
+ shot_fired = true;
+ fire_been_pressed = true; // stops auto fire
+ sx=tur_x + 12;
+ sy=tur_y - 7;
+ }
+
+
+ // check for turret shot going off screen before allowing to fire again
+ if (shot_fired) {
+ sy -= 8;
+ if (sy < 22) {
+ shot_fired = false;
+ sx = -32;
+ sy = -32;
+ }
+ }
+
+
+ // check for invader shot going off screen before allowing to fire again
+ if (inv_shot_y > 150
+ ) {
+ inv_fired = false;
+ inv_shot_x = inv_x - 1;
+ inv_shot_y = inv_y + 7;
+ inv_fire_pause = 30;
+ }
+
+
+ // check for turret shot and invader collision
+ if ((sx >= inv_x) && (sx <= inv_x + 20) && (sy <= inv_y + 14)) {
+ sx = -32;
+ sy = -32;
+ been_hit = true;
+ score += 10;
+ }
+
+
+ // check for invader shot and turret collision
+ if ((inv_shot_x + 4) >= (tur_x) && (inv_shot_x) <= (tur_x + 24) && (inv_shot_y + 8) >= (tur_y + 6)) {
+ if (!(turret_been_hit)) {
+ lives -= 1;
+
+ if (lives == 0) {
+ game_state = 3;
+ Bangle.buzz();
+ }
+ turret_been_hit = true;
+ }
+ }
+
+
+ // - draw sprites ----------------------------------
+ // invader sprites
+ if(!(been_hit)) {
+ if(inv_frame == 1) {
+ // - invader frame 1
+ g.drawImage(invader_a, inv_x, inv_y, {scale:2});
+ }
+ else if(inv_frame == 2) {
+ // - invader frame 2
+ g.drawImage(invader_b, inv_x, inv_y, {scale:2});
+ }
+ }
+ else {
+ // - invader explosion
+ g.drawImage(boom, inv_x, inv_y, {scale:2});
+ }
+ // - invader shot
+ if (inv_fired) {
+ g.drawImage(inv_shot, inv_shot_x, inv_shot_y, {scale:2});
+ }
+ else {
+ g.drawImage(inv_shot, -32, -32, {scale:2});
+ }
+
+ // turret sprites
+ if(!(turret_been_hit)) {
+ // - undamaged turret
+ g.drawImage(turret, tur_x, tur_y, {scale:2});
+ }
+ else {
+ if(turret_exp_frame == 1) {
+ // - turret explosion frame 1
+ g.drawImage(tur_exp_a, tur_x, tur_y, {scale:2});
+ }
+ else if(turret_exp_frame == 2) {
+ // - turret explosion frame 2
+ g.drawImage(tur_exp_b, tur_x, tur_y, {scale:2});
+ }
+ }
+ // - turret shot
+ g.drawImage(shot, sx, sy, {scale:2});
+
+
+ // call function to make invader explode then randomly start somewhere else
+ invader_hit();
+
+
+ // call function to make turret explode (when hit) then start back in center
+ turret_hit();
+
+
+ // - draw text -------------------------------------
+ g.setFont("4x6", 2); // set font and size x 2
+ g.setColor(0,0,0); // set color (black)
+ g.drawString("SCORE:" + score ,5,5);
+ g.drawString("LIVES:" + lives ,117,5);
+
+
+ g.flip();
+ }
+
+} // end main loop ---------------------------------------------------------
+
+
+gameStart();
+
+
diff --git a/apps/invader/app.png b/apps/invader/app.png
new file mode 100644
index 000000000..3b9f82205
Binary files /dev/null and b/apps/invader/app.png differ
diff --git a/apps/invader/metadata.json b/apps/invader/metadata.json
new file mode 100644
index 000000000..bb74f5122
--- /dev/null
+++ b/apps/invader/metadata.json
@@ -0,0 +1,15 @@
+{ "id": "invader",
+ "name": "Invader",
+ "shortName":"Invader",
+ "version":"0.11",
+ "description": "A Space Invader game-like demo - work in progress",
+ "icon": "app.png",
+ "screenshots" : [ { "url":"screenshot_0.png" }, { "url":"screenshot_1.png" }, { "url":"screenshot_2.png" } ],
+ "tags": "game",
+ "supports" : ["BANGLEJS2"],
+ "readme": "README.md",
+ "storage": [
+ {"name":"invader.app.js","url":"app.js"},
+ {"name":"invader.img","url":"app-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/invader/screenshot.png b/apps/invader/screenshot.png
new file mode 100644
index 000000000..ae936f6c7
Binary files /dev/null and b/apps/invader/screenshot.png differ
diff --git a/apps/invader/screenshot_0.png b/apps/invader/screenshot_0.png
new file mode 100644
index 000000000..804f3e435
Binary files /dev/null and b/apps/invader/screenshot_0.png differ
diff --git a/apps/invader/screenshot_1.png b/apps/invader/screenshot_1.png
new file mode 100644
index 000000000..14164bc6e
Binary files /dev/null and b/apps/invader/screenshot_1.png differ
diff --git a/apps/invader/screenshot_2.png b/apps/invader/screenshot_2.png
new file mode 100644
index 000000000..5f6ade79c
Binary files /dev/null and b/apps/invader/screenshot_2.png differ
diff --git a/apps/kanawatch/ChangeLog b/apps/kanawatch/ChangeLog
new file mode 100644
index 000000000..7b83706bf
--- /dev/null
+++ b/apps/kanawatch/ChangeLog
@@ -0,0 +1 @@
+0.01: First release
diff --git a/apps/kanawatch/README.md b/apps/kanawatch/README.md
new file mode 100644
index 000000000..1fdf1927c
--- /dev/null
+++ b/apps/kanawatch/README.md
@@ -0,0 +1,12 @@
+# kanawatch
+
+A simple watchface design with hiragana and katakana
+cards for learning.
+
+## Author
+
+Written by pancake in 2022, powered by insomnia
+
+## Screenshots
+
+
diff --git a/apps/kanawatch/app-icon.js b/apps/kanawatch/app-icon.js
new file mode 100644
index 000000000..a17f21d56
--- /dev/null
+++ b/apps/kanawatch/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwxEBAH4A/AEn/AAgrrAA4ttGL4hF9fGsU1pMNmti43rGLwcD/3MxEAud413p6uuvFzgGI5n+GDQaD6F8i2p8KKH8Opi186AwYC4Xv08A0fnXhfn0cA0/vGCoVC7+ItHNE4vQ+oxH5toxHfGCYTC8t/xaKH5VY+CUIxd/8owSCIPxymB8wkH8UA2yTI82Byn4F6AXCwNH7YjI7UATAwAD7dHHgYuP4sAc5XLgHrBpXAjngGBwOCrmJ/whJ1syBgXw7v6Bov+xObF5rWDgHWKJWEt3l4mQjkAoHzBwvWgHhGBgMC1WIDQuw1/L427z8ygAABp+R3vqH4+I1QvO/1R5YZF+t1FINWuMAy/W+BuKZ4NRT4ReL7kc+waG/fy/n/9kA74tLAAP2jncAgPBF5W5yIeLZgPxEgf3CJOR3JTCF5WU3wvL6sA/YFC7e0CJO+ygDB94vKt3aF5fHoQDB+/dzdL4nb+YRG7VuAYP5F5VF9ovL3dP3t8pOKgFw0+CjmT84RE9tFAYP+F6/uwMm1Hd/vCk3oQYWGl3XF6aPK/e0oVwrohCmu9Bof5sVF+yPSd5PtuWA9m7o///uCwH9B4m9gHKd6W5yIuG9NV3v+//Gjn/2VA9wQF6UA2AFCyO5AYPcF5Xcjh1DAAPnp/SEYnJiy2EAAXTgGvAgP2jncAgPBF44wC/1R5a7EsZHCAAPegEA3afH4sA4wEB5dROgP/FxBgD1WIPgky/QGD5MAxYfCAAuGjnvAgNHuBLCF5nhgHWAoWvuwEC9mWLwN+Fw6aB1wEB60A44EB6ovJGAebxJSC1lF4/AyMNoXBzUN/IuF5kmyP8VgOJrgKCFxUB8QOB8Ec4CnCLIMAmWr+v/9Vy/otD+WWmu7BAXAjnFF5xgD21H7f//u+0vN/CKH9Ojse4+QHC7dH2wuPgPVCAP4yk98wqHAAf734OF82ByhCDF5pgD/9/xfhGBYAF8OLv/lFyIABU4XfxFo5ouP5toxHfFyZhE9+ngGj84tL8+jgGn94uVSQvQvkW1KUI8Opi186AIDFygwF/3MxEAuew6fp9PT2FzgGI5n+FzQwFAAPr42fu9JpN3z/G9YPFFzAxIABYtbGKItfGZYrlAH4A+A"))
diff --git a/apps/kanawatch/app.js b/apps/kanawatch/app.js
new file mode 100644
index 000000000..ada6aa6df
--- /dev/null
+++ b/apps/kanawatch/app.js
@@ -0,0 +1,825 @@
+const stripe_width = 32;
+const stripe_pos = 40;
+const stripe2_pos = 110;
+const h = g.getHeight();
+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=')
+};
+/// /////////////////////////////////////////
+
+let kana = katakana.KI;
+let scroll = 0;
+
+function drawWheel () {
+ if (scroll > 20 || scroll < -20) {
+ scroll = 0;
+ next();
+ }
+}
+let hiramode = false;
+let curkana = 'KA';
+function next () {
+ let found = false;
+ for (const k of Object.keys(katakana).sort()) {
+ if (found) {
+ kana = hiramode ? hiragana[k] : katakana[k];
+ curkana = k;
+ return;
+ }
+ if (curkana === k) {
+ found = true;
+ }
+ }
+ curkana = 'KA';
+ kana = hiramode ? hiragana[curkana] : katakana[curkana];
+}
+
+function prev () {
+ let oldk = '';
+ let count = 0;
+ for (const k of Object.keys(katakana).sort()) {
+ if (curkana === k) {
+ if (count > 0) {
+ curkana = oldk;
+ kana = katakana[curkana];
+ return;
+ } else {
+ }
+ }
+ oldk = k;
+ count++;
+ }
+ curkana = oldk;
+ kana = katakana[curkana];
+}
+
+const kanacolors = {
+ A: []
+};
+
+const clocktop = false;
+
+function updateWatch (hhmm) {
+ if (!hhmm) {
+ hhmm = ohhmm;
+ }
+ 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);
+ whitecolor = true;
+ } else if (curkana.indexOf('I') != -1) {
+ g.setColor(0, 1, 0);
+ } else if (curkana.indexOf('U') != -1) {
+ g.setColor(0, 0, 1);
+ whitecolor = true;
+ } else if (curkana.indexOf('E') != -1) {
+ g.setColor(1, 1, 0);
+ } else {
+ 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 {
+ 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;
+ }
+ g.drawString(hhmm, x - 3, y - 3);
+ if (whitecolor) {
+ g.setColor(1, 1, 1);
+ } else {
+ g.setColor(0, 0, 0);
+ }
+ g.drawString(hhmm, x, y - 1);
+ }
+ // drawKana(hira_a, 0, 60);
+ drawKana(hiragana.KA, g.getWidth() / 6, 60);
+ Bangle.drawWidgets();
+}
+function drawKana (img, 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);
+ }
+ }
+}
+
+var ohhmm = '';
+
+function tickWatch () {
+ const now = Date();
+ function zpad (n) {
+ return (n < 10) ? '0' + n : n;
+ }
+ const hhmm = zpad(now.getHours()) + ':' + zpad(now.getMinutes());
+ if (hhmm !== ohhmm) {
+ updateWatch(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))) {
+ next();
+ } else {
+ hiramode = !hiramode;
+ }
+ kana = hiramode ? hiragana[curkana] : katakana[curkana];
+ tickWatch();
+});
+
+Bangle.loadWidgets();
+tickWatch();
+setInterval(tickWatch, 1000);
+
diff --git a/apps/kanawatch/app.png b/apps/kanawatch/app.png
new file mode 100644
index 000000000..cf081937b
Binary files /dev/null and b/apps/kanawatch/app.png differ
diff --git a/apps/kanawatch/metadata.json b/apps/kanawatch/metadata.json
new file mode 100644
index 000000000..09bfc2d36
--- /dev/null
+++ b/apps/kanawatch/metadata.json
@@ -0,0 +1,31 @@
+{
+ "id": "kanawatch",
+ "name": "Kanawatch",
+ "shortName": "Kanawatch",
+ "version": "0.01",
+ "type": "clock",
+ "description": "Learn Hiragana and Katakana",
+ "icon": "app.png",
+ "allow_emulator": true,
+ "tags": "clock",
+ "supports": [
+ "BANGLEJS2"
+ ],
+ "readme": "README.md",
+ "storage": [
+ {
+ "name": "kanawatch.app.js",
+ "url": "app.js"
+ },
+ {
+ "name": "kanawatch.img",
+ "url": "app-icon.js",
+ "evaluate": true
+ }
+ ],
+ "screenshots": [
+ {
+ "url": "screenshot.jpg"
+ }
+ ]
+}
diff --git a/apps/kanawatch/screenshot.jpg b/apps/kanawatch/screenshot.jpg
new file mode 100644
index 000000000..ac7447ee8
Binary files /dev/null and b/apps/kanawatch/screenshot.jpg differ
diff --git a/apps/lcars/ChangeLog b/apps/lcars/ChangeLog
index e622feb1f..9a8ac4008 100644
--- a/apps/lcars/ChangeLog
+++ b/apps/lcars/ChangeLog
@@ -19,4 +19,5 @@
0.19: Alarms can not go bigger than 100.
0.20: Use alarm for alarm functionality instead of own implementation.
0.21: Add custom theming.
-0.22: Fix alarm and add build in function for step counting.
\ No newline at end of file
+0.22: Fix alarm and add build in function for step counting.
+0.23: Add warning for low flash memory
diff --git a/apps/lcars/lcars.app.js b/apps/lcars/lcars.app.js
index 07ca51fd9..e81c0d6f3 100644
--- a/apps/lcars/lcars.app.js
+++ b/apps/lcars/lcars.app.js
@@ -147,8 +147,7 @@ var iconCharging = {
buffer : require("heatshrink").decompress(atob("23btugAwUBtoICARG0h048eODQYCJ6P/AAUCCJfbo4SDxYRLtEcuPHjlwgoRJ7RnIloUHoYjDAQfAExEAwUIkACEkSAIEYwCBhZKH6EIJI0CJRFHEY0BJRWBSgf//0AJRYSE4BKLj4SE8BKLv4RD/hK/JS2AXY0gXwRKG4cMmACCJQMAg8csEFJQsBAwfasEAm379u0gFbcBfHzgFBz1xMQZKBjY/D0E2+BOChu26yVEEYdww+cgAFCg+cgIfB6RKF4HbgEIkGChEAthfCJQ0eEAIjBBAMxk6GCJQtgtyVBwRKBAQMbHAJKGXIIFCgACBhl54qVG2E+EAJKBJoWAm0WJQ6SCXgdxFgMLJQvYjeAEAUwFIUitEtJQ14NwUHgEwKYZKGwOwNYX7XgWCg3CJQ5rB4MevPnAoPDJRJrCgEG/ECAoNsJRUwoEesIIBiJKI3CVDti/CJRKVDiJHBSo0YsOGjED8AjBcAcIgdhcAXAPIUAcAYIBcA4dBAQUG8BrBgBuCgOwcBEeXIK2BBAIFBgRqBGoYAChq8CcYUE4FbUYOACQsHzgjDgwFBCIImBAQsDtwYD7cAloRI22B86YBw5QBgoRJ7dAgYEDCJaeBJoMcsARMAQNoJIIRE6A"))
};
-var iconNoBattery = {
- text: "NO BAT",
+var iconWarning = {
width : 50, height : 50, bpp : 3,
transparent : 1,
buffer : require("heatshrink").decompress(atob("kmSpIC/AWMyoQIFsmECJFJhMmA4QXByVICIwODAQ4RRFIQGD5JVLkIGDzJqMyAGDph8MiRKGyApEAoZKFyYIDQwMkSQNkQZABBhIIOOJRuEL5gRIAUKACVQMhmUSNYNDQYJTBBwYFByGTkOE5FJWYNMknCAQKYCiaSCpmGochDoSYBhMwTAZrChILBhmEzKPBF4ImBTAREBDoMmEwJVDoYjBycJFgWEJQRuLJQ1kmQCCjJlCBYbjCagaDBwyDBmBuBF4TjJAUQKINBChCDQxZBcZIIQF4NIgEAgKSDiQmEVQKMBoARBAAMCSQLLBVoxqKL4gaCChVCNwoRKOIo4CJIgABBoSMHpIRFgDdJOIJUBCAUJRgJuEAQb+DIIgRIAX4C/ASOQA"))
@@ -321,19 +320,21 @@ function drawState(){
if(!isAlarmEnabled()){
var bat = E.getBattery();
+ var flash = storage.getFree() / process.env.STORAGE;
var current = new Date();
var hours = current.getHours();
- var iconImg =
- Bangle.isCharging() ? iconCharging :
- bat < 30 ? iconNoBattery :
- Bangle.isGPSOn() ? iconSatellite :
- hours % 4 == 0 ? iconSaturn :
- hours % 4 == 1 ? iconMars :
- hours % 4 == 2 ? iconMoon :
- iconEarth;
- g.drawImage(iconImg, 23, 118);
+ var iconMsg =
+ Bangle.isCharging() ? { icon: iconCharging, text: "STATUS" } :
+ bat < 30 ? { icon: iconWarning, text: "BAT" } :
+ flash < 0.1 ? { icon: iconWarning, text: "DISK" } :
+ Bangle.isGPSOn() ? { icon: iconSatellite, text: "STATUS" } :
+ hours % 4 == 0 ? { icon: iconSaturn, text: "STATUS" } :
+ hours % 4 == 1 ? { icon: iconMars, text: "STATUS" } :
+ hours % 4 == 2 ? { icon: iconMoon, text: "STATUS" } :
+ { icon: iconEarth, text: "STATUS" };
+ g.drawImage(iconMsg.icon, 23, 118);
g.setColor(cWhite);
- g.drawString("STATUS", 23+26, 108);
+ g.drawString(iconMsg.text, 23+26, 108);
} else {
// Alarm within symbol
g.setColor(color2);
diff --git a/apps/lcars/metadata.json b/apps/lcars/metadata.json
index 40da1b37f..62a1c67db 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.22",
+ "version":"0.23",
"readme": "README.md",
"supports": ["BANGLEJS2"],
"description": "Library Computer Access Retrieval System (LCARS) clock.",
diff --git a/apps/messages/ChangeLog b/apps/messages/ChangeLog
index 53157f0d8..4b577e191 100644
--- a/apps/messages/ChangeLog
+++ b/apps/messages/ChangeLog
@@ -51,3 +51,5 @@
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
+ Don't turn on the screen after unread timeout expires (#1873)
diff --git a/apps/messages/app.js b/apps/messages/app.js
index 745f7d208..d4540b797 100644
--- a/apps/messages/app.js
+++ b/apps/messages/app.js
@@ -317,7 +317,7 @@ 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), pad: 3, cb:()=>{
+ { 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);
}},
@@ -411,19 +411,17 @@ function cancelReloadTimeout() {
unreadTimeout = undefined;
}
-
g.clear();
+
Bangle.loadWidgets();
Bangle.drawWidgets();
+
setTimeout(() => {
- var unreadTimeoutSecs = settings.unreadTimeout;
- if (unreadTimeoutSecs===undefined) unreadTimeoutSecs=60;
- if (unreadTimeoutSecs)
- unreadTimeout = setTimeout(function() {
- print("Message not seen - reloading");
- load();
- }, unreadTimeoutSecs*1000);
+ 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});
-},10); // if checkMessages wants to 'load', do that
+ var newMusic = MESSAGES.some(m => m.id === "music" && m.new);
+ checkMessages({ clockIfNoMsg: 0, clockIfAllRead: 0, showMsgIfUnread: 1, openMusic: newMusic && settings.openMusic });
+}, 10); // if checkMessages wants to 'load', do that
diff --git a/apps/messages/metadata.json b/apps/messages/metadata.json
index fd09fdfe4..4bc9f59e4 100644
--- a/apps/messages/metadata.json
+++ b/apps/messages/metadata.json
@@ -1,7 +1,7 @@
{
"id": "messages",
"name": "Messages",
- "version": "0.38",
+ "version": "0.39",
"description": "App to display notifications from iOS and Gadgetbridge/Android",
"icon": "app.png",
"type": "app",
diff --git a/apps/messages/settings.js b/apps/messages/settings.js
index adea36f12..1b166dcf6 100644
--- a/apps/messages/settings.js
+++ b/apps/messages/settings.js
@@ -1,7 +1,7 @@
(function(back) {
function settings() {
let settings = require('Storage').readJSON("messages.settings.json", true) || {};
- if (settings.vibrate===undefined) settings.vibrate=".";
+ if (settings.vibrate===undefined) settings.vibrate=":";
if (settings.repeat===undefined) settings.repeat=4;
if (settings.unreadTimeout===undefined) settings.unreadTimeout=60;
settings.unlockWatch=!!settings.unlockWatch;
diff --git a/apps/messages/widget.js b/apps/messages/widget.js
index 4b368ffd6..25573220f 100644
--- a/apps/messages/widget.js
+++ b/apps/messages/widget.js
@@ -27,7 +27,6 @@ draw:function(recall) {
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();
- Bangle.setLCDPower(1);// turns screen on
},hide:function() {
delete WIDGETS["messages"].t;
delete WIDGETS["messages"].l;
@@ -35,7 +34,7 @@ draw:function(recall) {
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 || ".");
+ 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;
diff --git a/apps/miclock2/ChangeLog b/apps/miclock2/ChangeLog
index 55c60accd..534332e63 100644
--- a/apps/miclock2/ChangeLog
+++ b/apps/miclock2/ChangeLog
@@ -1,2 +1,3 @@
0.01: New App!
0.02: Redraw only when seconds change
+0.03: Fix typo in redraw check
diff --git a/apps/miclock2/clock-mixed.js b/apps/miclock2/clock-mixed.js
index edc68959c..bb1537313 100644
--- a/apps/miclock2/clock-mixed.js
+++ b/apps/miclock2/clock-mixed.js
@@ -48,7 +48,7 @@ function setLineWidth(x1, y1, x2, y2, lw) {
function drawMixedClock(force) {
var date = new Date();
- if ((force || Bangle.isLCDOn()) && buf.buffer && date.getSeconds() === lastDate.getSeconds()) {
+ if ((force || Bangle.isLCDOn()) && buf.buffer && date.getSeconds() !== lastDate.getSeconds()) {
lastDate = date;
var dateArray = date.toString().split(" ");
var isEn = locale.name.startsWith("en");
diff --git a/apps/miclock2/metadata.json b/apps/miclock2/metadata.json
index e1481dbd2..094d0995a 100644
--- a/apps/miclock2/metadata.json
+++ b/apps/miclock2/metadata.json
@@ -1,7 +1,7 @@
{
"id": "miclock2",
"name": "Mixed Clock 2",
- "version": "0.02",
+ "version": "0.03",
"description": "White color variant of the Mixed Clock with thicker clock hands for better readability in the bright sunlight, extra space under the clock for widgets and seconds in the digital clock.",
"icon": "clock-mixed.png",
"type": "clock",
diff --git a/apps/multitimer/alarm.js b/apps/multitimer/alarm.js
index 97cbaa5fa..eb1b3b259 100644
--- a/apps/multitimer/alarm.js
+++ b/apps/multitimer/alarm.js
@@ -120,7 +120,7 @@ function showAlarm(alarm) {
Bangle.setLocked(false);
}
- require("buzz").pattern(alarm.vibrate === undefined ? ".." : alarm.vibrate).then(() => {
+ require("buzz").pattern(alarm.vibrate === undefined ? "::" : alarm.vibrate).then(() => {
if (buzzCount--) {
setTimeout(buzz, settings.buzzIntervalMillis);
} else if (alarm.as) { // auto-snooze
diff --git a/apps/noteify/app.js b/apps/noteify/app.js
index c19694ea4..2b3ee64f0 100644
--- a/apps/noteify/app.js
+++ b/apps/noteify/app.js
@@ -5,7 +5,7 @@ var notes = require("Storage").readJSON("noteify.json", true) || [];
var alarms = require("sched").getAlarms();
msg = "";
-function startNote(idx) {
+function startNote(idx) {
idx == undefined ? note = "" : note = notes[idx].note;
require("textinput").input({text:note}).then(result => {
if (result != "") {
@@ -23,20 +23,20 @@ function viewNote(idx) {
textY += e.dy;
g.setClipRect(0, 30, g.getWidth(), g.getHeight());
if (textY > 30) textY = 30;
- if (textY < textBound) textY = textBound;
+ if (textY < textBound) textY = textBound;
g.clearRect(0, 30, g.getWidth(), g.getHeight()).setColor(g.theme.fg).setFont("6x8:2").setFontAlign(-1, -1).drawString(g.wrapString(notes[idx].note, g.getWidth()).join("\n"), 0, textY);
},back:()=>{
Bangle.setUI();
showEditMenu(idx);
}});
-
+
}
function showMainMenu() {
var mainMenu = {
"" : { "title" : "Noteify" },
"< Back" : function() { load(); },
- "New note" : function() {
+ "New note" : function() {
E.showMenu();
startNote();
},
@@ -187,7 +187,7 @@ function editAlarm(alarmIndex, alarm) {
as : false,
dow : 0b1111111,
last : 0,
- vibrate : ".."
+ vibrate : "::"
};
if (msg != "") a["msg"] = msg;
if (!newAlarm) Object.assign(a, alarms[alarmIndex]);
@@ -195,7 +195,7 @@ function editAlarm(alarmIndex, alarm) {
var t = decodeTime(a.t);
var alarmTitle = (a.msg == undefined) ? 'Alarm' : (a.msg.length > 12) ? a.msg.replace(/\n/g, " ").substring(0, 12)+"..." : msg.replace(/\n/g, " ").substring(0, 12)+"...";
-
+
const menu = {
'': { 'title': alarmTitle },
'< Back' : () => showAlarmMenu(),
@@ -264,7 +264,7 @@ function editTimer(alarmIndex, alarm) {
var t = decodeTime(a.timer);
var timerTitle = (a.msg == undefined) ? 'Timer' : (a.msg.length > 12) ? a.msg.replace(/\n/g, " ").substring(0, 12)+"..." : msg.replace(/\n/g, " ").substring(0, 12)+"...";
-
+
const menu = {
'': { 'title': timerTitle },
'< Back' : () => showMainMenu(),
diff --git a/apps/novaclock/README.md b/apps/novaclock/README.md
new file mode 100644
index 000000000..b54f44241
--- /dev/null
+++ b/apps/novaclock/README.md
@@ -0,0 +1,9 @@
+# Nova Clock
+A simple clock app that uses a clockwork star, from Kirby.
+
+*Note: This clock draws slightly into the widget area, but since it's in the middle, it shouln't matter that much (nobody has that many widgets... right?)*
+
+## Credits
+Pixel art by me, [dronesflier](https://github.com/dronesflier)
+
+The Kirby series belongs to Nintendo/HAL Labs
diff --git a/apps/novaclock/app-icon.js b/apps/novaclock/app-icon.js
new file mode 100644
index 000000000..cf159ba1f
--- /dev/null
+++ b/apps/novaclock/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwxH+AH4A/AE3N5ouuGFovuFwYwrF9wuFGFIqC5nMF9guBF9ReD43GGFJeDF9ReFGFImCFwYvBGAReqMEAdCAAwvKAA4pWFxYwNGhQoJAAouHYQYAEGBwsIFBIANGRBgLFzIwDYhoweFx4xGGC4uSGDYuFFpqTIF1BhXFzBhVXSZhNF6QuWGAgvSFzAvS4wved6KOsSDovJ5gACF9IsCBIQxFF8ItEAAYxEF7qHDFowxGBwZeaFxgPEAwYvYABAONF74PVF64RbF6IThDZYVnDIoXtAH4A/AH4AkA=="))
diff --git a/apps/novaclock/app.js b/apps/novaclock/app.js
new file mode 100644
index 000000000..e5bd37b06
--- /dev/null
+++ b/apps/novaclock/app.js
@@ -0,0 +1,274 @@
+function nova() {
+ var nova = require("heatshrink").decompress(atob("rFYxH+AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AHsAABZM/K5fM5kAv2igGi1EAvFUK/6v/K8ipC0SvCAQhM/K5l+AQ5XG5nM1BrBBgJX/V4WoWQRXHKoIYGBBBX2vxOBKIQFCLoRMD1BXHLPpOBK4gFCK4RIDK5IDFK+4AKIwpYFBYuiKfLtBABAPEK5i9IKtg8BAAUPABHMlYADgBXMAwyqrKQOiKA0OAIRWDqwACLAOoWgZQILFg4B0SoHKIYADiEr5moKwgABW4R0BKowUBK1f+VIkOU4QCBToQACJoRTBIgJWDK4YDBWQRVCWFZEBVZBQEAAxNDlbGBBQgABAoguEWE6IBKoxUCIAr8EUIQIDCQUALgOiAAJnCWQavoVgb8BKoeoKoRBBfQZYEBAa9E5mo1FUK4IOEV9JWBVYyUBSoZCBTAJXCLAfMAohgD0V+WITIDLAivkKw6qFTIV+AQKZCKIagDAAl+qgUBV4pYEK8ZWFVgJQBVISvDLIQCCIQigELwQHB0V4vCuCC4IPEK8ZWGKoSvDAAhdC5pYEq3MWgQOCOgJbD1AGBPoZrCgGoK075CHoQAELQi6C0RXCLAKhEDIwADYgYqCK0xGBKoStGBAKaCLAbxDTgRVC5nNCIN+DIQRDCogSBK8ZWBKopPDAIJZDBgZYFUIS8EvwPCAoKvENgZXeKwpJDABxlFK4ZMBK4ZmCAQXNWoJYDYghWk64AEh0Oh8OhAKFLAmiHwVO0RVBvFUBoQAEBgKyBKwVWLDxXGKwpSBKoQOBLIxYE5oFD0V+VQKyCAIIFBV4IABLIZXCqxXaKwcOVw5VF1gIBA4JXGIoUAACitCWDhXCEgJWGJoIABGYYKELApXBDIpuCh4GB1gACBYIICDIJXFlaubLAJXFHgJWFLASyDWAwZFhCmGaghXKWC5XDVw8OHwI9HK4xYCDIoYHDIZ8BDIhXjdgi4DAAxWDdwrIFK5KwOAoJWYgCvHJAMOHpJZCSwZXGJIIZIB4MHa4JXELIxLJVyBXFFwKVJLIavKhBxJDASvFKwpXJgGoK6GiK4pYBK5QMBK5UHDJgNBK4tWK5cA0SvXdoQuDK8ANCDIZSBK4YOBJI9+K5RWHHoiwBF4OsKxo9B1BXEBoRWJP4hXDAApOGV5gMBK4eiK4I+EK4RYHBQMOhyuE1ByBAA6cDAQQNJK5pCBV547BWBBZBSYsIMYZXDDIQfB1B5BAYIlDAAXN5wECNoSuHK45YCK56vDLApOBAIYCBdIJWILAYABEQRTD5uoqmi5otCTYJWIYJKvUK4vXBwIACKoRWEK5RZHWoRXBAgRXHqxXCZwQADAwKvRFAKwGLIwKFKAZzDWAt+BYJbC5oDBWoIRBK5eivF+AAl4K540BQIQ+CAB5VCIQL7CeoKrFAQS1EB4YeCLQwvJK5kOAYItBbomiJIRgEforfBN4IYBUoIEC5pjDL4K9FBgQsBb4ZYGJw4vBK54uDFQRMFLwIADVgj+DUAV4IoqxEBAmiQgRgFLQZXGgF+V5sPUghKCeALiEAAr3GKoQCBVAKxBBYQZKFYLCBQ4xXJRYJXPIII4BLgRYEABCRDDIZEBUoYECDZd45qGFgA9BAAJLH1BXOS4gBBdwTZBHwwGEHAKuCAoQMELAZgDJQLeDCgKEFK4MPAYJMIK5JYDh0AdAZCCFILlE0QABGgI1CCQiXBJYYRBVw5YDEgYsCKx4AMK4UIUgeoE4StBJYRBDRYiSCKIacCKY5MFCoUAbQKuSK56vBRIpWCKQYADBAZRCKYRKDJILPCOIQABvwDCBYSxBZQRtDK7cQDYKwCFAOiFoL5DAQQAIIgIDEKIZoDAAJxDAYJeBgAHCCQRWCK66wDK4KVDK4JSDGoKYCSwShGfggcBBYahCMAYdCvALBGIOiAoZWZK4qvBJwIhBTIRfCTwb4EdAQOCYoLAKa4QkBvx2BvCDEK8CwCIQQEBGAKbBAARBEewRaCL4JXDXQmoUALGDPAgrCKwpXZLAcIEwZYDKopZEWYgBBTANUgF4vwYEDYqzCNgR7EgEOKzSwELAjyDRIIxBHoiwCWoZWCegRpFCwgUDFIQoCKwZXbLAYhBLAeip0Ac4Y3CK4YGB0V+NIKqB5gCBLoJQFVgQACVoIVB0QRBgBVBKziwEK4ifDWYXNfoKYDUwIACqheDlUrCwZ4DOYQKC1EqvxYBFQKueLBd+qkqqhODAAl4p1OSwXNLAVUAIMqCo4SBp1UB4N4WgKufLBg1BAAIzBAolO1F4TwSkDLAb9BJoN+DIJSCB4ICCVoJWiK4pYF5oyBKgQDDJ4RTCAoRcECQV+BId+CAQjBKIMPhxXiLBWoAASrBAAIEBMgZIBLohICJ4JWBAgN4VwRwDgBVCh+iK0CxMKIQCCVIhbEAAYFCUoQYCCYmoVsxYIh0ALIg+DWoKhBUwRHC0QKBW4IKEAAqmBKwMPiBWmWI6yFAAq1BV4YKGNYYAFKoQACK05YJLJCnBJZBVKKwMOK1hYDLJ2iUwJPH5pVIAAJXBBARWpLAX+LApZCLQqxCLIvNK4YVCDQMIVoXMFIQAsJwZZFLQZbDLAKqFBwYVChEIKoQMCK1qyLLY4AGfwZUBVYSsxWRJaKABy5EK2ZZELRKiCUYZUKKu5aG/y0OKYXMCwYA/WgoAE0QEDB4RV/LRQAJJn4A/AH4A/AFo"));
+ return nova;
+}
+
+function novaEyesStage1() {
+ var novaEyesStage1 = require("heatshrink").decompress(atob("rFYxH+AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4ArgAAFDNhVj5gAFH58rDIXN5movwZRKs+oAIg/OgEr5mi1Go0V+AYIFCLN7oGABIZiK0fXAAkOh0Ph0IBQo+HDI0PhEHhEPDJpWoKQJVCh5ZHHwoZFCgcPg50BLIpYpHohVF1hFChxXODAQACL4hXsHgwABgAACBQg+HDIhuChAZEOYJYtJYkIKwo+EBoJXKJYIABDIyxGK8yUFSYpXKHwTIFKw4NFWFKUGHg0AKwaWGDIrHGWGA9QLIUIK5LJBDJAaDK9o8BhA9JHwSvLK5QQBhEOK9qVLgAMBK5bJJAAMHK9yuBJIRXWh78BDJBWBDIpXnWAIvB1hWNK4xYCOJK7BDIxYoGAZYGBQMOdg6wFBgZWFg7IGK9JYCGQI8FdYpXILAMIDI7IILFZOBAIYCBIoI8KOQ4VDYwRWtHorwCAAQ/DBohXKLAZZCLYQZKLFRZFBQo8HDJLLBDJpYlAB4ZiLE3M5moAQIAFHhgZD5oaCDofNK14/E1Go0QADHaBZDKwQBBKuTyKDNgA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AD4A=="));
+ return novaEyesStage1;
+}
+
+function novaEyesStage0() {
+ var novaEyesStage0 = require("heatshrink").decompress(atob("rFYxH+AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4ArgAAFDNhVj64AFH58rDLBVrH6EAlYZXK0Y7KH4gZiK1MOh0Ph0IHxoZGh8Ig8Ih5YwHgpSBKoUPLI4+FDIoUDh8HOgJZFLFI9EKousIoUOK5wYCAARfEK9g8GAAMAAAQKEHw4ZENwUIDIhzBLFpLEhBWFHwgNBK5RLBAAIZGWIxXmSgqTFK5Q+CZApWHBoqwpSgw8GgBWDSwwZFY4ywwHqBZChBXJZIIZIDQZXtHgMIHpI+CV5ZXKCAMIhxXtSpcABgJXLZJIABg5XuVwJJCK60PfgIZIKwIZFK86wBF4OsKxpXGLARxJXYIZGLFAwDLAwKBhzsHWAoMDKwsHZAxXpLAQyBHgrrFK5BYBhAZHZBBYrJwIBDAQJFBHhRyHCobGCK1o9FeAQACH4YNEK5RYDLIRbCDJRYqLIoKFHg4ZJZYIZNLFYAIHhIZZLEo/LBgIZkLNw7QDLJZnAAgZsAH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4AfA"));
+ return novaEyesStage0;
+}
+
+function novaEyesStage2() {
+ var novaEyesStage2 = require("heatshrink").decompress(atob("rFYxH+AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4ArgAAFDNhVj5nM1ABC5g/PlYQB1AYCvwZRKsxWE0RZPBgOiv2iAQNUAYIACLN6RBHYQAETQKcDHxMAvwACLIYaEOYRWtGQYAD1BAELBKsBBgRRCWIQBCDwZYqKwKoGLowFDHwpWCDIRRBAQPMAwd+vANBWNRXBJASoDeRKWGDIoEBOoQDDDAQeBK9CUBJQiTCAAq2CB4Q+DDITGFCAZ2DOohYngAAVDLhWj64ACh0PhA0G6+sBoMPCQY+BgAKCBYMOhwZHBoYZFK88PAAKMHKwYABK4oZEKw5YEDIxXpHpRZChBXJZIIZIDQZXtHgMIHpI+CV5ZXKCAMIhxXtSpcABgJXLZJIABg5XuVwJJCK60PfgIZIKwIZFK86wBF4OsgErKxZXGLARxJXYIZGLEoAVDLhXk1AAC5mo0QGDABJXEBIgaBDYfMAA4ZELEouEHgIALHgsABAN+5hUB0R1CDJywlRIvNAQYECMgZXGY4ZXBCAIDCKQTIILFAAISYo8IOQYUDAoQDCBQRWrHwQABcoxdFHhJyCC4i3DDoWiK1hYDRgSQCAAgMBDJ3NDQJWE5oZMLM6ZGHaBZDOgQBBKuQ/FAAgZsAH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4AfA"));
+ return novaEyesStage2;
+}
+
+function novaEyesStage3() {
+ var novaEyesStage3 = require("heatshrink").decompress(atob("rFYxH+AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4ArgAAFDNhVj5nM1ABC5g/PlYQB1AYCvwZRKsxWE0RZPBgOiv2iAQNUAYIACLN6RBHYQAETQKcDHxMAvwACLIYaEOYRWtGQYAD1BAELBKsBBgRRCWIQBCBQV+LFRWBVAxdGAoY+FKwQZCJgICB5hZCvF+qgFBWNRXBJASoDeROiK4ytDCIOoC4R5DAYQIBK9A8BGIY2EABQ+DZARRBUoJVCDYzTELE49CVgo7KHosABoocFZYgBDK8yuGABJfGHwIZQLYQcDLEsAAC4ZaK8nXAAMPh8OGhHX1gPBhATCK4QZDh0PJ5IaCh4ZEK848BhCOKHwI9FOIpXKCAMIhxXpdrAZZK8mo1Gi0V+AIYABBAOiBoIAEBAJXCAwQYDDIwAFDYRXoJAozCAYRDCAAN4I4RXCJAoFELBIdBK8o+DGoyzDKQKcDHgpyBYwquECoYMCAQJWmWAgvCUYadHdgzKF5jMCAITTD5gIDK84+B5gABG4SZDd5A8FZQZLDLQIWGDJCwlLAQAC5oCDAgQABVwoZEKIQeCAYTGFDI5YmcogEEAAg8IOQYUDAoQDCBQRWrHwQABJoV+SQ48KOQRpEW4YdC0RWsLAaMCAIQAEBgIZO5oaCKwfNDJhZnTIw7QLIZ0EKuQ/FAAgZsAH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4AfA=="));
+ return novaEyesStage3;
+}
+
+function novaEyesStage4() {
+ var novaEyesStage4 = require("heatshrink").decompress(atob("rFYxH+AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4ArgAAFDNhVj5nM1ABC5g/PlYQB1AYCvwZRKsxWE0RZPBgOiv2iAQNUAYIACLN6RBHYQAETQKcDHxMAvwACLIYaEOYRWtGQYAD1BAELBKsBBgRRCWIQBCBQV+LFRWBVAxdGAoY+FKwQZCJgICB5hZCvF+qgFBWNRXBJASoDeROiK4ytDCIOoC4R5DAYQIBK9EAJQg2EBAoFEHwauBUIN4p1UKoQbGaYhYnK4KvCKoQFBHYi6EHosrBQQaCYwRbBDQd4XYSwoKwIuB5oACIAJBBAYpMDAAI+BKwIKDDAJKBvC2BEgIUDEwawmdgIvDGIhVDLYYAEHoIZBVIQPBKwJeBZoLJEXgYHBK8wzBHISMF5xdFK45GCMwQDBNYonBBAN+XAV+K8wAPlYHGDKIAHV8ztCqiHBWo6gEdgOoK4SpEAgQODDwIjBAAIjBCIRXm5g1CHQQCDHgJWCAgIAD0RXDDIPM5oRC5oECDYR+CK9aVES4ZQFHQShEK4yvEAAbJGAARXlHwI9BJYIAEAwLsEql4vwBBHgZYCCQRrEEYgMDAQJWmWAgvCJQJaDUQQ8DVwYZFVAS9CAITED5gIDK84+BcIQ3CTIa5HKwjKFJYZaBCwwZIWEpYCAAXNAQYECAAJkBK4ysCYYR2DAQIICAAZXpd4zqFAAg8IOQYUDAoQDCBQRWrHwQABJoV+SQ48KOQRpEW4YdC0RWsLAaMCAIQAEBgIZO5oaCKwfNDJhZnTIw7QLIZ0EKuQ/FAAgZsAH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4AfA=="));
+ return novaEyesStage4;
+
+}
+
+function novaEyesWhiteStage0() {
+ var novaEyesWhiteStage0 = require("heatshrink").decompress(atob("rFYxH+AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4ArgAAFDNhVj64AFH58rDLBVrH6EAlYZXK0Y7KH4gZiK2Q+JDLJW0Hw4ZZK/5X6HiY+FDLJX/K/Q8VHwYZZK/5X/K/5X/K/5X/K/5X/K+Y+WHgYZZK/5X7Hyg8FDLJX/K/Y+SHg4ZZLGg8JDLJYlH5YMBDMhZuHaAZZLM4AEDNgA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AD4A="));
+ return novaEyesWhiteStage0;
+}
+
+function novaEyesTransStage1() {
+ var novaEyesTransStage1 = require("heatshrink").decompress(atob("rFYxH+AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4ArgAAFDNhVj5gAFH58rDIXN5movwZRKs+oAIg/OgEr5mi1Go0V+AYIFCLN7oGABIZiK0YRYDLJW0CY4ZZK/5X6FCoVDDLJX/K/QmXC4IZZK/5X/K/5X/K/5X/K/5X/K+YmWCoYZZK/5X7FCgTFDLJX/K/YqSCI4ZZLEoAPDMRYm5nM1ACBAAo8MDIfNDQQdD5pWvH4mo1GiAAY7QLIZWCAIJVyeRQZsAH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4AfA"));
+ return novaEyesTransStage1;
+}
+
+function novaEyesTransStage2() {
+ var novaEyesTransStage2 = require("heatshrink").decompress(atob("rFYxH+AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4ArgAAFDNhVj5nM1ABC5g/PlYQB1AYCvwZRKsxWE0RZPBgOiv2iAQNUAYIACLN6RBHYQAETQKcDHxMAvwACLIYaEOYRWtGQYAD1BAELBKsBBgRRCWIQBCDwZYqKwKoGLowFDHwpWCDIRRBAQPMAwd+vANBWNRXBJASoDeRKWGDIoEBOoQDDDAQeBK9CUBJQiTCAAq2CB4Q+DDITGFCAZ2DOohYngAAVDLhWjC7AZZK/5X/K/5X/K/5X/K/5X/K+YmBACoZcK8moAAXM1GiAwYAJK4gJEDQIbD5gAHDIhYlFwg8BABY8FgAIBv3MKgOiOoQZOWEqJF5oCDAgRkDK4zHDK4IQBAYRSCZBBYoABCTFHhByDCgYFCAYQKCK1Y+CAALlGLoo8JOQQXEW4YdC0RWsLAaMCSAQAEBgIZO5oaBKwnNDJhZnTIw7QLIZ0CAIJVyH4oAEDNgA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AD4A=="));
+ return novaEyesTransStage2;
+}
+
+function novaEyesTransStage3() {
+ var novaEyesTransStage3 = require("heatshrink").decompress(atob("rFYxH+AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4ArgAAFDNhVj5nM1ABC5g/PlYQB1AYCvwZRKsxWE0RZPBgOiv2iAQNUAYIACLN6RBHYQAETQKcDHxMAvwACLIYaEOYRWtGQYAD1BAELBKsBBgRRCWIQBCBQV+LFRWBVAxdGAoY+FKwQZCJgICB5hZCvF+qgFBWNRXBJASoDeROiK4ytDCIOoC4R5DAYQIBK9A8BGIY2EABQ+DZARRBUoJVCDYzTELE49CVgo7KHosABoocFZYgBDK8yuGABJfGHwIZQLYQcDLEsAAC4ZaK8gXYDLJX/K/jtYDLJXk1Go0WivwBDAAIIB0QNBAAgIBK4QGCDAYZGAAobCK9BIFGYQDCIYQABvBHCK4RIFAohYJDoJXlHwY1GWYZSBTgY8FOQLGFVwgVDBgQCBK0ywEF4SjDTo7sGZQvMZgQBCaYfMBAZXnHwPMAAI3CTIbvIHgrKDJYZaBCwwZIWEpYCAAXNAQYECAAKuFDIhRCDwQDCYwoZHLEzlEAggAEHhByDCgYFCAYQKCK1Y+CAAJNCvySHHhRyCNIi3DDoWiK1hYDRgQBCAAgMBDJ3NDQRWD5oZMLM6ZGHaBZDOghVyH4oAEDNgA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AD4="));
+ return novaEyesTransStage3;
+}
+
+function novaTopRedraw() {
+ var novaTopRedraw = require("heatshrink").decompress(atob("rFYxH+AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AHsAABZM/K5fM5kAv2igGi1EAvFUK/6v/K8isDAQV+BAK4BJn5XL1BOB1GoLod4LQIRE0XM5vM1HN5pX/5hXBIwJdGKwgYGA45X3TgJRDLQZXFAH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AB8AIH4A/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/AH4AP"));
+ return novaTopRedraw;
+}
+
+function star() {
+ var backgroundstar = require("heatshrink").decompress(atob("rFYxH+AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A9qxA/K/5W/AH5XGLH6w/K34A/AA2BwJB/K62sIP5XWIH4AW1iv/K/5X/AHFWqwCCq2BJ4ICB1mBAAJZBAQIQBlYUCAYMrAgJZ8KQICBU4QCBLIJVBK4QOCNAJSCOQQA/XpQA/K6q3CAH5XUwJB/ACqu/V7BA/AC2BwJB/LC5A/V/4AtqxX/AC2slZB/ACuBqxB/LC5A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4Ao"));
+ return backgroundstar;
+}
+
+function novaOpenEyes(speed, white, animation) {
+ if (!white) {
+ g.drawImage(novaEyesStage4(), -10, -10, {
+ scale: 2.2
+ });
+ setTimeout(function() {
+ g.drawImage(novaEyesStage3(), -10, -10, {
+ scale: 2.2
+ });
+ }, speed * 2);
+ setTimeout(function() {
+ g.drawImage(novaEyesStage2(), -10, -10, {
+ scale: 2.2
+ });
+ }, speed * 3);
+ setTimeout(function() {
+ g.drawImage(novaEyesStage1(), -10, -10, {
+ scale: 2.2
+ });
+ }, speed * 4);
+ if (animation) {
+ setTimeout(function() {
+ g.drawImage(novaEyesStage0(), -10, -10, {
+ scale: 2.2
+ });
+ }, speed * 5);
+ } else {}
+ } else {
+
+ g.drawImage(novaEyesStage4(), -10, -10, {
+ scale: 2.2
+ });
+ setTimeout(function() {
+ g.drawImage(novaEyesWhiteStage0(), -10, -10, {
+ scale: 2.2
+ });
+ timedraw(true);
+ g.drawImage(novaEyesTransStage3(), -10, -10, {
+ scale: 2.2
+ });
+ }, speed * 2);
+ setTimeout(function() {
+ g.drawImage(novaEyesWhiteStage0(), -10, -10, {
+ scale: 2.2
+ });
+ timedraw(true);
+ g.drawImage(novaEyesTransStage2(), -10, -10, {
+ scale: 2.2
+ });
+ }, speed * 3);
+ setTimeout(function() {
+ g.drawImage(novaEyesWhiteStage0(), -10, -10, {
+ scale: 2.2
+ });
+ timedraw(true);
+ g.drawImage(novaEyesTransStage1(), -10, -10, {
+ scale: 2.2
+ });
+ open = true;
+ }, speed * 4);
+ if (animation) {
+ setTimeout(function() {
+ g.drawImage(novaEyesWhiteStage0(), -10, -10, {
+ scale: 2.2
+ });
+ open = true;
+ }, speed * 5);
+ } else {}
+ }
+}
+
+function novaCloseEyes(speed, white, animation) {
+ if (!white) { // for other
+ if (animation) {
+ g.drawImage(novaEyesStage0(), -10, -10, {
+ scale: 2.2
+ });
+ } else {}
+ setTimeout(function() {
+ g.drawImage(novaEyesStage1(), -10, -10, {
+ scale: 2.2
+ });
+ }, speed * 2);
+ setTimeout(function() {
+ g.drawImage(novaEyesStage2(), -10, -10, {
+ scale: 2.2
+ });
+ }, speed * 3);
+ setTimeout(function() {
+ g.drawImage(novaEyesStage3(), -10, -10, {
+ scale: 2.2
+ });
+ }, speed * 4);
+ setTimeout(function() {
+ g.drawImage(novaEyesStage4(), -10, -10, {
+ scale: 2.2
+ });
+ }, speed * 5);
+ } else { // for time
+
+ if (animation) {
+ timedraw(true);
+ g.drawImage(novaEyesWhiteStage0(), -10, -10, {
+ scale: 2.2
+ });
+ } else {}
+ setTimeout(function() {
+ timedraw(true);
+ g.drawImage(novaEyesTransStage1(), -10, -10, {
+ scale: 2.2
+ });
+ }, speed * 2);
+ setTimeout(function() {
+ timedraw(true);
+ g.drawImage(novaEyesTransStage2(), -10, -10, {
+ scale: 2.2
+ });
+ }, speed * 3);
+ setTimeout(function() {
+ timedraw(true);
+ g.drawImage(novaEyesTransStage3(), -10, -10, {
+ scale: 2.2
+ });
+ }, speed * 4);
+ setTimeout(function() {
+ g.drawImage(novaEyesStage4(), -10, -10, {
+ scale: 2.2
+ });
+ }, speed * 5);
+ open = false;
+ }
+}
+
+function timedraw(animation) {
+ if (open && timemode || animation) {
+ g.setFont("6x8", 4);
+ g.setColor("#00F");
+ var d = new Date();
+ var h = d.getHours(),
+ m = d.getMinutes();
+ g.drawImage(novaEyesWhiteStage0(), -10, -10, {
+ scale: 2.2
+ });
+ g.drawImage(novaEyesTransStage1(), -10, -10, {
+ scale: 2.2
+ });
+ // Check if single digit
+ var hourDigits = h.toString();
+ if (hourDigits.length === 1) { // if hour digits only one, render in middle
+ g.drawString(h, 50, 66);
+ } else {
+ g.drawString(h, 38, 66);
+ }
+ var minutes = m.toString();
+ if (minutes.length === 1) { // same for mins
+ g.drawString(m, 107, 66);
+ } else {
+ g.drawString(m, 94, 66);
+ }
+ }
+}
+
+function main() {
+ Bangle.on("lock", function(lock) {
+ g.drawImage(novaTopRedraw(), -10, novaYPos, {
+ scale: 2.2
+ });
+ if (lock) {
+ novaCloseEyes(200, true, false);
+ setTimeout(function() {
+ novaOpenEyes(100, false, false);
+ timemode = false;
+ }, 1200);
+ }else{
+ novaCloseEyes(100, false, false);
+ setTimeout(function() {
+ timemode = true;
+ novaOpenEyes(200, true, false);
+ }, 600);
+ }
+ });
+}
+
+
+g.setFont("6x8", 4);
+g.setColor("#FFF");
+var open = false;
+var timemode = true;
+var clockmode;
+var novaYPos = -7;
+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});
+g.drawImage(star(), 120, -5, {scale:0.8});
+
+
+
+var secondInterval = setInterval(function() {
+ timedraw();
+ g.drawImage(novaTopRedraw(), -10, novaYPos, {
+ scale: 2.2
+ });
+}, 1000);
+
+novaOpenEyes(300, true, false);
+main();
diff --git a/apps/novaclock/app.png b/apps/novaclock/app.png
new file mode 100644
index 000000000..6270be5d3
Binary files /dev/null and b/apps/novaclock/app.png differ
diff --git a/apps/novaclock/metadata.json b/apps/novaclock/metadata.json
new file mode 100644
index 000000000..c1dad60a1
--- /dev/null
+++ b/apps/novaclock/metadata.json
@@ -0,0 +1,15 @@
+{ "id": "novaclock",
+ "name": "Nova Clock",
+ "shortName":"Nova Clock",
+ "icon": "app.png",
+ "type": "clock",
+ "version":"0.1",
+ "description": "A clock inspired by the Kirby series",
+ "tags": "clock",
+ "supports": ["BANGLEJS2"],
+ "readme":"README.md",
+ "storage": [
+ {"name":"novaclock.app.js","url":"app.js"},
+ {"name":"novaclock.img","url":"app-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/pongclock/ChangeLog b/apps/pongclock/ChangeLog
new file mode 100644
index 000000000..9b83b345f
--- /dev/null
+++ b/apps/pongclock/ChangeLog
@@ -0,0 +1,2 @@
+0.01: First release
+0.02: added missing type i metadata
diff --git a/apps/pongclock/README.md b/apps/pongclock/README.md
new file mode 100644
index 000000000..894070b73
--- /dev/null
+++ b/apps/pongclock/README.md
@@ -0,0 +1,15 @@
+# Pong Clock
+
+A clock which is playing Pong while showing the current time as score
+* Settings
+ * Show or hide widgets (auto detecting the used widgets areas)
+ * Use inverted or standard theme colors for the play area
+ * Optionally pause while locked (saving battery)
+* Loosely based on [https://codepen.io/Rabrennie/pen/WxNEoe](https://codepen.io/Rabrennie/pen/WxNEoe)
+
+
+
+
+
+## Creator
+[@pidajo](https://github.com/pidajo)
diff --git a/apps/pongclock/app.js b/apps/pongclock/app.js
new file mode 100644
index 000000000..2a10bf6ed
--- /dev/null
+++ b/apps/pongclock/app.js
@@ -0,0 +1,310 @@
+class Ball {
+ constructor(collision) {
+ this.collision = collision;
+ this.w = 4;
+ this.h = this.w;
+ this.y = height / 2 - this.h / 2;
+ this.x = width / 2 - this.w / 2;
+ this.oldX = this.x;
+ this.oldY = this.y;
+ this.velX = 6;
+ this.velY = 3.5 + Math.random();
+ }
+
+ reset() {
+ this.y = height / 2 - this.h / 2;
+ this.x = width / 2 - this.w / 2;
+ this.velX = 6;
+ this.velY = 3.5 + Math.random();
+ }
+
+ checkCollision(that, isLeft) {
+ let test = false;
+ if (isLeft) {
+ test = this.x <= that.w + this.w && this.y > that.y && this.y < that.y + that.h;
+ } else {
+ test = this.x >= that.x + this.w && this.y > that.y && this.y < that.y + that.h;
+ }
+ if (test) {
+ this.velX = -this.velX;
+ this.velY = (3.5 + 2 * Math.random()) * this.velY / Math.abs(this.velY);
+
+ if (isLeft) {
+ right.follow = this;
+ left.follow = null;
+ } else {
+ left.follow = this;
+ right.follow = null;
+ }
+ }
+ }
+
+ move() {
+ if (this.velX > 0) {
+ this.checkCollision(right, false);
+ } else {
+ this.checkCollision(left, true);
+ }
+
+ this.x += this.velX;
+ this.y += this.velY;
+
+ if (this.y <= this.h) {
+ this.y = this.h;
+ this.velY = -this.velY;
+ }
+
+ if (this.y >= height - this.h) {
+ this.y = height - this.h;
+ this.velY = -this.velY;
+ }
+
+ if (this.x >= width) {
+ left.scored();
+ restart();
+ } else if (this.x < 0) {
+ right.scored();
+ restart();
+ }
+
+ }
+}
+
+class Paddle {
+ constructor(side) {
+ this.side = side;
+ this.w = 4; //15;
+ this.h = 30; //80;
+ this.y = height / 2 - this.h / 2;
+ this.follow = null;
+ this.target = height / 2 - this.h / 2;
+ this.score = 99;
+ this.hasLost = false;
+ }
+
+ reset() {
+ this.follow = null;
+ this.hasLost = false;
+ this.target = height / 2 - this.h / 2;
+ this.y = height / 2 - this.h / 2;
+ this.move();
+ }
+
+ scored() {
+ let d = new Date();
+ let value = 0;
+ if (this.side == "left") {
+ value = d.getHours();
+ } else {
+ value = d.getMinutes();
+ }
+ if (this.score < value) {
+ this.score++;
+ } else {
+ this.score = value;
+ }
+ }
+
+ move() {
+
+ if (this.follow && !this.hasLost) {
+ var dy = this.follow.y - this.y - this.h / 2;
+ this.y += dy / 2;
+ } else {
+ this.y += (this.target - this.y) / 10;
+ }
+ if (this.y < 0) {
+ this.y = 0;
+ }
+ if (this.y > height - this.h) {
+ this.y = height - this.h;
+ }
+ }
+}
+
+var updateTimeout = null;
+
+function update() {
+ var d = new Date();
+ var lastStep = Date.now();
+ left.move();
+ right.move();
+ if (d.getHours() != left.score) {
+ right.follow = null;
+ right.hasLost = true;
+ }
+ if (d.getMinutes() != right.score) {
+ left.follow = null;
+ left.hasLost = true;
+ }
+
+ ball.move();
+ redraw();
+ var nextStep = 40 - (Date.now() - lastStep);
+ //console.log(nextStep);
+ updateTimeout = setTimeout(update, nextStep > 0 ? nextStep : 0);
+ return lastStep;
+}
+
+function redraw() {
+ let fontHeight = width / 3.6;
+ let fontTop = top + height / 11;
+ let topHeight = top + height;
+ g.reset();
+
+ if (settings.isInvers) {
+ g.setColor(g.theme.bg);
+ g.setBgColor(g.theme.fg);
+ }
+
+ g.clearRect(0, top + left.oldY, left.w, top + left.oldY + left.h);
+ g.clearRect(width - right.w, top + right.oldY, width, top + right.oldY + right.h);
+ //g.clearRect(width / 2 - fontHeight * 1.4, fontTop, width / 2 + fontHeight * 1.4, fontTop + fontHeight);
+ g.clearRect(ball.oldX - ball.w, top + ball.oldY - ball.h, ball.oldX + ball.w, top + ball.oldY + ball.h);
+
+ g.setFontVector(fontHeight);
+ /**/
+ g.setFontAlign(1, -1);
+ g.drawString(("0" + left.score).substr(-2), 5 * width / 11, fontTop, true);
+ g.setFontAlign(-1, -1);
+ g.drawString(("0" + right.score).substr(-2), 6 * width / 11, fontTop, true);
+ /**/
+
+ g.drawLine(width / 2, top, width / 2, topHeight);
+ g.fillRect(0, top + left.y, left.w, top + left.y + left.h);
+ left.oldY = left.y;
+ g.fillRect(width - right.w, top + right.y, width, top + right.y + right.h);
+ right.oldY = right.y;
+ g.fillCircle(ball.x, top + ball.y, ball.w);
+ ball.oldX = ball.x;
+ ball.oldY = ball.y;
+}
+
+function restart() {
+ g.reset();
+ if (settings.isInvers) {
+ g.setColor(g.theme.bg);
+ g.setBgColor(g.theme.fg);
+ }
+ g.clearRect(0, top, width, top + height);
+ ball.reset();
+ left.reset();
+ right.reset();
+ right.follow = ball;
+ left.move();
+ right.move();
+ if (settings.withWidgets) {
+ Bangle.drawWidgets();
+ }
+}
+
+function stop() {
+ if (updateTimeout) {
+ clearTimeout(updateTimeout);
+ }
+ updateTimeout = null;
+ if (pauseTimeout) {
+ clearTimeout(pauseTimeout);
+ }
+ pauseTimeout = null;
+}
+
+var pauseTimeout = null;
+
+function pause() {
+ stop();
+ left.scored();
+ right.scored();
+ redraw();
+ pauseTimeout = setTimeout(pause, Date.now() % 60000);
+}
+
+//load settings
+const SETTINGS_FILE = "pongclock.json";
+var settings = Object.assign({
+ // default values
+ withWidgets: true,
+ isInvers: false,
+ playLocked: true,
+}, require('Storage').readJSON(SETTINGS_FILE, true) || {});
+require('Storage').writeJSON(SETTINGS_FILE, settings);
+
+//make clock
+Bangle.setUI("clock");
+
+//setup play area
+var height = g.getHeight(),
+ width = g.getWidth();
+var top = 0;
+
+g.reset();
+g.clearRect(0, top, width, height);
+
+if (settings.withWidgets) {
+ Bangle.loadWidgets();
+ Bangle.drawWidgets();
+ //console.log(WIDGETS);
+ if (global.WIDGETS) {
+ let bottom = 0;
+ for (var i in WIDGETS) {
+ var w = WIDGETS[i];
+ if (w.area) {
+ if (w.area.indexOf("t") >= 0) {
+ top = Bangle.appRect.y;
+ }
+ if (w.area.indexOf("b") >= 0) {
+ bottom = height - Bangle.appRect.y2;
+ }
+ }
+ }
+ height -= top + bottom;
+ }
+}
+
+if (settings.isInvers) {
+ g.setColor(g.theme.bg);
+ g.setBgColor(g.theme.fg);
+}
+g.clearRect(0, top, width, top + height);
+
+//setup game
+var left = new Paddle("left");
+var right = new Paddle("right");
+var ball = new Ball(true);
+
+left.x = 20;
+right.x = width - 20;
+
+left.scored();
+right.scored();
+
+Bangle.on("lock", (on) => {
+ //console.log(on);
+ if (!settings.playLocked) {
+ if (on) {
+ pause();
+ } else {
+ stop();
+ update();
+ }
+ }
+});
+
+//start clock
+restart();
+if (!settings.playLocked && Bangle.isLocked()) {
+ pause();
+} else {
+ update();
+}
+
+/*
+//local testing
+require("Storage").write("pongclock.info",{
+ "id":"pongclock",
+ "name":"Pong Clock",
+ "type":"clock",
+ "src":"pongclock.app.js",
+ "icon":"pongclock.img"
+});
+*/
diff --git a/apps/pongclock/metadata.json b/apps/pongclock/metadata.json
new file mode 100644
index 000000000..c714e9a10
--- /dev/null
+++ b/apps/pongclock/metadata.json
@@ -0,0 +1,21 @@
+{ "id": "pongclock",
+ "name": "Pong Clock",
+ "shortName":"Pong Clock",
+ "icon": "pongclock.png",
+ "version":"0.02",
+ "description": "A Pong playing clock",
+ "type": "clock",
+ "tags": "",
+ "allow_emulator":true,
+ "supports": ["BANGLEJS", "BANGLEJS2"],
+ "readme":"README.md",
+ "screenshots" : [ { "url":"screenshot.png" }, { "url":"screenshot_settings.png" }, { "url":"screenshot_invers_full.png" } ],
+ "storage": [
+ {"name":"pongclock.app.js","url":"app.js"},
+ {"name":"pongclock.img","url":"pongclock-icon.js","evaluate":true},
+ {"name":"pongclock.settings.js","url":"settings.js"}
+ ],
+ "data": [
+ {"name":"pongclock.json"}
+ ]
+}
diff --git a/apps/pongclock/pongclock-icon.js b/apps/pongclock/pongclock-icon.js
new file mode 100644
index 000000000..22e472af4
--- /dev/null
+++ b/apps/pongclock/pongclock-icon.js
@@ -0,0 +1 @@
+atob("MDCEBAAAAAAAAAAAAAAALyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALyAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAIiAAAAAAAAAAEAAAAAAAABP/EAE//yAAAAAAAD8wAAAT//IAAAAAAv//EA////IAAAAAAP8gAAH///8gAAAAAvP/EB/xA/MAIiAAAf8QAAL/ED8wAAAAAxD/EC8wAf8ALyAAAv8AAAPyAC/wAAAAAAD/ED/wAf8ALyAAD/MAAA/zAC8wAAAAAAD/EAAAAv8ALyAAH/EAAAAAAD8wAAAAAAD/EAAAA/IALyAAL/AAAAAAAP8gAAAAAAD/EAAAH/EALyAAPzD/IAAAAv8AAAAAAAD/EAAAPzAALyAA/yD/IAAAD/IAAAAAAAD/EAAB/xAALyAB/xD/IAAAL/AAAAAAAAD/EAAP8wAALyAC/wD/IAAB/yAAAAAAAAD/EAAv8QAAIiAP8wD/IAAD/wAAAAAAAAD/EAD/MAAAAAAf/zP/MwAf8gAAAA//AAD/EAL/AAAAAAAf////8wA/8AAAAA//AAD/EB/zAAAAIiACIiL/MgL/IAAAAA//AAD/EC////8ALyAAAAD/IAP////wAA//AAD/EC////8ALyAAAAD/IAP////wAA//AAAAAAAAAAAALyAAAAAAAAAAAAAAAA//AAAAAAAAAAAALyAAAAAAAAAAAAAAAA//AAAAAAAAAAAALyAAAAAAAAAAAAAAAA//AAAAAAAAAAAALyAAAAAAAAAAAAAAAA//AAAAAAAAAAAALyAAAAAAAAAAAAAAAA//AAAAAAAAAAAALyAAAAAAAAAAAAAAAA//AAAAAAAAAAAAIiAAAAAAAAAAAAAAAA//AAAAAAAAAAAAAAAAAAAAAAAAAAD/8A//AAAAAAAAAAAAAAAAAAAAAAAAAAD/8AAAAAAAAAAAAAAAIiAAAAAAAAAAAAD/8AAAAAAAASEAAAAALyAAAAAAAAAAAAD/8AAAAAAAP/8wAAAALyAAAAAAAAAAAAD/8AAAAAAB///xAAAALyAAAAAAAAAAAAD/8AAAAAAC///yAAAALyAAAAAAAAAAAAD/8AAAAAAB///xAAAALyAAAAAAAAAAAAD/8AAAAAAAP/8wAAAALyAAAAAAAAAAAAD/8AAAAAAAASEAAAAALyAAAAAAAAAAAAD/8AAAAAAAAAAAAAAALyAAAAAAAAAAAAD/8AAAAAAAAAAAAAAAIiAAAAAAAAAAAAD/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALyAAAAAAAAAAAAAAAA==")
diff --git a/apps/pongclock/pongclock.png b/apps/pongclock/pongclock.png
new file mode 100644
index 000000000..78934d7c6
Binary files /dev/null and b/apps/pongclock/pongclock.png differ
diff --git a/apps/pongclock/screenshot.png b/apps/pongclock/screenshot.png
new file mode 100644
index 000000000..0750105f0
Binary files /dev/null and b/apps/pongclock/screenshot.png differ
diff --git a/apps/pongclock/screenshot_invers_full.png b/apps/pongclock/screenshot_invers_full.png
new file mode 100644
index 000000000..434e24872
Binary files /dev/null and b/apps/pongclock/screenshot_invers_full.png differ
diff --git a/apps/pongclock/screenshot_settings.png b/apps/pongclock/screenshot_settings.png
new file mode 100644
index 000000000..0b27286ea
Binary files /dev/null and b/apps/pongclock/screenshot_settings.png differ
diff --git a/apps/pongclock/settings.js b/apps/pongclock/settings.js
new file mode 100644
index 000000000..0d61d013d
--- /dev/null
+++ b/apps/pongclock/settings.js
@@ -0,0 +1,44 @@
+(function(back) {
+ var FILE = "pongclock.json";
+ // Load settings
+ var settings = Object.assign({
+ // default values
+ withWidgets: true,
+ isInvers: false,
+ playLocked: true,
+ }, require('Storage').readJSON(FILE, true) || {});
+
+ function writeSettings() {
+ require('Storage').writeJSON(FILE, settings);
+ }
+
+ // Show the menu
+ E.showMenu({
+ "" : { "title" : "Pong Clock" },
+ "< Back" : () => back(),
+ 'Widgets?': {
+ value: !!settings.withWidgets, // !! converts undefined to false
+ format: v => v?"Show":"Hide",
+ onchange: v => {
+ settings.withWidgets = v;
+ writeSettings();
+ }
+ },
+ 'Inverted?': {
+ value: !!settings.isInvers, // !! converts undefined to false
+ format: v => v?"Yes":"No",
+ onchange: v => {
+ settings.isInvers = v;
+ writeSettings();
+ }
+ },
+ 'On Lock?': {
+ value: !!settings.playLocked, // !! converts undefined to false
+ format: v => v?"Play":"Pause",
+ onchange: v => {
+ settings.playLocked = v;
+ writeSettings();
+ }
+ }
+ });
+})/*(load)/**/
diff --git a/apps/qrcode/ChangeLog b/apps/qrcode/ChangeLog
index 6d9cc0569..52eadbcf9 100644
--- a/apps/qrcode/ChangeLog
+++ b/apps/qrcode/ChangeLog
@@ -3,3 +3,4 @@
0.03: Forces integer scaling and adds more configuration (error correction, description, display)
0.04: Allow scanning of QR codes from camera or file
0.05: Change brightness on touch
+0.06: Add ability to generate contact info (MeCard format) QR code
diff --git a/apps/qrcode/custom.html b/apps/qrcode/custom.html
index 7ae3eb3af..9955ea6c9 100644
--- a/apps/qrcode/custom.html
+++ b/apps/qrcode/custom.html
@@ -8,6 +8,8 @@
+
+
@@ -64,6 +66,14 @@
+
+
Try your QR Code:
@@ -156,7 +166,7 @@
function toggleVis(id){
console.info("Got id", id);
- ["srcScanFile", "srcText", "srcWifi", "srcScanCam"].forEach(function (item){
+ ["srcScanFile", "srcText", "srcWifi", "srcScanCam", "srcMeCard"].forEach(function (item){
document.getElementById(item).style.display = "none";
});
if (id != undefined && id != null) document.getElementById(id).style.display = "block";
@@ -188,6 +198,37 @@
}
return qrstring;
}
+
+ function generateMeCardString(meNameFirst, meNameLast, mePhoneNumber, meEmail, meWebsite){
+ var meCardStringOutput = 'MECARD:';
+
+ //first & Last name part of string, can have one or both
+ if (meNameFirst.trim().length != 0 && meNameLast.trim().length != 0) {
+ meCardStringOutput += 'N:'+meNameLast.trim()+','+meNameFirst.trim()+';';
+ }
+ else if (meNameLast.trim().length != 0) {
+ meCardStringOutput += 'N:'+meNameLast.trim()+';';
+ }
+ else if (meNameFirst.trim().length != 0) {
+ meCardStringOutput += 'N:'+meNameFirst.trim()+';';
+ }
+
+ if (mePhoneNumber.trim().length != 0) {
+ meCardStringOutput += 'TEL:'+mePhoneNumber.trim()+';';
+ }
+
+ if (meEmail.trim().length != 0) {
+ meCardStringOutput += 'EMAIL:'+meEmail.trim()+';';
+ }
+
+ if (meWebsite.trim().length != 0) {
+ meCardStringOutput += 'URL:'+meWebsite.trim()+';';
+ }
+
+ meCardStringOutput += ';';
+ return meCardStringOutput;
+ }
+
function refreshQRCode(){
if (qrcode == null){
qrcode = new QRCode("qrcode", {
@@ -206,6 +247,14 @@
const hidden = document.getElementById("hidden").checked;
const wifiString = generateWifiString(ssid, password, hidden, encryption);
qrText= wifiString;
+ } else if (document.getElementById("useMECARD").checked) {
+ const meNameFirst = document.getElementById("meNameFirst").value;
+ const meNameLast = document.getElementById("meNameLast").value;
+ const mePhoneNumber = document.getElementById("mePhoneNumber").value;
+ const meEmail = document.getElementById("meEmail").value;
+ const meWebsite = document.getElementById("meWebsite").value;
+ const meCardString = generateMeCardString(meNameFirst, meNameLast, mePhoneNumber, meEmail, meWebsite);
+ qrText = meCardString;
} else if (document.getElementById("useCAM").checked) {
qrText= document.getElementById("camQrResult").innerText;
} else if (document.getElementById("useFILE").checked) {
@@ -258,6 +307,14 @@
}
document.getElementById("useTEXT").addEventListener("change",function(){toggleVis("srcText");});
+
+ document.getElementById("useMECARD").addEventListener("change",function(){toggleVis("srcMeCard");});
+ document.getElementById("meNameFirst").addEventListener("change",refreshQRCode);
+ document.getElementById("meNameLast").addEventListener("change",refreshQRCode);
+ document.getElementById("mePhoneNumber").addEventListener("change",refreshQRCode);
+ document.getElementById("meEmail").addEventListener("change",refreshQRCode);
+ document.getElementById("meWebsite").addEventListener("change",refreshQRCode);
+
document.getElementById("useCAM").addEventListener("change",function(){
initQrScanner();
initQrCam();
@@ -314,7 +371,6 @@ g.setColor(1,1,1);
});
-
document.getElementById('camList').addEventListener('change', event => {
scanner.setCamera(event.target.value).then(updateFlashAvailability);
});
diff --git a/apps/qrcode/metadata.json b/apps/qrcode/metadata.json
index 22f8f7b53..24af7b813 100644
--- a/apps/qrcode/metadata.json
+++ b/apps/qrcode/metadata.json
@@ -1,7 +1,7 @@
{
"id": "qrcode",
"name": "Custom QR Code",
- "version": "0.05",
+ "version": "0.06",
"description": "Use this to upload a customised QR code to Bangle.js",
"icon": "app.png",
"tags": "qrcode",
diff --git a/apps/rebble/ChangeLog b/apps/rebble/ChangeLog
index 4b415c1c5..4e2e76484 100644
--- a/apps/rebble/ChangeLog
+++ b/apps/rebble/ChangeLog
@@ -6,3 +6,4 @@
0.06: Add 12h support and autocycle control
0.07: added localization, removed deprecated code
0.08: removed unused font, fix autocycle, imported suncalc and trimmed, removed pedometer dependency, "tap to cycle" setting
+0.09: fix battery icon size
\ No newline at end of file
diff --git a/apps/rebble/metadata.json b/apps/rebble/metadata.json
index e28c67784..ec7650f53 100644
--- a/apps/rebble/metadata.json
+++ b/apps/rebble/metadata.json
@@ -2,7 +2,7 @@
"id": "rebble",
"name": "Rebble Clock",
"shortName": "Rebble",
- "version": "0.08",
+ "version": "0.09",
"description": "A Pebble style clock, with configurable background, three sidebars including steps, day, date, sunrise, sunset, long live the rebellion",
"readme": "README.md",
"icon": "rebble.png",
diff --git a/apps/rebble/rebble.app.js b/apps/rebble/rebble.app.js
index 8ba61f818..fc91fe0ac 100644
--- a/apps/rebble/rebble.app.js
+++ b/apps/rebble/rebble.app.js
@@ -236,7 +236,7 @@ function drawBattery(x,y,wi,hi) {
g.clearRect(x+2,y+2+2,x+wi-4-2,y+2+hi-2); // centre
g.setColor(g.theme.fg);
g.fillRect(x+wi-3,y+2+(((hi - 1)/2)-1),x+wi-2,y+2+(((hi - 1)/2)-1)+4); // contact
- g.fillRect(x+3, y+5, x +4 + E.getBattery()*(wi-12)/100, y+hi-1); // the level
+ g.fillRect(x+3, y+5, x +3 + E.getBattery()*(wi-10)/100, y+hi-1); // the level
if( Bangle.isCharging() )
{
diff --git a/apps/rgb/ChangeLog b/apps/rgb/ChangeLog
new file mode 100644
index 000000000..a08433b7c
--- /dev/null
+++ b/apps/rgb/ChangeLog
@@ -0,0 +1 @@
+0.01: initial release
diff --git a/apps/rgb/README.md b/apps/rgb/README.md
new file mode 100644
index 000000000..5723f99c6
--- /dev/null
+++ b/apps/rgb/README.md
@@ -0,0 +1,16 @@
+rgb
+===
+
+A simple RGB color selector utility for the BangleJS2 smartwatch.
+
+Features a vector toggle widget and swipe interactivity.
+
+Author
+------
+
+Written by pancake in 2022
+
+Screenshots
+-----------
+
+
diff --git a/apps/rgb/app-icon.js b/apps/rgb/app-icon.js
new file mode 100644
index 000000000..9919a4365
--- /dev/null
+++ b/apps/rgb/app-icon.js
@@ -0,0 +1 @@
+atob("MDCI/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/tfX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX1/7+/v7+/v7+/v7+/tfX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX1/7+/v7+/v7+/v7+/tfX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX1/7+/v7+/v7+/tfX19fX1wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANfX19fX1/7+/v7+/tfX19fX1wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANfX19fX1/7+/v7+/tfX19fX1wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANfX19fX1/7+/v7+/tfX1wAAALS0tLS0tLS0tB4eHh4eHh4eHh4eHgUFBQUFBQUFBQAAANfX1/7+/v7+/tfX1wAAALS0tLS0tLS0tB4eHh4eHh4eHh4eHgUFBQUFBQUFBQAAANfX1/7+/v7+/tfX1wAAALS0tLS0tLS0tB4eHh4eHh4eHh4eHgUFBQUFBQUFBQAAANfX1/7+/v7+/tfX1wAAALS0tLS0tLS0tB4eHh4eHh4eHh4eHgUFBQUFBQUFBQAAANfX1/7+/v7+/tfX1wAAALS0tLS0tLS0tB4eHh4eHh4eHh4eHgUFBQUFBQUFBQAAANfX1/7+/v7+/tfX1wAAALS0tLS0tLS0tB4eHh4eHh4eHh4eHgUFBQUFBQUFBQAAANfX1/7+/v7+/tfX1wAAALS0tLS0tLS0tB4eHh4eHh4eHh4eHgUFBQUFBQUFBQAAANfX1/7+/v7+/tfX1wAAALS0tLS0tLS0tB4eHh4eHh4eHh4eHgUFBQUFBQUFBQAAANfX1/7+/v7+/tfX1wAAALS0tLS0tLS0tB4eHh4eHh4eHh4eHgUFBQUFBQUFBQAAANfX1/7+/v7+/tfX1wAAALS0tLS0tLS0tB4eHh4eHh4eHh4eHgUFBQUFBQUFBQAAANfX1/7+/v7+/tfX1wAAALS0tLS0tLS0tB4eHh4eHh4eHh4eHgUFBQUFBQUFBQAAANfX1/7+/v7+/tfX1wAAALS0tLS0tLS0tB4eHh4eHh4eHh4eHgUFBQUFBQUFBQAAANfX1/7+/v7+/tfX1wAAALS0tLS0tLS0tB4eHh4eHh4eHh4eHgUFBQUFBQMDAwAAANfX1/7+/v7+/tfX1wAAALS0tLS0tLS0tB4eHh4eHh4eHh4eHgUFBQUFBQMDAwAAANfX1/7+/v7+/tfX1wAAALS0tLS0tLS0tB4eHh4eHh4eHh4eHgUFBQUFBQMDAwAAANfX1/7+/v7+/tfX1wAAALS0tLS0tLS0tB4eHh4eHh4eHhISEgMDAwMDAwMDAwAAANfX1/7+/v7+/tfX1wAAALS0tLS0tLS0tB4eHh4eHh4eHhISEgMDAwMDAwMDAwAAANfX1/7+/v7+/tfX1wAAALS0tLS0tLS0tB4eHh4eHh4eHhISEgMDAwMDAwMDAwAAANfX1/7+/v7+/tfX1wAAALS0tGxsbGxsbBISEhISEhISEhISEgMDAwMDAwMDAwAAANfX1/7+/v7+/tfX1wAAALS0tGxsbGxsbBISEhISEhISEhISEgMDAwMDAwMDAwAAANfX1/7+/v7+/tfX1wAAALS0tGxsbGxsbBISEhISEhISEhISEgMDAwMDAwMDAwAAANfX1/7+/v7+/tfX19fX1wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANfX19fX1/7+/v7+/tfX19fX1wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANfX19fX1/7+/v7+/tfX19fX1wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANfX19fX1/7+/v7+/v7+/tfX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX1/7+/v7+/v7+/v7+/tfX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX1/7+/v7+/v7+/v7+/tfX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX1/7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/g==")
diff --git a/apps/rgb/app.js b/apps/rgb/app.js
new file mode 100644
index 000000000..373ee6f54
--- /dev/null
+++ b/apps/rgb/app.js
@@ -0,0 +1,134 @@
+const rgb = [0, 0, 0];
+const hex = '0123456789ABCDEF';
+const w = g.getWidth();
+const h = g.getHeight();
+function drawToggle (value, x, y, options) {
+ if (!options) options = {};
+ if (!options.scale) options.scale = 1;
+ const h = (options.scale * 16);
+ const h2 = h / 2;
+ const w = (options.scale) * 32;
+
+ g.setColor(0.3, 0, 0.3);
+ g.fillCircle(x + h2, y + h2, h2 - 1);
+ g.fillCircle(x + w - h2, y + h2, h2 - 1);
+ g.fillRect(x + h2, y, x + w - h2, y + h);
+
+ y += 4;
+ g.setColor(0.6, 0.6, 0.6);
+ g.fillCircle(x + h2, y + h2 + 2, h2 - 1);
+ g.fillCircle(x + w - h2, y + h2 + 2, h2 - 1);
+ g.fillRect(x + h2, y + 2, x + w - h2, y + h + 1);
+
+ if (value) {
+ x += w - h;
+ }
+ g.setColor(0, 0.5, 0);
+ g.fillCircle(x + h2, y + h2 + 2, h2 - 1);
+ y -= 4;
+ if (colorMode) {
+ g.setColor(0, 1, 0);
+ } else {
+ g.setColor(0.5, 0.5, 0.5);
+ }
+ g.fillCircle(x + h2, y + h2 + 2, h2 - 1);
+
+ g.setColor(0, 0.8, 0);
+ g.fillCircle(x + h2 - 2, y + h2, h2 - 8);
+ if (colorMode) {
+ g.setColor(0, 1, 0);
+ } else {
+ g.setColor(0.5, 0.5, 0.5);
+ } g.fillCircle(x + h2 + 4, y + h2 + 4, h2 - 9);
+}
+
+function refresh () {
+ g.setBgColor(rgb[0] / 255, rgb[1] / 255, rgb[2] / 255);
+ g.clear();
+ g.setColor(1, 1, 1);
+ g.setFont12x20(1);
+ g.setColor(0, 0, 0);
+ let s = '#' + hex[(rgb[0] >> 4) & 0xf] + hex[rgb[0] & 0xf];
+ s += hex[(rgb[1] >> 4) & 0xf] + hex[rgb[1] & 0xf];
+ s += hex[(rgb[2] >> 4) & 0xf] + hex[rgb[2] & 0xf];
+ g.setColor(1, 0, 1);
+ g.fillRect(0, 0, w, 32);
+ g.setColor(0.2, 0, 1);
+ g.fillRect(0, 32, w, 33);
+ g.setColor(1, 1, 1);
+ g.drawString(s, 8, 8);
+ // drawToggle (colorMode, w - 8 - 32*(rgb[0]/50), 8 + 255- rgb[2], {scale:1 * rgb[0] / 50});
+ drawToggle(colorMode, w - 40, 4, { scale: 1.2 });
+
+ if (colorMode) {
+ g.setColor(1, 0, 0);
+ g.fillRect(0, h, w / 3, h - 32);
+ g.setColor(0, 1, 0);
+ g.fillRect(w / 3, h, w - (w / 3), h - 32);
+ g.setColor(0, 0, 1);
+ g.fillRect(w - (w / 3), h, w, h - 32);
+
+ g.setColor(0.5, 0, 0);
+ g.fillRect(0, h - 33, w / 3, h - 34);
+ g.setColor(0, 0.5, 0);
+ g.fillRect(w / 3, h - 33, w - (w / 3), h - 34);
+ g.setColor(0, 0, 0.5);
+ g.fillRect(w - (w / 3), h - 33, w, h - 34);
+ } else {
+ g.setColor(0.5, 0.5, 0.5);
+ g.fillRect(0, h, w, h - 32);
+ g.setColor(0.2, 0.2, 0.2);
+ g.fillRect(0, h - 33, w, h - 34);
+ }
+ // column lines
+ function f (x) {
+ const s = '' + (rgb[x] / 255);
+ return s.substring(0, 4);
+ }
+ g.setColor(1, 1, 1);
+ g.drawLine(w / 3, h, w / 3, h / 2);
+ g.drawLine(w - (w / 3), h, w - (w / 3), h / 2);
+ g.setFont6x15(2);
+ g.drawString(f(0), 8, h - 28);
+ g.drawString(f(1), 8 + (w / 3), h - 28);
+ g.drawString(f(2), 8 + (2 * w / 3), h - 28);
+}
+let k = -1;
+var colorMode = true;
+Bangle.on('touch', function (wat, tap) {
+ if (tap.x > w / 2 && tap.y < 32) {
+ colorMode = !colorMode;
+ refresh();
+ }
+});
+
+function deltaComponent (k, dy) {
+ rgb[k] -= dy;
+ if (rgb[k] > 255) {
+ rgb[k] = 255;
+ } else if (rgb[k] < 0) {
+ rgb[k] = 0;
+ }
+}
+Bangle.on("button", function() {
+ rgb[0] = rgb[1] = rgb[2] = 127;
+});
+Bangle.on('drag', function (tap, top) {
+ if (colorMode) {
+ if (tap.x < w / 3) {
+ k = 0;
+ } else if (tap.x > (w - (w / 3))) {
+ k = 2;
+ } else {
+ k = 1;
+ }
+ deltaComponent(k, tap.dy);
+ } else {
+ deltaComponent(0, tap.dy);
+ deltaComponent(1, tap.dy);
+ deltaComponent(2, tap.dy);
+ }
+ refresh();
+});
+refresh();
+
diff --git a/apps/rgb/app.png b/apps/rgb/app.png
new file mode 100644
index 000000000..e9210d2b6
Binary files /dev/null and b/apps/rgb/app.png differ
diff --git a/apps/rgb/metadata.json b/apps/rgb/metadata.json
new file mode 100644
index 000000000..7a47e65b9
--- /dev/null
+++ b/apps/rgb/metadata.json
@@ -0,0 +1,31 @@
+{
+ "id": "rgb",
+ "name": "rgb",
+ "shortName": "rgb",
+ "version": "0.01",
+ "type": "app",
+ "description": "RGB utility",
+ "icon": "app.png",
+ "allow_emulator": true,
+ "tags": "tools",
+ "supports": [
+ "BANGLEJS2"
+ ],
+ "readme": "README.md",
+ "storage": [
+ {
+ "name": "rgb.app.js",
+ "url": "app.js"
+ },
+ {
+ "name": "rgb.img",
+ "url": "app-icon.js",
+ "evaluate": true
+ }
+ ],
+ "screenshots": [
+ {
+ "url": "screenshot.png"
+ }
+ ]
+}
diff --git a/apps/rgb/screenshot.png b/apps/rgb/screenshot.png
new file mode 100644
index 000000000..1815cd492
Binary files /dev/null and b/apps/rgb/screenshot.png differ
diff --git a/apps/sched/ChangeLog b/apps/sched/ChangeLog
index e003248a3..5728bf734 100644
--- a/apps/sched/ChangeLog
+++ b/apps/sched/ChangeLog
@@ -10,3 +10,6 @@
0.09: Move some functions to new time_utils module
0.10: Default to sched.js if custom js not found
0.11: Fix default dow
+0.12: Update default buzz patterns to new values
+ Improve timer message using formatDuration
+ Fix wrong fallback for buzz pattern
diff --git a/apps/sched/lib.js b/apps/sched/lib.js
index 315e4e387..5089ed46c 100644
--- a/apps/sched/lib.js
+++ b/apps/sched/lib.js
@@ -106,8 +106,8 @@ exports.getSettings = function () {
defaultRepeat: false,
buzzCount: 10,
buzzIntervalMillis: 3000, // 3 seconds
- defaultAlarmPattern: "..",
- defaultTimerPattern: ".."
+ defaultAlarmPattern: "::",
+ defaultTimerPattern: "::"
},
require("Storage").readJSON("sched.settings.json", true) || {}
);
diff --git a/apps/sched/metadata.json b/apps/sched/metadata.json
index 76341a7ad..c8ed3acb8 100644
--- a/apps/sched/metadata.json
+++ b/apps/sched/metadata.json
@@ -1,7 +1,7 @@
{
"id": "sched",
"name": "Scheduler",
- "version": "0.11",
+ "version": "0.12",
"description": "Scheduling library for alarms and timers",
"icon": "app.png",
"type": "scheduler",
diff --git a/apps/sched/sched.js b/apps/sched/sched.js
index f4d1bc9ad..012e8ed35 100644
--- a/apps/sched/sched.js
+++ b/apps/sched/sched.js
@@ -9,7 +9,7 @@ function showAlarm(alarm) {
const settings = require("sched").getSettings();
let msg = "";
- msg += require("time_utils").formatTime(alarm.timer ? alarm.timer : alarm.t);
+ msg += alarm.timer ? require("time_utils").formatDuration(alarm.timer) : require("time_utils").formatTime(alarm.t);
if (alarm.msg) {
msg += "\n"+alarm.msg;
} else {
@@ -50,7 +50,8 @@ function showAlarm(alarm) {
Bangle.setLocked(false);
}
- require("buzz").pattern(alarm.vibrate === undefined ? ".." : alarm.vibrate).then(() => {
+ const pattern = alarm.vibrate || (alarm.timer ? settings.defaultTimerPattern : settings.defaultAlarmPattern);
+ require("buzz").pattern(pattern).then(() => {
if (buzzCount--) {
setTimeout(buzz, settings.buzzIntervalMillis);
} else if (alarm.as) { // auto-snooze
diff --git a/apps/sched/settings.js b/apps/sched/settings.js
index 5ddb4dab2..b73cd41d1 100644
--- a/apps/sched/settings.js
+++ b/apps/sched/settings.js
@@ -4,11 +4,10 @@
E.showMenu({
"": { "title": /*LANG*/"Scheduler" },
- /*LANG*/"< Back": () => back(),
+ "< Back": () => back(),
/*LANG*/"Unlock at Buzz": {
value: settings.unlockAtBuzz,
- format: v => v ? /*LANG*/"Yes" : /*LANG*/"No",
onchange: v => {
settings.unlockAtBuzz = v;
require("sched").setSettings(settings);
@@ -17,7 +16,6 @@
/*LANG*/"Default Auto Snooze": {
value: settings.defaultAutoSnooze,
- format: v => v ? /*LANG*/"Yes" : /*LANG*/"No",
onchange: v => {
settings.defaultAutoSnooze = v;
require("sched").setSettings(settings);
@@ -29,7 +27,7 @@
min: 5,
max: 30,
step: 5,
- format: v => v + /*LANG*/" min",
+ format: v => v + /*LANG*/"m",
onchange: v => {
settings.defaultSnoozeMillis = v * 60000;
require("sched").setSettings(settings);
@@ -38,7 +36,6 @@
/*LANG*/"Default Repeat": {
value: settings.defaultRepeat,
- format: v => v ? /*LANG*/"Yes" : /*LANG*/"No",
onchange: v => {
settings.defaultRepeat = v;
require("sched").setSettings(settings);
diff --git a/apps/setting/ChangeLog b/apps/setting/ChangeLog
index eca2b7938..1513194fe 100644
--- a/apps/setting/ChangeLog
+++ b/apps/setting/ChangeLog
@@ -50,3 +50,6 @@
UI improvements to Locale and Date & Time menu
0.45: Add calibrate battery option
0.46: Fix regression after making 'calibrate battery' only for Bangle.js 2
+0.47: Allow colors to be translated
+ Improve "Turn Off" user experience
+0.48: Allow reading custom themes from files
diff --git a/apps/setting/metadata.json b/apps/setting/metadata.json
index 183290a85..17519730e 100644
--- a/apps/setting/metadata.json
+++ b/apps/setting/metadata.json
@@ -1,7 +1,7 @@
{
"id": "setting",
"name": "Settings",
- "version": "0.46",
+ "version": "0.48",
"description": "A menu for setting up Bangle.js",
"icon": "settings.png",
"tags": "tool,system",
diff --git a/apps/setting/settings.js b/apps/setting/settings.js
index 150251e7d..3bb9b4e22 100644
--- a/apps/setting/settings.js
+++ b/apps/setting/settings.js
@@ -221,7 +221,8 @@ function showThemeMenu() {
Bangle.drawWidgets();
m.draw();
}
- var m = E.showMenu({
+
+ var themesMenu = {
'':{title:/*LANG*/'Theme'},
'< Back': ()=>showSystemMenu(),
/*LANG*/'Dark BW': ()=>{
@@ -239,9 +240,26 @@ function showThemeMenu() {
fgH:cl("#000"), bgH:cl("#0ff"),
dark:false
});
- },
- /*LANG*/'Customize': ()=>showCustomThemeMenu(),
- });
+ }
+ };
+
+ require("Storage").list(/^.*\.theme$/).forEach(
+ n => {
+ let newTheme = require("Storage").readJSON(n);
+ themesMenu[newTheme.name ? newTheme.name : n] = () => {
+ upd({
+ fg:cl(newTheme.fg), bg:cl(newTheme.bg),
+ fg2:cl(newTheme.fg2), bg2:cl(newTheme.bg2),
+ fgH:cl(newTheme.fgH), bgH:cl(newTheme.bgH),
+ dark:newTheme.dark
+ });
+ };
+ }
+ );
+
+ themesMenu[/*LANG*/'Customize'] = () => showCustomThemeMenu();
+
+ var m = E.showMenu(themesMenu);
function showCustomThemeMenu() {
function setT(t, v) {
@@ -252,22 +270,28 @@ function showThemeMenu() {
}
upd(th);
}
- let rgb = {
- black: "#000", white: "#fff",
- red: "#f00", green: "#0f0", blue: "#00f",
- cyan: "#0ff", magenta: "#f0f", yellow: "#ff0",
- };
- if (!BANGLEJS2) Object.assign(rgb, {
+ let rgb = {};
+ rgb[/*LANG*/'black'] = "#000";
+ rgb[/*LANG*/'white'] = "#fff";
+ rgb[/*LANG*/'red'] = "#f00";
+ rgb[/*LANG*/'green'] = "#0f0";
+ rgb[/*LANG*/'blue'] = "#00f";
+ rgb[/*LANG*/'cyan'] = "#0ff";
+ rgb[/*LANG*/'magenta'] = "#f0f";
+ rgb[/*LANG*/'yellow'] = "#ff0";
+ if (!BANGLEJS2) {
// these would cause dithering, which is not great for e.g. text
- orange: "#ff7f00", purple: "#7f00ff", grey: "#7f7f7f",
- });
+ rgb[/*LANG*/'orange'] = "#ff7f00";
+ rgb[/*LANG*/'purple'] = "#7f00ff";
+ rgb[/*LANG*/'grey'] = "#7f7f7f";
+ }
let colors = [], names = [];
for(const c in rgb) {
names.push(c);
colors.push(cl(rgb[c]));
}
let menu = {
- '':{title:'Custom Theme'},
+ '':{title:/*LANG*/'Custom Theme'},
"< Back": () => showThemeMenu()
};
const labels = {
@@ -569,7 +593,25 @@ function showUtilMenu() {
} else showUtilMenu();
});
};
- menu[/*LANG*/'Turn Off'] = ()=>{ if (Bangle.softOff) Bangle.softOff(); else Bangle.off() };
+ menu[/*LANG*/"Turn Off"] = () => {
+ E.showPrompt(/*LANG*/"Are you sure? Alarms and timers won't fire", {
+ title:/*LANG*/"Turn Off"
+ }).then((confirmed) => {
+ if (confirmed) {
+ E.showMessage(/*LANG*/"See you\nlater!", /*LANG*/"Goodbye");
+ setTimeout(() => {
+ // clear the screen so when the user will turn on the watch they'll see
+ // an empty screen instead of the latest displayed screen
+ E.showMessage();
+ g.clear(true);
+
+ Bangle.softOff ? Bangle.softOff() : Bangle.off();
+ }, 2500);
+ } else {
+ showUtilMenu();
+ }
+ });
+ };
if (Bangle.factoryReset) {
menu[/*LANG*/'Factory Reset'] = ()=>{
diff --git a/apps/sleepphasealarm/ChangeLog b/apps/sleepphasealarm/ChangeLog
index 208058472..6bf296342 100644
--- a/apps/sleepphasealarm/ChangeLog
+++ b/apps/sleepphasealarm/ChangeLog
@@ -7,3 +7,7 @@
use Layout library and display ETA
0.07: Add check for day of week
0.08: Update to new time_utils module
+0.09: Vibrate with configured pattern
+ Add setting to defer start of algorithm
+ Add setting to disable scheduler alarm
+
diff --git a/apps/sleepphasealarm/README.md b/apps/sleepphasealarm/README.md
index c33c9c807..ecb3feb06 100644
--- a/apps/sleepphasealarm/README.md
+++ b/apps/sleepphasealarm/README.md
@@ -4,10 +4,19 @@ The alarm must be in the next 24h.
The display shows:
-- the current time
-- time of the next alarm or timer
-- time difference between current time and alarm time (ETA)
-- current state of the ESS algorithm, "Sleep" or "Awake", useful for debugging
+- The current time.
+- Time of the next alarm or timer.
+- Time difference between current time and alarm time (ETA).
+- Current state of the ESS algorithm, "Sleep" or "Awake", useful for debugging. State can also be "Deferred", see the "Run before alarm"-option.
+
+## Settings
+
+* **Keep alarm enabled**
+ - Yes: (default) Alert will stay enabled, e.g. for an alarm at 7:00 the clock will buzz at the calculated time from the ESS algorithm (for example 6:45) and again at 7:00.
+ - No: No action at configured alarm time from scheduler.
+* **Run before alarm**
+ - disabled: (default) The ESS algorithm starts immediately when the application starts.
+ - 1..23: The ESS algorithm starts the configured time before the alarm. E.g. when set to 1h for an alarm at 7:00 the ESS algorithm will start at 6:00. This improves battery life.
## Logging
diff --git a/apps/sleepphasealarm/app.js b/apps/sleepphasealarm/app.js
index febc8a259..b19799c4b 100644
--- a/apps/sleepphasealarm/app.js
+++ b/apps/sleepphasealarm/app.js
@@ -1,9 +1,18 @@
const BANGLEJS2 = process.env.HWVERSION == 2; // check for bangle 2
+const CONFIGFILE = "sleepphasealarm.json";
const Layout = require("Layout");
const locale = require('locale');
const alarms = require("Storage").readJSON("sched.json",1) || [];
-const config = require("Storage").readJSON("sleepphasealarm.json",1) || {logs: []};
+const config = Object.assign({
+ logs: [], // array of length 31 with one entry for each day of month
+ settings: {
+ startBeforeAlarm: 0, // 0 = start immediately, 1..23 = start 1h..23h before alarm time
+ disableAlarm: false,
+ }
+}, require("Storage").readJSON(CONFIGFILE,1) || {});
const active = alarms.filter(a=>a.on);
+const schedSettings = require("sched").getSettings();
+let buzzCount = schedSettings.buzzCount;
let logs = [];
// Sleep/Wake detection with Estimation of Stationary Sleep-segments (ESS):
@@ -43,7 +52,8 @@ function calc_ess(acc_magn) {
}
// locate next alarm
-var nextAlarm;
+var nextAlarmDate;
+var nextAlarmConfig;
active.forEach(alarm => {
const now = new Date();
const time = require("time_utils").decodeTime(alarm.t);
@@ -52,8 +62,9 @@ active.forEach(alarm => {
dateAlarm.setTime(dateAlarm.getTime() + (24*60*60*1000));
}
if ((alarm.dow >> dateAlarm.getDay()) & 1) { // check valid day of week
- if (nextAlarm === undefined || dateAlarm < nextAlarm) {
- nextAlarm = dateAlarm;
+ if (nextAlarmDate === undefined || dateAlarm < nextAlarmDate) {
+ nextAlarmDate = dateAlarm;
+ nextAlarmConfig = alarm;
}
}
});
@@ -69,8 +80,8 @@ var layout = new Layout({
}, {lazy:true});
function drawApp() {
- var alarmHour = nextAlarm.getHours();
- var alarmMinute = nextAlarm.getMinutes();
+ var alarmHour = nextAlarmDate.getHours();
+ var alarmMinute = nextAlarmDate.getMinutes();
if (alarmHour < 10) alarmHour = "0" + alarmHour;
if (alarmMinute < 10) alarmMinute = "0" + alarmMinute;
layout.alarm_date.label = "Alarm at " + alarmHour + ":" + alarmMinute;
@@ -80,82 +91,108 @@ function drawApp() {
if (Bangle.isLCDOn()) {
const now = new Date();
layout.date.label = locale.time(now, BANGLEJS2 && Bangle.isLocked() ? 1 : 0); // hide seconds on bangle 2
- const diff = nextAlarm - now;
+ const diff = nextAlarmDate - now;
const diffHour = Math.floor((diff % 86400000) / 3600000).toString();
const diffMinutes = Math.floor(((diff % 86400000) % 3600000) / 60000).toString();
layout.eta.label = "ETA: -"+ diffHour + ":" + diffMinutes.padStart(2, '0');
layout.render();
}
+
+ setTimeout(()=>{
+ drawTime();
+ }, 1000 - (Date.now() % 1000));
}
drawTime();
- setInterval(drawTime, 500); // 2Hz
}
-var buzzCount = 19;
function buzz() {
if ((require('Storage').readJSON('setting.json',1)||{}).quiet>1) return; // total silence
- Bangle.setLCDPower(1);
- Bangle.buzz().then(()=>{
- if (buzzCount--) {
- setTimeout(buzz, 500);
- } else {
- // back to main after finish
- setTimeout(load, 1000);
- }
- });
+ Bangle.setLCDPower(1);
+ require("buzz").pattern(nextAlarmConfig.vibrate || ";");
+ if (buzzCount--) {
+ setTimeout(buzz, schedSettings.buzzIntervalMillis);
+ } else {
+ // back to main after finish
+ setTimeout(load, 1000);
+ }
}
function addLog(time, type) {
logs.push({time: time, type: type});
- require("Storage").writeJSON("sleepphasealarm.json", config);
+ if (logs.length > 1) { // Do not write if there is only one state
+ require("Storage").writeJSON(CONFIGFILE, config);
+ }
}
// run
var minAlarm = new Date();
var measure = true;
-if (nextAlarm !== undefined) {
- config.logs[nextAlarm.getDate()] = []; // overwrite log on each day of month
- logs = config.logs[nextAlarm.getDate()];
+if (nextAlarmDate !== undefined) {
+ config.logs[nextAlarmDate.getDate()] = []; // overwrite log on each day of month
+ logs = config.logs[nextAlarmDate.getDate()];
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
let swest_last;
// minimum alert 30 minutes early
- minAlarm.setTime(nextAlarm.getTime() - (30*60*1000));
- Bangle.on('accel', (accelData) => { // 12.5Hz
- const now = new Date();
- const acc = accelData.mag;
- const swest = calc_ess(acc);
+ minAlarm.setTime(nextAlarmDate.getTime() - (30*60*1000));
+ run = () => {
+ layout.state.label = "Start";
+ layout.render();
+ Bangle.setOptions({powerSave: false}); // do not dynamically change accelerometer poll interval
+ Bangle.setPollInterval(80); // 12.5Hz
+ Bangle.on('accel', (accelData) => {
+ const now = new Date();
+ const acc = accelData.mag;
+ const swest = calc_ess(acc);
- if (swest !== undefined) {
- if (Bangle.isLCDOn()) {
- layout.state.label = swest ? "Sleep" : "Awake";
- layout.render();
- }
- // log
- if (swest_last != swest) {
- if (swest) {
- addLog(new Date(now - sleepthresh*13/12.5*1000), "sleep"); // calculate begin of no motion phase, 13 values/second at 12.5Hz
- } else {
- addLog(now, "awake");
+ if (swest !== undefined) {
+ if (Bangle.isLCDOn()) {
+ layout.state.label = swest ? "Sleep" : "Awake";
+ layout.render();
+ }
+ // log
+ if (swest_last != swest) {
+ if (swest) {
+ addLog(new Date(now - sleepthresh*13/12.5*1000), "sleep"); // calculate begin of no motion phase, 13 values/second at 12.5Hz
+ } else {
+ addLog(now, "awake");
+ }
+ swest_last = swest;
}
- swest_last = swest;
}
- }
- if (now >= nextAlarm) {
- // The alarm widget should handle this one
- addLog(now, "alarm");
- setTimeout(load, 1000);
- } else if (measure && now >= minAlarm && swest === false) {
- addLog(now, "alarm");
- buzz();
- measure = false;
- }
- });
+ if (now >= nextAlarmDate) {
+ // The alarm widget should handle this one
+ addLog(now, "alarm");
+ setTimeout(load, 1000);
+ } else if (measure && now >= minAlarm && swest_last === false) {
+ addLog(now, "alarm");
+ buzz();
+ measure = false;
+ if (config.settings.disableAlarm) {
+ // disable alarm for scheduler
+ nextAlarmConfig.last = now.getDate();
+ require("Storage").writeJSON("sched.json", alarms);
+ }
+ }
+ });
+ };
drawApp();
+ if (config.settings.startBeforeAlarm === 0) {
+ // Start immediately
+ run();
+ } else {
+ // defer start
+ layout.state.label = "Deferred";
+ layout.render();
+ const diff = nextAlarmDate - Date.now();
+ let timeout = diff-config.settings.startBeforeAlarm*60*60*1000;
+ if (timeout < 0) timeout = 0;
+ setTimeout(run, timeout);
+ }
} else {
E.showMessage('No Alarm');
setTimeout(load, 1000);
diff --git a/apps/sleepphasealarm/interface.html b/apps/sleepphasealarm/interface.html
index 9a7cb0f93..f45c183e1 100644
--- a/apps/sleepphasealarm/interface.html
+++ b/apps/sleepphasealarm/interface.html
@@ -1,7 +1,6 @@
-
Please select a wakeup day:
diff --git a/apps/sleepphasealarm/metadata.json b/apps/sleepphasealarm/metadata.json
index c74a617ab..6ec5f4180 100644
--- a/apps/sleepphasealarm/metadata.json
+++ b/apps/sleepphasealarm/metadata.json
@@ -2,7 +2,7 @@
"id": "sleepphasealarm",
"name": "SleepPhaseAlarm",
"shortName": "SleepPhaseAlarm",
- "version": "0.08",
+ "version": "0.09",
"description": "Uses the accelerometer to estimate sleep and wake states with the principle of Estimation of Stationary Sleep-segments (ESS, see https://ubicomp.eti.uni-siegen.de/home/datasets/ichi14/index.html.en). This app will read the next alarm from the alarm application and will wake you up to 30 minutes early at the best guessed time when you are almost already awake.",
"icon": "app.png",
"tags": "alarm",
@@ -11,6 +11,7 @@
"dependencies": {"scheduler":"type"},
"storage": [
{"name":"sleepphasealarm.app.js","url":"app.js"},
+ {"name":"sleepphasealarm.settings.js","url":"settings.js"},
{"name":"sleepphasealarm.img","url":"app-icon.js","evaluate":true}
],
"data": [{"name":"sleepphasealarm.json","storageFile":true}],
diff --git a/apps/sleepphasealarm/settings.js b/apps/sleepphasealarm/settings.js
new file mode 100644
index 000000000..a79abb598
--- /dev/null
+++ b/apps/sleepphasealarm/settings.js
@@ -0,0 +1,37 @@
+(function(back) {
+ const CONFIGFILE = "sleepphasealarm.json";
+ // Load settings
+ const config = Object.assign({
+ logs: [], // array of length 31 with one entry for each day of month
+ settings: {
+ startBeforeAlarm: 0, // 0 = start immediately, 1..23 = start 1h..23h before alarm time
+ disableAlarm: false,
+ }
+ }, require("Storage").readJSON(CONFIGFILE,1) || {});
+
+ function writeSettings() {
+ require('Storage').writeJSON(CONFIGFILE, config);
+ }
+
+ // Show the menu
+ E.showMenu({
+ "" : { "title" : "SleepPhaseAlarm" },
+ 'Keep alarm enabled': {
+ value: !!config.settings.disableAlarm,
+ format: v => v?"No":"Yes",
+ onchange: v => {
+ config.settings.disableAlarm = v;
+ writeSettings();
+ }
+ }, "< Back" : () => back(),
+ 'Run before alarm': {
+ format: v => v === 0 ? 'disabled' : v+'h',
+ value: config.settings.startBeforeAlarm,
+ min: 0, max: 23,
+ onchange: v => {
+ config.settings.startBeforeAlarm = v;
+ writeSettings();
+ }
+ },
+ });
+})
diff --git a/apps/speedalt2/README.md b/apps/speedalt2/README.md
index c124e0c00..a86f787cf 100644
--- a/apps/speedalt2/README.md
+++ b/apps/speedalt2/README.md
@@ -37,7 +37,7 @@ When the GPS obtains a fix the number of satellites is displayed as 'Sats:nn'. W
## Power Saving
-The The GPS Adv Sport app obeys the watch screen off timeouts as a power saving measure. Restore the screen as per any of the colck/watch apps. Use BTN2 to lock the screen on but doing this will use more battery.
+The The GPS Adv Sport app obeys the watch screen off timeouts as a power saving measure. Restore the screen as per any of the clock/watch apps. Use BTN2 to lock the screen on but doing this will use more battery.
This app will work quite happily on its own but will use the [GPS Setup App](https://banglejs.com/apps/#gps%20setup) if it is installed. You may choose to use the GPS Setup App to gain significantly longer battery life while the GPS is on. Please read the Low Power GPS Setup App Readme to understand what this does.
@@ -65,7 +65,7 @@ The Droidscript script file is called : **GPS Adv Sports II.js**
Start/Stop buttons tell the Bangle.js to start or stop sending BLE data packets to the Android device. While stopped the Bangle.js reverts to full power saving mode when the screen is asleep.
-When runnig a blue 'led' will flash each time a data packet is recieved to refresh the android display.
+When running a blue 'led' will flash each time a data packet is recieved to refresh the android display.
An orange 'led' will flash for each reconnection attempt if no data is received for 30 seconds. It will keep trying to reconnect so you can restart the Bangle, run another Bangle app or temprarily turn off bluetooth. The android mirror display will automatically reconnect when the GPS Adv Sports II app is running on the Bangle again. ( Designed to leave the Android device running as the display mirror in a sealed case all day while retaining the ability to do other functions on the Bangle.js and returning to the GPS Speed Alt II app. )
diff --git a/apps/tinyVario/README.md b/apps/tinyVario/README.md
new file mode 100644
index 000000000..e23f3af66
--- /dev/null
+++ b/apps/tinyVario/README.md
@@ -0,0 +1,17 @@
+# Turn your Bangle.js2 into a flight computer!
+
+## This is a work in progress. Working features so far:
+- Altimeter
+- Variometer
+- Average rate of climb
+- Ground speed
+- Flying time with automatic take-off detection
+
+## Planned features:
+- Settings page to adjust QNH, change units
+- final glide computer
+- waypoint navigation
+- flight log (possibly IGC file export)
+
+Contact me for feedback and suggestions!
+tinyVario@dumke.org
diff --git a/apps/tinyVario/app.js b/apps/tinyVario/app.js
index 31f00307d..0b92c40f9 100644
--- a/apps/tinyVario/app.js
+++ b/apps/tinyVario/app.js
@@ -451,3 +451,4 @@ Bangle.on('pressure', function(e) {
});
initPFD();
+
diff --git a/apps/tinyVario/metadata.json b/apps/tinyVario/metadata.json
index be8618ec9..c8bc3659e 100644
--- a/apps/tinyVario/metadata.json
+++ b/apps/tinyVario/metadata.json
@@ -3,6 +3,7 @@
"shortName" : "tinyVario",
"version":"0.01",
"icon": "app.png",
+ "readme": "README.md",
"description": "A very simple app for gliding / paragliding / hang gliding etc.",
"tags": "outdoors",
"supports" : ["BANGLEJS2"],
diff --git a/apps/vectorclock/ChangeLog b/apps/vectorclock/ChangeLog
index 02831edde..6693f57ec 100644
--- a/apps/vectorclock/ChangeLog
+++ b/apps/vectorclock/ChangeLog
@@ -6,3 +6,4 @@
0.06: Redraw widgets when time is updated
0.07: Fix problem with "Bangle.CLOCK": github.com/espruino/BangleApps/issues/1437
0.08: Redraw widgets only once per minute
+0.09: Workaround for issue in 2v14 firmware (fix #1959)
diff --git a/apps/vectorclock/app.js b/apps/vectorclock/app.js
index 663a4c84f..ee3a4ea53 100644
--- a/apps/vectorclock/app.js
+++ b/apps/vectorclock/app.js
@@ -16,7 +16,7 @@ var commands = [];
var showSeconds = true;
function pushCommand(command) {
- let hash = E.CRC32(E.toJS(arguments));
+ var hash = E.CRC32(E.toJS(arguments));
if (!delete rectsToClear[hash]) {
commands.push({hash: hash, command: Function.apply.bind(command, null, arguments.slice(1))});
}
diff --git a/apps/vectorclock/metadata.json b/apps/vectorclock/metadata.json
index 541766fa2..245aad044 100644
--- a/apps/vectorclock/metadata.json
+++ b/apps/vectorclock/metadata.json
@@ -1,7 +1,7 @@
{
"id": "vectorclock",
"name": "Vector Clock",
- "version": "0.08",
+ "version": "0.09",
"description": "A digital clock that uses the built-in vector font.",
"icon": "app.png",
"type": "clock",
diff --git a/apps/weather/readme.md b/apps/weather/readme.md
index 6d0ea04a5..b37d0b38e 100644
--- a/apps/weather/readme.md
+++ b/apps/weather/readme.md
@@ -11,6 +11,9 @@ You can view the full report through the app:
1. Install [Gadgetbridge for Android](https://f-droid.org/packages/nodomain.freeyourgadget.gadgetbridge/) on your phone.
2. Set up [Gadgetbridge weather reporting](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Weather).
+If using the `Bangle.js Gadgetbridge` app on your phone (as opposed to the standard F-Droid `Gadgetbridge`) you need to set the package name
+to `com.espruino.gadgetbridge.banglejs` in the settings of the weather app (`settings -> gadgetbridge support -> package name`).
+
## Settings
* Expiration timespan can be set after which the local weather data is considered as invalid
diff --git a/apps/widbaroalarm/ChangeLog b/apps/widbaroalarm/ChangeLog
index 5786741c7..86a902605 100644
--- a/apps/widbaroalarm/ChangeLog
+++ b/apps/widbaroalarm/ChangeLog
@@ -1,3 +1,5 @@
0.01: Initial version
-0.02: Do not warn multiple times for the same exceedance
+0.02: Do not warn multiple times for the same exceed
0.03: Fix crash
+0.04: Use Prompt with dismiss and pause
+ Improve barometer value median calculation
diff --git a/apps/widbaroalarm/README.md b/apps/widbaroalarm/README.md
index fdc239170..59d91ff66 100644
--- a/apps/widbaroalarm/README.md
+++ b/apps/widbaroalarm/README.md
@@ -15,7 +15,8 @@ Get a notification when the pressure reaches defined thresholds.
0 to disable this alarm.
* Show widget: Enable/disable widget visibility
* Buzz on alarm: Enable/disable buzzer on alarm
-
+* Dismiss delay: Delay added before the next alert if the alert is dismissed. From 5 to 60 min
+* Pause delay: Same as Dismiss delay but longer (useful for meetings and such). From 30 to 240 min
## Widget
The widget shows two rows: pressure value of last measurement and pressure average of the the last three hours.
diff --git a/apps/widbaroalarm/default.json b/apps/widbaroalarm/default.json
index 3d81baa81..696c70819 100644
--- a/apps/widbaroalarm/default.json
+++ b/apps/widbaroalarm/default.json
@@ -7,5 +7,7 @@
"drop3halarm": 2,
"raise3halarm": 0,
"show": true,
- "interval": 15
+ "interval": 15,
+ "dismissDelayMin": 15,
+ "pauseDelayMin": 60
}
diff --git a/apps/widbaroalarm/metadata.json b/apps/widbaroalarm/metadata.json
index 134f03623..41b8d3e17 100644
--- a/apps/widbaroalarm/metadata.json
+++ b/apps/widbaroalarm/metadata.json
@@ -2,13 +2,12 @@
"id": "widbaroalarm",
"name": "Barometer Alarm Widget",
"shortName": "Barometer Alarm",
- "version": "0.03",
+ "version": "0.04",
"description": "A widget that can alarm on when the pressure reaches defined thresholds.",
"icon": "widget.png",
"type": "widget",
"tags": "tool,barometer",
"supports": ["BANGLEJS2"],
- "dependencies": {"notify":"type"},
"readme": "README.md",
"storage": [
{"name":"widbaroalarm.wid.js","url":"widget.js"},
diff --git a/apps/widbaroalarm/settings.js b/apps/widbaroalarm/settings.js
index bea6319d1..ee8ce82c2 100644
--- a/apps/widbaroalarm/settings.js
+++ b/apps/widbaroalarm/settings.js
@@ -87,6 +87,26 @@
},
onchange: x => save('buzz', x)
},
+ 'Dismiss delay': {
+ value: settings.dismissDelayMin,
+ min: 5, max: 60,
+ onchange: v => {
+ save('dismissDelayMin', v)
+ },
+ format: x => {
+ return x + " min";
+ }
+ },
+ 'Pause delay': {
+ value: settings.pauseDelayMin,
+ min: 30, max: 240,
+ onchange: v => {
+ save('pauseDelayMin', v)
+ },
+ format: x => {
+ return x + " min";
+ }
+ },
};
E.showMenu(menu);
}
diff --git a/apps/widbaroalarm/widget.js b/apps/widbaroalarm/widget.js
index 2745db8ad..e1516b6f1 100644
--- a/apps/widbaroalarm/widget.js
+++ b/apps/widbaroalarm/widget.js
@@ -2,6 +2,7 @@
let medianPressure;
let threeHourAvrPressure;
let currentPressures = [];
+ let stop = false; // semaphore
const LOG_FILE = "widbaroalarm.log.json";
const SETTINGS_FILE = "widbaroalarm.json";
@@ -32,24 +33,50 @@
let history3 = storage.readJSON(LOG_FILE, true) || []; // history of recent 3 hours
- function showAlarm(body, title) {
+ function showAlarm(body, key) {
if (body == undefined) return;
+ stop = true;
- require("notify").show({
- title: title || "Pressure",
- body: body,
- icon: require("heatshrink").decompress(atob("jEY4cA///gH4/++mkK30kiWC4H8x3BGDmSGgYDCgmSoEAg3bsAIDpAIFkmSpMAm3btgIFDQwIGNQpTYkAIJwAHEgMoCA0JgMEyBnBCAW3KoQQDhu3oAIH5JnDBAW24IIBEYm2EYwACBCIACA"))
+ E.showPrompt(body, {
+ title: "Pressure alarm",
+ buttons: {
+ "Ok": 1,
+ "Dismiss": 2,
+ "Pause": 3
+ }
+ }).then(function(v) {
+ const tsNow = Math.round(Date.now() / 1000); // seconds
+
+ if (v == 1) {
+ saveSetting(key, tsNow);
+ }
+ if (v == 2) {
+ // save timestamp of the future so that we do not warn again for the same event until then
+ saveSetting(key, tsNow + 60 * setting('dismissDelayMin'));
+ }
+ if (v == 3) {
+ // save timestamp of the future so that we do not warn again for the same event until then
+ saveSetting(key, tsNow + 60 * setting('pauseDelayMin'));
+ }
+ stop = false;
+ load();
});
if (setting("buzz") &&
!(storage.readJSON('setting.json', 1) || {}).quiet) {
Bangle.buzz();
}
+
+ setTimeout(function() {
+ stop = false;
+ load();
+ }, 20000);
}
- function didWeAlreadyWarn(key) {
- return setting(key) == undefined || setting(key) > 0;
+ function doWeNeedToWarn(key) {
+ const tsNow = Math.round(Date.now() / 1000); // seconds
+ return setting(key) == 0 || setting(key) < tsNow;
}
function checkForAlarms(pressure) {
@@ -77,31 +104,25 @@
if (setting("lowalarm")) {
// Is below the alarm threshold?
if (pressure <= setting("min")) {
- if (!didWeAlreadyWarn("lastLowWarningTs")) {
- showAlarm("Pressure low: " + Math.round(pressure) + " hPa");
- saveSetting("lastLowWarningTs", ts);
+ if (!doWeNeedToWarn("lastLowWarningTs")) {
+ showAlarm("Pressure low: " + Math.round(pressure) + " hPa", "lastLowWarningTs");
alreadyWarned = true;
}
} else {
saveSetting("lastLowWarningTs", 0);
}
- } else {
- saveSetting("lastLowWarningTs", 0);
}
if (setting("highalarm")) {
// Is above the alarm threshold?
if (pressure >= setting("max")) {
- if (!didWeAlreadyWarn("lastHighWarningTs")) {
- showAlarm("Pressure high: " + Math.round(pressure) + " hPa");
- saveSetting("lastHighWarningTs", ts);
+ if (doWeNeedToWarn("lastHighWarningTs")) {
+ showAlarm("Pressure high: " + Math.round(pressure) + " hPa", "lastHighWarningTs");
alreadyWarned = true;
}
} else {
saveSetting("lastHighWarningTs", 0);
}
- } else {
- saveSetting("lastHighWarningTs", 0);
}
if (history3.length > 0 && !alreadyWarned) {
@@ -110,22 +131,22 @@
const raise3halarm = setting("raise3halarm");
if (drop3halarm > 0 || raise3halarm > 0) {
// we need at least 30min of data for reliable detection
- if (history3[0]["ts"] > ts - (30 * 60)) {
+ const diffDateAge = Math.abs(history3[0]["ts"] - ts);
+ if (diffDateAge < 10 * 60) { // todo change to 1800
return;
}
// Get oldest entry:
const oldestPressure = history3[0]["p"];
if (oldestPressure != undefined && oldestPressure > 0) {
- const diff = oldestPressure - pressure;
+ const diffPressure = Math.abs(oldestPressure - pressure);
// drop alarm
if (drop3halarm > 0 && oldestPressure > pressure) {
- if (Math.abs(diff) > drop3halarm) {
- if (!didWeAlreadyWarn("lastDropWarningTs")) {
- showAlarm((Math.round(Math.abs(diff) * 10) / 10) + " hPa/3h from " +
- Math.round(oldestPressure) + " to " + Math.round(pressure) + " hPa", "Pressure drop");
- saveSetting("lastDropWarningTs", ts);
+ if (diffPressure > drop3halarm) {
+ if (doWeNeedToWarn("lastDropWarningTs")) {
+ showAlarm((Math.round(diffPressure * 10) / 10) + " hPa/3h from " +
+ Math.round(oldestPressure) + " to " + Math.round(pressure) + " hPa", "lastDropWarningTs");
}
} else {
saveSetting("lastDropWarningTs", 0);
@@ -136,11 +157,10 @@
// raise alarm
if (raise3halarm > 0 && oldestPressure < pressure) {
- if (Math.abs(diff) > raise3halarm) {
- if (!didWeAlreadyWarn("lastRaiseWarningTs")) {
- showAlarm((Math.round(Math.abs(diff) * 10) / 10) + " hPa/3h from " +
- Math.round(oldestPressure) + " to " + Math.round(pressure) + " hPa", "Pressure raise");
- saveSetting("lastRaiseWarningTs", ts);
+ if (diffPressure > raise3halarm) {
+ if (doWeNeedToWarn("lastRaiseWarningTs")) {
+ showAlarm((Math.round(diffPressure * 10) / 10) + " hPa/3h from " +
+ Math.round(oldestPressure) + " to " + Math.round(pressure) + " hPa", "lastRaiseWarningTs");
}
} else {
saveSetting("lastRaiseWarningTs", 0);
@@ -157,51 +177,52 @@
storage.writeJSON(LOG_FILE, history3);
// calculate 3h average for widget
- let sum = 0;
- for (let i = 0; i < history3.length; i++) {
- sum += history3[i]["p"];
- }
- threeHourAvrPressure = sum / history3.length;
- }
-
-
-
- function baroHandler(data) {
- if (data) {
- const pressure = Math.round(data.pressure);
- if (pressure == undefined || pressure <= 0) return;
- currentPressures.push(pressure);
+ if (history3.length > 0) {
+ let sum = 0;
+ for (let i = 0; i < history3.length; i++) {
+ sum += history3[i]["p"];
+ }
+ threeHourAvrPressure = sum / history3.length;
+ } else {
+ threeHourAvrPressure = undefined;
}
}
+
/*
turn on barometer power
- take 5 measurements
+ take multiple measurements
sort the results
take the middle one (median)
turn off barometer power
*/
function check() {
+ if (stop) return;
+ const MEDIANLENGTH = 20;
Bangle.setBarometerPower(true, "widbaroalarm");
+ Bangle.on('pressure', function(e) {
+ while (currentPressures.length > MEDIANLENGTH) currentPressures.pop();
+ currentPressures.unshift(e.pressure);
+ median = currentPressures.slice().sort();
+
+ if (median.length > 10) {
+ var mid = median.length >> 1;
+ medianPressure = Math.round(E.sum(median.slice(mid - 4, mid + 5)) / 9);
+ if (medianPressure > 0) {
+ turnOff();
+ checkForAlarms(medianPressure);
+ }
+ }
+ });
+
setTimeout(function() {
- currentPressures = [];
+ turnOff();
+ }, 10000);
+ }
- Bangle.getPressure().then(baroHandler);
- Bangle.getPressure().then(baroHandler);
- Bangle.getPressure().then(baroHandler);
- Bangle.getPressure().then(baroHandler);
- Bangle.getPressure().then(baroHandler);
-
- setTimeout(function() {
- Bangle.setBarometerPower(false, "widbaroalarm");
-
- currentPressures.sort();
-
- // take median value
- medianPressure = currentPressures[3];
- checkForAlarms(medianPressure);
- }, 1000);
- }, 500);
+ function turnOff() {
+ if (Bangle.isBarometerOn())
+ Bangle.setBarometerPower(false, "widbaroalarm");
}
function reload() {
@@ -209,31 +230,39 @@
}
function draw() {
- if (global.WIDGETS != undefined && typeof WIDGETS === "object") {
- WIDGETS["baroalarm"] = {
+ if (global.WIDGETS != undefined && typeof global.WIDGETS === "object") {
+ global.WIDGETS["baroalarm"] = {
width: setting("show") ? 24 : 0,
reload: reload,
area: "tr",
draw: draw
};
}
-
g.reset();
- if (setting("show") && medianPressure != undefined) {
+ if (setting("show")) {
g.setFont("6x8", 1).setFontAlign(1, 0);
- g.drawString(Math.round(medianPressure), this.x + 24, this.y + 6);
+ if (medianPressure == undefined) {
+ check();
+ const x = this.x,
+ y = this.y;
+ g.drawString("...", x + 24, y + 6);
+ setTimeout(function() {
+ g.setFont("6x8", 1).setFontAlign(1, 0);
+ g.drawString(Math.round(medianPressure), x + 24, y + 6);
+ }, 10000);
+ } else {
+ g.drawString(Math.round(medianPressure), this.x + 24, this.y + 6);
+ }
+
if (threeHourAvrPressure != undefined && threeHourAvrPressure > 0) {
g.drawString(Math.round(threeHourAvrPressure), this.x + 24, this.y + 6 + 10);
}
}
}
- // Let's delay the first check a bit
- setTimeout(function() {
- check();
- if (interval > 0) {
- setInterval(check, interval * 60000);
- }
- }, 1000);
+ if (interval > 0) {
+ setInterval(check, interval * 60000);
+ }
+ draw();
})();
diff --git a/apps/widday/ChangeLog b/apps/widday/ChangeLog
new file mode 100644
index 000000000..7b83706bf
--- /dev/null
+++ b/apps/widday/ChangeLog
@@ -0,0 +1 @@
+0.01: First release
diff --git a/apps/widday/README.md b/apps/widday/README.md
new file mode 100644
index 000000000..61f48aa4c
--- /dev/null
+++ b/apps/widday/README.md
@@ -0,0 +1,9 @@
+# Day Widget
+
+Just shows the day of the current date, to save space in the widget area. The month and year should be known because they don't change that often. Just the number in maximum size for readability.
+
+
+
+## Creator
+[@pidajo](https://github.com/pidajo)
+
diff --git a/apps/widday/app-icon.js b/apps/widday/app-icon.js
new file mode 100644
index 000000000..aa17aedc5
--- /dev/null
+++ b/apps/widday/app-icon.js
@@ -0,0 +1 @@
+atob("MDCEAiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIBEREREREREREREREREREREREREREQIiIf///////////////////////////xIiIf///////////////////////////xIiIf///////////////////////////xIiIf///////////////////////////xIiIf///////////////////////////xIiIf///////////////////////////xIiIf///////////////////////////xIiIf///////zMz////8zMzMzMzP////xIiIf///////czM////PMzMzMzMzP///xIiIf/////zzMzM////PMzMzMzMzP///xIiIf////PczMzM////PMzMzMzMzP///xIiIf////3MzMzM////8zMzMz3MzP///xIiIf////3MM8zM//////////zMw////xIiIf////0/88zM/////////zzMz////xIiIf//////88zM/////////8zM3////xIiIf//////88zM////////88zM/////xIiIf//////88zM/////////MzN/////xIiIf//////88zM////////PMzD/////xIiIf//////88zM////////3Mzf/////xIiIf//////88zM////////zMw//////xIiIf//////88zM///////9zMz//////xIiIf//////88zM///////8zMP//////xIiIf//////88zM//////88zM///////xIiIf//////88zM///////MzN///////xIiIf////8zM8zM//////PMzD///////xIiIf////3MzMzMzM3///3Mzf///////xIiIf////3MzMzMzM3//zzMw////////xIiIf////PMzMzMzM3//9zMz////////xIiIf///////////////////////////xIiIf///////////////////////////xIiIf///////////////////////////xIiIf///////////////////////////xIiIf///////////////////////////xIiIf///////////////////////////xIiIf///////////////////////////xIiIf///////////////////////////xIiIBEREREREREREREREREREREREREREQIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIg==")
diff --git a/apps/widday/metadata.json b/apps/widday/metadata.json
new file mode 100644
index 000000000..82de23035
--- /dev/null
+++ b/apps/widday/metadata.json
@@ -0,0 +1,15 @@
+{ "id": "widday",
+ "name": "Day Widget",
+ "shortName":"My Timer",
+ "icon": "widget.png",
+ "type": "widget",
+ "version":"0.01",
+ "description": "Just the day of the current date as widget",
+ "readme": "README.md",
+ "tags": "widget,say,date",
+ "supports": ["BANGLEJS", "BANGLEJS2"],
+ "screenshots" : [ { "url":"screenshot.png" } ],
+ "storage": [
+ {"name":"widday.wid.js","url":"widget.js"}
+ ]
+}
diff --git a/apps/widday/screenshot.png b/apps/widday/screenshot.png
new file mode 100644
index 000000000..9a7a9f8cf
Binary files /dev/null and b/apps/widday/screenshot.png differ
diff --git a/apps/widday/widget.js b/apps/widday/widget.js
new file mode 100644
index 000000000..cdea76a29
--- /dev/null
+++ b/apps/widday/widget.js
@@ -0,0 +1,27 @@
+(() => {
+ var width = 32; // width of the widget
+
+ function draw() {
+ var date = new Date();
+ g.reset(); // reset the graphics context to defaults (color/font/etc)
+ g.setFontAlign(0,1); // center fonts
+ //g.drawRect(this.x, this.y, this.x+width-1, this.y+23); // check the bounds!
+
+ var text = date.getDate();
+ g.setFont("Vector", 24);
+ g.drawString(text, this.x+width/2+1, this.y + 28);
+ //g.setColor(0, 0, 1);
+ //g.drawRect(this.x, this.y, this.x+width-2, this.y+1);
+ }
+
+ setInterval(function() {
+ WIDGETS["widday"].draw(WIDGETS["widdateday"]);
+ }, 10*60000); // update every 10 minutes
+
+ // add your widget
+ WIDGETS["widday"]={
+ area:"bl", // tl (top left), tr (top right), bl (bottom left), br (bottom right)
+ width: width, // how wide is the widget? You can change this and call Bangle.drawWidgets() to re-layout
+ draw:draw // called to draw the widget
+ };
+})()
diff --git a/apps/widday/widget.png b/apps/widday/widget.png
new file mode 100644
index 000000000..462097871
Binary files /dev/null and b/apps/widday/widget.png differ
diff --git a/apps/widmp/ChangeLog b/apps/widmp/ChangeLog
index 02658296a..f0cd6bd8a 100644
--- a/apps/widmp/ChangeLog
+++ b/apps/widmp/ChangeLog
@@ -2,3 +2,5 @@
0.02: Fix position and overdraw bugs
0.03: Better memory usage, theme support
0.04: Replace the 8 phases by a more exact drawing, see forum.espruino.com/conversations/371985
+0.05: Fixed the algorithm for calculating the moon's phase
+0.06: Darkmode, custom colours, and fix a bug with acting on mylocation changes
diff --git a/apps/widmp/metadata.json b/apps/widmp/metadata.json
index 94f05a426..b1e6a6c8c 100644
--- a/apps/widmp/metadata.json
+++ b/apps/widmp/metadata.json
@@ -1,13 +1,15 @@
{
"id": "widmp",
- "name": "Moon Phase Widget",
- "version": "0.04",
- "description": "Display the current moon phase in blueish for both hemispheres. In the southern hemisphere the 'My Location' app is needed.",
+ "name": "Moon Phase",
+ "version": "0.06",
+ "description": "Display the current moon phase in blueish (in light mode) or white (in dark mode) for both hemispheres. In the southern hemisphere the 'My Location' app is needed.",
"icon": "widget.png",
"type": "widget",
"tags": "widget,tools",
"supports": ["BANGLEJS","BANGLEJS2"],
"storage": [
- {"name":"widmp.wid.js","url":"widget.js"}
- ]
+ {"name":"widmp.wid.js","url":"widget.js"},
+ {"name":"widmp.settings.js","url":"settings.js"}
+ ],
+ "data": [{"name":"widmp.json"}]
}
diff --git a/apps/widmp/settings.js b/apps/widmp/settings.js
new file mode 100644
index 000000000..46c5d3609
--- /dev/null
+++ b/apps/widmp/settings.js
@@ -0,0 +1,73 @@
+(function(back) {
+
+ var settings = Object.assign({
+ default_colour: true,
+ red: 0,
+ green: 0,
+ blue: 0,
+ }, require('Storage').readJSON("widmp.json", true) || {});
+
+ function writeSettings() {
+ require('Storage').writeJSON("widmp.json", settings);
+ if (WIDGETS["widmp"]) WIDGETS["widmp"].draw();
+ }
+
+ function writeSettingsCustom() {
+ settings.default_colour = false;
+ mainmenu["Default"].value = false;
+ writeSettings();
+ }
+
+ var mainmenu = {
+ "": {
+ "title": "Moon colour"
+ },
+ "< Back": () => back(),
+ "Default": {
+ value: (settings.default_colour !== undefined ? settings.default_colour : true),
+ format: v => v ? "Yes" : "No",
+ onchange: v => {
+ settings.default_colour = v;
+ writeSettings();
+ }
+ },
+ "Custom...": () => E.showMenu(custommenu)
+ };
+
+ var custommenu = {
+ "": {
+ "title": "Custom colour..."
+ },
+ "< Back": () => E.showMenu(mainmenu),
+ "red": {
+ value: 0|settings.red,
+ min: 0,
+ max: 4,
+ onchange: v => {
+ settings.red = v;
+ writeSettingsCustom();
+ }
+ },
+ "green": {
+ value: 0|settings.green,
+ min: 0,
+ max: 4,
+ onchange: v => {
+ settings.green = v;
+ writeSettingsCustom();
+ }
+ },
+ "blue": {
+ value: 0|settings.blue,
+ min: 0,
+ max: 4,
+ onchange: v => {
+ settings.blue = v;
+ writeSettingsCustom();
+ }
+ }
+ };
+
+ E.showMenu(mainmenu);
+
+});
diff --git a/apps/widmp/widget.js b/apps/widmp/widget.js
index 6da572aab..22a7d6572 100644
--- a/apps/widmp/widget.js
+++ b/apps/widmp/widget.js
@@ -1,25 +1,27 @@
-WIDGETS["widmoon"] = { area: "tr", width: 24, draw: function() {
- const CenterX = this.x + 12, CenterY = this.y + 12, Radius = 11;
- var southernHemisphere = false; // when in southern hemisphere, use the "My Location" App
+(() => {
- const simulate = false; // simulate one month in one minute
- const updateR = 1000; // update every x ms in simulation
+ var lastCalculated = 0; // When we last calculated the phase
+ var phase = 0; // The last phase we calculated
+ var southernHemisphere = false; // when in southern hemisphere -- use the "My Location" App
- function moonPhase() {
- const d = Date();
- var month = d.getMonth(), year = d.getFullYear(), day = d.getDate();
- if (simulate) day = d.getSeconds() / 2 +1;
- if (month < 3) {year--; month += 12;}
- mproz = ((365.25 * year + 30.6 * ++month + day - 694039.09) / 29.5305882);
- mproz = mproz - (mproz | 0); // strip integral digits, result is between 0 and <1
- if (simulate) console.log(mproz + " " + day);
- return (mproz);
+ // https://deirdreobyrne.github.io/calculating_moon_phases/
+ function moonPhase(millis) {
+ k = (millis - 946728000000) / 3155760000000;
+ mp = (8328.69142475915 * k) + 2.35555563685;
+ m = (628.30195516723 * k) + 6.24006012726;
+ d = (7771.37714483372 * k) + 5.19846652984;
+ t = d + (0.109764 * Math.sin (mp)) - (0.036652 * Math.sin(m)) + (0.022235 * Math.sin(d+d-mp)) + (0.011484 * Math.sin(d+d)) + (0.003735 * Math.sin(mp+mp)) + (0.00192 * Math.sin(d));
+ k = (1 - Math.cos(t))/2;
+ if (Math.sin(t) < 0) {
+ k = -k;
+ }
+ return (k); // Goes 0 -> 1 for waxing, and from -1 -> 0 for waning
}
function loadLocation() {
// "mylocation.json" is created by the "My Location" app
location = require("Storage").readJSON("mylocation.json",1)||{"lat":50.1236,"lon":8.6553,"location":"Frankfurt"};
- if (location.lat < 0) southernHemisphere = true;
+ southernHemisphere = (location.lat < 0);
}
// code source: github.com/rozek/banglejs-2-activities/blob/main/README.md#drawmoonphase
@@ -38,26 +40,60 @@ WIDGETS["widmoon"] = { area: "tr", width: 24, draw: function() {
g.drawLine(CenterX-leftFactor*y,CenterY+x, CenterX+rightFactor*y,CenterY+x);
}
}
+
+ function setMoonColour(g) {
+ var settings = Object.assign({
+ default_colour: true,
+ red: 0,
+ green: 0,
+ blue: 0,
+ }, require('Storage').readJSON("widmp.json", true) || {});
+ if (settings.default_colour) {
+ if (g.theme.dark) {
+ g.setColor(0xffff); // white
+ } else {
+ // rrrrrggggggbbbbb
+ // 0000010000011111
+ g.setColor(0x41f); // blue-ish
+ }
+ } else {
+ g.setColor(settings.red/4, settings.green/4, settings.blue/4);
+ }
+ }
- function updateWidget() {
+
+ function draw() {
+ const CenterX = this.x + 12, CenterY = this.y + 12, Radius = 11;
+
+ loadLocation();
g.reset().setColor(g.theme.bg);
g.fillRect(CenterX - Radius, CenterY - Radius, CenterX + Radius, CenterY + Radius);
- g.setColor(0x41f);
- mproz = moonPhase(); // mproz = 0..<1
+ millis = (new Date()).getTime();
+ if ((millis - lastCalculated) >= 7000000) { // if it's more than 7,000 sec since last calculation, re-calculate!
+ phase = moonPhase(millis);
+ lastCalculated = millis;
+ }
- leftFactor = mproz * 4 - 1;
- rightFactor = (1 - mproz) * 4 - 1;
- if (mproz >= 0.5) leftFactor = 1; else rightFactor = 1;
+ if (phase < 0) { // waning - phase goes from -1 to 0
+ leftFactor = 1;
+ rightFactor = -1 - 2*phase;
+ } else { // waxing - phase goes from 0 to 1
+ rightFactor = 1;
+ leftFactor = -1 + 2*phase;
+ }
if (true == southernHemisphere) {
var tmp=leftFactor; leftFactor=rightFactor; rightFactor=tmp;
}
+ setMoonColour(g);
drawMoonPhase(CenterX,CenterY, Radius, leftFactor,rightFactor);
-
- if (simulate) setTimeout(updateWidget, updateR);
}
- loadLocation();
- updateWidget();
-} };
+ WIDGETS["widmp"] = {
+ area: "tr",
+ width: 24,
+ draw: draw
+ };
+
+})();
diff --git a/apps/widshipbell/metadata.json b/apps/widshipbell/metadata.json
new file mode 100644
index 000000000..c130b04ee
--- /dev/null
+++ b/apps/widshipbell/metadata.json
@@ -0,0 +1,16 @@
+{
+ "id": "widshipbell",
+ "name": "Ship's bell Widget",
+ "shortName": "Ship's bell",
+ "version": "0.01",
+ "description": "A widget that buzzes according to a nautical bell, one strike at 04:30, two strikes at 05:00, up to eight strikes at 08:00 and so on.",
+ "icon": "widget.png",
+ "type": "widget",
+ "tags": "widget",
+ "supports": ["BANGLEJS","BANGLEJS2"],
+ "storage": [
+ {"name":"widshipbell.wid.js","url":"widget.js"},
+ {"name":"widshipbell.settings.js","url":"settings.js"}
+ ],
+ "data": [{"name":"widshipbell.json"}]
+}
diff --git a/apps/widshipbell/settings.js b/apps/widshipbell/settings.js
new file mode 100644
index 000000000..bb47e9b20
--- /dev/null
+++ b/apps/widshipbell/settings.js
@@ -0,0 +1,27 @@
+(function(back) {
+ var FILE = "widshipbell.json";
+ // Load settings
+ var settings = Object.assign({
+ strength: 1,
+ }, require('Storage').readJSON(FILE, true) || {});
+
+ function writeSettings() {
+ require('Storage').writeJSON(FILE, settings);
+ }
+
+ // Show the menu
+ E.showMenu({
+ "" : { "title" : "Ship's bell" },
+ "< Back" : () => back(),
+ 'Strength': {
+ value: settings.strength,
+ min: 0, max: 2,
+ format: v => ["Off", "Weak", "Strong"][v],
+ onchange: v => {
+ settings.strength = v;
+ writeSettings();
+ }
+ },
+ });
+})
+
diff --git a/apps/widshipbell/widget.js b/apps/widshipbell/widget.js
new file mode 100644
index 000000000..e37edb6fd
--- /dev/null
+++ b/apps/widshipbell/widget.js
@@ -0,0 +1,52 @@
+(() => {
+ const strength = Object.assign({
+ strength: 1,
+ }, require('Storage').readJSON("widshipbell.json", true) || {}).strength;
+
+ function replaceAll(target, search, replacement) {
+ return target.split(search).join(replacement);
+ }
+
+ function check() {
+ const now = new Date();
+ const currentMinute = now.getMinutes();
+ const currentSecond = now.getSeconds();
+ const etaMinute = 30-(currentMinute % 30);
+
+ if (etaMinute === 30 && currentSecond === 0) {
+ const strikeHour = now.getHours() % 4;
+ // buzz now
+ let pattern='';
+ if (strikeHour === 0 && currentMinute == 0) {
+ pattern = '.. .. .. ..';
+ } else if (strikeHour === 0 && currentMinute === 30) {
+ pattern = '.';
+ } else if (strikeHour === 1 && currentMinute === 0) {
+ pattern = '..';
+ } else if (strikeHour === 1 && currentMinute === 30) {
+ pattern = '.. .';
+ } else if (strikeHour === 2 && currentMinute === 0) {
+ pattern = '.. ..';
+ } else if (strikeHour === 2 && currentMinute === 30) {
+ pattern = '.. .. .';
+ } else if (strikeHour === 3 && currentMinute === 0) {
+ pattern = '.. .. ..';
+ } else if (strikeHour === 3 && currentMinute === 30) {
+ pattern = '.. .. .. .';
+ }
+ pattern = replaceAll(pattern, ' ', ' '); // 4x pause
+ pattern = replaceAll(pattern, '.', '. '); // pause between bells
+ if (strength === 2) { // strong selected
+ pattern = replaceAll(pattern, '.', ':');
+ }
+ require("buzz").pattern(pattern);
+ }
+
+ const etaSecond = etaMinute*60-currentSecond;
+ setTimeout(check, etaSecond*1000);
+ }
+
+ if (strength !== 0) {
+ check();
+ }
+})();
diff --git a/apps/widshipbell/widget.png b/apps/widshipbell/widget.png
new file mode 100644
index 000000000..891679f7d
Binary files /dev/null and b/apps/widshipbell/widget.png differ
diff --git a/apps/wristlight/Icon.png b/apps/wristlight/Icon.png
new file mode 100644
index 000000000..0bbcd3e36
Binary files /dev/null and b/apps/wristlight/Icon.png differ
diff --git a/apps/wristlight/README.md b/apps/wristlight/README.md
new file mode 100644
index 000000000..e69d61b76
--- /dev/null
+++ b/apps/wristlight/README.md
@@ -0,0 +1,9 @@
+# Wrist Light
+
+A flash light on your wrist with different colors
+
+
+
+
+## Creator
+[@pidajo](https://github.com/pidajo)
diff --git a/apps/wristlight/app.js b/apps/wristlight/app.js
new file mode 100644
index 000000000..24bc1eab2
--- /dev/null
+++ b/apps/wristlight/app.js
@@ -0,0 +1,52 @@
+function draw(color) {
+ if (color == undefined) {
+ color = -1;
+ }
+ g.clear();
+ g.setColor(color);
+ g.fillRect(0, 0, g.getWidth(), g.getHeight());
+}
+
+function draw2Pattern() {
+ colors = ["ff0000", "8080ff", "00ff00",
+ "ffffff"];
+ drawPattern(2, colors);
+}
+
+function draw3Pattern() {
+ colors = ["ff0000", "00ff00", "0000ff",
+ "ff00ff", "ffffff", "00ffff",
+ "ffff00", "ff8000", "ff0080"];
+ drawPattern(3, colors);
+}
+
+function drawPattern(size, colors) {
+ g.clear();
+ var w = g.getWidth() / size;
+ var h = g.getHeight() / size;
+ for (var i = 0; i < size; i++) {
+ for (var j = 0; j < size; j++) {
+ var color = colors[i*size + j];
+ g.setColor("#" + color);
+ g.fillRect(j * w, i * h, j * w + w, i * h + h);
+ }
+ }
+ Bangle.on("touch", function(btn, xy) {
+ var x = parseInt((xy.x) / w);
+ var y = parseInt((xy.y) / h);
+ draw("#" + colors[y * size + x]);
+ });
+}
+
+// Clear the screen once, at startup
+// draw immediately at first
+draw3Pattern();
+
+/*
+require("Storage").write("wristlight.info",{
+ "id":"wristlight",
+ "name":"Wrist Light",
+ "src":"wristlight.app.js",
+ "icon":"wristlight.img"
+});
+*/
diff --git a/apps/wristlight/metadata.json b/apps/wristlight/metadata.json
new file mode 100644
index 000000000..af1d700df
--- /dev/null
+++ b/apps/wristlight/metadata.json
@@ -0,0 +1,16 @@
+{ "id": "wristlight",
+ "name": "Wrist Light",
+ "shortName":"Wrist Light",
+ "icon": "wristlight48.png",
+ "version":"0.01",
+ "description": "A flash light with different colors on your wrist",
+ "tags": "flash,light",
+ "allow_emulator":true,
+ "supports": ["BANGLEJS", "BANGLEJS2"],
+ "readme":"README.md",
+ "screenshots" : [ { "url":"screenshot.png" }, { "url":"screenshot_red.png" } ],
+ "storage": [
+ {"name":"wristlight.app.js","url":"app.js"},
+ {"name":"wristlight.img","url":"wristlight-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/wristlight/screenshot.png b/apps/wristlight/screenshot.png
new file mode 100644
index 000000000..31850d1ca
Binary files /dev/null and b/apps/wristlight/screenshot.png differ
diff --git a/apps/wristlight/screenshot_red.png b/apps/wristlight/screenshot_red.png
new file mode 100644
index 000000000..06ac7a190
Binary files /dev/null and b/apps/wristlight/screenshot_red.png differ
diff --git a/apps/wristlight/wristlight-icon.js b/apps/wristlight/wristlight-icon.js
new file mode 100644
index 000000000..689372499
--- /dev/null
+++ b/apps/wristlight/wristlight-icon.js
@@ -0,0 +1 @@
+atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMzMzMzMAN3d3d3d3QDu7u7u7gAAAAAADMzMzMzMDd3d3d3d3dDu7u7u7uAAAAAAzMzMzMzMDd3d3d3d3dDu7u7u7u4AAAAMzMzMzMzMDd3d3d3d3dDu7u7u7u7gAAAMzMzMzMzMDd3d3d3d3dDu7u7u7u7gAAAMzMzMzMzMDd3d3d3d3dDu7u7u7u7gAAAMzMzMzMzMDd3d3d3d3dDu7u7u7u7gAAAMzMzMzMzMDd3d3d3d3dDu7u7u7u7gAAAMzMzMzMzMDd3d3d3d3dDu7u7u7u7gAAAMzMzMzMzMDd3d3d3d3dDu7u7u7u7gAAAMzMzMzMzMDd3d3d3d3dDu7u7u7u7gAAAMzMzMzMzMDd3d3d3d3dDu7u7u7u7gAAAMzMzMzMzMDd3d3d3d3dDu7u7u7u7gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAu7u7u7uwAAAA//8AAAAHd3d3d3cAAAALu7u7u7u7AAD/////AAB3d3d3d3dwAAALu7u7u7u7AA//////8AB3d3d3d3dwAAALu7u7u7u7AP///////wB3d3d3d3dwAAALu7u7u7u7AP///////wB3d3d3d3dwAAALu7u7u7u7D/////////B3d3d3d3dwAAALu7u7u7u7D/////////B3d3d3d3dwAAALu7u7u7u7D/////////B3d3d3d3dwAAALu7u7u7u7A////////zB3d3d3d3dwAAALu7u7u7u7AP///////wB3d3d3d3dwAAALu7u7u7u7AP///////wB3d3d3d3dwAAALu7u7u7u7AA//////8AB3d3d3d3dwAAALu7u7u7u7AAD/////AAB3d3d3d3dwAAAMu7u7u7u8AAACMzMgAAB3d3d3d3dwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALu7u7u7u7AIiIiIiIiACIiIiIiIiAAAALu7u7u7u7CYiIiIiIiJCIiIiIiIiAAAALu7u7u7u7CYiIiIiIiJCIiIiIiIiAAAALu7u7u7u7CYiIiIiIiJCIiIiIiIiAAAALu7u7u7u7CYiIiIiIiJCIiIiIiIiAAAALu7u7u7u7CYiIiIiIiJCIiIiIiIiAAAALu7u7u7u7CYiIiIiIiJCIiIiIiIiAAAALu7u7u7u7CYiIiIiIiJCIiIiIiIiAAAALu7u7u7u7CYiIiIiIiJCIiIiIiIiAAAALu7u7u7u7CYiIiIiIiJCIiIiIiIiAAAAAu7u7u7u7CYiIiIiIiJCIiIiIiIgAAAAAq7u7u7u7CYiIiIiIiJCIiIiIiIAAAAAAALu7u7u7CoiIiIiIiKCIiIiIiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")
diff --git a/apps/wristlight/wristlight48.png b/apps/wristlight/wristlight48.png
new file mode 100644
index 000000000..8f0ff59f7
Binary files /dev/null and b/apps/wristlight/wristlight48.png differ
diff --git a/bin/firmwaremaker_c.js b/bin/firmwaremaker_c.js
index 7940e551d..fd8072e06 100755
--- a/bin/firmwaremaker_c.js
+++ b/bin/firmwaremaker_c.js
@@ -1,4 +1,4 @@
-#!/usr/bin/nodejs
+#!/usr/bin/node
/*
Mashes together a bunch of different apps into a big binary blob.
We then store this *inside* the Bangle.js firmware and can use it
diff --git a/core b/core
index 2054537a9..c46b4edd2 160000
--- a/core
+++ b/core
@@ -1 +1 @@
-Subproject commit 2054537a9958f9812ae2cad908b6597ff01e449d
+Subproject commit c46b4edd2052d0df37fea41f8839af8175a78ec9
diff --git a/lang/de_DE.json b/lang/de_DE.json
index e0deef7fb..ef22b588f 100644
--- a/lang/de_DE.json
+++ b/lang/de_DE.json
@@ -20,11 +20,11 @@
"On": "Ein",
"Off": "Aus",
"Ok": "OK",
- "New Timer": "Neue Zeitschaltuhr",
+ "New Timer": "Neuer Kurzzeitwecker",
"(repeat)": "(Wiederholung)",
"music": "Musik",
- "Keep Msgs": "Msgs behalten",
- "circle count": "Kreiszahl",
+ "Keep Msgs": "Nachrichten behalten",
+ "circle count": "Anzahl Kreise",
"Auto snooze": "Automatisches Schlummern",
"week": "Woche",
"Heartrate": "Herzfrequenz",
@@ -43,7 +43,7 @@
"colorize icon": "Symbol einfärben",
"min. confidence": "Mindestvertrauen",
"maximum": "maximal",
- "distance goal": "Fernziel",
+ "distance goal": "Entfernungsziel",
"Circle": "Kreis",
"Yes\ndefinitely": "Ja\ndefinitiv",
"TAP right top/bottom": "TAP rechts oben/unten",
@@ -52,28 +52,29 @@
"Mark Unread": "Ungelesen markieren",
"Delete all messages": "Alle Nachrichten löschen",
"Unread timer": "Ungelesener Timer",
- "Quiet Mode": "Leiser Modus",
+ "Quiet Mode": "Stiller Modus",
"Utils": "Werkzeuge",
"Piezo": "Piezo",
"LCD": "LCD",
- "Record Run": "Rekordlauf",
+ "Record Run": "Lauf aufzeichnen",
"Apps": "Apps",
"Delete All Messages": "Alle Nachrichten löschen",
"start&lap/reset, BTN1: EXIT": "Start&Runde/Zurücksetzen, BTN1: EXIT",
"No Messages": "Keine Nachrichten",
"Bluetooth": "Bluetooth",
- "BTNs 1:startlap 2:exit 3:reset": "BTNs 1:startlap 2:exit 3:reset",
+ "BTNs 1:startlap 2:exit 3:reset": "BTNs 1:Rundenstart 2:Exit 3:Reset",
"View Message": "Nachricht anzeigen",
"Vector font size": "Vektor-Schriftgröße",
- "Light BW": "Licht BW",
+ "Light BW": "Hell S/W",
"BLE": "BLE",
"Make Connectable": "Verbindbar machen",
"Vibration": "Vibration",
- "Foreground": "Vorderseite",
- "Customize": "Anpassen",
+ "Foreground": "Vordergrund",
+ "Customize": "Individualisieren",
+ "Custom Theme": "Individueller Stil",
"HID": "HID",
- "Dark BW": "Dunkel BW",
- "Passkey BETA": "Hauptschlüssel BETA",
+ "Dark BW": "Dunkel S/W",
+ "Passkey BETA": "Passwort BETA",
"Show clocks": "Uhren anzeigen",
"Font": "Schriftart",
"Launcher Settings": "Launcher-Einstellungen",
@@ -82,30 +83,30 @@
"Background 2": "Hintergrund 2",
"Foreground 2": "Vordergrund 2",
"Add Device": "Gerät hinzufügen",
- "Highlight BG": "Hervorhebung BG",
+ "Highlight BG": "Hervorhebung HG",
"Background": "Hintergrund",
- "Highlight FG": "Highlight FG",
+ "Highlight FG": "Hervorhebung VG",
"Wake on Touch": "Wecken bei Berührung",
"Twist Timeout": "Twist Timeout",
"Twist Max Y": "Twist Max Y",
- "LCD Timeout": "LCD-Zeitüberschreitung",
+ "LCD Timeout": "LCD-Leuchtdauer",
"LCD Brightness": "LCD-Helligkeit",
- "Utilities": "Versorgungsunternehmen",
+ "Utilities": "Werkzeuge",
"Log": "Protokoll",
- "Compact Storage": "Kompakte Lagerung",
- "Wake on BTN3": "Wake auf BTN3",
- "Twist Threshold": "Schwellenwert verdrehen",
+ "Compact Storage": "Speicherwartung",
+ "Wake on BTN3": "Aufwachen auf BTN3",
+ "Twist Threshold": "Twist Schwellenwert",
"Remove": "entfernen",
"Connect device\nto add to\nwhitelist": "Gerät verbinden\nzum Hinzufügen zur\nWhitelist",
"Debug Info": "Debug-Informationen",
"Time Zone": "Zeitzone",
- "Clock Style": "Uhr Stil",
- "Wake on BTN2": "Wake auf BTN2",
- "Wake on FaceUp": "Wake on FaceUp",
- "Wake on BTN1": "Wake auf BTN1",
- "Wake on Twist": "Wake on Twist",
- "Connectable": "Anschließbar",
- "Second": "Zweite",
+ "Clock Style": "Uhrenstil",
+ "Wake on BTN2": "Aufwecken mit BTN2",
+ "Wake on FaceUp": "Aufwecken mit Display oben",
+ "Wake on BTN1": "Aufwecken mit BTN1",
+ "Wake on Twist": "Aufwecken mit Twist",
+ "Connectable": "Erreichbar",
+ "Second": "Sekunde",
"Minute": "Minute",
"Turn Off": "Ausschalten",
"No Clocks Found": "Keine Uhren gefunden",
@@ -114,10 +115,10 @@
"Reset to Defaults": "Auf Standardwerte zurücksetzen",
"Flattening battery - this can take hours.\nLong-press button to cancel": "Entladen der Batterie - dies kann Stunden dauern.\nLanger Tastendruck zum Abbrechen",
"Reset Settings": "Einstellungen zurücksetzen",
- "Rewrite Settings": "Einstellungen umschreiben",
- "Compacting...\nTakes approx\n1 minute": "Verdichten...\nDauert ca.\n1 Minute",
- "Stay Connectable": "Anschlussfähig bleiben",
- "Storage": "Lagerung",
+ "Rewrite Settings": "Einstellungen neu schreiben",
+ "Compacting...\nTakes approx\n1 minute": "Speicherwartung...\nDauert ca.\n1 Minute",
+ "Stay Connectable": "Erreichbar bleiben",
+ "Storage": "Speicher",
"This will remove everything": "Dadurch wird alles entfernt",
"on": "auf",
"TIMER": "TIMER",
@@ -126,9 +127,10 @@
"Beep": "Piep",
"Reset": "Zurücksetzen",
"No app has settings": "Keine App hat Einstellungen",
+ "Day": "Tag",
"Month": "Monat",
"Reset All": "Alle zurücksetzen",
- "Flatten Battery": "Batterie abflachen",
+ "Flatten Battery": "Batterie entladen",
"Right": "Rechts",
"Side": "Seite",
"Left": "Links",
@@ -141,12 +143,12 @@
"Vibrate": "Vibrieren",
"Reset all widgets": "Alle Widgets zurücksetzen",
"System": "System",
- "Alerts": "Warnungen",
- "Locale": "Schauplatz",
+ "Alerts": "Alarme",
+ "Locale": "Ort",
"Whitelist": "Whitelist",
"Select Clock": "Uhr auswählen",
- "Disable": "Deaktivieren Sie",
- "Timer": "Zeitschaltuhr",
+ "Disable": "Deaktivieren",
+ "Timer": "Kurzzeitwecker",
"Error in settings": "Fehler in den Einstellungen",
"Set Time": "Zeit einstellen",
"ALARM": "ALARM",
@@ -164,7 +166,7 @@
"Music": "Musik",
"color": "Farbe",
"off": "aus",
- "Theme": "Thema",
+ "Theme": "Stil",
"one": "eins",
"two": "zwei",
"three": "drei",
@@ -176,7 +178,22 @@
"nine": "neun",
"ten": "zehn",
"eleven": "elf",
- "twelve": "zwölf"
+ "twelve": "zwölf",
+ "Time Format": "Zeitformat",
+ "Start Week On": "Wochenbeginn",
+ "Date & Time": "Datum und Zeit",
+ "Calibrate Battery": "Akku kalibrieren",
+ "black": "Schwarz",
+ "white": "Weiß",
+ "red": "Rot",
+ "green": "Grün",
+ "blue": "Blau",
+ "yellow": "Gelb",
+ "magenta": "Magenta",
+ "cyan": "Cyan",
+ "orange": "Orange",
+ "purple": "Violett",
+ "grey": "Grau"
},
"alarm": {
"//": "App-specific overrides",
@@ -189,7 +206,7 @@
"ten past *$1": "zehn nach *$1",
"quarter past *$1": "viertel nach *$1",
"twenty past *$1": "zwanzig nach *$1",
- "twenty five past *$1": "fünf for halb *$2",
+ "twenty five past *$1": "fünf vor halb *$2",
"half past *$1": "halb *$2",
"twenty five to *$2": "fünf nach halb *$2",
"twenty to *$2": "zwanzig vor *$2",
diff --git a/lang/index.json b/lang/index.json
index 20ceaab92..474a530ab 100644
--- a/lang/index.json
+++ b/lang/index.json
@@ -12,6 +12,7 @@
{"code":"tr_TR","name":"Turkish","url":"tr_TR.json"},
{"code":"ru_RU","name":"Russian","url":"ru_RU.json", "disabled":"Characters not in ISO Latin codepage"},
{"code":"pt_PT","name":"Portuguese","url":"pt_PT.json"},
+ {"code":"pt_BR","name":"Portuguese Brasil","url":"pt_BR.json"},
{"code":"bg_BG","name":"Bulgarian","url":"bg_BG.json", "disabled":"Characters not in ISO Latin codepage"},
{"code":"da_DA","name":"Danish","url":"da_DA.json"},
{"code":"el_EL","name":"Greek","url":"el_EL.json", "disabled":"Characters not in ISO Latin codepage"},
@@ -23,5 +24,5 @@
{"code":"sk_SK","name":"Slovak","url":"sk_SK.json"},
{"code":"sl_SL","name":"Slovenian","url":"sl_SL.json"},
{"code":"nn_NO","name":"Norwegian (Nynorsk)","url":"nn_NO.json"},
- {"code":"hr_HR","name":"Croatian","url":"hr_HR.json"}
+ {"code":"hr_HR","name":"Croatian","url":"hr_HR.json"}
]
diff --git a/lang/pt_BR.json b/lang/pt_BR.json
new file mode 100644
index 000000000..75849ddad
--- /dev/null
+++ b/lang/pt_BR.json
@@ -0,0 +1,167 @@
+{
+ "//": "Portuguese Brasil language translations",
+ "GLOBAL": {
+ "//": "Translations that apply for all apps",
+ "New Timer": "Novo Temporizador",
+ "New Alarm": "Novo Alarme",
+ "Auto snooze": "Soneca automática",
+ "week": "semana",
+ "circle 3": "círculo 3",
+ "(repeat)": "(repetir)",
+ "Save": "Salvar",
+ "Keep Msgs": "Manter Msgs",
+ "music": "música",
+ "circle 4": "círculo 4",
+ "circle 2": "círculo 2",
+ "circle count": "contagem em círculo",
+ "circle 1": "círculo 1",
+ "battery warn": "aviso de bateria",
+ "show widgets": "exibir widgets",
+ "data": "dados",
+ "heartrate": "frequência cardíaca",
+ "distance goal": "distancia de chegada",
+ "Circle": "Círculo",
+ "colorize icon": "colorir ícone",
+ "min. confidence": "min. confiança",
+ "minimum": "mínimo",
+ "maximum": "máximo",
+ "Heartrate": "Frequência Cardíaca",
+ "weather circle": "círculo meteorológico",
+ "step length": "comprimento do passo",
+ "valid period": "período válido",
+ "TAP right top/bottom": "TAP superior/inferior direita",
+ "Vector font size": "Tamanho de letra vectorial",
+ "Yes\ndefinitely": "Sim\ndefinitivamente",
+ "BTNs 1:startlap 2:exit 3:reset": "BTNs 1:startlap 2:exit 3:reset",
+ "STEPS": "ETAPAS",
+ "Font": "Fonte",
+ "Show clocks": "Mostrar relógios",
+ "App Source\nNot found": "Fonte do aplicativo\nNão encontrado",
+ "Mark Unread": "Marcar como não lido",
+ "View Message": "Ver Mensagem",
+ "start&lap/reset, BTN1: EXIT": "start&lap/reset, BTN1: SAÍDA",
+ "Launcher Settings": "Configurações do Launcher",
+ "Delete All Messages": "Apagar todas as mensagens",
+ "Delete all messages": "Apagar todas as mensagens",
+ "Utils": "Utils",
+ "LCD": "LCD",
+ "Apps": "Apps",
+ "Record Run": "Gravar Corrida",
+ "No Messages": "Sem Mensagens",
+ "Unread timer": "Temporizador não visto",
+ "Are you sure": "Tem certeza",
+ "Make Connectable": "Habilitar Conexão",
+ "Piezo": "Piezo",
+ "Bluetooth": "Bluetooth",
+ "BLE": "BLE",
+ "Programmable": "Programável",
+ "Vibration": "Vibração",
+ "Quiet Mode": "Modo Silencioso",
+ "Foreground": "Primeiro plano",
+ "Passkey BETA": "Senha BETA",
+ "HID": "HID",
+ "Light BW": "BW Leve",
+ "Foreground 2": "Primeiro plano 2",
+ "Dark BW": "BW Escuro",
+ "Background": "Plano de Fundo",
+ "Highlight FG": "Destaque FG",
+ "Customize": "Personalizar",
+ "Background 2": "Plano de Fundo 2",
+ "Wake on BTN3": "Acordar no BTN3",
+ "Wake on BTN2": "Acordar no BTN2",
+ "Highlight BG": "Destaque BG",
+ "LCD Timeout": "Tempo limite do LCD",
+ "Wake on FaceUp": "Acordar no FaceUp",
+ "Wake on BTN1": "Acordar no BTN1",
+ "Wake on Twist": "Acordar ao Balançar",
+ "Wake on Touch": "Acordar ao Tocar",
+ "Connect device\nto add to\nwhitelist": "Ligar dispositivo\npara adicionar a\nWhitelist",
+ "Remove": "Remover",
+ "Add Device": "Adicionar dispositivo",
+ "LCD Brightness": "Luminosidade do LCD",
+ "Twist Max Y": "Max Y do Balanço",
+ "Utilities": "Utilidades",
+ "Twist Threshold": "Limiar do Balanço",
+ "Time Zone": "Fuso horário",
+ "Twist Timeout": "Timeout do Balanço",
+ "Clock Style": "Estilo do Relógio",
+ "Debug Info": "Debug Infos",
+ "Log": "Logs",
+ "Storage": "Armazenamento",
+ "Rewrite Settings": "Re-escrever Configurações",
+ "Compacting...\nTakes approx\n1 minute": "Compactando...\nLeva aproximadamente\n1 minuto",
+ "Flatten Battery": "Drenar Bateria",
+ "Reset Settings": "Resetar Configurações",
+ "Compact Storage": "Armazenamento compacto",
+ "Stay Connectable": "Manter Conectavel",
+ "Turn Off": "Desativar",
+ "Connectable": "Conectável",
+ "This will remove everything": "Esta ação irá apagar tudo",
+ "Date": "Data",
+ "Month": "Mês",
+ "Second": "Segundo",
+ "Minute": "Minuto",
+ "Flattening battery - this can take hours.\nLong-press button to cancel": "Drenando Bateria - isto pode demorar horas.\nSegure o Botão para Cancelar",
+ "Reset to Defaults": "Resetar Dispositivo",
+ "Hour": "Hora",
+ "No Clocks Found": "Não encontramos nenhum Relogio",
+ "Right": "Confirmar",
+ "No app has settings": "Nenhum aplicativo possui configurações",
+ "App Settings": "Configurações do aplicativo",
+ "OFF": "DESLIGADO",
+ "Side": "Lado",
+ "Left": "Esquerda",
+ "Sort Order": "Ordem de classificação",
+ "Widgets": "Widgets",
+ "Invalid settings": "Configurações inválidas",
+ "Sleep Phase Alarm": "Alarme da Fase do Sono",
+ "Alarm": "Alarme",
+ "Minutes": "Minutos",
+ "TIMER": "TIMER",
+ "Hours": "Horário",
+ "on": "em",
+ "Reset All": "Resetar tudo",
+ "Repeat": "Repetir",
+ "Delete": "Deletar",
+ "Enabled": "Habilitado",
+ "Reset all widgets": "Redefinir todos os widgets",
+ "Reset": "Resetar",
+ "goal": "meta",
+ "Message": "Mensagem",
+ "Beep": "Bip",
+ "Vibrate": "Vibrar",
+ "System": "Sistema",
+ "Alerts": "Alertas",
+ "Locale": "Localização",
+ "Set Time": "Tempo Definido",
+ "Whitelist": "Whitelist",
+ "Select Clock": "Selecionar Relógio",
+ "BACK": "VOLTAR",
+ "Timer": "Temporizador",
+ "Error in settings": "Erro nas Configurações",
+ "Disable": "Desativar",
+ "Factory Reset": "Reset de Fábrica",
+ "Connected": "Conectado",
+ "ALARM": "ALARME",
+ "Sleep": "Dormir",
+ "Messages": "Mensagens",
+ "Hide": "Esconder",
+ "Show": "Mostrar",
+ "On": "Ativo",
+ "Ok": "Ok",
+ "No": "Não",
+ "Settings": "Configurações",
+ "steps": "passos",
+ "back": "voltar",
+ "Steps": "Passos",
+ "Year": "Ano",
+ "Yes": "Sim",
+ "Loading": "Carregando",
+ "Music": "Música",
+ "color": "cor",
+ "off": "desativado",
+ "Off": "Desativado",
+ "Theme": "Tema",
+ "Back": "Voltar"
+ }
+}
diff --git a/loader.js b/loader.js
index e9887f3d4..0e0fdcba0 100644
--- a/loader.js
+++ b/loader.js
@@ -16,7 +16,7 @@ if (window.location.host=="banglejs.com") {
'This is not the official Bangle.js App Loader - you can try the Official Version here.';
}
-var RECOMMENDED_VERSION = "2v13";
+var RECOMMENDED_VERSION = "2v14";
// could check http://www.espruino.com/json/BANGLEJS.json for this
// We're only interested in Bangles
diff --git a/modules/ClockFace.js b/modules/ClockFace.js
index d6c3a2e66..f8dc33287 100644
--- a/modules/ClockFace.js
+++ b/modules/ClockFace.js
@@ -10,7 +10,8 @@ function ClockFace(options) {
"precision",
"init", "draw", "update",
"pause", "resume",
- "up", "down", "upDown"
+ "up", "down", "upDown",
+ "settingsFile",
].includes(k)) throw `Invalid ClockFace option: ${k}`;
});
if (!options.draw && !options.update) throw "ClockFace needs at least one of draw() or update() functions";
@@ -33,7 +34,18 @@ function ClockFace(options) {
};
if (options.upDown) this._upDown = options.upDown;
- this.is12Hour = !!(require("Storage").readJSON("setting.json", 1) || {})["12hour"];
+ if (options.settingsFile) {
+ const settings = (require("Storage").readJSON(options.settingsFile, true) || {});
+ Object.keys(settings).forEach(k => {
+ this[k] = settings[k];
+ });
+ }
+ // these default to true
+ ["showDate", "loadWidgets"].forEach(k => {
+ if (this[k]===undefined) this[k] = true;
+ });
+ // use global 24/12-hour setting if not set by clock-settings
+ if (!('is12Hour' in this)) this.is12Hour = !!(require("Storage").readJSON("setting.json", true) || {})["12hour"];
}
ClockFace.prototype.tick = function() {
@@ -46,7 +58,7 @@ ClockFace.prototype.tick = function() {
};
if (!this._last) {
g.clear(true);
- Bangle.drawWidgets();
+ if (global.WIDGETS) Bangle.drawWidgets();
g.reset();
this.draw.apply(this, [time, {d: true, h: true, m: true, s: true}]);
} else {
@@ -70,7 +82,7 @@ ClockFace.prototype.start = function() {
.CLOCK is set by Bangle.setUI('clock') but we want to load widgets so we can check appRect and *then*
call setUI. see #1864 */
Bangle.CLOCK = 1;
- Bangle.loadWidgets();
+ if (this.loadWidgets) Bangle.loadWidgets();
if (this.init) this.init.apply(this);
if (this._upDown) Bangle.setUI("clockupdown", d=>this._upDown.apply(this,[d]));
else Bangle.setUI("clock");
diff --git a/modules/ClockFace.md b/modules/ClockFace.md
index e760c3e74..b2332c805 100644
--- a/modules/ClockFace.md
+++ b/modules/ClockFace.md
@@ -85,6 +85,7 @@ var clock = new ClockFace({
if (dir === -1) // Up
else // (dir === 1): Down
},
+ settingsFile: 'appid.settings.json', // optional, values from file will be applied to `this`
});
clock.start();
@@ -110,11 +111,51 @@ clock.start();
```
+
+SettingsFile
+------------
+If you use the `settingsFile` option, values from that file are loaded and set
+directly on the clock.
+
+For example:
+
+```json
+// example.settings.json:
+{
+ "showDate": false,
+ "foo": 123
+}
+```
+```js
+ var ClockFace = require("ClockFace");
+ var clock = new ClockFace({
+ draw: function(){/*...*/},
+ settingsFile: "example.settings.json",
+ });
+ // now
+ clock.showDate === false;
+ clock.foo === 123;
+ clock.loadWidgets === true; // default when not in settings file
+ clock.is12Hour === ??; // not in settings file: uses global setting
+ clock.start();
+
+```
+
Properties
----------
The following properties are automatically set on the clock:
* `is12Hour`: `true` if the "Time Format" setting is set to "12h", `false` for "24h".
* `paused`: `true` while the clock is paused. (You don't need to check this inside your `draw()` code)
+* `showDate`: `true` (if not overridden through the settings file.)
+* `loadWidgets`: `true` (if not overridden through the settings file.)
+ If set to `false` before calling `start()`, the clock won't call `Bangle.loadWidgets();` for you.
+ Best is to add a setting for this, but if you never want to load widgets, you could do this:
+ ```js
+ var ClockFace = require("ClockFace");
+ var clock = new ClockFace({draw: function(){/*...*/}});
+ clock.loadWidgets = false; // prevent loading of widgets
+ clock.start();
+ ```
Inside the `draw()`/`update()` function you can access these using `this`:
@@ -133,4 +174,42 @@ Bangle.on('step', function(steps) {
if (clock.paused === false) // draw step count
});
+```
+
+
+ClockFace_menu
+==============
+If your clock comes with a settings menu, you can use this library to easily add
+some common options:
+
+```js
+
+let settings = require("Storage").readJSON(".settings.json", true)||{};
+function save(key, value) {
+ settings[key] = value;
+ require("Storage").writeJSON(".settings.json", settings);
+}
+
+let menu = {
+ "": {"title": /*LANG*/" Settings"},
+};
+require("ClockFace_menu").addItems(menu, save, {
+ showDate: settings.showDate,
+ loadWidgets: settings.loadWidgets,
+});
+E.showMenu(menu);
+
+```
+
+Or even simpler, if you just want to use a basic settings file:
+```js
+let menu = {
+ "": {"title": /*LANG*/" Settings"},
+ /*LANG*/"< Back": back,
+};
+require("ClockFace_menu").addSettingsFile(menu, ".settings.json", [
+ "showDate", "loadWidgets"
+]);
+E.showMenu(menu);
+
```
\ No newline at end of file
diff --git a/modules/ClockFace_menu.js b/modules/ClockFace_menu.js
new file mode 100644
index 000000000..f2267d9ca
--- /dev/null
+++ b/modules/ClockFace_menu.js
@@ -0,0 +1,48 @@
+/**
+ * Add setting items to a menu
+ *
+ * @param {object} menu Menu to add items to
+ * @param {function} callback Callback when value changes
+ * @param {object} items Menu items to add, with their current value
+ */
+exports.addItems = function(menu, callback, items) {
+ Object.keys(items).forEach(key => {
+ let value = items[key];
+ const label = {
+ showDate:/*LANG*/"Show date",
+ loadWidgets:/*LANG*/"Load widgets",
+ }[key];
+ switch(key) {
+ case "showDate":
+ case "loadWidgets":
+ // boolean options, which default to true
+ if (value===undefined) value = true;
+ menu[label] = {
+ value: !!value,
+ onchange: v => callback(key, v),
+ };
+ }
+ });
+};
+
+/**
+ * Create a basic settings menu for app, reading/writing to settings file
+ *
+ * @param {object} menu Menu to add settings to
+ * @param {string} settingsFile File to read/write settings to/from
+ * @param {string[]} items List of settings to add
+ */
+exports.addSettingsFile = function(menu, settingsFile, items) {
+ let s = require("Storage").readJSON(settingsFile, true) || {};
+
+ function save(key, value) {
+ s[key] = value;
+ require("Storage").writeJSON(settingsFile, s);
+ }
+
+ let toAdd = {};
+ items.forEach(function(key) {
+ toAdd[key] = s[key];
+ });
+ exports.addItems(menu, save, toAdd);
+};
\ No newline at end of file
diff --git a/modules/Layout.js b/modules/Layout.js
index 19cfabe11..fd5809a93 100644
--- a/modules/Layout.js
+++ b/modules/Layout.js
@@ -2,6 +2,14 @@
// See Layout.md for documentation
+/* Minify to 'Layout.min.js' by:
+
+ * checking out: https://github.com/espruino/EspruinoDocs
+ * run: ../EspruinoDocs/bin/minify.js modules/Layout.js modules/Layout.min.js
+
+*/
+
+
function Layout(layout, options) {
this._l = this.l = layout;
// Do we have >1 physical buttons?
@@ -71,7 +79,7 @@ function Layout(layout, options) {
Layout.prototype.setUI = function() {
Bangle.setUI(); // remove all existing input handlers
- var uiSet;
+ let uiSet;
if (this.buttons) {
// multiple buttons so we'll jus use back/next/select
Bangle.setUI({mode:"updown", back:this.options.back}, dir=>{
diff --git a/modules/Layout.min.js b/modules/Layout.min.js
new file mode 100644
index 000000000..4523c547c
--- /dev/null
+++ b/modules/Layout.min.js
@@ -0,0 +1,14 @@
+function p(b,k){function d(h){h.id&&(f[h.id]=h);h.type||(h.type="");h.c&&h.c.forEach(d)}this._l=this.l=b;this.physBtns=2==process.env.HWVERSION?1:3;this.options=k||{};this.lazy=this.options.lazy||!1;if(2!=process.env.HWVERSION){var a=[];function h(m){"btn"==m.type&&a.push(m);m.c&&m.c.forEach(h)}h(b);a.length&&(this.physBtns=0,this.buttons=a,this.selectedButton=-1)}if(this.options.btns)if(this.b=b=this.options.btns,this.physBtns>=b.length){let h=Math.floor(Bangle.appRect.h/
+this.physBtns);for(2b.length;)b.push({label:""});this._l.width=g.getWidth()-8;this._l={type:"h",filly:1,c:[this._l,{type:"v",pad:1,filly:1,c:b.map(m=>(m.type="txt",m.font="6x8",m.height=h,m.r=1,m))}]}}else this._l.width=g.getWidth()-32,this._l={type:"h",c:[this._l,{type:"v",c:b.map(h=>(h.type="btn",h.filly=1,h.width=32,h.r=1,h))}]},a&&a.push.apply(a,this._l.c[1].c);this.setUI();var f=this;d(this._l);this.updateNeeded=!0}function r(b,
+k,d,a,f){var h=null==b.bgCol?f:g.toColor(b.bgCol);if(h!=f||"txt"==b.type||"btn"==b.type||"img"==b.type||"custom"==b.type){var m=b.c;delete b.c;var c="H"+E.CRC32(E.toJS(b));m&&(b.c=m);delete k[c]||((a[c]=[b.x,b.y,b.x+b.w-1,b.y+b.h-1]).bg=null==f?g.theme.bg:f,d&&(d.push(b),d=null))}if(b.c)for(var l of b.c)r(l,k,d,a,h)}p.prototype.setUI=function(){Bangle.setUI();let b;this.buttons&&(Bangle.setUI({mode:"updown",back:this.options.back},k=>{var d=this.selectedButton,a=this.buttons.length;if(void 0===k&&
+this.buttons[d])return this.buttons[d].cb();this.buttons[d]&&(delete this.buttons[d].selected,this.render(this.buttons[d]));d=(d+a+k)%a;this.buttons[d]&&(this.buttons[d].selected=1,this.render(this.buttons[d]));this.selectedButton=d}),b=!0);this.options.back&&!b&&Bangle.setUI({mode:"custom",back:this.options.back});if(this.b){function k(d,a){.75=d.x&&a.y>=d.y&&a.x<=d.x+d.w&&a.y<=d.y+d.h&&(2==a.type&&d.cbl?d.cbl(a):d.cb&&d.cb(a));d.c&&d.c.forEach(f=>k(f,a))}Bangle.touchHandler=(d,a)=>k(this._l,a);Bangle.on("touch",Bangle.touchHandler)}};
+p.prototype.render=function(b){function k(c){"ram";g.reset();void 0!==c.col&&g.setColor(c.col);void 0!==c.bgCol&&g.setBgColor(c.bgCol).clearRect(c.x,c.y,c.x+c.w-1,c.y+c.h-1);d[c.type](c)}b||(b=this._l);this.updateNeeded&&this.update();var d={"":function(){},txt:function(c){if(c.wrap){g.setFont(c.font).setFontAlign(0,-1);var l=g.wrapString(c.label,c.w),e=c.y+(c.h-g.getFontHeight()*l.length>>1);l.forEach((n,q)=>g.drawString(n,c.x+(c.w>>1),e+g.getFontHeight()*q))}else g.setFont(c.font).setFontAlign(0,
+0,c.r).drawString(c.label,c.x+(c.w>>1),c.y+(c.h>>1))},btn:function(c){var l=c.x+(0|c.pad),e=c.y+(0|c.pad),n=c.w-(c.pad<<1),q=c.h-(c.pad<<1);l=[l,e+4,l+4,e,l+n-5,e,l+n-1,e+4,l+n-1,e+q-5,l+n-5,e+q-1,l+4,e+q-1,l,e+q-5,l,e+4];e=c.selected?g.theme.bgH:g.theme.bg2;g.setColor(e).fillPoly(l).setColor(c.selected?g.theme.fgH:g.theme.fg2).drawPoly(l);void 0!==c.col&&g.setColor(c.col);c.src?g.setBgColor(e).drawImage("function"==typeof c.src?c.src():c.src,c.x+c.w/2,c.y+c.h/2,{scale:c.scale||void 0,rotate:.5*Math.PI*
+(c.r||0)}):g.setFont(c.font||"6x8:2").setFontAlign(0,0,c.r).drawString(c.label,c.x+c.w/2,c.y+c.h/2)},img:function(c){g.drawImage("function"==typeof c.src?c.src():c.src,c.x+c.w/2,c.y+c.h/2,{scale:c.scale||void 0,rotate:.5*Math.PI*(c.r||0)})},custom:function(c){c.render(c)},h:function(c){c.c.forEach(k)},v:function(c){c.c.forEach(k)}};if(this.lazy){this.rects||(this.rects={});var a=this.rects.clone(),f=[];r(b,a,f,this.rects,null);for(var h in a)delete this.rects[h];b=Object.keys(a).map(c=>a[c]).reverse();
+for(var m of b)g.setBgColor(m.bg).clearRect.apply(g,m);f.forEach(k)}else k(b)};p.prototype.forgetLazyState=function(){this.rects={}};p.prototype.layout=function(b){switch(b.type){case "h":var k=b.x+(0|b.pad),d=0,a=b.c&&b.c.reduce((e,n)=>e+(0|n.fillx),0);a||(k+=b.w-b._w>>1,a=1);var f=k;b.c.forEach(e=>{e.x=0|f;k+=e._w;d+=0|e.fillx;f=k+Math.floor(d*(b.w-b._w)/a);e.w=0|f-e.x;e.h=0|(e.filly?b.h-(b.pad<<1):e._h);e.y=0|b.y+(0|b.pad)+((1+(0|e.valign))*(b.h-(b.pad<<1)-e.h)>>1);e.c&&this.layout(e)});break;
+case "v":var h=b.y+(0|b.pad),m=0,c=b.c&&b.c.reduce((e,n)=>e+(0|n.filly),0);c||(h+=b.h-b._h>>1,c=1);var l=h;b.c.forEach(e=>{e.y=0|l;h+=e._h;m+=0|e.filly;l=h+Math.floor(m*(b.h-b._h)/c);e.h=0|l-e.y;e.w=0|(e.fillx?b.w-(b.pad<<1):e._w);e.x=0|b.x+(0|b.pad)+((1+(0|e.halign))*(b.w-(b.pad<<1)-e.w)>>1);e.c&&this.layout(e)})}};p.prototype.debug=function(b,k){b||(b=this._l);k=k||1;g.setColor(k&1,k&2,k&4).drawRect(b.x+k-1,b.y+k-1,b.x+b.w-k,b.y+b.h-k);b.pad&&g.drawRect(b.x+b.pad-1,b.y+b.pad-1,b.x+b.w-b.pad,b.y+
+b.h-b.pad);k++;b.c&&b.c.forEach(d=>this.debug(d,k))};p.prototype.update=function(){function b(a){"ram";k[a.type](a);if(a.r&1){var f=a._w;a._w=a._h;a._h=f}a._w=0|Math.max(a._w+(a.pad<<1),0|a.width);a._h=0|Math.max(a._h+(a.pad<<1),0|a.height)}delete this.updateNeeded;var k={txt:function(a){a.font.endsWith("%")&&(a.font="Vector"+Math.round(g.getHeight()*a.font.slice(0,-1)/100));if(a.wrap)a._h=a._w=0;else{var f=g.setFont(a.font).stringMetrics(a.label);a._w=f.width;a._h=f.height}},btn:function(a){a.font&&
+a.font.endsWith("%")&&(a.font="Vector"+Math.round(g.getHeight()*a.font.slice(0,-1)/100));var f=a.src?g.imageMetrics("function"==typeof a.src?a.src():a.src):g.setFont(a.font||"6x8:2").stringMetrics(a.label);a._h=16+f.height;a._w=20+f.width},img:function(a){var f=g.imageMetrics("function"==typeof a.src?a.src():a.src),h=a.scale||1;a._w=f.width*h;a._h=f.height*h},"":function(a){a._w=0;a._h=0},custom:function(a){a._w=0;a._h=0},h:function(a){a.c.forEach(b);a._h=a.c.reduce((f,h)=>Math.max(f,h._h),0);a._w=
+a.c.reduce((f,h)=>f+h._w,0);null==a.fillx&&a.c.some(f=>f.fillx)&&(a.fillx=1);null==a.filly&&a.c.some(f=>f.filly)&&(a.filly=1)},v:function(a){a.c.forEach(b);a._h=a.c.reduce((f,h)=>f+h._h,0);a._w=a.c.reduce((f,h)=>Math.max(f,h._w),0);null==a.fillx&&a.c.some(f=>f.fillx)&&(a.fillx=1);null==a.filly&&a.c.some(f=>f.filly)&&(a.filly=1)}},d=this._l;b(d);d.fillx||d.filly?(d.w=Bangle.appRect.w,d.h=Bangle.appRect.h,d.x=Bangle.appRect.x,d.y=Bangle.appRect.y):(d.w=d._w,d.h=d._h,d.x=Bangle.appRect.w-d.w>>1,d.y=
+Bangle.appRect.y+(Bangle.appRect.h-d.h>>1));this.layout(d)};p.prototype.clear=function(b){b||(b=this._l);g.reset();void 0!==b.bgCol&&g.setBgColor(b.bgCol);g.clearRect(b.x,b.y,b.x+b.w-1,b.y+b.h-1)};exports=p
diff --git a/modules/buzz.js b/modules/buzz.js
index 488d0228d..aed0e2e7b 100644
--- a/modules/buzz.js
+++ b/modules/buzz.js
@@ -1,14 +1,33 @@
-/* Call this with a pattern like '.-.', '.. .' or '..' to buzz that pattern
-out on the internal vibration motor. use buzz_menu to display a menu
-where the patterns can be chosen. */
+/**
+ * Buzz the passed `pattern` out on the internal vibration motor.
+ *
+ * A pattern is a sequence of `.`, `,`, `-`, `:`, `;` and `=` where
+ * - `.` is one short and weak vibration
+ * - `,` is one medium and weak vibration
+ * - `-` is one long and weak vibration
+ * - `:` is one short and strong vibration
+ * - `;` is one medium and strong vibration
+ * - `=` is one long and strong vibration
+ *
+ * You can use the `buzz_menu` module to display a menu where some common patterns can be chosen.
+ *
+ * @param {string} pattern A string like `.-.`, `..=`, `:.:`, `..`, etc.
+ * @returns a Promise
+ */
exports.pattern = pattern => new Promise(resolve => {
- function b() {
- if (pattern=="") resolve();
+ function doBuzz() {
+ if (pattern == "") resolve();
var c = pattern[0];
pattern = pattern.substr(1);
- if (c==".") Bangle.buzz().then(()=>setTimeout(b,100));
- else if (c=="-") Bangle.buzz(500).then(()=>setTimeout(b,100));
- else setTimeout(b,100);
+ const BUZZ_WEAK = 0.25, BUZZ_STRONG = 1;
+ const SHORT_MS = 100, MEDIUM_MS = 200, LONG_MS = 500;
+ if (c == ".") Bangle.buzz(SHORT_MS, BUZZ_WEAK).then(() => setTimeout(doBuzz, 100));
+ else if (c == ",") Bangle.buzz(MEDIUM_MS, BUZZ_WEAK).then(() => setTimeout(doBuzz, 100));
+ else if (c == "-") Bangle.buzz(LONG_MS, BUZZ_WEAK).then(() => setTimeout(doBuzz, 100));
+ else if (c == ":") Bangle.buzz(SHORT_MS, BUZZ_STRONG).then(() => setTimeout(doBuzz, 100));
+ else if (c == ";") Bangle.buzz(MEDIUM_MS, BUZZ_STRONG).then(() => setTimeout(doBuzz, 100));
+ else if (c == "=") Bangle.buzz(LONG_MS, BUZZ_STRONG).then(() => setTimeout(doBuzz, 100));
+ else setTimeout(doBuzz, 100);
}
- b();
+ doBuzz();
});
diff --git a/modules/buzz_menu.js b/modules/buzz_menu.js
index c5b41a997..7ca155a2c 100644
--- a/modules/buzz_menu.js
+++ b/modules/buzz_menu.js
@@ -1,14 +1,19 @@
-/* Display a menu to select from various vibration patterns for use with buzz.js */
-
-exports.pattern = function(value, callback) {
- var vibPatterns = ["", ".", "..", "-", "--", "-.-", "---"];
+/**
+ * Display a menu to select from various common vibration patterns for use with buzz.js.
+ *
+ * @param {string} value The pre-selected pattern
+ * @param {*} callback A function called with the user selected pattern
+ */
+exports.pattern = function (value, callback) {
+ var patterns = ["", ".", ":", "..", "::", ",", ";", ",,", ";;", "-", "=", "--", "==", "...", ":::", "---", ";;;", "==="];
return {
- value: Math.max(0,vibPatterns.indexOf(value)),
- min: 0, max: vibPatterns.length-1,
- format: v => vibPatterns[v]||/*LANG*/"Off",
+ value: Math.max(0, patterns.indexOf(value)),
+ min: 0,
+ max: patterns.length - 1,
+ format: v => patterns[v] || /*LANG*/"Off",
onchange: v => {
- require("buzz").pattern(vibPatterns[v]);
- callback(vibPatterns[v]);
+ require("buzz").pattern(patterns[v]);
+ callback(patterns[v]);
}
};
}
diff --git a/modules/exstats.js b/modules/exstats.js
index 63d94ec7b..461ae727f 100644
--- a/modules/exstats.js
+++ b/modules/exstats.js
@@ -128,10 +128,11 @@ function formatPace(speed, paceLength) {
Bangle.on("GPS", function(fix) {
if (!fix.fix) return; // only process actual fixes
-
- if (!state.active) return;
state.lastGPS = state.thisGPS;
state.thisGPS = fix;
+ if (stats["altg"]) stats["altg"].emit("changed",stats["altg"]);
+ if (stats["speed"]) stats["speed"].emit("changed",stats["speed"]);
+ if (!state.active) return;
if (state.lastGPS.fix)
state.distance += calcDistance(state.lastGPS, fix);
if (stats["dist"]) stats["dist"].emit("changed",stats["dist"]);
@@ -140,7 +141,6 @@ Bangle.on("GPS", function(fix) {
if (!isNaN(fix.speed)) state.curSpeed = state.curSpeed*0.8 + fix.speed*0.2/3.6; // meters/sec
if (stats["pacea"]) stats["pacea"].emit("changed",stats["pacea"]);
if (stats["pacec"]) stats["pacec"].emit("changed",stats["pacec"]);
- if (stats["speed"]) stats["speed"].emit("changed",stats["speed"]);
if (state.notify.dist.increment > 0 && state.notify.dist.next <= state.distance) {
stats["dist"].emit("notify",stats["dist"]);
state.notify.dist.next = state.notify.dist.next + state.notify.dist.increment;
@@ -168,10 +168,21 @@ Bangle.on("HRM", function(h) {
if (stats["bpm"]) stats["bpm"].emit("changed",stats["bpm"]);
}
});
+if (Bangle.setBarometerPower) Bangle.on("pressure", function(e) {
+ if (state.alt === undefined)
+ state.alt = e.altitude;
+ else
+ state.alt = state.alt*0.9 + e.altitude*0.1;
+ var i = Math.round(state.alt);
+ if (i!==state.alti) {
+ state.alti = i;
+ if (stats["altb"]) stats["altb"].emit("changed",stats["altb"]);
+ }
+});
/** Get list of available statistic types */
exports.getList = function() {
- return [
+ var l = [
{name: "Time", id:"time"},
{name: "Distance", id:"dist"},
{name: "Steps", id:"step"},
@@ -181,7 +192,10 @@ exports.getList = function() {
{name: "Pace (curr)", id:"pacec"},
{name: "Speed", id:"speed"},
{name: "Cadence", id:"caden"},
+ {name: "Altitude (GPS)", id:"altg"}
];
+ if (Bangle.setBarometerPower) l.push({name: "Altitude (baro)", id:"altb"});
+ return l;
};
/** Instantiate the given list of statistic IDs (see comments at top)
options = {
@@ -205,7 +219,7 @@ exports.getStats = function(statIDs, options) {
options.notify.dist.increment = (options.notify && options.notify.dist && options.notify.dist.increment)||0;
options.notify.step.increment = (options.notify && options.notify.step && options.notify.step.increment)||0;
options.notify.time.increment = (options.notify && options.notify.time && options.notify.time.increment)||0;
- var needGPS,needHRM;
+ var needGPS,needHRM,needBaro;
// ======================
if (statIDs.includes("time")) {
stats["time"]={
@@ -276,10 +290,27 @@ exports.getStats = function(statIDs, options) {
getString : function() { return state.stepsPerMin; },
};
}
+ if (statIDs.includes("altg")) {
+ needGPS = true;
+ stats["altg"]={
+ title : "Altitude",
+ getValue : function() { return state.thisGPS.alt; },
+ getString : function() { return (state.thisGPS.alt===undefined)?"-":Math.round(state.thisGPS.alt)+"m"; },
+ };
+ }
+ if (statIDs.includes("altb")) {
+ needBaro = true;
+ stats["altb"]={
+ title : "Altitude",
+ getValue : function() { return state.alt; },
+ getString : function() { return (state.alt===undefined)?"-":state.alti+"m"; },
+ };
+ }
// ======================
for (var i in stats) stats[i].id=i; // set up ID field
if (needGPS) Bangle.setGPSPower(true,"exs");
if (needHRM) Bangle.setHRMPower(true,"exs");
+ if (needBaro) Bangle.setBarometerPower(true,"exs");
setInterval(function() { // run once a second....
if (!state.active) return;
// called once a second
@@ -315,6 +346,8 @@ exports.getStats = function(statIDs, options) {
state.BPM = 0;
state.BPMage = 0;
state.maxBPM = 0;
+ state.alt = undefined; // barometer altitude (meters)
+ state.alti = 0; // integer ver of state.alt (to avoid repeated 'changed' notifications)
state.notify = options.notify;
if (options.notify.dist.increment > 0) {
state.notify.dist.next = state.distance + options.notify.dist.increment;
diff --git a/modules/time_utils.js b/modules/time_utils.js
index 5ca153710..6a3ed6faf 100644
--- a/modules/time_utils.js
+++ b/modules/time_utils.js
@@ -55,7 +55,7 @@ exports.decodeTime = (millis) => {
*/
exports.formatTime = (value) => {
var time = safeTime(typeof value === "object" ? value : exports.decodeTime(value));
- if (time.d != 0) throw "(d)ays not supported here";
+ if (time.d != 0) throw "days not supported here";
if (time.h < 0 || time.h > 23) throw "Invalid value: must be 0 <= h <= 23";
if (time.m < 0 || time.m > 59) throw "Invalid value: must be 0 <= m <= 59";
return time.h + ":" + ("0" + time.m).substr(-2);
@@ -63,16 +63,19 @@ exports.formatTime = (value) => {
/**
* @param {object|int} value {d, h, m, s} object or milliseconds
- * @returns an human-readable duration string like "3d 1h 10m 45s"
+ * @param {boolean} compact `true` to remove all whitespaces between the values
+ * @returns an human-readable duration string like "3d 1h 10m 45s" (or "3d1h10m45s" if `compact` is `true`)
*/
-exports.formatDuration = (value) => {
+exports.formatDuration = (value, compact) => {
+ compact = compact || false;
var duration = "";
var time = safeTime(typeof value === "object" ? value : exports.decodeTime(value));
if (time.d > 0) duration += time.d + "d ";
if (time.h > 0) duration += time.h + "h ";
if (time.m > 0) duration += time.m + "m ";
if (time.s > 0) duration += time.s + "s"
- return duration.trim();
+ duration = duration.trim()
+ return compact ? duration.replace(" ", "") : duration;
}
exports.getCurrentTimeMillis = () => {
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 000000000..6872e616c
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,2168 @@
+{
+ "name": "BangleApps",
+ "version": "0.0.1",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "@eslint/eslintrc": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz",
+ "integrity": "sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==",
+ "dev": true,
+ "requires": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^9.3.2",
+ "globals": "^13.15.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ }
+ },
+ "@humanwhocodes/config-array": {
+ "version": "0.9.5",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz",
+ "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==",
+ "dev": true,
+ "requires": {
+ "@humanwhocodes/object-schema": "^1.2.1",
+ "debug": "^4.1.1",
+ "minimatch": "^3.0.4"
+ }
+ },
+ "@humanwhocodes/object-schema": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
+ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
+ "dev": true
+ },
+ "@sindresorhus/is": {
+ "version": "0.14.0",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
+ "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==",
+ "dev": true
+ },
+ "@szmarczak/http-timer": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz",
+ "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==",
+ "dev": true,
+ "requires": {
+ "defer-to-connect": "^1.0.1"
+ }
+ },
+ "@types/json5": {
+ "version": "0.0.29",
+ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
+ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
+ "dev": true
+ },
+ "abbrev": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
+ "dev": true
+ },
+ "acorn": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
+ "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A=="
+ },
+ "acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true
+ },
+ "ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "requires": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ }
+ },
+ "ansi-align": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
+ "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==",
+ "dev": true,
+ "requires": {
+ "string-width": "^4.1.0"
+ }
+ },
+ "ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "anymatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
+ "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
+ "dev": true,
+ "requires": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ }
+ },
+ "argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true
+ },
+ "array-includes": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.5.tgz",
+ "integrity": "sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.19.5",
+ "get-intrinsic": "^1.1.1",
+ "is-string": "^1.0.7"
+ }
+ },
+ "array.prototype.flat": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz",
+ "integrity": "sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.2",
+ "es-shim-unscopables": "^1.0.0"
+ }
+ },
+ "balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true
+ },
+ "binary-extensions": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+ "dev": true
+ },
+ "boxen": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz",
+ "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==",
+ "dev": true,
+ "requires": {
+ "ansi-align": "^3.0.0",
+ "camelcase": "^6.2.0",
+ "chalk": "^4.1.0",
+ "cli-boxes": "^2.2.1",
+ "string-width": "^4.2.2",
+ "type-fest": "^0.20.2",
+ "widest-line": "^3.1.0",
+ "wrap-ansi": "^7.0.0"
+ }
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dev": true,
+ "requires": {
+ "fill-range": "^7.0.1"
+ }
+ },
+ "cacheable-request": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz",
+ "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==",
+ "dev": true,
+ "requires": {
+ "clone-response": "^1.0.2",
+ "get-stream": "^5.1.0",
+ "http-cache-semantics": "^4.0.0",
+ "keyv": "^3.0.0",
+ "lowercase-keys": "^2.0.0",
+ "normalize-url": "^4.1.0",
+ "responselike": "^1.0.2"
+ },
+ "dependencies": {
+ "get-stream": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
+ "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
+ "dev": true,
+ "requires": {
+ "pump": "^3.0.0"
+ }
+ },
+ "lowercase-keys": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
+ "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
+ "dev": true
+ }
+ }
+ },
+ "call-bind": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
+ "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
+ "dev": true,
+ "requires": {
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.0.2"
+ }
+ },
+ "callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true
+ },
+ "camelcase": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+ "dev": true
+ },
+ "chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "chokidar": {
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+ "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+ "dev": true,
+ "requires": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "fsevents": "~2.3.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "dependencies": {
+ "glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "requires": {
+ "is-glob": "^4.0.1"
+ }
+ }
+ }
+ },
+ "ci-info": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz",
+ "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==",
+ "dev": true
+ },
+ "cli-boxes": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz",
+ "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==",
+ "dev": true
+ },
+ "clone-response": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz",
+ "integrity": "sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q==",
+ "dev": true,
+ "requires": {
+ "mimic-response": "^1.0.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true
+ },
+ "configstore": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz",
+ "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==",
+ "dev": true,
+ "requires": {
+ "dot-prop": "^5.2.0",
+ "graceful-fs": "^4.1.2",
+ "make-dir": "^3.0.0",
+ "unique-string": "^2.0.0",
+ "write-file-atomic": "^3.0.0",
+ "xdg-basedir": "^4.0.0"
+ }
+ },
+ "confusing-browser-globals": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz",
+ "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==",
+ "dev": true
+ },
+ "cross-spawn": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+ "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "dev": true,
+ "requires": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ }
+ },
+ "crypto-random-string": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
+ "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==",
+ "dev": true
+ },
+ "debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dev": true,
+ "requires": {
+ "ms": "2.1.2"
+ }
+ },
+ "decompress-response": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz",
+ "integrity": "sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==",
+ "dev": true,
+ "requires": {
+ "mimic-response": "^1.0.0"
+ }
+ },
+ "deep-extend": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+ "dev": true
+ },
+ "deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true
+ },
+ "defer-to-connect": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz",
+ "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==",
+ "dev": true
+ },
+ "define-properties": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz",
+ "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==",
+ "dev": true,
+ "requires": {
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ }
+ },
+ "doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
+ "requires": {
+ "esutils": "^2.0.2"
+ }
+ },
+ "dot-prop": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz",
+ "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==",
+ "dev": true,
+ "requires": {
+ "is-obj": "^2.0.0"
+ }
+ },
+ "duplexer3": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
+ "integrity": "sha512-CEj8FwwNA4cVH2uFCoHUrmojhYh1vmCdOaneKJXwkeY1i9jnlslVo9dx+hQ5Hl9GnH/Bwy/IjxAyOePyPKYnzA==",
+ "dev": true
+ },
+ "emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "end-of-stream": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
+ "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+ "dev": true,
+ "requires": {
+ "once": "^1.4.0"
+ }
+ },
+ "es-abstract": {
+ "version": "1.20.1",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.1.tgz",
+ "integrity": "sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "es-to-primitive": "^1.2.1",
+ "function-bind": "^1.1.1",
+ "function.prototype.name": "^1.1.5",
+ "get-intrinsic": "^1.1.1",
+ "get-symbol-description": "^1.0.0",
+ "has": "^1.0.3",
+ "has-property-descriptors": "^1.0.0",
+ "has-symbols": "^1.0.3",
+ "internal-slot": "^1.0.3",
+ "is-callable": "^1.2.4",
+ "is-negative-zero": "^2.0.2",
+ "is-regex": "^1.1.4",
+ "is-shared-array-buffer": "^1.0.2",
+ "is-string": "^1.0.7",
+ "is-weakref": "^1.0.2",
+ "object-inspect": "^1.12.0",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.2",
+ "regexp.prototype.flags": "^1.4.3",
+ "string.prototype.trimend": "^1.0.5",
+ "string.prototype.trimstart": "^1.0.5",
+ "unbox-primitive": "^1.0.2"
+ }
+ },
+ "es-shim-unscopables": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz",
+ "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==",
+ "dev": true,
+ "requires": {
+ "has": "^1.0.3"
+ }
+ },
+ "es-to-primitive": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
+ "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+ "dev": true,
+ "requires": {
+ "is-callable": "^1.1.4",
+ "is-date-object": "^1.0.1",
+ "is-symbol": "^1.0.2"
+ }
+ },
+ "escape-goat": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz",
+ "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==",
+ "dev": true
+ },
+ "escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true
+ },
+ "eslint": {
+ "version": "8.17.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.17.0.tgz",
+ "integrity": "sha512-gq0m0BTJfci60Fz4nczYxNAlED+sMcihltndR8t9t1evnU/azx53x3t2UHXC/uRjcbvRw/XctpaNygSTcQD+Iw==",
+ "dev": true,
+ "requires": {
+ "@eslint/eslintrc": "^1.3.0",
+ "@humanwhocodes/config-array": "^0.9.2",
+ "ajv": "^6.10.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.3.2",
+ "doctrine": "^3.0.0",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^7.1.1",
+ "eslint-utils": "^3.0.0",
+ "eslint-visitor-keys": "^3.3.0",
+ "espree": "^9.3.2",
+ "esquery": "^1.4.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "functional-red-black-tree": "^1.0.1",
+ "glob-parent": "^6.0.1",
+ "globals": "^13.15.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.0.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "js-yaml": "^4.1.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.1",
+ "regexpp": "^3.2.0",
+ "strip-ansi": "^6.0.1",
+ "strip-json-comments": "^3.1.0",
+ "text-table": "^0.2.0",
+ "v8-compile-cache": "^2.0.3"
+ }
+ },
+ "eslint-config-airbnb-base": {
+ "version": "15.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz",
+ "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==",
+ "dev": true,
+ "requires": {
+ "confusing-browser-globals": "^1.0.10",
+ "object.assign": "^4.1.2",
+ "object.entries": "^1.1.5",
+ "semver": "^6.3.0"
+ }
+ },
+ "eslint-import-resolver-node": {
+ "version": "0.3.6",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz",
+ "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==",
+ "dev": true,
+ "requires": {
+ "debug": "^3.2.7",
+ "resolve": "^1.20.0"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ }
+ }
+ },
+ "eslint-module-utils": {
+ "version": "2.7.3",
+ "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz",
+ "integrity": "sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ==",
+ "dev": true,
+ "requires": {
+ "debug": "^3.2.7",
+ "find-up": "^2.1.0"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ }
+ }
+ },
+ "eslint-plugin-import": {
+ "version": "2.26.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz",
+ "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==",
+ "dev": true,
+ "requires": {
+ "array-includes": "^3.1.4",
+ "array.prototype.flat": "^1.2.5",
+ "debug": "^2.6.9",
+ "doctrine": "^2.1.0",
+ "eslint-import-resolver-node": "^0.3.6",
+ "eslint-module-utils": "^2.7.3",
+ "has": "^1.0.3",
+ "is-core-module": "^2.8.1",
+ "is-glob": "^4.0.3",
+ "minimatch": "^3.1.2",
+ "object.values": "^1.1.5",
+ "resolve": "^1.22.0",
+ "tsconfig-paths": "^3.14.1"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "dev": true,
+ "requires": {
+ "esutils": "^2.0.2"
+ }
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "dev": true
+ }
+ }
+ },
+ "eslint-scope": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz",
+ "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==",
+ "dev": true,
+ "requires": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ }
+ },
+ "eslint-utils": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz",
+ "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==",
+ "dev": true,
+ "requires": {
+ "eslint-visitor-keys": "^2.0.0"
+ },
+ "dependencies": {
+ "eslint-visitor-keys": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
+ "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
+ "dev": true
+ }
+ }
+ },
+ "eslint-visitor-keys": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz",
+ "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==",
+ "dev": true
+ },
+ "espree": {
+ "version": "9.3.2",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz",
+ "integrity": "sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==",
+ "dev": true,
+ "requires": {
+ "acorn": "^8.7.1",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "dependencies": {
+ "acorn": {
+ "version": "8.7.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz",
+ "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==",
+ "dev": true
+ }
+ }
+ },
+ "esquery": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz",
+ "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==",
+ "dev": true,
+ "requires": {
+ "estraverse": "^5.1.0"
+ }
+ },
+ "esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "requires": {
+ "estraverse": "^5.2.0"
+ }
+ },
+ "estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true
+ },
+ "esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true
+ },
+ "fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true
+ },
+ "fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true
+ },
+ "fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true
+ },
+ "file-entry-cache": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+ "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+ "dev": true,
+ "requires": {
+ "flat-cache": "^3.0.4"
+ }
+ },
+ "fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dev": true,
+ "requires": {
+ "to-regex-range": "^5.0.1"
+ }
+ },
+ "find-up": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
+ "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==",
+ "dev": true,
+ "requires": {
+ "locate-path": "^2.0.0"
+ }
+ },
+ "flat-cache": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
+ "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==",
+ "dev": true,
+ "requires": {
+ "flatted": "^3.1.0",
+ "rimraf": "^3.0.2"
+ }
+ },
+ "flatted": {
+ "version": "3.2.5",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz",
+ "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==",
+ "dev": true
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true
+ },
+ "fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "optional": true
+ },
+ "function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+ "dev": true
+ },
+ "function.prototype.name": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz",
+ "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.0",
+ "functions-have-names": "^1.2.2"
+ }
+ },
+ "functional-red-black-tree": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
+ "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==",
+ "dev": true
+ },
+ "functions-have-names": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+ "dev": true
+ },
+ "get-intrinsic": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz",
+ "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==",
+ "dev": true,
+ "requires": {
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.3"
+ }
+ },
+ "get-stream": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
+ "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
+ "dev": true,
+ "requires": {
+ "pump": "^3.0.0"
+ }
+ },
+ "get-symbol-description": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
+ "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.1"
+ }
+ },
+ "glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "dev": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "requires": {
+ "is-glob": "^4.0.3"
+ }
+ },
+ "global-dirs": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz",
+ "integrity": "sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==",
+ "dev": true,
+ "requires": {
+ "ini": "2.0.0"
+ }
+ },
+ "globals": {
+ "version": "13.15.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.15.0.tgz",
+ "integrity": "sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog==",
+ "dev": true,
+ "requires": {
+ "type-fest": "^0.20.2"
+ }
+ },
+ "got": {
+ "version": "9.6.0",
+ "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz",
+ "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==",
+ "dev": true,
+ "requires": {
+ "@sindresorhus/is": "^0.14.0",
+ "@szmarczak/http-timer": "^1.1.2",
+ "cacheable-request": "^6.0.0",
+ "decompress-response": "^3.3.0",
+ "duplexer3": "^0.1.4",
+ "get-stream": "^4.1.0",
+ "lowercase-keys": "^1.0.1",
+ "mimic-response": "^1.0.1",
+ "p-cancelable": "^1.0.0",
+ "to-readable-stream": "^1.0.0",
+ "url-parse-lax": "^3.0.0"
+ }
+ },
+ "graceful-fs": {
+ "version": "4.2.10",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
+ "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==",
+ "dev": true
+ },
+ "has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "dev": true,
+ "requires": {
+ "function-bind": "^1.1.1"
+ }
+ },
+ "has-bigints": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
+ "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==",
+ "dev": true
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true
+ },
+ "has-property-descriptors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz",
+ "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==",
+ "dev": true,
+ "requires": {
+ "get-intrinsic": "^1.1.1"
+ }
+ },
+ "has-symbols": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+ "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+ "dev": true
+ },
+ "has-tostringtag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
+ "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
+ "dev": true,
+ "requires": {
+ "has-symbols": "^1.0.2"
+ }
+ },
+ "has-yarn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz",
+ "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==",
+ "dev": true
+ },
+ "http-cache-semantics": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
+ "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==",
+ "dev": true
+ },
+ "ignore": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz",
+ "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==",
+ "dev": true
+ },
+ "ignore-by-default": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
+ "dev": true
+ },
+ "import-fresh": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+ "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+ "dev": true,
+ "requires": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ }
+ },
+ "import-lazy": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz",
+ "integrity": "sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A==",
+ "dev": true
+ },
+ "imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "dev": true,
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
+ },
+ "ini": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz",
+ "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==",
+ "dev": true
+ },
+ "internal-slot": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz",
+ "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==",
+ "dev": true,
+ "requires": {
+ "get-intrinsic": "^1.1.0",
+ "has": "^1.0.3",
+ "side-channel": "^1.0.4"
+ }
+ },
+ "is-bigint": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
+ "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
+ "dev": true,
+ "requires": {
+ "has-bigints": "^1.0.1"
+ }
+ },
+ "is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "requires": {
+ "binary-extensions": "^2.0.0"
+ }
+ },
+ "is-boolean-object": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
+ "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "is-callable": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz",
+ "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==",
+ "dev": true
+ },
+ "is-ci": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz",
+ "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==",
+ "dev": true,
+ "requires": {
+ "ci-info": "^2.0.0"
+ }
+ },
+ "is-core-module": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz",
+ "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==",
+ "dev": true,
+ "requires": {
+ "has": "^1.0.3"
+ }
+ },
+ "is-date-object": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
+ "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
+ "dev": true,
+ "requires": {
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true
+ },
+ "is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "requires": {
+ "is-extglob": "^2.1.1"
+ }
+ },
+ "is-installed-globally": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz",
+ "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==",
+ "dev": true,
+ "requires": {
+ "global-dirs": "^3.0.0",
+ "is-path-inside": "^3.0.2"
+ }
+ },
+ "is-negative-zero": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
+ "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==",
+ "dev": true
+ },
+ "is-npm": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-5.0.0.tgz",
+ "integrity": "sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==",
+ "dev": true
+ },
+ "is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true
+ },
+ "is-number-object": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz",
+ "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==",
+ "dev": true,
+ "requires": {
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "is-obj": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
+ "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
+ "dev": true
+ },
+ "is-path-inside": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+ "dev": true
+ },
+ "is-regex": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
+ "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "is-shared-array-buffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz",
+ "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2"
+ }
+ },
+ "is-string": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
+ "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==",
+ "dev": true,
+ "requires": {
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "is-symbol": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
+ "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
+ "dev": true,
+ "requires": {
+ "has-symbols": "^1.0.2"
+ }
+ },
+ "is-typedarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+ "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
+ "dev": true
+ },
+ "is-weakref": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
+ "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2"
+ }
+ },
+ "is-yarn-global": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz",
+ "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==",
+ "dev": true
+ },
+ "isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true
+ },
+ "js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "requires": {
+ "argparse": "^2.0.1"
+ }
+ },
+ "json-buffer": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz",
+ "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==",
+ "dev": true
+ },
+ "json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true
+ },
+ "json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true
+ },
+ "json5": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+ "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+ "dev": true,
+ "requires": {
+ "minimist": "^1.2.0"
+ }
+ },
+ "keyv": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz",
+ "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==",
+ "dev": true,
+ "requires": {
+ "json-buffer": "3.0.0"
+ }
+ },
+ "latest-version": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz",
+ "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==",
+ "dev": true,
+ "requires": {
+ "package-json": "^6.3.0"
+ }
+ },
+ "levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "requires": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ }
+ },
+ "locate-path": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
+ "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==",
+ "dev": true,
+ "requires": {
+ "p-locate": "^2.0.0",
+ "path-exists": "^3.0.0"
+ }
+ },
+ "lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true
+ },
+ "lowercase-keys": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz",
+ "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==",
+ "dev": true
+ },
+ "lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "requires": {
+ "yallist": "^4.0.0"
+ }
+ },
+ "make-dir": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+ "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+ "dev": true,
+ "requires": {
+ "semver": "^6.0.0"
+ }
+ },
+ "mimic-response": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
+ "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
+ "dev": true
+ },
+ "minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "minimist": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
+ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
+ "dev": true
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ },
+ "natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true
+ },
+ "nodemon": {
+ "version": "2.0.16",
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.16.tgz",
+ "integrity": "sha512-zsrcaOfTWRuUzBn3P44RDliLlp263Z/76FPoHFr3cFFkOz0lTPAcIw8dCzfdVIx/t3AtDYCZRCDkoCojJqaG3w==",
+ "dev": true,
+ "requires": {
+ "chokidar": "^3.5.2",
+ "debug": "^3.2.7",
+ "ignore-by-default": "^1.0.1",
+ "minimatch": "^3.0.4",
+ "pstree.remy": "^1.1.8",
+ "semver": "^5.7.1",
+ "supports-color": "^5.5.0",
+ "touch": "^3.1.0",
+ "undefsafe": "^2.0.5",
+ "update-notifier": "^5.1.0"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true
+ },
+ "semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ }
+ }
+ },
+ "nopt": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
+ "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==",
+ "dev": true,
+ "requires": {
+ "abbrev": "1"
+ }
+ },
+ "normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true
+ },
+ "normalize-url": {
+ "version": "4.5.1",
+ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz",
+ "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==",
+ "dev": true
+ },
+ "npm-watch": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/npm-watch/-/npm-watch-0.11.0.tgz",
+ "integrity": "sha512-wAOd0moNX2kSA2FNvt8+7ORwYaJpQ1ZoWjUYdb1bBCxq4nkWuU0IiJa9VpVxrj5Ks+FGXQd62OC/Bjk0aSr+dg==",
+ "dev": true,
+ "requires": {
+ "nodemon": "^2.0.7",
+ "through2": "^4.0.2"
+ }
+ },
+ "object-inspect": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz",
+ "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==",
+ "dev": true
+ },
+ "object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "dev": true
+ },
+ "object.assign": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
+ "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.0",
+ "define-properties": "^1.1.3",
+ "has-symbols": "^1.0.1",
+ "object-keys": "^1.1.1"
+ }
+ },
+ "object.entries": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz",
+ "integrity": "sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.1"
+ }
+ },
+ "object.values": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz",
+ "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.1"
+ }
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "optionator": {
+ "version": "0.9.1",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
+ "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
+ "dev": true,
+ "requires": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.3"
+ }
+ },
+ "p-cancelable": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz",
+ "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==",
+ "dev": true
+ },
+ "p-limit": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
+ "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==",
+ "dev": true,
+ "requires": {
+ "p-try": "^1.0.0"
+ }
+ },
+ "p-locate": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
+ "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==",
+ "dev": true,
+ "requires": {
+ "p-limit": "^1.1.0"
+ }
+ },
+ "p-try": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
+ "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==",
+ "dev": true
+ },
+ "package-json": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz",
+ "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==",
+ "dev": true,
+ "requires": {
+ "got": "^9.6.0",
+ "registry-auth-token": "^4.0.0",
+ "registry-url": "^5.0.0",
+ "semver": "^6.2.0"
+ }
+ },
+ "parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "requires": {
+ "callsites": "^3.0.0"
+ }
+ },
+ "path-exists": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+ "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==",
+ "dev": true
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true
+ },
+ "path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true
+ },
+ "path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true
+ },
+ "picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true
+ },
+ "prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true
+ },
+ "prepend-http": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz",
+ "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==",
+ "dev": true
+ },
+ "pstree.remy": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
+ "dev": true
+ },
+ "pump": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
+ "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
+ "dev": true,
+ "requires": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "punycode": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+ "dev": true
+ },
+ "pupa": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz",
+ "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==",
+ "dev": true,
+ "requires": {
+ "escape-goat": "^2.0.0"
+ }
+ },
+ "rc": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+ "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+ "dev": true,
+ "requires": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ },
+ "dependencies": {
+ "ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "dev": true
+ },
+ "strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
+ "dev": true
+ }
+ }
+ },
+ "readable-stream": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+ "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+ "dev": true,
+ "requires": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ }
+ },
+ "readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "requires": {
+ "picomatch": "^2.2.1"
+ }
+ },
+ "regexp.prototype.flags": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz",
+ "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "functions-have-names": "^1.2.2"
+ }
+ },
+ "regexpp": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
+ "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==",
+ "dev": true
+ },
+ "registry-auth-token": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz",
+ "integrity": "sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw==",
+ "dev": true,
+ "requires": {
+ "rc": "^1.2.8"
+ }
+ },
+ "registry-url": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz",
+ "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==",
+ "dev": true,
+ "requires": {
+ "rc": "^1.2.8"
+ }
+ },
+ "resolve": {
+ "version": "1.22.0",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz",
+ "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==",
+ "dev": true,
+ "requires": {
+ "is-core-module": "^2.8.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ }
+ },
+ "resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true
+ },
+ "responselike": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz",
+ "integrity": "sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==",
+ "dev": true,
+ "requires": {
+ "lowercase-keys": "^1.0.0"
+ }
+ },
+ "rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "dev": true,
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true
+ },
+ "semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true
+ },
+ "semver-diff": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz",
+ "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==",
+ "dev": true,
+ "requires": {
+ "semver": "^6.3.0"
+ }
+ },
+ "shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "requires": {
+ "shebang-regex": "^3.0.0"
+ }
+ },
+ "shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true
+ },
+ "side-channel": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
+ "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.0",
+ "get-intrinsic": "^1.0.2",
+ "object-inspect": "^1.9.0"
+ }
+ },
+ "signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true
+ },
+ "string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ }
+ },
+ "string.prototype.trimend": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz",
+ "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.19.5"
+ }
+ },
+ "string.prototype.trimstart": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz",
+ "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.19.5"
+ }
+ },
+ "string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "dev": true,
+ "requires": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^5.0.1"
+ }
+ },
+ "strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+ "dev": true
+ },
+ "strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ },
+ "supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true
+ },
+ "text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+ "dev": true
+ },
+ "through2": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz",
+ "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==",
+ "dev": true,
+ "requires": {
+ "readable-stream": "3"
+ }
+ },
+ "to-readable-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz",
+ "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==",
+ "dev": true
+ },
+ "to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "requires": {
+ "is-number": "^7.0.0"
+ }
+ },
+ "touch": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz",
+ "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==",
+ "dev": true,
+ "requires": {
+ "nopt": "~1.0.10"
+ }
+ },
+ "tsconfig-paths": {
+ "version": "3.14.1",
+ "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz",
+ "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==",
+ "dev": true,
+ "requires": {
+ "@types/json5": "^0.0.29",
+ "json5": "^1.0.1",
+ "minimist": "^1.2.6",
+ "strip-bom": "^3.0.0"
+ }
+ },
+ "type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "requires": {
+ "prelude-ls": "^1.2.1"
+ }
+ },
+ "type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+ "dev": true
+ },
+ "typedarray-to-buffer": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
+ "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
+ "dev": true,
+ "requires": {
+ "is-typedarray": "^1.0.0"
+ }
+ },
+ "unbox-primitive": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
+ "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "has-bigints": "^1.0.2",
+ "has-symbols": "^1.0.3",
+ "which-boxed-primitive": "^1.0.2"
+ }
+ },
+ "undefsafe": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
+ "dev": true
+ },
+ "unique-string": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz",
+ "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==",
+ "dev": true,
+ "requires": {
+ "crypto-random-string": "^2.0.0"
+ }
+ },
+ "update-notifier": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-5.1.0.tgz",
+ "integrity": "sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw==",
+ "dev": true,
+ "requires": {
+ "boxen": "^5.0.0",
+ "chalk": "^4.1.0",
+ "configstore": "^5.0.1",
+ "has-yarn": "^2.1.0",
+ "import-lazy": "^2.1.0",
+ "is-ci": "^2.0.0",
+ "is-installed-globally": "^0.4.0",
+ "is-npm": "^5.0.0",
+ "is-yarn-global": "^0.3.0",
+ "latest-version": "^5.1.0",
+ "pupa": "^2.1.1",
+ "semver": "^7.3.4",
+ "semver-diff": "^3.1.1",
+ "xdg-basedir": "^4.0.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "7.3.7",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
+ "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
+ "dev": true,
+ "requires": {
+ "lru-cache": "^6.0.0"
+ }
+ }
+ }
+ },
+ "uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "requires": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "url-parse-lax": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
+ "integrity": "sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==",
+ "dev": true,
+ "requires": {
+ "prepend-http": "^2.0.0"
+ }
+ },
+ "util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true
+ },
+ "v8-compile-cache": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
+ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
+ "dev": true
+ },
+ "which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ },
+ "which-boxed-primitive": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
+ "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
+ "dev": true,
+ "requires": {
+ "is-bigint": "^1.0.1",
+ "is-boolean-object": "^1.1.0",
+ "is-number-object": "^1.0.4",
+ "is-string": "^1.0.5",
+ "is-symbol": "^1.0.3"
+ }
+ },
+ "widest-line": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz",
+ "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==",
+ "dev": true,
+ "requires": {
+ "string-width": "^4.0.0"
+ }
+ },
+ "word-wrap": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
+ "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
+ "dev": true
+ },
+ "wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ }
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "dev": true
+ },
+ "write-file-atomic": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz",
+ "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==",
+ "dev": true,
+ "requires": {
+ "imurmurhash": "^0.1.4",
+ "is-typedarray": "^1.0.0",
+ "signal-exit": "^3.0.2",
+ "typedarray-to-buffer": "^3.1.5"
+ }
+ },
+ "xdg-basedir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz",
+ "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==",
+ "dev": true
+ },
+ "yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true
+ }
+ }
+}
diff --git a/tests/Layout/bin/runtest.sh b/tests/Layout/bin/runtest.sh
index e06dec86b..9bac72283 100755
--- a/tests/Layout/bin/runtest.sh
+++ b/tests/Layout/bin/runtest.sh
@@ -18,24 +18,35 @@ SRCJS=$1
SRCBMP=$SRCDIR/`basename $SRCJS .js`.bmp
echo "TEST $SRCJS ($SRCBMP)"
-cat ../../modules/Layout.js > $TESTJS
-echo 'Bangle = { setUI : function(){}, appRect:{x:0,y:0,w:176,h:176,x2:175,y2:175} };BTN1=0;process.env = process.env;process.env.HWVERSION=2;' >> $TESTJS
-echo 'g = Graphics.createArrayBuffer(176,176,4);' >> $TESTJS
-cat $SRCJS >> $TESTJS || exit 1
-echo 'layout.render()' >> $TESTJS
-#echo 'layout.debug()' >> $TESTJS
-echo 'require("fs").writeFileSync("'$TESTBMP'",g.asBMP())' >> $TESTJS
+run_test () {
+ LAYOUTFILE=$1
+ echo 'exports = {};' > $TESTJS
+ cat $LAYOUTFILE >> $TESTJS
+ echo ';' >> $TESTJS
+ echo 'Layout = exports;' >> $TESTJS
+ echo 'Bangle = { setUI : function(){}, appRect:{x:0,y:0,w:176,h:176,x2:175,y2:175} };BTN1=0;process.env = process.env;process.env.HWVERSION=2;' >> $TESTJS
+ echo 'g = Graphics.createArrayBuffer(176,176,4);' >> $TESTJS
+ cat $SRCJS >> $TESTJS || exit 1
+ echo 'layout.render()' >> $TESTJS
+ #echo 'layout.debug()' >> $TESTJS
+ echo 'require("fs").writeFileSync("'$TESTBMP'",g.asBMP())' >> $TESTJS
+ echo =============================================
+ echo TESTING $LAYOUTFILE $SRCJS
+ bin/espruino $TESTJS || exit 1
+ if ! cmp $TESTBMP $SRCBMP >/dev/null 2>&1
+ then
+ echo =============================================
+ echo $LAYOUTFILE
+ echo $TESTBMP $SRCBMP differ
+ echo ==============================================
+ convert "+append" $TESTBMP $SRCBMP testresult.bmp
+ display testresult.bmp
+ exit 1
+ else
+ echo Files are the same
+ exit 0
+ fi
+}
-bin/espruino $TESTJS || exit 1
-if ! cmp $TESTBMP $SRCBMP >/dev/null 2>&1
-then
- echo =============================================
- echo $TESTBMP $SRCBMP differ
- echo ==============================================
- convert "+append" $TESTBMP $SRCBMP testresult.bmp
- display testresult.bmp
- exit 1
-else
- echo Files are the same
- exit 0
-fi
+run_test ../../modules/Layout.js
+run_test ../../modules/Layout.min.js
diff --git a/typescript/types/globals.d.ts b/typescript/types/globals.d.ts
index e82c3da3d..e2da49a0e 100644
--- a/typescript/types/globals.d.ts
+++ b/typescript/types/globals.d.ts
@@ -33,7 +33,7 @@ done "heatshrink": "readonly",
"Math": "readonly",
"Modules": "readonly",
"NRF": "readonly",
- "Number": "readonly",
+ "Number": "readonly",
"Object": "readonly",
"OneWire": "readonly",
"Pin": "readonly",
@@ -176,8 +176,9 @@ declare type GraphicsApi = {
declare const Graphics: GraphicsApi;
declare const g: GraphicsApi;
+type WidgetArea = 'tl' | 'tr' | 'bl' | 'br';
declare type Widget = {
- area: 'tr' | 'tl';
+ area: WidgetArea;
width: number;
draw: (this: { x: number; y: number }) => void;
};