forked from FOSS/BangleApps
Added customised app loading
parent
2f9c24d433
commit
eb71f80ef2
|
@ -0,0 +1,84 @@
|
|||
Bangle.js App Loader (and Apps)
|
||||
================================
|
||||
|
||||
### How does it work?
|
||||
|
||||
* A list of apps is in `apps.json`
|
||||
* Each element references an app in `apps/` which is uploaded
|
||||
* When it starts, BangleAppLoader checks the JSON and compares
|
||||
it with the files it sees in the watch's storage.
|
||||
* To upload an app, BangleAppLoader checks the files that are
|
||||
listed in `apps.json`, loads them, and sends them over Web Bluetooth.
|
||||
|
||||
### What filenames are used
|
||||
|
||||
Filenames in storage are limited to 8 characters. To
|
||||
easily distinguish between file types, we use the following:
|
||||
|
||||
* `+stuff` is JSON for an app
|
||||
* `*stuff` is an image
|
||||
* `-stuff` is JS code
|
||||
* `=stuff` is JS code for stuff that is run at boot time - eg. handling settings or creating widgets on the clock screen
|
||||
|
||||
### Developing your own app
|
||||
|
||||
* Start writing your code in the IDE, with `Save on Send` in settings set to
|
||||
the *default* of `To RAM`
|
||||
* When you have your app as you want it, add it as a file in `apps/`, lets assume `apps/my-great-app.js`
|
||||
* Come up with a unique 7 character name, we'll assume `7chname`
|
||||
* Create `apps/my-great-app.png` as a 48px icon
|
||||
* Use http://www.espruino.com/Image+Converter to create as 1 bit, 4 bit or 8 bit Web Palette "Image String" and save it as `apps/my-great-app-icon.js`
|
||||
* Create an entry in `apps/my-great-app.json` as follows:
|
||||
|
||||
```
|
||||
{
|
||||
"name":"Short Name",
|
||||
"icon":"*7chname",
|
||||
"src":"-7chname"
|
||||
}
|
||||
```
|
||||
|
||||
* Create an entry in `apps.json` as follows:
|
||||
|
||||
```
|
||||
{ "id": "7chname",
|
||||
"name": "My app's human readable name",
|
||||
"icon": "my-great-app.png",
|
||||
"description": "A detailed description of my great app",
|
||||
"tags": "",
|
||||
"storage": [
|
||||
{"name":"+7chname","url":"my-great-app.json"},
|
||||
{"name":"-7chname","url":"my-great-app.js"},
|
||||
{"name":"*7chname","url":"my-great-app.js","evaluate":true}
|
||||
]
|
||||
},
|
||||
```
|
||||
|
||||
### `apps.json` format
|
||||
|
||||
```
|
||||
{ "id": "appid", // 7 character app id
|
||||
"name": "Readable name", // readable name
|
||||
"icon": "icon.png", // icon in apps/
|
||||
"description": "...", // long description
|
||||
"tags": "", // comma separated tag list for searching
|
||||
|
||||
"custom": "custom.html", // if supplied, apps/custom.html is loaded in an
|
||||
// iframe, and it must post back an 'app' structure
|
||||
// like this one with 'storage','name' and 'id' set up
|
||||
|
||||
"storage": [ // list of files to add to storage
|
||||
{"name":"-appid", // filename to use in storage
|
||||
"url":"", // URL of file to load (currently relative to apps/)
|
||||
"content":"..." // if supplied, this content is loaded directly
|
||||
"evaluate":true // if supplied, data isn't quoted into a String before upload
|
||||
// (eg it's evaluated as JS)
|
||||
},
|
||||
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Credits
|
||||
|
||||
The majority of icons used for these apps are from [Icons8](https://icons8.com/) - we have a commercial license but icons are also free for Open Source projects.
|
61
apps.json
61
apps.json
|
@ -5,9 +5,9 @@
|
|||
"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","evaluate":true}
|
||||
{"name":"+trex","url":"trex.json"},
|
||||
{"name":"-trex","url":"trex.js"},
|
||||
{"name":"*trex","url":"trex-icon.js","evaluate":true}
|
||||
]
|
||||
},
|
||||
{ "id": "compass",
|
||||
|
@ -16,9 +16,9 @@
|
|||
"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","evaluate":true}
|
||||
{"name":"+compass","url":"compass.json"},
|
||||
{"name":"-compass","url":"compass.js"},
|
||||
{"name":"*compass","url":"compass-icon.js","evaluate":true}
|
||||
]
|
||||
},
|
||||
{ "id": "clock",
|
||||
|
@ -27,9 +27,9 @@
|
|||
"description": "7 segment clock that morphs between minutes and hours",
|
||||
"tags": "clock",
|
||||
"storage": [
|
||||
{"name":"+clock","file":"clock.json"},
|
||||
{"name":"-clock","file":"clock-morphing.js"},
|
||||
{"name":"*clock","file":"clock-icon.js","evaluate":true}
|
||||
{"name":"+clock","url":"clock.json"},
|
||||
{"name":"-clock","url":"clock-morphing.js"},
|
||||
{"name":"*clock","url":"clock-icon.js","evaluate":true}
|
||||
]
|
||||
},
|
||||
{ "id": "gpstime",
|
||||
|
@ -38,9 +38,9 @@
|
|||
"description": "Update the Bangle.js's clock based on the time from the GPS receiver",
|
||||
"tags": "tool",
|
||||
"storage": [
|
||||
{"name":"+gpstime","file":"gpstime.json"},
|
||||
{"name":"-gpstime","file":"gpstime.js"},
|
||||
{"name":"*gpstime","file":"gpstime-icon.js","evaluate":true}
|
||||
{"name":"+gpstime","url":"gpstime.json"},
|
||||
{"name":"-gpstime","url":"gpstime.js"},
|
||||
{"name":"*gpstime","url":"gpstime-icon.js","evaluate":true}
|
||||
]
|
||||
},
|
||||
{ "id": "openloc",
|
||||
|
@ -49,8 +49,8 @@
|
|||
"description": "Convert your current GPS location to a series of characters",
|
||||
"tags": "tool,outdoors",
|
||||
"storage": [
|
||||
{"name":"+openloc","file":"openlocation.json"},
|
||||
{"name":"-openloc","file":"openlocation.js","evaluate":true}
|
||||
{"name":"+openloc","url":"openlocation.json"},
|
||||
{"name":"-openloc","url":"openlocation.js","evaluate":true}
|
||||
]
|
||||
},
|
||||
{ "id": "speedo",
|
||||
|
@ -59,9 +59,9 @@
|
|||
"description": "Show the current speed according to the GPS",
|
||||
"tags": "tool,outdoors",
|
||||
"storage": [
|
||||
{"name":"+speedo","file":"speedo.json"},
|
||||
{"name":"-speedo","file":"speedo.js"},
|
||||
{"name":"*speedo","file":"speedo-icon.js","evaluate":true}
|
||||
{"name":"+speedo","url":"speedo.json"},
|
||||
{"name":"-speedo","url":"speedo.js"},
|
||||
{"name":"*speedo","url":"speedo-icon.js","evaluate":true}
|
||||
]
|
||||
},
|
||||
{ "id": "slevel",
|
||||
|
@ -70,9 +70,30 @@
|
|||
"description": "Show the current angle of the watch, so you can use it to make sure something is absolutely flat",
|
||||
"tags": "tool",
|
||||
"storage": [
|
||||
{"name":"+slevel","file":"spiritlevel.json"},
|
||||
{"name":"-slevel","file":"spiritlevel.js"},
|
||||
{"name":"*slevel","file":"spiritlevel-icon.js","evaluate":true}
|
||||
{"name":"+slevel","url":"spiritlevel.json"},
|
||||
{"name":"-slevel","url":"spiritlevel.js"},
|
||||
{"name":"*slevel","url":"spiritlevel-icon.js","evaluate":true}
|
||||
]
|
||||
},
|
||||
{ "id": "sbat",
|
||||
"name": "Battery Level Widget",
|
||||
"icon": "widget-battery.png",
|
||||
"description": "Show the current battery level and charging status in the top right of the clock",
|
||||
"tags": "widget,battery",
|
||||
"storage": [
|
||||
{"name":"=sbat","url":"widget-battery.js"}
|
||||
]
|
||||
},
|
||||
{ "id": "qrcode",
|
||||
"name": "Custom QR Code",
|
||||
"icon": "qrcode.png",
|
||||
"description": "Use this to upload a customised QR code to Bangle.js",
|
||||
"tags": "",
|
||||
"custom": "qrcode.html",
|
||||
"storage": [
|
||||
{"name":"-qrcode"},
|
||||
{"name":"+qrcode"},
|
||||
{"name":"=qrcode"}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="../css/spectre.min.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<p>Enter a URL: <input type="text" id="url" value="http://espruino.com"></p>
|
||||
<p>Try your QR Code: <div id="qrcode"></div></p>
|
||||
<p>Click <button id="upload" class="btn btn-primary">Upload</button></p>
|
||||
|
||||
|
||||
<script src="../lib/qrcode.min.js"></script><!-- https://davidshimjs.github.io/qrcodejs/ -->
|
||||
<script src="https://espruino.github.io/EspruinoWebTools/heatshrink.js"></script>
|
||||
<script src="https://espruino.github.io/EspruinoWebTools/imageconverter.js"></script>
|
||||
|
||||
<script>
|
||||
|
||||
var qrcode = new QRCode("qrcode", {
|
||||
text: document.getElementById("url").value,
|
||||
width: 200,
|
||||
height: 200,
|
||||
colorDark : "#000000",
|
||||
colorLight : "#ffffff",
|
||||
});
|
||||
|
||||
document.getElementById("url").addEventListener("change", function() {
|
||||
qrcode.clear(); // clear the code.
|
||||
qrcode.makeCode(document.getElementById("url").value); // make another code.
|
||||
});
|
||||
document.getElementById("upload").addEventListener("click", function() {
|
||||
var url = document.getElementById("url").value;
|
||||
var img = imageconverter.canvastoString(document.getElementsByTagName("canvas")[0],{mode:"1bit",output:"string",compression:true});
|
||||
var app = `var img = ${img};
|
||||
var url = ${JSON.stringify(url)};
|
||||
g.setColor(1,1,1);
|
||||
g.fillRect(0,0,239,239);
|
||||
g.drawImage(img,20,20);
|
||||
g.setFontAlign(0,0);
|
||||
g.setFont("6x8");
|
||||
g.setColor(0,0,0);
|
||||
g.drawString(url,120,230);
|
||||
g.setColor(1,1,1);
|
||||
`;
|
||||
console.log(app);
|
||||
var json = JSON.stringify({
|
||||
name:"QR Code",
|
||||
icon:"*qrcode",
|
||||
src:"-qrcode"
|
||||
});
|
||||
var icon = `require("heatshrink").decompress(atob("mEwgP/AEX8gE8nkAn4FSngCWF6xfYDgIABHAQFPDQXD4YgDApxNDMooFOAQIdDAqIvWfcYA="))`;
|
||||
|
||||
window.postMessage({
|
||||
id : "qrcode",
|
||||
|
||||
storage:[
|
||||
{name:"-qrcode", content:app},
|
||||
{name:"+qrcode", content:json},
|
||||
{name:"*qrcode", content:icon, "evaluate":true},
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Binary file not shown.
After Width: | Height: | Size: 219 B |
|
@ -0,0 +1,24 @@
|
|||
(function(){
|
||||
var img_charge = E.toArrayBuffer(atob("DhgBHOBzgc4HOP////////////////////3/4HgB4AeAHgB4AeAHgB4AeAHg"));
|
||||
var xpos = WIDGETPOS.tr-64;
|
||||
WIDGETPOS.tr-=68;
|
||||
|
||||
function draw() {
|
||||
var s = 63;
|
||||
var x = xpos, y = 0;
|
||||
g.clearRect(x,y,x+s,y+23);
|
||||
if (Bangle.isCharging()) {
|
||||
g.drawImage(img_charge,x,y);
|
||||
x+=16;
|
||||
s-=16;
|
||||
}
|
||||
g.setColor(1,1,1);
|
||||
g.fillRect(x,y+2,x+s-4,y+21);
|
||||
g.clearRect(x+2,y+4,x+s-6,y+19);
|
||||
g.fillRect(x+s-3,y+10,x+s,y+14);
|
||||
g.fillRect(x+4,y+6,x+4+E.getBattery()*(s-12)/100,y+17);
|
||||
g.setColor(1,1,1);
|
||||
}
|
||||
Bangle.on('charging',function(charging) { draw(); g.flip(); if(charging)Bangle.buzz(); });
|
||||
WIDGETS["battery"]={draw:draw};
|
||||
})()
|
Binary file not shown.
After Width: | Height: | Size: 297 B |
14
comms.js
14
comms.js
|
@ -17,9 +17,17 @@ var Comms = {
|
|||
uploadApp : app => {
|
||||
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=>`\x10require('Storage').write(${toJS(storageFile.name)},${storageFile.evaluate ? contents.trim() : toJS(contents)});`)))
|
||||
Promise.all(app.storage.map(storageFile => {
|
||||
var promise;
|
||||
if (storageFile.content)
|
||||
promise = Promise.resolve(storageFile.content);
|
||||
else if (storageFile.url)
|
||||
promise = httpGet("apps/"+storageFile.url);
|
||||
else promise = Promise.resolve();
|
||||
// then map each file to a command to load into storage
|
||||
return promise.then(contents =>
|
||||
contents?`\x10require('Storage').write(${toJS(storageFile.name)},${storageFile.evaluate ? contents.trim() : toJS(contents)});`:"")
|
||||
})) // now we just have a list of commands...
|
||||
.then((fileContents) => {
|
||||
fileContents = fileContents.join("\n")+"\n";
|
||||
console.log("uploadApp",fileContents);
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -3,9 +3,9 @@
|
|||
<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">
|
||||
<link rel="stylesheet" href="css/spectre.min.css">
|
||||
<link rel="stylesheet" href="css/spectre-exp.min.css">
|
||||
<link rel="stylesheet" href="css/spectre-icons.min.css">
|
||||
<title>Bangle.js loader</title>
|
||||
<style>
|
||||
.navbar { background-color: #5755d9; padding: 0.5em 1em 0.5em 1em; }
|
||||
|
@ -91,8 +91,6 @@
|
|||
<p>Using <a href="https://espruino.com/">Espruino</a>, Icons from <a href="https://icons8.com/">icons8.com</a></p>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<script src="https://www.puck-js.com/puck.js"></script>
|
||||
<script src="utils.js"></script>
|
||||
<script src="comms.js"></script>
|
||||
|
|
69
index.js
69
index.js
|
@ -11,8 +11,12 @@ httpGet("apps.json").then(apps=>{
|
|||
// =========================================== Top Navigation
|
||||
function showToast(message, type) {
|
||||
// toast-primary, toast-success, toast-warning or toast-error
|
||||
var style = "toast-primary";
|
||||
if (type=="success") style = "toast-success";
|
||||
else if (type=="error") style = "toast-error";
|
||||
else if (type!==undefined) console.log("showToast: unknown toast "+type);
|
||||
var toastcontainer = document.getElementById("toastcontainer");
|
||||
var msgDiv = htmlElement(`<div class="toast toast-primary"></div>`);
|
||||
var msgDiv = htmlElement(`<div class="toast ${style}"></div>`);
|
||||
msgDiv.innerHTML = message;
|
||||
toastcontainer.append(msgDiv);
|
||||
setTimeout(function() {
|
||||
|
@ -44,6 +48,7 @@ function showPrompt(title, text) {
|
|||
document.body.append(modal);
|
||||
htmlToArray(modal.getElementsByTagName("button")).forEach(button => {
|
||||
button.addEventListener("click",event => {
|
||||
event.preventDefault();
|
||||
var isYes = event.target.getAttribute("isyes")=="1";
|
||||
if (isYes) resolve();
|
||||
else reject();
|
||||
|
@ -52,6 +57,40 @@ function showPrompt(title, text) {
|
|||
});
|
||||
});
|
||||
}
|
||||
function handleCustomApp(app) {
|
||||
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" style="height:100%">
|
||||
<div class="modal-header">
|
||||
<a href="#close" class="btn btn-clear float-right" aria-label="Close"></a>
|
||||
<div class="modal-title h5">${escapeHtml(app.name)}</div>
|
||||
</div>
|
||||
<div class="modal-body" style="height:100%">
|
||||
<div class="content" style="height:100%">
|
||||
<iframe src="apps/${app.custom}" style="width:100%;height:100%;border:0px;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`);
|
||||
document.body.append(modal);
|
||||
htmlToArray(modal.getElementsByTagName("a")).forEach(button => {
|
||||
button.addEventListener("click",event => {
|
||||
event.preventDefault();
|
||||
modal.remove();
|
||||
reject("Window closed");
|
||||
});
|
||||
});
|
||||
|
||||
var iframe = modal.getElementsByTagName("iframe")[0];
|
||||
iframe.contentWindow.addEventListener("message", function(event) {
|
||||
var app = event.data;
|
||||
console.log("Received custom app", app);
|
||||
modal.remove();
|
||||
Comms.uploadApp(app).then(resolve,reject);
|
||||
}, false);
|
||||
});
|
||||
}
|
||||
// =========================================== Top Navigation
|
||||
function showTab(tabname) {
|
||||
htmlToArray(document.querySelectorAll("#tab-navigate .tab-item")).forEach(tab => {
|
||||
|
@ -67,7 +106,13 @@ function showTab(tabname) {
|
|||
// =========================================== Library
|
||||
function refreshLibrary() {
|
||||
var panelbody = document.querySelector("#librarycontainer .panel-body");
|
||||
panelbody.innerHTML = appJSON.map((app,idx) => `<div class="tile column col-6 col-sm-12 col-xs-12">
|
||||
panelbody.innerHTML = appJSON.map((app,idx) => {
|
||||
var icon = "icon-upload";
|
||||
if (app.custom)
|
||||
icon = "icon-menu";
|
||||
if (appsInstalled.includes(app.id))
|
||||
icon = "icon-delete";
|
||||
return `<div class="tile column col-6 col-sm-12 col-xs-12">
|
||||
<div class="tile-icon">
|
||||
<figure class="avatar"><img src="apps/${app.icon?app.icon:"apps/unknown.png"}" alt="${escapeHtml(app.name)}"></figure>
|
||||
</div>
|
||||
|
@ -76,10 +121,10 @@ function refreshLibrary() {
|
|||
<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 ${appsInstalled.includes(app.id)?"icon-delete":"icon-upload"}" appid="${app.id}"></i></button>
|
||||
<button class="btn btn-link btn-action btn-lg"><i class="icon ${icon}" appid="${app.id}"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
`).join("");
|
||||
`;}).join("");
|
||||
// set badge up top
|
||||
var tab = document.querySelector("#tab-librarycontainer a");
|
||||
tab.classList.add("badge");
|
||||
|
@ -104,6 +149,22 @@ function refreshLibrary() {
|
|||
icon.classList.remove("loading");
|
||||
icon.classList.add("icon-upload");
|
||||
});
|
||||
} else if (icon.classList.contains("icon-menu")) {
|
||||
if (app.custom) {
|
||||
icon.classList.remove("icon-menu");
|
||||
icon.classList.add("loading");
|
||||
handleCustomApp(app).then(() => {
|
||||
appsInstalled.push(app.id);
|
||||
showToast(app.name+" Uploaded!", "success");
|
||||
icon.classList.remove("loading");
|
||||
icon.classList.add("icon-delete");
|
||||
refreshMyApps();
|
||||
}).catch(err => {
|
||||
showToast("Customise failed, "+err, "error");
|
||||
icon.classList.remove("loading");
|
||||
icon.classList.add("icon-menu");
|
||||
});
|
||||
}
|
||||
} else {
|
||||
icon.classList.remove("icon-delete");
|
||||
icon.classList.add("loading");
|
||||
|
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue