Merge branch 'espruino:master' into master

master
Jelco Huijser 2022-03-25 00:43:34 +01:00 committed by GitHub
commit efc7507be8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 794 additions and 370 deletions

140
apps/2047pp/2047pp.app.js Normal file
View File

@ -0,0 +1,140 @@
class TwoK {
constructor() {
this.b = Array(4).fill().map(() => Array(4).fill(0));
this.score = 0;
this.cmap = {0: "#caa", 2:"#ccc", 4: "#bcc", 8: "#ba6", 16: "#e61", 32: "#d20", 64: "#d00", 128: "#da0", 256: "#ec0", 512: "#dd0"};
}
drawBRect(x1, y1, x2, y2, th, c, cf, fill) {
g.setColor(c);
for (i=0; i<th; ++i) g.drawRect(x1+i, y1+i, x2-i, y2-i);
if (fill) g.setColor(cf).fillRect(x1+th, y1+th, x2-th, y2-th);
}
render() {
const yo = 20;
const xo = yo/2;
h = g.getHeight()-yo;
w = g.getWidth()-yo;
bh = Math.floor(h/4);
bw = Math.floor(w/4);
g.clearRect(0, 0, g.getWidth()-1, yo).setFontAlign(0, 0, 0);
g.setFont("Vector", 16).setColor("#fff").drawString("Score:"+this.score.toString(), g.getWidth()/2, 8);
this.drawBRect(xo-3, yo-3, xo+w+2, yo+h+2, 4, "#a88", "#caa", false);
for (y=0; y<4; ++y)
for (x=0; x<4; ++x) {
b = this.b[y][x];
this.drawBRect(xo+x*bw, yo+y*bh-1, xo+(x+1)*bh-1, yo+(y+1)*bh-2, 4, "#a88", this.cmap[b], true);
if (b > 4) g.setColor(1, 1, 1);
else g.setColor(0, 0, 0);
g.setFont("Vector", bh*(b>8 ? (b>64 ? (b>512 ? 0.32 : 0.4) : 0.6) : 0.7));
if (b>0) g.drawString(b.toString(), xo+(x+0.5)*bw+1, yo+(y+0.5)*bh);
}
}
shift(d) { // +/-1: shift x, +/- 2: shift y
var crc = E.CRC32(this.b.toString());
if (d==-1) { // shift x left
for (y=0; y<4; ++y) {
for (x=2; x>=0; x--)
if (this.b[y][x]==0) {
for (i=x; i<3; i++) this.b[y][i] = this.b[y][i+1];
this.b[y][3] = 0;
}
for (x=0; x<3; ++x)
if (this.b[y][x]==this.b[y][x+1]) {
this.score += 2*this.b[y][x];
this.b[y][x] += this.b[y][x+1];
for (j=x+1; j<3; ++j) this.b[y][j] = this.b[y][j+1];
this.b[y][3] = 0;
}
}
}
else if (d==1) { // shift x right
for (y=0; y<4; ++y) {
for (x=1; x<4; x++)
if (this.b[y][x]==0) {
for (i=x; i>0; i--) this.b[y][i] = this.b[y][i-1];
this.b[y][0] = 0;
}
for (x=3; x>0; --x)
if (this.b[y][x]==this.b[y][x-1]) {
this.score += 2*this.b[y][x];
this.b[y][x] += this.b[y][x-1] ;
for (j=x-1; j>0; j--) this.b[y][j] = this.b[y][j-1];
this.b[y][0] = 0;
}
}
}
else if (d==-2) { // shift y down
for (x=0; x<4; ++x) {
for (y=1; y<4; y++)
if (this.b[y][x]==0) {
for (i=y; i>0; i--) this.b[i][x] = this.b[i-1][x];
this.b[0][x] = 0;
}
for (y=3; y>0; y--)
if (this.b[y][x]==this.b[y-1][x] || this.b[y][x]==0) {
this.score += 2*this.b[y][x];
this.b[y][x] += this.b[y-1][x];
for (j=y-1; j>0; j--) this.b[j][x] = this.b[j-1][x];
this.b[0][x] = 0;
}
}
}
else if (d==2) { // shift y up
for (x=0; x<4; ++x) {
for (y=2; y>=0; y--)
if (this.b[y][x]==0) {
for (i=y; i<3; i++) this.b[i][x] = this.b[i+1][x];
this.b[3][x] = 0;
}
for (y=0; y<3; ++y)
if (this.b[y][x]==this.b[y+1][x] || this.b[y][x]==0) {
this.score += 2*this.b[y][x];
this.b[y][x] += this.b[y+1][x];
for (j=y+1; j<3; ++j) this.b[j][x] = this.b[j+1][x];
this.b[3][x] = 0;
}
}
}
return (E.CRC32(this.b.toString())!=crc);
}
addDigit() {
var d = Math.random()>0.9 ? 4 : 2;
var id = Math.floor(Math.random()*16);
while (this.b[Math.floor(id/4)][id%4] > 0) id = Math.floor(Math.random()*16);
this.b[Math.floor(id/4)][id%4] = d;
}
}
function dragHandler(e) {
if (e.b && (Math.abs(e.dx)>7 || Math.abs(e.dy)>7)) {
var res = false;
if (Math.abs(e.dx)>Math.abs(e.dy)) {
if (e.dx>0) res = twok.shift(1);
if (e.dx<0) res = twok.shift(-1);
}
else {
if (e.dy>0) res = twok.shift(-2);
if (e.dy<0) res = twok.shift(2);
}
if (res) twok.addDigit();
twok.render();
}
}
function swipeHandler() {
}
function buttonHandler() {
}
var twok = new TwoK();
twok.addDigit(); twok.addDigit();
twok.render();
if (process.env.HWVERSION==2) Bangle.on("drag", dragHandler);
if (process.env.HWVERSION==1) {
Bangle.on("swipe", (e) => { res = twok.shift(e); if (res) twok.addDigit(); twok.render(); });
setWatch(() => { res = twok.shift(2); if (res) twok.addDigit(); twok.render(); }, BTN1, {repeat: true});
setWatch(() => { res = twok.shift(-2); if (res) twok.addDigit(); twok.render(); }, BTN3, {repeat: true});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

9
apps/2047pp/README.md Normal file
View File

@ -0,0 +1,9 @@
# Game of 2047pp (2047++)
Tile shifting game inspired by the well known 2048 game. Also very similar to another Bangle game, Game1024.
Attempt to combine equal numbers by swiping left, right, up or down (on Bangle 2) or swiping left/right and using
the top/bottom button (Bangle 1).
![Screenshot](./2047pp_screenshot.png)

1
apps/2047pp/app-icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A31gAeFtoxPF9wujGBYQG1YAWF6ur5gAYGIovOFzIABF6ReaMAwv/F/4v/F7ejv9/0Yvq1Eylksv4vqvIuBF9ZeDF9ZeBqovr1AsB0YvrLwXMF9ReDF9ZeBq1/v4vBqowKF7lWFYIAFF/7vXAAa/qF+jxB0YvsABov/F/4v/F6WsF7YgEF5xgaLwgvPGIQAWDwwvQADwvJGEguKF+AxhFpoA/AH4A/AFI="))

BIN
apps/2047pp/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 759 B

15
apps/2047pp/metadata.json Normal file
View File

@ -0,0 +1,15 @@
{ "id": "2047pp",
"name": "2047pp",
"shortName":"2047pp",
"icon": "app.png",
"version":"0.01",
"description": "Bangle version of a tile shifting game",
"supports" : ["BANGLEJS","BANGLEJS2"],
"allow_emulator": true,
"readme": "README.md",
"tags": "game",
"storage": [
{"name":"2047pp.app.js","url":"2047pp.app.js"},
{"name":"2047pp.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -4,3 +4,4 @@
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 0.05: Add support for bulk importing and exporting tokens
0.06: Add spaces to codes for improved readability (thanks @BartS23) 0.06: Add spaces to codes for improved readability (thanks @BartS23)
0.07: Bangle 2: Improve drag responsiveness and exit on button press

View File

@ -1,6 +1,10 @@
const tokenextraheight = 16; const COUNTER_TRIANGLE_SIZE = 10;
var tokendigitsheight = 30; const TOKEN_EXTRA_HEIGHT = 16;
var tokenheight = tokendigitsheight + tokenextraheight; var TOKEN_DIGITS_HEIGHT = 30;
var TOKEN_HEIGHT = TOKEN_DIGITS_HEIGHT + TOKEN_EXTRA_HEIGHT;
const PROGRESSBAR_HEIGHT = 3;
const IDLE_REPEATS = 1; // when idle, the number of extra timed periods to show before hiding
const SETTINGS = "authentiwatch.json";
// Hash functions // Hash functions
const crypto = require("crypto"); const crypto = require("crypto");
const algos = { const algos = {
@ -8,33 +12,24 @@ const algos = {
"SHA256":{sha:crypto.SHA256,retsz:32,blksz:64 }, "SHA256":{sha:crypto.SHA256,retsz:32,blksz:64 },
"SHA1" :{sha:crypto.SHA1 ,retsz:20,blksz:64 }, "SHA1" :{sha:crypto.SHA1 ,retsz:20,blksz:64 },
}; };
const calculating = "Calculating"; const CALCULATING = /*LANG*/"Calculating";
const notokens = "No tokens"; const NO_TOKENS = /*LANG*/"No tokens";
const notsupported = "Not supported"; const NOT_SUPPORTED = /*LANG*/"Not supported";
// sample settings: // sample settings:
// {tokens:[{"algorithm":"SHA1","digits":6,"period":30,"issuer":"","account":"","secret":"Bbb","label":"Aaa"}],misc:{}} // {tokens:[{"algorithm":"SHA1","digits":6,"period":30,"issuer":"","account":"","secret":"Bbb","label":"Aaa"}],misc:{}}
var settings = require("Storage").readJSON("authentiwatch.json", true) || {tokens:[],misc:{}}; var settings = require("Storage").readJSON(SETTINGS, true) || {tokens:[], misc:{}};
if (settings.data ) tokens = settings.data ; /* v0.02 settings */ if (settings.data ) tokens = settings.data ; /* v0.02 settings */
if (settings.tokens) tokens = settings.tokens; /* v0.03+ settings */ if (settings.tokens) tokens = settings.tokens; /* v0.03+ settings */
// QR Code Text
//
// Example:
//
// otpauth://totp/${url}:AA_${algorithm}_${digits}dig_${period}s@${url}?algorithm=${algorithm}&digits=${digits}&issuer=${url}&period=${period}&secret=${secret}
//
// ${algorithm} : one of SHA1 / SHA256 / SHA512
// ${digits} : one of 6 / 8
// ${period} : one of 30 / 60
// ${url} : a domain name "example.com"
// ${secret} : the seed code
function b32decode(seedstr) { function b32decode(seedstr) {
// RFC4648 // RFC4648 Base16/32/64 Data Encodings
var i, buf = 0, bitcount = 0, retstr = ""; let buf = 0, bitcount = 0, retstr = "";
for (i in seedstr) { for (let c of seedstr.toUpperCase()) {
var c = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".indexOf(seedstr.charAt(i).toUpperCase(), 0); if (c == '0') c = 'O';
if (c == '1') c = 'I';
if (c == '8') c = 'B';
c = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".indexOf(c);
if (c != -1) { if (c != -1) {
buf <<= 5; buf <<= 5;
buf |= c; buf |= c;
@ -46,195 +41,127 @@ function b32decode(seedstr) {
} }
} }
} }
var retbuf = new Uint8Array(retstr.length); let retbuf = new Uint8Array(retstr.length);
for (i in retstr) { for (let i in retstr) {
retbuf[i] = retstr.charCodeAt(i); retbuf[i] = retstr.charCodeAt(i);
} }
return retbuf; return retbuf;
} }
function do_hmac(key, message, algo) {
var a = algos[algo]; function hmac(key, message, algo) {
// RFC2104 let a = algos[algo.toUpperCase()];
// RFC2104 HMAC
if (key.length > a.blksz) { if (key.length > a.blksz) {
key = a.sha(key); key = a.sha(key);
} }
var istr = new Uint8Array(a.blksz + message.length); let istr = new Uint8Array(a.blksz + message.length);
var ostr = new Uint8Array(a.blksz + a.retsz); let ostr = new Uint8Array(a.blksz + a.retsz);
for (var i = 0; i < a.blksz; ++i) { for (let i = 0; i < a.blksz; ++i) {
var c = (i < key.length) ? key[i] : 0; let c = (i < key.length) ? key[i] : 0;
istr[i] = c ^ 0x36; istr[i] = c ^ 0x36;
ostr[i] = c ^ 0x5C; ostr[i] = c ^ 0x5C;
} }
istr.set(message, a.blksz); istr.set(message, a.blksz);
ostr.set(a.sha(istr), a.blksz); ostr.set(a.sha(istr), a.blksz);
var ret = a.sha(ostr); let ret = a.sha(ostr);
// RFC4226 dynamic truncation // RFC4226 HOTP (dynamic truncation)
var v = new DataView(ret, ret[ret.length - 1] & 0x0F, 4); let v = new DataView(ret, ret[ret.length - 1] & 0x0F, 4);
return v.getUint32(0) & 0x7FFFFFFF; return v.getUint32(0) & 0x7FFFFFFF;
} }
function hotp(d, token, dohmac) {
var tick; function formatOtp(otp, digits) {
// add 0 padding
let ret = "" + otp % Math.pow(10, digits);
while (ret.length < digits) {
ret = "0" + ret;
}
// add a space after every 3rd or 4th digit
let re = (digits % 3 == 0 || (digits % 3 >= digits % 4 && digits % 4 != 0)) ? "" : ".";
return ret.replace(new RegExp("(..." + re + ")", "g"), "$1 ").trim();
}
function hotp(token) {
let d = Date.now();
let tick, next;
if (token.period > 0) { if (token.period > 0) {
// RFC6238 - timed // RFC6238 - timed
var seconds = Math.floor(d.getTime() / 1000); tick = Math.floor(Math.floor(d / 1000) / token.period);
tick = Math.floor(seconds / token.period); next = (tick + 1) * token.period * 1000;
} else { } else {
// RFC4226 - counter // RFC4226 - counter
tick = -token.period; tick = -token.period;
next = d + 30000;
} }
var msg = new Uint8Array(8); let msg = new Uint8Array(8);
var v = new DataView(msg.buffer); let v = new DataView(msg.buffer);
v.setUint32(0, tick >> 16 >> 16); v.setUint32(0, tick >> 16 >> 16);
v.setUint32(4, tick & 0xFFFFFFFF); v.setUint32(4, tick & 0xFFFFFFFF);
var ret = calculating; let ret;
if (dohmac) { try {
try { ret = hmac(b32decode(token.secret), msg, token.algorithm);
var hash = do_hmac(b32decode(token.secret), msg, token.algorithm.toUpperCase()); ret = formatOtp(ret, token.digits);
ret = "" + hash % Math.pow(10, token.digits); } catch(err) {
while (ret.length < token.digits) { ret = NOT_SUPPORTED;
ret = "0" + ret;
}
// add a space after every 3rd or 4th digit
var re = (token.digits % 3 == 0 || (token.digits % 3 >= token.digits % 4 && token.digits % 4 != 0)) ? "" : ".";
ret = ret.replace(new RegExp("(..." + re + ")", "g"), "$1 ").trim();
} catch(err) {
ret = notsupported;
}
} }
return {hotp:ret, next:((token.period > 0) ? ((tick + 1) * token.period * 1000) : d.getTime() + 30000)}; return {hotp:ret, next:next};
} }
// Tokens are displayed in three states:
// 1. Unselected (state.id<0)
// 2. Selected, inactive (no code) (state.id>=0,state.hotp.hotp=="")
// 3. Selected, active (code showing) (state.id>=0,state.hotp.hotp!="")
var fontszCache = {};
var state = { var state = {
listy: 0, listy:0, // list scroll position
prevcur:0, id:-1, // current token ID
curtoken:-1, hotp:{hotp:"",next:0}
nextTime:0,
otp:"",
rem:0,
hide:0
}; };
function drawToken(id, r) { function sizeFont(id, txt, w) {
var x1 = r.x; let sz = fontszCache[id];
var y1 = r.y; if (!sz) {
var x2 = r.x + r.w - 1; sz = TOKEN_DIGITS_HEIGHT;
var y2 = r.y + r.h - 1;
var adj, lbl, sz;
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));
lbl = tokens[id].label.substr(0, 10);
if (id == state.curtoken) {
// current token
g.setColor(g.theme.fgH)
.setBgColor(g.theme.bgH)
.setFont("Vector", tokenextraheight)
// center just below top line
.setFontAlign(0, -1, 0);
adj = y1;
} else {
g.setColor(g.theme.fg)
.setBgColor(g.theme.bg);
sz = tokendigitsheight;
do { do {
g.setFont("Vector", sz--); g.setFont("Vector", sz--);
} while (g.stringWidth(lbl) > r.w); } while (g.stringWidth(txt) > w);
// center in box fontszCache[id] = ++sz;
g.setFontAlign(0, 0, 0);
adj = (y1 + y2) / 2;
} }
g.clearRect(x1, y1, x2, y2) g.setFont("Vector", sz);
.drawString(lbl, (x1 + x2) / 2, adj, false);
if (id == state.curtoken) {
if (tokens[id].period > 0) {
// timed - draw progress bar
let xr = Math.floor(Bangle.appRect.w * state.rem / tokens[id].period);
g.fillRect(x1, y2 - 4, xr, y2 - 1);
adj = 0;
} else {
// counter - draw triangle as swipe hint
let yc = (y1 + y2) / 2;
g.fillPoly([0, yc, 10, yc - 10, 10, yc + 10, 0, yc]);
adj = 12;
}
// digits just below label
sz = tokendigitsheight;
do {
g.setFont("Vector", sz--);
} while (g.stringWidth(state.otp) > (r.w - adj));
g.drawString(state.otp, (x1 + adj + x2) / 2, y1 + tokenextraheight, false);
}
// shaded lines top and bottom
g.setColor(0.5, 0.5, 0.5)
.drawLine(x1, y1, x2, y1)
.drawLine(x1, y2, x2, y2)
.setClipRect(0, 0, g.getWidth(), g.getHeight());
} }
function draw() { tokenY = id => id * TOKEN_HEIGHT + AR.y - state.listy;
var timerfn = exitApp; half = n => Math.floor(n / 2);
var timerdly = 10000;
var d = new Date(); function timerCalc() {
if (state.curtoken != -1) { let timerfn = exitApp;
var t = tokens[state.curtoken]; let timerdly = 10000;
if (state.otp == calculating) { if (state.id >= 0 && state.hotp.hotp != "") {
state.otp = hotp(d, t, true).hotp; if (tokens[state.id].period > 0) {
} // timed HOTP
if (d.getTime() > state.nextTime) { if (state.hotp.next < Date.now()) {
if (state.hide == 0) { if (state.cnt > 0) {
// auto-hide the current token state.cnt--;
if (state.curtoken != -1) { state.hotp = hotp(tokens[state.id]);
state.prevcur = state.curtoken; } else {
state.curtoken = -1; state.hotp.hotp = "";
} }
state.nextTime = 0; timerdly = 1;
timerfn = updateCurrentToken;
} else { } else {
// time to generate a new token timerdly = 1000;
var r = hotp(d, t, state.otp != ""); timerfn = updateProgressBar;
state.nextTime = r.next;
state.otp = r.hotp;
if (t.period <= 0) {
state.hide = 1;
}
state.hide--;
}
}
state.rem = Math.max(0, Math.floor((state.nextTime - d.getTime()) / 1000));
}
if (tokens.length > 0) {
var drewcur = false;
var id = Math.floor(state.listy / tokenheight);
var y = id * tokenheight + Bangle.appRect.y - state.listy;
while (id < tokens.length && y < Bangle.appRect.y2) {
drawToken(id, {x:Bangle.appRect.x, y:y, w:Bangle.appRect.w, h:tokenheight});
if (id == state.curtoken && (tokens[id].period <= 0 || state.nextTime != 0)) {
drewcur = true;
}
id += 1;
y += tokenheight;
}
if (drewcur) {
// the current token has been drawn - schedule a redraw
if (tokens[state.curtoken].period > 0) {
timerdly = (state.otp == calculating) ? 1 : 1000; // timed
} else {
timerdly = state.nexttime - d.getTime(); // counter
}
timerfn = draw;
if (tokens[state.curtoken].period <= 0) {
state.hide = 0;
} }
} else { } else {
// de-select the current token if it is scrolled out of view // counter HOTP
if (state.curtoken != -1) { if (state.cnt > 0) {
state.prevcur = state.curtoken; state.cnt--;
state.curtoken = -1; timerdly = 30000;
} else {
state.hotp.hotp = "";
timerdly = 1;
} }
state.nexttime = 0; timerfn = updateCurrentToken;
} }
} else {
g.setFont("Vector", tokendigitsheight)
.setFontAlign(0, 0, 0)
.drawString(notokens, Bangle.appRect.x + Bangle.appRect.w / 2, Bangle.appRect.y + Bangle.appRect.h / 2, false);
} }
if (state.drawtimer) { if (state.drawtimer) {
clearTimeout(state.drawtimer); clearTimeout(state.drawtimer);
@ -242,97 +169,236 @@ function draw() {
state.drawtimer = setTimeout(timerfn, timerdly); state.drawtimer = setTimeout(timerfn, timerdly);
} }
function onTouch(zone, e) { function updateCurrentToken() {
if (e) { drawToken(state.id);
var id = Math.floor((state.listy + (e.y - Bangle.appRect.y)) / tokenheight); timerCalc();
if (id == state.curtoken || tokens.length == 0 || id >= tokens.length) { }
id = -1;
} function updateProgressBar() {
if (state.curtoken != id) { drawProgressBar();
if (id != -1) { timerCalc();
var y = id * tokenheight - state.listy; }
if (y < 0) {
state.listy += y; function drawProgressBar() {
y = 0; let id = state.id;
if (id >= 0 && tokens[id].period > 0) {
let rem = Math.min(tokens[id].period, Math.floor((state.hotp.next - Date.now()) / 1000));
if (rem >= 0) {
let y1 = tokenY(id);
let y2 = y1 + TOKEN_HEIGHT - 1;
if (y2 >= AR.y && y1 <= AR.y2) {
// token visible
y1 = y2 - PROGRESSBAR_HEIGHT;
if (y1 <= AR.y2)
{
// progress bar visible
y2 = Math.min(y2, AR.y2);
let xr = Math.floor(AR.w * rem / tokens[id].period) + AR.x;
g.setColor(g.theme.fgH)
.setBgColor(g.theme.bgH)
.fillRect(AR.x, y1, xr, y2)
.clearRect(xr + 1, y1, AR.x2, y2);
} }
y += tokenheight; } else {
if (y > Bangle.appRect.h) { // token not visible
state.listy += (y - Bangle.appRect.h); state.id = -1;
}
state.otp = "";
} }
state.nextTime = 0;
state.curtoken = id;
state.hide = 2;
} }
} }
draw(); }
// id = token ID number (0...)
function drawToken(id) {
let x1 = AR.x;
let y1 = tokenY(id);
let x2 = AR.x2;
let y2 = y1 + TOKEN_HEIGHT - 1;
let lbl = (id >= 0 && id < tokens.length) ? tokens[id].label.substr(0, 10) : "";
let adj;
g.setClipRect(x1, Math.max(y1, AR.y), x2, Math.min(y2, AR.y2));
if (id === state.id) {
g.setColor(g.theme.fgH)
.setBgColor(g.theme.bgH);
} else {
g.setColor(g.theme.fg)
.setBgColor(g.theme.bg);
}
if (id == state.id && state.hotp.hotp != "") {
// small label centered just below top line
g.setFont("Vector", TOKEN_EXTRA_HEIGHT)
.setFontAlign(0, -1, 0);
adj = y1;
} else {
// large label centered in box
sizeFont("l" + id, lbl, AR.w);
g.setFontAlign(0, 0, 0);
adj = half(y1 + y2);
}
g.clearRect(x1, y1, x2, y2)
.drawString(lbl, half(x1 + x2), adj, false);
if (id == state.id && state.hotp.hotp != "") {
adj = 0;
if (tokens[id].period <= 0) {
// counter - draw triangle as swipe hint
let yc = half(y1 + y2);
adj = COUNTER_TRIANGLE_SIZE;
g.fillPoly([AR.x, yc, AR.x + adj, yc - adj, AR.x + adj, yc + adj]);
adj += 2;
}
// digits just below label
x1 = half(x1 + adj + x2);
y1 += TOKEN_EXTRA_HEIGHT;
if (state.hotp.hotp == CALCULATING) {
sizeFont("c", CALCULATING, AR.w - adj);
g.drawString(CALCULATING, x1, y1, false)
.flip();
state.hotp = hotp(tokens[id]);
g.clearRect(AR.x + adj, y1, AR.x2, y2);
}
sizeFont("d" + id, state.hotp.hotp, AR.w - adj);
g.drawString(state.hotp.hotp, x1, y1, false);
if (tokens[id].period > 0) {
drawProgressBar();
}
}
g.setClipRect(0, 0, g.getWidth() - 1, g.getHeight() - 1);
}
function changeId(id) {
if (id != state.id) {
state.hotp.hotp = CALCULATING;
let pid = state.id;
state.id = id;
if (pid >= 0) {
drawToken(pid);
}
if (id >= 0) {
drawToken( id);
}
}
} }
function onDrag(e) { function onDrag(e) {
if (e.x > g.getWidth() || e.y > g.getHeight()) return; state.cnt = IDLE_REPEATS;
if (e.dx == 0 && e.dy == 0) return; if (e.b != 0 && e.dy != 0) {
var newy = Math.min(state.listy - e.dy, tokens.length * tokenheight - Bangle.appRect.h); let y = E.clip(state.listy - E.clip(e.dy, -AR.h, AR.h), 0, Math.max(0, tokens.length * TOKEN_HEIGHT - AR.h));
state.listy = Math.max(0, newy); if (state.listy != y) {
draw(); let id, dy = state.listy - y;
state.listy = y;
g.setClipRect(AR.x, AR.y, AR.x2, AR.y2)
.scroll(0, dy);
if (dy > 0) {
id = Math.floor((state.listy + dy) / TOKEN_HEIGHT);
y = tokenY(id + 1);
do {
drawToken(id);
id--;
y -= TOKEN_HEIGHT;
} while (y > AR.y);
}
if (dy < 0) {
id = Math.floor((state.listy + dy + AR.h) / TOKEN_HEIGHT);
y = tokenY(id);
while (y < AR.y2) {
drawToken(id);
id++;
y += TOKEN_HEIGHT;
}
}
}
}
if (e.b == 0) {
timerCalc();
}
}
function onTouch(zone, e) {
state.cnt = IDLE_REPEATS;
if (e) {
let id = Math.floor((state.listy + e.y - AR.y) / TOKEN_HEIGHT);
if (id == state.id || tokens.length == 0 || id >= tokens.length) {
id = -1;
}
if (state.id != id) {
if (id >= 0) {
// scroll token into view if necessary
let dy = 0;
let y = id * TOKEN_HEIGHT - state.listy;
if (y < 0) {
dy -= y;
y = 0;
}
y += TOKEN_HEIGHT;
if (y > AR.h) {
dy -= (y - AR.h);
}
onDrag({b:1, dy:dy});
}
changeId(id);
}
}
timerCalc();
} }
function onSwipe(e) { function onSwipe(e) {
if (e == 1) { state.cnt = IDLE_REPEATS;
switch (e) {
case 1:
exitApp(); exitApp();
break;
case -1:
if (state.id >= 0 && tokens[state.id].period <= 0) {
tokens[state.id].period--;
require("Storage").writeJSON(SETTINGS, {tokens:tokens, misc:settings.misc});
state.hotp.hotp = CALCULATING;
drawToken(state.id);
}
break;
} }
if (e == -1 && state.curtoken != -1 && tokens[state.curtoken].period <= 0) { timerCalc();
tokens[state.curtoken].period--;
let newsettings={tokens:tokens,misc:settings.misc};
require("Storage").writeJSON("authentiwatch.json", newsettings);
state.nextTime = 0;
state.otp = "";
state.hide = 2;
}
draw();
} }
function bangle1Btn(e) { function bangleBtn(e) {
state.cnt = IDLE_REPEATS;
if (tokens.length > 0) { if (tokens.length > 0) {
if (state.curtoken == -1) { let id = E.clip(state.id + e, 0, tokens.length - 1);
state.curtoken = state.prevcur; onDrag({b:1, dy:state.listy - E.clip(id * TOKEN_HEIGHT - half(AR.h - TOKEN_HEIGHT), 0, Math.max(0, tokens.length * TOKEN_HEIGHT - AR.h))});
} else { changeId(id);
switch (e) { drawProgressBar();
case -1: state.curtoken--; break;
case 1: state.curtoken++; break;
}
}
state.curtoken = Math.max(state.curtoken, 0);
state.curtoken = Math.min(state.curtoken, tokens.length - 1);
state.listy = state.curtoken * tokenheight;
state.listy -= (Bangle.appRect.h - tokenheight) / 2;
state.listy = Math.min(state.listy, tokens.length * tokenheight - Bangle.appRect.h);
state.listy = Math.max(state.listy, 0);
var fakee = {};
fakee.y = state.curtoken * tokenheight - state.listy + Bangle.appRect.y;
state.curtoken = -1;
state.nextTime = 0;
onTouch(0, fakee);
} else {
draw(); // resets idle timer
} }
timerCalc();
} }
function exitApp() { function exitApp() {
if (state.drawtimer) {
clearTimeout(state.drawtimer);
}
Bangle.showLauncher(); Bangle.showLauncher();
} }
Bangle.on('touch', onTouch); Bangle.on('touch', onTouch);
Bangle.on('drag' , onDrag ); Bangle.on('drag' , onDrag );
Bangle.on('swipe', onSwipe); Bangle.on('swipe', onSwipe);
if (typeof BTN2 == 'number') { if (typeof BTN1 == 'number') {
setWatch(function(){bangle1Btn(-1);}, BTN1, {edge:"rising" , debounce:50, repeat:true}); if (typeof BTN2 == 'number' && typeof BTN3 == 'number') {
setWatch(function(){exitApp(); }, BTN2, {edge:"falling", debounce:50}); setWatch(()=>bangleBtn(-1), BTN1, {edge:"rising" , debounce:50, repeat:true});
setWatch(function(){bangle1Btn( 1);}, BTN3, {edge:"rising" , debounce:50, repeat:true}); setWatch(()=>exitApp() , BTN2, {edge:"falling", debounce:50});
setWatch(()=>bangleBtn( 1), BTN3, {edge:"rising" , debounce:50, repeat:true});
} else {
setWatch(()=>exitApp() , BTN1, {edge:"falling", debounce:50});
}
} }
Bangle.loadWidgets(); Bangle.loadWidgets();
const AR = Bangle.appRect;
// Clear the screen once, at startup // draw the initial display
g.clear(); g.clear();
draw(); if (tokens.length > 0) {
state.listy = AR.h;
onDrag({b:1, dy:AR.h});
} else {
g.setFont("Vector", TOKEN_DIGITS_HEIGHT)
.setFontAlign(0, 0, 0)
.drawString(NO_TOKENS, AR.x + half(AR.w), AR.y + half(AR.h), false);
}
timerCalc();
Bangle.drawWidgets(); Bangle.drawWidgets();

View File

@ -54,9 +54,9 @@ 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('0', 'O');
ret = ret.replaceAll(/1/g, 'I'); ret = ret.replaceAll('1', 'I');
ret = ret.replaceAll(/8/g, 'B'); ret = ret.replaceAll('8', 'B');
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, '');
@ -81,9 +81,9 @@ function b32encode(str) {
function b32decode(seedstr) { function b32decode(seedstr) {
// RFC4648 // RFC4648
var i, buf = 0, bitcount = 0, ret = ''; var buf = 0, bitcount = 0, ret = '';
for (i in seedstr) { for (var c of seedstr.toUpperCase()) {
var c = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'.indexOf(seedstr.charAt(i).toUpperCase(), 0); c = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'.indexOf(c);
if (c != -1) { if (c != -1) {
buf <<= 5; buf <<= 5;
buf |= c; buf |= c;
@ -405,7 +405,7 @@ class proto3decoder {
constructor(str) { constructor(str) {
this.buf = []; this.buf = [];
for (let i in str) { for (let i in str) {
this.buf = this.buf.concat(str.charCodeAt(i)); this.buf.push(str.charCodeAt(i));
} }
} }
getVarint() { getVarint() {
@ -487,7 +487,7 @@ function startScan(handler,cancel) {
document.body.className = 'scanning'; document.body.className = 'scanning';
navigator.mediaDevices navigator.mediaDevices
.getUserMedia({video:{facingMode:'environment'}}) .getUserMedia({video:{facingMode:'environment'}})
.then(function(stream){ .then(stream => {
scanning=true; scanning=true;
video.setAttribute('playsinline',true); video.setAttribute('playsinline',true);
video.srcObject = stream; video.srcObject = stream;

View File

@ -4,7 +4,7 @@
"shortName": "AuthWatch", "shortName": "AuthWatch",
"icon": "app.png", "icon": "app.png",
"screenshots": [{"url":"screenshot1.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"},{"url":"screenshot4.png"}], "screenshots": [{"url":"screenshot1.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"},{"url":"screenshot4.png"}],
"version": "0.06", "version": "0.07",
"description": "Google Authenticator compatible tool.", "description": "Google Authenticator compatible tool.",
"tags": "tool", "tags": "tool",
"interface": "interface.html", "interface": "interface.html",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -49,3 +49,4 @@
0.43: Fix Gadgetbridge handling with Programmable:off 0.43: Fix Gadgetbridge handling with Programmable:off
0.44: Write .boot0 without ever having it all in RAM (fix Bangle.js 1 issues with BTHRM) 0.44: Write .boot0 without ever having it all in RAM (fix Bangle.js 1 issues with BTHRM)
0.45: Fix 0.44 regression (auto-add semi-colon between each boot code chunk) 0.45: Fix 0.44 regression (auto-add semi-colon between each boot code chunk)
0.46: Fix no clock found error on Bangle.js 2

View File

@ -14,6 +14,6 @@ if (!clockApp) {
if (clockApp) if (clockApp)
clockApp = require("Storage").read(clockApp.src); clockApp = require("Storage").read(clockApp.src);
} }
if (!clockApp) clockApp=`E.showMessage("No Clock Found");setWatch(()=>{Bangle.showLauncher();}, BTN2, {repeat:false,edge:"falling"});`; if (!clockApp) clockApp=`E.showMessage("No Clock Found");setWatch(()=>{Bangle.showLauncher();}, global.BTN2||BTN, {repeat:false,edge:"falling"});`;
eval(clockApp); eval(clockApp);
delete clockApp; delete clockApp;

View File

@ -1,7 +1,7 @@
{ {
"id": "boot", "id": "boot",
"name": "Bootloader", "name": "Bootloader",
"version": "0.45", "version": "0.46",
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings", "description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
"icon": "bootloader.png", "icon": "bootloader.png",
"type": "bootloader", "type": "bootloader",

View File

@ -1,2 +1,3 @@
0.01: Initial upload 0.01: Initial upload
0.2: Added scrollable calendar and swipe gestures 0.2: Added scrollable calendar and swipe gestures
0.3: Configurable drag gestures

View File

@ -9,25 +9,24 @@ I know that it seems redundant because there already **is** a *time&cal*-app, bu
|![unlocked screen](screenshot2.png)|unlocked: smaller clock, but with seconds| |![unlocked screen](screenshot2.png)|unlocked: smaller clock, but with seconds|
|![big calendar](screenshot3.png)|swipe up for big calendar, (up down to scroll, left/right to exit)| |![big calendar](screenshot3.png)|swipe up for big calendar, (up down to scroll, left/right to exit)|
## Configurable Features ## Configurable Features
- Number of calendar rows (weeks) - Number of calendar rows (weeks)
- Buzz on connect/disconnect (I know, this should be an extra widget, but for now, it is included) - Buzz on connect/disconnect (I know, this should be an extra widget, but for now, it is included)
- Clock Mode (24h/12h). Doesn't have an am/pm indicator. It's only there because it was easy. - Clock Mode (24h/12h). (No am/pm indicator)
- First day of the week - First day of the week
- Red Saturday - Red Saturday/Sunday
- Red Sunday - Swipe/Drag gestures to launch features or apps.
- Swipes (to disable all gestures)
- Swipes: music (swipe down)
- Spipes: messages (swipe right)
## Auto detects your message/music apps: ## Auto detects your message/music apps:
- swiping down will search your files for an app with the string "music" in its filename and launch it - swiping down will search your files for an app with the string "message" in its filename and launch it. (configurable)
- swiping right will search your files for an app with the string "message" in its filename and launch it. - swiping right will search your files for an app with the string "music" in its filename and launch it. (configurable)
- Configurable apps coming soon.
## Feedback ## Feedback
The clock works for me in a 24h/MondayFirst/WeekendFree environment but is not well-tested with other settings. The clock works for me in a 24h/MondayFirst/WeekendFree environment but is not well-tested with other settings.
So if something isn't working, please tell me: https://github.com/foostuff/BangleApps/issues So if something isn't working, please tell me: https://github.com/foostuff/BangleApps/issues
## Planned features:
- Internal lightweight music control, because switching apps has a loading time.
- Clean up settings
- Maybe am/pm indicator for 12h-users
- Step count (optional)

View File

@ -4,12 +4,13 @@ var s = Object.assign({
CAL_ROWS: 4, //number of calendar rows.(weeks) Shouldn't exceed 5 when using widgets. CAL_ROWS: 4, //number of calendar rows.(weeks) Shouldn't exceed 5 when using widgets.
BUZZ_ON_BT: true, //2x slow buzz on disconnect, 2x fast buzz on connect. Will be extra widget eventually BUZZ_ON_BT: true, //2x slow buzz on disconnect, 2x fast buzz on connect. Will be extra widget eventually
MODE24: true, //24h mode vs 12h mode MODE24: true, //24h mode vs 12h mode
FIRSTDAYOFFSET: 6, //First day of the week: 0-6: Sun, Sat, Fri, Thu, Wed, Tue, Mon FIRSTDAY: 6, //First day of the week: mo, tu, we, th, fr, sa, su
REDSUN: true, // Use red color for sunday? REDSUN: true, // Use red color for sunday?
REDSAT: true, // Use red color for saturday? REDSAT: true, // Use red color for saturday?
DRAGENABLED: true, DRAGDOWN: "[AI:messg]",
DRAGMUSIC: true, DRAGRIGHT: "[AI:music]",
DRAGMESSAGES: true DRAGLEFT: "[ignore]",
DRAGUP: "[calend.]"
}, require('Storage').readJSON("clockcal.json", true) || {}); }, require('Storage').readJSON("clockcal.json", true) || {});
const h = g.getHeight(); const h = g.getHeight();
@ -27,13 +28,13 @@ var monthOffset = 0;
*/ */
function drawFullCalendar(monthOffset) { function drawFullCalendar(monthOffset) {
addMonths = function (_d, _am) { addMonths = function (_d, _am) {
var ay = 0, m = _d.getMonth(), y = _d.getFullYear(); var ay = 0, m = _d.getMonth(), y = _d.getFullYear();
while ((m + _am) > 11) { ay++; _am -= 12; } while ((m + _am) > 11) { ay++; _am -= 12; }
while ((m + _am) < 0) { ay--; _am += 12; } while ((m + _am) < 0) { ay--; _am += 12; }
n = new Date(_d.getTime()); n = new Date(_d.getTime());
n.setMonth(m + _am); n.setMonth(m + _am);
n.setFullYear(y + ay); n.setFullYear(y + ay);
return n; return n;
}; };
monthOffset = (typeof monthOffset == "undefined") ? 0 : monthOffset; monthOffset = (typeof monthOffset == "undefined") ? 0 : monthOffset;
state = "calendar"; state = "calendar";
@ -45,22 +46,22 @@ function drawFullCalendar(monthOffset) {
if (typeof minuteInterval !== "undefined") clearTimeout(minuteInterval); if (typeof minuteInterval !== "undefined") clearTimeout(minuteInterval);
d = addMonths(Date(), monthOffset); d = addMonths(Date(), monthOffset);
tdy = Date().getDate() + "." + Date().getMonth(); tdy = Date().getDate() + "." + Date().getMonth();
newmonth=false; newmonth = false;
c_y = 0; c_y = 0;
g.reset(); g.reset();
g.setBgColor(0); g.setBgColor(0);
g.clear(); g.clear();
var prevmonth = addMonths(d, -1) var prevmonth = addMonths(d, -1);
const today = prevmonth.getDate(); const today = prevmonth.getDate();
var rD = new Date(prevmonth.getTime()); var rD = new Date(prevmonth.getTime());
rD.setDate(rD.getDate() - (today - 1)); rD.setDate(rD.getDate() - (today - 1));
const dow = (s.FIRSTDAYOFFSET + rD.getDay()) % 7; const dow = (s.FIRSTDAY + rD.getDay()) % 7;
rD.setDate(rD.getDate() - dow); rD.setDate(rD.getDate() - dow);
var rDate = rD.getDate(); var rDate = rD.getDate();
bottomrightY = c_y - 3; bottomrightY = c_y - 3;
clrsun=s.REDSUN?'#f00':'#fff'; clrsun = s.REDSUN ? '#f00' : '#fff';
clrsat=s.REDSUN?'#f00':'#fff'; clrsat = s.REDSUN ? '#f00' : '#fff';
var fg=[clrsun,'#fff','#fff','#fff','#fff','#fff',clrsat]; var fg = [clrsun, '#fff', '#fff', '#fff', '#fff', '#fff', clrsat];
for (var y = 1; y <= 11; y++) { for (var y = 1; y <= 11; y++) {
bottomrightY += CELL_H; bottomrightY += CELL_H;
bottomrightX = -2; bottomrightX = -2;
@ -69,14 +70,14 @@ function drawFullCalendar(monthOffset) {
rMonth = rD.getMonth(); rMonth = rD.getMonth();
rDate = rD.getDate(); rDate = rD.getDate();
if (tdy == rDate + "." + rMonth) { if (tdy == rDate + "." + rMonth) {
caldrawToday(rDate); caldrawToday(rDate);
} else if (rDate == 1) { } else if (rDate == 1) {
caldrawFirst(rDate); caldrawFirst(rDate);
} else { } else {
caldrawNormal(rDate,fg[rD.getDay()]); caldrawNormal(rDate, fg[rD.getDay()]);
} }
if (newmonth && x == 7) { if (newmonth && x == 7) {
caldrawMonth(rDate,monthclr[rMonth % 6],months[rMonth],rD); caldrawMonth(rDate, monthclr[rMonth % 6], months[rMonth], rD);
} }
rD.setDate(rDate + 1); rD.setDate(rDate + 1);
} }
@ -84,7 +85,7 @@ function drawFullCalendar(monthOffset) {
delete addMonths; delete addMonths;
if (DEBUG) console.log("Calendar performance (ms):" + (Date().getTime() - start)); if (DEBUG) console.log("Calendar performance (ms):" + (Date().getTime() - start));
} }
function caldrawMonth(rDate,c,m,rD) { function caldrawMonth(rDate, c, m, rD) {
g.setColor(c); g.setColor(c);
g.setFont("Vector", 18); g.setFont("Vector", 18);
g.setFontAlign(-1, 1, 1); g.setFontAlign(-1, 1, 1);
@ -93,29 +94,29 @@ function caldrawMonth(rDate,c,m,rD) {
newmonth = false; newmonth = false;
} }
function caldrawToday(rDate) { function caldrawToday(rDate) {
g.setFont("Vector", 16); g.setFont("Vector", 16);
g.setFontAlign(1, 1); g.setFontAlign(1, 1);
g.setColor('#0f0'); g.setColor('#0f0');
g.fillRect(bottomrightX - CELL2_W + 1, bottomrightY - CELL_H - 1, bottomrightX, bottomrightY - 2); g.fillRect(bottomrightX - CELL2_W + 1, bottomrightY - CELL_H - 1, bottomrightX, bottomrightY - 2);
g.setColor('#000'); g.setColor('#000');
g.drawString(rDate, bottomrightX, bottomrightY); g.drawString(rDate, bottomrightX, bottomrightY);
} }
function caldrawFirst(rDate) { function caldrawFirst(rDate) {
g.flip(); g.flip();
g.setFont("Vector", 16); g.setFont("Vector", 16);
g.setFontAlign(1, 1); g.setFontAlign(1, 1);
bottomrightY += 3; bottomrightY += 3;
newmonth = true; newmonth = true;
g.setColor('#0ff'); g.setColor('#0ff');
g.fillRect(bottomrightX - CELL2_W + 1, bottomrightY - CELL_H - 1, bottomrightX, bottomrightY - 2); g.fillRect(bottomrightX - CELL2_W + 1, bottomrightY - CELL_H - 1, bottomrightX, bottomrightY - 2);
g.setColor('#000'); g.setColor('#000');
g.drawString(rDate, bottomrightX, bottomrightY); g.drawString(rDate, bottomrightX, bottomrightY);
} }
function caldrawNormal(rDate,c) { function caldrawNormal(rDate, c) {
g.setFont("Vector", 16); g.setFont("Vector", 16);
g.setFontAlign(1, 1); g.setFontAlign(1, 1);
g.setColor(c); g.setColor(c);
g.drawString(rDate, bottomrightX, bottomrightY);//100 g.drawString(rDate, bottomrightX, bottomrightY);//100
} }
function drawMinutes() { function drawMinutes() {
if (DEBUG) console.log("|-->minutes"); if (DEBUG) console.log("|-->minutes");
@ -163,7 +164,7 @@ function drawWatch() {
g.clear(); g.clear();
drawMinutes(); drawMinutes();
if (!dimSeconds) drawSeconds(); if (!dimSeconds) drawSeconds();
const dow = (s.FIRSTDAYOFFSET + d.getDay()) % 7; //MO=0, SU=6 const dow = (s.FIRSTDAY + d.getDay()) % 7; //MO=0, SU=6
const today = d.getDate(); const today = d.getDate();
var rD = new Date(d.getTime()); var rD = new Date(d.getTime());
rD.setDate(rD.getDate() - dow); rD.setDate(rD.getDate() - dow);
@ -205,27 +206,52 @@ function BTevent() {
setTimeout(function () { Bangle.buzz(interval); }, interval * 3); setTimeout(function () { Bangle.buzz(interval); }, interval * 3);
} }
} }
function action(a) {
g.reset();
if (typeof secondInterval !== "undefined") clearTimeout(secondInterval);
if (DEBUG) console.log("action:" + a);
switch (a) {
case "[ignore]":
break;
case "[calend.]":
drawFullCalendar();
break;
case "[AI:music]":
l = require("Storage").list(RegExp("music.*app.js"));
if (l.length > 0) {
load(l[0]);
} else E.showAlert("Music app not found", "Not found").then(drawWatch);
break;
case "[AI:messg]":
l = require("Storage").list(RegExp("message.*app.js"));
if (l.length > 0) {
load(l[0]);
} else E.showAlert("Message app not found", "Not found").then(drawWatch);
break;
default:
l = require("Storage").list(RegExp(a + ".app.js"));
if (l.length > 0) {
load(l[0]);
} else E.showAlert(a + ": App not found", "Not found").then(drawWatch);
break;
}
}
function input(dir) { function input(dir) {
if (s.DRAGENABLED) { Bangle.buzz(100, 1);
Bangle.buzz(100,1); if (DEBUG) console.log("swipe:" + dir);
console.log("swipe:"+dir);
switch (dir) { switch (dir) {
case "r": case "r":
if (state == "calendar") { if (state == "calendar") {
drawWatch(); drawWatch();
} else { } else {
if (s.DRAGMUSIC) { action(s.DRAGRIGHT);
l=require("Storage").list(RegExp("music.*app"));
if (l.length > 0) {
load(l[0]);
} else Bangle.buzz(3000,1);//not found
}
} }
break; break;
case "l": case "l":
if (state == "calendar") { if (state == "calendar") {
drawWatch(); drawWatch();
} else {
action(s.DRAGLEFT);
} }
break; break;
case "d": case "d":
@ -233,21 +259,15 @@ function input(dir) {
monthOffset--; monthOffset--;
drawFullCalendar(monthOffset); drawFullCalendar(monthOffset);
} else { } else {
if (s.DRAGMESSAGES) { action(s.DRAGDOWN);
l=require("Storage").list(RegExp("message.*app"));
if (l.length > 0) {
load(l[0]);
} else Bangle.buzz(3000,1);//not found
}
} }
break; break;
case "u": case "u":
if (state == "watch") { if (state == "calendar") {
state = "calendar";
drawFullCalendar(0);
} else if (state == "calendar") {
monthOffset++; monthOffset++;
drawFullCalendar(monthOffset); drawFullCalendar(monthOffset);
} else {
action(s.DRAGUP);
} }
break; break;
default: default:
@ -255,26 +275,24 @@ function input(dir) {
drawWatch(); drawWatch();
} }
break; break;
} }
}
} }
let drag; let drag;
Bangle.on("drag", e => { Bangle.on("drag", e => {
if (s.DRAGENABLED) { if (!drag) {
if (!drag) { drag = { x: e.x, y: e.y };
drag = { x: e.x, y: e.y }; } else if (!e.b) {
} else if (!e.b) { const dx = e.x - drag.x, dy = e.y - drag.y;
const dx = e.x - drag.x, dy = e.y - drag.y; var dir = "t";
var dir = "t"; if (Math.abs(dx) > Math.abs(dy) + 20) {
if (Math.abs(dx) > Math.abs(dy) + 10) { dir = (dx > 0) ? "r" : "l";
dir = (dx > 0) ? "r" : "l"; } else if (Math.abs(dy) > Math.abs(dx) + 20) {
} else if (Math.abs(dy) > Math.abs(dx) + 10) { dir = (dy > 0) ? "d" : "u";
dir = (dy > 0) ? "d" : "u";
}
drag = null;
input(dir);
} }
drag = null;
input(dir);
} }
}); });

View File

@ -1,7 +1,7 @@
{ {
"id": "clockcal", "id": "clockcal",
"name": "Clock & Calendar", "name": "Clock & Calendar",
"version": "0.2", "version": "0.3",
"description": "Clock with Calendar", "description": "Clock with Calendar",
"readme":"README.md", "readme":"README.md",
"icon": "app.png", "icon": "app.png",

View File

@ -1,18 +1,21 @@
(function (back) { (function (back) {
var FILE = "clockcal.json"; var FILE = "clockcal.json";
defaults={
settings = Object.assign({
CAL_ROWS: 4, //number of calendar rows.(weeks) Shouldn't exceed 5 when using widgets. CAL_ROWS: 4, //number of calendar rows.(weeks) Shouldn't exceed 5 when using widgets.
BUZZ_ON_BT: true, //2x slow buzz on disconnect, 2x fast buzz on connect. Will be extra widget eventually BUZZ_ON_BT: true, //2x slow buzz on disconnect, 2x fast buzz on connect. Will be extra widget eventually
MODE24: true, //24h mode vs 12h mode MODE24: true, //24h mode vs 12h mode
FIRSTDAY: 6, //First day of the week: mo, tu, we, th, fr, sa, su FIRSTDAY: 6, //First day of the week: mo, tu, we, th, fr, sa, su
REDSUN: true, // Use red color for sunday? REDSUN: true, // Use red color for sunday?
REDSAT: true, // Use red color for saturday? REDSAT: true, // Use red color for saturday?
DRAGENABLED: true, //Enable drag gestures (bigger calendar etc) DRAGDOWN: "[AI:messg]",
DRAGMUSIC: true, //Enable drag down for music (looks for "music*app") DRAGRIGHT: "[AI:music]",
DRAGMESSAGES: true //Enable drag right for messages (looks for "message*app") DRAGLEFT: "[ignore]",
}, require('Storage').readJSON(FILE, true) || {}); DRAGUP: "[calend.]"
};
settings = Object.assign(defaults, require('Storage').readJSON(FILE, true) || {});
actions = ["[ignore]","[calend.]","[AI:music]","[AI:messg]"];
require("Storage").list(RegExp(".app.js")).forEach(element => actions.push(element.replace(".app.js","")));
function writeSettings() { function writeSettings() {
require('Storage').writeJSON(FILE, settings); require('Storage').writeJSON(FILE, settings);
@ -70,27 +73,39 @@
writeSettings(); writeSettings();
} }
}, },
'Swipes (big cal.)?': { 'Drag Up ': {
value: settings.DRAGENABLED, min:0, max:actions.length-1,
format: v => v ? "On" : "Off", value: actions.indexOf(settings.DRAGUP),
format: v => actions[v],
onchange: v => { onchange: v => {
settings.DRAGENABLED = v; settings.DRAGUP = actions[v];
writeSettings(); writeSettings();
} }
}, },
'Swipes (music)?': { 'Drag Right': {
value: settings.DRAGMUSIC, min:0, max:actions.length-1,
format: v => v ? "On" : "Off", value: actions.indexOf(settings.DRAGRIGHT),
format: v => actions[v],
onchange: v => { onchange: v => {
settings.DRAGMUSIC = v; settings.DRAGRIGHT = actions[v];
writeSettings(); writeSettings();
} }
}, },
'Swipes (messg)?': { 'Drag Down': {
value: settings.DRAGMESSAGES, min:0, max:actions.length-1,
format: v => v ? "On" : "Off", value: actions.indexOf(settings.DRAGDOWN),
format: v => actions[v],
onchange: v => { onchange: v => {
settings.DRAGMESSAGES = v; settings.DRGDOWN = actions[v];
writeSettings();
}
},
'Drag Left': {
min:0, max:actions.length-1,
value: actions.indexOf(settings.DRAGLEFT),
format: v => actions[v],
onchange: v => {
settings.DRAGLEFT = actions[v];
writeSettings(); writeSettings();
} }
}, },
@ -100,17 +115,7 @@
format: v => ["No", "Yes"][v], format: v => ["No", "Yes"][v],
onchange: v => { onchange: v => {
if (v == 1) { if (v == 1) {
settings = { settings = defaults;
CAL_ROWS: 4, //number of calendar rows.(weeks) Shouldn't exceed 5 when using widgets.
BUZZ_ON_BT: true, //2x slow buzz on disconnect, 2x fast buzz on connect.
MODE24: true, //24h mode vs 12h mode
FIRSTDAY: 6, //First day of the week: mo, tu, we, th, fr, sa, su
REDSUN: true, // Use red color for sunday?
REDSAT: true, // Use red color for saturday?
DRAGENABLED: true,
DRAGMUSIC: true,
DRAGMESSAGES: true
};
writeSettings(); writeSettings();
load(); load();
} }

View File

@ -1,2 +1,3 @@
0.01: New App! 0.01: New App!
0.02: Load widgets after setUI so widclk knows when to hide 0.02: Load widgets after setUI so widclk knows when to hide
0.03: Show the day of the week

View File

@ -41,6 +41,7 @@ function draw() {
var date = new Date(); var date = new Date();
var timeStr = require("locale").time(date,1); var timeStr = require("locale").time(date,1);
var dateStr = require("locale").date(date).toUpperCase(); var dateStr = require("locale").date(date).toUpperCase();
var dowStr = require("locale").dow(date).toUpperCase();
// draw time // draw time
g.setFontAlign(0,0).setFont("ZCOOL"); g.setFontAlign(0,0).setFont("ZCOOL");
g.drawString(timeStr,x,y); g.drawString(timeStr,x,y);
@ -48,6 +49,9 @@ function draw() {
y += 35; y += 35;
g.setFontAlign(0,0,1).setFont("6x8"); g.setFontAlign(0,0,1).setFont("6x8");
g.drawString(dateStr,g.getWidth()-8,g.getHeight()/2); g.drawString(dateStr,g.getWidth()-8,g.getHeight()/2);
// draw the day of the week
g.setFontAlign(0,0,3).setFont("6x8");
g.drawString(dowStr,8,g.getHeight()/2);
// queue draw in one minute // queue draw in one minute
queueDraw(); queueDraw();
} }

View File

@ -1,7 +1,7 @@
{ {
"id": "waveclk", "id": "waveclk",
"name": "Wave Clock", "name": "Wave Clock",
"version": "0.02", "version": "0.03",
"description": "A clock using a wave image by [Lillith May](https://www.instagram.com/_lilustrations_/)", "description": "A clock using a wave image by [Lillith May](https://www.instagram.com/_lilustrations_/)",
"icon": "app.png", "icon": "app.png",
"screenshots": [{"url":"screenshot.png"}], "screenshots": [{"url":"screenshot.png"}],

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

162
gadgetbridge.js Normal file
View File

@ -0,0 +1,162 @@
/* Detects if we're running under Gadgetbridge in a WebView, and if
so it overwrites the 'Puck' library with a special one that calls back
into Gadgetbridge to handle watch communications */
/*// test code
Android = {
bangleTx : function(data) {
console.log("TX : "+JSON.stringify(data));
}
};*/
if (typeof Android!=="undefined") {
console.log("Running under Gadgetbridge, overwrite Puck library");
var isBusy = false;
var queue = [];
var connection = {
cb : function(data) {},
write : function(data, writecb) {
Android.bangleTx(data);
Puck.writeProgress(data.length, data.length);
if (writecb) setTimeout(writecb,10);
},
close : function() {},
received : "",
hadData : false
}
function bangleRx(data) {
// document.getElementById("status").innerText = "RX:"+data;
connection.received += data;
connection.hadData = true;
if (connection.cb) connection.cb(data);
}
function log(level, s) {
if (Puck.log) Puck.log(level, s);
}
function handleQueue() {
if (!queue.length) return;
var q = queue.shift();
log(3,"Executing "+JSON.stringify(q)+" from queue");
if (q.type == "write") Puck.write(q.data, q.callback, q.callbackNewline);
else log(1,"Unknown queue item "+JSON.stringify(q));
}
/* convenience function... Write data, call the callback with data:
callbackNewline = false => if no new data received for ~0.2 sec
callbackNewline = true => after a newline */
function write(data, callback, callbackNewline) {
let result;
/// If there wasn't a callback function, then promisify
if (typeof callback !== 'function') {
callbackNewline = callback;
result = new Promise((resolve, reject) => callback = (value, err) => {
if (err) reject(err);
else resolve(value);
});
}
if (isBusy) {
log(3, "Busy - adding Puck.write to queue");
queue.push({type:"write", data:data, callback:callback, callbackNewline:callbackNewline});
return result;
}
var cbTimeout;
function onWritten() {
if (callbackNewline) {
connection.cb = function(d) {
var newLineIdx = connection.received.indexOf("\n");
if (newLineIdx>=0) {
var l = connection.received.substr(0,newLineIdx);
connection.received = connection.received.substr(newLineIdx+1);
connection.cb = undefined;
if (cbTimeout) clearTimeout(cbTimeout);
cbTimeout = undefined;
if (callback)
callback(l);
isBusy = false;
handleQueue();
}
};
}
// wait for any received data if we have a callback...
var maxTime = 300; // 30 sec - Max time we wait in total, even if getting data
var dataWaitTime = callbackNewline ? 100/*10 sec if waiting for newline*/ : 3/*300ms*/;
var maxDataTime = dataWaitTime; // max time we wait after having received data
cbTimeout = setTimeout(function timeout() {
cbTimeout = undefined;
if (maxTime) maxTime--;
if (maxDataTime) maxDataTime--;
if (connection.hadData) maxDataTime=dataWaitTime;
if (maxDataTime && maxTime) {
cbTimeout = setTimeout(timeout, 100);
} else {
connection.cb = undefined;
if (callback)
callback(connection.received);
isBusy = false;
handleQueue();
connection.received = "";
}
connection.hadData = false;
}, 100);
}
if (!connection.txInProgress) connection.received = "";
isBusy = true;
connection.write(data, onWritten);
return result
}
// ----------------------------------------------------------
Puck = {
/// Are we writing debug information? 0 is no, 1 is some, 2 is more, 3 is all.
debug : Puck.debug,
/// Should we use flow control? Default is true
flowControl : true,
/// Used internally to write log information - you can replace this with your own function
log : function(level, s) { if (level <= this.debug) console.log("<BLE> "+s)},
/// Called with the current send progress or undefined when done - you can replace this with your own function
writeProgress : Puck.writeProgress,
connect : function(callback) {
setTimeout(callback, 10);
},
write : write,
eval : function(expr, cb) {
const response = write('\x10Bluetooth.println(JSON.stringify(' + expr + '))\n', true)
.then(function (d) {
try {
return JSON.parse(d);
} catch (e) {
log(1, "Unable to decode " + JSON.stringify(d) + ", got " + e.toString());
return Promise.reject(d);
}
});
if (cb) {
return void response.then(cb, (err) => cb(null, err));
} else {
return response;
}
},
isConnected : function() { return true; },
getConnection : function() { return connection; },
close : function() {
if (connection)
connection.close();
},
};
// no need for header
document.getElementsByTagName("header")[0].style="display:none";
// force connection attempt automatically
setTimeout(function() {
getInstalledApps(true).catch(err => {
showToast("Device connection failed, "+err,"error");
});
}, 100);
}

View File

@ -179,5 +179,6 @@
<script src="core/js/appinfo.js"></script> <script src="core/js/appinfo.js"></script>
<script src="core/js/index.js"></script> <script src="core/js/index.js"></script>
<script src="core/js/pwa.js" defer></script> <script src="core/js/pwa.js" defer></script>
<script src="gadgetbridge.js"></script>
</body> </body>
</html> </html>