Initial stab at app loading tool

pull/5/head
Gordon Williams 2019-10-30 17:33:58 +00:00
commit 4c20d622ea
14 changed files with 611 additions and 0 deletions

24
apps.json Normal file
View File

@ -0,0 +1,24 @@
[
{ "id": "trex",
"name": "T-Rex",
"icon": "trex.png",
"description": "T-Rex game in the style of Chrome's offline game",
"tags": "game",
"storage": [
{"name":"+trex","file":"trex.json"},
{"name":"-trex","file":"trex.js"},
{"name":"*trex","file":"trex-icon.js"}
]
},
{ "id": "compass",
"name": "Compass",
"icon": "compass.png",
"description": "Simple compass that points North",
"tags": "tool,outdoors",
"storage": [
{"name":"+compass","file":"compass.json"},
{"name":"-compass","file":"compass.js"},
{"name":"*compass","file":"compass-icon.js"}
]
}
]

1
apps/compass-icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwghC/AE8IxAAEwAWVDB4WIDBwWJAAIWOwcz///mc4DBhFDwYVBAAYYDJJAWJDAoXKCw//+YXJIwWPCQk/Aof4JBAuHC4v/GBBdHC4nzMIZGHCAIOBC4vz75hDJAgXCCgS9CC4fdAYQXGIwsyCAPyl//nvdVQoXFRofzkYXCCwJGBSIgXFQ4kymcykfdIwZgDC5XzkUyCwJGDC6FNCwPTC5i9FmQXCMgLZFC48zLgMilUv/vdkUjBII9BC6HSC55HD1WiklDNIgXIBok61QYBkSBFC5kqCwMjC6RGB1RcCR4gXIx4MC+Wqkfyl70BEQf4C4+DIwYqBC4XzGAc4C4sISAfz0QDCFgUzRwmAC4wQB+QTCC4f/AYJeCC4hIEPQi9FIwwXDbIzVHC4xICSIYXGRoRGFGAgqFXgouGC4iqDLo4XIJAQYHCwZGHGAgYBXQUzCwYuIDAwAHCxRJEAAxFJDBgWNDBAWPAH4AYA="))

34
apps/compass.js Normal file
View File

@ -0,0 +1,34 @@
g.clear();
g.setColor(0,0.5,1);
g.fillCircle(120,130,80,80);
g.setColor(0,0,0);
g.fillCircle(120,130,70,70);
function arrow(r,c) {
r=-r*Math.PI/180;
var p = Math.PI/2;
g.setColor(c);
g.fillPoly([
120+60*Math.sin(r), 130-60*Math.cos(r),
120+10*Math.sin(r+p), 130-10*Math.cos(r+p),
120+10*Math.sin(r+-p), 130-10*Math.cos(r-p),
]);
}
var oldHeading = 0;
Bangle.on('mag', function(m) {
if (!Bangle.isLCDOn()) return;
g.setFont("6x8",3);
g.setColor(0);
g.fillRect(70,0,170,24);
g.setColor(0xffff);
g.setFontAlign(0,0);
g.drawString((m.heading===undefined)?"---":Math.round(m.heading),120,12);
g.setColor(0,0,0);
arrow(oldHeading,0);
arrow(oldHeading+180,0);
arrow(m.heading,0xF800);
arrow(m.heading+180,0x001F);
oldHeading = m.heading;
});
Bangle.setCompassPower(1);

5
apps/compass.json Normal file
View File

@ -0,0 +1,5 @@
{
"name":"Compass",
"icon":"*compass",
"src":"-compass"
}

BIN
apps/compass.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

1
apps/trex-icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgIXUn//4AFI///+AFC+YFKCIoFeHQYFH/4FGhkAgYKCAo8fApEPGggFG4YFBmAFHAgIdDAqA/BAo38K4gFJWIJ7DAoUB/AqC4EDLwIFCh0GFQPD4EYgP/4YABDwfwSggFFgAFCgOAAoYSDAox4BABQA=="))

