commit 4c20d622ea3c9e07617c421aef1a20821288eb68 Author: Gordon Williams Date: Wed Oct 30 17:33:58 2019 +0000 Initial stab at app loading tool diff --git a/apps.json b/apps.json new file mode 100644 index 000000000..7be3bf78d --- /dev/null +++ b/apps.json @@ -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"} + ] + } +] diff --git a/apps/compass-icon.js b/apps/compass-icon.js new file mode 100644 index 000000000..6a09df608 --- /dev/null +++ b/apps/compass-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwghC/AE8IxAAEwAWVDB4WIDBwWJAAIWOwcz///mc4DBhFDwYVBAAYYDJJAWJDAoXKCw//+YXJIwWPCQk/Aof4JBAuHC4v/GBBdHC4nzMIZGHCAIOBC4vz75hDJAgXCCgS9CC4fdAYQXGIwsyCAPyl//nvdVQoXFRofzkYXCCwJGBSIgXFQ4kymcykfdIwZgDC5XzkUyCwJGDC6FNCwPTC5i9FmQXCMgLZFC48zLgMilUv/vdkUjBII9BC6HSC55HD1WiklDNIgXIBok61QYBkSBFC5kqCwMjC6RGB1RcCR4gXIx4MC+Wqkfyl70BEQf4C4+DIwYqBC4XzGAc4C4sISAfz0QDCFgUzRwmAC4wQB+QTCC4f/AYJeCC4hIEPQi9FIwwXDbIzVHC4xICSIYXGRoRGFGAgqFXgouGC4iqDLo4XIJAQYHCwZGHGAgYBXQUzCwYuIDAwAHCxRJEAAxFJDBgWNDBAWPAH4AYA=")) diff --git a/apps/compass.js b/apps/compass.js new file mode 100644 index 000000000..8229eecb1 --- /dev/null +++ b/apps/compass.js @@ -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); diff --git a/apps/compass.json b/apps/compass.json new file mode 100644 index 000000000..a5766784f --- /dev/null +++ b/apps/compass.json @@ -0,0 +1,5 @@ +{ + "name":"Compass", + "icon":"*compass", + "src":"-compass" +} diff --git a/apps/compass.png b/apps/compass.png new file mode 100644 index 000000000..9230716a3 Binary files /dev/null and b/apps/compass.png differ diff --git a/apps/trex-icon.js b/apps/trex-icon.js new file mode 100644 index 000000000..d1d60c1a3 --- /dev/null +++ b/apps/trex-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgIXUn//4AFI///+AFC+YFKCIoFeHQYFH/4FGhkAgYKCAo8fApEPGggFG4YFBmAFHAgIdDAqA/BAo38K4gFJWIJ7DAoUB/AqC4EDLwIFCh0GFQPD4EYgP/4YABDwfwSggFFgAFCgOAAoYSDAox4BABQA==")) diff --git a/apps/trex.js b/apps/trex.js new file mode 100644 index 000000000..25ec65d51 --- /dev/null +++ b/apps/trex.js @@ -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<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 (lastCactix0.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(); diff --git a/apps/trex.json b/apps/trex.json new file mode 100644 index 000000000..afebc0b5c --- /dev/null +++ b/apps/trex.json @@ -0,0 +1,5 @@ +{ + "name":"T-Rex", + "icon":"*trex", + "src":"-trex" +} diff --git a/apps/trex.png b/apps/trex.png new file mode 100644 index 000000000..1e0b51322 Binary files /dev/null and b/apps/trex.png differ diff --git a/apps/unknown.png b/apps/unknown.png new file mode 100644 index 000000000..582cb2e08 Binary files /dev/null and b/apps/unknown.png differ diff --git a/comms.js b/comms.js new file mode 100644 index 000000000..b03ac9359 --- /dev/null +++ b/comms.js @@ -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); + }); + }); + }); +} +}; diff --git a/index.html b/index.html new file mode 100644 index 000000000..69d8e1ab3 --- /dev/null +++ b/index.html @@ -0,0 +1,85 @@ + + + + + + + + + Bangle.js loader + + + + + + +
+
+ +
+ +
+
+ +
+
+
+
+ + + + + + + + + + + + + diff --git a/index.js b/index.js new file mode 100644 index 000000000..770c6e5e5 --- /dev/null +++ b/index.js @@ -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(`
`); + msgDiv.innerHTML = message; + toastcontainer.append(msgDiv); + setTimeout(function() { + msgDiv.remove(); + }, 5000); +} +function showPrompt(title, text) { + return new Promise((resolve,reject) => { + var modal = htmlElement(``); + 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) => `
+
+
${escapeHtml(app.name)}
+
+
+

${escapeHtml(app.name)}

+

${escapeHtml(app.description)}

+
+
+ +
+
+ `); + // 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 = '
'; + // Get apps + Comms.getInstalledApps().then(appIDs => { + tab.setAttribute("data-badge", appIDs.length); + panelbody.innerHTML = appIDs.map(appNameToApp).sort(appSorter).map(app => `
+
+
${escapeHtml(app.name)}
+
+
+

${escapeHtml(app.name)}

+

${escapeHtml(app.description)}

+
+
+ +
+
+ `); + 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(); +}); diff --git a/utils.js b/utils.js new file mode 100644 index 000000000..44a1479ab --- /dev/null +++ b/utils.js @@ -0,0 +1,37 @@ +function escapeHtml(text) { + var map = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + 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