diff --git a/apps/astral/ChangeLog b/apps/astral/ChangeLog
index 747e5ac2e..d38ca0f6f 100644
--- a/apps/astral/ChangeLog
+++ b/apps/astral/ChangeLog
@@ -3,3 +3,4 @@
0.03: Update to use Bangle.setUI instead of setWatch
0.04: Tell clock widgets to hide.
0.05: Added adjustment for Bangle.js magnetometer heading fix
+0.06: optimized to update much faster
diff --git a/apps/astral/app.js b/apps/astral/app.js
index a435ca9e3..0bed41d1b 100644
--- a/apps/astral/app.js
+++ b/apps/astral/app.js
@@ -534,14 +534,7 @@ function mean_sidereal_time(lon) {
var mst = 280.46061837 + 360.98564736629 * jd
+ 0.000387933 * jt * jt - jt * jt * jt / 38710000 + lon;
- if (mst > 0.0) {
- while (mst > 360.0)
- mst = mst - 360.0;
- }
- else {
- while (mst < 0.0)
- mst = mst + 360.0;
- }
+ mst %=360;
return mst;
}
diff --git a/apps/astral/metadata.json b/apps/astral/metadata.json
index 647066a13..1d5a9c98f 100644
--- a/apps/astral/metadata.json
+++ b/apps/astral/metadata.json
@@ -1,7 +1,7 @@
{
"id": "astral",
"name": "Astral Clock",
- "version": "0.05",
+ "version": "0.06",
"description": "Clock that calculates and displays Alt Az positions of all planets, Sun as well as several other astronomy targets (customizable) and current Moon phase. Coordinates are calculated by GPS & time and onscreen compass assists orienting. See Readme before using.",
"icon": "app-icon.png",
"type": "clock",
diff --git a/apps/calendar/ChangeLog b/apps/calendar/ChangeLog
index 12776867f..6edb54f65 100644
--- a/apps/calendar/ChangeLog
+++ b/apps/calendar/ChangeLog
@@ -15,3 +15,5 @@
Display events for current month on touch
0.14: Add support for holidays
0.15: Edit holidays on device in settings
+0.16: Add menu to fast open settings to edit holidays
+ Display Widgets in menus
diff --git a/apps/calendar/README.md b/apps/calendar/README.md
index 5f90d0d52..7fa7bea1c 100644
--- a/apps/calendar/README.md
+++ b/apps/calendar/README.md
@@ -1,6 +1,6 @@
# Calendar
-Basic calendar
+Monthly calendar, displays holidays uploaded from the web interface and scheduled events.
## Usage
@@ -14,4 +14,4 @@ Basic calendar
## Settings
-- B2 Colors: use non-dithering colors (default, recommended for Bangle 2) or the original color scheme.
+B2 Colors: use non-dithering colors (default, recommended for Bangle 2) or the original color scheme.
diff --git a/apps/calendar/calendar.js b/apps/calendar/calendar.js
index 0ae852d83..7477775ca 100644
--- a/apps/calendar/calendar.js
+++ b/apps/calendar/calendar.js
@@ -1,3 +1,4 @@
+{
const maxX = g.getWidth();
const maxY = g.getHeight();
const fontSize = g.getWidth() > 200 ? 2 : 1;
@@ -17,71 +18,85 @@ const red = "#d41706";
const blue = "#0000ff";
const yellow = "#ffff00";
const cyan = "#00ffff";
-let bgColor = color4;
-let bgColorMonth = color1;
-let bgColorDow = color2;
-let bgColorWeekend = color3;
-let fgOtherMonth = gray1;
-let fgSameMonth = white;
-let bgEvent = blue;
-let bgOtherEvent = "#ff8800";
+let bgColor;
+let bgColorMonth;
+let bgColorDow;
+let bgColorWeekend;
+let fgOtherMonth;
+let fgSameMonth;
+let bgEvent;
+let bgOtherEvent;
const eventsPerDay=6; // how much different events per day we can display
const date = new Date();
const timeutils = require("time_utils");
-let settings = require('Storage').readJSON("calendar.json", true) || {};
let startOnSun = ((require("Storage").readJSON("setting.json", true) || {}).firstDayOfWeek || 0) === 0;
- // all alarms that run on a specific date
-const events = (require("Storage").readJSON("sched.json",1) || []).filter(a => a.on && a.date).map(a => {
- const date = new Date(a.date);
- const time = timeutils.decodeTime(a.t);
- date.setHours(time.h);
- date.setMinutes(time.m);
- date.setSeconds(time.s);
- return {date: date, msg: a.msg, type: "e"};
-});
-// add holidays & other events
-(require("Storage").readJSON("calendar.days.json",1) || []).forEach(d => {
- const date = new Date(d.date);
- const o = {date: date, msg: d.name, type: d.type};
- if (d.repeat) {
- o.repeat = d.repeat;
- }
- events.push(o);
-});
-
-if (settings.ndColors === undefined) {
- settings.ndColors = !g.theme.dark;
-}
-
-if (settings.ndColors === true) {
- bgColor = white;
- bgColorMonth = blue;
- bgColorDow = black;
- bgColorWeekend = yellow;
- fgOtherMonth = blue;
- fgSameMonth = black;
- bgEvent = color2;
- bgOtherEvent = cyan;
-}
-
-function getDowLbls(locale) {
- let days = startOnSun ? [0, 1, 2, 3, 4, 5, 6] : [1, 2, 3, 4, 5, 6, 0];
+let events;
+const dowLbls = function() {
+ const locale = require('locale').name;
+ const days = startOnSun ? [0, 1, 2, 3, 4, 5, 6] : [1, 2, 3, 4, 5, 6, 0];
const d = new Date();
return days.map(i => {
d.setDate(d.getDate() + (i + 7 - d.getDay()) % 7);
return require("locale").dow(d, 1);
});
-}
+}();
-function sameDay(d1, d2) {
+const loadEvents = () => {
+ // all alarms that run on a specific date
+ events = (require("Storage").readJSON("sched.json",1) || []).filter(a => a.on && a.date).map(a => {
+ const date = new Date(a.date);
+ const time = timeutils.decodeTime(a.t);
+ date.setHours(time.h);
+ date.setMinutes(time.m);
+ date.setSeconds(time.s);
+ return {date: date, msg: a.msg, type: "e"};
+ });
+ // add holidays & other events
+ (require("Storage").readJSON("calendar.days.json",1) || []).forEach(d => {
+ const date = new Date(d.date);
+ const o = {date: date, msg: d.name, type: d.type};
+ if (d.repeat) {
+ o.repeat = d.repeat;
+ }
+ events.push(o);
+ });
+};
+
+const loadSettings = () => {
+ let settings = require('Storage').readJSON("calendar.json", true) || {};
+ if (settings.ndColors === undefined) {
+ settings.ndColors = !g.theme.dark;
+ }
+ if (settings.ndColors === true) {
+ bgColor = white;
+ bgColorMonth = blue;
+ bgColorDow = black;
+ bgColorWeekend = yellow;
+ fgOtherMonth = blue;
+ fgSameMonth = black;
+ bgEvent = color2;
+ bgOtherEvent = cyan;
+ } else {
+ bgColor = color4;
+ bgColorMonth = color1;
+ bgColorDow = color2;
+ bgColorWeekend = color3;
+ fgOtherMonth = gray1;
+ fgSameMonth = white;
+ bgEvent = blue;
+ bgOtherEvent = "#ff8800";
+ }
+};
+
+const sameDay = function(d1, d2) {
"jit";
return d1.getFullYear() === d2.getFullYear() &&
d1.getMonth() === d2.getMonth() &&
d1.getDate() === d2.getDate();
-}
+};
-function drawEvent(ev, curDay, x1, y1, x2, y2) {
+const drawEvent = function(ev, curDay, x1, y1, x2, y2) {
"ram";
switch(ev.type) {
case "e": // alarm/event
@@ -99,9 +114,33 @@ function drawEvent(ev, curDay, x1, y1, x2, y2) {
g.setColor(bgOtherEvent).fillRect(x1+1, y1+1, x2-1, y2-1);
break;
}
-}
+};
-function drawCalendar(date) {
+const calcDays = (month, monthMaxDayMap, dowNorm) => {
+ "jit";
+ const maxDay = colN * (rowN - 1) + 1;
+ const days = [];
+ let nextMonthDay = 1;
+ let thisMonthDay = 51;
+ const month2 = month;
+ let prevMonthDay = monthMaxDayMap[month > 0 ? month - 1 : 11] - dowNorm + 1;
+
+ for (let i = 0; i < maxDay; i++) {
+ if (i < dowNorm) {
+ days.push(prevMonthDay);
+ prevMonthDay++;
+ } else if (thisMonthDay <= monthMaxDayMap[month] + 50) {
+ days.push(thisMonthDay);
+ thisMonthDay++;
+ } else {
+ days.push(nextMonthDay);
+ nextMonthDay++;
+ }
+ }
+ return days;
+};
+
+const drawCalendar = function(date) {
g.setBgColor(bgColor);
g.clearRect(0, 0, maxX, maxY);
g.setBgColor(bgColorMonth);
@@ -139,7 +178,6 @@ function drawCalendar(date) {
true
);
- let dowLbls = getDowLbls(require('locale').name);
dowLbls.forEach((lbl, i) => {
g.drawString(lbl, i * colW + colW / 2, headerH + rowH / 2);
});
@@ -163,23 +201,7 @@ function drawCalendar(date) {
11: 31
};
- let days = [];
- let nextMonthDay = 1;
- let thisMonthDay = 51;
- let prevMonthDay = monthMaxDayMap[month > 0 ? month - 1 : 11] - dowNorm + 1;
- for (let i = 0; i < colN * (rowN - 1) + 1; i++) {
- if (i < dowNorm) {
- days.push(prevMonthDay);
- prevMonthDay++;
- } else if (thisMonthDay <= monthMaxDayMap[month] + 50) {
- days.push(thisMonthDay);
- thisMonthDay++;
- } else {
- days.push(nextMonthDay);
- nextMonthDay++;
- }
- }
-
+ const days = calcDays(month, monthMaxDayMap, dowNorm);
const weekBeforeMonth = new Date(date.getTime());
weekBeforeMonth.setDate(weekBeforeMonth.getDate() - 7);
const week2AfterMonth = new Date(date.getFullYear(), date.getMonth()+1, 0);
@@ -189,8 +211,15 @@ function drawCalendar(date) {
ev.date.setFullYear(ev.date.getMonth() < 6 ? week2AfterMonth.getFullYear() : weekBeforeMonth.getFullYear());
}
});
- const eventsThisMonth = events.filter(ev => ev.date > weekBeforeMonth && ev.date < week2AfterMonth);
- eventsThisMonth.sort((a,b) => a.date - b.date);
+
+ const eventsThisMonthPerDay = events.filter(ev => ev.date > weekBeforeMonth && ev.date < week2AfterMonth).reduce((acc, ev) => {
+ const day = ev.date.getDate();
+ if (!acc[day]) {
+ acc[day] = [];
+ }
+ acc[day].push(ev);
+ return acc;
+ }, []);
let i = 0;
g.setFont("8x12", fontSize);
for (y = 0; y < rowN - 1; y++) {
@@ -205,13 +234,13 @@ function drawCalendar(date) {
const x2 = x * colW + colW;
const y2 = y * rowH + headerH + rowH + rowH;
- if (eventsThisMonth.length > 0) {
+ const eventsThisDay = eventsThisMonthPerDay[curDay.getDate()];
+ if (eventsThisDay && eventsThisDay.length > 0) {
// Display events for this day
- eventsThisMonth.forEach((ev, idx) => {
+ eventsThisDay.forEach((ev, idx) => {
if (sameDay(ev.date, curDay)) {
drawEvent(ev, curDay, x1, y1, x2, y2);
-
- eventsThisMonth.splice(idx, 1); // this event is no longer needed
+ eventsThisDay.splice(idx, 1); // this event is no longer needed
}
});
}
@@ -235,9 +264,44 @@ function drawCalendar(date) {
);
} // end for (x = 0; x < colN; x++)
} // end for (y = 0; y < rowN - 1; y++)
-} // end function drawCalendar
+}; // end function drawCalendar
+
+const showMenu = function() {
+ const menu = {
+ "" : {
+ title : "Calendar",
+ remove: () => {
+ require("widget_utils").show();
+ }
+ },
+ "< Back": () => {
+ require("widget_utils").hide();
+ E.showMenu();
+ setUI();
+ },
+ /*LANG*/"Exit": () => load(),
+ /*LANG*/"Settings": () => {
+ const appSettings = eval(require('Storage').read('calendar.settings.js'));
+ appSettings(() => {
+ loadSettings();
+ loadEvents();
+ showMenu();
+ });
+ },
+ };
+ if (require("Storage").read("alarm.app.js")) {
+ menu[/*LANG*/"Launch Alarms"] = () => {
+ load("alarm.app.js");
+ };
+ }
+ require("widget_utils").show();
+ E.showMenu(menu);
+};
+
+const setUI = function() {
+ require("widget_utils").hide(); // No space for widgets!
+ drawCalendar(date);
-function setUI() {
Bangle.setUI({
mode : "custom",
swipe: (dirLR, dirUD) => {
@@ -261,7 +325,14 @@ function setUI() {
drawCalendar(date);
}
},
- btn: (n) => n === (process.env.HWVERSION === 2 ? 1 : 3) && load(),
+ btn: (n) => {
+ if (process.env.HWVERSION === 2 || n === 2) {
+ showMenu();
+ } else if (n === 3) {
+ // directly exit only on Bangle.js 1
+ load();
+ }
+ },
touch: (n,e) => {
events.sort((a,b) => a.date - b.date);
const menu = events.filter(ev => ev.date.getFullYear() === date.getFullYear() && ev.date.getMonth() === date.getMonth()).map(e => {
@@ -274,16 +345,19 @@ function setUI() {
}
menu[""] = { title: require("locale").month(date) + " " + date.getFullYear() };
menu["< Back"] = () => {
+ require("widget_utils").hide();
E.showMenu();
- drawCalendar(date);
setUI();
};
+ require("widget_utils").show();
E.showMenu(menu);
}
});
-}
+};
+loadSettings();
+loadEvents();
+Bangle.loadWidgets();
require("Font8x12").add(Graphics);
-drawCalendar(date);
setUI();
-// No space for widgets!
+}
diff --git a/apps/calendar/metadata.json b/apps/calendar/metadata.json
index bd35c8879..e263efe35 100644
--- a/apps/calendar/metadata.json
+++ b/apps/calendar/metadata.json
@@ -1,8 +1,8 @@
{
"id": "calendar",
"name": "Calendar",
- "version": "0.15",
- "description": "Simple calendar",
+ "version": "0.16",
+ "description": "Monthly calendar, displays holidays uploaded from the web interface and scheduled events.",
"icon": "calendar.png",
"screenshots": [{"url":"screenshot_calendar.png"}],
"tags": "calendar,tool",
diff --git a/apps/calendar/settings.js b/apps/calendar/settings.js
index 40eca9f68..50beed8c0 100644
--- a/apps/calendar/settings.js
+++ b/apps/calendar/settings.js
@@ -1,14 +1,15 @@
(function (back) {
- var FILE = "calendar.json";
+ const FILE = "calendar.json";
const HOLIDAY_FILE = "calendar.days.json";
- var settings = require('Storage').readJSON(FILE, true) || {};
- if (settings.ndColors === undefined)
+ const settings = require('Storage').readJSON(FILE, true) || {};
+ 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 ae955aded..fd16aa878 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/multiclock/ChangeLog b/apps/multiclock/ChangeLog
index 442a5277a..50b9049ac 100644
--- a/apps/multiclock/ChangeLog
+++ b/apps/multiclock/ChangeLog
@@ -7,6 +7,5 @@
0.07: compatible with Bang;e.js 2
0.08: fix minute tick bug
0.09: use setUI clockupdown for controls + fix small display bug in nifty face
-
-
+0.10: stop widget field from flashing when moving to the dk clock face.
diff --git a/apps/multiclock/dk.face.js b/apps/multiclock/dk.face.js
index a89397a75..3a512f8e5 100644
--- a/apps/multiclock/dk.face.js
+++ b/apps/multiclock/dk.face.js
@@ -27,7 +27,6 @@
d[0] = locale.dow(now,3);
var dt=d[0]+" "+d[1]+" "+d[2];//+" "+d[3];
g.drawString(dt,W/2,H/2+24);
- g.flip();
}
diff --git a/apps/multiclock/metadata.json b/apps/multiclock/metadata.json
index 197e6631c..cdc84d78f 100644
--- a/apps/multiclock/metadata.json
+++ b/apps/multiclock/metadata.json
@@ -1,7 +1,7 @@
{
"id": "multiclock",
"name": "Multi Clock",
- "version": "0.09",
+ "version": "0.10",
"description": "Clock with multiple faces. Switch between faces with BTN1 & BTN3 (Bangle 2 touch top-right, bottom right). For best display set theme Background 2 to cyan or some other bright colour in settings.",
"screenshots": [{"url":"screen-ana.png"},{"url":"screen-big.png"},{"url":"screen-td.png"},{"url":"screen-nifty.png"},{"url":"screen-word.png"},{"url":"screen-sec.png"}],
"icon": "multiclock.png",
diff --git a/apps/setting/ChangeLog b/apps/setting/ChangeLog
index 4f4196e32..1fdeada0d 100644
--- a/apps/setting/ChangeLog
+++ b/apps/setting/ChangeLog
@@ -71,4 +71,6 @@ of 'Select Clock'
0.62: Fix whitelist showing as 'on' by default when it's not after 0.59
0.63: Whitelist: Try to resolve peer addresses using NRF.resolveAddress() - for 2v19 or 2v18 cutting edge builds
Remove 'beta' label from passkey - it's been around for a while and works ok
-0.64: Default to wakeOnTwist being off
\ No newline at end of file
+0.64: Default to wakeOnTwist being off
+0.65: Prepend 'LCD->Calibration' touch listener and stop event propagation.
+
diff --git a/apps/setting/metadata.json b/apps/setting/metadata.json
index 07376468a..4b5a02135 100644
--- a/apps/setting/metadata.json
+++ b/apps/setting/metadata.json
@@ -1,7 +1,7 @@
{
"id": "setting",
"name": "Settings",
- "version": "0.64",
+ "version": "0.65",
"description": "A menu for setting up Bangle.js",
"icon": "settings.png",
"tags": "tool,system",
diff --git a/apps/setting/settings.js b/apps/setting/settings.js
index d19bb14d7..2a928a7a0 100644
--- a/apps/setting/settings.js
+++ b/apps/setting/settings.js
@@ -899,6 +899,7 @@ function showTouchscreenCalibration() {
}
function touchHandler(_,e) {
+ E.stopEventPropagation&&E.stopEventPropagation();
var spot = corners[currentCorner];
// store averages
if (spot[0]*2 < g.getWidth())
@@ -921,7 +922,7 @@ function showTouchscreenCalibration() {
}
showTapSpot();
}
- Bangle.on('touch', touchHandler);
+ Bangle.prependListener&&Bangle.prependListener('touch',touchHandler)||Bangle.on('touch',touchHandler);
showTapSpot();
}
diff --git a/apps/widminbate/ChangeLog b/apps/widminbate/ChangeLog
index c1952a33d..56c73beca 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 dfa5a69fa..5fed8eef5 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 0bf4ceee3..4d4cbbe49 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 000000000..7fa2adba8
--- /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 000000000..eb2291d25
--- /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).