Merge branch 'espruino:master' into master
|
|
@ -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});
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
|
|
@ -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).
|
||||
|
||||

|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A31gAeFtoxPF9wujGBYQG1YAWF6ur5gAYGIovOFzIABF6ReaMAwv/F/4v/F7ejv9/0Yvq1Eylksv4vqvIuBF9ZeDF9ZeBqovr1AsB0YvrLwXMF9ReDF9ZeBq1/v4vBqowKF7lWFYIAFF/7vXAAa/qF+jxB0YvsABov/F/4v/F6WsF7YgEF5xgaLwgvPGIQAWDwwvQADwvJGEguKF+AxhFpoA/AH4A/AFI="))
|
||||
|
After Width: | Height: | Size: 759 B |
|
|
@ -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}
|
||||
]
|
||||
}
|
||||
|
|
@ -4,3 +4,4 @@
|
|||
0.04: Fix tapping at very bottom of list, exit on inactivity
|
||||
0.05: Add support for bulk importing and exporting tokens
|
||||
0.06: Add spaces to codes for improved readability (thanks @BartS23)
|
||||
0.07: Bangle 2: Improve drag responsiveness and exit on button press
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
const tokenextraheight = 16;
|
||||
var tokendigitsheight = 30;
|
||||
var tokenheight = tokendigitsheight + tokenextraheight;
|
||||
const COUNTER_TRIANGLE_SIZE = 10;
|
||||
const TOKEN_EXTRA_HEIGHT = 16;
|
||||
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
|
||||
const crypto = require("crypto");
|
||||
const algos = {
|
||||
|
|
@ -8,33 +12,24 @@ const algos = {
|
|||
"SHA256":{sha:crypto.SHA256,retsz:32,blksz:64 },
|
||||
"SHA1" :{sha:crypto.SHA1 ,retsz:20,blksz:64 },
|
||||
};
|
||||
const calculating = "Calculating";
|
||||
const notokens = "No tokens";
|
||||
const notsupported = "Not supported";
|
||||
const CALCULATING = /*LANG*/"Calculating";
|
||||
const NO_TOKENS = /*LANG*/"No tokens";
|
||||
const NOT_SUPPORTED = /*LANG*/"Not supported";
|
||||
|
||||
// sample settings:
|
||||
// {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.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) {
|
||||
// RFC4648
|
||||
var i, buf = 0, bitcount = 0, retstr = "";
|
||||
for (i in seedstr) {
|
||||
var c = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".indexOf(seedstr.charAt(i).toUpperCase(), 0);
|
||||
// RFC4648 Base16/32/64 Data Encodings
|
||||
let buf = 0, bitcount = 0, retstr = "";
|
||||
for (let c of seedstr.toUpperCase()) {
|
||||
if (c == '0') c = 'O';
|
||||
if (c == '1') c = 'I';
|
||||
if (c == '8') c = 'B';
|
||||
c = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".indexOf(c);
|
||||
if (c != -1) {
|
||||
buf <<= 5;
|
||||
buf |= c;
|
||||
|
|
@ -46,195 +41,127 @@ function b32decode(seedstr) {
|
|||
}
|
||||
}
|
||||
}
|
||||
var retbuf = new Uint8Array(retstr.length);
|
||||
for (i in retstr) {
|
||||
let retbuf = new Uint8Array(retstr.length);
|
||||
for (let i in retstr) {
|
||||
retbuf[i] = retstr.charCodeAt(i);
|
||||
}
|
||||
return retbuf;
|
||||
}
|
||||
function do_hmac(key, message, algo) {
|
||||
var a = algos[algo];
|
||||
// RFC2104
|
||||
|
||||
function hmac(key, message, algo) {
|
||||
let a = algos[algo.toUpperCase()];
|
||||
// RFC2104 HMAC
|
||||
if (key.length > a.blksz) {
|
||||
key = a.sha(key);
|
||||
}
|
||||
var istr = new Uint8Array(a.blksz + message.length);
|
||||
var ostr = new Uint8Array(a.blksz + a.retsz);
|
||||
for (var i = 0; i < a.blksz; ++i) {
|
||||
var c = (i < key.length) ? key[i] : 0;
|
||||
let istr = new Uint8Array(a.blksz + message.length);
|
||||
let ostr = new Uint8Array(a.blksz + a.retsz);
|
||||
for (let i = 0; i < a.blksz; ++i) {
|
||||
let c = (i < key.length) ? key[i] : 0;
|
||||
istr[i] = c ^ 0x36;
|
||||
ostr[i] = c ^ 0x5C;
|
||||
}
|
||||
istr.set(message, a.blksz);
|
||||
ostr.set(a.sha(istr), a.blksz);
|
||||
var ret = a.sha(ostr);
|
||||
// RFC4226 dynamic truncation
|
||||
var v = new DataView(ret, ret[ret.length - 1] & 0x0F, 4);
|
||||
let ret = a.sha(ostr);
|
||||
// RFC4226 HOTP (dynamic truncation)
|
||||
let v = new DataView(ret, ret[ret.length - 1] & 0x0F, 4);
|
||||
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) {
|
||||
// RFC6238 - timed
|
||||
var seconds = Math.floor(d.getTime() / 1000);
|
||||
tick = Math.floor(seconds / token.period);
|
||||
tick = Math.floor(Math.floor(d / 1000) / token.period);
|
||||
next = (tick + 1) * token.period * 1000;
|
||||
} else {
|
||||
// RFC4226 - counter
|
||||
tick = -token.period;
|
||||
next = d + 30000;
|
||||
}
|
||||
var msg = new Uint8Array(8);
|
||||
var v = new DataView(msg.buffer);
|
||||
let msg = new Uint8Array(8);
|
||||
let v = new DataView(msg.buffer);
|
||||
v.setUint32(0, tick >> 16 >> 16);
|
||||
v.setUint32(4, tick & 0xFFFFFFFF);
|
||||
var ret = calculating;
|
||||
if (dohmac) {
|
||||
try {
|
||||
var hash = do_hmac(b32decode(token.secret), msg, token.algorithm.toUpperCase());
|
||||
ret = "" + hash % Math.pow(10, token.digits);
|
||||
while (ret.length < token.digits) {
|
||||
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;
|
||||
}
|
||||
let ret;
|
||||
try {
|
||||
ret = hmac(b32decode(token.secret), msg, token.algorithm);
|
||||
ret = formatOtp(ret, token.digits);
|
||||
} catch(err) {
|
||||
ret = NOT_SUPPORTED;
|
||||
}
|
||||
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 = {
|
||||
listy: 0,
|
||||
prevcur:0,
|
||||
curtoken:-1,
|
||||
nextTime:0,
|
||||
otp:"",
|
||||
rem:0,
|
||||
hide:0
|
||||
listy:0, // list scroll position
|
||||
id:-1, // current token ID
|
||||
hotp:{hotp:"",next:0}
|
||||
};
|
||||
|
||||
function drawToken(id, r) {
|
||||
var x1 = r.x;
|
||||
var y1 = r.y;
|
||||
var x2 = r.x + r.w - 1;
|
||||
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;
|
||||
function sizeFont(id, txt, w) {
|
||||
let sz = fontszCache[id];
|
||||
if (!sz) {
|
||||
sz = TOKEN_DIGITS_HEIGHT;
|
||||
do {
|
||||
g.setFont("Vector", sz--);
|
||||
} while (g.stringWidth(lbl) > r.w);
|
||||
// center in box
|
||||
g.setFontAlign(0, 0, 0);
|
||||
adj = (y1 + y2) / 2;
|
||||
} while (g.stringWidth(txt) > w);
|
||||
fontszCache[id] = ++sz;
|
||||
}
|
||||
g.clearRect(x1, y1, x2, y2)
|
||||
.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());
|
||||
g.setFont("Vector", sz);
|
||||
}
|
||||
|
||||
function draw() {
|
||||
var timerfn = exitApp;
|
||||
var timerdly = 10000;
|
||||
var d = new Date();
|
||||
if (state.curtoken != -1) {
|
||||
var t = tokens[state.curtoken];
|
||||
if (state.otp == calculating) {
|
||||
state.otp = hotp(d, t, true).hotp;
|
||||
}
|
||||
if (d.getTime() > state.nextTime) {
|
||||
if (state.hide == 0) {
|
||||
// auto-hide the current token
|
||||
if (state.curtoken != -1) {
|
||||
state.prevcur = state.curtoken;
|
||||
state.curtoken = -1;
|
||||
tokenY = id => id * TOKEN_HEIGHT + AR.y - state.listy;
|
||||
half = n => Math.floor(n / 2);
|
||||
|
||||
function timerCalc() {
|
||||
let timerfn = exitApp;
|
||||
let timerdly = 10000;
|
||||
if (state.id >= 0 && state.hotp.hotp != "") {
|
||||
if (tokens[state.id].period > 0) {
|
||||
// timed HOTP
|
||||
if (state.hotp.next < Date.now()) {
|
||||
if (state.cnt > 0) {
|
||||
state.cnt--;
|
||||
state.hotp = hotp(tokens[state.id]);
|
||||
} else {
|
||||
state.hotp.hotp = "";
|
||||
}
|
||||
state.nextTime = 0;
|
||||
timerdly = 1;
|
||||
timerfn = updateCurrentToken;
|
||||
} else {
|
||||
// time to generate a new token
|
||||
var r = hotp(d, t, state.otp != "");
|
||||
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;
|
||||
timerdly = 1000;
|
||||
timerfn = updateProgressBar;
|
||||
}
|
||||
} else {
|
||||
// de-select the current token if it is scrolled out of view
|
||||
if (state.curtoken != -1) {
|
||||
state.prevcur = state.curtoken;
|
||||
state.curtoken = -1;
|
||||
// counter HOTP
|
||||
if (state.cnt > 0) {
|
||||
state.cnt--;
|
||||
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) {
|
||||
clearTimeout(state.drawtimer);
|
||||
|
|
@ -242,97 +169,236 @@ function draw() {
|
|||
state.drawtimer = setTimeout(timerfn, timerdly);
|
||||
}
|
||||
|
||||
function onTouch(zone, e) {
|
||||
if (e) {
|
||||
var id = Math.floor((state.listy + (e.y - Bangle.appRect.y)) / tokenheight);
|
||||
if (id == state.curtoken || tokens.length == 0 || id >= tokens.length) {
|
||||
id = -1;
|
||||
}
|
||||
if (state.curtoken != id) {
|
||||
if (id != -1) {
|
||||
var y = id * tokenheight - state.listy;
|
||||
if (y < 0) {
|
||||
state.listy += y;
|
||||
y = 0;
|
||||
function updateCurrentToken() {
|
||||
drawToken(state.id);
|
||||
timerCalc();
|
||||
}
|
||||
|
||||
function updateProgressBar() {
|
||||
drawProgressBar();
|
||||
timerCalc();
|
||||
}
|
||||
|
||||
function drawProgressBar() {
|
||||
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;
|
||||
if (y > Bangle.appRect.h) {
|
||||
state.listy += (y - Bangle.appRect.h);
|
||||
}
|
||||
state.otp = "";
|
||||
} else {
|
||||
// token not visible
|
||||
state.id = -1;
|
||||
}
|
||||
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) {
|
||||
if (e.x > g.getWidth() || e.y > g.getHeight()) return;
|
||||
if (e.dx == 0 && e.dy == 0) return;
|
||||
var newy = Math.min(state.listy - e.dy, tokens.length * tokenheight - Bangle.appRect.h);
|
||||
state.listy = Math.max(0, newy);
|
||||
draw();
|
||||
state.cnt = IDLE_REPEATS;
|
||||
if (e.b != 0 && e.dy != 0) {
|
||||
let y = E.clip(state.listy - E.clip(e.dy, -AR.h, AR.h), 0, Math.max(0, tokens.length * TOKEN_HEIGHT - AR.h));
|
||||
if (state.listy != y) {
|
||||
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) {
|
||||
if (e == 1) {
|
||||
state.cnt = IDLE_REPEATS;
|
||||
switch (e) {
|
||||
case 1:
|
||||
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) {
|
||||
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();
|
||||
timerCalc();
|
||||
}
|
||||
|
||||
function bangle1Btn(e) {
|
||||
function bangleBtn(e) {
|
||||
state.cnt = IDLE_REPEATS;
|
||||
if (tokens.length > 0) {
|
||||
if (state.curtoken == -1) {
|
||||
state.curtoken = state.prevcur;
|
||||
} else {
|
||||
switch (e) {
|
||||
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
|
||||
let id = E.clip(state.id + e, 0, tokens.length - 1);
|
||||
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))});
|
||||
changeId(id);
|
||||
drawProgressBar();
|
||||
}
|
||||
timerCalc();
|
||||
}
|
||||
|
||||
function exitApp() {
|
||||
if (state.drawtimer) {
|
||||
clearTimeout(state.drawtimer);
|
||||
}
|
||||
Bangle.showLauncher();
|
||||
}
|
||||
|
||||
Bangle.on('touch', onTouch);
|
||||
Bangle.on('drag' , onDrag );
|
||||
Bangle.on('swipe', onSwipe);
|
||||
if (typeof BTN2 == 'number') {
|
||||
setWatch(function(){bangle1Btn(-1);}, BTN1, {edge:"rising" , debounce:50, repeat:true});
|
||||
setWatch(function(){exitApp(); }, BTN2, {edge:"falling", debounce:50});
|
||||
setWatch(function(){bangle1Btn( 1);}, BTN3, {edge:"rising" , debounce:50, repeat:true});
|
||||
if (typeof BTN1 == 'number') {
|
||||
if (typeof BTN2 == 'number' && typeof BTN3 == 'number') {
|
||||
setWatch(()=>bangleBtn(-1), BTN1, {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();
|
||||
|
||||
// Clear the screen once, at startup
|
||||
const AR = Bangle.appRect;
|
||||
// draw the initial display
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -54,9 +54,9 @@ var tokens = settings.tokens;
|
|||
*/
|
||||
function base32clean(val, nows) {
|
||||
var ret = val.replaceAll(/\s+/g, ' ');
|
||||
ret = ret.replaceAll(/0/g, 'O');
|
||||
ret = ret.replaceAll(/1/g, 'I');
|
||||
ret = ret.replaceAll(/8/g, 'B');
|
||||
ret = ret.replaceAll('0', 'O');
|
||||
ret = ret.replaceAll('1', 'I');
|
||||
ret = ret.replaceAll('8', 'B');
|
||||
ret = ret.replaceAll(/[^A-Za-z2-7 ]/g, '');
|
||||
if (nows) {
|
||||
ret = ret.replaceAll(/\s+/g, '');
|
||||
|
|
@ -81,9 +81,9 @@ function b32encode(str) {
|
|||
|
||||
function b32decode(seedstr) {
|
||||
// RFC4648
|
||||
var i, buf = 0, bitcount = 0, ret = '';
|
||||
for (i in seedstr) {
|
||||
var c = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'.indexOf(seedstr.charAt(i).toUpperCase(), 0);
|
||||
var buf = 0, bitcount = 0, ret = '';
|
||||
for (var c of seedstr.toUpperCase()) {
|
||||
c = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'.indexOf(c);
|
||||
if (c != -1) {
|
||||
buf <<= 5;
|
||||
buf |= c;
|
||||
|
|
@ -405,7 +405,7 @@ class proto3decoder {
|
|||
constructor(str) {
|
||||
this.buf = [];
|
||||
for (let i in str) {
|
||||
this.buf = this.buf.concat(str.charCodeAt(i));
|
||||
this.buf.push(str.charCodeAt(i));
|
||||
}
|
||||
}
|
||||
getVarint() {
|
||||
|
|
@ -487,7 +487,7 @@ function startScan(handler,cancel) {
|
|||
document.body.className = 'scanning';
|
||||
navigator.mediaDevices
|
||||
.getUserMedia({video:{facingMode:'environment'}})
|
||||
.then(function(stream){
|
||||
.then(stream => {
|
||||
scanning=true;
|
||||
video.setAttribute('playsinline',true);
|
||||
video.srcObject = stream;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"shortName": "AuthWatch",
|
||||
"icon": "app.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.",
|
||||
"tags": "tool",
|
||||
"interface": "interface.html",
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 1.7 KiB |
|
|
@ -49,3 +49,4 @@
|
|||
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.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
|
||||
|
|
|
|||
|
|
@ -14,6 +14,6 @@ if (!clockApp) {
|
|||
if (clockApp)
|
||||
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);
|
||||
delete clockApp;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "boot",
|
||||
"name": "Bootloader",
|
||||
"version": "0.45",
|
||||
"version": "0.46",
|
||||
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
|
||||
"icon": "bootloader.png",
|
||||
"type": "bootloader",
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
0.01: Initial upload
|
||||
0.2: Added scrollable calendar and swipe gestures
|
||||
0.3: Configurable drag gestures
|
||||
|
|
|
|||
|
|
@ -9,25 +9,24 @@ I know that it seems redundant because there already **is** a *time&cal*-app, bu
|
|||
||unlocked: smaller clock, but with seconds|
|
||||
||swipe up for big calendar, (up down to scroll, left/right to exit)|
|
||||
|
||||
|
||||
|
||||
|
||||
## Configurable Features
|
||||
- Number of calendar rows (weeks)
|
||||
- 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
|
||||
- Red Saturday
|
||||
- Red Sunday
|
||||
- Swipes (to disable all gestures)
|
||||
- Swipes: music (swipe down)
|
||||
- Spipes: messages (swipe right)
|
||||
- Red Saturday/Sunday
|
||||
- Swipe/Drag gestures to launch features or 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 right will search your files for an app with the string "message" in its filename and launch it.
|
||||
- Configurable apps coming soon.
|
||||
- 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 "music" in its filename and launch it. (configurable)
|
||||
|
||||
## Feedback
|
||||
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
|
||||
|
||||
## 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)
|
||||
|
|
|
|||
|
|
@ -4,12 +4,13 @@ var s = Object.assign({
|
|||
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
|
||||
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?
|
||||
REDSAT: true, // Use red color for saturday?
|
||||
DRAGENABLED: true,
|
||||
DRAGMUSIC: true,
|
||||
DRAGMESSAGES: true
|
||||
DRAGDOWN: "[AI:messg]",
|
||||
DRAGRIGHT: "[AI:music]",
|
||||
DRAGLEFT: "[ignore]",
|
||||
DRAGUP: "[calend.]"
|
||||
}, require('Storage').readJSON("clockcal.json", true) || {});
|
||||
|
||||
const h = g.getHeight();
|
||||
|
|
@ -27,13 +28,13 @@ var monthOffset = 0;
|
|||
*/
|
||||
function drawFullCalendar(monthOffset) {
|
||||
addMonths = function (_d, _am) {
|
||||
var ay = 0, m = _d.getMonth(), y = _d.getFullYear();
|
||||
while ((m + _am) > 11) { ay++; _am -= 12; }
|
||||
while ((m + _am) < 0) { ay--; _am += 12; }
|
||||
n = new Date(_d.getTime());
|
||||
n.setMonth(m + _am);
|
||||
n.setFullYear(y + ay);
|
||||
return n;
|
||||
var ay = 0, m = _d.getMonth(), y = _d.getFullYear();
|
||||
while ((m + _am) > 11) { ay++; _am -= 12; }
|
||||
while ((m + _am) < 0) { ay--; _am += 12; }
|
||||
n = new Date(_d.getTime());
|
||||
n.setMonth(m + _am);
|
||||
n.setFullYear(y + ay);
|
||||
return n;
|
||||
};
|
||||
monthOffset = (typeof monthOffset == "undefined") ? 0 : monthOffset;
|
||||
state = "calendar";
|
||||
|
|
@ -45,22 +46,22 @@ function drawFullCalendar(monthOffset) {
|
|||
if (typeof minuteInterval !== "undefined") clearTimeout(minuteInterval);
|
||||
d = addMonths(Date(), monthOffset);
|
||||
tdy = Date().getDate() + "." + Date().getMonth();
|
||||
newmonth=false;
|
||||
newmonth = false;
|
||||
c_y = 0;
|
||||
g.reset();
|
||||
g.setBgColor(0);
|
||||
g.clear();
|
||||
var prevmonth = addMonths(d, -1)
|
||||
var prevmonth = addMonths(d, -1);
|
||||
const today = prevmonth.getDate();
|
||||
var rD = new Date(prevmonth.getTime());
|
||||
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);
|
||||
var rDate = rD.getDate();
|
||||
bottomrightY = c_y - 3;
|
||||
clrsun=s.REDSUN?'#f00':'#fff';
|
||||
clrsat=s.REDSUN?'#f00':'#fff';
|
||||
var fg=[clrsun,'#fff','#fff','#fff','#fff','#fff',clrsat];
|
||||
clrsun = s.REDSUN ? '#f00' : '#fff';
|
||||
clrsat = s.REDSUN ? '#f00' : '#fff';
|
||||
var fg = [clrsun, '#fff', '#fff', '#fff', '#fff', '#fff', clrsat];
|
||||
for (var y = 1; y <= 11; y++) {
|
||||
bottomrightY += CELL_H;
|
||||
bottomrightX = -2;
|
||||
|
|
@ -69,14 +70,14 @@ function drawFullCalendar(monthOffset) {
|
|||
rMonth = rD.getMonth();
|
||||
rDate = rD.getDate();
|
||||
if (tdy == rDate + "." + rMonth) {
|
||||
caldrawToday(rDate);
|
||||
caldrawToday(rDate);
|
||||
} else if (rDate == 1) {
|
||||
caldrawFirst(rDate);
|
||||
caldrawFirst(rDate);
|
||||
} else {
|
||||
caldrawNormal(rDate,fg[rD.getDay()]);
|
||||
caldrawNormal(rDate, fg[rD.getDay()]);
|
||||
}
|
||||
if (newmonth && x == 7) {
|
||||
caldrawMonth(rDate,monthclr[rMonth % 6],months[rMonth],rD);
|
||||
caldrawMonth(rDate, monthclr[rMonth % 6], months[rMonth], rD);
|
||||
}
|
||||
rD.setDate(rDate + 1);
|
||||
}
|
||||
|
|
@ -84,7 +85,7 @@ function drawFullCalendar(monthOffset) {
|
|||
delete addMonths;
|
||||
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.setFont("Vector", 18);
|
||||
g.setFontAlign(-1, 1, 1);
|
||||
|
|
@ -93,29 +94,29 @@ function caldrawMonth(rDate,c,m,rD) {
|
|||
newmonth = false;
|
||||
}
|
||||
function caldrawToday(rDate) {
|
||||
g.setFont("Vector", 16);
|
||||
g.setFontAlign(1, 1);
|
||||
g.setColor('#0f0');
|
||||
g.fillRect(bottomrightX - CELL2_W + 1, bottomrightY - CELL_H - 1, bottomrightX, bottomrightY - 2);
|
||||
g.setColor('#000');
|
||||
g.drawString(rDate, bottomrightX, bottomrightY);
|
||||
g.setFont("Vector", 16);
|
||||
g.setFontAlign(1, 1);
|
||||
g.setColor('#0f0');
|
||||
g.fillRect(bottomrightX - CELL2_W + 1, bottomrightY - CELL_H - 1, bottomrightX, bottomrightY - 2);
|
||||
g.setColor('#000');
|
||||
g.drawString(rDate, bottomrightX, bottomrightY);
|
||||
}
|
||||
function caldrawFirst(rDate) {
|
||||
g.flip();
|
||||
g.setFont("Vector", 16);
|
||||
g.setFontAlign(1, 1);
|
||||
bottomrightY += 3;
|
||||
newmonth = true;
|
||||
g.setColor('#0ff');
|
||||
g.fillRect(bottomrightX - CELL2_W + 1, bottomrightY - CELL_H - 1, bottomrightX, bottomrightY - 2);
|
||||
g.setColor('#000');
|
||||
g.drawString(rDate, bottomrightX, bottomrightY);
|
||||
g.flip();
|
||||
g.setFont("Vector", 16);
|
||||
g.setFontAlign(1, 1);
|
||||
bottomrightY += 3;
|
||||
newmonth = true;
|
||||
g.setColor('#0ff');
|
||||
g.fillRect(bottomrightX - CELL2_W + 1, bottomrightY - CELL_H - 1, bottomrightX, bottomrightY - 2);
|
||||
g.setColor('#000');
|
||||
g.drawString(rDate, bottomrightX, bottomrightY);
|
||||
}
|
||||
function caldrawNormal(rDate,c) {
|
||||
g.setFont("Vector", 16);
|
||||
g.setFontAlign(1, 1);
|
||||
g.setColor(c);
|
||||
g.drawString(rDate, bottomrightX, bottomrightY);//100
|
||||
function caldrawNormal(rDate, c) {
|
||||
g.setFont("Vector", 16);
|
||||
g.setFontAlign(1, 1);
|
||||
g.setColor(c);
|
||||
g.drawString(rDate, bottomrightX, bottomrightY);//100
|
||||
}
|
||||
function drawMinutes() {
|
||||
if (DEBUG) console.log("|-->minutes");
|
||||
|
|
@ -163,7 +164,7 @@ function drawWatch() {
|
|||
g.clear();
|
||||
drawMinutes();
|
||||
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();
|
||||
var rD = new Date(d.getTime());
|
||||
rD.setDate(rD.getDate() - dow);
|
||||
|
|
@ -205,27 +206,52 @@ function BTevent() {
|
|||
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) {
|
||||
if (s.DRAGENABLED) {
|
||||
Bangle.buzz(100,1);
|
||||
console.log("swipe:"+dir);
|
||||
Bangle.buzz(100, 1);
|
||||
if (DEBUG) console.log("swipe:" + dir);
|
||||
switch (dir) {
|
||||
case "r":
|
||||
if (state == "calendar") {
|
||||
drawWatch();
|
||||
} else {
|
||||
if (s.DRAGMUSIC) {
|
||||
l=require("Storage").list(RegExp("music.*app"));
|
||||
if (l.length > 0) {
|
||||
load(l[0]);
|
||||
} else Bangle.buzz(3000,1);//not found
|
||||
}
|
||||
action(s.DRAGRIGHT);
|
||||
}
|
||||
break;
|
||||
case "l":
|
||||
if (state == "calendar") {
|
||||
drawWatch();
|
||||
} else {
|
||||
action(s.DRAGLEFT);
|
||||
}
|
||||
break;
|
||||
case "d":
|
||||
|
|
@ -233,21 +259,15 @@ function input(dir) {
|
|||
monthOffset--;
|
||||
drawFullCalendar(monthOffset);
|
||||
} else {
|
||||
if (s.DRAGMESSAGES) {
|
||||
l=require("Storage").list(RegExp("message.*app"));
|
||||
if (l.length > 0) {
|
||||
load(l[0]);
|
||||
} else Bangle.buzz(3000,1);//not found
|
||||
}
|
||||
action(s.DRAGDOWN);
|
||||
}
|
||||
break;
|
||||
case "u":
|
||||
if (state == "watch") {
|
||||
state = "calendar";
|
||||
drawFullCalendar(0);
|
||||
} else if (state == "calendar") {
|
||||
if (state == "calendar") {
|
||||
monthOffset++;
|
||||
drawFullCalendar(monthOffset);
|
||||
} else {
|
||||
action(s.DRAGUP);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
|
@ -255,26 +275,24 @@ function input(dir) {
|
|||
drawWatch();
|
||||
}
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let drag;
|
||||
Bangle.on("drag", e => {
|
||||
if (s.DRAGENABLED) {
|
||||
if (!drag) {
|
||||
drag = { x: e.x, y: e.y };
|
||||
} else if (!e.b) {
|
||||
const dx = e.x - drag.x, dy = e.y - drag.y;
|
||||
var dir = "t";
|
||||
if (Math.abs(dx) > Math.abs(dy) + 10) {
|
||||
dir = (dx > 0) ? "r" : "l";
|
||||
} else if (Math.abs(dy) > Math.abs(dx) + 10) {
|
||||
dir = (dy > 0) ? "d" : "u";
|
||||
}
|
||||
drag = null;
|
||||
input(dir);
|
||||
if (!drag) {
|
||||
drag = { x: e.x, y: e.y };
|
||||
} else if (!e.b) {
|
||||
const dx = e.x - drag.x, dy = e.y - drag.y;
|
||||
var dir = "t";
|
||||
if (Math.abs(dx) > Math.abs(dy) + 20) {
|
||||
dir = (dx > 0) ? "r" : "l";
|
||||
} else if (Math.abs(dy) > Math.abs(dx) + 20) {
|
||||
dir = (dy > 0) ? "d" : "u";
|
||||
}
|
||||
drag = null;
|
||||
input(dir);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "clockcal",
|
||||
"name": "Clock & Calendar",
|
||||
"version": "0.2",
|
||||
"version": "0.3",
|
||||
"description": "Clock with Calendar",
|
||||
"readme":"README.md",
|
||||
"icon": "app.png",
|
||||
|
|
|
|||
|
|
@ -1,19 +1,22 @@
|
|||
(function (back) {
|
||||
var FILE = "clockcal.json";
|
||||
|
||||
settings = Object.assign({
|
||||
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. Will be extra widget eventually
|
||||
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, //Enable drag gestures (bigger calendar etc)
|
||||
DRAGMUSIC: true, //Enable drag down for music (looks for "music*app")
|
||||
DRAGMESSAGES: true //Enable drag right for messages (looks for "message*app")
|
||||
}, require('Storage').readJSON(FILE, true) || {});
|
||||
|
||||
DRAGDOWN: "[AI:messg]",
|
||||
DRAGRIGHT: "[AI:music]",
|
||||
DRAGLEFT: "[ignore]",
|
||||
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() {
|
||||
require('Storage').writeJSON(FILE, settings);
|
||||
}
|
||||
|
|
@ -70,27 +73,39 @@
|
|||
writeSettings();
|
||||
}
|
||||
},
|
||||
'Swipes (big cal.)?': {
|
||||
value: settings.DRAGENABLED,
|
||||
format: v => v ? "On" : "Off",
|
||||
'Drag Up ': {
|
||||
min:0, max:actions.length-1,
|
||||
value: actions.indexOf(settings.DRAGUP),
|
||||
format: v => actions[v],
|
||||
onchange: v => {
|
||||
settings.DRAGENABLED = v;
|
||||
settings.DRAGUP = actions[v];
|
||||
writeSettings();
|
||||
}
|
||||
},
|
||||
'Swipes (music)?': {
|
||||
value: settings.DRAGMUSIC,
|
||||
format: v => v ? "On" : "Off",
|
||||
'Drag Right': {
|
||||
min:0, max:actions.length-1,
|
||||
value: actions.indexOf(settings.DRAGRIGHT),
|
||||
format: v => actions[v],
|
||||
onchange: v => {
|
||||
settings.DRAGMUSIC = v;
|
||||
settings.DRAGRIGHT = actions[v];
|
||||
writeSettings();
|
||||
}
|
||||
},
|
||||
'Swipes (messg)?': {
|
||||
value: settings.DRAGMESSAGES,
|
||||
format: v => v ? "On" : "Off",
|
||||
'Drag Down': {
|
||||
min:0, max:actions.length-1,
|
||||
value: actions.indexOf(settings.DRAGDOWN),
|
||||
format: v => actions[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();
|
||||
}
|
||||
},
|
||||
|
|
@ -100,17 +115,7 @@
|
|||
format: v => ["No", "Yes"][v],
|
||||
onchange: v => {
|
||||
if (v == 1) {
|
||||
settings = {
|
||||
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
|
||||
};
|
||||
settings = defaults;
|
||||
writeSettings();
|
||||
load();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
0.01: New App!
|
||||
0.02: Load widgets after setUI so widclk knows when to hide
|
||||
0.03: Show the day of the week
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ function draw() {
|
|||
var date = new Date();
|
||||
var timeStr = require("locale").time(date,1);
|
||||
var dateStr = require("locale").date(date).toUpperCase();
|
||||
var dowStr = require("locale").dow(date).toUpperCase();
|
||||
// draw time
|
||||
g.setFontAlign(0,0).setFont("ZCOOL");
|
||||
g.drawString(timeStr,x,y);
|
||||
|
|
@ -48,6 +49,9 @@ function draw() {
|
|||
y += 35;
|
||||
g.setFontAlign(0,0,1).setFont("6x8");
|
||||
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
|
||||
queueDraw();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "waveclk",
|
||||
"name": "Wave Clock",
|
||||
"version": "0.02",
|
||||
"version": "0.03",
|
||||
"description": "A clock using a wave image by [Lillith May](https://www.instagram.com/_lilustrations_/)",
|
||||
"icon": "app.png",
|
||||
"screenshots": [{"url":"screenshot.png"}],
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 6.1 KiB |
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -179,5 +179,6 @@
|
|||
<script src="core/js/appinfo.js"></script>
|
||||
<script src="core/js/index.js"></script>
|
||||
<script src="core/js/pwa.js" defer></script>
|
||||
<script src="gadgetbridge.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||