Merge branch 'master' of github.com:nxdefiant/BangleApps

pull/2764/head
Erik Andresen 2023-05-18 20:04:21 +02:00
commit e358e3ebc6
245 changed files with 5807 additions and 1273 deletions

View File

@ -1,5 +1,10 @@
# App Name
More information on making apps:
* http://www.espruino.com/Bangle.js+First+App
* http://www.espruino.com/Bangle.js+App+Loader
Describe the app...
Add screen shots (if possible) to the app folder and link then into this file with ![](<name>.png)

View File

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

View File

@ -0,0 +1,27 @@
# Clock Info Name
More info on making Clock Infos and what they are: http://www.espruino.com/Bangle.js+Clock+Info
Describe the clock info...
Add screen shots (if possible) to the app folder and link then into this file with ![](<name>.png)
## Usage
Describe how to use it
## Features
Name the function
## Controls
Name the buttons and what they are used for
## Requests
Name who should be contacted for support/update requests
## Creator
Your name

View File

@ -0,0 +1,16 @@
(function() {
return {
name: "Bangle",
// img: 24x24px image for this list of items. The default "Bangle" list has its own image so this is not needed
items: [
{ name : "Item1",
get : function() { return { text : "TextOfItem1",
// v : 10, min : 0, max : 100, - optional
img : atob("GBiBAAAAAAAAAAAYAAD/AAOBwAYAYAwAMAgAEBgAGBAACBCBCDHDjDCBDBAACBAACBhCGAh+EAwYMAYAYAOBwAD/AAAYAAAAAAAAAA==") }},
show : function() {},
hide : function() {},
// run : function() {} optional (called when tapped)
}
]
};
}) // must not have a semi-colon!

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,14 @@
{ "id": "7chname",
"name": "My clock info's human readable name",
"shortName":"Short Name",
"version":"0.01",
"description": "A detailed description of my clock info",
"icon": "icon.png",
"type": "clkinfo",
"tags": "clkinfo",
"supports" : ["BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"7chname.clkinfo.js","url":"clkinfo.js"}
]
}

View File

