diff --git a/apps.json b/apps.json
index 8e0de6827..a312b90a3 100644
--- a/apps.json
+++ b/apps.json
@@ -16,7 +16,7 @@
{
"id": "boot",
"name": "Bootloader",
- "version": "0.36",
+ "version": "0.37",
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
"icon": "bootloader.png",
"type": "bootloader",
@@ -119,7 +119,7 @@
{
"id": "setting",
"name": "Settings",
- "version": "0.33",
+ "version": "0.34",
"description": "A menu for setting up Bangle.js",
"icon": "settings.png",
"tags": "tool,system",
@@ -269,6 +269,20 @@
],
"data": [{"name":"gbridge.json"}]
},
+ { "id": "gbdebug",
+ "name": "Gadgetbridge Debug",
+ "shortName":"GB Debug",
+ "version":"0.01",
+ "description": "Debug info for Gadgetbridge. Run this app and when Gadgetbridge messages arrive they are displayed on-screen.",
+ "icon": "app.png",
+ "tags": "",
+ "supports" : ["BANGLEJS2"],
+ "readme": "README.md",
+ "storage": [
+ {"name":"gbdebug.app.js","url":"app.js"},
+ {"name":"gbdebug.img","url":"app-icon.js","evaluate":true}
+ ]
+ },
{
"id": "mclock",
"name": "Morphing Clock",
@@ -1914,7 +1928,7 @@
"id": "openstmap",
"name": "OpenStreetMap",
"shortName": "OpenStMap",
- "version": "0.09",
+ "version": "0.10",
"description": "[BETA] Loads map tiles from OpenStreetMap onto your Bangle.js and displays a map of where you are",
"icon": "app.png",
"tags": "outdoors,gps",
@@ -3797,10 +3811,11 @@
"id": "qmsched",
"name": "Quiet Mode Schedule and Widget",
"shortName": "Quiet Mode",
- "version": "0.03",
- "description": "Automatically turn Quiet Mode on or off at set times",
+ "version": "0.04",
+ "description": "Automatically turn Quiet Mode on or off at set times, and change LCD options while Quiet Mode is active.",
"icon": "app.png",
- "screenshots": [{"url":"screenshot_edit.png"},{"url":"screenshot_main.png"},{"url":"screenshot_widget_alarms.png"},{"url":"screenshot_widget_silent.png"}],
+ "screenshots": [{"url":"screenshot_b1_main.png"},{"url":"screenshot_b1_edit.png"},{"url":"screenshot_b1_lcd.png"},
+ {"url":"screenshot_b2_main.png"},{"url":"screenshot_b2_edit.png"},{"url":"screenshot_b2_lcd.png"}],
"tags": "tool,widget",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
@@ -4641,5 +4656,24 @@
{"name":"pebble.settings.js","url":"pebble.settings.js"},
{"name":"pebble.img","url":"pebble.icon.js","evaluate":true}
]
+ },
+ { "id": "pooqroman",
+ "name": "pooq Roman watch face",
+ "shortName":"pooq Roman",
+ "version":"0.0.0",
+ "description": "A classic watch face with a certain dynamicity. Most amusing in 24h mode. Slide up to show more hands, down for less(!). By design does not support standard widgets, sorry!",
+ "icon": "app.png",
+ "type": "clock",
+ "tags": "clock",
+ "supports" : ["BANGLEJS2"],
+ "allow_emulator":true,
+ "readme": "README.md",
+ "storage": [
+ {"name":"pooqroman.app.js","url":"app.js"},
+ {"name":"pooqroman.img","url":"app-icon.js","evaluate":true}
+ ],
+ "data": [
+ {"name":"pooqroman.json"}
+ ]
}
]
diff --git a/apps/boot/ChangeLog b/apps/boot/ChangeLog
index 98f80efd9..ffc2be495 100644
--- a/apps/boot/ChangeLog
+++ b/apps/boot/ChangeLog
@@ -40,3 +40,4 @@
0.35: Add Bangle.appRect polyfill
Don't set beep vibration up on Bangle.js 2 (built in)
0.36: Add comments to .boot0 to make debugging a bit easier
+0.37: Remove Quiet Mode settings: now handled by Quiet Mode Schedule app
diff --git a/apps/boot/bootupdate.js b/apps/boot/bootupdate.js
index d642426c2..daf311fe6 100644
--- a/apps/boot/bootupdate.js
+++ b/apps/boot/bootupdate.js
@@ -78,13 +78,7 @@ boot += `E.on('errorFlag', function(errorFlags) {
if (global.save) boot += `global.save = function() { throw new Error("You can't use save() on Bangle.js without overwriting the bootloader!"); }\n`;
// Apply any settings-specific stuff
if (s.options) boot+=`Bangle.setOptions(${E.toJS(s.options)});\n`;
-if (s.quiet && s.qmOptions) boot+=`Bangle.setOptions(${E.toJS(s.qmOptions)});\n`;
-if (s.quiet && s.qmBrightness) {
- if (s.qmBrightness!=1) boot+=`Bangle.setLCDBrightness(${s.qmBrightness});\n`;
-} else {
- if (s.brightness && s.brightness!=1) boot+=`Bangle.setLCDBrightness(${s.brightness});\n`;
-}
-if (s.quiet && s.qmTimeout) boot+=`Bangle.setLCDTimeout(${s.qmTimeout});\n`;
+if (s.brightness && s.brightness!=1) boot+=`Bangle.setLCDBrightness(${s.brightness});\n`;
if (s.passkey!==undefined && s.passkey.length==6) boot+=`NRF.setSecurity({passkey:${s.passkey}, mitm:1, display:1});\n`;
if (s.whitelist) boot+=`NRF.on('connect', function(addr) { if (!(require('Storage').readJSON('setting.json',1)||{}).whitelist.includes(addr)) NRF.disconnect(); });\n`;
// Pre-2v10 firmwares without a theme/setUI
diff --git a/apps/gbdebug/ChangeLog b/apps/gbdebug/ChangeLog
new file mode 100644
index 000000000..5560f00bc
--- /dev/null
+++ b/apps/gbdebug/ChangeLog
@@ -0,0 +1 @@
+0.01: New App!
diff --git a/apps/gbdebug/README.md b/apps/gbdebug/README.md
new file mode 100644
index 000000000..47b1525b8
--- /dev/null
+++ b/apps/gbdebug/README.md
@@ -0,0 +1,26 @@
+# Gadgetbridge Debug
+
+This is useful if your Bangle isn't responding to the Gadgetbridge
+Android app properly.
+
+This app disables all existing Gadgetbridge handlers and then displays the
+messages that come from Gadgetbridge on the screen
+of the watch. It also saves the last 10 messages in a variable
+called `history`.
+
+More info on Gadgetbridge at http://www.espruino.com/Gadgetbridge
+
+## Usage
+
+* Run the `GB Debug` app on your Bangle
+* Connect your Bangle to Gadgetbridge
+* Do whatever was causing you problems (eg receiving a call)
+* The Gadgetbridge message should now be displayed on-screen
+
+If you want to get the *actual* data rather than copying it from the screen.
+
+* Ensure the `GB Debug` app is kept running after the above steps
+* Disconnect Gadgetbridge from the Bangle
+* Connect the Web IDE on your PC
+* Type `show()` on the left-hand side of the IDE and the
+last 10 messages from Gadgetbridge will be shown.
diff --git a/apps/gbdebug/app-icon.js b/apps/gbdebug/app-icon.js
new file mode 100644
index 000000000..a701ef3a9
--- /dev/null
+++ b/apps/gbdebug/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEw4cBzsE/4AClMywH680rlOW9N9kmSpICnyBBBgQRMkBUDgIRKoBoGGRYAFHBGARpARHT5MJKxQAFLgzELCIlIBQkSCIsEPRKBHCIYbGoIRFiQRJhJgFCISeEBwMQOQykCCIqlBpMEBIgRHOQYRIYQbPDhAbBNwgRJVwOCTIgRFMAJKDgQRGOQprBCIMSGogHBJwwbBkC2FCJNbUgMNwHYBYPJCIhODju0yFNCIUGCJGCoE2NwO24EAmw1FHgWCpMGgQOBBIMwCJGSpMmyAjDCI6eBCIWAhu2I4IRCUIYREk+Ah3brEB2CzFAAIRCl3b23btsNCJckjoRC1h2CyAREtoNC9oDC2isCCIgHBjdt5MtCJj2CowjD2uyCIOSCI83lu123tAQIRI4EB28/++39/0mwRCoARCgbfByU51/3rev+mWCIQwCPok0EYIRB/gRDpJ+EcYQRJkARQdgq/Bl5HE7IRDZAltwAREyXbCIbIFgEfCIXsBwQCDQAYRNLgvfCIXtCI44Dm3JCIUlYoYCGkrjBk9bxMkyy9CChICFA="))
diff --git a/apps/gbdebug/app.js b/apps/gbdebug/app.js
new file mode 100644
index 000000000..ee5e46999
--- /dev/null
+++ b/apps/gbdebug/app.js
@@ -0,0 +1,21 @@
+E.showMessage("Waiting for message");
+Bangle.loadWidgets();
+Bangle.drawWidgets();
+
+var history = [];
+
+GB = function(e) {
+ if (history.length > 10)
+ history = history.slice(history.length-10);
+ history.push(e);
+
+ var s = JSON.stringify(e,null,2);
+
+ g.reset().clear(Bangle.appRect);
+ g.setFont("6x8").setFontAlign(-1,0);
+ g.drawString(s, 10, g.getHeight()/2);
+};
+
+function show() {
+ print(JSON.stringify(history,null,2));
+}
diff --git a/apps/gbdebug/app.png b/apps/gbdebug/app.png
new file mode 100644
index 000000000..f70bce7ad
Binary files /dev/null and b/apps/gbdebug/app.png differ
diff --git a/apps/openstmap/ChangeLog b/apps/openstmap/ChangeLog
index 60b9d9ae3..69c34ed4e 100644
--- a/apps/openstmap/ChangeLog
+++ b/apps/openstmap/ChangeLog
@@ -7,3 +7,4 @@
0.07: Move to 96px tiles - less files (64 -> 25) and speed up rendering
0.08: Update for drag event refactor
0.09: Use current theme cols when drawing GPS info
+0.10: Improve scale factor calculation to fix scaling issues (#984)
diff --git a/apps/openstmap/custom.html b/apps/openstmap/custom.html
index 88d94ed37..eeb148f54 100644
--- a/apps/openstmap/custom.html
+++ b/apps/openstmap/custom.html
@@ -63,10 +63,17 @@ TODO:
/* Can see possible tiles on http://leaflet-extras.github.io/leaflet-providers/preview/
However some don't allow cross-origin use */
var TILELAYER = 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png'; // simple, high contrast
- //var TILELAYER = 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
+ var PREVIEWTILELAYER = 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
//var TILELAYER = 'http://a.tile.stamen.com/toner/{z}/{x}/{y}.png'; // black and white
+
var map = L.map('map').locate({setView: true, maxZoom: 16});
- var tileLayer = L.tileLayer(TILELAYER, {
+ // Tiles used for Bangle.js itself
+ var bangleTileLayer = L.tileLayer(TILELAYER, {
+ maxZoom: 18,
+ attribution: 'Map data © OpenStreetMap contributors'
+ });
+ // Tiles used for the may the user sees (faster)
+ var previewTileLayer = L.tileLayer(PREVIEWTILELAYER, {
maxZoom: 18,
attribution: 'Map data © OpenStreetMap contributors'
});
@@ -83,7 +90,7 @@ TODO:
}
var mapFiles = [];
- tileLayer.addTo(map);
+ previewTileLayer.addTo(map);
function tilesLoaded(ctx, width, height) {
var options = {
@@ -122,16 +129,35 @@ TODO:
}
document.getElementById("getmap").addEventListener("click", function() {
- var bounds = map.getBounds();
var zoom = map.getZoom();
- var centerlatlon = bounds.getCenter();
- var center = map.project(centerlatlon, zoom).divideBy(256);
+ var centerlatlon = map.getBounds().getCenter();
+ var center = map.project(centerlatlon, zoom).divideBy(OSMTILESIZE);
var ox = Math.round((center.x - Math.floor(center.x)) * OSMTILESIZE);
var oy = Math.round((center.y - Math.floor(center.y)) * OSMTILESIZE);
- center = center.floor();
+ center = center.floor(); // make sure we're in the middle of a tile
+ // JS version of Bangle.js's projection
+ function bproject(lat, lon) {
+ const degToRad = Math.PI / 180; // degree to radian conversion
+ const latMax = 85.0511287798; // clip latitude to sane values
+ const R = 6378137; // earth radius in m
+ if (lat > latMax) lat=latMax;
+ if (lat < -latMax) lat=-latMax;
+ var s = Math.sin(lat * degToRad);
+ return new L.Point(
+ (R * lon * degToRad),
+ (R * Math.log((1 + s) / (1 - s)) / 2)
+ );
+ }
+ // Work out scale factors (how much from Bangle.project does one pixel equate to?)
+ var pc = map.unproject(center.multiplyBy(OSMTILESIZE), zoom);
+ var pd = map.unproject(center.multiplyBy(OSMTILESIZE).add({x:1,y:0}), zoom);
+ var bc = bproject(pc.lat, pc.lng)
+ var bd = bproject(pd.lat, pd.lng)
+ var scale = bc.distanceTo(bd);
+
var tileGetters = [];
- // Render everything to a canvas - 512 x 512 px
+ // Render everything to a canvas...
var canvas = document.getElementById("maptiles");
canvas.style.display="";
var ctx = canvas.getContext('2d');
@@ -150,7 +176,8 @@ TODO:
resolve();
};
}));
- img.src = tileLayer.getTileUrl(coords);
+ bangleTileLayer._tileZoom = previewTileLayer._tileZoom;
+ img.src = bangleTileLayer.getTileUrl(coords);
})(i,j);
}
}
@@ -163,7 +190,7 @@ TODO:
imgx : canvas.width,
imgy : canvas.height,
tilesize : TILESIZE,
- scale : 10000*Math.pow(2,16-zoom), // FIXME - this is probably wrong
+ scale : scale, // how much of Bangle.project(latlon) does one pixel equate to?
lat : centerlatlon.lat,
lon : centerlatlon.lng
})});
diff --git a/apps/openstmap/openstmap.js b/apps/openstmap/openstmap.js
index 554a71ca3..d995aca25 100644
--- a/apps/openstmap/openstmap.js
+++ b/apps/openstmap/openstmap.js
@@ -34,8 +34,8 @@ exports.draw = function() {
var cx = g.getWidth()/2;
var cy = g.getHeight()/2;
var p = Bangle.project({lat:m.lat,lon:m.lon});
- var ix = (p.x-map.center.x)*4096/map.scale + (map.imgx/2) - cx;
- var iy = (map.center.y-p.y)*4096/map.scale + (map.imgy/2) - cy;
+ var ix = (p.x-map.center.x)/map.scale + (map.imgx/2) - cx;
+ var iy = (map.center.y-p.y)/map.scale + (map.imgy/2) - cy;
//console.log(ix,iy);
var tx = 0|(ix/map.tilesize);
var ty = 0|(iy/map.tilesize);
@@ -57,8 +57,8 @@ exports.latLonToXY = function(lat, lon) {
var cx = g.getWidth()/2;
var cy = g.getHeight()/2;
return {
- x : (q.x-p.x)*4096/map.scale + cx,
- y : cy - (q.y-p.y)*4096/map.scale
+ x : (q.x-p.x)/map.scale + cx,
+ y : cy - (q.y-p.y)/map.scale
};
};
@@ -66,6 +66,6 @@ exports.latLonToXY = function(lat, lon) {
exports.scroll = function(x,y) {
var a = Bangle.project({lat:this.lat,lon:this.lon});
var b = Bangle.project({lat:this.lat+1,lon:this.lon+1});
- this.lon += x * this.map.scale / ((a.x-b.x) * 4096);
- this.lat -= y * this.map.scale / ((a.y-b.y) * 4096);
+ this.lon += x * this.map.scale / (a.x-b.x);
+ this.lat -= y * this.map.scale / (a.y-b.y);
};
diff --git a/apps/pooqroman/README.md b/apps/pooqroman/README.md
new file mode 100644
index 000000000..b41a4a316
--- /dev/null
+++ b/apps/pooqroman/README.md
@@ -0,0 +1,42 @@
+# pooq Roman: a classic watch face with amusing dynamicity
+
+This is a normal watch face for telling the time.
+It is unusual in that it supports the 24 hour clock by dynamically updating the labels on the face
+(so, if you enable 24 hour mode, you will get to see a hand pointing to XXIII o'clock each evening).
+
+The date and day of the week can also be displayed, and they choose their own spelling depending on the available screen space. It's fun!
+
+## Options
+
+Because sometimes I don't want to burn what I'm cooking and other times I'm lazy and just want to know if it's afternoon yet,
+you can alter the number of hands on the display. When the watch is unlocked, slide up to add minute and second hands, or down to remove the distraction.
+There's also a setting that displays the second hand, but only if the watch is perfectly face-to-the-sky, in case you want
+the ability to check the _exact_ time, hands free, without the impact on battery life this usually entails.
+
+Although we genrally obey the system-wide theming, you can long press on the display for a menu of additional options specific to the face.
+You can also override the system 12/24 hour setting just for this face here, since it's, well, a rather different experience than with numeric displays.
+
+One other thing: there's some integration with system timers and alarms; they will show as small pips at the appropriate places
+in the day around the display. When they come within an hour, the pips turn to crosses relating to the minute hand, and the minute
+hand turns itself on. When timers are mere seconds away, the display changes again and the second hand activates itself, so you
+can watch as your doom approaches.
+
+## Limitations
+
+Since this is intended as a design exercise, it does not and will probably never support the Bangle's standard widgets.
+Sorry about that, but control of all the pixels was just too important to me.
+
+There's also no support for internationalisation at present. This irks me, but... well, talk to me about it if there's a language you'd like.
+
+## The future
+
+The design is begging for integration with host-device calendars, and proper time zone/DST support. We'll see what the future holds.
+
+## Feedback
+
+[I'd be happy to hear your feedback](https://www.github.com/stephenPspackman) if you have comments or find any bugs, or (most especially)
+if you find this work interesting.
+
+## By
+
+Made by [Stephen P Spackman](https://www.github.com/stephenPspackman).
diff --git a/apps/pooqroman/app-icon.js b/apps/pooqroman/app-icon.js
new file mode 100644
index 000000000..20a9c8b0a
--- /dev/null
+++ b/apps/pooqroman/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwkBiIAWiEAgIpKEwgrFgAaBgIcBAAwREC4oVBBoQoCAQoXJBogXqI653DC6SnEC9RHXX/6/kSgIAGU5wAICQhfGACAX/C/4AOXIIX/C/4X/C/4XUgEBF6wYHI6AYGL6MACIgXRCIISDR6QYEU6YYDX6gYCAAKxHDB4XTDAYXUL6oAgA=="))
diff --git a/apps/pooqroman/app.js b/apps/pooqroman/app.js
new file mode 100644
index 000000000..d25fcf1a8
--- /dev/null
+++ b/apps/pooqroman/app.js
@@ -0,0 +1,761 @@
+// pooqRoman
+//
+// Copyright (c) 2021 Stephen P Spackman
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// Notes:
+//
+// This only works for Bangle 2.
+
+//////////////////////////////////////////////////////////////////////////////
+/* System integration */
+
+const storage = require('Storage');
+
+const settings = storage.readJSON("setting.json", true) || {};
+
+const alarms = storage.readJSON('alarm.json', true) || [];
+
+/*
+ { on : true,
+ hr : 6.5, // hours + minutes/60
+ msg : "Eat chocolate",
+ last : 0, // last day of the month we alarmed on - so we don't alarm twice in one day!
+ rp : true, // repeat
+ as : false, // auto snooze
+ timer : 5, // OPTIONAL - if set, this is a timer and it's the time in minutes
+ }
+*/
+
+//////////////////////////////////////////////////////////////////////////////
+/* Face-specific options */
+
+class Options {
+ // Protocol: subclasses must have static id and defaults fields.
+ // Only fields named in the defaults will be saved.
+ constructor() {
+ this.id = this.constructor.id;
+ this.file = `${this.id}.json`;
+ this.backing = storage.readJSON(this.file, true) || {};
+ this.defaults = this.constructor.defaults;
+ Object.keys(this.defaults).forEach(k => this.bless(k));
+ }
+
+ writeBack(delay) {
+ if (this.timeout) clearTimeout(this.timeout);
+ this.timeout = setTimeout(
+ () => {
+ this.timeout = null;
+ storage.writeJSON(this.file, this.backing);
+ },
+ delay
+ );
+ }
+
+ bless(k) {
+ Object.defineProperty(this, k, {
+ get: () => this.backing[k] == null ? this.defaults[k] : this.backing[k],
+ set: v => {
+ this.backing[k] = v;
+ // Ten second writeback delay, since the user will roll values up and down.
+ this.writeBack(10000);
+ }
+ });
+ }
+
+ showMenu(m) {
+ if (m) {
+ for (const k in m) if ('init' in m[k]) m[k].value = m[k].init();
+ m[''].selected = -1; // Workaround for self-selection bug.
+ }
+ E.showMenu(m);
+ }
+
+ reset() {
+ this.backing = {};
+ this.writeBack(0);
+ }
+
+ interact() {this.showMenu(this.menu);}
+}
+
+class RomanOptions extends Options {
+ constructor() {
+ super();
+ this.menu = {
+ '': {title: '* face options *'},
+ '< Back': _ => {this.showMenu(); this.emit('done');},
+ Ticks: {
+ init: _ => this.resolution,
+ min: 0, max: 3,
+ onchange: x => this.resolution = x,
+ format: x => ['seconds', 'seconds (up)', 'minutes', 'hours'][x]
+ },
+ 'Display': {
+ init: _ => this.o24h == null ? 0 : 1 + this.o24h,
+ min: 0, max: 2,
+ onchange: x => this.o24h = [null, 0, 1][x],
+ format: x => ['system', '12h', '24h'][x]
+ },
+ 'Day of Week': {
+ init: _ => this.dow,
+ onchange: x => this.dow = x
+ },
+ Calendar: {
+ init: _ => this.calendric,
+ min: 0, max: 2,
+ onchange: x => this.calendric = x,
+ format: x => ['none', 'day', 'date'][x]
+ },
+ Defaults: _ => {this.reset();}
+ };
+ }
+}
+
+RomanOptions.id = 'pooqroman';
+
+RomanOptions.defaults = {
+ resolution: 1,
+ dow: true,
+ calendric: 2,
+ o24h: !settings["12hour"],
+ bg: g.theme.bg,
+ fg: g.theme.fg,
+ barBg: g.theme.fg,
+ barFg: g.theme.bg,
+ hourFg: g.theme.fg,
+ minuteFg: g.theme.fg,
+ secondFg: g.theme.fg2,
+ rectFg: g.theme.fg,
+ hubFg: g.theme.fg,
+ alarmFg: '#f00',
+ timerFg: '#0f0',
+ active: g.theme.fg2,
+};
+
+//////////////////////////////////////////////////////////////////////////////
+/* Assets (generated by resourcer.js, in this directory) */
+
+const heatshrink = require('heatshrink');
+const dec = x => E.toString(heatshrink.decompress(atob(x)));
+const romanPartsF = [
+ dec(
+ 'wEBsEB3//7//9//+0AjUAguAg3AgYQJjfAgv+gH/8Fg/0gh/AgP4gf2h/j/+BCAP' +
+ 'wgFggEggEQgEMgEHwEDEIIyDuED3kD7+H9vn2k/hEPgMP4Xevd+j4QB7kA9kAmkA' +
+ 'hUGgOH8Hn3le4+GgH32PuvfGj+CCAMDgXD4dz+evt9DgcL7fXn87h8NCAMP+Ef/0' +
+ 'eg+egPugF2j0bCAPAh3wh88h8P/8BNwI'
+ ), 97, dec('gUDgUGgUJgYFBhsBhMJhgA=='), 17
+];const fontF = [
+ dec(
+ 'AAUwAIM/4F/8HguHAmABBAoIJBBoIUBkEwsEw//wAIIdDBoUQBoIfC+HB+Hj2F/m' +
+ 'E+CIXAoHEsHMuHcmH8mHuuHH8GBGIUAwEBwEHwH/wH5+EBAIILCCAP8oH8EYXMmA' +
+ 'BB5wjCgYjCAYMP8E+uF8mHsCIWHCIgCBAIXw4fw54tBgBsBGgUAnKLC99w40wAII' +
+ 'FBBIINBCIM8gF+iHnmHDuHD8HnDYMAjizEMYJJBn+A+OAAYIHBBYKjDXYKvDYZYP' +
+ 'D40AAIYMBZYgkC4Hg4DnDuH/8H/BYIVCv/wnEAjwBCAoIJBEIYRFh0Ag8AgPAEYQ' +
+ 'RCJIJNBfYRXKnFAvlg9ihE8dwsfgkLFHMYgJF8DNCh+AUYWAA4ILBAAJGB/4PB+D' +
+ '9CgADCEoIPCJobbBB4IBBAoJdDEgXggvwhuwAIcH8EDRIh/BhkwAIMOuAPCMYQDB' +
+ 'A4ILBCIcGsECoAPLU4oPDH42ggeAB4XEg/mh1zhkzh03g/+h/4J4nwg0AhjbDRII' +
+ 'vCt/wAIIVFAoKTBCYIXBDIYHHEIYVFGJJxHSI8P/8H/6hLF44BBM4IABg8gh6NEh' +
+ 'vwgngBoITBv/Av7PBV4kAsArCfYIVBuEABYNwA4I3BD4cPL4UAM4IXBBYQfC4kP8' +
+ '0AucAmcAu8PXogA='
+ ), 32, dec('gINMgUAhMHhIAGCQ0KAQIKBgwEBgcIBAQVEhIJBhAeIBQIADAoUDEQULBQcHg4FD' +
+ 'CII='), 16
+];
+const lockI = dec('iMSwMAgfwgf8geHgeB4PA8HguFwnH//9//+4gPf//v//3gE7//9//+8EHCAO///A');
+const batteryI = dec('h8SwMAgPggfAv/4//x//j//H/+P/8f/0//gOOA==');
+const chargeI = dec('h0MwIEBkEBwEMgFwgeAj/w/+AjkA8EDgEYgFAA==');
+const GPSI = dec('iUQwMAhEAgsAgUggFEgEKvEBn0Aj+AgfgglygsJosgxNGiNIgWJ4FBEoM4gA');
+const HRMI = dec('iMRwMAnken8fzfd7v+/3/v9/38/z+b5tiiM3/eP/+D/+AAIM/wEPwEDwEAAIIA==');
+const compassI = dec('iMRwMAgfgg/8g8ng0Q40ImcOjcHg+DwfB4Ph2Hw7FsolmkUxwEwuFwj/wEIMAA==');
+
+//////////////////////////////////////////////////////////////////////////////
+/* Squeezable strings */
+
+class Formattable {
+ width(g) {return this.w != null ? this.w : (this.w = g.stringWidth(this.text));}
+ print(g, x, y) {g.drawString(this.text, x, y); return this.width();}
+}
+
+class Fixed extends Formattable {
+ constructor(text) {
+ super();
+ this.text = text;
+ }
+ squeeze() {return false;}
+}
+
+class Squeezable extends Formattable {
+ constructor(named, index) {
+ super();
+ this.named = named;
+ this.index = index;
+ this.end = index + named.forms;
+ }
+ squeeze() {
+ if (this.index >= this.end) return false;
+ this.index++;
+ this.w = null;
+ return true;
+ }
+ get text() {return this.named.table[this.index];}
+}
+
+class Named {
+ constructor(forms, table) {
+ this.forms = forms;
+ this.table = table;
+ }
+ on(index) {return new Squeezable(this, this.forms * index);}
+}
+
+//////////////////////////////////////////////////////////////////////////////
+/* Face */
+
+// Static geometry
+const barW = 26, barH = g.getHeight(), barX = g.getWidth() - barW, barY = 0;
+const faceW = g.getWidth() - barW, faceH = g.getHeight();
+const faceX = 0, faceY = 0, faceCX = faceW / 2, faceCY = faceH / 2;
+const rectX = faceX + 35, rectY = faceY + 24, rectW = 80, rectH = 128;
+
+// Extended-Roman-numeral labels
+const layout = E.toUint8Array(
+ 75, 23, // XII
+ 132, 24, // I
+ 132, 61, // II
+ 132, 97, // III
+ 132, 133, // IV
+ 132, 170, // V
+ 75, 171, // VI
+ 18, 170, // VII
+ 18, 133, // VIII
+ 18, 97, // IX
+ 18, 61, // X
+ 18, 24 // XI
+);
+
+const numeral = (n, options) => [
+ 'n', // 0
+ 'abc', // I
+ 'abdc', // II
+ 'abddc', // III
+ 'abefg', // IV
+ 'hfg', // V
+ 'hfibc', // VI
+ 'hfibdc', // VII
+ 'hfibddc', // VIII
+ 'abjk', // IX
+ 'kjk', // X
+ 'kjbc', // XI
+ 'kjbdc', // XII
+ 'kjbddc', // XIII
+ 'kjbefg', // XIV
+ 'kjefg', // XV
+ 'labc', // XVI
+ 'labdc', // XVII
+ 'labddc', // XVIII
+ 'kjbjk', // XIX
+ 'kjjk', // XX
+ 'mabc', // XXI
+ 'mabdc', // XXII
+ 'mabddc', // XXIII
+][options.o24h ? n % 24 : (n + 11) % 12 + 1];
+
+const formatMonth = new Named(4, [
+ 'January', 'Jan.', 'Jan', 'I',
+ 'February', 'Feb.', 'Feb', 'II',
+ 'March', 'Mar.', 'Mar', 'III',
+ 'April', 'Apr.', 'Apr', 'IV',
+ 'May', 'May', 'May', 'V',
+ 'June', 'June', 'Jun', 'VI',
+ 'July', 'July', 'Jul', 'VII',
+ 'August', 'Aug.', 'Aug', 'VIII', // VIII *is* narrower than Aug, our I is thin.
+ 'September', 'Sept.', 'Sep', 'IX',
+ 'October', 'Oct.', 'Oct', 'X',
+ 'November', 'Nov.', 'Nov', 'XI',
+ 'December', 'Dec.', 'Dec', 'XII'
+]);
+const formatDom = {
+ on: d => new Fixed(d.toString())
+};
+const formatDow = new Named(4, [
+ 'Sunday', 'Sun.', 'Sun', 'Su',
+ 'Monday', 'Mon.', 'Mon', 'M',
+ 'Tuesday', 'Tues.', 'Tue', 'Tu',
+ 'Wednesday', 'Weds.', 'Wed', 'W',
+ 'Thursday', 'Thurs.', 'Thu', 'Th',
+ 'Friday', 'Fri.', 'Fri', 'F',
+ 'Saturday', 'Sat.', 'Sat', 'Sa'
+]);
+
+const hceil = x => Math.ceil(x / 3600000) * 3600000;
+const hfloor = x => Math.floor(x / 3600000) * 3600000;
+const isString = x => typeof x == 'string';
+const imageWidth = i => isString(i) ? i.charCodeAt(0) : i.width;
+const imageHeight = i => isString(i) ? i.charCodeAt(1) : i.height;
+
+const events = {
+ // Items are {time: number, wall: boolean, priority: number,
+ // past: bool, future: bool, precision: number,
+ // colour: colour, dramatic?: bool, event?: any}
+ fixed: [{time: Number.POSITIVE_INFINITY}], // indexed by ms absolute
+ wall: [{time: Number.POSITIVE_INFINITY}], // indexed by nominal ms + TZ ms
+
+ clean: function(now, l) {
+ let o = now.getTimezoneOffset() * 60000;
+ let tf = now.getTime() + l, tw = tf - o;
+ // Discard stale events:
+ while (this.wall[0].time <= tw) this.wall.shift();
+ while (this.fixed[0].time <= tf) this.fixed.shift();
+ },
+
+ scan: function(now, from, to, f) {
+ result = Infinity;
+ let o = now.getTimezoneOffset() * 60000;
+ let t = now.getTime() - o;
+ let c, p, i, l = from - o, h = to - o;
+ for (i = 0; (c = this.wall[i]).time < l; i++) ;
+ for (; (c = this.wall[i]).time < h; i++) {
+ if ((p = c.time < t) ? c.past : c.future)
+ result = Math.min(result, f(c, new Date(c.time + o), p));
+ }
+ l += o; h += o; t += o;
+ for (i = 0; (c = this.fixed[i]).time < l; i++) ;
+ for (; (c = this.fixed[i]).time < h; i++) {
+ if ((p = c.time < t) ? c.past : c.future)
+ result = Math.min(f(c, new Date(c.time), p));
+ }
+ return result;
+ },
+
+ span: function(now, from, to, width) {
+ let o = now.getTimezoneOffset() * 60000;
+ let t = now.getTime() - o;
+ let lfence = [], rfence = [];
+ this.scan(now, from, to, (e, d, p) => {
+ if (p) {
+ for (let j = 0; j <= e.priority; j++) {
+ if (d < (lfence[e.priority] || t)) lfence[e.priority] = d;
+ }
+ } else {
+ for (let j = 0; j <= e.priority; j++) {
+ if (d > (rfence[e.priority] || t)) rfence[e.priority] = d;
+ }
+ }
+ });
+ for (let j = 0; ; j += 0.5) {
+ if ((rfence[Math.ceil(j)] - lfence[Math.floor(j)] || 0) <= width) {
+ return [lfence[Math.floor(j)] || now, rfence[Math.ceil(j)] || now];
+ }
+ }
+ },
+
+ insert: function(t, wall, e) {
+ let v = wall ? this.wall : this.fixed;
+ e.time = t = t - (wall ? t.getTimezoneOffset() * 60000 : 0);
+ v.splice(v.findIndex(x => x.time > t), 0, e);
+ },
+
+ loadFromSystem: function(options) {
+ alarms.forEach(x => {
+ if (x.on) {
+ const t = new Date();
+ let h = x.hr;
+ let m = h % 1 * 60;
+ let s = m % 1 * 60;
+ let ms = s % 1 * 1000;
+ t.setHours(h - h % 1, m - m % 1, s - s % 1, ms);
+ // There's a race condition here, but I'm not sure what we can do about it.
+ if (t < Date.now() || x.last === t.getDate()) t.setDate(t.getDate() + 1);
+ this.insert(t, true, {
+ priority: 0,
+ past: false, // System alarms seem uninteresting if past?
+ future: true,
+ precision: x.timer ? 1000 : 60000,
+ colour: x.timer ? options.timerFg : options.alarmFg,
+ event: x
+ });
+ }
+ });
+ return this;
+ },
+};
+
+//////////////////////////////////////////////////////////////////////////////
+/* The main face logic */
+
+class Sidebar {
+ constructor(g, x, y, w, h, options) {
+ this.g = g;
+ this.options = options;
+ this.x = x;
+ this.y = this.initY = y;
+ this.h = h;
+ this.rate = Infinity;
+ this.doLocked = Sidebar.status(_ => Bangle.isLocked(), lockI);
+ this.doHRM = Sidebar.status(_ => Bangle.isHRMOn(), HRMI);
+ this.doGPS = Sidebar.status(_ => Bangle.isGPSOn(), GPSI, Sidebar.gpsColour(options));
+ }
+ reset(rate) {this.y = this.initY; this.rate = rate; return this;}
+ print(t) {
+ this.y += 4 + t.print(
+ this.g.setColor(this.options.barFg).setFontAlign(-1, 1, 1),
+ this.x + 3, this.y + 4
+ );
+ return this;
+ }
+ pad(n) {this.y += n; return this;}
+ free() {return this.h - this.y;}
+ static status(p, i, c) {
+ return function() {
+ if (p()) {
+ this.g.setColor(c ? c() : this.options.barFg)
+ .drawImage(i, this.x + 4, this.y += 4);
+ this.y += imageHeight(i);
+ }
+ return this;
+ };
+ }
+ static gpsColour(o) {
+ const fix = Bangle.getGPSFix();
+ return fix && fix.fix ? o.active : o.barFg;
+ }
+ doPower() {
+ const c = Bangle.isCharging();
+ const b = E.getBattery();
+ if (c || b < 50) {
+ let g = this.g, x = this.x, y = this.y, options = this.options;
+ g.setColor(options.barFg).drawImage(batteryI, x + 4, y + 4);
+ g.setColor(b <= 10 ? '#f00' : b <= 30 ? '#ff0' : '#0f0');
+ const h = 13 * (100 - b) / 100;
+ g.fillRect(x + 8, y + 7 + h, x + 17, y + 20);
+ // Espruino disallows blank leading rows in icons, for some reason.
+ if (c) g.setColor(options.barBg).drawImage(chargeI, x + 4, y + 8);
+ this.y = y + imageHeight(batteryI) + 4;
+ }
+ return this;
+ }
+ doCompass() {
+ if (Bangle.isCompassOn()) {
+ const c = Bangle.getCompass();
+ const a = c && this.rate <= 1000;
+ this.g.setColor(a ? this.options.active : this.options.barFg).drawImage(
+ compassI,
+ this.x + 4 + imageWidth(compassI) / 2,
+ this.y + 4 + imageHeight(compassI) / 2,
+ a ? {rotate: c.heading / 180 * Math.PI} : undefined
+ );
+ this.y += 4 + imageHeight(compassI);
+ }
+ return this;
+ }
+}
+
+class Roman {
+ constructor(g, events) {
+ this.g = g;
+ this.state = {};
+ const options = this.options = new RomanOptions();
+ this.events = events.loadFromSystem(this.options);
+ this.timescales = [1000, [1000, 60000], 60000, 3600000];
+ this.sidebar = new Sidebar(g, barX, barY, barW, barH, options);
+ this.hours = Roman.hand(g, 3, 0.5, 12, _ => options.hourFg);
+ this.minutes = Roman.hand(g, 2, 0.9, 60, _ => options.minuteFg);
+ this.seconds = Roman.hand(g, 1, 0.9, 60, _ => options.secondFg);
+ }
+
+ reset() {this.state = {}; this.g.clear(true);}
+
+ doIcons(which) {this.state.iconsOk = null;}
+
+ // Watch hands. These could be improved, graphically.
+ // If we restricted them to 60 positions, we could feasibly hand-draw them?
+ static hand(g, w, l, d, c) {
+ return p => {
+ g.setColor(c());
+ p = ((12 * p / d) + 1) % 12;
+ let h = l * rectW / 2;
+ let v = l * rectH / 2;
+ let poly =
+ p <= 2 ? [faceCX + w, faceCY, faceCX - w, faceCY,
+ faceCX + h * (p - 1), faceCY - v,
+ faceCX + h * (p - 1) + 1, faceCY - v]
+ : p < 6 ? [faceCX + 1, faceCY + w, faceCX + 1, faceCY - w,
+ faceCX + h, faceCY + v / 2 * (p - 4),
+ faceCX + h, faceCY + v / 2 * (p - 4) + 1]
+ : p <= 8 ? [faceCX - w, faceCY + 1, faceCX + w, faceCY + 1,
+ faceCX - h * (p - 7), faceCY + v,
+ faceCX - h * (p - 7) - 1, faceCY + v]
+ : [faceCX, faceCY - w, faceCX, faceCY + w,
+ faceCX - h, faceCY - v / 2 * (p - 10),
+ faceCX - h, faceCY - v / 2 * (p - 10) - 1];
+ g.fillPoly(poly);
+ };
+ }
+
+ static pos(p, r) {
+ let h = r * rectW / 2;
+ let v = r * rectH / 2;
+ p = (p + 1) % 12;
+ return p <= 2 ? [faceCX + h * (p - 1), faceCY - v]
+ : p < 6 ? [faceCX + h, faceCY + v / 2 * (p - 4)]
+ : p <= 8 ? [faceCX - h * (p - 7), faceCY + v]
+ : [faceCX - h, faceCY - v / 2 * (p - 10)];
+ }
+
+ alert(e, date, now, past) {
+ const g = this.g;
+ g.setColor(e.colour);
+ const dt = date - now;
+ if (e.precision < 60000 && dt >= 0 && e.future && dt <= 59000) { // Seconds away
+ const p = Roman.pos(date.getSeconds() / 5, 0.95);
+ g.drawLine(faceCX, faceCY, p[0], p[1]);
+ return 1000;
+ } else if (e.precision < 3600000 && dt >= 0 && e.future && dt <= 3540000) { // Minutes away
+ const p = Roman.pos(date.getMinutes() / 5 + date.getSeconds() / 300, 0.8);
+ g.drawLine(p[0] - 5, p[1], p[0] + 5, p[1]);
+ g.drawLine(p[0], p[1] - 5, p[0], p[1] + 5);
+ return dt < 119000 ? 1000 : 60000; // Turn on second hand two minutes up.
+ } else if (e.precision < 43200000 && dt >= 0 ? e.future : e.past) { // Hours away
+ const p = Roman.pos(date.getHours() + date.getMinutes() / 60, 0.6);
+ const poly = [p[0] - 4, p[1], p[0], p[1] - 4, p[0] + 4, p[1], p[0], p[1] + 4];
+ if (date >= now) g.fillPoly(poly);
+ else g.drawPoly(poly, true);
+ return 3600000;
+ }
+ return Infinity;
+ }
+
+ render(d, rate) {
+ const g = this.g;
+ const state = this.state;
+ const options = this.options;
+ const events = this.events;
+ events.clean(d, -39600000); // 11h
+
+ // Sidebar: icons and date
+ if (d.getDate() !== state.date || !state.iconsOk) {
+ const sidebar = this.sidebar;
+ state.date = d.getDate();
+ state.iconsOk = true;
+ g.setColor(options.barBg).fillRect(barX, barY, barX + barW, barY + barH);
+
+ sidebar.reset(rate).doLocked().doPower().doGPS().doHRM().doCompass();
+ g.setFontCustom.apply(g, fontF);
+ let formatters = [];
+ let month, dom, dow;
+ if (options.calendric > 1) {
+ formatters.push(month = formatMonth.on(d.getMonth()));
+ }
+ if (options.calendric > 0) {
+ formatters.push(dom = formatDom.on(d.getDate()));
+ }
+ if (options.dow) {
+ formatters.push(dow = formatDow.on(d.getDay()));
+ }
+ // Obnoxiously inefficient iterative method :(
+ let ava = sidebar.free() - 3, use, i = 0, j = 0;
+ while ((use = formatters.reduce((l, f) => l + f.width(g) + 4, 0)) > ava &&
+ j < formatters.length)
+ for (j = 0;
+ !formatters[i++ % formatters.length].squeeze() &&
+ j < formatters.length;
+ j++) ;
+ if (dow) sidebar.print(dow);
+ sidebar.pad(ava - use);
+ if (month) sidebar.print(month);
+ if (dom) sidebar.print(dom);
+ }
+
+ // Hour labels and (purely aesthetic) box; clear inner face.
+ let keyHour = d.getHours() < 12 ? 1 : 13;
+ let alertSpan = events.span(d, hceil(d) - 39600000, hfloor(d) + 39600000, 39600000);
+ let l = alertSpan[0].getHours(), h = alertSpan[1].getHours();
+ if ((l - keyHour + 24) % 24 >= 12 || (h - keyHour + 24) % 24 >= 12) keyHour = l;
+ if (keyHour !== state.keyHour) {
+ state.keyHour = keyHour;
+ g.setColor(options.bg)
+ .fillRect(faceX, faceY, faceX + faceW, faceY + faceH)
+ .setFontCustom.apply(g, romanPartsF)
+ .setFontAlign(0, 1)
+ .setColor(options.fg);
+ // In order to deal with timezone changes more logic will be required,
+ // since the labels may be in unusual locations (even offset when
+ // a non-integral zone is involved). The value of keyHour can be
+ // anything in [hr-12, hr] mod 24.
+ for (let h = keyHour; h < keyHour + 12; h++) {
+ g.drawString(
+ numeral(h % 24, options),
+ faceX + layout[h % 12 * 2],
+ faceY + layout[h % 12 * 2 + 1]
+ );
+ }
+ g.setColor(options.rectFg)
+ .drawRect(rectX, rectY, rectX + rectW - 1, rectY + rectH - 1);
+ } else {
+ g.setColor(options.bg)
+ .fillRect(rectX + 1, rectY + 1, rectX + rectW - 2, rectY + rectH - 2)
+ .setColor(options.fg);
+ }
+
+ // Alerts
+ let requestedRate = events.scan(
+ d, hfloor(alertSpan[0] + 0), hceil(alertSpan[1] + 0) + 1,
+ (e, t, p) => this.alert(e, t, d, p)
+ );
+ if (rate > requestedRate) rate = requestedRate;
+
+ // Hands
+ // Here we are using incremental hands for hours and minutes.
+ // If we quantised, we could use hand-crafted bitmaps, though.
+ this.hours(d.getHours() + d.getMinutes() / 60);
+ if (rate < 3600000) {
+ this.minutes(d.getMinutes() + d.getSeconds() / 60);
+ }
+ if (rate < 60000) this.seconds(d.getSeconds());
+ g.setColor(options.hubFg).fillCircle(faceCX, faceCY, 3);
+ return requestedRate;
+ }
+}
+
+//////////////////////////////////////////////////////////////////////////////
+/* Master clock */
+
+class Clock {
+ constructor(face) {
+ this.face = face;
+ this.timescales = face.timescales;
+ this.options = face.options;
+ this.rates = {};
+
+ this.options.on('done', () => this.start());
+
+ this.listeners = {
+ lcdPower: on => on ? this.active() : this.inactive(),
+ charging: () => {face.doIcons('charging'); this.active();},
+ lock: () => {face.doIcons('locked'); this.active();},
+ faceUp: up => {this.conservative = !up; this.active();},
+ drag: e => {
+ if (this.t0) {
+ if (e.b) {
+ e.x > this.xN && (this.xN = e.x) || e.x > this.xX && (this.xX = e.x);
+ e.y > this.yN && (this.yN = e.y) || e.y > this.yX && (this.xY = e.y);
+ } else if (this.xX - this.xN < 20) {
+ if (e.y - this.e0.y < -50) {
+ this.options.resolution > 0 && this.options.resolution--;
+ this.rates.clock = this.timescales[this.options.resolution];
+ this.active();
+ } else if (e.y - this.e0.y > 50) {
+ this.options.resolution < this.timescales.length - 1 &&
+ this.options.resolution++;
+ this.rates.clock = this.timescales[this.options.resolution];
+ this.active();
+ } else if (this.yX - this.yN < 20 && Date.now() - this.t0 > 500) {
+ this.stop();
+ this.options.interact();
+ }
+ this.t0 = null;
+ }
+ } else if (e.b) {
+ this.t0 = Date.now(); this.e0 = e;
+ this.xN = this.xX = e.x; this.yN = this.yX = e.y;
+ }
+ }
+ };
+ }
+
+ redraw(rate) {
+ const now = this.updated = new Date();
+ if (this.refresh) this.face.reset();
+ this.refresh = false;
+ rate = this.face.render(now, rate);
+ if (rate !== this.rates.face) {
+ this.rates.face = rate;
+ this.active();
+ }
+ return this;
+ }
+
+ inactive() {
+ this.timeout && clearTimeout(this.timeout);
+ this.exception && clearTimeout(this.exception);
+ this.interval && clearInterval(this.interval);
+ this.timeout = this.exception = this.interval = this.rate = null;
+ return this;
+ }
+
+ active() {
+ const prev = this.rate;
+ const now = Date.now();
+ let rate = Infinity;
+ for (const k in this.rates) {
+ let r = this.rates[k];
+ r === +r || (r = r[+this.conservative])
+ r < rate && (rate = r);
+ }
+ const delay = rate - now % rate + 1;
+ this.refresh = true;
+
+ if (rate !== prev) {
+ this.inactive();
+ this.redraw(rate);
+ if (rate < 31622400000) { // A year!
+ this.timeout = setTimeout(
+ () => {
+ this.inactive();
+ this.interval = setInterval(() => this.redraw(rate), rate);
+ if (delay > 1000) this.redraw(rate);
+ this.rate = rate;
+ }, delay
+ );
+ }
+ } else if (rate > 1000) {
+ if (!this.exception) this.exception = setTimeout(() => {
+ this.redraw(rate);
+ this.exception = null;
+ }, this.updated + 1000 - Date.now());
+ }
+ return this;
+ }
+
+ stop() {
+ this.inactive();
+ for (const l in this.listeners) {
+ Bangle.removeListener(l, this.listeners[l]);
+ }
+ return this;
+ }
+
+ start() {
+ this.inactive(); // Reset to known state.
+ this.conservative = false;
+ this.rates.clock = this.timescales[this.options.resolution];
+ this.active();
+ for (const l in this.listeners) {
+ Bangle.on(l, this.listeners[l]);
+ }
+ Bangle.setUI('clock');
+ return this;
+ }
+}
+
+//////////////////////////////////////////////////////////////////////////////
+/* Main */
+
+const clock = new Clock(new Roman(g, events)).start();
diff --git a/apps/pooqroman/app.png b/apps/pooqroman/app.png
new file mode 100644
index 000000000..bd27186e0
Binary files /dev/null and b/apps/pooqroman/app.png differ
diff --git a/apps/pooqroman/resourcer.js b/apps/pooqroman/resourcer.js
new file mode 100644
index 000000000..69365018e
--- /dev/null
+++ b/apps/pooqroman/resourcer.js
@@ -0,0 +1,721 @@
+// pooqRoman resource maker
+//
+// Copyright (c) 2021 Stephen P Spackman
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// Notes:
+//
+//////////////////////////////////////////////////////////////////////////////
+/* ==ASSETS== */
+
+const heatshrink = require('heatshrink');
+
+const enc = x => {
+ const d = btoa(require("heatshrink").compress(x));
+ var r = "'" + d.substr(0, 64);
+ for (let i = 64; i < d.length; i += 64) r += "' +\n '" + d.substr(i, 64);
+ return r + "'";
+};
+
+const prepBitmap = (name, data) => {
+ const image = Graphics.createImage(data);
+ const raw = String.fromCharCode(image.width, image.height, 0x81, 0) + image.buffer;
+ const x = `
+const ${name}I = dec(${enc(raw)});
+`;
+ return x;
+};
+
+const prepFont = (name, data) => {
+ const image = Graphics.createImage(data);
+ const lengths = Uint8Array(256);
+ const offsets = Uint16Array(256);
+ const adjustments = Uint16Array(256);
+ let min = Infinity, max = -Infinity;
+ const lines = data.split('\n');
+ let m;
+ // This regexp is clearly suboptimal, but Espruino's regexp engine is really wonky
+ // and doesn't process nested parentheses or alternation correctly.
+ for (let i = 0; i < 5 && !(m = /^(<*)=([*\d]+)(=*)(>*)$/.exec(lines[i])); i++);
+ if (!m) throw new Error('Missing or incorrect header');
+ const desc = m[1].length, body = 1 + m[2].length + m[3].length, asc = m[4].length;
+ const h = desc + body + asc;
+ let width = m[2] == '*' ? null : +m[2];
+ let c = null, o = 0;
+ lines.forEach((line, l) => {
+ if (m = /^(<*)(=)([*\d]*)(=*)(>*)$/.exec(line) || /^(<*)(-)(.)(-*)(>*)$/.exec(line)) {
+ const h = m[2] == '=';
+ if (m[1].length > desc || h && m[1].length != desc)
+ throw new Error('Invalid descender height at ' + l);
+ if (m[2].length + m[3].length + m[4].length != body)
+ throw new Error('Invalid body height at ' + l);
+ if (m[5].length > asc || h && m[5].length != asc)
+ throw new Error('Invalid ascender height at ' + l);
+ if (c != null) {
+ lengths[c] = l - o;
+ if (width !== null && width !== lengths[c])
+ throw new Error(
+ `Character has width ${lengths[c]} != ${width} at ${offsets[c]}`
+ );
+ c = null
+ }
+ if (!h) {
+ c = m[3].charCodeAt(0);
+ if (c < min) min = c;
+ if (c > max) max = c;
+ o = l + 1;
+ offsets[c] = l;
+ adjustments[c] = m[1].length
+ }
+ }
+ });
+ const xoffs = Uint8Array(lines.length);
+ const ypos = Uint16Array(lines.length);
+ ypos.fill(0xffff);
+ const w0 = lengths[min];
+ let widths = '';
+ for (c = min, o = 0; c <= max; c++) {
+ for (i = 0, j = offsets[c]; i < lengths[c]; i++) {
+ xoffs[j] = asc + body + adjustments[c] - 1;
+ ypos[j++] = o++;
+ }
+ widths += String.fromCharCode(lengths[c]);
+ }
+ const raster = Graphics.createArrayBuffer(h, o, 1, {msb: true});
+ const writer = Graphics.createCallback(
+ image.width, image.height, 1,
+ (x, y, col) => raster.setPixel(xoffs[y] - x, ypos[y], col)
+ );
+ writer.drawImage(image);
+ if (width === null) width = `dec(${enc(widths)})`;
+ const x = `const ${name}F = [
+ dec(
+ ${enc(raster.buffer)}
+ ), ${min}, ${width}, ${h}
+];`;
+ return x;
+};
+
+res = `
+const heatshrink = require('heatshrink');
+const dec = x => E.toString(heatshrink.decompress(atob(x)));
+`;
+
+res += prepFont('romanParts', `
+<=*==============
+-a--------------
+x x
+xx xx
+-b--------------
+xxxxxxxxxxxxxxxx
+xxxxxxxxxxxxxxxx
+xxxxxxxxxxxxxxxx
+-c--------------
+xx xx
+x x
+-d--------------
+xx xx
+xx xx
+xx xx
+xxxxxxxxxxxxxxxx
+xxxxxxxxxxxxxxxx
+xxxxxxxxxxxxxxxx
+-e--------------
+xx xx
+x xxxx
+<-f--------------
+ xxxxxxxx
+ xxxxxxxxxxx
+ xxxxxxx xx
+ xxxxxx x
+xxxxx
+ xxxxxx x
+ xxxxxxx xx
+ xxxxxxxxxxx
+ xxxxxxxx
+-g--------------
+ xxxx
+ xx
+ x
+-h--------------
+ x
+ xx
+ xxxx
+-i--------------
+x xxxx
+xx xx
+-j--------------
+xx xx
+xxx xxx
+xxxx xxxx
+xxxxxx xxxxxx
+xx xxxx xxxx xx
+x xxxxxx x
+ xxxx
+x xxxxxx x
+xx xxxx xxxx xx
+xxxxxx xxxxxx
+xxxx xxxx
+xxx xxx
+xx xx
+-k--------------
+x x
+<-l--------------
+ xx x
+ xxxxxx xx
+ xxxx xxxx xxx
+ xxxx xx xxxx x
+xxx xx
+ xxxx xx xxxx x
+ xxxx xxxx xxx
+ xxxxxx xx
+ xx x
+-m--------------
+x xx x
+xx xxxx xx
+xxx xxxxxx xxx
+x xxxx xx xxxx x
+ xx xx
+x xxxx xx xxxx x
+xxx xxxxxx xxx
+xx xxxx xx
+x xx x
+-n--------------
+ xxxxxxxx
+ xxxxxxxxxxxx
+ xxxx xxxx
+xxxx xxxx
+xxx xxx
+xx xxxx xx
+xx xxxx xx
+xxx xxx
+xxxx xxxx
+ xxxx xxxx
+ xxxxxxxxxxxx
+ xxxxxxxx
+<=*==============
+`);
+
+res += prepFont('font', `
+<<<<=*======>>>>
+- ------
+
+-.------
+xx
+xx
+-0------>>>>
+ xxxxxxxx
+ xxxxxxxxxx
+xxx xxx
+xx xx
+xx xx
+xxx xxx
+ xxxxxxxxxx
+ xxxxxxxx
+
+-1------>>>>
+xx x
+xx xx
+xxxxxxxxxxxx
+xxxxxxxxxxxx
+xx
+xx
+
+-2------>>>>
+x x
+xx xx
+xxx xxx
+xxxx xx
+xxxxx xx
+xx xxx xxx
+xx xxxxxxx
+xx xxxxx
+
+-3------>>>>
+ x xx
+ xx x xx
+xxx xx xx
+xx xxx xx
+xx xxxxxx
+xxx xxx xxx
+ xxxxxx xx
+ xxx x
+
+-4------>>>>
+ x
+ xx
+ xxxx
+ xxxxxxxxx
+xxxxx xxxxx
+xxxxx
+ xx
+ xx
+
+-5------>>>>
+ x xxxxxx
+ xx xxxxxx
+xxx xx xx
+xx xx xx
+xx xx xx
+xxx xxx xx
+ xxxxxx xx
+ xxxx
+
+-6------>>>>
+ xxxx
+ xxxxxxx
+xxx xxxxx
+xx xxxxx
+xx xx xxx
+xxx xxx xx
+ xxxxxx x
+ xxxx
+
+-7------>>>>
+ xx
+ xx
+xxxx xx
+xxxxxx xx
+ xxxx xx
+ xxxxxx
+ xxxx
+ x
+
+-8------>>>>
+ xxx xxx
+ xxxxxxxxxx
+xxx xxxx xxx
+xx xx xx
+xx xx xx
+xxx xxxx xxx
+ xxxxxxxxxx
+ xxx xxx
+
+-9------>>>>
+ xxxx
+x xxxxxx
+xx xxx xxx
+xxx xx xx
+ xxxxx xx
+ xxxxx xxx
+ xxxxxxx
+ xxx
+
+-A------>>>>
+xx
+xxxxx
+ xxxxxxx
+ xxxxxxx
+ xx xxxx
+ xxxxxxx
+ xxxxxxx
+xxxxx
+xx
+
+-D------>>>>
+xx xx
+xxxxxxxxxxxx
+xxxxxxxxxxxx
+xx xx
+xx xx
+xxx xxx
+ xxxxxxxxxx
+ xxxxxxxx
+
+-F------>>>>
+xxxxxxxxxxxx
+xxxxxxxxxxxx
+ xx xx
+ xx xx
+ xx xx
+ xx
+-I------>>>>
+xxxxxxxxxxxx
+xxxxxxxxxxxx
+
+-J------>>>>
+ xx
+ xxx xx
+xxx xx
+xx xx
+xxx xx
+ xxxxxxxxxxx
+ xxxxxxxxxx
+ xx
+-M------>>>>
+xxxxxxxxxxxx
+xxxxxxxxxxx
+ xxx
+ xxxx
+ xxxx
+ xxx
+xxxxxxxxxxx
+xxxxxxxxxxxx
+
+-N------>>>>
+xxxxxxxxxxxx
+xxxxxxxxxxx
+ xxx
+ xxx
+ xxx
+ xxx
+ xxxxxxxxxxx
+xxxxxxxxxxxx
+
+-O------>>>>
+ xxxxxxxx
+ xxxxxxxxxx
+xxx xxx
+xx xx
+xx xx
+xxx xxx
+ xxxxxxxxxx
+ xxxxxxxx
+
+-S------>>>>
+ x xxx
+ xx xxxxx
+xxx xx xxx
+xx xx xx
+xx xx xx
+xxx xx xxx
+ xxxxx xx
+ xxx x
+
+-T------>>>>
+ xx
+ xx
+ xx
+xxxxxxxxxxxx
+xxxxxxxxxxxx
+ xx
+ xx
+ xx
+-V------>>>>
+ xxx
+ xxxxxx
+ xxxxx
+xxxxx
+ xxxxx
+ xxxxxx
+ xxx
+
+-W------>>>>
+ xxxx
+ xxxxxxxx
+xxxxxxxx
+ xxxx
+ xxxx
+ xxxx
+xxxxxxxx
+ xxxxxxxx
+ xxxx
+-X------>>>>
+xx xx
+xxx xxx
+ xxx xxx
+ xxxx
+ xxxx
+ xxx xxx
+xxx xxx
+xx xx
+
+-a------
+ xxx
+xxxxx x
+xx xx xx
+xx xx xx
+xx xx xx
+ xxxxxx
+xxxxxx
+
+-b------>>>>
+xxxxxxxxxxxx
+ xxxxxxxxxxx
+xx xx
+xx xx
+xxx xxx
+ xxxxxx
+ xxxx
+
+-c------
+ xxxx
+ xxxxxx
+xxx xxx
+xx xx
+xx xx
+ xx xx
+ x x
+
+-d------>>>>
+ xxxx
+ xxxxxx
+xxx xxx
+xx xx
+xx xx
+ xxxxxxxxxxx
+xxxxxxxxxxxx
+
+-e------
+ xxxx
+ xxxxxx
+xx xx xx
+xx xx xx
+xx xx xx
+ x xxxx
+ xxx
+
+<<<<-g------
+ x xxxx
+ xx xxxxxx
+xx xxx xxx
+xx xx xx
+xxx xx xxx
+ xxxxxxxxxx
+ xxxxxxxxx
+
+-h------>>>>
+xxxxxxxxxxxx
+xxxxxxxxxxxx
+ xx
+ xx
+ xxx
+xxxxxxx
+xxxxxx
+
+-i------>>>>
+xxxxxxxx xx
+xxxxxxxx xx
+
+-l------>>>>
+xxxxxxxxxxxx
+xxxxxxxxxxxx
+
+-m------
+xxxxxxxx
+xxxxxxx
+ xxx
+ xxx
+xxxxxxx
+xxxxxxx
+ xxx
+ xxx
+xxxxxxx
+xxxxxx
+
+-n------
+xxxxxxxx
+xxxxxxx
+ xxx
+ xx
+ xxx
+xxxxxxx
+xxxxxx
+
+-o------
+ xxxx
+ xxxxxx
+xxx xxx
+xx xx
+xxx xxx
+ xxxxxx
+ xxxx
+
+<<<<-p------
+xxxxxxxxxxxx
+xxxxxxxxxxx
+ xx xx
+ xx xx
+ xxx xxx
+ xxxxxx
+ xxxx
+
+-r------
+xxxxxxxx
+xxxxxxx
+ xxx
+ xx
+ xx
+ xx
+
+-s------
+ x xxx
+xx xxxxx
+xx xx xx
+xx xx xx
+xxxxx xx
+ xxx x
+
+-t------>>>>
+ xx
+ xxxxxxxxx
+ xxxxxxxxxx
+xxx xx
+xx xx
+xx xx
+ xx
+
+-u------
+ xxxxxx
+ xxxxxxx
+xxx
+xx
+xxx
+ xxxxxxx
+xxxxxxxx
+
+-v------
+ xx
+ xxxx
+ xxxx
+xxxx
+ xxxx
+ xxxx
+ xx
+
+<<<<-y------
+ x xxxxxx
+ xx xxxxxxx
+xx xxx
+xx xx
+xxx xxx
+ xxxxxxxxxxx
+ xxxxxxxxx
+
+<<<<=*======>>>>
+`);
+
+res += prepBitmap('lock', `
+ xxxxxx
+ xxxxxxxx
+ xxx xxx
+ xxx xxx
+ xxx xxx
+ xxx xxx
+ xxx xxx
+ xxxxxxxxxxxxxxxx
+ xxxxxxxxxxxxxxxx
+ xxx xxx
+ xxxxxxxxxxxxxxxx
+ xxxxxxxxxxxxxxxx
+ xxx xxx
+ xxxxxxxxxxxxxxxx
+ xxxxxxxxxxxxxxxx
+ xxx xxx
+ xxxxxxxxxxxxxxxx
+ xxxxxxxxxxxxxxxx
+`);
+
+res += prepBitmap('battery', `
+ xxxx
+ xxxx
+ xxxxxxxxxxxx
+ xxxxxxxxxxxx
+ xxxxxxxxxxxx
+ xxxxxxxxxxxx
+ xxxxxxxxxxxx
+ xxxxxxxxxxxx
+ xxxxxxxxxxxx
+ xxxxxxxxxxxx
+ xxxxxxxxxxxx
+ xxxxxxxxxxxx
+ xxxxxxxxxxxx
+ xxxxxxxxxxxx
+ xxxxxxxxxxxx
+ xxxxxxxxxxxx
+ xxxxxxxxxxxx
+ xxxxxxxxxxxx
+`);
+
+res += prepBitmap('charge', `
+ x
+ xx
+ xx
+ xxx
+ xxx
+ xxxxxxxxx
+ xxxxxxxxx
+ xxx
+ xxx
+ xx
+ xx
+ x
+`);
+
+res += prepBitmap('GPS', `
+ x
+ x x
+ x x
+ x x
+ x x xxxx
+ x xxxxx
+ xxxxxx
+ xxxxx
+ x xxx x
+ x x x x x
+ x x x x x
+ x x xx x x
+ x x x x
+ x xxx x
+ x
+ xxx
+`);
+
+res += prepBitmap('HRM', `
+ xxxx xxxx
+ xxxxxx xxxxxx
+ xx xxxx xxx xxx
+xxx xxxxxxxx xxxx
+xxx xxxxxxxx xxxx
+xxx xxxxxxxx xxxx
+xx xxxxxxx xxxx
+xx xx xxxx xx x
+ xx x x x
+ xx xxxxxxxx xxx
+ xxxxxxxxxxxxx
+ xxxxxxxxxxx
+ xxxxxxxxx
+ xxxxxxx
+ xxxxx
+ xxx
+ x
+`);
+
+res += prepBitmap('compass', `
+ xxxxx
+ xxxxxxxxx
+ xxx x xxx
+ xx x xx
+ xx x xx
+ xx xxx xx
+xx xxx xx
+xx xxx xx
+xx xxx xx
+xx xx xx xx
+xx xx xx xx
+ xx x x xx
+ xx x x xx
+ xx xx
+ xxx xxx
+ xxxxxxxxx
+ xxxxx
+`);
+
+print(res);
diff --git a/apps/qmsched/ChangeLog b/apps/qmsched/ChangeLog
index 27b5421e8..0b8d67e76 100644
--- a/apps/qmsched/ChangeLog
+++ b/apps/qmsched/ChangeLog
@@ -1,3 +1,4 @@
0.01: First version
0.02: Add widget
0.03: Bangle.js 2 support
+0.04: Move Quiet Mode LCD options from global settings to this app
diff --git a/apps/qmsched/README.md b/apps/qmsched/README.md
index 033014789..535ae56e4 100644
--- a/apps/qmsched/README.md
+++ b/apps/qmsched/README.md
@@ -1,9 +1,14 @@
# Quiet Mode Schedule and Widget
-Automatically turn Quiet Mode on or off at set times, and display a widget when enabled.
+Automatically turn Quiet Mode on or off at set times, and display a widget when Quiet Mode is active.
-### Edit Schedule:
- 
+| Bangle.js 1 | Bangle.js 2 |
+|:---------------------------------------------:|:---------------------------------------------:|
+| (widget: Silent mode) | (widget: Alarms mode) |
+|  |  |
+|  |  |
+|  |  |
-### Widget:
- 
+### LCD Settings:
+
+If set, these override the default LCD settings while Quiet Mode is active.
\ No newline at end of file
diff --git a/apps/qmsched/app.js b/apps/qmsched/app.js
index c6377d4ba..7be3339fb 100644
--- a/apps/qmsched/app.js
+++ b/apps/qmsched/app.js
@@ -2,27 +2,74 @@ Bangle.loadWidgets();
Bangle.drawWidgets();
const modeNames = ["Off", "Alarms", "Silent"];
-let scheds = require("Storage").readJSON("qmsched.json", 1);
-/*scheds = [
- { hr : 6.5, // hours + minutes/60
- mode : 1, // quiet mode (0/1/2)
- }
-];*/
-if (!scheds) {
- // set default schedule on first load of app
- scheds = [
- {"hr": 8, "mode": 0},
- {"hr": 22, "mode": 1},
- ];
- require("Storage").writeJSON("qmsched.json", scheds);
+
+// load global brightness setting
+let bSettings = require('Storage').readJSON('setting.json',true)||{};
+let current = 0|bSettings.quiet;
+delete bSettings; // we don't need any other global settings
+
+
+
+
+
+
+/**
+ * Save settings to qmsched.json
+ */
+function save() {
+ require('Storage').writeJSON('qmsched.json', settings);
}
-if (scheds.length && scheds.some(s => "last" in s)) {
- // cleanup: remove "last" values (used by old versions)
- scheds = scheds.map(s => {
- delete s.last;
- return s;
- });
- require("Storage").writeJSON("qmsched.json", scheds);
+function get(key, def) {
+ return (key in settings) ? settings[key] : def;
+}
+function set(key, val) {
+ settings[key] = val; save();
+ scheds = settings.scheds; options = settings.options; // update references
+}
+function unset(key) {
+ delete settings[key]; save();
+}
+
+let settings,
+ scheds, options; // references for convenience
+/**
+ * Load settings file, check if we need to migrate old setting formats to new
+ */
+function loadSettings() {
+ settings = require('Storage').readJSON("qmsched.json", true) || {};
+
+ if (Array.isArray(settings)) {
+ // migrate old file (plain array of schedules, qmOptions stored in global settings file)
+ require("Storage").erase("qmsched.json"); // need to erase old file, or Things Break, somehow...
+ let bOptions = require('Storage').readJSON('setting.json',true)||{};
+ settings = {
+ options: bOptions.qmOptions || {},
+ scheds: settings,
+ };
+ // store new format
+ save();
+ // and clean up qmOptions from global settings file
+ delete bOptions.qmOptions;
+ require('Storage').writeJSON('setting.json',bOptions);
+ }
+ // apply defaults
+ settings = Object.assign({
+ options: {}, // Bangle options to override during quiet mode, default = none
+ scheds: [
+ // default schedule:
+ {"hr": 8, "mode": 0},
+ {"hr": 22, "mode": 1},
+ ],
+ }, settings);
+ scheds = settings.scheds; options = settings.options;
+
+ if (scheds.length && scheds.some(s => "last" in s)) {
+ // cleanup: remove "last" values (used by older versions)
+ set('scheds', scheds.map(s => {
+ delete s.last;
+ return s;
+ }));
+ }
}
function formatTime(t) {
@@ -32,29 +79,35 @@ function formatTime(t) {
}
function showMainMenu() {
- let menu = {"": {"title": "Quiet Mode"}};
+ let _m, menu = {
+ "": {"title": "Quiet Mode"},
+ "< Exit": () => load()
+ };
// "Current Mode""Silent" won't fit on Bangle.js 2
- menu["Current" + ((process.env.HWVERSION===2)?"":" Mode")]= {
- value: (require("Storage").readJSON("setting.json", 1) || {}).quiet|0,
+ menu["Current"+((process.env.HWVERSION===2) ? "" : " Mode")] = {
+ value: current,
format: v => modeNames[v],
onchange: function(v) {
if (v<0) {v = 2;}
if (v>2) {v = 0;}
require("qmsched").setMode(v);
+ current = v;
this.value = v;
},
};
scheds.sort((a, b) => (a.hr-b.hr));
scheds.forEach((sched, idx) => {
- const name = modeNames[sched.mode];
- const txt = formatTime(sched.hr)+" ".repeat(14-name.length)+name;
- menu[txt] = function() {
- showEditMenu(idx);
+ menu[formatTime(sched.hr)] = {
+ format: () => modeNames[sched.mode], // abuse format to right-align text
+ onchange: function() {
+ _m.draw = ()=> {}; // prevent redraw of main menu over edit menu
+ showEditMenu(idx);
+ }
};
});
menu["Add Schedule"] = () => showEditMenu(-1);
- menu["< Back"] = () => {load();};
- return E.showMenu(menu);
+ menu["LCD Settings"] = () => showOptionsMenu();
+ _m = E.showMenu(menu);
}
function showEditMenu(index) {
@@ -69,6 +122,7 @@ function showEditMenu(index) {
}
const menu = {
"": {"title": (isNew ? "Add" : "Edit")+" Schedule"},
+ "< Cancel": () => showMainMenu(),
"Hours": {
value: hrs,
onchange: function(v) {
@@ -110,18 +164,88 @@ function showEditMenu(index) {
} else {
scheds[index] = getSched();
}
- require("Storage").writeJSON("qmsched.json", scheds);
+ save();
showMainMenu();
};
if (!isNew) {
menu["> Delete"] = function() {
scheds.splice(index, 1);
- require("Storage").writeJSON("qmsched.json", scheds);
+ save();
showMainMenu();
};
}
- menu["< Cancel"] = showMainMenu;
return E.showMenu(menu);
}
+function showOptionsMenu() {
+ const disabledFormat = v => v ? "Off" : "-";
+ function toggle(option) {
+ // we disable wakeOn* events by setting them to `false` in options
+ // not disabled = not present in options at all
+ if (option in options) {
+ delete options[option];
+ } else {
+ options[option] = false;
+ }
+ save();
+ }
+ let resetTimeout;
+ const oMenu = {
+ "": {"title": "LCD Settings"},
+ "< Back": () => showMainMenu(),
+ "LCD Brightness": {
+ value: get("brightness", 0),
+ min: 0, // 0 = use default
+ max: 1,
+ step: 0.1,
+ format: v => (v>0.05) ? v : "-",
+ onchange: v => {
+ if (v>0.05) { // prevent v=0.000000000000001 bugs
+ set("brightness", v);
+ Bangle.setLCDBrightness(v); // show result, even if not quiet right now
+ // restore brightness after half a second
+ if (resetTimeout) clearTimeout(resetTimeout);
+ resetTimeout = setTimeout(() => {
+ resetTimeout = undefined;
+ require("qmsched").setMode(current);
+ }, 500);
+ } else {
+ unset("brightness");
+ require("qmsched").setMode(current);
+ }
+ },
+ },
+ "LCD Timeout": {
+ value: get("timeout", 0),
+ min: 0, // 0 = use default (no constant on for quiet mode)
+ max: 60,
+ step: 5,
+ format: v => v>1 ? v : "-",
+ onchange: v => {
+ if (v>1) set("timeout", v);
+ else unset("timeout");
+ },
+ },
+ // we disable wakeOn* events by overwriting them as false in options
+ // not disabled = not present in options at all
+ "Wake on FaceUp": {
+ value: "wakeOnFaceUp" in options,
+ format: disabledFormat,
+ onchange: () => {toggle("wakeOnFaceUp");},
+ },
+ "Wake on Touch": {
+ value: "wakeOnTouch" in options,
+ format: disabledFormat,
+ onchange: () => {toggle("wakeOnTouch");},
+ },
+ "Wake on Twist": {
+ value: "wakeOnTwist" in options,
+ format: disabledFormat,
+ onchange: () => {toggle("wakeOnTwist");},
+ },
+ };
+ return E.showMenu(oMenu);
+}
+
+loadSettings();
showMainMenu();
diff --git a/apps/qmsched/boot.js b/apps/qmsched/boot.js
index 2712cab30..c3bc49b58 100644
--- a/apps/qmsched/boot.js
+++ b/apps/qmsched/boot.js
@@ -1,7 +1,13 @@
// apply Quiet Mode schedules
(function qm() {
- let scheds = require("Storage").readJSON("qmsched.json", 1) || [];
- if (!scheds.length) { return;}
+ let bSettings = require('Storage').readJSON('setting.json',true)||{};
+ const curr = 0|bSettings.quiet;
+ delete bSettings;
+ if (curr) require("qmsched").applyOptions(curr); // no need to re-apply default options
+
+ let settings = require('Storage').readJSON('qmsched.json',true)||{};
+ let scheds = settings.scheds||[];
+ if (!scheds.length) {return;}
const now = new Date(),
hr = now.getHours()+(now.getMinutes()/60)+(now.getSeconds()/3600); // current (decimal) hour
scheds.sort((a, b) => a.hr-b.hr);
diff --git a/apps/qmsched/lib.js b/apps/qmsched/lib.js
index a3d36ed34..9b307769a 100644
--- a/apps/qmsched/lib.js
+++ b/apps/qmsched/lib.js
@@ -1,18 +1,23 @@
+/**
+ * Apply LCD options for given mode
+ * @param {int} mode Quiet Mode
+ */
+exports.applyOptions = function(mode) {
+ const s = require("Storage").readJSON(mode ? "qmsched.json" : "setting.json", 1) || {};
+ const get = (k, d) => k in s ? s[k] : d;
+ Bangle.setOptions(get("options", {}));
+ Bangle.setLCDBrightness(get("brightness", 1));
+ Bangle.setLCDTimeout(get("timeout", 10));
+};
/**
* Set new Quiet Mode and apply Bangle options
* @param {int} mode Quiet Mode
*/
exports.setMode = function(mode) {
- let s = require("Storage").readJSON("setting.json", 1) || {};
- s.quiet = mode;
- require("Storage").writeJSON("setting.json", s);
- if (s.options) Bangle.setOptions(s.options);
- if (mode && s.qmOptions) Bangle.setOptions(s.qmOptions);
- if (mode && s.qmBrightness) {
- if (s.qmBrightness!=1) Bangle.setLCDBrightness(s.qmBrightness);
- } else {
- if (s.brightness && s.brightness!=1) Bangle.setLCDBrightness(s.brightness);
- }
- if (mode && s.qmTimeout) Bangle.setLCDTimeout(s.qmTimeout);
- if (typeof (WIDGETS)!=="undefined" && "qmsched" in WIDGETS) {WIDGETS["qmsched"].draw();}
-};
\ No newline at end of file
+ require("Storage").writeJSON("setting.json", Object.assign(
+ require("Storage").readJSON("setting.json", 1) || {},
+ {quiet:mode}
+ ));
+ exports.applyOptions(mode);
+ if (WIDGETS && "qmsched" in WIDGETS) WIDGETS["qmsched"].draw();
+};
diff --git a/apps/qmsched/screenshot_b1_edit.png b/apps/qmsched/screenshot_b1_edit.png
new file mode 100644
index 000000000..ec82e92e6
Binary files /dev/null and b/apps/qmsched/screenshot_b1_edit.png differ
diff --git a/apps/qmsched/screenshot_b1_lcd.png b/apps/qmsched/screenshot_b1_lcd.png
new file mode 100644
index 000000000..16f9356b8
Binary files /dev/null and b/apps/qmsched/screenshot_b1_lcd.png differ
diff --git a/apps/qmsched/screenshot_b1_main.png b/apps/qmsched/screenshot_b1_main.png
new file mode 100644
index 000000000..803ca69d5
Binary files /dev/null and b/apps/qmsched/screenshot_b1_main.png differ
diff --git a/apps/qmsched/screenshot_b2_edit.png b/apps/qmsched/screenshot_b2_edit.png
new file mode 100644
index 000000000..d26ff02cb
Binary files /dev/null and b/apps/qmsched/screenshot_b2_edit.png differ
diff --git a/apps/qmsched/screenshot_b2_lcd.png b/apps/qmsched/screenshot_b2_lcd.png
new file mode 100644
index 000000000..3f06488c3
Binary files /dev/null and b/apps/qmsched/screenshot_b2_lcd.png differ
diff --git a/apps/qmsched/screenshot_b2_main.png b/apps/qmsched/screenshot_b2_main.png
new file mode 100644
index 000000000..f6d22a8b8
Binary files /dev/null and b/apps/qmsched/screenshot_b2_main.png differ
diff --git a/apps/qmsched/screenshot_edit.png b/apps/qmsched/screenshot_edit.png
deleted file mode 100644
index 88b7fcad4..000000000
Binary files a/apps/qmsched/screenshot_edit.png and /dev/null differ
diff --git a/apps/qmsched/screenshot_main.png b/apps/qmsched/screenshot_main.png
deleted file mode 100644
index 634abd633..000000000
Binary files a/apps/qmsched/screenshot_main.png and /dev/null differ
diff --git a/apps/qmsched/screenshot_widget_alarms.png b/apps/qmsched/screenshot_widget_alarms.png
deleted file mode 100644
index 52dbe2464..000000000
Binary files a/apps/qmsched/screenshot_widget_alarms.png and /dev/null differ
diff --git a/apps/qmsched/screenshot_widget_silent.png b/apps/qmsched/screenshot_widget_silent.png
deleted file mode 100644
index 38b133650..000000000
Binary files a/apps/qmsched/screenshot_widget_silent.png and /dev/null differ
diff --git a/apps/qmsched/widget.js b/apps/qmsched/widget.js
index c602288ad..8a8333ba5 100644
--- a/apps/qmsched/widget.js
+++ b/apps/qmsched/widget.js
@@ -16,9 +16,9 @@ WIDGETS["qmsched"] = {
}
let x = this.x, y = this.y;
g.clearRect(x, y, x+23, y+23);
- // quiet mode: draw dim red one-way-street sign
+ // quiet mode: draw red one-way-street sign (dim red on Bangle.js 1)
x = this.x+11;y = this.y+11; // center of widget
- g.setColor(0.8, 0, 0).fillCircle(x, y, 8);
+ g.setColor(process.env.HWVERSION===2 ? 1 : 0.8, 0, 0).fillCircle(x, y, 8);
g.setColor(g.theme.bg).fillRect(x-6, y-2, x+6, y+2);
if (mode>1) {return;} // no alarms
// alarms still on: draw alarm icon in bottom-right corner
diff --git a/apps/setting/ChangeLog b/apps/setting/ChangeLog
index faa50405f..d840654fe 100644
--- a/apps/setting/ChangeLog
+++ b/apps/setting/ChangeLog
@@ -36,3 +36,4 @@
0.31: Remove Bangle 1 settings when running on Bangle 2
0.32: Fix 'beep' menu on Bangle.js 2
0.33: Really fix 'beep' menu on Bangle.js 2 this time
+0.34: Remove Quiet Mode LCD settings: now handled by Quiet Mode Schedule app
diff --git a/apps/setting/README.md b/apps/setting/README.md
index 1875fc3b0..fb567030f 100644
--- a/apps/setting/README.md
+++ b/apps/setting/README.md
@@ -44,6 +44,4 @@ The exact effects depend on the app. In general the watch will not wake up by i
- Off: Normal operation
- Alarms: Stops notifications, but "alarm" apps will still work
- Silent: Blocks even alarms
-* **LCD Brightness**, **LCD Timeout**, **Wake on X**:
- Override default settings while Quit Mode is active (either as *Alarms* or *Silent*)
\ No newline at end of file
diff --git a/apps/setting/settings.js b/apps/setting/settings.js
index fcf651b6f..9432d0a38 100644
--- a/apps/setting/settings.js
+++ b/apps/setting/settings.js
@@ -7,17 +7,12 @@ let settings;
function updateSettings() {
//storage.erase('setting.json'); // - not needed, just causes extra writes if settings were the same
- if (Object.keys(settings.qmOptions).length === 0) delete settings.qmOptions;
storage.write('setting.json', settings);
- if (!('qmOptions' in settings)) settings.qmOptions = {}; // easier if this always exists in this file
}
function updateOptions() {
updateSettings();
Bangle.setOptions(settings.options)
- if (settings.quiet) {
- Bangle.setOptions(settings.qmOptions)
- }
}
function gToInternal(g) {
@@ -56,18 +51,12 @@ function resetSettings() {
twistMaxY: -800,
twistTimeout: 1000
},
- // Quiet Mode options:
- // we only set these if we want to override the default value
- // qmOptions: {},
- // qmBrightness: undefined,
- // qmTimeout: undefined,
};
updateSettings();
}
settings = storage.readJSON('setting.json', 1);
if (!settings) resetSettings();
-if (!('qmOptions' in settings)) settings.qmOptions = {}; // easier if this always exists in here
const boolFormat = v => v ? "On" : "Off";
@@ -130,7 +119,16 @@ function showMainMenu() {
}
}
},
- "Quiet Mode": ()=>showQuietModeMenu(),
+ "Quiet Mode": {
+ value: settings.quiet|0,
+ format: v => ["Off", "Alarms", "Silent"][v%3],
+ onchange: v => {
+ settings.quiet = v%3;
+ updateSettings();
+ updateOptions();
+ if ("qmsched" in WIDGETS) WIDGETS["qmsched"].draw();
+ },
+ },
'Locale': ()=>showLocaleMenu(),
'Select Clock': ()=>showClockMenu(),
'Set Time': ()=>showSetTimeMenu(),
@@ -352,9 +350,7 @@ function showLCDMenu() {
onchange: v => {
settings.brightness = v || 1;
updateSettings();
- if (!(settings.quiet && "qmBrightness" in settings)) {
- Bangle.setLCDBrightness(settings.brightness);
- }
+ Bangle.setLCDBrightness(settings.brightness);
}
},
'LCD Timeout': {
@@ -365,9 +361,7 @@ function showLCDMenu() {
onchange: v => {
settings.timeout = 0 | v;
updateSettings();
- if (!(settings.quiet && "qmTimeout" in settings)) {
- Bangle.setLCDTimeout(settings.timeout);
- }
+ Bangle.setLCDTimeout(settings.timeout);
}
},
'Wake on BTN1': {
@@ -455,105 +449,6 @@ function showLCDMenu() {
});
return E.showMenu(lcdMenu)
}
-function showQuietModeMenu() {
- // we always keep settings.quiet and settings.qmOptions
- // other qm values are deleted when not set
- const modes = ["Off", "Alarms", "Silent"];
- const qmDisabledFormat = v => v ? "Off" : "-";
- const qmMenu = {
- "": {"title": "Quiet Mode"},
- "< Back": () => showMainMenu(),
- "Quiet Mode": {
- value: settings.quiet|0,
- format: v => modes[v%3],
- onchange: v => {
- settings.quiet = v%3;
- updateSettings();
- updateOptions();
- if ("qmsched" in WIDGETS) {WIDGETS["qmsched"].draw();}
- },
- },
- "LCD Brightness": {
- value: settings.qmBrightness || 0,
- min: 0, // 0 = use default
- max: 1,
- step: 0.1,
- format: v => (v>0.05) ? v : "-",
- onchange: v => {
- if (v>0.05) { // prevent v=0.000000000000001 bugs
- settings.qmBrightness = v;
- } else {
- delete settings.qmBrightness;
- }
- updateSettings();
- if (settings.qmBrightness) { // show result, even if not quiet right now
- Bangle.setLCDBrightness(v);
- } else {
- Bangle.setLCDBrightness(settings.brightness);
- }
- },
- },
- "LCD Timeout": {
- value: settings.qmTimeout || 0,
- min: 0, // 0 = use default (no constant on for quiet mode)
- max: 60,
- step: 5,
- format: v => v>1 ? v : "-",
- onchange: v => {
- if (v>1) {
- settings.qmTimeout = v;
- } else {
- delete settings.qmTimeout;
- }
- updateSettings();
- if (settings.quiet && v>1) {
- Bangle.setLCDTimeout(v);
- } else {
- Bangle.setLCDTimeout(settings.timeout);
- }
- },
- },
- // we disable wakeOn* events by overwriting them as false in qmOptions
- // not disabled = not present in qmOptions at all
- "Wake on FaceUp": {
- value: "wakeOnFaceUp" in settings.qmOptions,
- format: qmDisabledFormat,
- onchange: () => {
- if ("wakeOnFaceUp" in settings.qmOptions) {
- delete settings.qmOptions.wakeOnFaceUp;
- } else {
- settings.qmOptions.wakeOnFaceUp = false;
- }
- updateOptions();
- },
- },
- "Wake on Touch": {
- value: "wakeOnTouch" in settings.qmOptions,
- format: qmDisabledFormat,
- onchange: () => {
- if ("wakeOnTouch" in settings.qmOptions) {
- delete settings.qmOptions.wakeOnTouch;
- } else {
- settings.qmOptions.wakeOnTouch = false;
- }
- updateOptions();
- },
- },
- "Wake on Twist": {
- value: "wakeOnTwist" in settings.qmOptions,
- format: qmDisabledFormat,
- onchange: () => {
- if ("wakeOnTwist" in settings.qmOptions) {
- delete settings.qmOptions.wakeOnTwist;
- } else {
- settings.qmOptions.wakeOnTwist = false;
- }
- updateOptions();
- },
- },
- };
- return E.showMenu(qmMenu);
-}
function showLocaleMenu() {
const localemenu = {
diff --git a/core b/core
index cd3b4def8..23854083e 160000
--- a/core
+++ b/core
@@ -1 +1 @@
-Subproject commit cd3b4def869cac4d7f18e7329e640e51b26758c8
+Subproject commit 23854083e0c3f83c649073a2d85e8079efc471d3