forked from FOSS/BangleApps
Merge branch 'espruino:master' into master
commit
a737e7f772
|
@ -7,7 +7,7 @@
|
||||||
active = active.sort((a,b)=>(a.hr-b.hr)+(a.last-b.last)*24);
|
active = active.sort((a,b)=>(a.hr-b.hr)+(a.last-b.last)*24);
|
||||||
var hr = time.getHours()+(time.getMinutes()/60)+(time.getSeconds()/3600);
|
var hr = time.getHours()+(time.getMinutes()/60)+(time.getSeconds()/3600);
|
||||||
if (!require('Storage').read("alarm.js")) {
|
if (!require('Storage').read("alarm.js")) {
|
||||||
console.log(/*LANG*/"No alarm app!");
|
console.log("No alarm app!");
|
||||||
require('Storage').write('alarm.json',"[]");
|
require('Storage').write('alarm.json',"[]");
|
||||||
} else {
|
} else {
|
||||||
var t = 3600000*(active[0].hr-hr);
|
var t = 3600000*(active[0].hr-hr);
|
||||||
|
|
|
@ -2,3 +2,4 @@
|
||||||
0.02: Fix JSON save format
|
0.02: Fix JSON save format
|
||||||
0.03: Add "Calculating" placeholder, update JSON save format
|
0.03: Add "Calculating" placeholder, update JSON save format
|
||||||
0.04: Fix tapping at very bottom of list, exit on inactivity
|
0.04: Fix tapping at very bottom of list, exit on inactivity
|
||||||
|
0.05: Add support for bulk importing and exporting tokens
|
||||||
|
|
|
@ -3,6 +3,15 @@
|
||||||
* GitHub: https://github.com/andrewgoz/Authentiwatch <-- Report bugs here
|
* GitHub: https://github.com/andrewgoz/Authentiwatch <-- Report bugs here
|
||||||
* Bleeding edge AppLoader: https://andrewgoz.github.io/Authentiwatch/
|
* Bleeding edge AppLoader: https://andrewgoz.github.io/Authentiwatch/
|
||||||
|
|
||||||
|
## Important!
|
||||||
|
|
||||||
|
Tokens are stored *ONLY* on the watch. Make sure you do one or more of the following:
|
||||||
|
|
||||||
|
* Make a backup copy of the "authentiwatch.json" file.
|
||||||
|
* Export all your tokens to another device or print the QR code.
|
||||||
|
|
||||||
|
Keep those copies safe and secure.
|
||||||
|
|
||||||
## Supports
|
## Supports
|
||||||
|
|
||||||
* Google Authenticator compatible 2-factor authentication
|
* Google Authenticator compatible 2-factor authentication
|
||||||
|
@ -14,8 +23,8 @@
|
||||||
* Between 6 and 10 digits
|
* Between 6 and 10 digits
|
||||||
* Phone/PC configuration web page:
|
* Phone/PC configuration web page:
|
||||||
* Add/edit/delete/arrange tokens
|
* Add/edit/delete/arrange tokens
|
||||||
* Scan QR codes
|
* Scan token and migration(import) QR codes
|
||||||
* Produce scannable QR codes
|
* Produce scannable token and migration(export) QR codes
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
@ -24,6 +33,8 @@
|
||||||
* Swipe right to exit to the app launcher.
|
* Swipe right to exit to the app launcher.
|
||||||
* Swipe left on selected counter token to advance the counter to the next value.
|
* Swipe left on selected counter token to advance the counter to the next value.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Creator
|
## Creator
|
||||||
|
|
||||||
Andrew Gregory (andrew.gregory at gmail)
|
Andrew Gregory (andrew.gregory at gmail)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const tokenentryheight = 46;
|
const tokenextraheight = 16;
|
||||||
|
var tokendigitsheight = 30;
|
||||||
// Hash functions
|
// Hash functions
|
||||||
const crypto = require("crypto");
|
const crypto = require("crypto");
|
||||||
const algos = {
|
const algos = {
|
||||||
|
@ -44,9 +45,6 @@ function b32decode(seedstr) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (bitcount > 0) {
|
|
||||||
retstr += String.fromCharCode(buf << (8 - bitcount));
|
|
||||||
}
|
|
||||||
var retbuf = new Uint8Array(retstr.length);
|
var retbuf = new Uint8Array(retstr.length);
|
||||||
for (i in retstr) {
|
for (i in retstr) {
|
||||||
retbuf[i] = retstr.charCodeAt(i);
|
retbuf[i] = retstr.charCodeAt(i);
|
||||||
|
@ -117,27 +115,31 @@ function drawToken(id, r) {
|
||||||
var y1 = r.y;
|
var y1 = r.y;
|
||||||
var x2 = r.x + r.w - 1;
|
var x2 = r.x + r.w - 1;
|
||||||
var y2 = r.y + r.h - 1;
|
var y2 = r.y + r.h - 1;
|
||||||
var adj, sz;
|
var adj, lbl, sz;
|
||||||
g.setClipRect(Math.max(x1, Bangle.appRect.x ), Math.max(y1, Bangle.appRect.y ),
|
g.setClipRect(Math.max(x1, Bangle.appRect.x ), Math.max(y1, Bangle.appRect.y ),
|
||||||
Math.min(x2, Bangle.appRect.x2), Math.min(y2, Bangle.appRect.y2));
|
Math.min(x2, Bangle.appRect.x2), Math.min(y2, Bangle.appRect.y2));
|
||||||
|
lbl = tokens[id].label.substr(0, 10);
|
||||||
if (id == state.curtoken) {
|
if (id == state.curtoken) {
|
||||||
// current token
|
// current token
|
||||||
g.setColor(g.theme.fgH);
|
g.setColor(g.theme.fgH);
|
||||||
g.setBgColor(g.theme.bgH);
|
g.setBgColor(g.theme.bgH);
|
||||||
g.setFont("Vector", 16);
|
g.setFont("Vector", tokenextraheight);
|
||||||
// center just below top line
|
// center just below top line
|
||||||
g.setFontAlign(0, -1, 0);
|
g.setFontAlign(0, -1, 0);
|
||||||
adj = y1;
|
adj = y1;
|
||||||
} else {
|
} else {
|
||||||
g.setColor(g.theme.fg);
|
g.setColor(g.theme.fg);
|
||||||
g.setBgColor(g.theme.bg);
|
g.setBgColor(g.theme.bg);
|
||||||
g.setFont("Vector", 30);
|
sz = tokendigitsheight;
|
||||||
|
do {
|
||||||
|
g.setFont("Vector", sz--);
|
||||||
|
} while (g.stringWidth(lbl) > r.w);
|
||||||
// center in box
|
// center in box
|
||||||
g.setFontAlign(0, 0, 0);
|
g.setFontAlign(0, 0, 0);
|
||||||
adj = (y1 + y2) / 2;
|
adj = (y1 + y2) / 2;
|
||||||
}
|
}
|
||||||
g.clearRect(x1, y1, x2, y2);
|
g.clearRect(x1, y1, x2, y2);
|
||||||
g.drawString(tokens[id].label.substr(0, 10), (x1 + x2) / 2, adj, false);
|
g.drawString(lbl, (x1 + x2) / 2, adj, false);
|
||||||
if (id == state.curtoken) {
|
if (id == state.curtoken) {
|
||||||
if (tokens[id].period > 0) {
|
if (tokens[id].period > 0) {
|
||||||
// timed - draw progress bar
|
// timed - draw progress bar
|
||||||
|
@ -148,14 +150,14 @@ function drawToken(id, r) {
|
||||||
// counter - draw triangle as swipe hint
|
// counter - draw triangle as swipe hint
|
||||||
let yc = (y1 + y2) / 2;
|
let yc = (y1 + y2) / 2;
|
||||||
g.fillPoly([0, yc, 10, yc - 10, 10, yc + 10, 0, yc]);
|
g.fillPoly([0, yc, 10, yc - 10, 10, yc + 10, 0, yc]);
|
||||||
adj = 10;
|
adj = 12;
|
||||||
}
|
}
|
||||||
// digits just below label
|
// digits just below label
|
||||||
sz = 30;
|
sz = tokendigitsheight;
|
||||||
do {
|
do {
|
||||||
g.setFont("Vector", sz--);
|
g.setFont("Vector", sz--);
|
||||||
} while (g.stringWidth(state.otp) > (r.w - adj));
|
} while (g.stringWidth(state.otp) > (r.w - adj));
|
||||||
g.drawString(state.otp, (x1 + adj + x2) / 2, y1 + 16, false);
|
g.drawString(state.otp, (x1 + adj + x2) / 2, y1 + tokenextraheight, false);
|
||||||
}
|
}
|
||||||
// shaded lines top and bottom
|
// shaded lines top and bottom
|
||||||
g.setColor(0.5, 0.5, 0.5);
|
g.setColor(0.5, 0.5, 0.5);
|
||||||
|
@ -196,15 +198,15 @@ function draw() {
|
||||||
}
|
}
|
||||||
if (tokens.length > 0) {
|
if (tokens.length > 0) {
|
||||||
var drewcur = false;
|
var drewcur = false;
|
||||||
var id = Math.floor(state.listy / tokenentryheight);
|
var id = Math.floor(state.listy / (tokendigitsheight + tokenextraheight));
|
||||||
var y = id * tokenentryheight + Bangle.appRect.y - state.listy;
|
var y = id * (tokendigitsheight + tokenextraheight) + Bangle.appRect.y - state.listy;
|
||||||
while (id < tokens.length && y < Bangle.appRect.y2) {
|
while (id < tokens.length && y < Bangle.appRect.y2) {
|
||||||
drawToken(id, {x:Bangle.appRect.x, y:y, w:Bangle.appRect.w, h:tokenentryheight});
|
drawToken(id, {x:Bangle.appRect.x, y:y, w:Bangle.appRect.w, h:(tokendigitsheight + tokenextraheight)});
|
||||||
if (id == state.curtoken && (tokens[id].period <= 0 || state.nextTime != 0)) {
|
if (id == state.curtoken && (tokens[id].period <= 0 || state.nextTime != 0)) {
|
||||||
drewcur = true;
|
drewcur = true;
|
||||||
}
|
}
|
||||||
id += 1;
|
id += 1;
|
||||||
y += tokenentryheight;
|
y += (tokendigitsheight + tokenextraheight);
|
||||||
}
|
}
|
||||||
if (drewcur) {
|
if (drewcur) {
|
||||||
// the current token has been drawn - schedule a redraw
|
// the current token has been drawn - schedule a redraw
|
||||||
|
@ -226,7 +228,7 @@ function draw() {
|
||||||
state.nexttime = 0;
|
state.nexttime = 0;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
g.setFont("Vector", 30);
|
g.setFont("Vector", tokendigitsheight);
|
||||||
g.setFontAlign(0, 0, 0);
|
g.setFontAlign(0, 0, 0);
|
||||||
g.drawString(notokens, Bangle.appRect.x + Bangle.appRect.w / 2, Bangle.appRect.y + Bangle.appRect.h / 2, false);
|
g.drawString(notokens, Bangle.appRect.x + Bangle.appRect.w / 2, Bangle.appRect.y + Bangle.appRect.h / 2, false);
|
||||||
}
|
}
|
||||||
|
@ -238,18 +240,18 @@ function draw() {
|
||||||
|
|
||||||
function onTouch(zone, e) {
|
function onTouch(zone, e) {
|
||||||
if (e) {
|
if (e) {
|
||||||
var id = Math.floor((state.listy + (e.y - Bangle.appRect.y)) / tokenentryheight);
|
var id = Math.floor((state.listy + (e.y - Bangle.appRect.y)) / (tokendigitsheight + tokenextraheight));
|
||||||
if (id == state.curtoken || tokens.length == 0 || id >= tokens.length) {
|
if (id == state.curtoken || tokens.length == 0 || id >= tokens.length) {
|
||||||
id = -1;
|
id = -1;
|
||||||
}
|
}
|
||||||
if (state.curtoken != id) {
|
if (state.curtoken != id) {
|
||||||
if (id != -1) {
|
if (id != -1) {
|
||||||
var y = id * tokenentryheight - state.listy;
|
var y = id * (tokendigitsheight + tokenextraheight) - state.listy;
|
||||||
if (y < 0) {
|
if (y < 0) {
|
||||||
state.listy += y;
|
state.listy += y;
|
||||||
y = 0;
|
y = 0;
|
||||||
}
|
}
|
||||||
y += tokenentryheight;
|
y += (tokendigitsheight + tokenextraheight);
|
||||||
if (y > Bangle.appRect.h) {
|
if (y > Bangle.appRect.h) {
|
||||||
state.listy += (y - Bangle.appRect.h);
|
state.listy += (y - Bangle.appRect.h);
|
||||||
}
|
}
|
||||||
|
@ -266,12 +268,15 @@ function onTouch(zone, e) {
|
||||||
function onDrag(e) {
|
function onDrag(e) {
|
||||||
if (e.x > g.getWidth() || e.y > g.getHeight()) return;
|
if (e.x > g.getWidth() || e.y > g.getHeight()) return;
|
||||||
if (e.dx == 0 && e.dy == 0) return;
|
if (e.dx == 0 && e.dy == 0) return;
|
||||||
var newy = Math.min(state.listy - e.dy, tokens.length * tokenentryheight - Bangle.appRect.h);
|
var newy = Math.min(state.listy - e.dy, tokens.length * (tokendigitsheight + tokenextraheight) - Bangle.appRect.h);
|
||||||
state.listy = Math.max(0, newy);
|
state.listy = Math.max(0, newy);
|
||||||
draw();
|
draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSwipe(e) {
|
function onSwipe(e) {
|
||||||
|
if (e == 1) {
|
||||||
|
exitApp();
|
||||||
|
}
|
||||||
if (e == -1 && state.curtoken != -1 && tokens[state.curtoken].period <= 0) {
|
if (e == -1 && state.curtoken != -1 && tokens[state.curtoken].period <= 0) {
|
||||||
tokens[state.curtoken].period--;
|
tokens[state.curtoken].period--;
|
||||||
let newsettings={tokens:tokens,misc:settings.misc};
|
let newsettings={tokens:tokens,misc:settings.misc};
|
||||||
|
@ -296,7 +301,7 @@ function bangle1Btn(e) {
|
||||||
state.curtoken = Math.max(state.curtoken, 0);
|
state.curtoken = Math.max(state.curtoken, 0);
|
||||||
state.curtoken = Math.min(state.curtoken, tokens.length - 1);
|
state.curtoken = Math.min(state.curtoken, tokens.length - 1);
|
||||||
var fakee = {};
|
var fakee = {};
|
||||||
fakee.y = state.curtoken * tokenentryheight - state.listy + Bangle.appRect.y;
|
fakee.y = state.curtoken * (tokendigitsheight + tokenextraheight) - state.listy + Bangle.appRect.y;
|
||||||
state.curtoken = -1;
|
state.curtoken = -1;
|
||||||
state.nextTime = 0;
|
state.nextTime = 0;
|
||||||
onTouch(0, fakee);
|
onTouch(0, fakee);
|
||||||
|
|
|
@ -7,7 +7,10 @@
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
body{font-family:sans-serif}
|
body{font-family:sans-serif}
|
||||||
body div{display:none}
|
body div{display:none}
|
||||||
body.select div#tokens,body.editing div#edit,body.scanning div#scan,body.showqr div#tokenqr{display:block}
|
body.select tr>:first-child,body.export tr>:nth-child(3),body.export tr>:nth-child(4){display:none}
|
||||||
|
body.select div.select,body.export div.export{display:block}
|
||||||
|
body.select div.export,body.export div.select{display:none}
|
||||||
|
body.select div#tokens,body.editing div#edit,body.scanning div#scan,body.showqr div#showqr,body.export div#tokens{display:block}
|
||||||
#tokens th,#tokens td{padding:5px}
|
#tokens th,#tokens td{padding:5px}
|
||||||
#tokens tr:nth-child(odd){background-color:#ccc}
|
#tokens tr:nth-child(odd){background-color:#ccc}
|
||||||
#tokens tr:nth-child(even){background-color:#eee}
|
#tokens tr:nth-child(even){background-color:#eee}
|
||||||
|
@ -33,6 +36,12 @@ form.totp tr.hotp,form.hotp tr.totp{display:none}
|
||||||
/* Start of all TOTP URLs */
|
/* Start of all TOTP URLs */
|
||||||
const otpAuthUrl = 'otpauth://';
|
const otpAuthUrl = 'otpauth://';
|
||||||
|
|
||||||
|
/* Start of all OTP migration URLs */
|
||||||
|
const otpMigrUrl = 'otpauth-migration://offline?data=';
|
||||||
|
|
||||||
|
/* Hash algorithms */
|
||||||
|
const otpAlgos = ['SHA1','SHA256','SHA512'];
|
||||||
|
|
||||||
const tokentypes = ['TOTP (Timed)', 'HOTP (Counter)'];
|
const tokentypes = ['TOTP (Timed)', 'HOTP (Counter)'];
|
||||||
|
|
||||||
/* Settings */
|
/* Settings */
|
||||||
|
@ -45,6 +54,8 @@ var tokens = settings.tokens;
|
||||||
*/
|
*/
|
||||||
function base32clean(val, nows) {
|
function base32clean(val, nows) {
|
||||||
var ret = val.replaceAll(/\s+/g, ' ');
|
var ret = val.replaceAll(/\s+/g, ' ');
|
||||||
|
ret = ret.replaceAll(/0/g, 'O');
|
||||||
|
ret = ret.replaceAll(/1/g, 'I');
|
||||||
ret = ret.replaceAll(/[^A-Za-z2-7 ]/g, '');
|
ret = ret.replaceAll(/[^A-Za-z2-7 ]/g, '');
|
||||||
if (nows) {
|
if (nows) {
|
||||||
ret = ret.replaceAll(/\s+/g, '');
|
ret = ret.replaceAll(/\s+/g, '');
|
||||||
|
@ -52,6 +63,48 @@ function base32clean(val, nows) {
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function b32encode(str) {
|
||||||
|
let buf = 0, bitcount = 0, ret = '';
|
||||||
|
while (str.length > 0) {
|
||||||
|
buf <<= 8;
|
||||||
|
buf |= str.charCodeAt(0);
|
||||||
|
bitcount += 8;
|
||||||
|
str = str.substr(1);
|
||||||
|
while (bitcount >= 5) {
|
||||||
|
ret += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'[(buf >> (bitcount - 5)) & 31];
|
||||||
|
bitcount -= 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function b32decode(seedstr) {
|
||||||
|
// RFC4648
|
||||||
|
var i, buf = 0, bitcount = 0, ret = '';
|
||||||
|
for (i in seedstr) {
|
||||||
|
var c = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'.indexOf(seedstr.charAt(i).toUpperCase(), 0);
|
||||||
|
if (c != -1) {
|
||||||
|
buf <<= 5;
|
||||||
|
buf |= c;
|
||||||
|
bitcount += 5;
|
||||||
|
if (bitcount >= 8) {
|
||||||
|
ret += String.fromCharCode(buf >> (bitcount - 8));
|
||||||
|
buf &= (0xFF >> (16 - bitcount));
|
||||||
|
bitcount -= 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeLabel(token) {
|
||||||
|
let lbl = token['label'];
|
||||||
|
if (lbl == '') {
|
||||||
|
lbl = (token['issuer'] == '') ? token['account'] : token['issuer'] + ' (' + token['account'] + ')';
|
||||||
|
}
|
||||||
|
token['label'] = lbl.substr(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
/* Save changes to a token to the global tokens[] array.
|
/* Save changes to a token to the global tokens[] array.
|
||||||
* id is the index into the global tokens[].
|
* id is the index into the global tokens[].
|
||||||
* forget is a flag indicating if the token should be forgotten.
|
* forget is a flag indicating if the token should be forgotten.
|
||||||
|
@ -84,9 +137,16 @@ function saveEdit(id, forget) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showQr(url) {
|
||||||
|
tokenqr.clear();
|
||||||
|
tokenqr.makeCode(url);
|
||||||
|
qrPreviousClass = document.body.className;
|
||||||
|
document.body.className = 'showqr';
|
||||||
|
}
|
||||||
|
|
||||||
/* Generate and display a QR-code representing the current token.
|
/* Generate and display a QR-code representing the current token.
|
||||||
*/
|
*/
|
||||||
function showQrCode() {
|
function showTokenQr() {
|
||||||
var fe = document.forms['edittoken'].elements;
|
var fe = document.forms['edittoken'].elements;
|
||||||
var url = new String(otpAuthUrl);
|
var url = new String(otpAuthUrl);
|
||||||
switch (fe['type'].value) {
|
switch (fe['type'].value) {
|
||||||
|
@ -122,9 +182,7 @@ function showQrCode() {
|
||||||
if (fe['algorithm'].value != 'SHA1') {
|
if (fe['algorithm'].value != 'SHA1') {
|
||||||
url += '&algorithm=' + fe['algorithm'].value;
|
url += '&algorithm=' + fe['algorithm'].value;
|
||||||
}
|
}
|
||||||
tokenqr.clear();
|
showQr(url);
|
||||||
tokenqr.makeCode(url);
|
|
||||||
document.body.className = 'showqr';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTypeChanged() {
|
function onTypeChanged() {
|
||||||
|
@ -138,6 +196,7 @@ function onTypeChanged() {
|
||||||
* id is the index into the global tokens[].
|
* id is the index into the global tokens[].
|
||||||
*/
|
*/
|
||||||
function editToken(id) {
|
function editToken(id) {
|
||||||
|
if (document.body.className == 'export') return;
|
||||||
var p;
|
var p;
|
||||||
const selectMarkup = function(name, ary, cur, onchg) {
|
const selectMarkup = function(name, ary, cur, onchg) {
|
||||||
var ret = '<select name="' + name + '"' + ((typeof onchg == 'string') ? ' onchange="' + onchg + '"' : '') + '>';
|
var ret = '<select name="' + name + '"' + ((typeof onchg == 'string') ? ' onchange="' + onchg + '"' : '') + '>';
|
||||||
|
@ -163,7 +222,7 @@ function editToken(id) {
|
||||||
markup += selectMarkup('digits', ['6','7','8','9','10'], tokens[id].digits);
|
markup += selectMarkup('digits', ['6','7','8','9','10'], tokens[id].digits);
|
||||||
markup += '</td></tr>';
|
markup += '</td></tr>';
|
||||||
markup += '<tr><td>Hash:</td><td>';
|
markup += '<tr><td>Hash:</td><td>';
|
||||||
markup += selectMarkup('algorithm', ['SHA1','SHA256','SHA512'], tokens[id].algorithm);
|
markup += selectMarkup('algorithm', otpAlgos, tokens[id].algorithm);
|
||||||
markup += '</td></tr>';
|
markup += '</td></tr>';
|
||||||
markup += '</tbody><tr><td id="advbtn" colspan="2">';
|
markup += '</tbody><tr><td id="advbtn" colspan="2">';
|
||||||
markup += '<button type="button" onclick="document.getElementById(\'edittoken\').classList.toggle(\'showadv\')">Advanced</button>';
|
markup += '<button type="button" onclick="document.getElementById(\'edittoken\').classList.toggle(\'showadv\')">Advanced</button>';
|
||||||
|
@ -171,9 +230,9 @@ function editToken(id) {
|
||||||
markup += '<button type="button" onclick="updateTokens()">Cancel Edit</button>';
|
markup += '<button type="button" onclick="updateTokens()">Cancel Edit</button>';
|
||||||
markup += '<button type="button" onclick="saveEdit(' + id + ', false)">Save Changes</button>';
|
markup += '<button type="button" onclick="saveEdit(' + id + ', false)">Save Changes</button>';
|
||||||
if (tokens[id].isnew) {
|
if (tokens[id].isnew) {
|
||||||
markup += '<button type="button" onclick="startScan()">Scan QR Code</button>';
|
markup += '<button type="button" onclick="startScan(handleTokenQr,cancelTokenQr)">Scan QR</button>';
|
||||||
} else {
|
} else {
|
||||||
markup += '<button type="button" onclick="showQrCode()">Show QR Code</button>';
|
markup += '<button type="button" onclick="showTokenQr()">Show QR</button>';
|
||||||
markup += '<button type="button" onclick="saveEdit(' + id + ', true)">Forget Token</button>';
|
markup += '<button type="button" onclick="saveEdit(' + id + ', true)">Forget Token</button>';
|
||||||
}
|
}
|
||||||
document.getElementById('edit').innerHTML = markup;
|
document.getElementById('edit').innerHTML = markup;
|
||||||
|
@ -188,6 +247,46 @@ function addToken() {
|
||||||
editToken(tokens.length - 1);
|
editToken(tokens.length - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Convert a number to a proto3 varint.
|
||||||
|
*/
|
||||||
|
function int2proto3varint(val) {
|
||||||
|
var ret = '';
|
||||||
|
do {
|
||||||
|
let c = val & 0x7F;
|
||||||
|
val >>>= 7;
|
||||||
|
if (val > 0) {
|
||||||
|
c |= 0x80;
|
||||||
|
}
|
||||||
|
ret += String.fromCharCode(c);
|
||||||
|
} while (val > 0);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Convert a string to a proto3 field.
|
||||||
|
*/
|
||||||
|
function str2proto3(field_number, str) {
|
||||||
|
return int2proto3varint((field_number << 3) + 2) + int2proto3varint(str.length) + str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Convert a number to a proto3 field.
|
||||||
|
*/
|
||||||
|
function int2proto3(field_number, val) {
|
||||||
|
return int2proto3varint(field_number << 3) + int2proto3varint(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Convert the specified token to its proto3 representation.
|
||||||
|
*/
|
||||||
|
function token2proto3(id) {
|
||||||
|
var secret = str2proto3(1, b32decode(tokens[id].secret));
|
||||||
|
var name = str2proto3(2, (tokens[id].account == '') ? tokens[id].label : tokens[id].account);
|
||||||
|
var issuer = (tokens[id].issuer == '') ? '' : str2proto3(3, tokens[id].issuer);
|
||||||
|
var algorithm = int2proto3(4, (tokens[id].algorithm == 'SHA512') ? 3 : ((tokens[id].algorithm == 'SHA256') ? 2 : 1));
|
||||||
|
var digits = int2proto3(5, (tokens[id].digits == 8) ? 2 : 1);
|
||||||
|
var type = int2proto3(6, (tokens[id].period <= 0) ? 1 : 2);
|
||||||
|
var counter = (tokens[id].period <= 0) ? int2proto3(7, -tokens[id].period) : '';
|
||||||
|
return str2proto3(1, secret + name + issuer + algorithm + digits + type + counter);
|
||||||
|
}
|
||||||
|
|
||||||
/* Move the specified token up or down in the global tokens[].
|
/* Move the specified token up or down in the global tokens[].
|
||||||
* id is the index in the global tokens[] of the token to move.
|
* id is the index in the global tokens[] of the token to move.
|
||||||
* dir is the direction to move: -1=up, 1=down.
|
* dir is the direction to move: -1=up, 1=down.
|
||||||
|
@ -200,10 +299,15 @@ function moveToken(id, dir) {
|
||||||
/* Update the display listing all the tokens.
|
/* Update the display listing all the tokens.
|
||||||
*/
|
*/
|
||||||
function updateTokens() {
|
function updateTokens() {
|
||||||
|
const tokenSelect = function(id) {
|
||||||
|
return '<input name="exp_' + id + '" type="checkbox" onclick="exportTokens(false, \'' + id + '\')">';
|
||||||
|
};
|
||||||
const tokenButton = function(fn, id, label, dir) {
|
const tokenButton = function(fn, id, label, dir) {
|
||||||
return '<button type="button" onclick="' + fn + '(' + id + (dir ? ',' + dir : '') + ')">' + label + '</button>';
|
return '<button type="button" onclick="' + fn + '(' + id + (dir ? ',' + dir : '') + ')">' + label + '</button>';
|
||||||
};
|
};
|
||||||
var markup = '<table><tr><th>Token</th><th colspan="2">Order</th></tr>';
|
var markup = '<table><tr><th>';
|
||||||
|
markup += tokenSelect('all');
|
||||||
|
markup += '</th><th>Token</th><th colspan="2">Order</th></tr>';
|
||||||
/* any tokens marked new are cancelled new additions and must be removed */
|
/* any tokens marked new are cancelled new additions and must be removed */
|
||||||
for (let i = 0; i < tokens.length; i++) {
|
for (let i = 0; i < tokens.length; i++) {
|
||||||
if (tokens[i].isnew) {
|
if (tokens[i].isnew) {
|
||||||
|
@ -212,6 +316,8 @@ function updateTokens() {
|
||||||
}
|
}
|
||||||
for (let i = 0; i < tokens.length; i++) {
|
for (let i = 0; i < tokens.length; i++) {
|
||||||
markup += '<tr><td>';
|
markup += '<tr><td>';
|
||||||
|
markup += tokenSelect(i);
|
||||||
|
markup += '</td><td>';
|
||||||
markup += tokenButton('editToken', i, tokens[i].label);
|
markup += tokenButton('editToken', i, tokens[i].label);
|
||||||
markup += '</td><td>';
|
markup += '</td><td>';
|
||||||
if (i < (tokens.length - 1)) {
|
if (i < (tokens.length - 1)) {
|
||||||
|
@ -224,14 +330,20 @@ function updateTokens() {
|
||||||
markup += '</td></tr>';
|
markup += '</td></tr>';
|
||||||
}
|
}
|
||||||
markup += '</table>';
|
markup += '</table>';
|
||||||
|
markup += '<div class="select">';
|
||||||
markup += '<button type="button" onclick="addToken()">Add Token</button>';
|
markup += '<button type="button" onclick="addToken()">Add Token</button>';
|
||||||
markup += '<button type="button" onclick="saveTokens()">Save to watch</button>';
|
markup += '<button type="button" onclick="saveTokens()">Save to watch</button>';
|
||||||
|
markup += '<button type="button" onclick="startScan(handleImportQr,cancelImportQr)">Import</button>';
|
||||||
|
markup += '<button type="button" onclick="document.body.className=\'export\'">Export</button>';
|
||||||
|
markup += '</div><div class="export">';
|
||||||
|
markup += '<button type="button" onclick="document.body.className=\'select\'">Cancel</button>';
|
||||||
|
markup += '<button type="button" onclick="exportTokens(true, null)">Show QR</button>';
|
||||||
|
markup += '</div>';
|
||||||
document.getElementById('tokens').innerHTML = markup;
|
document.getElementById('tokens').innerHTML = markup;
|
||||||
document.body.className = 'select';
|
document.body.className = 'select';
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Original QR-code reader: https://www.sitepoint.com/create-qr-code-reader-mobile-website/ */
|
function handleTokenQr(res) {
|
||||||
qrcode.callback = res => {
|
|
||||||
if (res) {
|
if (res) {
|
||||||
if (res.startsWith(otpAuthUrl)) {
|
if (res.startsWith(otpAuthUrl)) {
|
||||||
res = decodeURIComponent(res);
|
res = decodeURIComponent(res);
|
||||||
|
@ -243,7 +355,8 @@ qrcode.callback = res => {
|
||||||
'counter':'0',
|
'counter':'0',
|
||||||
'period':'30',
|
'period':'30',
|
||||||
'secret':'',
|
'secret':'',
|
||||||
'issuer':''
|
'issuer':'',
|
||||||
|
'label':''
|
||||||
};
|
};
|
||||||
var otpok = true;
|
var otpok = true;
|
||||||
for (let pi in params) {
|
for (let pi in params) {
|
||||||
|
@ -261,8 +374,7 @@ qrcode.callback = res => {
|
||||||
if (otpok) {
|
if (otpok) {
|
||||||
scanning = false;
|
scanning = false;
|
||||||
editToken(parseInt(document.forms['edittoken'].elements['tokenid'].value));
|
editToken(parseInt(document.forms['edittoken'].elements['tokenid'].value));
|
||||||
t['label'] = (t['issuer'] == '') ? t['account'] : t['issuer'] + ' (' + t['account'] + ')';
|
makeLabel(t);
|
||||||
t['label'] = t['label'].substr(0, 10);
|
|
||||||
var fe = document.forms['edittoken'].elements;
|
var fe = document.forms['edittoken'].elements;
|
||||||
if (res.startsWith(otpAuthUrl + 'hotp/')) {
|
if (res.startsWith(otpAuthUrl + 'hotp/')) {
|
||||||
t['period'] = '30';
|
t['period'] = '30';
|
||||||
|
@ -283,8 +395,94 @@ qrcode.callback = res => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
function cancelTokenQr() {
|
||||||
|
scanning = false;
|
||||||
|
editToken(parseInt(document.forms['edittoken'].elements['tokenid'].value));
|
||||||
|
}
|
||||||
|
class proto3decoder {
|
||||||
|
constructor(str) {
|
||||||
|
this.buf = [];
|
||||||
|
for (let i in str) {
|
||||||
|
this.buf = this.buf.concat(str.charCodeAt(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getVarint() {
|
||||||
|
let c, ret = 0
|
||||||
|
do {
|
||||||
|
c = this.buf.shift();
|
||||||
|
ret = (ret << 7) | (c & 0x7F);
|
||||||
|
} while ((c & 0x80) != 0);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
getString(length) {
|
||||||
|
let ret = '';
|
||||||
|
for (let i = 0; i < length; ++i) {
|
||||||
|
ret += String.fromCharCode(this.buf.shift());
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
parse() {
|
||||||
|
let ret = null;
|
||||||
|
if (this.buf.length > 0) {
|
||||||
|
let field_data = null;
|
||||||
|
let field_type = this.getVarint();
|
||||||
|
let field_number = field_type >>> 3;
|
||||||
|
let wire_type = field_type & 7;
|
||||||
|
switch (wire_type) {
|
||||||
|
case 0: field_data = this.getVarint(); break;
|
||||||
|
case 2: field_data = this.getString(this.getVarint()); break;
|
||||||
|
}
|
||||||
|
ret = {number:field_number,data:field_data};
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function handleImportQr(res) {
|
||||||
|
if (res) {
|
||||||
|
if (res.startsWith(otpMigrUrl)) {
|
||||||
|
scanning = false;
|
||||||
|
let data = new proto3decoder(atob(decodeURIComponent(res.substr(otpMigrUrl.length))));
|
||||||
|
while (data.buf.length > 0) {
|
||||||
|
let field = data.parse();
|
||||||
|
if (field?.number == 1) {
|
||||||
|
let newtoken = {'algorithm':'SHA1','digits':6,'period':30,'issuer':'','account':'','secret':'','label':''};
|
||||||
|
let p3token = new proto3decoder(field.data);
|
||||||
|
while (p3token.buf.length > 0) {
|
||||||
|
let buf = p3token.parse();
|
||||||
|
switch (buf?.number) {
|
||||||
|
case 1: newtoken.secret = b32encode(buf.data); break;
|
||||||
|
case 2: newtoken.account = buf.data; break;
|
||||||
|
case 3: newtoken.issuer = buf.data; break;
|
||||||
|
case 4: newtoken.algorithm = otpAlgos[buf.data - 1]; break;
|
||||||
|
case 5: newtoken.digits = (['6','8'])[buf.data - 1]; break;
|
||||||
|
case 7: newtoken.period = -buf.data; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
makeLabel(newtoken);
|
||||||
|
tokens[tokens.length] = newtoken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateTokens();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function cancelImportQr() {
|
||||||
|
scanning = false;
|
||||||
|
document.body.className = 'select';
|
||||||
|
}
|
||||||
|
/* Original QR-code reader: https://www.sitepoint.com/create-qr-code-reader-mobile-website/ */
|
||||||
|
qrcode.callback = res => {
|
||||||
|
if (res) {
|
||||||
|
scanCallback(res);
|
||||||
|
if (scanning) {
|
||||||
|
scanBack();
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
function startScan() {
|
function startScan(handler,cancel) {
|
||||||
|
scanCallback = handler;
|
||||||
|
scanBack = cancel;
|
||||||
document.body.className = 'scanning';
|
document.body.className = 'scanning';
|
||||||
navigator.mediaDevices
|
navigator.mediaDevices
|
||||||
.getUserMedia({video:{facingMode:'environment'}})
|
.getUserMedia({video:{facingMode:'environment'}})
|
||||||
|
@ -339,36 +537,93 @@ function saveTokens() {
|
||||||
Util.hideModal();
|
Util.hideModal();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
/* Handle token export.
|
||||||
|
* showqr is true if the QR code should be shown, if false the checkboxes need updating
|
||||||
|
* id is the name of the clicked checkbox, or null if the export button was pressed
|
||||||
|
*/
|
||||||
|
function exportTokens(showqr, id) {
|
||||||
|
let allchecked = true, allclear = true;
|
||||||
|
let cball;
|
||||||
|
let exp = '';
|
||||||
|
for (let cb of document.querySelectorAll('input[type=checkbox]')) {
|
||||||
|
let cbid = cb.name.substring(4);
|
||||||
|
if (cbid == 'all') {
|
||||||
|
cball = cb;
|
||||||
|
} else {
|
||||||
|
if (id == 'all') {
|
||||||
|
cb.checked = cball.checked;
|
||||||
|
} else {
|
||||||
|
if (cb.checked) {
|
||||||
|
if (showqr) {
|
||||||
|
exp += token2proto3(parseInt(cbid));
|
||||||
|
}
|
||||||
|
allclear = false;
|
||||||
|
} else {
|
||||||
|
allchecked = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (id != 'all') {
|
||||||
|
if (allclear) {
|
||||||
|
cball.indeterminate = false;
|
||||||
|
cball.checked = false;
|
||||||
|
} else if (allchecked) {
|
||||||
|
cball.indeterminate = false;
|
||||||
|
cball.checked = true;
|
||||||
|
} else {
|
||||||
|
cball.indeterminate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (showqr) {
|
||||||
|
if (exp != '') {
|
||||||
|
/* add version, batch_size, batch_index, but no batch_id */
|
||||||
|
exp += int2proto3(2, 1) + int2proto3(3, 1) + int2proto3(4, 0);
|
||||||
|
let url = otpMigrUrl + encodeURIComponent(btoa(exp));
|
||||||
|
showQr(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
function onInit() {
|
function onInit() {
|
||||||
loadTokens();
|
loadTokens();
|
||||||
updateTokens();
|
updateTokens();
|
||||||
}
|
}
|
||||||
|
function qrBack() {
|
||||||
|
document.body.className = qrPreviousClass;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body class="select">
|
<body class="select">
|
||||||
<h1>Authentiwatch</h1>
|
<h1>Authentiwatch</h1>
|
||||||
|
|
||||||
<div id="tokens">
|
<div id="tokens">
|
||||||
<p>No watch comms.</p>
|
<p>No watch comms.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="scan">
|
<div id="scan">
|
||||||
<table>
|
<table>
|
||||||
<tr><td><canvas id="qr-canvas"></canvas></td></tr>
|
<tr><td><canvas id="qr-canvas"></canvas></td></tr>
|
||||||
<tr><td><button type="button" onclick="editToken(parseInt(document.forms['edittoken'].elements['tokenid'].value))">Cancel</button></td></tr>
|
<tr><td><button type="button" onclick="scanBack()">Cancel</button></td></tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="edit">
|
<div id="edit">
|
||||||
</div>
|
</div>
|
||||||
<div id="tokenqr">
|
|
||||||
|
<div id="showqr">
|
||||||
<table><tr><td id="qrcode"></td></tr><tr><td>
|
<table><tr><td id="qrcode"></td></tr><tr><td>
|
||||||
<button type="button" onclick="document.body.className='editing'">Back</button>
|
<button type="button" onclick="qrBack()">Back</button>
|
||||||
</td></tr></table>
|
</td></tr></table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
const video=document.createElement('video');
|
const video=document.createElement('video');
|
||||||
const canvasElement=document.getElementById('qr-canvas');
|
const canvasElement=document.getElementById('qr-canvas');
|
||||||
const canvas=canvasElement.getContext('2d');
|
const canvas=canvasElement.getContext('2d');
|
||||||
let scanning=false;
|
let scanning=false;
|
||||||
const tokenqr=new QRCode(document.getElementById('qrcode'), '');
|
const tokenqr=new QRCode(document.getElementById('qrcode'), {width:354,height:354});
|
||||||
</script>
|
</script>
|
||||||
<script src="../../core/lib/interface.js"></script>
|
<script src="../../core/lib/interface.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
"shortName": "AuthWatch",
|
"shortName": "AuthWatch",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"screenshots": [{"url":"screenshot.png"}],
|
"screenshots": [{"url":"screenshot.png"}],
|
||||||
"version": "0.04",
|
"version": "0.05",
|
||||||
"description": "Google Authenticator compatible tool.",
|
"description": "Google Authenticator compatible tool.",
|
||||||
"tags": "tool",
|
"tags": "tool",
|
||||||
"interface": "interface.html",
|
"interface": "interface.html",
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
0.01: Display pressure as number and hand
|
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("mEwwhC/AH4AVmczmALI7oWJgYXBmYLHhvd6AuKGBHdAAYXLDAwXRJIvd73u9oXSLoPuAAJhHOwYYGIYIXDGAwWGMIYvMC5QwBC4ZeMC4x3KL44XEU6KQEC5gAMCqoXZAH4AchAXWxAXWwBGWC62IC6sILywXXxAXUhWqzAXTCwIABOyYXD0AXSCwQABC/4XaO68JC6wYCCygA/AH4AGA"))
|
|
@ -0,0 +1,120 @@
|
||||||
|
const center = {
|
||||||
|
x: g.getWidth()/2,
|
||||||
|
y: g.getHeight()/2,
|
||||||
|
};
|
||||||
|
|
||||||
|
const MIN = 940;
|
||||||
|
const MAX = 1090;
|
||||||
|
const NUMBER_OF_VALUES = MAX - MIN;
|
||||||
|
const SCALE_TICK_STEP = 5;
|
||||||
|
const SCALE_VALUES_STEP = 25;
|
||||||
|
const NUMBER_OF_LABELS = NUMBER_OF_VALUES / SCALE_VALUES_STEP;
|
||||||
|
const NUMBER_OF_TICKS = NUMBER_OF_VALUES / SCALE_TICK_STEP;
|
||||||
|
const ZERO_OFFSET = (Math.PI / 4) * 3;
|
||||||
|
const SCALE_SPAN = (Math.PI / 2) * 3;
|
||||||
|
const TICK_LENGTH = 10;
|
||||||
|
const HAND_LENGTH = 45;
|
||||||
|
const HAND_WIDTH = 5;
|
||||||
|
|
||||||
|
function generatePoly(radius, width, angle){
|
||||||
|
const x = center.x + Math.cos(angle) * radius;
|
||||||
|
const y = center.y + Math.sin(angle) * radius;
|
||||||
|
const d = {
|
||||||
|
x: width/2 * Math.cos(angle + Math.PI/2),
|
||||||
|
y: width/2 * Math.sin(angle + Math.PI/2),
|
||||||
|
};
|
||||||
|
|
||||||
|
const poly = [center.x - d.x, center.y - d.y, center.x + d.x, center.y + d.y, x + d.x, y + d.y, x - d.x, y - d.y];
|
||||||
|
|
||||||
|
return poly;
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawHand(value){
|
||||||
|
g.setColor(256, 0, 0);
|
||||||
|
|
||||||
|
g.setFontAlign(0,0);
|
||||||
|
g.setFont("Vector",15);
|
||||||
|
g.drawString(value, center.x, center.y * 2 - 15, true);
|
||||||
|
|
||||||
|
const angle = SCALE_SPAN / NUMBER_OF_VALUES * (value - MIN) + ZERO_OFFSET;
|
||||||
|
g.fillPoly(generatePoly(HAND_LENGTH, HAND_WIDTH, angle), true);
|
||||||
|
g.fillCircle(center.x ,center.y, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function drawTicks(){
|
||||||
|
g.setColor(1,1,1);
|
||||||
|
for(let i= 0; i <= NUMBER_OF_TICKS; i++){
|
||||||
|
const angle = (i * (SCALE_SPAN/NUMBER_OF_TICKS)) + ZERO_OFFSET;
|
||||||
|
|
||||||
|
const tickWidth = i%5==0 ? 5 : 2;
|
||||||
|
g.fillPoly(generatePoly(center.x, tickWidth, angle), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
g.setColor(0,0,0);
|
||||||
|
g.fillCircle(center.x,center.y,center.x - TICK_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function drawScaleLabels(){
|
||||||
|
g.setColor(1,1,1);
|
||||||
|
g.setFont("Vector",12);
|
||||||
|
|
||||||
|
let label = MIN;
|
||||||
|
for (let i=0;i <= NUMBER_OF_LABELS; i++){
|
||||||
|
const angle = (i * (SCALE_SPAN/NUMBER_OF_LABELS)) + ZERO_OFFSET;
|
||||||
|
const labelDimensions = g.stringMetrics(label);
|
||||||
|
|
||||||
|
const LABEL_PADDING = 5;
|
||||||
|
const radius = center.x - TICK_LENGTH - LABEL_PADDING;
|
||||||
|
const x = center.x + Math.cos(angle) * radius;
|
||||||
|
const y = center.y + Math.sin(angle) * radius;
|
||||||
|
|
||||||
|
const visualX = x > center.x ? x - labelDimensions.width : x + labelDimensions.width > center.x ? x - (labelDimensions.width / 2) : x;
|
||||||
|
const visualY = y >= center.y - labelDimensions.height / 2 ? y - labelDimensions.height / 2 : y;
|
||||||
|
|
||||||
|
g.drawString(label, visualX, visualY);
|
||||||
|
|
||||||
|
label += SCALE_VALUES_STEP;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawIcons() {
|
||||||
|
const sunIcon = {
|
||||||
|
width : 24, height : 24, bpp : 4,
|
||||||
|
transparent : 0,
|
||||||
|
buffer : require("heatshrink").decompress(atob("AAkP+ALeA40PAYf/BYv/CYYLBBwIICCQ4ACHI4ICEIgkEAg48GDApcFAoYPBBY5NDBZIjLHZpTLNZiDKTZSzMZZT7iA="))
|
||||||
|
};
|
||||||
|
g.drawImage(sunIcon, center.x + 15, center.y - 12);
|
||||||
|
|
||||||
|
const sunRainIcon = {
|
||||||
|
width : 24, height : 24, bpp : 4,
|
||||||
|
transparent : 0,
|
||||||
|
buffer : require("heatshrink").decompress(atob("AB/wBbEPBAoGEDI/wh4jJBQIMJEgUP///IpAJCBgf/+ALCAQRJFAoIHECgI7FIYwSEHAoGBEQwsEDIJdHCYYLLFwwTEQQwGFQQQACYpYpLf0AAEA"))
|
||||||
|
};
|
||||||
|
g.drawImage(sunRainIcon, center.x - 12, 30);
|
||||||
|
|
||||||
|
const rainIcon = {
|
||||||
|
width : 24, height : 24, bpp : 4,
|
||||||
|
transparent : 0,
|
||||||
|
buffer : require("heatshrink").decompress(atob("ADnwBRP/AIQAGh4ZKA4YLLh//EwoTFh4GCCIIfGDAQ5DIQ5bIBbQvII4gAGWLwzBOoarLCw4RKLBAAgA"))
|
||||||
|
};
|
||||||
|
g.drawImage(rainIcon, center.x - 44, center.y - 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
g.setBgColor(0,0,0);
|
||||||
|
g.clear();
|
||||||
|
|
||||||
|
drawTicks();
|
||||||
|
drawScaleLabels();
|
||||||
|
drawIcons();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Bangle.getPressure().then(data => {
|
||||||
|
drawHand(Math.round(data.pressure));
|
||||||
|
});
|
||||||
|
} catch(e) {
|
||||||
|
print(e.message);
|
||||||
|
print("barometer not supporter, show a demo value");
|
||||||
|
drawHand(MIN);
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 887 B |
|
@ -0,0 +1,15 @@
|
||||||
|
{ "id": "barometer",
|
||||||
|
"name": "Barometer",
|
||||||
|
"shortName":"Barometer",
|
||||||
|
"version":"0.01",
|
||||||
|
"description": "A simple barometer that displays the current air pressure",
|
||||||
|
"icon": "barometer.png",
|
||||||
|
"tags": "tool,outdoors",
|
||||||
|
"allow_emulator":true,
|
||||||
|
"screenshots" : [ { "url": "screenshot.png" } ],
|
||||||
|
"supports" : ["BANGLEJS2"],
|
||||||
|
"storage": [
|
||||||
|
{"name":"barometer.app.js","url":"app.js"},
|
||||||
|
{"name":"barometer.img","url":"app-icon.js","evaluate":true}
|
||||||
|
]
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 3.1 KiB |
|
@ -14,3 +14,6 @@
|
||||||
Always emit BTHRM event
|
Always emit BTHRM event
|
||||||
Cleanup promises code and allow to configure custom additional waiting times to work around bugs
|
Cleanup promises code and allow to configure custom additional waiting times to work around bugs
|
||||||
Disconnect cleanly on exit
|
Disconnect cleanly on exit
|
||||||
|
0.06: Fix bug if no request waiting time is set
|
||||||
|
Fix bug if no connection data was cached
|
||||||
|
Fix error during disconnect
|
||||||
|
|
|
@ -365,18 +365,18 @@
|
||||||
|
|
||||||
if (settings.gracePeriodRequest){
|
if (settings.gracePeriodRequest){
|
||||||
log("Add " + settings.gracePeriodRequest + "ms grace period after request");
|
log("Add " + settings.gracePeriodRequest + "ms grace period after request");
|
||||||
|
|
||||||
promise = promise.then((d)=>{
|
|
||||||
log("Got device: ", d);
|
|
||||||
d.on('gattserverdisconnected', onDisconnect);
|
|
||||||
device = d;
|
|
||||||
});
|
|
||||||
|
|
||||||
promise = promise.then(()=>{
|
|
||||||
log("Wait after request");
|
|
||||||
return waitingPromise(settings.gracePeriodRequest);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
promise = promise.then((d)=>{
|
||||||
|
log("Got device: ", d);
|
||||||
|
d.on('gattserverdisconnected', onDisconnect);
|
||||||
|
device = d;
|
||||||
|
});
|
||||||
|
|
||||||
|
promise = promise.then(()=>{
|
||||||
|
log("Wait after request");
|
||||||
|
return waitingPromise(settings.gracePeriodRequest);
|
||||||
|
});
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
promise = Promise.resolve();
|
promise = Promise.resolve();
|
||||||
|
@ -426,14 +426,14 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
promise = promise.then(()=>{
|
promise = promise.then(()=>{
|
||||||
var getCharacteristicsPromise = Promise.resolve();
|
var characteristicsPromise = Promise.resolve();
|
||||||
if (characteristics.length == 0){
|
if (characteristics.length == 0){
|
||||||
getCharacteristicsPromise = getCharacteristicsPromise.then(()=>{
|
characteristicsPromise = characteristicsPromise.then(()=>{
|
||||||
log("Getting services");
|
log("Getting services");
|
||||||
return gatt.getPrimaryServices();
|
return gatt.getPrimaryServices();
|
||||||
});
|
});
|
||||||
|
|
||||||
getCharacteristicsPromise = getCharacteristicsPromise().then((services)=>{
|
characteristicsPromise = characteristicsPromise.then((services)=>{
|
||||||
log("Got services:", services);
|
log("Got services:", services);
|
||||||
var result = Promise.resolve();
|
var result = Promise.resolve();
|
||||||
for (var service of services){
|
for (var service of services){
|
||||||
|
@ -453,11 +453,11 @@
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
for (var characteristic of characteristics){
|
for (var characteristic of characteristics){
|
||||||
getCharacteristicsPromise = attachCharacteristicPromise(getCharacteristicsPromise, characteristic, true);
|
characteristicsPromise = attachCharacteristicPromise(characteristicsPromise, characteristic, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return getCharacteristicsPromise;
|
return characteristicsPromise;
|
||||||
});
|
});
|
||||||
|
|
||||||
promise = promise.then(()=>{
|
promise = promise.then(()=>{
|
||||||
|
@ -489,8 +489,8 @@
|
||||||
if (gatt.connected){
|
if (gatt.connected){
|
||||||
log("Disconnect with gatt: ", gatt);
|
log("Disconnect with gatt: ", gatt);
|
||||||
gatt.disconnect().then(()=>{
|
gatt.disconnect().then(()=>{
|
||||||
log("Successful disconnect", e);
|
log("Successful disconnect");
|
||||||
}).catch(()=>{
|
}).catch((e)=>{
|
||||||
log("Error during disconnect", e);
|
log("Error during disconnect", e);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"id": "bthrm",
|
"id": "bthrm",
|
||||||
"name": "Bluetooth Heart Rate Monitor",
|
"name": "Bluetooth Heart Rate Monitor",
|
||||||
"shortName": "BT HRM",
|
"shortName": "BT HRM",
|
||||||
"version": "0.05",
|
"version": "0.06",
|
||||||
"description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.",
|
"description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"type": "app",
|
"type": "app",
|
||||||
|
|
|
@ -1,43 +1,43 @@
|
||||||
(function(back) {
|
(function(back) {
|
||||||
Bangle.removeAllListeners('drag');
|
Bangle.removeAllListeners('drag');
|
||||||
Bangle.setUI("");
|
Bangle.setUI("");
|
||||||
var settings = require('Storage').readJSON('contourclock.json', true) || {};
|
var settings = require('Storage').readJSON('contourclock.json', true) || {};
|
||||||
if (settings.fontIndex==undefined) {
|
if (settings.fontIndex==undefined) {
|
||||||
settings.fontIndex=0;
|
settings.fontIndex=0;
|
||||||
require('Storage').writeJSON("myapp.json", settings);
|
require('Storage').writeJSON("myapp.json", settings);
|
||||||
}
|
}
|
||||||
savedIndex=settings.fontIndex;
|
savedIndex=settings.fontIndex;
|
||||||
saveListener = setWatch(function() { //save changes and return to settings menu
|
saveListener = setWatch(function() { //save changes and return to settings menu
|
||||||
require('Storage').writeJSON('contourclock.json', settings);
|
require('Storage').writeJSON('contourclock.json', settings);
|
||||||
Bangle.removeAllListeners('swipe');
|
Bangle.removeAllListeners('swipe');
|
||||||
Bangle.removeAllListeners('lock');
|
Bangle.removeAllListeners('lock');
|
||||||
clearWatch(saveListener);
|
clearWatch(saveListener);
|
||||||
g.clear();
|
g.clear();
|
||||||
back();
|
back();
|
||||||
}, BTN, { repeat:false, edge:'falling' });
|
}, BTN, { repeat:false, edge:'falling' });
|
||||||
lockListener = Bangle.on('lock', function () { //discard changes and return to clock
|
lockListener = Bangle.on('lock', function () { //discard changes and return to clock
|
||||||
settings.fontIndex=savedIndex;
|
settings.fontIndex=savedIndex;
|
||||||
require('Storage').writeJSON('contourclock.json', settings);
|
require('Storage').writeJSON('contourclock.json', settings);
|
||||||
Bangle.removeAllListeners('swipe');
|
Bangle.removeAllListeners('swipe');
|
||||||
Bangle.removeAllListeners('lock');
|
Bangle.removeAllListeners('lock');
|
||||||
clearWatch(saveListener);
|
clearWatch(saveListener);
|
||||||
g.clear();
|
g.clear();
|
||||||
load();
|
load();
|
||||||
});
|
});
|
||||||
swipeListener = Bangle.on('swipe', function (direction) {
|
swipeListener = Bangle.on('swipe', function (direction) {
|
||||||
var fontName = require('contourclock').drawClock(settings.fontIndex+direction);
|
var fontName = require('contourclock').drawClock(settings.fontIndex+direction);
|
||||||
if (fontName) {
|
if (fontName) {
|
||||||
settings.fontIndex+=direction;
|
settings.fontIndex+=direction;
|
||||||
g.clearRect(0,0,g.getWidth()-1,16);
|
g.clearRect(0,0,g.getWidth()-1,16);
|
||||||
g.setFont('6x8:2x2').setFontAlign(0,-1).drawString(fontName,g.getWidth()/2,0);
|
g.setFont('6x8:2x2').setFontAlign(0,-1).drawString(fontName,g.getWidth()/2,0);
|
||||||
} else {
|
} else {
|
||||||
require('contourclock').drawClock(settings.fontIndex);
|
require('contourclock').drawClock(settings.fontIndex);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
g.reset();
|
g.reset();
|
||||||
g.clear();
|
g.clear();
|
||||||
g.setFont('6x8:2x2').setFontAlign(0,-1);
|
g.setFont('6x8:2x2').setFontAlign(0,-1);
|
||||||
g.drawString(require('contourclock').drawClock(settings.fontIndex),g.getWidth()/2,0);
|
g.drawString(require('contourclock').drawClock(settings.fontIndex),g.getWidth()/2,0);
|
||||||
g.drawString('Swipe - change',g.getWidth()/2,g.getHeight()-36);
|
g.drawString('Swipe - change',g.getWidth()/2,g.getHeight()-36);
|
||||||
g.drawString('BTN - save',g.getWidth()/2,g.getHeight()-18);
|
g.drawString('BTN - save',g.getWidth()/2,g.getHeight()-18);
|
||||||
})
|
})
|
||||||
|
|
|
@ -5,3 +5,4 @@
|
||||||
0.05: Tweaks for 'HRM-raw' handling
|
0.05: Tweaks for 'HRM-raw' handling
|
||||||
0.06: Add widgets
|
0.06: Add widgets
|
||||||
0.07: Update scaling for new firmware
|
0.07: Update scaling for new firmware
|
||||||
|
0.08: Don't force backlight on/watch unlocked on Bangle 2
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
Bangle.setLCDPower(1);
|
if (process.env.HWVERSION == 1) {
|
||||||
Bangle.setLCDTimeout(0);
|
Bangle.setLCDPower(1);
|
||||||
|
Bangle.setLCDTimeout(0);
|
||||||
|
}
|
||||||
|
|
||||||
Bangle.setHRMPower(1);
|
Bangle.setHRMPower(1);
|
||||||
var hrmInfo, hrmOffset = 0;
|
var hrmInfo, hrmOffset = 0;
|
||||||
var hrmInterval;
|
var hrmInterval;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "hrm",
|
"id": "hrm",
|
||||||
"name": "Heart Rate Monitor",
|
"name": "Heart Rate Monitor",
|
||||||
"version": "0.07",
|
"version": "0.08",
|
||||||
"description": "Measure your heart rate and see live sensor data",
|
"description": "Measure your heart rate and see live sensor data",
|
||||||
"icon": "heartrate.png",
|
"icon": "heartrate.png",
|
||||||
"tags": "health",
|
"tags": "health",
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
require("Storage").write("launch.json",settings);
|
require("Storage").write("launch.json",settings);
|
||||||
}
|
}
|
||||||
const appMenu = {
|
const appMenu = {
|
||||||
/*LANG*/"": {"title": /*LANG*/"Launcher Settings"},
|
"": {"title": /*LANG*/"Launcher Settings"},
|
||||||
/*LANG*/"< Back": back,
|
/*LANG*/"< Back": back,
|
||||||
/*LANG*/"Font": {
|
/*LANG*/"Font": {
|
||||||
value: fonts.includes(settings.font)? fonts.indexOf(settings.font) : fonts.indexOf("12x20"),
|
value: fonts.includes(settings.font)? fonts.indexOf(settings.font) : fonts.indexOf("12x20"),
|
||||||
|
|
|
@ -11,4 +11,5 @@
|
||||||
0.11: Show the gadgetbridge weather temperature (settings).
|
0.11: Show the gadgetbridge weather temperature (settings).
|
||||||
0.12: Added humidity as an option to display.
|
0.12: Added humidity as an option to display.
|
||||||
0.13: Improved battery visualization.
|
0.13: Improved battery visualization.
|
||||||
0.14: Added altitude as an option to display.
|
0.14: Added altitude as an option to display.
|
||||||
|
0.15: Using wpedom to count steps.
|
|
@ -448,16 +448,17 @@ function draw(){
|
||||||
* Step counter via widget
|
* Step counter via widget
|
||||||
*/
|
*/
|
||||||
function getSteps() {
|
function getSteps() {
|
||||||
var steps = 0;
|
try{
|
||||||
let health;
|
if (WIDGETS.wpedom !== undefined) {
|
||||||
try {
|
return WIDGETS.wpedom.getSteps();
|
||||||
health = require("health");
|
} else if (WIDGETS.activepedom !== undefined) {
|
||||||
|
return WIDGETS.activepedom.getSteps();
|
||||||
|
}
|
||||||
} catch(ex) {
|
} catch(ex) {
|
||||||
return steps;
|
// In case we failed, we can only show 0 steps.
|
||||||
}
|
}
|
||||||
|
|
||||||
health.readDay(new Date(), h=>steps+=h.steps);
|
return 0;
|
||||||
return steps;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"name": "LCARS Clock",
|
"name": "LCARS Clock",
|
||||||
"shortName":"LCARS",
|
"shortName":"LCARS",
|
||||||
"icon": "lcars.png",
|
"icon": "lcars.png",
|
||||||
"version":"0.14",
|
"version":"0.15",
|
||||||
"readme": "README.md",
|
"readme": "README.md",
|
||||||
"supports": ["BANGLEJS2"],
|
"supports": ["BANGLEJS2"],
|
||||||
"description": "Library Computer Access Retrieval System (LCARS) clock.",
|
"description": "Library Computer Access Retrieval System (LCARS) clock.",
|
||||||
|
|
|
@ -258,7 +258,7 @@ var locales = {
|
||||||
temperature: "°C",
|
temperature: "°C",
|
||||||
ampm: { 0: "", 1: "" },
|
ampm: { 0: "", 1: "" },
|
||||||
timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" },
|
timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" },
|
||||||
datePattern: { 0: "%A %d %B %Y", "1": "%d/%m/%Y" }, // dimanche 1 mars 2020 // 01/03/2020
|
datePattern: { 0: "%d %B %Y", "1": "%d/%m/%Y" }, // 1 mars 2020 // 01/03/2020
|
||||||
abmonth: "janv,févr,mars,avril,mai,juin,juil,août,sept,oct,nov,déc",
|
abmonth: "janv,févr,mars,avril,mai,juin,juil,août,sept,oct,nov,déc",
|
||||||
month: "janvier,février,mars,avril,mai,juin,juillet,août,septembre,octobre,novembre,décembre",
|
month: "janvier,février,mars,avril,mai,juin,juillet,août,septembre,octobre,novembre,décembre",
|
||||||
abday: "dim,lun,mar,mer,jeu,ven,sam",
|
abday: "dim,lun,mar,mer,jeu,ven,sam",
|
||||||
|
|
|
@ -28,3 +28,5 @@
|
||||||
Spread message action buttons out
|
Spread message action buttons out
|
||||||
Back button now goes back to list of messages
|
Back button now goes back to list of messages
|
||||||
If showMessage called with no message (eg all messages deleted) now return to the clock (fix #1267)
|
If showMessage called with no message (eg all messages deleted) now return to the clock (fix #1267)
|
||||||
|
0.19: Use a larger font for message text if it'll fit
|
||||||
|
0.20: Allow tapping on the body to show a scrollable view of the message and title in a bigger font (fix #1405, #1031)
|
||||||
|
|
|
@ -26,19 +26,15 @@ When a new message is received:
|
||||||
|
|
||||||
When a message is shown, you'll see a screen showing the message title and text.
|
When a message is shown, you'll see a screen showing the message title and text.
|
||||||
|
|
||||||
### Android
|
* The 'back-arrow' button (or physical button on Bangle.js 2) goes back to Messages, marking the current message as read.
|
||||||
|
|
||||||
* The 'back-arrow' button goes back to Messages, marking the current message as read.
|
|
||||||
* If shown, the 'tick' button opens the notification on the phone
|
|
||||||
* If shown, the 'cross' button dismisses the notification on the phone
|
|
||||||
* The top-left icon shows more options, for instance deleting the message of marking unread
|
|
||||||
|
|
||||||
### iOS
|
|
||||||
|
|
||||||
* The 'back-arrow' button goes back to Messages, marking the current message as read.
|
|
||||||
* If shown, the 'tick' button responds positively to the notification (accept call/etc)
|
|
||||||
* If shown, the 'cross' button responds negatively to the notification (dismiss call/etc)
|
|
||||||
* The top-left icon shows more options, for instance deleting the message of marking unread
|
* The top-left icon shows more options, for instance deleting the message of marking unread
|
||||||
|
* On Bangle.js 2 you can tap on the message body to view a scrollable version of the title and text (or can use the top-left icon + `View Message`)
|
||||||
|
* If shown, the 'tick' button:
|
||||||
|
* **Android** opens the notification on the phone
|
||||||
|
* **iOS** responds positively to the notification (accept call/etc)
|
||||||
|
* If shown, the 'cross' button:
|
||||||
|
* **Android** dismisses the notification on the phone
|
||||||
|
* **iOS** responds negatively to the notification (dismiss call/etc)
|
||||||
|
|
||||||
## Images
|
## Images
|
||||||
_1. Screenshot of a notification_
|
_1. Screenshot of a notification_
|
||||||
|
|
|
@ -198,9 +198,39 @@ function showMusicMessage(msg) {
|
||||||
layout.render();
|
layout.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showMessageScroller(msg) {
|
||||||
|
var bodyFont = fontBig;
|
||||||
|
g.setFont(bodyFont);
|
||||||
|
var lines = [];
|
||||||
|
if (msg.title) lines = g.wrapString(msg.title, g.getWidth()-10)
|
||||||
|
var titleCnt = lines.length;
|
||||||
|
if (titleCnt) lines.push(""); // add blank line after title
|
||||||
|
lines = lines.concat(g.wrapString(msg.body, g.getWidth()-10),["",/*LANG*/"< Back"]);
|
||||||
|
E.showScroller({
|
||||||
|
h : g.getFontHeight(), // height of each menu item in pixels
|
||||||
|
c : lines.length, // number of menu items
|
||||||
|
// a function to draw a menu item
|
||||||
|
draw : function(idx, r) {
|
||||||
|
// FIXME: in 2v13 onwards, clearRect(r) will work fine. There's a bug in 2v12
|
||||||
|
g.setBgColor(idx<titleCnt ? colBg : g.theme.bg).clearRect(r.x,r.y,r.x+r.w, r.y+r.h);
|
||||||
|
g.setFont(bodyFont).drawString(lines[idx], r.x, r.y);
|
||||||
|
}, select : function(idx) {
|
||||||
|
if (idx>=lines.length-2)
|
||||||
|
showMessage(msg.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// ensure button-press on Bangle.js 2 takes us back
|
||||||
|
if (process.env.HWVERSION>1) Bangle.btnWatches = [
|
||||||
|
setWatch(() => showMessage(msg.id), BTN1, {repeat:1,edge:"falling"})
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
function showMessageSettings(msg) {
|
function showMessageSettings(msg) {
|
||||||
E.showMenu({"":{"title":/*LANG*/"Message"},
|
E.showMenu({"":{"title":/*LANG*/"Message"},
|
||||||
"< Back" : () => showMessage(msg.id),
|
"< Back" : () => showMessage(msg.id),
|
||||||
|
/*LANG*/"View Message" : () => {
|
||||||
|
showMessageScroller(msg);
|
||||||
|
},
|
||||||
/*LANG*/"Delete" : () => {
|
/*LANG*/"Delete" : () => {
|
||||||
MESSAGES = MESSAGES.filter(m=>m.id!=msg.id);
|
MESSAGES = MESSAGES.filter(m=>m.id!=msg.id);
|
||||||
saveMessages();
|
saveMessages();
|
||||||
|
@ -245,12 +275,13 @@ function showMessage(msgid) {
|
||||||
title = (lines.length>2) ? lines.slice(0,2).join("\n")+"..." : lines.join("\n");
|
title = (lines.length>2) ? lines.slice(0,2).join("\n")+"..." : lines.join("\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function goBack() {
|
||||||
|
msg.new = false; saveMessages(); // read mail
|
||||||
|
cancelReloadTimeout(); // don't auto-reload to clock now
|
||||||
|
checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:0});
|
||||||
|
}
|
||||||
var buttons = [
|
var buttons = [
|
||||||
{type:"btn", src:getBackImage(), cb:()=>{
|
{type:"btn", src:getBackImage(), cb:goBack} // back
|
||||||
msg.new = false; saveMessages(); // read mail
|
|
||||||
cancelReloadTimeout(); // don't auto-reload to clock now
|
|
||||||
checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:0});
|
|
||||||
}} // back
|
|
||||||
];
|
];
|
||||||
if (msg.positive) {
|
if (msg.positive) {
|
||||||
buttons.push({fillx:1});
|
buttons.push({fillx:1});
|
||||||
|
@ -270,9 +301,18 @@ function showMessage(msgid) {
|
||||||
checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1});
|
checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1});
|
||||||
}});
|
}});
|
||||||
}
|
}
|
||||||
var bodyFont = fontMedium;
|
// If body of message is only two lines long w/ large font, use large font.
|
||||||
lines = g.setFont(bodyFont).wrapString(msg.body, g.getWidth()-10);
|
var body=msg.body, bodyFont = fontLarge, lines;
|
||||||
var body = (lines.length>4) ? lines.slice(0,4).join("\n")+"..." : lines.join("\n");
|
if (body) {
|
||||||
|
var w = g.getWidth()-48;
|
||||||
|
if (g.setFont(bodyFont).stringWidth(body) > w * 2)
|
||||||
|
bodyFont = fontMedium;
|
||||||
|
if (g.setFont(bodyFont).stringWidth(body) > w) {
|
||||||
|
lines = g.setFont(bodyFont).wrapString(msg.body, g.getWidth()-10);
|
||||||
|
body = (lines.length>4) ? lines.slice(0,4).join("\n")+"..." : lines.join("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
layout = new Layout({ type:"v", c: [
|
layout = new Layout({ type:"v", c: [
|
||||||
{type:"h", fillx:1, bgCol:colBg, c: [
|
{type:"h", fillx:1, bgCol:colBg, c: [
|
||||||
{ type:"btn", src:getMessageImage(msg), col:getMessageImageCol(msg), pad: 3, cb:()=>{
|
{ type:"btn", src:getMessageImage(msg), col:getMessageImageCol(msg), pad: 3, cb:()=>{
|
||||||
|
@ -284,11 +324,18 @@ function showMessage(msgid) {
|
||||||
title?{type:"txt", font:titleFont, label:title, bgCol:colBg, fillx:1, pad:2 }:{},
|
title?{type:"txt", font:titleFont, label:title, bgCol:colBg, fillx:1, pad:2 }:{},
|
||||||
]},
|
]},
|
||||||
]},
|
]},
|
||||||
{type:"txt", font:bodyFont, label:body, fillx:1, filly:1, pad:2 },
|
{type:"txt", font:bodyFont, label:body, fillx:1, filly:1, pad:2, cb:()=>{
|
||||||
|
// allow tapping to show a larger version
|
||||||
|
showMessageScroller(msg);
|
||||||
|
} },
|
||||||
{type:"h",fillx:1, c: buttons}
|
{type:"h",fillx:1, c: buttons}
|
||||||
]});
|
]});
|
||||||
g.clearRect(Bangle.appRect);
|
g.clearRect(Bangle.appRect);
|
||||||
layout.render();
|
layout.render();
|
||||||
|
// ensure button-press on Bangle.js 2 takes us back
|
||||||
|
if (process.env.HWVERSION>1) Bangle.btnWatches = [
|
||||||
|
setWatch(goBack, BTN1, {repeat:1,edge:"falling"})
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "messages",
|
"id": "messages",
|
||||||
"name": "Messages",
|
"name": "Messages",
|
||||||
"version": "0.18",
|
"version": "0.20",
|
||||||
"description": "App to display notifications from iOS and Gadgetbridge",
|
"description": "App to display notifications from iOS and Gadgetbridge",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"type": "app",
|
"type": "app",
|
||||||
|
|
|
@ -11,3 +11,5 @@
|
||||||
0.11: Changed cycle on minute to prevInfo to avoid the 2nd one being the blank line
|
0.11: Changed cycle on minute to prevInfo to avoid the 2nd one being the blank line
|
||||||
0.12: Removed dependancy on widpedom, now uses Bangle.getHealthStatus("day").steps
|
0.12: Removed dependancy on widpedom, now uses Bangle.getHealthStatus("day").steps
|
||||||
which requires 2.11.27 firmware to reset at midnight
|
which requires 2.11.27 firmware to reset at midnight
|
||||||
|
0.13: call process.memory(false) to avoid triggering a GC of memory
|
||||||
|
supported in pre 2.12.13 firmware
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"id": "pastel",
|
"id": "pastel",
|
||||||
"name": "Pastel Clock",
|
"name": "Pastel Clock",
|
||||||
"shortName": "Pastel",
|
"shortName": "Pastel",
|
||||||
"version": "0.12",
|
"version": "0.13",
|
||||||
"description": "A Configurable clock with custom fonts, background and weather display. Has a cyclic information line that includes, day, date, battery, sunrise and sunset times. Requires firmware 2.11.27",
|
"description": "A Configurable clock with custom fonts, background and weather display. Has a cyclic information line that includes, day, date, battery, sunrise and sunset times. Requires firmware 2.11.27",
|
||||||
"icon": "pastel.png",
|
"icon": "pastel.png",
|
||||||
"dependencies": {"mylocation":"app","weather":"app"},
|
"dependencies": {"mylocation":"app","weather":"app"},
|
||||||
|
|
|
@ -83,7 +83,7 @@ const infoData = {
|
||||||
ID_SS: { calc: () => 'Sunset: ' + sunSet },
|
ID_SS: { calc: () => 'Sunset: ' + sunSet },
|
||||||
ID_STEP: { calc: () => 'Steps: ' + getSteps() },
|
ID_STEP: { calc: () => 'Steps: ' + getSteps() },
|
||||||
ID_BATT: { calc: () => 'Battery: ' + E.getBattery() + '%' },
|
ID_BATT: { calc: () => 'Battery: ' + E.getBattery() + '%' },
|
||||||
ID_MEM: { calc: () => {var val = process.memory(); return 'Ram: ' + Math.round(val.usage*100/val.total) + '%';} },
|
ID_MEM: { calc: () => {var val = process.memory(false); return 'Ram: ' + Math.round(val.usage*100/val.total) + '%';} },
|
||||||
ID_ID: { calc: () => {var val = NRF.getAddress().split(':'); return 'Id: ' + val[4] + val[5];} },
|
ID_ID: { calc: () => {var val = NRF.getAddress().split(':'); return 'Id: ' + val[4] + val[5];} },
|
||||||
ID_FW: { calc: () => 'Fw: ' + process.env.VERSION }
|
ID_FW: { calc: () => 'Fw: ' + process.env.VERSION }
|
||||||
};
|
};
|
||||||
|
|
|
@ -13,3 +13,5 @@
|
||||||
Move recording for CoreTemp to its own app
|
Move recording for CoreTemp to its own app
|
||||||
0.08: Memory usage improvements for recorder app itself
|
0.08: Memory usage improvements for recorder app itself
|
||||||
0.09: Show correct number for log in overwrite prompt
|
0.09: Show correct number for log in overwrite prompt
|
||||||
|
0.10: Fix broken recorder settings (when launched from settings app)
|
||||||
|
0.11: Fix KML and GPX export when there is no GPS data
|
||||||
|
|
|
@ -10,6 +10,9 @@
|
||||||
var domTracks = document.getElementById("tracks");
|
var domTracks = document.getElementById("tracks");
|
||||||
|
|
||||||
function saveKML(track,title) {
|
function saveKML(track,title) {
|
||||||
|
// only include data points with GPS values
|
||||||
|
track=track.filter(pt=>pt.Latitude!="" && pt.Longitude!="");
|
||||||
|
// Now output KML
|
||||||
var kml = `<?xml version="1.0" encoding="UTF-8"?>
|
var kml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<kml xmlns="http://www.opengis.net/kml/2.2">
|
<kml xmlns="http://www.opengis.net/kml/2.2">
|
||||||
<Document>
|
<Document>
|
||||||
|
@ -37,7 +40,6 @@ ${track.map(pt=>` <when>${pt.Time.toISOString()}</when>\n`).join("")}
|
||||||
${track.map(pt=>` <gx:coord>${pt.Longitude} ${pt.Latitude} ${pt.Altitude}</gx:coord>\n`).join("")}
|
${track.map(pt=>` <gx:coord>${pt.Longitude} ${pt.Latitude} ${pt.Altitude}</gx:coord>\n`).join("")}
|
||||||
<ExtendedData>
|
<ExtendedData>
|
||||||
<SchemaData schemaUrl="#schema">
|
<SchemaData schemaUrl="#schema">
|
||||||
|
|
||||||
${track[0].Heartrate!==undefined ? `<gx:SimpleArrayData name="heartrate">
|
${track[0].Heartrate!==undefined ? `<gx:SimpleArrayData name="heartrate">
|
||||||
${track.map(pt=>` <gx:value>${0|pt.Heartrate}</gx:value>\n`).join("")}
|
${track.map(pt=>` <gx:value>${0|pt.Heartrate}</gx:value>\n`).join("")}
|
||||||
</gx:SimpleArrayData>`:``}
|
</gx:SimpleArrayData>`:``}
|
||||||
|
@ -80,7 +82,7 @@ function saveGPX(track, title) {
|
||||||
<name>${title}</name>
|
<name>${title}</name>
|
||||||
<trkseg>`;
|
<trkseg>`;
|
||||||
track.forEach(pt=>{
|
track.forEach(pt=>{
|
||||||
gpx += `
|
if (pt.Latitude!="" && pt.Longitude!="") gpx += `
|
||||||
<trkpt lat="${pt.Latitude}" lon="${pt.Longitude}">
|
<trkpt lat="${pt.Latitude}" lon="${pt.Longitude}">
|
||||||
<ele>${pt.Altitude}</ele>
|
<ele>${pt.Altitude}</ele>
|
||||||
<time>${pt.Time.toISOString()}</time>
|
<time>${pt.Time.toISOString()}</time>
|
||||||
|
@ -122,6 +124,7 @@ function saveCSV(track, title) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function trackLineToObject(headers, l) {
|
function trackLineToObject(headers, l) {
|
||||||
|
if (l===undefined) return {};
|
||||||
var t = l.trim().split(",");
|
var t = l.trim().split(",");
|
||||||
var o = {};
|
var o = {};
|
||||||
headers.forEach((header,i) => o[header] = t[i]);
|
headers.forEach((header,i) => o[header] = t[i]);
|
||||||
|
@ -155,7 +158,7 @@ function getTrackList() {
|
||||||
Util.showModal(`Loading Track ${trackNo}...`);
|
Util.showModal(`Loading Track ${trackNo}...`);
|
||||||
Puck.eval(`(function(fn) {
|
Puck.eval(`(function(fn) {
|
||||||
var f = require("Storage").open(fn,"r");
|
var f = require("Storage").open(fn,"r");
|
||||||
var headers = f.readLine();
|
var headers = f.readLine().trim();
|
||||||
var data = f.readLine();
|
var data = f.readLine();
|
||||||
var lIdx = headers.split(",").indexOf("Latitude");
|
var lIdx = headers.split(",").indexOf("Latitude");
|
||||||
if (lIdx >= 0) {
|
if (lIdx >= 0) {
|
||||||
|
@ -184,14 +187,14 @@ function getTrackList() {
|
||||||
var html = `<div class="container">
|
var html = `<div class="container">
|
||||||
<div class="columns">\n`;
|
<div class="columns">\n`;
|
||||||
trackList.forEach(track => {
|
trackList.forEach(track => {
|
||||||
var trackData = trackLineToObject(track.info.headers, track.info.l);
|
|
||||||
console.log("track", track);
|
console.log("track", track);
|
||||||
|
var trackData = trackLineToObject(track.info.headers, track.info.l);
|
||||||
console.log("trackData", trackData);
|
console.log("trackData", trackData);
|
||||||
html += `
|
html += `
|
||||||
<div class="column col-12">
|
<div class="column col-12">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="card-title h5">Track ${track.number}</div>
|
<div class="card-title h5">Track ${track.number}</div>
|
||||||
<div class="card-subtitle text-gray">${trackData.Time.toLocaleDateString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}</div>
|
<div class="card-subtitle text-gray">${trackData.Time?trackData.Time.toLocaleDateString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }):"No track data"}</div>
|
||||||
</div>
|
</div>
|
||||||
${trackData.Latitude ? `
|
${trackData.Latitude ? `
|
||||||
<div class="card-image">
|
<div class="card-image">
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"id": "recorder",
|
"id": "recorder",
|
||||||
"name": "Recorder",
|
"name": "Recorder",
|
||||||
"shortName": "Recorder",
|
"shortName": "Recorder",
|
||||||
"version": "0.09",
|
"version": "0.11",
|
||||||
"description": "Record GPS position, heart rate and more in the background, then download to your PC.",
|
"description": "Record GPS position, heart rate and more in the background, then download to your PC.",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"tags": "tool,outdoors,gps,widget",
|
"tags": "tool,outdoors,gps,widget",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
(function(back) {
|
(function(back) {
|
||||||
// just go right to our app - we need all the memory
|
// just go right to our app - we need all the memory
|
||||||
load("record.app.js");
|
load("recorder.app.js");
|
||||||
})();
|
})
|
||||||
|
|
|
@ -10,3 +10,5 @@
|
||||||
0.10: Add Kalman filter to smooth the speed and altitude values. Can be disabled in settings.
|
0.10: Add Kalman filter to smooth the speed and altitude values. Can be disabled in settings.
|
||||||
1.06: Misc memory and screen optimisations.
|
1.06: Misc memory and screen optimisations.
|
||||||
1.10: Adds Kalman filter.
|
1.10: Adds Kalman filter.
|
||||||
|
1.14: Add VMG and coordinates screens
|
||||||
|
1.43: Adds mirroring of the watch face to an Android device. See README.md
|
||||||
|
|
|
@ -0,0 +1,273 @@
|
||||||
|
// Add this script to DroidScript on an android device.
|
||||||
|
// Uses the PuckJS plugin to provide mirroring of the GPS Adv Sports II Bangle app face onto an android device.
|
||||||
|
|
||||||
|
app.LoadPlugin("PuckJS");
|
||||||
|
|
||||||
|
|
||||||
|
//Called when application is started.
|
||||||
|
function OnStart() {
|
||||||
|
|
||||||
|
v = '1.49' // Version of this script
|
||||||
|
requiredBangleVer = '1.46'; // Minimum speedalt2 version required on Bangle
|
||||||
|
curBangleVer = '-.--'
|
||||||
|
isStopped = true; // Data receive turned off
|
||||||
|
lastData = new Date().getTime() / 1000; // Time of last data received
|
||||||
|
addr = ''; // Address of last connection
|
||||||
|
|
||||||
|
// Mode = 0 // 0=SPD, 1=ALT, 2=DST, 3=VMG, 4=POSN, 5=TIME
|
||||||
|
btnOff = '#175A63'
|
||||||
|
btnOn = '#4285F4'
|
||||||
|
col = new Array(['black'],['#64FF00'],['#FCFA00'],['#00E4FF']) // bg, main, units, wp - 0xFFFF,0x007F,0x0054,0x0054
|
||||||
|
|
||||||
|
// Connect to Bangle
|
||||||
|
puck = app.CreatePuckJS();
|
||||||
|
puck.SetOnConnect(onConnect); // Callback.
|
||||||
|
puck.SetOnReceive(readResponse); // Callback to capture console output from app.
|
||||||
|
puck.Scan("Bangle");
|
||||||
|
setInterval(checkConnection,5000) // Periodic check for data timeout and attempt a reconnect
|
||||||
|
|
||||||
|
// Controls
|
||||||
|
app.SetScreenMode("Full")
|
||||||
|
|
||||||
|
//Create a layout with objects vertically centered.
|
||||||
|
layVert = app.CreateLayout("Linear", "VCenter,FillXY")
|
||||||
|
layVert.SetPadding(0.02, 0.02, 0.02, 0.02);
|
||||||
|
layVert.SetBackColor(col[0])
|
||||||
|
|
||||||
|
//Create a text label and add it to layout.
|
||||||
|
val = app.CreateText('', -1, -1, "Html,Multiline") // main value
|
||||||
|
val.SetTextSize(120)
|
||||||
|
val.SetTextColor(col[1]) // green
|
||||||
|
layVert.AddChild(val)
|
||||||
|
|
||||||
|
val2 = app.CreateText('') // minor value or waypoint name
|
||||||
|
val2.SetTextSize(50)
|
||||||
|
val2.SetTextColor(col[3]) // cyan
|
||||||
|
layVert.AddChild(val2)
|
||||||
|
|
||||||
|
// Units and status text
|
||||||
|
layHor = app.CreateLayout("Linear", "Horizontal")
|
||||||
|
layHor.SetMargins(-1, -1, -1, 10, 'px')
|
||||||
|
unit = app.CreateText('')
|
||||||
|
unit.SetSize(200, -1, "px")
|
||||||
|
unit.SetTextSize(32)
|
||||||
|
unit.SetTextColor('#FCFA00') // yellow
|
||||||
|
layHor.AddChild(unit)
|
||||||
|
|
||||||
|
// mode = app.CreateText( '' ,.3,-1,"Center" )
|
||||||
|
mode = app.CreateText('', -1, -1)
|
||||||
|
mode.SetSize(200, -1, "px")
|
||||||
|
mode.SetTextSize(32)
|
||||||
|
mode.SetTextColor('#FCFA00') // yellow
|
||||||
|
layHor.AddChild(mode)
|
||||||
|
|
||||||
|
// sats = app.CreateText( '' ,.3,-1,"Right")
|
||||||
|
sats = app.CreateText('', -1, -1, "FillXY,Bottom")
|
||||||
|
sats.SetSize(200, -1, "px")
|
||||||
|
sats.SetTextSize(20)
|
||||||
|
sats.SetTextColor(col[3]) // cyan
|
||||||
|
layHor.AddChild(sats)
|
||||||
|
|
||||||
|
layVert.AddChild(layHor)
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
layBtn = app.CreateLayout("Linear", "Horizontal")
|
||||||
|
|
||||||
|
btnAbout = app.CreateButton("About");
|
||||||
|
btnAbout.SetOnTouch(btn_OnAbout);
|
||||||
|
btnAbout.SetBackColor(btnOff)
|
||||||
|
layBtn.AddChild(btnAbout);
|
||||||
|
|
||||||
|
btnStart = app.CreateButton("Start");
|
||||||
|
btnStart.SetOnTouch(btn_OnStart);
|
||||||
|
btnStart.SetBackColor(btnOff)
|
||||||
|
layBtn.AddChild(btnStart);
|
||||||
|
|
||||||
|
btnStop = app.CreateButton("Stop");
|
||||||
|
btnStop.SetOnTouch(btn_OnStop);
|
||||||
|
btnStop.SetBackColor(btnOff)
|
||||||
|
layBtn.AddChild(btnStop);
|
||||||
|
|
||||||
|
btnScan = app.CreateButton("Scan");
|
||||||
|
btnScan.SetOnTouch(btn_OnScan);
|
||||||
|
btnScan.SetBackColor(btnOff)
|
||||||
|
layBtn.AddChild(btnScan);
|
||||||
|
|
||||||
|
// Status 'LED'
|
||||||
|
led = app.AddCanvas(layBtn, 0.1, 0.1, "FillXY,Bottom")
|
||||||
|
|
||||||
|
layVert.AddChild(layBtn)
|
||||||
|
|
||||||
|
//Add layout to app.
|
||||||
|
app.AddLayout(layVert)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function readResponse(data) {
|
||||||
|
if (data.substring(0, 1) != '{') return; // ignore non JSON
|
||||||
|
btnStart.SetBackColor(btnOn)
|
||||||
|
|
||||||
|
lastData = new Date().getTime() / 1000; // Time of last data received
|
||||||
|
|
||||||
|
d = JSON.parse(data);
|
||||||
|
|
||||||
|
if ( ( d.id != 'speedalt2' ) || (parseFloat(d.v) < parseFloat(requiredBangleVer)) || (typeof(d.v) == 'undefined')) {
|
||||||
|
btn_OnStop()
|
||||||
|
app.Alert('The GPS Adv Sports II app on your Bangle must be at least version ' + requiredBangleVer, 'Bangle App Upgrade Required')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
curBangleVer = d.v
|
||||||
|
|
||||||
|
if (parseFloat(v) < parseFloat(d.vd)) {
|
||||||
|
btn_OnStop()
|
||||||
|
app.Alert('This GPS Adv Sports II script must be at least version ' + d.vd, 'Droidscript script Upgrade Required')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
isStopped = false; // Flag that we are running and receiving data
|
||||||
|
|
||||||
|
// Flash blue 'led' indicator when data packet received.
|
||||||
|
setLED(led,true,"#0051FF")
|
||||||
|
setTimeout(function() {setLED(led,false,-1)}, 500)
|
||||||
|
|
||||||
|
if (d.m == 0) { // Speed ( dont need pos or time here )
|
||||||
|
val.SetTextSize(120)
|
||||||
|
val2.SetTextSize(50)
|
||||||
|
|
||||||
|
val.SetText(d.sp)
|
||||||
|
val2.SetText('')
|
||||||
|
unit.SetText(d.spd_unit)
|
||||||
|
mode.SetText('SPD')
|
||||||
|
sats.SetText(d.sats)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (d.m == 1) { // Alt
|
||||||
|
val.SetTextSize(120)
|
||||||
|
val2.SetTextSize(50)
|
||||||
|
|
||||||
|
val.SetText(d.al)
|
||||||
|
val2.SetText('')
|
||||||
|
unit.SetText(d.alt_unit)
|
||||||
|
mode.SetText('ALT')
|
||||||
|
sats.SetText(d.sats)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (d.m == 2) { // Dist
|
||||||
|
val.SetTextSize(120)
|
||||||
|
val2.SetTextSize(50)
|
||||||
|
|
||||||
|
val.SetText(d.di)
|
||||||
|
val2.SetText(d.wp)
|
||||||
|
unit.SetText(d.dist_unit)
|
||||||
|
mode.SetText('DST')
|
||||||
|
sats.SetText(d.sats)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (d.m == 3) { // VMG
|
||||||
|
val.SetTextSize(120)
|
||||||
|
val2.SetTextSize(50)
|
||||||
|
|
||||||
|
val.SetText(d.vmg)
|
||||||
|
val2.SetText(d.wp)
|
||||||
|
unit.SetText(d.spd_unit)
|
||||||
|
mode.SetText('VMG')
|
||||||
|
sats.SetText(d.sats)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (d.m == 4) { // POS
|
||||||
|
val.SetTextSize(80)
|
||||||
|
val2.SetTextSize(10)
|
||||||
|
|
||||||
|
txt = d.lat +
|
||||||
|
' <font color='+col[2]+'><small><small>' +
|
||||||
|
d.ns +
|
||||||
|
"</small></small></font><br>" +
|
||||||
|
d.lon +
|
||||||
|
' <font color='+col[2]+'><small><small>' +
|
||||||
|
d.ew +
|
||||||
|
"</small></small></font>"
|
||||||
|
|
||||||
|
val.SetHtml(txt)
|
||||||
|
val2.SetText('')
|
||||||
|
unit.SetText('')
|
||||||
|
mode.SetText('')
|
||||||
|
sats.SetText(d.sats)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (d.m == 5) { // Time
|
||||||
|
val.SetTextSize(90)
|
||||||
|
val2.SetTextSize(10)
|
||||||
|
|
||||||
|
dt = new Date();
|
||||||
|
|
||||||
|
val.SetText(String(dt.getHours()).padStart(2, '0') + "\n" + String(dt.getMinutes()).padStart(2, '0'))
|
||||||
|
val2.SetText('')
|
||||||
|
unit.SetText('')
|
||||||
|
mode.SetText('')
|
||||||
|
sats.SetText(d.sats)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLED(canvas,on,colour) {
|
||||||
|
if ( on ) {
|
||||||
|
canvas.SetPaintColor(colour)
|
||||||
|
canvas.DrawCircle(0.5, 0.5, 0.1)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
canvas.Clear()
|
||||||
|
}
|
||||||
|
canvas.Update()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onConnect(name, address, bonded, rssi) {
|
||||||
|
addr = address
|
||||||
|
console.log( "Connected to " + address );
|
||||||
|
btn_OnStart() // Once connect tell app to start sending updates
|
||||||
|
}
|
||||||
|
|
||||||
|
// Periodic check for data timeout and attempt a reconnect
|
||||||
|
function checkConnection() {
|
||||||
|
if (isStopped) return
|
||||||
|
if ( parseFloat(new Date().getTime() / 1000) - lastData > 30 ) {
|
||||||
|
|
||||||
|
console.log( "Reconnecting to : "+addr);
|
||||||
|
|
||||||
|
// Flash orange 'led' indicator for connection attempts.
|
||||||
|
setLED(led,true,"#FC8A00")
|
||||||
|
setTimeout(function() {setLED(led,false,-1)}, 500)
|
||||||
|
puck.Connect(addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function btn_OnAbout() {
|
||||||
|
app.ShowPopup("GPS Adv Sports II\nAndroid Mirror : "+v+"\nBangle.js : "+curBangleVer,"Long")
|
||||||
|
}
|
||||||
|
|
||||||
|
function btn_OnStart() {
|
||||||
|
btnStart.SetBackColor(btnOn)
|
||||||
|
btnStop.SetBackColor(btnOff)
|
||||||
|
puck.SendCode('btOn(1)\n') // Enable the data send
|
||||||
|
isStopped = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function btn_OnStop() {
|
||||||
|
btnStart.SetBackColor(btnOff)
|
||||||
|
btnStop.SetBackColor(btnOn)
|
||||||
|
puck.SendCode('btOn(0)\n') // Disable the data send
|
||||||
|
isStopped = true
|
||||||
|
val.SetText('')
|
||||||
|
val2.SetText('')
|
||||||
|
unit.SetText('')
|
||||||
|
mode.SetText('')
|
||||||
|
sats.SetText('')
|
||||||
|
}
|
||||||
|
|
||||||
|
function btn_OnScan() {
|
||||||
|
btnStart.SetBackColor(btnOff)
|
||||||
|
btnStop.SetBackColor(btnOff)
|
||||||
|
puck.Scan("Bangle");
|
||||||
|
}
|
||||||
|
|
|
@ -1,12 +1,6 @@
|
||||||
# GPS Speed, Altimeter and Distance to Waypoint
|
# GPS Speed, Altitude, Distance and VMG
|
||||||
|
|
||||||
What is the difference between **GPS Adventure Sports** and **GPS Adventure Sports II** ?
|
**GPS Adventure Sports II** has 6 screens, each of which displays just one of Speed, Altitude, Distance to waypoint, VMG to waypoint, Position or Time.
|
||||||
|
|
||||||
**GPS Adventure Sports** has 3 screens, each of which display different sets of information.
|
|
||||||
|
|
||||||
**GPS Adventure Sports II** has 5 screens, each of which displays just one of Speed, Altitude, Distance to waypoint, Position or Time.
|
|
||||||
|
|
||||||
In all other respect they perform the same functions.
|
|
||||||
|
|
||||||
The waypoints list is the same as that used with the [GPS Navigation](https://banglejs.com/apps/#gps%20navigation) app so the same set of waypoints can be used across both apps. Refer to that app for waypoint file information.
|
The waypoints list is the same as that used with the [GPS Navigation](https://banglejs.com/apps/#gps%20navigation) app so the same set of waypoints can be used across both apps. Refer to that app for waypoint file information.
|
||||||
|
|
||||||
|
@ -14,17 +8,17 @@ The waypoints list is the same as that used with the [GPS Navigation](https://ba
|
||||||
|
|
||||||
**BTN1** ( Speed and Altitude ) Short press < 2 secs toggles the display between last reading and maximum recorded. Long press > 2 secs resets the recorded maximum values.
|
**BTN1** ( Speed and Altitude ) Short press < 2 secs toggles the display between last reading and maximum recorded. Long press > 2 secs resets the recorded maximum values.
|
||||||
|
|
||||||
**BTN1** ( Distance ) Select next waypoint. Last fix distance from selected waypoint is displayed.
|
**BTN1** ( Distance and VMG ) Select next waypoint. Last fix distance from selected waypoint or speed towards is displayed.
|
||||||
|
|
||||||
**BTN2** : Disables/Restores power saving timeout. Locks the screen on and GPS in SuperE mode to enable reading for longer periods but uses maximum battery drain. Red LED (dot) at top of screen when screen is locked on. Press again to restore power saving timeouts.
|
**BTN2** : Disables/Restores power saving timeout. Locks the screen on and GPS in SuperE mode to enable reading for longer periods but uses maximum battery drain. Red LED (dot) at top of screen when screen is locked on. Press again to restore power saving timeouts.
|
||||||
|
|
||||||
**BTN3** : Cycles the screens between Speed, Altitude, Distance to waypoint, Position and Time
|
**BTN3** : Cycles the screens between Speed, Altitude, Distance to waypoint, VMG to waypoint, Position and Time
|
||||||
|
|
||||||
**BTN3** : Long press exit and return to watch.
|
**BTN3** : Long press exit and return to watch.
|
||||||
|
|
||||||
**Touch Screen** If the 'Touch' setting is ON then :
|
**Touch Screen** If the 'Touch' setting is ON then :
|
||||||
|
|
||||||
Swipe Left/Right cycles between the five screens.
|
Swipe Left/Right cycles between the six screens.
|
||||||
|
|
||||||
Touch functions as BTN1 short press.
|
Touch functions as BTN1 short press.
|
||||||
|
|
||||||
|
@ -51,9 +45,36 @@ When using the GPS Setup App this app switches the GPS to SuperE (default) mode
|
||||||
|
|
||||||
The MAX values continue to be collected with the display off so may appear a little odd after the intermittent fixes of the low power mode.
|
The MAX values continue to be collected with the display off so may appear a little odd after the intermittent fixes of the low power mode.
|
||||||
|
|
||||||
|
## Velocity Made Good - VMG
|
||||||
|
|
||||||
|
This implementation of VMG is very simplistic and is simply the component of your current vector ( course and speed ) that is towards your selected waypoint. It is displayed as negative if you are moving away from the waypoint. For it to be displayed you must be moving and the GPS must be able to detemrine a course. If not it will display '---' as the VMG.
|
||||||
|
|
||||||
|
## Mirroring to Android
|
||||||
|
|
||||||
|
This feature is an optional extra to solve and enhance a specific requirement for me. While sailing the Bangle.js watch screen is very difficult to read in bright sunlight while wearing the polaroid prescription lenses that I require on the water. The solution is to mirror the Bangle.js screen to an android device with a daylight readable OLED screen that I keep in a clear waterproof case on the boat. Using this mirroring feature I can see the GPS Adv Sports II app easily at all times, either on my wrist or on the bigger android device while still having full control over the display via the watch buttons.
|
||||||
|
|
||||||
|
There is a caveat. While in use the watch GPS stays in SuperE mode in order to keep the android screen updates going which means a higher battery use on the Bangle.js.
|
||||||
|
|
||||||
|
How is this mirroring done?
|
||||||
|
|
||||||
|
Install Droidscript on your Android device. Must have BLE suport and the PuckJS plugin installed. The Droidscript script can be found in the BangleApps GIT repository : https://github.com/espruino/BangleApps/tree/master/apps/speedalt2
|
||||||
|
|
||||||
|
The Droidscript script file is called : **GPS Adv Sports II.js**
|
||||||
|
|
||||||
|
**Important Gotcha :** For the BLE comms to find and connect to the Bangle.js it must be paired with the Android device but **NOT** connected. The Bangle.js must have been set in the Bluetooth settings as connectable.
|
||||||
|
|
||||||
|
Start/Stop buttons tell the Bangle.js to start or stop sending BLE data packets to the Android device. While stopped the Bangle.js reverts to full power saving mode when the screen is asleep.
|
||||||
|
|
||||||
|
When runnig a blue 'led' will flash each time a data packet is recieved to refresh the android display.
|
||||||
|
|
||||||
|
An orange 'led' will flash for each reconnection attempt if no data is received for 30 seconds. It will keep trying to reconnect so you can restart the Bangle, run another Bangle app or temprarily turn off bluetooth. The android mirror display will automatically reconnect when the GPS Adv Sports II app is running on the Bangle again. ( Designed to leave the Android device running as the display mirror in a sealed case all day while retaining the ability to do other functions on the Bangle.js and returning to the GPS Speed Alt II app. )
|
||||||
|
|
||||||
|
Android Screen Mirroring:<br>
|
||||||
|
<p>
|
||||||
|
|
||||||
## Waypoints
|
## Waypoints
|
||||||
|
|
||||||
Waypoints are used in [D]istance mode. Create a file waypoints.json and write to storage on the Bangle.js using the IDE. The first 6 characters of the name are displayed in Speed+[D]istance mode.
|
Waypoints are used in Distance and VMG modes. Create a file waypoints.json and write to storage on the Bangle.js using the IDE. The first 6 characters of the name are displayed in Speed+[D]istance mode.
|
||||||
|
|
||||||
The [GPS Navigation](https://banglejs.com/apps/#gps%20navigation) app in the App Loader has a really nice waypoints file editor. (Must be connected to your Bangle.JS and then click on the Download icon.)
|
The [GPS Navigation](https://banglejs.com/apps/#gps%20navigation) app in the App Loader has a really nice waypoints file editor. (Must be connected to your Bangle.JS and then click on the Download icon.)
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,12 @@
|
||||||
Speed and Altitude [speedalt2]
|
Speed and Altitude [speedalt2]
|
||||||
Mike Bennett mike[at]kereru.com
|
Mike Bennett mike[at]kereru.com
|
||||||
1.10 : add inverted colours
|
1.10 : add inverted colours
|
||||||
|
1.14 : Add VMG screen
|
||||||
|
1.34 : Add bluetooth data stream for Droidscript
|
||||||
|
1.43 : Keep GPS in SuperE mode while using Droiscript screen mirroring
|
||||||
*/
|
*/
|
||||||
var v = '1.10';
|
var v = '1.46';
|
||||||
|
var vDroid = '1.46'; // Required DroidScript program version
|
||||||
|
|
||||||
/*kalmanjs, Wouter Bulten, MIT, https://github.com/wouterbulten/kalmanjs */
|
/*kalmanjs, Wouter Bulten, MIT, https://github.com/wouterbulten/kalmanjs */
|
||||||
var KalmanFilter = (function () {
|
var KalmanFilter = (function () {
|
||||||
|
@ -180,14 +184,10 @@ let LED = // LED as minimal and only definition (as instance / singleton)
|
||||||
, toggle: function() { this.set( ! this.isOn); } // toggle the LED
|
, toggle: function() { this.set( ! this.isOn); } // toggle the LED
|
||||||
}, LED1 = LED; // LED1 as 'synonym' for LED
|
}, LED1 = LED; // LED1 as 'synonym' for LED
|
||||||
|
|
||||||
// Load fonts
|
|
||||||
//require("Font7x11Numeric7Seg").add(Graphics);
|
|
||||||
|
|
||||||
var lf = {fix:0,satellites:0};
|
var lf = {fix:0,satellites:0};
|
||||||
var showMax = 0; // 1 = display the max values. 0 = display the cur fix
|
var showMax = 0; // 1 = display the max values. 0 = display the cur fix
|
||||||
var pwrSav = 1; // 1 = default power saving with watch screen off and GPS to PMOO mode. 0 = screen kept on.
|
var pwrSav = 1; // 1 = default power saving with watch screen off and GPS to PMOO mode. 0 = screen kept on.
|
||||||
var canDraw = 1;
|
var canDraw = 1;
|
||||||
var time = ''; // Last time string displayed. Re displayed in background colour to remove before drawing new time.
|
|
||||||
var tmrLP; // Timer for delay in switching to low power after screen turns off
|
var tmrLP; // Timer for delay in switching to low power after screen turns off
|
||||||
|
|
||||||
var maxSpd = 0;
|
var maxSpd = 0;
|
||||||
|
@ -195,6 +195,8 @@ var maxAlt = 0;
|
||||||
var maxN = 0; // counter. Only start comparing for max after a certain number of fixes to allow kalman filter to have smoohed the data.
|
var maxN = 0; // counter. Only start comparing for max after a certain number of fixes to allow kalman filter to have smoohed the data.
|
||||||
|
|
||||||
var emulator = (process.env.BOARD=="EMSCRIPTEN")?1:0; // 1 = running in emulator. Supplies test values;
|
var emulator = (process.env.BOARD=="EMSCRIPTEN")?1:0; // 1 = running in emulator. Supplies test values;
|
||||||
|
var bt = 0; // 0 = bluetooth data feed off. 1 = on
|
||||||
|
var btLast = 0; // time of last bt transmit
|
||||||
|
|
||||||
var wp = {}; // Waypoint to use for distance from cur position.
|
var wp = {}; // Waypoint to use for distance from cur position.
|
||||||
|
|
||||||
|
@ -215,16 +217,27 @@ function radians(a) {
|
||||||
return a*Math.PI/180;
|
return a*Math.PI/180;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function degrees(a) {
|
||||||
|
var d = a*180/Math.PI;
|
||||||
|
return (d+360)%360;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bearing(a,b){
|
||||||
|
var delta = radians(b.lon-a.lon);
|
||||||
|
var alat = radians(a.lat);
|
||||||
|
var blat = radians(b.lat);
|
||||||
|
var y = Math.sin(delta) * Math.cos(blat);
|
||||||
|
var x = Math.cos(alat)*Math.sin(blat) -
|
||||||
|
Math.sin(alat)*Math.cos(blat)*Math.cos(delta);
|
||||||
|
return Math.round(degrees(Math.atan2(y, x)));
|
||||||
|
}
|
||||||
|
|
||||||
function distance(a,b){
|
function distance(a,b){
|
||||||
var x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2));
|
var x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2));
|
||||||
var y = radians(b.lat-a.lat);
|
var y = radians(b.lat-a.lat);
|
||||||
|
|
||||||
// Distance in selected units
|
// Distance in metres
|
||||||
var d = Math.sqrt(x*x + y*y) * 6371000;
|
var d = Math.sqrt(x*x + y*y) * 6371000;
|
||||||
d = (d/parseFloat(cfg.dist)).toFixed(2);
|
|
||||||
if ( d >= 100 ) d = parseFloat(d).toFixed(1);
|
|
||||||
if ( d >= 1000 ) d = parseFloat(d).toFixed(0);
|
|
||||||
|
|
||||||
return d;
|
return d;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -328,8 +341,8 @@ function drawClock() {
|
||||||
function drawWP(wp) {
|
function drawWP(wp) {
|
||||||
buf.setColor(3);
|
buf.setColor(3);
|
||||||
buf.setFontAlign(0,1); //left, bottom
|
buf.setFontAlign(0,1); //left, bottom
|
||||||
buf.setFontVector(48);
|
buf.setFontVector(40);
|
||||||
buf.drawString(wp,120,140);
|
buf.drawString(wp,120,132);
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawSats(sats) {
|
function drawSats(sats) {
|
||||||
|
@ -362,14 +375,18 @@ if ( emulator ) {
|
||||||
var m;
|
var m;
|
||||||
|
|
||||||
var sp = '---';
|
var sp = '---';
|
||||||
var al = '---';
|
var al = sp;
|
||||||
var di = '---';
|
var di = sp;
|
||||||
var age = '---';
|
var brg = ''; // bearing
|
||||||
|
var crs = ''; // course
|
||||||
|
var age = sp;
|
||||||
var lat = '---.--';
|
var lat = '---.--';
|
||||||
var ns = '';
|
var ns = '';
|
||||||
var ew = '';
|
var ew = '';
|
||||||
var lon = '---.--';
|
var lon = '---.--';
|
||||||
var sats = '---';
|
var sats = sp;
|
||||||
|
var vmg = sp;
|
||||||
|
|
||||||
|
|
||||||
// Waypoint name
|
// Waypoint name
|
||||||
var wpName = wp.name;
|
var wpName = wp.name;
|
||||||
|
@ -387,21 +404,33 @@ if ( emulator ) {
|
||||||
lf.smoothed = 1;
|
lf.smoothed = 1;
|
||||||
if ( maxN <= 15 ) maxN++;
|
if ( maxN <= 15 ) maxN++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bearing to waypoint
|
||||||
|
brg = bearing(lf,wp);
|
||||||
|
|
||||||
|
// Current course
|
||||||
|
crs = lf.course;
|
||||||
|
|
||||||
|
// Relative angle to wp
|
||||||
|
var a = Math.max(crs,brg) - Math.min(crs,brg);
|
||||||
|
if ( a >= 180 ) a = 360 -a;
|
||||||
|
|
||||||
// Speed
|
// Speed
|
||||||
if ( cfg.spd == 0 ) {
|
sp = parseFloat(lf.speed)/parseFloat(cfg.spd); // Calculate for selected units
|
||||||
m = require("locale").speed(lf.speed).match(/([0-9,\.]+)(.*)/); // regex splits numbers from units
|
|
||||||
sp = parseFloat(m[1]);
|
// vmg
|
||||||
cfg.spd_unit = m[2];
|
if ( a >= 90 ) vmg = sp * Math.cos(radians(180-a)) * -1; // moving away from WP
|
||||||
}
|
else vmg = sp * Math.cos(radians(a)); // towards wp
|
||||||
else sp = parseFloat(lf.speed)/parseFloat(cfg.spd); // Calculate for selected units
|
|
||||||
|
|
||||||
if ( sp < 10 ) sp = sp.toFixed(1);
|
if ( sp < 10 ) sp = sp.toFixed(1);
|
||||||
else sp = Math.round(sp);
|
else sp = Math.round(sp);
|
||||||
if (isNaN(sp)) sp = '---';
|
if (isNaN(sp)) sp = '---';
|
||||||
|
|
||||||
if (parseFloat(sp) > parseFloat(maxSpd) && maxN > 15 ) maxSpd = sp;
|
if (parseFloat(sp) > parseFloat(maxSpd) && maxN > 15 ) maxSpd = sp;
|
||||||
|
|
||||||
|
if ( Math.abs(vmg) >= 0.05 && Math.abs(vmg) < 10 ) vmg = vmg.toFixed(1);
|
||||||
|
else vmg = Math.round(vmg);
|
||||||
|
if (isNaN(vmg)) vmg = '---';
|
||||||
|
|
||||||
// Altitude
|
// Altitude
|
||||||
al = lf.alt;
|
al = lf.alt;
|
||||||
al = Math.round(parseFloat(al)/parseFloat(cfg.alt));
|
al = Math.round(parseFloat(al)/parseFloat(cfg.alt));
|
||||||
|
@ -410,8 +439,11 @@ if ( emulator ) {
|
||||||
|
|
||||||
// Distance to waypoint
|
// Distance to waypoint
|
||||||
di = distance(lf,wp);
|
di = distance(lf,wp);
|
||||||
if (isNaN(di)) di = '--------';
|
di = (di/parseFloat(cfg.dist)).toFixed(2);
|
||||||
|
if ( di >= 100 ) di = parseFloat(di).toFixed(1);
|
||||||
|
if ( di >= 1000 ) di = parseFloat(di).toFixed(0);
|
||||||
|
if (isNaN(di)) di = '------';
|
||||||
|
|
||||||
// Age of last fix (secs)
|
// Age of last fix (secs)
|
||||||
age = Math.max(0,Math.round(getTime())-(lf.time.getTime()/1000));
|
age = Math.max(0,Math.round(getTime())-(lf.time.getTime()/1000));
|
||||||
|
|
||||||
|
@ -433,9 +465,30 @@ if ( emulator ) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bluetooth send data
|
||||||
|
btSend({
|
||||||
|
id:'speedalt2',
|
||||||
|
v:v,
|
||||||
|
vd:vDroid,
|
||||||
|
m:cfg.modeA,
|
||||||
|
spd_unit:cfg.spd_unit,
|
||||||
|
alt_unit:cfg.alt_unit,
|
||||||
|
dist_unit:cfg.dist_unit,
|
||||||
|
wp:wpName,
|
||||||
|
sp:sp,
|
||||||
|
al:al,
|
||||||
|
di:di,
|
||||||
|
sats:sats,
|
||||||
|
vmg:vmg,
|
||||||
|
lat:lat,
|
||||||
|
lon:lon,
|
||||||
|
ns:ns,
|
||||||
|
ew:ew
|
||||||
|
});
|
||||||
|
|
||||||
if ( cfg.modeA == 0 ) {
|
if ( cfg.modeA == 0 ) {
|
||||||
// Speed
|
// Speed
|
||||||
if ( showMax )
|
if ( showMax ) {
|
||||||
drawScrn({
|
drawScrn({
|
||||||
val:maxSpd,
|
val:maxSpd,
|
||||||
unit:cfg.spd_unit,
|
unit:cfg.spd_unit,
|
||||||
|
@ -444,7 +497,8 @@ if ( emulator ) {
|
||||||
max:'MAX',
|
max:'MAX',
|
||||||
wp:''
|
wp:''
|
||||||
}); // Speed maximums
|
}); // Speed maximums
|
||||||
else
|
}
|
||||||
|
else {
|
||||||
drawScrn({
|
drawScrn({
|
||||||
val:sp,
|
val:sp,
|
||||||
unit:cfg.spd_unit,
|
unit:cfg.spd_unit,
|
||||||
|
@ -453,11 +507,12 @@ if ( emulator ) {
|
||||||
max:'SPD',
|
max:'SPD',
|
||||||
wp:''
|
wp:''
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( cfg.modeA == 1 ) {
|
if ( cfg.modeA == 1 ) {
|
||||||
// Alt
|
// Alt
|
||||||
if ( showMax )
|
if ( showMax ) {
|
||||||
drawScrn({
|
drawScrn({
|
||||||
val:maxAlt,
|
val:maxAlt,
|
||||||
unit:cfg.alt_unit,
|
unit:cfg.alt_unit,
|
||||||
|
@ -466,7 +521,8 @@ if ( emulator ) {
|
||||||
max:'MAX',
|
max:'MAX',
|
||||||
wp:''
|
wp:''
|
||||||
}); // Alt maximums
|
}); // Alt maximums
|
||||||
else
|
}
|
||||||
|
else {
|
||||||
drawScrn({
|
drawScrn({
|
||||||
val:al,
|
val:al,
|
||||||
unit:cfg.alt_unit,
|
unit:cfg.alt_unit,
|
||||||
|
@ -475,6 +531,7 @@ if ( emulator ) {
|
||||||
max:'ALT',
|
max:'ALT',
|
||||||
wp:''
|
wp:''
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( cfg.modeA == 2 ) {
|
if ( cfg.modeA == 2 ) {
|
||||||
|
@ -490,6 +547,18 @@ if ( emulator ) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( cfg.modeA == 3 ) {
|
if ( cfg.modeA == 3 ) {
|
||||||
|
// VMG
|
||||||
|
drawScrn({
|
||||||
|
val:vmg,
|
||||||
|
unit:cfg.spd_unit,
|
||||||
|
sats:sats,
|
||||||
|
age:age,
|
||||||
|
max:'VMG',
|
||||||
|
wp:wpName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( cfg.modeA == 4 ) {
|
||||||
// Position
|
// Position
|
||||||
drawPosn({
|
drawPosn({
|
||||||
sats:sats,
|
sats:sats,
|
||||||
|
@ -501,7 +570,7 @@ if ( emulator ) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( cfg.modeA == 4 ) {
|
if ( cfg.modeA == 5 ) {
|
||||||
// Large clock
|
// Large clock
|
||||||
drawClock();
|
drawClock();
|
||||||
}
|
}
|
||||||
|
@ -510,14 +579,14 @@ if ( emulator ) {
|
||||||
|
|
||||||
function prevScrn() {
|
function prevScrn() {
|
||||||
cfg.modeA = cfg.modeA-1;
|
cfg.modeA = cfg.modeA-1;
|
||||||
if ( cfg.modeA < 0 ) cfg.modeA = 4;
|
if ( cfg.modeA < 0 ) cfg.modeA = 5;
|
||||||
savSettings();
|
savSettings();
|
||||||
onGPS(lf);
|
onGPS(lf);
|
||||||
}
|
}
|
||||||
|
|
||||||
function nextScrn() {
|
function nextScrn() {
|
||||||
cfg.modeA = cfg.modeA+1;
|
cfg.modeA = cfg.modeA+1;
|
||||||
if ( cfg.modeA > 4 ) cfg.modeA = 0;
|
if ( cfg.modeA > 5 ) cfg.modeA = 0;
|
||||||
savSettings();
|
savSettings();
|
||||||
onGPS(lf);
|
onGPS(lf);
|
||||||
}
|
}
|
||||||
|
@ -529,14 +598,14 @@ function nextFunc(dur) {
|
||||||
if ( dur < 2 ) showMax = !showMax; // Short press toggle fix/max display
|
if ( dur < 2 ) showMax = !showMax; // Short press toggle fix/max display
|
||||||
else { maxSpd = 0; maxAlt = 0; } // Long press resets max values.
|
else { maxSpd = 0; maxAlt = 0; } // Long press resets max values.
|
||||||
}
|
}
|
||||||
else if ( cfg.modeA == 2) nxtWp(); // Dist mode - Select next waypoint
|
else if ( cfg.modeA == 2 || cfg.modeA == 3) nxtWp(); // Dist or VMG mode - Select next waypoint
|
||||||
onGPS(lf);
|
onGPS(lf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function updateClock() {
|
function updateClock() {
|
||||||
if (!canDraw) return;
|
if (!canDraw) return;
|
||||||
if ( cfg.modeA != 4 ) return;
|
if ( cfg.modeA != 5 ) return;
|
||||||
drawClock();
|
drawClock();
|
||||||
if ( emulator ) {maxSpd++;maxAlt++;}
|
if ( emulator ) {maxSpd++;maxAlt++;}
|
||||||
}
|
}
|
||||||
|
@ -551,6 +620,7 @@ function startDraw(){
|
||||||
|
|
||||||
function stopDraw() {
|
function stopDraw() {
|
||||||
canDraw=false;
|
canDraw=false;
|
||||||
|
if ( bt ) return; // If bt screen mirror to Droidscript in use then keep GPS in SuperE mode to keep screen updates going.
|
||||||
if (!tmrLP) tmrLP=setInterval(function () {if (lf.fix) setLpMode('PSMOO');}, 10000); //Drop to low power in 10 secs. Keep lp mode off until we have a first fix.
|
if (!tmrLP) tmrLP=setInterval(function () {if (lf.fix) setLpMode('PSMOO');}, 10000); //Drop to low power in 10 secs. Keep lp mode off until we have a first fix.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -564,6 +634,20 @@ function setLpMode(m) {
|
||||||
gpssetup.setPowerMode({power_mode:m});
|
gpssetup.setPowerMode({power_mode:m});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// == Droidscript bluetooth data
|
||||||
|
|
||||||
|
function btOn(b) {
|
||||||
|
bt = b; // Turn data transmit on/off
|
||||||
|
}
|
||||||
|
|
||||||
|
function btSend(dat) {
|
||||||
|
if ( ! bt ) return; // bt transmit off
|
||||||
|
var dur = getTime() - btLast;
|
||||||
|
if ( dur < 1.0 ) return; // Don't need to transmit more than every 1.0 secs.
|
||||||
|
btLast = getTime();
|
||||||
|
console.log(JSON.stringify(dat)); // transmit the data
|
||||||
|
}
|
||||||
|
|
||||||
// == Events
|
// == Events
|
||||||
|
|
||||||
function setButtons(){
|
function setButtons(){
|
||||||
|
@ -593,17 +677,6 @@ function setButtons(){
|
||||||
setWatch(function(e){
|
setWatch(function(e){
|
||||||
nextScrn();
|
nextScrn();
|
||||||
}, BTN3, {repeat:true,edge:"falling"});
|
}, BTN3, {repeat:true,edge:"falling"});
|
||||||
|
|
||||||
/*
|
|
||||||
// Touch screen same as BTN1 short
|
|
||||||
setWatch(function(e){
|
|
||||||
nextFunc(1); // Same as BTN1 short
|
|
||||||
}, BTN4, {repeat:true,edge:"falling"});
|
|
||||||
setWatch(function(e){
|
|
||||||
nextFunc(1); // Same as BTN1 short
|
|
||||||
}, BTN5, {repeat:true,edge:"falling"});
|
|
||||||
*/
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Bangle.on('lcdPower',function(on) {
|
Bangle.on('lcdPower',function(on) {
|
||||||
|
@ -621,40 +694,22 @@ Bangle.on('swipe',function(dir) {
|
||||||
Bangle.on('touch', function(button){
|
Bangle.on('touch', function(button){
|
||||||
if ( ! cfg.touch ) return;
|
if ( ! cfg.touch ) return;
|
||||||
nextFunc(0); // Same function as short BTN1
|
nextFunc(0); // Same function as short BTN1
|
||||||
/*
|
});
|
||||||
switch(button){
|
|
||||||
case 1: // BTN4
|
|
||||||
console.log('BTN4');
|
|
||||||
prevScrn();
|
|
||||||
break;
|
|
||||||
case 2: // BTN5
|
|
||||||
console.log('BTN5');
|
|
||||||
nextScrn();
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
console.log('MDL');
|
|
||||||
nextFunc(0); // Centre - same function as short BTN1
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// == Main Prog
|
// == Main Prog
|
||||||
|
|
||||||
// Read settings.
|
// Read settings.
|
||||||
let cfg = require('Storage').readJSON('speedalt2.json',1)||{};
|
let cfg = require('Storage').readJSON('speedalt2.json',1)||{};
|
||||||
|
|
||||||
cfg.spd = cfg.spd||0; // Multiplier for speed unit conversions. 0 = use the locale values for speed
|
cfg.spd = cfg.spd||1; // Multiplier for speed unit conversions. 0 = use the locale values for speed
|
||||||
cfg.spd_unit = cfg.spd_unit||''; // Displayed speed unit
|
cfg.spd_unit = cfg.spd_unit||'kph'; // Displayed speed unit
|
||||||
cfg.alt = cfg.alt||0.3048;// Multiplier for altitude unit conversions.
|
cfg.alt = cfg.alt||0.3048;// Multiplier for altitude unit conversions.
|
||||||
cfg.alt_unit = cfg.alt_unit||'feet'; // Displayed altitude units
|
cfg.alt_unit = cfg.alt_unit||'feet'; // Displayed altitude units
|
||||||
cfg.dist = cfg.dist||1000;// Multiplier for distnce unit conversions.
|
cfg.dist = cfg.dist||1000;// Multiplier for distnce unit conversions.
|
||||||
cfg.dist_unit = cfg.dist_unit||'km'; // Displayed altitude units
|
cfg.dist_unit = cfg.dist_unit||'km'; // Displayed altitude units
|
||||||
cfg.colour = cfg.colour||0; // Colour scheme.
|
cfg.colour = cfg.colour||0; // Colour scheme.
|
||||||
cfg.wp = cfg.wp||0; // Last selected waypoint for dist
|
cfg.wp = cfg.wp||0; // Last selected waypoint for dist
|
||||||
cfg.modeA = cfg.modeA||0; // 0=Speed 1=Alt 2=Dist 3=Position 4=Clock
|
cfg.modeA = cfg.modeA||0; // 0=Speed 1=Alt 2=Dist 3 = vmg 4=Position 5=Clock
|
||||||
cfg.primSpd = cfg.primSpd||0; // 1 = Spd in primary, 0 = Spd in secondary
|
cfg.primSpd = cfg.primSpd||0; // 1 = Spd in primary, 0 = Spd in secondary
|
||||||
|
|
||||||
cfg.spdFilt = cfg.spdFilt==undefined?true:cfg.spdFilt;
|
cfg.spdFilt = cfg.spdFilt==undefined?true:cfg.spdFilt;
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"id": "speedalt2",
|
"id": "speedalt2",
|
||||||
"name": "GPS Adventure Sports II",
|
"name": "GPS Adventure Sports II",
|
||||||
"shortName":"GPS Adv Sport II",
|
"shortName":"GPS Adv Sport II",
|
||||||
"version":"1.10",
|
"version":"1.46",
|
||||||
"description": "GPS speed, altitude and distance to waypoint display. Designed for easy viewing and use during outdoor activities such as para-gliding, hang-gliding, sailing, cycling etc.",
|
"description": "GPS speed, altitude and distance to waypoint display. Designed for easy viewing and use during outdoor activities such as para-gliding, hang-gliding, sailing, cycling etc.",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"type": "app",
|
"type": "app",
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 1.3 MiB |
|
@ -48,7 +48,7 @@
|
||||||
const unitsMenu = {
|
const unitsMenu = {
|
||||||
'': {'title': 'Units'},
|
'': {'title': 'Units'},
|
||||||
'< Back': function() { E.showMenu(appMenu); },
|
'< Back': function() { E.showMenu(appMenu); },
|
||||||
'default (spd)' : function() { setUnits(0,''); },
|
// 'default (spd)' : function() { setUnits(0,''); },
|
||||||
'Kph (spd)' : function() { setUnits(1,'kph'); },
|
'Kph (spd)' : function() { setUnits(1,'kph'); },
|
||||||
'Knots (spd)' : function() { setUnits(1.852,'kts'); },
|
'Knots (spd)' : function() { setUnits(1.852,'kts'); },
|
||||||
'Mph (spd)' : function() { setUnits(1.60934,'mph'); },
|
'Mph (spd)' : function() { setUnits(1.60934,'mph'); },
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
0.01: New App!
|
0.01: New App!
|
||||||
0.02: Change start sequence to BTN1/3/1/3 to avoid accidental turning on (fix #342)
|
0.02: Change start sequence to BTN1/3/1/3 to avoid accidental turning on (fix #342)
|
||||||
0.03: Add Color Changing Settings
|
0.03: Add Color Changing Settings
|
||||||
|
0.04: Add Support For Bangle.js 2
|
||||||
|
|
|
@ -14,5 +14,6 @@ g.setColor(settings.bg);
|
||||||
g.fillRect(0,0,g.getWidth(),g.getHeight());
|
g.fillRect(0,0,g.getWidth(),g.getHeight());
|
||||||
// Any button turns off
|
// Any button turns off
|
||||||
setWatch(()=>load(), BTN1);
|
setWatch(()=>load(), BTN1);
|
||||||
setWatch(()=>load(), BTN2);
|
if (global.BTN2) setWatch(()=>load(), BTN2);
|
||||||
setWatch(()=>load(), BTN3);
|
if (global.BTN3) setWatch(()=>load(), BTN3);
|
||||||
|
|
||||||
|
|
|
@ -2,14 +2,14 @@
|
||||||
"id": "torch",
|
"id": "torch",
|
||||||
"name": "Torch",
|
"name": "Torch",
|
||||||
"shortName": "Torch",
|
"shortName": "Torch",
|
||||||
"version": "0.03",
|
"version": "0.04",
|
||||||
"description": "Turns screen white to help you see in the dark. Select from the launcher or press BTN1,BTN3,BTN1,BTN3 quickly to start when in any app that shows widgets. You can also set the color through the apps settings menu.",
|
"description": "Turns screen white to help you see in the dark. Select from the launcher or press BTN1,BTN3,BTN1,BTN3 quickly to start when in any app that shows widgets on Bangle.js 1. You can also set the color through the app's setting menu.",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"tags": "tool,torch",
|
"tags": "tool,torch",
|
||||||
"supports": ["BANGLEJS"],
|
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||||
"storage": [
|
"storage": [
|
||||||
{"name":"torch.app.js","url":"app.js"},
|
{"name":"torch.app.js","url":"app.js"},
|
||||||
{"name":"torch.wid.js","url":"widget.js"},
|
{"name":"torch.wid.js","url":"widget.js","supports": ["BANGLEJS"]},
|
||||||
{"name":"torch.img","url":"app-icon.js","evaluate":true},
|
{"name":"torch.img","url":"app-icon.js","evaluate":true},
|
||||||
{"name":"torch.settings.js","url":"settings.js"}
|
{"name":"torch.settings.js","url":"settings.js"}
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
0.01: Created
|
0.01: Created
|
||||||
0.02: Set sort order to -10 so always display in right hand corner
|
0.02: Set sort order to -10 so always display in right hand corner
|
||||||
0.03: Set sort order from the code
|
0.03: Set sort order from the code
|
||||||
|
0.04: fix vertical align
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
"shortName":"Battery Theme",
|
"shortName":"Battery Theme",
|
||||||
"icon": "widbata.png",
|
"icon": "widbata.png",
|
||||||
"screenshots": [{"url":"screenshot_widbata_1.png"}],
|
"screenshots": [{"url":"screenshot_widbata_1.png"}],
|
||||||
"version":"0.03",
|
"version":"0.04",
|
||||||
"type": "widget",
|
"type": "widget",
|
||||||
"supports": ["BANGLEJS", "BANGLEJS2"],
|
"supports": ["BANGLEJS", "BANGLEJS2"],
|
||||||
"readme": "README.md",
|
"readme": "README.md",
|
||||||
|
|
|
@ -5,7 +5,7 @@ Bangle.on('lcdPower', function(on) {
|
||||||
WIDGETS["bata"]={area:"tr",sortorder:-10,width:27,draw:function() {
|
WIDGETS["bata"]={area:"tr",sortorder:-10,width:27,draw:function() {
|
||||||
var s = 26;
|
var s = 26;
|
||||||
var t = 13; // thickness
|
var t = 13; // thickness
|
||||||
var x = this.x, y = this.y;
|
var x = this.x, y = this.y + 5;
|
||||||
g.reset();
|
g.reset();
|
||||||
g.setColor(g.theme.fg);
|
g.setColor(g.theme.fg);
|
||||||
g.fillRect(x,y+2,x+s-4,y+2+t); // outer
|
g.fillRect(x,y+2,x+s-4,y+2+t); // outer
|
||||||
|
|
|
@ -157,19 +157,29 @@ log(untranslatedStrings.filter(e => e.uses>2).filter(e => !translatedStrings.fin
|
||||||
log("");
|
log("");
|
||||||
//process.exit(1);
|
//process.exit(1);
|
||||||
|
|
||||||
var languages = JSON.parse(fs.readFileSync(BASEDIR+"/lang/index.json").toString());
|
let languages = JSON.parse(fs.readFileSync(`${BASEDIR}/lang/index.json`).toString());
|
||||||
languages.forEach(language => {
|
languages.forEach(language => {
|
||||||
if (language.code=="en_GB") {
|
if (language.code == "en_GB") {
|
||||||
console.log("Ignoring "+language.code);
|
console.log(`Ignoring ${language.code}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log("Scanning "+language.code);
|
console.log(`Scanning ${language.code}`);
|
||||||
log(language.code);
|
log(language.code);
|
||||||
log("==========");
|
log("==========");
|
||||||
var translations = JSON.parse(fs.readFileSync(BASEDIR+"/lang/"+language.url).toString());
|
let translations = JSON.parse(fs.readFileSync(`${BASEDIR}/lang/${language.url}`).toString());
|
||||||
translatedStrings.forEach(str => {
|
translatedStrings.forEach(translationItem => {
|
||||||
if (!translations.GLOBAL[str.str])
|
if (!translations.GLOBAL[translationItem.str]) {
|
||||||
console.log(`Missing translation for ${JSON.stringify(str)}`);
|
console.log(`Missing GLOBAL translation for ${JSON.stringify(translationItem)}`);
|
||||||
|
translationItem.files.forEach(file => {
|
||||||
|
let m = file.match(/\/([a-zA-Z0-9_-]*)\//g);
|
||||||
|
if (m && m[0]) {
|
||||||
|
let appName = m[0].replaceAll("/", "");
|
||||||
|
if (translations[appName] && translations[appName][translationItem.str]) {
|
||||||
|
console.log(` but LOCAL translation found in \"${appName}\"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
log("");
|
log("");
|
||||||
});
|
});
|
||||||
|
|
2
core
2
core
|
@ -1 +1 @@
|
||||||
Subproject commit c243e6e71f88358de720ad16ba8515b32b8d650f
|
Subproject commit 187af1527e0b830c804049aae834ed658ffeed08
|
159
lang/it_IT.json
159
lang/it_IT.json
|
@ -1,21 +1,148 @@
|
||||||
{
|
{
|
||||||
"//":"Italian language translations",
|
"//1": "Italian language translations",
|
||||||
"GLOBAL": {
|
"GLOBAL": {
|
||||||
"//":"Translations that apply for all apps",
|
"//": "Translations that apply for all apps",
|
||||||
"Alarms" : "Sveglie",
|
"On": "On",
|
||||||
"Hours" : "Ore",
|
"on": "on",
|
||||||
"Minutes" : "Minuti",
|
"Off": "Off",
|
||||||
"Enabled" : "Attiva",
|
"off": "off",
|
||||||
"New Alarm" : "Nuova sveglia",
|
"Ok": "Ok",
|
||||||
"Save" : "Salva",
|
"Yes": "Sì",
|
||||||
"Back" : "Indietro",
|
"No": "No",
|
||||||
"Repeat" : "Ripeti",
|
"Alarm": "Sveglia",
|
||||||
"Delete" : "Cancella",
|
"ALARM": "SVEGLIA",
|
||||||
"ALARM!" : "SVEGLIA!",
|
"Alarms": "Sveglie",
|
||||||
"Sleep" : "Dormi"
|
"Date": "Data",
|
||||||
|
"Year": "Anno",
|
||||||
|
"Month": "Mese",
|
||||||
|
"Day": "Giorno",
|
||||||
|
"Hour": "Ora",
|
||||||
|
"Hours": "Ore",
|
||||||
|
"Minute": "Minuto",
|
||||||
|
"Minutes": "Minuti",
|
||||||
|
"Second": "Secondo",
|
||||||
|
"Seconds": "Secondi",
|
||||||
|
"week": "settimana",
|
||||||
|
"Week": "Settimana",
|
||||||
|
"Enabled": "Attivo/a",
|
||||||
|
"New Alarm": "Nuova sveglia",
|
||||||
|
"Save": "Salva",
|
||||||
|
"Cancel": "Annulla",
|
||||||
|
"Back": "Indietro",
|
||||||
|
"Repeat": "Ripeti",
|
||||||
|
"Delete": "Cancella",
|
||||||
|
"ALARM!": "SVEGLIA!",
|
||||||
|
"Sleep": "Dormi",
|
||||||
|
"Timer": "Timer",
|
||||||
|
"TIMER": "TIMER",
|
||||||
|
"New Timer": "Nuovo timer",
|
||||||
|
"(repeat)": "(ripeti)",
|
||||||
|
"Auto snooze": "Posticipa automaticamente",
|
||||||
|
"Connected": "Connesso",
|
||||||
|
"Delete all messages": "Cancella tutti i messaggi",
|
||||||
|
"Delete All Messages": "Cancella tutti i messaggi",
|
||||||
|
"Message": "Messaggio",
|
||||||
|
"Messages": "Messaggi",
|
||||||
|
"No Messages": "Nessun messaggio",
|
||||||
|
"Keep Msgs": "Tieni i messaggi",
|
||||||
|
"Mark Unread": "Segna come non letto",
|
||||||
|
"Vibrate": "Vibrazione",
|
||||||
|
"Are you sure": "Sei sicuro/a",
|
||||||
|
"Music": "Musica",
|
||||||
|
"Apps": "App",
|
||||||
|
"App Settings": "Impostazioni app",
|
||||||
|
"Bluetooth": "Bluetooth",
|
||||||
|
"BLE": "BLE",
|
||||||
|
"Make Connectable": "Rendi collegabile",
|
||||||
|
"Programmable": "Programmabile",
|
||||||
|
"Remove": "Rimuovi",
|
||||||
|
"Utils": "Utilità",
|
||||||
|
"LCD": "LCD",
|
||||||
|
"LCD Brightness": "Luminosità LCD",
|
||||||
|
"LCD Timeout": "Timeout LCD",
|
||||||
|
"Wake on BTN1": "Risveglia con BTN1",
|
||||||
|
"Wake on BTN2": "Risveglia con BTN2",
|
||||||
|
"Wake on BTN3": "Risveglia con BTN3",
|
||||||
|
"Wake on FaceUp": "Risveglia a faccia in su",
|
||||||
|
"Wake on Touch": "Risveglia al tocco",
|
||||||
|
"Wake on Twist": "Risveglia con polso",
|
||||||
|
"Twist Timeout": "Timeout torsione",
|
||||||
|
"Twist Max Y": "Torsione Y max",
|
||||||
|
"Twist Threshold": "Soglia torsione",
|
||||||
|
"Customize": "Personalizza",
|
||||||
|
"Add Device": "Aggiungi dispositivo",
|
||||||
|
"Left": "Sinistra",
|
||||||
|
"Right": "Destra",
|
||||||
|
"Widgets": "Widget",
|
||||||
|
"Settings": "Impostazioni",
|
||||||
|
"No app has settings": "Non ci sono app con delle impostazioni",
|
||||||
|
"System": "Sistema",
|
||||||
|
"Alerts": "Avvisi",
|
||||||
|
"Theme": "Tema",
|
||||||
|
"Foreground": "Primo piano",
|
||||||
|
"Background": "Sfondo",
|
||||||
|
"Foreground 2": "Primo piano 2",
|
||||||
|
"Background 2": "Sfondo 2",
|
||||||
|
"Highlight FG": "Selezione PP",
|
||||||
|
"Highlight BG": "Selezione Sf",
|
||||||
|
"Utilities": "Utilità",
|
||||||
|
"Storage": "Memoria",
|
||||||
|
"Compact Storage": "Compatta memoria",
|
||||||
|
"Select Clock": "Seleziona orologio",
|
||||||
|
"No Clocks Found": "Nessun orologio trovato",
|
||||||
|
"Locale": "Localizzazione",
|
||||||
|
"Set Time": "Imposta orario",
|
||||||
|
"Time Zone": "Fuso orario",
|
||||||
|
"Whitelist": "Whitelist",
|
||||||
|
"Quiet Mode": "Modalità silenziosa",
|
||||||
|
"Disable": "Disabilita",
|
||||||
|
"Vibration": "Vibrazione",
|
||||||
|
"Show": "Mostra",
|
||||||
|
"Hide": "Nascondi",
|
||||||
|
"Rewrite Settings": "Riscrivi impostazioni",
|
||||||
|
"Reset Settings": "Reset impostazioni",
|
||||||
|
"Factory Reset": "Ripristino condizioni iniziali",
|
||||||
|
"Flatten Battery": "Scarica la batteria",
|
||||||
|
"Turn Off": "Spegni",
|
||||||
|
"This will remove everything": "Questo rimuoverà TUTTO",
|
||||||
|
"Error in settings": "Errore nelle impostazioni",
|
||||||
|
"Invalid settings": "Impostazioni non valide",
|
||||||
|
"Loading": "Caricamento",
|
||||||
|
"Launcher Settings": "Impostazioni Launcher",
|
||||||
|
"Font": "Font",
|
||||||
|
"Show clocks": "Mostra orologi",
|
||||||
|
"Log": "Log",
|
||||||
|
"Steps": "Passi",
|
||||||
|
"steps": "passi"
|
||||||
},
|
},
|
||||||
"alarm": {
|
"//2": "App-specific overrides",
|
||||||
"//":"App-specific overrides",
|
"launch": {
|
||||||
"rpt" : "ripeti"
|
"Vector font size": "Dim. font vett.",
|
||||||
|
"App Source\nNot found": "Codice app\nnon trovato"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"Unread timer": "Timer msg non letti"
|
||||||
|
},
|
||||||
|
"run": {
|
||||||
|
"Record Run": "Registra corsa"
|
||||||
|
},
|
||||||
|
"setting": {
|
||||||
|
"Clock Style": "Formato ora",
|
||||||
|
"Compacting...\nTakes approx\n1 minute": "Compattamento in corso...\nCi vorrà circa un minuto",
|
||||||
|
"//1": "The new line before 'operazione' improves the layout",
|
||||||
|
"Flattening battery - this can take hours.\nLong-press button to cancel": "Scaricamento batteria in corso - l'\noperazione può richiedere ore. Tieni premuto il pulsante per annullare",
|
||||||
|
"Reset to Defaults": "Ripristinare le impostazioni predefinite",
|
||||||
|
"Connectable": "Collegamento",
|
||||||
|
"Connect device\nto add to\nwhitelist": "Collega un\ndispositivo\nper metterlo\nin whitelist",
|
||||||
|
"Stay Connectable": "Rimanere collegabile",
|
||||||
|
"Light BW": "Chiaro",
|
||||||
|
"Dark BW": "Scuro"
|
||||||
|
},
|
||||||
|
"wid_edit": {
|
||||||
|
"Reset": "Ripristina",
|
||||||
|
"Reset All": "Ripristina tutto",
|
||||||
|
"Reset all widgets": "Ripristina tutti i widget",
|
||||||
|
"Sort Order": "Ordinamento",
|
||||||
|
"Side": "Lato"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -164,7 +164,7 @@ function Layout(layout, options) {
|
||||||
|
|
||||||
// Handler for touch events
|
// Handler for touch events
|
||||||
function touchHandler(l,e) {
|
function touchHandler(l,e) {
|
||||||
if (l.type=="btn" && l.cb && e.x>=l.x && e.y>=l.y && e.x<=l.x+l.w && e.y<=l.y+l.h) {
|
if (l.cb && e.x>=l.x && e.y>=l.y && e.x<=l.x+l.w && e.y<=l.y+l.h) {
|
||||||
if (e.type==2 && l.cbl) l.cbl(e); else if (l.cb) l.cb(e);
|
if (e.type==2 && l.cbl) l.cbl(e); else if (l.cb) l.cb(e);
|
||||||
}
|
}
|
||||||
if (l.c) l.c.forEach(n => touchHandler(n,e));
|
if (l.c) l.c.forEach(n => touchHandler(n,e));
|
||||||
|
|
Loading…
Reference in New Issue