@ -1,5 +1,7 @@
# Widget Name
More info on making Widgets and what they are: http://www.espruino.com/Bangle.js+Widgets
Describe the app...
Add screen shots (if possible) to the app folder and link then into this file with ![](<name>.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -3,7 +3,7 @@
"shortName":"Short Name",
"version":"0.01",
"description": "A detailed description of my great widget",
"icon": "widget.png",
"icon": "icon.png",
"type": "widget",
"tags": "widget",
"supports" : ["BANGLEJS2"],

View File

@ -1,16 +1,14 @@
/* run widgets in their own function scope so they don't interfere with
currently-running apps */
/* run widgets in their own function scope if they need to define local
variables so they don't interfere with currently-running apps */
(() => {
function draw() {
g.reset(); // reset the graphics context to defaults (color/font/etc)
// add your code
g.drawString("X", this.x, this.y);
}
// add your widget
WIDGETS["mywidget"]={
area:"tl", // tl (top left), tr (top right), bl (bottom left), br (bottom right), be aware that not all apps support widgets at the bottom of the screen
width: 28, // how wide is the widget? You can change this and call Bangle.drawWidgets() to re-layout
draw:draw // called to draw the widget
draw:function() {
g.reset(); // reset the graphics context to defaults (color/font/etc)
// add your code
g.drawString("X", this.x, this.y);
} // called to draw the widget
};
})()

View File

@ -6,3 +6,4 @@
0.06: ClockInfo Fix: Use .get instead of .show as .show is not implemented for weather etc.
0.07: Use clock_info.addInteractive instead of a custom implementation
0.08: Use clock_info module as an app
0.09: clock_info now uses app name to maintain settings specifically for this clock face

View File

@ -193,6 +193,7 @@ function queueDraw() {
*/
let clockInfoItems = clock_info.load();
let clockInfoMenu = clock_info.addInteractive(clockInfoItems, {
app : "aiclock",
x : 0,
y: 0,
w: W,

View File

@ -3,7 +3,7 @@
"name": "AI Clock",
"shortName":"AI Clock",
"icon": "aiclock.png",
"version":"0.08",
"version":"0.09",
"readme": "README.md",
"supports": ["BANGLEJS2"],
"dependencies" : { "clock_info":"module" },

View File

@ -1,3 +1,4 @@
0.01: New App!
0.02: Barometer altitude adjustment setting
0.03: Use default Bangle formatter for booleans
0.04: Add options for units in locale and recording GPS

View File

@ -403,6 +403,8 @@ function onGPS(fix) {
if ( sp < 10 ) sp = sp.toFixed(1);
else sp = Math.round(sp);
if (isNaN(sp)) sp = '---';
if (parseFloat(sp) > parseFloat(max.spd) && max.n > 15 ) max.spd = parseFloat(sp);
// Altitude
@ -416,6 +418,12 @@ function onGPS(fix) {
// Age of last fix (secs)
age = Math.max(0,Math.round(getTime())-(lf.time.getTime()/1000));
} else {
// populate spd_unit
if (cfg.spd == 0) {
m = require("locale").speed(0).match(/[0-9,\.]+(.*)/);
cfg.spd_unit = m[1];
}
}
if ( cfg.modeA == 1 ) {
@ -465,7 +473,7 @@ function updateClock() {
// Read settings.
let cfg = require('Storage').readJSON('bikespeedo.json',1)||{};
cfg.spd = 1; // Multiplier for speed unit conversions. 0 = use the locale values for speed
cfg.spd = !cfg.localeUnits; // Multiplier for speed unit conversions. 0 = use the locale values for speed
cfg.spd_unit = 'km/h'; // Displayed speed unit
cfg.alt = 1; // Multiplier for altitude unit conversions. (feet:'0.3048')
cfg.alt_unit = 'm'; // Displayed altitude units ('feet')
@ -499,14 +507,6 @@ function onPressure(dat) {
altiBaro = Number(dat.altitude.toFixed(0)) + Number(cfg.altDiff);
}
Bangle.setBarometerPower(1); // needs some time...
g.clearRect(0,screenYstart,screenW,screenH);
onGPS(lf);
Bangle.setGPSPower(1);
Bangle.on('GPS', onGPS);
Bangle.on('pressure', onPressure);
Bangle.setCompassPower(1);
var CALIBDATA = require("Storage").readJSON("magnav.json",1)||null;
if (!CALIBDATA) calibrateCompass = true;
function Compass_tiltfixread(O,S){
@ -544,11 +544,33 @@ function Compass_reading() {
Compass_heading = Compass_newHeading(d,Compass_heading);
hdngCompass = Compass_heading.toFixed(0);
}
if (!calibrateCompass) setInterval(Compass_reading,200);
setButtons();
if (emulator) setInterval(updateClock, 2000);
else setInterval(updateClock, 10000);
function start() {
Bangle.setBarometerPower(1); // needs some time...
g.clearRect(0,screenYstart,screenW,screenH);
onGPS(lf);
Bangle.setGPSPower(1);
Bangle.on('GPS', onGPS);
Bangle.on('pressure', onPressure);
Bangle.setCompassPower(1);
if (!calibrateCompass) setInterval(Compass_reading,200);
setButtons();
if (emulator) setInterval(updateClock, 2000);
else setInterval(updateClock, 10000);
Bangle.drawWidgets();
}
Bangle.loadWidgets();
Bangle.drawWidgets();
if (cfg.record && WIDGETS["recorder"]) {
WIDGETS["recorder"]
.setRecording(true)
.then(start);
if (cfg.recordStopOnExit)
E.on('kill', () => WIDGETS["recorder"].setRecording(false));
} else {
start();
}

View File

@ -2,7 +2,7 @@
"id": "bikespeedo",
"name": "Bike Speedometer (beta)",
"shortName": "Bike Speedometer",
"version": "0.03",
"version": "0.04",
"description": "Shows GPS speed, GPS heading, Compass heading, GPS altitude and Barometer altitude from internal sources",
"icon": "app.png",
"screenshots": [{"url":"Screenshot.png"}],

View File

@ -11,9 +11,34 @@
'< Back': back,
'< Load Bike Speedometer': ()=>{load('bikespeedo.app.js');},
'Barometer Altitude adjustment' : function() { E.showMenu(altdiffMenu); },
'Kalman Filters' : function() { E.showMenu(kalMenu); }
'Kalman Filters' : function() { E.showMenu(kalMenu); },
'Speed units': {
value: !!settings.localeUnits,
format: b => b ? "Locale" : "km/h",
onchange: b => {
settings.localeUnits = b;
writeSettings();
}
},
};
if (global.WIDGETS && WIDGETS["recorder"]) {
appMenu[/*LANG*/"Record rides"] = {
value : !!settings.record,
onchange : v => {
settings.record = v;
writeSettings();
}
};
appMenu[/*LANG*/"Stop record on exit"] = {
value : !!settings.recordStopOnExit,
onchange : v => {
settings.recordStopOnExit = v;
writeSettings();
}
};
}
const altdiffMenu = {
'': { 'title': 'Altitude adjustment' },
'< Back': function() { E.showMenu(appMenu); },

View File

@ -32,3 +32,4 @@ clkinfo.addInteractive that would cause ReferenceError.
0.30: Use widget_utils
0.31: Use clock_info module as an app
0.32: Make the border of the clock_info box extend all the way to the right of the screen.
0.33: Fix issue rendering ClockInfos with for fg+bg color set to the same (#2749)

View File

@ -140,11 +140,8 @@ let clockInfoMenu = clock_info.addInteractive(clockInfoItems, {
draw : (itm, info, options) => {
var hideClkInfo = info.text == null;
g.setColor(g.theme.fg);
g.fillRect(options.x, options.y, options.x+options.w, options.y+options.h);
g.setFontAlign(0,0);
g.setColor(g.theme.bg);
g.reset().setBgColor(g.theme.fg).clearRect(options.x, options.y, options.x+options.w, options.y+options.h);
g.setFontAlign(0,0).setColor(g.theme.bg);
if (options.focus){
var y = hideClkInfo ? options.y+20 : options.y+2;

View File

@ -1,7 +1,7 @@
{
"id": "bwclk",
"name": "BW Clock",
"version": "0.32",
"version": "0.33",
"description": "A very minimalistic clock.",
"readme": "README.md",
"icon": "app.png",

View File

@ -35,3 +35,4 @@ clkinfo.addInteractive that would cause ReferenceError.
Remove invertion of theme as this doesn'twork very well with fastloading.
Do an quick inital fillRect on theclock info area.
0.33: Make the border of the clock_info box extend all the way to the right of the screen.
0.34: Fix issue rendering ClockInfos with for fg+bg color set to the same (#2749)

View File

@ -100,11 +100,8 @@ let clockInfoMenu = clock_info.addInteractive(clockInfoItems, {
draw : (itm, info, options) => {
let hideClkInfo = info.text == null;
g.setColor(g.theme.fg);
g.fillRect(options.x, options.y, options.x+options.w, options.y+options.h);
g.setFontAlign(0,0);
g.setColor(g.theme.bg);
g.reset().setBgColor(g.theme.fg).clearRect(options.x, options.y, options.x+options.w, options.y+options.h);
g.setFontAlign(0,0).setColor(g.theme.bg);
if (options.focus){
let y = hideClkInfo ? options.y+20 : options.y+2;

View File

@ -1,7 +1,7 @@
{
"id": "bwclklite",
"name": "BW Clock Lite",
"version": "0.33",
"version": "0.34",
"description": "A very minimalistic clock. This version of BW Clock is quicker at the cost of the custom font.",
"readme": "README.md",
"icon": "app.png",

View File

@ -3,6 +3,7 @@
"version":"0.01",
"description": "For clocks that display 'clockinfo' (messages that can be cycled through using the clock_info module) this displays the day of the month in the icon, and the weekday",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "clkinfo",
"tags": "clkinfo,calendar",
"supports" : ["BANGLEJS2"],

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

1
apps/clkinfom/ChangeLog Normal file
View File

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

11
apps/clkinfom/README.md Normal file
View File

@ -0,0 +1,11 @@
# RAM Clock Info
![](app.png)
A clock info that displays the % memory used
## Screenshots
![](screenshot.png)
Written by: [Hugh Barney](https://github.com/hughbarney) For support and discussion please post in the [Bangle JS Forum](http://forum.espruino.com/microcosms/1424/)

BIN
apps/clkinfom/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

61
apps/clkinfom/clkinfo.js Normal file
View File

@ -0,0 +1,61 @@
(function () {
var timeout;
var debug = function(o) {
//console.log(o);
};
var clearTimer = function() {
if (timeout) {
clearTimeout(timeout);
timeout = undefined;
debug("timer cleared");
}
};
var queueRedraw = function() {
clearTimer();
timeout = setTimeout(function() {
timeout = undefined;
queueRedraw();
}, 60000);
info.items[0].emit("redraw");
debug("queued");
};
var img = function() {
return atob("GBgBAAAAAAAAAAAAB//gD//wH//4HgB4HAA4HAA4HAA4HDw4HDw4HDw4HDw4HAA4HAA4HAA4HgB4H//4D//wB//gAAAAAAAAAAAA");
};
var text = function() {
var val = process.memory(false);
return '' + Math.round(val.usage*100 / val.total) + '%';
};
var info = {
name: "Bangle",
items: [
{
name: "ram",
get: function () { return ({
img: img(),
text: text()
}); },
run : function() {
debug("run");
queueRedraw();
},
show: function () {
debug("show");
this.run();
},
hide: function() {
debug("hide");
clearTimer();
}
}
]
};
return info;
});

View File

@ -0,0 +1,15 @@
{
"id": "clkinfom",
"name": "RAM Clock Info",
"version":"0.01",
"description": "Clockinfo that displays % used memory",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "clkinfo",
"tags": "clkinfo",
"supports" : ["BANGLEJS2"],
"readme":"README.md",
"storage": [
{"name":"ram.clkinfo.js","url":"clkinfo.js"}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1,2 @@
0.01: New Widget!
0.02: Ensure that the generated image is transparent (2v18+)

View File

@ -0,0 +1,49 @@
(function() {
var heading, cnt;
function magHandler(m) {
var h = m.heading;
if (isNaN(heading) || isNaN(h))
heading = h;
else {
// Average
if (Math.abs(heading-h)>180) {
if (h<180 && heading>180) h+=360;
if (h>180 && heading<180) h-=360;
}
heading = heading*0.8 + h*0.2;
if (heading<0) heading+=360;
if (heading>=360) heading-=360;
}
// only draw 1 in 2 to try and save some power!
if (!(1&cnt++)) ci.items[0].emit('redraw');
}
var ci = {
name: "Bangle",
items: [
{ name : "Compass",
get : function() {
var g = Graphics.createArrayBuffer(24,24,1,{msb:true});
if (isNaN(heading))
g.drawLine(8,12,16,12);
else
g.fillPoly(g.transformVertices([0,-10,4,10,-4,10],{x:12,y:12,rotate:-heading/57}));
g.transparent=0; // only works on 2v18+, ignored otherwise (makes image background transparent)
return { text : isNaN(heading)?"--":Math.round(heading),
v : 0|heading, min : 0, max : 360,
img : g.asImage("string") }},
show : function() {
Bangle.setCompassPower(1,"clkinfomag");
Bangle.on('mag',magHandler);
cnt=0;
heading = undefined;
},
hide : function() {
Bangle.removeListener('mag', magHandler);
Bangle.setCompassPower(0,"clkinfomag");
},
run : function() { Bangle.resetCompass(); }
}
]
};
return ci;
}) // must not have a semi-colon!

BIN
apps/clkinfomag/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,12 @@
{ "id": "clkinfomag",
"name": "Compass Clockinfo",
"version":"0.02",
"description": "Extra information to add to clock screens. When selected, displays the compass heading and an arrow pointing North",
"icon": "icon.png",
"type": "clkinfo",
"tags": "clkinfo,compass,mag,magnetometer",
"supports" : ["BANGLEJS2"],
"storage": [
{"name":"clkinfomag.clkinfo.js","url":"clkinfo.js"}
]
}

View File

@ -1,2 +1,6 @@
0.01: Moved from modules/clock_info.js
0.02: Fix settings page
0.03: Reported image for battery now reflects charge level
0.04: On 2v18+ firmware, we can now stop swipe events from being handled by other apps
eg. when a clockinfo is selected, swipes won't affect swipe-down widgets
0.05: Reported image for battery is now transparent (2v18+)

View File

@ -25,7 +25,7 @@ By default Clock Info provides:
But by installing other apps that are tagged with the type `clkinfo` you can
add extra features. For example [Sunrise Clockinfo](http://banglejs.com/apps/?id=clkinfosunrise)
A full list is available at https://banglejs.com/apps/?q=clkinfo
A full list is available at https://banglejs.com/apps/?c=clkinfo
## Settings

View File

@ -53,10 +53,18 @@ exports.load = function() {
items: [
{ name : "Battery",
hasRange : true,
get : () => { let v = E.getBattery(); return {
text : v + "%", v : v, min:0, max:100,
img : atob(Bangle.isCharging() ? "GBiBAAABgAADwAAHwAAPgACfAAHOAAPkBgHwDwP4Hwf8Pg/+fB//OD//kD//wD//4D//8D//4B//QB/+AD/8AH/4APnwAHAAACAAAA==" : "GBiBAAAAAAAAAAAAAAAAAAAAAD//+P///IAAAr//Ar//Ar//A7//A7//A7//A7//Ar//AoAAAv///D//+AAAAAAAAAAAAAAAAAAAAA==")
}},
get : () => { let v = E.getBattery();
var img;
if (!Bangle.isCharging()) {
var s=24, g=Graphics.createArrayBuffer(24,24,1,{msb:true});
g.fillRect(0,6,s-3,18).clearRect(2,8,s-5,16).fillRect(s-2,10,s,15).fillRect(3,9,3+v*(s-9)/100,15);
g.transparent=0; // only works on 2v18+, ignored otherwise (makes image background transparent)
img = g.asImage("string");
} else img=atob("GBiBAAABgAADwAAHwAAPgACfAAHOAAPkBgHwDwP4Hwf8Pg/+fB//OD//kD//wD//4D//8D//4B//QB/+AD/8AH/4APnwAHAAACAAAA==");
return {
text : v + "%", v : v, min:0, max:100, img : img
}
},
show : function() { this.interval = setInterval(()=>this.emit('redraw'), 60000); Bangle.on("charging", batteryUpdateHandler); batteryUpdateHandler(); },
hide : function() { clearInterval(this.interval); delete this.interval; Bangle.removeListener("charging", batteryUpdateHandler); },
},
@ -261,6 +269,8 @@ exports.addInteractive = function(menu, options) {
let settings = exports.loadSettings();
settings.apps[appName] = {a:options.menuA,b:options.menuB};
require("Storage").writeJSON("clock_info.json",settings);
// On 2v18+ firmware we can stop other event handlers from being executed since we handled this
E.stopEventPropagation&&E.stopEventPropagation();
}
Bangle.on("swipe",swipeHandler);
let touchHandler, lockHandler;

View File

@ -1,7 +1,7 @@
{ "id": "clock_info",
"name": "Clock Info Module",
"shortName": "Clock Info",
"version":"0.02",
"version":"0.05",
"description": "A library used by clocks to provide extra information on the clock face (Altitude, BPM, etc)",
"icon": "app.png",
"type": "module",

View File

@ -5,7 +5,7 @@
"version": "0.08",
"description": "Read BLE enabled cycling speed and cadence sensor and display readings on watch",
"icon": "icons8-cycling-48.png",
"tags": "outdoors,exercise,ble,bluetooth",
"tags": "outdoors,exercise,ble,bluetooth,bike,cycle,bicycle",
"supports": ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
"storage": [

View File

@ -1 +1,4 @@
0.01: New app!
0.02: Allow boot exceptions, e.g. to load DST
0.03: Permit exceptions to load in low-power mode, e.g. daylight saving time.
Also avoid polluting global scope.

View File

@ -35,14 +35,16 @@ var draw = function () {
var dateStr = require("locale").date(date, 0).toUpperCase() +
"\n" +
require("locale").dow(date, 0).toUpperCase();
var x2 = x + 6;
var y2 = y + 66;
g.reset()
.clearRect(Bangle.appRect)
.setFont("Vector", 55)
.setFontAlign(0, 0)
.drawString(timeStr, x, y)
.setFont("Vector", 24)
.drawString(dateStr, x, y + 56)
.drawString("".concat(E.getBattery(), "%"), x, y + 104);
.drawString(dateStr, x2, y2)
.drawString("".concat(E.getBattery(), "%"), x2, y2 + 48);
if (nextDraw)
clearTimeout(nextDraw);
nextDraw = setTimeout(function () {
@ -75,9 +77,9 @@ var reload = function () {
};
reload();
Bangle.emit("drained", E.getBattery());
var _a = require("Storage").readJSON("".concat(app, ".setting.json"), true) || {}, _b = _a.disableBoot, disableBoot = _b === void 0 ? false : _b, _c = _a.restore, restore = _c === void 0 ? 20 : _c;
var _a = require("Storage").readJSON("".concat(app, ".setting.json"), true) || {}, _b = _a.keepStartup, keepStartup = _b === void 0 ? true : _b, _c = _a.restore, restore = _c === void 0 ? 20 : _c, _d = _a.exceptions, exceptions = _d === void 0 ? ["widdst.0"] : _d;
function drainedRestore() {
if (disableBoot) {
if (!keepStartup) {
try {
eval(require('Storage').read('bootupdate.js'));
}
@ -87,16 +89,28 @@ function drainedRestore() {
}
load();
}
if (disableBoot) {
var checkCharge_1 = function () {
if (E.getBattery() < restore)
return;
drainedRestore();
};
if (Bangle.isCharging())
checkCharge_1();
Bangle.on("charging", function (charging) {
if (charging)
checkCharge_1();
});
var checkCharge = function () {
if (E.getBattery() < restore)
return;
drainedRestore();
};
if (Bangle.isCharging())
checkCharge();
Bangle.on("charging", function (charging) {
if (charging)
checkCharge();
});
if (!keepStartup) {
var storage = require("Storage");
for (var _i = 0, exceptions_1 = exceptions; _i < exceptions_1.length; _i++) {
var boot = exceptions_1[_i];
try {
var js = storage.read("".concat(boot, ".boot.js"));
if (js)
eval(js);
}
catch (e) {
console.log("error loading boot exception \"".concat(boot, "\": ").concat(e));
}
}
}

View File

@ -52,6 +52,8 @@ const draw = () => {
const dateStr = require("locale").date(date, 0).toUpperCase() +
"\n" +
require("locale").dow(date, 0).toUpperCase();
const x2 = x + 6;
const y2 = y + 66;
g.reset()
.clearRect(Bangle.appRect)
@ -59,8 +61,8 @@ const draw = () => {
.setFontAlign(0, 0)
.drawString(timeStr, x, y)
.setFont("Vector", 24)
.drawString(dateStr, x, y + 56)
.drawString(`${E.getBattery()}%`, x, y + 104);
.drawString(dateStr, x2, y2)
.drawString(`${E.getBattery()}%`, x2, y2 + 48);
if(nextDraw) clearTimeout(nextDraw);
nextDraw = setTimeout(() => {
@ -97,12 +99,12 @@ reload();
Bangle.emit("drained", E.getBattery());
// restore normal boot on charge
const { disableBoot = false, restore = 20 }: DrainedSettings
const { keepStartup = true, restore = 20, exceptions = ["widdst.0"] }: DrainedSettings
= require("Storage").readJSON(`${app}.setting.json`, true) || {};
// re-enable normal boot code when we're above a threshold:
function drainedRestore() { // "public", to allow users to call
if(disableBoot){
if(!keepStartup){
try{
eval(require('Storage').read('bootupdate.js'));
}catch(e){
@ -112,16 +114,26 @@ function drainedRestore() { // "public", to allow users to call
load(); // necessary after updating boot.0
}
if(disableBoot){
const checkCharge = () => {
if(E.getBattery() < restore) return;
drainedRestore();
};
const checkCharge = () => {
if(E.getBattery() < restore) return;
drainedRestore();
};
if (Bangle.isCharging())
checkCharge();
if (Bangle.isCharging())
checkCharge();
Bangle.on("charging", charging => {
if(charging) checkCharge();
});
Bangle.on("charging", charging => {
if(charging) checkCharge();
});
if(!keepStartup){
const storage = require("Storage");
for(const boot of exceptions){
try{
const js = storage.read(`${boot}.boot.js`);
if(js) eval(js);
}catch(e){
console.log(`error loading boot exception "${boot}": ${e}`);
}
}
}

View File

@ -1,13 +1,13 @@
{
var _a = require("Storage").readJSON("drained.setting.json", true) || {}, _b = _a.battery, threshold_1 = _b === void 0 ? 5 : _b, _c = _a.interval, interval = _c === void 0 ? 10 : _c, _d = _a.disableBoot, disableBoot_1 = _d === void 0 ? false : _d;
(function () {
var _a = require("Storage").readJSON("drained.setting.json", true) || {}, _b = _a.battery, threshold = _b === void 0 ? 5 : _b, _c = _a.interval, interval = _c === void 0 ? 10 : _c, _d = _a.keepStartup, keepStartup = _d === void 0 ? true : _d;
drainedInterval = setInterval(function () {
if (Bangle.isCharging())
return;
if (E.getBattery() > threshold_1)
if (E.getBattery() > threshold)
return;
var app = "drained.app.js";
if (disableBoot_1)
if (!keepStartup)
require("Storage").write(".boot0", "if(typeof __FILE__ === \"undefined\" || __FILE__ !== \"".concat(app, "\") setTimeout(load, 100, \"").concat(app, "\");"));
load(app);
}, interval * 60 * 1000);
}
})();

View File

@ -1,5 +1,5 @@
{
const { battery: threshold = 5, interval = 10, disableBoot = false }: DrainedSettings
(() => {
const { battery: threshold = 5, interval = 10, keepStartup = true }: DrainedSettings
= require("Storage").readJSON(`drained.setting.json`, true) || {};
drainedInterval = setInterval(() => {
@ -10,7 +10,7 @@ drainedInterval = setInterval(() => {
const app = "drained.app.js";
if(disableBoot)
if(!keepStartup)
require("Storage").write(
".boot0",
`if(typeof __FILE__ === "undefined" || __FILE__ !== "${app}") setTimeout(load, 100, "${app}");`
@ -18,4 +18,4 @@ drainedInterval = setInterval(() => {
load(app);
}, interval * 60 * 1000);
}
})()

View File

@ -1,7 +1,7 @@
{
"id": "drained",
"name": "Drained",
"version": "0.01",
"version": "0.03",
"description": "Switches to displaying a simple clock when the battery percentage is low, and disables some peripherals",
"readme": "README.md",
"icon": "icon.png",
@ -14,5 +14,8 @@
{"name":"drained.app.js","url":"app.js"},
{"name":"drained.settings.js","url":"settings.js"},
{"name":"drained.img","url":"app-icon.js","evaluate":true}
],
"data": [
{"name":"drained.setting.json"}
]
}

View File

@ -1,26 +1,19 @@
(function (back) {
var _a, _b, _c, _d;
var _a, _b, _c, _d, _e;
var SETTINGS_FILE = "drained.setting.json";
var storage = require("Storage");
var settings = storage.readJSON(SETTINGS_FILE, true) || {};
(_a = settings.battery) !== null && _a !== void 0 ? _a : (settings.battery = 5);
(_b = settings.restore) !== null && _b !== void 0 ? _b : (settings.restore = 20);
(_c = settings.interval) !== null && _c !== void 0 ? _c : (settings.interval = 10);
(_d = settings.disableBoot) !== null && _d !== void 0 ? _d : (settings.disableBoot = false);
(_d = settings.keepStartup) !== null && _d !== void 0 ? _d : (settings.keepStartup = true);
(_e = settings.exceptions) !== null && _e !== void 0 ? _e : (settings.exceptions = ["widdst.0"]);
var save = function () {
storage.writeJSON(SETTINGS_FILE, settings);
};
E.showMenu({
var menu = {
"": { "title": "Drained" },
"< Back": back,
"Keep startup code": {
value: settings.disableBoot,
format: function () { return settings.disableBoot ? "No" : "Yes"; },
onchange: function () {
settings.disableBoot = !settings.disableBoot;
save();
},
},
"Trigger at batt%": {
value: settings.battery,
min: 0,
@ -54,5 +47,44 @@
save();
},
},
});
"Keep startup code": {
value: settings.keepStartup,
onchange: function (b) {
settings.keepStartup = b;
save();
updateAndRedraw();
},
},
};
var updateAndRedraw = function () {
setTimeout(function () { E.showMenu(menu); }, 10);
if (settings.keepStartup) {
delete menu["Startup exceptions"];
return;
}
menu["Startup exceptions"] = function () { return E.showMenu(bootExceptions); };
var bootExceptions = {
"": { "title": "Startup exceptions" },
"< Back": function () { return E.showMenu(menu); },
};
storage.list(/\.boot\.js/)
.map(function (name) { return name.replace(".boot.js", ""); })
.forEach(function (name) {
bootExceptions[name] = {
value: settings.exceptions.indexOf(name) >= 0,
onchange: function (b) {
if (b) {
settings.exceptions.push(name);
}
else {
var i = settings.exceptions.indexOf(name);
if (i >= 0)
settings.exceptions.splice(i, 1);
}
save();
},
};
});
};
updateAndRedraw();
});

View File

@ -2,7 +2,8 @@ type DrainedSettings = {
battery?: number,
restore?: number,
interval?: number,
disableBoot?: ShortBoolean,
keepStartup?: ShortBoolean,
exceptions?: string[],
};
(back => {
@ -13,23 +14,16 @@ type DrainedSettings = {
settings.battery ??= 5;
settings.restore ??= 20;
settings.interval ??= 10;
settings.disableBoot ??= false;
settings.keepStartup ??= true;
settings.exceptions ??= ["widdst.0"]; // daylight savings
const save = () => {
storage.writeJSON(SETTINGS_FILE, settings)
};
E.showMenu({
const menu: Menu = {
"": { "title": "Drained" },
"< Back": back,
"Keep startup code": {
value: settings.disableBoot,
format: () => settings.disableBoot ? "No" : "Yes",
onchange: () => {
settings.disableBoot = !settings.disableBoot;
save();
},
},
"Trigger at batt%": {
value: settings.battery,
min: 0,
@ -63,5 +57,48 @@ type DrainedSettings = {
save();
},
},
});
"Keep startup code": {
value: settings.keepStartup as boolean,
onchange: (b: boolean) => {
settings.keepStartup = b;
save();
updateAndRedraw();
},
},
};
const updateAndRedraw = () => {
// will change the menu, queue redraw:
setTimeout(() => { E.showMenu(menu) }, 10);
if (settings.keepStartup) {
delete menu["Startup exceptions"];
return;
}
menu["Startup exceptions"] = () => E.showMenu(bootExceptions);
const bootExceptions: Menu = {
"": { "title" : "Startup exceptions" },
"< Back": () => E.showMenu(menu),
};
storage.list(/\.boot\.js/)
.map(name => name.replace(".boot.js", ""))
.forEach((name: string) => {
bootExceptions[name] = {
value: settings.exceptions!.indexOf(name) >= 0,
onchange: (b: boolean) => {
if (b) {
settings.exceptions!.push(name);
} else {
const i = settings.exceptions!.indexOf(name);
if (i >= 0) settings.exceptions!.splice(i, 1);
}
save();
},
};
});
};
updateAndRedraw();
}) satisfies SettingsFunc

View File

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

View File

@ -0,0 +1,34 @@
# Folder launcher
Launcher that allows you to put your apps into folders
![](screenshot1.png)
![](screenshot2.png)
## Launcher UI
The apps and folders will be presented in a grid layout, configurable in size. Tapping on a folder will open the folder. Folders can contain both apps and more folders. Tapping on an app will launch the app. If there is more than one page, there will be a scroll bar on the right side to indicate how far through the list you have scrolled. Folders will be displayed before apps, in the order that they were added. Apps will honor their sort order, if it exists.
Swiping up and down will scroll. Swiping from the left, using the back button, or pressing BTN1 will take you up a level to the folder containing the current one, or exit the launcher if you are at the top level.
## Settings menu
* Show clocks / Show launcher: Whether clock and launcher apps are displayed in the UI to be launched. The default is no.
* Hidden apps: Displays the list of installed apps, enabling them to be manually hidden. (Or unhidden, if hidden from here.) This may be convenient for apps that you have some other shortcut to access, or apps that are only shortcuts to an infrequently used settings menu. By default, no apps are hidden.
* Display:
* Rows: The side length of the square grid. Lowest value is 1, no upper limit. The default is 2, but 3 is also convenient.
* Show icons?: Whether app and folder icons are displayed. The default is yes.
* Font size: How much height of each grid cell to allocate for the app or folder name. If size zero is selected, there will be no title for apps and folders will use a size of 12. (This is important because it is not possible to distinguish folders solely by icon.) The default is 12.
To prevent the launcher from becoming unusable, if neither icons nor text are enabled in the settings menu, text will still be drawn.
* Timeout: If the launcher is left idle for too long, return to the clock. This is convenient if you often accidentally open the launcher without noticing. At zero seconds, the timeout is disabled. The default is 30 seconds.
* Folder management: Open the folder management menu for the root folder. (The folder first displayed when opening the launcher.) The folder management menu contains the following:
* New subfolder: Open the keyboard to enter the name for a new subfolder to be created. If left blank or given the name of an existing subfolder, no folder is created. If a subfolder is created, open the folder management menu for the new folder.
* Move app here: Display a list of apps. Selecting one moves it into the folder.
* One menu entry for each subfolder, which opens the folder management menu for that subfolder.
* View apps: Only present if this folder contains apps, Display a menu of all apps in the folder. This is for information only, tapping the apps does nothing.
* Delete folder: Only present if not viewing the root folder. Delete the current folder and move all apps into the parent folder.

205
apps/folderlaunch/app.js Normal file
View File

@ -0,0 +1,205 @@
{
var loader = require('folderlaunch-configLoad.js');
var storage_1 = require('Storage');
var FOLDER_ICON_1 = require("heatshrink").decompress(atob("mEwwMA///wAJCAoPAAongAonwAon4Aon8Aon+Aon/AooA/AH4A/AFgA="));
var config_1 = loader.getConfig();
var timeout_1;
var resetTimeout_1 = function () {
if (timeout_1) {
clearTimeout(timeout_1);
}
if (config_1.timeout != 0) {
timeout_1 = setTimeout(function () {
Bangle.showClock();
}, config_1.timeout);
}
};
var folderPath_1 = [];
var getFolder_1 = function (folderPath) {
var result = config_1.rootFolder;
for (var _i = 0, folderPath_2 = folderPath; _i < folderPath_2.length; _i++) {
var folderName = folderPath_2[_i];
result = result.folders[folderName];
}
nPages_1 = Math.ceil((result.apps.length + Object.keys(result.folders).length) / (config_1.display.rows * config_1.display.rows));
return result;
};
var folder_1 = getFolder_1(folderPath_1);
var getFontSize_1 = function (length, maxWidth, minSize, maxSize) {
var size = Math.floor(maxWidth / length);
size *= (20 / 12);
if (size < minSize)
return minSize;
else if (size > maxSize)
return maxSize;
else
return Math.floor(size);
};
var grid_1 = [];
for (var x = 0; x < config_1.display.rows; x++) {
grid_1.push([]);
for (var y = 0; y < config_1.display.rows; y++) {
grid_1[x].push({
type: 'empty',
id: ''
});
}
}
var render_1 = function () {
var gridSize = config_1.display.rows * config_1.display.rows;
var startIndex = page_1 * gridSize;
for (var i = 0; i < gridSize; i++) {
var y = Math.floor(i / config_1.display.rows);
var x = i % config_1.display.rows;
var folderIndex = startIndex + i;
var appIndex = folderIndex - Object.keys(folder_1.folders).length;
if (folderIndex < Object.keys(folder_1.folders).length) {
grid_1[x][y].type = 'folder';
grid_1[x][y].id = Object.keys(folder_1.folders)[folderIndex];
}
else if (appIndex < folder_1.apps.length) {
grid_1[x][y].type = 'app';
grid_1[x][y].id = folder_1.apps[appIndex];
}
else
grid_1[x][y].type = 'empty';
}
var squareSize = (g.getHeight() - 24) / config_1.display.rows;
if (!config_1.display.icon && !config_1.display.font)
config_1.display.font = 12;
g.clearRect(0, 24, g.getWidth(), g.getHeight())
.reset()
.setFontAlign(0, -1);
var empty = true;
for (var x = 0; x < config_1.display.rows; x++) {
for (var y = 0; y < config_1.display.rows; y++) {
var entry = grid_1[x][y];
var icon = void 0;
var text = void 0;
var fontSize = void 0;
switch (entry.type) {
case 'app':
var app_1 = storage_1.readJSON(entry.id + '.info', false);
icon = storage_1.read(app_1.icon);
text = app_1.name;
empty = false;
fontSize = config_1.display.font;
break;
case 'folder':
icon = FOLDER_ICON_1;
text = entry.id;
empty = false;
fontSize = config_1.display.font ? config_1.display.font : 12;
break;
default:
continue;
}
var iconSize = config_1.display.icon ? Math.max(0, squareSize - fontSize) : 0;
var iconScale = iconSize / 48;
var posX = 12 + (x * squareSize);
var posY = 24 + (y * squareSize);
if (config_1.display.icon && iconSize != 0)
try {
g.drawImage(icon, posX + (squareSize - iconSize) / 2, posY, { scale: iconScale });
}
catch (error) {
console.log("Failed to draw icon for ".concat(text, ": ").concat(error));
console.log(icon);
}
if (fontSize)
g.setFont('Vector', getFontSize_1(text.length, squareSize, 6, squareSize - iconSize))
.drawString(text, posX + (squareSize / 2), posY + iconSize);
}
}
if (empty)
E.showMessage('Folder is empty. Swipe from left, back button, or BTN1 to go back.');
if (nPages_1 > 1) {
var barSize = (g.getHeight() - 24) / nPages_1;
var barTop = 24 + (page_1 * barSize);
g.fillRect(g.getWidth() - 8, barTop, g.getWidth() - 4, barTop + barSize);
}
};
var onTouch = function (_button, xy) {
var x = Math.floor((xy.x - 12) / ((g.getWidth() - 24) / config_1.display.rows));
if (x < 0)
x = 0;
else if (x >= config_1.display.rows)
x = config_1.display.rows - 1;
var y = Math.floor((xy.y - 24) / ((g.getHeight() - 24) / config_1.display.rows));
if (y < 0)
y = 0;
else if (y >= config_1.display.rows)
y = config_1.display.rows - 1;
var entry = grid_1[x][y];
switch (entry.type) {
case "app":
Bangle.buzz();
var infoFile = storage_1.readJSON(entry.id + '.info', false);
load(infoFile.src);
break;
case "folder":
Bangle.buzz();
resetTimeout_1();
page_1 = 0;
folderPath_1.push(entry.id);
folder_1 = getFolder_1(folderPath_1);
render_1();
break;
default:
resetTimeout_1();
break;
}
};
var page_1 = 0;
var nPages_1;
var onSwipe = function (lr, ud) {
if (lr == 1 && ud == 0) {
onBackButton_1();
return;
}
else if (ud == 1) {
resetTimeout_1();
if (page_1 == 0) {
Bangle.buzz(200);
return;
}
else
page_1--;
}
else if (ud == -1) {
resetTimeout_1();
if (page_1 == nPages_1 - 1) {
Bangle.buzz(200);
return;
}
else
page_1++;
}
render_1();
};
var onBackButton_1 = function () {
Bangle.buzz();
if (folderPath_1.length == 0)
Bangle.showClock();
else {
folderPath_1.pop();
folder_1 = getFolder_1(folderPath_1);
resetTimeout_1();
page_1 = 0;
render_1();
}
};
Bangle.loadWidgets();
Bangle.drawWidgets();
Bangle.setUI({
mode: 'custom',
back: onBackButton_1,
btn: onBackButton_1,
swipe: onSwipe,
touch: onTouch,
remove: function () { if (timeout_1)
clearTimeout(timeout_1); }
});
resetTimeout_1();
render_1();
}

271
apps/folderlaunch/app.ts Normal file
View File

@ -0,0 +1,271 @@
{
const loader = require('folderlaunch-configLoad.js')
const storage = require('Storage')
const FOLDER_ICON = require("heatshrink").decompress(atob("mEwwMA///wAJCAoPAAongAonwAon4Aon8Aon+Aon/AooA/AH4A/AFgA="))
let config: Config = loader.getConfig();
let timeout: any;
/**
* If a timeout to return to the clock is set, reset it.
*/
let resetTimeout = function () {
if (timeout) {
clearTimeout(timeout);
}
if (config.timeout != 0) {
timeout = setTimeout(() => {
Bangle.showClock();
}, config.timeout);
}
}
let folderPath: Array<string> = [];
/**
* Get the folder at the provided path
*
* @param folderPath a path for the desired folder
* @return the folder that was found
*/
let getFolder = function (folderPath: Array<string>): Folder {
let result: Folder = config.rootFolder;
for (let folderName of folderPath)
result = result.folders[folderName]!;
nPages = Math.ceil((result.apps.length + Object.keys(result.folders).length) / (config.display.rows * config.display.rows));
return result;
}
let folder: Folder = getFolder(folderPath);
/**
* Determine the font size needed to fit a string of the given length widthin maxWidth number of pixels, clamped between minSize and maxSize
*
* @param length the number of characters of the string
* @param maxWidth the maximum allowable width
* @param minSize the minimum acceptable font size
* @param maxSize the maximum acceptable font size
* @return the calculated font size
*/
let getFontSize = function (length: number, maxWidth: number, minSize: number, maxSize: number): number {
let size = Math.floor(maxWidth / length); //Number of pixels of width available to character
size *= (20 / 12); //Convert to height, assuming 20 pixels of height for every 12 of width
// Clamp to within range
if (size < minSize) return minSize;
else if (size > maxSize) return maxSize;
else return Math.floor(size);
}
// grid[x][y] = id of app at column x row y, or undefined if no app displayed there
let grid: Array<Array<GridEntry>> = [];
for (let x = 0; x < config.display.rows; x++) {
grid.push([]);
for (let y = 0; y < config.display.rows; y++) {
grid[x]!.push({
type: 'empty',
id: ''
});
}
}
let render = function () {
let gridSize: number = config.display.rows * config.display.rows;
let startIndex: number = page * gridSize; // Start at this position in the folders
// Populate the grid
for (let i = 0; i < gridSize; i++) {
// Calculate coordinates
let y = Math.floor(i / config.display.rows);
let x = i % config.display.rows;
// Try to place a folder
let folderIndex = startIndex + i;
let appIndex = folderIndex - Object.keys(folder.folders).length;
if (folderIndex < Object.keys(folder.folders).length) {
grid[x]![y]!.type = 'folder';
grid[x]![y]!.id = Object.keys(folder.folders)[folderIndex];
}
// If that fails, try to place an app
else if (appIndex < folder.apps.length) {
grid[x]![y]!.type = 'app';
grid[x]![y]!.id = folder.apps[appIndex]!;
}
// If that also fails, make the space empty
else grid[x]![y]!.type = 'empty';
}
// Prepare to draw the grid
let squareSize: number = (g.getHeight() - 24) / config.display.rows;
if (!config.display.icon && !config.display.font) config.display.font = 12; // Fallback in case user disabled both icon and text
g.clearRect(0, 24, g.getWidth(), g.getHeight())
.reset()
.setFontAlign(0, -1);
// Actually draw the grid
let empty = true; // Set to empty upon drawing something, so we can know whether to draw a nice message rather than leaving the screen completely blank
for (let x = 0; x < config.display.rows; x++) {
for (let y = 0; y < config.display.rows; y++) {
let entry: GridEntry = grid[x]![y]!;
let icon: string | ArrayBuffer;
let text: string;
let fontSize: number;
// Get the icon and text, skip if the space is empty. Always draw text for folders even if disabled
switch (entry.type) {
case 'app':
let app: AppInfo = storage.readJSON(entry.id + '.info', false);
icon = storage.read(app.icon!)!;
text = app.name;
empty = false;
fontSize = config.display.font;
break;
case 'folder':
icon = FOLDER_ICON;
text = entry.id;
empty = false;
fontSize = config.display.font ? config.display.font : 12;
break;
default:
continue;
}
// Calculate position and icon size
let iconSize = config.display.icon ? Math.max(0, squareSize - fontSize) : 0; // If icon is disabled, stay at zero. Otherwise, subtract font size from square
let iconScale: number = iconSize / 48;
let posX = 12 + (x * squareSize);
let posY = 24 + (y * squareSize);
// Draw the icon
if (config.display.icon && iconSize != 0)
try {
g.drawImage(icon, posX + (squareSize - iconSize) / 2, posY, { scale: iconScale });
} catch (error) {
console.log(`Failed to draw icon for ${text}: ${error}`);
console.log(icon);
}
// Draw the text
if (fontSize)
g.setFont('Vector', getFontSize(text.length, squareSize, 6, squareSize - iconSize))
.drawString(text, posX + (squareSize / 2), posY + iconSize);
}
}
// Draw a nice message if there is nothing to see, so the user doesn't think the app is broken
if (empty) E.showMessage(/*LANG*/'Folder is empty. Swipe from left, back button, or BTN1 to go back.');
// Draw a scroll bar if necessary
if (nPages > 1) { // Avoid divide-by-zero and pointless scroll bars
let barSize = (g.getHeight() - 24) / nPages;
let barTop = 24 + (page * barSize);
g.fillRect(
g.getWidth() - 8, barTop,
g.getWidth() - 4, barTop + barSize);
}
}
/**
* Handle a touch
*
* @param _button 1 for left half, 2 for right half
* @param xy postion on screen
*/
let onTouch = function (_button: number, xy: { x: number, y: number } | undefined) {
// Determine which grid cell was tapped
let x: number = Math.floor((xy!.x - 12) / ((g.getWidth() - 24) / config.display.rows));
if (x < 0) x = 0;
else if (x >= config.display.rows) x = config.display.rows - 1;
let y: number = Math.floor((xy!.y - 24) / ((g.getHeight() - 24) / config.display.rows));
if (y < 0) y = 0;
else if (y >= config.display.rows) y = config.display.rows - 1;
// Handle the grid cell
let entry: GridEntry = grid[x]![y]!;
switch (entry.type) {
case "app":
Bangle.buzz();
let infoFile = storage.readJSON(entry.id + '.info', false);
load(infoFile.src);
break;
case "folder":
Bangle.buzz();
resetTimeout();
page = 0;
folderPath.push(entry.id);
folder = getFolder(folderPath);
render();
break;
default:
resetTimeout();
break;
}
}
let page: number = 0;
let nPages: number; // Set when setting folder
/**
* Handle a swipe
*
* A swipe from left is treated as the back button. Up and down swipes change pages
*
* @param lr -1 if left, 0 if pure up/down, 1 if right
* @param ud -1 if up, 0 if pure left/right, 1 if down
*/
let onSwipe = function (lr: -1 | 0 | 1 | undefined, ud: -1 | 0 | 1 | undefined) {
if (lr == 1 && ud == 0) {
onBackButton();
return;
} else if (ud == 1) {
resetTimeout();
if (page == 0) {
Bangle.buzz(200);
return;
} else page--;
} else if (ud == -1) {
resetTimeout();
if (page == nPages - 1) {
Bangle.buzz(200);
return;
} else page++;
}
// If we reached this point, the page number has been changed and is valid.
render();
}
/**
* Go back up a level. If already at the root folder, exit the launcher
*/
let onBackButton = () => {
Bangle.buzz();
if (folderPath.length == 0)
Bangle.showClock();
else {
folderPath.pop();
folder = getFolder(folderPath);
resetTimeout();
page = 0;
render();
}
}
Bangle.loadWidgets();
Bangle.drawWidgets();
Bangle.setUI({
mode: 'custom',
back: onBackButton,
btn: onBackButton,
swipe: onSwipe,
touch: onTouch,
remove: () => { if (timeout) clearTimeout(timeout); }
});
resetTimeout();
render();
}

View File

@ -0,0 +1,105 @@
var storage = require("Storage");
var SETTINGS_FILE = "folderlaunch.json";
var DEFAULT_CONFIG = {
showClocks: false,
showLaunchers: false,
hidden: [],
display: {
rows: 2,
icon: true,
font: 12
},
timeout: 30000,
rootFolder: {
folders: {},
apps: []
},
apps: {},
hash: 0
};
function clearFolder(folder) {
for (var childName in folder.folders)
folder.folders[childName] = clearFolder(folder.folders[childName]);
folder.apps = [];
return folder;
}
function cleanAndSave(config) {
var infoFiles = storage.list(/\.info$/);
var installedAppIds = [];
for (var _i = 0, infoFiles_1 = infoFiles; _i < infoFiles_1.length; _i++) {
var infoFile = infoFiles_1[_i];
installedAppIds.push(storage.readJSON(infoFile, true).id);
}
var toRemove = [];
for (var appId in config.apps)
if (!installedAppIds.includes(appId))
toRemove.push(appId);
for (var _a = 0, toRemove_1 = toRemove; _a < toRemove_1.length; _a++) {
var appId = toRemove_1[_a];
delete config.apps[appId];
}
storage.writeJSON(SETTINGS_FILE, config);
return config;
}
var infoFileSorter = function (a, b) {
var aJson = storage.readJSON(a, false);
var bJson = storage.readJSON(b, false);
var n = (0 | aJson.sortorder) - (0 | bJson.sortorder);
if (n)
return n;
if (aJson.name < bJson.name)
return -1;
if (aJson.name > bJson.name)
return 1;
return 0;
};
module.exports = {
cleanAndSave: cleanAndSave,
infoFileSorter: infoFileSorter,
getConfig: function () {
var config = storage.readJSON(SETTINGS_FILE, true) || DEFAULT_CONFIG;
if (config.hash == storage.hash(/\.info$/)) {
return config;
}
E.showMessage('Rebuilding cache...');
config.rootFolder = clearFolder(config.rootFolder);
var infoFiles = storage.list(/\.info$/);
infoFiles.sort(infoFileSorter);
for (var _i = 0, infoFiles_2 = infoFiles; _i < infoFiles_2.length; _i++) {
var infoFile = infoFiles_2[_i];
var app_1 = storage.readJSON(infoFile, false);
if ((!config.showClocks && app_1.type == 'clock') ||
(!config.showLaunchers && app_1.type == 'launch') ||
(app_1.type == 'widget') ||
(!app_1.src)) {
if (Object.keys(config.hidden).includes(app_1.id))
delete config.apps[app_1.id];
continue;
}
if (!config.apps.hasOwnProperty(app_1.id)) {
config.apps[app_1.id] = {
folder: [],
nagged: false
};
}
if (config.hidden.includes(app_1.id))
continue;
var curFolder = config.rootFolder;
var depth = 0;
for (var _a = 0, _b = config.apps[app_1.id].folder; _a < _b.length; _a++) {
var folderName = _b[_a];
if (curFolder.folders.hasOwnProperty(folderName)) {
curFolder = curFolder.folders[folderName];
depth++;
}
else {
config.apps[app_1.id].folder = config.apps[app_1.id].folder.slice(0, depth);
break;
}
}
curFolder.apps.push(app_1.id);
}
config.hash = storage.hash(/\.info$/);
return cleanAndSave(config);
}
};

View File

@ -0,0 +1,156 @@
const storage = require("Storage");
const SETTINGS_FILE: string = "folderlaunch.json";
const DEFAULT_CONFIG: Config = {
showClocks: false,
showLaunchers: false,
hidden: [],
display: {
rows: 2,
icon: true,
font: 12
},
timeout: 30000,
rootFolder: {
folders: {},
apps: []
},
apps: {},
hash: 0
}
/**
* Recursively remove all apps from a folder
*
* @param folder the folder to clean
* @return the folder with all apps removed
*/
function clearFolder(folder: Folder): Folder {
for (let childName in folder.folders)
folder.folders[childName] = clearFolder(folder.folders[childName]!);
folder.apps = [];
return folder;
}
/**
* Clean and save the configuration.
*
* Assume that:
* - All installed apps have an appInfo entry
* - References to nonexistent folders have been removed from appInfo
* And therefore we do not need to do this ourselves.
* Note: It is not a real problem if the assumptions are not true. If this was called by getConfig, the assumptions are already taken care of. If this was called somewhere else, they will be taken care of the next time getConfig is called.
*
* Perform the following cleanup:
* - Remove appInfo entries for nonexistent apps, to prevent irrelevant data invisible to the user from accumulating
*
* @param config the configuration to be cleaned
* @return the cleaned configuration
*/
function cleanAndSave(config: Config): Config {
// Get the list of installed apps
let infoFiles: Array<string> = storage.list(/\.info$/);
let installedAppIds: Array<string> = [];
for (let infoFile of infoFiles)
installedAppIds.push(storage.readJSON(infoFile, true).id);
// Remove nonexistent apps from appInfo
let toRemove: Array<string> = [];
for (let appId in config.apps)
if (!installedAppIds.includes(appId))
toRemove.push(appId);
for (let appId of toRemove)
delete config.apps[appId];
// Save and return
storage.writeJSON(SETTINGS_FILE, config);
return config;
}
/**
* Comparator function to sort a list of app info files.
* Copied and slightly modified (mainly to port to Typescript) from dtlaunch.
*
* @param a the first
* @param b the second
* @return negative if a should go first, positive if b should go first, zero if equivalent.
*/
let infoFileSorter = (a: string, b: string): number => {
let aJson: AppInfo = storage.readJSON(a, false);
let bJson: AppInfo = storage.readJSON(b, false);
var n = (0 | aJson.sortorder!) - (0 | bJson.sortorder!);
if (n) return n; // do sortorder first
if (aJson.name < bJson.name) return -1;
if (aJson.name > bJson.name) return 1;
return 0;
}
export = {
cleanAndSave: cleanAndSave,
infoFileSorter: infoFileSorter,
/**
* Get the configuration for the launcher. Perform a cleanup if any new apps were installed or any apps refer to nonexistent folders.
*
* @param keepHidden if true, don't exclude apps that would otherwise be hidden
* @return the loaded configuration
*/
getConfig: (): Config => {
let config = storage.readJSON(SETTINGS_FILE, true) || DEFAULT_CONFIG;
// We only need to load data from the filesystem if there is a change
if (config.hash == storage.hash(/\.info$/)) {
return config;
}
E.showMessage(/*LANG*/'Rebuilding cache...')
config.rootFolder = clearFolder(config.rootFolder);
let infoFiles: Array<string> = storage.list(/\.info$/);
infoFiles.sort(infoFileSorter);
for (let infoFile of infoFiles) {
let app: AppInfo = storage.readJSON(infoFile, false);
// If the app is to be hidden by policy, exclude it completely
if (
(!config.showClocks && app.type == 'clock') ||
(!config.showLaunchers && app.type == 'launch') ||
(app.type == 'widget') ||
(!app.src)
) {
if (Object.keys(config.hidden).includes(app.id)) delete config.apps[app.id];
continue;
}
// Creates the apps entry if it doesn't exist yet.
if (!config.apps.hasOwnProperty(app.id)) {
config.apps[app.id] = {
folder: [],
nagged: false
};
}
// If the app is manually hidden, don't put it in a folder but still keep information about it
if (config.hidden.includes(app.id)) continue;
// Place apps in folders, deleting references to folders that no longer exist
// Note: Relies on curFolder secretly being a reference rather than a copy
let curFolder: Folder = config.rootFolder;
let depth = 0;
for (let folderName of config.apps[app.id].folder) {
if (curFolder.folders.hasOwnProperty(folderName)) {
curFolder = curFolder.folders[folderName]!;
depth++;
} else {
config.apps[app.id].folder = config.apps[app.id].folder.slice(0, depth);
break;
}
}
curFolder.apps.push(app.id);
}
config.hash = storage.hash(/\.info$/);
return cleanAndSave(config);
}
}

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwMA///wAJCAoPAAongAonwAon4Aon8Aon+Aon/AooA/AH4A/AFgA="))

BIN
apps/folderlaunch/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

View File

@ -0,0 +1,48 @@
{
"id": "folderlaunch",
"name": "Folder launcher",
"version": "0.01",
"description": "Launcher that allows you to put your apps into folders",
"icon": "icon.png",
"type": "launch",
"tags": "tool,system,launcher",
"supports": [
"BANGLEJS2"
],
"readme": "README.md",
"storage": [
{
"name": "folderlaunch.app.js",
"url": "app.js"
},
{
"name": "folderlaunch.settings.js",
"url": "settings.js"
},
{
"name": "folderlaunch-configLoad.js",
"url": "configLoad.js"
},
{
"name": "folderlaunch.img",
"url": "icon.js",
"evaluate": true
}
],
"data": [
{
"name": "folderlaunch.json"
}
],
"dependencies": {
"textinput": "type"
},
"screenshots": [
{
"url": "screenshot1.png"
},
{
"url": "screenshot2.png"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,248 @@
(function (back) {
var loader = require('folderlaunch-configLoad.js');
var storage = require('Storage');
var textinput = require('textinput');
var config = loader.getConfig();
var changed = false;
var hiddenAppsMenu = function () {
var menu = {
'': {
'title': 'Hide?',
'back': showMainMenu
}
};
var onchange = function (value, appId) {
if (value && !config.hidden.includes(appId))
config.hidden.push(appId);
else if (!value && config.hidden.includes(appId))
config.hidden = config.hidden.filter(function (item) { return item != appId; });
changed = true;
};
onchange;
for (var app_1 in config.apps) {
var appInfo = storage.readJSON(app_1 + '.info', false);
menu[appInfo.name] = {
value: config.hidden.includes(app_1),
format: function (value) { return (value ? 'Yes' : 'No'); },
onchange: eval("(value) => { onchange(value, \"".concat(app_1, "\"); }"))
};
}
E.showMenu(menu);
};
var getAppInfo = function (id) {
return storage.readJSON(id + '.info', false);
};
var showFolderMenu = function (path) {
var folder = config.rootFolder;
for (var _i = 0, path_1 = path; _i < path_1.length; _i++) {
var folderName = path_1[_i];
try {
folder = folder.folders[folderName];
}
catch (_a) {
E.showAlert('BUG: Nonexistent folder ' + path);
}
}
var back = function () {
if (path.length) {
path.pop();
showFolderMenu(path);
}
else
showMainMenu();
};
var menu = {
'': {
'title': path.length ? path[path.length - 1] : 'Root folder',
'back': back
},
'New subfolder': function () {
textinput.input({ text: '' }).then(function (result) {
if (result && !Object.keys(folder.folders).includes(result)) {
folder.folders[result] = {
folders: {},
apps: []
};
changed = true;
path.push(result);
showFolderMenu(path);
}
else {
E.showAlert('No folder created').then(function () {
showFolderMenu(path);
});
}
});
},
'Move app here': function () {
var menu = {
'': {
'title': 'Select app',
'back': function () {
showFolderMenu(path);
}
}
};
var mover = function (appId) {
var folder = config.rootFolder;
for (var _i = 0, _a = config.apps[appId].folder; _i < _a.length; _i++) {
var folderName = _a[_i];
folder = folder.folders[folderName];
}
folder.apps = folder.apps.filter(function (item) { return item != appId; });
config.apps[appId].folder = path.slice();
folder = config.rootFolder;
for (var _b = 0, path_2 = path; _b < path_2.length; _b++) {
var folderName = path_2[_b];
folder = folder.folders[folderName];
}
folder.apps.push(appId);
changed = true;
showFolderMenu(path);
};
mover;
E.showMessage('Loading apps...');
for (var _i = 0, _a = Object.keys(config.apps)
.filter(function (item) { return !folder.apps.includes(item); })
.map(function (item) { return item + '.info'; })
.sort(loader.infoFileSorter)
.map(function (item) { return item.split('.info')[0]; }); _i < _a.length; _i++) {
var appId = _a[_i];
menu[getAppInfo(appId).name] = eval("() => { mover(\"".concat(appId, "\"); }"));
}
E.showMenu(menu);
}
};
var switchToFolder = function (subfolder) {
path.push(subfolder);
showFolderMenu(path);
};
switchToFolder;
for (var _b = 0, _c = Object.keys(folder.folders); _b < _c.length; _b++) {
var subfolder = _c[_b];
menu[subfolder] = eval("() => { switchToFolder(\"".concat(subfolder, "\") }"));
}
if (folder.apps.length)
menu['View apps'] = function () {
var menu = {
'': {
'title': path[path.length - 1],
'back': function () { showFolderMenu(path); }
}
};
for (var _i = 0, _a = folder.apps; _i < _a.length; _i++) {
var appId = _a[_i];
menu[storage.readJSON(appId + '.info', false).name] = function () { };
}
E.showMenu(menu);
};
if (path.length)
menu['Delete folder'] = function () {
var apps = folder.apps;
var subfolders = folder.folders;
var toDelete = path.pop();
folder = config.rootFolder;
for (var _i = 0, path_3 = path; _i < path_3.length; _i++) {
var folderName = path_3[_i];
folder = folder.folders[folderName];
}
for (var _a = 0, apps_1 = apps; _a < apps_1.length; _a++) {
var appId = apps_1[_a];
config.apps[appId].folder.pop();
folder.apps.push(appId);
}
for (var _b = 0, _c = Object.keys(subfolders); _b < _c.length; _b++) {
var subfolder = _c[_b];
folder.folders[subfolder] = subfolders[subfolder];
}
delete folder.folders[toDelete];
changed = true;
showFolderMenu(path);
};
E.showMenu(menu);
};
var save = function () {
if (changed) {
E.showMessage('Saving...');
config.hash = 0;
loader.cleanAndSave(config);
changed = false;
}
};
E.on('kill', save);
var showMainMenu = function () {
E.showMenu({
'': {
'title': 'Folder launcher',
'back': function () {
save();
back();
}
},
'Show clocks': {
value: config.showClocks,
format: function (value) { return (value ? 'Yes' : 'No'); },
onchange: function (value) {
config.showClocks = value;
changed = true;
}
},
'Show launchers': {
value: config.showLaunchers,
format: function (value) { return (value ? 'Yes' : 'No'); },
onchange: function (value) {
config.showLaunchers = value;
changed = true;
}
},
'Hidden apps': hiddenAppsMenu,
'Display': function () {
E.showMenu({
'': {
'title': 'Display',
'back': showMainMenu
},
'Rows': {
value: config.display.rows,
min: 1,
onchange: function (value) {
config.display.rows = value;
changed = true;
}
},
'Show icons?': {
value: config.display.icon,
format: function (value) { return (value ? 'Yes' : 'No'); },
onchange: function (value) {
config.display.icon = value;
changed = true;
}
},
'Font size': {
value: config.display.font,
min: 0,
format: function (value) { return (value ? value : 'Icon only'); },
onchange: function (value) {
config.display.font = value;
changed = true;
}
}
});
},
'Timeout': {
value: config.timeout,
format: function (value) { return value ? "".concat(value / 1000, " sec") : 'None'; },
min: 0,
step: 1000,
onchange: function (value) {
config.timeout = value;
changed = true;
}
},
'Folder management': function () {
showFolderMenu([]);
}
});
};
showMainMenu();
});

View File

@ -0,0 +1,264 @@
(function (back) {
const loader = require('folderlaunch-configLoad.js');
const storage = require('Storage');
const textinput = require('textinput');
let config: Config = loader.getConfig();
let changed: boolean = false;
let hiddenAppsMenu = () => {
let menu: Menu = {
'': {
'title': 'Hide?',
'back': showMainMenu
}
}
let onchange = (value: boolean, appId: string) => {
if (value && !config.hidden.includes(appId)) // Hiding, not already hidden
config.hidden.push(appId);
else if (!value && config.hidden.includes(appId)) // Unhiding, already hidden
config.hidden = config.hidden.filter(item => item != appId)
changed = true;
}
onchange // Do nothing, but stop typescript from yelling at me for this function being unused. It gets used by eval. I know eval is evil, but the menus are a bit limited.
for (let app in config.apps) {
let appInfo: AppInfo = storage.readJSON(app + '.info', false);
menu[appInfo.name] = {
value: config.hidden.includes(app),
format: (value: boolean) => (value ? 'Yes' : 'No'),
onchange: eval(`(value) => { onchange(value, "${app}"); }`)
}
}
E.showMenu(menu);
};
let getAppInfo = (id: string): AppInfo => {
return storage.readJSON(id + '.info', false);
}
let showFolderMenu = (path: Array<string>) => {
let folder: Folder = config.rootFolder;
for (let folderName of path)
try {
folder = folder.folders[folderName]!;
} catch {
E.showAlert(/*LANG*/'BUG: Nonexistent folder ' + path);
}
let back = () => {
if (path.length) {
path.pop();
showFolderMenu(path);
} else showMainMenu();
};
let menu: Menu = {
'': {
'title': path.length ? path[path.length - 1]! : /*LANG*/ 'Root folder',
'back': back
},
/*LANG*/'New subfolder': () => {
textinput.input({ text: '' }).then((result: string) => {
if (result && !Object.keys(folder.folders).includes(result)) {
folder.folders[result] = {
folders: {},
apps: []
};
changed = true;
path.push(result);
showFolderMenu(path);
} else {
E.showAlert(/*LANG*/'No folder created').then(() => {
showFolderMenu(path);
})
}
});
},
/*LANG*/'Move app here': () => {
let menu: Menu = {
'': {
'title': /*LANG*/'Select app',
'back': () => {
showFolderMenu(path);
}
}
};
let mover = (appId: string) => {
// Delete app from old folder
let folder: Folder = config.rootFolder;
for (let folderName of config.apps[appId]!.folder)
folder = folder.folders[folderName]!;
folder.apps = folder.apps.filter((item: string) => item != appId);
// Change folder in app info, .slice is to force a copy rather than a reference
config.apps[appId]!.folder = path.slice();
// Place app in new folder (here)
folder = config.rootFolder;
for (let folderName of path)
folder = folder.folders[folderName]!;
folder.apps.push(appId);
// Mark changed and refresh menu
changed = true;
showFolderMenu(path);
};
mover;
E.showMessage(/*LANG*/'Loading apps...');
for (
let appId of Object.keys(config.apps) // All known apps
.filter(item => !folder.apps.includes(item)) // Filter out ones already in this folder
.map(item => item + '.info') // Convert to .info files
.sort(loader.infoFileSorter) // Sort the info files using infoFileSorter
.map(item => item.split('.info')[0]) // Back to app ids
) {
menu[getAppInfo(appId).name] = eval(`() => { mover("${appId}"); }`);
}
E.showMenu(menu);
}
};
let switchToFolder = (subfolder: string) => {
path.push(subfolder);
showFolderMenu(path);
};
switchToFolder;
for (let subfolder of Object.keys(folder.folders)) {
menu[subfolder] = eval(`() => { switchToFolder("${subfolder}") }`);
}
if (folder.apps.length) menu[/*LANG*/'View apps'] = () => {
let menu: Menu = {
'': {
'title': path[path.length - 1]!,
'back': () => { showFolderMenu(path); }
}
}
for (let appId of folder.apps) {
menu[storage.readJSON(appId + '.info', false).name] = () => { };
}
E.showMenu(menu);
}
if (path.length) menu[/*LANG*/'Delete folder'] = () => {
// Cache apps for changing the folder reference
let apps: Array<string> = folder.apps;
let subfolders = folder.folders;
// Move up to the parent folder
let toDelete: string = path.pop()!;
folder = config.rootFolder;
for (let folderName of path)
folder = folder.folders[folderName]!;
// Move all apps and folders to the parent folder, then delete this one
for (let appId of apps) {
config.apps[appId]!.folder.pop();
folder.apps.push(appId);
}
for (let subfolder of Object.keys(subfolders))
folder.folders[subfolder] = subfolders[subfolder]!;
delete folder.folders[toDelete];
// Mark as modified and go back
changed = true;
showFolderMenu(path);
}
E.showMenu(menu);
};
let save = () => {
if (changed) {
E.showMessage(/*LANG*/'Saving...');
config.hash = 0; // Invalidate the cache so changes to hidden apps or folders actually get reflected
loader.cleanAndSave(config);
changed = false; // So we don't do it again on exit
}
};
E.on('kill', save);
let showMainMenu = () => {
E.showMenu({
'': {
'title': 'Folder launcher',
'back': () => {
save();
back();
}
},
'Show clocks': {
value: config.showClocks,
format: value => (value ? 'Yes' : 'No'),
onchange: value => {
config.showClocks = value;
changed = true;
}
},
'Show launchers': {
value: config.showLaunchers,
format: value => (value ? 'Yes' : 'No'),
onchange: value => {
config.showLaunchers = value;
changed = true;
}
},
'Hidden apps': hiddenAppsMenu,
'Display': () => {
E.showMenu({
'': {
'title': 'Display',
'back': showMainMenu
},
'Rows': {
value: config.display.rows,
min: 1,
onchange: value => {
config.display.rows = value;
changed = true;
}
},
'Show icons?': {
value: config.display.icon,
format: value => (value ? 'Yes' : 'No'),
onchange: value => {
config.display.icon = value;
changed = true;
}
},
'Font size': {
value: config.display.font as any,
min: 0,
format: (value: any) => (value ? value : 'Icon only'),
onchange: (value: any) => {
config.display.font = value;
changed = true;
}
}
});
},
'Timeout': {
value: config.timeout,
format: value => value ? `${value / 1000} sec` : 'None',
min: 0,
step: 1000,
onchange: value => {
config.timeout = value;
changed = true;
}
},
'Folder management': () => {
showFolderMenu([]);
}
});
};
showMainMenu();
} satisfies SettingsFunc);

34
apps/folderlaunch/types.d.ts vendored Normal file
View File

@ -0,0 +1,34 @@
type Folder = {
folders: { // name: folder pairs of all nested folders
[key: string]: Folder
},
apps: Array<string> // List of ids of all apps in this folder
};
type FolderList = Array<string>;
type Config = {
showClocks: boolean, // Whether clocks are shown
showLaunchers: boolean, // Whether launchers are shown
hidden: Array<String>, // IDs of apps to explicitly hide
display: {
rows: number, // Display an X by X grid of apps
icon: boolean, // Whether to show icons
font: number // Which font to use for the name, or false to not show the name
},
timeout: number, // How many ms before returning to the clock, or zero to never return
rootFolder: Folder, // The top level folder, first displayed when opened
apps: { // Saved info for each app
[key: string]: {
folder: FolderList, // Folder path
fast: boolean, // Whether the app should be fast launched
nagged: boolean // Whether the app's fast launch setting was configured
}
},
hash: number // Hash of .info files
};
type GridEntry = { // An entry in the grid displayed on-screen
type: 'app' | 'folder' | 'empty', // Which type of item is in this space
id: string // The id of that item
}

View File

@ -1 +1,3 @@
0.01: New app!
0.01: New app!
0.02: Add sensor icons
Customize code directly, remove config file

View File

@ -1,5 +1,5 @@
(function () {
const sb = () => require('hasensors').sendBattery();
const sb = () => require("hasensors").sendBattery();
Bangle.on("charging", sb);
NRF.on("connect", () => setTimeout(sb, 2000));
setInterval(sb, 10 * 60 * 1000);

View File

@ -39,14 +39,27 @@
<a href="https://my.home-assistant.io/redirect/profile/" target="_blank">your user profile</a>.</span></label>
</form>
<p>
<button id="upload" class="btn btn-primary">Upload</button>
<button id="upload" class="btn btn-primary" disabled>Upload</button>
</p>
<script src="../../core/lib/customize.js"></script>
<script>
const STORAGE_KEY = 'hasensors-config';
const fields = ['id', 'name', 'url', 'token'];
const form = document.getElementById('sensorform');
const STORAGE_KEY = "hasensors-config";
const fields = ["id", "name", "url", "token"];
const form = document.getElementById("sensorform");
const LIBRARY_URL = "./lib.js";
// fetch library code template, enable upload button once we"ve got it
let libTpl;
fetch(LIBRARY_URL).then(response=>{
if (! response.ok) return;
console.log(response);
response.text().then(code=>{
libTpl = code;
document.getElementById("upload").disabled = false;
});
});
// try to pre-fill form with values previously saved in localStorage
let stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
try {
@ -62,7 +75,7 @@
}
document.getElementById("upload").addEventListener("click", function () {
let config = {};
// validate form fields or bail out
for (const field of fields) {
if (!form[field].validity.valid) {
form[field].focus();
@ -70,18 +83,21 @@
return;
}
}
let config = {};
for (const field of fields) {
config[field] = form[field].value
config[field] = form[field].value;
}
console.log('config:', config, JSON.stringify(config));
// save config to localStorage for re-use next time
localStorage.setItem(STORAGE_KEY, JSON.stringify(config));
// replace {placeholders} in library code template
const lib = libTpl.replace(/\{(\w+)\}/g, (_,f) => config[f]);
console.log("config:", config, JSON.stringify(config));
sendCustomizedApp({
id: "hasensors",
storage: [
{name: "hasensors.boot.js", url: "boot.js"},
{name: "hasensors", url: "lib.js"},
{name: "hasensors.settings.json", content: JSON.stringify(config)},
]
{name: "hasensors", content: lib},
],
});
});
</script>

View File

@ -1,35 +1,43 @@
// split out into a separate file to keep bootcode short.
function s(key) {
return (require('Storage').readJSON('hasensors.settings.js', true) || {})[key];
}
// placeholders are replaced by custom.html before upload
function post(sensor, data) {
const url = s('url') + '/api/states/sensor.' + s('id') + '_' + sensor;
const url = "{url}/api/states/sensor.{id}_" + sensor;
Bangle.http(url, {
method: 'POST',
method: "POST",
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + s('token'),
"Content-Type": "application/json",
Authorization: "Bearer {token}",
}
});
}
exports.sendBattery = function () {
if (!NRF.getSecurityStatus().connected) return;
post('battery_level', {
state: E.getBattery(),
const b = E.getBattery(),
c = Bangle.isCharging();
let i = "mdi:battery";
if (c) i += "-charging";
post("battery_state", {
state: c ? "charging" : "discharging",
attributes: {
friendly_name: s('name') + " Battery Level",
friendly_name: "{name} Battery State",
icon: i + (c ? "" : "-minus"),
}
});
if (b<10) i += "-outline"; // there is no battery-0
else if (b<100 || c) i += "-" + Math.floor(b/10)*10; // no battery-100 either
post("battery_level", {
state: b,
attributes: {
friendly_name: "{name} Battery Level",
unit_of_measurement: "%",
device_class: "battery",
state_class: "measurement",
}
});
post('battery_state', {
state: Bangle.isCharging() ? 'charging' : 'discharging',
attributes: {
friendly_name: s('name') + " Battery State",
icon: i,
}
});
}

View File

@ -2,7 +2,7 @@
"id": "hasensors",
"name": "Home Assistant Sensors",
"shortName": "HA sensors",
"version": "0.01",
"version": "0.02",
"description": "Send sensor values to Home Assistant using the Android Integration.",
"icon": "ha.png",
"type": "bootloader",
@ -14,8 +14,5 @@
"storage": [
{"name":"hasensors","url":"lib.js"},
{"name":"hasensors.boot.js","url":"boot.js"}
],
"data": [
{"name":"hasensors.settings.json"}
]
}

View File

@ -2,8 +2,6 @@
Logs health data to a file in a defined interval, and provides an app to view it
**BETA - requires firmware 2v11 or later**
## Usage
Once installed, health data is logged automatically.

View File

@ -1,3 +1,13 @@
0.01: New app!
0.02-0.07: Bug fixes
0.08: Submitted to the app loader
0.08: Submitted to the app loader
0.09: Added weather dependency
Up and down swipes can now be configured separately
The settings menu can now handle having shortcuts configured to apps that were removed
Default notification app is now messageui rather than messages
Support for dual stage unlock
Support for a calendar bar
The clock face is redrawn less often, hoping to save some battery
Option to show the seconds when unlocked, even when otherwise hidden by other settings
Broke out config loading into separate file to avoid duplicating a whole bunch of code
Added support for fast loading

View File

@ -16,6 +16,8 @@ There are generally a few apps that the user uses far more frequently than the o
## Configurability
Dual stage unlock allows for unlocking to be split into two stages: lighting the screen upon the actual unlock, and displaying the extra information and shortcuts after a user-configurable number of taps. This may be useful if you want to quickly glance at the clock with a wrist flick in the dark, or if you want to show the time to other people. Swipe shortcuts are active even after the first stage.
Displaying the seconds allows for more precise timing, but waking up the CPU to refresh the display more often consumes battery. The user can enable or disable them completely, but can also configure them to be enabled or disabled automatically based on some hueristics:
* They can be hidden while the display is locked, if the user expects to unlock their watch when they need the seconds.

View File

@ -1,280 +1,302 @@
const SETTINGS_FILE = "infoclk.json";
const FONT = require('infoclk-font.js');
{
const FONT = require('infoclk-font.js');
const storage = require("Storage");
const locale = require("locale");
const weather = require('weather');
const storage = require("Storage");
const locale = require("locale");
const weather = require('weather');
let config = Object.assign({
seconds: {
// Displaying the seconds can reduce battery life because the CPU must wake up more often to update the display.
// The seconds will be shown unless one of these conditions is enabled here, and currently true.
hideLocked: false, // Hide the seconds when the display is locked.
hideBattery: 20, // Hide the seconds when the battery is at or below a defined percentage.
hideTime: true, // Hide the seconds when between a certain period of time. Useful for when you are sleeping and don't need the seconds
hideStart: 2200, // The time when the seconds are hidden: first 2 digits are hours on a 24 hour clock, last 2 are minutes
hideEnd: 700, // The time when the seconds are shown again
hideAlways: false, // Always hide (never show) the seconds
},
let config = require('infoclk-config.js').getConfig();
date: {
// Settings related to the display of the date
mmdd: true, // If true, display the month first. If false, display the date first.
separator: '-', // The character that goes between the month and date
monthName: false, // If false, display the month as a number. If true, display the name.
monthFullName: false, // If displaying the name: If false, display an abbreviation. If true, display a full name.
dayFullName: false, // If false, display the day of the week's abbreviation. If true, display the full name.
},
// Return whether the given time (as a date object) is between start and end (as a number where the first 2 digits are hours on a 24 hour clock and the last 2 are minutes), with end time wrapping to next day if necessary
let timeInRange = function (start, time, end) {
bottomLocked: {
display: 'weather' // What to display in the bottom row when locked:
// 'weather': The current temperature and weather description
// 'steps': Step count
// 'health': Step count and bpm
// 'progress': Day progress bar
// false: Nothing
},
// Convert the given date object to a time number
let timeNumber = time.getHours() * 100 + time.getMinutes();
shortcuts: [
//8 shortcuts, displayed in the bottom half of the screen (2 rows of 4 shortcuts) when unlocked
// false = no shortcut
// '#LAUNCHER' = open the launcher
// any other string = name of app to open
'stlap', 'keytimer', 'pomoplus', 'alarm',
'rpnsci', 'calendar', 'torch', 'weather'
],
swipe: {
// 3 shortcuts to launch upon swiping:
// false = no shortcut
// '#LAUNCHER' = open the launcher
// any other string = name of app to open
up: 'messages', // Swipe up or swipe down, due to limitation of event handler
left: '#LAUNCHER',
right: '#LAUNCHER',
},
dayProgress: {
// A progress bar representing how far through the day you are
enabledLocked: true, // Whether this bar is enabled when the watch is locked
enabledUnlocked: false, // Whether the bar is enabled when the watch is unlocked
color: [0, 0, 1], // The color of the bar
start: 700, // The time of day that the bar starts filling
end: 2200, // The time of day that the bar becomes full
reset: 300 // The time of day when the progress bar resets from full to empty
},
lowBattColor: {
// The text can change color to indicate that the battery is low
level: 20, // The percentage where this happens
color: [1, 0, 0] // The color that the text changes to
}
}, storage.readJSON(SETTINGS_FILE));
// Return whether the given time (as a date object) is between start and end (as a number where the first 2 digits are hours on a 24 hour clock and the last 2 are minutes), with end time wrapping to next day if necessary
function timeInRange(start, time, end) {
// Convert the given date object to a time number
let timeNumber = time.getHours() * 100 + time.getMinutes();
// Normalize to prevent the numbers from wrapping around at midnight
if (end <= start) {
end += 2400;
if (timeNumber < start) timeNumber += 2400;
}
return start <= timeNumber && timeNumber <= end;
}
// Return whether settings should be displayed based on the user's configuration
function shouldDisplaySeconds(now) {
return !(
(config.seconds.hideAlways) ||
(config.seconds.hideLocked && Bangle.isLocked()) ||
(E.getBattery() <= config.seconds.hideBattery) ||
(config.seconds.hideTime && timeInRange(config.seconds.hideStart, now, config.seconds.hideEnd))
);
}
// Determine the font size needed to fit a string of the given length widthin maxWidth number of pixels, clamped between minSize and maxSize
function getFontSize(length, maxWidth, minSize, maxSize) {
let size = Math.floor(maxWidth / length); //Number of pixels of width available to character
size *= (20 / 12); //Convert to height, assuming 20 pixels of height for every 12 of width
// Clamp to within range
if (size < minSize) return minSize;
else if (size > maxSize) return maxSize;
else return Math.floor(size);
}
// Get the current day of the week according to user settings
function getDayString(now) {
if (config.date.dayFullName) return ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][now.getDay()];
else return ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][now.getDay()];
}
// Pad a number with zeros to be the given number of digits
function pad(number, digits) {
let result = '' + number;
while (result.length < digits) result = '0' + result;
return result;
}
// Get the current date formatted according to the user settings
function getDateString(now) {
let month;
if (!config.date.monthName) month = pad(now.getMonth() + 1, 2);
else if (config.date.monthFullName) month = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'][now.getMonth()];
else month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][now.getMonth()];
if (config.date.mmdd) return `${month}${config.date.separator}${pad(now.getDate(), 2)}`;
else return `${pad(now.getDate(), 2)}${config.date.separator}${month}`;
}
// Get a floating point number from 0 to 1 representing how far between the user-defined start and end points we are
function getDayProgress(now) {
let start = config.dayProgress.start;
let current = now.getHours() * 100 + now.getMinutes();
let end = config.dayProgress.end;
let reset = config.dayProgress.reset;
// Normalize
if (end <= start) end += 2400;
if (current < start) current += 2400;
if (reset < start) reset += 2400;
// Convert an hhmm number into a floating-point hours
function toDecimalHours(time) {
let hours = Math.floor(time / 100);
let minutes = time % 100;
return hours + (minutes / 60);
}
start = toDecimalHours(start);
current = toDecimalHours(current);
end = toDecimalHours(end);
reset = toDecimalHours(reset);
let progress = (current - start) / (end - start);
if (progress < 0 || progress > 1) {
if (current < reset) return 1;
else return 0;
} else {
return progress;
}
}
// Get a Gadgetbridge weather string
function getWeatherString() {
let current = weather.get();
if (current) return locale.temp(current.temp - 273.15) + ', ' + current.txt;
else return 'Weather unknown!';
}
// Get a second weather row showing humidity, wind speed, and wind direction
function getWeatherRow2() {
let current = weather.get();
if (current) return `${current.hum}%, ${locale.speed(current.wind)} ${current.wrose}`;
else return 'Check Gadgetbridge';
}
// Get a step string
function getStepsString() {
return '' + Bangle.getHealthStatus('day').steps + ' steps';
}
// Get a health string including daily steps and recent bpm
function getHealthString() {
return `${Bangle.getHealthStatus('day').steps} steps ${Bangle.getHealthStatus('last').bpm} bpm`;
}
// Set the next timeout to draw the screen
let drawTimeout;
function setNextDrawTimeout() {
if (drawTimeout) {
clearTimeout(drawTimeout);
drawTimeout = undefined;
}
let time;
let now = new Date();
if (shouldDisplaySeconds(now)) time = 1000 - (now.getTime() % 1000);
else time = 60000 - (now.getTime() % 60000);
drawTimeout = setTimeout(draw, time);
}
const DIGIT_WIDTH = 40; // How much width is allocated for each digit, 37 pixels + 3 pixels of space (which will go off of the screen on the right edge)
const COLON_WIDTH = 19; // How much width is allocated for the colon, 16 pixels + 3 pixels of space
const HHMM_TOP = 27; // 24 pixels for widgets + 3 pixels of space
const DIGIT_HEIGHT = 64; // How tall the digits are
const SECONDS_TOP = HHMM_TOP + DIGIT_HEIGHT + 3; // The top edge of the seconds, top of hours and minutes + digit height + space
const SECONDS_LEFT = 2 * DIGIT_WIDTH + COLON_WIDTH; // The left edge of the seconds: displayed after 2 digits and the colon
const DATE_LETTER_HEIGHT = DIGIT_HEIGHT / 2; // Each letter of the day of week and date will be half the height of the time digits
const DATE_CENTER_X = SECONDS_LEFT / 2; // Day of week and date will be centered between left edge of screen and where seconds start
const DOW_CENTER_Y = SECONDS_TOP + (DATE_LETTER_HEIGHT / 2); // Day of week will be the top row
const DATE_CENTER_Y = DOW_CENTER_Y + DATE_LETTER_HEIGHT; // Date will be the bottom row
const DOW_DATE_CENTER_Y = SECONDS_TOP + (DIGIT_HEIGHT / 2); // When displaying both on one row, center it
const BOTTOM_CENTER_Y = ((SECONDS_TOP + DIGIT_HEIGHT + 3) + g.getHeight()) / 2;
// Draw the clock
function draw() {
//Prepare to draw
g.reset()
.setFontAlign(0, 0);
if (E.getBattery() <= config.lowBattColor.level) {
let color = config.lowBattColor.color;
g.setColor(color[0], color[1], color[2]);
}
now = new Date();
if (Bangle.isLocked()) { //When the watch is locked
g.clearRect(0, 24, g.getWidth(), g.getHeight());
//Draw the hours and minutes
let x = 0;
for (let digit of locale.time(now, 1)) { //apparently this is how you get an hh:mm time string adjusting for the user's 12/24 hour preference
if (digit != ' ') g.drawImage(FONT[digit], x, HHMM_TOP);
if (digit == ':') x += COLON_WIDTH;
else x += DIGIT_WIDTH;
}
if (storage.readJSON('setting.json')['12hour']) g.drawImage(FONT[(now.getHours() < 12) ? 'am' : 'pm'], 0, HHMM_TOP);
//Draw the seconds if necessary
if (shouldDisplaySeconds(now)) {
let tens = Math.floor(now.getSeconds() / 10);
let ones = now.getSeconds() % 10;
g.drawImage(FONT[tens], SECONDS_LEFT, SECONDS_TOP)
.drawImage(FONT[ones], SECONDS_LEFT + DIGIT_WIDTH, SECONDS_TOP);
// Draw the day of week and date assuming the seconds are displayed
g.setFont('Vector', getFontSize(getDayString(now).length, SECONDS_LEFT, 6, DATE_LETTER_HEIGHT))
.drawString(getDayString(now), DATE_CENTER_X, DOW_CENTER_Y)
.setFont('Vector', getFontSize(getDateString(now).length, SECONDS_LEFT, 6, DATE_LETTER_HEIGHT))
.drawString(getDateString(now), DATE_CENTER_X, DATE_CENTER_Y);
} else {
//Draw the day of week and date without the seconds
let string = getDayString(now) + ' ' + getDateString(now);
g.setFont('Vector', getFontSize(string.length, g.getWidth(), 6, DATE_LETTER_HEIGHT))
.drawString(string, g.getWidth() / 2, DOW_DATE_CENTER_Y);
// Normalize to prevent the numbers from wrapping around at midnight
if (end <= start) {
end += 2400;
if (timeNumber < start) timeNumber += 2400;
}
// Draw the bottom area
if (config.bottomLocked.display == 'progress') {
let color = config.dayProgress.color;
return start <= timeNumber && timeNumber <= end;
}
// Return whether settings should be displayed based on the user's configuration
let shouldDisplaySeconds = function (now) {
return (config.seconds.forceWhenUnlocked > 0 && getUnlockStage() >= config.seconds.forceWhenUnlocked) || !(
(config.seconds.hideAlways) ||
(config.seconds.hideLocked && getUnlockStage() < 2) ||
(E.getBattery() <= config.seconds.hideBattery) ||
(config.seconds.hideTime && timeInRange(config.seconds.hideStart, now, config.seconds.hideEnd))
);
}
// Determine the font size needed to fit a string of the given length widthin maxWidth number of pixels, clamped between minSize and maxSize
let getFontSize = function (length, maxWidth, minSize, maxSize) {
let size = Math.floor(maxWidth / length); //Number of pixels of width available to character
size *= (20 / 12); //Convert to height, assuming 20 pixels of height for every 12 of width
// Clamp to within range
if (size < minSize) return minSize;
else if (size > maxSize) return maxSize;
else return Math.floor(size);
}
// Get the current day of the week according to user settings
let getDayString = function (now) {
if (config.date.dayFullName) return ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][now.getDay()];
else return ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][now.getDay()];
}
// Pad a number with zeros to be the given number of digits
let pad = function (number, digits) {
let result = '' + number;
while (result.length < digits) result = '0' + result;
return result;
}
// Get the current date formatted according to the user settings
let getDateString = function (now) {
let month;
if (!config.date.monthName) month = pad(now.getMonth() + 1, 2);
else if (config.date.monthFullName) month = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'][now.getMonth()];
else month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][now.getMonth()];
if (config.date.mmdd) return `${month}${config.date.separator}${pad(now.getDate(), 2)}`;
else return `${pad(now.getDate(), 2)}${config.date.separator}${month}`;
}
// Get a Gadgetbridge weather string
let getWeatherString = function () {
let current = weather.get();
if (current) return locale.temp(current.temp - 273.15) + ', ' + current.txt;
else return 'Weather unknown!';
}
// Get a second weather row showing humidity, wind speed, and wind direction
let getWeatherRow2 = function () {
let current = weather.get();
if (current) return `${current.hum}%, ${locale.speed(current.wind)} ${current.wrose}`;
else return 'Check Gadgetbridge';
}
// Get a step string
let getStepsString = function () {
return '' + Bangle.getHealthStatus('day').steps + ' steps';
}
// Get a health string including daily steps and recent bpm
let getHealthString = function () {
return `${Bangle.getHealthStatus('day').steps} steps ${Bangle.getHealthStatus('last').bpm} bpm`;
}
// Set the next timeout to draw the screen
let drawTimeout;
let setNextDrawTimeout = function () {
if (drawTimeout !== undefined) {
clearTimeout(drawTimeout);
drawTimeout = undefined;
}
let time;
let now = new Date();
if (shouldDisplaySeconds(now)) time = 1000 - (now.getTime() % 1000);
else time = 60000 - (now.getTime() % 60000);
drawTimeout = setTimeout(drawLockedSeconds, time);
}
/** Return one of the following values:
* 0: Watch is locked
* 1: Watch is unlocked, but should still be displaying the large clock (first stage unlock)
* 2: Watch is unlocked and should be displaying the extra info and icons (second stage unlock)
*/
let getUnlockStage = function () {
if (Bangle.isLocked()) return 0;
else if (dualStageTaps < config.dualStageUnlock) return 1;
else return 2;
}
const DIGIT_WIDTH = 40; // How much width is allocated for each digit, 37 pixels + 3 pixels of space (which will go off of the screen on the right edge)
const COLON_WIDTH = 19; // How much width is allocated for the colon, 16 pixels + 3 pixels of space
const HHMM_TOP = 27; // 24 pixels for widgets + 3 pixels of space
const DIGIT_HEIGHT = 64; // How tall the digits are
const SECONDS_TOP = HHMM_TOP + DIGIT_HEIGHT + 3; // The top edge of the seconds, top of hours and minutes + digit height + space
const SECONDS_LEFT = 2 * DIGIT_WIDTH + COLON_WIDTH; // The left edge of the seconds: displayed after 2 digits and the colon
const DATE_LETTER_HEIGHT = DIGIT_HEIGHT / 2; // Each letter of the day of week and date will be half the height of the time digits
const DATE_CENTER_X = SECONDS_LEFT / 2; // Day of week and date will be centered between left edge of screen and where seconds start
const DOW_CENTER_Y = SECONDS_TOP + (DATE_LETTER_HEIGHT / 2); // Day of week will be the top row
const DATE_CENTER_Y = DOW_CENTER_Y + DATE_LETTER_HEIGHT; // Date will be the bottom row
const DOW_DATE_CENTER_Y = SECONDS_TOP + (DIGIT_HEIGHT / 2); // When displaying both on one row, center it
const BOTTOM_CENTER_Y = ((SECONDS_TOP + DIGIT_HEIGHT + 3) + g.getHeight()) / 2;
// Draw a bar with the given top and bottom position
let drawBar = function (x1, y1, x2, y2) {
// Draw a day progress bar at the given position with given width and height
let drawDayProgress = function (x1, y1, x2, y2) {
// Get a floating point number from 0 to 1 representing how far between the user-defined start and end points we are
let getDayProgress = function (now) {
let start = config.bar.dayProgress.start;
let current = now.getHours() * 100 + now.getMinutes();
let end = config.bar.dayProgress.end;
let reset = config.bar.dayProgress.reset;
// Normalize
if (end <= start) end += 2400;
if (current < start) current += 2400;
if (reset < start) reset += 2400;
// Convert an hhmm number into a floating-point hours
let toDecimalHours = function (time) {
let hours = Math.floor(time / 100);
let minutes = time % 100;
return hours + (minutes / 60);
}
start = toDecimalHours(start);
current = toDecimalHours(current);
end = toDecimalHours(end);
reset = toDecimalHours(reset);
let progress = (current - start) / (end - start);
if (progress < 0 || progress > 1) {
if (current < reset) return 1;
else return 0;
} else {
return progress;
}
}
let color = config.bar.dayProgress.color;
g.setColor(color[0], color[1], color[2])
.fillRect(0, SECONDS_TOP + DIGIT_HEIGHT + 3, g.getWidth() * getDayProgress(now), g.getHeight());
} else {
.fillRect(x1, y1, x1 + (x2 - x1) * getDayProgress(now), y2);
}
// Draw a calendar bar at the given position with given width and height
let drawCalendar = function (x1, y1, x2, y2) {
let calendar = storage.readJSON('android.calendar.json', true) || [];
let now = (new Date()).getTime();
let endTime = now + config.bar.calendar.duration * 1000;
// Events must end in the future. Requirement to end in the future rather than start is so ongoing events display partially at the left
// Events must start before the end of the lookahead window
// Sort longer events first, so shorter events get placed on top. Tries to prevent the situation where an event entirely within the timespan of another gets completely covered
calendar = calendar.filter(event => ((now < 1000 * (event.timestamp + event.durationInSeconds)) && (event.timestamp * 1000 < endTime)))
.sort((a, b) => { return b.durationInSeconds - a.durationInSeconds; });
pipes = []; // Cache the pipes and draw them all at once, on top of the bar
for (let event of calendar) {
// left = boundary + how far event is in the future mapped from our allowed duration to a distance in pixels, clamped to x1
let leftUnclamped = x1 + (event.timestamp * 1000 - now) * (x2 - x1) / (config.bar.calendar.duration * 1000);
let left = Math.max(leftUnclamped, x1);
// right = unclamped left + how long the event is mapped from seconds to a distance in pixels, clamped to x2
let rightUnclamped = leftUnclamped + event.durationInSeconds * (x2 - x1) / (config.bar.calendar.duration)
let right = Math.min(rightUnclamped, x2);
//Draw the actual bar
if (event.color) g.setColor("#" + (0x1000000 + Number(event.color)).toString(16).padStart(6, "0")); // Line plagiarized from the agenda app
else {
let color = config.bar.calendar.defaultColor;
g.setColor(color[0], color[1], color[2]);
}
g.fillRect(left, y1, right, y2);
// Cache the pipes if necessary
if (leftUnclamped == left) pipes.push(left);
if (rightUnclamped == right) pipes.push(right);
}
// Draw the pipes
let color = config.bar.calendar.pipeColor;
g.setColor(color[0], color[1], color[2]);
for (let pipe of pipes) {
g.fillRect(pipe - 1, y1, pipe + 1, y2);
}
}
if (config.bar.type == 'dayProgress') {
drawDayProgress(x1, y1, x2, y2);
} else if (config.bar.type == 'calendar') {
drawCalendar(x1, y1, x2, y2);
} else if (config.bar.type == 'split') {
let xavg = (x1 + x2) / 2;
drawDayProgress(x1, y1, xavg, y2);
drawCalendar(xavg, y1, x2, y2);
g.setColor(g.theme.fg).fillRect(xavg - 1, y1, xavg + 1, y2);
}
}
// Return whether low battery behavior should be used.
// - If the watch isn't charging and the battery is low, mark it low. Once the battery is marked low, it stays marked low for subsequent calls.
// - When the watch sees external power, unmark the low battery.
// This allows us to redraw the full time in the low battery color to avoid only the seconds changing, but still do it once. And it avoids alternating.
let lowBattery = false;
let checkLowBattery = function () {
if (!Bangle.isCharging() && E.getBattery() <= config.lowBattColor.level) lowBattery = true;
else if (Bangle.isCharging()) lowBattery = false;
return lowBattery;
}
let onCharging = charging => {
checkLowBattery();
drawLockedSeconds(true);
}
Bangle.on('charging', onCharging);
// Draw the big seconds that are displayed when the screen is locked. Call drawClock if anything else needs to be updated
let drawLockedSeconds = function (forceDrawClock) {
// If the watch is in the second stage of unlock, call drawClock()
if (getUnlockStage() == 2) {
drawClock();
setNextDrawTimeout();
return
}
now = new Date();
// If we should not be displaying the seconds right now, call drawClock()
if (!shouldDisplaySeconds(now)) {
drawClock();
setNextDrawTimeout();
return;
}
// If the seconds are zero, or we are forced to raw the clock, call drawClock() but also display the seconds
else if (now.getSeconds() == 0 || forceDrawClock) {
drawClock();
}
// If none of the prior conditions are met, draw the seconds only and do not call drawClock()
g.reset()
.setFontAlign(0, 0)
.clearRect(SECONDS_LEFT, SECONDS_TOP, g.getWidth(), SECONDS_TOP + DIGIT_HEIGHT);
// If the battery is low, redraw the clock so it can change color
if (checkLowBattery()) {
let color = config.lowBattColor.color;
g.setColor(color[0], color[1], color[2]);
}
let tens = Math.floor(now.getSeconds() / 10);
let ones = now.getSeconds() % 10;
g.drawImage(FONT[tens], SECONDS_LEFT, SECONDS_TOP)
.drawImage(FONT[ones], SECONDS_LEFT + DIGIT_WIDTH, SECONDS_TOP);
setNextDrawTimeout();
}
// Draw the bottom text area
let drawBottomText = function () {
g.clearRect(0, SECONDS_TOP + DIGIT_HEIGHT, g.getWidth(), g.getHeight());
if (config.bottomLocked.display == 'progress') drawBar(0, SECONDS_TOP + DIGIT_HEIGHT + 3, g.getWidth(), g.getHeight());
else {
let bottomString;
if (config.bottomLocked.display == 'weather') bottomString = getWeatherString();
@ -282,124 +304,214 @@ function draw() {
else if (config.bottomLocked.display == 'health') bottomString = getHealthString();
else bottomString = ' ';
g.setFont('Vector', getFontSize(bottomString.length, 176, 6, g.getHeight() - (SECONDS_TOP + DIGIT_HEIGHT + 3)))
g.reset()
.setFontAlign(0, 0)
.setFont('Vector', getFontSize(bottomString.length, 176, 6, g.getHeight() - (SECONDS_TOP + DIGIT_HEIGHT + 3)))
.drawString(bottomString, g.getWidth() / 2, BOTTOM_CENTER_Y);
}
}
// Draw the day progress bar between the rows if necessary
if (config.dayProgress.enabledLocked && config.bottomLocked.display != 'progress') {
let color = config.dayProgress.color;
g.setColor(color[0], color[1], color[2])
.fillRect(0, HHMM_TOP + DIGIT_HEIGHT, g.getWidth() * getDayProgress(now), SECONDS_TOP);
// Draw the clock
let drawClock = function (now) {
//Prepare to draw
g.reset()
.setFontAlign(0, 0);
if (checkLowBattery()) {
let color = config.lowBattColor.color;
g.setColor(color[0], color[1], color[2]);
}
} else {
if (now == undefined) now = new Date();
//If the watch is unlocked
g.clearRect(0, 24, g.getWidth(), g.getHeight() / 2);
rows = [
`${getDayString(now)} ${getDateString(now)} ${locale.time(now, 1)}`,
getHealthString(),
getWeatherString(),
getWeatherRow2()
];
if (shouldDisplaySeconds(now)) rows[0] += ':' + pad(now.getSeconds(), 2);
if (storage.readJSON('setting.json')['12hour']) rows[0] += ((now.getHours() < 12) ? ' AM' : ' PM');
//When the watch is locked or in first stage
if (getUnlockStage() < 2) {
let maxHeight = ((g.getHeight() / 2) - HHMM_TOP) / (config.dayProgress.enabledUnlocked ? (rows.length + 1) : rows.length);
//Draw the hours and minutes
g.clearRect(0, 24, g.getWidth(), SECONDS_TOP);
let x = 0;
let y = HHMM_TOP + maxHeight / 2;
for (let row of rows) {
let size = getFontSize(row.length, g.getWidth(), 6, maxHeight);
g.setFont('Vector', size)
.drawString(row, g.getWidth() / 2, y);
y += maxHeight;
for (let digit of locale.time(now, 1)) { //apparently this is how you get an hh:mm time string adjusting for the user's 12/24 hour preference
if (digit != ' ') g.drawImage(FONT[digit], x, HHMM_TOP);
if (digit == ':') x += COLON_WIDTH;
else x += DIGIT_WIDTH;
}
if (storage.readJSON('setting.json')['12hour']) g.drawImage(FONT[(now.getHours() < 12) ? 'am' : 'pm'], 0, HHMM_TOP);
// If the seconds should be displayed, don't use the area when drawing the date
if (shouldDisplaySeconds(now)) {
g.clearRect(0, SECONDS_TOP, SECONDS_LEFT, SECONDS_TOP + DIGIT_HEIGHT)
.setFont('Vector', getFontSize(getDayString(now).length, SECONDS_LEFT, 6, DATE_LETTER_HEIGHT))
.drawString(getDayString(now), DATE_CENTER_X, DOW_CENTER_Y)
.setFont('Vector', getFontSize(getDateString(now).length, SECONDS_LEFT, 6, DATE_LETTER_HEIGHT))
.drawString(getDateString(now), DATE_CENTER_X, DATE_CENTER_Y);
}
// Otherwise, use the seconds area
else {
let string = getDayString(now) + ' ' + getDateString(now);
g.clearRect(0, SECONDS_TOP, g.getWidth(), SECONDS_TOP + DIGIT_HEIGHT)
.setFont('Vector', getFontSize(string.length, g.getWidth(), 6, DATE_LETTER_HEIGHT))
.drawString(string, g.getWidth() / 2, DOW_DATE_CENTER_Y);
}
drawBottomText();
// Draw the bar between the rows if necessary
if (config.bar.enabledLocked && config.bottomLocked.display != 'progress') drawBar(0, HHMM_TOP + DIGIT_HEIGHT, g.getWidth(), SECONDS_TOP);
}
// When watch in second stage
else {
g.clearRect(0, 24, g.getWidth(), g.getHeight() / 2);
rows = [
`${getDayString(now)} ${getDateString(now)} ${locale.time(now, 1)}`,
getHealthString(),
getWeatherString(),
getWeatherRow2()
];
if (shouldDisplaySeconds(now)) rows[0] += ':' + pad(now.getSeconds(), 2);
if (storage.readJSON('setting.json')['12hour']) rows[0] += ((now.getHours() < 12) ? ' AM' : ' PM');
if (config.dayProgress.enabledUnlocked) {
let color = config.dayProgress.color;
g.setColor(color[0], color[1], color[2])
.fillRect(0, y - maxHeight / 2, 176 * getDayProgress(now), y + maxHeight / 2);
let maxHeight = ((g.getHeight() / 2) - HHMM_TOP) / (config.bar.enabledUnlocked ? (rows.length + 1) : rows.length);
let y = HHMM_TOP + maxHeight / 2;
for (let row of rows) {
let size = getFontSize(row.length, g.getWidth(), 6, maxHeight);
g.setFont('Vector', size)
.drawString(row, g.getWidth() / 2, y);
y += maxHeight;
}
if (config.bar.enabledUnlocked) drawBar(0, y - maxHeight / 2, g.getWidth(), y + maxHeight / 2);
}
}
setNextDrawTimeout();
}
// Draw the icons. This is done separately from the main draw routine to avoid having to scale and draw a bunch of images repeatedly.
function drawIcons() {
g.reset().clearRect(0, 24, g.getWidth(), g.getHeight());
for (let i = 0; i < 8; i++) {
let x = [0, 44, 88, 132, 0, 44, 88, 132][i];
let y = [88, 88, 88, 88, 132, 132, 132, 132][i];
let appId = config.shortcuts[i];
let appInfo = storage.readJSON(appId + '.info', 1);
if (!appInfo) continue;
icon = storage.read(appInfo.icon);
g.drawImage(icon, x, y, {
scale: 0.916666666667
});
// Draw the icons. This is done separately from the main draw routine to avoid having to scale and draw a bunch of images repeatedly.
let drawIcons = function () {
g.reset().clearRect(0, 24, g.getWidth(), g.getHeight());
for (let i = 0; i < 8; i++) {
let x = [0, 44, 88, 132, 0, 44, 88, 132][i];
let y = [88, 88, 88, 88, 132, 132, 132, 132][i];
let appId = config.shortcuts[i];
let appInfo = storage.readJSON(appId + '.info', 1);
if (!appInfo) continue;
icon = storage.read(appInfo.icon);
g.drawImage(icon, x, y, {
scale: 0.916666666667
});
}
}
}
weather.on("update", draw);
Bangle.on("step", draw);
Bangle.on('lock', locked => {
//If the watch is unlocked, draw the icons
if (!locked) drawIcons();
draw();
});
// Draw only the bottom row if we are in first or second stage unlock, otherwise call drawClock()
let drawBottomRowOrClock = function () {
if (getUnlockStage() < 2) drawBottomText();
else drawClock();
}
// Show launcher when middle button pressed
Bangle.setUI("clock");
// Load widgets
Bangle.loadWidgets();
Bangle.drawWidgets();
weather.on("update", drawBottomRowOrClock);
Bangle.on("step", drawBottomRowOrClock);
let onLock = locked => {
//If the watch is unlocked and the necessary number of dual stage taps have been performed, draw the shortcuts
if (!locked && dualStageTaps >= config.dualStageUnlock) drawIcons();
// Launch an app given the current ID. Handles special cases:
// false: Do nothing
// '#LAUNCHER': Open the launcher
// nonexistent app: Do nothing
function launch(appId) {
if (appId == false) return;
else if (appId == '#LAUNCHER') {
Bangle.buzz();
Bangle.showLauncher();
} else {
let appInfo = storage.readJSON(appId + '.info', 1);
if (appInfo) {
// If locked, reset dual stage taps to zero
else if (locked) dualStageTaps = 0;
drawLockedSeconds(true);
};
Bangle.on('lock', onLock);
// Launch an app given the current ID. Handles special cases:
// false: Do nothing
// '#LAUNCHER': Open the launcher
// nonexistent app: Do nothing
let launch = function (appId, fast) {
if (appId == false) return;
else if (appId == '#LAUNCHER') {
Bangle.buzz();
load(appInfo.src);
Bangle.showLauncher();
} else {
let appInfo = storage.readJSON(appId + '.info', 1);
if (appInfo) {
Bangle.buzz();
if (fast) Bangle.load(appInfo.src);
else load(appInfo.src);
}
}
}
}
//Set up touch to launch the selected app
Bangle.on('touch', function (button, xy) {
let x = Math.floor(xy.x / 44);
if (x < 0) x = 0;
else if (x > 3) x = 3;
//Set up touch to launch the selected app, and to handle dual stage unlock
let dualStageTaps = 0;
let y = Math.floor(xy.y / 44);
if (y < 0) y = -1;
else if (y > 3) y = 1;
else y -= 2;
let onTouch = function (button, xy) {
// If only the first stage has been unlocked, increase the counter
if (dualStageTaps < config.dualStageUnlock) {
dualStageTaps++;
Bangle.buzz();
if (y < 0) {
Bangle.buzz();
Bangle.showLauncher();
} else {
let i = 4 * y + x;
launch(config.shortcuts[i]);
// If we reach the unlock threshold, redraw the screen because we have now done the second unlock stage
if (dualStageTaps == config.dualStageUnlock) {
drawIcons();
drawClock();
setNextDrawTimeout(); // In case we need to replace an every minute timeout with an every second timeout
}
// If we have unlocked both stages, handle a shortcut tap
} else {
let x = Math.floor(xy.x / 44);
if (x < 0) x = 0;
else if (x > 3) x = 3;
let y = Math.floor(xy.y / 44);
if (y < 0) y = -1;
else if (y > 3) y = 1;
else y -= 2;
if (y < 0) {
Bangle.buzz();
Bangle.showLauncher();
} else {
let i = 4 * y + x;
launch(config.shortcuts[i], config.fastLoad.shortcuts[i]);
}
}
};
Bangle.on('touch', onTouch);
//Set up swipe handler
let onSwipe = function (lr, ud) {
if (lr == -1) launch(config.swipe.left, config.fastLoad.swipe.left);
else if (lr == 1) launch(config.swipe.right, config.fastLoad.swipe.right);
else if (ud == -1) launch(config.swipe.up, config.fastLoad.swipe.up);
else if (ud == 1) launch(config.swipe.down, config.fastLoad.swipe.down);
};
Bangle.on('swipe', onSwipe);
// If the clock starts with the watch unlocked, the first stage of unlocking is skipped
if (!Bangle.isLocked()) {
dualStageTaps = config.dualStageUnlock;
drawIcons();
}
});
//Set up swipe handler
Bangle.on('swipe', function (direction) {
if (direction == -1) launch(config.swipe.left);
else if (direction == 0) launch(config.swipe.up);
else launch(config.swipe.right);
});
// Show launcher when middle button pressed, and enable fast loading
Bangle.setUI({
mode: "clock", remove: () => {
if (drawTimeout !== undefined) {
clearTimeout(drawTimeout);
drawTimeout = undefined;
}
Bangle.removeListener('charging', onCharging);
weather.removeListener('update', drawBottomRowOrClock);
Bangle.removeListener('step', drawBottomRowOrClock);
Bangle.removeListener('lock', onLock);
Bangle.removeListener('touch', onTouch);
Bangle.removeListener('swipe', onSwipe);
g.reset();
}
});
if (!Bangle.isLocked()) drawIcons();
// Load widgets
Bangle.loadWidgets();
Bangle.drawWidgets();
draw();
drawLockedSeconds(true);
}

124
apps/infoclk/configLoad.js Normal file
View File

@ -0,0 +1,124 @@
const storage = require("Storage");
const SETTINGS_FILE = "infoclk.json";
let defaultConfig = {
dualStageUnlock: 0,
seconds: {
// Displaying the seconds can reduce battery life because the CPU must wake up more often to update the display.
// The seconds will be shown unless one of these conditions is enabled here, and currently true.
hideLocked: false, // Hide the seconds when the display is locked.
hideBattery: 20, // Hide the seconds when the battery is at or below a defined percentage.
hideTime: true, // Hide the seconds when between a certain period of time. Useful for when you are sleeping and don't need the seconds
hideStart: 2200, // The time when the seconds are hidden: first 2 digits are hours on a 24 hour clock, last 2 are minutes
hideEnd: 700, // The time when the seconds are shown again
hideAlways: false, // Always hide (never show) the seconds
forceWhenUnlocked: 1, // Force the seconds to be displayed when the watch is unlocked, no matter the other settings. 0 = never, 1 = first or second stage unlock, 2 = second stage unlock only
},
date: {
// Settings related to the display of the date
mmdd: true, // If true, display the month first. If false, display the date first.
separator: '-', // The character that goes between the month and date
monthName: false, // If false, display the month as a number. If true, display the name.
monthFullName: false, // If displaying the name: If false, display an abbreviation. If true, display a full name.
dayFullName: false, // If false, display the day of the week's abbreviation. If true, display the full name.
},
bottomLocked: {
display: 'weather' // What to display in the bottom row when locked:
// 'weather': The current temperature and weather description
// 'steps': Step count
// 'health': Step count and bpm
// 'progress': Day progress bar
// false: Nothing
},
shortcuts: [
//8 shortcuts, displayed in the bottom half of the screen (2 rows of 4 shortcuts) when unlocked
// false = no shortcut
// '#LAUNCHER' = open the launcher
// any other string = name of app to open
'stlap', 'keytimer', 'pomoplus', 'alarm',
'rpnsci', 'calendar', 'torch', 'weather'
],
swipe: {
// 4 shortcuts to launch upon swiping:
// false = no shortcut
// '#LAUNCHER' = open the launcher
// any other string = name of app to open
up: 'messageui', // Swipe up or swipe down, due to limitation of event handler
down: 'messageui',
left: '#LAUNCHER',
right: '#LAUNCHER',
},
fastLoad: {
shortcuts: [
false, false, false, false,
false, false, false, false
],
swipe: {
up: false,
down: false,
left: false,
right: false
}
},
bar: {
enabledLocked: true, // Whether this bar is enabled when the watch is locked
enabledUnlocked: false, // Whether the bar is enabled when the watch is unlocked
type: 'split', // off = no bar, dayProgress = day progress bar, calendar = calendar bar, split = both
dayProgress: { // A progress bar representing how far through the day you are
color: [0, 0, 1], // The color of the bar
start: 700, // The time of day that the bar starts filling
end: 2200, // The time of day that the bar becomes full
reset: 300 // The time of day when the progress bar resets from full to empty
},
calendar: {
duration: 10800,
pipeColor: [1, 1, 1],
defaultColor: [0, 0, 1]
},
},
lowBattColor: {
// The text can change color to indicate that the battery is low
level: 20, // The percentage where this happens
color: [1, 0, 0] // The color that the text changes to
}
}
let storedConfig = storage.readJSON(SETTINGS_FILE, true) || {};
// Ugly slow workaround because object.constructor doesn't exist on Bangle
function isDictionary(object) {
return JSON.stringify(object)[0] == '{';
}
/** Merge two objects recursively. (Object.assign() cannot be used here because it is NOT recursive.)
* Any key that is in one object but not the other will be included as is.
* Any key that is in both objects, but whose value is not a dictionary in both objects, will have the version in overlay included.
* Any key that whose value is a dictionary in both properties will have its result be set to a recursive call to merge.
*/
function merge(overlay, base) {
let result = base;
for (objectKey in overlay) {
if (!Object.keys(base).includes(objectKey)) result[objectKey] = overlay[objectKey]; // If the key isn't there, add it
else if (isDictionary(base[objectKey]) && isDictionary(overlay[objectKey])) // If the key is a dictionary in both, do recursive call
result[objectKey] = merge(overlay[objectKey], base[objectKey]);
else result[objectKey] = overlay[objectKey]; // Otherwise, override
}
return result;
}
exports.getConfig = () => {
return merge(storedConfig, defaultConfig);
};

View File

@ -1 +1 @@
require("heatshrink").decompress(atob("mEwgOAA4YFS/4AKEf5BlABcAjAgBjAfBAuhH/Apo"))
require("heatshrink").decompress(atob("mEwgOAA4YFS/4AKEf5BlABcAjAgBjAfBAuhH/Apo"))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 B

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -1,8 +1,7 @@
{
"id": "infoclk",
"name": "Informational clock",
"version": "0.08",
"dependencies": {"weather":"app"},
"version": "0.09",
"description": "A configurable clock with extra info and shortcuts when unlocked, but large time when locked",
"readme": "README.md",
"icon": "icon.png",
@ -24,6 +23,10 @@
"name": "infoclk-font.js",
"url": "font.js"
},
{
"name": "infoclk-config.js",
"url": "configLoad.js"
},
{
"name": "infoclk.img",
"url": "icon.js",
@ -34,5 +37,8 @@
{
"name": "infoclk.json"
}
]
}
],
"dependencies": {
"weather": "app"
}
}

View File

@ -2,71 +2,7 @@
const SETTINGS_FILE = "infoclk.json";
const storage = require('Storage');
let config = Object.assign({
seconds: {
// Displaying the seconds can reduce battery life because the CPU must wake up more often to update the display.
// The seconds will be shown unless one of these conditions is enabled here, and currently true.
hideLocked: false, // Hide the seconds when the display is locked.
hideBattery: 20, // Hide the seconds when the battery is at or below a defined percentage.
hideTime: true, // Hide the seconds when between a certain period of time. Useful for when you are sleeping and don't need the seconds
hideStart: 2200, // The time when the seconds are hidden: first 2 digits are hours on a 24 hour clock, last 2 are minutes
hideEnd: 700, // The time when the seconds are shown again
hideAlways: false, // Always hide (never show) the seconds
},
date: {
// Settings related to the display of the date
mmdd: true, // If true, display the month first. If false, display the date first.
separator: '-', // The character that goes between the month and date
monthName: false, // If false, display the month as a number. If true, display the name.
monthFullName: false, // If displaying the name: If false, display an abbreviation. If true, display a full name.
dayFullName: false, // If false, display the day of the week's abbreviation. If true, display the full name.
},
bottomLocked: {
display: 'weather' // What to display in the bottom row when locked:
// 'weather': The current temperature and weather description
// 'steps': Step count
// 'health': Step count and bpm
// 'progress': Day progress bar
// false: Nothing
},
shortcuts: [
//8 shortcuts, displayed in the bottom half of the screen (2 rows of 4 shortcuts) when unlocked
// false = no shortcut
// '#LAUNCHER' = open the launcher
// any other string = name of app to open
'stlap', 'keytimer', 'pomoplus', 'alarm',
'rpnsci', 'calendar', 'torch', 'weather'
],
swipe: {
// 3 shortcuts to launch upon swiping:
// false = no shortcut
// '#LAUNCHER' = open the launcher
// any other string = name of app to open
up: 'messages', // Swipe up or swipe down, due to limitation of event handler
left: '#LAUNCHER',
right: '#LAUNCHER',
},
dayProgress: {
// A progress bar representing how far through the day you are
enabledLocked: true, // Whether this bar is enabled when the watch is locked
enabledUnlocked: false, // Whether the bar is enabled when the watch is unlocked
color: [0, 0, 1], // The color of the bar
start: 700, // The time of day that the bar starts filling
end: 2200, // The time of day that the bar becomes full
reset: 300 // The time of day when the progress bar resets from full to empty
},
lowBattColor: {
// The text can change color to indicate that the battery is low
level: 20, // The percentage where this happens
color: [1, 0, 0] // The color that the text changes to
}
}, storage.readJSON(SETTINGS_FILE));
let config = require('infoclk-config.js').getConfig();
function saveSettings() {
storage.writeJSON(SETTINGS_FILE, config);
@ -172,6 +108,18 @@
}
}
});
},
'...unconditionally when unlocked': {
value: config.seconds.forceWhenUnlocked,
format: value => ['No', 'First or second stage', 'Second stage only'][value],
onchange: value => {
config.seconds.forceWhenUnlocked = value;
saveSettings();
},
min: 0,
max: 2,
step: 1,
wrap: false
}
});
}
@ -190,7 +138,7 @@
{ name: 'Weather', val: 'weather' },
{ name: 'Step count', val: 'steps' },
{ name: 'Steps + BPM', val: 'health' },
{ name: 'Day progresss bar', val: 'progress' },
{ name: 'Bar', val: 'progress' },
{ name: 'Nothing', val: false }
];
@ -222,128 +170,260 @@
},
'Top first': {
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[0]),
format: value => shortcutOptions[value].name,
format: value => (value == -1) ? 'Unknown app!' : shortcutOptions[value].name,
min: 0,
max: shortcutOptions.length - 1,
wrap: false,
onchange: value => {
config.shortcuts[0] = shortcutOptions[value].val;
config.fastLoad.shortcuts[0] = false;
saveSettings();
}
},
'Top second': {
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[1]),
format: value => shortcutOptions[value].name,
format: value => (value == -1) ? 'Unknown app!' : shortcutOptions[value].name,
min: 0,
max: shortcutOptions.length - 1,
wrap: false,
onchange: value => {
config.shortcuts[1] = shortcutOptions[value].val;
config.fastLoad.shortcuts[1] = false;
saveSettings();
}
},
'Top third': {
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[2]),
format: value => shortcutOptions[value].name,
format: value => (value == -1) ? 'Unknown app!' : shortcutOptions[value].name,
min: 0,
max: shortcutOptions.length - 1,
wrap: false,
onchange: value => {
config.shortcuts[2] = shortcutOptions[value].val;
config.fastLoad.shortcuts[2] = false;
saveSettings();
}
},
'Top fourth': {
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[3]),
format: value => shortcutOptions[value].name,
format: value => (value == -1) ? 'Unknown app!' : shortcutOptions[value].name,
min: 0,
max: shortcutOptions.length - 1,
wrap: false,
onchange: value => {
config.shortcuts[3] = shortcutOptions[value].val;
config.fastLoad.shortcuts[3] = false;
saveSettings();
}
},
'Bottom first': {
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[4]),
format: value => shortcutOptions[value].name,
format: value => (value == -1) ? 'Unknown app!' : shortcutOptions[value].name,
min: 0,
max: shortcutOptions.length - 1,
wrap: false,
onchange: value => {
config.shortcuts[4] = shortcutOptions[value].val;
config.fastLoad.shortcuts[4] = false;
saveSettings();
}
},
'Bottom second': {
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[5]),
format: value => shortcutOptions[value].name,
format: value => (value == -1) ? 'Unknown app!' : shortcutOptions[value].name,
min: 0,
max: shortcutOptions.length - 1,
wrap: false,
onchange: value => {
config.shortcuts[5] = shortcutOptions[value].val;
config.fastLoad.shortcuts[5] = false;
saveSettings();
}
},
'Bottom third': {
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[6]),
format: value => shortcutOptions[value].name,
format: value => (value == -1) ? 'Unknown app!' : shortcutOptions[value].name,
min: 0,
max: shortcutOptions.length - 1,
wrap: false,
onchange: value => {
config.shortcuts[6] = shortcutOptions[value].val;
config.fastLoad.shortcuts[6] = false;
saveSettings();
}
},
'Bottom fourth': {
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[7]),
format: value => shortcutOptions[value].name,
format: value => (value == -1) ? 'Unknown app!' : shortcutOptions[value].name,
min: 0,
max: shortcutOptions.length - 1,
wrap: false,
onchange: value => {
config.shortcuts[7] = shortcutOptions[value].val;
config.fastLoad.shortcuts[7] = false;
saveSettings();
}
},
'Swipe up': {
value: shortcutOptions.map(item => item.val).indexOf(config.swipe.up),
format: value => shortcutOptions[value].name,
format: value => (value == -1) ? 'Unknown app!' : shortcutOptions[value].name,
min: 0,
max: shortcutOptions.length - 1,
wrap: false,
onchange: value => {
config.swipe.up = shortcutOptions[value].val;
config.fastLoad.swipe.up = false;
saveSettings();
}
},
'Swipe down': {
value: shortcutOptions.map(item => item.val).indexOf(config.swipe.down),
format: value => (value == -1) ? 'Unknown app!' : shortcutOptions[value].name,
min: 0,
max: shortcutOptions.length - 1,
wrap: false,
onchange: value => {
config.swipe.down = shortcutOptions[value].val;
config.fastLoad.swipe.down = false;
saveSettings();
}
},
'Swipe left': {
value: shortcutOptions.map(item => item.val).indexOf(config.swipe.left),
format: value => shortcutOptions[value].name,
format: value => (value == -1) ? 'Unknown app!' : shortcutOptions[value].name,
min: 0,
max: shortcutOptions.length - 1,
wrap: false,
onchange: value => {
config.swipe.left = shortcutOptions[value].val;
config.fastLoad.swipe.left = false;
saveSettings();
}
},
'Swipe right': {
value: shortcutOptions.map(item => item.val).indexOf(config.swipe.right),
format: value => shortcutOptions[value].name,
format: value => (value == -1) ? 'Unknown app!' : shortcutOptions[value].name,
min: 0,
max: shortcutOptions.length - 1,
wrap: false,
onchange: value => {
config.swipe.right = shortcutOptions[value].val;
config.fastLoad.swipe.right = false;
saveSettings();
}
},
}
});
}
// The menu for configuring which apps can be fast loaded
function showFastLoadMenu() {
E.showMenu();
E.showAlert(/*LANG*/"WARNING! Only enable fast loading for apps that use widgets.").then(() => {
E.showMenu({
'': {
'title': 'Shortcuts',
'back': showMainMenu
},
'Top first': {
value: config.fastLoad.shortcuts[0],
format: value => value ? 'Fast' : 'Slow',
onchange: value => {
config.fastLoad.shortcuts[0] = value;
saveSettings();
}
},
'Top second': {
value: config.fastLoad.shortcuts[1],
format: value => value ? 'Fast' : 'Slow',
onchange: value => {
config.fastLoad.shortcuts[1] = value;
saveSettings();
}
},
'Top third': {
value: config.fastLoad.shortcuts[2],
format: value => value ? 'Fast' : 'Slow',
onchange: value => {
config.fastLoad.shortcuts[2] = value;
saveSettings();
}
},
'Top fourth': {
value: config.fastLoad.shortcuts[3],
format: value => value ? 'Fast' : 'Slow',
onchange: value => {
config.fastLoad.shortcuts[3] = value;
saveSettings();
}
},
'Bottom first': {
value: config.fastLoad.shortcuts[4],
format: value => value ? 'Fast' : 'Slow',
onchange: value => {
config.fastLoad.shortcuts[4] = value;
saveSettings();
}
},
'Bottom second': {
value: config.fastLoad.shortcuts[5],
format: value => value ? 'Fast' : 'Slow',
onchange: value => {
config.fastLoad.shortcuts[5] = value;
saveSettings();
}
},
'Bottom third': {
value: config.fastLoad.shortcuts[6],
format: value => value ? 'Fast' : 'Slow',
onchange: value => {
config.fastLoad.shortcuts[6] = value;
saveSettings();
}
},
'Bottom fourth': {
value: config.fastLoad.shortcuts[7],
format: value => value ? 'Fast' : 'Slow',
onchange: value => {
config.fastLoad.shortcuts[7] = value;
saveSettings();
}
},
'Swipe up': {
value: config.fastLoad.swipe.up,
format: value => value ? 'Fast' : 'Slow',
onchange: value => {
config.fastLoad.swipe.up = value;
saveSettings();
}
},
'Swipe down': {
value: config.fastLoad.swipe.down,
format: value => value ? 'Fast' : 'Slow',
onchange: value => {
config.fastLoad.swipe.down = value;
saveSettings();
}
},
'Swipe left': {
value: config.fastLoad.swipe.left,
format: value => value ? 'Fast' : 'Slow',
onchange: value => {
config.fastLoad.swipe.left = value;
saveSettings();
}
},
'Swipe right': {
value: config.fastLoad.swipe.right,
format: value => value ? 'Fast' : 'Slow',
onchange: value => {
config.fastLoad.swipe.right = value;
saveSettings();
}
}
});
})
}
const COLOR_OPTIONS = [
{ name: 'Black', val: [0, 0, 0] },
{ name: 'Blue', val: [0, 0, 1] },
@ -355,11 +435,197 @@
{ name: 'White', val: [1, 1, 1] }
];
const BAR_MODE_OPTIONS = [
{ name: 'None', val: 'off' },
{ name: 'Day progress only', val: 'dayProgress' },
{ name: 'Calendar only', val: 'calendar' },
{ name: 'Split', val: 'split' }
];
// Workaround for being unable to use == on arrays: convert them into strings
function colorString(color) {
return `${color[0]} ${color[1]} ${color[2]}`;
}
//Menu to configure the bar
function showBarMenu() {
E.showMenu({
'': {
'title': 'Bar',
'back': showMainMenu
},
'Enable while locked': {
value: config.bar.enabledLocked,
onchange: value => {
config.bar.enableLocked = value;
saveSettings();
}
},
'Enable while unlocked': {
value: config.bar.enabledUnlocked,
onchange: value => {
config.bar.enabledUnlocked = value;
saveSettings();
}
},
'Mode': {
value: BAR_MODE_OPTIONS.map(item => item.val).indexOf(config.bar.type),
format: value => BAR_MODE_OPTIONS[value].name,
onchange: value => {
config.bar.type = BAR_MODE_OPTIONS[value].val;
saveSettings();
},
min: 0,
max: BAR_MODE_OPTIONS.length - 1,
wrap: true
},
'Day progress': () => {
E.showMenu({
'': {
'title': 'Day progress',
'back': showBarMenu
},
'Color': {
value: COLOR_OPTIONS.map(item => colorString(item.val)).indexOf(colorString(config.bar.dayProgress.color)),
format: value => COLOR_OPTIONS[value].name,
min: 0,
max: COLOR_OPTIONS.length - 1,
wrap: false,
onchange: value => {
config.bar.dayProgress.color = COLOR_OPTIONS[value].val;
saveSettings();
}
},
'Start hour': {
value: Math.floor(config.bar.dayProgress.start / 100),
format: hourToString,
min: 0,
max: 23,
wrap: true,
onchange: hour => {
minute = config.bar.dayProgress.start % 100;
config.bar.dayProgress.start = (100 * hour) + minute;
saveSettings();
}
},
'Start minute': {
value: config.bar.dayProgress.start % 100,
min: 0,
max: 59,
wrap: true,
onchange: minute => {
hour = Math.floor(config.bar.dayProgress.start / 100);
config.bar.dayProgress.start = (100 * hour) + minute;
saveSettings();
}
},
'End hour': {
value: Math.floor(config.bar.dayProgress.end / 100),
format: hourToString,
min: 0,
max: 23,
wrap: true,
onchange: hour => {
minute = config.bar.dayProgress.end % 100;
config.bar.dayProgress.end = (100 * hour) + minute;
saveSettings();
}
},
'End minute': {
value: config.bar.dayProgress.end % 100,
min: 0,
max: 59,
wrap: true,
onchange: minute => {
hour = Math.floor(config.bar.dayProgress.end / 100);
config.bar.dayProgress.end = (100 * hour) + minute;
saveSettings();
}
},
'Reset hour': {
value: Math.floor(config.bar.dayProgress.reset / 100),
format: hourToString,
min: 0,
max: 23,
wrap: true,
onchange: hour => {
minute = config.bar.dayProgress.reset % 100;
config.bar.dayProgress.reset = (100 * hour) + minute;
saveSettings();
}
},
'Reset minute': {
value: config.bar.dayProgress.reset % 100,
min: 0,
max: 59,
wrap: true,
onchange: minute => {
hour = Math.floor(config.bar.dayProgress.reset / 100);
config.bar.dayProgress.reset = (100 * hour) + minute;
saveSettings();
}
}
});
},
'Calendar bar': () => {
E.showMenu({
'': {
'title': 'Calendar bar',
'back': showBarMenu
},
'Look ahead duration': {
value: config.bar.calendar.duration,
format: value => {
let hours = value / 3600;
let minutes = (value % 3600) / 60;
let seconds = value % 60;
let result = (hours == 0) ? '' : `${hours} hr`;
if (minutes != 0) {
if (result == '') result = `${minutes} min`;
else result += `, ${minutes} min`;
}
if (seconds != 0) {
if (result == '') result = `${seconds} sec`;
else result += `, ${seconds} sec`;
}
return result;
},
onchange: value => {
config.bar.calendar.duration = value;
saveSettings();
},
min: 900,
max: 86400,
step: 900
},
'Pipe color': {
value: COLOR_OPTIONS.map(color => colorString(color.val)).indexOf(colorString(config.bar.calendar.pipeColor)),
format: value => COLOR_OPTIONS[value].name,
onchange: value => {
config.bar.calendar.pipeColor = COLOR_OPTIONS[value].val;
saveSettings();
},
min: 0,
max: COLOR_OPTIONS.length - 1,
wrap: true
},
'Default color': {
value: COLOR_OPTIONS.map(color => colorString(color.val)).indexOf(colorString(config.bar.calendar.defaultColor)),
format: value => COLOR_OPTIONS[value].name,
onchange: value => {
config.bar.calendar.defaultColor = COLOR_OPTIONS[value].val;
saveSettings();
},
min: 0,
max: COLOR_OPTIONS.length - 1,
wrap: true
}
});
}
});
}
//Shows the top level menu
function showMainMenu() {
E.showMenu({
@ -367,6 +633,16 @@
'title': 'Informational Clock',
'back': back
},
'Dual stage unlock': {
value: config.dualStageUnlock,
format: value => (value == 0) ? "Off" : `${value} taps`,
min: 0,
step: 1,
onchange: value => {
config.dualStageUnlock = value;
saveSettings();
}
},
'Seconds display': showSecondsMenu,
'Day of week format': {
value: config.date.dayFullName,
@ -433,108 +709,8 @@
}
},
'Shortcuts': showShortcutMenu,
'Day progress': () => {
E.showMenu({
'': {
'title': 'Day progress',
'back': showMainMenu
},
'Enable while locked': {
value: config.dayProgress.enabledLocked,
onchange: value => {
config.dayProgress.enableLocked = value;
saveSettings();
}
},
'Enable while unlocked': {
value: config.dayProgress.enabledUnlocked,
onchange: value => {
config.dayProgress.enabledUnlocked = value;
saveSettings();
}
},
'Color': {
value: COLOR_OPTIONS.map(item => colorString(item.val)).indexOf(colorString(config.dayProgress.color)),
format: value => COLOR_OPTIONS[value].name,
min: 0,
max: COLOR_OPTIONS.length - 1,
wrap: false,
onchange: value => {
config.dayProgress.color = COLOR_OPTIONS[value].val;
saveSettings();
}
},
'Start hour': {
value: Math.floor(config.dayProgress.start / 100),
format: hourToString,
min: 0,
max: 23,
wrap: true,
onchange: hour => {
minute = config.dayProgress.start % 100;
config.dayProgress.start = (100 * hour) + minute;
saveSettings();
}
},
'Start minute': {
value: config.dayProgress.start % 100,
min: 0,
max: 59,
wrap: true,
onchange: minute => {
hour = Math.floor(config.dayProgress.start / 100);
config.dayProgress.start = (100 * hour) + minute;
saveSettings();
}
},
'End hour': {
value: Math.floor(config.dayProgress.end / 100),
format: hourToString,
min: 0,
max: 23,
wrap: true,
onchange: hour => {
minute = config.dayProgress.end % 100;
config.dayProgress.end = (100 * hour) + minute;
saveSettings();
}
},
'End minute': {
value: config.dayProgress.end % 100,
min: 0,
max: 59,
wrap: true,
onchange: minute => {
hour = Math.floor(config.dayProgress.end / 100);
config.dayProgress.end = (100 * hour) + minute;
saveSettings();
}
},
'Reset hour': {
value: Math.floor(config.dayProgress.reset / 100),
format: hourToString,
min: 0,
max: 23,
wrap: true,
onchange: hour => {
minute = config.dayProgress.reset % 100;
config.dayProgress.reset = (100 * hour) + minute;
saveSettings();
}
},
'Reset minute': {
value: config.dayProgress.reset % 100,
min: 0,
max: 59,
wrap: true,
onchange: minute => {
hour = Math.floor(config.dayProgress.reset / 100);
config.dayProgress.reset = (100 * hour) + minute;
saveSettings();
}
}
});
},
'Fast load shortcuts': showFastLoadMenu,
'Bar': showBarMenu,
'Low battery color': () => {
E.showMenu({
'': {

3
apps/kanagsec/ChangeLog Normal file
View File

@ -0,0 +1,3 @@
0.01: New App!
0.02: Fixes temperature not loading at start.
0.03: reduces energy consumption, fixes bug where app thinks that screen is still unlocked after receiving a message

14
apps/kanagsec/README.md Normal file
View File

@ -0,0 +1,14 @@
# Kanagawa clock
This clock displays the great wave of kanagawa in the background.
It also displays:
- Hours, minutes and seconds.
- Seconds are only displayed when screen is unlocked.
- Years, monts and day as in numbers.
- Name representation of the current day.
- Current temperature from weather, if available.
- Battery level in percent.
![](screenshot.png)
![](screenshot2.png)

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+1YA/ABusBJIACGMwnFF0paGC6wvYDKATDCweHK54YGSY4RKAAOIwIaJC5YVKCJmHF4QzDF6gyFAYIMHwIABBgICGF7OlFYwvEqwEBMQWIL6gOCChAjBq8yruBrsyllXBgaRBAAIuRahQgBAIMyhFXGYMHmQ5Bw5eBJBIvUJoJZBq0shEIFoMIlkyF4IfFMgQtSF4kyQ4IpBAYICBFwQ4BrtdZogYBMRgvMFgQpBAIQHCg8HYQeIwRiESZiPKFoIvFMAQABqwZBwLDCMQaaGGRgHDw6ABqwvEAQUzrur0ouBXYa/LGBIIGwItDFwZXEw4WCGYKOMABReBGwJfBq4CBYIS9DGAOlCAI3CwOlFqgXBAAIFBGgKSCL4KEILgItUFgJ7BDQL1EAANdMIIvJMIKPUF4OHroqCE4IBCrqPBciQvOEILoCmVWAwOrR4Usca4AIbAWrMAIqBRILABFwJfkAAOIXIIwBq5lBd5QvYLgOrw+sdISSBFwQEBSDgcCEoWBLwNXFQIBBBQIGBY4OrAAgkLBgIOEDAssQwWBq4qBLIYHBGoQWFACWl0q7BAAKICcwIuCXIzzBq4VDDYOlEAQjDAgIBBBwITDwAACwK0EAYKGBdA4QBCgIaBD4YAJFIeAvVWqwCCJ4IvDEgIuHSYIPBEAIZBDwYATXQQsBhAuFcgqhCD5erFpZ3BDwIADw4pFGAoSCFxJFCFhGAZIIOBPwNXRRAIH1mHFxBCDwAuFFoZMDEg+rw5mFFoIHBKggaBAwRrBAoeA0mlFweIwNdLZCVHHIYaCE4LWBhA0ClkrlYEBqGACQddq8zq9dwOIxB0Cw+rFwIHBIAWIq5vCIoRHCw4yCfQTfDH4YEBrtdKIQoBE4TyGMgQ/Bq+IJYInBAwKFBN5BwCD4oeBq5FBJAISELoaFCHQRGClhaBPQMsbRC0HrtWF4LxGN4TnBGgIQBwOlq5bBA4Msg8yGBLnBIwddmcylh4BNQKqCEAJwBFwQ8BE4MyvWAaoQKBqwwBq5mBwQYBEQLqDq6jBAAIuCK4Q8DL4YyBAwOrCQMIq2AJQIxBAQIeBlkIliwCA4NWRAI8BAAL8BdQjLDFYQPBKwQIBEgQwBPwYDCEAI9BFgIGBhBcDCwLjDD4NdGYgoDGIIFBSIIhBGAQADveA0mk0oGBGgI3CAAN7ZgbBBSYQ3BHAaKBb4LsBHgI4ChBPBvWlAAJFCFoIyBGYd6PIRWDUQQACBYOragRGBBwMyg5FCBgIwBGIOB0pGBAAR7DAwWHd4JGBg8HLgIxCYoIpCZgSvDg8yrrNDqwcCJIaHCFwR8DSQb2BcwIUCEQLWBCgJIFmUrZ4NdEAWrRoVcCQgKBw4ACNYgXDA"))

152
apps/kanagsec/app.js Normal file

File diff suppressed because one or more lines are too long

BIN
apps/kanagsec/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -0,0 +1,23 @@
{
"id": "kanagsec",
"name": "Kanagawa clock",
"shortName":"kanagawa",
"version":"0.03",
"description": "A clock that displays the great wave of kanagawa (image from wikipedia) with seconds in active mode.",
"icon": "app.png",
"tags": "clock, kanagawa, wave",
"type": "clock",
"supports" : ["BANGLEJS2"],
"readme": "README.md",
"allow_emulator":true,
"storage":
[
{"name":"kanagsec.app.js","url":"app.js"},
{"name":"kanagsec.img","url":"app-icon.js","evaluate":true}
],
"screenshots" :
[
{ "url":"screenshot.png" },
{ "url":"screenshot2.png" }
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

@ -4,3 +4,4 @@
0.04: Show a random kana every minute to improve learning
0.05: Tell clock widgets to hide
0.06: Fix exception when showing missing hiragana 'WO'
0.07: Fix regression in bitmap selection on some code paths

View File

@ -224,6 +224,7 @@ function drawKana (x, y) {
g.setColor(0, 0, 0);
g.fillRect(0, 0, g.getWidth(), 6 * (h / 8) + 1);
g.setColor(1, 1, 1);
kana = hiramode ? hiragana[curkana] : katakana[curkana];
g.drawImage(kana, x + 20, 40, { scale: 1.6 });
g.setColor(1, 1, 1);
g.setFont('Vector', 24);
@ -266,4 +267,3 @@ Bangle.setUI('clock');
Bangle.loadWidgets();
tickWatch();
setInterval(tickWatch, 1000 * 60);

View File

@ -2,7 +2,7 @@
"id": "kanawatch",
"name": "Kanawatch",
"shortName": "Kanawatch",
"version": "0.06",
"version": "0.07",
"type": "clock",
"description": "Learn Hiragana and Katakana",
"icon": "app.png",

View File

@ -1,2 +1,3 @@
0.01: New app!
0.02: Submitted to the app loader
0.02: Submitted to the app loader
0.03: Rewrote to use scheduler library

View File

@ -1,27 +1,27 @@
Bangle.keytimer_ACTIVE = true;
const storage = require('Storage');
const common = require("keytimer-com.js");
const storage = require("Storage");
const keypad = require("keytimer-keys.js");
const timerView = require("keytimer-tview.js");
Bangle.KEYTIMER = true;
Bangle.loadWidgets();
Bangle.drawWidgets();
//Save our state when the app is closed
E.on('kill', () => {
storage.writeJSON(common.STATE_PATH, common.state);
storage.writeJSON('keytimer.json', common.state);
});
//Handle touch here. I would implement these separately in each view, but I can't figure out how to clear the event listeners.
// Handle touch here. I would implement these separately in each view, but I can't figure out how to clear the event listeners.
Bangle.on('touch', (button, xy) => {
if (common.state.wasRunning) timerView.touch(button, xy);
if (common.timerExists()) timerView.touch(button, xy);
else keypad.touch(button, xy);
});
Bangle.on('swipe', dir => {
if (!common.state.wasRunning) keypad.swipe(dir);
if (!common.timerExists()) keypad.swipe(dir);
});
if (common.state.wasRunning) timerView.show(common);
if (common.timerExists()) timerView.show(common);
else keypad.show(common);

View File

@ -1,11 +0,0 @@
const keytimer_common = require("keytimer-com.js");
//Only start the timeout if the timer is running
if (keytimer_common.state.running) {
setTimeout(() => {
//Check now to avoid race condition
if (Bangle.keytimer_ACTIVE === undefined) {
load('keytimer-ring.js');
}
}, keytimer_common.getTimeLeft());
}

View File

@ -1,42 +1,49 @@
const storage = require("Storage");
const heatshrink = require("heatshrink");
const sched = require('sched');
const storage = require('Storage');
exports.STATE_PATH = "keytimer.state.json";
exports.BUTTON_ICONS = {
play: heatshrink.decompress(atob("jEYwMAkAGBnACBnwCBn+AAQPgAQPwAQP8AQP/AQXAAQPwAQP8AQP+AQgICBwQUCEAn4FggyBHAQ+CIgQ")),
pause: heatshrink.decompress(atob("jEYwMA/4BBAX4CEA")),
reset: heatshrink.decompress(atob("jEYwMA/4BB/+BAQPDAQPnAQIAKv///0///8j///EP//wAQQICBwQUCEhgyCHAQ+CIgI="))
exports.running = function () {
return sched.getAlarm('keytimer') != undefined;
};
//Store the minimal amount of information to be able to reconstruct the state of the timer at any given time.
//This is necessary because it is necessary to write to flash to let the timer run in the background, so minimizing the writes is necessary.
exports.STATE_DEFAULT = {
wasRunning: false, //If the timer ever was running. Used to determine whether to display a reset button
running: false, //Whether the timer is currently running
startTime: 0, //When the timer was last started. Difference between this and now is how long timer has run continuously.
pausedTime: 0, //When the timer was last paused. Used for expiration and displaying timer while paused.
elapsedTime: 0, //How much time the timer had spent running before the current start time. Update on pause or user skipping stages.
setTime: 0, //How long the user wants the timer to run for
inputString: '0' //The string of numbers the user typed in.
};
exports.state = storage.readJSON(exports.STATE_PATH);
if (!exports.state) {
exports.state = exports.STATE_DEFAULT;
exports.timerExists = function () {
return exports.running() || (exports.state.timeLeft != 0);
}
//Get the number of milliseconds until the timer expires
exports.getTimeLeft = function () {
if (!exports.state.wasRunning) {
//If the timer never ran, the time left is just the set time
return exports.setTime
} else if (exports.state.running) {
//If the timer is running, the time left is current time - start time + preexisting time
var runningTime = (new Date()).getTime() - exports.state.startTime + exports.state.elapsedTime;
if (exports.running()) {
return sched.getTimeToAlarm(sched.getAlarm('keytimer'));
} else {
//If the timer is not running, the same as above but use when the timer was paused instead of now.
var runningTime = exports.state.pausedTime - exports.state.startTime + exports.state.elapsedTime;
return exports.state.timeLeft;
}
}
return exports.state.setTime - runningTime;
exports.state = storage.readJSON('keytimer.json') || {
inputString: '0',
timeLeft: 0
};
exports.startTimer = function (time) {
let timer = sched.newDefaultTimer();
timer.timer = time;
common.state.timeLeft = time;
timer.del = true;
timer.appid = 'keytimer';
timer.js = "load('keytimer-ring.js')";
sched.setAlarm('keytimer', timer);
sched.reload();
}
exports.pauseTimer = function () {
exports.state.timeLeft = exports.getTimeLeft();
sched.setAlarm('keytimer');
sched.reload();
}
exports.deleteTimer = function () {
sched.setAlarm('keytimer');
exports.state.timeLeft = 0;
sched.reload();
}

View File

@ -34,7 +34,6 @@ class NumberButton {
onclick() {
if (common.state.inputString == '0') common.state.inputString = this.label;
else common.state.inputString += this.label;
common.state.setTime = inputStringToTime(common.state.inputString);
feedback(true);
updateDisplay();
}
@ -44,7 +43,6 @@ let ClearButton = {
label: 'Clr',
onclick: () => {
common.state.inputString = '0';
common.state.setTime = 0;
updateDisplay();
feedback(true);
}
@ -53,10 +51,7 @@ let ClearButton = {
let StartButton = {
label: 'Go',
onclick: () => {
common.state.startTime = (new Date()).getTime();
common.state.elapsedTime = 0;
common.state.wasRunning = true;
common.state.running = true;
common.startTimer(inputStringToTime(common.state.inputString));
feedback(true);
require('keytimer-tview.js').show(common);
}

View File

@ -1,7 +1,7 @@
{
"id": "keytimer",
"name": "Keypad Timer",
"version": "0.02",
"version": "0.03",
"description": "A timer with a keypad that runs in the background",
"icon": "icon.png",
"type": "app",
@ -10,6 +10,7 @@
"BANGLEJS2"
],
"allow_emulator": true,
"dependencies": {"scheduler":"type"},
"storage": [
{
"name": "keytimer.app.js",
@ -20,10 +21,6 @@
"url": "icon.js",
"evaluate": true
},
{
"name": "keytimer.boot.js",
"url": "boot.js"
},
{
"name": "keytimer-com.js",
"url": "common.js"
@ -41,4 +38,4 @@
"url": "timerview.js"
}
]
}
}

View File

@ -1,28 +1,95 @@
const common = require('keytimer-com.js');
Bangle.loadWidgets()
Bangle.drawWidgets()
Bangle.setLocked(false);
Bangle.setLCDPower(true);
let brightness = 0;
setInterval(() => {
Bangle.buzz(200);
Bangle.setLCDBrightness(1 - brightness);
brightness = 1 - brightness;
}, 400);
Bangle.buzz(200);
function stopTimer() {
common.state.wasRunning = false;
common.state.running = false;
require("Storage").writeJSON(common.STATE_PATH, common.state);
// Chances are boot0.js got run already and scheduled *another*
// 'load(sched.js)' - so let's remove it first!
if (Bangle.SCHED) {
clearInterval(Bangle.SCHED);
delete Bangle.SCHED;
}
E.showAlert("Timer expired!").then(() => {
stopTimer();
load();
});
E.on('kill', stopTimer);
function showAlarm(alarm) {
const alarmIndex = alarms.indexOf(alarm);
const settings = require("sched").getSettings();
let message = "";
if (alarm.msg) {
message += alarm.msg;
} else {
message = (alarm.timer
? atob("ACQswgD//33vRcGHIQAAABVVVAAAAAAAABVVVAAAAAAAABVVVAAAAAAAABVVVAAAAAAAABVVVAAAAAAAABVVVAAAAAAAAAP/wAAAAAAAAAP/wAAAAAAAAAqqoAPAAAAAAqqqqoP8AAAAKqqqqqv/AAACqqqqqqq/wAAKqqqlWqqvwAAqqqqlVaqrAACqqqqlVVqqAAKqqqqlVVaqgAKqaqqlVVWqgAqpWqqlVVVqoAqlWqqlVVVaoCqlV6qlVVVaqCqVVfqlVVVWqCqVVf6lVVVWqKpVVX/lVVVVqqpVVV/+VVVVqqpVVV//lVVVqqpVVVfr1VVVqqpVVVfr1VVVqqpVVVb/lVVVqqpVVVW+VVVVqqpVVVVVVVVVqiqVVVVVVVVWqCqVVVVVVVVWqCqlVVVVVVVaqAqlVVVVVVVaoAqpVVVVVVVqoAKqVVVVVVWqgAKqlVVVVVaqgACqpVVVVVqqAAAqqlVVVaqoAAAKqqVVWqqgAAACqqqqqqqAAAAAKqqqqqgAAAAAAqqqqoAAAAAAAAqqoAAAAA==")
: atob("AC0swgF97///RcEpMlVVVVVVf9VVVVVVVVX/9VVf9VVf/1VVV///1Vf9VX///VVX///VWqqlV///1Vf//9aqqqqpf//9V///2qqqqqqn///V///6qqqqqqr///X//+qqoAAKqqv//3//6qoAAAAKqr//3//qqAAAAAAqq//3/+qoAADwAAKqv/3/+qgAADwAACqv/3/aqAAADwAAAqp/19qoAAADwAAAKqfV1qgAAADwAAACqXVWqgAAADwAAACqlVWqAAAADwAAAAqlVWqAAAADwAAAAqlVWqAAAADwAAAAqlVaoAAAADwAAAAKpVaoAAAADwAAAAKpVaoAAAADwAAAAKpVaoAAAAOsAAAAKpVaoAAAAOsAAAAKpVaoAAAAL/AAAAKpVaoAAAAgPwAAAKpVaoAAACAD8AAAKpVWqAAAIAA/AAAqlVWqAAAgAAPwAAqlVWqAACAAADwAAqlVWqgAIAAAAAACqlVVqgAgAAAAAACqVVVqoAAAAAAAAKqVVVaqAAAAAAAAqpVVVWqgAAAAAACqlVVVWqoAAAAAAKqlVVVVqqAAAAAAqqVVVVVaqoAAAAKqpVVVVVeqqoAAKqqtVVVVV/6qqqqqqr/VVVVX/2qqqqqqn/1VVVf/VaqqqqpV/9VVVf9VVWqqlVVf9VVVf1VVVVVVVVX9VQ==")
) + /*LANG*/" TIMER"
}
Bangle.loadWidgets();
Bangle.drawWidgets();
let buzzCount = settings.buzzCount;
E.showPrompt(message, {
title: alarm.timer ? /*LANG*/"TIMER!" : /*LANG*/"ALARM!",
buttons: { /*LANG*/"Snooze": true, /*LANG*/"Stop": false } // default is sleep so it'll come back in some mins
}).then(function (sleep) {
buzzCount = 0;
if (sleep) {
if (alarm.ot === undefined) {
alarm.ot = alarm.t;
}
alarm.t += settings.defaultSnoozeMillis;
} else {
let del = alarm.del === undefined ? settings.defaultDeleteExpiredTimers : alarm.del;
if (del) {
alarms.splice(alarmIndex, 1);
let state = require('Storage').readJSON('keytimer.json');
state.timeLeft = 0;
require('Storage').writeJSON('keytimer.json', state);
} else {
if (!alarm.timer) {
alarm.last = new Date().getDate();
}
if (alarm.ot !== undefined) {
alarm.t = alarm.ot;
delete alarm.ot;
}
if (!alarm.rp) {
alarm.on = false;
}
}
}
// The updated alarm is still a member of 'alarms'
// so writing to array writes changes back directly
require("sched").setAlarms(alarms);
load();
});
function buzz() {
if (settings.unlockAtBuzz) {
Bangle.setLocked(false);
}
const pattern = alarm.vibrate || (alarm.timer ? settings.defaultTimerPattern : settings.defaultAlarmPattern);
require("buzz").pattern(pattern).then(() => {
if (buzzCount--) {
setTimeout(buzz, settings.buzzIntervalMillis);
} else if (alarm.as) { // auto-snooze
buzzCount = settings.buzzCount;
setTimeout(buzz, settings.defaultSnoozeMillis);
}
});
}
if ((require("Storage").readJSON("setting.json", 1) || {}).quiet > 1)
return;
buzz();
}
let alarms = require("sched").getAlarms();
let active = require("sched").getActiveAlarms(alarms);
if (active.length) {
// if there's an alarm, show it
showAlarm(active[0]);
} else {
// otherwise just go back to default app
setTimeout(load, 100);
}

View File

@ -1,3 +1,10 @@
const heatshrink = require("heatshrink");
const BUTTON_ICONS = {
play: heatshrink.decompress(atob("jEYwMAkAGBnACBnwCBn+AAQPgAQPwAQP8AQP/AQXAAQPwAQP8AQP+AQgICBwQUCEAn4FggyBHAQ+CIgQ")),
pause: heatshrink.decompress(atob("jEYwMA/4BBAX4CEA")),
reset: heatshrink.decompress(atob("jEYwMA/4BB/+BAQPDAQPnAQIAKv///0///8j///EP//wAQQICBwQUCEhgyCHAQ+CIgI="))
};
let common;
function drawButtons() {
@ -10,11 +17,11 @@ function drawButtons() {
.drawLine(g.getWidth() / 2, BAR_TOP, g.getWidth() / 2, g.getHeight())
//Draw the buttons
.drawImage(common.BUTTON_ICONS.reset, g.getWidth() / 4, BAR_TOP);
if (common.state.running) {
g.drawImage(common.BUTTON_ICONS.pause, g.getWidth() * 3 / 4, BAR_TOP);
.drawImage(BUTTON_ICONS.reset, g.getWidth() / 4, BAR_TOP);
if (common.running()) {
g.drawImage(BUTTON_ICONS.pause, g.getWidth() * 3 / 4, BAR_TOP);
} else {
g.drawImage(common.BUTTON_ICONS.play, g.getWidth() * 3 / 4, BAR_TOP);
g.drawImage(BUTTON_ICONS.play, g.getWidth() * 3 / 4, BAR_TOP);
}
}
@ -38,8 +45,6 @@ function drawTimer() {
if (hours >= 1) return `${parseInt(hours)}:${pad(minutes)}:${pad(seconds)}`;
else return `${parseInt(minutes)}:${pad(seconds)}`;
})(), g.getWidth() / 2, g.getHeight() / 2)
if (timeLeft <= 0) load('keytimer-ring.js');
}
let timerInterval;
@ -51,14 +56,14 @@ function setupTimerInterval() {
setTimeout(() => {
timerInterval = setInterval(drawTimer, 1000);
drawTimer();
}, common.timeLeft % 1000);
}, common.getTimeLeft() % 1000);
}
exports.show = function (callerCommon) {
common = callerCommon;
drawButtons();
drawTimer();
if (common.state.running) {
if (common.running()) {
setupTimerInterval();
}
}
@ -71,37 +76,22 @@ function clearTimerInterval() {
}
exports.touch = (button, xy) => {
if (xy.y < 152) return;
if (xy !== undefined && xy.y < 152) return;
if (button == 1) {
//Reset the timer
let setTime = common.state.setTime;
let inputString = common.state.inputString;
common.state = common.STATE_DEFAULT;
common.state.setTime = setTime;
common.state.inputString = inputString;
common.deleteTimer();
clearTimerInterval();
require('keytimer-keys.js').show(common);
} else {
if (common.state.running) {
//Record the exact moment that we paused
let now = (new Date()).getTime();
common.state.pausedTime = now;
//Stop the timer
common.state.running = false;
if (common.running()) {
common.pauseTimer();
clearTimerInterval();
drawTimer();
drawButtons();
} else {
//Start the timer and record when we started
let now = (new Date()).getTime();
common.state.elapsedTime += common.state.pausedTime - common.state.startTime;
common.state.startTime = now;
common.state.running = true;
drawTimer();
common.startTimer(common.getTimeLeft());
setupTimerInterval();
drawButtons();
}
drawTimer();
drawButtons();
}
};

View File

@ -1,2 +1,3 @@
0.01: first release
0.02: Use clock_info module as an app
0.03: clock_info now uses app name to maintain settings specifically for this clock face

View File

@ -37,13 +37,13 @@ Graphics.prototype.setFontLatoSmall = function(scale) {
{
// must be inside our own scope here so that when we are unloaded everything disappears
// we also define functions using 'let fn = function() {..}' for the same reason. function decls are global
let draw = function() {
var date = new Date();
var timeStr = require("locale").time(date,1);
var h = g.getHeight();
var w = g.getWidth();
g.reset();
g.setColor(g.theme.bg);
g.fillRect(Bangle.appRect);
@ -66,7 +66,7 @@ Graphics.prototype.setFontLatoSmall = function(scale) {
/**
* clock_info_support
* this is the callback function that get invoked by clockInfoMenu.redraw();
*
*
* We will display the image and text on the same line and centre the combined
* length of the image+text
*
@ -76,7 +76,7 @@ Graphics.prototype.setFontLatoSmall = function(scale) {
//g.reset().setFont('Vector',24).setBgColor(options.bg).setColor(options.fg);
g.reset().setFontLatoSmall();
g.setBgColor(options.bg).setColor(options.fg);
//use info.text.toString(), steps does not have length defined
var text_w = g.stringWidth(info.text.toString());
// gap between image and text
@ -88,7 +88,7 @@ Graphics.prototype.setFontLatoSmall = function(scale) {
// clear the whole info line, allow additional 2 pixels in case LatoFont overflows area
g.clearRect(0, options.y -2, g.getWidth(), options.y+ 23 + 2);
// draw the image if we have one
if (info.img) {
// image start
@ -110,8 +110,8 @@ Graphics.prototype.setFontLatoSmall = function(scale) {
// clock_info_support
// setup the way we wish to interact with the menu
// the hl property defines the color the of the info when the menu is selected after tapping on it
let clockInfoMenu = require("clock_info").addInteractive(clockInfoItems, { x:64, y:132, w:50, h:40, draw : clockInfoDraw, bg : g.theme.bg, fg : g.theme.fg, hl : "#0ff"} );
let clockInfoMenu = require("clock_info").addInteractive(clockInfoItems, { app : "lato", x:64, y:132, w:50, h:40, draw : clockInfoDraw, bg : g.theme.bg, fg : g.theme.fg, hl : "#0ff"} );
// timeout used to update every minute
var drawTimeout;
g.clear();

View File

@ -37,13 +37,13 @@ Graphics.prototype.setFontLatoSmall = function(scale) {
{
// must be inside our own scope here so that when we are unloaded everything disappears
// we also define functions using 'let fn = function() {..}' for the same reason. function decls are global
let draw = function() {
var date = new Date();
var timeStr = require("locale").time(date,1);
var h = g.getHeight();
var w = g.getWidth();
g.reset();
g.setColor(g.theme.bg);
g.fillRect(Bangle.appRect);
@ -66,7 +66,7 @@ Graphics.prototype.setFontLatoSmall = function(scale) {
/**
* clock_info_support
* this is the callback function that get invoked by clockInfoMenu.redraw();
*
*
* We will display the image and text on the same line and centre the combined
* length of the image+text
*
@ -76,7 +76,7 @@ Graphics.prototype.setFontLatoSmall = function(scale) {
//g.reset().setFont('Vector',24).setBgColor(options.bg).setColor(options.fg);
g.reset().setFontLatoSmall();
g.setBgColor(options.bg).setColor(options.fg);
//use info.text.toString(), steps does not have length defined
var text_w = g.stringWidth(info.text.toString());
// gap between image and text
@ -88,7 +88,7 @@ Graphics.prototype.setFontLatoSmall = function(scale) {
// clear the whole info line, allow additional 2 pixels in case LatoFont overflows area
g.clearRect(0, options.y -2, g.getWidth(), options.y+ 23 + 2);
// draw the image if we have one
if (info.img) {
// image start
@ -110,8 +110,8 @@ Graphics.prototype.setFontLatoSmall = function(scale) {
// clock_info_support
// setup the way we wish to interact with the menu
// the hl property defines the color the of the info when the menu is selected after tapping on it
let clockInfoMenu = require("clock_info").addInteractive(clockInfoItems, { x:64, y:132, w:50, h:40, draw : clockInfoDraw, bg : g.theme.bg, fg : g.theme.fg, hl : "#0ff"} );
let clockInfoMenu = require("clock_info").addInteractive(clockInfoItems, { app : "lato", x:64, y:132, w:50, h:40, draw : clockInfoDraw, bg : g.theme.bg, fg : g.theme.fg, hl : "#0ff"} );
// timeout used to update every minute
var drawTimeout;
g.clear();

View File

@ -64,7 +64,7 @@ function queueDraw() {
/**
* clock_info_support
* this is the callback function that get invoked by clockInfoMenu.redraw();
*
*
* We will display the image and text on the same line and centre the combined
* length of the image+text
*
@ -109,8 +109,8 @@ let clockInfoItems = require("clock_info").load();
* selected after tapping on it
*
*/
let clockInfoMenu = require("clock_info").addInteractive(clockInfoItems, { x:64, y:132, w:50, h:40, draw : clockInfoDraw, bg : g.theme.bg, fg : g.theme.fg, hl : "#0ff"} );
let clockInfoMenu = require("clock_info").addInteractive(clockInfoItems, { app : "lato", x:64, y:132, w:50, h:40, draw : clockInfoDraw, bg : g.theme.bg, fg : g.theme.fg, hl : "#0ff"} );
g.clear();
// Show launcher when middle button pressed

View File

@ -1,7 +1,7 @@
{
"id": "lato",
"name": "Lato",
"version": "0.02",
"version": "0.03",
"description": "A Lato Font clock with fast load and clock_info",
"readme": "README.md",
"icon": "app.png",

Some files were not shown because too many files have changed in this diff Show More