1
0
Fork 0

Merge branch 'espruino:master' into sleeplogalarm

master
storm64 2022-12-05 02:56:31 +01:00 committed by GitHub
commit ada3ea74c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
96 changed files with 1385 additions and 829 deletions

View File

@ -11,3 +11,4 @@
0.11: Bangle.js2: New pixels, btn1 to exit
0.12: Actual pixels as of 29th Nov 2021
0.13: Bangle.js 2: Use setUI to add software back button
0.14: Add automatic translation of more strings

View File

@ -11,8 +11,8 @@ g.drawString("BANGLEJS.COM",120,y-4);
} else {
y=-(4+h); // small screen, start right at top
}
g.drawString("Powered by Espruino",0,y+=4+h);
g.drawString("Version "+ENV.VERSION,0,y+=h);
g.drawString(/*LANG*/"Powered by Espruino",0,y+=4+h);
g.drawString(/*LANG*/"Version "+ENV.VERSION,0,y+=h);
g.drawString("Commit "+ENV.GIT_COMMIT,0,y+=h);
function getVersion(name,file) {
var j = s.readJSON(file,1);
@ -24,9 +24,9 @@ getVersion("Launcher","launch.info");
getVersion("Settings","setting.info");
y+=h;
g.drawString(MEM.total+" JS Variables available",0,y+=h);
g.drawString("Storage: "+(require("Storage").getFree()>>10)+"k free",0,y+=h);
if (ENV.STORAGE) g.drawString(" "+(ENV.STORAGE>>10)+"k total",0,y+=h);
g.drawString(MEM.total+/*LANG*/" JS Variables available",0,y+=h);
g.drawString("Storage: "+(require("Storage").getFree()>>10)+/*LANG*/"k free",0,y+=h);
if (ENV.STORAGE) g.drawString(" "+(ENV.STORAGE>>10)+/*LANG*/"k total",0,y+=h);
if (ENV.SPIFLASH) g.drawString("SPI Flash: "+(ENV.SPIFLASH>>10)+"k",0,y+=h);
g.setFontAlign(0,-1);
g.flip();

View File

@ -35,17 +35,17 @@ function drawInfo() {
g.setFont("4x6").setFontAlign(0,0).drawString("BANGLEJS.COM",W-30,56);
var h=8, y = 24-h;
g.setFont("6x8").setFontAlign(-1,-1);
g.drawString("Powered by Espruino",0,y+=4+h);
g.drawString("Version "+ENV.VERSION,0,y+=h);
g.drawString(/*LANG*/"Powered by Espruino",0,y+=4+h);
g.drawString(/*LANG*/"Version "+ENV.VERSION,0,y+=h);
g.drawString("Commit "+ENV.GIT_COMMIT,0,y+=h);
getVersion("Bootloader","boot.info");
getVersion("Launcher","launch.info");
getVersion("Settings","setting.info");
g.drawString(MEM.total+" JS Vars",0,y+=h);
g.drawString("Storage: "+(require("Storage").getFree()>>10)+"k free",0,y+=h);
if (ENV.STORAGE) g.drawString(" "+(ENV.STORAGE>>10)+"k total",0,y+=h);
g.drawString(MEM.total+/*LANG*/" JS Vars",0,y+=h);
g.drawString("Storage: "+(require("Storage").getFree()>>10)+/*LANG*/"k free",0,y+=h);
if (ENV.STORAGE) g.drawString(" "+(ENV.STORAGE>>10)+/*LANG*/"k total",0,y+=h);
if (ENV.SPIFLASH) g.drawString("SPI Flash: "+(ENV.SPIFLASH>>10)+"k",0,y+=h);
imageTop = y+h;
imgScroll = imgHeight-imageTop;

View File

@ -1,7 +1,7 @@
{
"id": "about",
"name": "About",
"version": "0.13",
"version": "0.14",
"description": "Bangle.js About page - showing software version, stats, and a collaborative mural from the Bangle.js KickStarter backers",
"icon": "app.png",
"tags": "tool,system",

View File

@ -1,3 +1,4 @@
0.01: New app!
0.02: Design improvements and fixes.
0.03: Indicate battery level through line occurrence.
0.03: Indicate battery level through line occurrence.
0.04: Use widget_utils module.

View File

@ -215,8 +215,7 @@ Bangle.loadWidgets();
* so we will blank out the draw() functions of each widget and change the
* area to the top bar doesn't get cleared.
*/
for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
require('widget_utils').hide();
// Clear the screen once, at startup and draw clock
g.setTheme({bg:"#fff",fg:"#000",dark:false}).clear();
draw();

View File

@ -3,7 +3,7 @@
"name": "AI Clock",
"shortName":"AI Clock",
"icon": "aiclock.png",
"version":"0.03",
"version":"0.04",
"readme": "README.md",
"supports": ["BANGLEJS2"],
"description": "A watch face that was designed by an AI (stable diffusion) and implemented by a human.",

View File

@ -34,4 +34,6 @@
0.32: Fix wrong hidden filter
Add option for auto-delete a timer after it expires
0.33: Allow hiding timers&alarms
0.34: Add "Confirm" option to alarm/timer edit menus
0.35: Add automatic translation of more strings

View File

@ -128,7 +128,12 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex) {
value: alarm.hidden || false,
onchange: v => alarm.hidden = v
},
/*LANG*/"Cancel": () => showMainMenu()
/*LANG*/"Cancel": () => showMainMenu(),
/*LANG*/"Confirm": () => {
prepareAlarmForSave(alarm, alarmIndex, time);
saveAndReload();
showMainMenu();
}
};
if (!isNew) {
@ -178,7 +183,7 @@ function decodeDOW(alarm) {
.map((day, index) => alarm.dow & (1 << (index + firstDayOfWeek)) ? day : "_")
.join("")
.toLowerCase()
: "Once"
: /*LANG*/"Once"
}
function showEditRepeatMenu(repeat, dow, dowChangeCallback) {
@ -293,7 +298,12 @@ function showEditTimerMenu(selectedTimer, timerIndex) {
onchange: v => timer.hidden = v
},
/*LANG*/"Vibrate": require("buzz_menu").pattern(timer.vibrate, v => timer.vibrate = v),
/*LANG*/"Cancel": () => showMainMenu()
/*LANG*/"Cancel": () => showMainMenu(),
/*LANG*/"Confirm": () => {
prepareTimerForSave(timer, timerIndex, time);
saveAndReload();
showMainMenu();
}
};
if (!isNew) {

View File

@ -2,7 +2,7 @@
"id": "alarm",
"name": "Alarms & Timers",
"shortName": "Alarms",
"version": "0.33",
"version": "0.35",
"description": "Set alarms and timers on your Bangle",
"icon": "app.png",
"tags": "tool,alarm,widget",

View File

@ -12,4 +12,5 @@
0.12: Add settings to hide date,widgets
0.13: Add font setting
0.14: Use ClockFace_menu.addItems
0.15: Add Power saving option
0.15: Add Power saving option
0.16: Support Fast Loading

View File

@ -1,124 +1,128 @@
/* jshint esversion: 6 */
/**
* A simple digital clock showing seconds as a bar
**/
{
/**
* A simple digital clock showing seconds as a bar
**/
// Check settings for what type our clock should be
let locale = require("locale");
{ // add some more info to locale
let date = new Date();
date.setFullYear(1111);
date.setMonth(1, 3); // februari: months are zero-indexed
const localized = locale.date(date, true);
locale.dayFirst = /3.*2/.test(localized);
locale.hasMeridian = (locale.meridian(date)!=="");
}
let barW = 0, prevX = 0;
function renderBar(l) {
"ram";
if (l) prevX = 0; // called from Layout: drawing area was cleared
else l = clock.layout.bar;
let x2 = l.x+barW;
if (clock.powerSave && Bangle.isLocked()) x2 = 0; // hide bar
if (x2===prevX) return; // nothing to do
if (x2===0) x2--; // don't leave 1px line
if (x2<Math.max(0, prevX)) g.setBgColor(l.bgCol || g.theme.bg).clearRect(x2+1, l.y, prevX, l.y2);
else g.setColor(l.col || g.theme.fg).fillRect(prevX+1, l.y, x2, l.y2);
prevX = x2;
}
function timeText(date) {
if (!clock.is12Hour) {
return locale.time(date, true);
let locale = require("locale");
{ // add some more info to locale
let date = new Date();
date.setFullYear(1111);
date.setMonth(1, 3); // februari: months are zero-indexed
const localized = locale.date(date, true);
locale.dayFirst = /3.*2/.test(localized);
locale.hasMeridian = (locale.meridian(date)!=="");
}
const date12 = new Date(date.getTime());
const hours = date12.getHours();
if (hours===0) {
date12.setHours(12);
} else if (hours>12) {
date12.setHours(hours-12);
}
return locale.time(date12, true);
}
function ampmText(date) {
return (clock.is12Hour && locale.hasMeridian) ? locale.meridian(date) : "";
}
function dateText(date) {
const dayName = locale.dow(date, true),
month = locale.month(date, true),
day = date.getDate();
const dayMonth = locale.dayFirst ? `${day} ${month}` : `${month} ${day}`;
return `${dayName} ${dayMonth}`;
}
const ClockFace = require("ClockFace"),
clock = new ClockFace({
precision: 1,
settingsFile: "barclock.settings.json",
init: function() {
const Layout = require("Layout");
this.layout = new Layout({
type: "v", c: [
{
type: "h", c: [
{id: "time", label: "88:88", type: "txt", font: "6x8:5", col: g.theme.fg, bgCol: g.theme.bg}, // updated below
{id: "ampm", label: " ", type: "txt", font: "6x8:2", col: g.theme.fg, bgCol: g.theme.bg},
],
},
{id: "bar", type: "custom", fillx: 1, height: 6, col: g.theme.fg2, render: renderBar},
this.showDate ? {height: 40} : {},
this.showDate ? {id: "date", type: "txt", font: "10%", valign: 1} : {},
],
}, {lazy: true});
// adjustments based on screen size and whether we display am/pm
let thickness; // bar thickness, same as time font "pixel block" size
if (this.is12Hour && locale.hasMeridian) {
// Maximum font size = (<screen width> - <ampm: 2chars * (2*6)px>) / (5chars * 6px)
thickness = Math.floor((Bangle.appRect.w-24)/(5*6));
} else {
this.layout.ampm.label = "";
thickness = Math.floor(Bangle.appRect.w/(5*6));
}
let bar = this.layout.bar;
bar.height = thickness+1;
if (this.font===1) { // vector
const B2 = process.env.HWVERSION>1;
let barW = 0, prevX = 0;
const renderBar = function (l) {
"ram";
if (l) prevX = 0; // called from Layout: drawing area was cleared
else l = clock.layout.bar;
let x2 = l.x+barW;
if (clock.powerSave && Bangle.isLocked()) x2 = 0; // hide bar
if (x2===prevX) return; // nothing to do
if (x2===0) x2--; // don't leave 1px line
if (x2<Math.max(0, prevX)) g.setBgColor(l.bgCol || g.theme.bg).clearRect(x2+1, l.y, prevX, l.y2);
else g.setColor(l.col || g.theme.fg).fillRect(prevX+1, l.y, x2, l.y2);
prevX = x2;
}
const timeText = function(date) {
if (!clock.is12Hour) {
return locale.time(date, true);
}
const date12 = new Date(date.getTime());
const hours = date12.getHours();
if (hours===0) {
date12.setHours(12);
} else if (hours>12) {
date12.setHours(hours-12);
}
return locale.time(date12, true);
}
const ampmText = date => (clock.is12Hour && locale.hasMeridian) ? locale.meridian(date) : "";
const dateText = date => {
const dayName = locale.dow(date, true),
month = locale.month(date, true),
day = date.getDate();
const dayMonth = locale.dayFirst ? `${day} ${month}` : `${month} ${day}`;
return `${dayName} ${dayMonth}`;
};
const ClockFace = require("ClockFace"),
clock = new ClockFace({
precision: 1,
settingsFile: "barclock.settings.json",
init: function() {
const Layout = require("Layout");
this.layout = new Layout({
type: "v", c: [
{
type: "h", c: [
{id: "time", label: "88:88", type: "txt", font: "6x8:5", col: g.theme.fg, bgCol: g.theme.bg}, // updated below
{id: "ampm", label: " ", type: "txt", font: "6x8:2", col: g.theme.fg, bgCol: g.theme.bg},
],
},
{id: "bar", type: "custom", fillx: 1, height: 6, col: g.theme.fg2, render: renderBar},
this.showDate ? {height: 40} : {},
this.showDate ? {id: "date", type: "txt", font: "10%", valign: 1} : {},
],
}, {lazy: true});
// adjustments based on screen size and whether we display am/pm
let thickness; // bar thickness, same as time font "pixel block" size
if (this.is12Hour && locale.hasMeridian) {
this.layout.time.font = "Vector:"+(B2 ? 50 : 60);
this.layout.ampm.font = "Vector:"+(B2 ? 20 : 40);
// Maximum font size = (<screen width> - <ampm: 2chars * (2*6)px>) / (5chars * 6px)
thickness = Math.floor((Bangle.appRect.w-24)/(5*6));
} else {
this.layout.time.font = "Vector:"+(B2 ? 60 : 80);
this.layout.ampm.label = "";
thickness = Math.floor(Bangle.appRect.w/(5*6));
}
} else {
this.layout.time.font = "6x8:"+thickness;
}
this.layout.update();
bar.y2 = bar.y+bar.height-1;
},
update: function(date, c) {
"ram";
if (c.m) this.layout.time.label = timeText(date);
if (c.h) this.layout.ampm.label = ampmText(date);
if (c.d && this.showDate) this.layout.date.label = dateText(date);
if (c.m) this.layout.render();
if (c.s) {
barW = Math.round(date.getSeconds()/60*this.layout.bar.w);
renderBar();
}
},
resume: function() {
prevX = 0; // force redraw of bar
this.layout.forgetLazyState();
},
});
let bar = this.layout.bar;
bar.height = thickness+1;
if (this.font===1) { // vector
const B2 = process.env.HWVERSION>1;
if (this.is12Hour && locale.hasMeridian) {
this.layout.time.font = "Vector:"+(B2 ? 50 : 60);
this.layout.ampm.font = "Vector:"+(B2 ? 20 : 40);
} else {
this.layout.time.font = "Vector:"+(B2 ? 60 : 80);
}
} else {
this.layout.time.font = "6x8:"+thickness;
}
this.layout.update();
bar.y2 = bar.y+bar.height-1;
},
update: function(date, c) {
"ram";
if (c.m) this.layout.time.label = timeText(date);
if (c.h) this.layout.ampm.label = ampmText(date);
if (c.d && this.showDate) this.layout.date.label = dateText(date);
if (c.m) this.layout.render();
if (c.s) {
barW = Math.round(date.getSeconds()/60*this.layout.bar.w);
renderBar();
}
},
resume: function() {
prevX = 0; // force redraw of bar
this.layout.forgetLazyState();
},
remove: function() {
if (this.onLock) Bangle.removeListener("lock", this.onLock);
},
});
// power saving: only update once a minute while locked, hide bar
if (clock.powerSave) {
Bangle.on("lock", lock => {
clock.precision = lock ? 60 : 1;
clock.tick();
renderBar(); // hide/redraw bar right away
});
}
// power saving: only update once a minute while locked, hide bar
if (clock.powerSave) {
clock.onLock = lock => {
clock.precision = lock ? 60 : 1;
clock.tick();
renderBar(); // hide/redraw bar right away
}
Bangle.on("lock", clock.onLock);
}
clock.start();
clock.start();
}

View File

@ -1,7 +1,7 @@
{
"id": "barclock",
"name": "Bar Clock",
"version": "0.15",
"version": "0.16",
"description": "A simple digital clock showing seconds as a bar",
"icon": "clock-bar.png",
"screenshots": [{"url":"screenshot.png"},{"url":"screenshot_pm.png"}],

View File

@ -2,3 +2,4 @@
0.02: More compact rendering & app icon
0.03: Tell clock widgets to hide.
0.04: Improve current time readability in light theme.
0.05: Show calendar colors & improved all day events.

View File

@ -20,41 +20,55 @@ function zp(str) {
}
function drawEventHeader(event, y) {
g.setFont("Vector", 24);
var x = 0;
var time = isActive(event) ? new Date() : new Date(event.timestamp * 1000);
var timeStr = zp(time.getHours()) + ":" + zp(time.getMinutes());
g.drawString(timeStr, 5, y);
y += 24;
//Don't need to know what time the event is at if its all day
if (isActive(event) || !event.allDay) {
g.setFont("Vector", 24);
var timeStr = zp(time.getHours()) + ":" + zp(time.getMinutes());
g.drawString(timeStr, 0, y);
y += 3;
x = 13*timeStr.length+5;
}
g.setFont("12x20", 1);
if (isActive(event)) {
g.drawString(zp(time.getDate())+". " + require("locale").month(time,1),15*timeStr.length,y-21);
g.drawString(zp(time.getDate())+". " + require("locale").month(time,1),x,y);
} else {
var offset = 0-time.getTimezoneOffset()/1440;
var days = Math.floor((time.getTime()/1000)/86400+offset)-Math.floor(getTime()/86400+offset);
if(days > 0) {
if(days > 0 || event.allDay) {
var daysStr = days===1?/*LANG*/"tomorrow":/*LANG*/"in "+days+/*LANG*/" days";
g.drawString(daysStr,15*timeStr.length,y-21);
g.drawString(daysStr,x,y);
}
}
y += 21;
return y;
}
function drawEventBody(event, y) {
g.setFont("12x20", 1);
var lines = g.wrapString(event.title, g.getWidth()-10);
var lines = g.wrapString(event.title, g.getWidth()-15);
var yStart = y;
if (lines.length > 2) {
lines = lines.slice(0,2);
lines[1] = lines[1].slice(0,-3)+"...";
}
g.drawString(lines.join('\n'), 5, y);
g.drawString(lines.join('\n'),10,y);
y+=20 * lines.length;
if(event.location) {
g.drawImage(atob("DBSBAA8D/H/nDuB+B+B+B3Dn/j/B+A8A8AYAYAYAAAAAAA=="),5,y);
g.drawString(event.location, 20, y);
g.drawImage(atob("DBSBAA8D/H/nDuB+B+B+B3Dn/j/B+A8A8AYAYAYAAAAAAA=="),10,y);
g.drawString(event.location,25,y);
y+=20;
}
if (event.color) {
var oldColor = g.getColor();
g.setColor("#"+(0x1000000+Number(event.color)).toString(16).padStart(6,"0"));
g.fillRect(0,yStart,5,y-3);
g.setColor(oldColor);
}
y+=5;
return y;
}
@ -68,19 +82,19 @@ function drawEvent(event, y) {
var curEventHeight = 0;
function drawCurrentEvents(y) {
g.setColor(g.theme.dark ? "#0ff" : "#0000ff");
g.clearRect(5, y, g.getWidth() - 5, y + curEventHeight);
g.setColor(g.theme.dark ? "#0ff" : "#00f");
g.clearRect(0,y,g.getWidth()-5,y+curEventHeight);
curEventHeight = y;
if(current.length === 0) {
y = drawEvent({timestamp: getTime(), durationInSeconds: 100}, y);
} else {
y = drawEventHeader(current[0], y);
y = drawEventHeader(current[0],y);
for (var e of current) {
y = drawEventBody(e, y);
y = drawEventBody(e,y);
}
}
curEventHeight = y - curEventHeight;
curEventHeight = y-curEventHeight;
return y;
}
@ -94,7 +108,7 @@ function drawFutureEvents(y) {
}
function fullRedraw() {
g.clearRect(5,24,g.getWidth()-5,g.getHeight());
g.clearRect(0,24,g.getWidth()-5,g.getHeight());
updateCalendar();
var y = 30;
y = drawCurrentEvents(y);
@ -117,3 +131,4 @@ var minuteInterval = setInterval(redraw, 60 * 1000);
Bangle.setUI("clock");
Bangle.loadWidgets();
Bangle.drawWidgets();

View File

@ -2,7 +2,7 @@
"id": "calclock",
"name": "Calendar Clock",
"shortName": "CalClock",
"version": "0.04",
"version": "0.05",
"description": "Show the current and upcoming events synchronized from Gadgetbridge",
"icon": "calclock.png",
"type": "clock",

View File

@ -0,0 +1,32 @@
diff --git a/apps/calclock/calclock.js b/apps/calclock/calclock.js
index cb8c6100e..2092c1a4e 100644
--- a/apps/calclock/calclock.js
+++ b/apps/calclock/calclock.js
@@ -3,9 +3,24 @@ var current = [];
var next = [];
function updateCalendar() {
- calendar = require("Storage").readJSON("android.calendar.json",true)||[];
- calendar = calendar.filter(e => isActive(e) || getTime() <= e.timestamp);
- calendar.sort((a,b) => a.timestamp - b.timestamp);
+ calendar = [
+ {
+ t: "calendar",
+ id: 2, type: 0, timestamp: getTime(), durationInSeconds: 200,
+ title: "Capture Screenshot",
+ description: "Capture Screenshot",
+ location: "",
+ calName: "",
+ color: -7151168, allDay: true },
+ {
+ t: "calendar",
+ id: 7186, type: 0, timestamp: getTime() + 2000, durationInSeconds: 100,
+ title: "Upload to BangleApps",
+ description: "",
+ location: "",
+ calName: "",
+ color: -509406, allDay: false }
+ ];
current = calendar.filter(isActive);
next = calendar.filter(e=>!isActive(e));

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -31,3 +31,4 @@
0.16: Fix const error
Use widget_utils if available
0.17: Load circles from clkinfo
0.18: Improved clkinfo handling and using it for the weather circle

View File

@ -106,6 +106,10 @@ let circleItemNum = [
2, // circle3
3, // circle4
];
let weatherCircleNum = 0;
let weatherCircleDataNum = 0;
let weatherCircleCondNum = 0;
let weatherCircleTempNum = 0;
function hideWidgets() {
/*
@ -323,6 +327,55 @@ function getImage(graphic, color) {
}
function drawWeather(w) {
if (!w) w = getCircleXPosition("weather");
let weatherInfo = menu[weatherCircleNum];
let weatherCond = weatherCircleCondNum >= 0? weatherInfo.items[weatherCircleCondNum]: undefined;
let weatherData = weatherCircleDataNum >= 0? weatherInfo.items[weatherCircleDataNum]: undefined;
let weatherTemp = weatherCircleTempNum >= 0? weatherInfo.items[weatherCircleTempNum]: undefined;
let color = getCircleColor("weather");
let percent = 0;
let data = settings.weatherCircleData;
let tempString = "?", icon = undefined;
let scale = 16/24; //our icons are 16x16 while clkinfo's are 24x24
if(weatherCond) {
weatherCond.show()
weatherCond.hide()
let data = weatherCond.get()
if(settings.legacyWeatherIcons) { //may disappear in future
icon = getWeatherIconByCode(data.v);
scale = 1;
} else
icon = data.img;
}
if(weatherTemp) {
weatherTemp.show()
weatherTemp.hide()
tempString = weatherTemp.get().text;
}
drawCircleBackground(w);
if(weatherData) {
weatherData.show();
weatherData.hide();
let data = weatherData.get();
if(weatherData.hasRange) percent = (data.v-data.min) / (data.max-data.min);
drawGauge(w, h3, percent, color);
}
drawInnerCircleAndTriangle(w);
writeCircleText(w, tempString);
if(icon) {
g.setColor(getCircleIconColor("weather", color, percent))
.drawImage(icon, w - iconOffset, h3 + radiusOuter - iconOffset, {scale: scale});
} else {
g.drawString("?", w, h3 + radiusOuter);
}
}
function drawWeatherOld(w) {
if (!w) w = getCircleXPosition("weather");
let weather = getWeather();
let tempString = weather ? locale.temp(weather.temp - 273.15) : undefined;
@ -659,12 +712,16 @@ function reloadMenu() {
let parts = settings['circle'+i].split("/");
let infoName = parts[0], itemName = parts[1];
let infoNum = menu.findIndex(e=>e.name==infoName);
let itemNum = 0;
//suppose unnamed are varying (like timers or events), pick the first
if(itemName)
let itemNum = 0; //get first if dynamic
if(!menu[infoNum].dynamic)
itemNum = menu[infoNum].items.findIndex(it=>it.name==itemName);
circleInfoNum[i-1] = infoNum;
circleItemNum[i-1] = itemNum;
} else if(settings['circle'+i] == "weather") {
weatherCircleNum = menu.findIndex(e=>e.name.toLowerCase() == "weather");
weatherCircleDataNum = menu[weatherCircleNum].items.findIndex(it=>it.name==settings.weatherCircleData);
weatherCircleCondNum = menu[weatherCircleNum].items.findIndex(it=>it.name=="condition");
weatherCircleTempNum = menu[weatherCircleNum].items.findIndex(it=>it.name=="temperature");
}
}
//reload periodically for changes?
@ -685,22 +742,23 @@ function drawClkInfo(index, w) {
if (!w) w = getCircleXPosition(type);
drawCircleBackground(w);
const color = getCircleColor(type);
if(!info || !info.items.length) {
var item = info.items[circleItemNum[index-1]];
if(!info || !item) {
drawEmpty(info? info.img : null, w, color);
return;
}
var item = info.items[circleItemNum[index-1]];
//TODO do hide()+get() here
item.show();
item.hide();
item=item.get();
var img = item.img;
var data=item.get();
var img = data.img;
var percent = 1; //fill up if no range
var txt = data.text;
if(!img) img = info.img;
let percent = (item.v-item.min) / item.max;
if(isNaN(percent)) percent = 1; //fill it up
if(item.hasRange) percent = (data.v-data.min) / (data.max-data.min);
if(data.short) txt = data.short;
drawGauge(w, h3, percent, color);
drawInnerCircleAndTriangle(w);
writeCircleText(w, item.text);
writeCircleText(w, txt);
g.setColor(getCircleIconColor(type, color, percent))
.drawImage(img, w - iconOffset, h3 + radiusOuter - iconOffset, {scale: 16/24});
}

View File

@ -1,7 +1,7 @@
{ "id": "circlesclock",
"name": "Circles clock",
"shortName":"Circles clock",
"version":"0.17",
"version":"0.18",
"description": "A clock with three or four circles for different data at the bottom in a probably familiar style",
"icon": "app.png",
"screenshots": [{"url":"screenshot-dark.png"}, {"url":"screenshot-light.png"}, {"url":"screenshot-dark-4.png"}, {"url":"screenshot-light-4.png"}],

View File

@ -17,10 +17,7 @@
var valuesCircleTypes = ["empty","weather", "sunprogress"];
var namesCircleTypes = ["empty","weather", "sun"];
clock_info.load().forEach(e=>{
//TODO filter for hasRange and other
if(!e.items.length || !e.items[0].name) {
//suppose unnamed are varying (like timers or events), pick the first
item = e.items[0];
if(e.dynamic) {
valuesCircleTypes = valuesCircleTypes.concat([e.name+"/"]);
namesCircleTypes = namesCircleTypes.concat([e.name]);
} else {
@ -85,6 +82,12 @@
},
onchange: x => save('updateInterval', x),
},
//TODO deprecated local icons, may disappear in future
/*LANG*/'legacy weather icons': {
value: !!settings.legacyWeatherIcons,
format: () => (settings.legacyWeatherIcons ? 'Yes' : 'No'),
onchange: x => save('legacyWeatherIcons', x),
},
/*LANG*/'show big weather': {
value: !!settings.showBigWeather,
format: () => (settings.showBigWeather ? 'Yes' : 'No'),

3
apps/fastload/ChangeLog Normal file
View File

@ -0,0 +1,3 @@
0.01: New App!
0.02: Allow redirection of loads to the launcher
0.03: Allow hiding the fastloading info screen

21
apps/fastload/README.md Normal file
View File

@ -0,0 +1,21 @@
# Fastload Utils
*EXPERIMENTAL* Use this with caution. When you find something misbehaving please check if the problem actually persists when removing this app.
This allows fast loading of all apps with two conditions:
* Loaded app contains `Bangle.loadWidgets`. This is needed to prevent problems with apps not expecting widgets to be already loaded.
* Current app can be removed completely from RAM.
## Settings
* Allows to redirect all loads usually loading the clock to the launcher instead
* The "Fastloading..." screen can be switched off
## Technical infos
This is still experimental but it uses the same mechanism as `.bootcde` does.
It checks the app to be loaded for widget use and stores the result of that and a hash of the js in a cache.
# Creator
[halemmerich](https://github.com/halemmerich)

66
apps/fastload/boot.js Normal file
View File

@ -0,0 +1,66 @@
{
const SETTINGS = require("Storage").readJSON("fastload.json") || {};
let loadingScreen = function(){
g.reset();
let x = g.getWidth()/2;
let y = g.getHeight()/2;
g.setColor(g.theme.bg);
g.fillRect(x-49, y-19, x+49, y+19);
g.setColor(g.theme.fg);
g.drawRect(x-50, y-20, x+50, y+20);
g.setFont("6x8");
g.setFontAlign(0,0);
g.drawString("Fastloading...", x, y);
g.flip(true);
};
let cache = require("Storage").readJSON("fastload.cache") || {};
let checkApp = function(n){
// no widgets, no problem
if (!global.WIDGETS) return true;
let app = require("Storage").read(n);
if (cache[n] && E.CRC32(app) == cache[n].crc)
return cache[n].fast
cache[n] = {};
cache[n].fast = app.includes("Bangle.loadWidgets");
cache[n].crc = E.CRC32(app);
require("Storage").writeJSON("fastload.cache", cache);
return cache[n].fast;
}
global._load = load;
let slowload = function(n){
global._load(n);
}
let fastload = function(n){
if (!n || checkApp(n)){
// Bangle.load can call load, to prevent recursion this must be the system load
global.load = slowload;
Bangle.load(n);
// if fastloading worked, we need to set load back to this method
global.load = fastload;
}
else
slowload(n);
};
global.load = fastload;
Bangle.load = (o => (name) => {
if (Bangle.uiRemove && !SETTINGS.hideLoading) loadingScreen();
if (SETTINGS.autoloadLauncher && !name){
let orig = Bangle.load;
Bangle.load = (n)=>{
Bangle.load = orig;
fastload(n);
}
Bangle.showLauncher();
Bangle.load = orig;
} else
o(name);
})(Bangle.load);
}

BIN
apps/fastload/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,16 @@
{ "id": "fastload",
"name": "Fastload Utils",
"shortName" : "Fastload Utils",
"version": "0.03",
"icon": "icon.png",
"description": "Enable experimental fastloading for more apps",
"type":"bootloader",
"tags": "system",
"supports": ["BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"fastload.5.boot.js","url":"boot.js"},
{"name":"fastload.settings.js","url":"settings.js"}
],
"data": [{"name":"fastload.json"}]
}

38
apps/fastload/settings.js Normal file
View File

@ -0,0 +1,38 @@
(function(back) {
var FILE="fastload.json";
var settings;
function writeSettings(key, value) {
var s = require('Storage').readJSON(FILE, true) || {};
s[key] = value;
require('Storage').writeJSON(FILE, s);
readSettings();
}
function readSettings(){
settings = require('Storage').readJSON(FILE, true) || {};
}
readSettings();
function buildMainMenu(){
var mainmenu = {
'': { 'title': 'Fastload', back: back },
'Force load to launcher': {
value: !!settings.autoloadLauncher,
onchange: v => {
writeSettings("autoloadLauncher",v);
}
},
'Hide "Fastloading..."': {
value: !!settings.hideLoading,
onchange: v => {
writeSettings("hideLoading",v);
}
}
};
return mainmenu;
}
E.showMenu(buildMainMenu());
})

View File

@ -8,6 +8,8 @@ Upon opening the gallery app, you will be presented with a list of images that y
## Adding images
Once this app is installed you can manage images by pressing the Disk icon next to it or by following the manual steps below:
1. The gallery app does not perform any scaling, and does not support panning. Therefore, you should use your favorite image editor to produce an image of the appropriate size for your watch. (240x240 for Bangle 1 or 176x176 for Bangle 2.) How you achieve this is up to you. If on a Bangle 2, I recommend adjusting the colors here to comply with the color restrictions.
2. Upload your image to the [Espruino image converter](https://www.espruino.com/Image+Converter). I recommend enabling compression and choosing one of the following color settings:
@ -15,4 +17,4 @@ Upon opening the gallery app, you will be presented with a list of images that y
* 3 bit RGB for Bangle 2
* 1 bit black/white for monochrome images that you want to respond to your system theme. (White will be rendered as your foreground color and black will be rendered as your background color.)
3. Set the output format to an image string, copy it into the [IDE](https://www.espruino.com/ide/), and set the destination to a file in storage. The file name should begin with "gal-" (without the quotes) and end with ".img" (without the quotes) to appear in the gallery. Note that the gal- prefix and .img extension will be removed in the UI. Upload the file.
3. Set the output format to an image string, copy it into the [IDE](https://www.espruino.com/ide/), and set the destination to a file in storage. The file name should begin with "gal-" (without the quotes) and end with ".img" (without the quotes) to appear in the gallery. Note that the gal- prefix and .img extension will be removed in the UI. Upload the file.

165
apps/gallery/interface.html Normal file
View File

@ -0,0 +1,165 @@
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css">
</head>
<body>
<script src="../../webtools/heatshrink.js"></script>
<script src="../../webtools/imageconverter.js"></script>
<script src="../../core/lib/interface.js"></script>
<h4>Existing Images</h4>
<ul id="imagelist">
</ul>
<h4>Convert & Upload Images</h4>
<input type="file" id="fileLoader"/><br/>
<input type="checkbox" id="compression" onchange="imageLoaded()"> Use Compression?</input><br/>
<input type="checkbox" id="alphaToColor" onchange="imageLoaded()"> Transparency to Color</input><br/>
<input type="checkbox" id="transparent" onchange="imageLoaded()" checked> Transparency?</input><br/>
<input type="checkbox" id="inverted" onchange="imageLoaded()"> Inverted?</input><br/>
<input type="checkbox" id="autoCrop" onchange="imageLoaded()"> Crop?</input><br/>
Diffusion: <select id="diffusion" onchange="imageLoaded()"></select><br/>
Brightness: <span id="brightnessv"></span>
<input type="range" id="brightness" min="-127" max="127" value="0" onchange="imageLoaded()"></input><br/>
Contrast: <span id="contrastv"></span>
<input type="range" id="contrast" min="-255" max="255" value="0" onchange="imageLoaded()"></input><br/>
Colours: <select id="colorStyle" onchange="imageLoaded()"></select><br/>
<canvas id="canvas" style="display:none;"></canvas>
<button class="btn btn-default" id="btnUpload" disabled="disabled">Upload</button>
<script>
// load available colour formats and diffusion...
imageconverter.setFormatOptions(document.getElementById("colorStyle"));
imageconverter.setDiffusionOptions(document.getElementById("diffusion"));
let img;
let screenSize;
let imgstr;
const uploadBtn = document.getElementById("btnUpload");
uploadBtn.addEventListener("click", function() {
const filename = document.getElementById("fileLoader").value.split(/(\\|\/)/g).pop();
const filenameWithoutExt = filename.replace(/\.[^/.]+$/, "");
Util.showModal("Uploading...");
Util.writeStorage("gal-" + filenameWithoutExt.substring(0, 12) + ".img", imgstr, () => {
Util.hideModal();
updateFileList();
});
});
function imageLoaded() {
if (img === undefined) return;
if (screenSize !== img.width + "x" + img.height) {
alert("Image must be " + screenSize);
return;
}
let options = {};
let diffusionSelect = document.getElementById("diffusion");
options.diffusion = diffusionSelect.options[diffusionSelect.selectedIndex].value;
options.compression = document.getElementById("compression").checked;
options.alphaToColor = document.getElementById("alphaToColor").checked;
options.transparent = document.getElementById("transparent").checked;
options.inverted = document.getElementById("inverted").checked;
options.autoCrop = document.getElementById("autoCrop").checked;
options.brightness = 0|document.getElementById("brightness").value;
document.getElementById("brightnessv").innerText = options.brightness;
options.contrast = 0|document.getElementById("contrast").value;
document.getElementById("contrastv").innerText = options.contrast;
let colorSelect = document.getElementById("colorStyle");
options.mode = colorSelect.options[colorSelect.selectedIndex].value;
options.output = "string";
let canvas = document.getElementById("canvas")
canvas.width = img.width*2;
canvas.height = img.height;
canvas.style = "display:block;border:1px solid black;margin:8px;"
let ctx = canvas.getContext("2d");
ctx.drawImage(img,0,0);
let imageData1 = ctx.getImageData(0, 0, img.width, img.height);
ctx.fillStyle = 'white';
ctx.fillRect(options.width, 0, img.width, img.height);
let rgba = imageData1.data;
options.rgbaOut = rgba;
options.width = img.width;
options.height = img.height;
imgstr = imageconverter.RGBAtoString(rgba, options);
let outputImageData = new ImageData(options.rgbaOut, options.width, options.height);
ctx.putImageData(outputImageData,img.width,0);
// checkerboard for transparency on original image
let imageData2 = ctx.getImageData(0, 0, img.width, img.height);
imageconverter.RGBAtoCheckerboard(imageData2.data, {width:img.width,height:img.height});
ctx.putImageData(imageData2,0,0);
uploadBtn.disabled=false;
}
function handleFileSelect(event) {
if (event.target.files.length != 1) return;
let reader = new FileReader();
reader.onload = function(event) {
img = new Image();
img.onload = imageLoaded;
img.src = event.target.result;
};
reader.readAsDataURL(event.target.files[0]);
};
document.getElementById('fileLoader').addEventListener('change', handleFileSelect, false);
function updateFileList() {
Puck.write(`\x10(function() {
Bluetooth.print(JSON.stringify(require("Storage").list(/^gal-.*\.img/).sort()));
})()\n`, contents => {
let fileNames = JSON.parse(contents);
const imagelist = document.getElementById("imagelist");
imagelist.innerHTML=""; // remove all children
// add a list of existing files
if (fileNames.length === 0) {
const span = document.createElement("span");
span.textContent = "No existing images";
imagelist.appendChild(span);
}
fileNames.forEach(fileName => {
const li = document.createElement("li");
const span = document.createElement("span");
span.classList.add("label");
span.textContent = fileName.substr(4, fileName.length - 8);
li.appendChild(span);
const buttonDelete = document.createElement("button");
buttonDelete.classList.add('btn');
buttonDelete.classList.add('btn-link');
buttonDelete.textContent = "Delete";
buttonDelete.onclick = () => {
Util.showModal(`Erasing ${fileName}...`);
Util.eraseStorage(fileName, () => {
Util.hideModal();
updateFileList();
});
}
li.appendChild(buttonDelete);
imagelist.appendChild(li);
});
Util.hideModal(); // Loading modal
});
}
Util.showModal("Loading...");
// Called when app starts
function onInit() {
// Read BangleJS screen size
Puck.write(`\x10(function() {
Bluetooth.print(g.getWidth() + "x" + g.getHeight());
})()\n`, contents => {
screenSize = contents;
updateFileList();
});
}
</script>
</body>
</html>

View File

@ -12,6 +12,7 @@
"BANGLEJS"
],
"allow_emulator": true,
"interface": "interface.html",
"storage": [
{
"name": "gallery.app.js",
@ -23,4 +24,4 @@
"evaluate": true
}
]
}
}

View File

@ -14,3 +14,5 @@
0.13: Add support for internationalization
0.14: Move settings
0.15: Fix charts (fix #1366)
0.16: Code tidyup, add back button in top left of health app graphs
0.17: Add automatic translation of bar chart labels

View File

@ -1,6 +1,4 @@
function menuMain() {
swipe_enabled = false;
clearButton();
E.showMenu({
"": { title: /*LANG*/"Health Tracking" },
/*LANG*/"< Back": () => load(),
@ -12,8 +10,6 @@ function menuMain() {
}
function menuStepCount() {
swipe_enabled = false;
clearButton();
E.showMenu({
"": { title:/*LANG*/"Steps" },
/*LANG*/"< Back": () => menuMain(),
@ -23,8 +19,6 @@ function menuStepCount() {
}
function menuMovement() {
swipe_enabled = false;
clearButton();
E.showMenu({
"": { title:/*LANG*/"Movement" },
/*LANG*/"< Back": () => menuMain(),
@ -34,8 +28,6 @@ function menuMovement() {
}
function menuHRM() {
swipe_enabled = false;
clearButton();
E.showMenu({
"": { title:/*LANG*/"Heart Rate" },
/*LANG*/"< Back": () => menuMain(),
@ -48,22 +40,16 @@ function stepsPerHour() {
E.showMessage(/*LANG*/"Loading...");
var data = new Uint16Array(24);
require("health").readDay(new Date(), h=>data[h.hr]+=h.steps);
g.clear(1);
Bangle.drawWidgets();
g.reset();
setButton(menuStepCount);
barChart("HOUR", data);
barChart(/*LANG*/"HOUR", data);
}
function stepsPerDay() {
E.showMessage(/*LANG*/"Loading...");
var data = new Uint16Array(31);
require("health").readDailySummaries(new Date(), h=>data[h.day]+=h.steps);
g.clear(1);
Bangle.drawWidgets();
g.reset();
setButton(menuStepCount);
barChart("DAY", data);
barChart(/*LANG*/"DAY", data);
}
function hrmPerHour() {
@ -75,11 +61,8 @@ function hrmPerHour() {
if (h.bpm) cnt[h.hr]++;
});
data.forEach((d,i)=>data[i] = d/cnt[i]);
g.clear(1);
Bangle.drawWidgets();
g.reset();
setButton(menuHRM);
barChart("HOUR", data);
barChart(/*LANG*/"HOUR", data);
}
function hrmPerDay() {
@ -91,37 +74,27 @@ function hrmPerDay() {
if (h.bpm) cnt[h.day]++;
});
data.forEach((d,i)=>data[i] = d/cnt[i]);
g.clear(1);
Bangle.drawWidgets();
g.reset();
setButton(menuHRM);
barChart("DAY", data);
barChart(/*LANG*/"DAY", data);
}
function movementPerHour() {
E.showMessage(/*LANG*/"Loading...");
var data = new Uint16Array(24);
require("health").readDay(new Date(), h=>data[h.hr]+=h.movement);
g.clear(1);
Bangle.drawWidgets();
g.reset();
setButton(menuMovement);
barChart("HOUR", data);
barChart(/*LANG*/"HOUR", data);
}
function movementPerDay() {
E.showMessage(/*LANG*/"Loading...");
var data = new Uint16Array(31);
require("health").readDailySummaries(new Date(), h=>data[h.day]+=h.movement);
g.clear(1);
Bangle.drawWidgets();
g.reset();
setButton(menuMovement);
barChart("DAY", data);
barChart(/*LANG*/"DAY", data);
}
// Bar Chart Code
const w = g.getWidth();
const h = g.getHeight();
@ -130,13 +103,10 @@ var chart_index;
var chart_max_datum;
var chart_label;
var chart_data;
var swipe_enabled = false;
var btn;
// find the max value in the array, using a loop due to array size
function max(arr) {
var m = -Infinity;
for(var i=0; i< arr.length; i++)
if(arr[i] > m) m = arr[i];
return m;
@ -145,10 +115,8 @@ function max(arr) {
// find the end of the data, the array might be for 31 days but only have 2 days of data in it
function get_data_length(arr) {
var nlen = arr.length;
for(var i = arr.length - 1; i > 0 && arr[i] == 0; i--)
nlen--;
return nlen;
}
@ -167,15 +135,11 @@ function drawBarChart() {
const bar_width = (w - 2) / 9; // we want 9 bars, bar 5 in the centre
var bar_top;
var bar;
g.setColor(g.theme.bg);
g.fillRect(0,24,w,h);
g.reset().clearRect(0,24,w,h);
for (bar = 1; bar < 10; bar++) {
if (bar == 5) {
g.setFont('6x8', 2);
g.setFontAlign(0,-1);
g.setColor(g.theme.fg);
g.setFont('6x8', 2).setFontAlign(0,-1).setColor(g.theme.fg);
g.drawString(chart_label + " " + (chart_index + bar -1) + " " + chart_data[chart_index + bar - 1], g.getWidth()/2, 150);
g.setColor("#00f");
} else {
@ -189,45 +153,26 @@ function drawBarChart() {
bar_top = bar_bot;
g.fillRect( 1 + (bar - 1)* bar_width, bar_bot, 1 + bar*bar_width, bar_top);
g.setColor(g.theme.fg);
g.drawRect( 1 + (bar - 1)* bar_width, bar_bot, 1 + bar*bar_width, bar_top);
g.setColor(g.theme.fg).drawRect( 1 + (bar - 1)* bar_width, bar_bot, 1 + bar*bar_width, bar_top);
}
}
function next_bar() {
chart_index = Math.min(data_len - 5, chart_index + 1);
}
function prev_bar() {
// HOUR data starts at index 0, DAY data starts at index 1
chart_index = Math.max((chart_label == "DAY") ? -3 : -4, chart_index - 1);
}
Bangle.on('swipe', dir => {
if (!swipe_enabled) return;
if (dir == 1) prev_bar(); else next_bar();
drawBarChart();
});
// use setWatch() as Bangle.setUI("updown",..) interacts with swipes
function setButton(fn) {
// cancel callback, otherwise a slight up down movement will show the E.showMenu()
Bangle.setUI("updown", undefined);
if (process.env.HWVERSION == 1)
btn = setWatch(fn, BTN2);
else
btn = setWatch(fn, BTN1);
}
function clearButton() {
if (btn !== undefined) {
clearWatch(btn);
btn = undefined;
}
Bangle.setUI({mode:"custom",
back:fn,
swipe:(lr,ud) => {
if (lr == 1) {
// HOUR data starts at index 0, DAY data starts at index 1
chart_index = Math.max((chart_label == /*LANG*/"DAY") ? -3 : -4, chart_index - 1);
} else if (lr<0) {
chart_index = Math.min(data_len - 5, chart_index + 1);
} else {
return fn();
}
drawBarChart();
}});
}
Bangle.loadWidgets();
Bangle.drawWidgets();
menuMain();

View File

@ -1,7 +1,7 @@
{
"id": "health",
"name": "Health Tracking",
"version": "0.15",
"version": "0.17",
"description": "Logs health data and provides an app to view it",
"icon": "app.png",
"tags": "tool,system,health",

View File

@ -3,3 +3,4 @@
0.03: Settings page now uses built-in min/max/wrap (fix #1607)
0.04: Add masking widget input to other apps (using espruino/Espruino#2151), add a oversize option to increase the touch area.
0.05: Prevent drawing into app area.
0.06: Fix issue where .draw was being called by reference (not allowing widgets to be hidden)

View File

@ -2,7 +2,7 @@
"id": "lightswitch",
"name": "Light Switch Widget",
"shortName": "Light Switch",
"version": "0.05",
"version": "0.06",
"description": "A fast way to switch LCD backlight on/off, change the brightness and show the lock status. All in one widget.",
"icon": "images/app.png",
"screenshots": [

View File

@ -224,28 +224,20 @@
// main widget function //
// display and setup/reset function
draw: function(locked) {
draw: function() {
// setup shortcut to this widget
var w = WIDGETS.lightswitch;
// set lcd brightness on unlocking
// all other cases are catched by the boot file
if (locked === false) Bangle.setLCDBrightness(w.isOn ? w.value : 0);
// read lock status
locked = Bangle.isLocked();
var locked = Bangle.isLocked();
// remove listeners to prevent uncertainties
Bangle.removeListener("lock", w.draw);
Bangle.removeListener("touch", w.touchListener);
Bangle.removeListener("tap", require("lightswitch.js").tapListener);
// draw widget icon
w.drawIcon(locked);
// add lock listener
Bangle.on("lock", w.draw);
// add touch listener to control the light depending on settings at first position
if (w.touchOn === "always" || !global.__FILE__ ||
w.touchOn.includes(__FILE__) ||
@ -259,7 +251,15 @@
w = undefined;
}
});
Bangle.on("lock", locked => {
var w = WIDGETS.lightswitch;
// set lcd brightness on unlocking
// all other cases are catched by the boot file
if (locked === false) Bangle.setLCDBrightness(w.isOn ? w.value : 0);
w.draw()
});
// clear variable
settings = undefined;
delete settings;
})()

View File

@ -77,3 +77,5 @@
0.54: Move icons out to messageicons module
0.55: Rename to messagegui, move global message handling library to message module
Move widget to widmessage
0.56: Fix handling of music messages
0.57: Fix "unread Timeout" = off (previously defaulted to 60s)

View File

@ -112,9 +112,11 @@ function showMapMessage(msg) {
Bangle.setUI({mode:"updown", back: back}, back); // any input takes us back
}
var updateLabelsInterval;
let updateLabelsInterval,
music = {artist: "", album: "", title: ""}; // defaults, so e.g. msg.title.length doesn't error
function showMusicMessage(msg) {
active = "music";
msg = Object.assign(music, msg); // combine+remember "musicinfo" and "musicstate" messages
openMusic = msg.state=="play";
var trackScrollOffset = 0;
var artistScrollOffset = 0;
@ -444,10 +446,9 @@ require("messages").toggleWidget(false);
Bangle.drawWidgets();
setTimeout(() => {
var unreadTimeoutMillis = (settings.unreadTimeout || 60) * 1000;
if (unreadTimeoutMillis) {
unreadTimeout = setTimeout(load, unreadTimeoutMillis);
}
if (!isFinite(settings.unreadTimeout)) settings.unreadTimeout=60;
if (settings.unreadTimeout)
unreadTimeout = setTimeout(load, settings.unreadTimeout*1000);
// only openMusic on launch if music is new
var newMusic = MESSAGES.some(m => m.id === "music" && m.new);
checkMessages({ clockIfNoMsg: 0, clockIfAllRead: 0, showMsgIfUnread: 1, openMusic: newMusic && settings.openMusic });

View File

@ -15,12 +15,12 @@ exports.listener = function(type, msg) {
const appSettings = require("Storage").readJSON("messages.settings.json", 1) || {};
let loadMessages = (Bangle.CLOCK || event.important);
if (type==="music") {
if (Bangle.CLOCK && msg.new && appSettings.openMusic) loadMessages = true;
if (Bangle.CLOCK && msg.state && msg.title && appSettings.openMusic) loadMessages = true;
else return;
}
require("messages").save(msg);
msg.handled = true;
if (msg.t!=="add" || !msg.new) {
if ((msg.t!=="add" || !msg.new) && (type!=="music")) { // music always has t:"modify"
return;
}

View File

@ -1,7 +1,7 @@
{
"id": "messagegui",
"name": "Message UI",
"version": "0.55",
"version": "0.57",
"description": "Default app to display notifications from iOS and Gadgetbridge/Android",
"icon": "app.png",
"type": "app",

View File

@ -5,44 +5,73 @@ exports.getImage = function(msg) {
*/
if (msg.img) return atob(msg.img);
const s = (("string"=== typeof msg) ? msg : (msg.src || "")).toLowerCase();
if (s=="airbnb") return atob("GBgBAAAAAAAAAAAAADwAAH4AAGYAAMMAAIEAAYGAAYGAAzzAA2bABmZgBmZgDGYwDDwwCDwQCBgQDDwwB+fgA8PAAAAAAAAAAAAA");
if (s=="alarm" || s =="alarmclockreceiver") return atob("GBjBAP////8AAAAAAAACAEAHAOAefng5/5wTgcgHAOAOGHAMGDAYGBgYGBgYGBgYGBgYDhgYBxgMATAOAHAHAOADgcAB/4AAfgAAAAAAAAA=");
if (s=="airbnb") return atob("GBgBAAAAADwAAH4AAMMAAIMAAYGAAQGAAwDAAwDABjxgBn5gDMMwDMMwGMMYGMMYMGYMMGYMIDwEIBgEIDwEMH4MHee4D4HwAAAA"); // icons/airbnb.png
if (s=="alarm" || s =="alarmclockreceiver") return atob("GBgBAAAAAAAAAgBABwDgHn54Of+cE8PIBwDgDhhwDBgwHBg4GBgYGBgYGBgYGA4YHAc4DAEwDgBwBwDgA8PAAf+AAH4AAAAAAAAA"); // icons/alarm.png
if (s=="amazon shopping") return atob("GBgBAAAAAP8AAf+AA//AA+fAA8PAAIPAAD/AAP/AA//AA+PAB8PAB8fAB8fgB//gA//gA/3AAPCecAAeOAAeDwH0B//kAf+AAAAA"); // icons/amazon.png
if (s=="bibel") return atob("GBgBAAAAA//wD//4D//4H//4H/f4H/f4H+P4H4D4H4D4H/f4H/f4H/f4H/f4H/f4H//4H//4H//4GAAAEAAAEAAACAAAB//4AAAA");
if (s=="bitwarden" || s=="1password" || s=="lastpass" || s=="dashlane") return atob("GBgBAAAAABgAAP8AA//AD4/wHg/4GA/4GA/4GA/4GA/4GA/4GA/4H/AYH/AYH/A4D/AwD/BwB/BgB/DgA/HAAfeAAP8AADwAAAAA"); // icons/security.png
if (s=="bring") return atob("GBgBAAAAAAAAAAAAAAAAAHwAAFoAAf+AA/+AA/+AA/+AA/eAA+eAA0+AAx+AA7+AA/+AA//AA/+AAf8AAAIAAAAAAAAAAAAAAAAA");
if (s=="calendar" || s=="etar") return atob("GBiBAAAAAAAAAAAAAA//8B//+BgAGBgAGBgAGB//+B//+B//+B9m2B//+B//+Btm2B//+B//+Btm+B//+B//+A//8AAAAAAAAAAAAA==");
if (s=="corona-warn") return atob("GBgBAAAAABwAAP+AAf/gA//wB/PwD/PgDzvAHzuAP8EAP8AAPAAAPMAAP8AAH8AAHzsADzuAB/PAB/PgA//wAP/gAH+AAAwAAAAA");
if (s=="discord") return atob("GBgBAAAAAAAAAAAAAIEABwDgDP8wH//4H//4P//8P//8P//8Pjx8fhh+fzz+f//+f//+e//ePH48HwD4AgBAAAAAAAAAAAAAAAAA");
if (s=="facebook" || s=="messenger") return atob("GBiBAAAAAAAAAAAYAAD/AAP/wAf/4A/48A/g8B/g+B/j+B/n+D/n/D8A/B8A+B+B+B/n+A/n8A/n8Afn4APnwADnAAAAAAAAAAAAAA==");
if (s=="chat") return atob("GBgBAAAAAf/8A//+A//+A//+OAB+e/8+e/++e/++e/++e/++e/++e/++ef+8fAAAf//Af//Af//Af//Af/+AcAAAYAAAQAAAAAAA"); // icons/google chat.png
if (s=="chrome") return atob("GBgBAAAAAAAAAP8AA//AB+fgDwDwHgB4HAA4Pj/8OmYcO8McMYEMMYEMOMMcOGccOD4cHAw4Hgx4DxjwB//gA//AAP8AAAAAAAAA"); // icons/chrome.png
if (s=="corona-warn") return atob("GBgBAAAAAAAAABgAABgABhhgDn5wD//wA8PAA+fAB2bgBgBgPpl8Ppl8BgBgB2bgA+fAA8PAD//wDn5wBhhgABgAABgAAAAAAAAA"); // icons/coronavirus.png
if (s=="bmo" || s=="desjardins" || s=="rbc mobile" || s=="nbc" || s=="rabobank" || s=="scotiabank" || s=="td (canada)") return atob("GBgBAAAAADgAAP4AAe8AB4PAHgDwP//4P//4AAAAAAAADjjgDjjgDjjgDjjgDjjgDjjgDjjgAAAAAAAAP//4P//4AAAAAAAAAAAA"); // icons/bank.png
if (s=="discord") return atob("GBgBAAAAAAAAAAAAAAAAA4HAD//wH//4H//4P//8P//8P//8fn5+fDw+fDw+fn5+f//+f//+ff++PgB8DgBwAAAAAAAAAAAAAAAA"); // icons/discord.png
if (s=="drive") return atob("GBgBAAAAAAAAAH8AAH8AAT+AA7/AA9/AB8/gB+/gD+fwD+fwH8P4P8P8P4H8fwAAf3/+Pn/8Pv/8HP/4Df/wC//wAAAAAAAAAAAA"); // icons/google drive.png
if (s=="element") return atob("GBgBAAAAAHwAAH4AAH8AAAeAAePAB+HAD+DgHgDgPADuOADucAAOcAAOdwAcdwA8BwB4BwfwA4fgA8eAAeAAAP4AAH4AAD4AAAAA"); // icons/matrix element.png
if (s=="facebook") return atob("GBgBAAAAAAAAAH4AAf+AB//gD//wD/DwH+D4H+P4P+f8P+f8P+f8PwD8PwD8PwD8H+f4H+f4D+fwD+fwB+fgAeeAAOcAAAAAAAAA"); // icons/facebook.png
if (s=="messenger") return atob("GBgBAAAAAAAAAP8AA//AB//gD//wH//4H//4P//8P9+8P458PwB8PgD8PnH8Pfv8H//4H//4D//wB//gB//AB/8AAwAAAAAAAAAA"); // icons/facebook messenger.png
if (s=="firefox" || s=="firefox beta" || s=="firefox nightly") return atob("GBgBAAAAAAAAAAMAAAcAAAeABA/ADY/gH4P4H4H4H8H8P/H8P+D8PwD8PwD8PwD8H4H4H8P4H//4D//wB//gA//AAP8AAAAAAAAA"); // icons/firefox.png
if (s=="f-droid" || s=="neo store" || s=="aurora droid") return atob("GBgBAAAAQAACYAAGP//8H//4H//4HH44HH44H//4AAAAH//4H8P4H734H374HsN4Hvl4Hv14Hvl4HsN4H374H734H8P4D//wAAAA"); // icons/security.png
if (s=="github") return atob("GBgBAAAAAAAAAH4AAf+AB//gD//wDv9wHgB4HgB4PAA8PAA8PAA8PAA8PAA8PgB8HwD4G8P4DcPwDgPwB4PgAcOAAAAAAAAAAAAA"); // icons/github.png
if (s=="gitlab") return atob("GBgBAAAABAAgDAAwDAAwHgB4HgB4PgB8PwD8P//8f//+f//+f//+f//+f//+f//+P//8H//4D//wA//AAf+AAP8AADwAABgAAAAA"); // icons/gitlab.png
if (s=="gmx") return atob("GBgBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEJmfmd8Zuc85v847/88Z9s8fttmHIHiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
if (s=="google") return atob("GBiBAAAAAAD/AAP/wAf/4A/D4B8AwDwAADwAAHgAAHgAAHAAAHAH/nAH/nAH/ngH/ngAHjwAPDwAfB8A+A/D8Af/4AP/wAD/AAAAAA==");
if (s=="google home") return atob("GBiCAAAAAAAAAAAAAAAAAAAAAoAAAAAACqAAAAAAKqwAAAAAqroAAAACquqAAAAKq+qgAAAqr/qoAACqv/6qAAKq//+qgA6r///qsAqr///6sAqv///6sAqv///6sAqv///6sA6v///6sA6v///qsA6qqqqqsA6qqqqqsA6qqqqqsAP7///vwAAAAAAAAAAAAAAAAA=="); // 2 bit unpaletted
if (s=="google") return atob("GBgBAAAAAP8AA//AB//gD//gH+fAP4CAPwAAPgAAfAAAfA/+fA/+fA/+fA/+fAA+PgA+PwB8P4D8H+f4D//4B//wA//AAP8AAAAA"); // icons/google.png
if (s=="google home") return atob("GBgBAAAAABgAADwAAH4AAf4AA/zAB/vgD/fwH+f4P4H8fwD+fgB+fAA+eAA+cAA+bAA+HAA+PAA+ff++ff++ff++ff++Pf+8AAAA"); // icons/google home.png
if (s=="google play store") return atob("GBgBAAAAAAAAAH4AAP8AAMMAAMMAP//8P//8MAAMMAAMMGAMMHgMMH4MMH8MMH4MMHgMMGAMMAAMMAAMP//8H//4AAAAAAAAAAAA"); // icons/google play store.png
if (s=="home assistant") return atob("FhaBAAAAAADAAAeAAD8AAf4AD/3AfP8D7fwft/D/P8ec572zbzbNsOEhw+AfD8D8P4fw/z/D/P8P8/w/z/AAAAA=");
if (s=="instagram") return atob("GBiBAAAAAAAAAAAAAAAAAAP/wAYAYAwAMAgAkAh+EAjDEAiBEAiBEAiBEAiBEAjDEAh+EAgAEAwAMAYAYAP/wAAAAAAAAAAAAAAAAA==");
if (s=="instagram") return atob("GBgBAAAAD//wH//4OAAccAAOYABmYDxmYP8GYeeGYYGGY4HGYwDGYwDGY4HGYYGGYeeGYP8GYDwGYAAGcAAOOAAcH//4D//wAAAA"); // icons/instagram.png
if (s=="kalender") return atob("GBgBBgBgBQCgff++RQCiRgBiQAACf//+QAACQAACR//iRJkiRIEiR//iRNsiRIEiRJkiR//iRIEiRIEiR//iQAACQAACf//+AAAA");
if (s=="lieferando") return atob("GBgBABgAAH5wAP9wAf/4A//4B//4D//4H//4P/88fV8+fV4//V4//Vw/HVw4HVw4HBg4HBg4HBg4HDg4Hjw4Hj84Hj44Hj44Hj44");
if (s=="mattermost") return atob("GBgBAAAAAPAAA+EAB4MADgcYHAcYOA8MOB8OeD8GcD8GcH8GcD8HcD8HeBwHeAAOfAAOfgAePwA8P8D8H//4D//wB//gAf/AAH4A");
if (s=="keep notes") return atob("GBgBAAAAAAAAH//4P//8P8P8Pzz8P378Pv98Pv98Pv98Pv98P378Pzz8P738P4H8P738P738P4GMP8OYP/+wP//gH//AAAAAAAAA"); // icons/google keep.png
if (s=="lieferando") return atob("GBgBAAAAADwAAH4AAP/gAf/wA//wB//wD//wH//4H/98Pt58ft5+Ptx8DtxwDtxwDhxwDhhwDhhwDzhwD75wD75wD75wB77gAAAA"); // icons/lieferando.png
if (s=="linkedin") return atob("GBgBAAAAf//+f//+f//+ef/+cf/+cf/+f//+f//+ccw+ccAeccAecccOcceOcceOcceOcceOcceOcceOec+ef//+f//+f//+AAAA"); // icons/linkedin.png
if (s=="maps" || s=="organic maps" || s=="osmand") return atob("GBgBAAAAAAAAAAAAAeAYD/z4H//4GMeYGMMYGMMYGMMYGMMYGMMYGMMYGMMYGMMYGMMYGMMYGeMYH//4Hz/wGAeAAAAAAAAAAAAA"); // icons/map.png
if (s=="mastodon" || s=="fedilab" || s=="tooot" || s=="tusky") return atob("GBgBAAAAB//gD//4H//4P//8PBg8PAA8fOMeeOeeeOeeOOeeOOecOP+cOP+cP//8P//4P//4P//gHwAAH4AAD+cAB/8AAf4AAAAA"); // icons/mastodon.png
if (s=="mattermost") return atob("GBgBAAAAAPAAA+EAB4GADgOQHAeYOA+cOB+MeB+OcD+GcD+GcD+GeD8OeB4OeAAOfAAePgA8P4B8H/f4D//wB//gA//AAP8AAAAA"); // icons/mattermost.png
if (s=="n26") return atob("GBgBAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAOIAAOIAAPIAANoAANoAAM4AAMYAAMYAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAA");
if (s=="netflix") return atob("GBgBAAAAA8PAA+PAAePAAePAAfPAAvPAA/PAAvvAAn/AA/nAA3/AA/7AA5/AA/5AA99AA8/AA89AA8+AA8eAA8eAA8fAA8PAAAAA"); // icons/netflix.png
if (s=="news" || s=="cbc news" || s=="rc info" || s=="reuters" || s=="ap news" || s=="la presse" || s=="nbc news") return atob("GBgBAAAAAAAAAAAALaW0P//8P//8P//8P//8MAAMMAAMMAAMP//8P//8MBwcMBwcMB/8MB/8MBwcMBwcP//8P//8AAAAAAAAAAAA"); // icons/news.png
if (s=="nextbike") return atob("GBgBAAAAAAAAAAAAAAAAAAAAAACAfgDAPwDAP4HAH4N4H8f8D82GMd8CMDsDMGMDMGGGGMHOD4D8AAAAAAAAAAAAAAAAAAAAAAAA");
if (s=="nina") return atob("GBgBAAAABAAQCAAICAAIEAAEEgAkJAgSJBwSKRxKSj4pUn8lVP+VVP+VUgAlSgApKQBKJAASJAASEgAkEAAECAAICAAIBAAQAAAA");
if (s=="outlook mail") return atob("HBwBAAAAAAAAAAAIAAAfwAAP/gAB/+AAP/5/A//v/D/+/8P/7/g+Pv8Dye/gPd74w5znHDnOB8Oc4Pw8nv/Dwe/8Pj7/w//v/D/+/8P/7/gf/gAA/+AAAfwAAACAAAAAAAAAAAA=");
if (s=="paypal") return atob("GBgBAAAAAAAAAAAAAf+AAf/AAf/gA//gA//gA//wA//wA//wA//wB//wB//wB//gB/+AB/gAB/gAB/gAAPgAAPgAAAAAAAAAAAAA");
if (s=="phone") return atob("FxeBABgAAPgAAfAAB/AAD+AAH+AAP8AAP4AAfgAA/AAA+AAA+AAA+AAB+AAB+AAB+OAB//AB//gB//gA//AA/8AAf4AAPAA=");
if (s=="post & dhl") return atob("GBgBAPgAE/5wMwZ8NgN8NgP4NgP4HgP4HgPwDwfgD//AB/+AAf8AAAAABs7AHcdgG4MwAAAAGESAFESAEkSAEnyAEkSAFESAGETw");
if (s=="signal") return atob("GBgBAAAAAGwAAQGAAhggCP8QE//AB//oJ//kL//wD//0D//wT//wD//wL//0J//kB//oA//ICf8ABfxgBYBAADoABMAABAAAAAAA");
if (s=="skype") return atob("GhoBB8AAB//AA//+Af//wH//+D///w/8D+P8Afz/DD8/j4/H4fP5/A/+f4B/n/gP5//B+fj8fj4/H8+DB/PwA/x/A/8P///B///gP//4B//8AD/+AAA+AA==");
if (s=="slack") return atob("GBiBAAAAAAAAAABAAAHvAAHvAADvAAAPAB/PMB/veD/veB/mcAAAABzH8B3v+B3v+B3n8AHgAAHuAAHvAAHvAADGAAAAAAAAAAAAAA==");
if (s=="snapchat") return atob("GBgBAAAAAAAAAH4AAf+AAf+AA//AA//AA//AA//AA//AH//4D//wB//gA//AB//gD//wH//4f//+P//8D//wAf+AAH4AAAAAAAAA");
if (s=="steam") return atob("GBgBAAAAAAAAAAAAAAAAAAAAAAfgAAwwAAvQABvQABvQADvQgDww4H/g+f8A/zwAf9gAH9AAB8AAACAAAcAAAAAAAAAAAAAAAAAA");
if (s=="teams") return atob("GBgBAAAAAAAAAAQAAB4AAD8IAA8cP/M+f/scf/gIeDgAfvvefvvffvvffvvffvvff/vff/veP/PeAA/cAH/AAD+AAD8AAAQAAAAA");
if (s=="telegram" || s=="telegram foss") return atob("GBiBAAAAAAAAAAAAAAAAAwAAHwAA/wAD/wAf3gD/Pgf+fh/4/v/z/P/H/D8P/Acf/AM//AF/+AF/+AH/+ADz+ADh+ADAcAAAMAAAAA==");
if (s=="threema") return atob("GBjB/4Yx//8AAAAAAAAAAAAAfgAB/4AD/8AH/+AH/+AP//AP2/APw/APw/AHw+AH/+AH/8AH/4AH/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=");
if (s=="to do" || s=="opentasks") return atob("GBgBAAAAAAAAAAAwAAB4AAD8AAH+AAP/DAf/Hg//Px/+f7/8///4///wf//gP//AH/+AD/8AB/4AA/wAAfgAAPAAAGAAAAAAAAAA");
if (s=="twitch") return atob("GBgBH//+P//+P//+eAAGeAAGeAAGeDGGeDOGeDOGeDOGeDOGeDOGeDOGeAAOeAAOeAAcf4/4f5/wf7/gf//Af/+AA/AAA+AAAcAA");
if (s=="twitter") return atob("GhYBAABgAAB+JgA/8cAf/ngH/5+B/8P8f+D///h///4f//+D///g///wD//8B//+AP//gD//wAP/8AB/+AB/+AH//AAf/AAAYAAA");
if (s=="outlook mail") return atob("GBgBAAAAAAAAAP/8AP/8AP/8AJjMf/jMf//8f//8cHjMd3jMZz/+Zz/+d3jecHj+f//mf/eGf/PGAwDmAwA+A//+Af/+AAAAAAAA"); // icons/outlook.png
if (s=="paypal") return atob("GBgBAAAAA/+AA//gA//wB//wB//wB//wB//wB//wB//gD//gD//ID/+ID/wwD4BwD5/gD74AH7gAHzAAHzAAHzAAAHAAAHAAAAAA"); // icons/paypal.png
if (s=="phone") return atob("GBgBAAAAAAAAH4AAP8AAP8AAP8AAH8AAH8AAH8AAH4AADwAADwAABwAAA4AAA8HwAeP8AP/8AH/8AD/8AA/8AAP8AAB4AAAAAAAA"); // icons/phone.png
if (s=="plex") return atob("GBgBAAAAB/gAB/gAA/wAAf4AAf4AAP8AAH+AAH+AAD/AAB/gAB/gAB/gAB/gAD/AAH+AAH+AAP8AAf4AAf4AA/wAB/gAB/gAAAAA"); // icons/plex.png
if (s=="pocket") return atob("GBgBAAAAAAAAP//8f//+f//+f//+f//+f//+fP8+eH4efDw+fhh+fwD+f4H+P8P8P+f8H//4H//4D//wB//gAf+AAH4AAAAAAAAA"); // icons/pocket.png
if (s=="post & dhl") return atob("GBgBAAAAAAAAAAAAAAAAP/+Af/+AYAGAYAGAYAHwYAH4YAGMYAGGYAH+YAH+bwH+f//+ef+eGYGYH4H4DwDwAAAAAAAAAAAAAAAA"); // icons/delivery.png
if (s=="proton mail") return atob("GBgBAAAAAAAAAAAAQAACYAAGcAAOeAAePABeXgDebwHed4Pee/fefe/efh/ef//ef//ef//ef//ef//ef//eP//cAAAAAAAAAAAA"); // icons/protonmail.png
if (s=="reddit" || s=="sync pro" || s=="sync dev" || s=="boost" || s=="infinity" || s=="slide") return atob("GBgBAAAAAAAAAAYwAAX4AAh4AAgwAAgAAAgAAH4AAf+AN//sf//+fn5+PDw8HDw4Hn54H//4H//4DzzwB4HgAf+AAH4AAAAAAAAA"); // icons/reddit.png
if (s=="signal") return atob("GBgBAAAAAL0AAYGABH4gCf+QE//IB//gL//0b//2H//4X//6X//6X//6X//6H//4b//2L//0D//gL//ID/+QYH4gVYGAcL0AAAAA"); // icons/signal.png
if (s=="skype") return atob("GBgBAAAAB8AAH/8AP//AP//gf8fwfwD4fgB4fjx8fj/8Pg/8PwH8P4B8P/h8Pnx+Pjx+Hhh+HwD+D8P+B//8A//8AP/4AAPgAAAA"); // icons/skype.png
if (s=="slack") return atob("GBgBAAAAAOcAAeeAAeeAAeeAAGeAAAeAP+ecf+eef+e+f+e+AAAAAAAAfef+fef+eef+Oef8AeAAAeYAAeeAAeeAAeeAAOcAAAAA"); // icons/slack.png
if (s=="snapchat") return atob("GBgBAAAAAAAAAAAAAH4AAf+AAYGAAwDAAwDAAwDADwDwDwDwDgBwBwDgBwDgDgBwHAA4OAAcHAA4D4HwB//gAH4AAAAAAAAAAAAA"); // icons/snapchat.png
if (s=="starbucks") return atob("GBgBAAAAAAAAAAAAD//4D//8DADMDADMDADMDAD8DAD4DADADADADADADADADgHAB/+AA/8AAAAAAAAAP//wP//wAAAAAAAAAAAA"); // icons/cafe.png
if (s=="steam") return atob("GBgBAAAAAAAAAf+AA//AD//wD//wH/g4P/OcP/RcP+RcP+ReH8OeB4A+AAH+AMP8IC/8OS/8HN/4Dj/wD//wA//AAf+AAAAAAAAA"); // icons/steam.png
if (s=="teams") return atob("GBgBAAAAAAgAAD4AADcYAGM8AGNmP/dmP/48MDAYMD/+PP/+PPBmPPBmPPBmPPBmP/BmP/BmH+B+AYD4AMDAAOOAAH8AABwAAAAA"); // icons/teams.png
if (s=="telegram" || s=="telegram foss") return atob("GBgBAAAAAAAAAAAAAAAeAAB+AAP+AA/+AD/+Af9+B/z+H/n8f+P8f8f8Dw/8AB/8AB/8AB/4AAf4AAP4AAD4AABwAAAAAAAAAAAA"); // icons/telegram.png
if (s=="threema") return atob("GBgBAAAAAP8AA//AB//gD//wH8P4H9v4H734P5n8P4H8P4H8H4H4H4H4D//wD//gD//AH/8AHDwAAAAAAAAABhhgDzzwBhhgAAAA"); // icons/threema.png
if (s=="tiktok") return atob("GBgBAAAAAAAAAAcAAAcAAAeAAAfAAAfwAAf4AAf4AMd4A8cAB8cAD8cADwcAHgcAHgcAHg8ADw8AD/4AB/4AA/wAAfAAAAAAAAAA"); // icons/tiktok.png
if (s=="to do" || s=="opentasks" || s=="tasks") return atob("GBgBAAAAAHwAAf+AA//ID4GcHwA8HAB4PADwOAHgcAPGcAeOcY8Oc94OcfwOcPgOOHAcOCAcHAA4DgB4D4HwB//gAf+AAH4AAAAA"); // icons/task.png
if (s=="transit") return atob("GBgBAAAAD//wP//8P//8f//+f/j+ffA+eOA+eOMef+cefef+eOe+fecef+e+eOf+eOcefAcefA++fx/+f//+P//8P//8D//wAAAA"); // icons/transit.png
if (s=="twitch") return atob("GBgBAAAAA//8B//8DgAMHgAMPhjMPhjMPhjMPhjMPhjMPgAMPgAMPgAYPgAwP+fgP+/AP/+AP/8AP/4AAeAAAcAAAYAAAQAAAAAA"); // icons/twitch.png
if (s=="twitter") return atob("GBgBAAAAAAAAAAAAAAPAIAf8MA/4PA/8Pg/4H//4H//4P//4P//wH//wD//wD//gD//AA//AAf+AB/8AP/wAD/AAAAAAAAAAAAAA"); // icons/twitter.png
if (s=="uber" || s=="lyft") return atob("GBgBAAAAAAAAAAAAAH4AAH4AB//gB//gDgBwDAAwDAAwH//4H//4GAAYG4HYG4HYG4HYGAAYH//4H//4HAA4HAA4AAAAAAAAAAAA"); // icons/taxi.png
if (s=="vlc") return atob("GBgBAAAAABgAABgAADwAADwAAAAAAAAAAAAAAAAAAIEAAP8AAP8AAf+AAP8AAAAADAAwDAAwHAA4HwD4H//4P//8P//8P//8AAAA"); // icons/vlc.png
if (s=="warnapp") return atob("GBgBAAAAAAAAAAAAAH4AAP8AA//AA//AD//gP//gf//4f//+/+P+/8H//8n//4n/fxh/fzg+Pj88Dn44AA4AAAwAAAwAAAgAAAAA");
if (s=="whatsapp") return atob("GBiBAAB+AAP/wAf/4A//8B//+D///H9//n5//nw//vw///x///5///4///8e//+EP3/APn/wPn/+/j///H//+H//8H//4H//wMB+AA==");
if (s=="whatsapp") return atob("GBgBAAAAAP8AA//AB4HwDgB4HAA4OAAcMYAMc8AOc8AGY8AGYcAGYeAGYPOGcH/OcD/OMA+MOAAcMAA4MgBwf8Pgf//AcP8AAAAA"); // icons/whatsapp.png
if (s=="wordfeud") return atob("GBgCWqqqqqqlf//////9v//////+v/////++v/////++v8///Lu+v8///L++v8///P/+v8v//P/+v9v//P/+v+fx/P/+v+Pk+P/+v/PN+f/+v/POuv/+v/Ofdv/+v/NvM//+v/I/Y//+v/k/k//+v/i/w//+v/7/6//+v//////+v//////+f//////9Wqqqqqql");
if (s=="youtube" || s=="newpipe") return atob("GBgBAAAAAAAAAAAAAAAAAf8AH//4P//4P//8P//8P5/8P4/8f4P8f4P8P4/8P5/8P//8P//8P//4H//4Af8AAAAAAAAAAAAAAAAA");
if (s=="youtube" || s=="newpipe") return atob("GBgBAAAAAAAAAAAAAAAAAAAAH//4P//8P//8f//+f8/+f8P+f8D+f8D+f8P+f8/+f//+P//8P//8H//4AAAAAAAAAAAAAAAAAAAA"); // icons/youtube.png
if (s=="zoom" || s=="meet") return atob("GBgBAAAAAAAAAAAAP/+Af//Af//AcADicADmcADucAD+cAD+cAD+cAD+cAD+cAD+cADucADmcADif//Af//AP/+AAAAAAAAAAAAA"); // icons/videoconf.png
if (msg.id=="music") return atob("FhaBAH//+/////////////h/+AH/4Af/gB/+H3/7/f/v9/+/3/7+f/vB/w8H+Dwf4PD/x/////////////3//+A=");
// if (s=="sms message" || s=="mail" || s=="gmail") // .. default icon (below)
return atob("FhKBAH//+P//yf/+c//z5/+fz/z/n+f/Pz/+ef/8D///////////////////////f//4///A");
@ -57,7 +86,7 @@ exports.getColor = function(msg,options) {
return {
// generic colors, using B2-safe colors
// DO NOT USE BLACK OR WHITE HERE, just leave the declaration out and then the theme's fg color will be used
"airbnb": "#f00",
"airbnb": "#ff385c", // https://news.airbnb.com/media-assets/category/brand/
"mail": "#ff0",
"music": "#f0f",
"phone": "#0f0",
@ -66,39 +95,44 @@ exports.getColor = function(msg,options) {
// all dithered on B2, but we only use the color for the icons. (Could maybe pick the closest 3-bit color for B2?)
"bibel": "#54342c",
"bring": "#455a64",
"discord": "#738adb",
"discord": "#5865f2", // https://discord.com/branding
"etar": "#36a18b",
"facebook": "#4267b2",
"facebook": "#1877f2", // https://www.facebook.com/brand/resources/facebookapp/logo
"gmail": "#ea4335",
"gmx": "#1c449b",
"google": "#4285F4",
"google home": "#fbbc05",
// "home assistant": "#41bdf5", // ha-blue is #41bdf5, but that's the background
"instagram": "#dd2a7b",
"lieferando": "#ee5c00",
"instagram": "#ff0069", // https://about.instagram.com/brand/gradient
"lieferando": "#ff8000",
"linkedin": "#0a66c2", // https://brand.linkedin.com/
"messenger": "#0078ff",
"mastodon": "#563acc", // https://www.joinmastodon.org/branding
"mattermost": "#00f",
"n26": "#36a18b",
"nextbike": "#00f",
"newpipe": "#f00",
"nina": "#e57004",
"opentasks": "#409f8f",
"outlook mail": "#0072c6",
"outlook mail": "#0078d4", // https://developer.microsoft.com/en-us/fluentui#/styles/web/colors/products
"paypal": "#003087",
"pocket": "#ef4154f", // https://blog.getpocket.com/press/
"post & dhl": "#f2c101",
"signal": "#00f",
"skype": "#00aff0",
"reddit": "#ff4500", // https://www.redditinc.com/brand
"signal": "#3a76f0", // https://github.com/signalapp/Signal-Desktop/blob/main/images/signal-logo.svg
"skype": "#0078d4", // https://developer.microsoft.com/en-us/fluentui#/styles/web/colors/products
"slack": "#e51670",
"snapchat": "#ff0",
"steam": "#171a21",
"teams": "#464eb8",
"teams": "#6264a7", // https://developer.microsoft.com/en-us/fluentui#/styles/web/colors/products
"telegram": "#0088cc",
"telegram foss": "#0088cc",
"to do": "#3999e5",
"twitch": "#6441A4",
"twitter": "#1da1f2",
"twitch": "#9146ff", // https://brand.twitch.tv/
"twitter": "#1d9bf0", // https://about.twitter.com/en/who-we-are/brand-toolkit
"vlc": "#ff8800",
"whatsapp": "#4fce5d",
"wordfeud": "#e7d3c7",
"youtube": "#f00",
"youtube": "#f00", // https://www.youtube.com/howyoutubeworks/resources/brand-resources/#logos-icons-and-colors
}[s]||options.default;
};

View File

@ -1 +1,2 @@
0.55: Moved messages library into standalone library
0.56: Fix handling of music messages

View File

@ -9,13 +9,14 @@ Assuming you are using GadgetBridge and "overlay notifications":
1. Gadgetbridge sends an event to your watch for an incoming message
2. The `android` app parses the message, and calls `require("messages").pushMessage({/** the message */})`
3. `require("messages")` (provided by `messagelib`) calls `Bangle.emit("message", "text", {/** the message */})`
3. `require("messages")` calls `Bangle.emit("message", "text", {/** the message */})`
4. Overlay Notifications shows the message in an overlay, and marks it as `handled`
5. The default GUI app (`messages`) sees the event is marked as `handled`, so does nothing.
5. The default UI app (Message UI, `messagegui`) sees the event is marked as `handled`, so does nothing.
6. The default widget (`widmessages`) does nothing with `handled`, and shows a notification icon.
7. You tap the notification, in order to open the full GUI Overlay Notifications
7. You tap the notification, in order to open the full GUI: Overlay Notifications
calls `require("messages").openGUI({/** the message */})`
8. The default GUI app (`messages`) sees the "messageGUI" event, and launches itself
8. `openGUI` calls `require("messagegui").open(/** copy of the message */)`.
9. The `messagegui` library loads the Message UI app.
@ -28,7 +29,7 @@ it like this:
myMessageListener = Bangle.on("message", (type, message)=>{
if (message.handled) return; // another app already handled this message
// <type> is one of "text", "call", "alarm", "map", or "music"
// see `messagelib/lib.js` for possible <message> formats
// see `messages/lib.js` for possible <message> formats
// message.t could be "add", "modify" or "remove"
E.showMessage(`${message.title}\n${message.body}`, `${message.t} ${type} message`);
// You can prevent the default `message` app from loading by setting `message.handled = true`:
@ -52,7 +53,7 @@ Bangle.on("messageGUI", message=>{
## Requests
Please file any issues on https://github.com/espruino/BangleApps/issues/new?title=messagelib%library
Please file any issues on https://github.com/espruino/BangleApps/issues/new?title=[messages]%20library
## Creator

View File

@ -6,7 +6,6 @@ exports.music = {};
function emit(msg) {
let type = "text";
if (["call", "music", "map"].includes(msg.id)) type = msg.id;
if (type==="music" && msg.t!=="remove" && (!("state" in msg) || (!("track" in msg)))) return; // wait for complete music info
if (msg.src && msg.src.toLowerCase().startsWith("alarm")) type = "alarm";
Bangle.emit("message", type, msg);
}

View File

@ -1,7 +1,7 @@
{
"id": "messages",
"name": "Messages",
"version": "0.55",
"version": "0.56",
"description": "Library to handle, load and store message events received from Android/iOS",
"icon": "app.png",
"type": "module",

View File

@ -1,3 +1,5 @@
0.01: New App!
0.02: Remove one line of code that didn't do anything other than in some instances hinder the function of the app.
0.03: Use the new messages library
0.03: Use the new messages library
0.04: Fix dependency on messages library
Fix loading message UI

View File

@ -1 +1 @@
require('messages').openGUI({"t":"add","artist":" ","album":" ","track":" ","dur":0,"c":-1,"n":-1,"id":"music","title":"Music","state":"play","new":true});
setTimeout(()=>require('messages').openGUI({"t":"add","artist":" ","album":" ","track":" ","dur":0,"c":-1,"n":-1,"id":"music","title":"Music","state":"play","new":true}));

View File

@ -1,7 +1,7 @@
{
"id": "messagesmusic",
"name":"Messages Music",
"version":"0.03",
"version":"0.04",
"description": "Uses Messages library to push a music message which in turn displays Messages app music controls",
"icon":"app.png",
"type": "app",
@ -13,6 +13,5 @@
{"name":"messagesmusic.app.js","url":"app.js"},
{"name":"messagesmusic.img","url":"app-icon.js","evaluate":true}
],
"dependencies" : { "messagelib":"module" }
"dependencies":{"messages":"module"}
}

View File

@ -18,6 +18,11 @@
var notesElement = document.getElementById("notes");
var notes = {};
function disableFormInput() {
document.querySelectorAll(".form-input").forEach(el => el.disabled = true);
document.querySelectorAll(".btn").forEach(el => el.disabled = true);
}
function getData() {
// show loading window
Util.showModal("Loading...");
@ -53,8 +58,10 @@ function getData() {
buttonSave.classList.add('btn-default');
buttonSave.onclick = function() {
notes[i].note = textarea.value;
Util.writeStorage("noteify.json", JSON.stringify(notes));
location.reload();
disableFormInput();
Util.writeStorage("noteify.json", JSON.stringify(notes), () => {
location.reload(); // reload so we see current data
});
}
divColumn2.appendChild(buttonSave);
@ -64,8 +71,10 @@ function getData() {
buttonDelete.onclick = function() {
notes[i].note = textarea.value;
notes.splice(i, 1);
Util.writeStorage("noteify.json", JSON.stringify(notes));
location.reload(); // reload so we see current data
disableFormInput();
Util.writeStorage("noteify.json", JSON.stringify(notes), () => {
location.reload(); // reload so we see current data
});
}
divColumn2.appendChild(buttonDelete);
divColumn.appendChild(divColumn2);
@ -77,8 +86,10 @@ function getData() {
document.getElementById("btnAdd").addEventListener("click", function() {
const note = document.getElementById("note-new").value;
notes.push({"note": note});
Util.writeStorage("noteify.json", JSON.stringify(notes));
location.reload(); // reload so we see current data
disableFormInput();
Util.writeStorage("noteify.json", JSON.stringify(notes), () => {
location.reload(); // reload so we see current data
});
});
});
}

View File

@ -48,16 +48,18 @@
<div style="display:inline-block;text-align:center;vertical-align: top;" id="3bitdiv"> <input type="checkbox" id="3bit"></input><br/><span>3 bit</span></div>
<div class="form-group" style="display:inline-block;">
<select class="form-select" id="mapSize">
<option value="4">Small</option>
<option value="5" selected>Medium</option>
<option value="6">Large</option>
<option value="7">XL</option>
<option value="4">Small (4x4)</option>
<option value="5" selected>Medium (5x5)</option>
<option value="7">Large (7x7)</option>
<option value="10">XL (10x10)</option>
<option value="15">XXL (15x15)</option>
</select>
</div>
<button id="getmap" class="btn btn-primary">Get Map</button><button class="btn" onclick="showLoadedMaps()">Map List</button><br/>
<canvas id="maptiles" style="display:none"></canvas>
<div id="uploadbuttons" style="display:none"><button id="upload" class="btn btn-primary">Upload</button>
<button id="cancel" class="btn">Cancel</button></div>
<button id="cancel" class="btn">Cancel</button>
<span id="mapstats"></span></div>
</div>
</div>
@ -174,12 +176,14 @@ TODO:
</div>
</div>
`;
let map = L.map(`tile-map-${mapNumber}`);
L.tileLayer(PREVIEWTILELAYER, {
maxZoom: 18
}).addTo(map);
let marker = new L.marker(latlon).addTo(map);
map.fitBounds(latlon.toBounds(2000/*meters*/), {animation: false});
setTimeout(function() {
let map = L.map(`tile-map-${mapNumber}`);
L.tileLayer(PREVIEWTILELAYER, {
maxZoom: 18
}).addTo(map);
let marker = new L.marker(latlon).addTo(map);
map.fitBounds(latlon.toBounds(2000/*meters*/), {animation: false});
}, 100);
}
resolve();
});
@ -312,11 +316,13 @@ TODO:
var zoom = map.getZoom();
var centerlatlon = map.getBounds().getCenter();
var center = map.project(centerlatlon, zoom).divideBy(OSMTILESIZE);
// Reason for 16px adjustment below not 100% known, but it seems to
// align everything perfectly: https://github.com/espruino/BangleApps/issues/984
var ox = Math.round((center.x - Math.floor(center.x)) * OSMTILESIZE) + 16;
var oy = Math.round((center.y - Math.floor(center.y)) * OSMTILESIZE) + 16;
var center = map.project(centerlatlon, zoom).divideBy(OSMTILESIZE); // the center of our map
// ox/oy = offset in pixels
var ox = Math.round((center.x - Math.floor(center.x)) * OSMTILESIZE);
var oy = Math.round((center.y - Math.floor(center.y)) * OSMTILESIZE);
// adjust offset because we want to center our map
ox -= MAPTILES * TILESIZE / 2;
oy -= MAPTILES * TILESIZE / 2;
center = center.floor(); // make sure we're in the middle of a tile
// JS version of Bangle.js's projection
function bproject(lat, lon) {
@ -353,10 +359,12 @@ TODO:
var ctx = canvas.getContext('2d');
canvas.width = MAPSIZE;
canvas.height = MAPSIZE;
for (var i = 0; i < OSMTILECOUNT; i++) {
for (var j = 0; j < OSMTILECOUNT; j++) {
var tileMin = Math.round(-OSMTILECOUNT/2);
var tileMax = Math.round(OSMTILECOUNT/2);
for (var i = tileMin; i <= tileMax; i++) {
for (var j = tileMin; j <= tileMax; j++) {
(function(i,j){
var coords = new L.Point(center.x+i-1, center.y+j-1);
var coords = new L.Point(center.x+i, center.y+j);
coords.z = zoom;
var img = new Image();
img.crossOrigin = "Anonymous";
@ -368,6 +376,8 @@ TODO:
ctx.fillRect(testPt.x-1, testPt.y-5, 3,10);
ctx.fillRect(testPt.x-5, testPt.y-1, 10,3);
}*/
/*ctx.fillStyle="black";
ctx.fillRect(i*OSMTILESIZE - ox, j*OSMTILESIZE - oy, 6,6);*/
resolve();
};
}));
@ -395,6 +405,8 @@ TODO:
h : Math.round(canvas.height / TILESIZE), // height in tiles
fn : mapImageFile
})});
var mapSizeInK = Math.round(mapFiles.reduce((r,m)=>m.content.length+r,0)/1000);
document.getElementById("mapstats").innerText = "Size : "+ (mapSizeInK+"kb");
console.log(mapFiles);
});
});

View File

@ -1 +1,2 @@
0.01: New App!
0.02: Fix fast loading on swipe to clock

View File

@ -1,120 +1,129 @@
{
require("Font8x12").add(Graphics);
// load pinned apps from config
var settings = require("Storage").readJSON("qcenter.json", 1) || {};
var pinnedApps = settings.pinnedApps || [];
var exitGesture = settings.exitGesture || "swipeup";
let settings = require("Storage").readJSON("qcenter.json", 1) || {};
let pinnedApps = settings.pinnedApps || [];
let exitGesture = settings.exitGesture || "swipeup";
// if empty load a default set of apps as an example
if (pinnedApps.length == 0) {
pinnedApps = [
{ src: "setting.app.js", icon: "setting.img" },
{ src: "about.app.js", icon: "about.img" },
];
pinnedApps = [
{ src: "setting.app.js", icon: "setting.img" },
{ src: "about.app.js", icon: "about.img" },
];
}
// button drawing from Layout.js, edited to have completely custom button size with icon
function drawButton(l) {
var x = l.x + (0 | l.pad),
y = l.y + (0 | l.pad),
w = l.w - (l.pad << 1),
h = l.h - (l.pad << 1);
var poly = [
x,
y + 4,
x + 4,
y,
x + w - 5,
y,
x + w - 1,
y + 4,
x + w - 1,
y + h - 5,
x + w - 5,
y + h - 1,
x + 4,
y + h - 1,
x,
y + h - 5,
x,
y + 4,
],
bg = l.selected ? g.theme.bgH : g.theme.bg2;
g.setColor(bg)
.fillPoly(poly)
.setColor(l.selected ? g.theme.fgH : g.theme.fg2)
.drawPoly(poly);
if (l.src)
g.setBgColor(bg).drawImage(
"function" == typeof l.src ? l.src() : l.src,
l.x + l.w / 2,
l.y + l.h / 2,
{ scale: l.scale || undefined, rotate: Math.PI * 0.5 * (l.r || 0) }
);
let drawButton = function(l) {
let x = l.x + (0 | l.pad),
y = l.y + (0 | l.pad),
w = l.w - (l.pad << 1),
h = l.h - (l.pad << 1);
let poly = [
x,
y + 4,
x + 4,
y,
x + w - 5,
y,
x + w - 1,
y + 4,
x + w - 1,
y + h - 5,
x + w - 5,
y + h - 1,
x + 4,
y + h - 1,
x,
y + h - 5,
x,
y + 4,
],
bg = l.selected ? g.theme.bgH : g.theme.bg2;
g.setColor(bg)
.fillPoly(poly)
.setColor(l.selected ? g.theme.fgH : g.theme.fg2)
.drawPoly(poly);
if (l.src)
g.setBgColor(bg).drawImage(
"function" == typeof l.src ? l.src() : l.src,
l.x + l.w / 2,
l.y + l.h / 2,
{ scale: l.scale || undefined, rotate: Math.PI * 0.5 * (l.r || 0) }
);
}
// function to split array into group of 3, for button placement
function groupBy3(data) {
var result = [];
for (var i = 0; i < data.length; i += 3) result.push(data.slice(i, i + 3));
return result;
let groupBy3 = function(data) {
let result = [];
for (let i = 0; i < data.length; i += 3) result.push(data.slice(i, i + 3));
return result;
}
// generate object with buttons for apps by group of 3
var appButtons = groupBy3(pinnedApps).map((appGroup, i) => {
return appGroup.map((app, j) => {
return {
type: "custom",
render: drawButton,
width: 50,
height: 50,
pad: 5,
src: require("Storage").read(app.icon),
scale: 0.75,
cb: (l) => Bangle.load(app.src),
};
});
let appButtons = groupBy3(pinnedApps).map((appGroup, i) => {
return appGroup.map((app, j) => {
return {
type: "custom",
render: drawButton,
width: 50,
height: 50,
pad: 5,
src: require("Storage").read(app.icon),
scale: 0.75,
cb: (l) => load(app.src),
};
});
});
// create basic layout content with status info and sensor status on top
var layoutContent = [
{
type: "h",
pad: 5,
fillx: 1,
c: [
{ type: "txt", font: "8x12", pad: 3, scale: 2, label: E.getBattery() + "%" },
{ type: "txt", font: "8x12", pad: 3, scale: 2, label: "GPS: " + (Bangle.isGPSOn() ? "ON" : "OFF") },
],
},
let layoutContent = [
{
type: "h",
pad: 5,
fillx: 1,
c: [
{ type: "txt", font: "8x12", pad: 3, scale: 2, label: E.getBattery() + "%" },
{ type: "txt", font: "8x12", pad: 3, scale: 2, label: "GPS: " + (Bangle.isGPSOn() ? "ON" : "OFF") },
],
},
];
// create rows for buttons and add them to layoutContent
appButtons.forEach((appGroup) => {
layoutContent.push({
type: "h",
pad: 2,
c: appGroup,
});
layoutContent.push({
type: "h",
pad: 2,
c: appGroup,
});
});
// create layout with content
Bangle.loadWidgets();
var Layout = require("Layout");
var layout = new Layout({
type: "v",
c: layoutContent,
let Layout = require("Layout");
let layout = new Layout({
type: "v",
c: layoutContent
}, {
remove: ()=>{
Bangle.removeListener("swipe", onSwipe);
delete Graphics.prototype.setFont8x12;
}
});
g.clear();
layout.render();
Bangle.drawWidgets();
// swipe event listener for exit gesture
Bangle.on("swipe", function (lr, ud) {
if(exitGesture == "swipeup" && ud == -1) Bangle.showClock();
if(exitGesture == "swipedown" && ud == 1) Bangle.showClock();
if(exitGesture == "swipeleft" && lr == -1) Bangle.showClock();
if(exitGesture == "swiperight" && lr == 1) Bangle.showClock();
});
let onSwipe = function (lr, ud) {
if(exitGesture == "swipeup" && ud == -1) Bangle.showClock();
if(exitGesture == "swipedown" && ud == 1) Bangle.showClock();
if(exitGesture == "swipeleft" && lr == -1) Bangle.showClock();
if(exitGesture == "swiperight" && lr == 1) Bangle.showClock();
}
Bangle.on("swipe", onSwipe);
}

View File

@ -1,17 +1,18 @@
{
"id": "qcenter",
"name": "Quick Center",
"shortName": "QCenter",
"version": "0.01",
"description": "An app for quickly launching your favourite apps, inspired by the control centres of other watches.",
"icon": "app.png",
"tags": "",
"supports": ["BANGLEJS2"],
"readme": "README.md",
"screenshots": [{ "url": "screenshot.png" }],
"storage": [
{ "name": "qcenter.app.js", "url": "app.js" },
{ "name": "qcenter.settings.js", "url": "settings.js" },
{ "name": "qcenter.img", "url": "app-icon.js", "evaluate": true }
]
"id": "qcenter",
"name": "Quick Center",
"shortName": "QCenter",
"version": "0.02",
"description": "An app for quickly launching your favourite apps, inspired by the control centres of other watches.",
"icon": "app.png",
"tags": "",
"supports": ["BANGLEJS2"],
"readme": "README.md",
"screenshots": [{ "url": "screenshot.png" }],
"storage": [
{ "name": "qcenter.app.js", "url": "app.js" },
{ "name": "qcenter.settings.js", "url": "settings.js" },
{ "name": "qcenter.img", "url": "app-icon.js", "evaluate": true }
],
"data": [{"name":"qcenter.json"}]
}

View File

@ -1,141 +1,133 @@
// make sure to enclose the function in parentheses
(function (back) {
let settings = require("Storage").readJSON("qcenter.json", 1) || {};
var apps = require("Storage")
.list(/\.info$/)
.map((app) => {
var a = require("Storage").readJSON(app, 1);
return (
a && {
name: a.name,
type: a.type,
sortorder: a.sortorder,
src: a.src,
icon: a.icon,
}
);
})
.filter(
(app) =>
app &&
(app.type == "app" ||
app.type == "launch" ||
app.type == "clock" ||
!app.type)
);
apps.sort((a, b) => {
var n = (0 | a.sortorder) - (0 | b.sortorder);
if (n) return n; // do sortorder first
if (a.name < b.name) return -1;
if (a.name > b.name) return 1;
return 0;
});
let settings = require("Storage").readJSON("qcenter.json", 1) || {};
var apps = require("Storage")
.list(/\.info$/)
.map((app) => {
var a = require("Storage").readJSON(app, 1);
return (
a && {
name: a.name,
type: a.type,
sortorder: a.sortorder,
src: a.src,
icon: a.icon,
}
);
})
.filter(
(app) =>
app &&
(app.type == "app" ||
app.type == "launch" ||
app.type == "clock" ||
!app.type)
);
apps.sort((a, b) => {
var n = (0 | a.sortorder) - (0 | b.sortorder);
if (n) return n; // do sortorder first
if (a.name < b.name) return -1;
if (a.name > b.name) return 1;
return 0;
});
function save(key, value) {
settings[key] = value;
require("Storage").write("qcenter.json", settings);
}
function save(key, value) {
settings[key] = value;
require("Storage").write("qcenter.json", settings);
}
var pinnedApps = settings.pinnedApps || [];
var exitGesture = settings.exitGesture || "swipeup";
var pinnedApps = settings.pinnedApps || [];
var exitGesture = settings.exitGesture || "swipeup";
function showMainMenu() {
var mainmenu = {
"": { title: "Quick Center" },
"< Back": () => {
load();
},
};
function showMainMenu() {
var mainmenu = {
"": { title: "Quick Center", back: back},
};
// Set exit gesture
mainmenu["Exit Gesture: " + exitGesture] = function () {
E.showMenu(exitGestureMenu);
};
// Set exit gesture
mainmenu["Exit Gesture: " + exitGesture] = function () {
E.showMenu(exitGestureMenu);
};
//List all pinned apps, redirecting to menu with options to unpin and reorder
pinnedApps.forEach((app, i) => {
mainmenu[app.name] = function () {
E.showMenu({
"": { title: app.name },
"< Back": () => {
showMainMenu();
},
"Unpin": () => {
pinnedApps.splice(i, 1);
save("pinnedApps", pinnedApps);
showMainMenu();
},
"Move Up": () => {
if (i > 0) {
pinnedApps.splice(i - 1, 0, pinnedApps.splice(i, 1)[0]);
save("pinnedApps", pinnedApps);
showMainMenu();
}
},
"Move Down": () => {
if (i < pinnedApps.length - 1) {
pinnedApps.splice(i + 1, 0, pinnedApps.splice(i, 1)[0]);
save("pinnedApps", pinnedApps);
showMainMenu();
}
},
});
};
});
//List all pinned apps, redirecting to menu with options to unpin and reorder
pinnedApps.forEach((app, i) => {
mainmenu[app.name] = function () {
E.showMenu({
"": { title: app.name, back: showMainMenu },
"Unpin": () => {
pinnedApps.splice(i, 1);
save("pinnedApps", pinnedApps);
showMainMenu();
},
"Move Up": () => {
if (i > 0) {
pinnedApps.splice(i - 1, 0, pinnedApps.splice(i, 1)[0]);
save("pinnedApps", pinnedApps);
showMainMenu();
}
},
"Move Down": () => {
if (i < pinnedApps.length - 1) {
pinnedApps.splice(i + 1, 0, pinnedApps.splice(i, 1)[0]);
save("pinnedApps", pinnedApps);
showMainMenu();
}
},
});
};
});
// Show pin app menu, or show alert if max amount of apps are pinned
mainmenu["Pin App"] = function () {
if (pinnedApps.length < 6) {
E.showMenu(pinAppMenu);
} else {
E.showAlert("Max apps pinned").then(showMainMenu);
}
};
// Show pin app menu, or show alert if max amount of apps are pinned
mainmenu["Pin App"] = function () {
if (pinnedApps.length < 6) {
E.showMenu(pinAppMenu);
} else {
E.showAlert("Max apps pinned").then(showMainMenu);
}
};
return E.showMenu(mainmenu);
}
return E.showMenu(mainmenu);
}
// menu for adding apps to the quick launch menu, listing all apps
var pinAppMenu = {
"": { title: "Add App" },
"< Back": showMainMenu,
};
apps.forEach((a) => {
pinAppMenu[a.name] = function () {
// strip unncecessary properties
delete a.type;
delete a.sortorder;
pinnedApps.push(a);
save("pinnedApps", pinnedApps);
showMainMenu();
};
});
// menu for adding apps to the quick launch menu, listing all apps
var pinAppMenu = {
"": { title: "Add App", back: showMainMenu }
};
apps.forEach((a) => {
pinAppMenu[a.name] = function () {
// strip unncecessary properties
delete a.type;
delete a.sortorder;
pinnedApps.push(a);
save("pinnedApps", pinnedApps);
showMainMenu();
};
});
// menu for setting exit gesture
var exitGestureMenu = {
"": { title: "Exit Gesture" },
"< Back": showMainMenu,
};
exitGestureMenu["Swipe Up"] = function () {
exitGesture = "swipeup";
save("exitGesture", "swipeup");
showMainMenu();
};
exitGestureMenu["Swipe Down"] = function () {
exitGesture = "swipedown";
save("exitGesture", "swipedown");
showMainMenu();
};
exitGestureMenu["Swipe Left"] = function () {
exitGesture = "swipeleft";
save("exitGesture", "swipeleft");
showMainMenu();
};
exitGestureMenu["Swipe Right"] = function () {
exitGesture = "swiperight";
save("exitGesture", "swiperight");
showMainMenu();
};
// menu for setting exit gesture
var exitGestureMenu = {
"": { title: "Exit Gesture", back: showMainMenu }
};
exitGestureMenu["Swipe Up"] = function () {
exitGesture = "swipeup";
save("exitGesture", "swipeup");
showMainMenu();
};
exitGestureMenu["Swipe Down"] = function () {
exitGesture = "swipedown";
save("exitGesture", "swipedown");
showMainMenu();
};
exitGestureMenu["Swipe Left"] = function () {
exitGesture = "swipeleft";
save("exitGesture", "swipeleft");
showMainMenu();
};
exitGestureMenu["Swipe Right"] = function () {
exitGesture = "swiperight";
save("exitGesture", "swiperight");
showMainMenu();
};
showMainMenu();
showMainMenu();
});

View File

@ -15,3 +15,4 @@
Fix wrong fallback for buzz pattern
0.13: Ask to delete a timer after stopping it
0.14: Added clkinfo for alarms and timers
0.15: Automatic translation of some string in clkinfo

View File

@ -31,7 +31,7 @@
function getAlarmText(a){
if(a.timer) {
if(!a.on) return "off";
if(!a.on) return /*LANG*/"off";
let time = Math.round(alarm.getTimeToAlarm(a)/(60*1000));
if(time > 60)
time = Math.round(time / 60) + "h";
@ -52,7 +52,7 @@
var img = iconAlarmOn;
//get only alarms not created by other apps
var alarmItems = {
name: "Alarms",
name: /*LANG*/"Alarms",
img: img,
dynamic: true,
items: alarm.getAlarms().filter(a=>!a.appid)

View File

@ -1,7 +1,7 @@
{
"id": "sched",
"name": "Scheduler",
"version": "0.14",
"version": "0.15",
"description": "Scheduling library for alarms and timers",
"icon": "app.png",
"type": "scheduler",

View File

@ -59,3 +59,4 @@
0.52: Add option for left-handed users
0.53: Ensure that when clock is set, clockHasWidgets is set correctly too
0.54: If setting.json is corrupt, ensure it gets re-written
0.55: More strings tagged for automatic translation.

View File

@ -1,7 +1,7 @@
{
"id": "setting",
"name": "Settings",
"version": "0.54",
"version": "0.55",
"description": "A menu for setting up Bangle.js",
"icon": "settings.png",
"tags": "tool,system",

View File

@ -146,7 +146,7 @@ function showAlertsMenu() {
},
/*LANG*/"Quiet Mode": {
value: settings.quiet|0,
format: v => ["Off", "Alarms", "Silent"][v%3],
format: v => [/*LANG*/"Off", /*LANG*/"Alarms", /*LANG*/"Silent"][v%3],
onchange: v => {
settings.quiet = v%3;
updateSettings();
@ -162,9 +162,9 @@ function showAlertsMenu() {
function showBLEMenu() {
var hidV = [false, "kbmedia", "kb", "com", "joy"];
var hidN = ["Off", "Kbrd & Media", "Kbrd", "Kbrd & Mouse" ,"Joystick"];
var hidN = [/*LANG*/"Off", /*LANG*/"Kbrd & Media", /*LANG*/"Kbrd", /*LANG*/"Kbrd & Mouse", /*LANG*/"Joystick"];
E.showMenu({
'': { 'title': 'Bluetooth' },
'': { 'title': /*LANG*/'Bluetooth' },
'< Back': ()=>showMainMenu(),
/*LANG*/'Make Connectable': ()=>makeConnectable(),
/*LANG*/'BLE': {
@ -193,11 +193,11 @@ function showBLEMenu() {
}
},
/*LANG*/'Passkey BETA': {
value: settings.passkey?settings.passkey:"none",
value: settings.passkey?settings.passkey:/*LANG*/"none",
onchange: () => setTimeout(showPasskeyMenu) // graphical_menu redraws after the call
},
/*LANG*/'Whitelist': {
value: settings.whitelist?(settings.whitelist.length+" devs"):"off",
value: settings.whitelist?(settings.whitelist.length+/*LANG*/" devs"):/*LANG*/"off",
onchange: () => setTimeout(showWhitelistMenu) // graphical_menu redraws after the call
}
});
@ -606,7 +606,7 @@ function showUtilMenu() {
menu[/*LANG*/'Reset Settings'] = () => {
E.showPrompt(/*LANG*/'Reset to Defaults?',{title:/*LANG*/"Settings"}).then((v) => {
if (v) {
E.showMessage('Resetting');
E.showMessage(/*LANG*/'Resetting');
resetSettings();
setTimeout(showMainMenu, 50);
} else showUtilMenu();
@ -824,6 +824,7 @@ function showAppSettings(app) {
function showTouchscreenCalibration() {
Bangle.setUI();
require('widget_utils').hide();
// disable touchscreen calibration (passed coords right through)
Bangle.setOptions({touchX1: 0, touchY1: 0, touchX2: g.getWidth(), touchY2: g.getHeight() });
@ -847,7 +848,7 @@ function showTouchscreenCalibration() {
g.drawLine(spot[0],spot[1]-32,spot[0],spot[1]+32);
g.drawCircle(spot[0],spot[1], 16);
var tapsLeft = (1-currentTry)*4+(4-currentCorner);
g.setFont("6x8:2").setFontAlign(0,0).drawString(tapsLeft+" taps\nto go", g.getWidth()/2, g.getHeight()/2);
g.setFont("6x8:2").setFontAlign(0,0).drawString(tapsLeft+/*LANG*/" taps\nto go", g.getWidth()/2, g.getHeight()/2);
}
function calcCalibration() {
@ -870,7 +871,7 @@ function showTouchscreenCalibration() {
var s = storage.readJSON("setting.json",1)||{};
s.touch = calib;
storage.writeJSON("setting.json",s);
g.setFont("6x8:2").setFontAlign(0,0).drawString("Calibrated!", g.getWidth()/2, g.getHeight()/2);
g.setFont("6x8:2").setFontAlign(0,0).drawString(/*LANG*/"Calibrated!", g.getWidth()/2, g.getHeight()/2);
// now load the main menu again
setTimeout(showLCDMenu, 500);
}

View File

@ -7,3 +7,4 @@
0.10: Complete rework off this app!
0.10beta: Add interface.html to view saved log data, add "View log" function for debugging log, send data for gadgetbridge, change caching for global getStats
0.11: Prevent module not found error
0.12: Improve README, option to add functions triggered by status changes or time periods, remove old log (<0.10) conversion

View File

@ -1,11 +1,19 @@
# Sleep Log
This app logs and displays the following states:
This app logs and displays the following states:
- sleepling status: _unknown, not worn, awake, light sleep, deep sleep_
- consecutive sleep status: _unknown, not consecutive, consecutive_
It is using the built in movement calculation to decide your sleeping state. While charging it is assumed that you are not wearing the watch and if the status changes to _deep sleep_ the internal heartrate sensor is used to detect if you are wearing the watch.
#### Explanations
* __Detection of Sleep__
The movement value of bangle's build in health event that is triggered every 10 minutes is checked against the thresholds for light and deep sleep. If the measured movement is lower or equal to the __Deep Sleep__-threshold a deep sleep phase is detected for the last 10 minutes. If the threshold is exceeded but not the __Light Sleep__-threshold than the last timeperiod is detected as light sleep phase. On exceeding even this threshold it is assumed that you were awake.
* __True Sleep__
The true sleep value is a simple addition of all registered sleeping periods.
* __Consecutive Sleep__
In addition the consecutive sleep value tries to predict the complete time you were asleep, even the very light sleeping periods when an awake period is detected based on the registered movements. All periods after a sleeping period will be summarized until the first following non sleeping period that is longer then the maximal awake duration (__Max Awake__). If this sum is lower than the minimal consecutive sleep duration (__Min Consecutive__) it is not considered, otherwise it will be added to the consecutive sleep value.
Logfiles are not removed on un-/reinstall to prevent data loss.
| Filename (* _example_) | Content | Removeable in |
@ -16,59 +24,73 @@ Logfiles are not removed on un-/reinstall to prevent data loss.
---
### App Usage
### Main App Usage
---
#### On the main app screen:
- __swipe left & right__
#### Controls:
- __swipe left & right__
to change the displayed day
- __touch the "title"__ (e.g. `Night to Fri 20/05/2022`)
- __touch the "title"__ (e.g. `Night to Fri 20/05/2022`)
to enter day selection prompt
- __touch the info area__
to change the displayed information
- __touch the info area__
to change the displayed information
(by default: consecutive & true sleeping)
- __touch the wrench__ (upper right corner)
- __touch the wrench__ (upper right corner)
to enter the settings
- __use back button widget__ (upper left corner)
- __use back button widget__ (upper left corner)
exit the app
#### Inside the settings:
- __Thresholds__ submenu
#### View:
| Status | Color | Height |
|-------------|:------:|----------:|
| unknown | black | 0% |
| not worn | red | 40% |
| awake | green | 60% |
| light sleep | cyan | 80% |
| deep sleep | blue | 100% |
| consecutive | violet | as status |
---
### Settings Usage
---
- __Thresholds__ submenu
Changes take effect from now on, not retrospective!
- __Max Awake__ | maximal awake duration
- __Max Awake__ | maximal awake duration
_10min_ / _20min_ / ... / __60min__ / ... / _120min_
- __Min Consecutive__ | minimal consecutive sleep duration
- __Min Consecutive__ | minimal consecutive sleep duration
_10min_ / _20min_ / ... / __30min__ / ... / _120min_
- __Deep Sleep__ | deep sleep threshold
- __Deep Sleep__ | deep sleep threshold
_30_ / _31_ / ... / __100__ / ... / _200_
- __Light Sleep__ | light sleep threshold
_100_ / _110_ / ... / __200__ / ... / _400_
- __Light Sleep__ | light sleep threshold
_100_ / _110_ / ... / __200__ / ... / _400_
- __Reset to Default__ | reset to bold values above
- __BreakToD__ | time of day to break view
- __BreakToD__ | time of day to break view
_0:00_ / _1:00_ / ... / __12:00__ / ... / _23:00_
- __App Timeout__ | app specific lock timeout
- __App Timeout__ | app specific lock timeout
__0s__ / _10s_ / ... / _120s_
- __Enabled__ | completely en-/disables the background service
- __Enabled__ | completely en-/disables the background service
__on__ / _off_
- __Debugging__ submenu
- __View log__ | display logfile data
Select the logfile by its starting time.
Thresholds are shown as line with its value.
- __swipe left & right__
- __Debugging__ submenu
- __View log__ | display logfile data
Select the logfile by its starting time.
Thresholds are shown as line with its value.
- __swipe left & right__
to change displayed duration
- __swipe up & down__
- __swipe up & down__
to change displayed value range
- __touch the graph__
- __touch the graph__
to change between light & dark colors
- __use back button widget__ (upper left corner)
- __use back button widget__ (upper left corner)
to go back to the logfile selection
- __Enabled__ | en-/disables debugging
- __Enabled__ | en-/disables debugging
_on_ / __off__
- __write File__ | toggles if a logfile is written
- __write File__ | toggles if a logfile is written
_on_ / __off__
- __Duration__ | duration for writing into logfile
_1h_ / _2h_ / ... / __12h__ / _96_
- The following data is logged to a csv-file:
- __Duration__ | duration for writing into logfile
_1h_ / _2h_ / ... / __12h__ / _96_
- The following data is logged to a csv-file:
_timestamp_ (in days since 1900-01-01 00:00 UTC used by office software) _, movement, status, consecutive, asleepSince, awakeSince, bpm, bpmConfidence_
@ -78,48 +100,50 @@ Logfiles are not removed on un-/reinstall to prevent data loss.
Available through the App Loader when your watch is connected.
- __view data__
- __view data__
Display the data to each timestamp in a table.
- __save csv-file__
Download a csv-file with the data to each timestamp.
The time format is chooseable beneath the file list.
- __delete file__
- __save csv-file__
Download a csv-file with the data to each timestamp.
The time format is chooseable beneath the file list.
- __delete file__
Deletes the logfile from the watch. __Please backup your data first!__
---
### Timestamps and files
### Timestamps and Files
---
1. externally visible/usable timestamps (in `global.sleeplog`) are formatted as Bangle timestamps:
1. externally visible/usable timestamps (in `global.sleeplog`) are formatted as Bangle timestamps:
seconds since 1970-01-01 00:00 UTC
2. internally used and logged (to `sleeplog.log (StorageFile)`) is within the highest available resolution:
2. internally used and logged (to `sleeplog.log (StorageFile)`) is within the highest available resolution:
10 minutes since 1970-01-01 00:00 UTC (`Bangle / (10 * 60 * 1000)`)
3. debug .csv file ID (`sleeplog_123456.csv`) has a hourly resolution:
hours since 1970-01-01 00:00 UTC (`Bangle / (60 * 60 * 1000)`)
4. logged timestamps inside the debug .csv file are formatted for office calculation software:
4. logged timestamps inside the debug .csv file are formatted for office calculation software:
days since 1900-01-01 00:00 UTC (`Bangle / (24 * 60 * 60 * 1000) + 25569`)
5. every 14 days the `sleeplog.log (StorageFile)` is reduced and old entries are moved into separat files for each fortnight (`sleeplog_1234.log`) but still accessible though the app:
5. every 14 days the `sleeplog.log (StorageFile)` is reduced and old entries are moved into separat files for each fortnight (`sleeplog_1234.log`) but still accessible though the app:
fortnights since 1970-01-04 12:00 UTC (converted with `require("sleeplog").msToFn(Bangle)` and `require("sleeplog").fnToMs(fortnight)`)
- __Logfiles from before 0.10:__
- __Logfiles from before 0.10:__
timestamps and sleeping status of old logfiles are automatically converted on your first consecutive sleep or manually by `require("sleeplog").convertOldLog()`
- __View logged data:__
if you'd like to view your logged data in the IDE, you can access it with `require("sleeplog").printLog(since, until)` or `require("sleeplog").readLog(since, until)` to view the raw data
- __View logged data:__
if you'd like to view your logged data in the IDE, you can access it with `require("sleeplog").printLog(since, until)` or `require("sleeplog").readLog(since, until)` to view the raw data
since & until in Bangle timestamp, e.g. `require("sleeplog").printLog(Date()-24*60*60*1000, Date())` for the last 24h
---
### Access statistics (developer information)
### Developer Information
---
- Last Asleep Time [Date]:
#### Access statistics
- Last Asleep Time [Date]:
`Date(sleeplog.awakeSince)`
- Last Awake Duration [ms]:
- Last Awake Duration [ms]:
`Date() - sleeplog.awakeSince`
- Last Statistics [object]:
- Last Statistics [object]:
```
// get stats of the last night (period as displayed inside the app)
// as this might be the mostly used function the data is cached inside the global object
// as this might be the mostly used function the data is cached inside the global object
sleeplog.getStats();
// get stats of the last 24h
@ -130,25 +154,50 @@ Available through the App Loader when your watch is connected.
={ calculatedAt: 1653123553810, deepSleep: 250, lightSleep: 150, awakeSleep: 10,
consecSleep: 320, awakeTime: 1030, notWornTime: 0, unknownTime: 0, logDuration: 1440,
firstDate: 1653036600000, lastDate: 1653111600000 }
// to get the start of a period defined by "Break TOD" of any date
var startOfBreak = require("sleeplog").getLastBreak();
// same as
var startOfBreak = require("sleeplog").getLastBreak(Date.now());
// output as date
=Date: Sat May 21 2022 12:00:00 GMT+0200
// get stats of this period as displayed inside the app
require("sleeplog").getStats(require("sleeplog").getLastBreak(), 24*60*60*1000);
// or any other day
require("sleeplog").getStats(require("sleeplog").getLastBreak(Date(2022,4,10)), 24*60*60*1000);
```
- Total Statistics [object]:
- Total Statistics [object]:
```
// use with caution, may take a long time !
require("sleeplog").getStats(0, 0, require("sleeplog").readLog());
```
#### Add functions triggered by status changes or inside a specified time period
With the following code it is possible to add functions that will be called every 10 minutes after new movement data when meeting the specified parameters on each :
```
// first ensure that the sleeplog trigger object is available (sleeplog is enabled)
if (typeof (global.sleeplog || {}).trigger === "object") {
// then add your parameters with the function to call as object into the trigger object
sleeplog.trigger["my app name"] = {
onChange: false, // false as default, if true call fn only on a status change
from: 0, // 0 as default, in ms, first time fn will be called
to: 24*60*60*1000, // 24h as default, in ms, last time fn will be called
// reference time to from & to is rounded to full minutes
fn: function(data) { print(data); } // function to be executed
};
}
```
The passed data object has the following properties:
- timestamp: of the status change as date object,
(should be around 10min. before "now", the actual call of the function)
- status: value of the new status (0-4),
(0 = unknown, 1 = not worn, 2 = awake, 3 = light sleep, 4 = deep sleep)
- consecutive: value of the new status (0-2),
(0 = unknown, 1 = no consecutive sleep, 2 = consecutive sleep)
- prevStatus: if changed the value of the previous status (0-4) else undefined,
- prevConsecutive: if changed the value of the previous status (0-2) else undefined
---
### Worth Mentioning
@ -156,14 +205,14 @@ Available through the App Loader when your watch is connected.
#### To do list
- Check translations.
- Add more functionallities to interface.html.
- Enable recieving data on the Gadgetbridge side + testing.
- Enable receiving data on the Gadgetbridge side + testing.
__Help appreciated!__
#### Requests, Bugs and Feedback
Please leave requests and bug reports by raising an issue at [github.com/storm64/BangleApps](https://github.com/storm64/BangleApps) (or send me a [mail](mailto:banglejs@storm64.de)).
#### Creator
Storm64 ([Mail](mailto:banglejs@storm64.de), [github](https://github.com/storm64))
Storm64 ([mail](mailto:banglejs@storm64.de), [github](https://github.com/storm64))
#### Contributors
myxor ([github](https://github.com/myxor))

View File

@ -124,7 +124,7 @@ if (sleeplog.conf.enabled) {
if (!sleeplog.info.saveUpToDate || force) {
// save status, consecutive status and info timestamps to restore on reload
var save = [sleeplog.info.lastCheck, sleeplog.info.awakeSince, sleeplog.info.asleepSince];
// add debuging status if active
// add debuging status if active
if (sleeplog.debug) save.push(sleeplog.debug.writeUntil, sleeplog.debug.fileid);
// stringify entries
@ -253,11 +253,38 @@ if (sleeplog.conf.enabled) {
}
}
// check if the status has changed
var changed = data.status !== this.status || data.consecutive !== this.consecutive;
// read and check trigger entries
var triggers = Object.keys(this.trigger) || [];
if (triggers.length) {
// calculate time from timestamp in ms on full minutes
var time = new Date();
time = (time.getHours() * 60 + time.getMinutes()) * 60 * 1000;
// go through all triggers
triggers.forEach(key => {
// read entry to key
var entry = this.trigger[key];
// check if the event matches the entries requirements
if (typeof entry.fn === "function" && (changed || !entry.onChange) &&
(entry.from || 0) <= time && (entry.to || 24 * 60 * 60 * 1000) >= time)
// and call afterwards with status data
setTimeout(entry.fn, 100, {
timestamp: new Date(data.timestamp),
status: data.status,
consecutive: data.consecutive,
prevStatus: data.status === this.status ? undefined : this.status,
prevConsecutive: data.consecutive === this.consecutive ? undefined : this.consecutive
});
});
}
// cache change into a known consecutive state
var changeIntoConsec = data.consecutive;
// check if the status has changed
if (data.status !== this.status || data.consecutive !== this.consecutive) {
// actions on a status change
if (changed) {
// append status
this.appendStatus(data.timestamp, data.status, data.consecutive);
@ -268,7 +295,7 @@ if (sleeplog.conf.enabled) {
// reset saveUpToDate status
delete this.info.saveUpToDate;
}
// send status to gadgetbridge
var gb_kinds = "unknown,not_worn,activity,light_sleep,deep_sleep";
Bluetooth.println(JSON.stringify({
@ -319,7 +346,10 @@ if (sleeplog.conf.enabled) {
}
// return stats cache
return this.statsCache;
}
},
// define trigger object
trigger: {}
}, sleeplog);
// initial starting

View File

@ -149,14 +149,6 @@ exports = {
// define move log function, move StorageFile content into files seperated by fortnights
moveLog: function(force) {
/** convert old logfile (< v0.10) if present **/
if (require("Storage").list("sleeplog.log", {
sf: false
}).length) {
convertOldLog();
}
/** may be removed in later versions **/
// first day of this fortnight period
var thisFirstDay = this.fnToMs(this.msToFn(Date.now()));
@ -384,82 +376,5 @@ exports = {
"unknown,not worn,awake,light sleep,deep sleep".split(",")[entry[1]].padEnd(12) +
"for" + (duration + "min").padStart(8));
});
},
/** convert old (< v0.10) to new logfile data **/
convertOldLog: function() {
// read old logfile
var oldLog = require("Storage").read("sleeplog.log") || "";
// decode data if needed
if (!oldLog.startsWith("[")) oldLog = atob(oldLog);
// delete old logfile and return if it is empty or corrupted
if (!oldLog.startsWith("[[") || !oldLog.endsWith("]]")) {
require("Storage").erase("sleeplog.log");
return;
}
// transform into StorageFile and clear oldLog to have more free ram accessable
require("Storage").open("sleeplog_old.log", "w").write(JSON.parse(oldLog).reverse().join("\n"));
oldLog = undefined;
// calculate fortnight from now
var fnOfNow = this.msToFn(Date.now());
// open StorageFile with old log data
var file = require("Storage").open("sleeplog_old.log", "r");
// define active fortnight and file cache
var activeFn = true;
var fileCache = [];
// loop through StorageFile entries
while (activeFn) {
// define fortnight for this entry
var thisFn = false;
// cache new line
var line = file.readLine();
// check if line is filled
if (line) {
// parse line
line = line.substr(0, 15).split(",").map(e => parseInt(e));
// calculate fortnight for this entry
thisFn = this.msToFn(line[0]);
// convert timestamp into 10min steps
line[0] = line[0] / 6E5 | 0;
// set consecutive to unknown
line.push(0);
}
// check if active fortnight and file cache is set, fortnight has changed and
// active fortnight is not fortnight from now
if (activeFn && fileCache.length && activeFn !== thisFn && activeFn !== fnOfNow) {
// write file cache into new file according to fortnight
require("Storage").writeJSON("sleeplog_" + activeFn + ".log", fileCache);
// clear file cache
fileCache = [];
}
// add line to file cache if it is filled
if (line) fileCache.push(line);
// set active fortnight
activeFn = thisFn;
}
// check if entries are leftover
if (fileCache.length) {
// format fileCache entries into a string
fileCache = fileCache.map(e => e.join(",")).join("\n");
// read complete new log StorageFile as string
file = require("Storage").open("sleeplog.log", "r");
var newLogString = file.read(file.getLength());
// add entries at the beginning of the new log string
newLogString = fileCache + "\n" + newLogString;
// rewrite new log StorageFile
require("Storage").open("sleeplog.log", "w").write(newLogString);
}
// free ram
file = undefined;
fileCache = undefined;
// clean up old files
require("Storage").erase("sleeplog.log");
require("Storage").open("sleeplog_old.log", "w").erase();
}
/** may be removed in later versions **/
};

View File

@ -2,7 +2,7 @@
"id":"sleeplog",
"name":"Sleep Log",
"shortName": "SleepLog",
"version": "0.11",
"version": "0.12",
"description": "Log and view your sleeping habits. This app is using the built in movement calculation.",
"icon": "app.png",
"type": "app",

View File

@ -11,3 +11,4 @@
Add setting to defer start of algorithm
Add setting to disable scheduler alarm
0.10: Fix: Do not wake when falling asleep
0.11: Minor tweaks

View File

@ -21,8 +21,8 @@ let logs = [];
//
// Function needs to be called for every measurement but returns a value at maximum once a second (see winwidth)
// start of sleep marker is delayed by sleepthresh due to continous data reading
const winwidth=13;
const nomothresh=0.03; // 0.006 was working on Bangle1, but Bangle2 has higher noise.
const winwidth=13; // Actually 12.5 Hz, rounded
const nomothresh=0.023; // Original implementation: 6, resolution 11 bit, scale +-4G = 6/(2^(11-1))*4 = 0.023438 in G
const sleepthresh=600;
var ess_values = [];
var slsnds = 0;
@ -69,6 +69,9 @@ active.forEach(alarm => {
}
});
const LABEL_ETA = /*LANG*/"ETA";
const LABEL_WAKEUP_TIME = /*LANG*/"Alarm at";
var layout = new Layout({
type:"v", c: [
{type:"txt", font:"10%", label:"Sleep Phase Alarm", bgCol:g.theme.bgH, fillx: true, height:Bangle.appRect.h/6},
@ -84,7 +87,7 @@ function drawApp() {
var alarmMinute = nextAlarmDate.getMinutes();
if (alarmHour < 10) alarmHour = "0" + alarmHour;
if (alarmMinute < 10) alarmMinute = "0" + alarmMinute;
layout.alarm_date.label = "Alarm at " + alarmHour + ":" + alarmMinute;
layout.alarm_date.label = `${LABEL_WAKEUP_TIME}: ${alarmHour}:${alarmMinute}`;
layout.render();
function drawTime() {
@ -94,7 +97,7 @@ function drawApp() {
const diff = nextAlarmDate - now;
const diffHour = Math.floor((diff % 86400000) / 3600000).toString();
const diffMinutes = Math.floor(((diff % 86400000) % 3600000) / 60000).toString();
layout.eta.label = "ETA: -"+ diffHour + ":" + diffMinutes.padStart(2, '0');
layout.eta.label = `${LABEL_ETA}: ${diffHour}:${diffMinutes.padStart(2, '0')}`;
layout.render();
}
@ -139,7 +142,7 @@ if (nextAlarmDate !== undefined) {
// minimum alert 30 minutes early
minAlarm.setTime(nextAlarmDate.getTime() - (30*60*1000));
run = () => {
layout.state.label = "Start";
layout.state.label = /*LANG*/"Start";
layout.render();
Bangle.setOptions({powerSave: false}); // do not dynamically change accelerometer poll interval
Bangle.setPollInterval(80); // 12.5Hz
@ -150,7 +153,7 @@ if (nextAlarmDate !== undefined) {
if (swest !== undefined) {
if (Bangle.isLCDOn()) {
layout.state.label = swest ? "Sleep" : "Awake";
layout.state.label = swest ? /*LANG*/"Sleep" : /*LANG*/"Awake";
layout.render();
}
// log

View File

@ -2,7 +2,7 @@
"id": "sleepphasealarm",
"name": "SleepPhaseAlarm",
"shortName": "SleepPhaseAlarm",
"version": "0.10",
"version": "0.11",
"description": "Uses the accelerometer to estimate sleep and wake states with the principle of Estimation of Stationary Sleep-segments (ESS, see https://ubicomp.eti.uni-siegen.de/home/datasets/ichi14/index.html.en). This app will read the next alarm from the alarm application and will wake you up to 30 minutes early at the best guessed time when you are almost already awake.",
"icon": "app.png",
"tags": "alarm",

View File

@ -1,3 +1,4 @@
0.01: Initial creation of "Swipe back to the Clock" App. Let's you swipe from left to right on any app to return back to the clock face.
0.02: Fix deleting from white and black lists.
0.03: Adapt to availability of Bangle.showClock and Bangle.load
0.04: Fix 'Uncaught ReferenceError: "__FILE__" is not defined' error (fix #2326)

View File

@ -29,12 +29,13 @@
})(Bangle.load);
let swipeHandler = (dir) => {
log("swipe:" + dir + " on app: " + __FILE__);
let currentFile = global.__FILE__||"default";
log("swipe:" + dir + " on app: " + currentFile);
if (!inhibit && dir === 1 && !Bangle.CLOCK && __FILE__ != ".bootcde") {
log("on a not clock app " + __FILE__);
if ((settings.mode === 1 && settings.whiteList.includes(__FILE__)) || // "White List"
(settings.mode === 2 && !settings.blackList.includes(__FILE__)) || // "Black List"
if (!inhibit && dir === 1 && !Bangle.CLOCK) {
log("on a not clock app " + currentFile);
if ((settings.mode === 1 && settings.whiteList.includes(currentFile)) || // "White List"
(settings.mode === 2 && !settings.blackList.includes(currentFile)) || // "Black List"
settings.mode === 3) { // "Always"
log("load clock");
Bangle.showClock();

View File

@ -2,7 +2,7 @@
"id": "swp2clk",
"name": "Swipe back to the Clock",
"shortName": "Swipe to Clock",
"version": "0.03",
"version": "0.04",
"description": "Let's you swipe from left to right on any app to return back to the clock face. Please configure in the settings app after installing to activate, since its disabled by default.",
"icon": "app.png",
"type": "bootloader",

View File

@ -1 +1,2 @@
0.01: New App!
0.02: Use Bangle.showClock for changing to clock (Backport from launch)

View File

@ -69,16 +69,6 @@ let tagKeys = Object.keys(tags).filter(tag => tag !== "clock" || settings.showCl
if (!settings.fullscreen)
Bangle.loadWidgets();
let returnToClock = function() {
// unload everything manually
// ... or we could just call `load();` but it will be slower
Bangle.setUI(); // remove scroller's handling
if (lockTimeout) clearTimeout(lockTimeout);
Bangle.removeListener("lock", lockHandler);
// now load the default clock - just call .bootcde as this has the code already
setTimeout(eval,0,s.read(".bootcde"));
};
let showTagMenu = (tag) => {
E.showScroller({
h : 64*scaleval, c : appsByTag[tag].length,
@ -121,7 +111,12 @@ let showMainMenu = () => {
let tag = tagKeys[i];
showTagMenu(tag);
},
back : returnToClock // button press or tap in top left calls returnToClock now
back : Bangle.showClock, // button press or tap in top left shows clock now
remove : () => {
// cleanup the timeout to not leave anything behind after being removed from ram
if (lockTimeout) clearTimeout(lockTimeout);
Bangle.removeListener("lock", lockHandler);
}
});
};
showMainMenu();
@ -134,7 +129,7 @@ let lockHandler = function(locked) {
if (lockTimeout) clearTimeout(lockTimeout);
lockTimeout = undefined;
if (locked) {
lockTimeout = setTimeout(returnToClock, 10000);
lockTimeout = setTimeout(Bangle.showClock, 10000);
}
};
Bangle.on("lock", lockHandler);

View File

@ -2,7 +2,7 @@
"id": "taglaunch",
"name": "Tag Launcher",
"shortName": "Taglauncher",
"version": "0.01",
"version": "0.02",
"description": "Launcher that puts all applications into submenus based on their tag. With many applications installed this can result in a faster application selection than the linear access of the default launcher.",
"readme": "README.md",
"icon": "app.png",

View File

@ -5,7 +5,7 @@
"description": "Tetris",
"icon": "tetris.png",
"readme": "README.md",
"tags": "games",
"tags": "game",
"supports" : ["BANGLEJS2"],
"storage": [
{"name":"tetris.app.js","url":"tetris.app.js"},

View File

@ -8,3 +8,4 @@
0.08: Force background of widget field to the torch colour
0.09: Change code taking FW tweaks into account
0.10: Introduce fast switching.
0.11: Make compatible with Fastload Utils by loading and hiding widgets.

View File

@ -1,14 +1,16 @@
{
const SETTINGS_FILE = "torch.json";
let settings;
let s = require("Storage");
let wu = require("widget_utils");
let loadSettings = function() {
settings = require("Storage").readJSON(SETTINGS_FILE,1)|| {'bg': '#FFFFFF', 'color': 'White'};
settings = s.readJSON(SETTINGS_FILE,1)|| {'bg': '#FFFFFF', 'color': 'White'};
};
loadSettings();
let brightnessBackup = require("Storage").readJSON('setting.json').brightness;
let brightnessBackup = s.readJSON('setting.json').brightness;
let optionsBackup = Bangle.getOptions();
Bangle.setLCDBrightness(1);
Bangle.setLCDPower(1);
@ -18,6 +20,8 @@
g.setTheme({bg:settings.bg,fg:"#000"});
g.setColor(settings.bg);
g.fillRect(0,0,g.getWidth(),g.getHeight());
Bangle.loadWidgets();
wu.hide();
Bangle.setUI({
mode : 'custom',
back : Bangle.showClock, // B2: SW back button to exit
@ -26,6 +30,7 @@
Bangle.setLCDBrightness(brightnessBackup);
Bangle.setOptions(optionsBackup);
g.setTheme(themeBackup);
wu.show();
}
});
}

View File

@ -2,7 +2,7 @@
"id": "torch",
"name": "Torch",
"shortName": "Torch",
"version": "0.10",
"version": "0.11",
"description": "Turns screen white to help you see in the dark. Select from the launcher or press BTN1,BTN3,BTN1,BTN3 quickly to start when in any app that shows widgets on Bangle.js 1. You can also set the color through the app's setting menu.",
"icon": "app.png",
"tags": "tool,torch",

View File

@ -15,3 +15,5 @@
0.16: Don't mark app as clock
0.17: Added clkinfo for clocks.
0.18: Added hasRange to clkinfo.
0.19: Added weather condition to clkinfo.
0.20: Added weather condition with temperature to clkinfo.

View File

@ -3,6 +3,7 @@
temp: "?",
hum: "?",
wind: "?",
txt: "?",
};
var weatherJson = require("Storage").readJSON('weather.json');
@ -14,17 +15,41 @@
weather.wind = Math.round(weather.wind[1]) + "kph";
}
function weatherIcon(code) {
var ovr = Graphics.createArrayBuffer(24,24,1,{msb:true});
require("weather").drawIcon({code:code},12,12,12,ovr);
var img = ovr.asImage();
img.transparent = 0;
//for (var i=0;i<img.buffer.length;i++) img.buffer[i]^=255;
return img;
//g.setColor("#0ff").drawImage(img, 42, 42);
}
//FIXME ranges are somehow arbitrary
var weatherItems = {
name: "Weather",
img: atob("GBiBAf+///u5//n7//8f/9wHP8gDf/gB//AB/7AH/5AcP/AQH/DwD/uAD84AD/4AA/wAAfAAAfAAAfAAAfgAA/////+bP/+zf/+zfw=="),
items: [
{
name: "conditionWithTemperature",
get: () => ({ text: weather.temp, img: weatherIcon(weather.code),
v: parseInt(weather.temp), min: -30, max: 55}),
show: function() { this.emit("redraw"); },
hide: function () {}
},
{
name: "condition",
get: () => ({ text: weather.txt, img: weatherIcon(weather.code),
v: weather.code}),
show: function() { this.emit("redraw"); },
hide: function () {}
},
{
name: "temperature",
hasRange : true,
get: () => ({ text: weather.temp, img: atob("GBiBAAA8AAB+AADnAADDAADDAADDAADDAADDAADbAADbAADbAADbAADbAADbAAHbgAGZgAM8wAN+wAN+wAM8wAGZgAHDgAD/AAA8AA=="),
v: parseInt(weather.temp), min: -30, max: 55}),
show: function() { weatherItems.items[0].emit("redraw"); },
show: function() { this.emit("redraw"); },
hide: function () {}
},
{
@ -32,7 +57,7 @@
hasRange : true,
get: () => ({ text: weather.hum, img: atob("GBiBAAAEAAAMAAAOAAAfAAAfAAA/gAA/gAI/gAY/AAcfAA+AQA+A4B/A4D/B8D/h+D/j+H/n/D/n/D/n/B/H/A+H/AAH/AAD+AAA8A=="),
v: parseInt(weather.hum), min: 0, max: 100}),
show: function() { weatherItems.items[1].emit("redraw"); },
show: function() { this.emit("redraw"); },
hide: function () {}
},
{
@ -40,7 +65,7 @@
hasRange : true,
get: () => ({ text: weather.wind, img: atob("GBiBAAHgAAPwAAYYAAwYAAwMfAAY/gAZh3/xg//hgwAAAwAABg///g//+AAAAAAAAP//wH//4AAAMAAAMAAYMAAYMAAMcAAP4AADwA=="),
v: parseInt(weather.wind), min: 0, max: 118}),
show: function() { weatherItems.items[2].emit("redraw"); },
show: function() { this.emit("redraw"); },
hide: function () {}
},
]

View File

@ -62,12 +62,29 @@ scheduleExpiry(storage.readJSON('weather.json')||{});
* @param x Left
* @param y Top
* @param r Icon Size
* @param ovr Graphics instance (or undefined for g)
*/
exports.drawIcon = function(cond, x, y, r) {
exports.drawIcon = function(cond, x, y, r, ovr) {
var palette;
var monochrome=1;
if(!ovr) {
ovr = g;
monochrome=0;
}
if(monochrome) {
palette = {
sun: '#FFF',
cloud: '#FFF',
bgCloud: '#FFF',
rain: '#FFF',
lightning: '#FFF',
snow: '#FFF',
mist: '#FFF',
background: '#000'
};
} else
if (B2) {
if (g.theme.dark) {
if (ovr.theme.dark) {
palette = {
sun: '#FF0',
cloud: '#FFF',
@ -89,7 +106,7 @@ exports.drawIcon = function(cond, x, y, r) {
};
}
} else {
if (g.theme.dark) {
if (ovr.theme.dark) {
palette = {
sun: '#FE0',
cloud: '#BBB',
@ -113,19 +130,19 @@ exports.drawIcon = function(cond, x, y, r) {
}
function drawSun(x, y, r) {
g.setColor(palette.sun);
g.fillCircle(x, y, r);
ovr.setColor(palette.sun);
ovr.fillCircle(x, y, r);
}
function drawCloud(x, y, r, c) {
const u = r/12;
if (c==null) c = palette.cloud;
g.setColor(c);
g.fillCircle(x-8*u, y+3*u, 4*u);
g.fillCircle(x-4*u, y-2*u, 5*u);
g.fillCircle(x+4*u, y+0*u, 4*u);
g.fillCircle(x+9*u, y+4*u, 3*u);
g.fillPoly([
ovr.setColor(c);
ovr.fillCircle(x-8*u, y+3*u, 4*u);
ovr.fillCircle(x-4*u, y-2*u, 5*u);
ovr.fillCircle(x+4*u, y+0*u, 4*u);
ovr.fillCircle(x+9*u, y+4*u, 3*u);
ovr.fillPoly([
x-8*u, y+7*u,
x-8*u, y+3*u,
x-4*u, y-2*u,
@ -137,19 +154,23 @@ exports.drawIcon = function(cond, x, y, r) {
function drawBrokenClouds(x, y, r) {
drawCloud(x+1/8*r, y-1/8*r, 7/8*r, palette.bgCloud);
if(monochrome)
drawCloud(x-1/8*r, y+2/16*r, r, palette.background);
drawCloud(x-1/8*r, y+1/8*r, 7/8*r);
}
function drawFewClouds(x, y, r) {
drawSun(x+3/8*r, y-1/8*r, 5/8*r);
if(monochrome)
drawCloud(x-1/8*r, y+2/16*r, r, palette.background);
drawCloud(x-1/8*r, y+1/8*r, 7/8*r);
}
function drawRainLines(x, y, r) {
g.setColor(palette.rain);
ovr.setColor(palette.rain);
const y1 = y+1/2*r;
const y2 = y+1*r;
const poly = g.fillPolyAA ? p => g.fillPolyAA(p) : p => g.fillPoly(p);
const poly = ovr.fillPolyAA ? p => ovr.fillPolyAA(p) : p => ovr.fillPoly(p);
poly([
x-6/12*r, y1,
x-8/12*r, y2,
@ -182,8 +203,8 @@ exports.drawIcon = function(cond, x, y, r) {
function drawThunderstorm(x, y, r) {
function drawLightning(x, y, r) {
g.setColor(palette.lightning);
g.fillPoly([
ovr.setColor(palette.lightning);
ovr.fillPoly([
x-2/6*r, y-r,
x-4/6*r, y+1/6*r,
x-1/6*r, y+1/6*r,
@ -194,8 +215,9 @@ exports.drawIcon = function(cond, x, y, r) {
]);
}
drawBrokenClouds(x, y-1/3*r, r);
if(monochrome) drawBrokenClouds(x, y-1/3*r, r);
drawLightning(x-1/12*r, y+1/2*r, 1/2*r);
drawBrokenClouds(x, y-1/3*r, r);
}
function drawSnow(x, y, r) {
@ -210,7 +232,7 @@ exports.drawIcon = function(cond, x, y, r) {
}
}
g.setColor(palette.snow);
ovr.setColor(palette.snow);
const w = 1/12*r;
for(let i = 0; i<=6; ++i) {
const points = [
@ -220,7 +242,7 @@ exports.drawIcon = function(cond, x, y, r) {
x+w, y+r,
];
rotatePoints(points, x, y, i/3*Math.PI);
g.fillPoly(points);
ovr.fillPoly(points);
for(let j = -1; j<=1; j += 2) {
const points = [
@ -231,7 +253,7 @@ exports.drawIcon = function(cond, x, y, r) {
];
rotatePoints(points, x, y+7/12*r, j/3*Math.PI);
rotatePoints(points, x, y, i/3*Math.PI);
g.fillPoly(points);
ovr.fillPoly(points);
}
}
}
@ -245,18 +267,18 @@ exports.drawIcon = function(cond, x, y, r) {
[-0.2, 0.3],
];
g.setColor(palette.mist);
ovr.setColor(palette.mist);
for(let i = 0; i<5; ++i) {
g.fillRect(x+layers[i][0]*r, y+(0.4*i-0.9)*r, x+layers[i][1]*r,
ovr.fillRect(x+layers[i][0]*r, y+(0.4*i-0.9)*r, x+layers[i][1]*r,
y+(0.4*i-0.7)*r-1);
g.fillCircle(x+layers[i][0]*r, y+(0.4*i-0.8)*r-0.5, 0.1*r-0.5);
g.fillCircle(x+layers[i][1]*r, y+(0.4*i-0.8)*r-0.5, 0.1*r-0.5);
ovr.fillCircle(x+layers[i][0]*r, y+(0.4*i-0.8)*r-0.5, 0.1*r-0.5);
ovr.fillCircle(x+layers[i][1]*r, y+(0.4*i-0.8)*r-0.5, 0.1*r-0.5);
}
}
function drawUnknown(x, y, r) {
drawCloud(x, y, r, palette.bgCloud);
g.setColor(g.theme.fg).setFontAlign(0, 0).setFont('Vector', r*2).drawString("?", x+r/10, y+r/6);
ovr.setColor(ovr.theme.fg).setFontAlign(0, 0).setFont('Vector', r*2).drawString("?", x+r/10, y+r/6);
}
/*

View File

@ -1,7 +1,7 @@
{
"id": "weather",
"name": "Weather",
"version": "0.18",
"version": "0.20",
"description": "Show Gadgetbridge weather report",
"icon": "icon.png",
"screenshots": [{"url":"screenshot.png"}],

View File

@ -4,3 +4,4 @@
0.04: Set sortorder to -1 so that widget always takes up the furthest left position
0.05: Set sortorder to -10 so that others can take -1 etc
0.06: Set sortorder to -10 in widget code
0.07: Remove check for .isLocked (extremely old firmwares), speed up widget loading

View File

@ -1,7 +1,7 @@
{
"id": "widlock",
"name": "Lock Widget",
"version": "0.06",
"version": "0.07",
"description": "On devices with always-on display (Bangle.js 2) this displays lock icon whenever the display is locked",
"icon": "widget.png",
"type": "widget",

View File

@ -1,11 +1,8 @@
(function(){
if (!Bangle.isLocked) return; // bail out on old firmware
Bangle.on("lock", function(on) {
WIDGETS["lock"].width = Bangle.isLocked()?16:0;
Bangle.drawWidgets();
});
WIDGETS["lock"]={area:"tl",sortorder:10,width:Bangle.isLocked()?16:0,draw:function(w) {
if (Bangle.isLocked())
g.reset().drawImage(atob("DhABH+D/wwMMDDAwwMf/v//4f+H/h/8//P/z///f/g=="), w.x+1, w.y+4);
}};
})()
Bangle.on("lock", function() {
WIDGETS["lock"].width = Bangle.isLocked()?16:0;
Bangle.drawWidgets();
});
WIDGETS["lock"]={area:"tl",sortorder:10,width:Bangle.isLocked()?16:0,draw:function(w) {
if (Bangle.isLocked())
g.reset().drawImage(atob("DhABH+D/wwMMDDAwwMf/v//4f+H/h/8//P/z///f/g=="), w.x+1, w.y+4);
}};

View File

@ -0,0 +1 @@
0.01: First commit

View File

@ -0,0 +1,13 @@
{
"id": "widlockunlock",
"name": "Lock/Unlock Widget",
"version": "0.01",
"description": "On devices with always-on display (Bangle.js 2) this displays lock icon whenever the display is locked, or an unlock icon otherwise",
"icon": "widget.png",
"type": "widget",
"tags": "widget,lock",
"supports": ["BANGLEJS","BANGLEJS2"],
"storage": [
{"name":"widlockunlock.wid.js","url":"widget.js"}
]
}

View File

@ -0,0 +1,6 @@
Bangle.on("lockunlock", function() {
Bangle.drawWidgets();
});
WIDGETS["lockunlock"]={area:"tl",sortorder:10,width:14,draw:function(w) {
g.reset().drawImage(atob(Bangle.isLocked() ? "DBGBAAAA8DnDDCBCBP////////n/n/n//////z/A" : "DBGBAAAA8BnDDCBABP///8A8A8Y8Y8Y8A8A//z/A"), w.x+1, w.y+3);
}};

Binary file not shown.

After

Width:  |  Height:  |  Size: 726 B

2
core

@ -1 +1 @@
Subproject commit f15e99fbe25b2991719011e6da9bc9c7be401a7e
Subproject commit 3a953179b7bb9f574d4e77d5f34b6b7deee1e884

View File

@ -9,7 +9,7 @@ function ClockFace(options) {
if (![
"precision",
"init", "draw", "update",
"pause", "resume",
"pause", "resume", "remove",
"up", "down", "upDown",
"settingsFile",
].includes(k)) throw `Invalid ClockFace option: ${k}`;
@ -27,6 +27,7 @@ function ClockFace(options) {
if (options.init) this.init = options.init;
if (options.pause) this._pause = options.pause;
if (options.resume) this._resume = options.resume;
if (options.remove) this._remove = options.remove;
if ((options.up || options.down) && options.upDown) throw "ClockFace up/down and upDown cannot be used together";
if (options.up || options.down) this._upDown = (dir) => {
if (dir<0 && options.up) options.up.apply(this);
@ -44,8 +45,15 @@ function ClockFace(options) {
["showDate", "loadWidgets"].forEach(k => {
if (this[k]===undefined) this[k] = true;
});
let s = require("Storage").readJSON("setting.json",1)||{};
if ((global.__FILE__===undefined || global.__FILE__===s.clock)
&& s.clockHasWidgets!==this.loadWidgets) {
// save whether we can Fast Load
s.clockHasWidgets = this.loadWidgets;
require("Storage").writeJSON("setting.json", s);
}
// use global 24/12-hour setting if not set by clock-settings
if (!('is12Hour' in this)) this.is12Hour = !!(require("Storage").readJSON("setting.json", true) || {})["12hour"];
if (!('is12Hour' in this)) this.is12Hour = !!(s["12hour"]);
}
ClockFace.prototype.tick = function() {
@ -85,16 +93,27 @@ ClockFace.prototype.start = function() {
Bangle.CLOCK = 1;
if (this.loadWidgets) Bangle.loadWidgets();
if (this.init) this.init.apply(this);
if (this._upDown) Bangle.setUI("clockupdown", d=>this._upDown.apply(this,[d]));
else Bangle.setUI("clock");
const uiRemove = this._remove ? () => this.remove() : undefined;
if (this._upDown) {
Bangle.setUI({
mode: "clockupdown",
remove: uiRemove,
}, d => this._upDown.apply(this, [d]));
} else {
Bangle.setUI({
mode: "clock",
remove: uiRemove,
});
}
delete this._last;
this.paused = false;
this.tick();
Bangle.on("lcdPower", on => {
this._onLcd = on => {
if (on) this.resume();
else this.pause();
});
};
Bangle.on("lcdPower", this._onLcd);
};
ClockFace.prototype.pause = function() {
@ -111,6 +130,11 @@ ClockFace.prototype.resume = function() {
if (this._resume) this._resume.apply(this);
this.tick();
};
ClockFace.prototype.remove = function() {
if (this._timeout) clearTimeout(this._timeout);
Bangle.removeListener("lcdPower", this._onLcd);
if (this._remove) this._remove.apply(this);
};
/**
* Force a complete redraw

View File

@ -77,6 +77,11 @@ var clock = new ClockFace({
resume: function() { // optional, called when the screen turns on
// for example: turn GPS/compass back on
},
remove: function() { // optional, used for Fast Loading
// for example: remove listeners
// Fast Loading will not be used unless this function is present,
// if there is nothing to clean up, you can just leave it empty.
},
up: function() { // optional, up handler
},
down: function() { // optional, down handler

View File

@ -83,7 +83,7 @@ Layout.prototype.setUI = function() {
let uiSet;
if (this.buttons) {
// multiple buttons so we'll jus use back/next/select
Bangle.setUI({mode:"updown", back:this.options.back}, dir=>{
Bangle.setUI({mode:"updown", back:this.options.back, remove:this.options.remove}, dir=>{
var s = this.selectedButton, l=this.buttons.length;
if (dir===undefined && this.buttons[s])
return this.buttons[s].cb();
@ -100,7 +100,7 @@ Layout.prototype.setUI = function() {
});
uiSet = true;
}
if (this.options.back && !uiSet) Bangle.setUI({mode: "custom", back: this.options.back});
if ((this.options.back || this.options.remove) && !uiSet) Bangle.setUI({mode: "custom", back: this.options.back, remove: this.options.remove});
// physical buttons -> actual applications
if (this.b) {
// Handler for button watch events

View File

@ -59,6 +59,7 @@ layout.render();
- `cb` - a callback function
- `cbl` - a callback function for long presses
- `back` - a callback function, passed as `back` into Bangle.setUI (which usually adds an icon in the top left)
- `remove` - a cleanup function, passed as `remove` into Bangle.setUI (allows to cleanly remove the app from memory)
If automatic lazy rendering is enabled, calls to `layout.render()` will attempt to automatically determine what objects have changed or moved, clear their previous locations, and re-render just those objects.

28
modules/Layout.min.js vendored
View File

@ -1,14 +1,14 @@
function p(d,h){function b(e){"ram";e.id&&(a[e.id]=e);e.type||(e.type="");e.c&&e.c.forEach(b)}this._l=this.l=d;this.options=h||{};this.lazy=this.options.lazy||!1;this.physBtns=1;let f;if(2!=process.env.HWVERSION){this.physBtns=3;f=[];function e(l){"ram";"btn"==l.type&&f.push(l);l.c&&l.c.forEach(e)}e(d);f.length&&(this.physBtns=0,this.buttons=f,this.selectedButton=-1)}if(this.options.btns)if(d=this.options.btns,this.physBtns>=d.length){this.b=d;let e=Math.floor(Bangle.appRect.h/
this.physBtns);for(2<this.physBtns&&1==d.length&&d.unshift({label:""});this.physBtns>d.length;)d.push({label:""});this._l.width=g.getWidth()-8;this._l={type:"h",filly:1,c:[this._l,{type:"v",pad:1,filly:1,c:d.map(l=>(l.type="txt",l.font="6x8",l.height=e,l.r=1,l))}]}}else this._l.width=g.getWidth()-32,this._l={type:"h",c:[this._l,{type:"v",c:d.map(e=>(e.type="btn",e.filly=1,e.width=32,e.r=1,e))}]},f&&f.push.apply(f,this._l.c[1].c);this.setUI();var a=this;b(this._l);this.updateNeeded=!0}function t(d,
h,b,f,a){var e=null==d.bgCol?a:g.toColor(d.bgCol);if(e!=a||"txt"==d.type||"btn"==d.type||"img"==d.type||"custom"==d.type){var l=d.c;delete d.c;var k="H"+E.CRC32(E.toJS(d));l&&(d.c=l);delete h[k]||((f[k]=[d.x,d.y,d.x+d.w-1,d.y+d.h-1]).bg=null==a?g.theme.bg:a,b&&(b.push(d),b=null))}if(d.c)for(var c of d.c)t(c,h,b,f,e)}p.prototype.setUI=function(){Bangle.setUI();let d;this.buttons&&(Bangle.setUI({mode:"updown",back:this.options.back},h=>{var b=this.selectedButton,f=this.buttons.length;if(void 0===h&&
this.buttons[b])return this.buttons[b].cb();this.buttons[b]&&(delete this.buttons[b].selected,this.render(this.buttons[b]));b=(b+f+h)%f;this.buttons[b]&&(this.buttons[b].selected=1,this.render(this.buttons[b]));this.selectedButton=b}),d=!0);this.options.back&&!d&&Bangle.setUI({mode:"custom",back:this.options.back});if(this.b){function h(b,f){.75<f.time-f.lastTime&&this.b[b].cbl?this.b[b].cbl(f):this.b[b].cb&&this.b[b].cb(f)}Bangle.btnWatches&&Bangle.btnWatches.forEach(clearWatch);Bangle.btnWatches=
[];this.b[0]&&Bangle.btnWatches.push(setWatch(h.bind(this,0),BTN1,{repeat:!0,edge:-1}));this.b[1]&&Bangle.btnWatches.push(setWatch(h.bind(this,1),BTN2,{repeat:!0,edge:-1}));this.b[2]&&Bangle.btnWatches.push(setWatch(h.bind(this,2),BTN3,{repeat:!0,edge:-1}))}if(2==process.env.HWVERSION){function h(b,f){b.cb&&f.x>=b.x&&f.y>=b.y&&f.x<=b.x+b.w&&f.y<=b.y+b.h&&(2==f.type&&b.cbl?b.cbl(f):b.cb&&b.cb(f));b.c&&b.c.forEach(a=>h(a,f))}Bangle.touchHandler=(b,f)=>h(this._l,f);Bangle.on("touch",Bangle.touchHandler)}};
p.prototype.render=function(d){function h(c){"ram";b.reset();void 0!==c.col&&b.setColor(c.col);void 0!==c.bgCol&&b.setBgColor(c.bgCol).clearRect(c.x,c.y,c.x+c.w-1,c.y+c.h-1);f[c.type](c)}d||(d=this._l);this.updateNeeded&&this.update();var b=g,f={"":function(){},txt:function(c){"ram";if(c.wrap){var m=b.setFont(c.font).setFontAlign(0,-1).wrapString(c.label,c.w),n=c.y+(c.h-b.getFontHeight()*m.length>>1);b.drawString(m.join("\n"),c.x+(c.w>>1),n)}else b.setFont(c.font).setFontAlign(0,0,c.r).drawString(c.label,
c.x+(c.w>>1),c.y+(c.h>>1))},btn:function(c){"ram";var m=c.x+(0|c.pad),n=c.y+(0|c.pad),q=c.w-(c.pad<<1),r=c.h-(c.pad<<1);m=[m,n+4,m+4,n,m+q-5,n,m+q-1,n+4,m+q-1,n+r-5,m+q-5,n+r-1,m+4,n+r-1,m,n+r-5,m,n+4];n=c.selected?b.theme.bgH:b.theme.bg2;b.setColor(n).fillPoly(m).setColor(c.selected?b.theme.fgH:b.theme.fg2).drawPoly(m);void 0!==c.col&&b.setColor(c.col);c.src?b.setBgColor(n).drawImage("function"==typeof c.src?c.src():c.src,c.x+c.w/2,c.y+c.h/2,{scale:c.scale||void 0,rotate:.5*Math.PI*(c.r||0)}):b.setFont(c.font||
"6x8:2").setFontAlign(0,0,c.r).drawString(c.label,c.x+c.w/2,c.y+c.h/2)},img:function(c){"ram";b.drawImage("function"==typeof c.src?c.src():c.src,c.x+c.w/2,c.y+c.h/2,{scale:c.scale||void 0,rotate:.5*Math.PI*(c.r||0)})},custom:function(c){"ram";c.render(c)},h:function(c){"ram";c.c.forEach(h)},v:function(c){"ram";c.c.forEach(h)}};if(this.lazy){this.rects||(this.rects={});var a=this.rects.clone(),e=[];t(d,a,e,this.rects,null);for(var l in a)delete this.rects[l];d=Object.keys(a).map(c=>a[c]).reverse();
for(var k of d)b.setBgColor(k.bg).clearRect.apply(g,k);e.forEach(h)}else h(d)};p.prototype.forgetLazyState=function(){this.rects={}};p.prototype.layout=function(d){var h={h:function(b){"ram";var f=b.x+(0|b.pad),a=0,e=b.c&&b.c.reduce((k,c)=>k+(0|c.fillx),0);e||(f+=b.w-b._w>>1,e=1);var l=f;b.c.forEach(k=>{k.x=0|l;f+=k._w;a+=0|k.fillx;l=f+Math.floor(a*(b.w-b._w)/e);k.w=0|l-k.x;k.h=0|(k.filly?b.h-(b.pad<<1):k._h);k.y=0|b.y+(0|b.pad)+((1+(0|k.valign))*(b.h-(b.pad<<1)-k.h)>>1);if(k.c)h[k.type](k)})},v:function(b){"ram";
var f=b.y+(0|b.pad),a=0,e=b.c&&b.c.reduce((k,c)=>k+(0|c.filly),0);e||(f+=b.h-b._h>>1,e=1);var l=f;b.c.forEach(k=>{k.y=0|l;f+=k._h;a+=0|k.filly;l=f+Math.floor(a*(b.h-b._h)/e);k.h=0|l-k.y;k.w=0|(k.fillx?b.w-(b.pad<<1):k._w);k.x=0|b.x+(0|b.pad)+((1+(0|k.halign))*(b.w-(b.pad<<1)-k.w)>>1);if(k.c)h[k.type](k)})}};h[d.type](d)};p.prototype.debug=function(d,h){d||(d=this._l);h=h||1;g.setColor(h&1,h&2,h&4).drawRect(d.x+h-1,d.y+h-1,d.x+d.w-h,d.y+d.h-h);d.pad&&g.drawRect(d.x+d.pad-1,d.y+d.pad-1,d.x+d.w-d.pad,
d.y+d.h-d.pad);h++;d.c&&d.c.forEach(b=>this.debug(b,h))};p.prototype.update=function(){function d(a){"ram";b[a.type](a);if(a.r&1){var e=a._w;a._w=a._h;a._h=e}a._w=Math.max(a._w+(a.pad<<1),0|a.width);a._h=Math.max(a._h+(a.pad<<1),0|a.height)}delete this.updateNeeded;var h=g,b={txt:function(a){"ram";a.font.endsWith("%")&&(a.font="Vector"+Math.round(h.getHeight()*a.font.slice(0,-1)/100));if(a.wrap)a._h=a._w=0;else{var e=g.setFont(a.font).stringMetrics(a.label);a._w=e.width;a._h=e.height}},btn:function(a){"ram";
a.font&&a.font.endsWith("%")&&(a.font="Vector"+Math.round(h.getHeight()*a.font.slice(0,-1)/100));var e=a.src?h.imageMetrics("function"==typeof a.src?a.src():a.src):h.setFont(a.font||"6x8:2").stringMetrics(a.label);a._h=16+e.height;a._w=20+e.width},img:function(a){"ram";var e=h.imageMetrics("function"==typeof a.src?a.src():a.src),l=a.scale||1;a._w=e.width*l;a._h=e.height*l},"":function(a){"ram";a._w=0;a._h=0},custom:function(a){"ram";a._w=0;a._h=0},h:function(a){"ram";a.c.forEach(d);a._h=a.c.reduce((e,
l)=>Math.max(e,l._h),0);a._w=a.c.reduce((e,l)=>e+l._w,0);null==a.fillx&&a.c.some(e=>e.fillx)&&(a.fillx=1);null==a.filly&&a.c.some(e=>e.filly)&&(a.filly=1)},v:function(a){"ram";a.c.forEach(d);a._h=a.c.reduce((e,l)=>e+l._h,0);a._w=a.c.reduce((e,l)=>Math.max(e,l._w),0);null==a.fillx&&a.c.some(e=>e.fillx)&&(a.fillx=1);null==a.filly&&a.c.some(e=>e.filly)&&(a.filly=1)}},f=this._l;d(f);delete b;f.fillx||f.filly?(f.w=Bangle.appRect.w,f.h=Bangle.appRect.h,f.x=Bangle.appRect.x,f.y=Bangle.appRect.y):(f.w=f._w,
f.h=f._h,f.x=Bangle.appRect.w-f.w>>1,f.y=Bangle.appRect.y+(Bangle.appRect.h-f.h>>1));this.layout(f)};p.prototype.clear=function(d){d||(d=this._l);g.reset();void 0!==d.bgCol&&g.setBgColor(d.bgCol);g.clearRect(d.x,d.y,d.x+d.w-1,d.y+d.h-1)};exports=p
function p(d,h){function b(e){"ram";e.id&&(a[e.id]=e);e.type||(e.type="");e.c&&e.c.forEach(b)}this._l=this.l=d;this.options=h||{};this.lazy=this.options.lazy||!1;this.physBtns=1;let f;if(2!=process.env.HWVERSION){this.physBtns=3;f=[];function e(l){"ram";"btn"==l.type&&f.push(l);l.c&&l.c.forEach(e)}e(d);f.length&&(this.physBtns=0,this.buttons=f,this.selectedButton=-1)}if(this.options.btns)if(d=this.options.btns,this.physBtns>=d.length){this.b=d;let e=Math.floor(Bangle.appRect.h/this.physBtns);
for(2<this.physBtns&&1==d.length&&d.unshift({label:""});this.physBtns>d.length;)d.push({label:""});this._l.width=g.getWidth()-8;this._l={type:"h",filly:1,c:[this._l,{type:"v",pad:1,filly:1,c:d.map(l=>(l.type="txt",l.font="6x8",l.height=e,l.r=1,l))}]}}else this._l.width=g.getWidth()-32,this._l={type:"h",c:[this._l,{type:"v",c:d.map(e=>(e.type="btn",e.filly=1,e.width=32,e.r=1,e))}]},f&&f.push.apply(f,this._l.c[1].c);this.setUI();var a=this;b(this._l);this.updateNeeded=!0}function t(d,h,b,f,a){var e=
null==d.bgCol?a:g.toColor(d.bgCol);if(e!=a||"txt"==d.type||"btn"==d.type||"img"==d.type||"custom"==d.type){var l=d.c;delete d.c;var k="H"+E.CRC32(E.toJS(d));l&&(d.c=l);delete h[k]||((f[k]=[d.x,d.y,d.x+d.w-1,d.y+d.h-1]).bg=null==a?g.theme.bg:a,b&&(b.push(d),b=null))}if(d.c)for(var c of d.c)t(c,h,b,f,e)}p.prototype.setUI=function(){Bangle.setUI();let d;this.buttons&&(Bangle.setUI({mode:"updown",back:this.options.back,remove:this.options.remove},h=>{var b=this.selectedButton,f=this.buttons.length;if(void 0===
h&&this.buttons[b])return this.buttons[b].cb();this.buttons[b]&&(delete this.buttons[b].selected,this.render(this.buttons[b]));b=(b+f+h)%f;this.buttons[b]&&(this.buttons[b].selected=1,this.render(this.buttons[b]));this.selectedButton=b}),d=!0);!this.options.back&&!this.options.remove||d||Bangle.setUI({mode:"custom",back:this.options.back,remove:this.options.remove});if(this.b){function h(b,f){.75<f.time-f.lastTime&&this.b[b].cbl?this.b[b].cbl(f):this.b[b].cb&&this.b[b].cb(f)}Bangle.btnWatches&&Bangle.btnWatches.forEach(clearWatch);
Bangle.btnWatches=[];this.b[0]&&Bangle.btnWatches.push(setWatch(h.bind(this,0),BTN1,{repeat:!0,edge:-1}));this.b[1]&&Bangle.btnWatches.push(setWatch(h.bind(this,1),BTN2,{repeat:!0,edge:-1}));this.b[2]&&Bangle.btnWatches.push(setWatch(h.bind(this,2),BTN3,{repeat:!0,edge:-1}))}if(2==process.env.HWVERSION){function h(b,f){b.cb&&f.x>=b.x&&f.y>=b.y&&f.x<=b.x+b.w&&f.y<=b.y+b.h&&(2==f.type&&b.cbl?b.cbl(f):b.cb&&b.cb(f));b.c&&b.c.forEach(a=>h(a,f))}Bangle.touchHandler=(b,f)=>h(this._l,f);Bangle.on("touch",
Bangle.touchHandler)}};p.prototype.render=function(d){function h(c){"ram";b.reset();void 0!==c.col&&b.setColor(c.col);void 0!==c.bgCol&&b.setBgColor(c.bgCol).clearRect(c.x,c.y,c.x+c.w-1,c.y+c.h-1);f[c.type](c)}d||(d=this._l);this.updateNeeded&&this.update();var b=g,f={"":function(){},txt:function(c){"ram";if(c.wrap){var m=b.setFont(c.font).setFontAlign(0,-1).wrapString(c.label,c.w),n=c.y+(c.h-b.getFontHeight()*m.length>>1);b.drawString(m.join("\n"),c.x+(c.w>>1),n)}else b.setFont(c.font).setFontAlign(0,
0,c.r).drawString(c.label,c.x+(c.w>>1),c.y+(c.h>>1))},btn:function(c){"ram";var m=c.x+(0|c.pad),n=c.y+(0|c.pad),q=c.w-(c.pad<<1),r=c.h-(c.pad<<1);m=[m,n+4,m+4,n,m+q-5,n,m+q-1,n+4,m+q-1,n+r-5,m+q-5,n+r-1,m+4,n+r-1,m,n+r-5,m,n+4];n=c.selected?b.theme.bgH:b.theme.bg2;b.setColor(n).fillPoly(m).setColor(c.selected?b.theme.fgH:b.theme.fg2).drawPoly(m);void 0!==c.col&&b.setColor(c.col);c.src?b.setBgColor(n).drawImage("function"==typeof c.src?c.src():c.src,c.x+c.w/2,c.y+c.h/2,{scale:c.scale||void 0,rotate:.5*
Math.PI*(c.r||0)}):b.setFont(c.font||"6x8:2").setFontAlign(0,0,c.r).drawString(c.label,c.x+c.w/2,c.y+c.h/2)},img:function(c){"ram";b.drawImage("function"==typeof c.src?c.src():c.src,c.x+c.w/2,c.y+c.h/2,{scale:c.scale||void 0,rotate:.5*Math.PI*(c.r||0)})},custom:function(c){"ram";c.render(c)},h:function(c){"ram";c.c.forEach(h)},v:function(c){"ram";c.c.forEach(h)}};if(this.lazy){this.rects||(this.rects={});var a=this.rects.clone(),e=[];t(d,a,e,this.rects,null);for(var l in a)delete this.rects[l];d=
Object.keys(a).map(c=>a[c]).reverse();for(var k of d)b.setBgColor(k.bg).clearRect.apply(g,k);e.forEach(h)}else h(d)};p.prototype.forgetLazyState=function(){this.rects={}};p.prototype.layout=function(d){var h={h:function(b){"ram";var f=b.x+(0|b.pad),a=0,e=b.c&&b.c.reduce((k,c)=>k+(0|c.fillx),0);e||(f+=b.w-b._w>>1,e=1);var l=f;b.c.forEach(k=>{k.x=0|l;f+=k._w;a+=0|k.fillx;l=f+Math.floor(a*(b.w-b._w)/e);k.w=0|l-k.x;k.h=0|(k.filly?b.h-(b.pad<<1):k._h);k.y=0|b.y+(0|b.pad)+((1+(0|k.valign))*(b.h-(b.pad<<
1)-k.h)>>1);if(k.c)h[k.type](k)})},v:function(b){"ram";var f=b.y+(0|b.pad),a=0,e=b.c&&b.c.reduce((k,c)=>k+(0|c.filly),0);e||(f+=b.h-b._h>>1,e=1);var l=f;b.c.forEach(k=>{k.y=0|l;f+=k._h;a+=0|k.filly;l=f+Math.floor(a*(b.h-b._h)/e);k.h=0|l-k.y;k.w=0|(k.fillx?b.w-(b.pad<<1):k._w);k.x=0|b.x+(0|b.pad)+((1+(0|k.halign))*(b.w-(b.pad<<1)-k.w)>>1);if(k.c)h[k.type](k)})}};h[d.type](d)};p.prototype.debug=function(d,h){d||(d=this._l);h=h||1;g.setColor(h&1,h&2,h&4).drawRect(d.x+h-1,d.y+h-1,d.x+d.w-h,d.y+d.h-h);
d.pad&&g.drawRect(d.x+d.pad-1,d.y+d.pad-1,d.x+d.w-d.pad,d.y+d.h-d.pad);h++;d.c&&d.c.forEach(b=>this.debug(b,h))};p.prototype.update=function(){function d(a){"ram";b[a.type](a);if(a.r&1){var e=a._w;a._w=a._h;a._h=e}a._w=Math.max(a._w+(a.pad<<1),0|a.width);a._h=Math.max(a._h+(a.pad<<1),0|a.height)}delete this.updateNeeded;var h=g,b={txt:function(a){"ram";a.font.endsWith("%")&&(a.font="Vector"+Math.round(h.getHeight()*a.font.slice(0,-1)/100));if(a.wrap)a._h=a._w=0;else{var e=g.setFont(a.font).stringMetrics(a.label);
a._w=e.width;a._h=e.height}},btn:function(a){"ram";a.font&&a.font.endsWith("%")&&(a.font="Vector"+Math.round(h.getHeight()*a.font.slice(0,-1)/100));var e=a.src?h.imageMetrics("function"==typeof a.src?a.src():a.src):h.setFont(a.font||"6x8:2").stringMetrics(a.label);a._h=16+e.height;a._w=20+e.width},img:function(a){"ram";var e=h.imageMetrics("function"==typeof a.src?a.src():a.src),l=a.scale||1;a._w=e.width*l;a._h=e.height*l},"":function(a){"ram";a._w=0;a._h=0},custom:function(a){"ram";a._w=0;a._h=0},
h:function(a){"ram";a.c.forEach(d);a._h=a.c.reduce((e,l)=>Math.max(e,l._h),0);a._w=a.c.reduce((e,l)=>e+l._w,0);null==a.fillx&&a.c.some(e=>e.fillx)&&(a.fillx=1);null==a.filly&&a.c.some(e=>e.filly)&&(a.filly=1)},v:function(a){"ram";a.c.forEach(d);a._h=a.c.reduce((e,l)=>e+l._h,0);a._w=a.c.reduce((e,l)=>Math.max(e,l._w),0);null==a.fillx&&a.c.some(e=>e.fillx)&&(a.fillx=1);null==a.filly&&a.c.some(e=>e.filly)&&(a.filly=1)}},f=this._l;d(f);delete b;f.fillx||f.filly?(f.w=Bangle.appRect.w,f.h=Bangle.appRect.h,
f.x=Bangle.appRect.x,f.y=Bangle.appRect.y):(f.w=f._w,f.h=f._h,f.x=Bangle.appRect.w-f.w>>1,f.y=Bangle.appRect.y+(Bangle.appRect.h-f.h>>1));this.layout(f)};p.prototype.clear=function(d){d||(d=this._l);g.reset();void 0!==d.bgCol&&g.setBgColor(d.bgCol);g.clearRect(d.x,d.y,d.x+d.w-1,d.y+d.h-1)};exports=p