1
0
Fork 0

Merge branch 'espruino:master' into quicklaunch

master
thyttan 2022-11-11 06:53:38 +01:00 committed by GitHub
commit 9e69da1354
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
236 changed files with 6151 additions and 4182 deletions

View File

@ -2,3 +2,4 @@
0.02: Load AGPS data on app start and automatically in background
0.03: Do not load AGPS data on boot
Increase minimum interval to 6 hours
0.04: Write AGPS data chunks with delay to improve reliability

View File

@ -36,7 +36,7 @@ function updateAgps() {
g.clear();
if (!waiting) {
waiting = true;
display("Updating A-GPS...");
display("Updating A-GPS...", "takes ~ 10 seconds");
require("agpsdata").pull(function() {
waiting = false;
display("A-GPS updated.", "touch to close");

View File

@ -8,41 +8,52 @@ var FILE = "agpsdata.settings.json";
var settings;
readSettings();
function setAGPS(data) {
var js = jsFromBase64(data);
try {
eval(js);
return true;
}
catch(e) {
console.log("error:", e);
}
return false;
function setAGPS(b64) {
return new Promise(function(resolve, reject) {
var initCommands = "Bangle.setGPSPower(1);\n"; // turn GPS on
const gnsstype = settings.gnsstype || 1; // default GPS
initCommands += `Serial1.println("${CASIC_CHECKSUM("$PCAS04," + gnsstype)}")\n`; // set GNSS mode
// What about:
// NAV-TIMEUTC (0x01 0x10)
// NAV-PV (0x01 0x03)
// or AGPS.zip uses AID-INI (0x0B 0x01)
eval(initCommands);
try {
writeChunks(atob(b64), resolve);
} catch (e) {
console.log("error:", e);
reject();
}
});
}
function jsFromBase64(b64) {
var bin = atob(b64);
var chunkSize = 128;
var js = "Bangle.setGPSPower(1);\n"; // turn GPS on
var gnsstype = settings.gnsstype || 1; // default GPS
js += `Serial1.println("${CASIC_CHECKSUM("$PCAS04,"+gnsstype)}")\n`; // set GNSS mode
// What about:
// NAV-TIMEUTC (0x01 0x10)
// NAV-PV (0x01 0x03)
// or AGPS.zip uses AID-INI (0x0B 0x01)
var chunkI = 0;
function writeChunks(bin, resolve) {
return new Promise(function(resolve2) {
const chunkSize = 128;
setTimeout(function() {
if (chunkI < bin.length) {
var chunk = bin.substr(chunkI, chunkSize);
js = `Serial1.write(atob("${btoa(chunk)}"))\n`;
eval(js);
for (var i=0;i<bin.length;i+=chunkSize) {
var chunk = bin.substr(i,chunkSize);
js += `Serial1.write(atob("${btoa(chunk)}"))\n`;
}
return js;
chunkI += chunkSize;
writeChunks(bin, resolve);
} else {
if (resolve)
resolve(); // call outer resolve
}
}, 200);
});
}
function CASIC_CHECKSUM(cmd) {
var cs = 0;
for (var i=1;i<cmd.length;i++)
for (var i = 1; i < cmd.length; i++)
cs = cs ^ cmd.charCodeAt(i);
return cmd+"*"+cs.toString(16).toUpperCase().padStart(2, '0');
return cmd + "*" + cs.toString(16).toUpperCase().padStart(2, '0');
}
function updateLastUpdate() {
@ -53,23 +64,30 @@ function updateLastUpdate() {
}
exports.pull = function(successCallback, failureCallback) {
let uri = "https://www.espruino.com/agps/casic.base64";
if (Bangle.http){
Bangle.http(uri, {timeout:10000}).then(event => {
let result = setAGPS(event.resp);
if (result) {
updateLastUpdate();
if (successCallback) successCallback();
} else {
console.log("error applying AGPS data");
if (failureCallback) failureCallback("Error applying AGPS data");
}
}).catch((e)=>{
console.log("error", e);
if (failureCallback) failureCallback(e);
});
const uri = "https://www.espruino.com/agps/casic.base64";
if (Bangle.http) {
Bangle.http(uri, {timeout : 10000})
.then(event => {
setAGPS(event.resp)
.then(r => {
updateLastUpdate();
if (successCallback)
successCallback();
})
.catch((e) => {
console.log("error", e);
if (failureCallback)
failureCallback(e);
});
})
.catch((e) => {
console.log("error", e);
if (failureCallback)
failureCallback(e);
});
} else {
console.log("error: No http method found");
if (failureCallback) failureCallback(/*LANG*/"No http method");
if (failureCallback)
failureCallback(/*LANG*/ "No http method");
}
};

View File

@ -2,7 +2,7 @@
"name": "A-GPS Data Downloader App",
"shortName":"A-GPS Data",
"icon": "agpsdata.png",
"version":"0.03",
"version":"0.04",
"description": "Once installed, this app allows you to download assisted GPS (A-GPS) data directly to your Bangle.js **via Gadgetbridge on an Android phone** when you run the app. If you just want to upload the latest AGPS data from this app loader, please use the `Assisted GPS Update (AGPS)` app.",
"tags": "boot,tool,assisted,gps,agps,http",
"allow_emulator":true,

1
apps/barwatch/ChangeLog Normal file
View File

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

5
apps/barwatch/README.md Normal file
View File

@ -0,0 +1,5 @@
# BarWatch - an experimental watch
For too long the watches have shown the time with digits or hands. No more!
With this stylish watch the time is represented by bars. Up to 24 as the day goes by.
Practical? Not really, but a different look!

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("l0uwkE/4A/AH4A/AB0gicQmUB+EPgEigExh8gj8A+ECAgMQn4WCgcACyotWC34W/C34W/CycACw0wgYWFBYIWCAAc/+YGHCAgNFACkxl8hGYwAMLYUvCykQC34WycoIW/C34W0gAWTmUjkUzkbmSAFY="))

76
apps/barwatch/app.js Normal file
View File

@ -0,0 +1,76 @@
// timeout used to update every minute
var drawTimeout;
// schedule a draw for the next minute
function queueDraw() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, 60000 - (Date.now() % 60000));
}
function draw() {
g.reset();
if(g.theme.dark){
g.setColor(1,1,1);
}else{
g.setColor(0,0,0);
}
// work out how to display the current time
var d = new Date();
var h = d.getHours(), m = d.getMinutes();
// hour bars
var bx_offset = 10, by_offset = 35;
var b_width = 8, b_height = 60;
var b_space = 5;
for(var i=0; i<h; i++){
if(i > 11){
by_offset = 105;
}
var iter = i % 12;
//console.log(iter);
g.fillRect(bx_offset+(b_width*(iter+1))+(b_space*iter),
by_offset,
bx_offset+(b_width*iter)+(b_space*iter),
by_offset+b_height);
}
// minute bar
if(h > 11){
by_offset = 105;
}
var m_bar = h % 12;
if(m != 0){
g.fillRect(bx_offset+(b_width*(m_bar+1))+(b_space*m_bar),
by_offset+b_height-m,
bx_offset+(b_width*m_bar)+(b_space*m_bar),
by_offset+b_height);
}
// queue draw in one minute
queueDraw();
}
// Clear the screen once, at startup
g.clear();
// draw immediately at first
draw();
// Stop updates when LCD is off, restart when on
Bangle.on('lcdPower',on=>{
if (on) {
draw(); // draw immediately, queue redraw
} else { // stop draw timer
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
}
});
Bangle.setUI("clock");
Bangle.loadWidgets();
Bangle.drawWidgets();

BIN
apps/barwatch/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 973 B

View File

@ -0,0 +1,18 @@
{
"id": "barwatch",
"name": "BarWatch",
"shortName":"BarWatch",
"version":"0.01",
"description": "A watch that displays the time using bars. One bar for each hour.",
"readme": "README.md",
"icon": "screenshot.png",
"tags": "clock",
"type": "clock",
"allow_emulator":true,
"screenshots" : [ { "url": "screenshot.png" } ],
"supports" : ["BANGLEJS2"],
"storage": [
{"name":"barwatch.app.js","url":"app.js"},
{"name":"barwatch.img","url":"app-icon.js","evaluate":true}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -30,3 +30,7 @@
Allow recording unmodified internal HR
Better connection retry handling
0.13: Less time used during boot if disabled
0.14: Allow bonding (Debug menu)
Prevent mixing of BT and internal HRM events if both are enabled
Always use a grace period (default 0 ms) to decouple some connection steps
Device not found errors now utilize increasing timeouts

View File

@ -16,5 +16,6 @@
"gracePeriodNotification": 0,
"gracePeriodConnect": 0,
"gracePeriodService": 0,
"gracePeriodRequest": 0
"gracePeriodRequest": 0,
"bonding": false
}

View File

@ -109,6 +109,7 @@ exports.enable = () => {
if (supportedCharacteristics["0x2a37"].active) stopFallback();
if (bpmTimeout) clearTimeout(bpmTimeout);
bpmTimeout = setTimeout(()=>{
bpmTimeout = undefined;
supportedCharacteristics["0x2a37"].active = false;
startFallback();
}, 3000);
@ -154,8 +155,8 @@ exports.enable = () => {
src: "bthrm"
};
log("Emitting HRM", repEvent);
Bangle.emit("HRM_int", repEvent);
log("Emitting aggregated HRM", repEvent);
Bangle.emit("HRM_R", repEvent);
}
var newEvent = {
@ -280,7 +281,11 @@ exports.enable = () => {
log("Disconnect: " + reason);
log("GATT", gatt);
log("Characteristics", characteristics);
clearRetryTimeout(reason != "Connection Timeout");
var retryTimeResetNeeded = true;
retryTimeResetNeeded &= reason != "Connection Timeout";
retryTimeResetNeeded &= reason != "No device found matching filters";
clearRetryTimeout(retryTimeResetNeeded);
supportedCharacteristics["0x2a37"].active = false;
startFallback();
blockInit = false;
@ -312,13 +317,13 @@ exports.enable = () => {
result = result.then(()=>{
log("Starting notifications", newCharacteristic);
var startPromise = newCharacteristic.startNotifications().then(()=>log("Notifications started", newCharacteristic));
if (settings.gracePeriodNotification > 0){
log("Add " + settings.gracePeriodNotification + "ms grace period after starting notifications");
startPromise = startPromise.then(()=>{
log("Wait after connect");
return waitingPromise(settings.gracePeriodNotification);
});
}
log("Add " + settings.gracePeriodNotification + "ms grace period after starting notifications");
startPromise = startPromise.then(()=>{
log("Wait after connect");
return waitingPromise(settings.gracePeriodNotification);
});
return startPromise;
});
}
@ -429,30 +434,30 @@ exports.enable = () => {
var connectPromise = gatt.connect(connectSettings).then(function() {
log("Connected.");
});
if (settings.gracePeriodConnect > 0){
log("Add " + settings.gracePeriodConnect + "ms grace period after connecting");
connectPromise = connectPromise.then(()=>{
log("Wait after connect");
return waitingPromise(settings.gracePeriodConnect);
});
}
log("Add " + settings.gracePeriodConnect + "ms grace period after connecting");
connectPromise = connectPromise.then(()=>{
log("Wait after connect");
return waitingPromise(settings.gracePeriodConnect);
});
return connectPromise;
} else {
return Promise.resolve();
}
});
/* promise = promise.then(() => {
log(JSON.stringify(gatt.getSecurityStatus()));
if (gatt.getSecurityStatus()['bonded']) {
log("Already bonded");
return Promise.resolve();
} else {
log("Start bonding");
return gatt.startBonding()
.then(() => console.log(gatt.getSecurityStatus()));
}
});*/
if (settings.bonding){
promise = promise.then(() => {
log(JSON.stringify(gatt.getSecurityStatus()));
if (gatt.getSecurityStatus()['bonded']) {
log("Already bonded");
return Promise.resolve();
} else {
log("Start bonding");
return gatt.startBonding()
.then(() => console.log(gatt.getSecurityStatus()));
}
});
}
promise = promise.then(()=>{
if (!characteristics || characteristics.length === 0){
@ -476,13 +481,11 @@ exports.enable = () => {
log("Supporting service", service.uuid);
result = attachServicePromise(result, service);
}
if (settings.gracePeriodService > 0) {
log("Add " + settings.gracePeriodService + "ms grace period after services");
result = result.then(()=>{
log("Wait after services");
return waitingPromise(settings.gracePeriodService);
});
}
log("Add " + settings.gracePeriodService + "ms grace period after services");
result = result.then(()=>{
log("Wait after services");
return waitingPromise(settings.gracePeriodService);
});
return result;
});
} else {
@ -538,35 +541,33 @@ exports.enable = () => {
};
if (settings.replace){
// register a listener for original HRM events and emit as HRM_int
Bangle.on("HRM", (e) => {
e.modified = true;
Bangle.emit("HRM_int", e);
if (fallbackActive){
// if fallback to internal HRM is active, emit as HRM_R to which everyone listens
Bangle.emit("HRM_R", e);
}
});
Bangle.origOn = Bangle.on;
Bangle.on = function(name, callback) {
if (name == "HRM") {
Bangle.origOn("HRM_int", callback);
} else {
Bangle.origOn(name, callback);
}
};
Bangle.origRemoveListener = Bangle.removeListener;
Bangle.removeListener = function(name, callback) {
if (name == "HRM") {
Bangle.origRemoveListener("HRM_int", callback);
} else {
Bangle.origRemoveListener(name, callback);
}
};
// force all apps wanting to listen to HRM to actually get events for HRM_R
Bangle.on = ( o => (name, cb) => {
o = o.bind(Bangle);
if (name == "HRM") o("HRM_R", cb);
else o(name, cb);
})(Bangle.on);
Bangle.removeListener = ( o => (name, cb) => {
o = o.bind(Bangle);
if (name == "HRM") o("HRM_R", cb);
else o(name, cb);
})(Bangle.removeListener);
}
Bangle.origSetHRMPower = Bangle.setHRMPower;
if (settings.startWithHrm){
Bangle.setHRMPower = function(isOn, app) {
log("setHRMPower for " + app + ": " + (isOn?"on":"off"));
if (settings.enabled){

View File

@ -2,7 +2,7 @@
"id": "bthrm",
"name": "Bluetooth Heart Rate Monitor",
"shortName": "BT HRM",
"version": "0.13",
"version": "0.14",
"description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.",
"icon": "app.png",
"type": "app",

View File

@ -96,6 +96,12 @@
writeSettings("debuglog",v);
}
},
'Use bonding': {
value: !!settings.bonding,
onchange: v => {
writeSettings("bonding",v);
}
},
'Grace periods': function() { E.showMenu(submenu_grace); }
};

View File

@ -3,3 +3,5 @@
0.03: Support for different screen sizes and touchscreen
0.04: Display current operation on LHS
0.05: Grid positioning and swipe controls to switch between numbers, operators and special (for Bangle.js 2)
0.06: Bangle.js 2: Exit with a short press of the physical button
0.07: Bangle.js 2: Exit by pressing upper left corner of the screen

View File

@ -12,12 +12,20 @@ Basic calculator reminiscent of MacOs's one. Handy for small calculus.
## Controls
Bangle.js 1
- UP: BTN1
- DOWN: BTN3
- LEFT: BTN4
- RIGHT: BTN5
- SELECT: BTN2
Bangle.js 2
- Swipes to change visible buttons
- Click physical button to exit
- Press upper left corner of screen to exit (where the red back button would be)
## Creator
<https://twitter.com/fredericrous>
## Contributors
[thyttan](https://github.com/thyttan)

View File

@ -3,6 +3,8 @@
*
* Original Author: Frederic Rousseau https://github.com/fredericrous
* Created: April 2020
*
* Contributors: thyttan https://github.com/thyttan
*/
g.clear();
@ -402,43 +404,42 @@ if (process.env.HWVERSION==1) {
swipeEnabled = false;
drawGlobal();
} else { // touchscreen?
selected = "NONE";
selected = "NONE";
swipeEnabled = true;
prepareScreen(numbers, numbersGrid, COLORS.DEFAULT);
prepareScreen(operators, operatorsGrid, COLORS.OPERATOR);
prepareScreen(specials, specialsGrid, COLORS.SPECIAL);
drawNumbers();
Bangle.on('touch',(n,e)=>{
for (var key in screen) {
if (typeof screen[key] == "undefined") break;
var r = screen[key].xy;
if (e.x>=r[0] && e.y>=r[1] &&
e.x<r[2] && e.y<r[3]) {
//print("Press "+key);
buttonPress(""+key);
Bangle.setUI({
mode : 'custom',
back : load, // Clicking physical button or pressing upper left corner turns off (where red back button would be)
touch : (n,e)=>{
for (var key in screen) {
if (typeof screen[key] == "undefined") break;
var r = screen[key].xy;
if (e.x>=r[0] && e.y>=r[1] && e.x<r[2] && e.y<r[3]) {
//print("Press "+key);
buttonPress(""+key);
}
}
}
});
var lastX = 0, lastY = 0;
Bangle.on('drag', (e) => {
if (!e.b) {
if (lastX > 50) { // right
},
swipe : (LR, UD) => {
if (LR == 1) { // right
drawSpecials();
} else if (lastX < -50) { // left
}
if (LR == -1) { // left
drawOperators();
} else if (lastY > 50) { // down
drawNumbers();
} else if (lastY < -50) { // up
}
if (UD == 1) { // down
drawNumbers();
}
if (UD == -1) { // up
drawNumbers();
}
lastX = 0;
lastY = 0;
} else {
lastX = lastX + e.dx;
lastY = lastY + e.dy;
}
});
}
displayOutput(0);

View File

@ -2,7 +2,7 @@
"id": "calculator",
"name": "Calculator",
"shortName": "Calculator",
"version": "0.05",
"version": "0.07",
"description": "Basic calculator reminiscent of MacOs's one. Handy for small calculus.",
"icon": "calculator.png",
"screenshots": [{"url":"screenshot_calculator.png"}],

View File

@ -3,3 +3,4 @@
0.03: Made the code shorter and somewhat more readable by writing some functions. Also made it work as a library where it returns the text once finished. The keyboard is now made to exit correctly when the 'back' event is called. The keyboard now uses theme colors correctly, although it still looks best with dark theme. The numbers row is now solidly green - except for highlights.
0.04: Now displays the opened text string at launch.
0.05: Now scrolls text when string gets longer than screen width.
0.06: The code is now more reliable and the input snappier. Widgets will be drawn if present.

View File

@ -1,12 +1,9 @@
//Keep banglejs screen on for 100 sec at 0.55 power level for development purposes
//Bangle.setLCDTimeout(30);
//Bangle.setLCDPower(1);
exports.input = function(options) {
options = options||{};
var text = options.text;
if ("string"!=typeof text) text="";
var R = Bangle.appRect;
var BGCOLOR = g.theme.bg;
var HLCOLOR = g.theme.fg;
var ABCCOLOR = g.toColor(1,0,0);//'#FF0000';
@ -17,35 +14,38 @@ exports.input = function(options) {
var SMALLFONTWIDTH = parseInt(SMALLFONT.charAt(0)*parseInt(SMALLFONT.charAt(-1)));
var ABC = 'abcdefghijklmnopqrstuvwxyz'.toUpperCase();
var ABCPADDING = (g.getWidth()-6*ABC.length)/2;
var ABCPADDING = ((R.x+R.w)-6*ABC.length)/2;
var NUM = ' 1234567890!?,.- ';
var NUMHIDDEN = ' 1234567890!?,.- ';
var NUMPADDING = (g.getWidth()-6*NUM.length)/2;
var NUMPADDING = ((R.x+R.w)-6*NUM.length)/2;
var rectHeight = 40;
var delSpaceLast;
function drawAbcRow() {
g.clear();
try { // Draw widgets if they are present in the current app.
if (WIDGETS) Bangle.drawWidgets();
} catch (_) {}
g.setFont(SMALLFONT);
g.setColor(ABCCOLOR);
g.drawString(ABC, ABCPADDING, g.getHeight()/2);
g.fillRect(0, g.getHeight()-26, g.getWidth(), g.getHeight());
g.setFontAlign(-1, -1, 0);
g.drawString(ABC, ABCPADDING, (R.y+R.h)/2);
g.fillRect(0, (R.y+R.h)-26, (R.x+R.w), (R.y+R.h));
}
function drawNumRow() {
g.setFont(SMALLFONT);
g.setColor(NUMCOLOR);
g.drawString(NUM, NUMPADDING, g.getHeight()/4);
g.setFontAlign(-1, -1, 0);
g.drawString(NUM, NUMPADDING, (R.y+R.h)/4);
g.fillRect(NUMPADDING, g.getHeight()-rectHeight*4/3, g.getWidth()-NUMPADDING, g.getHeight()-rectHeight*2/3);
g.fillRect(NUMPADDING, (R.y+R.h)-rectHeight*4/3, (R.x+R.w)-NUMPADDING, (R.y+R.h)-rectHeight*2/3);
}
function updateTopString() {
"ram"
g.setColor(BGCOLOR);
g.fillRect(0,4+20,176,13+20);
g.setColor(0.2,0,0);
@ -54,13 +54,10 @@ exports.input = function(options) {
g.setColor(0.7,0,0);
g.fillRect(rectLen+5,4+20,rectLen+10,13+20);
g.setColor(1,1,1);
g.setFontAlign(-1, -1, 0);
g.drawString(text.length<=27? text.substr(-27, 27) : '<- '+text.substr(-24,24), 5, 5+20);
}
drawAbcRow();
drawNumRow();
updateTopString();
var abcHL;
var abcHLPrev = -10;
var numHL;
@ -68,194 +65,182 @@ exports.input = function(options) {
var type = '';
var typePrev = '';
var largeCharOffset = 6;
function resetChars(char, HLPrev, typePadding, heightDivisor, rowColor) {
"ram"
"ram";
// Small character in list
g.setColor(rowColor);
g.setFont(SMALLFONT);
g.drawString(char, typePadding + HLPrev*6, g.getHeight()/heightDivisor);
g.setFontAlign(-1, -1, 0);
g.drawString(char, typePadding + HLPrev*6, (R.y+R.h)/heightDivisor);
// Large character
g.setColor(BGCOLOR);
g.fillRect(0,g.getHeight()/3,176,g.getHeight()/3+24);
//g.drawString(charSet.charAt(HLPrev), typePadding + HLPrev*6 -largeCharOffset, g.getHeight()/3);; //Old implementation where I find the shape and place of letter to remove instead of just a rectangle.
g.fillRect(0,(R.y+R.h)/3,176,(R.y+R.h)/3+24);
//g.drawString(charSet.charAt(HLPrev), typePadding + HLPrev*6 -largeCharOffset, (R.y+R.h)/3);; //Old implementation where I find the shape and place of letter to remove instead of just a rectangle.
// mark in the list
}
function showChars(char, HL, typePadding, heightDivisor) {
"ram"
"ram";
// mark in the list
g.setColor(HLCOLOR);
g.setFont(SMALLFONT);
if (char != 'del' && char != 'space') g.drawString(char, typePadding + HL*6, g.getHeight()/heightDivisor);
g.setFontAlign(-1, -1, 0);
if (char != 'del' && char != 'space') g.drawString(char, typePadding + HL*6, (R.y+R.h)/heightDivisor);
// show new large character
g.setFont(BIGFONT);
g.drawString(char, typePadding + HL*6 -largeCharOffset, g.getHeight()/3);
g.drawString(char, typePadding + HL*6 -largeCharOffset, (R.y+R.h)/3);
g.setFont(SMALLFONT);
}
function initDraw() {
//var R = Bangle.appRect; // To make sure it's properly updated. Not sure if this is needed.
drawAbcRow();
drawNumRow();
updateTopString();
}
initDraw();
//setTimeout(initDraw, 0); // So Bangle.appRect reads the correct environment. It would draw off to the side sometimes otherwise.
function changeCase(abcHL) {
g.setColor(BGCOLOR);
g.drawString(ABC, ABCPADDING, g.getHeight()/2);
g.setFontAlign(-1, -1, 0);
g.drawString(ABC, ABCPADDING, (R.y+R.h)/2);
if (ABC.charAt(abcHL) == ABC.charAt(abcHL).toUpperCase()) ABC = ABC.toLowerCase();
else ABC = ABC.toUpperCase();
g.setColor(ABCCOLOR);
g.drawString(ABC, ABCPADDING, g.getHeight()/2);
g.drawString(ABC, ABCPADDING, (R.y+R.h)/2);
}
return new Promise((resolve,reject) => {
// Interpret touch input
// Interpret touch input
Bangle.setUI({
mode: 'custom',
back: ()=>{
Bangle.setUI();
g.clearRect(Bangle.appRect);
resolve(text);
},
drag: function(event) {
mode: 'custom',
back: ()=>{
Bangle.setUI();
g.clearRect(Bangle.appRect);
resolve(text);
},
drag: function(event) {
"ram";
// ABCDEFGHIJKLMNOPQRSTUVWXYZ
// Choose character by draging along red rectangle at bottom of screen
if (event.y >= ( (R.y+R.h) - 12 )) {
// Translate x-position to character
if (event.x < ABCPADDING) { abcHL = 0; }
else if (event.x >= 176-ABCPADDING) { abcHL = 25; }
else { abcHL = Math.floor((event.x-ABCPADDING)/6); }
// ABCDEFGHIJKLMNOPQRSTUVWXYZ
// Choose character by draging along red rectangle at bottom of screen
if (event.y >= ( g.getHeight() - 12 )) {
// Translate x-position to character
if (event.x < ABCPADDING) { abcHL = 0; }
else if (event.x >= 176-ABCPADDING) { abcHL = 25; }
else { abcHL = Math.floor((event.x-ABCPADDING)/6); }
// Datastream for development purposes
//print(event.x, event.y, event.b, ABC.charAt(abcHL), ABC.charAt(abcHLPrev));
// Datastream for development purposes
//print(event.x, event.y, event.b, ABC.charAt(abcHL), ABC.charAt(abcHLPrev));
// Unmark previous character and mark the current one...
// Handling switching between letters and numbers/punctuation
if (typePrev != 'abc') resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
// Unmark previous character and mark the current one...
// Handling switching between letters and numbers/punctuation
if (typePrev != 'abc') resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
if (abcHL != abcHLPrev) {
resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
showChars(ABC.charAt(abcHL), abcHL, ABCPADDING, 2);
if (abcHL != abcHLPrev) {
resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
showChars(ABC.charAt(abcHL), abcHL, ABCPADDING, 2);
}
// Print string at top of screen
if (event.b == 0) {
text = text + ABC.charAt(abcHL);
updateTopString();
// Autoswitching letter case
if (ABC.charAt(abcHL) == ABC.charAt(abcHL).toUpperCase()) changeCase(abcHL);
}
// Update previous character to current one
abcHLPrev = abcHL;
typePrev = 'abc';
}
// 12345678901234567890
// Choose number or puctuation by draging on green rectangle
else if ((event.y < ( g.getHeight() - 12 )) && (event.y > ( g.getHeight() - 52 ))) {
// Translate x-position to character
if (event.x < NUMPADDING) { numHL = 0; }
else if (event.x > 176-NUMPADDING) { numHL = NUM.length-1; }
else { numHL = Math.floor((event.x-NUMPADDING)/6); }
// Datastream for development purposes
//print(event.x, event.y, event.b, NUM.charAt(numHL), NUM.charAt(numHLPrev));
// Unmark previous character and mark the current one...
// Handling switching between letters and numbers/punctuation
if (typePrev != 'num') resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
if (numHL != numHLPrev) {
resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
showChars(NUM.charAt(numHL), numHL, NUMPADDING, 4);
}
// Print string at top of screen
if (event.b == 0) {
g.setColor(HLCOLOR);
// Backspace if releasing before list of numbers/punctuation
if (event.x < NUMPADDING) {
// show delete sign
showChars('del', 0, g.getWidth()/2 +6 -27 , 4);
delSpaceLast = 1;
text = text.slice(0, -1);
updateTopString();
//print(text);
}
// Append space if releasing after list of numbers/punctuation
else if (event.x > g.getWidth()-NUMPADDING) {
//show space sign
showChars('space', 0, g.getWidth()/2 +6 -6*3*5/2 , 4);
delSpaceLast = 1;
text = text + ' ';
updateTopString();
//print(text);
}
// Append selected number/punctuation
else {
text = text + NUMHIDDEN.charAt(numHL);
// Print string at top of screen
if (event.b == 0) {
text = text + ABC.charAt(abcHL);
updateTopString();
// Autoswitching letter case
if ((text.charAt(text.length-1) == '.') || (text.charAt(text.length-1) == '!')) changeCase();
if (ABC.charAt(abcHL) == ABC.charAt(abcHL).toUpperCase()) changeCase(abcHL);
}
// Update previous character to current one
abcHLPrev = abcHL;
typePrev = 'abc';
}
// Update previous character to current one
numHLPrev = numHL;
typePrev = 'num';
}
// Make a space or backspace by swiping right or left on screen above green rectangle
else if (event.y > 20+4) {
if (event.b == 0) {
g.setColor(HLCOLOR);
if (event.x < g.getWidth()/2) {
resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
// 12345678901234567890
// Choose number or puctuation by draging on green rectangle
else if ((event.y < ( (R.y+R.h) - 12 )) && (event.y > ( (R.y+R.h) - 52 ))) {
// Translate x-position to character
if (event.x < NUMPADDING) { numHL = 0; }
else if (event.x > 176-NUMPADDING) { numHL = NUM.length-1; }
else { numHL = Math.floor((event.x-NUMPADDING)/6); }
// Datastream for development purposes
//print(event.x, event.y, event.b, NUM.charAt(numHL), NUM.charAt(numHLPrev));
// Unmark previous character and mark the current one...
// Handling switching between letters and numbers/punctuation
if (typePrev != 'num') resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
if (numHL != numHLPrev) {
resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
// show delete sign
showChars('del', 0, g.getWidth()/2 +6 -27 , 4);
delSpaceLast = 1;
// Backspace and draw string upper right corner
text = text.slice(0, -1);
updateTopString();
if (text.length==0) changeCase(abcHL);
//print(text, 'undid');
showChars(NUM.charAt(numHL), numHL, NUMPADDING, 4);
}
else {
resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
// Print string at top of screen
if (event.b == 0) {
g.setColor(HLCOLOR);
// Backspace if releasing before list of numbers/punctuation
if (event.x < NUMPADDING) {
// show delete sign
showChars('del', 0, (R.x+R.w)/2 +6 -27 , 4);
delSpaceLast = 1;
text = text.slice(0, -1);
updateTopString();
//print(text);
}
// Append space if releasing after list of numbers/punctuation
else if (event.x > (R.x+R.w)-NUMPADDING) {
//show space sign
showChars('space', 0, (R.x+R.w)/2 +6 -6*3*5/2 , 4);
delSpaceLast = 1;
text = text + ' ';
updateTopString();
//print(text);
}
// Append selected number/punctuation
else {
text = text + NUMHIDDEN.charAt(numHL);
updateTopString();
//show space sign
showChars('space', 0, g.getWidth()/2 +6 -6*3*5/2 , 4);
delSpaceLast = 1;
// Autoswitching letter case
if ((text.charAt(text.length-1) == '.') || (text.charAt(text.length-1) == '!')) changeCase();
}
}
// Update previous character to current one
numHLPrev = numHL;
typePrev = 'num';
}
// Append space and draw string upper right corner
text = text + NUMHIDDEN.charAt(0);
updateTopString();
//print(text, 'made space');
// Make a space or backspace by swiping right or left on screen above green rectangle
else if (event.y > 20+4) {
if (event.b == 0) {
g.setColor(HLCOLOR);
if (event.x < (R.x+R.w)/2) {
resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
// show delete sign
showChars('del', 0, (R.x+R.w)/2 +6 -27 , 4);
delSpaceLast = 1;
// Backspace and draw string upper right corner
text = text.slice(0, -1);
updateTopString();
if (text.length==0) changeCase(abcHL);
//print(text, 'undid');
}
else {
resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
//show space sign
showChars('space', 0, (R.x+R.w)/2 +6 -6*3*5/2 , 4);
delSpaceLast = 1;
// Append space and draw string upper right corner
text = text + NUMHIDDEN.charAt(0);
updateTopString();
//print(text, 'made space');
}
}
}
}
}
});
});
/* return new Promise((resolve,reject) => {
Bangle.setUI({mode:"custom", back:()=>{
Bangle.setUI();
g.clearRect(Bangle.appRect);
Bangle.setUI();
resolve(text);
}});
}); */
});
});
};

View File

@ -1,6 +1,6 @@
{ "id": "dragboard",
"name": "Dragboard",
"version":"0.05",
"version":"0.06",
"description": "A library for text input via swiping keyboard",
"icon": "app.png",
"type":"textinput",

View File

@ -14,3 +14,8 @@
0.14: Don't move pages when doing exit swipe - Bangle 2.
0.15: 'Swipe to exit'-code is slightly altered to be more reliable - Bangle 2.
0.16: Use default Bangle formatter for booleans
0.17: Bangle 2: Fast loading on exit to clock face. Added option for exit to
clock face by timeout.
0.18: Move interactions inside setUI. Replace "one click exit" with
back-functionality through setUI, adding the red back button as well. Hardware
button to exit is no longer an option.

View File

@ -1,28 +1,27 @@
{ // must be inside our own scope here so that when we are unloaded everything disappears
/* Desktop launcher
*
*/
var settings = Object.assign({
let settings = Object.assign({
showClocks: true,
showLaunchers: true,
direct: false,
oneClickExit:false,
swipeExit: false
swipeExit: false,
timeOut: "Off"
}, require('Storage').readJSON("dtlaunch.json", true) || {});
if( settings.oneClickExit)
setWatch(_=> load(), BTN1);
var s = require("Storage");
var apps = s.list(/\.info$/).map(app=>{
var a=s.readJSON(app,1);
let s = require("Storage");
var apps = s.list(/\.info$/).map(app=>{
let a=s.readJSON(app,1);
return a && {
name:a.name, type:a.type, icon:a.icon, sortorder:a.sortorder, src:a.src
};}).filter(
app=>app && (app.type=="app" || (app.type=="clock" && settings.showClocks) || (app.type=="launch" && settings.showLaunchers) || !app.type));
apps.sort((a,b)=>{
var n=(0|a.sortorder)-(0|b.sortorder);
let n=(0|a.sortorder)-(0|b.sortorder);
if (n) return n; // do sortorder first
if (a.name<b.name) return -1;
if (a.name>b.name) return 1;
@ -33,29 +32,28 @@ apps.forEach(app=>{
app.icon = s.read(app.icon); // should just be a link to a memory area
});
var Napps = apps.length;
var Npages = Math.ceil(Napps/4);
var maxPage = Npages-1;
var selected = -1;
var oldselected = -1;
var page = 0;
let Napps = apps.length;
let Npages = Math.ceil(Napps/4);
let maxPage = Npages-1;
let selected = -1;
let oldselected = -1;
let page = 0;
const XOFF = 24;
const YOFF = 30;
function draw_icon(p,n,selected) {
var x = (n%2)*72+XOFF;
var y = n>1?72+YOFF:YOFF;
let drawIcon= function(p,n,selected) {
let x = (n%2)*72+XOFF;
let y = n>1?72+YOFF:YOFF;
(selected?g.setColor(g.theme.fgH):g.setColor(g.theme.bg)).fillRect(x+11,y+3,x+60,y+52);
g.clearRect(x+12,y+4,x+59,y+51);
g.setColor(g.theme.fg);
try{g.drawImage(apps[p*4+n].icon,x+12,y+4);} catch(e){}
g.setFontAlign(0,-1,0).setFont("6x8",1);
var txt = apps[p*4+n].name.replace(/([a-z])([A-Z])/g, "$1 $2").split(" ");
var lineY = 0;
var line = "";
while (txt.length > 0){
var c = txt.shift();
let txt = apps[p*4+n].name.replace(/([a-z])([A-Z])/g, "$1 $2").split(" ");
let lineY = 0;
let line = "";
while (txt.length > 0){
let c = txt.shift();
if (c.length + 1 + line.length > 13){
if (line.length > 0){
g.drawString(line.trim(),x+36,y+54+lineY*8);
@ -67,29 +65,34 @@ function draw_icon(p,n,selected) {
}
}
g.drawString(line.trim(),x+36,y+54+lineY*8);
}
};
function drawPage(p){
let drawPage = function(p){
g.reset();
g.clearRect(0,24,175,175);
var O = 88+YOFF/2-12*(Npages/2);
for (var j=0;j<Npages;j++){
var y = O+j*12;
let O = 88+YOFF/2-12*(Npages/2);
for (let j=0;j<Npages;j++){
let y = O+j*12;
g.setColor(g.theme.fg);
if (j==page) g.fillCircle(XOFF/2,y,4);
else g.drawCircle(XOFF/2,y,4);
}
for (var i=0;i<4;i++) {
for (let i=0;i<4;i++) {
if (!apps[p*4+i]) return i;
draw_icon(p,i,selected==i && !settings.direct);
drawIcon(p,i,selected==i && !settings.direct);
}
g.flip();
}
};
Bangle.on("swipe",(dirLeftRight, dirUpDown)=>{
Bangle.loadWidgets();
//g.clear();
//Bangle.drawWidgets();
drawPage(0);
let swipeListenerDt = function(dirLeftRight, dirUpDown){
selected = 0;
oldselected=-1;
if(settings.swipeExit && dirLeftRight==1) load();
if(settings.swipeExit && dirLeftRight==1) returnToClock();
if (dirUpDown==-1||dirLeftRight==-1){
++page; if (page>maxPage) page=0;
drawPage(page);
@ -97,24 +100,24 @@ Bangle.on("swipe",(dirLeftRight, dirUpDown)=>{
--page; if (page<0) page=maxPage;
drawPage(page);
}
});
};
function isTouched(p,n){
let isTouched = function(p,n){
if (n<0 || n>3) return false;
var x1 = (n%2)*72+XOFF; var y1 = n>1?72+YOFF:YOFF;
var x2 = x1+71; var y2 = y1+81;
let x1 = (n%2)*72+XOFF; let y1 = n>1?72+YOFF:YOFF;
let x2 = x1+71; let y2 = y1+81;
return (p.x>x1 && p.y>y1 && p.x<x2 && p.y<y2);
}
};
Bangle.on("touch",(_,p)=>{
var i;
let touchListenerDt = function(_,p){
let i;
for (i=0;i<4;i++){
if((page*4+i)<Napps){
if (isTouched(p,i)) {
draw_icon(page,i,true && !settings.direct);
drawIcon(page,i,true && !settings.direct);
if (selected>=0 || settings.direct) {
if (selected!=i && !settings.direct){
draw_icon(page,selected,false);
drawIcon(page,selected,false);
} else {
load(apps[page*4+i].src);
}
@ -125,12 +128,32 @@ Bangle.on("touch",(_,p)=>{
}
}
if ((i==4 || (page*4+i)>Napps) && selected>=0) {
draw_icon(page,selected,false);
drawIcon(page,selected,false);
selected=-1;
}
};
const returnToClock = function() {
Bangle.setUI();
setTimeout(eval, 0, s.read(".bootcde"));
};
Bangle.setUI({
mode : 'custom',
back : returnToClock,
swipe : swipeListenerDt,
touch : touchListenerDt
});
Bangle.loadWidgets();
g.clear();
Bangle.drawWidgets();
drawPage(0);
// taken from Icon Launcher with minor alterations
var timeoutToClock;
const updateTimeoutToClock = function(){
if (settings.timeOut!="Off"){
let time=parseInt(settings.timeOut); //the "s" will be trimmed by the parseInt
if (timeoutToClock) clearTimeout(timeoutToClock);
timeoutToClock = setTimeout(returnToClock,time*1000);
}
};
updateTimeoutToClock();
} // end of app scope

View File

@ -1,7 +1,7 @@
{
"id": "dtlaunch",
"name": "Desktop Launcher",
"version": "0.16",
"version": "0.18",
"description": "Desktop style App Launcher with six (four for Bangle 2) apps per page - fast access if you have lots of apps installed.",
"screenshots": [{"url":"shot1.png"},{"url":"shot2.png"},{"url":"shot3.png"}],
"icon": "icon.png",

View File

@ -5,51 +5,56 @@
showClocks: true,
showLaunchers: true,
direct: false,
oneClickExit:false,
swipeExit: false
swipeExit: false,
timeOut: "Off"
}, require('Storage').readJSON(FILE, true) || {});
function writeSettings() {
require('Storage').writeJSON(FILE, settings);
}
const timeOutChoices = [/*LANG*/"Off", "10s", "15s", "20s", "30s"];
E.showMenu({
"" : { "title" : "Desktop launcher" },
"< Back" : () => back(),
'Show clocks': {
/*LANG*/"< Back" : () => back(),
/*LANG*/'Show clocks': {
value: settings.showClocks,
onchange: v => {
settings.showClocks = v;
writeSettings();
}
},
'Show launchers': {
/*LANG*/'Show launchers': {
value: settings.showLaunchers,
onchange: v => {
settings.showLaunchers = v;
writeSettings();
}
},
'Direct launch': {
/*LANG*/'Direct launch': {
value: settings.direct,
onchange: v => {
settings.direct = v;
writeSettings();
}
},
'Swipe Exit': {
/*LANG*/'Swipe Exit': {
value: settings.swipeExit,
onchange: v => {
settings.swipeExit = v;
writeSettings();
}
},
'One click exit': {
value: settings.oneClickExit,
/*LANG*/'Time Out': { // Adapted from Icon Launcher
value: timeOutChoices.indexOf(settings.timeOut),
min: 0,
max: timeOutChoices.length-1,
format: v => timeOutChoices[v],
onchange: v => {
settings.oneClickExit = v;
settings.timeOut = timeOutChoices[v];
writeSettings();
}
}
});
})
});

1
apps/entonclk/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.1: New App!

9
apps/entonclk/README.md Normal file
View File

@ -0,0 +1,9 @@
Enton - Enhanced Anton Clock
This clock face is based on the 'Anton Clock'.
Things I changed:
- The main font for the time is now Audiowide
- Removed the written out day name and replaced it with steps and bpm
- Changed the date string to a (for me) more readable string

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwkE/4A/AH4A/AH4A/AH4Aw+cikf/mQDCAAIFBAwQDBBYgXCgEDAQIABn4JBkAFBgIKDgQwFmMD+UCmcgl/zEIMzmcQmYKBmYiCAAfxC4QrBl8wBwcgkYsGC4sAiMAF4UxiIGBn8QAgMSC48wgMRiEDBAISCiYcFC48v//yC4PzgJAGiAXIiczPgPzC4JyBmf/AYQXI+KcCj8wmYFCgEjAYQ3G+cjbQIABJIMzAoUin7XIADpSEK4rWGI4MhmRJBn8j+U/d4MimUTkUzIw5dBl4UBMgIXBAgMyLYKOBmQXHiSbCDgMyl8z+UjmJ1BHgJbHCgM/IYQABAgQJBYYYA/AH4AtaQU/mTvBBozWBd44KBkUSkLnBEo8jkcvBI0/CgMiDAIXHHYIXImUzJQJHH+Y+Bn6Z/ABQA=="))

67
apps/entonclk/app.js Normal file
View File

@ -0,0 +1,67 @@
Graphics.prototype.setFontAudiowide = function() {
// Actual height 33 (36 - 4)
var widths = atob("CiAsESQjJSQkHyQkDA==");
var font = atob("AAAAAAAAAAAAAAAAAAAAAPAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAADgAAAAAAHgAAAAAAfgAAAAAA/gAAAAAD/gAAAAAH/gAAAAAf/AAAAAB/8AAAAAD/4AAAAAP/gAAAAAf/AAAAAB/8AAAAAD/4AAAAAP/gAAAAAf+AAAAAB/8AAAAAH/wAAAAAP/gAAAAA/+AAAAAB/8AAAAAD/wAAAAAD/gAAAAAD+AAAAAAD4AAAAAADwAAAAAADAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAA//+AAAAB///AAAAH///wAAAP///4AAAf///8AAA////+AAA/4AP+AAB/gAD/AAB/AA9/AAD+AB+/gAD+AD+/gAD+AD+/gAD8AH+fgAD8AP8fgAD8AP4fgAD8Af4fgAD8A/wfgAD8A/gfgAD8B/gfgAD8D/AfgAD8D+AfgAD8H+AfgAD8P8AfgAD8P4AfgAD8f4AfgAD8/wAfgAD8/gAfgAD+/gA/gAD+/AA/gAB/eAB/AAB/sAD/AAB/wAH/AAA////+AAAf///8AAAP///4AAAH///wAAAD///gAAAA//+AAAAAP/4AAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD/////gAD/////gAD/////gAD/////gAD/////gAD/////gAD/////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//gAAAAH//gAAAAP//gAD8Af//gAD8A///gAD8B///gAD8B///gAD8B/AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD+D+AfgAD//+AfgAD//+AfgAB//8AfgAA//4AfgAAf/wAfgAAP/gAfgAAB8AAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD+B+A/gAD/////gAB/////AAB/////AAA////+AAAf///8AAAP///4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//4AAAAD//8AAAAD//+AAAAD//+AAAAD//+AAAAD//+AAAAD//+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAD/////gAD/////gAD/////gAD/////gAD/////gAD/////gAD/////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//AAfgAD//wAfgAD//4AfgAD//8AfgAD//8AfgAD//+AfgAD8D+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B/A/gAD8B///gAD8B///gAD8A///AAD8A///AAAAAf/+AAAAAP/4AAAAAD/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB///AAAAH///wAAAf///8AAAf///8AAA////+AAB/////AAB/h+H/AAD/B+B/gAD+B+A/gAD+B+A/gAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B/A/gAD8B///gAD8B///gAD8A///AAAAAf//AAAAAf/+AAAAAH/4AAAAAB/gAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD8AAAAgAD8AAABgAD8AAAHgAD8AAAfgAD8AAA/gAD8AAD/gAD8AAP/gAD8AA//gAD8AB//AAD8AH/8AAD8Af/wAAD8A//AAAD8D/+AAAD8P/4AAAD8f/gAAAD9//AAAAD//8AAAAD//wAAAAD//gAAAAD/+AAAAAD/4AAAAAD/wAAAAAD/AAAAAAD8AAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAH/4AAAAAP/8AAAH+f/+AAAf////AAA/////gAB/////gAB///A/gAD//+AfgAD//+AfgAD+D+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD+D+AfgAD//+AfgAD//+AfgAB///A/gAB/////gAA/////AAAP////AAAD+f/+AAAAAP/8AAAAAH/4AAAAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/AAAAAAf/wAAAAA//4AAAAB//8AAAAB//8AfgAD//+AfgAD/D+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD+B+A/gAD+B+A/gAD/B+B/gAB/////AAB/////AAA////+AAAf///8AAAP///4AAAH///wAAAB///AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAPAAAA/AAfgAAA/AAfgAAA/AAfgAAA/AAfgAAAeAAPAAAAAAAAAAAAAAAAAAAAAAAAAA");
var scale = 1; // size multiplier for this font
g.setFontCustom(font, 46, widths, 48+(scale<<8)+(1<<16));
};
function getSteps() {
var steps = 0;
try{
if (WIDGETS.wpedom !== undefined) {
steps = WIDGETS.wpedom.getSteps();
} else if (WIDGETS.activepedom !== undefined) {
steps = WIDGETS.activepedom.getSteps();
} else {
steps = Bangle.getHealthStatus("day").steps;
}
} catch(ex) {
// In case we failed, we can only show 0 steps.
return "?";
}
return Math.round(steps);
}
{ // 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 drawTimeout;
// Actually draw the watch face
let draw = function() {
var x = g.getWidth() / 2;
var y = g.getHeight() / 2;
g.reset().clearRect(Bangle.appRect); // clear whole background (w/o widgets)
var date = new Date();
var timeStr = require("locale").time(date, 1); // Hour and minute
g.setFontAlign(0, 0).setFont("Audiowide").drawString(timeStr, x, y);
var dateStr = require("locale").date(date, 1).toUpperCase();
g.setFontAlign(0, 0).setFont("6x8", 2).drawString(dateStr, x, y+28);
g.setFontAlign(0, 0).setFont("6x8", 2);
g.drawString(getSteps(), 50, y+70);
g.drawString(Math.round(Bangle.getHealthStatus("last").bpm), g.getWidth() -37, y + 70);
// queue next draw
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, 60000 - (Date.now() % 60000));
};
// Show launcher when middle button pressed
Bangle.setUI({
mode : "clock",
remove : function() {
// Called to unload all of the clock app
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
delete Graphics.prototype.setFontAnton;
}});
// Load widgets
Bangle.loadWidgets();
draw();
setTimeout(Bangle.drawWidgets,0);
}

BIN
apps/entonclk/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 905 B

View File

@ -0,0 +1,17 @@
{
"id": "entonclk",
"name": "Enton Clock",
"version": "0.1",
"description": "A simple clock using the Audiowide font. ",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS2"],
"allow_emulator": true,
"readme":"README.md",
"storage": [
{"name":"entonclk.app.js","url":"app.js"},
{"name":"entonclk.img","url":"app-icon.js","evaluate":true}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

2
apps/gallery/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: New app!
0.02: Submitted to app loader

18
apps/gallery/README.md Normal file
View File

@ -0,0 +1,18 @@
# Gallery
A simple gallery app
## Usage
Upon opening the gallery app, you will be presented with a list of images that you can display. Tap the image to show it. Brightness will be set to full, and the screen timeout will be disabled. When you are done viewing the image, you can tap the screen to go back to the list of images. Press BTN1 to flip the image upside down.
## Adding images
1. The gallery app does not perform any scaling, and does not support panning. Therefore, you should use your favorite image editor to produce an image of the appropriate size for your watch. (240x240 for Bangle 1 or 176x176 for Bangle 2.) How you achieve this is up to you. If on a Bangle 2, I recommend adjusting the colors here to comply with the color restrictions.
2. Upload your image to the [Espruino image converter](https://www.espruino.com/Image+Converter). I recommend enabling compression and choosing one of the following color settings:
* 16 bit RGB565 for Bangle 1
* 3 bit RGB for Bangle 2
* 1 bit black/white for monochrome images that you want to respond to your system theme. (White will be rendered as your foreground color and black will be rendered as your background color.)
3. Set the output format to an image string, copy it into the [IDE](https://www.espruino.com/ide/), and set the destination to a file in storage. The file name should begin with "gal-" (without the quotes) and end with ".img" (without the quotes) to appear in the gallery. Note that the gal- prefix and .img extension will be removed in the UI. Upload the file.

52
apps/gallery/app.js Normal file
View File

@ -0,0 +1,52 @@
const storage = require('Storage');
let imageFiles = storage.list(/^gal-.*\.img/).sort();
let imageMenu = { '': { 'title': 'Gallery' } };
for (let fileName of imageFiles) {
let displayName = fileName.substr(4, fileName.length - 8); // Trim off the 'gal-' and '.img' for a friendly display name
imageMenu[displayName] = eval(`() => { drawImage("${fileName}"); }`); // Unfortunately, eval is the only reasonable way to do this
}
let cachedOptions = Bangle.getOptions(); // We will change the backlight and timeouts later, and need to restore them when displaying the menu
let backlightSetting = storage.readJSON('setting.json').brightness; // LCD brightness is not included in there for some reason
let angle = 0; // Store the angle of rotation
let image; // Cache the image here because we access it in multiple places
function drawMenu() {
Bangle.removeListener('touch', drawMenu); // We no longer want touching to reload the menu
Bangle.setOptions(cachedOptions); // The drawImage function set no timeout, undo that
Bangle.setLCDBrightness(backlightSetting); // Restore backlight
image = undefined; // Delete the image from memory
E.showMenu(imageMenu);
}
function drawImage(fileName) {
E.showMenu(); // Remove the menu to prevent it from breaking things
setTimeout(() => { Bangle.on('touch', drawMenu); }, 300); // Touch the screen to go back to the image menu (300ms timeout to allow user to lift finger)
Bangle.setOptions({ // Disable display power saving while showing the image
lockTimeout: 0,
lcdPowerTimeout: 0,
backlightTimeout: 0
});
Bangle.setLCDBrightness(1); // Full brightness
image = eval(storage.read(fileName)); // Sadly, the only reasonable way to do this
g.clear().reset().drawImage(image, 88, 88, { rotate: angle });
}
setWatch(info => {
if (image) {
if (angle == 0) angle = Math.PI;
else angle = 0;
Bangle.buzz();
g.clear().reset().drawImage(image, 88, 88, { rotate: angle })
}
}, BTN1, { repeat: true });
// We don't load the widgets because there is no reasonable way to unload them
drawMenu();

1
apps/gallery/icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgIOLgf/AAX8Av4FBJgkMAos/CIfMAv4Fe4AF/Apq5EAAw"))

BIN
apps/gallery/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 B

View File

@ -0,0 +1,26 @@
{
"id": "gallery",
"name": "Gallery",
"version": "0.02",
"description": "A gallery that lets you view images uploaded with the IDE (see README)",
"readme": "README.md",
"icon": "icon.png",
"type": "app",
"tags": "tools",
"supports": [
"BANGLEJS2",
"BANGLEJS"
],
"allow_emulator": true,
"storage": [
{
"name": "gallery.app.js",
"url": "app.js"
},
{
"name": "gallery.img",
"url": "icon.js",
"evaluate": true
}
]
}

View File

@ -6,3 +6,5 @@
0.05: Added adjustment for Bangle.js magnetometer heading fix
0.06: Fix waypoint menu always selecting last waypoint
Fix widget adding listeners more than once
0.07: Show checkered flag for target markers
Single waypoints are now shown in the compass view

View File

@ -10,7 +10,7 @@ Tapping or button to switch to the next information display, swipe right for the
Choose either a route or a waypoint as basis for the display.
After this selection and availability of a GPS fix the compass will show a blue dot for your destination and a green one for possibly available waypoints on the way.
After this selection and availability of a GPS fix the compass will show a checkered flag for your destination and a green dot for possibly available waypoints on the way.
Waypoints are shown with name if available and distance to waypoint.
As long as no GPS signal is available the compass shows the heading from the build in magnetometer. When a GPS fix becomes available, the compass display shows the GPS course. This can be differentiated by the display of bubble levels on top and sides of the compass.

View File

@ -239,8 +239,14 @@ function getCompassSlice(compassDataSource){
} else {
bpos=Math.round(bpos*increment);
}
graphics.setColor(p.color);
graphics.fillCircle(bpos,y+height-12,Math.floor(width*0.03));
if (p.color){
graphics.setColor(p.color);
}
if (p.icon){
graphics.drawImage(p.icon, bpos,y+height-12, {rotate:0,scale:2});
} else {
graphics.fillCircle(bpos,y+height-12,Math.floor(width*0.03));
}
}
}
if (compassDataSource.getMarkers){
@ -595,8 +601,8 @@ function showBackgroundMenu(){
"title" : "Background",
back : showMenu,
},
"Start" : ()=>{ E.showPrompt("Start?").then((v)=>{ if (v) {WIDGETS.gpstrek.start(true); removeMenu();} else {E.showMenu(mainmenu);}});},
"Stop" : ()=>{ E.showPrompt("Stop?").then((v)=>{ if (v) {WIDGETS.gpstrek.stop(true); removeMenu();} else {E.showMenu(mainmenu);}});},
"Start" : ()=>{ E.showPrompt("Start?").then((v)=>{ if (v) {WIDGETS.gpstrek.start(true); removeMenu();} else {showMenu();}}).catch(()=>{E.showMenu(mainmenu);});},
"Stop" : ()=>{ E.showPrompt("Stop?").then((v)=>{ if (v) {WIDGETS.gpstrek.stop(true); removeMenu();} else {showMenu();}}).catch(()=>{E.showMenu(mainmenu);});},
};
E.showMenu(menu);
}
@ -677,13 +683,15 @@ function setClosestWaypoint(route, startindex, progress){
let screen = 1;
const finishIcon = atob("CggB//meZmeZ+Z5n/w==");
const compassSliceData = {
getCourseType: function(){
return (state.currentPos && state.currentPos.course) ? "GPS" : "MAG";
},
getCourse: function (){
if(compassSliceData.getCourseType() == "GPS") return state.currentPos.course;
return state.compassHeading?360-state.compassHeading:undefined;
return state.compassHeading?state.compassHeading:undefined;
},
getPoints: function (){
let points = [];
@ -691,7 +699,10 @@ const compassSliceData = {
points.push({bearing:bearing(state.currentPos, state.route.currentWaypoint), color:"#0f0"});
}
if (state.currentPos && state.currentPos.lon && state.route){
points.push({bearing:bearing(state.currentPos, getLast(state.route)), color:"#00f"});
points.push({bearing:bearing(state.currentPos, getLast(state.route)), icon: finishIcon});
}
if (state.currentPos && state.currentPos.lon && state.waypoint){
points.push({bearing:bearing(state.currentPos, state.waypoint), icon: finishIcon});
}
return points;
},

View File

@ -1,7 +1,7 @@
{
"id": "gpstrek",
"name": "GPS Trekking",
"version": "0.06",
"version": "0.07",
"description": "Helper for tracking the status/progress during hiking. Do NOT depend on this for navigation!",
"icon": "icon.png",
"screenshots": [{"url":"screen1.png"},{"url":"screen2.png"},{"url":"screen3.png"},{"url":"screen4.png"}],

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -24,7 +24,7 @@ function onGPS(fix) {
}
function onMag(e) {
if (!state.compassHeading) state.compassHeading = 360-e.heading;
if (!state.compassHeading) state.compassHeading = e.heading;
//if (a+180)mod 360 == b then
//return (a+b)/2 mod 360 and ((a+b)/2 mod 360) + 180 (they are both the solution, so you may choose one depending if you prefer counterclockwise or clockwise direction)

1
apps/henkinen/ChangeLog Normal file
View File

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

7
apps/henkinen/README.md Normal file
View File

@ -0,0 +1,7 @@
# Henkinen
By Jukio Kallio
A tiny app helping you to breath and relax.
![](screenshot1.png)

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwkEogA0/4AKCpNPCxYAB+gtTGJQuOGBAWPGAwuQGAwXH+cykc/C6UhgMSkMQiQXKBQsgiYFDmMCMBIIEmAWEDAUDC5nzBwogDMYgXHBoohJC4wuJEQwXG+ALDmUQgMjEYcPC5MhAYXxgAACj4ICVYYXGIwXzCwYABHAUwC5HyEwXwC4pEC+MvC4/xEoUQC4sBHIQlCC4vwIxBIEGYQXFmJKCC45ECfQQXIRoiRGC5EiOxB4EBwQXdI653XU67XX+QJCPAwrC+JKCC4v/gZIIHIUwCAQXGkIDCSIg4C/8SC5PwEwX/mUQgMjAwXzJQQXH+ZICAA8wEYYXGBgoAEEQoXHGBIhFC44OBcgQADmIgFC5H/kAYEmMCBooXDp4KFkMBiUhiCjDAAX0C5RjBmUjPo4XMABQXEMAwALCwgwRFwowRCwwwPFw4xOCpIArA"))

127
apps/henkinen/app.js Normal file
View File

@ -0,0 +1,127 @@
// Henkinen
//
// Bangle.js 2 breathing helper
// by Jukio Kallio
// www.jukiokallio.com
require("FontHaxorNarrow7x17").add(Graphics);
// settings
const breath = {
theme: "default",
x:0, y:0, w:0, h:0,
size: 60,
bgcolor: g.theme.bg,
incolor: g.theme.fg,
keepcolor: g.theme.fg,
outcolor: g.theme.fg,
font: "HaxorNarrow7x17", fontsize: 1,
textcolor: g.theme.fg,
texty: 18,
in: 4000,
keep: 7000,
out: 8000
};
// set some additional settings
breath.w = g.getWidth(); // size of the background
breath.h = g.getHeight();
breath.x = breath.w * 0.5; // position of the circles
breath.y = breath.h * 0.45;
breath.texty = breath.y + breath.size + breath.texty; // text position
var wait = 100; // wait time, normally a minute
var time = 0; // for time keeping
// timeout used to update every minute
var drawTimeout;
// schedule a draw for the next minute
function queueDraw() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, wait - (Date.now() % wait));
}
// main function
function draw() {
// make date object
var date = new Date();
// update current time
time += wait - (Date.now() % wait);
if (time > breath.in + breath.keep + breath.out) time = 0; // reset time
// Reset the state of the graphics library
g.reset();
// Clear the area where we want to draw the time
g.setColor(breath.bgcolor);
g.fillRect(0, 0, breath.w, breath.h);
// calculate circle size
var circle = 0;
if (time < breath.in) {
// breath in
circle = time / breath.in;
g.setColor(breath.incolor);
} else if (time < breath.in + breath.keep) {
// keep breath
circle = 1;
g.setColor(breath.keepcolor);
} else if (time < breath.in + breath.keep + breath.out) {
// breath out
circle = ((breath.in + breath.keep + breath.out) - time) / breath.out;
g.setColor(breath.outcolor);
}
// draw breath circle
g.fillCircle(breath.x, breath.y, breath.size * circle);
// breath area
g.setColor(breath.textcolor);
g.drawCircle(breath.x, breath.y, breath.size);
// draw text
g.setFontAlign(0,0).setFont(breath.font, breath.fontsize).setColor(breath.textcolor);
if (time < breath.in) {
// breath in
g.drawString("Breath in", breath.x, breath.texty);
} else if (time < breath.in + breath.keep) {
// keep breath
g.drawString("Keep it in", breath.x, breath.texty);
} else if (time < breath.in + breath.keep + breath.out) {
// breath out
g.drawString("Breath out", breath.x, breath.texty);
}
// queue draw
queueDraw();
}
// Clear the screen once, at startup
g.clear();
// draw immediately at first
draw();
// keep LCD on
Bangle.setLCDPower(1);
// Show launcher when middle button pressed
Bangle.setUI("clock");

BIN
apps/henkinen/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -0,0 +1,15 @@
{ "id": "henkinen",
"name": "Henkinen - Tiny Breathing Helper",
"shortName":"Henkinen",
"version":"0.01",
"description": "A tiny app helping you to breath and relax.",
"icon": "app.png",
"screenshots": [{"url":"screenshot1.png"}],
"tags": "outdoors",
"supports" : ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"henkinen.app.js","url":"app.js"},
{"name":"henkinen.img","url":"app-icon.js","evaluate":true}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -8,3 +8,9 @@
Add swipe-to-exit
0.08: Only use fast loading for switching to clock to prevent problems in full screen apps
0.09: Remove fast load option since clocks containing Bangle.loadWidgets are now always normally loaded
0.10: changed the launch.json file name in iconlaunch.json ( launch.cache.json -> iconlaunch.cache.json)
used Object.assing for the settings
fix cache not deleted when "showClocks" options is changed
added timeOut to return to the clock
0.11: Cleanup timeout when changing to clock
Reset timeout on swipe and drag

View File

@ -1,12 +1,20 @@
{
const s = require("Storage");
const settings = s.readJSON("launch.json", true) || { showClocks: true, fullscreen: false,direct:false,swipeExit:false,oneClickExit:false};
const settings = Object.assign({
showClocks: true,
fullscreen: false,
direct: false,
oneClickExit: false,
swipeExit: false,
timeOut:"Off"
}, s.readJSON("iconlaunch.json", true) || {});
if (!settings.fullscreen) {
Bangle.loadWidgets();
Bangle.drawWidgets();
}
let launchCache = s.readJSON("launch.cache.json", true)||{};
let launchHash = require("Storage").hash(/\.info/);
let launchCache = s.readJSON("iconlaunch.cache.json", true)||{};
let launchHash = s.hash(/\.info/);
if (launchCache.hash!=launchHash) {
launchCache = {
hash : launchHash,
@ -20,7 +28,7 @@
if (a.name>b.name) return 1;
return 0;
}) };
s.writeJSON("launch.cache.json", launchCache);
s.writeJSON("iconlaunch.cache.json", launchCache);
}
let scroll = 0;
let selectedItem = -1;
@ -124,6 +132,7 @@
g.flip();
const itemsN = Math.ceil(launchCache.apps.length / appsN);
let onDrag = function(e) {
updateTimeout();
g.setColor(g.theme.fg);
g.setBgColor(g.theme.bg);
let dy = e.dy;
@ -173,6 +182,7 @@
drag: onDrag,
touch: (_, e) => {
if (e.y < R.y - 4) return;
updateTimeout();
let i = YtoIdx(e.y);
selectItem(i, e);
},
@ -193,11 +203,23 @@
delete idxToY;
delete YtoIdx;
delete settings;
if (timeout) clearTimeout(timeout);
setTimeout(eval, 0, s.read(".bootcde"));
};
if (settings.oneClickExit) mode.btn = returnToClock;
let timeout;
const updateTimeout = function(){
if (settings.timeOut!="Off"){
let time=parseInt(settings.timeOut); //the "s" will be trimmed by the parseInt
if (timeout) clearTimeout(timeout);
timeout = setTimeout(returnToClock,time*1000);
}
}
updateTimeout();
Bangle.setUI(mode);
}

View File

@ -2,7 +2,7 @@
"id": "iconlaunch",
"name": "Icon Launcher",
"shortName" : "Icon launcher",
"version": "0.09",
"version": "0.11",
"icon": "app.png",
"description": "A launcher inspired by smartphones, with an icon-only scrollable menu.",
"tags": "tool,system,launcher",
@ -12,6 +12,7 @@
{ "name": "iconlaunch.app.js", "url": "app.js" },
{ "name": "iconlaunch.settings.js", "url": "settings.js" }
],
"data": [{"name":"iconlaunch.json"},{"name":"iconlaunch.cache.json"}],
"screenshots": [{ "url": "screenshot1.png" }, { "url": "screenshot2.png" }],
"readme": "README.md"
}

View File

@ -1,24 +1,29 @@
// make sure to enclose the function in parentheses
(function(back) {
const s = require("Storage");
let settings = Object.assign({
showClocks: true,
fullscreen: false,
direct: false,
oneClickExit: false,
swipeExit: false
}, require("Storage").readJSON("launch.json", true) || {});
swipeExit: false,
timeOut:"Off"
}, s.readJSON("iconlaunch.json", true) || {});
let fonts = g.getFonts();
function save(key, value) {
settings[key] = value;
require("Storage").write("launch.json",settings);
s.write("iconlaunch.json",settings);
}
const timeOutChoices = [/*LANG*/"Off", "10s", "15s", "20s", "30s"];
const appMenu = {
"": { "title": /*LANG*/"Launcher" },
/*LANG*/"< Back": back,
/*LANG*/"Show Clocks": {
value: settings.showClocks == true,
onchange: (m) => { save("showClocks", m) }
onchange: (m) => {
save("showClocks", m);
s.erase("iconlaunch.cache.json"); //delete the cache app list
}
},
/*LANG*/"Fullscreen": {
value: settings.fullscreen == true,
@ -35,7 +40,15 @@
/*LANG*/"Swipe exit": {
value: settings.swipeExit == true,
onchange: m => { save("swipeExit", m) }
}
},
/*LANG*/'Time Out': {
value: timeOutChoices.indexOf(settings.timeOut),
min: 0, max: timeOutChoices.length-1,
format: v => timeOutChoices[v],
onchange: m => {
save("timeOut", timeOutChoices[m]);
}
},
};
E.showMenu(appMenu);
});

View File

@ -12,3 +12,7 @@
0.10: Fix clock not correctly refreshing when drawing in timeouts option is not on
0.11: Additional option in customizer to force drawing directly
Fix some problems in handling timeouts
0.12: Use widget_utils module
Fix colorsetting in promises in generated code
Some performance improvements by caching lookups
Activate UI after first draw is complete to prevent drawing over launcher

View File

@ -202,27 +202,39 @@ let firstDraw = true;
let firstDigitY = element.Y;
let imageIndex = element.ImageIndex ? element.ImageIndex : 0;
let firstImage;
if (imageIndex){
firstImage = getByPath(resources, [], "" + (0 + imageIndex));
} else {
firstImage = getByPath(resources, element.ImagePath, 0);
let firstImage = element.cachedFirstImage;
if (!firstImage && !element.cachedFirstImageMissing){
if (imageIndex){
firstImage = getByPath(resources, [], "" + (0 + imageIndex));
} else {
firstImage = getByPath(resources, element.ImagePath, 0);
}
element.cachedFirstImage = firstImage;
if (!firstImage) element.cachedFirstImageMissing = true;
}
let minusImage;
if (imageIndexMinus){
minusImage = getByPath(resources, [], "" + (0 + imageIndexMinus));
} else {
minusImage = getByPath(resources, element.ImagePath, "minus");
let minusImage = element.cachedMinusImage;
if (!minusImage && !element.cachedMinusImageMissing){
if (imageIndexMinus){
minusImage = getByPath(resources, [], "" + (0 + imageIndexMinus));
} else {
minusImage = getByPath(resources, element.ImagePath, "minus");
}
element.cachedMinusImage = minusImage;
if (!minusImage) element.cachedMinusImageMissing = true;
}
let unitImage;
let unitImage = element.cachedUnitImage;
//print("Get image for unit", imageIndexUnit);
if (imageIndexUnit !== undefined){
unitImage = getByPath(resources, [], "" + (0 + imageIndexUnit));
//print("Unit image is", unitImage);
} else if (element.Unit){
unitImage = getByPath(resources, element.ImagePath, getMultistate(element.Unit, "unknown"));
if (!unitImage && !element.cachedUnitImageMissing){
if (imageIndexUnit !== undefined){
unitImage = getByPath(resources, [], "" + (0 + imageIndexUnit));
//print("Unit image is", unitImage);
} else if (element.Unit){
unitImage = getByPath(resources, element.ImagePath, getMultistate(element.Unit, "unknown"));
}
unitImage = element.cachedUnitImage;
if (!unitImage) element.cachedUnitImageMissing = true;
}
let numberWidth = (numberOfDigits * firstImage.width) + (Math.max((numberOfDigits - 1),0) * spacing);
@ -292,14 +304,7 @@ let firstDraw = true;
if (resource){
prepareImg(resource);
//print("lastElem", typeof resource)
if (resource) {
element.cachedImage[cacheKey] = resource;
//print("cache res ",typeof element.cachedImage[cacheKey]);
} else {
element.cachedImage[cacheKey] = null;
//print("cache null",typeof element.cachedImage[cacheKey]);
//print("Could not create image from", resource);
}
element.cachedImage[cacheKey] = resource;
} else {
//print("Could not get resource from", element, lastElem);
}
@ -604,18 +609,15 @@ let firstDraw = true;
promise.then(()=>{
let currentDrawingTime = Date.now();
if (showWidgets && global.WIDGETS){
//print("Draw widgets");
if (showWidgets){
restoreWidgetDraw();
Bangle.drawWidgets();
g.setColor(g.theme.fg);
g.drawLine(0,24,g.getWidth(),24);
}
lastDrawTime = Date.now() - start;
isDrawing=false;
firstDraw=false;
requestRefresh = false;
endPerfLog("initialDraw");
if (!Bangle.uiRemove) setUi();
}).catch((e)=>{
print("Error during drawing", e);
});
@ -751,30 +753,19 @@ let firstDraw = true;
let showWidgetsChanged = false;
let currentDragDistance = 0;
let restoreWidgetDraw = function(){
if (global.WIDGETS) {
for (let w in global.WIDGETS) {
let wd = global.WIDGETS[w];
wd.draw = originalWidgetDraw[w];
wd.area = originalWidgetArea[w];
}
}
require("widget_utils").show();
Bangle.drawWidgets();
};
let handleDrag = function(e){
//print("handleDrag");
currentDragDistance += e.dy;
if (Math.abs(currentDragDistance) < 10) return;
dragDown = currentDragDistance > 0;
currentDragDistance = 0;
if (!showWidgets && dragDown){
let handleSwipe = function(lr, ud){
if (!showWidgets && ud == 1){
//print("Enable widgets");
restoreWidgetDraw();
showWidgetsChanged = true;
}
if (showWidgets && !dragDown){
if (showWidgets && ud == -1){
//print("Disable widgets");
clearWidgetsDraw();
firstDraw = true;
@ -783,12 +774,12 @@ let firstDraw = true;
if (showWidgetsChanged){
showWidgetsChanged = false;
//print("Draw after widget change");
showWidgets = dragDown;
showWidgets = ud == 1;
initialDraw();
}
};
Bangle.on('drag', handleDrag);
Bangle.on('swipe', handleSwipe);
if (!events || events.includes("pressure")){
Bangle.on('pressure', handlePressure);
@ -814,62 +805,54 @@ let firstDraw = true;
let clearWidgetsDraw = function(){
//print("Clear widget draw calls");
if (global.WIDGETS) {
originalWidgetDraw = {};
originalWidgetArea = {};
for (let w in global.WIDGETS) {
let wd = global.WIDGETS[w];
originalWidgetDraw[w] = wd.draw;
originalWidgetArea[w] = wd.area;
wd.draw = () => {};
wd.area = "";
}
}
require("widget_utils").hide();
}
handleLock(Bangle.isLocked(), true);
Bangle.setUI({
mode : "clock",
remove : function() {
//print("remove calls");
// Called to unload all of the clock app
Bangle.setHRMPower(0, "imageclock");
Bangle.setBarometerPower(0, 'imageclock');
let setUi = function(){
Bangle.setUI({
mode : "clock",
remove : function() {
//print("remove calls");
// Called to unload all of the clock app
Bangle.setHRMPower(0, "imageclock");
Bangle.setBarometerPower(0, 'imageclock');
Bangle.removeListener('drag', handleDrag);
Bangle.removeListener('lock', handleLock);
Bangle.removeListener('charging', handleCharging);
Bangle.removeListener('HRM', handleHrm);
Bangle.removeListener('pressure', handlePressure);
Bangle.removeListener('swipe', handleSwipe);
Bangle.removeListener('lock', handleLock);
Bangle.removeListener('charging', handleCharging);
Bangle.removeListener('HRM', handleHrm);
Bangle.removeListener('pressure', handlePressure);
if (deferredTimout) clearTimeout(deferredTimout);
if (initialDrawTimeoutUnlocked) clearTimeout(initialDrawTimeoutUnlocked);
if (initialDrawTimeoutLocked) clearTimeout(initialDrawTimeoutLocked);
if (deferredTimout) clearTimeout(deferredTimout);
if (initialDrawTimeoutUnlocked) clearTimeout(initialDrawTimeoutUnlocked);
if (initialDrawTimeoutLocked) clearTimeout(initialDrawTimeoutLocked);
for (let i of unlockedDrawInterval){
//print("Clearing unlocked", i);
clearInterval(i);
for (let i of global.unlockedDrawInterval){
//print("Clearing unlocked", i);
clearInterval(i);
}
delete global.unlockedDrawInterval;
for (let i of global.lockedDrawInterval){
//print("Clearing locked", i);
clearInterval(i);
}
delete global.lockedDrawInterval;
delete global.showWidgets;
delete global.firstDraw;
delete Bangle.printPerfLog;
if (settings.perflog){
delete Bangle.resetPerfLog;
delete performanceLog;
}
cleanupDelays();
restoreWidgetDraw();
}
delete unlockedDrawInterval;
for (let i of lockedDrawInterval){
//print("Clearing locked", i);
clearInterval(i);
}
delete lockedDrawInterval;
delete showWidgets;
delete firstDraw;
delete Bangle.printPerfLog;
if (settings.perflog){
delete Bangle.resetPerfLog;
delete performanceLog;
}
cleanupDelays();
restoreWidgetDraw();
}
});
});
}
Bangle.loadWidgets();
clearWidgetsDraw();

View File

@ -714,13 +714,13 @@
}
if (addDebug()) code += 'print("Element condition is ' + condition + '");' + "\n";
code += "" + colorsetting;
code += (condition.length > 0 ? "if (" + condition + "){\n" : "");
if (wrapInTimeouts && (plane != 0 || forceUseOrigPlane)){
code += "p = p.then(()=>delay(0)).then(()=>{\n";
} else {
code += "p = p.then(()=>{\n";
}
code += "" + colorsetting;
if (addDebug()) code += 'print("Drawing element ' + elementIndex + ' with type ' + c.type + ' on plane ' + planeName + '");' + "\n";
code += "draw" + c.type + "(" + planeName + ", wr, wf.Collapsed[" + elementIndex + "].value);\n";

View File

@ -2,7 +2,7 @@
"id": "imageclock",
"name": "Imageclock",
"shortName": "Imageclock",
"version": "0.11",
"version": "0.12",
"type": "clock",
"description": "BETA!!! File formats still subject to change --- This app is a highly customizable watchface. To use it, you need to select a watchface. You can build the watchfaces yourself without programming anything. All you need to do is write some json and create image files.",
"icon": "app.png",

3
apps/infoclk/ChangeLog Normal file
View File

@ -0,0 +1,3 @@
0.01: New app!
0.02-0.07: Bug fixes
0.08: Submitted to the app loader

33
apps/infoclk/README.md Normal file
View File

@ -0,0 +1,33 @@
# Informational clock
A configurable clock with extra info and shortcuts when unlocked, but large time when locked
## Information
The clock has two different screen arrangements, depending on whether the watch is locked or unlocked. The most commonly viewed piece of information is the time, so when the watch is locked it optimizes for the time being visible at a glance without the backlight. The hours and minutes take up nearly the entire top half of the display, with the date and seconds taking up nearly the entire bottom half. The day progress bar is between them if enabled, unless configured to be on the bottom row. The bottom row can be configured to display a weather summary, step count, step count and heart rate, the daily progress bar, or nothing.
When the watch is unlocked, it can be assumed that the backlight is on and the user is actively looking at the watch, so instead we can optimize for information density. The bottom half of the display becomes shortcuts, and the top half of the display becomes 4 rows of information (date and time, step count and heart rate, 2 line weather summary) + an optional daily progress bar. (The daily progress bar can be independently enabled when locked and unlocked.)
Most things are self-explanatory, but the day progress bar might not be. The day progress bar is intended to show approximately how far through the day you are, in the form of a progress bar. You might want to configure it to show how far you are through your waking hours, or you might want to use it to show how far you are through your work or school day.
## Shortcuts
There are generally a few apps that the user uses far more frequently than the others. For example, they might use a timer, alarm clock, and calculator every day, while everything else (such as the settings app) gets used only occasionally. This clock has space for 8 apps in the bottom half of the screen only one tap away, avoiding the need to wait for the launcher to open and then scroll through it. Tapping the top of the watch opens the launcher, eliminating the need for the button (which still opens the launcher due to bangle.js conventions). There is also handling for left, right, and vertical swipes. A vertical swipe by default opens the messages app, mimicking mobile operating systems which use a swipe down to view the notification shade.
## Configurability
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.
* They can be hidden when the battery is too low, to make the last portion of the battery last a little bit longer.
* They can be hidden during a period of time such as when the user is asleep and therefore unlikely to need very much precision.
The date format can be changed.
As described earlier, the contents of the bottom row when locked can be changed.
The 8 tap-based shortcuts on the bottom and the 3 swipe-based shortcuts can be changed to nothing, the launcher, or any app on the watch.
The start and end time of the day progress bar can be changed. It can be enabled or disabled separately when the watch is locked and unlocked. The color can be changed. The time when it resets from full to empty can be changed.
When the battery is below a defined point, the watch's color can change to another chosen color to help the user notice that the battery is low.

405
apps/infoclk/app.js Normal file
View File

@ -0,0 +1,405 @@
const SETTINGS_FILE = "infoclk.json";
const FONT = require('infoclk-font.js');
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
},
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));
// 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);
}
// Draw the bottom area
if (config.bottomLocked.display == 'progress') {
let color = config.dayProgress.color;
g.setColor(color[0], color[1], color[2])
.fillRect(0, SECONDS_TOP + DIGIT_HEIGHT + 3, g.getWidth() * getDayProgress(now), g.getHeight());
} else {
let bottomString;
if (config.bottomLocked.display == 'weather') bottomString = getWeatherString();
else if (config.bottomLocked.display == 'steps') bottomString = getStepsString();
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)))
.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);
}
} else {
//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');
let maxHeight = ((g.getHeight() / 2) - HHMM_TOP) / (config.dayProgress.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.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);
}
}
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
});
}
}
weather.on("update", draw);
Bangle.on("step", draw);
Bangle.on('lock', locked => {
//If the watch is unlocked, draw the icons
if (!locked) drawIcons();
draw();
});
// Show launcher when middle button pressed
Bangle.setUI("clock");
// Load widgets
Bangle.loadWidgets();
Bangle.drawWidgets();
// 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) {
Bangle.buzz();
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;
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]);
}
});
//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);
});
if (!Bangle.isLocked()) drawIcons();
draw();

23
apps/infoclk/font.js Normal file
View File

@ -0,0 +1,23 @@
const heatshrink = require("heatshrink")
function decompress(string) {
return heatshrink.decompress(atob(string))
}
exports = {
'0': decompress("ktAwIEB////EAj4EB+EDAYP/8E/AgWDAYX+CIX/+IDC//PBoYIDAAvwgEHAgOAgAnB/kAgIvCgEPAgJCBv5CCHwXAI4X+KAYk/En4kmAA4qBAAP7BAePAYX4BofBAYX8F4Q+BEwRHBIQI5BA"),
'1': decompress("ktAwIGDj/4AgX/4ADBg/+BAU/+ADBgP/wAEBh/8BoV/8ADBgf/En4k/En4k/EgQ="),
'2': decompress("ktAwMA/4AB/EHAgXwn4EC8IDC/+PAYX+v4EC+YND74NDBAYAE4A0Bg/+HIU/+ADBgP/wAEBh/8BoV/8ADBgf/BAUf/AECElQdBPA2HAYX8OYfHBAYRD8Z3Dj6TG/kPPYZm4EiwAHO4f7BAfPfI/xBoaTEPAfgQwY"),
'3': decompress("ktAwMA/4AB/EHAgXwn4EC8IDC/+PAYX+v4EC+YND74NDBAYAE4A0Bg/+HIU/+ADBgP/wAEBh/8BoV/8ADBgf/BAUf/AECElJWIAEpu/EhpgS34DC/IID54DC/l/AgXDAYX4j57DA"),
'4': decompress("ktAwMA//AgEf//+BYP///wgEHAgOAgE///8gEBBAPggEPAgIWBv///EAgYIBEn4kXABf9AgfnAgY4BAAP4BAfDAYX+EwfwIQRRCJIJRBJIRRBJIQICj5RBJIRRBJIJRCNwJRBNwQk/Ei4A=="),
'5': decompress("ktAwIEB/4AB/EfAgXDAYX+n4EC+YDC/+fAYX9BAfvAgYAJ+AwBgP/wAEBh/8H4V/8ADBgf/BAUf/AEC//AAYMH/wICn4kpPYUPAgXgv4EC4JfDg4DC/iFD8ANDwaTDCQfwEoZ2/EhrXNAAm/AYX5BAfPQoaTD4ahDj57DA=="),
'6': decompress("ktAwIEB/4AB/EfAgXDAYX+n4EC+YDC/+fAYX9BAfvAgYAJ+AwBgP/wAEBh/8H4V/8ADBgf/BAUf/AEC//AAYMH/wICn4kpPYUPAgXgv6AG/6JD/gID84ED358NJIIsCKIQ0BKIRJCFgJJCSYcHAgJuBXYJuBKIQkpAA58D/YIDx6PDBofBQoYvCHwImCI4KUCwA="),
'7': decompress("ktAwMA/4AB/EHAgXwn4EC8IDC/+PAYX+v4EC+YND74NDBAYAE4A0Bg/+HIU/+ADBgP/wAEBh/8BoV/8ADBgf/BAUf/AECEn4k/En4kVA"),
'8': decompress("ktAwIEB////EAj4EB+EDAYP/8E/AgWDAYX+CIX/+IDC//PBoYIDAAvwgEHAgOAgAnB/kAgIvCgEPAgJCBv5CCHwXAI4X+KAYkpAFpu/EhwAHFQIAB/YIDx4DC/AND4IDC/ieD4AmCI4JCBHIIA=="),
'9': decompress("ktAwIEB////EAj4EB+EDAYP/8E/AgWDAYX+CIX/+IDC//PBoYIDAAvwgEHAgOAgAnB/kAgIvCgEPAgJCBv5CCHwXAI4X+KAYkpABf9AgfnAgaFD/AID4Z8DEwfwIQRRCJIJRBJIRRBJIQICj5RBJIRRBJIJRCNwJRBNwQkoPhoAE34DC/L0H/iwBQAv4WAJ7CA=="),
':': decompress("iFAwITQg/gj/4n/8v/+AIP/ABQPDCoIZBDoJTfH94A=="),
'am': decompress("jFAwIEBngCEvwCH/4CFwEBAQkD//AgfnAQcH4fgAQsPwPwAQf/+Ef//4AQn8n0AvgCCHQN+vkAnwCC/EAj4CF+EAh4CCNIoLFC4v8gE/AQv+gF/AQpwB/4CDwICG+/D94CD8/v+fn54CC+P/x4CF+H/IgICFvwCEngCD"),
'pm': decompress("jFAwMAn///l///+/4AE+EAh4CaEYoABFgX8BwMAAUwAFIIv4gEfAQX8OYICF/0Av4CF/8AKQICCwICG+/D94CD8/v+fn54CC+P/x4CF+H/IgICFvwCEngCDA")
}

BIN
apps/infoclk/font/am.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 B

BIN
apps/infoclk/font/colon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 B

BIN
apps/infoclk/font/pm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 B

1
apps/infoclk/icon.js Normal file
View File

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

BIN
apps/infoclk/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 B

View File

@ -0,0 +1,38 @@
{
"id": "infoclk",
"name": "Informational clock",
"version": "0.08",
"description": "A configurable clock with extra info and shortcuts when unlocked, but large time when locked",
"readme": "README.md",
"icon": "icon.png",
"type": "clock",
"tags": "clock",
"supports": [
"BANGLEJS2"
],
"allow_emulator": true,
"storage": [
{
"name": "infoclk.app.js",
"url": "app.js"
},
{
"name": "infoclk.settings.js",
"url": "settings.js"
},
{
"name": "infoclk-font.js",
"url": "font.js"
},
{
"name": "infoclk.img",
"url": "icon.js",
"evaluate": true
}
],
"data": [
{
"name": "infoclk.json"
}
]
}

571
apps/infoclk/settings.js Normal file
View File

@ -0,0 +1,571 @@
(function (back) {
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));
function saveSettings() {
storage.writeJSON(SETTINGS_FILE, config);
}
function hourToString(hour) {
if (storage.readJSON('setting.json')['12hour']) {
if (hour == 0) return '12 AM';
else if (hour < 12) return `${hour} AM`;
else if (hour == 12) return '12 PM';
else return `${hour - 12} PM`;
} else return '' + hour;
}
// The menu for configuring when the seconds are shown
function showSecondsMenu() {
E.showMenu({
'': {
'title': 'Seconds display',
'back': showMainMenu
},
'Show seconds': {
value: !config.seconds.hideAlways,
onchange: value => {
config.seconds.hideAlways = !value;
saveSettings();
}
},
'...unless locked': {
value: config.seconds.hideLocked,
onchange: value => {
config.seconds.hideLocked = value;
saveSettings();
}
},
'...unless battery below': {
value: config.seconds.hideBattery,
min: 0,
max: 100,
format: value => `${value}%`,
onchange: value => {
config.seconds.hideBattery = value;
saveSettings();
}
},
'...unless between these 2 times...': () => {
E.showMenu({
'': {
'title': 'Hide seconds between',
'back': showSecondsMenu
},
'Enabled': {
value: config.seconds.hideTime,
onchange: value => {
config.seconds.hideTime = value;
saveSettings();
}
},
'Start hour': {
value: Math.floor(config.seconds.hideStart / 100),
format: hourToString,
min: 0,
max: 23,
wrap: true,
onchange: hour => {
minute = config.seconds.hideStart % 100;
config.seconds.hideStart = (100 * hour) + minute;
saveSettings();
}
},
'Start minute': {
value: config.seconds.hideStart % 100,
min: 0,
max: 59,
wrap: true,
onchange: minute => {
hour = Math.floor(config.seconds.hideStart / 100);
config.seconds.hideStart = (100 * hour) + minute;
saveSettings();
}
},
'End hour': {
value: Math.floor(config.seconds.hideEnd / 100),
format: hourToString,
min: 0,
max: 23,
wrap: true,
onchange: hour => {
minute = config.seconds.hideEnd % 100;
config.seconds.hideEnd = (100 * hour) + minute;
saveSettings();
}
},
'End minute': {
value: config.seconds.hideEnd % 100,
min: 0,
max: 59,
wrap: true,
onchange: minute => {
hour = Math.floor(config.seconds.hideEnd / 100);
config.seconds.hideEnd = (100 * hour) + minute;
saveSettings();
}
}
});
}
});
}
// Available month/date separators
const SEPARATORS = [
{ name: 'Slash', char: '/' },
{ name: 'Dash', char: '-' },
{ name: 'Space', char: ' ' },
{ name: 'Comma', char: ',' },
{ name: 'None', char: '' }
];
// Available bottom row display options
const BOTTOM_ROW_OPTIONS = [
{ name: 'Weather', val: 'weather' },
{ name: 'Step count', val: 'steps' },
{ name: 'Steps + BPM', val: 'health' },
{ name: 'Day progresss bar', val: 'progress' },
{ name: 'Nothing', val: false }
];
// The menu for configuring which apps have shortcut icons
function showShortcutMenu() {
//Builds the shortcut options
let shortcutOptions = [
{ name: 'Nothing', val: false },
{ name: 'Launcher', val: '#LAUNCHER' },
];
let infoFiles = storage.list(/\.info$/).sort((a, b) => {
if (a.name < b.name) return -1;
else if (a.name > b.name) return 1;
else return 0;
});
for (let infoFile of infoFiles) {
let appInfo = storage.readJSON(infoFile);
if (appInfo.src) shortcutOptions.push({
name: appInfo.name,
val: appInfo.id
});
}
E.showMenu({
'': {
'title': 'Shortcuts',
'back': showMainMenu
},
'Top first': {
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[0]),
format: value => shortcutOptions[value].name,
min: 0,
max: shortcutOptions.length - 1,
wrap: false,
onchange: value => {
config.shortcuts[0] = shortcutOptions[value].val;
saveSettings();
}
},
'Top second': {
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[1]),
format: value => shortcutOptions[value].name,
min: 0,
max: shortcutOptions.length - 1,
wrap: false,
onchange: value => {
config.shortcuts[1] = shortcutOptions[value].val;
saveSettings();
}
},
'Top third': {
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[2]),
format: value => shortcutOptions[value].name,
min: 0,
max: shortcutOptions.length - 1,
wrap: false,
onchange: value => {
config.shortcuts[2] = shortcutOptions[value].val;
saveSettings();
}
},
'Top fourth': {
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[3]),
format: value => shortcutOptions[value].name,
min: 0,
max: shortcutOptions.length - 1,
wrap: false,
onchange: value => {
config.shortcuts[3] = shortcutOptions[value].val;
saveSettings();
}
},
'Bottom first': {
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[4]),
format: value => shortcutOptions[value].name,
min: 0,
max: shortcutOptions.length - 1,
wrap: false,
onchange: value => {
config.shortcuts[4] = shortcutOptions[value].val;
saveSettings();
}
},
'Bottom second': {
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[5]),
format: value => shortcutOptions[value].name,
min: 0,
max: shortcutOptions.length - 1,
wrap: false,
onchange: value => {
config.shortcuts[5] = shortcutOptions[value].val;
saveSettings();
}
},
'Bottom third': {
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[6]),
format: value => shortcutOptions[value].name,
min: 0,
max: shortcutOptions.length - 1,
wrap: false,
onchange: value => {
config.shortcuts[6] = shortcutOptions[value].val;
saveSettings();
}
},
'Bottom fourth': {
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[7]),
format: value => shortcutOptions[value].name,
min: 0,
max: shortcutOptions.length - 1,
wrap: false,
onchange: value => {
config.shortcuts[7] = shortcutOptions[value].val;
saveSettings();
}
},
'Swipe up': {
value: shortcutOptions.map(item => item.val).indexOf(config.swipe.up),
format: value => shortcutOptions[value].name,
min: 0,
max: shortcutOptions.length - 1,
wrap: false,
onchange: value => {
config.swipe.up = shortcutOptions[value].val;
saveSettings();
}
},
'Swipe left': {
value: shortcutOptions.map(item => item.val).indexOf(config.swipe.left),
format: value => shortcutOptions[value].name,
min: 0,
max: shortcutOptions.length - 1,
wrap: false,
onchange: value => {
config.swipe.left = shortcutOptions[value].val;
saveSettings();
}
},
'Swipe right': {
value: shortcutOptions.map(item => item.val).indexOf(config.swipe.right),
format: value => shortcutOptions[value].name,
min: 0,
max: shortcutOptions.length - 1,
wrap: false,
onchange: value => {
config.swipe.right = shortcutOptions[value].val;
saveSettings();
}
},
});
}
const COLOR_OPTIONS = [
{ name: 'Black', val: [0, 0, 0] },
{ name: 'Blue', val: [0, 0, 1] },
{ name: 'Green', val: [0, 1, 0] },
{ name: 'Cyan', val: [0, 1, 1] },
{ name: 'Red', val: [1, 0, 0] },
{ name: 'Magenta', val: [1, 0, 1] },
{ name: 'Yellow', val: [1, 1, 0] },
{ name: 'White', val: [1, 1, 1] }
];
// Workaround for being unable to use == on arrays: convert them into strings
function colorString(color) {
return `${color[0]} ${color[1]} ${color[2]}`;
}
//Shows the top level menu
function showMainMenu() {
E.showMenu({
'': {
'title': 'Informational Clock',
'back': back
},
'Seconds display': showSecondsMenu,
'Day of week format': {
value: config.date.dayFullName,
format: value => value ? 'Full name' : 'Abbreviation',
onchange: value => {
config.date.dayFullName = value;
saveSettings();
}
},
'Date format': () => {
E.showMenu({
'': {
'title': 'Date format',
'back': showMainMenu,
},
'Order': {
value: config.date.mmdd,
format: value => value ? 'Month first' : 'Date first',
onchange: value => {
config.date.mmdd = value;
saveSettings();
}
},
'Separator': {
value: SEPARATORS.map(item => item.char).indexOf(config.date.separator),
format: value => SEPARATORS[value].name,
min: 0,
max: SEPARATORS.length - 1,
wrap: true,
onchange: value => {
config.date.separator = SEPARATORS[value].char;
saveSettings();
}
},
'Month format': {
// 0 = number only
// 1 = abbreviation
// 2 = full name
value: config.date.monthName ? (config.date.monthFullName ? 2 : 1) : 0,
format: value => ['Number', 'Abbreviation', 'Full name'][value],
min: 0,
max: 2,
wrap: true,
onchange: value => {
if (value == 0) config.date.monthName = false;
else {
config.date.monthName = true;
config.date.monthFullName = (value == 2);
}
saveSettings();
}
}
});
},
'Bottom row': {
value: BOTTOM_ROW_OPTIONS.map(item => item.val).indexOf(config.bottomLocked.display),
format: value => BOTTOM_ROW_OPTIONS[value].name,
min: 0,
max: BOTTOM_ROW_OPTIONS.length - 1,
wrap: true,
onchange: value => {
config.bottomLocked.display = BOTTOM_ROW_OPTIONS[value].val;
saveSettings();
}
},
'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();
}
}
});
},
'Low battery color': () => {
E.showMenu({
'': {
'title': 'Low battery color',
back: showMainMenu
},
'Low battery threshold': {
value: config.lowBattColor.level,
min: 0,
max: 100,
format: value => `${value}%`,
onchange: value => {
config.lowBattColor.level = value;
saveSettings();
}
},
'Color': {
value: COLOR_OPTIONS.map(item => colorString(item.val)).indexOf(colorString(config.lowBattColor.color)),
format: value => COLOR_OPTIONS[value].name,
min: 0,
max: COLOR_OPTIONS.length - 1,
wrap: false,
onchange: value => {
config.lowBattColor.color = COLOR_OPTIONS[value].val;
saveSettings();
}
}
});
},
});
}
showMainMenu();
});

View File

@ -2,3 +2,4 @@
0.02: Now keeps user input trace intact by changing how the screen is updated.
0.03: Positioning of marker now takes the height of the widget field into account.
0.04: Fix issue if going back without typing.
0.05: Keep drag-function in ram, hopefully improving performance and input reliability somewhat.

View File

@ -139,6 +139,7 @@ exports.getStrokes( (id,s) => Bangle.strokes[id] = Unistroke.new(s) );
return new Promise((resolve,reject) => {
var l;//last event
Bangle.setUI({mode:"custom", drag:e=>{
"ram";
if (l) g.reset().setColor("#f00").drawLine(l.x,l.y,e.x,e.y);
l = e.b ? e : 0;
},touch:() => {

View File

@ -1,6 +1,6 @@
{ "id": "kbswipe",
"name": "Swipe keyboard",
"version":"0.04",
"version":"0.05",
"description": "A library for text input via PalmOS style swipe gestures (beta!)",
"icon": "app.png",
"type":"textinput",

2
apps/keytimer/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: New app!
0.02: Submitted to the app loader

27
apps/keytimer/app.js Normal file
View File

@ -0,0 +1,27 @@
Bangle.keytimer_ACTIVE = true;
const common = require("keytimer-com.js");
const storage = require("Storage");
const keypad = require("keytimer-keys.js");
const timerView = require("keytimer-tview.js");
Bangle.loadWidgets();
Bangle.drawWidgets();
//Save our state when the app is closed
E.on('kill', () => {
storage.writeJSON(common.STATE_PATH, 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.
Bangle.on('touch', (button, xy) => {
if (common.state.wasRunning) timerView.touch(button, xy);
else keypad.touch(button, xy);
});
Bangle.on('swipe', dir => {
if (!common.state.wasRunning) keypad.swipe(dir);
});
if (common.state.wasRunning) timerView.show(common);
else keypad.show(common);

11
apps/keytimer/boot.js Normal file
View File

@ -0,0 +1,11 @@
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());
}

42
apps/keytimer/common.js Normal file
View File

@ -0,0 +1,42 @@
const storage = require("Storage");
const heatshrink = require("heatshrink");
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="))
};
//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;
}
//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;
} 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.setTime - runningTime;
}

1
apps/keytimer/icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwcAkmSpICOggRPpEACJ9AgESCJxMBhu27dtARVgCIMBCJpxDmwRL7ARDgwRL4CWECJaoFjYRJ2ARFgYRJwDNGCJFsb46SIRgQAFSRAQHSRCMEAAqSGRgoAFRhaSKRgySKRg6SIRhCSIRhCSICBqSCRhSSGRhY2FkARPhMkCJ9JkiONgECCIOQCJsSCIOSCJuSCIVACBcECIdICJYOBCIVJRhYRFSRSMBCIiSKBwgCCSRCMCCIqSIRgYCFRhYCFSQyMEAQqSGBw6SIRgySKRgtO4iSJBAmT23bOIqSCRgvtCINsSQ4aEndtCINt2KSGIggOBCIW2JQlARgZECCIhKEpBEGCIpKEA=="))

BIN
apps/keytimer/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 B

BIN
apps/keytimer/img/pause.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
apps/keytimer/img/play.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
apps/keytimer/img/reset.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

136
apps/keytimer/keypad.js Normal file
View File

@ -0,0 +1,136 @@
let common;
function inputStringToTime(inputString) {
let number = parseInt(inputString);
let hours = Math.floor(number / 10000);
let minutes = Math.floor((number % 10000) / 100);
let seconds = number % 100;
return 3600000 * hours +
60000 * minutes +
1000 * seconds;
}
function pad(number) {
return ('00' + parseInt(number)).slice(-2);
}
function inputStringToDisplayString(inputString) {
let number = parseInt(inputString);
let hours = Math.floor(number / 10000);
let minutes = Math.floor((number % 10000) / 100);
let seconds = number % 100;
if (hours == 0 && minutes == 0) return '' + seconds;
else if (hours == 0) return `${pad(minutes)}:${pad(seconds)}`;
else return `${hours}:${pad(minutes)}:${pad(seconds)}`;
}
class NumberButton {
constructor(number) {
this.label = '' + number;
}
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();
}
}
let ClearButton = {
label: 'Clr',
onclick: () => {
common.state.inputString = '0';
common.state.setTime = 0;
updateDisplay();
feedback(true);
}
};
let StartButton = {
label: 'Go',
onclick: () => {
common.state.startTime = (new Date()).getTime();
common.state.elapsedTime = 0;
common.state.wasRunning = true;
common.state.running = true;
feedback(true);
require('keytimer-tview.js').show(common);
}
};
const BUTTONS = [
[new NumberButton(7), new NumberButton(8), new NumberButton(9), ClearButton],
[new NumberButton(4), new NumberButton(5), new NumberButton(6), new NumberButton(0)],
[new NumberButton(1), new NumberButton(2), new NumberButton(3), StartButton]
];
function feedback(acceptable) {
if (acceptable) Bangle.buzz(50, 0.5);
else Bangle.buzz(200, 1);
}
function drawButtons() {
g.reset().clearRect(0, 44, 175, 175).setFont("Vector", 15).setFontAlign(0, 0);
//Draw lines
for (let x = 44; x <= 176; x += 44) {
g.drawLine(x, 44, x, 175);
}
for (let y = 44; y <= 176; y += 44) {
g.drawLine(0, y, 175, y);
}
for (let row = 0; row < 3; row++) {
for (let col = 0; col < 4; col++) {
g.drawString(BUTTONS[row][col].label, 22 + 44 * col, 66 + 44 * row);
}
}
}
function getFontSize(length) {
let size = Math.floor(176 / length); //Characters of width needed per pixel
size *= (20 / 12); //Convert to height
// Clamp to between 6 and 20
if (size < 6) return 6;
else if (size > 20) return 20;
else return Math.floor(size);
}
function updateDisplay() {
let displayString = inputStringToDisplayString(common.state.inputString);
g.clearRect(0, 24, 175, 43).setColor(storage.readJSON('setting.json').theme.fg2).setFontAlign(1, -1).setFont("Vector", getFontSize(displayString.length)).drawString(displayString, 176, 24);
}
exports.show = function (callerCommon) {
common = callerCommon;
g.reset();
drawButtons();
updateDisplay();
};
exports.touch = function (button, xy) {
let row = Math.floor((xy.y - 44) / 44);
let col = Math.floor(xy.x / 44);
if (row < 0) return;
if (row > 2) row = 2;
if (col < 0) col = 0;
if (col > 3) col = 3;
BUTTONS[row][col].onclick();
};
exports.swipe = function (dir) {
if (dir == -1) {
if (common.state.inputString.length == 1) common.state.inputString = '0';
else common.state.inputString = common.state.inputString.substring(0, common.state.inputString.length - 1);
common.state.setTime = inputStringToTime(common.state.inputString);
feedback(true);
updateDisplay();
} else if (dir == 0) {
EnterButton.onclick();
}
};

View File

@ -0,0 +1,44 @@
{
"id": "keytimer",
"name": "Keypad Timer",
"version": "0.02",
"description": "A timer with a keypad that runs in the background",
"icon": "icon.png",
"type": "app",
"tags": "tools",
"supports": [
"BANGLEJS2"
],
"allow_emulator": true,
"storage": [
{
"name": "keytimer.app.js",
"url": "app.js"
},
{
"name": "keytimer.img",
"url": "icon.js",
"evaluate": true
},
{
"name": "keytimer.boot.js",
"url": "boot.js"
},
{
"name": "keytimer-com.js",
"url": "common.js"
},
{
"name": "keytimer-ring.js",
"url": "ring.js"
},
{
"name": "keytimer-keys.js",
"url": "keypad.js"
},
{
"name": "keytimer-tview.js",
"url": "timerview.js"
}
]
}

28
apps/keytimer/ring.js Normal file
View File

@ -0,0 +1,28 @@
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);
}
E.showAlert("Timer expired!").then(() => {
stopTimer();
load();
});
E.on('kill', stopTimer);

107
apps/keytimer/timerview.js Normal file
View File

@ -0,0 +1,107 @@
let common;
function drawButtons() {
//Draw the backdrop
const BAR_TOP = g.getHeight() - 24;
g.setColor(0, 0, 1).setFontAlign(0, -1)
.clearRect(0, BAR_TOP, g.getWidth(), g.getHeight())
.fillRect(0, BAR_TOP, g.getWidth(), g.getHeight())
.setColor(1, 1, 1)
.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);
} else {
g.drawImage(common.BUTTON_ICONS.play, g.getWidth() * 3 / 4, BAR_TOP);
}
}
function drawTimer() {
let timeLeft = common.getTimeLeft();
g.reset()
.setFontAlign(0, 0)
.setFont("Vector", 36)
.clearRect(0, 24, 176, 152)
//Draw the timer
.drawString((() => {
let hours = timeLeft / 3600000;
let minutes = (timeLeft % 3600000) / 60000;
let seconds = (timeLeft % 60000) / 1000;
function pad(number) {
return ('00' + parseInt(number)).slice(-2);
}
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;
function setupTimerInterval() {
if (timerInterval !== undefined) {
clearInterval(timerInterval);
}
setTimeout(() => {
timerInterval = setInterval(drawTimer, 1000);
drawTimer();
}, common.timeLeft % 1000);
}
exports.show = function (callerCommon) {
common = callerCommon;
drawButtons();
drawTimer();
if (common.state.running) {
setupTimerInterval();
}
}
function clearTimerInterval() {
if (timerInterval !== undefined) {
clearInterval(timerInterval);
timerInterval = undefined;
}
}
exports.touch = (button, xy) => {
if (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;
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;
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();
setupTimerInterval();
drawButtons();
}
}
};

View File

@ -17,3 +17,5 @@
0.15: Support for unload and quick return to the clock on 2v16
0.16: Use a cache of app.info files to speed up loading the launcher
0.17: Don't display 'Loading...' now the watch has its own loading screen
0.18: Add 'back' icon in top-left to go back to clock
0.19: Fix regression after back button added (returnToClock was called twice!)

View File

@ -41,6 +41,17 @@ let apps = launchCache.apps;
// Now apps list is loaded - render
if (!settings.fullscreen)
Bangle.loadWidgets();
let returnToClock = function() {
// unload everything manually
// ... or we could just call `load();` but it will be slower
Bangle.setUI(); // remove scroller's handling
if (lockTimeout) clearTimeout(lockTimeout);
Bangle.removeListener("lock", lockHandler);
// now load the default clock - just call .bootcde as this has the code already
setTimeout(eval,0,s.read(".bootcde"));
}
E.showScroller({
h : 64*scaleval, c : apps.length,
draw : (i, r) => {
@ -62,26 +73,11 @@ E.showScroller({
} else {
load(app.src);
}
}
},
back : returnToClock // button press or tap in top left calls returnToClock now
});
g.flip(); // force a render before widgets have finished drawing
let returnToClock = function() {
// unload everything manually
// ... or we could just call `load();` but it will be slower
Bangle.setUI(); // remove scroller's handling
if (lockTimeout) clearTimeout(lockTimeout);
Bangle.removeListener("lock", lockHandler);
// now load the default clock - just call .bootcde as this has the code already
setTimeout(eval,0,s.read(".bootcde"));
}
// on bangle.js 2, the screen is used for navigating, so the single button goes back
// on bangle.js 1, the buttons are used for navigating
if (process.env.HWVERSION==2) {
setWatch(returnToClock, BTN1, {edge:"falling"});
}
// 10s of inactivity goes back to clock
Bangle.setLocked(false); // unlock initially
let lockTimeout;

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