Merge remote-tracking branch 'upstream/master'

master
hughbarney 2021-09-27 23:04:14 +01:00
commit 8a9910d7d4
48 changed files with 632 additions and 292 deletions

3
.gitignore vendored
View File

@ -7,4 +7,5 @@ appdates.csv
.vscode .vscode
.idea/ .idea/
_config.yml _config.yml
tests/Layout/bin/tmp.*
tests/Layout/testresult.bmp

View File

@ -94,7 +94,7 @@
"name": "Notifications (default)", "name": "Notifications (default)",
"shortName":"Notifications", "shortName":"Notifications",
"icon": "notify.png", "icon": "notify.png",
"version":"0.09", "version":"0.10",
"description": "A handler for displaying notifications that displays them in a bar at the top of the screen", "description": "A handler for displaying notifications that displays them in a bar at the top of the screen",
"tags": "widget", "tags": "widget",
"type": "notify", "type": "notify",
@ -107,9 +107,9 @@
"name": "Fullscreen Notifications", "name": "Fullscreen Notifications",
"shortName":"Notifications", "shortName":"Notifications",
"icon": "notify.png", "icon": "notify.png",
"version":"0.10", "version":"0.11",
"description": "A handler for displaying notifications that displays them fullscreen. This may not fully restore the screen after on some apps. See `Notifications (default)` for more information about the notifications library.", "description": "A handler for displaying notifications that displays them fullscreen. This may not fully restore the screen after on some apps. See `Notifications (default)` for more information about the notifications library.",
"tags": "widget", "tags": "widget,b2",
"type": "notify", "type": "notify",
"storage": [ "storage": [
{"name":"notify","url":"notify.js"} {"name":"notify","url":"notify.js"}
@ -155,7 +155,7 @@
"icon": "app.png", "icon": "app.png",
"version":"0.24", "version":"0.24",
"description": "The default notification handler for Gadgetbridge notifications from Android", "description": "The default notification handler for Gadgetbridge notifications from Android",
"tags": "tool,system,android,widget", "tags": "tool,system,android,widget,b2",
"readme": "README.md", "readme": "README.md",
"type":"widget", "type":"widget",
"dependencies": { "notify":"type" }, "dependencies": { "notify":"type" },
@ -571,7 +571,7 @@
{ "id": "weather", { "id": "weather",
"name": "Weather", "name": "Weather",
"icon": "icon.png", "icon": "icon.png",
"version":"0.07", "version":"0.08",
"description": "Show Gadgetbridge weather report", "description": "Show Gadgetbridge weather report",
"readme": "readme.md", "readme": "readme.md",
"tags": "widget,outdoors", "tags": "widget,outdoors",
@ -1184,7 +1184,7 @@
{ "id": "widpedom", { "id": "widpedom",
"name": "Pedometer widget", "name": "Pedometer widget",
"icon": "widget.png", "icon": "widget.png",
"version":"0.18", "version":"0.19",
"description": "Daily pedometer widget", "description": "Daily pedometer widget",
"tags": "widget,b2", "tags": "widget,b2",
"type":"widget", "type":"widget",
@ -1670,7 +1670,7 @@
"name": "Calculator", "name": "Calculator",
"shortName":"Calculator", "shortName":"Calculator",
"icon": "calculator.png", "icon": "calculator.png",
"version":"0.03", "version":"0.04",
"description": "Basic calculator reminiscent of MacOs's one. Handy for small calculus.", "description": "Basic calculator reminiscent of MacOs's one. Handy for small calculus.",
"tags": "app,tool,b2", "tags": "app,tool,b2",
"storage": [ "storage": [
@ -2508,7 +2508,7 @@
"name": "Cycling speed sensor", "name": "Cycling speed sensor",
"shortName":"CSCSensor", "shortName":"CSCSensor",
"icon": "icons8-cycling-48.png", "icon": "icons8-cycling-48.png",
"version":"0.04", "version":"0.05",
"description": "Read BLE enabled cycling speed and cadence sensor and display readings on watch", "description": "Read BLE enabled cycling speed and cadence sensor and display readings on watch",
"tags": "outdoors,exercise,ble,bluetooth", "tags": "outdoors,exercise,ble,bluetooth",
"readme": "README.md", "readme": "README.md",
@ -3520,5 +3520,29 @@
"data": [ "data": [
{"name":"pastel.json"} {"name":"pastel.json"}
] ]
},
{ "id": "antonclk",
"name": "Anton Clock",
"icon": "app.png",
"version":"0.01",
"description": "A simple clock using the bold Anton font.",
"tags":"clock,b2",
"type":"clock",
"storage": [
{"name":"antonclk.app.js","url":"app.js"},
{"name":"antonclk.img","url":"app-icon.js","evaluate":true}
]
},
{ "id": "waveclk",
"name": "Wave Clock",
"icon": "app.png",
"version":"0.01",
"description": "A clock using a wave image by [Lillith May](https://www.instagram.com/_lilustrations_/). **Note: This requires a bugfix for #2049 on Bangle.js 1**",
"tags":"clock,b2",
"type":"clock",
"storage": [
{"name":"waveclk.app.js","url":"app.js"},
{"name":"waveclk.img","url":"app-icon.js","evaluate":true}
]
} }
] ]

View File

@ -97,7 +97,7 @@ function startRecord(force) {
{type:"txt", id:"samples", font:"6x8:2", label:" - ", pad:5}, {type:"txt", id:"samples", font:"6x8:2", label:" - ", pad:5},
{type:"txt", font:"6x8", label:"Time", pad:2}, {type:"txt", font:"6x8", label:"Time", pad:2},
{type:"txt", id:"time", font:"6x8:2", label:" - ", pad:5}, {type:"txt", id:"time", font:"6x8:2", label:" - ", pad:5},
{type:"txt", font:"6x8:2", label:"RECORDING", bgCol:"#f00", pad:5, fillx:true}, {type:"txt", font:"6x8:2", label:"RECORDING", bgCol:"#f00", pad:5, fillx:1},
] ]
},[ // Buttons... },[ // Buttons...
{label:"STOP", cb:()=>{ {label:"STOP", cb:()=>{
@ -105,7 +105,6 @@ function startRecord(force) {
showMenu(); showMenu();
}} }}
]); ]);
layout.update();
layout.render(); layout.render();
// now start writing // now start writing

1
apps/antonclk/ChangeLog Normal file
View File

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

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgX/AH4A/AAf+14BBAoPq/Wq1QFB+EP+kLAoNA+CdB3//yEP6ALB/sABYf8gEMgALB6ALJqoLB+tVgH//kQhkQhUABYImCIwPwh3whcA94UCgJHD+AXB/4LCcQQLCKYQLB+EDBZQFCBY/Qh4LJ4ALLl4LFioLCgE/KZMAv4XFBYimBC4/+BYw7DRwQLIV4ILWTQQLDOIUA/ALDAC+t1uv/263/6/Pq/YLC3vv//vM4Oq33rBYP6/u//2/C4Pr1YXC12vBwO+BYO69XmJDQA/ACIA=="))

57
apps/antonclk/app.js Normal file

File diff suppressed because one or more lines are too long

BIN
apps/antonclk/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 759 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

View File

@ -1,3 +1,4 @@
0.01: New App! 0.01: New App!
0.02: fix precision rounding issue + no reset when equals pressed 0.02: fix precision rounding issue + no reset when equals pressed
0.03: Support for different screen sizes and touchscreen 0.03: Support for different screen sizes and touchscreen
0.04: Display current operation on LHS

View File

