+ Please customize your analog clock for the Bangle.js 2 according to your needs.
+ When finished, click on "Upload" at the bottom of this form.
+
+ (Pressing "Upload" will also backup your current configuration so that you
+ won't have to enter the same settings over and over again when you come back
+ to this page later)
+
+
+
Clock Size Calculation
+
+
+ Click on the desired clock size calculator (if you installed some widgets
+ on your Bangle.js 2, the smart one may produce larger clock faces than the
+ simple one):
+
+
+
+
+
+ simple
+
+
+
+
+ smart
+
+
+
+
+ (custom)
+
+
+
+
+ If you prefer a "custom" clock size calculator, please enter the URL
+ of its JavaScript module below:
+
+ custom URL:
+
+
+
Clock Face
+
+
+ Click on the desired clock face:
+
+
+
+
+
+ none
+
+
+
+
+ four-fold
+
+
+
+
+ twelve-fold
+
+
+
+
+ "rainbow" colored
+
+
+
+
+ (custom)
+
+
+
+
+ Clock faces are drawn in the configured foreground and background colors
+ (you may select them at the end of this form)
+
+ "Four-fold" clock faces may draw indian-arabic or roman numerals. Which do you prefer?
+
+ The "twelve-fold" and "rainbow"-colored faces may be drawn with or without
+ dots marking the position of every minute. Which variant do you prefer?
+
+ without dots
+ with dots
+
+ If you prefer a "custom" clock face, please enter the URL
+ of its JavaScript module below:
+
+ custom URL:
+
+
+
Clock Hands
+
+
+ Click on the desired clock hands:
+
+
+
+
+
+ simple
+
+
+
+
+ rounded
+
+
+
+
+ hollow
+
+
+
+
+ (custom)
+
+
+
+
+ Clock hands are drawn in the configured foreground and background colors
+ (you may select them at the end of this form)
+
+ Hollow clock hands may optionally be filled with a given color. If you have
+ chosen hollow hands, please specify the desired fill mode and color below:
+
+ Hollow Hand Fill Color:
+
+
+
+
+
+
+
+
+
+
+
+
+ Additionally, all clock hands may be drawn with or without second hands.
+ If you want them to be drawn, please click on their desired color below
+ (or choose "themed" to use your Bangle's configured theme) - if not, just
+ select "none":
+
+ Second Hand Color:
+
+
+
+
+
+
+
+
+
+
+
+
+ If you prefer "custom" clock hands, please enter the URL
+ of their JavaScript module below:
+
+ custom URL:
+
+
+
Complications
+
+
+ Complications are small displays for additional information. If you want
+ one or multiple complications to be added to your clock, you'll have to
+ specify which one to be loaded and where it should be placed.
+
+ Up to 6 possible positions exist (top-left, top-right, left, right,
+ bottom-left and bottom-right). Alternatively, the positions "top-left" and
+ "top-right" may be traded for a slightly larger complication at position
+ "top" or "bottom-left" and "bottom-right" for one at the "bottom":
+
+
+
+
+
+
+
top-left:
+
+
+
Complication:
+
+
+
+
+
+
+
custom URL:
+
+
+
+
+
top:
+
+
+
Complication:
+
+
+
+
+
+
+
custom URL:
+
+
+
+
+
top-right:
+
+
+
Complication:
+
+
+
+
+
+
+
custom URL:
+
+
+
+
+
left:
+
+
+
Complication:
+
+
+
+
+
+
+
custom URL:
+
+
+
+
+
right:
+
+
+
Complication:
+
+
+
+
+
+
+
custom URL:
+
+
+
+
+
bottom-left:
+
+
+
Complication:
+
+
+
+
+
+
+
custom URL:
+
+
+
+
+
bottom:
+
+
+
Complication:
+
+
+
+
+
+
+
custom URL:
+
+
+
+
+
bottom-right:
+
+
+
Complication:
+
+
+
+
+
+
+
custom URL:
+
+
+
+
+
+
Settings
+
+
+ Color faces, hands and complications are often drawn using configurable
+ foreground and background colors.
+
+ Here you may specify these colors. Click on a color to select it - or on
+ "themed" if you want the clock to use the currently configured theme on
+ your Bangle.js 2:
+
+ Background Color:
+
+
+
+
+
+
+
+
+
+
+
+ Foreground Color:
+
+
+
+
+
+
+
+
+
+
+
+ When you are satisfied with your configuration, just click on "Upload" in
+ order to generate the specified clock and upload it to your Bangle.js 2:
+
+
+
+
+
+ This application is based on the author's
+ Analog Clock Construction Kit (ACCK).
+ If you need a different "clockwork", clock size calculation or clock face,
+ or specific clock hands or complications, just follow the link to learn how to
+ implement your own clock parts.
+
+
+
+
diff --git a/apps/ac_ac/README.md b/apps/ac_ac/README.md
new file mode 100644
index 000000000..05e5f4798
--- /dev/null
+++ b/apps/ac_ac/README.md
@@ -0,0 +1,34 @@
+# AC-AC - A Configurable Analog Clock #
+
+This app implements an analog clock with various faces, hands and complications
+to choose from before uploading to a Bangle.js 2.
+
+It is based on the [Analog Clock Construction Kit (ACCK)](https://github.com/rozek/banglejs-2-analog-clock-construction-kit)
+and makes most of the currently implemented parts available with a few mouse
+clicks - just click on "Upload" and you will be directed to a web form where
+you compose your very own, personal analog clock.
+
+You currently have the choice between
+
+* 2 different clock sizes,
+* 4 different clock faces,
+* 3 different clock hands and
+* 4 different complications
+
+Alternatively, you may specify the GitHub URL of ACCK compatible modules for
+external clock sizes, faces, hands or complications.
+
+Additionally, you may use the currently configured global theme or configure
+your own colors for clock fore- and background and second hands.
+
+Consequently, even without external modules you already have the choice between
+102144 combinations!
+
+
+
+## License ##
+
+[MIT License](LICENSE)
diff --git a/apps/ac_ac/RainbowClockFace.png b/apps/ac_ac/RainbowClockFace.png
new file mode 100644
index 000000000..2defa759b
Binary files /dev/null and b/apps/ac_ac/RainbowClockFace.png differ
diff --git a/apps/ac_ac/app-icon.js b/apps/ac_ac/app-icon.js
new file mode 100644
index 000000000..20caf2c8e
--- /dev/null
+++ b/apps/ac_ac/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwgn/ABH+AQPvBpIAI/n8/3f5/PCp/v9oHF7w1CABffGxAYMH4f9z/514YDCxW/O4gFBxwHD/ZEL7/9GgX8GwQLCBQQXH/uP/Hf/2N44IBAgIXJ7oaD/3v/3uAYIIB9wQGAA2+/iRG5oSIM4f+1nrPYgAB3aHIAC77QYYRoCAAP676ICABXYFIntDoPf3+PC5f+BoPOX4vPNBn7IogEB/eu3QXC9wNEAAeKBIP+dgbSCDYMwgEApQVEygPCeRH8iAWBAAMHPwXDgoRGAonACwYABgN5uMAC4q8GC4U0DQsAggRF9gXFgggB/2hC4kdVAQCBVAX7xwXCVAnGCwUadAeeDYfr7IhEAAf93e+A4gpB9yRB/mqcgndRgQAHzqRE1gEC/KoCjLZEsgCB9evO4gOC/RyEgqdC2KnFO4S/KgFYsC/Ga5EBs1AX5bXHgx1C2YXEnp7GCARgB4AfE64WCnawFCgf9VAK/G/3M7zWDz4PF/maXJIAD7D8EVAP85QXN3OP/42DfoQXN/wvE/ySGABa8FAC37AgepVwQ9E1SfBAAJIEAAnrBQ39xgwJ7pRHFQX+3QECCAbyG9bPDzwXC9QMBdgQXIAAf41wEC5pLCJJBcF9fZQ5IAGYYn81q7RJQwWC/wXM9/tA4veCxooDIAPv55PEABwpB97rDAAw"))
\ No newline at end of file
diff --git a/apps/ac_ac/app-icon.png b/apps/ac_ac/app-icon.png
new file mode 100644
index 000000000..b83541133
Binary files /dev/null and b/apps/ac_ac/app-icon.png differ
diff --git a/apps/ac_ac/app-screenshot.png b/apps/ac_ac/app-screenshot.png
new file mode 100644
index 000000000..0aef3fa38
Binary files /dev/null and b/apps/ac_ac/app-screenshot.png differ
diff --git a/apps/ac_ac/app.js b/apps/ac_ac/app.js
new file mode 100644
index 000000000..1d9b2e3c6
--- /dev/null
+++ b/apps/ac_ac/app.js
@@ -0,0 +1,2 @@
+let Clockwork = require('https://raw.githubusercontent.com/rozek/banglejs-2-simple-clockwork/main/Clockwork.js');
+Clockwork.windUp();
\ No newline at end of file
diff --git a/apps/ac_ac/custom.png b/apps/ac_ac/custom.png
new file mode 100644
index 000000000..14d797ba3
Binary files /dev/null and b/apps/ac_ac/custom.png differ
diff --git a/apps/ac_ac/fourfoldClockFace.png b/apps/ac_ac/fourfoldClockFace.png
new file mode 100644
index 000000000..391303b31
Binary files /dev/null and b/apps/ac_ac/fourfoldClockFace.png differ
diff --git a/apps/ac_ac/hollowClockHands.png b/apps/ac_ac/hollowClockHands.png
new file mode 100644
index 000000000..2dce42ef5
Binary files /dev/null and b/apps/ac_ac/hollowClockHands.png differ
diff --git a/apps/ac_ac/largePlaceholders.png b/apps/ac_ac/largePlaceholders.png
new file mode 100644
index 000000000..b7272e57c
Binary files /dev/null and b/apps/ac_ac/largePlaceholders.png differ
diff --git a/apps/ac_ac/none.png b/apps/ac_ac/none.png
new file mode 100644
index 000000000..6f8d8ae14
Binary files /dev/null and b/apps/ac_ac/none.png differ
diff --git a/apps/ac_ac/roundedClockHands.png b/apps/ac_ac/roundedClockHands.png
new file mode 100644
index 000000000..cbd48e856
Binary files /dev/null and b/apps/ac_ac/roundedClockHands.png differ
diff --git a/apps/ac_ac/simpleClockHands.png b/apps/ac_ac/simpleClockHands.png
new file mode 100644
index 000000000..820606f27
Binary files /dev/null and b/apps/ac_ac/simpleClockHands.png differ
diff --git a/apps/ac_ac/simpleClockSize.png b/apps/ac_ac/simpleClockSize.png
new file mode 100644
index 000000000..49650586e
Binary files /dev/null and b/apps/ac_ac/simpleClockSize.png differ
diff --git a/apps/ac_ac/smallPlaceholders.png b/apps/ac_ac/smallPlaceholders.png
new file mode 100644
index 000000000..43569e56d
Binary files /dev/null and b/apps/ac_ac/smallPlaceholders.png differ
diff --git a/apps/ac_ac/smartClockSize.png b/apps/ac_ac/smartClockSize.png
new file mode 100644
index 000000000..6891acc89
Binary files /dev/null and b/apps/ac_ac/smartClockSize.png differ
diff --git a/apps/ac_ac/twelvefoldClockFace.png b/apps/ac_ac/twelvefoldClockFace.png
new file mode 100644
index 000000000..fc04d865e
Binary files /dev/null and b/apps/ac_ac/twelvefoldClockFace.png differ
diff --git a/apps/accelgraph/ChangeLog b/apps/accelgraph/ChangeLog
new file mode 100644
index 000000000..5560f00bc
--- /dev/null
+++ b/apps/accelgraph/ChangeLog
@@ -0,0 +1 @@
+0.01: New App!
diff --git a/apps/accelgraph/app-icon.js b/apps/accelgraph/app-icon.js
new file mode 100644
index 000000000..d45b8cc63
--- /dev/null
+++ b/apps/accelgraph/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEw4UA/4AB304ief85L/ABNVAAwKCgILHoALBgoLHqALOrVVr4BEBZIFBBYiaCAAPq2oLQEYlqF5VrBZWnBZWvBZNWz4LGBoQLHJ4O///6v/1BZHa/4LFLYOlr9pR49r1ILJ09qr4ZBBY2vrWdBY5PBq2uyoLIquqBY5bBKoZTFLYILJJ4STDBY77IJ4QLUJ4QLU1QAE0oLPqoAGBZ0BBY9ABYMABY4KCAH4AGA="))
diff --git a/apps/accelgraph/app.js b/apps/accelgraph/app.js
new file mode 100644
index 000000000..a59d636d2
--- /dev/null
+++ b/apps/accelgraph/app.js
@@ -0,0 +1,24 @@
+Bangle.loadWidgets();
+g.clear(1);
+Bangle.drawWidgets();
+var R = Bangle.appRect;
+
+var x = 0;
+var last;
+
+function getY(v) {
+ return (R.y+R.y2 + v*R.h/2)/2;
+}
+Bangle.on('accel', a => {
+ g.reset();
+ if (last) {
+ g.setColor("#f00").drawLine(x-1,getY(last.x),x,getY(a.x));
+ g.setColor("#0f0").drawLine(x-1,getY(last.y),x,getY(a.y));
+ g.setColor("#00f").drawLine(x-1,getY(last.z),x,getY(a.z));
+ }
+ last = a;x++;
+ if (x>=g.getWidth()) {
+ x = 1;
+ g.clearRect(R);
+ }
+});
diff --git a/apps/accelgraph/app.png b/apps/accelgraph/app.png
new file mode 100644
index 000000000..b0ba00ee7
Binary files /dev/null and b/apps/accelgraph/app.png differ
diff --git a/apps/accelgraph/screenshot.png b/apps/accelgraph/screenshot.png
new file mode 100644
index 000000000..404243d85
Binary files /dev/null and b/apps/accelgraph/screenshot.png differ
diff --git a/apps/antonclk/ChangeLog b/apps/antonclk/ChangeLog
index fdf20c175..4dca8053e 100644
--- a/apps/antonclk/ChangeLog
+++ b/apps/antonclk/ChangeLog
@@ -4,4 +4,7 @@
0.04: Clock can optionally show seconds, date optionally in ISO-8601 format, weekdays and uppercase configurable, too.
0.05: Clock can optionally show ISO-8601 calendar weeknumber (default: Off)
when weekday name "Off": week #:
- when weekday name "On": weekday name is cut at 6th position and .# is added
\ No newline at end of file
+ when weekday name "On": weekday name is cut at 6th position and .# is added
+0.06: fixes #1271 - wrong settings name
+ when weekday name and calendar weeknumber are on then display is #
+ week is buffered until date or timezone changes
\ No newline at end of file
diff --git a/apps/antonclk/README.md b/apps/antonclk/README.md
index 85c03788d..28a38f5fd 100644
--- a/apps/antonclk/README.md
+++ b/apps/antonclk/README.md
@@ -40,9 +40,9 @@ The main menu contains several settings covering Anton clock in general.
* **Show Weekday** - Weekday is shown in the time presentation without seconds.
Weekday name depends on the current locale.
If seconds are shown, the weekday is never shown as there is not enough space on the watch face.
-* **Show Weeknumber** - Week-number (ISO-8601) is shown. (default: Off)
-If "Show Weekday" is "Off" the week-number is displayed as "week #:".
-If "Show Weekday" is "On" the weekday name is cut at 6th position and suffixed with ".#".
+* **Show CalWeek** - Week-number (ISO-8601) is shown. (default: Off)
+If "Show Weekday" is "Off" displays the week-number as "week #".
+If "Show Weekday" is "On" displays "weekday name short" with " #" .
If seconds are shown, the week number is never shown as there is not enough space on the watch face.
* **Vector font** - Use the built-in vector font for dates and weekday.
This can improve readability.
diff --git a/apps/antonclk/app.js b/apps/antonclk/app.js
index 05758cbfd..7b40d8eb5 100644
--- a/apps/antonclk/app.js
+++ b/apps/antonclk/app.js
@@ -1,6 +1,6 @@
// Clock with large digits using the "Anton" bold font
-var SETTINGSFILE = "antonclk.json";
+const SETTINGSFILE = "antonclk.json";
Graphics.prototype.setFontAnton = function(scale) {
// Actual height 69 (68 - 0)
@@ -28,7 +28,7 @@ var drawTimeout;
var queueMillis = 1000;
var secondsScreen = true;
-var isBangle1 = (g.getWidth() == 240);
+var isBangle1 = (process.env.HWVERSION == 1);
//For development purposes
/*
@@ -50,13 +50,11 @@ require('Storage').writeJSON(SETTINGSFILE, {
require('Storage').erase(SETTINGSFILE);
*/
-// Helper method for loading the settings
-function def(value, def) {
- return (value !== undefined ? value : def);
-}
-
// Load settings
function loadSettings() {
+ // Helper function default setting
+ function def (value, def) {return value !== undefined ? value : def;}
+
var settings = require('Storage').readJSON(SETTINGSFILE, true) || {};
secondsMode = def(settings.secondsMode, "Never");
secondsColoured = def(settings.secondsColoured, true);
@@ -104,7 +102,12 @@ function isoStr(date) {
return date.getFullYear() + "-" + ("0" + (date.getMonth() + 1)).substr(-2) + "-" + ("0" + date.getDate()).substr(-2);
}
+var calWeekBuffer = [false,false,false]; //buffer tz, date, week no (once calculated until other tz or date is requested)
function ISO8601calWeek(date) { //copied from: https://gist.github.com/IamSilviu/5899269#gistcomment-3035480
+ dateNoTime = date; dateNoTime.setHours(0,0,0,0);
+ if (calWeekBuffer[0] === date.getTimezoneOffset() && calWeekBuffer[1] === dateNoTime) return calWeekBuffer[2];
+ calWeekBuffer[0] = date.getTimezoneOffset();
+ calWeekBuffer[1] = dateNoTime;
var tdt = new Date(date.valueOf());
var dayn = (date.getDay() + 6) % 7;
tdt.setDate(tdt.getDate() - dayn + 3);
@@ -113,7 +116,8 @@ function ISO8601calWeek(date) { //copied from: https://gist.github.com/IamSilviu
if (tdt.getDay() !== 4) {
tdt.setMonth(0, 1 + ((4 - tdt.getDay()) + 7) % 7);
}
- return 1 + Math.ceil((firstThursday - tdt) / 604800000);
+ calWeekBuffer[2] = 1 + Math.ceil((firstThursday - tdt) / 604800000);
+ return calWeekBuffer[2];
}
function doColor() {
@@ -186,13 +190,17 @@ function draw() {
else
g.setFont("6x8", 2);
g.drawString(dateStr, x, y);
- if (weekDay || calWeek) {
- var dowwumStr = require("locale").dow(date);
+ if (calWeek || weekDay) {
+ var dowcwStr = "";
if (calWeek)
- dowwumStr = (weekDay ? dowwumStr.substr(0,Math.min(dowwumStr.length,6)) + (dowwumStr.length>=6 ? "." : "") : "week ") + "#" + ISO8601calWeek(date); //TODO: locale for "week"
+ dowcwStr = " #" + ("0" + ISO8601calWeek(date)).substring(-2);
+ if (weekDay)
+ dowcwStr = require("locale").dow(date, calWeek ? 1 : 0) + dowcwStr; //weekDay e.g. Monday or weekDayShort # e.g. Mon #01
+ else //week #01
+ dowcwStr = /*LANG*/"week" + dowcwStr;
if (upperCase)
- dowwumStr = dowwumStr.toUpperCase();
- g.drawString(dowwumStr, x, y + (vectorFont ? 26 : 16));
+ dowcwStr = dowcwStr.toUpperCase();
+ g.drawString(dowcwStr, x, y + (vectorFont ? 26 : 16));
}
}
diff --git a/apps/antonclk/settings.js b/apps/antonclk/settings.js
index 293aa0438..e452b02c7 100644
--- a/apps/antonclk/settings.js
+++ b/apps/antonclk/settings.js
@@ -47,11 +47,11 @@
writeSettings();
}
},
- "Show Weeknumber": {
- value: (settings.weekNum !== undefined ? settings.weekNum : true),
+ "Show CalWeek": {
+ value: (settings.calWeek !== undefined ? settings.calWeek : false),
format: v => v ? "On" : "Off",
onchange: v => {
- settings.weekNum = v;
+ settings.calWeek = v;
writeSettings();
}
},
diff --git a/apps/assistedgps/ChangeLog b/apps/assistedgps/ChangeLog
index 4ec2c8f71..739ccf915 100644
--- a/apps/assistedgps/ChangeLog
+++ b/apps/assistedgps/ChangeLog
@@ -1,2 +1,3 @@
0.01: New App!
0.02: Update to work with Bangle.js 2
+0.03: Select GNSS systems to use for Bangle.js 2
diff --git a/apps/assistedgps/custom.html b/apps/assistedgps/custom.html
index fa11b696c..80d68a71f 100644
--- a/apps/assistedgps/custom.html
+++ b/apps/assistedgps/custom.html
@@ -27,6 +27,31 @@
+
Using fewer GNSS systems may decrease the time to fix. (If unsure, select only GPS)
+
+
+
+
+
+
+
+
+
+
Click
@@ -116,8 +141,13 @@
}
if (isB2) { // CASIC
- // Disable BDS, use just GPS (supposedly improve lock time)
- js += `\x10Serial1.println("${CASIC_CHECKSUM("$PCAS04,1")}")\n`; // set GPS-only mode
+ // Select what GNSS System to use for decreased fix time.
+ var radios = document.getElementsByName('gnss_select');
+ var gnss_select="1";
+ for (var i=0; i {
result = [bpm];
- bpm = 0;
+ bpm = "";
return result;
},
start : () => {
diff --git a/apps/circlesclock/app.js b/apps/circlesclock/app.js
index 822802afa..88a04d4b9 100644
--- a/apps/circlesclock/app.js
+++ b/apps/circlesclock/app.js
@@ -136,8 +136,10 @@ function getCirclePosition(type) {
if (setting == type) return circlePosX[i - 1];
}
for (let i = 0; i < defaultCircleTypes.length; i++) {
- if (type == defaultCircleTypes[i]) return circlePosX[i];
- }
+ if (type == defaultCircleTypes[i] && (!settings || settings['circle' + (i + 1)] == undefined)) {
+ return circlePosX[i];
+ }
+ }
return undefined;
}
diff --git a/apps/colorful_clock/LICENSE b/apps/colorful_clock/LICENSE
new file mode 100644
index 000000000..7487dd5da
--- /dev/null
+++ b/apps/colorful_clock/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Andreas Rozek
+
+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.
diff --git a/apps/configurable_clock/BangleApps__apps__variable_clock__README.md b/apps/configurable_clock/BangleApps__apps__variable_clock__README.md
new file mode 100644
index 000000000..da5bed56d
--- /dev/null
+++ b/apps/configurable_clock/BangleApps__apps__variable_clock__README.md
@@ -0,0 +1,27 @@
+# Variable Analog Clock #
+
+This app implements an analog clock with various faces, hands and colors to
+choose from.
+
+You have the choice between:
+
+* 4 different clock faces     and
+* 3 different clock hands (optionally with or without second hands)   
+
+Additionally, you may use the currently configured global theme or configure
+your own colors for clock fore- and background and second hands.
+
+Just swipe up or down to switch from clock display to configuration screen
+
+  
+ 
+
+Chosen settings will be written to the Bangle.js's flash memory and restored
+whenever the clock is started again.
+
+This clock also acts as an example for the building blocks found in the author's
+[GitHub repository](https://github.com/rozek/banglejs-2-activities)
+
+## License ##
+
+[MIT License](LICENSE)
diff --git a/apps/configurable_clock/LICENSE b/apps/configurable_clock/LICENSE
new file mode 100644
index 000000000..7487dd5da
--- /dev/null
+++ b/apps/configurable_clock/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Andreas Rozek
+
+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.
diff --git a/apps/configurable_clock/README.md b/apps/configurable_clock/README.md
new file mode 100644
index 000000000..faddd092a
--- /dev/null
+++ b/apps/configurable_clock/README.md
@@ -0,0 +1,29 @@
+# Configurable Analog Clock #
+
+This app implements an analog clock with various faces, hands and colors to
+choose from.
+
+You have the choice between:
+
+* 4 different clock faces     and
+* 3 different clock hands (optionally with or without second hands)   
+
+Additionally, you may use the currently configured global theme or configure
+your own colors for clock fore- and background and second hands.
+
+Just swipe up or down to switch from clock display to the first configuration
+screen and continue from there
+
+ 
+ 
+
+
+Chosen settings will be written to the Bangle.js's flash memory and restored
+whenever the clock is started again.
+
+This clock also acts as an example for the building blocks found in the author's
+[GitHub repository](https://github.com/rozek/banglejs-2-activities)
+
+## License ##
+
+[MIT License](LICENSE)
diff --git a/apps/configurable_clock/Screenshot-01.png b/apps/configurable_clock/Screenshot-01.png
new file mode 100644
index 000000000..b2367784c
Binary files /dev/null and b/apps/configurable_clock/Screenshot-01.png differ
diff --git a/apps/configurable_clock/Screenshot-02.png b/apps/configurable_clock/Screenshot-02.png
new file mode 100644
index 000000000..909a2a04a
Binary files /dev/null and b/apps/configurable_clock/Screenshot-02.png differ
diff --git a/apps/configurable_clock/Screenshot-03.png b/apps/configurable_clock/Screenshot-03.png
new file mode 100644
index 000000000..80407c84f
Binary files /dev/null and b/apps/configurable_clock/Screenshot-03.png differ
diff --git a/apps/configurable_clock/Screenshot-04.png b/apps/configurable_clock/Screenshot-04.png
new file mode 100644
index 000000000..175476c81
Binary files /dev/null and b/apps/configurable_clock/Screenshot-04.png differ
diff --git a/apps/configurable_clock/Screenshot-11.png b/apps/configurable_clock/Screenshot-11.png
new file mode 100644
index 000000000..bca534613
Binary files /dev/null and b/apps/configurable_clock/Screenshot-11.png differ
diff --git a/apps/configurable_clock/Screenshot-12.png b/apps/configurable_clock/Screenshot-12.png
new file mode 100644
index 000000000..973b6da5e
Binary files /dev/null and b/apps/configurable_clock/Screenshot-12.png differ
diff --git a/apps/configurable_clock/Screenshot-13.png b/apps/configurable_clock/Screenshot-13.png
new file mode 100644
index 000000000..b87d97712
Binary files /dev/null and b/apps/configurable_clock/Screenshot-13.png differ
diff --git a/apps/configurable_clock/Screenshot-21.png b/apps/configurable_clock/Screenshot-21.png
new file mode 100644
index 000000000..46d799e6d
Binary files /dev/null and b/apps/configurable_clock/Screenshot-21.png differ
diff --git a/apps/configurable_clock/Screenshot-22.png b/apps/configurable_clock/Screenshot-22.png
new file mode 100644
index 000000000..7ee02568e
Binary files /dev/null and b/apps/configurable_clock/Screenshot-22.png differ
diff --git a/apps/configurable_clock/Screenshot-23.png b/apps/configurable_clock/Screenshot-23.png
new file mode 100644
index 000000000..f3248993b
Binary files /dev/null and b/apps/configurable_clock/Screenshot-23.png differ
diff --git a/apps/configurable_clock/Screenshot-24.png b/apps/configurable_clock/Screenshot-24.png
new file mode 100644
index 000000000..8a7753bfc
Binary files /dev/null and b/apps/configurable_clock/Screenshot-24.png differ
diff --git a/apps/configurable_clock/Screenshot-25.png b/apps/configurable_clock/Screenshot-25.png
new file mode 100644
index 000000000..c2950d7b2
Binary files /dev/null and b/apps/configurable_clock/Screenshot-25.png differ
diff --git a/apps/configurable_clock/app-icon.js b/apps/configurable_clock/app-icon.js
new file mode 100644
index 000000000..b0cf74241
--- /dev/null
+++ b/apps/configurable_clock/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwgZC/AB1RgkQsAQMyUKAYMIkAPJgNFiEBgACBg0YCRMogEJkGSAwMSEZNAAQMAEAMGgBKHgXAlECwMgzcAmkAhgRGilRssUgMEEYcBwARFiBHBgQKB7AjCawIQEgoCCigDBjEBwwEBEwIAGlmSEYYABI4PAEYhEBNYIjCAYVtwCSElG2xdoAwQjDhpZEEAMUqAHDCIaPBEYlAiwjItkAgYjFqJHDCIdhI4j1CAAhlEZoTUEAAcGEYZKEEYWgCIgjEWYkBoqwCCITLBgcMmPXhgjCgUB2iFDm3pw0YLAMygEgc4QjF49cmA3BbQQjDgGkI5OwNZZ9FEYoRLEYxmBCI5jBEYQACyQRHgmAEYsEEZEka4kAhEEEY8BCIMJCIYjKgGChAFDCwKzDNYyKEJgUDlgRBAoPDRQQjEZQZzEjScIhgjBEwQjEH4aXEgIjBjYCBjQCBMYYADmAjDFIjcGKocAjBKCgJRCAAwaCEARQBmARIhBrEgSMEAApEBmHAAQJrCABUCjFhwwQMI4oA7"))
\ No newline at end of file
diff --git a/apps/configurable_clock/app-icon.png b/apps/configurable_clock/app-icon.png
new file mode 100644
index 000000000..58f50365d
Binary files /dev/null and b/apps/configurable_clock/app-icon.png differ
diff --git a/apps/configurable_clock/app-screenshot.png b/apps/configurable_clock/app-screenshot.png
new file mode 100644
index 000000000..528721759
Binary files /dev/null and b/apps/configurable_clock/app-screenshot.png differ
diff --git a/apps/configurable_clock/app.js b/apps/configurable_clock/app.js
new file mode 100644
index 000000000..157d57741
--- /dev/null
+++ b/apps/configurable_clock/app.js
@@ -0,0 +1,1380 @@
+ let Layout = require('Layout');
+
+ let Caret = require("heatshrink").decompress(atob("hEUgMAsFgmEwjEYhkMg0GAYIHBBYIPBgAA=="));
+
+ let ScreenWidth = g.getWidth(), CenterX;
+ let ScreenHeight = g.getHeight(), CenterY, outerRadius;
+
+ Bangle.loadWidgets();
+
+/**** updateClockFaceSize ****/
+
+ function updateClockFaceSize () {
+ CenterX = ScreenWidth/2;
+ CenterY = ScreenHeight/2;
+
+ outerRadius = Math.min(CenterX,CenterY);
+
+ if (global.WIDGETS == null) { return; }
+
+ let WidgetLayouts = {
+ tl:{ x:0, y:0, Direction:0 },
+ tr:{ x:ScreenWidth-1, y:0, Direction:1 },
+ bl:{ x:0, y:ScreenHeight-24, Direction:0 },
+ br:{ x:ScreenWidth-1, y:ScreenHeight-24, Direction:1 }
+ };
+
+ for (let Widget of WIDGETS) {
+ let WidgetLayout = WidgetLayouts[Widget.area]; // reference, not copy!
+ if (WidgetLayout == null) { continue; }
+
+ Widget.x = WidgetLayout.x - WidgetLayout.Direction * Widget.width;
+ Widget.y = WidgetLayout.y;
+
+ WidgetLayout.x += Widget.width * (1-2*WidgetLayout.Direction);
+ }
+
+ let x,y, dx,dy;
+ let cx = CenterX, cy = CenterY, r = outerRadius, r2 = r*r;
+
+ x = WidgetLayouts.tl.x; y = WidgetLayouts.tl.y+24; dx = x - cx; dy = y - cy;
+ if (dx*dx + dy*dy < r2) {
+ cy = CenterY + 12; dy = y - cy; r2 = dx*dx + dy*dy; r = Math.min(Math.sqrt(r2),cy-24);
+ }
+
+ x = WidgetLayouts.tr.x; y = WidgetLayouts.tr.y+24; dx = x - cx; dy = y - cy;
+ if (dx*dx + dy*dy < r2) {
+ cy = CenterY + 12; dy = y - cy; r2 = dx*dx + dy*dy; r = Math.min(Math.sqrt(r2),cy-24);
+ }
+
+ x = WidgetLayouts.bl.x; y = WidgetLayouts.bl.y; dx = x - cx; dy = y - cy;
+ if (dx*dx + dy*dy < r2) {
+ cy = CenterY - 12; dy = y - cy; r2 = dx*dx + dy*dy; r = Math.min(Math.sqrt(r2),cy);
+ }
+
+ x = WidgetLayouts.br.x; y = WidgetLayouts.br.y; dx = x - cx; dy = y - cy;
+ if (dx*dx + dy*dy < r2) {
+ cy = CenterY - 12; dy = y - cy; r2 = dx*dx + dy*dy; r = Math.min(Math.sqrt(r2),cy);
+ }
+
+ CenterX = cx; CenterY = cy; outerRadius = r - 4;
+ }
+
+ updateClockFaceSize();
+
+/**** custom version of Bangle.drawWidgets (does not clear the widget areas) ****/
+
+ Bangle.drawWidgets = function () {
+ var w = g.getWidth(), h = g.getHeight();
+
+ var pos = {
+ tl:{x:0, y:0, r:0, c:0}, // if r==1, we're right->left
+ tr:{x:w-1, y:0, r:1, c:0},
+ bl:{x:0, y:h-24, r:0, c:0},
+ br:{x:w-1, y:h-24, r:1, c:0}
+ };
+
+ if (global.WIDGETS) {
+ for (var wd of WIDGETS) {
+ var p = pos[wd.area];
+ if (!p) continue;
+
+ wd.x = p.x - p.r*wd.width;
+ wd.y = p.y;
+
+ p.x += wd.width*(1-2*p.r);
+ p.c++;
+ }
+
+ g.reset(); // also loads the current theme
+
+ if (pos.tl.c || pos.tr.c) {
+ g.setClipRect(0,h-24,w-1,h-1);
+ g.reset(); // also (re)loads the current theme
+ }
+
+ if (pos.bl.c || pos.br.c) {
+ g.setClipRect(0,h-24,w-1,h-1);
+ g.reset(); // also (re)loads the current theme
+ }
+
+ try {
+ for (wd of WIDGETS) {
+ g.clearRect(wd.x,wd.y, wd.x+wd.width-1,23);
+ wd.draw(wd);
+ }
+ } catch (e) { print(e); }
+ }
+ };
+
+/**** EventConsumerAtPoint ****/
+
+ let activeLayout;
+
+ function EventConsumerAtPoint (HandlerName, x,y) {
+ let Layout = (activeLayout || {}).l;
+ if (Layout == null) { return; }
+
+ function ConsumerIn (Control) {
+ if (
+ (x < Control.x) || (x >= Control.x + Control.w) ||
+ (y < Control.y) || (y >= Control.y + Control.h)
+ ) { return undefined; }
+
+ if (typeof Control[HandlerName] === 'function') { return Control; }
+
+ if (Control.c != null) {
+ let ControlList = Control.c;
+ for (let i = 0, l = ControlList.length; i < l; i++) {
+ let Consumer = ConsumerIn(ControlList[i]);
+ if (Consumer != null) { return Consumer; }
+ }
+ }
+
+ return undefined;
+ }
+
+ return ConsumerIn(Layout);
+ }
+
+/**** dispatchTouchEvent ****/
+
+ function dispatchTouchEvent (DefaultHandler) {
+ function handleTouchEvent (Button, xy) {
+ if (activeLayout == null) {
+ if (typeof DefaultHandler === 'function') {
+ DefaultHandler();
+ }
+ } else {
+ let Control = EventConsumerAtPoint('onTouch', xy.x,xy.y);
+ if (Control != null) {
+ Control.onTouch(Control, Button, xy);
+ }
+ }
+ }
+ Bangle.on('touch',handleTouchEvent);
+ }
+ dispatchTouchEvent();
+
+/**** dispatchStrokeEvent ****/
+
+ function dispatchStrokeEvent (DefaultHandler) {
+ function handleStrokeEvent (Coordinates) {
+ if (activeLayout == null) {
+ if (typeof DefaultHandler === 'function') {
+ DefaultHandler();
+ }
+ } else {
+ let Control = EventConsumerAtPoint('onStroke', Coordinates.xy[0],Coordinates.xy[1]);
+ if (Control != null) {
+ Control.onStroke(Control, Coordinates);
+ }
+ }
+ }
+ Bangle.on('stroke',handleStrokeEvent);
+ }
+ dispatchStrokeEvent();
+/**** Label ****/
+
+ function Label (Text, Options) {
+ function renderLabel (Details) {
+ let x = Details.x, xAlignment = Details.halign || 0;
+ let y = Details.y, yAlignment = Details.valign || 0;
+
+ let Width = Details.w, halfWidth = Width/2;
+ let Height = Details.h, halfHeight = Height/2;
+
+ let Border = Details.border || 0, BorderColor = Details.BorderColor;
+ let Padding = Details.pad || 0;
+ let Hilite = Details.hilite || false;
+ let bold = Details.bold ? 1 : 0;
+
+ if (Hilite || (Details.bgCol != null)) {
+ g.setBgColor(Hilite ? g.theme.bgH : Details.bgCol);
+ g.clearRect(x,y, x + Width-1,y + Height-1);
+ }
+
+ if ((Border > 0) && (BorderColor !== null)) {// draw border of layout cell
+ g.setColor(BorderColor || Details.col || g.theme.fg);
+
+ switch (Border) {
+ case 1: g.drawRect(x,y, x+Width-1,y+Height-1); break;
+ case 2: g.drawRect(x,y, x+Width-1,y+Height-1);
+ g.drawRect(x+1,y+1, x+Width-2,y+Height-2); break;
+ default: g.fillPoly([
+ x,y, x+Width,y, x+Width,y+Height, x,y+Height, x,y,
+ x+Border,y+Border, x+Border,y+Height-Border,
+ x+Width-Border,y+Height-Border, x+Width-Border,y+Border,
+ x+Border,y+Border
+ ]);
+ }
+ }
+
+ g.setClipRect(
+ x+Border+Padding,y+Border+Padding,
+ x + Width-Border-Padding-1,y + Height-Border-Padding-1
+ );
+
+ x += halfWidth + xAlignment*(halfWidth - Border - Padding);
+ y += halfHeight + yAlignment*(halfHeight - Border - Padding);
+
+ g.setColor (Hilite ? g.theme.fgH : Details.col || g.theme.fg);
+ g.setBgColor(Hilite ? g.theme.bgH : Details.bgCol || g.theme.bg);
+
+ if (Details.font != null) { g.setFont(Details.font); }
+ g.setFontAlign(xAlignment,yAlignment);
+
+ g.drawString(Details.label, x,y);
+ if (bold !== 0) {
+ g.drawString(Details.label, x+1,y);
+ g.drawString(Details.label, x,y+1);
+ g.drawString(Details.label, x+1,y+1);
+ }
+ }
+
+ let Result = Object.assign((
+ Options == null ? {} : Object.assign({}, Options.common || {}, Options)
+ ), {
+ type:'custom', render:renderLabel, label:Text || ''
+ });
+ let Border = Result.border || 0;
+ let Padding = Result.pad || 0;
+
+ let TextMetrics;
+ if (! Result.width || ! Result.height) {
+ if (Result.font == null) {
+ Result.font = g.getFont();
+ } else {
+ g.setFont(Result.font);
+ }
+ TextMetrics = g.stringMetrics(Result.label);
+ }
+
+ if (Result.col == null) { Result.col = g.getColor(); }
+ if (Result.bgCol == null) { Result.bgCol = g.getBgColor(); }
+
+ Result.width = Result.width || TextMetrics.width + 2*Border + 2*Padding;
+ Result.height = Result.height || TextMetrics.height + 2*Border + 2*Padding;
+ return Result;
+ }
+
+/**** Image ****/
+
+ function Image (Image, Options) {
+ function renderImage (Details) {
+ let x = Details.x, xAlignment = Details.halign || 0;
+ let y = Details.y, yAlignment = Details.valign || 0;
+
+ let Width = Details.w, halfWidth = Width/2 - Details.ImageWidth/2;
+ let Height = Details.h, halfHeight = Height/2 - Details.ImageHeight/2;
+
+ let Border = Details.border || 0, BorderColor = Details.BorderColor;
+ let Padding = Details.pad || 0;
+ let Hilite = Details.hilite || false;
+
+ if (Hilite || (Details.bgCol != null)) {
+ g.setBgColor(Hilite ? g.theme.bgH : Details.bgCol);
+ g.clearRect(x,y, x + Width-1,y + Height-1);
+ }
+
+ if ((Border > 0) && (BorderColor !== null)) {// draw border of layout cell
+ g.setColor(BorderColor || Details.col || g.theme.fg);
+
+ switch (Border) {
+ case 1: g.drawRect(x,y, x+Width-1,y+Height-1); break;
+ case 2: g.drawRect(x,y, x+Width-1,y+Height-1);
+ g.drawRect(x+1,y+1, x+Width-2,y+Height-2); break;
+ default: g.fillPoly([
+ x,y, x+Width,y, x+Width,y+Height, x,y+Height, x,y,
+ x+Border,y+Border, x+Border,y+Height-Border,
+ x+Width-Border,y+Height-Border, x+Width-Border,y+Border,
+ x+Border,y+Border
+ ]);
+ }
+ }
+
+ g.setClipRect(
+ x+Border+Padding,y+Border+Padding,
+ x + Width-Border-Padding-1,y + Height-Border-Padding-1
+ );
+
+ x += halfWidth + xAlignment*(halfWidth - Border - Padding);
+ y += halfHeight + yAlignment*(halfHeight - Border - Padding);
+
+ if ('rotate' in Details) { // "rotate" centers image at x,y!
+ x += Details.ImageWidth/2;
+ y += Details.ImageHeight/2;
+ }
+
+ g.setColor (Hilite ? g.theme.fgH : Details.col || g.theme.fg);
+ g.setBgColor(Hilite ? g.theme.bgH : Details.bgCol || g.theme.bg);
+
+ g.drawImage(Image, x,y, Details.ImageOptions);
+ }
+
+ let Result = Object.assign((
+ Options == null ? {} : Object.assign({}, Options.common || {}, Options)
+ ), {
+ type:'custom', render:renderImage, Image:Image
+ });
+ let ImageMetrics = g.imageMetrics(Image);
+ let Scale = Result.scale || 1;
+ let Border = Result.border || 0;
+ let Padding = Result.pad || 0;
+
+ Result.ImageWidth = Scale * ImageMetrics.width;
+ Result.ImageHeight = Scale * ImageMetrics.height;
+
+ if (('rotate' in Result) || ('scale' in Result) || ('frame' in Result)) {
+ Result.ImageOptions = {};
+ if ('rotate' in Result) { Result.ImageOptions.rotate = Result.rotate; }
+ if ('scale' in Result) { Result.ImageOptions.scale = Result.scale; }
+ if ('frame' in Result) { Result.ImageOptions.frame = Result.frame; }
+ }
+
+ Result.width = Result.width || Result.ImageWidth + 2*Border + 2*Padding;
+ Result.height = Result.height || Result.ImageHeight + 2*Border + 2*Padding;
+ return Result;
+ }
+
+/**** Drawable ****/
+
+ function Drawable (Callback, Options) {
+ function renderDrawable (Details) {
+ let x = Details.x, xAlignment = Details.halign || 0;
+ let y = Details.y, yAlignment = Details.valign || 0;
+
+ let Width = Details.w, DrawableWidth = Details.DrawableWidth || Width;
+ let Height = Details.h, DrawableHeight = Details.DrawableHeight || Height;
+
+ let halfWidth = Width/2 - DrawableWidth/2;
+ let halfHeight = Height/2 - DrawableHeight/2;
+
+ let Border = Details.border || 0, BorderColor = Details.BorderColor;
+ let Padding = Details.pad || 0;
+ let Hilite = Details.hilite || false;
+
+ if (Hilite || (Details.bgCol != null)) {
+ g.setBgColor(Hilite ? g.theme.bgH : Details.bgCol);
+ g.clearRect(x,y, x + Width-1,y + Height-1);
+ }
+
+ if ((Border > 0) && (BorderColor !== null)) {// draw border of layout cell
+ g.setColor(BorderColor || Details.col || g.theme.fg);
+
+ switch (Border) {
+ case 1: g.drawRect(x,y, x+Width-1,y+Height-1); break;
+ case 2: g.drawRect(x,y, x+Width-1,y+Height-1);
+ g.drawRect(x+1,y+1, x+Width-2,y+Height-2); break;
+ default: g.fillPoly([
+ x,y, x+Width,y, x+Width,y+Height, x,y+Height, x,y,
+ x+Border,y+Border, x+Border,y+Height-Border,
+ x+Width-Border,y+Height-Border, x+Width-Border,y+Border,
+ x+Border,y+Border
+ ]);
+ }
+ }
+
+ let DrawableX = x + halfWidth + xAlignment*(halfWidth - Border - Padding);
+ let DrawableY = y + halfHeight + yAlignment*(halfHeight - Border - Padding);
+
+ g.setClipRect(
+ Math.max(x+Border+Padding,DrawableX),
+ Math.max(y+Border+Padding,DrawableY),
+ Math.min(x+Width -Border-Padding,DrawableX+DrawableWidth)-1,
+ Math.min(y+Height-Border-Padding,DrawableY+DrawableHeight)-1
+ );
+
+ g.setColor (Hilite ? g.theme.fgH : Details.col || g.theme.fg);
+ g.setBgColor(Hilite ? g.theme.bgH : Details.bgCol || g.theme.bg);
+
+ Callback(DrawableX,DrawableY, DrawableWidth,DrawableHeight, Details);
+ }
+
+ let Result = Object.assign((
+ Options == null ? {} : Object.assign({}, Options.common || {}, Options)
+ ), {
+ type:'custom', render:renderDrawable, cb:Callback
+ });
+ let DrawableWidth = Result.DrawableWidth || 10;
+ let DrawableHeight = Result.DrawableHeight || 10;
+
+ let Border = Result.border || 0;
+ let Padding = Result.pad || 0;
+
+ Result.width = Result.width || DrawableWidth + 2*Border + 2*Padding;
+ Result.height = Result.height || DrawableHeight + 2*Border + 2*Padding;
+ return Result;
+ }
+
+ if (g.drawRoundedRect == null) {
+ g.drawRoundedRect = function drawRoundedRect (x1,y1, x2,y2, r) {
+ let x,y;
+ if (x1 > x2) { x = x1; x1 = x2; x2 = x; }
+ if (y1 > y2) { y = y1; y1 = y2; y2 = y; }
+
+ r = Math.min(r || 0, (x2-x1)/2, (y2-y1)/2);
+
+ let cx1 = x1+r, cx2 = x2-r;
+ let cy1 = y1+r, cy2 = y2-r;
+
+ this.drawLine(cx1,y1, cx2,y1);
+ this.drawLine(cx1,y2, cx2,y2);
+ this.drawLine(x1,cy1, x1,cy2);
+ this.drawLine(x2,cy1, x2,cy2);
+
+ x = r; y = 0;
+
+ let dx,dy, Error = 0;
+ while (y <= x) {
+ dy = 1 + 2*y; y++; Error -= dy;
+ if (Error < 0) {
+ dx = 1 - 2*x; x--; Error -= dx;
+ }
+
+ this.setPixel(cx1 - x, cy1 - y); this.setPixel(cx1 - y, cy1 - x);
+ this.setPixel(cx2 + x, cy1 - y); this.setPixel(cx2 + y, cy1 - x);
+ this.setPixel(cx2 + x, cy2 + y); this.setPixel(cx2 + y, cy2 + x);
+ this.setPixel(cx1 - x, cy2 + y); this.setPixel(cx1 - y, cy2 + x);
+ }
+ };
+ }
+
+ if (g.fillRoundedRect == null) {
+ g.fillRoundedRect = function fillRoundedRect (x1,y1, x2,y2, r) {
+ let x,y;
+ if (x1 > x2) { x = x1; x1 = x2; x2 = x; }
+ if (y1 > y2) { y = y1; y1 = y2; y2 = y; }
+
+ r = Math.min(r || 0, (x2-x1)/2, (y2-y1)/2);
+
+ let cx1 = x1+r, cx2 = x2-r;
+ let cy1 = y1+r, cy2 = y2-r;
+
+ this.fillRect(x1,cy1, x2,cy2);
+
+ x = r; y = 0;
+
+ let dx,dy, Error = 0;
+ while (y <= x) {
+ dy = 1 + 2*y; y++; Error -= dy;
+ if (Error < 0) {
+ dx = 1 - 2*x; x--; Error -= dx;
+ }
+
+ this.drawLine(cx1 - x, cy1 - y, cx2 + x, cy1 - y);
+ this.drawLine(cx1 - y, cy1 - x, cx2 + y, cy1 - x);
+ this.drawLine(cx1 - x, cy2 + y, cx2 + x, cy2 + y);
+ this.drawLine(cx1 - y, cy2 + x, cx2 + y, cy2 + x);
+ }
+ };
+ }
+
+
+/**** Button ****/
+
+ function Button (Text, Options) {
+ function renderButton (Details) {
+ let x = Details.x, Width = Details.w, halfWidth = Width/2;
+ let y = Details.y, Height = Details.h, halfHeight = Height/2;
+
+ let Padding = Details.pad || 0;
+ let Hilite = Details.hilite || false;
+
+ if (Details.bgCol != null) {
+ g.setBgColor(Details.bgCol);
+ g.clearRect(x,y, x + Width-1,y + Height-1);
+ }
+
+ if (Hilite) {
+ g.setColor(g.theme.bgH); // no typo!
+ g.fillRoundedRect(x+Padding,y+Padding, x+Width-Padding-1,y+Height-Padding-1,8);
+ }
+
+ g.setColor (Hilite ? g.theme.fgH : Details.col || g.theme.fg);
+ g.setBgColor(Hilite ? g.theme.bgH : Details.bgCol || g.theme.bg);
+
+ if (Details.font != null) { g.setFont(Details.font); }
+ g.setFontAlign(0,0);
+
+ g.drawRoundedRect(x+Padding,y+Padding, x+Width-Padding-1,y+Height-Padding-1,8);
+
+ g.setClipRect(x+Padding,y+Padding, x+Width-Padding-1,y+Height-Padding-1);
+
+ g.drawString(Details.label, x+halfWidth,y+halfHeight);
+ g.drawString(Details.label, x+halfWidth+1,y+halfHeight);
+ g.drawString(Details.label, x+halfWidth,y+halfHeight+1);
+ g.drawString(Details.label, x+halfWidth+1,y+halfHeight+1);
+ }
+
+ let Result = Object.assign((
+ Options == null ? {} : Object.assign({}, Options.common || {}, Options)
+ ), {
+ type:'custom', render:renderButton, label:Text || 'Tap'
+ });
+ let Padding = Result.pad || 0;
+
+ let TextMetrics;
+ if (! Result.width || ! Result.height) {
+ if (Result.font == null) {
+ Result.font = g.getFont();
+ } else {
+ g.setFont(Result.font);
+ }
+ TextMetrics = g.stringMetrics(Result.label);
+ }
+
+ Result.width = Result.width || TextMetrics.width + 2*10 + 2*Padding;
+ Result.height = Result.height || TextMetrics.height + 2*5 + 2*Padding;
+ return Result;
+ }
+
+ const Checkbox_checked = require("heatshrink").decompress(atob("ikUgMf/+GgEGoEAlEAgOAgEYsFhw8OjE54OB/EYh4OB+EYj+BwecjFw8OGg0YDocUgECsEAsP//A"));
+ const Checkbox_unchecked = require("heatshrink").decompress(atob("ikUgMf/+GgEGoEAlEAgOAgEYAjkUgECsEAsP//A="));
+
+/**** Checkbox ****/
+
+ function Checkbox (Options) {
+ function renderCheckbox (Details) {
+ let x = Details.x, xAlignment = Details.halign || 0;
+ let y = Details.y, yAlignment = Details.valign || 0;
+
+ let Width = Details.w, halfWidth = Width/2 - 10;
+ let Height = Details.h, halfHeight = Height/2 - 10;
+
+ let Padding = Details.pad || 0;
+
+ if (Details.bgCol != null) {
+ g.setBgColor(Details.bgCol);
+ g.clearRect(x,y, x + Width-1,y + Height-1);
+ }
+
+ x += halfWidth + xAlignment*(halfWidth - Padding);
+ y += halfHeight + yAlignment*(halfHeight - Padding);
+
+ g.setColor (Details.col || g.theme.fg);
+ g.setBgColor(Details.bgCol || g.theme.bg);
+
+ g.drawImage(
+ Details.checked ? Checkbox_checked : Checkbox_unchecked, x,y
+ );
+ }
+
+ let Result = Object.assign((
+ Options == null ? {} : Object.assign({}, Options.common || {}, Options)
+ ), {
+ type:'custom', render:renderCheckbox, onTouch:toggleCheckbox
+ });
+ let Padding = Result.pad || 0;
+
+ Result.width = Result.width || 20 + 2*Padding;
+ Result.height = Result.height || 20 + 2*Padding;
+
+ if (Result.checked == null) { Result.checked = false; }
+ return Result;
+ }
+
+ /* private */ function toggleCheckbox (Control) {
+ g.reset();
+
+ Control.checked = ! Control.checked;
+ Control.render(Control);
+
+ if (typeof Control.onChange === 'function') {
+ Control.onChange(Control);
+ }
+ }
+
+/**** toggleInnerCheckbox ****/
+
+ /* export */ function toggleInnerCheckbox (Control) {
+ if (Control.c == null) {
+ if (('checked' in Control) && ! ('GroupName' in Control)) {
+ toggleCheckbox(Control);
+ return true;
+ }
+ } else {
+ let ControlList = Control.c;
+ for (let i = 0, l = ControlList.length; i < l; i++) {
+ let done = toggleInnerCheckbox(ControlList[i]);
+ if (done) { return true; }
+ }
+ }
+ }
+
+ const Radiobutton_checked = require("heatshrink").decompress(atob("ikUgMB/EAsFgjEBwUAgkggFEgECoEAlEPgOB/EYj+BAgmA+EUCYciDodBwEYg0GgEfwA"));
+ const Radiobutton_unchecked = require("heatshrink").decompress(atob("ikUgMB/EAsFgjEBwUAgkggFEgECoEAlEAgOAgEYAhEUCYciDodBwEYg0GgEfwAA="));
+
+/**** Radiobutton ****/
+
+ function Radiobutton (Options) {
+ function renderRadiobutton (Details) {
+ let x = Details.x, xAlignment = Details.halign || 0;
+ let y = Details.y, yAlignment = Details.valign || 0;
+
+ let Width = Details.w, halfWidth = Width/2 - 10;
+ let Height = Details.h, halfHeight = Height/2 - 10;
+
+ let Padding = Details.pad || 0;
+
+ if (Details.bgCol != null) {
+ g.setBgColor(Details.bgCol);
+ g.clearRect(x,y, x + Width-1,y + Height-1);
+ }
+
+ x += halfWidth + xAlignment*(halfWidth - Padding);
+ y += halfHeight + yAlignment*(halfHeight - Padding);
+
+ g.setColor (Details.col || g.theme.fg);
+ g.setBgColor(Details.bgCol || g.theme.bg);
+
+ g.drawImage(
+ Details.checked ? Radiobutton_checked : Radiobutton_unchecked, x,y
+ );
+ }
+
+ let Result = Object.assign((
+ Options == null ? {} : Object.assign({}, Options.common || {}, Options)
+ ), {
+ type:'custom', render:renderRadiobutton, onTouch:checkRadiobutton
+ });
+ let Padding = Result.pad || 0;
+
+ Result.width = Result.width || 20 + 2*Padding;
+ Result.height = Result.height || 20 + 2*Padding;
+
+ if (Result.checked == null) { Result.checked = false; }
+ return Result;
+ }
+
+ /* private */ function checkRadiobutton (Control) {
+ if (! Control.checked) {
+ uncheckRadiobuttonsIn((activeLayout || {}).l,Control.GroupName);
+ toggleRadiobutton(Control);
+
+ if (typeof Control.onChange === 'function') {
+ Control.onChange(Control);
+ }
+ }
+ }
+
+ /* private */ function toggleRadiobutton (Control) {
+ g.reset();
+
+ Control.checked = ! Control.checked;
+ Control.render(Control);
+ }
+
+ /* private */ function uncheckRadiobuttonsIn (Control,GroupName) {
+ if ((Control == null) || (GroupName == null)) { return; }
+
+ if (Control.c == null) {
+ if (('checked' in Control) && (Control.GroupName === GroupName)) {
+ if (Control.checked) { toggleRadiobutton(Control); }
+ }
+ } else {
+ let ControlList = Control.c;
+ for (let i = 0, l = ControlList.length; i < l; i++) {
+ uncheckRadiobuttonsIn(ControlList[i],GroupName);
+ }
+ }
+ }
+
+/**** checkInnerRadiobutton ****/
+
+ /* export */ function checkInnerRadiobutton (Control) {
+ if (Control.c == null) {
+ if (('checked' in Control) && ('GroupName' in Control)) {
+ checkRadiobutton(Control);
+ return true;
+ }
+ } else {
+ let ControlList = Control.c;
+ for (let i = 0, l = ControlList.length; i < l; i++) {
+ let done = checkInnerRadiobutton(ControlList[i]);
+ if (done) { return true; }
+ }
+ }
+ }
+
+
+ let Theme = g.theme;
+ g.clear(true);
+
+/**** Settings ****/
+
+ let Settings;
+
+ function readSettings () {
+ Settings = Object.assign({},
+ {
+ Face:'1-12', colored:true,
+ Hands:'rounded', withSeconds:true,
+ Foreground:'Theme', Background:'Theme', Seconds:'#FF0000'
+ },
+ require('Storage').readJSON('configurable_clock.json', true) || {}
+ );
+
+ prepareTransformedPolygon();
+ }
+
+ function saveSettings () {
+ require('Storage').writeJSON('configurable_clock.json', Settings);
+ prepareTransformedPolygon();
+ }
+
+ function prepareTransformedPolygon () {
+ switch (Settings.Hands) {
+ case 'simple': transformedPolygon = new Array(simpleHourHandPolygon.length); break;
+ case 'rounded': transformedPolygon = new Array(roundedHandPolygon.length); break;
+ case 'hollow': transformedPolygon = new Array(hollowHandPolygon.length);
+ }
+ }
+
+//readSettings(); // not yet
+
+
+/**** Hands ****/
+
+ let HourHandLength = outerRadius * 0.5;
+ let HourHandWidth = 2*3, halfHourHandWidth = HourHandWidth/2;
+
+ let MinuteHandLength = outerRadius * 0.8;
+ let MinuteHandWidth = 2*2, halfMinuteHandWidth = MinuteHandWidth/2;
+
+ let SecondHandLength = outerRadius * 0.9;
+ let SecondHandOffset = 10;
+
+ let twoPi = 2*Math.PI, deg2rad = Math.PI/180;
+ let Pi = Math.PI;
+ let halfPi = Math.PI/2;
+
+ let sin = Math.sin, cos = Math.cos;
+
+/**** simple Hands ****/
+
+ let simpleHourHandPolygon = [
+ -halfHourHandWidth,halfHourHandWidth,
+ -halfHourHandWidth,halfHourHandWidth-HourHandLength,
+ halfHourHandWidth,halfHourHandWidth-HourHandLength,
+ halfHourHandWidth,halfHourHandWidth,
+ ];
+
+ let simpleMinuteHandPolygon = [
+ -halfMinuteHandWidth,halfMinuteHandWidth,
+ -halfMinuteHandWidth,halfMinuteHandWidth-MinuteHandLength,
+ halfMinuteHandWidth,halfMinuteHandWidth-MinuteHandLength,
+ halfMinuteHandWidth,halfMinuteHandWidth,
+ ];
+
+
+/**** rounded Hands ****/
+
+ let outerBoltRadius = halfHourHandWidth + 2;
+ let innerBoltRadius = outerBoltRadius - 4;
+ let roundedHandOffset = outerBoltRadius + 4;
+
+ let sine = [0, sin(30*deg2rad), sin(60*deg2rad), 1];
+
+ let roundedHandPolygon = [
+ -sine[3],-sine[0], -sine[2],-sine[1], -sine[1],-sine[2], -sine[0],-sine[3],
+ sine[0],-sine[3], sine[1],-sine[2], sine[2],-sine[1], sine[3],-sine[0],
+ sine[3], sine[0], sine[2], sine[1], sine[1], sine[2], sine[0], sine[3],
+ -sine[0], sine[3], -sine[1], sine[2], -sine[2], sine[1], -sine[3], sine[0],
+ ];
+
+ let roundedHourHandPolygon = new Array(roundedHandPolygon.length);
+ for (let i = 0, l = roundedHandPolygon.length; i < l; i+=2) {
+ roundedHourHandPolygon[i] = halfHourHandWidth*roundedHandPolygon[i];
+ roundedHourHandPolygon[i+1] = halfHourHandWidth*roundedHandPolygon[i+1];
+ if (i < l/2) { roundedHourHandPolygon[i+1] -= HourHandLength; }
+ if (i > l/2) { roundedHourHandPolygon[i+1] += roundedHandOffset; }
+ }
+ let roundedMinuteHandPolygon = new Array(roundedHandPolygon.length);
+ for (let i = 0, l = roundedHandPolygon.length; i < l; i+=2) {
+ roundedMinuteHandPolygon[i] = halfMinuteHandWidth*roundedHandPolygon[i];
+ roundedMinuteHandPolygon[i+1] = halfMinuteHandWidth*roundedHandPolygon[i+1];
+ if (i < l/2) { roundedMinuteHandPolygon[i+1] -= MinuteHandLength; }
+ if (i > l/2) { roundedMinuteHandPolygon[i+1] += roundedHandOffset; }
+ }
+
+
+/**** hollow Hands ****/
+
+ let BoltRadius = 3;
+ let hollowHandOffset = BoltRadius + 15;
+
+ let hollowHandPolygon = [
+ -sine[3],-sine[0], -sine[2],-sine[1], -sine[1],-sine[2], -sine[0],-sine[3],
+ sine[0],-sine[3], sine[1],-sine[2], sine[2],-sine[1], sine[3],-sine[0],
+ sine[3], sine[0], sine[2], sine[1], sine[1], sine[2], sine[0], sine[3],
+ 0,0,
+ -sine[0], sine[3], -sine[1], sine[2], -sine[2], sine[1], -sine[3], sine[0]
+ ];
+
+ let hollowHourHandPolygon = new Array(hollowHandPolygon.length);
+ for (let i = 0, l = hollowHandPolygon.length; i < l; i+=2) {
+ hollowHourHandPolygon[i] = halfHourHandWidth*hollowHandPolygon[i];
+ hollowHourHandPolygon[i+1] = halfHourHandWidth*hollowHandPolygon[i+1];
+ if (i < l/2) { hollowHourHandPolygon[i+1] -= HourHandLength; }
+ if (i > l/2) { hollowHourHandPolygon[i+1] -= hollowHandOffset; }
+ }
+ hollowHourHandPolygon[25] = -BoltRadius;
+
+ let hollowMinuteHandPolygon = new Array(hollowHandPolygon.length);
+ for (let i = 0, l = hollowHandPolygon.length; i < l; i+=2) {
+ hollowMinuteHandPolygon[i] = halfMinuteHandWidth*hollowHandPolygon[i];
+ hollowMinuteHandPolygon[i+1] = halfMinuteHandWidth*hollowHandPolygon[i+1];
+ if (i < l/2) { hollowMinuteHandPolygon[i+1] -= MinuteHandLength; }
+ if (i > l/2) { hollowMinuteHandPolygon[i+1] -= hollowHandOffset; }
+ }
+ hollowMinuteHandPolygon[25] = -BoltRadius;
+
+
+
+/**** transform polygon ****/
+
+ let transformedPolygon;
+
+ function transformPolygon (originalPolygon, OriginX,OriginY, Phi) {
+ let sPhi = sin(Phi), cPhi = cos(Phi), x,y;
+
+ for (let i = 0, l = originalPolygon.length; i < l; i+=2) {
+ x = originalPolygon[i];
+ y = originalPolygon[i+1];
+
+ transformedPolygon[i] = OriginX + x*cPhi + y*sPhi;
+ transformedPolygon[i+1] = OriginY + x*sPhi - y*cPhi;
+ }
+ }
+
+/**** refreshClock ****/
+
+ let Timer;
+ function refreshClock () {
+ activeLayout = null;
+
+ g.setTheme({
+ fg:(Settings.Foreground === 'Theme' ? Theme.fg : Settings.Foreground || '#000000'),
+ bg:(Settings.Background === 'Theme' ? Theme.bg : Settings.Background || '#FFFFFF')
+ });
+ g.clear(true); // also installs the current theme
+
+ Bangle.drawWidgets();
+ renderClock();
+
+ let Period = (Settings.withSeconds ? 1000 : 60000);
+
+ let Pause = Period - (Date.now() % Period);
+ Timer = setTimeout(refreshClock,Pause);
+ }
+
+/**** renderClock ****/
+
+ function renderClock () {
+ g.setColor (Settings.Foreground === 'Theme' ? Theme.fg : Settings.Foreground || '#000000');
+ g.setBgColor(Settings.Background === 'Theme' ? Theme.bg : Settings.Background || '#FFFFFF');
+
+ switch (Settings.Face) {
+ case 'none':
+ break;
+ case '3,6,9,12':
+ g.setFont('Vector', 22);
+
+ g.setFontAlign(0,-1);
+ g.drawString('12', CenterX,CenterY-outerRadius);
+
+ g.setFontAlign(1,0);
+ g.drawString('3', CenterX+outerRadius,CenterY);
+
+ g.setFontAlign(0,1);
+ g.drawString('6', CenterX,CenterY+outerRadius);
+
+ g.setFontAlign(-1,0);
+ g.drawString('9', CenterX-outerRadius,CenterY);
+ break;
+ case '1-12':
+ let innerRadius = outerRadius * 0.9 - 10;
+
+ let dark = g.theme.dark;
+
+ let Saturations = [0.8,1.0,1.0,1.0,1.0,1.0,1.0,0.9,0.7,0.7,0.9,0.9];
+ let Brightnesses = [1.0,0.9,0.6,0.6,0.8,0.8,0.7,1.0,1.0,1.0,1.0,1.0,];
+
+ for (let i = 0; i < 60; i++) {
+ let Phi = i * twoPi/60;
+
+ let x = CenterX + outerRadius * sin(Phi);
+ let y = CenterY - outerRadius * cos(Phi);
+
+ if (Settings.colored) {
+ let j = Math.floor(i / 5);
+ let Saturation = (dark ? Saturations[j] : 1.0);
+ let Brightness = (dark ? 1.0 : Brightnesses[j]);
+
+ let Color = E.HSBtoRGB(i/60,Saturation,Brightness, true);
+ g.setColor(Color[0]/255,Color[1]/255,Color[2]/255);
+ }
+
+ g.fillCircle(x,y, 1);
+ }
+
+ g.setFont('Vector', 20);
+ g.setFontAlign(0,0);
+
+ for (let i = 0; i < 12; i++) {
+ let Phi = i * twoPi/12;
+
+ let Radius = innerRadius;
+ if (i >= 10) { Radius -= 4; }
+
+ let x = CenterX + Radius * sin(Phi);
+ let y = CenterY - Radius * cos(Phi);
+
+ if (Settings.colored) {
+ let Saturation = (dark ? Saturations[i] : 1.0);
+ let Brightness = (dark ? 1.0 : Brightnesses[i]);
+
+ let Color = E.HSBtoRGB(i/12,Saturation,Brightness, true);
+ g.setColor(Color[0]/255,Color[1]/255,Color[2]/255);
+ }
+
+ g.drawString(i == 0 ? '12' : '' + i, x,y);
+ }
+ }
+
+ let now = new Date();
+
+ let Hours = now.getHours() % 12;
+ let Minutes = now.getMinutes();
+
+ let HoursAngle = (Hours+(Minutes/60))/12 * twoPi - Pi;
+ let MinutesAngle = (Minutes/60) * twoPi - Pi;
+
+ g.setColor(Settings.Foreground === 'Theme' ? Theme.fg : Settings.Foreground || '#000000');
+
+ switch (Settings.Hands) {
+ case 'simple':
+ transformPolygon(simpleHourHandPolygon, CenterX,CenterY, HoursAngle);
+ g.fillPoly(transformedPolygon);
+
+ transformPolygon(simpleMinuteHandPolygon, CenterX,CenterY, MinutesAngle);
+ g.fillPoly(transformedPolygon);
+ break;
+ case 'rounded':
+ transformPolygon(roundedHourHandPolygon, CenterX,CenterY, HoursAngle);
+ g.fillPoly(transformedPolygon);
+
+ transformPolygon(roundedMinuteHandPolygon, CenterX,CenterY, MinutesAngle);
+ g.fillPoly(transformedPolygon);
+
+// g.setColor(Settings.Foreground === 'Theme' ? Theme.fg || '#000000');
+ g.fillCircle(CenterX,CenterY, outerBoltRadius);
+
+ g.setColor(Settings.Background === 'Theme' ? Theme.bg : Settings.Background || '#FFFFFF');
+ g.drawCircle(CenterX,CenterY, outerBoltRadius);
+ g.fillCircle(CenterX,CenterY, innerBoltRadius);
+ break;
+ case 'hollow':
+ transformPolygon(hollowHourHandPolygon, CenterX,CenterY, HoursAngle);
+ g.drawPoly(transformedPolygon,true);
+
+ transformPolygon(hollowMinuteHandPolygon, CenterX,CenterY, MinutesAngle);
+ g.drawPoly(transformedPolygon,true);
+
+ g.drawCircle(CenterX,CenterY, BoltRadius);
+ }
+
+ if (Settings.withSeconds) {
+ g.setColor(Settings.Seconds === 'Theme' ? Theme.fgH : Settings.Seconds || '#FF0000');
+
+ let Seconds = now.getSeconds();
+ let SecondsAngle = (Seconds/60) * twoPi - Pi;
+
+ let sPhi = Math.sin(SecondsAngle), cPhi = Math.cos(SecondsAngle);
+
+ g.drawLine(
+ CenterX + SecondHandOffset*sPhi,
+ CenterY - SecondHandOffset*cPhi,
+ CenterX - SecondHandLength*sPhi,
+ CenterY + SecondHandLength*cPhi
+ );
+ }
+ }
+
+
+/**** MainScreen Logic ****/
+
+ let Changes = {}, KeysToChange;
+
+ let fullScreen = {
+ x:0,y:0, w:ScreenWidth,h:ScreenHeight, x2:ScreenWidth-1,y2:ScreenHeight-1
+ };
+ let AppRect;
+
+ function openMainScreen () {
+ if (Timer != null) { clearTimeout(Timer); Timer = undefined; }
+ if (AppRect == null) { AppRect = Bangle.appRect; Bangle.appRect = fullScreen; }
+
+ Bangle.buzz();
+
+ KeysToChange = 'Face colored Hands withSeconds Foreground Background Seconds';
+
+ g.setTheme({ fg:'#000000', bg:'#FFFFFF' });
+ g.clear(true); // also installs the current theme
+
+ (activeLayout = MainScreen).render();
+ }
+
+ function applySettings () { Bangle.buzz(); saveSettings(); Bangle.appRect = AppRect; refreshClock(); }
+ function withdrawSettings () { Bangle.buzz(); readSettings(); Bangle.appRect = AppRect; refreshClock(); }
+
+/**** FacesScreen Logic ****/
+
+ function openFacesScreen () {
+ Bangle.buzz();
+
+ KeysToChange = 'Face colored';
+ Bangle.appRect = fullScreen;
+ refreshFacesScreen();
+ }
+
+ function refreshFacesScreen () {
+ activeLayout = FacesScreen;
+ activeLayout['none'].checked = ((Changes.Face || Settings.Face) === 'none');
+ activeLayout['3,6,9,12'].checked = ((Changes.Face || Settings.Face) === '3,6,9,12');
+ activeLayout['1-12'].checked = ((Changes.Face || Settings.Face) === '1-12');
+ activeLayout['colored'].checked = (Changes.colored == null ? Settings.colored : Changes.colored);
+ activeLayout.render();
+ }
+
+ function chooseFace (Control) { Bangle.buzz(); Changes.Face = Control.id; refreshFacesScreen(); }
+ function toggleColored () { Bangle.buzz(); Changes.colored = ! Changes.colored; refreshFacesScreen(); }
+
+/**** HandsScreen Logic ****/
+
+ function openHandsScreen () {
+ Bangle.buzz();
+
+ KeysToChange = 'Hands withSeconds';
+ Bangle.appRect = fullScreen;
+ refreshHandsScreen();
+ }
+
+ function refreshHandsScreen () {
+ activeLayout = HandsScreen;
+ activeLayout['simple'].checked = ((Changes.Hands || Settings.Hands) === 'simple');
+ activeLayout['rounded'].checked = ((Changes.Hands || Settings.Hands) === 'rounded');
+ activeLayout['hollow'].checked = ((Changes.Hands || Settings.Hands) === 'hollow');
+ activeLayout['withSeconds'].checked = (Changes.withSeconds == null ? Settings.withSeconds : Changes.withSeconds);
+ activeLayout.render();
+ }
+
+ function chooseHand (Control) { Bangle.buzz(); Changes.Hands = Control.id; refreshHandsScreen(); }
+ function toggleSeconds () { Bangle.buzz(); Changes.withSeconds = ! Changes.withSeconds; refreshHandsScreen(); }
+
+/**** ColorsScreen Logic ****/
+
+ function openColorsScreen () {
+ Bangle.buzz();
+
+ KeysToChange = 'Foreground Background Seconds';
+ Bangle.appRect = fullScreen;
+ refreshColorsScreen();
+ }
+
+ function refreshColorsScreen () {
+ let Foreground = (Changes.Foreground == null ? Settings.Foreground : Changes.Foreground);
+ let Background = (Changes.Background == null ? Settings.Background : Changes.Background);
+ let Seconds = (Changes.Seconds == null ? Settings.Seconds : Changes.Seconds);
+
+ activeLayout = ColorsScreen;
+ activeLayout['Foreground'].bgCol = (Foreground === 'Theme' ? Theme.fg : Foreground);
+ activeLayout['Background'].bgCol = (Background === 'Theme' ? Theme.bg : Background);
+ activeLayout['Seconds'].bgCol = (Seconds === 'Theme' ? Theme.fgH : Seconds);
+ activeLayout.render();
+ }
+
+ function selectForegroundColor () { ColorToChange = 'Foreground'; openColorChoiceScreen(); }
+ function selectBackgroundColor () { ColorToChange = 'Background'; openColorChoiceScreen(); }
+ function selectSecondsColor () { ColorToChange = 'Seconds'; openColorChoiceScreen(); }
+
+/**** ColorChoiceScreen Logic ****/
+
+ let ColorToChange, chosenColor;
+
+ function openColorChoiceScreen () {
+ Bangle.buzz();
+
+ chosenColor = (
+ Changes[ColorToChange] == null ? Settings[ColorToChange] : Changes[ColorToChange]
+ );
+ Bangle.appRect = fullScreen;
+ refreshColorChoiceScreen();
+ }
+
+ function refreshColorChoiceScreen () {
+ activeLayout = ColorChoiceScreen;
+ activeLayout['#000000'].selected = (chosenColor === '#000000');
+ activeLayout['#FF0000'].selected = (chosenColor === '#FF0000');
+ activeLayout['#00FF00'].selected = (chosenColor === '#00FF00');
+ activeLayout['#0000FF'].selected = (chosenColor === '#0000FF');
+ activeLayout['#FFFF00'].selected = (chosenColor === '#FFFF00');
+ activeLayout['#FF00FF'].selected = (chosenColor === '#FF00FF');
+ activeLayout['#00FFFF'].selected = (chosenColor === '#00FFFF');
+ activeLayout['#FFFFFF'].selected = (chosenColor === '#FFFFFF');
+ activeLayout['Theme'].selected = (chosenColor === 'Theme');
+ activeLayout.render();
+ }
+
+ function chooseColor (Control) { Bangle.buzz(); chosenColor = Control.id; refreshColorChoiceScreen(); }
+ function chooseThemeColor () { Bangle.buzz(); chosenColor = 'Theme'; refreshColorChoiceScreen(); }
+
+ function applyColor () {
+ Changes[ColorToChange] = chosenColor;
+ openColorsScreen();
+ }
+
+ function withdrawColor () {
+ openColorsScreen();
+ }
+
+/**** common logic for multiple screens ****/
+
+ function applyChanges () {
+ Settings = Object.assign(Settings,Changes);
+ Changes = {};
+ openMainScreen();
+ }
+
+ function withdrawChanges () {
+ Changes = {};
+ openMainScreen();
+ }
+
+
+ g.setFont12x20(); // does not seem to be respected in layout!
+
+ let OkCancelWidth = Math.max(
+ g.stringWidth('Ok'), g.stringWidth('Cancel')
+ ) + 2*10;
+
+ let StdFont = { font:'12x20' };
+ let legible = Object.assign({ col:'#000000', bgCol:'#FFFFFF' }, StdFont);
+ let leftAligned = Object.assign({ halign:-1, valign:0 }, legible);
+ let ColorView = Object.assign({ width:30, border:1, BorderColor:'#000000' }, StdFont);
+ let ColorChoice = Object.assign({ DrawableWidth:30, DrawableHeight:30, onTouch:chooseColor }, StdFont);
+
+/**** MainScreen ****/
+
+ let MainScreen = new Layout({
+ type:'v', c:[
+ Label('Settings', { common:legible, bold:true, filly:1 }),
+ { height:4 },
+ { type:'h', c:[
+ { width:4 },
+ Label('Faces', { common:leftAligned, fillx:1 }),
+ Image(Caret, { common:leftAligned }),
+ { width:4 },
+ ], filly:1, onTouch:openFacesScreen },
+ { type:'h', c:[
+ { width:4 },
+ Label('Hands', { common:leftAligned, fillx:1 }),
+ Image(Caret, { common:leftAligned }),
+ { width:4 },
+ ], filly:1, onTouch:openHandsScreen },
+ { type:'h', c:[
+ { width:4 },
+ Label('Colors', { common:leftAligned, fillx:1 }),
+ Image(Caret, { common:leftAligned }),
+ { width:4 },
+ ], filly:1, onTouch:openColorsScreen },
+ { height:4 },
+ { type:'h', c:[
+ Button('Ok', { common:legible, width:OkCancelWidth, onTouch:applySettings }),
+ { width:4 },
+ Button('Cancel', { common:legible, width:OkCancelWidth, onTouch:withdrawSettings }),
+ ], filly:1 },
+ ], bgCol:'#FFFFFF'
+ });
+
+
+/**** FacesScreen ****/
+
+ let FacesScreen = new Layout({
+ type:'v', c:[
+ Label('Clock Faces', { common:legible, bold:true, filly:1 }),
+ { height:4 },
+ { type:'h', c:[
+ { width:4 },
+ Radiobutton({ id:'none', GroupName:'Faces', common:legible, onChange:chooseFace }),
+ Label(' no Face', { common:leftAligned, pad:4, fillx:1 }),
+ ], filly:1, onTouch:checkInnerRadiobutton },
+ { type:'h', c:[
+ { width:4 },
+ Radiobutton({ id:'3,6,9,12', GroupName:'Faces', common:legible, onChange:chooseFace }),
+ Label(' 3, 6, 9 and 12', { common:leftAligned, pad:4, fillx:1 }),
+ ], filly:1, onTouch:checkInnerRadiobutton },
+ { type:'h', c:[
+ { width:4 },
+ Radiobutton({ id:'1-12', GroupName:'Faces', common:legible, onChange:chooseFace }),
+ Label(' numbers 1...12', { common:leftAligned, pad:4, fillx:1 }),
+ ], filly:1, onTouch:checkInnerRadiobutton },
+ { type:'h', c:[
+ { width:30 },
+ Checkbox({ id:'colored', common:legible, onChange:toggleColored }),
+ Label(' colorful', { common:leftAligned, pad:4, fillx:1 }),
+ ], filly:1, onTouch:toggleInnerCheckbox },
+ { height:4 },
+ { type:'h', c:[
+ Button('Ok', { common:legible, width:OkCancelWidth, onTouch:applyChanges }),
+ { width:4 },
+ Button('Cancel', { common:legible, width:OkCancelWidth, onTouch:withdrawChanges }),
+ ], filly:1 },
+ ], bgCol:'#FFFFFF'
+ });
+
+
+/**** HandsScreen ****/
+
+ let HandsScreen = new Layout({
+ type:'v', c:[
+ Label('Clock Hands', { common:legible, bold:true, filly:1 }),
+ { height:4 },
+ { type:'h', c:[
+ { width:4 },
+ Radiobutton({ id:'simple', GroupName:'Faces', common:legible, onChange:chooseHand }),
+ Label(' simple', { common:leftAligned, pad:4, fillx:1 }),
+ ], filly:1, onTouch:checkInnerRadiobutton },
+ { type:'h', c:[
+ { width:4 },
+ Radiobutton({ id:'rounded', GroupName:'Faces', common:legible, onChange:chooseHand }),
+ Label(' rounded + Bolt', { common:leftAligned, pad:4, fillx:1 }),
+ ], filly:1, onTouch:checkInnerRadiobutton },
+ { type:'h', c:[
+ { width:4 },
+ Radiobutton({ id:'hollow', GroupName:'Faces', common:legible, onChange:chooseHand }),
+ Label(' hollow + Bolt', { common:leftAligned, pad:4, fillx:1 }),
+ ], filly:1, onTouch:checkInnerRadiobutton },
+ { type:'h', c:[
+ { width:4 },
+ Checkbox({ id:'withSeconds', common:legible, onChange:toggleSeconds }),
+ Label(' show Seconds', { common:leftAligned, pad:4, fillx:1 }),
+ ], filly:1, onTouch:toggleInnerCheckbox },
+ { height:4 },
+ { type:'h', c:[
+ Button('Ok', { common:legible, width:OkCancelWidth, onTouch:applyChanges }),
+ { width:4 },
+ Button('Cancel', { common:legible, width:OkCancelWidth, onTouch:withdrawChanges }),
+ ], filly:1 },
+ ], bgCol:'#FFFFFF'
+ });
+
+
+/**** ColorsScreen ****/
+
+ let ColorsScreen = new Layout({
+ type:'v', c:[
+ Label('Clock Colors', { common:legible, bold:true, filly:1 }),
+ { height:4 },
+ { type:'h', c:[
+ { width:4 },
+ Label('Foreground', { common:leftAligned, pad:4, fillx:1 }),
+ Label('', { id:'Foreground', common:ColorView, bgCol:Theme.fg }),
+ { width:4 },
+ ], filly:1, onTouch:selectForegroundColor },
+ { type:'h', c:[
+ { width:4 },
+ Label('Background', { common:leftAligned, pad:4, fillx:1 }),
+ Label('', { id:'Background', common:ColorView, bgCol:Theme.bg }),
+ { width:4 },
+ ], filly:1, onTouch:selectBackgroundColor },
+ { type:'h', c:[
+ { width:4 },
+ Label('Seconds', { common:leftAligned, pad:4, fillx:1 }),
+ Label('', { id:'Seconds', common:ColorView, bgCol:Theme.fgH }),
+ { width:4 },
+ ], filly:1, onTouch:selectSecondsColor },
+ { height:4 },
+ { type:'h', c:[
+ Button('Ok', { common:legible, width:OkCancelWidth, onTouch:applyChanges }),
+ { width:4 },
+ Button('Cancel', { common:legible, width:OkCancelWidth, onTouch:withdrawChanges }),
+ ], filly:1 },
+ ], bgCol:'#FFFFFF'
+ });
+
+
+/**** ColorChoiceScreen ****/
+
+ function drawColorChoice (x,y, Width,Height, Details) {
+ let selected = Details.selected;
+ if (selected) {
+ g.setColor('#FF0000');
+ g.fillPoly([
+ x,y, x+Width-1,y, x+Width-1,y+Height-1, x,y+Height-1, x,y,
+ x+3,y+3, x+3,y+Height-4, x+Width-4,y+Height-4, x+Width-4,y+3, x+3,y+3
+ ]);
+ } else {
+ g.setColor('#000000');
+ g.drawRect(x+3,y+3, x+Width-4,y+Height-4);
+ }
+
+ g.setColor(Details.col);
+ g.fillRect(x+4,y+4, x+Width-5,y+Height-5);
+ }
+
+ let ColorChoiceScreen = new Layout({
+ type:'v', c:[
+ Label('Choose Color', { common:legible, bold:true, filly:1 }),
+ { height:4 },
+ { type:'h', c:[
+ Drawable(drawColorChoice, { id:'#000000', common:ColorChoice, col:'#000000' }),
+ { width:8 },
+ Drawable(drawColorChoice, { id:'#FF0000', common:ColorChoice, col:'#FF0000' }),
+ { width:8 },
+ Drawable(drawColorChoice, { id:'#00FF00', common:ColorChoice, col:'#00FF00' }),
+ { width:8 },
+ Drawable(drawColorChoice, { id:'#0000FF', common:ColorChoice, col:'#0000FF' }),
+ ], filly:1 },
+ { type:'h', c:[
+ Drawable(drawColorChoice, { id:'#FFFFFF', common:ColorChoice, col:'#FFFFFF' }),
+ { width:8 },
+ Drawable(drawColorChoice, { id:'#FFFF00', common:ColorChoice, col:'#FFFF00' }),
+ { width:8 },
+ Drawable(drawColorChoice, { id:'#FF00FF', common:ColorChoice, col:'#FF00FF' }),
+ { width:8 },
+ Drawable(drawColorChoice, { id:'#00FFFF', common:ColorChoice, col:'#00FFFF' }),
+ ], filly:1 },
+ { type:'h', c:[
+ Label('use Theme:', { id:'Theme', common:leftAligned, pad:4 }),
+ { width:10 },
+ Drawable(drawColorChoice, { id:'Theme', common:ColorChoice, col:Theme.fg }),
+ ], filly:1, onTouch:chooseThemeColor },
+ { height:4 },
+ { type:'h', c:[
+ Button('Ok', { common:legible, width:OkCancelWidth, onTouch:applyColor }),
+ { width:4 },
+ Button('Cancel', { common:legible, width:OkCancelWidth, onTouch:withdrawColor }),
+ ], filly:1 },
+ ], bgCol:'#FFFFFF'
+ });
+
+
+
+ readSettings();
+
+ Bangle.on('swipe', (Direction) => {
+ if (Direction === 0) { openMainScreen(); }
+ });
+
+ setTimeout(refreshClock, 500); // enqueue first draw request
+
+ Bangle.on('lcdPower', (on) => {
+ if (on) {
+ if (Timer != null) { clearTimeout(Timer); Timer = undefined; }
+ refreshClock();
+ }
+ });
+
+ Bangle.setUI('clock');
diff --git a/apps/coretemp/ChangeLog b/apps/coretemp/ChangeLog
index ea6911f1a..ad6f0742d 100644
--- a/apps/coretemp/ChangeLog
+++ b/apps/coretemp/ChangeLog
@@ -1,2 +1,3 @@
0.01: New app
0.02: Cleanup interface and add settings, widget, add skin temp reporting.
+0.03: Move code for recording to this app
diff --git a/apps/coretemp/recorder.js b/apps/coretemp/recorder.js
new file mode 100644
index 000000000..1499605f3
--- /dev/null
+++ b/apps/coretemp/recorder.js
@@ -0,0 +1,31 @@
+(function(recorders) {
+ recorders.coretemp = function() {
+ var core = "", skin = "";
+ var hasCore = false;
+ function onCore(c) {
+ core=c.core;
+ skin=c.skin;
+ hasCore = true;
+ }
+ return {
+ name : "Core",
+ fields : ["Core","Skin"],
+ getValues : () => {
+ var r = [core,skin];
+ core = "";
+ skin = "";
+ return r;
+ },
+ start : () => {
+ hasCore = false;
+ Bangle.on('CoreTemp', onCore);
+ },
+ stop : () => {
+ hasCore = false;
+ Bangle.removeListener('CoreTemp', onCore);
+ },
+ draw : (x,y) => g.setColor(hasCore?"#0f0":"#8f8").drawImage(atob("DAyBAAHh0js3EuDMA8A8AWBnDj9A8A=="),x,y)
+ };
+ }
+})
+
diff --git a/apps/ffcniftya/ChangeLog b/apps/ffcniftya/ChangeLog
index 18bc264a3..420c553f5 100644
--- a/apps/ffcniftya/ChangeLog
+++ b/apps/ffcniftya/ChangeLog
@@ -1 +1,2 @@
0.01: New Clock Nifty A
+0.02: Shows the current week number (ISO8601), can be disabled via settings ""
diff --git a/apps/ffcniftya/README.md b/apps/ffcniftya/README.md
index f1fee9b1f..86f1f5c2d 100644
--- a/apps/ffcniftya/README.md
+++ b/apps/ffcniftya/README.md
@@ -1,4 +1,14 @@
# Nifty-A Clock
+Colors are black/white - photos have non correct camera color "blue"
+
+## This is the clock
+

+## The week number (ISO8601) can be turned of in settings
+(default is **"On"**)
+
+
+
+
diff --git a/apps/ffcniftya/app.js b/apps/ffcniftya/app.js
index 31742f64a..5da1ec48e 100644
--- a/apps/ffcniftya/app.js
+++ b/apps/ffcniftya/app.js
@@ -1,5 +1,6 @@
const locale = require("locale");
const is12Hour = (require("Storage").readJSON("setting.json", 1) || {})["12hour"];
+const CFG = require('Storage').readJSON("ffcniftya.json", 1) || {showWeekNum: true};
/* Clock *********************************************/
const scale = g.getWidth() / 176;
@@ -16,6 +17,18 @@ const center = {
y: Math.round(((viewport.height - widget) / 2) + widget),
}
+function ISO8601_week_no(date) { //copied from: https://gist.github.com/IamSilviu/5899269#gistcomment-3035480
+ var tdt = new Date(date.valueOf());
+ var dayn = (date.getDay() + 6) % 7;
+ tdt.setDate(tdt.getDate() - dayn + 3);
+ var firstThursday = tdt.valueOf();
+ tdt.setMonth(0, 1);
+ if (tdt.getDay() !== 4) {
+ tdt.setMonth(0, 1 + ((4 - tdt.getDay()) + 7) % 7);
+ }
+ return 1 + Math.ceil((firstThursday - tdt) / 604800000);
+}
+
function d02(value) {
return ('0' + value).substr(-2);
}
@@ -29,23 +42,26 @@ function draw() {
const minutes = d02(now.getMinutes());
const day = d02(now.getDate());
const month = d02(now.getMonth() + 1);
- const year = now.getFullYear();
-
- const month2 = locale.month(now, 3);
- const day2 = locale.dow(now, 3);
+ const year = now.getFullYear(now);
+ const weekNum = d02(ISO8601_week_no(now));
+ const monthName = locale.month(now, 3);
+ const dayName = locale.dow(now, 3);
+ const centerTimeScaleX = center.x + 32 * scale;
g.setFontAlign(1, 0).setFont("Vector", 90 * scale);
- g.drawString(hour, center.x + 32 * scale, center.y - 31 * scale);
- g.drawString(minutes, center.x + 32 * scale, center.y + 46 * scale);
+ g.drawString(hour, centerTimeScaleX, center.y - 31 * scale);
+ g.drawString(minutes, centerTimeScaleX, center.y + 46 * scale);
g.fillRect(center.x + 30 * scale, center.y - 72 * scale, center.x + 32 * scale, center.y + 74 * scale);
+ const centerDatesScaleX = center.x + 40 * scale;
g.setFontAlign(-1, 0).setFont("Vector", 16 * scale);
- g.drawString(year, center.x + 40 * scale, center.y - 62 * scale);
- g.drawString(month, center.x + 40 * scale, center.y - 44 * scale);
- g.drawString(day, center.x + 40 * scale, center.y - 26 * scale);
- g.drawString(month2, center.x + 40 * scale, center.y + 48 * scale);
- g.drawString(day2, center.x + 40 * scale, center.y + 66 * scale);
+ g.drawString(year, centerDatesScaleX, center.y - 62 * scale);
+ g.drawString(month, centerDatesScaleX, center.y - 44 * scale);
+ g.drawString(day, centerDatesScaleX, center.y - 26 * scale);
+ if (CFG.showWeekNum) g.drawString(d02(ISO8601_week_no(now)), centerDatesScaleX, center.y + 15 * scale);
+ g.drawString(monthName, centerDatesScaleX, center.y + 48 * scale);
+ g.drawString(dayName, centerDatesScaleX, center.y + 66 * scale);
}
diff --git a/apps/ffcniftya/screenshot_nifty.png b/apps/ffcniftya/screenshot_nifty.png
index 0df056223..de939f6ba 100644
Binary files a/apps/ffcniftya/screenshot_nifty.png and b/apps/ffcniftya/screenshot_nifty.png differ
diff --git a/apps/ffcniftya/screenshot_settings_nifty.png b/apps/ffcniftya/screenshot_settings_nifty.png
new file mode 100644
index 000000000..b81a4662c
Binary files /dev/null and b/apps/ffcniftya/screenshot_settings_nifty.png differ
diff --git a/apps/ffcniftya/settings.js b/apps/ffcniftya/settings.js
new file mode 100644
index 000000000..46e4ef5aa
--- /dev/null
+++ b/apps/ffcniftya/settings.js
@@ -0,0 +1,23 @@
+(function(back) {
+ var FILE = "ffcniftya.json";
+ // Load settings
+ var cfg = require('Storage').readJSON(FILE, 1) || { showWeekNum: true };
+
+ function writeSettings() {
+ require('Storage').writeJSON(FILE, cfg);
+ }
+
+ // Show the menu
+ E.showMenu({
+ "" : { "title" : "Nifty-A Clock" },
+ "< Back" : () => back(),
+ 'week number?': {
+ value: cfg.showWeekNum,
+ format: v => v?"On":"Off",
+ onchange: v => {
+ cfg.showWeekNum = v;
+ writeSettings();
+ }
+ }
+ });
+})
\ No newline at end of file
diff --git a/apps/ftclock/ChangeLog b/apps/ftclock/ChangeLog
index 9db0e26c5..c944dd9ac 100644
--- a/apps/ftclock/ChangeLog
+++ b/apps/ftclock/ChangeLog
@@ -1 +1,2 @@
0.01: first release
+0.02: RAM efficient version of `fourTwentyTz.js` (as suggested by @gfwilliams).
diff --git a/apps/ftclock/app.js b/apps/ftclock/app.js
index 1aed8da54..b12db10f1 100644
--- a/apps/ftclock/app.js
+++ b/apps/ftclock/app.js
@@ -17,14 +17,13 @@ function queueDraw() {
function draw() {
g.reset();
- g.setBgColor("#ffffff");
let date = new Date();
let timeStr = require("locale").time(date,1);
let next420 = getNextFourTwenty();
g.clearRect(0,26,g.getWidth(),g.getHeight());
g.setColor("#00ff00").setFontAlign(0,-1).setFont("Teletext10x18Ascii",2);
g.drawString(next420.minutes? timeStr: `\0${leaf_img}${timeStr}\0${leaf_img}`, g.getWidth()/2, 28);
- g.setColor("#000000");
+ g.setColor(g.theme.fg);
g.setFontAlign(-1,-1).setFont("Teletext10x18Ascii");
g.drawString(g.wrapString(next420.text, g.getWidth()-8).join("\n"),4,60);
diff --git a/apps/ftclock/fourTwenty.js b/apps/ftclock/fourTwenty.js
index ac15f40e6..b2a2aa8fb 100644
--- a/apps/ftclock/fourTwenty.js
+++ b/apps/ftclock/fourTwenty.js
@@ -1,4 +1,6 @@
-let timezones = require("fourTwentyTz").timezones;
+let ftz = require("fourTwentyTz"),
+ offsets = ftz.offsets,
+ timezones = ftz.timezones;
function get420offset() {
let current_time = Math.floor((Date.now()%(24*3600*1000))/60000);
@@ -24,10 +26,10 @@ function makeFourTwentyText(minutes, places) {
function getNextFourTwenty() {
let offs = get420offset();
- for (let i=0; i {
if (err) {
console.log("Can't open output file");
@@ -65,8 +69,18 @@ fs.createReadStream(__dirname+'/country.csv')
fs.write(fd, "// Generated by mkFourTwentyTz.js\n", handleWrite);
fs.write(fd, `// ${Date()}\n`, handleWrite);
fs.write(fd, "// Data source: https://timezonedb.com/files/timezonedb.csv.zip\n", handleWrite);
- fs.write(fd, "exports.timezones = ", handleWrite);
- fs.write(fd, JSON.stringify(offsdict, null, 4), handleWrite);
+ fs.write(fd, "exports.offsets = ", handleWrite);
+ fs.write(fd, JSON.stringify(offsets), handleWrite);
+ fs.write(fd, ";\n", handleWrite);
+ fs.write(fd, "exports.timezones = function(offs) {\n", handleWrite);
+ fs.write(fd, " switch (offs) {\n", handleWrite);
+ for (i=0; i load()
};
// "Current Mode""Silent" won't fit on Bangle.js 2
menu["Current"+((process.env.HWVERSION===2) ? "" : " Mode")] = {
value: current,
- format: v => modeNames[v],
- onchange: function(v) {
- if (v<0) {v = 2;}
- if (v>2) {v = 0;}
- require("qmsched").setMode(v);
- current = v;
- this.value = v;
- },
+ min:0, max:2, wrap: true,
+ format: () => modeNames[current],
+ onchange: require("qmsched").setMode, // library calls setAppMode(), which updates `current`
};
scheds.sort((a, b) => (a.hr-b.hr));
scheds.forEach((sched, idx) => {
menu[formatTime(sched.hr)] = {
format: () => modeNames[sched.mode], // abuse format to right-align text
- onchange: function() {
- _m.draw = ()=> {}; // prevent redraw of main menu over edit menu
+ onchange: () => {
+ m.draw = ()=> {}; // prevent redraw of main menu over edit menu (needed because we abuse format/onchange)
showEditMenu(idx);
}
};
});
menu["Add Schedule"] = () => showEditMenu(-1);
+ menu["Switch Theme"] = {
+ value: !!get("switchTheme"),
+ format: v => v ? /*LANG*/"Yes" : /*LANG*/"No",
+ onchange: v => v ? set("switchTheme", v) : unset("switchTheme"),
+ };
menu["LCD Settings"] = () => showOptionsMenu();
- _m = E.showMenu(menu);
+ m = E.showMenu(menu);
}
function showEditMenu(index) {
@@ -125,31 +155,19 @@ function showEditMenu(index) {
"< Cancel": () => showMainMenu(),
"Hours": {
value: hrs,
- onchange: function(v) {
- if (v<0) {v = 23;}
- if (v>23) {v = 0;}
- hrs = v;
- this.value = v;
- }, // no arrow fn -> preserve 'this'
+ min:0, max:23, wrap:true,
+ onchange: v => {hrs = v;},
},
"Minutes": {
value: mins,
- onchange: function(v) {
- if (v<0) {v = 59;}
- if (v>59) {v = 0;}
- mins = v;
- this.value = v;
- }, // no arrow fn -> preserve 'this'
+ min:0, max:55, step:5, wrap:true,
+ onchange: v => {mins = v;},
},
"Switch to": {
value: mode,
+ min:0, max:2, wrap:true,
format: v => modeNames[v],
- onchange: function(v) {
- if (v<0) {v = 2;}
- if (v>2) {v = 0;}
- mode = v;
- this.value = v;
- }, // no arrow fn -> preserve 'this'
+ onchange: v => {mode = v;},
},
};
function getSched() {
@@ -174,7 +192,7 @@ function showEditMenu(index) {
showMainMenu();
};
}
- return E.showMenu(menu);
+ m = E.showMenu(menu);
}
function showOptionsMenu() {
@@ -244,7 +262,7 @@ function showOptionsMenu() {
onchange: () => {toggle("wakeOnTwist");},
},
};
- return E.showMenu(oMenu);
+ m = E.showMenu(oMenu);
}
loadSettings();
diff --git a/apps/qmsched/boot.js b/apps/qmsched/boot.js
index c3bc49b58..c4610ce3e 100644
--- a/apps/qmsched/boot.js
+++ b/apps/qmsched/boot.js
@@ -1,5 +1,7 @@
// apply Quiet Mode schedules
(function qm() {
+ if (Bangle.qmTimeout) clearTimeout(Bangle.qmTimeout); // so the app can eval() this file to apply changes right away
+ delete Bangle.qmTimeout;
let bSettings = require('Storage').readJSON('setting.json',true)||{};
const curr = 0|bSettings.quiet;
delete bSettings;
@@ -18,7 +20,7 @@
let t = 3600000*(next.hr-hr); // timeout in milliseconds
if (t<0) {t += 86400000;} // scheduled for tomorrow: add a day
/* update quiet mode at the correct time. */
- setTimeout(() => {
+ Bangle.qmTimeout=setTimeout(() => {
require("qmsched").setMode(mode);
qm(); // schedule next update
}, t);
diff --git a/apps/qmsched/lib.js b/apps/qmsched/lib.js
index e9ed3ec90..9696657cc 100644
--- a/apps/qmsched/lib.js
+++ b/apps/qmsched/lib.js
@@ -1,5 +1,31 @@
/**
- * Apply LCD options for given mode
+ * Apply appropriate theme for given mode
+ * @param {int} mode Quiet Mode
+ */
+function switchTheme(mode) {
+ if (!!mode === g.theme.dark) return; // nothing to do
+ let s = require("Storage").readJSON("setting.json", 1) || {};
+ // default themes, copied from settings.js:showThemeMenu()
+ function cl(x) { return g.setColor(x).getColor(); }
+ s.theme = mode ? {
+ // 'Dark BW'
+ fg: cl("#fff"), bg: cl("#000"),
+ fg2: cl("#0ff"), bg2: cl("#000"),
+ fgH: cl("#fff"), bgH: cl("#00f"),
+ dark: true
+ } : {
+ // 'Light BW'
+ fg: cl("#000"), bg: cl("#fff"),
+ fg2: cl("#000"), bg2: cl("#cff"),
+ fgH: cl("#000"), bgH: cl("#0ff"),
+ dark: false
+ };
+ require("Storage").writeJSON("setting.json", s);
+ // reload clocks with new theme, otherwise just wait for user to switch apps
+ if (Bangle.CLOCK) load(global.__FILE__);
+}
+/**
+ * Apply LCD options and theme for given mode
* @param {int} mode Quiet Mode
*/
exports.applyOptions = function(mode) {
@@ -8,6 +34,7 @@ exports.applyOptions = function(mode) {
Bangle.setOptions(get("options", {}));
Bangle.setLCDBrightness(get("brightness", 1));
Bangle.setLCDTimeout(get("timeout", 10));
+ if ((require("Storage").readJSON("qmsched.json", 1) || {}).switchTheme) switchTheme(mode);
};
/**
* Set new Quiet Mode and apply Bangle options
@@ -20,4 +47,5 @@ exports.setMode = function(mode) {
));
exports.applyOptions(mode);
if (typeof WIDGETS === "object" && "qmsched" in WIDGETS) WIDGETS["qmsched"].draw();
+ if (global.setAppQuietMode) setAppQuietMode(mode); // current app knows how to update itself
};
diff --git a/apps/qmsched/widget.js b/apps/qmsched/widget.js
index b25192b06..daa11ac71 100644
--- a/apps/qmsched/widget.js
+++ b/apps/qmsched/widget.js
@@ -18,7 +18,7 @@
return; // drawWidgets will call draw again
}
let x = this.x, y = this.y;
- g.clearRect(x, y, x+23, y+23);
+ g.reset().clearRect(x, y, x+23, y+23);
// quiet mode: draw red one-way-street sign (dim red on Bangle.js 1)
x = this.x+11;y = this.y+11; // center of widget
g.setColor(process.env.HWVERSION===2 ? 1 : 0.8, 0, 0).fillCircle(x, y, 8);
diff --git a/apps/recorder/ChangeLog b/apps/recorder/ChangeLog
index dbf086f7d..e2ae0111b 100644
--- a/apps/recorder/ChangeLog
+++ b/apps/recorder/ChangeLog
@@ -8,3 +8,6 @@
Fix execution of other recorders (*.recorder.js)
Modified icons and colors for better visibility
Only show plotting speed if Latitude is available
+0.07: Add recording for Barometer
+ Record all HRM events
+ Move recording for CoreTemp to its own app
diff --git a/apps/recorder/widget.js b/apps/recorder/widget.js
index 8f82f1f37..de465b7c1 100644
--- a/apps/recorder/widget.js
+++ b/apps/recorder/widget.js
@@ -52,19 +52,17 @@
};
},
hrm:function() {
- var bpm = 0, bpmConfidence = 0;
+ var bpm = "", bpmConfidence = "";
function onHRM(h) {
- if (h.confidence >= bpmConfidence) {
- bpmConfidence = h.confidence;
- bpm = h.bpm;
- }
+ bpmConfidence = h.confidence;
+ bpm = h.bpm;
}
return {
name : "HR",
fields : ["Heartrate", "Confidence"],
getValues : () => {
var r = [bpm,bpmConfidence];
- bpm = 0; bpmConfidence = 0;
+ bpm = ""; bpmConfidence = "";
return r;
},
start : () => {
@@ -92,32 +90,6 @@
draw : (x,y) => g.setColor(Bangle.isCharging() ? "#0f0" : "#ff0").drawImage(atob("DAwBAABgH4G4EYG4H4H4H4GIH4AA"),x,y)
};
},
- temp:function() {
- var core = 0, skin = 0;
- var hasCore = false;
- function onCore(c) {
- core=c.core;
- skin=c.skin;
- hasCore = true;
- }
- return {
- name : "Core",
- fields : ["Core","Skin"],
- getValues : () => {
- var r = [core,skin];
- return r;
- },
- start : () => {
- hasCore = false;
- Bangle.on('CoreTemp', onCore);
- },
- stop : () => {
- hasCore = false;
- Bangle.removeListener('CoreTemp', onCore);
- },
- draw : (x,y) => g.setColor(hasCore?"#0f0":"#8f8").drawImage(atob("DAwBAAAOAKPOfgZgZgZgZgfgPAAA"),x,y)
- };
- },
steps:function() {
var lastSteps = 0;
return {
@@ -133,8 +105,38 @@
draw : (x,y) => g.reset().drawImage(atob("DAwBAAMMeeeeeeeecOMMAAMMMMAA"),x,y)
};
}
- // TODO: recAltitude from pressure sensor
};
+ if (Bangle.getPressure){
+ recorders['baro'] = function() {
+ var temp="",press="",alt="";
+ function onPress(c) {
+ temp=c.temperature;
+ press=c.pressure;
+ alt=c.altitude;
+ }
+ return {
+ name : "Baro",
+ fields : ["Barometer Temperature", "Barometer Pressure", "Barometer Altitude"],
+ getValues : () => {
+ var r = [temp,press,alt];
+ temp="";
+ press="";
+ alt="";
+ return r;
+ },
+ start : () => {
+ Bangle.setBarometerPower(1,"recorder");
+ Bangle.on('pressure', onPress);
+ },
+ stop : () => {
+ Bangle.setBarometerPower(0,"recorder");
+ Bangle.removeListener('pressure', onPress);
+ },
+ draw : (x,y) => g.setColor("#0f0").drawImage(atob("DAwBAAH4EIHIEIHIEIHIEIEIH4AA"),x,y)
+ };
+ }
+ }
+
/* eg. foobar.recorder.js
(function(recorders) {
recorders.foobar = {
diff --git a/apps/run/ChangeLog b/apps/run/ChangeLog
new file mode 100644
index 000000000..5560f00bc
--- /dev/null
+++ b/apps/run/ChangeLog
@@ -0,0 +1 @@
+0.01: New App!
diff --git a/apps/run/README.md b/apps/run/README.md
new file mode 100644
index 000000000..c094d4873
--- /dev/null
+++ b/apps/run/README.md
@@ -0,0 +1,34 @@
+# Run App
+
+This app allows you to display the status of your run, it
+shows distance, time, steps, cadence, pace and more.
+
+To use it, start the app and press the middle button so that
+the red `STOP` in the bottom right turns to a green `RUN`.
+
+## Display
+
+* `DIST` - the distance travelled based on the GPS (if you have a GPS lock).
+ * NOTE: this is based on the GPS coordinates which are not 100% accurate, especially initially. As
+ the GPS updates your position as it gets more satellites your position changes and the distance
+ shown will increase, even if you are standing still.
+* `TIME` - the elapsed time for your run
+* `PACE` - the number of minutes it takes you to run a kilometer **based on your run so far**
+* `HEART` - Your heart rate
+* `STEPS` - Steps since you started exercising
+* `CADENCE` - Steps per second based on your step rate *over the last minute*
+* `GPS` - this is green if you have a GPS lock. GPS is turned on automatically
+so if you have no GPS lock you just need to wait.
+* The current time is displayed right at the bottom of the screen
+* `RUN/STOP` - whether the distance for your run is being displayed or not
+
+## Recording Tracks
+
+`Run` doesn't directly allow you to record your tracks at the moment.
+However you can just install the `Recorder` app, turn recording on in
+that, and then start the `Run` app.
+
+## TODO
+
+* Allow this app to trigger the `Recorder` app on and off directly.
+* Keep a log of each run's stats (distance/steps/etc)
diff --git a/apps/run/app-icon.js b/apps/run/app-icon.js
new file mode 100644
index 000000000..a97d1b8ce
--- /dev/null
+++ b/apps/run/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEw4UA///pH9vEFt9TIW0FqALJitUBZNVqoLqgo4BHZAUBtBTHgILB1XAEREV1WsEQ9AgWq1ALHgEO1WtBYxCBhWq0pdInWq2tABY8q1WVBZGq1XFBZS/IKQRvCDIsP9WsBZP60CTCBYs//+wLxALBTQ4AB///+AKHgYLB/gLK/4LHh//AIIwFitVr/8DIIwFLANXBAILIqogBn7DBEYrXBeQRgIBYKmHDgYLLZRBACBZYKJZIILKKRZeWgJGKAFQA=="))
diff --git a/apps/run/app.js b/apps/run/app.js
new file mode 100644
index 000000000..44c42711c
--- /dev/null
+++ b/apps/run/app.js
@@ -0,0 +1,158 @@
+var B2 = process.env.HWVERSION==2;
+var Layout = require("Layout");
+var locale = require("locale")
+var fontHeading = "6x8:2";
+var fontValue = B2 ? "6x15:2" : "6x8:3";
+var headingCol = "#888";
+var running = false;
+var startTime;
+var startSteps;
+// This & previous GPS readings
+var lastGPS, thisGPS;
+var distance = 0; ///< distance in meters
+var startSteps = Bangle.getStepCount(); ///< number of steps when we started
+var lastStepCount = startSteps; // last time 'step' was called
+var stepHistory = new Uint8Array(60); // steps each second for the last minute (0 = current minute)
+
+g.clear();
+Bangle.loadWidgets();
+Bangle.drawWidgets();
+
+// ---------------------------
+
+function formatTime(ms) {
+ var s = Math.round(ms/1000);
+ var min = Math.floor(s/60).toString();
+ s = (s%60).toString();
+ return min.padStart(2,0)+":"+s.padStart(2,0);
+}
+
+// Format speed in meters/second
+function formatPace(speed) {
+ if (speed < 0.1667) {
+ return `__'__"`;
+ }
+ const pace = Math.round(1000 / speed); // seconds for 1km
+ const min = Math.floor(pace / 60); // minutes for 1km
+ const sec = pace % 60;
+ return ('0' + min).substr(-2) + `'` + ('0' + sec).substr(-2) + `"`;
+}
+
+// ---------------------------
+
+function clearState() {
+ distance = 0;
+ startSteps = Bangle.getStepCount();
+ stepHistory.fill(0);
+ layout.dist.label=locale.distance(distance);
+ layout.time.label="00:00";
+ layout.pace.label=formatPace(0);
+ layout.hrm.label="--";
+ layout.steps.label=0;
+ layout.cadence.label= "0";
+ layout.status.bgCol = "#f00";
+}
+
+function onStartStop() {
+ running = !running;
+ if (running) {
+ clearState();
+ startTime = Date.now();
+ }
+ layout.button.label = running ? "STOP" : "START";
+ layout.status.label = running ? "RUN" : "STOP";
+ layout.status.bgCol = running ? "#0f0" : "#f00";
+ // if stopping running, don't clear state
+ // so we can at least refer to what we've done
+ layout.render();
+}
+
+var layout = new Layout( {
+ type:"v", c: [
+ { type:"h", filly:1, c:[
+ {type:"txt", font:fontHeading, label:"DIST", fillx:1, col:headingCol },
+ {type:"txt", font:fontHeading, label:"TIME", fillx:1, col:headingCol }
+ ]}, { type:"h", filly:1, c:[
+ {type:"txt", font:fontValue, label:"0.00", id:"dist", fillx:1 },
+ {type:"txt", font:fontValue, label:"00:00", id:"time", fillx:1 }
+ ]}, { type:"h", filly:1, c:[
+ {type:"txt", font:fontHeading, label:"PACE", fillx:1, col:headingCol },
+ {type:"txt", font:fontHeading, label:"HEART", fillx:1, col:headingCol }
+ ]}, { type:"h", filly:1, c:[
+ {type:"txt", font:fontValue, label:`__'__"`, id:"pace", fillx:1 },
+ {type:"txt", font:fontValue, label:"--", id:"hrm", fillx:1 }
+ ]}, { type:"h", filly:1, c:[
+ {type:"txt", font:fontHeading, label:"STEPS", fillx:1, col:headingCol },
+ {type:"txt", font:fontHeading, label:"CADENCE", fillx:1, col:headingCol }
+ ]}, { type:"h", filly:1, c:[
+ {type:"txt", font:fontValue, label:"0", id:"steps", fillx:1 },
+ {type:"txt", font:fontValue, label:"0", id:"cadence", fillx:1 }
+ ]}, { type:"h", filly:1, c:[
+ {type:"txt", font:fontHeading, label:"GPS", id:"gps", fillx:1, bgCol:"#f00" },
+ {type:"txt", font:fontHeading, label:"00:00", id:"clock", fillx:1, bgCol:g.theme.fg, col:g.theme.bg },
+ {type:"txt", font:fontHeading, label:"STOP", id:"status", fillx:1 }
+ ]},
+
+ ]
+},{lazy:true, btns:[{ label:"START", cb: onStartStop, id:"button"}]});
+clearState();
+layout.render();
+
+
+
+function onTimer() {
+ layout.clock.label = locale.time(new Date(),1);
+ if (!running) {
+ layout.render();
+ return;
+ }
+ // called once a second
+ var duration = Date.now() - startTime; // in ms
+ // set cadence based on steps over last minute
+ var stepsInMinute = E.sum(stepHistory);
+ var cadence = 60000 * stepsInMinute / Math.min(duration,60000);
+ // update layout
+ layout.time.label = formatTime(duration);
+ layout.steps.label = Bangle.getStepCount()-startSteps;
+ layout.cadence.label = Math.round(cadence);
+ layout.render();
+ // move step history onwards
+ stepHistory.set(stepHistory,1);
+ stepHistory[0]=0;
+}
+
+Bangle.on("GPS", function(fix) {
+ layout.gps.bgCol = fix.fix ? "#0f0" : "#f00";
+ lastGPS = thisGPS;
+ thisGPS = fix;
+ if (running && fix.fix && lastGPS.fix) {
+ // work out distance - moving from a to b
+ var a = Bangle.project(lastGPS);
+ var b = Bangle.project(thisGPS);
+ var dx = a.x-b.x, dy = a.y-b.y;
+ var d = Math.sqrt(dx*dx+dy*dy); // this should be the distance in meters
+ distance += d;
+ layout.dist.label=locale.distance(distance);
+ var duration = Date.now() - startTime; // in ms
+ var speed = distance * 1000 / duration; // meters/sec
+ layout.pace.label = formatPace(speed);
+ }
+});
+Bangle.on("HRM", function(h) {
+ layout.hrm.label = h.bpm;
+});
+Bangle.on("step", function(steps) {
+ if (running) {
+ layout.steps.label = steps-Bangle.getStepCount();
+ stepHistory[0] += steps-lastStepCount;
+ }
+ lastStepCount = steps;
+});
+
+// We always call ourselves once a second, if only to update the time
+setInterval(onTimer, 1000);
+
+/* Turn GPS and HRM on right at the start to ensure
+we get the highest chance of a lock. */
+Bangle.setHRMPower(true,"app");
+Bangle.setGPSPower(true,"app");
diff --git a/apps/run/app.png b/apps/run/app.png
new file mode 100644
index 000000000..7059b8b01
Binary files /dev/null and b/apps/run/app.png differ
diff --git a/apps/run/screenshot.png b/apps/run/screenshot.png
new file mode 100644
index 000000000..1a813f19d
Binary files /dev/null and b/apps/run/screenshot.png differ
diff --git a/apps/simple_clock/LICENSE b/apps/simple_clock/LICENSE
new file mode 100644
index 000000000..7487dd5da
--- /dev/null
+++ b/apps/simple_clock/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Andreas Rozek
+
+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.
diff --git a/apps/themesetter/LICENSE b/apps/themesetter/LICENSE
new file mode 100644
index 000000000..7487dd5da
--- /dev/null
+++ b/apps/themesetter/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Andreas Rozek
+
+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.
diff --git a/apps/timeandlife/ChangeLog b/apps/timeandlife/ChangeLog
new file mode 100644
index 000000000..c7b309a74
--- /dev/null
+++ b/apps/timeandlife/ChangeLog
@@ -0,0 +1 @@
+0.1: New app
diff --git a/apps/timeandlife/README.md b/apps/timeandlife/README.md
new file mode 100644
index 000000000..4a638c952
--- /dev/null
+++ b/apps/timeandlife/README.md
@@ -0,0 +1,5 @@
+# Time and Life
+
+A simple watchface which displays the time when the screen is tapped and decays according to the rules of [Conway's game of life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life).
+
+
diff --git a/apps/timeandlife/app-icon.js b/apps/timeandlife/app-icon.js
new file mode 100644
index 000000000..d7608fca4
--- /dev/null
+++ b/apps/timeandlife/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwkB/4AGCY4PHC/4X/C/4X/C/4XvJ/4X/C/4X/C/4X3AH4A/AH4A/AH4A/"))
diff --git a/apps/timeandlife/app.js b/apps/timeandlife/app.js
new file mode 100644
index 000000000..4fe758815
--- /dev/null
+++ b/apps/timeandlife/app.js
@@ -0,0 +1,225 @@
+// Globals
+const X = 176,
+ Y = 176; // screen resolution of bangle 2
+const STEP_TIMEOUT = 1000;
+const PAUSE_TIME = 3000;
+
+const ONE = [
+ [0, 1, 0],
+ [1, 1, 0],
+ [0, 1, 0],
+ [0, 1, 0],
+ [0, 1, 0],
+ [0, 1, 0],
+ [1, 1, 1],
+];
+const TWO = [
+ [0, 1, 0],
+ [1, 0, 1],
+ [0, 0, 1],
+ [0, 1, 0],
+ [1, 0, 0],
+ [1, 0, 0],
+ [1, 1, 1],
+];
+const THREE = [
+ [0, 1, 0],
+ [1, 0, 1],
+ [0, 0, 1],
+ [0, 1, 0],
+ [0, 0, 1],
+ [1, 0, 1],
+ [0, 1, 0],
+];
+const FOUR = [
+ [0, 0, 1],
+ [1, 0, 1],
+ [1, 0, 1],
+ [1, 1, 1],
+ [0, 0, 1],
+ [0, 0, 1],
+ [0, 0, 1],
+];
+const FIVE = [
+ [1, 1, 1],
+ [1, 0, 0],
+ [1, 0, 0],
+ [0, 1, 0],
+ [0, 0, 1],
+ [1, 0, 1],
+ [0, 1, 0],
+];
+const SIX = [
+ [0, 1, 0],
+ [1, 0, 1],
+ [1, 0, 0],
+ [1, 1, 0],
+ [1, 0, 1],
+ [1, 0, 1],
+ [0, 1, 0],
+];
+const SEVEN = [
+ [1, 1, 1],
+ [1, 0, 1],
+ [0, 0, 1],
+ [0, 1, 0],
+ [0, 1, 0],
+ [0, 1, 0],
+ [0, 1, 0],
+];
+const EIGHT = [
+ [0, 1, 0],
+ [1, 0, 1],
+ [1, 0, 1],
+ [0, 1, 0],
+ [1, 0, 1],
+ [1, 0, 1],
+ [0, 1, 0],
+];
+const NINE = [
+ [0, 1, 0],
+ [1, 0, 1],
+ [1, 0, 1],
+ [0, 1, 1],
+ [0, 0, 1],
+ [1, 0, 1],
+ [0, 1, 0],
+];
+const ZERO = [
+ [0, 1, 0],
+ [1, 0, 1],
+ [1, 0, 1],
+ [1, 0, 1],
+ [1, 0, 1],
+ [1, 0, 1],
+ [0, 1, 0],
+];
+const NUMBERS = [ZERO, ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE];
+
+// Arraybuffers to store game state
+// 484 8 bit integers that are either 1 or 0 form the 22 x 22 grid
+let data = new Uint8Array(484);
+let nextData = new Uint8Array(484);
+
+let palette = new Uint16Array(256); // palette for rendering data
+palette[0] = g.theme.bg;
+palette[1] = g.theme.fg;
+
+let lastPaused = new Date();
+
+// Conway's game of life
+// if < 2 neighbours, set off
+// if 2 or 3 neighbours, set on
+// if > 3 neighbours, set off
+/*const updateStateC = E.compiledC(`
+// void run(int, int)
+void run(char* n, char* m){
+ // n is a pointer to the first byte in data, m is for nextdata
+ int count = 0;
+ for (int i=0;i<484;i++) {
+ // Add 8 neighbours, wrapping around
+ count =
+ *(n+(i+484-23)%484) +
+ *(n+(i+484-22)%484) +
+ *(n+(i+484-21)%484) +
+ *(n+(i+484-1)%484) +
+ *(n+(i+484+1)%484) +
+ *(n+(i+484+21)%484) +
+ *(n+(i+484+22)%484) +
+ *(n+(i+484+23)%484);
+ if (count < 2 || count > 3) {
+ *(m+i) = 0;
+ } else {
+ *(m+i) = 1;
+ }
+ }
+}
+`);*/
+// precompiled - taken from file downloaded from Bangle.js storage after
+// Web IDE upload
+const updateStateC=function(a){return a=atob('ACLwtU/08nMBJxZGAvLNFQL1536V+/P0A/sUVJ778/UD+xXlAvLPHhD4BMBEXZ778/UD+xXlZERFXQLy4x4sRJ778/UD+xXlAvLlHkVdLESe+/P1A/sV5QLy+R5FXSxEnvvz9QP7FeUC9f1+RV0sRJ778/UD+xXlAvL7HkVdLESe+/P1A/sV5UVdLEQCPAIsNL88RjRGjFQBMrL18n+10fC9AAA='),{run:E.nativeCall(1,'void(int, int)',a)}}();
+
+function draw() {
+ g.drawImage({
+ width:22, height:22, bpp: 8,
+ palette : palette, // ideally we'd just have BPP 1 and would render direct but it makes the code tricky
+ buffer : data.buffer,
+ },0,0,{scale:8});
+}
+
+const step = () => {
+ if (new Date() - lastPaused < PAUSE_TIME) {
+ return;
+ }
+ let startTime = new Date();
+ const dataAddr = E.getAddressOf(data, true);
+ const nextDataAddr = E.getAddressOf(nextData, true);
+ updateStateC.run(dataAddr, nextDataAddr);
+ draw();
+ data.set(nextData);
+};
+
+const setPixel = (i, j) => {
+ data[i * 22 + j] = 1;
+ nextData[i * 22 + j] = 1;
+};
+
+const setNum = (character, i, j) => {
+ const startJ = j;
+ character.forEach(row => {
+ j = startJ;
+ row.forEach(pixel => {
+ if (pixel) setPixel(i, j);
+ j++;
+ });
+ i++;
+ });
+};
+
+const setDots = () => {
+ setPixel(10, 10);
+ setPixel(12, 10);
+};
+
+const drawTime = () => {
+ lastPaused = new Date();
+ g.clear();
+ data.fill(0);
+ const d = new Date();
+ const hourTens = Math.floor(d.getHours() / 10);
+ const hourOnes = d.getHours() % 10;
+ const minuteTens = Math.floor(d.getMinutes() / 10);
+ const minuteOnes = d.getMinutes() % 10;
+ setNum(NUMBERS[hourTens], 8, 1);
+ setNum(NUMBERS[hourOnes], 8, 6);
+ setDots();
+ setNum(NUMBERS[minuteTens], 8, 13);
+ setNum(NUMBERS[minuteOnes], 8, 18);
+ draw();
+};
+
+const start = () => {
+ Bangle.setUI("clock"); // Show launcher when middle button pressed
+ g.clear();
+ Bangle.setLCDTimeout(20); // backlight/lock timeout in seconds
+ let stepInterval = setInterval(step, STEP_TIMEOUT);
+
+ // Handlers
+ Bangle.on('touch', drawTime);
+
+ // Sleep mode
+ Bangle.on('lock', isLocked => {
+ if (stepInterval) {
+ clearInterval(stepInterval);
+ }
+ stepInterval = undefined;
+ if (!isLocked) {
+ drawTime();
+ stepInterval = setInterval(step, STEP_TIMEOUT);
+ }
+ });
+
+ drawTime();
+};
+
+start();
diff --git a/apps/timeandlife/app.png b/apps/timeandlife/app.png
new file mode 100644
index 000000000..b1e837d25
Binary files /dev/null and b/apps/timeandlife/app.png differ
diff --git a/apps/timeandlife/screenshot.png b/apps/timeandlife/screenshot.png
new file mode 100644
index 000000000..3058c9346
Binary files /dev/null and b/apps/timeandlife/screenshot.png differ
diff --git a/apps/widpa/ChangeLog b/apps/widpa/ChangeLog
new file mode 100644
index 000000000..7b83706bf
--- /dev/null
+++ b/apps/widpa/ChangeLog
@@ -0,0 +1 @@
+0.01: First release
diff --git a/apps/widpa/README.md b/apps/widpa/README.md
new file mode 100644
index 000000000..92fbb8c11
--- /dev/null
+++ b/apps/widpa/README.md
@@ -0,0 +1,16 @@
+# Simple Pedometer Widget
+
+*Displays the current step count from `Bangle.getHealthStatus("day").steps` in (6x8,2) font, Requires firmware v2.11.21 or later*
+
+* Designed to be small, minimal, does one thing well, no settings
+* Supports Bangle 1 and Bangle 2
+
+## Notes
+
+* Requires firmware v2.11.21 or later
+* `Bangle.getHealthStatus("day").steps` is reset to zero if you reboot your watch with a long BTN Press
+* The step count displayed may be a few steps more than that reported by widpedpm as widpedom may not always be loaded.
+
+
+
+Written by: [Hugh Barney](https://github.com/hughbarney) For support and discussion please post in the [Bangle JS Forum](http://forum.espruino.com/microcosms/1424/)
diff --git a/apps/widpa/screenshot_widpa.png b/apps/widpa/screenshot_widpa.png
new file mode 100644
index 000000000..e33550f50
Binary files /dev/null and b/apps/widpa/screenshot_widpa.png differ
diff --git a/apps/widpa/widpa.wid.js b/apps/widpa/widpa.wid.js
new file mode 100644
index 000000000..b8c439d2b
--- /dev/null
+++ b/apps/widpa/widpa.wid.js
@@ -0,0 +1,17 @@
+Bangle.on('step', function(s) { WIDGETS["widpa"].draw(); });
+Bangle.on('lcdPower', function(on) {
+ if (on) WIDGETS["widpa"].draw();
+});
+WIDGETS["widpa"]={area:"tl",width:13,draw:function() {
+ if (!Bangle.isLCDOn()) return; // dont redraw if LCD is off
+ var steps = Bangle.getHealthStatus("day").steps;
+ var w = 1 + (steps.toString().length)*12;
+ if (w > this.width) {this.width = w; setTimeout(() => Bangle.drawWidgets(),10); return;}
+ g.reset();
+ g.setColor(g.theme.bg);
+ g.fillRect(this.x, this.y, this.x + this.width, this.y + 23);
+ g.setColor(g.theme.fg);
+ g.setFont('6x8',2);
+ g.setFontAlign(-1, 0);
+ g.drawString(steps, this.x, this.y + 12);
+}};
diff --git a/apps/widpb/ChangeLog b/apps/widpb/ChangeLog
new file mode 100644
index 000000000..7b83706bf
--- /dev/null
+++ b/apps/widpb/ChangeLog
@@ -0,0 +1 @@
+0.01: First release
diff --git a/apps/widpb/README.md b/apps/widpb/README.md
new file mode 100644
index 000000000..bec127b6b
--- /dev/null
+++ b/apps/widpb/README.md
@@ -0,0 +1,17 @@
+# Lato Pedometer Widget
+
+*Displays the current step count from `Bangle.getHealthStatus("day").steps` in the Lato font, Requires firmware v2.11.21 or later*
+
+* Designed to be minimal, does one thing well, no settings
+* Supports Bangle 1 and Bangle 2
+
+## Notes
+
+* Requires firmware v2.11.21 or later
+* Uses the Lato custom font, so memory footprint is 500 bytes larger than 'Simple Pedometer Widget'
+* `Bangle.getHealthStatus("day").steps` is reset to zero if you reboot your watch with a long BTN Press
+* The step count displayed may be a few steps more than that reported by widpedpm as widpedom may not always be loaded.
+
+
+
+Written by: [Hugh Barney](https://github.com/hughbarney) For support and discussion please post in the [Bangle JS Forum](http://forum.espruino.com/microcosms/1424/)
diff --git a/apps/widpb/screenshot_widpb.png b/apps/widpb/screenshot_widpb.png
new file mode 100644
index 000000000..af1222e7e
Binary files /dev/null and b/apps/widpb/screenshot_widpb.png differ
diff --git a/apps/widpb/widpb.wid.js b/apps/widpb/widpb.wid.js
new file mode 100644
index 000000000..d65d7fe43
--- /dev/null
+++ b/apps/widpb/widpb.wid.js
@@ -0,0 +1,20 @@
+// on.step version
+Bangle.on('step', function(s) { WIDGETS["bata"].draw(); });
+Bangle.on('lcdPower', function(on) {
+ if (on) WIDGETS["bata"].draw();
+});
+WIDGETS["bata"]={area:"tl",width:13,draw:function() {
+ if (!Bangle.isLCDOn()) return; // dont redraw if LCD is off
+ var steps = Bangle.getHealthStatus("day").steps;
+ var w = 1 + (steps.toString().length)*12;
+ if (w > this.width) {this.width = w; setTimeout(() => Bangle.drawWidgets(),10); return;}
+ g.reset();
+ g.setColor(g.theme.bg);
+ g.fillRect(this.x, this.y, this.x + this.width, this.y + 23); // erase background
+ g.setColor(g.theme.fg);
+ // Lato from fonts.google.com, Actual height 17 (17 - 1), Numeric only
+ const scale = 1;
+ g.setFontCustom(atob("AAAAABwAAOAAAgAAHAADwAD4AB8AB8AA+AAeAADAAAAOAAP+AH/8B4DwMAGBgAwMAGBgAwOAOA//gD/4AD4AAAAAAAABgAAcAwDAGAwAwP/+B//wAAGAAAwAAGAAAAAAAAIAwHgOA4DwMA+BgOwMDmBg4wOeGA/gwDwGAAAAAAAAAGAHA8A4DwMAGBhAwMMGBjgwOcOA+/gDj4AAAAABgAAcAAHgADsAA5gAOMAHBgBwMAP/+B//wABgAAMAAAAAAAgD4OB/AwOYGBjAwMYGBjBwMe8Bh/AIHwAAAAAAAAAfAAP8AHxwB8GAdgwPMGBxgwMOOAB/gAH4AAAAAAABgAAMAABgAwMAeBgPgMHwBj4AN8AB+AAPAABAAAAAAAMfAH38B/xwMcGBhgwMMGBjgwP+OA+/gDj4AAAAAAAAOAAH4AA/gQMMGBgzwME8BhvAOPgA/4AD8AAEAAAAAAGAwA4OAHBwAAA="), 46, atob("BAgMDAwMDAwMDAwMBQ=="), 21+(scale<<8)+(1<<16));
+ g.setFontAlign(-1, 0);
+ g.drawString(steps, this.x, this.y + 12);
+}};
diff --git a/core b/core
index b05af96b2..649489412 160000
--- a/core
+++ b/core
@@ -1 +1 @@
-Subproject commit b05af96b2522a7a7225a56d804faf9383f8a8f97
+Subproject commit 649489412e27ef770bc0c8ed12cfca6a17a98c0d