219
apps/trex.js Normal file
View File

@ -0,0 +1,219 @@
greal = g;
g.clear();
g = Graphics.createArrayBuffer(120,64,1,{msb:true});
g.flip = function() {
greal.drawImage({
width:120,
height:64,
buffer:g.buffer
},0,(240-128)/2,{scale:2});
};
var W = g.getWidth();
var BTNL = BTN4;
var BTNR = BTN5;
var BTNU = BTN1;
// Images can be added like this in Espruino v2.00
var IMG = {
rex: [Graphics.createImage(\`
########
##########
## #######
##########
##########
##########
#####
########
# #####
# #######
## ##########
### ######### #
##############
##############
############
###########
#########
#######
### ##
## #
#
##
\`),Graphics.createImage(\`
########
##########
## #######
##########
##########
##########
#####
########
# #####
# #######
## ##########
### ######### #
##############
##############
############
###########
#########
#######
### ##
## ##
#
##
\`),Graphics.createImage(\`
########
# ######
# # ######
# ######
##########
##########
#####
########
# #####
# #######
## ##########
### ######### #
##############
##############
############
###########
#########
#######
### ##
## #
# #
## ##
\`)],
cacti: [Graphics.createImage(\`
##
####
####
####
####
#### #
# #### ###
### #### ###
### #### ###
### #### ###
### #### ###
### #### ###
### #### ###
### #### ###
###########
#########
####
####
####
####
####
####
####
####
\`),Graphics.createImage(\`
##
##
# ##
## ## #
## ## #
## ## #
## ## #
##### #
#### #
#####
####
##
##
##
##
##
##
##
\`)],
};
IMG.rex.forEach(i=>i.transparent=0);
IMG.cacti.forEach(i=>i.transparent=0);
var cacti, rex, frame;
function gameStart() {
rex = {
alive : true,
img : 0,
x : 10, y : 0,
vy : 0,
score : 0
};
cacti = [ { x:W, img:1 } ];
var random = new Uint8Array(128*3/8);
for (var i=0;i<50;i++) {
var a = 0|(Math.random()*random.length);
var b = 0|(Math.random()*8);
random[a]|=1<<b;
}
IMG.ground = { width: 128, height: 3, bpp : 1, buffer : random.buffer };
frame = 0;
setInterval(onFrame, 50);
}
function gameStop() {
rex.alive = false;
rex.img = 2; // dead
clearInterval();
setTimeout(function() {
setWatch(gameStart, BTNU, {repeat:0,debounce:50,edge:"rising"});
}, 1000);
setTimeout(onFrame, 10);
}
function onFrame() {
g.clear();
if (rex.alive) {
frame++;
rex.score++;
if (!(frame&3)) rex.img = rex.img?0:1;
// move rex
if (BTNL.read() && rex.x>0) rex.x--;
if (BTNR.read() && rex.x<20) rex.x++;
if (BTNU.read() && rex.y==0) rex.vy=4;
rex.y += rex.vy;
rex.vy -= 0.2;
if (rex.y<=0) {rex.y=0; rex.vy=0; }
// move cacti
var lastCactix = cacti.length?cacti[cacti.length-1].x:W-1;
if (lastCactix<W) {
cacti.push({
x : lastCactix + 24 + Math.random()*W,
img : (Math.random()>0.5)?1:0
});
}
cacti.forEach(c=>c.x--);
while (cacti.length && cacti[0].x<0) cacti.shift();
} else {
g.drawString("Game Over!",(W-g.stringWidth("Game Over!"))/2,20);
}
g.drawLine(0,60,239,60);
cacti.forEach(c=>g.drawImage(IMG.cacti[c.img],c.x,60-IMG.cacti[c.img].height));
// check against actual pixels
var rexx = rex.x;
var rexy = 38-rex.y;
if (rex.alive &&
(g.getPixel(rexx+0, rexy+13) ||
g.getPixel(rexx+2, rexy+15) ||
g.getPixel(rexx+5, rexy+19) ||
g.getPixel(rexx+10, rexy+19) ||
g.getPixel(rexx+12, rexy+15) ||
g.getPixel(rexx+13, rexy+13) ||
g.getPixel(rexx+15, rexy+11) ||
g.getPixel(rexx+17, rexy+7) ||
g.getPixel(rexx+19, rexy+5) ||
g.getPixel(rexx+19, rexy+1))) {
return gameStop();
}
g.drawImage(IMG.rex[rex.img], rexx, rexy);
var groundOffset = frame&127;
g.drawImage(IMG.ground, -groundOffset, 61);
g.drawImage(IMG.ground, 128-groundOffset, 61);
g.drawString(rex.score,(W-1)-g.stringWidth(rex.score));
g.flip();
}
gameStart();

5
apps/trex.json Normal file
View File

@ -0,0 +1,5 @@
{
"name":"T-Rex",
"icon":"*trex",
"src":"-trex"
}

BIN
apps/trex.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 B

BIN
apps/unknown.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

39
comms.js Normal file
View File

@ -0,0 +1,39 @@
Puck.debug=3;
var Comms = {
uploadApp : app => {
/* eg
{ name: "T-Rex",
icon: "trex.png",
description: "T-Rex game in the style of Chrome's offline game",
storage: [
{name:"+trex",file:"trex.json"},
{name:"-trex",file:"trex.js"},
{name:"*trex",file:"trex-icon.js"}
]
}
*/
return new Promise((resolve,reject) => {
// Load all files
Promise.all(app.storage.map(storageFile => httpGet("apps/"+storageFile.file)
// map each file to a command to load into storage
.then(contents=>`require('Storage').write(${toJS(storageFile.name)},${toJS(contents)});`)))
//
.then(function(fileContents) {
fileContents = fileContents.join("\n");
Puck.write(fileContents,function() {
resolve();
});
});
});
},
getInstalledApps : () => {
return new Promise((resolve,reject) => {
Puck.write("\x03",() => {
Puck.eval('require("Storage").list().filter(f=>f[0]=="+").map(f=>f.substr(1))', appList => {
resolve(appList);
});
});
});
}
};

85
index.html Normal file
View File

@ -0,0 +1,85 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://unpkg.com/spectre.css/dist/spectre.min.css">
<link rel="stylesheet" href="https://unpkg.com/spectre.css/dist/spectre-exp.min.css">
<link rel="stylesheet" href="https://unpkg.com/spectre.css/dist/spectre-icons.min.css">
<title>Bangle.js loader</title>
</head>
<body>
<header class="navbar">
<section class="navbar-section">
<a href="#" class="navbar-brand mr-2">Bangle.js Loader</a>
<!-- <a href="#" class="btn btn-link">...</a> -->
</section>
<!--<section class="navbar-section">
<div class="input-group input-inline">
<input class="form-input" type="text" placeholder="search">
<button class="btn btn-primary input-group-btn">Search</button>
</div>
</section>-->
</header>
<ul class="tab tab-block" id="tab-navigate">
<li class="tab-item active" id="tab-librarycontainer">
<a href="javascript:showTab('librarycontainer')">Library</a>
</li>
<li class="tab-item" id="tab-myappscontainer">
<a href="javascript:showTab('myappscontainer')">My Apps</a>
</li>
<li class="tab-item" id="tab-aboutcontainer">
<a href="javascript:showTab('aboutcontainer')">About</a>
</li>
</ul>
<div class="container" id="toastcontainer">
</div>
<div class="container bangle-tab" id="librarycontainer">
<!--<div class="filter-nav">
<label class="chip" filterid="">All</label>
<label class="chip" filterid="clock">Clocks</label>
<label class="chip" filterid="game">Games</label>
<label class="chip" filterid="tool">Tools</label>
<label class="chip" filterid="hardware">Hardware</label>
</div>-->
<div class="panel">
<div class="panel-header">
<!-- <div class="input-group">
<input class="form-input" type="text" placeholder="Keywords...">
<button class="btn btn-primary input-group-btn">Search</button>
</div>-->
</div>
<div class="panel-body"><!-- apps go here --></div>
</div>
</div>
<div class="container bangle-tab" id="myappscontainer" style="display:none">
<div class="panel">
<div class="panel-header" style="text-align:right">
<button class="btn input-group-btn" id="myappsrefresh">Refresh...</button>
</div>
<div class="panel-body"><!-- apps go here --></div>
</div>
</div>
<div class="container bangle-tab" id="aboutcontainer" style="display:none">
<div class="hero bg-gray">
<div class="hero-body">
<h1>About Bangle.js loader</h1>
<p>A tool for uploading and removing apps from <a href="https://banglejs.com">Bangle.js Smart Watches</a></p>
</div>
</div>
<p>Using <a href="https://espruino.com/">Espruino</a>, Icons from <a href="https://icons8.com/">icons8.com</a></p>
</div>
<script src="http://www.puck-js.com/puck.js"></script>
<script src="utils.js"></script>
<script src="comms.js"></script>
<script src="index.js"></script>
</body>
</html>

161
index.js Normal file
View File

@ -0,0 +1,161 @@
var appjson = [];
httpGet("apps.json").then(apps=>{
appjson = JSON.parse(apps);
appjson.sort(appSorter);
refreshLibrary();
});
// Status
// =========================================== Top Navigation
function showToast(message) {
// toast-primary, toast-success, toast-warning or toast-error
var toastcontainer = document.getElementById("toastcontainer");
var msgDiv = htmlElement(`<div class="toast toast-primary"></div>`);
msgDiv.innerHTML = message;
toastcontainer.append(msgDiv);
setTimeout(function() {
msgDiv.remove();
}, 5000);
}
function showPrompt(title, text) {
return new Promise((resolve,reject) => {
var modal = htmlElement(`<div class="modal active">
<!--<a href="#close" class="modal-overlay" aria-label="Close"></a>-->
<div class="modal-container">
<div class="modal-header">
<a href="#close" class="btn btn-clear float-right" aria-label="Close"></a>
<div class="modal-title h5">${escapeHtml(title)}</div>
</div>
<div class="modal-body">
<div class="content">
${escapeHtml(text)}
</div>
</div>
<div class="modal-footer">
<div class="modal-footer">
<button class="btn btn-primary" isyes="1">Yes</button>
<button class="btn" isyes="0">No</button>
</div>
</div>
</div>
</div>`);
document.body.append(modal);
htmlToArray(modal.getElementsByTagName("button")).forEach(button => {
button.addEventListener("click",event => {
var isYes = event.target.getAttribute("isyes");
if (isYes) resolve();
else reject();
modal.remove();
});
});
});
}
// =========================================== Top Navigation
function showTab(tabname) {
htmlToArray(document.querySelectorAll("#tab-navigate .tab-item")).forEach(tab => {
tab.classList.remove("active");
});
htmlToArray(document.querySelectorAll(".bangle-tab")).forEach(tab => {
tab.style.display = "none";
});
document.getElementById("tab-"+tabname).classList.add("active");
document.getElementById(tabname).style.display = "inherit";
}
// =========================================== Library
function refreshLibrary() {
var panelbody = document.querySelector("#librarycontainer .panel-body");
panelbody.innerHTML = appjson.map((app,idx) => `<div class="tile">
<div class="tile-icon">
<figure class="avatar"><img src="apps/${app.icon}" alt="${escapeHtml(app.name)}"></figure>
</div>
<div class="tile-content">
<p class="tile-title text-bold">${escapeHtml(app.name)}</p>
<p class="tile-subtitle">${escapeHtml(app.description)}</p>
</div>
<div class="tile-action">
<button class="btn btn-link btn-action btn-lg"><i class="icon icon-upload" appid="${app.id}"></i></button>
</div>
</div>
`);
// set badge up top
var tab = document.querySelector("#tab-librarycontainer a");
tab.classList.add("badge");
tab.setAttribute("data-badge", appjson.length);
htmlToArray(panelbody.getElementsByTagName("button")).forEach(button => {
button.addEventListener("click",event => {
var icon = event.target;
var appid = icon.getAttribute("appid");
var app = appjson.find(app=>app.id==appid);
if (!app) return;
icon.classList.remove("icon-upload");
icon.classList.add("loading");
Comms.uploadApp(app).then(() => {
showToast(app.name+" Uploaded!");
icon.classList.remove("loading");
icon.classList.add("icon-delete");
}).catch(() => {
icon.classList.remove("loading");
icon.classList.add("icon-upload");
});
});
});
}
refreshLibrary();
// =========================================== My Apps
function appNameToApp(appName) {
var app = appjson.find(app=>app.id==appName);
if (app) return app;
return { id: "appName",
name: "Unknown app "+appName,
icon: "unknown.png",
description: "Unknown app",
storage: [],
unknown: true,
};
}
function refreshMyApps() {
var panelbody = document.querySelector("#myappscontainer .panel-body");
var tab = document.querySelector("#tab-myappscontainer a");
// set badge up top
tab.classList.add("badge");
tab.setAttribute("data-badge", "");
// Loading indicator
panelbody.innerHTML = '<div class="loading loading-lg"></div>';
// Get apps
Comms.getInstalledApps().then(appIDs => {
tab.setAttribute("data-badge", appIDs.length);
panelbody.innerHTML = appIDs.map(appNameToApp).sort(appSorter).map(app => `<div class="tile">
<div class="tile-icon">
<figure class="avatar"><img src="apps/${app.icon}" alt="${escapeHtml(app.name)}"></figure>
</div>
<div class="tile-content">
<p class="tile-title text-bold">${escapeHtml(app.name)}</p>
<p class="tile-subtitle">${escapeHtml(app.description)}</p>
</div>
<div class="tile-action">
<button class="btn btn-link btn-action btn-lg"><i class="icon icon-delete" appid="${app.id}"></i></button>
</div>
</div>
`);
htmlToArray(panelbody.getElementsByTagName("button")).forEach(button => {
button.addEventListener("click",event => {
var icon = event.target;
var appid = icon.getAttribute("appid");
var app = appNameToApp(appid);
showPrompt("Delete","Really remove app '"+appid+"'?").then(() => {
// remove app!
refreshMyApps();
});
});
});
});
}
document.getElementById("myappsrefresh").addEventListener("click",event=>{
refreshMyApps();
});

37
utils.js Normal file
View File

@ -0,0 +1,37 @@
function escapeHtml(text) {
var map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, function(m) { return map[m]; });
}
function htmlToArray(collection) {
return [].slice.call(collection);
}
function htmlElement(str) {
var div = document.createElement('div');
div.innerHTML = str.trim();
return div.firstChild;
}
function httpGet(url) {
return new Promise((resolve,reject) => {
var oReq = new XMLHttpRequest();
oReq.addEventListener("load", () => resolve(oReq.responseText));
oReq.addEventListener("error", () => reject());
oReq.addEventListener("abort", () => reject());
oReq.open("GET", url);
oReq.send();
});
}
function toJS(txt) {
return JSON.stringify(txt);
}
// callback for sorting apps
function appSorter(a,b) {
if (a.unknown || b.unknown)
return (a.unknown)? 1 : -1;
return (a.name==b.name) ? 0 : ((a.name<b.name) ? -1 : 1);
}