Merge remote-tracking branch 'upstream/master'

pull/1007/head
hughbarney 2021-12-05 11:39:48 +00:00
commit bb81f98dc8
36 changed files with 1869 additions and 203 deletions

View File

@ -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"}
]
}
]

View File

@ -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

View File

@ -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

1
apps/gbdebug/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: New App!

26
apps/gbdebug/README.md Normal file
View File

@ -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.

1
apps/gbdebug/app-icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4cBzsE/4AClMywH680rlOW9N9kmSpICnyBBBgQRMkBUDgIRKoBoGGRYAFHBGARpARHT5MJKxQAFLgzELCIlIBQkSCIsEPRKBHCIYbGoIRFiQRJhJgFCISeEBwMQOQykCCIqlBpMEBIgRHOQYRIYQbPDhAbBNwgRJVwOCTIgRFMAJKDgQRGOQprBCIMSGogHBJwwbBkC2FCJNbUgMNwHYBYPJCIhODju0yFNCIUGCJGCoE2NwO24EAmw1FHgWCpMGgQOBBIMwCJGSpMmyAjDCI6eBCIWAhu2I4IRCUIYREk+Ah3brEB2CzFAAIRCl3b23btsNCJckjoRC1h2CyAREtoNC9oDC2isCCIgHBjdt5MtCJj2CowjD2uyCIOSCI83lu123tAQIRI4EB28/++39/0mwRCoARCgbfByU51/3rev+mWCIQwCPok0EYIRB/gRDpJ+EcYQRJkARQdgq/Bl5HE7IRDZAltwAREyXbCIbIFgEfCIXsBwQCDQAYRNLgvfCIXtCI44Dm3JCIUlYoYCGkrjBk9bxMkyy9CChICFA="))

21
apps/gbdebug/app.js Normal file
View File

@ -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));
}

BIN
apps/gbdebug/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -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)

View File

@ -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 &copy; <a href="http://openstreetmap.org/copyright">OpenStreetMap</a> contributors</a>'
});
// Tiles used for the may the user sees (faster)
var previewTileLayer = L.tileLayer(PREVIEWTILELAYER, {
maxZoom: 18,
attribution: 'Map data &copy; <a href="http://openstreetmap.org/copyright">OpenStreetMap</a> contributors</a>'
});
@ -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
})});

View File

@ -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);
};

42
apps/pooqroman/README.md Normal file
View File

@ -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).

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwkBiIAWiEAgIpKEwgrFgAaBgIcBAAwREC4oVBBoQoCAQoXJBogXqI653DC6SnEC9RHXX/6/kSgIAGU5wAICQhfGACAX/C/4AOXIIX/C/4X/C/4XUgEBF6wYHI6AYGL6MACIgXRCIISDR6QYEU6YYDX6gYCAAKxHDB4XTDAYXUL6oAgA=="))

761
apps/pooqroman/app.js Normal file
View File

@ -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();

BIN
apps/pooqroman/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

721
apps/pooqroman/resourcer.js Normal file
View File

@ -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);

View File

@ -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

View File

@ -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:
![Main menu](screenshot_main.png) ![Edit Schedule menu](screenshot_edit.png)
| Bangle.js 1 | Bangle.js 2 |
|:---------------------------------------------:|:---------------------------------------------:|
| (widget: Silent mode) | (widget: Alarms mode) |
| ![Main menu](screenshot_b1_main.png) | ![Main menu](screenshot_b2_main.png) |
| ![Edit Schedule menu](screenshot_b1_edit.png) | ![Edit Schedule menu](screenshot_b2_edit.png) |
| ![LCD Options menu](screenshot_b1_lcd.png) | ![LCD Options menu](screenshot_b2_lcd.png) |
### Widget:
![Widget, quiet mode: silent](screenshot_widget_silent.png) ![Widget, quiet mode: alarms](screenshot_widget_alarms.png)
### LCD Settings:
If set, these override the default LCD settings while Quiet Mode is active.

View File

@ -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();

View File

@ -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);

View File

@ -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();}
};
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();
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -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

View File

@ -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

View File

@ -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*)

View File

@ -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 = {

2
core

@ -1 +1 @@
Subproject commit cd3b4def869cac4d7f18e7329e640e51b26758c8
Subproject commit 23854083e0c3f83c649073a2d85e8079efc471d3