diff --git a/apps/calendar/settings.js b/apps/calendar/settings.js
index 40eca9f68a..de731c632e 100644
--- a/apps/calendar/settings.js
+++ b/apps/calendar/settings.js
@@ -1,14 +1,14 @@
(function (back) {
var FILE = "calendar.json";
const HOLIDAY_FILE = "calendar.days.json";
- var settings = require('Storage').readJSON(FILE, true) || {};
+ var settings = require('Storage').readJSON(FILE, 1) || {};
if (settings.ndColors === undefined)
if (process.env.HWVERSION == 2) {
settings.ndColors = true;
} else {
settings.ndColors = false;
}
- const holidays = require("Storage").readJSON(HOLIDAY_FILE,1).sort((a,b) => new Date(a.date) - new Date(b.date)) || [];
+ const holidays = (require("Storage").readJSON(HOLIDAY_FILE,1)||[]).sort((a,b) => new Date(a.date) - new Date(b.date)) || [];
function writeSettings() {
require('Storage').writeJSON(FILE, settings);
diff --git a/apps/fwupdate/custom.html b/apps/fwupdate/custom.html
index ae955adedb..fd16aa878c 100644
--- a/apps/fwupdate/custom.html
+++ b/apps/fwupdate/custom.html
@@ -12,7 +12,8 @@
see the Bangle.js 1 instructions
-
Your current firmware version is unknown and DFU is unknown
+
Your current firmware version is unknown and DFU is unknown.
+ The DFU (bootloader) rarely changes, so it does not have to be the same version as your main firmware.
If you have an early (KickStarter or developer) Bangle.js device and still have the old 2v10.x DFU, the Firmware Update
diff --git a/apps/grocery/ChangeLog b/apps/grocery/ChangeLog
index cb066ef3d7..38cef7076d 100644
--- a/apps/grocery/ChangeLog
+++ b/apps/grocery/ChangeLog
@@ -2,3 +2,4 @@
0.02: Refactor code to store grocery list in separate file
0.03: Sort selected items to bottom and enable Widgets
0.04: Add settings to edit list on device
+0.05: Drop app customiser as it is redundant with download interface
diff --git a/apps/grocery/app.js b/apps/grocery/app.js
index a68f530107..c087f1311d 100644
--- a/apps/grocery/app.js
+++ b/apps/grocery/app.js
@@ -1,18 +1,19 @@
-var filename = 'grocery_list.json';
-var settings = require("Storage").readJSON(filename,1)|| { products: [] };
+{
+const filename = 'grocery_list.json';
+const settings = require("Storage").readJSON(filename,1)|| { products: [] };
let menu;
-function updateSettings() {
+const updateSettings = function() {
require("Storage").writeJSON(filename, settings);
Bangle.buzz();
-}
+};
-function twoChat(n){
+const twoChat = function(n) {
if(n<10) return '0'+n;
return ''+n;
-}
+};
-function sortMenu() {
+const sortMenu = function() {
mainMenu.sort((a,b) => {
const byValue = a.value-b.value;
return byValue !== 0 ? byValue : a.index-b.index;
@@ -20,7 +21,7 @@ function sortMenu() {
if (menu) {
menu.draw();
}
-}
+};
const mainMenu = settings.products.map((p,i) => ({
title: twoChat(p.quantity)+' '+p.name,
@@ -35,9 +36,14 @@ const mainMenu = settings.products.map((p,i) => ({
}));
sortMenu();
-mainMenu[''] = { 'title': 'Grocery list' };
+mainMenu[''] = {
+ 'title': 'Grocery list',
+ remove: () => {
+ },
+};
mainMenu['< Back'] = ()=>{load();};
Bangle.loadWidgets();
menu = E.showMenu(mainMenu);
Bangle.drawWidgets();
+}
diff --git a/apps/grocery/grocery.html b/apps/grocery/grocery.html
deleted file mode 100644
index e717dee2ec..0000000000
--- a/apps/grocery/grocery.html
+++ /dev/null
@@ -1,116 +0,0 @@
-
-
function save(){
settings.products = products;
Util.showModal("Saving...");
- localStorage.setItem('grocery-product-list',JSON.stringify(products));
Util.writeStorage("grocery_list.json", JSON.stringify(settings), () => {
Util.hideModal();
});
diff --git a/apps/grocery/metadata.json b/apps/grocery/metadata.json
index 8cf304cc14..9262b0c5e7 100644
--- a/apps/grocery/metadata.json
+++ b/apps/grocery/metadata.json
@@ -1,13 +1,12 @@
{
"id": "grocery",
"name": "Grocery",
- "version": "0.04",
+ "version": "0.05",
"description": "Simple grocery (shopping) list - Display a list of product and track if you already put them in your cart.",
"icon": "grocery.png",
"type": "app",
"tags": "tool,shopping,list",
"supports": ["BANGLEJS", "BANGLEJS2"],
- "custom": "grocery.html",
"interface": "interface.html",
"allow_emulator": true,
"dependencies": {"textinput":"type"},
diff --git a/apps/grocery/settings.js b/apps/grocery/settings.js
index 5f84fd7825..a39f12d8c6 100644
--- a/apps/grocery/settings.js
+++ b/apps/grocery/settings.js
@@ -118,7 +118,7 @@
/*LANG*/"Edit List": () => editlist(),
/*LANG*/"Add item": () => {
settings.products.push({
- "name":/*LANG*/"New item",
+ "name":/*LANG*/"New",
"quantity":1,
"ok":false
});
diff --git a/apps/widminbate/ChangeLog b/apps/widminbate/ChangeLog
index c1952a33d7..56c73beca3 100644
--- a/apps/widminbate/ChangeLog
+++ b/apps/widminbate/ChangeLog
@@ -3,3 +3,4 @@
0.03: Do not clear outside of widget bar
0.04: Fork `widminbat`->`widminbate`. Only use the system theme foreground
colour.
+0.05: Fix broken fork which removed the `update` function
\ No newline at end of file
diff --git a/apps/widminbate/metadata.json b/apps/widminbate/metadata.json
index dfa5a69fab..5fed8eef5f 100644
--- a/apps/widminbate/metadata.json
+++ b/apps/widminbate/metadata.json
@@ -1,7 +1,7 @@
{ "id": "widminbate",
"name": "Extra Minimal Battery",
"shortName":"ExtraMinBat",
- "version":"0.04",
+ "version":"0.05",
"description": "An extra minimal (only use system theme foreground colour) version of the battery widget that only appears if the battery is running low (below 30%)",
"icon": "widget.png",
"type": "widget",
diff --git a/apps/widminbate/widget.js b/apps/widminbate/widget.js
index 0bf4ceee35..4d4cbbe493 100644
--- a/apps/widminbate/widget.js
+++ b/apps/widminbate/widget.js
@@ -1,7 +1,7 @@
-(()=>{
- function getWidth() {
+{
+ let getWidth = function() {
return E.getBattery() <= 30 || Bangle.isCharging() ? 40 : 0;
- }
+ };
WIDGETS.minbate={area:"tr",width:getWidth(),draw:function() {
if(this.width < 40) return;
var s = 39;
@@ -12,6 +12,7 @@
clearRect(x,y,x+s,y+23).
setColor(g.theme.fg).fillRect(x,y+2,x+s-4,y+21).clearRect(x+2,y+4,x+s-6,y+19).fillRect(x+s-3,y+10,x+s,y+14).//border
fillRect(x+4,y+6,x+4+barWidth,y+17);//indicator bar
+ },update: function() {
var newWidth = getWidth();
if(newWidth != this.width) {
this.width = newWidth;
@@ -22,7 +23,7 @@
}};
setInterval(()=>{
var widget = WIDGETS.minbate;
- if(widget) {widget.update();}
+ if(widget) widget.update();
}, 10*60*1000);
Bangle.on('charging', () => WIDGETS.minbate.update());
-})();
+}
\ No newline at end of file
diff --git a/modules/Slider.js b/modules/Slider.js
new file mode 100644
index 0000000000..7fa2adba8f
--- /dev/null
+++ b/modules/Slider.js
@@ -0,0 +1,233 @@
+/* Copyright (c) 2023 Bangle.js contributors. See the file LICENSE for copying permission. */
+
+// At time of writing in October 2023 this module is new and things are more likely to change during the coming weeks than in a month or two.
+
+// See Slider.md for documentation
+
+/* Minify to 'Slider.min.js' by: // TODO: Should we do this for Slider module?
+
+ * checking out: https://github.com/espruino/EspruinoDocs
+ * run: ../EspruinoDocs/bin/minify.js modules/Slider.js modules/Slider.min.js
+
+*/
+
+exports.create = function(cb, conf) {
+
+ const R = Bangle.appRect;
+
+ // Empty function added to cb if it's undefined.
+ if (!cb) cb = ()=>{};
+
+ let o = {};
+ o.v = {}; // variables go here.
+ o.f = {}; // functions go here.
+
+ // Default configuration for the indicator, modified by parameter `conf`:
+ o.c = Object.assign({ // constants go here.
+ initLevel:0,
+ horizontal:false,
+ xStart:R.x2-R.w/4-4,
+ width:R.w/4,
+ yStart:R.y+4,
+ height:R.h-10,
+ steps:30,
+
+ dragableSlider:true,
+ dragRect:R,
+ mode:"incr",
+ oversizeR:0,
+ oversizeL:0,
+ propagateDrag:false,
+ timeout:1,
+
+ drawableSlider:true,
+ colorFG:g.theme.fg2,
+ colorBG:g.theme.bg2,
+ rounded:true,
+ outerBorderSize:Math.round(2*R.w/176), // 176 is the # of pixels in a row on the Bangle.js 2's screen and typically also its app rectangles, used here to rescale to whatever pixel count is on the current app rectangle.
+ innerBorderSize:Math.round(2*R.w/176),
+
+ autoProgress:false,
+ },conf);
+
+ // If borders are bigger than the configured width, make them smaller to avoid glitches.
+ while (o.c.width <= 2*(o.c.outerBorderSize+o.c.innerBorderSize)) {
+ o.c.outerBorderSize--;
+ o.c.innerBorderSize--;
+ }
+ o.c.outerBorderSize = Math.max(0,o.c.outerBorderSize);
+ o.c.innerBorderSize = Math.max(0,o.c.innerBorderSize);
+
+ let totalBorderSize = o.c.outerBorderSize + o.c.innerBorderSize;
+ o.c.rounded = o.c.rounded?o.c.width/2:0;
+ if (o.c.rounded) o.c._rounded = (o.c.width-2*totalBorderSize)/2;
+
+ o.c.STEP_SIZE = ((o.c.height-2*totalBorderSize)-(!o.c.rounded?0:(2*o.c._rounded)))/o.c.steps;
+
+ // If horizontal, flip things around.
+ if (o.c.horizontal) {
+ let mediator = o.c.xStart;
+ o.c.xStart = o.c.yStart;
+ o.c.yStart = mediator;
+ mediator = o.c.width;
+ o.c.width = o.c.height;
+ o.c.height = mediator;
+ delete mediator;
+ }
+
+ // Make room for the border. Underscore indicates the area for the actual indicator bar without borders.
+ o.c._xStart = o.c.xStart + totalBorderSize;
+ o.c._width = o.c.width - 2*totalBorderSize;
+ o.c._yStart = o.c.yStart + totalBorderSize;
+ o.c._height = o.c.height - 2*totalBorderSize;
+
+ // Add a rectangle object with x, y, x2, y2, w and h values.
+ o.c.r = {x:o.c.xStart, y:o.c.yStart, x2:o.c.xStart+o.c.width, y2:o.c.yStart+o.c.height, w:o.c.width, h:o.c.height};
+
+ // Initialize the level
+ o.v.level = o.c.initLevel;
+
+ // Only add interactivity if wanted.
+ if (o.c.dragableSlider) {
+
+ let useMap = (o.c.mode==="map"||o.c.mode==="mapincr")?true:false;
+ let useIncr = (o.c.mode==="incr"||o.c.mode==="mapincr")?true:false;
+
+ const Y_MAX = g.getHeight()-1; // TODO: Should this take users screen calibration into account?
+
+ o.v.ebLast = 0;
+ o.v.dy = 0;
+
+ o.f.wasOnDragRect = (exFirst, eyFirst)=>{
+ "ram";
+ return exFirst>o.c.dragRect.x && exFirsto.c.dragRect.y && eyFirst{
+ "ram";
+ if (!o.c.horizontal) return exFirst>o.c._xStart-o.c.oversizeL*o.c._width && exFirsto.c._yStart-o.c.oversizeL*o.c._height && exFirst{
+ "ram";
+ if (o.v.ebLast==0) {
+ exFirst = o.c.horizontal?e.y:e.x;
+ eyFirst = o.c.horizontal?e.x:e.y;
+ }
+
+ // Only react if on allowed area.
+ if (o.f.wasOnDragRect(exFirst, eyFirst)) {
+ o.v.dragActive = true;
+ if (!o.c.propagateDrag) E.stopEventPropagation&&E.stopEventPropagation();
+
+ if (o.v.timeoutID) {clearTimeout(o.v.timeoutID); o.v.timeoutID = undefined;}
+ if (e.b==0 && !o.v.timeoutID && (o.c.timeout || o.c.timeout===0)) o.v.timeoutID = setTimeout(o.f.remove, 1000*o.c.timeout);
+
+ if (useMap && o.f.wasOnIndicator(exFirst)) { // If draging starts on the indicator, adjust one-to-one.
+
+ let input = !o.c.horizontal?
+ Math.min((Y_MAX-e.y)-o.c.yStart-3*o.c.rounded/4, o.c.height):
+ Math.min(e.x-o.c.xStart-3*o.c.rounded/4, o.c.width);
+ input = Math.round(input/o.c.STEP_SIZE);
+
+ o.v.level = Math.min(Math.max(input,0),o.c.steps);
+
+ o.v.cbObj = {mode:"map", value:o.v.level};
+
+ } else if (useIncr) { // Heavily inspired by "updown" mode of setUI.
+
+ o.v.dy += o.c.horizontal?-e.dx:e.dy;
+ //if (!e.b) o.v.dy=0;
+
+ while (Math.abs(o.v.dy)>32) {
+ let incr;
+ if (o.v.dy>0) { o.v.dy-=32; incr = 1;}
+ else { o.v.dy+=32; incr = -1;}
+ Bangle.buzz(20);
+
+ o.v.level = Math.min(Math.max(o.v.level-incr,0),o.c.steps);
+
+ o.v.cbObj = {mode:"incr", value:incr};
+ }
+ }
+ if (o.v.cbObj && (o.v.level!==o.v.prevLevel||o.v.level===0||o.v.level===o.c.steps)) {
+ cb(o.v.cbObj.mode, o.v.cbObj.value);
+ o.f.draw&&o.f.draw(o.v.level);
+ }
+ o.v.cbObj = null;
+ o.v.prevLevel = o.v.level;
+ o.v.ebLast = e.b;
+ }
+ };
+
+ // Cleanup.
+ o.f.remove = ()=> {
+ Bangle.removeListener('drag', o.f.dragSlider);
+ o.v.dragActive = false;
+ o.v.timeoutID = undefined;
+ cb("remove", o.v.level);
+ };
+ }
+
+ // Add standard slider graphics only if wanted.
+ if (o.c.drawableSlider) {
+
+ // Function for getting the indication bars size.
+ o.f.updateBar = (levelHeight)=>{
+ "ram";
+ if (!o.c.horizontal) return {x:o.c._xStart,y:o.c._yStart+o.c._height-levelHeight,w:o.c._width,y2:o.c._yStart+o.c._height,r:o.c.rounded};
+ if (o.c.horizontal) return {x:o.c._xStart,y:o.c._yStart,w:levelHeight,h:o.c._height,r:o.c.rounded};
+ };
+
+ o.c.borderRect = {x:o.c._xStart-totalBorderSize,y:o.c._yStart-totalBorderSize,w:o.c._width+2*totalBorderSize,h:o.c._height+2*totalBorderSize,r:o.c.rounded};
+
+ o.c.hollowRect = {x:o.c._xStart-o.c.innerBorderSize,y:o.c._yStart-o.c.innerBorderSize,w:o.c._width+2*o.c.innerBorderSize,h:o.c._height+2*o.c.innerBorderSize,r:o.c.rounded};
+
+ // Standard slider drawing method.
+ o.f.draw = (level)=>{
+ "ram";
+
+ g.setColor(o.c.colorFG).fillRect(o.c.borderRect). // To get outer border...
+ setColor(o.c.colorBG).fillRect(o.c.hollowRect). // ... and here it's made hollow.
+ setColor(0==level?o.c.colorBG:o.c.colorFG).fillRect(o.f.updateBar((!o.c.rounded?0:(2*o.c._rounded))+level*o.c.STEP_SIZE)); // Here the bar is drawn.
+ if (o.c.rounded && level===0) { // Hollow circle indicates level zero when slider is rounded.
+ g.setColor(o.c.colorFG).fillCircle(o.c._xStart+o.c._rounded, o.c._yStart+o.c._height-o.c._rounded, o.c._rounded).
+ setColor(o.c.colorBG).fillCircle(o.c._xStart+o.c._rounded, o.c._yStart+o.c._height-o.c._rounded, o.c._rounded-o.c.outerBorderSize);
+ }
+ };
+ }
+
+ // Add logic for auto progressing the slider only if wanted.
+ if (o.c.autoProgress) {
+ o.f.autoUpdate = ()=>{
+ o.v.level = o.v.autoInitLevel + Math.round((Date.now()-o.v.autoInitTime)/1000);
+ if (o.v.level>o.c.steps) o.v.level=o.c.steps;
+ cb("auto", o.v.level);
+ o.f.draw&&o.f.draw(o.v.level);
+ if (o.v.level==o.c.steps) {o.f.stopAutoUpdate();}
+ };
+ o.f.initAutoValues = ()=>{
+ o.v.autoInitTime=Date.now();
+ o.v.autoInitLevel=o.v.level;
+ };
+ o.f.startAutoUpdate = (intervalSeconds)=>{
+ if (!intervalSeconds) intervalSeconds = 1;
+ o.f.stopAutoUpdate();
+ o.f.initAutoValues();
+ o.f.draw&&o.f.draw(o.v.level);
+ o.v.autoIntervalID = setInterval(o.f.autoUpdate,1000*intervalSeconds);
+ };
+ o.f.stopAutoUpdate = ()=>{
+ if (o.v.autoIntervalID) {
+ clearInterval(o.v.autoIntervalID);
+ o.v.autoIntervalID = undefined;
+ }
+ o.v.autoInitLevel = undefined;
+ o.v.autoInitTime = undefined;
+ };
+ }
+
+ return o;
+};
diff --git a/modules/Slider.md b/modules/Slider.md
new file mode 100644
index 0000000000..eb2291d25d
--- /dev/null
+++ b/modules/Slider.md
@@ -0,0 +1,106 @@
+Slider Library
+==============
+
+*At time of writing in October 2023 this module is new and things are more likely to change during the coming weeks than in a month or two.*
+
+> Take a look at README.md for hints on developing with this library.
+
+Usage
+-----
+
+```js
+var Slider = require("Slider");
+var slider = Slider(callbackFunction, configObject);
+
+Bangle.on("drag", slider.f.dragSlider);
+
+// If the slider should take precedent over other drag handlers use (fw2v18 and up):
+// Bangle.prependListener("drag", slider.f.dragSlider);
+```
+
+`callbackFunction` (`cb`) (first argument) determines what `slider` is used for. `slider` will pass two arguments, `mode` and `feedback` (`fb`), into `callbackFunction` (if `slider` is interactive or auto progressing). The different `mode`/`feedback` combinations to expect are:
+- `"map", o.v.level` | current level when interacting by mapping interface.
+- `"incr", incr` | where `incr` == +/-1, when interacting by incrementing interface.
+- `"remove", o.v.level` | last level when the slider times out.
+- `"auto", o.v.level` | when auto progressing.
+
+`configObject` (`conf`) (second argument, optional) has the following defaults:
+
+```js
+R = Bangle.appRect; // For use when determining defaults below.
+
+{
+initLevel: 0, // The level to initialize the slider with.
+horizontal: false, // Slider should be horizontal?
+xStart: R.x2-R.w/4-4, // Leftmost x-coordinate. (Uppermost y-coordinate if horizontal)
+width: R.w/4, // Width of the slider. (Height if horizontal)
+yStart: R.y+4, // Uppermost y-coordinate. (Rightmost x-coordinate if horizontal)
+height: R.h-10, // Height of the slider. (Width if horizontal)
+steps: 30, // Number of discrete steps of the slider.
+
+dragableSlider: true, // Should supply the sliders standard interaction mechanisms?
+dragRect: R, // Accept input within this rectangle.
+mode: "incr", // What mode of draging to use: "map", "incr" or "mapincr".
+oversizeR: 0, // Determines if the mapping area should be extend outside the indicator (Right/Up).
+oversizeL: 0, // Determines if the mapping area should be extend outside the indicator (Left/Down).
+propagateDrag: false, // Pass the drag event on down the handler chain?
+timeout: 1, // Seconds until the slider times out. If set to `false` the slider stays active. The callback function is responsible for repainting over the slider graphics.
+
+drawableSlider: true, // Should supply the sliders standard drawing mechanism?
+colorFG: g.theme.fg2, // Foreground color.
+colorBG: g.theme.bg2, // Background color.
+rounded: true, // Slider should have rounded corners?
+outerBorderSize: Math.round(2*R.w/176), // The size of the visual border. Scaled in relation to Bangle.js 2 screen width/typical app rectangle widths.
+innerBorderSize: Math.round(2*R.w/176), // The distance between visual border and the slider.
+
+autoProgress: false, // The slider should be able to progress automatically?
+}
+```
+
+A slider initiated in the Web IDE terminal window reveals its internals to a degree:
+```js
+slider = require("Slider").create(()=>{}, {autoProgress:true})
+={
+ v: { level: 0, ebLast: 0, dy: 0 },
+ f: {
+ wasOnDragRect: function (exFirst,eyFirst) { ... }, // Used internally.
+ wasOnIndicator: function (exFirst) { ... }, // Used internally.
+ dragSlider: function (e) { ... }, // The drag handler.
+ remove: function () { ... }, // Used to remove the drag handler and run the callback function.
+ updateBar: function (levelHeight) { ... }, // Used internally to get the variable height rectangle for the indicator.
+ draw: function (level) { ... }, // Draw the slider with the supplied level.
+ autoUpdate: function () { ... }, // Used to update the slider when auto progressing.
+ initAutoValues: function () { ... }, // Used internally.
+ startAutoUpdate: function (intervalSeconds) { ... }, // `intervalSeconds` defaults to 1 second if it's not supplied when `startAutoUpdate` is called.
+ stopAutoUpdate: function () { ... } // Stop auto progressing and clear some related values.
+ },
+ c: { initLevel: 0, horizontal: false, xStart: 127, width: 44,
+ yStart: 4, height: 166, steps: 30, dragableSlider: true,
+ dragRect: { x: 0, y: 0, w: 176, h: 176,
+ x2: 175, y2: 175 },
+ mode: "incr",
+ oversizeR: 0, oversizeL: 0, propagateDrag: false, timeout: 1, drawableSlider: true,
+ colorFG: 63488, colorBG: 8, rounded: 22, outerBorderSize: 2, innerBorderSize: 2,
+ autoProgress: true, _rounded: 18, STEP_SIZE: 4.06666666666, _xStart: 131, _width: 36,
+ _yStart: 8, _height: 158,
+ r: { x: 127, y: 4, x2: 171, y2: 170,
+ w: 44, h: 166 },
+ borderRect: { x: 127, y: 4, w: 44, h: 166,
+ r: 22 },
+ hollowRect: { x: 129, y: 6, w: 40, h: 162,
+ r: 22 }
+ }
+ }
+>
+```
+Tips
+----
+
+You can implement custom graphics for a slider in the `callbackFunction`. The slider test app mentioned in the links below do this. To draw on top of the included slider graphics you need to wrap the drawing code in a timeout somewhat like so: `setTimeout(drawingFunction,0,fb)` (see [`setTimeout` documentation](https://www.espruino.com/Reference#l__global_setTimeout)).
+
+Links
+-----
+
+There is a [slider test app on thyttan's personal app loader](https://thyttan.github.io/BangleApps/?q=slidertest) (at time of writing). Looking at [its code](https://github.com/thyttan/BangleApps/blob/ui-slider-lib/apps/slidertest/app.js) is a good way to see how the slider is used in app development.
+
+The version of [Remote for Spotify on thyttan's personal app loader](https://thyttan.github.io/BangleApps/?q=spotrem) (at time of writing) also utilizes the `Slider` module. Here is [the code](https://github.com/thyttan/BangleApps/blob/ui-slider-lib/apps/spotrem/app.js).