mirror of https://github.com/espruino/BangleApps
Merge branch 'master' into master
commit
69c1d66a7f
23
apps.json
23
apps.json
|
@ -104,7 +104,7 @@
|
|||
"id": "launch",
|
||||
"name": "Launcher",
|
||||
"shortName": "Launcher",
|
||||
"version": "0.09",
|
||||
"version": "0.10",
|
||||
"description": "This is needed to display a menu allowing you to choose your own applications. You can replace this with a customised launcher.",
|
||||
"icon": "app.png",
|
||||
"type": "launch",
|
||||
|
@ -112,8 +112,10 @@
|
|||
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||
"storage": [
|
||||
{"name":"launch.app.js","url":"app-bangle1.js","supports":["BANGLEJS"]},
|
||||
{"name":"launch.app.js","url":"app-bangle2.js","supports":["BANGLEJS2"]}
|
||||
{"name":"launch.app.js","url":"app-bangle2.js","supports":["BANGLEJS2"]},
|
||||
{"name":"launch.settings.js","url":"settings.js","supports":["BANGLEJS2"]}
|
||||
],
|
||||
"data": [{"name":"launch.json"}],
|
||||
"sortorder": -10
|
||||
},
|
||||
{
|
||||
|
@ -4449,7 +4451,7 @@
|
|||
"shortName": "AuthWatch",
|
||||
"icon": "app.png",
|
||||
"screenshots": [{"url":"screenshot.png"}],
|
||||
"version": "0.03",
|
||||
"version": "0.04",
|
||||
"description": "Google Authenticator compatible tool.",
|
||||
"tags": "tool",
|
||||
"interface": "interface.html",
|
||||
|
@ -4687,6 +4689,21 @@
|
|||
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||
"storage": [
|
||||
{"name":"widChargingStatus.wid.js","url":"widget.js"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "flow",
|
||||
"name": "FLOW",
|
||||
"shortName": "FLOW",
|
||||
"version": "0.01",
|
||||
"description": "A game where you have to help a flow avoid white obstacles thing by tapping! This is a demake of an app which I forgot the name of. Press BTN(1) to restart. See if you can get to 2500 score!",
|
||||
"icon": "app.png",
|
||||
"tags": "game",
|
||||
"supports" : ["BANGLEJS", "BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name": "flow.app.js", "url": "app.js" },
|
||||
{"name": "flow.img", "url": "app-icon.js","evaluate": true }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
0.04: Fix tapping at very bottom of list, exit on inactivity
|
||||
0.03: Add "Calculating" placeholder, update JSON save format
|
||||
0.02: Fix JSON save format
|
||||
0.01: First release
|
||||
|
|
|
@ -10,6 +10,8 @@ const calculating = "Calculating";
|
|||
const notokens = "No tokens";
|
||||
const notsupported = "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:{}};
|
||||
if (settings.data ) tokens = settings.data ; /* v0.02 settings */
|
||||
if (settings.tokens) tokens = settings.tokens; /* v0.03+ settings */
|
||||
|
@ -146,14 +148,14 @@ function drawToken(id, r) {
|
|||
// counter - draw triangle as swipe hint
|
||||
let yc = (y1 + y2) / 2;
|
||||
g.fillPoly([0, yc, 10, yc - 10, 10, yc + 10, 0, yc]);
|
||||
adj = 5;
|
||||
adj = 10;
|
||||
}
|
||||
// digits just below label
|
||||
sz = 30;
|
||||
do {
|
||||
g.setFont("Vector", sz--);
|
||||
} while (g.stringWidth(state.otp) > (r.w - adj));
|
||||
g.drawString(state.otp, (x1 + x2) / 2 + adj, y1 + 16, false);
|
||||
g.drawString(state.otp, (x1 + adj + x2) / 2, y1 + 16, false);
|
||||
}
|
||||
// shaded lines top and bottom
|
||||
g.setColor(0.5, 0.5, 0.5);
|
||||
|
@ -163,6 +165,8 @@ function drawToken(id, r) {
|
|||
}
|
||||
|
||||
function draw() {
|
||||
var timerfn = exitApp;
|
||||
var timerdly = 10000;
|
||||
var d = new Date();
|
||||
if (state.curtoken != -1) {
|
||||
var t = tokens[state.curtoken];
|
||||
|
@ -203,17 +207,13 @@ function draw() {
|
|||
y += tokenentryheight;
|
||||
}
|
||||
if (drewcur) {
|
||||
// the current token has been drawn - draw it again in 1sec
|
||||
if (state.drawtimer) {
|
||||
clearTimeout(state.drawtimer);
|
||||
}
|
||||
var dly;
|
||||
// the current token has been drawn - schedule a redraw
|
||||
if (tokens[state.curtoken].period > 0) {
|
||||
dly = (state.otp == calculating) ? 1 : 1000;
|
||||
timerdly = (state.otp == calculating) ? 1 : 1000; // timed
|
||||
} else {
|
||||
dly = state.nexttime - d.getTime();
|
||||
timerdly = state.nexttime - d.getTime(); // counter
|
||||
}
|
||||
state.drawtimer = setTimeout(draw, dly);
|
||||
timerfn = draw;
|
||||
if (tokens[state.curtoken].period <= 0) {
|
||||
state.hide = 0;
|
||||
}
|
||||
|
@ -230,12 +230,16 @@ function draw() {
|
|||
g.setFontAlign(0, 0, 0);
|
||||
g.drawString(notokens, Bangle.appRect.x + Bangle.appRect.w / 2, Bangle.appRect.y + Bangle.appRect.h / 2, false);
|
||||
}
|
||||
if (state.drawtimer) {
|
||||
clearTimeout(state.drawtimer);
|
||||
}
|
||||
state.drawtimer = setTimeout(timerfn, timerdly);
|
||||
}
|
||||
|
||||
function onTouch(zone, e) {
|
||||
if (e) {
|
||||
var id = Math.floor((state.listy + (e.y - Bangle.appRect.y)) / tokenentryheight);
|
||||
if (id == state.curtoken || tokens.length == 0) {
|
||||
if (id == state.curtoken || tokens.length == 0 || id >= tokens.length) {
|
||||
id = -1;
|
||||
}
|
||||
if (state.curtoken != id) {
|
||||
|
@ -254,26 +258,20 @@ function onTouch(zone, e) {
|
|||
state.nextTime = 0;
|
||||
state.curtoken = id;
|
||||
state.hide = 2;
|
||||
draw();
|
||||
}
|
||||
}
|
||||
draw();
|
||||
}
|
||||
|
||||
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 * tokenentryheight - Bangle.appRect.h);
|
||||
newy = Math.max(0, newy);
|
||||
if (newy != state.listy) {
|
||||
state.listy = newy;
|
||||
draw();
|
||||
}
|
||||
state.listy = Math.max(0, newy);
|
||||
draw();
|
||||
}
|
||||
|
||||
function onSwipe(e) {
|
||||
if (e == 1) {
|
||||
Bangle.showLauncher();
|
||||
}
|
||||
if (e == -1 && state.curtoken != -1 && tokens[state.curtoken].period <= 0) {
|
||||
tokens[state.curtoken].period--;
|
||||
let newsettings={tokens:tokens,misc:settings.misc};
|
||||
|
@ -281,8 +279,8 @@ function onSwipe(e) {
|
|||
state.nextTime = 0;
|
||||
state.otp = "";
|
||||
state.hide = 2;
|
||||
draw();
|
||||
}
|
||||
draw();
|
||||
}
|
||||
|
||||
function bangle1Btn(e) {
|
||||
|
@ -302,16 +300,22 @@ function bangle1Btn(e) {
|
|||
state.curtoken = -1;
|
||||
state.nextTime = 0;
|
||||
onTouch(0, fakee);
|
||||
} else {
|
||||
draw(); // resets idle timer
|
||||
}
|
||||
}
|
||||
|
||||
function exitApp() {
|
||||
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(){Bangle.showLauncher();}, BTN2, {edge:"rising", debounce:50, repeat:true});
|
||||
setWatch(function(){bangle1Btn( 1); }, BTN3, {edge:"rising", debounce:50, repeat:true});
|
||||
setWatch(function(){bangle1Btn(-1);}, BTN1, {edge:"rising", debounce:50, repeat:true});
|
||||
setWatch(function(){exitApp(); }, BTN2, {edge:"rising", debounce:50, repeat:true});
|
||||
setWatch(function(){bangle1Btn( 1);}, BTN3, {edge:"rising", debounce:50, repeat:true});
|
||||
}
|
||||
Bangle.loadWidgets();
|
||||
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
# FLOW
|
||||
|
||||
This is a game where you have to help a flow avoid white obstacles thing by tapping!
|
||||
This is a demake of an app which I forgot the name of.
|
||||
Press BTN(1) to restart.
|
||||
See if you can get to 2500 score!
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||

|
||||

|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEw4X/AwX48EHgEC1WgCQkVqoDBBfuqBQcBqoLagEqGAguBqALaGAOoAoQuEBbEAKgIMBBQNUBbgMCyoKHBbBVBBYIKGBbEBtNVrQLfOgNaT4gLagp0CPQOABbcBFwNAgEKBgILbitVqAFClWq0ALZFwTDFGAQLZFwYwDBfg"))
|
|
@ -0,0 +1,220 @@
|
|||
const isB2 = process.env.HWVERSION === 2;
|
||||
|
||||
// Bangle.js 1 runs just too fast in direct mode??? (also no getPixel)
|
||||
if (!isB2) Bangle.setLCDMode("120x120");
|
||||
|
||||
const options = Bangle.getOptions();
|
||||
|
||||
options.lockTimeout = 0;
|
||||
options.lcdPowerTimeout = 0;
|
||||
|
||||
Bangle.setOptions(options);
|
||||
|
||||
g.reset();
|
||||
g.setBgColor(0, 0, 0);
|
||||
g.setColor(255, 255, 255);
|
||||
g.clear();
|
||||
const h = g.getHeight();
|
||||
|
||||
function trigToCoord(ret) {
|
||||
return ((ret + 1) * h) / 2;
|
||||
}
|
||||
|
||||
function trigToLen(ret) {
|
||||
return (ret * h) / 2;
|
||||
}
|
||||
|
||||
let i = 0.2;
|
||||
let speedCoef = 0.014;
|
||||
|
||||
let flowFile = require("Storage").readJSON("flow.json");
|
||||
|
||||
let highestI = (flowFile && flowFile.hiscore) || 0.1;
|
||||
|
||||
let colorA = [255, 255, 0];
|
||||
let colorB = [0, 255, 255];
|
||||
|
||||
let x = 0;
|
||||
let xt = 0;
|
||||
let safeMode = false;
|
||||
let lost = false;
|
||||
|
||||
function offsetRect(g, x, y, w) {
|
||||
g.fillRect(x, y, x + w, y + w);
|
||||
}
|
||||
|
||||
function getColor(num) {
|
||||
return [
|
||||
[1, 0, 0],
|
||||
[0, 1, 0],
|
||||
[0, 0, 1],
|
||||
[1, 1, 0],
|
||||
[0, 1, 1],
|
||||
[1, 0, 1],
|
||||
[0.5, 0.5, 1],
|
||||
[1, 0.5, 0],
|
||||
[0, 1, 0.5],
|
||||
[0.5, 0.5, 0.5],
|
||||
][num];
|
||||
}
|
||||
|
||||
function calculateColor(num) {
|
||||
colorA = getColor(Math.floor((num % 1) * 10));
|
||||
colorB = getColor(Math.floor((num % 10) - (num % 1)));
|
||||
}
|
||||
|
||||
calculateColor(highestI);
|
||||
|
||||
Bangle.on("touch", () => (safeMode = !safeMode));
|
||||
|
||||
function resetGame() {
|
||||
x = xt = 0;
|
||||
safeMode = lost = false;
|
||||
i = 0.2;
|
||||
speedCoef = 0.014;
|
||||
obstaclePeriod = 150;
|
||||
obstacleMode = 1;
|
||||
g.clear();
|
||||
shownScore = false;
|
||||
intervalId = setInterval(draw);
|
||||
}
|
||||
|
||||
function checkCollision() {
|
||||
lost = g.getPixel(trigToCoord(+x), (h * 2) / 3 - 4) !== 0;
|
||||
if (lost) {
|
||||
scoringI = i;
|
||||
speedCoef = Math.min(speedCoef, 0.02);
|
||||
g.setFont(isB2 ? "6x15" : "4x6", 3);
|
||||
g.setColor(colorA[0], colorA[1], colorA[2])
|
||||
.drawString(
|
||||
"Game over",
|
||||
trigToCoord(0) - g.stringWidth("Game over") / 2,
|
||||
trigToCoord(0)
|
||||
)
|
||||
.setColor(1, 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function drawPlayer() {
|
||||
if (!safeMode) xt = Math.cos(i * Math.PI * 4) / 7.5;
|
||||
else xt = -Math.cos(i * Math.PI * 2) / 20 + 0.35;
|
||||
x = x * 0.8 + xt * 0.2;
|
||||
if (highestI > 250) calculateColor(i);
|
||||
g.setColor(colorA[0], colorA[1], colorA[2]);
|
||||
offsetRect(g, trigToCoord(+x), (h * 2) / 3, 3);
|
||||
g.setColor(colorB[0], colorB[1], colorB[2]);
|
||||
offsetRect(g, trigToCoord(-x), (h * 2) / 3, 3);
|
||||
}
|
||||
|
||||
let obstaclePeriod = 150;
|
||||
let obstacleMode = 1;
|
||||
|
||||
function drawObstracle() {
|
||||
g.setColor(1, 1, 1);
|
||||
switch (obstacleMode) {
|
||||
case 0:
|
||||
offsetRect(g, trigToCoord(-0.15), 0, trigToLen(0.3));
|
||||
break;
|
||||
case 1:
|
||||
offsetRect(g, trigToCoord(0.2), 0, trigToLen(0.2));
|
||||
offsetRect(g, trigToCoord(-0.4), 0, trigToLen(0.2));
|
||||
break;
|
||||
case 2:
|
||||
break;
|
||||
}
|
||||
obstaclePeriod--;
|
||||
if (obstaclePeriod <= 0) {
|
||||
// If we are off cooldown mode, pick a random actual mode
|
||||
if (obstacleMode === 2) {
|
||||
obstaclePeriod = Math.random() * 50 + 50;
|
||||
obstacleMode = Math.round(Math.random());
|
||||
} else if (Math.random() > 0.5) {
|
||||
// Give it a chance to repeat with no cooldown
|
||||
obstaclePeriod = 25 + 2.5 * speedCoef;
|
||||
obstacleMode = 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let shownScore = false;
|
||||
let scoringI = 0;
|
||||
|
||||
function draw() {
|
||||
if (!lost) {
|
||||
drawPlayer();
|
||||
checkCollision();
|
||||
speedCoef *= 1.0005;
|
||||
drawObstracle();
|
||||
} else {
|
||||
speedCoef /= 1.05;
|
||||
if (speedCoef <= 0.005) {
|
||||
clearInterval(intervalId);
|
||||
i -= speedCoef;
|
||||
g.setFont(isB2 ? "6x15" : "4x6", 1);
|
||||
const str = "Hiscore: " + Math.round(highestI * 10);
|
||||
g.setColor(
|
||||
scoringI > highestI ? 0 : 255,
|
||||
0,
|
||||
scoringI > highestI ? 255 : 0
|
||||
)
|
||||
.drawString(
|
||||
str,
|
||||
trigToCoord(0) - g.stringWidth(str) / 2,
|
||||
trigToCoord(0)
|
||||
)
|
||||
.setColor(255, 255, 255);
|
||||
if (scoringI > highestI) {
|
||||
highestI = scoringI;
|
||||
require("Storage").writeJSON("flow.json", {
|
||||
hiscore: highestI,
|
||||
});
|
||||
calculateColor(highestI);
|
||||
}
|
||||
setTimeout(resetGame, 3000);
|
||||
} else if (speedCoef <= 0.01 && !shownScore) {
|
||||
shownScore = true;
|
||||
g.setFont(isB2 ? "6x15" : "4x6", 2);
|
||||
const str = "Score: " + Math.round(scoringI * 10);
|
||||
g.setColor(colorB[0], colorB[1], colorB[2])
|
||||
.drawString(
|
||||
str,
|
||||
trigToCoord(0) - g.stringWidth(str) / 2,
|
||||
trigToCoord(0)
|
||||
)
|
||||
.setColor(1, 1, 1);
|
||||
}
|
||||
}
|
||||
i += speedCoef;
|
||||
g.scroll(0, speedCoef * h);
|
||||
g.flip();
|
||||
}
|
||||
|
||||
let intervalId;
|
||||
|
||||
if (BTN.read()) {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
color = getColor(i);
|
||||
g.setColor(color[0], color[1], color[2]);
|
||||
g.fillRect((i / 10) * h, 0, ((i + 1) / 10) * h, h);
|
||||
}
|
||||
g.setColor(0);
|
||||
g.setFont("Vector", 9);
|
||||
let str = "Welcome to the debug screen!";
|
||||
g.drawString(
|
||||
str,
|
||||
trigToCoord(0) - g.stringWidth(str) / 2,
|
||||
trigToCoord(0) - 9
|
||||
);
|
||||
str = "Don't hold BTN while opening to play!";
|
||||
g.drawString(str, trigToCoord(0) - g.stringWidth(str) / 2, trigToCoord(0));
|
||||
g.flip();
|
||||
setInterval(() => {
|
||||
g.scroll(0, 0.014 * h);
|
||||
i += 0.014;
|
||||
calculateColor(i);
|
||||
g.setColor(colorA[0], colorA[1], colorA[2]);
|
||||
g.fillRect(0, 0, trigToCoord(0), 0.014 * h);
|
||||
g.setColor(colorB[0], colorB[1], colorB[2]);
|
||||
g.fillRect(trigToCoord(0), 0, trigToCoord(1), 0.014 * h);
|
||||
}, 1000 / 30);
|
||||
} else intervalId = setInterval(draw, 1000 / 30);
|
Binary file not shown.
After Width: | Height: | Size: 323 B |
Binary file not shown.
After Width: | Height: | Size: 992 B |
Binary file not shown.
After Width: | Height: | Size: 2.5 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
|
@ -8,3 +8,4 @@
|
|||
0.08: Merge Bangle.js 1 and 2 launchers
|
||||
0.09: Bangle.js 2 - pressing the button goes back to clock (fix #971)
|
||||
After 10s of being locked, the launcher goes back to the clock screen
|
||||
0.10: added in selectable font in settings including scalable vector font
|
||||
|
|
|
@ -1,4 +1,22 @@
|
|||
var s = require("Storage");
|
||||
let fonts = g.getFonts();
|
||||
var scaleval = 1;
|
||||
var vectorval = 20;
|
||||
var font = g.getFonts().includes("12x20") ? "12x20" : "6x8:2";
|
||||
let settings = require('Storage').readJSON("launch.json", true) || {};
|
||||
if ("vectorsize" in settings) {
|
||||
vectorval = parseInt(settings.vectorsize);
|
||||
}
|
||||
if ("font" in settings){
|
||||
if(settings.font == "Vector"){
|
||||
scaleval = vectorval/20;
|
||||
font = "Vector"+(vectorval).toString();
|
||||
}
|
||||
else{
|
||||
font = settings.font;
|
||||
scaleval = (font.split('x')[1])/20;
|
||||
}
|
||||
}
|
||||
var apps = s.list(/\.info$/).map(app=>{var a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src};}).filter(app=>app && (app.type=="app" || app.type=="clock" || !app.type));
|
||||
apps.sort((a,b)=>{
|
||||
var n=(0|a.sortorder)-(0|b.sortorder);
|
||||
|
@ -11,8 +29,6 @@ apps.forEach(app=>{
|
|||
if (app.icon)
|
||||
app.icon = s.read(app.icon); // should just be a link to a memory area
|
||||
});
|
||||
// FIXME: not needed after 2v11
|
||||
var font = g.getFonts().includes("12x20") ? "12x20" : "6x8:2";
|
||||
// FIXME: check not needed after 2v11
|
||||
if (g.wrapString) {
|
||||
g.setFont(font);
|
||||
|
@ -22,9 +38,9 @@ if (g.wrapString) {
|
|||
function drawApp(i, r) {
|
||||
var app = apps[i];
|
||||
if (!app) return;
|
||||
g.clearRect(r.x,r.y,r.x+r.w-1, r.y+r.h-1);
|
||||
g.setFont(font).setFontAlign(-1,0).drawString(app.name,64,r.y+32);
|
||||
if (app.icon) try {g.drawImage(app.icon,8,r.y+8);} catch(e){}
|
||||
g.clearRect((r.x),(r.y),(r.x+r.w-1), (r.y+r.h-1));
|
||||
g.setFont(font).setFontAlign(-1,0).drawString(app.name,64*scaleval,r.y+(32*scaleval));
|
||||
if (app.icon) try {g.drawImage(app.icon,8*scaleval, r.y+(8*scaleval), {scale: scaleval});} catch(e){}
|
||||
}
|
||||
|
||||
g.clear();
|
||||
|
@ -32,7 +48,7 @@ Bangle.loadWidgets();
|
|||
Bangle.drawWidgets();
|
||||
|
||||
E.showScroller({
|
||||
h : 64, c : apps.length,
|
||||
h : 64*scaleval, c : apps.length,
|
||||
draw : drawApp,
|
||||
select : i => {
|
||||
var app = apps[i];
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
// make sure to enclose the function in parentheses
|
||||
(function(back) {
|
||||
let settings = require('Storage').readJSON('launch.json',1)||{};
|
||||
let fonts = g.getFonts();
|
||||
function save(key, value) {
|
||||
settings[key] = value;
|
||||
require('Storage').write('launch.json',settings);
|
||||
}
|
||||
const appMenu = {
|
||||
'': {'title': 'Launcher Settings'},
|
||||
'< Back': back,
|
||||
'Font': {
|
||||
value: fonts.includes(settings.font)? fonts.indexOf(settings.font) : fonts.indexOf("12x20"),
|
||||
min:0, max:fonts.length-1, step:1,wrap:true,
|
||||
onchange: (m) => {save('font', fonts[m])},
|
||||
format: v => fonts[v]
|
||||
},
|
||||
'Vector font size': {
|
||||
value: settings.vectorsize || 10,
|
||||
min:10, max: 20,step:1,wrap:true,
|
||||
onchange: (m) => {save('vectorsize', m)}
|
||||
}
|
||||
};
|
||||
E.showMenu(appMenu);
|
||||
});
|
2
core
2
core
|
@ -1 +1 @@
|
|||
Subproject commit 23854083e0c3f83c649073a2d85e8079efc471d3
|
||||
Subproject commit 59f80bb52a38da12cb272f9106cb3951b49dab2e
|
Loading…
Reference in New Issue