@ -199,8 +199,7 @@ function doMath(x, y, operator) {
function displayOutput(num) { function displayOutput(num) {
var len; var len;
var minusMarge = 0; var minusMarge = 0;
g.setColor(0); g.setBgColor(0).clearRect(0, 0, g.getWidth(), RESULT_HEIGHT-1);
g.fillRect(0, 0, g.getWidth(), RESULT_HEIGHT-1);
g.setColor(-1); g.setColor(-1);
if (num === Infinity || num === -Infinity || isNaN(num)) { if (num === Infinity || num === -Infinity || isNaN(num)) {
// handle division by 0 // handle division by 0
@ -244,6 +243,10 @@ function displayOutput(num) {
} }
g.setFontAlign(1,0); g.setFontAlign(1,0);
g.drawString(num, g.getWidth()-20, RESULT_HEIGHT/2); g.drawString(num, g.getWidth()-20, RESULT_HEIGHT/2);
if (operator) {
g.setFont('Vector', 22).setFontAlign(1,0);
g.drawString(operator, g.getWidth()-1, RESULT_HEIGHT/2);
}
} }
var wasPressedEquals = false; var wasPressedEquals = false;
var hasPressedNumber = false; var hasPressedNumber = false;

View File

@ -2,4 +2,4 @@
0.02: Add wheel circumference settings dialog 0.02: Add wheel circumference settings dialog
0.03: Save total distance traveled 0.03: Save total distance traveled
0.04: Add sensor battery level indicator 0.04: Add sensor battery level indicator
0.05: Add cadence sensor support

View File

@ -13,6 +13,6 @@ Currently the app displays the following data:
Button 1 resets all measurements except total distance traveled. The latter gets preserved by being written to storage every 0.1 miles and upon exiting the app. Button 1 resets all measurements except total distance traveled. The latter gets preserved by being written to storage every 0.1 miles and upon exiting the app.
If the watch app has not received an update from the sensor for at least 10 seconds, pushing button 3 will attempt to reconnect to the sensor. If the watch app has not received an update from the sensor for at least 10 seconds, pushing button 3 will attempt to reconnect to the sensor.
Button 2 switches between the display for cycling speed and cadence.
I do not have access to a cadence sensor at the moment, so only the speed part is currently implemented. Values displayed are imperial or metric (depending on locale), Values displayed are imperial or metric (depending on locale), cadence is in RPM, the wheel circumference can be adjusted in the global settings app.
the wheel circumference can be adjusted in the global settings app.

View File

@ -28,6 +28,10 @@ class CSCSensor {
this.distFactor = this.qMetric ? 1.609344 : 1; this.distFactor = this.qMetric ? 1.609344 : 1;
this.screenInit = true; this.screenInit = true;
this.batteryLevel = -1; this.batteryLevel = -1;
this.lastCrankTime = 0;
this.lastCrankRevs = 0;
this.showCadence = false;
this.cadence = 0;
} }
reset() { reset() {
@ -40,6 +44,11 @@ class CSCSensor {
this.screenInit = true; this.screenInit = true;
} }
toggleDisplayCadence() {
this.showCadence = !this.showCadence;
this.screenInit = true;
}
setBatteryLevel(level) { setBatteryLevel(level) {
if (level!=this.batteryLevel) { if (level!=this.batteryLevel) {
this.batteryLevel = level; this.batteryLevel = level;
@ -62,7 +71,7 @@ class CSCSensor {
else g.setFontVector(14).setFontAlign(0, 0, 0).setColor(0xffff).drawString("?", 16, 66); else g.setFontVector(14).setFontAlign(0, 0, 0).setColor(0xffff).drawString("?", 16, 66);
} }
updateScreen() { updateScreenRevs() {
var dist = this.distFactor*(this.lastRevs-this.lastRevsStart)*this.wheelCirc/63360.0; var dist = this.distFactor*(this.lastRevs-this.lastRevsStart)*this.wheelCirc/63360.0;
var ddist = Math.round(100*dist)/100; var ddist = Math.round(100*dist)/100;
var tdist = Math.round(this.distFactor*this.totaldist*10)/10; var tdist = Math.round(this.distFactor*this.totaldist*10)/10;
@ -109,9 +118,51 @@ class CSCSensor {
g.setColor(0xffff).drawString(tdist + " " + this.distUnit, 92, 226); g.setColor(0xffff).drawString(tdist + " " + this.distUnit, 92, 226);
} }
updateScreenCadence() {
if (this.screenInit) {
for (var i=0; i<2; ++i) {
if ((i&1)==0) g.setColor(0, 0, 0);
else g.setColor(0x30cd);
g.fillRect(0, 48+i*32, 86, 48+(i+1)*32);
if ((i&1)==1) g.setColor(0);
else g.setColor(0x30cd);
g.fillRect(87, 48+i*32, 239, 48+(i+1)*32);
g.setColor(0.5, 0.5, 0.5).drawRect(87, 48+i*32, 239, 48+(i+1)*32).drawLine(0, 239, 239, 239);//.drawRect(0, 48, 87, 239);
g.moveTo(0, 80).lineTo(30, 80).lineTo(30, 48).lineTo(87, 48).lineTo(87, 239).lineTo(0, 239).lineTo(0, 80);
}
g.setFontAlign(1, 0, 0).setFontVector(19).setColor(1, 1, 0);
g.drawString("Cadence:", 87, 98);
this.drawBatteryIcon();
this.screenInit = false;
}
g.setFontAlign(-1, 0, 0).setFontVector(26);
g.setColor(0).fillRect(88, 81, 238, 111);
g.setColor(0xffff).drawString(Math.round(this.cadence), 92, 98);
}
updateScreen() {
if (!this.showCadence) {
this.updateScreenRevs();
} else {
this.updateScreenCadence();
}
}
updateSensor(event) { updateSensor(event) {
var qChanged = false; var qChanged = false;
if (event.target.uuid == "0x2a5b") { if (event.target.uuid == "0x2a5b") {
if (event.target.value.getUint8(0, true) & 0x2) {
// crank revolution
const crankRevs = event.target.value.getUint16(1, true);
const crankTime = event.target.value.getUint16(3, true);
if (crankTime > this.lastCrankTime) {
this.cadence = (crankRevs-this.lastCrankRevs)/(crankTime-this.lastCrankTime)*(60*1024);
qChanged = true;
}
this.lastCrankRevs = crankRevs;
this.lastCrankTime = crankTime;
} else {
// wheel revolution
var wheelRevs = event.target.value.getUint32(1, true); var wheelRevs = event.target.value.getUint32(1, true);
var dRevs = (this.lastRevs>0 ? wheelRevs-this.lastRevs : 0); var dRevs = (this.lastRevs>0 ? wheelRevs-this.lastRevs : 0);
if (dRevs>0) { if (dRevs>0) {
@ -148,6 +199,7 @@ class CSCSensor {
this.lastSpeed = this.speed; this.lastSpeed = this.speed;
if (this.speed>this.maxSpeed && (this.movingTime>3 || this.speed<20) && this.speed<50) this.maxSpeed = this.speed; if (this.speed>this.maxSpeed && (this.movingTime>3 || this.speed<20) && this.speed<50) this.maxSpeed = this.speed;
} }
}
if (qChanged && this.qUpdateScreen) this.updateScreen(); if (qChanged && this.qUpdateScreen) this.updateScreen();
} }
} }
@ -199,6 +251,7 @@ connection_setup();
setWatch(function() { mySensor.reset(); g.clearRect(0, 48, 239, 239); mySensor.updateScreen(); }, BTN1, {repeat:true, debounce:20}); setWatch(function() { mySensor.reset(); g.clearRect(0, 48, 239, 239); mySensor.updateScreen(); }, BTN1, {repeat:true, debounce:20});
E.on('kill',()=>{ if (gatt!=undefined) gatt.disconnect(); mySensor.settings.totaldist = mySensor.totaldist; storage.writeJSON(SETTINGS_FILE, mySensor.settings); }); E.on('kill',()=>{ if (gatt!=undefined) gatt.disconnect(); mySensor.settings.totaldist = mySensor.totaldist; storage.writeJSON(SETTINGS_FILE, mySensor.settings); });
setWatch(function() { if (Date.now()-mySensor.lastBangleTime>10000) connection_setup(); }, BTN3, {repeat:true, debounce:20}); setWatch(function() { if (Date.now()-mySensor.lastBangleTime>10000) connection_setup(); }, BTN3, {repeat:true, debounce:20});
setWatch(function() { mySensor.toggleDisplayCadence(); g.clearRect(0, 48, 239, 239); mySensor.updateScreen(); }, BTN2, {repeat:true, debounce:20});
NRF.on('disconnect', connection_setup); NRF.on('disconnect', connection_setup);
Bangle.loadWidgets(); Bangle.loadWidgets();

View File

@ -6,3 +6,4 @@
0.07: Auto-calculate height, and pad text down even when there's no title (so it stays on-screen) 0.07: Auto-calculate height, and pad text down even when there's no title (so it stays on-screen)
0.08: Don't turn on screen during Quiet Mode 0.08: Don't turn on screen during Quiet Mode
0.09: Add onHide callback 0.09: Add onHide callback
0.10: Improvements to help notifications work with themes

View File

@ -5,6 +5,10 @@ A handler for displaying notifications that displays them in a bar at the top of
This is not an app, but instead it is a library that can be used by This is not an app, but instead it is a library that can be used by
other applications or widgets to display messages. other applications or widgets to display messages.
**Note:** There are other implementations of this library available such
as `notifyfs` (Fullscreen Notifications). These can be used in the exact
same way from code, but they look different to the user.
## Usage ## Usage
```JS ```JS

View File

@ -96,15 +96,17 @@ exports.show = function(options) {
b = y+h-1, r = x+w-1; // bottom,right b = y+h-1, r = x+w-1; // bottom,right
g.setClipRect(x,y, r,b); g.setClipRect(x,y, r,b);
// clear area // clear area
g.setColor(options.bgColor||0).fillRect(x,y, r,b); g.reset();
if (options.bgColor!==undefined) g.setColor(options.bgColor);
g.clearRect(x,y, r,b);
// bottom border // bottom border
g.setColor(0x39C7).fillRect(0,b-1, r,b); g.setColor("#333").fillRect(0,b-1, r,b);
b -= 2;h -= 2; b -= 2;h -= 2;
// title bar // title bar
if (options.title || options.src) { if (options.title || options.src) {
g.setColor(options.titleBgColor||0x39C7).fillRect(x,y, r,y+20); g.setColor(options.titleBgColor||0x39C7).fillRect(x,y, r,y+20);
const title = options.title||options.src; const title = options.title||options.src;
g.setColor(-1).setFontAlign(-1, -1, 0).setFont("6x8", 2); g.setColor(g.theme.fg).setFontAlign(-1, -1, 0).setFont("6x8", 2);
g.drawString(title.trim().substring(0, 13), x+25,y+3); g.drawString(title.trim().substring(0, 13), x+25,y+3);
if (options.title && options.src) { if (options.title && options.src) {
g.setFont("6x8", 1).setFontAlign(1, 1, 0); g.setFont("6x8", 1).setFontAlign(1, 1, 0);
@ -122,7 +124,7 @@ exports.show = function(options) {
} }
// body text // body text
if (options.body) { if (options.body) {
g.setColor(-1).setFont("6x8", 1).setFontAlign(-1, -1, 0).drawString(text, x+6,y+4); g.setColor(g.theme.fg).setFont("6x8", 1).setFontAlign(-1, -1, 0).drawString(text, x+6,y+4);
} }
if (options.render) { if (options.render) {

View File

@ -8,3 +8,4 @@
0.08: Don't turn on screen during Quiet Mode 0.08: Don't turn on screen during Quiet Mode
0.09: Add onHide callback 0.09: Add onHide callback
0.10: Ensure dismissing a notification dismissal doesn't enter launcher if in clock mode 0.10: Ensure dismissing a notification dismissal doesn't enter launcher if in clock mode
0.11: Improvements to help notifications work with themes, Bangle.js 2 support

View File

@ -50,22 +50,24 @@ exports.show = function(options) {
if (options.on===undefined) options.on=true; if (options.on===undefined) options.on=true;
id = ("id" in options)?options.id:null; id = ("id" in options)?options.id:null;
let size = options.size||120; let size = options.size||120;
if (size>120) {size=120} if (size>120) size=120;
Bangle.setLCDMode("direct"); try { Bangle.setLCDMode("direct"); } catch(e) {} // not supported/needed on Bangle.js 2
let x = 0, let x = 0,
y = 40, y = 40,
w = 240, w = g.getWidth(),
h = size; h = size;
// clear screen // clear screen
g.setColor(options.bgColor||0).fillRect(0,0,g.getWidth(),g.getHeight()); g.reset();
if (options.bgColor!==undefined) g.setColor(options.bgColor);
g.clearRect(0,0,g.getWidth(),g.getHeight());
// top bar // top bar
if (options.title||options.src) { if (options.title||options.src) {
const title = options.title || options.src const title = options.title || options.src;
g.setColor(options.titleBgColor||0x39C7).fillRect(x, y, x+w-1, y+30); g.setColor(options.titleBgColor||"#333").fillRect(x, y, x+w-1, y+30);
g.setColor(-1).setFontAlign(-1, -1, 0).setFont("6x8", 3); g.setColor(g.theme.fg).setFontAlign(-1, -1, 0).setFont("6x8", 3);
g.drawString(title.trim().substring(0, 13), x+5, y+3); g.drawString(title.trim().substring(0, 13), x+5, y+3);
if (options.title && options.src) { if (options.title && options.src) {
g.setColor(-1).setFontAlign(1, 1, 0).setFont("6x8", 2); g.setColor(g.theme.fg).setFontAlign(1, 1, 0).setFont("6x8", 2);
// above drawing area, but we are fullscreen // above drawing area, but we are fullscreen
g.drawString(options.src.substring(0, 10), w-16, y-4); g.drawString(options.src.substring(0, 10), w-16, y-4);
} }
@ -73,8 +75,8 @@ exports.show = function(options) {
} }
if (options.icon) { if (options.icon) {
let i = options.icon, iw,ih; let i = options.icon, iw,ih;
if ("string"==typeof i) {iw=i.charCodeAt(0); ih=i.charCodeAt(1)} if ("string"==typeof i) {iw=i.charCodeAt(0); ih=i.charCodeAt(1);}
else {iw=i[0]; ih=i[1]} else {iw=i[0]; ih=i[1];}
const iy=y ? (y+4) : (h-ih)/2; // show below title bar if present, otherwise center vertically const iy=y ? (y+4) : (h-ih)/2; // show below title bar if present, otherwise center vertically
g.drawImage(i, x+4,iy); g.drawImage(i, x+4,iy);
x += iw+4;w -= iw+4; x += iw+4;w -= iw+4;
@ -84,16 +86,13 @@ exports.show = function(options) {
const maxRows = Math.floor((h-4)/16), // font=2*(6x8) const maxRows = Math.floor((h-4)/16), // font=2*(6x8)
maxChars = Math.floor((w-4)/12), maxChars = Math.floor((w-4)/12),
text=fitWords(options.body, maxRows, maxChars); text=fitWords(options.body, maxRows, maxChars);
g.setColor(-1).setFont("6x8", 2).setFontAlign(-1, -1, 0).drawString(text, x+4, y+4); g.setColor(g.theme.fg).setFont("6x8", 2).setFontAlign(-1, -1, 0).drawString(text, x+4, y+4);
} }
if (options.render) { if (options.render)
const area={x:x, y:y, w:w, h:h} options.render({x:x, y:y, w:w, h:h});
options.render(area); if (options.on && !(require('Storage').readJSON('setting.json',1)||{}).quiet)
}
if (options.on && !(require('Storage').readJSON('setting.json',1)||{}).quiet) {
Bangle.setLCDPower(1); // light up Bangle.setLCDPower(1); // light up
}
Bangle.on("touch", exports.hide); Bangle.on("touch", exports.hide);
if (options.onHide) if (options.onHide)
hideCallback = options.onHide; hideCallback = options.onHide;

1
apps/waveclk/ChangeLog Normal file
View File

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

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

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwge27dtAX4C+/dt+wFB/wCECIu3/dvBYNv34RC7/tCIu//99EYN9C4IpB74jG3379ovDFIIRBEYxHD/47D2wjHCIX+AQJHBCIIXBNZt/+5QBEZIgBAQX///9EZRWBBARHDEwhlC/9/EAJoBDQIOBNwyPCEYYCDJQ4CJSQ4CB0O2lojL2lwBIXFiwsK0f/KgUbuwRJo6cBPAO34cUmJHH7U/97tBgEGBIODEY/RXoOw7cAgHbtlxoojGx7hCjAjD20ANA1378MEIIAB4d0u5HGNAPYCAYAB2n2SQSPDjv3CIsF2lxEYto//+CoOGCIUt0O3EYtHvqMBvlw4UAgJQBqIjERgQDBsO+7FAhaMH64DB+4qB+3AgARG9uhIgQJD4dghd+7dLBQZoBaISwC4cArf27dpCIf/23f9uHCIQABhoNClsl20ttuwgYKBEAIAChOmCIOH/vx9ttwB2BDgMBAoIRBmnbpkbtk2ltsgAMCJQOwAgMBk+eq3AhiSBsE2GAX//4WCAAOBAoVbtt8mCJBgHHfYMHdgoRBott+zmDsEAn/tEwkC7UAVoYACgPbv4REAASTBEYY0BPIPwCJAjEu3Dvq8BCAnbsEwGgm2jbAC8EAjFvEAQREjuwDQXbvvx7cd2K/EgEb9oRCAoLOBjEAgk/A="))

72
apps/waveclk/app.js Normal file
View File

@ -0,0 +1,72 @@
function getImg() {
return require("heatshrink").decompress(atob("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"));
}
var IMAGEWIDTH = 176;
var IMAGEHEIGHT = 109;
Graphics.prototype.setFontZCOOL = function() {
// Actual height 40 (46 - 7)
var widths = atob("CxAhEh8hJCIjGSMdCw==");
var font = atob("AAAAAAAAAAAAAAAEAAAAAAAPAAAAAAAP4AAAAAAD+AAAAAAA/gAAAAAAP8AAAAAAB/AAAAAAAfAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAcAAAAAAAfgAAAAAAf4AAAAAA/+AAAAAA//AAAAAB/+AAAAAB/+AAAAAD/8AAAAAD/8AAAAAD/4AAAAAH/4AAAAAH/wAAAAAD/wAAAAAAfwAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAABgAAAAAAP8AAAAAB//gAAAAP//4AAAB////AAAP////wAAf///h8AAH//8AfAAD//gAHwAA/8AAB8AAPgAAAfAAD4AAAHwAAfAAAB+AAHwAAAPgAB8AAAD4AAfAAAA+AAHwAAAPgAB8AAAD4AAPgAAA+AAD4AAAPgAA+AAAD8AAPgAAAfAAD4AAAHwAA/////8AAH/////AAB/////wAAf////4AAD////8AAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAAB8AAAAAAA/AAAAAAAfgAAAAAAH4AAAAAAD8AAAAAAB+AAAAAAA/AAAAAAAPwAAH/gAH4D///4AD/////+AB//////gAf/////4AD////gAAA//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAB8AAAGAAAfAAAD4AAPwAAB/AAH4AAA/wAB8AAAf8AA/AAAP/AAfgAAP/gAHwAAH/4AD8AAD++AB+AAB/PgAfAAA/j4AHwAAfw+AB+AAP4PgAfwAH8D4AD8AH+A+AAfgD/AfAAD8B/AHwAAfg/gB8AAD8fwAfAAA/v4AHwAAH/8AB8AAA/+AAfAAAH/AAHwAAA/AAB8AAAHgAA+AAAAAAAPgAAAAAAD4AAAAAAA+AAAAAAAAAAAAAAABAAAAAAAD4AAAAAAA+AAAAAAAPgAAAAAAD4AAAAAAA+AAAAAAAHwAAMAAAB8AAfAAAAfAAHwAAAHwAB8AAAA+AAfAAAAPgAHwAfAD4AA+AP4A+AAPgD+APgAD4B/gB8AA+A/8AfAAPgf/AHwAD4H/wB8AA+D98AfgAHh+fgD4AB4/D4A+AAffg+A/gAH34Pg/4AB/8D8f8AAf+Aff+AAH/AH/+AAA/wB/+AAAP4Af+AAAD8AD/AAAA+AA/AAAAHgADAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAD+AAAAAAH/gAAAAAH/4AAAAAP/+AAAAAP//gAAAAf/z4AAAAf/w+AAAA//gPgAAA//gD4AAAf/AA+AAAH/AAPgAAB/AAD4AAAeAAA+AAACAAAPgAAAAAAD4AAAAAAA+AAAAAAAPgAAAAAAD4AAAAAAA+AAAAAAAP/+AAAAP///gAB/////4AAf////+AAH/////AAB///+AAAAf/APgAAAAAAD4AAAAAAA+AAAAAAAPgAAAAAAD4AAAAAAA+AAAAAAAPgAAAAAAD4AAAAAAAAAAAAAAAACAAAAAAAHwAAAAAAB8AAAAAAAfAAAAAAAH4AAAAAAA+AAAAAAAPgAAAAAAD4AAAAfwA+AAAH/+AHwAAf//gB8AAP//4AfAAH//+AHwAB/+PwB+AAfwB8APgAHwAfAD4AB8AHwA+AAfAB8APgAHwAfAB8AB8AHwAfAAfAB8AHwAHwAfAB8AB8AH4AfAAfAA+AH4AHwAPgH+AB8AD4H/gAfAA+H/wAHwAPv/4AB8AD//4AAfAA//wAAHwAP/wAAB8AD/gAAAAAAfgAAAAAABAAAAAAAAAAAAAAAAAHwAAAAAAH8AAAAAAD/gAAAAAD/4AAAAAD/+AAAAAD/3wAAAAD/x8AAAAD/4PgAAAD/4D4AAAD/4A+AAAB/4AHwAAB/4AB8AAB/4AAfAAB/4CAD4AB/8DgA+AAf8B8APgAD8AfAB8AA8AH4AfAAEAA+AH4AAAAPgA+AAAAD8AfgAAAAfAP4AAAAH4P8AAAAA+H+AAAAAPj+AAAAAB9/AAAAAAf/gAAAAAH/wAAAAAA/4AAAAAAP8AAAAAAB8AAAAAAAOAAAAAAAAAAAAAAAAAAAAD4AAAAAAA+AAAAAAAPwAAAAAAB8AAAAAAAfAAAAAAAHwAAAAAAB8AAAAAAAfAAAAAAAHwAAADAAB8AAAD4AAfAAAD+AAHwAAH/wAB8AAH/4AAfgAH/4AAH4AH/4AAA+AH/wAAAPgH/wAAAD4H/wAAAA+H/wAAAAPv/wAAAAD//wAAAAA//gAAAAAP/gAAAAAD/gAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAAAH4AAAAAAB+AAAHwAA/wAAB8AAf8AAA/gAP/AAAP8AH/wAAH/gD8+AAB/4B+PgAAf/A/j4AAPn4Pw+AAD4/H4HwAB+Pz8B8AAfB/+AfAAPwP/AHwAD4B/gB+AA+AfwAPgAfAP+AD4AHwD/wA+AB+B/+AfgAfw/PgPwAB/fj8D8AAP/wfh+AAB/4D8fAAAP+AfPwAAB/AH/4AAAPgA/8AAAAAAH/AAAAAAA/gAAAAAAPwAAAAAAB8AAAAAAAAAAAAAAAAAAAAAGAAAAAAAD4AAAAAAB/AAAAAAB/wAAAAAA/+AAAAAAf/gAAAAAf78AAAAAP8fAAAAAP+H4AAAAD+A+AAAAB/APgAAAAfAB8AAAAH4AfAAYAA+AD4AfAAPgA+AfwAD4APgf+AA+ABgf/AAHwAA//AAB8AA//AAAfAA//AAAHwA//AAAB+A/+AAAAPh/+AAAAD5/+AAAAA//+AAAAAP/+AAAAAB/8AAAAAAf8AAAAAAH8AAAAAAAAAAAAAAAAADgAOAAAAH4AfgAAAD+AP4AAAA/wD/AAAAH8AfwAAAB/AH8AAAAfwB/AAAADgAOAAAAAAAAAAAAAAAAA=");
var scale = 1; // size multiplier for this font
g.setFontCustom(font, 46, widths, 50+(scale<<8)+(1<<16));
};
// timeout used to update every minute
var drawTimeout;
// schedule a draw for the next minute
function queueDraw() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, 60000 - (Date.now() % 60000));
}
function draw() {
var x = g.getWidth()/2;
var y = 24+20;
g.reset().clearRect(0,24,g.getWidth(),g.getHeight()-IMAGEHEIGHT);
if (g.getWidth() == IMAGEWIDTH)
g.drawImage(getImg(),0,g.getHeight()-IMAGEHEIGHT);
else {
let scale = g.getWidth()/IMAGEWIDTH;
y *= scale;
g.drawImage(getImg(),0,g.getHeight()-IMAGEHEIGHT*scale,{scale:scale});
}
// work out locale-friendly date/time
var date = new Date();
var timeStr = require("locale").time(date,1);
var dateStr = require("locale").date(date).toUpperCase();
// draw time
g.setFontAlign(0,0).setFont("ZCOOL");
g.drawString(timeStr,x,y);
// draw date
y += 35;
g.setFontAlign(0,0,1).setFont("6x8");
g.drawString(dateStr,g.getWidth()-8,g.getHeight()/2);
// queue draw in one minute
queueDraw();
}
// Clear the screen once, at startup
g.setTheme({bg:"#f0f",fg:"#fff",dark:true}).clear();
// draw immediately at first, queue update
draw();
// Stop updates when LCD is off, restart when on
Bangle.on('lcdPower',on=>{
if (on) {
draw(); // draw immediately, queue redraw
} else { // stop draw timer
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
}
});
// Load widgets
Bangle.loadWidgets();
Bangle.drawWidgets();
// Show launcher when middle button pressed
Bangle.setUI("clock");

BIN
apps/waveclk/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
apps/waveclk/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -4,3 +4,4 @@
0.05: Add wind direction. 0.05: Add wind direction.
0.06: Use setUI for launcher. 0.06: Use setUI for launcher.
0.07: Add theme support and unknown icon. 0.07: Add theme support and unknown icon.
0.08: Refactor and reduce widget ram usage.

View File

@ -1,5 +1,6 @@
(() => { (() => {
const weather = require('weather'); const weather = require('weather');
let current = weather.get();
function formatDuration(millis) { function formatDuration(millis) {
let pluralize = (n, w) => n + " " + w + (n == 1 ? "" : "s"); let pluralize = (n, w) => n + " " + w + (n == 1 ? "" : "s");
@ -10,16 +11,15 @@
} }
function draw() { function draw() {
let w = weather.current;
g.reset(); g.reset();
g.clearRect(0, 24, 239, 239); g.clearRect(0, 24, 239, 239);
weather.drawIcon(w.txt, 65, 90, 55); weather.drawIcon(current.txt, 65, 90, 55);
const locale = require("locale"); const locale = require("locale");
g.reset(); g.reset();
const temp = locale.temp(w.temp-273.15).match(/^(\D*\d*)(.*)$/); const temp = locale.temp(current.temp-273.15).match(/^(\D*\d*)(.*)$/);
let width = g.setFont("Vector", 40).stringWidth(temp[1]); let width = g.setFont("Vector", 40).stringWidth(temp[1]);
width += g.setFont("Vector", 20).stringWidth(temp[2]); width += g.setFont("Vector", 20).stringWidth(temp[2]);
g.setFont("Vector", 40).setFontAlign(-1, -1, 0); g.setFont("Vector", 40).setFontAlign(-1, -1, 0);
@ -31,19 +31,19 @@
g.setFontAlign(-1, 0, 0); g.setFontAlign(-1, 0, 0);
g.drawString("Humidity", 135, 130); g.drawString("Humidity", 135, 130);
g.setFontAlign(1, 0, 0); g.setFontAlign(1, 0, 0);
g.drawString(w.hum+"%", 225, 130); g.drawString(current.hum+"%", 225, 130);
if ('wind' in w) { if ('wind' in current) {
g.setFontAlign(-1, 0, 0); g.setFontAlign(-1, 0, 0);
g.drawString("Wind", 135, 142); g.drawString("Wind", 135, 142);
g.setFontAlign(1, 0, 0); g.setFontAlign(1, 0, 0);
g.drawString(locale.speed(w.wind)+' '+w.wrose.toUpperCase(), 225, 142); g.drawString(locale.speed(current.wind)+' '+current.wrose.toUpperCase(), 225, 142);
} }
g.setFont("6x8", 2).setFontAlign(0, 0, 0); g.setFont("6x8", 2).setFontAlign(0, 0, 0);
g.drawString(w.loc, 120, 170); g.drawString(current.loc, 120, 170);
g.setFont("6x8", 1).setFontAlign(0, 0, 0); g.setFont("6x8", 1).setFontAlign(0, 0, 0);
g.drawString(w.txt.charAt(0).toUpperCase()+w.txt.slice(1), 120, 190); g.drawString(current.txt.charAt(0).toUpperCase()+current.txt.slice(1), 120, 190);
drawUpdateTime(); drawUpdateTime();
@ -51,8 +51,8 @@
} }
function drawUpdateTime() { function drawUpdateTime() {
if (!weather.current || !weather.current.time) return; if (!current || !current.time) return;
let text = `Last update received ${formatDuration(Date.now() - weather.current.time)} ago`; let text = `Last update received ${formatDuration(Date.now() - current.time)} ago`;
g.reset(); g.reset();
g.clearRect(0, 202, 239, 210); g.clearRect(0, 202, 239, 210);
g.setFont("6x8", 1).setFontAlign(0, 0, 0); g.setFont("6x8", 1).setFontAlign(0, 0, 0);
@ -60,8 +60,9 @@
} }
function update() { function update() {
current = weather.get();
NRF.removeListener("connect", update); NRF.removeListener("connect", update);
if (weather.current) { if (current) {
draw(); draw();
} else if (NRF.getSecurityStatus().connected) { } else if (NRF.getSecurityStatus().connected) {
E.showMessage("Weather unknown\n\nIs Gadgetbridge\nweather reporting\nset up on your\nphone?"); E.showMessage("Weather unknown\n\nIs Gadgetbridge\nweather reporting\nset up on your\nphone?");

View File

@ -1,6 +1,6 @@
const storage = require('Storage'); const storage = require('Storage');
let expiryTimeout = undefined; let expiryTimeout;
function scheduleExpiry(json) { function scheduleExpiry(json) {
if (expiryTimeout) { if (expiryTimeout) {
clearTimeout(expiryTimeout); clearTimeout(expiryTimeout);
@ -9,53 +9,35 @@ function scheduleExpiry(json) {
let expiry = "expiry" in json ? json.expiry : 2*3600000; let expiry = "expiry" in json ? json.expiry : 2*3600000;
if (json.weather && json.weather.time && expiry) { if (json.weather && json.weather.time && expiry) {
let t = json.weather.time + expiry - Date.now(); let t = json.weather.time + expiry - Date.now();
expiryTimeout = setTimeout(() => { expiryTimeout = setTimeout(update, t);
expiryTimeout = undefined;
let json = storage.readJSON('weather.json')||{};
delete json.weather;
storage.write('weather.json', json);
exports.current = undefined;
exports.emit("update");
}, t);
} }
} }
/**
* Convert numeric direction into human-readable label
*
* @param {number} deg - Direction in degrees
* @return {string|null} - Nearest compass point
*/
function compassRose(deg) {
if (typeof deg === 'undefined') return null;
while (deg<0 || deg>360) {
deg = (deg+360)%360;
}
return ['n','ne','e','se','s','sw','w','nw','n'][Math.floor((deg+22.5)/45)];
}
function setCurrentWeather(json) {
scheduleExpiry(json);
exports.current = json.weather;
}
function update(weatherEvent) { function update(weatherEvent) {
let weather = Object.assign({}, weatherEvent);
weather.time = Date.now();
if ('wdir' in weather) {
weather.wrose = compassRose(weather.wdir);
}
delete weather.t;
let json = storage.readJSON('weather.json')||{}; let json = storage.readJSON('weather.json')||{};
if (weatherEvent) {
let weather = weatherEvent.clone();
delete weather.t;
weather.time = Date.now();
if (weather.wdir != null) {
// Convert numeric direction into human-readable label
let deg = weather.wdir;
while (deg<0 || deg>360) {
deg = (deg+360)%360;
}
weather.wrose = ['n','ne','e','se','s','sw','w','nw','n'][Math.floor((deg+22.5)/45)];
}
json.weather = weather; json.weather = weather;
}
else {
delete json.weather;
}
storage.write('weather.json', json); storage.write('weather.json', json);
scheduleExpiry(json);
setCurrentWeather(json); exports.emit("update", json.weather);
exports.emit("update");
} }
const _GB = global.GB; const _GB = global.GB;
@ -64,7 +46,11 @@ global.GB = (event) => {
if (_GB) setTimeout(_GB, 0, event); if (_GB) setTimeout(_GB, 0, event);
}; };
setCurrentWeather(storage.readJSON('weather.json')||{}); exports.get = function() {
return storage.readJSON('weather.json').weather;
}
scheduleExpiry(storage.readJSON('weather.json')||{});
exports.drawIcon = function(cond, x, y, r) { exports.drawIcon = function(cond, x, y, r) {
function drawSun(x, y, r) { function drawSun(x, y, r) {

View File

@ -1,8 +1,37 @@
(() => { (() => {
const weather = require('weather'); const weather = require('weather');
function draw() { var dirty = false;
const w = weather.current;
weather.on("update", w => {
if (w) {
if (!WIDGETS["weather"].width) {
WIDGETS["weather"].width = 20;
Bangle.drawWidgets();
} else if (Bangle.isLCDOn()) {
WIDGETS["weather"].draw();
} else {
dirty = true;
}
}
else {
WIDGETS["weather"].width = 0;
Bangle.drawWidgets();
}
});
Bangle.on('lcdPower', on => {
if (on && dirty) {
WIDGETS["weather"].draw();
dirty = false;
}
});
WIDGETS["weather"] = {
area: "tl",
width: weather.get() ? 20 : 0,
draw: function() {
const w = weather.get();
if (!w) return; if (!w) return;
g.reset(); g.reset();
g.clearRect(this.x, this.y, this.x+this.width-1, this.y+23); g.clearRect(this.x, this.y, this.x+this.width-1, this.y+23);
@ -17,41 +46,6 @@
g.setFont('6x8', 1); g.setFont('6x8', 1);
g.drawString(t, this.x+10, this.y+24); g.drawString(t, this.x+10, this.y+24);
} }
} },
var dirty = false;
function update() {
if (!WIDGETS["weather"].width) {
WIDGETS["weather"].width = 20;
Bangle.drawWidgets();
} else if (Bangle.isLCDOn()) {
WIDGETS["weather"].draw();
} else {
dirty = true;
}
}
function hide() {
WIDGETS["weather"].width = 0;
Bangle.drawWidgets();
}
weather.on("update", () => {
if (weather.current) update();
else hide();
});
Bangle.on('lcdPower', on => {
if (on && dirty) {
WIDGETS["weather"].draw();
dirty = false;
}
});
WIDGETS["weather"] = {
area: "tl",
width: weather.current ? 20 : 0,
draw: draw,
}; };
})(); })();

View File

@ -15,3 +15,7 @@
0.16: Settings option to show large digits in widget area 0.16: Settings option to show large digits in widget area
0.17: Cope with 2v10+ firmware sometimes reporting >1 step 0.17: Cope with 2v10+ firmware sometimes reporting >1 step
0.18: Adjust widget width when displaying large text 0.18: Adjust widget width when displaying large text
0.19: Allow goal in large font mode
Stop goal drawing outside widget area
Fix issue with widget overwrite in large font mode
Memory usage enhancements

View File

@ -32,7 +32,7 @@
onchange: (g) => { onchange: (g) => {
s.goal = g s.goal = g
s.progress = !!g s.progress = !!g
save() save();
}, },
}, },
'Show Progress': { 'Show Progress': {
@ -40,7 +40,7 @@
format: () => (s.progress ? 'Yes' : 'No'), format: () => (s.progress ? 'Yes' : 'No'),
onchange: () => { onchange: () => {
s.progress = !s.progress s.progress = !s.progress
save() save();
}, },
}, },
'Large Digits': { 'Large Digits': {
@ -48,7 +48,7 @@
format: () => (s.large ? 'Yes' : 'No'), format: () => (s.large ? 'Yes' : 'No'),
onchange: () => { onchange: () => {
s.large = !s.large s.large = !s.large
save() save();
}, },
}, },
'Hide Widget': { 'Hide Widget': {
@ -56,7 +56,7 @@
format: () => (s.hide ? 'Yes' : 'No'), format: () => (s.hide ? 'Yes' : 'No'),
onchange: () => { onchange: () => {
s.hide = !s.hide s.hide = !s.hide
save() save();
}, },
}, },
'< Back': back, '< Back': back,

View File

@ -23,78 +23,6 @@
return (key in settings) ? settings[key] : DEFAULTS[key]; return (key in settings) ? settings[key] : DEFAULTS[key];
} }
function drawProgress(stps) {
if (setting('hide')) return;
const width = 24, half = width/2;
const goal = setting('goal'), left = Math.max(goal-stps,0);
const c = left ? "#00f" : "#090"; // blue or dark green
g.setColor(c).fillCircle(this.x + half, this.y + half, half);
const TAU = Math.PI*2;
if (left) {
const f = left/goal; // fraction to blank out
let p = [];
p.push(half,half);
p.push(half,0);
if(f>1/8) p.push(0,0);
if(f>2/8) p.push(0,half);
if(f>3/8) p.push(0,width);
if(f>4/8) p.push(half,width);
if(f>5/8) p.push(width,width);
if(f>6/8) p.push(width,half);
if(f>7/8) p.push(width,0);
p.push(half - Math.sin(f * TAU) * half);
p.push(half - Math.cos(f * TAU) * half);
for (let i = p.length; i; i -= 2) {
p[i - 2] += this.x;
p[i - 1] += this.y;
}
g.setColor(g.theme.bg).fillPoly(p);
}
}
// show the step count in the widget area in a readable sized font
function draw_large(st) {
this.width = 12 * st.length + 3;
g.reset();
g.clearRect(this.x, this.y, this.x + this.width, this.y + 16); // erase background
g.setColor(g.theme.fg);
g.setFont("6x8",2);
g.setFontAlign(-1, -1);
g.drawString(st, this.x + 4, this.y + 2);
}
// draw your widget
function draw() {
if (setting('hide')) return;
var width = 24;
if (stp_today > 99999){
stp_today = stp_today % 100000; // cap to five digits + comma = 6 characters
}
let stps = stp_today.toString();
if (setting('large')) {
draw_large.call(this, stps);
return;
}
g.reset().clearRect(this.x, this.y, this.x + width, this.y + 23); // erase background
if (setting('progress')){ drawProgress.call(this, stps); }
g.setColor(g.theme.fg);
if (stps.length > 3){
stps = stps.slice(0,-3) + "," + stps.slice(-3);
g.setFont("4x6", 1); // if big, shrink text to fix
} else {
g.setFont("6x8", 1);
}
g.setFontAlign(0, 0); // align to x: center, y: center
g.drawString(stps, this.x+width/2, this.y+19);
// on low bpp screens, draw 1 bit. Currently there is no getBPP so we just do it based on resolution
g.drawImage(atob("CgoCLguH9f2/7+v6/79f56CtAAAD9fw/n8Hx9A=="),this.x+(width-10)/2,this.y+2);
}
function reload() {
loadSettings()
draw()
}
Bangle.on('step', stepCount => { Bangle.on('step', stepCount => {
var steps = stepCount-lastStepCount; var steps = stepCount-lastStepCount;
if (lastStepCount===undefined || steps<0) steps=1; if (lastStepCount===undefined || steps<0) steps=1;
@ -115,11 +43,11 @@
} }
lastUpdate = date lastUpdate = date
//console.log("up: " + up + " stp: " + stp_today + " " + date.toString()); //console.log("up: " + up + " stp: " + stp_today + " " + date.toString());
if (Bangle.isLCDOn()) WIDGETS["wpedom"].draw(); WIDGETS["wpedom"].redraw();
}); });
// redraw when the LCD turns on // redraw when the LCD turns on
Bangle.on('lcdPower', function(on) { Bangle.on('lcdPower', function(on) {
if (on) WIDGETS["wpedom"].draw(); if (on) WIDGETS["wpedom"].redraw();
}); });
// When unloading, save state // When unloading, save state
E.on('kill', () => { E.on('kill', () => {
@ -134,8 +62,78 @@
// add your widget // add your widget
WIDGETS["wpedom"]={area:"tl",width:26, WIDGETS["wpedom"]={area:"tl",width:26,
draw:draw, redraw:function() { // work out the width, and queue a full redraw if needed
reload:reload, let stps = stp_today.toString();
let newWidth = 24;
if (setting('hide'))
newWidth = 0;
else {
if (setting('large')) {
newWidth = 12 * stps.length + 3;
if (setting('progress'))
newWidth += 24;
}
}
if (newWidth!=this.width) {
// width has changed, re-layout all widgets
this.width = newWidth;
Bangle.drawWidgets();
} else {
// width not changed - just redraw
WIDGETS["wpedom"].draw();
}
},
draw:function() {
if (setting('hide')) return;
if (stp_today > 99999)
stp_today = stp_today % 100000; // cap to five digits + comma = 6 characters
let stps = stp_today.toString();
g.reset().clearRect(this.x, this.y, this.x + this.width, this.y + 23); // erase background
if (setting('progress')) {
const width = 23, half = 11;
const goal = setting('goal'), left = Math.max(goal-stps,0);
// blue or dark green
g.setColor(left ? "#08f" : "#080").fillCircle(this.x + half, this.y + half, half);
if (left) {
const TAU = Math.PI*2;
const f = left/goal; // fraction to blank out
let p = [];
p.push(half,half);
p.push(half,0);
if(f>1/8) p.push(0,0);
if(f>2/8) p.push(0,half);
if(f>3/8) p.push(0,width);
if(f>4/8) p.push(half,width);
if(f>5/8) p.push(width,width);
if(f>6/8) p.push(width,half);
if(f>7/8) p.push(width,0);
p.push(half - Math.sin(f * TAU) * half);
p.push(half - Math.cos(f * TAU) * half);
g.setColor(g.theme.bg).fillPoly(g.transformVertices(p,{x:this.x,y:this.y}));
}
g.reset();
}
if (setting('large')) {
g.setFont("6x8",2);
g.setFontAlign(-1, 0);
g.drawString(stps, this.x + (setting('progress')?28:4), this.y + 12);
} else {
let w = 24;
if (stps.length > 3){
stps = stps.slice(0,-3) + "," + stps.slice(-3);
g.setFont("4x6", 1); // if big, shrink text to fix
} else {
g.setFont("6x8", 1);
}
g.setFontAlign(0, 0); // align to x: center, y: center
g.drawString(stps, this.x+w/2, this.y+19);
g.drawImage(atob("CgoCLguH9f2/7+v6/79f56CtAAAD9fw/n8Hx9A=="),this.x+(w-10)/2,this.y+2);
}
},
reload:function() {
loadSettings();
WIDGETS["wpedom"].redraw();
},
getSteps:()=>stp_today getSteps:()=>stp_today
}; };
// Load data at startup // Load data at startup

2
core

@ -1 +1 @@
Subproject commit 2aac601e38d659876eb7db5aebc7a12dd3c39da7 Subproject commit 0fd608f085deff9b39f2db3559ecc88edb232aba

View File

@ -40,6 +40,10 @@
<p id="requireHTTPS" class="hidden"> <p id="requireHTTPS" class="hidden">
<b>STOP!</b> This page <b>must</b> be served over HTTPS. Please <a>reload this page via HTTPS</a>. <b>STOP!</b> This page <b>must</b> be served over HTTPS. Please <a>reload this page via HTTPS</a>.
</p> </p>
<div class="toast toast-success flex-centered" id="bangle2ks">
<img src="data:image/png;base64,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" />
&nbsp;Bangle.js 2 is now on KickStarter! &nbsp;<a href="https://www.kickstarter.com/projects/gfw/banglejs-2-the-open-smart-watch" target="_blank">Check it out here</a>
</div>
</div> </div>

View File

@ -154,16 +154,20 @@ function touchHandler(l,e) {
if (l.c) l.c.forEach(n => touchHandler(n,e)); if (l.c) l.c.forEach(n => touchHandler(n,e));
} }
function prepareLazyRender(l, rectsToClear, drawList, rects, bgCol) { function prepareLazyRender(l, rectsToClear, drawList, rects, parentBg) {
if ((l.bgCol != null && l.bgCol != bgCol) || l.type == "txt" || l.type == "btn" || l.type == "img" || l.type == "custom") { var bgCol = l.bgCol == null ? parentBg : g.toColor(l.bgCol);
if (bgCol != parentBg || l.type == "txt" || l.type == "btn" || l.type == "img" || l.type == "custom") {
// Hash the layoutObject without including its children // Hash the layoutObject without including its children
let c = l.c; var c = l.c;
delete l.c; delete l.c;
let hash = "H"+E.CRC32(E.toJS(l)); // String keys maintain insertion order var hash = "H"+E.CRC32(E.toJS(l)); // String keys maintain insertion order
if (c) l.c = c; if (c) l.c = c;
if (!delete rectsToClear[hash]) { if (!delete rectsToClear[hash]) {
rects[hash] = {bg: bgCol, r: [l.x,l.y,l.x+l.w-1,l.y+l.h-1]}; rects[hash] = {
bg: parentBg == null ? g.theme.bg : parentBg,
r: [l.x,l.y,l.x+l.w-1,l.y+l.h-1]
};
if (drawList) { if (drawList) {
drawList.push(l); drawList.push(l);
drawList = null; // Prevent children from being redundantly added to the drawList drawList = null; // Prevent children from being redundantly added to the drawList
@ -171,7 +175,7 @@ function prepareLazyRender(l, rectsToClear, drawList, rects, bgCol) {
} }
} }
if (l.c) for (let ch of l.c) prepareLazyRender(ch, rectsToClear, drawList, rects, l.bgCol == null ? bgCol : l.bgCol); if (l.c) for (var ch of l.c) prepareLazyRender(ch, rectsToClear, drawList, rects, bgCol);
} }
Layout.prototype.render = function (l) { Layout.prototype.render = function (l) {
@ -189,20 +193,24 @@ Layout.prototype.render = function (l) {
"txt":function(l){ "txt":function(l){
g.setFont(l.font,l.fsz).setFontAlign(0,0,l.r).drawString(l.label, l.x+(l.w>>1), l.y+(l.h>>1)); g.setFont(l.font,l.fsz).setFontAlign(0,0,l.r).drawString(l.label, l.x+(l.w>>1), l.y+(l.h>>1));
}, "btn":function(l){ }, "btn":function(l){
var x = l.x+(0|l.pad);
var y = l.y+(0|l.pad);
var w = l.w-(l.pad<<1);
var h = l.h-(l.pad<<1);
var poly = [ var poly = [
l.x,l.y+4, x,y+4,
l.x+4,l.y, x+4,y,
l.x+l.w-5,l.y, x+w-5,y,
l.x+l.w-1,l.y+4, x+w-1,y+4,
l.x+l.w-1,l.y+l.h-5, x+w-1,y+h-5,
l.x+l.w-5,l.y+l.h-1, x+w-5,y+h-1,
l.x+4,l.y+l.h-1, x+4,y+h-1,
l.x,l.y+l.h-5, x,y+h-5,
l.x,l.y+4 x,y+4
]; ];
g.setColor(g.theme.bgH).fillPoly(poly).setColor(l.selected ? g.theme.fgH : g.theme.fg).drawPoly(poly).setFont("4x6",2).setFontAlign(0,0,l.r).drawString(l.label,l.x+l.w/2,l.y+l.h/2); g.setColor(g.theme.bgH).fillPoly(poly).setColor(l.selected ? g.theme.fgH : g.theme.fg).drawPoly(poly).setFont("4x6",2).setFontAlign(0,0,l.r).drawString(l.label,l.x+l.w/2,l.y+l.h/2);
}, "img":function(l){ }, "img":function(l){
g.drawImage(l.src(), l.x, l.y); g.drawImage(l.src(), l.x + (0|l.pad), l.y + (0|l.pad));
}, "custom":function(l){ }, "custom":function(l){
l.render(l); l.render(l);
},"h":function(l) { l.c.forEach(render); }, },"h":function(l) { l.c.forEach(render); },
@ -216,7 +224,7 @@ Layout.prototype.render = function (l) {
if (!this.rects) this.rects = {}; if (!this.rects) this.rects = {};
var rectsToClear = this.rects.clone(); var rectsToClear = this.rects.clone();
var drawList = []; var drawList = [];
prepareLazyRender(l, rectsToClear, drawList, this.rects, g.getBgColor()); prepareLazyRender(l, rectsToClear, drawList, this.rects, null);
for (var h in rectsToClear) delete this.rects[h]; for (var h in rectsToClear) delete this.rects[h];
var clearList = Object.keys(rectsToClear).map(k=>rectsToClear[k]).reverse(); // Rects are cleared in reverse order so that the original bg color is restored var clearList = Object.keys(rectsToClear).map(k=>rectsToClear[k]).reverse(); // Rects are cleared in reverse order so that the original bg color is restored
for (var r of clearList) g.setBgColor(r.bg).clearRect.apply(g, r.r); for (var r of clearList) g.setBgColor(r.bg).clearRect.apply(g, r.r);
@ -231,39 +239,29 @@ Layout.prototype.layout = function (l) {
// exw,exh = extra width/height available // exw,exh = extra width/height available
switch (l.type) { switch (l.type) {
case "h": { case "h": {
let x = l.x + (l.w-l._w)/2; var x = l.x + (0|l.pad);
var fillx = l.c && l.c.reduce((a,l)=>a+(0|l.fillx),0); var fillx = l.c && l.c.reduce((a,l)=>a+(0|l.fillx),0);
if (fillx) { x = l.x; } if (!fillx) { x += (l.w-l._w)/2; }
l.c.forEach(c => { l.c.forEach(c => {
c.w = c._w + ((0|c.fillx)*(l.w-l._w)/(fillx||1)); c.w = c._w + ((0|c.fillx)*(l.w-l._w)/(fillx||1));
c.h = c.filly ? l.h : c._h; c.h = c.filly ? l.h - (l.pad<<1) : c._h;
c.x = x; c.x = x;
c.y = l.y + (1+(0|c.valign))*(l.h-c.h)/2; c.y = l.y + (0|l.pad) + (1+(0|c.valign))*(l.h-(l.pad<<1)-c.h)/2;
x += c.w; x += c.w;
if (c.pad) {
x += c.pad*2;
c.w += c.pad*2;
c.h += c.pad*2;
}
if (c.c) this.layout(c); if (c.c) this.layout(c);
}); });
break; break;
} }
case "v": { case "v": {
let y = l.y + (l.h-l._h)/2; var y = l.y + (0|l.pad);;
var filly = l.c && l.c.reduce((a,l)=>a+(0|l.filly),0); var filly = l.c && l.c.reduce((a,l)=>a+(0|l.filly),0);
if (filly) { y = l.y; } if (!filly) { y += (l.h-l._h)/2 }
l.c.forEach(c => { l.c.forEach(c => {
c.w = c.fillx ? l.w : c._w; c.w = c.fillx ? l.w - (l.pad<<1) : c._w;
c.h = c._h + ((0|c.filly)*(l.h-l._h)/(filly||1)); c.h = c._h + ((0|c.filly)*(l.h-l._h)/(filly||1));
c.x = l.x + (1+(0|c.halign))*(l.w-c.w)/2;
c.y = y; c.y = y;
c.x = l.x + (0|l.pad) + (1+(0|c.halign))*(l.w-(l.pad<<1)-c.w)/2;
y += c.h; y += c.h;
if (c.pad) {
y += c.pad*2;
c.w += c.pad*2;
c.h += c.pad*2;
}
if (c.c) this.layout(c); if (c.c) this.layout(c);
}); });
break; break;
@ -274,6 +272,8 @@ Layout.prototype.debug = function(l,c) {
if (!l) l = this._l; if (!l) l = this._l;
c=c||1; c=c||1;
g.setColor(c&1,c&2,c&4).drawRect(l.x+c-1, l.y+c-1, l.x+l.w-c, l.y+l.h-c); g.setColor(c&1,c&2,c&4).drawRect(l.x+c-1, l.y+c-1, l.x+l.w-c, l.y+l.h-c);
if (l.pad)
g.drawRect(l.x+l.pad-1, l.y+l.pad-1, l.x+l.w-l.pad, l.y+l.h-l.pad);
c++; c++;
if (l.c) l.c.forEach(n => this.debug(n,c)); if (l.c) l.c.forEach(n => this.debug(n,c));
}; };
@ -288,8 +288,8 @@ Layout.prototype.update = function() {
if (l.r&1) { // rotation if (l.r&1) { // rotation
var t = l._w;l._w=l._h;l._h=t; var t = l._w;l._w=l._h;l._h=t;
} }
l._w = Math.max(l._w, 0|l.width); l._w = Math.max(l._w + (l.pad<<1), 0|l.width);
l._h = Math.max(l._h, 0|l.height); l._h = Math.max(l._h + (l.pad<<1), 0|l.height);
} }
var cb = { var cb = {
"txt" : function(l) { "txt" : function(l) {
@ -327,16 +327,16 @@ Layout.prototype.update = function() {
l._h = 0; l._h = 0;
}, "h": function(l) { }, "h": function(l) {
l.c.forEach(updateMin); l.c.forEach(updateMin);
l._h = l.c.reduce((a,b)=>Math.max(a,b._h+(b.pad<<1)),0); l._h = l.c.reduce((a,b)=>Math.max(a,b._h),0);
l._w = l.c.reduce((a,b)=>a+b._w+(b.pad<<1),0); l._w = l.c.reduce((a,b)=>a+b._w,0);
if (l.c.some(c=>c.fillx)) l.fillx = 1; if (l.fillx == null && l.c.some(c=>c.fillx)) l.fillx = 1;
if (l.c.some(c=>c.filly)) l.filly = 1; if (l.filly == null && l.c.some(c=>c.filly)) l.filly = 1;
}, "v": function(l) { }, "v": function(l) {
l.c.forEach(updateMin); l.c.forEach(updateMin);
l._h = l.c.reduce((a,b)=>a+b._h+(b.pad<<1),0); l._h = l.c.reduce((a,b)=>a+b._h,0);
l._w = l.c.reduce((a,b)=>Math.max(a,b._w+(b.pad<<1)),0); l._w = l.c.reduce((a,b)=>Math.max(a,b._w),0);
if (l.c.some(c=>c.fillx)) l.fillx = 1; if (l.fillx == null && l.c.some(c=>c.fillx)) l.fillx = 1;
if (l.c.some(c=>c.filly)) l.filly = 1; if (l.filly == null && l.c.some(c=>c.filly)) l.filly = 1;
} }
}; };
updateMin(l); updateMin(l);

BIN
tests/Layout/bin/espruino Executable file

Binary file not shown.

View File

@ -0,0 +1,4 @@
#!/bin/bash
cd `dirname $0`/..
ls tests/*.js | xargs -I{} bin/runtest.sh {}

43
tests/Layout/bin/runtest.sh Executable file
View File

@ -0,0 +1,43 @@
#!/bin/bash
# Requires Linux x64 (for ./espruino)
# Also imagemagick for display
cd `dirname $0`/..
if [ "$#" -ne 1 ]; then
echo "USAGE:"
echo " bin/runtest.sh tests/testxyz.js"
exit 1
fi
# temporary test files
TESTJS=bin/tmp.js
TESTBMP=bin/tmp.bmp
# actual source files
SRCDIR=tests
SRCJS=$1
SRCBMP=$SRCDIR/`basename $SRCJS .js`.bmp
echo "TEST $SRCJS ($SRCBMP)"
cat ../../modules/Layout.js > $TESTJS
echo 'Bangle = {};BTN1=0;process.env = process.env;process.env.HWVERSION=2;' >> $TESTJS
echo 'g = Graphics.createArrayBuffer(176,176,4);' >> $TESTJS
cat $SRCJS >> $TESTJS || exit 1
echo 'layout.render()' >> $TESTJS
#echo 'layout.debug()' >> $TESTJS
echo 'require("fs").writeFileSync("'$TESTBMP'",g.asBMP())' >> $TESTJS
bin/espruino $TESTJS || exit 1
if ! cmp $TESTBMP $SRCBMP >/dev/null 2>&1
then
echo =============================================
echo $TESTBMP $SRCBMP differ
echo ==============================================
convert "+append" $TESTBMP $SRCBMP testresult.bmp
display testresult.bmp
exit 1
else
echo Files are the same
exit 0
fi

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,13 @@
// Based on accellog usage
var layout = new Layout({ type: "v", c: [
{type:"txt", font:"6x8", label:"Samples", pad:2},
{type:"txt", id:"samples", font:"6x8:2", label:" - ", pad:5},
{type:"txt", font:"6x8", label:"Time", pad:2},
{type:"txt", id:"time", font:"6x8:2", label:" - ", pad:5},
{type:"txt", font:"6x8:2", label:"RECORDING", bgCol:"#f00", pad:5, fillx:1},
]
},[ // Buttons...
{label:"STOP", cb:()=>{}}
]);
layout.samples.label = "123";
layout.time.label = "123s";

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,9 @@
g.clear();
var layout = new Layout({type:"h", filly: 0, c: [
{type: "txt", font: "50%", label: "A"},
{type:"v", c: [
{type: "txt", font: "10%", label: "B"},
{filly: 1},
{type: "txt", font: "10%", label: "C"},
]},
]});

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,26 @@
var img = () => ({
width : 8, height : 8, bpp : 4,
transparent : 1,
buffer : E.toArrayBuffer(atob("Ee7uER7u7uHuDuDu7u7u7u7u7u7g7u4OHgAA4RHu7hE="))
});
var layout = new Layout({type: "v", c: [
{type: "txt", font: "6x8", bgCol: "#F00", pad: 5, label: "TEXT"},
{type: "img", font: "6x8", bgCol: "#0F0", pad: 5, src: img},
{type: "btn", font: "6x8", bgCol: "#00F", pad: 5, label: "BTN"},
{type: "v", bgCol: "#F0F", pad: 2, c: [
{type: "txt", font: "6x8", bgCol: "#F00", label: "v with children"},
{type: "txt", font: "6x8", bgCol: "#0F0", halign: -1, label: "halign -1"},
{type: "txt", font: "6x8", bgCol: "#00F", halign: 1, label: "halign 1"},
]},
{type: "h", bgCol: "#0FF", pad: 2, c: [
{type: "txt", font: "6x8:2", bgCol: "#F00", label: "h"},
{type: "txt", font: "6x8", bgCol: "#0F0", valign: -1, label: "valign -1"},
{type: "txt", font: "6x8", bgCol: "#00F", valign: 1, label: "valign 1"},
]},
{type: "h", bgCol: "#FF0", pad: 2, c: [
{type: "v", bgCol: "#0F0", pad: 2, c: [
{type: "txt", font: "6x8", bgCol: "#F00", pad: 2, label: "nested"},
]},
]},
]});

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,3 @@
var layout = new Layout({type:"v", fillx: true, c: [
{type: "txt", font: "10%", halign:1, pad: 20, label: "0123456789"},
]});

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,7 @@
var layout = new Layout({type: "v", c: [
{type: "h", pad: 20, c: [
{type: "txt", font: "10%", label: "abcd"},
{fillx: 1},
{type: "txt", font: "10%", label: "1234"},
]},
]});

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,26 @@
var img = () => ({
width : 8, height : 8, bpp : 4,
transparent : 1,
buffer : E.toArrayBuffer(atob("Ee7uER7u7uHuDuDu7u7u7u7u7u7g7u4OHgAA4RHu7hE="))
});
var layout = new Layout({type: "v", c: [
{type: "txt", font: "6x8", bgCol: "#F00", fillx: 1, pad: 5, label: "TEXT"},
{type: "img", font: "6x8", bgCol: "#0F0", filly: 1, fillx: 1,pad: 5, src: img},
{type: "btn", font: "6x8", bgCol: "#00F", fillx: 1, pad: 5, label: "BTN"},
{type: "v", bgCol: "#F0F", pad: 2, c: [
{type: "txt", font: "6x8", bgCol: "#F00", fillx: 1, filly: 1, label: "v with children"},
{type: "txt", font: "6x8", bgCol: "#0F0", halign: -1, label: "halign -1"},
{type: "txt", font: "6x8", bgCol: "#00F", halign: 1, label: "halign 1"},
]},
{type: "h", bgCol: "#0FF", pad: 2, c: [
{type: "txt", font: "6x8:2", bgCol: "#F00", fillx: 1, filly: 1, label: "h"},
{type: "txt", font: "6x8", bgCol: "#0F0", valign: -1, label: "valign -1"},
{type: "txt", font: "6x8", bgCol: "#00F", valign: 1, label: "valign 1"},
]},
{type: "h", bgCol: "#FF0", pad: 2, c: [
{type: "v", bgCol: "#0F0", pad: 2, c: [
{type: "txt", font: "6x8", bgCol: "#F00", fillx: 1, filly: 1, pad: 2, label: "nested"},
]},
]},
]});