From 4c20d622ea3c9e07617c421aef1a20821288eb68 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Wed, 30 Oct 2019 17:33:58 +0000 Subject: [PATCH] Initial stab at app loading tool --- apps.json | 24 +++++ apps/compass-icon.js | 1 + apps/compass.js | 34 +++++++ apps/compass.json | 5 + apps/compass.png | Bin 0 -> 2328 bytes apps/trex-icon.js | 1 + apps/trex.js | 219 +++++++++++++++++++++++++++++++++++++++++++ apps/trex.json | 5 + apps/trex.png | Bin 0 -> 396 bytes apps/unknown.png | Bin 0 -> 1620 bytes comms.js | 39 ++++++++ index.html | 85 +++++++++++++++++ index.js | 161 +++++++++++++++++++++++++++++++ utils.js | 37 ++++++++ 14 files changed, 611 insertions(+) create mode 100644 apps.json create mode 100644 apps/compass-icon.js create mode 100644 apps/compass.js create mode 100644 apps/compass.json create mode 100644 apps/compass.png create mode 100644 apps/trex-icon.js create mode 100644 apps/trex.js create mode 100644 apps/trex.json create mode 100644 apps/trex.png create mode 100644 apps/unknown.png create mode 100644 comms.js create mode 100644 index.html create mode 100644 index.js create mode 100644 utils.js 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 0000000000000000000000000000000000000000..9230716a3a2d48dbf930aa4e57d84c0dcf76be59 GIT binary patch literal 2328 zcmV+z3Fr2SP)9fA`HBdltX3u}us%*oLqe0|~_h6ha`7)KxwP<8*%28y9hC7LQwWvMFCW@&&TmY@^@B)9=$$6_@G8@%8* zn`e2qAI6^T&CIh2-@;#dAMSSU`QLl)J?EYWw{QzzZHSv9e1&a28$)4PJtQ; zV}XLf9$pC@K#M>glgPd2x;#f%FN`lc^_L=Wd!v866h;HcCnZ#jaOQSti0x`rX>42T zYQIq(Ziv97P5S7l$}fOtfV>->d=Mc4+f5;X=5{INdm&Z(PR9!2w~}Q^psq>Ud!zc>>B{HZB&+U_JT^IsP02iVARJr7e7ikk zd&GdhwJL0FRtOm;Rra__3znYzt_bDQFU!;k^hCW|(v`2&;I;ZFPfpFQJb>;CqWgnc z+f@agsxf%w{wSpm)9yVGxaQqxrC&}06SsPwAkF4$MoG5aALjN#Yjf%M`RVsrS^+al zB(K#+nOu}AQ^E_?jry`&W3v*d*xX+trF=QLu2O@|iz1A2Sn33T?hl~*0>kQin@h53 zVU&`zoe+X8Q=0lqvyID2AWsp$0!kBQ$#3sTDBpcuCu-z&q`EJF?$1p?^mk)FwkJ+v zHr7eu)xrt4I%3%{QRAV(s9=1}8-8Lx$6@@z{SF`)@3C?zOW>ir32hn}z zL#*_6Qt+FnasTdFf{*?HS(s`e%WI5uQ_3?U(Urp^uwE)s$S0Dko~<=9-Zmb$lex8% zIr!;mK0NU`(P(1j2EyT#4*)62+xt46HS@85^e@75>IvQZh)J$fh399a?t&uZa^93S zO9EA6^m$0e4VB$?sISa^<5Z0u&E)Z8Idka>?;JWBH>u>6*Zo11E9cOD_6Y8cs}OoG z(xu^F_4}-HmsT6N)I?cA#f0wuI!gkI!Q$ldf^x&s!_J#NnPQK|b+4cI4jt!gM`zsj z6(fq{oj>7Oy8!jX{`i={GaE2U%CpLJsE*_LW(&u$tlh)v%c>YMPcFB zYI*n2QNocZ2R}W{D6N3f?(;mbzmc-5Z7KaDGwxxa{%K3Oca=$YT}YHA!n6Y>0)}8p zvQwW@WH}EII}h_`*0BGRlVBK3`%e>fCw8J3sfS-7ie^ZE!3B;u{>*A?1YR8B2i$HOn)IXJO|-8;BiI)RbQO ziyAN#(@mIDre)s@bzYKQTfQ(9<;Pa5q)?-z(6k5)gv03mAUoghps%+FfISB;fs{xI zwO`m7H|t>{6YpTb#PN9CE;N@TYnUBZ#QdZ&y&g;iBwQ9r1Uh^QMfthULB8EicQC^4 zeSHZz5_TWxKoOFQQy!+aHVpkG$~=S>TD;mlRnF-roh7lxE#M{?Dxr)@+s?D&Dh*EsS? z4|Ow(*!c5VeCz9DQ3jn>zTKYk#V1r0?y8-{qS@0q-f|}6c?cV^Ig=p~J2IsrGcK(f zRT_b~ceBC=C<>u^RS~u zq1~HOH4qU>VT5E9{YiH7SyUf)@|i=<*LQBRLe` z301S0VMd4<28wx!=TLu$PAf-S(nNXKWu0Fuq&Jfmi$ z%EOoZ$v1ARzd|*P)gqP%8$ZI~7bRkQL@15foYZh|bAGd~m&$~){y74%WH9wH6-fn2 yzgdBH1g%oaql&ccJ#*D{>}LFCy@gx&s^kA(`(f?}jNR%00000i.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 0000000000000000000000000000000000000000..1e0b513224799d3f96d40ab3bf94b62055ead2d7 GIT binary patch literal 396 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTCwj^(N7l!{JxM1({$v_d#0*}aI z1_o|n5N2eUHAey{$X?><>&pI^OM*+#u&0x0Ay7y*GbEzKIX^cyHLnE7WngeFN=+uPSN!nBHT(T}$yUXLq^p$7!s58mL9=UfS+e62_Vdm_Y4NB(%&OBhWU|_${`sfEE z$DT~xx(gF-@;6FxzTb1{^p1Pg0n8JN&k1t;;ha@1vQ?&6OlL)7Sokc_pH=I=?kV0C zP||K$Cf39}zc}IBy|mkfRbdlOU1pV>V3EGldUoaZ$y35+TJDM4u4E`xnaj5N{%P^W z|5UQyAD;iS(fKm(;$4Rp9d~V67y9V-r2Dr2pVTG2a#iXI-Y~Ud_FC_JcaQT&0!@p2 jc(-g)lwtH)-1V2CXmNg4d*q#Wpg{3-^>bP0l+XkK4g#Ei literal 0 HcmV?d00001 diff --git a/apps/unknown.png b/apps/unknown.png new file mode 100644 index 0000000000000000000000000000000000000000..582cb2e0853a5a2899a3afbd7eb19cde2ee7f6a0 GIT binary patch literal 1620 zcmV-a2CMmrP)1gXjloC|3_d8m;N2OpV(|i0q4YwBna<2! zK9thw%-*|urnNbV{Gax^?eD+#{x0kLJ~)lj_;W+1>qV*k8akT^^dvctZccUyj4}H~#M%Wwee_v` zHMv7o%BM8@dBrLshn{wGD9BDl?^eV5vSM3T96;NnHvtc6La=(qzq)xrX1d8bK-TN- zrd_f$_O`9nEmS+_S7HTXK<&u;LDIW|qlN&KJvM}tt6TVVqL-AvNv`B*{NzNpBfSQwQP5~Sf(Dp@Vq1+3Q`N9wBQN2`J_?M^u0FIMlt?p^8 z%U3%80kIwg!T{E9<8J18S&$k1`eO)@HP+=TZKo(z3_A3VFYJB=sn`2^Q$mRE>02(+W)np;)L1!GUvU2{O{<&F_nE6Qe#D~Xf|dD z+?d3-D1(IUiL`C2;PPv4CKw8H)v7h8^obJ&Z6D0CjVUe8Xq_NAymxUyPAMU^CCrIu z%1M71EC`5o2if_~7E&h??0jeQ1Y3N6p?}G72FmS*)xQD)%wBE=2tW6@(+MTi!fk9H1pWKew2(jTXVu4%vk26QvSQCbGmk`Z)Y! zBIhh)6vG2)h6mF8wC^|l$M(Eo9D?JiW}=_T2jUA>LC80foTera{^p)Wi`>}Gf;(|ZwEZQ zS^k|*9wyt=f4ZOo!xty7{%}HKD9tBZ50g$=%v&&vMa!#@Nsf>EkEEDA*ST6fiC+An zsNK1#>!x0obq@j$QqYU-ad3ZvbjqUU+%iw(0WahgmHV6yeLWqoYkSl4pzFQ(_Vp&I ztO{WI-48rGLwQb?#vgVvduyd9_6W)rFRoQJq3I(J?{Xmin45#=3l9BmL6Bp<*MZej zrsWN7oRPUr7IvrHoIHOjS=gPTCw>d)^LQK+B|=f2qbGjrWaOd5D<<9Dv>MTW0X3z> zyPy}9`<>1~?NCx@m8G$_@rRTy5zH12YM&P)=tU+L^fgY z^0Z&_6^qdVuwgN3wt_Ze(10?J@%{C2grBk42hsu74qEo^nd&v`X`IHN9lrxzS~GeF S(*#!l0000 { + /* 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