Merge branch 'espruino:master' into master
55
apps.json
|
@ -99,7 +99,7 @@
|
|||
"id": "android",
|
||||
"name": "Android Integration",
|
||||
"shortName": "Android",
|
||||
"version": "0.04",
|
||||
"version": "0.05",
|
||||
"description": "Display notifications/music/etc from Gadgetbridge on Android. This replaces the old Gadgetbridge widget.",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,system,messages,notifications",
|
||||
|
@ -2168,14 +2168,15 @@
|
|||
{ "id": "snek",
|
||||
"name": "The snek game",
|
||||
"shortName":"Snek",
|
||||
"version": "0.01",
|
||||
"version": "0.02",
|
||||
"description": "A snek game where you control a snek to eat all the apples!",
|
||||
"icon": "snek-icon.js",
|
||||
"screenshots": [{"url":"screenshot_snek.png"}],
|
||||
"icon": "snek.png",
|
||||
"supports": ["BANGLEJS2"],
|
||||
"tags": "game,fun",
|
||||
"storage": [
|
||||
{"name":"snek.app.js","url":"snek.js"},
|
||||
{"name":"snek.img","url":"snek-icon.js","evaluate":true}
|
||||
{"name":"snek.img","url":"snek.icon.js","evaluate":true}
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -3295,7 +3296,7 @@
|
|||
{
|
||||
"id": "dtlaunch",
|
||||
"name": "Desktop Launcher",
|
||||
"version": "0.05",
|
||||
"version": "0.07",
|
||||
"description": "Desktop style App Launcher with six (four for Bangle 2) apps per page - fast access if you have lots of apps installed.",
|
||||
"screenshots": [{"url":"shot1.png"},{"url":"shot2.png"},{"url":"shot3.png"}],
|
||||
"icon": "icon.png",
|
||||
|
@ -3306,8 +3307,11 @@
|
|||
"storage": [
|
||||
{"name":"dtlaunch.app.js","url":"app-b1.js", "supports": ["BANGLEJS"]},
|
||||
{"name":"dtlaunch.app.js","url":"app-b2.js", "supports": ["BANGLEJS2"]},
|
||||
{"name":"dtlaunch.settings.js","url":"settings-b1.js", "supports": ["BANGLEJS"]},
|
||||
{"name":"dtlaunch.settings.js","url":"settings-b2.js", "supports": ["BANGLEJS2"]},
|
||||
{"name":"dtlaunch.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
],
|
||||
"data": [{"name":"dtlaunch.json"}]
|
||||
},
|
||||
{
|
||||
"id": "HRV",
|
||||
|
@ -4735,7 +4739,7 @@
|
|||
{ "id": "pooqroman",
|
||||
"name": "pooq Roman watch face",
|
||||
"shortName":"pooq Roman",
|
||||
"version":"0.02",
|
||||
"version":"0.03",
|
||||
"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",
|
||||
|
@ -4871,7 +4875,7 @@
|
|||
"id": "rebble",
|
||||
"name": "Rebble Clock",
|
||||
"shortName": "Rebble",
|
||||
"version": "0.01",
|
||||
"version": "0.02",
|
||||
"description": "A Pebble style clock, with configurable background, three sidebars including steps, day, date, sunrise, sunset, long live the rebellion",
|
||||
"readme": "README.md",
|
||||
"icon": "rebble.png",
|
||||
|
@ -4932,5 +4936,40 @@
|
|||
{"name":"awairmonitor.app.js","url":"app.js"},
|
||||
{"name":"awairmonitor.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
},
|
||||
{ "id": "pooqround",
|
||||
"name": "pooq Round watch face",
|
||||
"shortName":"pooq Round",
|
||||
"version":"0.00",
|
||||
"description": "A 24 hour analogue watchface with high legibility and a novel style.",
|
||||
"icon": "app.png",
|
||||
"type": "clock",
|
||||
"tags": "clock",
|
||||
"supports" : ["BANGLEJS2"],
|
||||
"allow_emulator":true,
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name":"pooqround.app.js","url":"app.js"},
|
||||
{"name":"pooqround.img","url":"app-icon.js","evaluate":true}
|
||||
],
|
||||
"data": [
|
||||
{"name":"pooqround.json"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "coretemp",
|
||||
"name": "Core Temp Display",
|
||||
"version": "0.01",
|
||||
"description": "Display CoreTemp device sensor data",
|
||||
"icon": "coretemp.png",
|
||||
"type": "app",
|
||||
"tags": "health",
|
||||
"readme": "README.md",
|
||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||
"storage": [
|
||||
{"name":"coretemp.boot.js","url":"boot.js"},
|
||||
{"name":"coretemp.app.js","url":"coretemp.js"},
|
||||
{"name":"coretemp.img","url":"coretemp-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -3,3 +3,4 @@
|
|||
Fix music control
|
||||
0.03: Handling of message actions (ok/clear)
|
||||
0.04: Android icon now goes to settings page with 'find phone'
|
||||
0.05: Fix handling of message actions
|
||||
|
|
|
@ -65,7 +65,7 @@
|
|||
// Message response
|
||||
Bangle.messageResponse = (msg,response) => {
|
||||
if (msg.id=="call") return gbSend({ t: "call", n:response?"ACCEPT":"REJECT" });
|
||||
if (isFinite(msg.id)) return gbSend({ t: "notify", n:response?"OPEN":"DISMISS" });
|
||||
if (isFinite(msg.id)) return gbSend({ t: "notify", n:response?"OPEN":"DISMISS", id: msg.id });
|
||||
// error/warn here?
|
||||
};
|
||||
})();
|
||||
|
|
|
@ -14,6 +14,8 @@ Displays the level of CO2, VOC, PM 2.5, Humidity and Temperature, from your Awai
|
|||
|
||||

|
||||
|
||||

|
||||
|
||||
## Creator
|
||||
[@alainsaas](https://github.com/alainsaas)
|
||||
|
||||
|
|
After Width: | Height: | Size: 146 KiB |
|
@ -0,0 +1 @@
|
|||
0.1: New app
|
|
@ -0,0 +1,20 @@
|
|||
# CoreTemp display
|
||||
|
||||
Basic bare-bones example of connecting to a bluetooth [CoreTemp](https://corebodytemp.com/) device and displaying the current body core temperature readings.
|
||||
|
||||
## Usage
|
||||
|
||||
On startup connects to a CoreTemp device (1809/2A1C) and emits a "Core, temp" value for each reading.
|
||||
The app simply displays these readings on screen.
|
||||
|
||||
## TODO
|
||||
|
||||
* Integrate with other tracking/sports apps to log data.
|
||||
* Add device selection
|
||||
* Provide enable/disable option
|
||||
* Check status, add Retry/reconnect
|
||||
* Also provide skin temp reading
|
||||
|
||||
## Creator
|
||||
|
||||
Ivor Hewitt
|
|
@ -0,0 +1,23 @@
|
|||
(function() {
|
||||
var gatt;
|
||||
|
||||
//Would it be better to scan by uuid rather than name?
|
||||
NRF.requestDevice({ timeout: 20000, filters: [{ name: 'CORE [a]' }] }).then(function(device) {
|
||||
return device.gatt.connect();
|
||||
}).then(function(g) {
|
||||
gatt = g;
|
||||
return gatt.getPrimaryService("1809");
|
||||
}).then(function(service) {
|
||||
return service.getCharacteristic("2A1C");
|
||||
}).then(function(characteristic) {
|
||||
characteristic.on('characteristicvaluechanged', function(event) {
|
||||
var dv = event.target.value;
|
||||
var core = (dv.buffer[2]*256+dv.buffer[1])/100;
|
||||
Bangle.emit('Core',{
|
||||
temp:core
|
||||
});
|
||||
});
|
||||
return characteristic.startNotifications();
|
||||
}).then(function() {
|
||||
});
|
||||
})();
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEw4UA///k0DxUFgsDCY8KwAfJlQLHhWglWq1WgBIcCA4QCB1WoComq0+iBYWqCwl//4OBAAQxChWlv/2BYIlCBYUqv9VvQLBwA9BBYWlqtV/QLBGoRIBgQLBr9aBYQ2BBYMKroLBtQLCgALClIKC1AXG1NVuoFBF4sC09V+woCBAJHCgWXq9oPQZrDgWdq9gBZG9rqgCTwSbCgVVqysDBYkK6tWYoa/DkEJ6vaaIgWBaAILCbQhUCBYXoc4wNBBZWqBfBtB1ALKKZILCR4J3FToQLBU4KPEWoQLNZYILIa4NVcYReEcYOnqtaDAbvDgALBcg4EBlNVqtqDoOgd4YoBBYNWytWCwQdCgQLBAAVaBYkA0oLDuwLFkv1BgZGDAAMJuoKCroWEGAOnDAVftShGr////1tDdG14LB+wiEAAdqHAjTHBYgA=="))
|
|
@ -0,0 +1,19 @@
|
|||
Bangle.setLCDPower(1);
|
||||
Bangle.setLCDTimeout(0);
|
||||
var btm = g.getHeight()-1;
|
||||
|
||||
function onCore(c) {
|
||||
var px = g.getWidth()/2;
|
||||
g.setFontAlign(0,0);
|
||||
g.clearRect(0,24,g.getWidth(),80);
|
||||
var str = c.temp + "C";
|
||||
g.setFontVector(40).drawString(str,px,45);
|
||||
}
|
||||
Bangle.on('Core', onCore);
|
||||
|
||||
g.clear();
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
|
||||
g.reset().setFont("6x8",2).setFontAlign(0,0);
|
||||
g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2 - 16);
|
After Width: | Height: | Size: 4.8 KiB |
|
@ -3,3 +3,5 @@
|
|||
0.03: cycle thru pages
|
||||
0.04: reset to clock after 2 mins of inactivity
|
||||
0.05: add Bangle 2 version
|
||||
0.06: Adds settings page (hide clocks or launchers)
|
||||
0.06: Adds setting for directly launching app on touch for Bangle 2
|
||||
|
|
|
@ -2,6 +2,11 @@
|
|||
*
|
||||
*/
|
||||
|
||||
var settings = Object.assign({
|
||||
showClocks: true,
|
||||
showLaunchers: true,
|
||||
}, require('Storage').readJSON("dtlaunch.json", true) || {});
|
||||
|
||||
function wdog(handle,timeout){
|
||||
if(handle !== undefined){
|
||||
wdog.handle = handle;
|
||||
|
@ -17,7 +22,13 @@ function wdog(handle,timeout){
|
|||
wdog(load,120000)
|
||||
|
||||
var s = require("Storage");
|
||||
var apps = s.list(/\.info$/).map(app=>{var a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src};}).filter(app=>app && (app.type=="app" || app.type=="clock" || !app.type));
|
||||
var apps = s.list(/\.info$/).map(app=>{
|
||||
var a=s.readJSON(app,1);
|
||||
return a && {
|
||||
name:a.name, type:a.type, icon:a.icon, sortorder:a.sortorder, src:a.src
|
||||
};}).filter(
|
||||
app=>app && (app.type=="app" || (app.type=="clock" && settings.showClocks) || (app.type=="launch" && settings.showLaunchers) || !app.type));
|
||||
|
||||
apps.sort((a,b)=>{
|
||||
var n=(0|a.sortorder)-(0|b.sortorder);
|
||||
if (n) return n; // do sortorder first
|
||||
|
|
|
@ -2,8 +2,20 @@
|
|||
*
|
||||
*/
|
||||
|
||||
var settings = Object.assign({
|
||||
showClocks: true,
|
||||
showLaunchers: true,
|
||||
direct: false,
|
||||
}, require('Storage').readJSON("dtlaunch.json", true) || {});
|
||||
|
||||
var s = require("Storage");
|
||||
var apps = s.list(/\.info$/).map(app=>{var a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src};}).filter(app=>app && (app.type=="app" || app.type=="clock" || !app.type));
|
||||
var apps = s.list(/\.info$/).map(app=>{
|
||||
var a=s.readJSON(app,1);
|
||||
return a && {
|
||||
name:a.name, type:a.type, icon:a.icon, sortorder:a.sortorder, src:a.src
|
||||
};}).filter(
|
||||
app=>app && (app.type=="app" || (app.type=="clock" && settings.showClocks) || (app.type=="launch" && settings.showLaunchers) || !app.type));
|
||||
|
||||
apps.sort((a,b)=>{
|
||||
var n=(0|a.sortorder)-(0|b.sortorder);
|
||||
if (n) return n; // do sortorder first
|
||||
|
@ -28,7 +40,7 @@ const YOFF = 30;
|
|||
function draw_icon(p,n,selected) {
|
||||
var x = (n%2)*72+XOFF;
|
||||
var y = n>1?72+YOFF:YOFF;
|
||||
(selected?g.setColor(g.theme.fgH):g.setColor(g.theme.bg)).fillRect(x+10,y+2,x+60,y+52);
|
||||
(selected?g.setColor(g.theme.fgH):g.setColor(g.theme.bg)).fillRect(x+11,y+3,x+60,y+52);
|
||||
g.clearRect(x+12,y+4,x+59,y+51);
|
||||
g.setColor(g.theme.fg);
|
||||
try{g.drawImage(apps[p*4+n].icon,x+12,y+4);} catch(e){}
|
||||
|
@ -52,7 +64,7 @@ function drawPage(p){
|
|||
}
|
||||
for (var i=0;i<4;i++) {
|
||||
if (!apps[p*4+i]) return i;
|
||||
draw_icon(p,i,selected==i);
|
||||
draw_icon(p,i,selected==i && !settings.direct);
|
||||
}
|
||||
g.flip();
|
||||
}
|
||||
|
@ -81,9 +93,9 @@ Bangle.on("touch",(_,p)=>{
|
|||
for (i=0;i<4;i++){
|
||||
if((page*4+i)<Napps){
|
||||
if (isTouched(p,i)) {
|
||||
draw_icon(page,i,true);
|
||||
if (selected>=0) {
|
||||
if (selected!=i){
|
||||
draw_icon(page,i,true && !settings.direct);
|
||||
if (selected>=0 || settings.direct) {
|
||||
if (selected!=i && !settings.direct){
|
||||
draw_icon(page,selected,false);
|
||||
} else {
|
||||
load(apps[page*4+i].src);
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
(function(back) {
|
||||
var FILE = "dtlaunch.json";
|
||||
|
||||
var settings = Object.assign({
|
||||
showClocks: true,
|
||||
showLaunchers: true
|
||||
}, require('Storage').readJSON(FILE, true) || {});
|
||||
|
||||
function writeSettings() {
|
||||
require('Storage').writeJSON(FILE, settings);
|
||||
}
|
||||
|
||||
E.showMenu({
|
||||
"" : { "title" : "Desktop launcher" },
|
||||
"< Back" : () => back(),
|
||||
'Show clocks': {
|
||||
value: settings.showClocks,
|
||||
format: v => v?"On":"Off",
|
||||
onchange: v => {
|
||||
settings.showClocks = v;
|
||||
writeSettings();
|
||||
}
|
||||
},
|
||||
'Show launchers': {
|
||||
value: settings.showLaunchers,
|
||||
format: v => v?"On":"Off",
|
||||
onchange: v => {
|
||||
settings.showLaunchers = v;
|
||||
writeSettings();
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
|
@ -0,0 +1,42 @@
|
|||
(function(back) {
|
||||
var FILE = "dtlaunch.json";
|
||||
|
||||
var settings = Object.assign({
|
||||
showClocks: true,
|
||||
showLaunchers: true,
|
||||
direct: false
|
||||
}, require('Storage').readJSON(FILE, true) || {});
|
||||
|
||||
function writeSettings() {
|
||||
require('Storage').writeJSON(FILE, settings);
|
||||
}
|
||||
|
||||
E.showMenu({
|
||||
"" : { "title" : "Desktop launcher" },
|
||||
"< Back" : () => back(),
|
||||
'Show clocks': {
|
||||
value: settings.showClocks,
|
||||
format: v => v?"On":"Off",
|
||||
onchange: v => {
|
||||
settings.showClocks = v;
|
||||
writeSettings();
|
||||
}
|
||||
},
|
||||
'Show launchers': {
|
||||
value: settings.showLaunchers,
|
||||
format: v => v?"On":"Off",
|
||||
onchange: v => {
|
||||
settings.showLaunchers = v;
|
||||
writeSettings();
|
||||
}
|
||||
},
|
||||
'Direct launch': {
|
||||
value: settings.direct,
|
||||
format: v => v?"On":"Off",
|
||||
onchange: v => {
|
||||
settings.direct = v;
|
||||
writeSettings();
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
|
@ -2,3 +2,4 @@
|
|||
0.02: included deployment of pebble.settings.js in apps.json
|
||||
0.03: Changed time+calendar font to LECO1976Regular, changed to slanting boot
|
||||
0.04: Fix widget hiding code (fix #1046)
|
||||
0.05: Fix typo in settings - Purple
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
storage.write(SETTINGS_FILE, settings)
|
||||
}
|
||||
|
||||
var color_options = ['Green','Orange','Cyan','Perple','Red','Blue'];
|
||||
var color_options = ['Green','Orange','Cyan','Purple','Red','Blue'];
|
||||
var bg_code = ['#0f0','#ff0','#0ff','#f0f','#f00','#00f'];
|
||||
|
||||
E.showMenu({
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
0.01: New App!
|
||||
0.02: Make internal menu time out + small fixes
|
||||
0.01: Initial check-in.
|
||||
0.02: Make internal menu time out + small fixes.
|
||||
0.03: Autolight feature.
|
||||
|
|
|
@ -13,9 +13,12 @@ you can alter the number of hands on the display. When the watch is unlocked, sl
|
|||
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.
|
||||
Although we generally 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.
|
||||
|
||||
By default, there is a backlight that comes on when you twist your wrist. This, of course, somewhat increases power draw and could be
|
||||
annoying in an intentionally dark environment, so there is an option to disable it.
|
||||
|
||||
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
|
||||
|
|
|
@ -138,6 +138,10 @@ class RomanOptions extends Options {
|
|||
onchange: x => this.calendric = x,
|
||||
format: x => ['none', 'day', 'date'][x]
|
||||
},
|
||||
'Auto-Illum.': {
|
||||
init: _ => this.autolight,
|
||||
onchange: x => this.autolight = x
|
||||
},
|
||||
Defaults: _ => {this.reset(); this.interact();}
|
||||
};
|
||||
}
|
||||
|
@ -164,6 +168,7 @@ RomanOptions.defaults = {
|
|||
alarmFg: '#f00',
|
||||
timerFg: '#0f0',
|
||||
activeFg: g.theme.fg2,
|
||||
autolight: true,
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -663,10 +668,10 @@ class Clock {
|
|||
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();},
|
||||
charging: _ => {face.doIcons('charging'); this.active();},
|
||||
lock: _ => {face.doIcons('locked'); this.active();},
|
||||
faceUp: up => {this.conservative = !up; this.active();},
|
||||
twist: _ => this.options.autolight && Bangle.setLCDPower(true),
|
||||
drag: e => {
|
||||
if (this.t0) {
|
||||
if (e.b) {
|
||||
|
@ -728,7 +733,6 @@ class Clock {
|
|||
}
|
||||
const delay = rate - now % rate + 1;
|
||||
this.refresh = true;
|
||||
|
||||
if (rate !== prev) {
|
||||
this.inactive();
|
||||
this.redraw(rate);
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.00: Initial check-in.
|
|
@ -0,0 +1,39 @@
|
|||
# pooq Round: a maximally readable, naturally 24hr, analogue watch face
|
||||
|
||||
This is a normal watch face for telling the time.
|
||||
It is unusual in that it uses a pie chart for the hour hand. This is much easier and
|
||||
more precise to read at a glance than a conventional hand, and as a bonus can distinguish
|
||||
midnight (all black) from noon (all white).
|
||||
|
||||
The day and date are optionally displayed, typographically smooshed into the corners.
|
||||
Either you'll like that, or you won't.
|
||||
|
||||
## Options
|
||||
|
||||
Because sometimes I don't want to burn what I'm cooking and others 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 dots representing the minute and second,
|
||||
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 generally obey the system-wide theming, you can long press on the display for a menu of additional options specific to the face.
|
||||
We don't observe the system 12/24 setting, since it the design of the face is equally good in either interpretation.
|
||||
|
||||
By default, there is a backlight that comes on when you twist your wrist. This, of course, somewhat increases power draw and could be
|
||||
annoying in an intentionally dark environment, so there is an option to disable it.
|
||||
|
||||
## 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 since every month and day name is hand-drawn,
|
||||
there's no fix other than hard work. Talk to me about it if there's a language you'd like.
|
||||
|
||||
## 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).
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwkB/4AW+ABCgAJEE4IXMh8ADAMPCoYXUK4gXMAAJHCN4oSG+I+CC4gEBC5gNBO4wuGC44IBGASPEC5ovHIox3JL4hdIR5xdIC54uIC5wWIC5hGKC5pGJC5QKCC6YKDCxIXIBQTCBC6IKDC6QKEC6IKFh52KC4gLHC5wLIC5oLJC5gLKC5YALC/4XfQZYAKh4X/C5B4V/4XYJChGBC7JIT/4wVh/wGCouBC6vwI4hIQagQWDDB5dBC45JNEwIXHGBhFDLwgYNCQQXKDBCgEC5QZFB4oGBA4IA="))
|
|
@ -0,0 +1,600 @@
|
|||
/* -*- mode: Javascript; c-basic-offset: 2; indent-tabs-mode: nil; coding: latin-1 -*- */
|
||||
// pooqRound
|
||||
|
||||
// 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.
|
||||
|
||||
const isString = x => typeof x === 'string';
|
||||
const imageWidth = i => isString(i) ? i.charCodeAt(0) : i.width;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
/* System integration */
|
||||
|
||||
const storage = require('Storage');
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
/* 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) || {};
|
||||
Object.setPrototypeOf(this.backing, this.constructor.defaults);
|
||||
this.reactivator = _ => this.active();
|
||||
Object.keys(this.constructor.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],
|
||||
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 instanceof Function) m = 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.
|
||||
Bangle.on('drag', this.reactivator);
|
||||
this.active();
|
||||
} else {
|
||||
if (this.bored) clearTimeout(this.bored);
|
||||
this.bored = null;
|
||||
Bangle.removeListener('drag', this.reactivator);
|
||||
this.emit('done');
|
||||
}
|
||||
g.clear(true);
|
||||
E.showMenu(m);
|
||||
}
|
||||
|
||||
active() {
|
||||
if (this.bored) clearTimeout(this.bored);
|
||||
this.bored = setTimeout(_ => this.showMenu(), 15000);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.backing = {__proto__: this.constructor.defaults};
|
||||
this.writeBack(0);
|
||||
}
|
||||
}
|
||||
|
||||
class RoundOptions extends Options {
|
||||
constructor() {
|
||||
super();
|
||||
this.menu = () => ({
|
||||
'': {title: '* face options *'},
|
||||
'< Back': _ => this.showMenu(),
|
||||
Ticks: {
|
||||
init: _ => this.resolution,
|
||||
min: 0, max: 3,
|
||||
onchange: x => this.resolution = x,
|
||||
format: x => ['seconds', 'seconds (up)', 'minutes', 'hours'][x]
|
||||
},
|
||||
Calendar: {
|
||||
init: _ => this.calendric,
|
||||
min: 0, max: 5,
|
||||
onchange: x => this.calendric = x,
|
||||
format: x => ['none', 'day', 'date', 'both', 'month', 'full'][x],
|
||||
},
|
||||
'Auto-Illum.': {
|
||||
init: _ => this.autolight,
|
||||
onchange: x => this.autolight = x
|
||||
},
|
||||
Defaults: _ => {this.reset(); this.interact();}
|
||||
});
|
||||
}
|
||||
|
||||
interact() {this.showMenu(this.menu);}
|
||||
}
|
||||
|
||||
RoundOptions.id = 'pooqround';
|
||||
|
||||
RoundOptions.defaults = {
|
||||
resolution: 1,
|
||||
calendric: 5,
|
||||
dayFg: '#fff',
|
||||
nightFg: '#000',
|
||||
autolight: true,
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
/* Assets (generated by resourcer.js, in this directory) */
|
||||
|
||||
const heatshrink = require('heatshrink');
|
||||
const dec = x => E.toString(heatshrink.decompress(atob(x)));
|
||||
const y10F = [
|
||||
dec(
|
||||
'g///EAh////AA4IIBgPwgE+gAOBg/AngXB+EPAYM8gfggEfgF8D4OAj4dB8EDAYI' +
|
||||
'fBBAISBAAMOAYUB4AECnEAkAuBgEQBAPgIYX8IYX/wYDCEwIiMMgUYgECCIZlBAY' +
|
||||
'N4CoRUBIoMP8AZBge8CgMB8+BCAPw+F/gf8jxDB/0D4BGBEQMPAYIeBoAfBnEwge' +
|
||||
'Ah0cB4MDx4PBgHn4EB8E7LQM8h/eJ4MDBgIpB+H+g/wnE/WwMMO4P8LwM/XAJLBT' +
|
||||
'gY7BAAN/wC9CQwV+jwDB/4pBgP/EQKYBBIIxBPQP+SATfCIYIiCO4I9BBwM//hlB' +
|
||||
'PQJlCwYGBTAPgIgM4CYM8hwKBMoODegPA8F+gZlBewP4hz/BE4QrBGgM/LAV//4+' +
|
||||
'BAYJyBPwM/KQMeGQMPFwM8H4UHBIPwGQNwn4yBnhxBGQJxBGQK5BGQKWDOwUACAM' +
|
||||
'D/BDCNYPg///8E5HwR2BIwMDSgK0FSocMAYTLBAAYpBQAPnDwJGBEwK+B/hlB+F8' +
|
||||
'TARABTAJABTAPBMoR+BMoKXBDoX5DwIuBMoUPS4THCGwJbBhAaBvh5B+EHwPAOwP' +
|
||||
'guA1BvCcB4E8nxlBn1/VoIyBwDKBO4SGCgA='
|
||||
), 48, dec('hgAI'), 34
|
||||
];const y1F = [
|
||||
dec(
|
||||
'g//AAPggE/AoX8gF/AoX+gF8CoU+gHwAoUPgAZBEIQFGCIodFFIo1FIIoADnAFEj' +
|
||||
'gFEh0AhA1EiAFCgeAFIf/4A1DFQIED/5MDGB6OEjAECHIIYDhkAuAFCjwFEj6DEn' +
|
||||
'+AAod74AFD/PgvAtC+Hwv/wgZSBvEfLwc8RISOBGAJsBVAXgggEBE4PgIgJLC8E8' +
|
||||
'I4fgXQS/B8IhBGwOA8YFCgfA9+eAoMB4H/j/ACIPA/kPCQJCB/DMDMoMBboYVBKo' +
|
||||
'IDBSYeAAoYlCAATpEg/4Xwc/QIcPFoJcBQIP8GILXCDYLXBbId//BeCL4QwDgIwD' +
|
||||
'AAIXBDAQfCEYSPBAoaPCPQKPCAoZgBAoYvBAoIXBBAIFB/ALDEoJHBAoaPDaQSPB' +
|
||||
'AoKcBJgY9DTQX/EoKmCC4SyCYYJJB+CHBj+Aj8ASYJNBBINwIIOAM4ILDAYN/wAB' +
|
||||
'BB4JBBI45vCRYgADApEHL4pHB8AECFIPhAYLCCAggFBAgaNCYwgFEbAkAwAFEc4S' +
|
||||
'PCj/+LIKPBv6PEAoRnBFIMDFYLXCKoTLDa4YRDBYIdDh4FDMoQ1DK4ZBBMQIDBJY' +
|
||||
'bWBFIMEIIQpBgxxBgZRBh8AAYN8AoQVBjgbBAoTZBvwRCvEBF4IdB+E/OIp9CJgZ' +
|
||||
'BCQQUAA='
|
||||
), 48, dec('hgAI'), 48
|
||||
];const y10sF = [
|
||||
dec(
|
||||
'j/+gP//0PgE8mEAmHwgfBBQINB8AWDgcAoEGAYMMj///H///wBwNgAQPAAQMgg8B' +
|
||||
'wE+hkA9kwg8Y+F4mP/4Fg/AVD4EBgcCg0MnEMmfgmH94PD4f+hkHIIgbBg44B/ng' +
|
||||
'h/H/H8n4IBg4QBhwUC//Bgf+FYMwAIPAjHDwPjg//gEPLgUAOYMAn/+DAM8j1gmH' +
|
||||
'h8fDBAMIHIRwDQAJtBg/8mH+gHPwEDCII/DAAM+n8B/v+h0+jkwuEw8fhV4UD8Yr' +
|
||||
'DjxDB/0Ch88CoLEB+fPwK0BKIOACoQA='
|
||||
), 48, dec('hAAI'), 22
|
||||
];const y1sF = [
|
||||
dec(
|
||||
'j///0A/4ABgfAgEPgwNBg0MAYMMjwDBvAWB//gh4DBEAUDgEgAYQeBgcDEwQSCCY' +
|
||||
'oDCiACBwFgGoOBwEAnODBwPhw/Ag+Bw/gv0Bwf/+EBwAkBgPgCYOA4EQgIeB8ASB' +
|
||||
'g/AgcGnuAg0N8fAnkfIwPwnEB/40BgE8IYX8AYN/7hDB/kcg4xBv4TBC4kcLgUcv' +
|
||||
'4ZBIgJIBHoNgHoJ8BgOGKQMHhijBnkYHoQlEv4DBRYWAv+eOgPwmEDg4mBXIXwni' +
|
||||
'SBDwRICSwIABWIM/HoM//57BEoMGv7dC/DrCLoU4eYfAv4kB8f/wPB98HLgP4TQM' +
|
||||
'B+EGh0PvE8QwN/+EP8E/LAK6CBIMAwPg+EDDwNgh8GJQP8h8Hz/gN4P+gBMBJIMA'
|
||||
), 48, dec('hEHhAAGA'), 31
|
||||
];const d10F = [
|
||||
dec(
|
||||
'AAXgjEAjkHgEDwPAgFwvEAh0f///44CB/ICB/4aDAQMcAQMDwAhBuAhBj0B4EH4E' +
|
||||
'wgP4h0Av4JBj3gnEHzkHgPjwF4/Fwh/+CQP/HwMD4E4gJLCvAuBj0ADgOGg+B8fA' +
|
||||
'uF5FoMeDQPH/l4vP8g/+vg4BzkAg/gA='
|
||||
), 49, dec('hcMhYA=='), 27
|
||||
];const d1F = [
|
||||
dec(
|
||||
'AB1/+AECj///4FCAgP/8EAgf/4F//EAg4CBgf8gEPwAUBn0AhwaCAYMeAoUPgEcA' +
|
||||
'oUHAowRFDoopFGopBFJopZGBgIKCABlAIIcA4AFDgIFEgZBCAoMHAohVBAoY6CHg' +
|
||||
'U/Aol/AogADGoQFUABEMAQM/AQN8bIRZBRgJ5BLILhBgP3LIcD84rDg/HWYcPw4F' +
|
||||
'Dj4PBAoU+Aol8Aon4PocB+CJDgfgAoXgh/ATYX4v+AU4X//w/DbYQFCCwJ3PvDIE' +
|
||||
'NYQCCdoJ6CgfAiCGCI4NwgEeFwISCLoMeJwJdCnkfHYd4v4FD+f5AoUB9/BAoUD/' +
|
||||
'4jCh8HG4IpCh5DBAIMeE4Q/BvjMCfoP8Z4Uf//wCgInB/5lCABs+AoicBAAUDAok' +
|
||||
'P9wFDv+OCAjUCHQP4AoY5BAoUHEIIFCv5JBAoLQBLQYqEApQpDArIAJv5IBnBTCV' +
|
||||
'4McJAQFBcYLvBB4IkBd4N4cYQBBeoLdBCYIFDngFECoIFDOwIdCc4QpCFwIZCjwu' +
|
||||
'BEoU8FwIxCvAIBEIPB+AUBJIP/8AmBLYWAd4RnBdx4XCcYf/Dgn//AuEP4LjBXoJ' +
|
||||
'AC//vQYT0BBIKDC+CZBOIM/wAFDVYIFCgIrBAoUDPoIdCO4QnBaQYnBGoQVBIIZI' +
|
||||
'CJoTNCLIY4CAYIaDAAKRCAASRDAAIaEYAQtDYAI5DRgZFCAAYuCQoQuBAgIFBvEH' +
|
||||
'AgIFB+CgBAAMB86lE76EBFwX/GocPNoYmBIwk/HQl8LpIAQRId/SoYDB4ZJCUoPn' +
|
||||
'VoUHwP3Y4YYBY4k+Y4h5BdILhBd4YFFCIodFFIo1FIIpNFLIplGAArMFn6oBHYMA' +
|
||||
'DYQFBgP5E4IFBgfgUgIFCwBZBEAL1BPYZbDA4Z7DLYRtCBYYlDBoIxCEYMBHoIvC' +
|
||||
'HAI7Dh5PBI4X/LIX//7+Dn52Eh4QCA=='
|
||||
), 48, dec('ikPigAGA'), 48
|
||||
];const dowF = [
|
||||
dec(
|
||||
'gf8AYNwgEP/4FBvEAj//wEAnkAn0H4EAjwNBgPgAoQZBAoMOgHwAongCIQFDDoIF' +
|
||||
'FDoPggYFBF4IFBGoI7B+AFCE4NwCIIlCuAdBIYU4gPwn5VBjEA//+M4d//AFDh4W' +
|
||||
'BB4IgBAAX/B4n/PoQACJQIcEAokHAqAXFEYhLF/6tCApIADn4ED/zFBAAX8gaGBA' +
|
||||
'AZZFQIR2GdQQYRBYgXFEYoWRKQQWCLoRrEHgoAIg7LEj7LEn4bEvk+AodwhwFD+C' +
|
||||
'5E8DFEAqIdFFIo1FIIpNFLIoEEAtShCVwQEDVwIFDKAJBvAAv/Bgn/RIjzGjwFEW' +
|
||||
'YicBAqAXFEYh6CRIgFKTYzjEAwt/AxxvDHAkf//AAgMDPIgVBGAnwAoYRBIYk/S4' +
|
||||
'kDMIgeBFIQEBBYRTBCAZ3FAggAMg4zEj7LEn7LEv++AodzxwFD+ePAofjw4FVDoo' +
|
||||
'pFv+eIImcJomYLImAAoZeEAtTyBAAQFEVYIFDSQIvhAojaCFwgABh4YEngFEuAqJ' +
|
||||
'gPAAocDApYuEgP/fgl/+B9HAAv+Aon8HQMOIAkeAokcAohaDAoM4Aol4AohmDAoJ' +
|
||||
'BDAoJsDAo7vhABbJDAo9/AojEFMYbKMArCBDFI41FWIYABggFEgbuCDYMPLIQbBj' +
|
||||
'//wBdCn0H4DZCvEBb4YZBdYZBBAofgCIQFDDoIFFDoPggYFBF4IFBGoI7B+AFCE4' +
|
||||
'NwCIIlCuAdBIYU4gPwn5VBjC7B/y0Dv/4YwcPCwMAjJlCAAM584FDufDCAUA8eBA' +
|
||||
'p/zC4n5EYj1BAoc//4RDU4IFDA=='
|
||||
), 48, dec('kElkMljsljw='), 48
|
||||
];const mF = [
|
||||
dec(
|
||||
'/AEDvEH4AFCgPAnwMDh0B+AGD8EPwAFCg8AvgMDuED8AMEj4MDDwI0DhwOB/4ACC' +
|
||||
'4M/AoX8HgIMDCoI0EAAI0EgA0DnACBGgXHL4Q0Bjn+IYXAgfOCwRpBnPHEQmcuAG' +
|
||||
'DBg3csAGDj4mCAAX/QwhkBWSEDDIp3BAoZ3BBgkeDIp9FOYQMJDIomGh5NFv/wVo' +
|
||||
'YABYIgZBYIYABgKWBHAcPHAKsCgF4VoJDD4AVCIYbtBfAnwgYDBg+Ag6bBEQM8EQ' +
|
||||
'KoCDwMDwP9EQI0Bnk9540DZ4Y/CZ4Y0BbggwBDIY0BgP8JIbcB7yBE/pjDEAOQbZ' +
|
||||
'8fRwT7DAAL7E/4zEjh9EKwLCEnB9BBhIZFgPzEwkP/jcFe4iYBdYLcEAwr5CBgYj' +
|
||||
'Hh65BAxU/AwjNCIhEH/BkEGYqTCRwYMFACE4AonHZ4kcIQkB5yOEnPHIYmcuAMK7' +
|
||||
'lgNJJQBJojkBKSB3BDIk/DIkBBgseDIpmEOYwMGDIsAOYkAgxBGGYjzBIwoMDXYI' +
|
||||
'tCaAQFCCwP8jiECCwMBBhAZGEwwzHIAxNGTY5UKTYIMEjkORwomEnEHBhQZFgPzT' +
|
||||
'gkP/hBEv+ACYivFe3adBAAfwAwoNFGYJkGh/+Axc/AwkfAoggFg/4ZgwzDj4GDiD' +
|
||||
'7CAAPxRQswNIp1FBgnH4TPE/0gC4fO8wMDnPHsAMDzl2BhXcsxpFBgZQB+xqE/4z' +
|
||||
'DAAMCLRJ3BwaWFBgvjDAkfuAGEu4MFfoYZBW4v/eIn/8CzEvEHBocB4E+BgcOgIn' +
|
||||
'DgHgh+ANAcAvgMDuED8AMEj4MDDwI0DhyECAAQXBn4FCf4MBBgYVBGggABGghrBD' +
|
||||
'gQqCGgJ0BL4QJBTYJDCBIMBJYRpCJoIAEUIoMGPIgmDVAYMFKgQAODJh3BBgkeDI' +
|
||||
'p9FnAMLDIomGh5NFv/we372/exgZDe0BpCDIbBBDIl/EwonBAogMEHIIZDD4KUBH' +
|
||||
'wYFDCAPBOwQWCjgMHDI4mGGYwcC+JNFiDAFOIswEAmDDAn8kAME8QYEjwMDAAN2Y' +
|
||||
'QtgTonmYQoMDEwP2YQoZEgECJoozEv5NEj/+LQaYB8YMDn0fM4mAu4MDnEHuAMD8' +
|
||||
'KVEIAPgEwn+WAuAK4LABj7PDwEAvhJBCwUB8EP8EffQMOgH4C4ITB+EHAYN4RwMA' +
|
||||
'ng/BE4PwDYITCnw2BF4YKBF4LwDgInBKYLoFFQIAJgZCBAAZdCTYjOE/p6DgE954' +
|
||||
'fEziUDgE544ME7gtEj/OExUP7hAEnJTKAAxuBFoa4BOokfBgkB4AzEniZBewhaEB' +
|
||||
'goZGj61BRxMHWQIADjwJCIgLICJQQABDIL9BAAKoBg4iCgYTBKoZABhwnDJoJCDg' +
|
||||
'4OCAAQXBewIABJoI5DHQSLBAAP8B4I6CcQgANgbVEOg0fEAkB8KOEnBNBVBIMMjh' +
|
||||
'yEWo0MhhSPgJoBwCZDNwp2BJor2LJpjAFAAImEJwI2BAAfwj4GEXYgMBAwKlFv4G' +
|
||||
'GFQpYFXQx0BAwx6DLQIGCIIgeCIAkHBgoAPn4FEh/8HQpPEn0fVCPhO4kfZ4hvGg' +
|
||||
'YSEgRGFngFEgf4AwkfSws/EwgtBBhQZFEw0cOwIHEuF4AocHWIL2LBgsHGoaBBn7' +
|
||||
'SD+DZEnzIFI4MPAoS1CAwbVRTYqoGWosB/p7EnvPD4mcbgk544ME7jcF5wmKh/cI' +
|
||||
'Ak5LUvhGYk4VAIfDwBaEBgsB4AZEjkOGYnA4AA=='
|
||||
), 49, dec('k0jk0kksmj0lk8lAwIA='), 52
|
||||
];
|
||||
const lockI = dec('hURwMAj0P485w1h3/4g15wFgjPmgOAs+Yg0B//AA');
|
||||
const lockSI = dec('hMNwMAjkfjHMt/8g1zgOc4FnmEf/AA==');
|
||||
const batteryI = dec('hERwMAjH/ABw');
|
||||
const chargeI = dec('g8NwMAgkYsHDh0fw8MmFhwUA');
|
||||
const HRMI = dec('iERwMAjk4l10t/29/3AIfn+ek6VTlPX9d3/U3/Ef/EP+EH8ED4EBwAA=');
|
||||
const compassI = dec('hMJwMAhEEg8Dwfh2Pc43BwA=');
|
||||
const y100I = dec('h8RwMAvk5/n6nOwm9w9lnzH+mO4sc4405xk7jE2mEssEd4EbgE+gE4A=');
|
||||
const y100sI = dec('hcKwMAsOWvHZ+c2s1s4uYmcD4EwA');
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
/* Status */
|
||||
|
||||
const status = (p, i) => function (g, x, y, rl) { // Nested arrows are currently broken!
|
||||
if (!p()) return x;
|
||||
if (rl) x -= imageWidth(i);
|
||||
g.setColor(g.theme.fg).drawImage(i, x, y);
|
||||
return rl ? x - 1 : x + imageWidth(i) + 1;
|
||||
};
|
||||
|
||||
const doLocked = status(_ => Bangle.isLocked(), lockI);
|
||||
const doPower = (g, x, y, rl) => {
|
||||
const c = Bangle.isCharging();
|
||||
const b = E.getBattery();
|
||||
if (!c && b > 50) return x;
|
||||
if (rl) x -= imageWidth(batteryI);
|
||||
g.setColor(g.theme.fg).drawImage(batteryI, x, y);
|
||||
g.setColor(b <= 10 ? '#f00' : b <= 30 ? '#ff0' : '#0f0');
|
||||
let h = 13 * (100 - b) / 100;
|
||||
g.fillRect(x + 1, y + 2 + h, x + 6, y + 15);
|
||||
if (c) g.setColor(g.theme.bg).drawImage(chargeI, x, y + 2);
|
||||
return rl ? x - 1 : x + imageWidth(batteryI) + 1;
|
||||
};
|
||||
|
||||
const doHRM = status(_ => Bangle.isHRMOn(), HRMI); // Might show Bangle.getHRM().bpm if confident?
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
/* Watch face */
|
||||
|
||||
class Round {
|
||||
constructor(g) {
|
||||
this.g = g;
|
||||
this.b = Graphics.createArrayBuffer(g.getWidth(), g.getHeight(), 1, {msb: true});
|
||||
this.bI = {
|
||||
width: this.b.getWidth(), height: this.b.getHeight(), bpp: this.b.getBPP(),
|
||||
buffer: this.b.buffer, transparent: 0
|
||||
};
|
||||
this.c = Graphics.createArrayBuffer(g.getWidth(), g.getHeight(), 1, {msb: true});
|
||||
this.cI = {
|
||||
width: this.c.getWidth(), height: this.c.getHeight(), bpp: this.c.getBPP(),
|
||||
buffer: this.c.buffer, transparent: 0
|
||||
};
|
||||
this.options = new RoundOptions();
|
||||
this.timescales = [1000, 0, 60000, 900000];
|
||||
this.state = {};
|
||||
// Precomputed polygons for the border areas.
|
||||
this.tl = [0, 0, 58, 0, 0, 58];
|
||||
this.tr = [176, 0, 176, 58, 119, 0];
|
||||
this.bl = [0, 176, 0, 119, 58, 176];
|
||||
this.br = [176, 176, 119, 176, 176, 119];
|
||||
this.xc = g.getWidth() / 2;
|
||||
this.yc = g.getHeight() / 2;
|
||||
this.minR = 5;
|
||||
this.secR = 3;
|
||||
this.r = this.xc - this.minR;
|
||||
}
|
||||
|
||||
reset() {this.state = {}; this.g.clear(true);}
|
||||
|
||||
doIcons(which) {
|
||||
this.state[which] = null;
|
||||
this.render(new Date()); // Not quite right, I think.
|
||||
}
|
||||
|
||||
pie(f, a0, a1, invert) {
|
||||
if (!invert) return this.pie(f, a1, a0 + 1, true);
|
||||
let t0 = Math.tan(a0 * 2 * Math.PI), t1 = Math.tan(a1 * 2 * Math.PI);
|
||||
let i0 = Math.floor(a0 * 4 + 0.5), i1 = Math.floor(a1 * 4 + 0.5);
|
||||
let x = f.getWidth() / 2, y = f.getHeight() / 2;
|
||||
let poly = [
|
||||
x + (i1 & 2 ? -x : x) * (i1 & 1 ? 1 : t1),
|
||||
y + (i1 & 2 ? y : -y) / (i1 & 1 ? t1 : 1),
|
||||
x,
|
||||
y,
|
||||
x + (i0 & 2 ? -x : x) * (i0 & 1 ? 1 : t0),
|
||||
y + (i0 & 2 ? y : -y) / (i0 & 1 ? t0 : 1),
|
||||
];
|
||||
if (i1 - i0 > 4) i1 = i0 + 4;
|
||||
for (i0++; i0 <= i1; i0++) poly.push(
|
||||
3 * i0 & 2 ? f.getWidth() : 0, i0 & 2 ? f.getHeight() : 0
|
||||
);
|
||||
f.setColor(0).fillPoly(poly);
|
||||
}
|
||||
|
||||
hand(t, d, c0, r0, c1, r1) {
|
||||
t *= Math.PI / 30;
|
||||
const r = this.r;
|
||||
const z = 2 * r0 + 1;
|
||||
const x = this.xc + r * Math.sin(t), y = this.yc - r * Math.cos(t);
|
||||
const x0 = x - r0, y0 = y - r0;
|
||||
d = d ? d[0] : Graphics.createArrayBuffer(z, z, 16, {msb: true});
|
||||
for (let i = 0; i < z; i++) for (let j = 0; j < z; j++) {
|
||||
d.setPixel(i, j, g.getPixel(x0 + i, y0 + j));
|
||||
}
|
||||
g.setColor(c0).fillCircle(x, y, r0);
|
||||
if (c1 !== undefined) g.setColor(c1).fillCircle(x, y, r1);
|
||||
return [d, x0, y0];
|
||||
}
|
||||
|
||||
render(d) {
|
||||
const g = this.g;
|
||||
const b = this.b, bI = this.bI;
|
||||
const c = this.c, cI = this.cI;
|
||||
const state = this.state;
|
||||
const options = this.options;
|
||||
const cal = options.calendric;
|
||||
const res = options.resolution;
|
||||
const dow = (cal == 1 || cal > 2) && d.getDay();
|
||||
const ts = res < 2 && d.getSeconds();
|
||||
const tm = res < 3 && d.getMinutes() + ts / 60;
|
||||
const th = d.getHours() + d.getMinutes() / 60;
|
||||
const dd = cal > 1 && d.getDate();
|
||||
const dm = cal > 3 && d.getMonth();
|
||||
const dy = cal > 4 && d.getFullYear();
|
||||
const xc = this.xc, yc = this.yc, r = this.r;
|
||||
const dlr = xc * 3/4, dlw = 8, dlhw = 4;
|
||||
|
||||
// Restore saveunders for fast-moving, overdrawing indicators.
|
||||
if (state.sd) g.drawImage.apply(g, state.sd);
|
||||
if (state.md) g.drawImage.apply(g, state.md);
|
||||
|
||||
if (dow !== state.dow) {
|
||||
g.setColor(g.theme.bg).fillPoly(this.tl);
|
||||
if (dow === +dow) {
|
||||
g.setColor(g.theme.fg).setFontCustom.apply(g, dowF).drawString(dow, 5, 5);
|
||||
}
|
||||
state.dow = dow;
|
||||
}
|
||||
|
||||
const locked = Bangle.isLocked();
|
||||
const charging = Bangle.isCharging();
|
||||
const battery = E.getBattery();
|
||||
const HRMOn = Bangle.isHRMOn();
|
||||
if (dy !== state.dy ||
|
||||
locked !== state.locked ||
|
||||
charging !== state.charging ||
|
||||
Math.abs(battery - state.battery) > 2 ||
|
||||
HRMOn !== state.HRMOn
|
||||
) {
|
||||
g.setColor(g.theme.bg).fillPoly(this.tr);
|
||||
const u = dy % 10;
|
||||
if (charging || battery < 50 || HRMOn || locked && dy !== +dy) {
|
||||
let x = 172, y = 5;
|
||||
x = doLocked(g, x, y, true);
|
||||
x = doPower(g, x, y, true);
|
||||
x = doHRM(g, x, y, true);
|
||||
if (dy === +dy) {
|
||||
g.setColor(g.theme.fg).drawImage(y100sI, 145, 23);
|
||||
g.setFontCustom.apply(g, y10sF).drawString((dy - u) / 10 % 10, 157, 23);
|
||||
g.setFontCustom.apply(g, y1sF).drawString(u, 165, 23);
|
||||
}
|
||||
} else if (dy === +dy) {
|
||||
g.setColor(g.theme.fg);
|
||||
if (locked) g.drawImage(lockSI, 136, 5);
|
||||
else g.drawImage(y100I, 130, 5);
|
||||
g.setFontCustom.apply(g, y10F).drawString((dy - u) / 10 % 10, 146, 5);
|
||||
g.setFontCustom.apply(g, y1F).drawString(u, 160, 5);
|
||||
}
|
||||
state.dy = dy;
|
||||
state.locked = Bangle.isLocked();
|
||||
state.charging = Bangle.isCharging();
|
||||
state.battery = E.getBattery() - E.getBattery() % 2;
|
||||
state.HRMOn = Bangle.isHRMOn();
|
||||
}
|
||||
if (dm !== state.dm) {
|
||||
g.setColor(g.theme.bg).fillPoly(this.bl);
|
||||
if (dm === +dm) {
|
||||
g.setColor(g.theme.fg).setFontCustom.apply(g, mF);
|
||||
g.drawString(String.fromCharCode(49 + dm), 5, 124);
|
||||
}
|
||||
state.dm = dm;
|
||||
}
|
||||
if (dd !== state.dd) {
|
||||
g.setColor(g.theme.bg).fillPoly(this.br);
|
||||
if (dd === +dd) {
|
||||
let u = dd % 10;
|
||||
g.setColor(g.theme.fg).setFontAlign(1, 1);
|
||||
g.setFontCustom.apply(g, d10F).drawString((dd - u) / 10, 152, 172);
|
||||
g.setFontAlign(-1, 1);
|
||||
g.setFontCustom.apply(g, d1F).drawString(u, 152, 172);
|
||||
g.setFontAlign(-1, -1);
|
||||
}
|
||||
}
|
||||
if (th !== state.th) {
|
||||
state.th = th;
|
||||
b.clear(true).fillCircle(88, 88, r - 1);
|
||||
g.setColor(options.nightFg).drawImage(bI);
|
||||
if (th < 12) this.pie(b, th / 12, 1, true);
|
||||
else this.pie(b, 1, th / 12, true);
|
||||
g.setColor(options.dayFg).drawImage(bI);
|
||||
}
|
||||
state.md = tm === +tm ?
|
||||
this.hand(tm, state.md, g.theme.bg, this.minR, g.theme.fg, this.minR - 1) :
|
||||
null;
|
||||
state.sd = ts === +ts ?
|
||||
this.hand(ts, state.sd, g.theme.fg2, this.secR) :
|
||||
null;
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
/* Master clock */
|
||||
|
||||
class Clock {
|
||||
constructor(face) {
|
||||
this.face = face;
|
||||
this.timescales = face.timescales;
|
||||
this.options = face.options;
|
||||
this.rates = {};
|
||||
this.faceUp = null;
|
||||
|
||||
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.faceUp = up;
|
||||
this.active();
|
||||
},
|
||||
twist: _ => this.options.autolight && Bangle.setLCDPower(true),
|
||||
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.yX = 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;
|
||||
this.face.reset(); // Cancel any ongoing background rendering
|
||||
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 Round(g)).start();
|
After Width: | Height: | Size: 2.9 KiB |
|
@ -1 +1,2 @@
|
|||
0.01: First release
|
||||
0.02: Fix dependancies, fix type to Purple
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
storage.write(SETTINGS_FILE, settings)
|
||||
}
|
||||
|
||||
var color_options = ['Green','Orange','Cyan','Perple','Red','Blue'];
|
||||
var color_options = ['Green','Orange','Cyan','Purple','Red','Blue'];
|
||||
var bg_code = ['#0f0','#ff0','#0ff','#f0f','#f00','#00f'];
|
||||
|
||||
E.showMenu({
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
0.01: First release
|
||||
0.02: Fixed snek.png and snek.icon.js to 64x64 to display in launcher, added screenshots, updated apps.json
|
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 2.5 KiB |
|
@ -1 +0,0 @@
|
|||
require("heatshrink").decompress(atob("pFIwkB+MRAEH/EUIA7i93u9xEUIABEf4AC+93/4CBETpDBv//gEHEf6MB9wkB/8HSDl3vwjCAAfnErIjiEQYdBAAXuAoSNXEYIdDAAoj4j/3DpN3v6NWAA3/fDYjgRgIjLu9xESj2BAAN/SQwLBEe4XDdwghDBQQjSCgN+C5D9FEebTDEZJEWEQSDVEdZpDZYPnETYhDAAhpeEbzREI0rTbEdXuETb4Bvz1BAYIj/EYxrg9yQDv/3JoS9WEcoaGAAQtBOwYABEaSMBWoYeFJKgjjiIUD9ySEEjwAJFogj0SQgAFBQ4jRABcfXoQj/TowjCOgIkeEf7lHvz+CEb93Ef4jHR8Rr/fY4jCuIifAAQj/EIojbeohGeEcQhHfLRFDeoIicEcQbDv3uEYiNXIgnu87SgEcf/DKnxBJEf/4ACESf/EcYA=="))
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("oFA4X/AAOJksvr2rmokYgWqB7sq/2AB5krgYPMgW8ioPc1X9i/oLplVqv+1BdK1OV//q9QPMv4PL1eqy/q1SRK3tVu+AgWCFxP96t+Vhn9qoPLgWr/+//wFBSBEq3/qlW+JwJ/I3eXDQIOBB5OrB5sC3xMD1WAH4+r6xsOtSpKLoYPN1fV1bpKTYf+RJAeDytXFxoPOdQYPNPpkCy1VtQPc6wvO62Vu+CbhfVN4P//+q//uMgwPH9QPH3tqqtpqoABv4wHfoOpBoP/6tVUg7uBFwIvB3xlIB4v+OpJsC1WA1fVQpiGCB52+uzlMB58A31XB5sqy4PNlYPfH50rywPN3++BxgPPgW9V5kCZ4L/HBwmq/tX1APM/4PMBwNVvxuKgW/tP/HxUq1X+1eqFxQPRAAKsLB4KqNAFY="))
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 2.7 KiB |