Merge branch 'master' of github.com:nxdefiant/BangleApps
|
@ -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 
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: New Widget!
|
|
@ -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 
|
||||
|
||||
## 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
|
|
@ -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!
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
@ -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"}
|
||||
]
|
||||
}
|
|
@ -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 
|
||||
|
|
After Width: | Height: | Size: 1.6 KiB |
|
@ -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"],
|
||||
|
|
|
@ -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
|
||||
};
|
||||
})()
|
||||
|
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
@ -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" },
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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"}],
|
||||
|
|
|
@ -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); },
|
||||
|
|
|
@ -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)
|
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"],
|
||||
|
|
After Width: | Height: | Size: 2.2 KiB |
|
@ -0,0 +1 @@
|
|||
0.01: First version
|
|
@ -0,0 +1,11 @@
|
|||
# RAM Clock Info
|
||||
|
||||

|
||||
|
||||
A clock info that displays the % memory used
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
|
||||
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/)
|
After Width: | Height: | Size: 206 B |
|
@ -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;
|
||||
});
|
|
@ -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"}
|
||||
]
|
||||
}
|
After Width: | Height: | Size: 2.8 KiB |
|
@ -0,0 +1,2 @@
|
|||
0.01: New Widget!
|
||||
0.02: Ensure that the generated image is transparent (2v18+)
|
|
@ -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!
|
After Width: | Height: | Size: 1.7 KiB |
|
@ -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"}
|
||||
]
|
||||
}
|
|
@ -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+)
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
})()
|
||||
|
|
|
@ -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"}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: New app!
|
|
@ -0,0 +1,34 @@
|
|||
# Folder launcher
|
||||
|
||||
Launcher that allows you to put your apps into folders
|
||||
|
||||

|
||||

|
||||
|
||||
## 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.
|
|
@ -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();
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwMA///wAJCAoPAAongAonwAon4Aon8Aon+Aon/AooA/AH4A/AFgA="))
|
After Width: | Height: | Size: 234 B |
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 3.8 KiB |
|
@ -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();
|
||||
});
|
|
@ -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);
|
|
@ -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
|
||||
}
|
|
@ -1 +1,3 @@
|
|||
0.01: New app!
|
||||
0.01: New app!
|
||||
0.02: Add sensor icons
|
||||
Customize code directly, remove config 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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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"}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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);
|
||||
};
|
|
@ -1 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwgOAA4YFS/4AKEf5BlABcAjAgBjAfBAuhH/Apo"))
|
||||
require("heatshrink").decompress(atob("mEwgOAA4YFS/4AKEf5BlABcAjAgBjAfBAuhH/Apo"))
|
||||
|
|
Before Width: | Height: | Size: 249 B After Width: | Height: | Size: 1.9 KiB |
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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({
|
||||
'': {
|
||||
|
|
|
@ -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
|
|
@ -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.
|
||||
|
||||

|
||||

|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwxH+1YA/ABusBJIACGMwnFF0paGC6wvYDKATDCweHK54YGSY4RKAAOIwIaJC5YVKCJmHF4QzDF6gyFAYIMHwIABBgICGF7OlFYwvEqwEBMQWIL6gOCChAjBq8yruBrsyllXBgaRBAAIuRahQgBAIMyhFXGYMHmQ5Bw5eBJBIvUJoJZBq0shEIFoMIlkyF4IfFMgQtSF4kyQ4IpBAYICBFwQ4BrtdZogYBMRgvMFgQpBAIQHCg8HYQeIwRiESZiPKFoIvFMAQABqwZBwLDCMQaaGGRgHDw6ABqwvEAQUzrur0ouBXYa/LGBIIGwItDFwZXEw4WCGYKOMABReBGwJfBq4CBYIS9DGAOlCAI3CwOlFqgXBAAIFBGgKSCL4KEILgItUFgJ7BDQL1EAANdMIIvJMIKPUF4OHroqCE4IBCrqPBciQvOEILoCmVWAwOrR4Usca4AIbAWrMAIqBRILABFwJfkAAOIXIIwBq5lBd5QvYLgOrw+sdISSBFwQEBSDgcCEoWBLwNXFQIBBBQIGBY4OrAAgkLBgIOEDAssQwWBq4qBLIYHBGoQWFACWl0q7BAAKICcwIuCXIzzBq4VDDYOlEAQjDAgIBBBwITDwAACwK0EAYKGBdA4QBCgIaBD4YAJFIeAvVWqwCCJ4IvDEgIuHSYIPBEAIZBDwYATXQQsBhAuFcgqhCD5erFpZ3BDwIADw4pFGAoSCFxJFCFhGAZIIOBPwNXRRAIH1mHFxBCDwAuFFoZMDEg+rw5mFFoIHBKggaBAwRrBAoeA0mlFweIwNdLZCVHHIYaCE4LWBhA0ClkrlYEBqGACQddq8zq9dwOIxB0Cw+rFwIHBIAWIq5vCIoRHCw4yCfQTfDH4YEBrtdKIQoBE4TyGMgQ/Bq+IJYInBAwKFBN5BwCD4oeBq5FBJAISELoaFCHQRGClhaBPQMsbRC0HrtWF4LxGN4TnBGgIQBwOlq5bBA4Msg8yGBLnBIwddmcylh4BNQKqCEAJwBFwQ8BE4MyvWAaoQKBqwwBq5mBwQYBEQLqDq6jBAAIuCK4Q8DL4YyBAwOrCQMIq2AJQIxBAQIeBlkIliwCA4NWRAI8BAAL8BdQjLDFYQPBKwQIBEgQwBPwYDCEAI9BFgIGBhBcDCwLjDD4NdGYgoDGIIFBSIIhBGAQADveA0mk0oGBGgI3CAAN7ZgbBBSYQ3BHAaKBb4LsBHgI4ChBPBvWlAAJFCFoIyBGYd6PIRWDUQQACBYOragRGBBwMyg5FCBgIwBGIOB0pGBAAR7DAwWHd4JGBg8HLgIxCYoIpCZgSvDg8yrrNDqwcCJIaHCFwR8DSQb2BcwIUCEQLWBCgJIFmUrZ4NdEAWrRoVcCQgKBw4ACNYgXDA"))
|
After Width: | Height: | Size: 6.7 KiB |
|
@ -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" }
|
||||
]
|
||||
}
|
After Width: | Height: | Size: 9.8 KiB |
After Width: | Height: | Size: 72 KiB |
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
};
|
|
@ -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
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|