mirror of https://github.com/espruino/BangleApps
Moving to git submodule for code app loader code
parent
f417f8b1c9
commit
d10df7a638
|
@ -0,0 +1,3 @@
|
||||||
|
[submodule "EspruinoAppLoaderCore"]
|
||||||
|
path = core
|
||||||
|
url = git@github.com:espruino/EspruinoAppLoaderCore.git
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 20a09f4f225ad0edae5e9b52b98900ebe8ef97cc
|
Binary file not shown.
Before Width: | Height: | Size: 472 B |
Binary file not shown.
Before Width: | Height: | Size: 926 B |
|
@ -1,49 +0,0 @@
|
||||||
{
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": 6,
|
|
||||||
"sourceType": "script"
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
"indent": [
|
|
||||||
"warn",
|
|
||||||
2,
|
|
||||||
{
|
|
||||||
"SwitchCase": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"no-undef": "warn",
|
|
||||||
"no-redeclare": "warn",
|
|
||||||
"no-var": "warn",
|
|
||||||
"no-unused-vars":"off" // we define stuff to use in other scripts
|
|
||||||
},
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"node": true
|
|
||||||
},
|
|
||||||
"extends": "eslint:recommended",
|
|
||||||
"globals": {
|
|
||||||
"btoa": "writable",
|
|
||||||
"Espruino": "writable",
|
|
||||||
|
|
||||||
"htmlElement": "readonly",
|
|
||||||
"Puck": "readonly",
|
|
||||||
"escapeHtml": "readonly",
|
|
||||||
"htmlToArray": "readonly",
|
|
||||||
"heatshrink": "readonly",
|
|
||||||
"Puck": "readonly",
|
|
||||||
"Promise": "readonly",
|
|
||||||
"Comms": "readonly",
|
|
||||||
"Progress": "readonly",
|
|
||||||
"showToast": "readonly",
|
|
||||||
"showPrompt": "readonly",
|
|
||||||
"httpGet": "readonly",
|
|
||||||
"getVersionInfo": "readonly",
|
|
||||||
"AppInfo": "readonly",
|
|
||||||
"marked": "readonly",
|
|
||||||
"appSorter": "readonly",
|
|
||||||
"Uint8Array" : "readonly",
|
|
||||||
"SETTINGS" : "readonly",
|
|
||||||
"globToRegex" : "readonly",
|
|
||||||
"toJS" : "readonly"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,164 +0,0 @@
|
||||||
if (typeof btoa==="undefined") {
|
|
||||||
// Don't define btoa as a function here because Apple's
|
|
||||||
// iOS browser defines the function even though it's in
|
|
||||||
// an IF statement that is never executed
|
|
||||||
btoa = function(d) { return Buffer.from(d,'binary').toString('base64'); }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Converts a string into most efficient way to send to Espruino (either json, base64, or compressed base64)
|
|
||||||
function toJS(txt) {
|
|
||||||
let isBinary = false;
|
|
||||||
for (let i=0;i<txt.length;i++) {
|
|
||||||
let ch = txt.charCodeAt(i);
|
|
||||||
if (ch==0 || ch>127) isBinary=true;
|
|
||||||
}
|
|
||||||
let json = JSON.stringify(txt);
|
|
||||||
let b64 = "atob("+JSON.stringify(btoa(txt))+")";
|
|
||||||
let js = (isBinary || (b64.length < json.length)) ? b64 : json;
|
|
||||||
|
|
||||||
if (typeof heatshrink !== "undefined") {
|
|
||||||
let ua = new Uint8Array(txt.length);
|
|
||||||
for (let i=0;i<txt.length;i++) ua[i] = txt.charCodeAt(i);
|
|
||||||
let c = heatshrink.compress(ua);
|
|
||||||
let cs = "";
|
|
||||||
for (let i=0;i<c.length;i++)
|
|
||||||
cs += String.fromCharCode(c[i]);
|
|
||||||
cs = 'require("heatshrink").decompress(atob("'+btoa(cs)+'"))';
|
|
||||||
// if it's more than a little smaller, use compressed version
|
|
||||||
if (cs.length*4 < js.length*3)
|
|
||||||
js = cs;
|
|
||||||
}
|
|
||||||
|
|
||||||
return js;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("undefined"!=typeof module)
|
|
||||||
Espruino = require("../lib/espruinotools.js");
|
|
||||||
|
|
||||||
const AppInfo = {
|
|
||||||
/* Get files needed for app.
|
|
||||||
options = {
|
|
||||||
fileGetter : callback for getting URL,
|
|
||||||
settings : global settings object
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
getFiles : (app,options) => {
|
|
||||||
return new Promise((resolve,reject) => {
|
|
||||||
// Load all files
|
|
||||||
Promise.all(app.storage.map(storageFile => {
|
|
||||||
if (storageFile.content!==undefined)
|
|
||||||
return Promise.resolve(storageFile);
|
|
||||||
else if (storageFile.url)
|
|
||||||
return options.fileGetter(`apps/${app.id}/${storageFile.url}`).then(content => {
|
|
||||||
if (storageFile.url.endsWith(".js") && !storageFile.url.endsWith(".min.js")) { // if original file ends in '.js'...
|
|
||||||
return Espruino.transform(content, {
|
|
||||||
SET_TIME_ON_WRITE : false,
|
|
||||||
PRETOKENISE : options.settings.pretokenise,
|
|
||||||
//MINIFICATION_LEVEL : "ESPRIMA", // disable due to https://github.com/espruino/BangleApps/pull/355#issuecomment-620124162
|
|
||||||
builtinModules : "Flash,Storage,heatshrink,tensorflow,locale,notify"
|
|
||||||
});
|
|
||||||
} else
|
|
||||||
return content;
|
|
||||||
}).then(content => {
|
|
||||||
return {
|
|
||||||
name : storageFile.name,
|
|
||||||
content : content,
|
|
||||||
evaluate : storageFile.evaluate
|
|
||||||
}});
|
|
||||||
else return Promise.resolve();
|
|
||||||
})).then(fileContents => { // now we just have a list of files + contents...
|
|
||||||
// filter out empty files
|
|
||||||
fileContents = fileContents.filter(x=>x!==undefined);
|
|
||||||
// What about minification?
|
|
||||||
// Add app's info JSON
|
|
||||||
return AppInfo.createAppJSON(app, fileContents);
|
|
||||||
}).then(fileContents => {
|
|
||||||
// then map each file to a command to load into storage
|
|
||||||
fileContents.forEach(storageFile => {
|
|
||||||
// format ready for Espruino
|
|
||||||
if (storageFile.evaluate) {
|
|
||||||
let js = storageFile.content.trim();
|
|
||||||
if (js.endsWith(";"))
|
|
||||||
js = js.slice(0,-1);
|
|
||||||
storageFile.cmd = `\x10require('Storage').write(${JSON.stringify(storageFile.name)},${js});`;
|
|
||||||
} else {
|
|
||||||
let code = storageFile.content;
|
|
||||||
// write code in chunks, in case it is too big to fit in RAM (fix #157)
|
|
||||||
let CHUNKSIZE = 4096;
|
|
||||||
storageFile.cmd = `\x10require('Storage').write(${JSON.stringify(storageFile.name)},${toJS(code.substr(0,CHUNKSIZE))},0,${code.length});`;
|
|
||||||
for (let i=CHUNKSIZE;i<code.length;i+=CHUNKSIZE)
|
|
||||||
storageFile.cmd += `\n\x10require('Storage').write(${JSON.stringify(storageFile.name)},${toJS(code.substr(i,CHUNKSIZE))},${i});`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
resolve(fileContents);
|
|
||||||
}).catch(err => reject(err));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
createAppJSON : (app, fileContents) => {
|
|
||||||
return new Promise((resolve,reject) => {
|
|
||||||
let appJSONName = app.id+".info";
|
|
||||||
// Check we don't already have a JSON file!
|
|
||||||
let appJSONFile = fileContents.find(f=>f.name==appJSONName);
|
|
||||||
if (appJSONFile) reject("App JSON file explicitly specified!");
|
|
||||||
// Now actually create the app JSON
|
|
||||||
let json = {
|
|
||||||
id : app.id
|
|
||||||
};
|
|
||||||
if (app.shortName) json.name = app.shortName;
|
|
||||||
else json.name = app.name;
|
|
||||||
if (app.type && app.type!="app") json.type = app.type;
|
|
||||||
if (fileContents.find(f=>f.name==app.id+".app.js"))
|
|
||||||
json.src = app.id+".app.js";
|
|
||||||
if (fileContents.find(f=>f.name==app.id+".img"))
|
|
||||||
json.icon = app.id+".img";
|
|
||||||
if (app.sortorder) json.sortorder = app.sortorder;
|
|
||||||
if (app.version) json.version = app.version;
|
|
||||||
let fileList = fileContents.map(storageFile=>storageFile.name);
|
|
||||||
fileList.unshift(appJSONName); // do we want this? makes life easier!
|
|
||||||
json.files = fileList.join(",");
|
|
||||||
if ('data' in app) {
|
|
||||||
let data = {dataFiles: [], storageFiles: []};
|
|
||||||
// add "data" files to appropriate list
|
|
||||||
app.data.forEach(d=>{
|
|
||||||
if (d.storageFile) data.storageFiles.push(d.name||d.wildcard)
|
|
||||||
else data.dataFiles.push(d.name||d.wildcard)
|
|
||||||
})
|
|
||||||
const dataString = AppInfo.makeDataString(data)
|
|
||||||
if (dataString) json.data = dataString
|
|
||||||
}
|
|
||||||
fileContents.push({
|
|
||||||
name : appJSONName,
|
|
||||||
content : JSON.stringify(json)
|
|
||||||
});
|
|
||||||
resolve(fileContents);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
// (<appid>.info).data holds filenames of data: both regular and storageFiles
|
|
||||||
// These are stored as: (note comma vs semicolons)
|
|
||||||
// "fil1,file2", "file1,file2;storageFileA,storageFileB" or ";storageFileA"
|
|
||||||
/**
|
|
||||||
* Convert appid.info "data" to object with file names/patterns
|
|
||||||
* Passing in undefined works
|
|
||||||
* @param data "data" as stored in appid.info
|
|
||||||
* @returns {{storageFiles:[], dataFiles:[]}}
|
|
||||||
*/
|
|
||||||
parseDataString(data) {
|
|
||||||
data = data || '';
|
|
||||||
let [files = [], storage = []] = data.split(';').map(d => d.split(','))
|
|
||||||
return {dataFiles: files, storageFiles: storage}
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Convert object with file names/patterns to appid.info "data" string
|
|
||||||
* Passing in an incomplete object will not work
|
|
||||||
* @param data {{storageFiles:[], dataFiles:[]}}
|
|
||||||
* @returns {string} "data" to store in appid.info
|
|
||||||
*/
|
|
||||||
makeDataString(data) {
|
|
||||||
if (!data.dataFiles.length && !data.storageFiles.length) { return '' }
|
|
||||||
if (!data.storageFiles.length) { return data.dataFiles.join(',') }
|
|
||||||
return [data.dataFiles.join(','),data.storageFiles.join(',')].join(';')
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if ("undefined"!=typeof module)
|
|
||||||
module.exports = AppInfo;
|
|
291
core/js/comms.js
291
core/js/comms.js
|
@ -1,291 +0,0 @@
|
||||||
//Puck.debug=3;
|
|
||||||
console.log("=============================================")
|
|
||||||
console.log("Type 'Puck.debug=3' for full BLE debug info")
|
|
||||||
console.log("=============================================")
|
|
||||||
|
|
||||||
// FIXME: use UART lib so that we handle errors properly
|
|
||||||
const Comms = {
|
|
||||||
reset : (opt) => new Promise((resolve,reject) => {
|
|
||||||
let tries = 8;
|
|
||||||
console.log("<COMMS> reset");
|
|
||||||
Puck.write(`\x03\x10reset(${opt=="wipe"?"1":""});\n`,function rstHandler(result) {
|
|
||||||
console.log("<COMMS> reset: got "+JSON.stringify(result));
|
|
||||||
if (result===null) return reject("Connection failed");
|
|
||||||
if (result=="" && (tries-- > 0)) {
|
|
||||||
console.log(`<COMMS> reset: no response. waiting ${tries}...`);
|
|
||||||
Puck.write("\x03",rstHandler);
|
|
||||||
} else {
|
|
||||||
console.log(`<COMMS> reset: complete.`);
|
|
||||||
setTimeout(resolve,250);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
uploadApp : (app,skipReset) => { // expects an apps.json structure (i.e. with `storage`)
|
|
||||||
Progress.show({title:`Uploading ${app.name}`,sticky:true});
|
|
||||||
return AppInfo.getFiles(app, {
|
|
||||||
fileGetter : httpGet,
|
|
||||||
settings : SETTINGS
|
|
||||||
}).then(fileContents => {
|
|
||||||
return new Promise((resolve,reject) => {
|
|
||||||
console.log("<COMMS> uploadApp:",fileContents.map(f=>f.name).join(", "));
|
|
||||||
let maxBytes = fileContents.reduce((b,f)=>b+f.cmd.length, 0)||1;
|
|
||||||
let currentBytes = 0;
|
|
||||||
|
|
||||||
let appInfoFileName = app.id+".info";
|
|
||||||
let appInfoFile = fileContents.find(f=>f.name==appInfoFileName);
|
|
||||||
if (!appInfoFile) reject(`${appInfoFileName} not found`);
|
|
||||||
let appInfo = JSON.parse(appInfoFile.content);
|
|
||||||
|
|
||||||
// Upload each file one at a time
|
|
||||||
function doUploadFiles() {
|
|
||||||
// No files left - print 'reboot' message
|
|
||||||
if (fileContents.length==0) {
|
|
||||||
Puck.write(`\x10E.showMessage('Hold BTN3\\nto reload')\n`,(result) => {
|
|
||||||
Progress.hide({sticky:true});
|
|
||||||
if (result===null) return reject("");
|
|
||||||
resolve(appInfo);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let f = fileContents.shift();
|
|
||||||
console.log(`<COMMS> Upload ${f.name} => ${JSON.stringify(f.content)}`);
|
|
||||||
// Chould check CRC here if needed instead of returning 'OK'...
|
|
||||||
// E.CRC32(require("Storage").read(${JSON.stringify(app.name)}))
|
|
||||||
let cmds = f.cmd.split("\n");
|
|
||||||
function uploadCmd() {
|
|
||||||
if (!cmds.length) return doUploadFiles();
|
|
||||||
let cmd = cmds.shift();
|
|
||||||
Progress.show({
|
|
||||||
min:currentBytes / maxBytes,
|
|
||||||
max:(currentBytes+cmd.length) / maxBytes});
|
|
||||||
currentBytes += cmd.length;
|
|
||||||
Puck.write(`${cmd};Bluetooth.println("OK")\n`,(result) => {
|
|
||||||
if (!result || result.trim()!="OK") {
|
|
||||||
Progress.hide({sticky:true});
|
|
||||||
return reject("Unexpected response "+(result||""));
|
|
||||||
}
|
|
||||||
uploadCmd();
|
|
||||||
}, true); // wait for a newline
|
|
||||||
}
|
|
||||||
uploadCmd();
|
|
||||||
}
|
|
||||||
// Start the upload
|
|
||||||
function doUpload() {
|
|
||||||
Puck.write(`\x10E.showMessage('Uploading\\n${app.id}...')\n`,(result) => {
|
|
||||||
if (result===null) {
|
|
||||||
Progress.hide({sticky:true});
|
|
||||||
return reject("");
|
|
||||||
}
|
|
||||||
doUploadFiles();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (skipReset) {
|
|
||||||
doUpload();
|
|
||||||
} else {
|
|
||||||
// reset to ensure we have enough memory to upload what we need to
|
|
||||||
Comms.reset().then(doUpload, reject)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
getInstalledApps : () => {
|
|
||||||
Progress.show({title:`Getting app list...`,sticky:true});
|
|
||||||
return new Promise((resolve,reject) => {
|
|
||||||
Puck.write("\x03",(result) => {
|
|
||||||
if (result===null) {
|
|
||||||
Progress.hide({sticky:true});
|
|
||||||
return reject("");
|
|
||||||
}
|
|
||||||
Puck.write('\x10Bluetooth.print("[");require("Storage").list(/\\.info$/).forEach(f=>{var j=require("Storage").readJSON(f,1)||{};j.id=f.slice(0,-5);Bluetooth.print(JSON.stringify(j)+",")});Bluetooth.println("0]")\n', (appList,err) => {
|
|
||||||
Progress.hide({sticky:true});
|
|
||||||
try {
|
|
||||||
appList = JSON.parse(appList);
|
|
||||||
// remove last element since we added a final '0'
|
|
||||||
// to make things easy on the Bangle.js side
|
|
||||||
appList = appList.slice(0,-1);
|
|
||||||
} catch (e) {
|
|
||||||
appList = null;
|
|
||||||
err = e.toString();
|
|
||||||
}
|
|
||||||
if (appList===null) return reject(err || "");
|
|
||||||
console.log("<COMMS> getInstalledApps", appList);
|
|
||||||
resolve(appList);
|
|
||||||
}, true /* callback on newline */);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
removeApp : app => { // expects an appid.info structure (i.e. with `files`)
|
|
||||||
if (!app.files && !app.data) return Promise.resolve(); // nothing to erase
|
|
||||||
Progress.show({title:`Removing ${app.name}`,sticky:true});
|
|
||||||
let cmds = '\x10const s=require("Storage");\n';
|
|
||||||
// remove App files: regular files, exact names only
|
|
||||||
cmds += app.files.split(',').map(file => `\x10s.erase(${toJS(file)});\n`).join("");
|
|
||||||
// remove app Data: (dataFiles and storageFiles)
|
|
||||||
const data = AppInfo.parseDataString(app.data)
|
|
||||||
const isGlob = f => /[?*]/.test(f)
|
|
||||||
// regular files, can use wildcards
|
|
||||||
cmds += data.dataFiles.map(file => {
|
|
||||||
if (!isGlob(file)) return `\x10s.erase(${toJS(file)});\n`;
|
|
||||||
const regex = new RegExp(globToRegex(file))
|
|
||||||
return `\x10s.list(${regex}).forEach(f=>s.erase(f));\n`;
|
|
||||||
}).join("");
|
|
||||||
// storageFiles, can use wildcards
|
|
||||||
cmds += data.storageFiles.map(file => {
|
|
||||||
if (!isGlob(file)) return `\x10s.open(${toJS(file)},'r').erase();\n`;
|
|
||||||
// storageFiles have a chunk number appended to their real name
|
|
||||||
const regex = globToRegex(file+'\u0001')
|
|
||||||
// open() doesn't want the chunk number though
|
|
||||||
let cmd = `\x10s.list(${regex}).forEach(f=>s.open(f.substring(0,f.length-1),'r').erase());\n`
|
|
||||||
// using a literal \u0001 char fails (not sure why), so escape it
|
|
||||||
return cmd.replace('\u0001', '\\x01')
|
|
||||||
}).join("");
|
|
||||||
console.log("<COMMS> removeApp", cmds);
|
|
||||||
return Comms.reset().then(() => new Promise((resolve,reject) => {
|
|
||||||
Puck.write(`\x03\x10E.showMessage('Erasing\\n${app.id}...')${cmds}\x10E.showMessage('Hold BTN3\\nto reload')\n`,(result) => {
|
|
||||||
Progress.hide({sticky:true});
|
|
||||||
if (result===null) return reject("");
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
})).catch(function(reason) {
|
|
||||||
Progress.hide({sticky:true});
|
|
||||||
return Promise.reject(reason);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
removeAllApps : () => {
|
|
||||||
console.log("<COMMS> removeAllApps start");
|
|
||||||
Progress.show({title:"Removing all apps",percent:"animate",sticky:true});
|
|
||||||
return new Promise((resolve,reject) => {
|
|
||||||
let timeout = 5;
|
|
||||||
function handleResult(result,err) {
|
|
||||||
console.log("<COMMS> removeAllApps: received "+JSON.stringify(result));
|
|
||||||
if (result=="" && (timeout--)) {
|
|
||||||
console.log("<COMMS> removeAllApps: no result - waiting some more ("+timeout+").");
|
|
||||||
// send space and delete - so it's something, but it should just cancel out
|
|
||||||
Puck.write(" \u0008", handleResult, true /* wait for newline */);
|
|
||||||
} else {
|
|
||||||
Progress.hide({sticky:true});
|
|
||||||
if (!result || result.trim()!="OK") {
|
|
||||||
if (!result) result = "No response";
|
|
||||||
else result = "Got "+JSON.stringify(result.trim());
|
|
||||||
return reject(err || result);
|
|
||||||
} else resolve();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Use write with newline here so we wait for it to finish
|
|
||||||
let cmd = '\x10E.showMessage("Erasing...");require("Storage").eraseAll();Bluetooth.println("OK");reset()\n';
|
|
||||||
Puck.write(cmd, handleResult, true /* wait for newline */);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
setTime : () => {
|
|
||||||
return new Promise((resolve,reject) => {
|
|
||||||
let d = new Date();
|
|
||||||
let tz = d.getTimezoneOffset()/-60
|
|
||||||
let cmd = '\x03\x10setTime('+(d.getTime()/1000)+');';
|
|
||||||
// in 1v93 we have timezones too
|
|
||||||
cmd += 'E.setTimeZone('+tz+');';
|
|
||||||
cmd += "(s=>{s&&(s.timezone="+tz+")&&require('Storage').write('setting.json',s);})(require('Storage').readJSON('setting.json',1))\n";
|
|
||||||
Puck.write(cmd, (result) => {
|
|
||||||
if (result===null) return reject("");
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
disconnectDevice: () => {
|
|
||||||
let connection = Puck.getConnection();
|
|
||||||
|
|
||||||
if (!connection) return;
|
|
||||||
|
|
||||||
connection.close();
|
|
||||||
},
|
|
||||||
watchConnectionChange : cb => {
|
|
||||||
let connected = Puck.isConnected();
|
|
||||||
|
|
||||||
//TODO Switch to an event listener when Puck will support it
|
|
||||||
let interval = setInterval(() => {
|
|
||||||
if (connected === Puck.isConnected()) return;
|
|
||||||
|
|
||||||
connected = Puck.isConnected();
|
|
||||||
cb(connected);
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
//stop watching
|
|
||||||
return () => {
|
|
||||||
clearInterval(interval);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
listFiles : () => {
|
|
||||||
return new Promise((resolve,reject) => {
|
|
||||||
Puck.write("\x03",(result) => {
|
|
||||||
if (result===null) return reject("");
|
|
||||||
//use encodeURIComponent to serialize octal sequence of append files
|
|
||||||
Puck.eval('require("Storage").list().map(encodeURIComponent)', (files,err) => {
|
|
||||||
if (files===null) return reject(err || "");
|
|
||||||
files = files.map(decodeURIComponent);
|
|
||||||
console.log("<COMMS> listFiles", files);
|
|
||||||
resolve(files);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
readFile : (file) => {
|
|
||||||
return new Promise((resolve,reject) => {
|
|
||||||
//encode name to avoid serialization issue due to octal sequence
|
|
||||||
const name = encodeURIComponent(file);
|
|
||||||
Puck.write("\x03",(result) => {
|
|
||||||
if (result===null) return reject("");
|
|
||||||
//TODO: big files will not fit in RAM.
|
|
||||||
//we should loop and read chunks one by one.
|
|
||||||
//Use btoa for binary content
|
|
||||||
Puck.eval(`btoa(require("Storage").read(decodeURIComponent("${name}"))))`, (content,err) => {
|
|
||||||
if (content===null) return reject(err || "");
|
|
||||||
resolve(atob(content));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
readStorageFile : (filename) => { // StorageFiles are different to normal storage entries
|
|
||||||
return new Promise((resolve,reject) => {
|
|
||||||
// Use "\xFF" to signal end of file (can't occur in files anyway)
|
|
||||||
let fileContent = "";
|
|
||||||
let fileSize = undefined;
|
|
||||||
let connection = Puck.getConnection();
|
|
||||||
connection.received = "";
|
|
||||||
connection.cb = function(d) {
|
|
||||||
let finished = false;
|
|
||||||
let eofIndex = d.indexOf("\xFF");
|
|
||||||
if (eofIndex>=0) {
|
|
||||||
finished = true;
|
|
||||||
d = d.substr(0,eofIndex);
|
|
||||||
}
|
|
||||||
fileContent += d;
|
|
||||||
if (fileSize === undefined) {
|
|
||||||
let newLineIdx = fileContent.indexOf("\n");
|
|
||||||
if (newLineIdx>=0) {
|
|
||||||
fileSize = parseInt(fileContent.substr(0,newLineIdx));
|
|
||||||
console.log("<COMMS> readStorageFile size is "+fileSize);
|
|
||||||
fileContent = fileContent.substr(newLineIdx+1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Progress.show({percent:100*fileContent.length / (fileSize||1000000)});
|
|
||||||
}
|
|
||||||
if (finished) {
|
|
||||||
Progress.hide();
|
|
||||||
connection.received = "";
|
|
||||||
connection.cb = undefined;
|
|
||||||
resolve(fileContent);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
console.log(`<COMMS> readStorageFile ${JSON.stringify(filename)}`);
|
|
||||||
connection.write(`\x03\x10(function() {
|
|
||||||
var f = require("Storage").open(${JSON.stringify(filename)},"r");
|
|
||||||
Bluetooth.println(f.getLength());
|
|
||||||
var l = f.readLine();
|
|
||||||
while (l!==undefined) { Bluetooth.print(l); l = f.readLine(); }
|
|
||||||
Bluetooth.print("\xFF");
|
|
||||||
})()\n`,() => {
|
|
||||||
Progress.show({title:`Reading ${JSON.stringify(filename)}`,percent:0});
|
|
||||||
console.log(`<COMMS> StorageFile read started...`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
754
core/js/index.js
754
core/js/index.js
|
@ -1,754 +0,0 @@
|
||||||
let appJSON = []; // List of apps and info from apps.json
|
|
||||||
let appsInstalled = []; // list of app JSON
|
|
||||||
let appSortInfo = {}; // list of data to sort by, from appdates.csv { created, modified }
|
|
||||||
let files = []; // list of files on the Espruimo Device
|
|
||||||
let DEFAULTSETTINGS = {
|
|
||||||
pretokenise : true,
|
|
||||||
favourites : ["boot","launch","setting"]
|
|
||||||
};
|
|
||||||
let SETTINGS = JSON.parse(JSON.stringify(DEFAULTSETTINGS)); // clone
|
|
||||||
|
|
||||||
httpGet("apps.json").then(apps=>{
|
|
||||||
try {
|
|
||||||
appJSON = JSON.parse(apps);
|
|
||||||
} catch(e) {
|
|
||||||
console.log(e);
|
|
||||||
showToast("App List Corrupted","error");
|
|
||||||
}
|
|
||||||
refreshLibrary();
|
|
||||||
refreshFilter();
|
|
||||||
});
|
|
||||||
|
|
||||||
httpGet("appdates.csv").then(csv=>{
|
|
||||||
document.querySelector(".sort-nav").classList.remove("hidden");
|
|
||||||
csv.split("\n").forEach(line=>{
|
|
||||||
let l = line.split(",");
|
|
||||||
appSortInfo[l[0]] = {
|
|
||||||
created : Date.parse(l[1]),
|
|
||||||
modified : Date.parse(l[2])
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}).catch(err=>{
|
|
||||||
console.log("No recent.csv - app sort disabled");
|
|
||||||
});
|
|
||||||
|
|
||||||
// =========================================== Top Navigation
|
|
||||||
function showChangeLog(appid) {
|
|
||||||
let app = appNameToApp(appid);
|
|
||||||
function show(contents) {
|
|
||||||
showPrompt(app.name+" Change Log",contents,{ok:true}).catch(()=>{});
|
|
||||||
}
|
|
||||||
httpGet(`apps/${appid}/ChangeLog`).
|
|
||||||
then(show).catch(()=>show("No Change Log available"));
|
|
||||||
}
|
|
||||||
function showReadme(appid) {
|
|
||||||
let app = appNameToApp(appid);
|
|
||||||
let appPath = `apps/${appid}/`;
|
|
||||||
let markedOptions = { baseUrl : appPath };
|
|
||||||
function show(contents) {
|
|
||||||
if (!contents) return;
|
|
||||||
showPrompt(app.name + " Documentation", marked(contents, markedOptions), {ok: true}, false).catch(() => {});
|
|
||||||
}
|
|
||||||
httpGet(appPath+app.readme).then(show).catch(()=>show("Failed to load README."));
|
|
||||||
}
|
|
||||||
function getAppDescription(app) {
|
|
||||||
let appPath = `apps/${app.id}/`;
|
|
||||||
let markedOptions = { baseUrl : appPath };
|
|
||||||
return marked(app.description, markedOptions);
|
|
||||||
}
|
|
||||||
function handleCustomApp(appTemplate) {
|
|
||||||
// Pops up an IFRAME that allows an app to be customised
|
|
||||||
if (!appTemplate.custom) throw new Error("App doesn't have custom HTML");
|
|
||||||
return new Promise((resolve,reject) => {
|
|
||||||
let 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(appTemplate.name)}</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body" style="height:100%">
|
|
||||||
<div class="content" style="height:100%">
|
|
||||||
<iframe src="apps/${appTemplate.id}/${appTemplate.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");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
let iframe = modal.getElementsByTagName("iframe")[0];
|
|
||||||
iframe.contentWindow.addEventListener("message", function(event) {
|
|
||||||
let appFiles = event.data;
|
|
||||||
let app = JSON.parse(JSON.stringify(appTemplate)); // clone template
|
|
||||||
// copy extra keys from appFiles
|
|
||||||
Object.keys(appFiles).forEach(k => {
|
|
||||||
if (k!="storage") app[k] = appFiles[k]
|
|
||||||
});
|
|
||||||
appFiles.storage.forEach(f => {
|
|
||||||
app.storage = app.storage.filter(s=>s.name!=f.name); // remove existing item
|
|
||||||
app.storage.push(f); // add new
|
|
||||||
});
|
|
||||||
console.log("Received custom app", app);
|
|
||||||
modal.remove();
|
|
||||||
checkDependencies(app)
|
|
||||||
.then(()=>Comms.uploadApp(app))
|
|
||||||
.then(()=>{
|
|
||||||
Progress.hide({sticky:true});
|
|
||||||
resolve();
|
|
||||||
}).catch(e => {
|
|
||||||
Progress.hide({sticky:true});
|
|
||||||
reject(e);
|
|
||||||
});
|
|
||||||
}, false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleAppInterface(app) {
|
|
||||||
// IFRAME interface window that can be used to get data from the app
|
|
||||||
if (!app.interface) throw new Error("App doesn't have interface HTML");
|
|
||||||
return new Promise((resolve,reject) => {
|
|
||||||
let 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 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");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
let iframe = modal.getElementsByTagName("iframe")[0];
|
|
||||||
iframe.onload = function() {
|
|
||||||
let iwin = iframe.contentWindow;
|
|
||||||
iwin.addEventListener("message", function(event) {
|
|
||||||
let msg = event.data;
|
|
||||||
if (msg.type=="eval") {
|
|
||||||
Puck.eval(msg.data, function(result) {
|
|
||||||
iwin.postMessage({
|
|
||||||
type : "evalrsp",
|
|
||||||
data : result,
|
|
||||||
id : msg.id
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else if (msg.type=="write") {
|
|
||||||
Puck.write(msg.data, function(result) {
|
|
||||||
iwin.postMessage({
|
|
||||||
type : "writersp",
|
|
||||||
data : result,
|
|
||||||
id : msg.id
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else if (msg.type=="readstoragefile") {
|
|
||||||
Comms.readStorageFile(msg.data/*filename*/).then(function(result) {
|
|
||||||
iwin.postMessage({
|
|
||||||
type : "readstoragefilersp",
|
|
||||||
data : result,
|
|
||||||
id : msg.id
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, false);
|
|
||||||
iwin.postMessage({type:"init"});
|
|
||||||
};
|
|
||||||
iframe.src = `apps/${app.id}/${app.interface}`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function changeAppFavourite(favourite, app) {
|
|
||||||
let favourites = SETTINGS.favourites;
|
|
||||||
if (favourite) {
|
|
||||||
SETTINGS.favourites = SETTINGS.favourites.concat([app.id]);
|
|
||||||
} else {
|
|
||||||
if ([ "boot","setting"].includes(app.id)) {
|
|
||||||
showToast(app.name + ' is required, can\'t remove it' , 'warning');
|
|
||||||
}else {
|
|
||||||
SETTINGS.favourites = SETTINGS.favourites.filter(e => e != app.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
saveSettings();
|
|
||||||
refreshLibrary();
|
|
||||||
}
|
|
||||||
|
|
||||||
// =========================================== Top Navigation
|
|
||||||
function showTab(tabname) {
|
|
||||||
htmlToArray(document.querySelectorAll("#tab-navigate .tab-item")).forEach(tab => {
|
|
||||||
tab.classList.remove("active");
|
|
||||||
});
|
|
||||||
htmlToArray(document.querySelectorAll(".apploader-tab")).forEach(tab => {
|
|
||||||
tab.style.display = "none";
|
|
||||||
});
|
|
||||||
document.getElementById("tab-"+tabname).classList.add("active");
|
|
||||||
document.getElementById(tabname).style.display = "inherit";
|
|
||||||
}
|
|
||||||
|
|
||||||
// =========================================== Library
|
|
||||||
|
|
||||||
// Can't use chip.attributes.filterid.value here because Safari/Apple's WebView doesn't handle it
|
|
||||||
let chips = Array.from(document.querySelectorAll('.filter-nav .chip')).map(chip => chip.getAttribute("filterid"));
|
|
||||||
let hash = "";
|
|
||||||
if (window.location.hash)
|
|
||||||
hash = decodeURIComponent(window.location.hash.slice(1)).toLowerCase();
|
|
||||||
|
|
||||||
let activeFilter = (chips.indexOf(hash)>=0) ? hash : '';
|
|
||||||
let activeSort = '';
|
|
||||||
let currentSearch = activeFilter ? '' : hash;
|
|
||||||
|
|
||||||
function refreshFilter(){
|
|
||||||
let filtersContainer = document.querySelector("#librarycontainer .filter-nav");
|
|
||||||
filtersContainer.querySelector('.active').classList.remove('active');
|
|
||||||
if(activeFilter) filtersContainer.querySelector('.chip[filterid="'+activeFilter+'"]').classList.add('active');
|
|
||||||
else filtersContainer.querySelector('.chip[filterid]').classList.add('active');
|
|
||||||
}
|
|
||||||
function refreshSort(){
|
|
||||||
let sortContainer = document.querySelector("#librarycontainer .sort-nav");
|
|
||||||
sortContainer.querySelector('.active').classList.remove('active');
|
|
||||||
if(activeSort) sortContainer.querySelector('.chip[sortid="'+activeSort+'"]').classList.add('active');
|
|
||||||
else sortContainer.querySelector('.chip[sortid]').classList.add('active');
|
|
||||||
}
|
|
||||||
function refreshLibrary() {
|
|
||||||
let panelbody = document.querySelector("#librarycontainer .panel-body");
|
|
||||||
let visibleApps = appJSON.slice(); // clone so we don't mess with the original
|
|
||||||
let favourites = SETTINGS.favourites;
|
|
||||||
|
|
||||||
if (activeFilter) {
|
|
||||||
if ( activeFilter == "favourites" ) {
|
|
||||||
visibleApps = visibleApps.filter(app => app.id && (favourites.filter( e => e == app.id).length));
|
|
||||||
} else {
|
|
||||||
visibleApps = visibleApps.filter(app => app.tags && app.tags.split(',').includes(activeFilter));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentSearch) {
|
|
||||||
visibleApps = visibleApps.filter(app => app.name.toLowerCase().includes(currentSearch) || app.tags.includes(currentSearch) || app.id.toLowerCase().includes(currentSearch));
|
|
||||||
}
|
|
||||||
|
|
||||||
visibleApps.sort(appSorter);
|
|
||||||
if (activeSort) {
|
|
||||||
if (activeSort=="created" || activeSort=="modified") {
|
|
||||||
visibleApps = visibleApps.sort((a,b) => appSortInfo[b.id][activeSort] - appSortInfo[a.id][activeSort]);
|
|
||||||
} else throw new Error("Unknown sort type "+activeSort);
|
|
||||||
}
|
|
||||||
|
|
||||||
panelbody.innerHTML = visibleApps.map((app,idx) => {
|
|
||||||
let appInstalled = appsInstalled.find(a=>a.id==app.id);
|
|
||||||
let version = getVersionInfo(app, appInstalled);
|
|
||||||
let versionInfo = version.text;
|
|
||||||
if (versionInfo) versionInfo = " <small>("+versionInfo+")</small>";
|
|
||||||
let readme = `<a class="c-hand" onclick="showReadme('${app.id}')">Read more...</a>`;
|
|
||||||
let favourite = favourites.find(e => e == app.id);
|
|
||||||
|
|
||||||
let githubURL = `${APP_SOURCECODE_URL}/${app.id}`;
|
|
||||||
let appurl = window.location.origin + window.location.pathname + "#" + encodeURIComponent(app.id);
|
|
||||||
|
|
||||||
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.id}/${app.icon}`:"unknown.png"}" alt="${escapeHtml(app.name)}"></figure><br/>
|
|
||||||
</div>
|
|
||||||
<div class="tile-content">
|
|
||||||
<p class="tile-title text-bold"><a name="${appurl}"></a>${escapeHtml(app.name)} ${versionInfo}</p>
|
|
||||||
<p class="tile-subtitle">${getAppDescription(app)}${app.readme?`<br/>${readme}`:""}</p>
|
|
||||||
<a href="${githubURL}" target="_blank" class="link-github"><img src="core/img/github-icon-sml.png" alt="See the code on GitHub"/></a>
|
|
||||||
</div>
|
|
||||||
<div class="tile-action">
|
|
||||||
<button class="btn btn-link btn-action btn-lg ${!app.custom?"text-error":"d-hide"}" appid="${app.id}" title="Favorite"><i class="icon"></i>${favourite?"♥":"♡"}</button>
|
|
||||||
<button class="btn btn-link btn-action btn-lg ${(appInstalled&&app.interface)?"":"d-hide"}" appid="${app.id}" title="Download data from app"><i class="icon icon-download"></i></button>
|
|
||||||
<button class="btn btn-link btn-action btn-lg ${app.allow_emulator?"":"d-hide"}" appid="${app.id}" title="Try in Emulator"><i class="icon icon-share"></i></button>
|
|
||||||
<button class="btn btn-link btn-action btn-lg ${version.canUpdate?"":"d-hide"}" appid="${app.id}" title="Update App"><i class="icon icon-refresh"></i></button>
|
|
||||||
<button class="btn btn-link btn-action btn-lg ${(!appInstalled && !app.custom)?"":"d-hide"}" appid="${app.id}" title="Upload App"><i class="icon icon-upload"></i></button>
|
|
||||||
<button class="btn btn-link btn-action btn-lg ${appInstalled?"":"d-hide"}" appid="${app.id}" title="Remove App"><i class="icon icon-delete"></i></button>
|
|
||||||
<button class="btn btn-link btn-action btn-lg ${app.custom?"":"d-hide"}" appid="${app.id}" title="Customise and Upload App"><i class="icon icon-menu"></i></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;}).join("");
|
|
||||||
// set badge up top
|
|
||||||
let 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 => {
|
|
||||||
let button = event.currentTarget;
|
|
||||||
let icon = button.firstChild;
|
|
||||||
let appid = button.getAttribute("appid");
|
|
||||||
let app = appNameToApp(appid);
|
|
||||||
if (!app) throw new Error("App "+appid+" not found");
|
|
||||||
// check icon to figure out what we should do
|
|
||||||
if (icon.classList.contains("icon-share")) {
|
|
||||||
// emulator
|
|
||||||
let file = app.storage.find(f=>f.name.endsWith('.js'));
|
|
||||||
if (!file) {
|
|
||||||
console.error("No entrypoint found for "+appid);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let baseurl = window.location.href;
|
|
||||||
baseurl = baseurl.substr(0,baseurl.lastIndexOf("/"));
|
|
||||||
let url = baseurl+"/apps/"+app.id+"/"+file.url;
|
|
||||||
window.open(`https://espruino.com/ide/emulator.html?codeurl=${url}&upload`);
|
|
||||||
} else if (icon.classList.contains("icon-upload")) {
|
|
||||||
// upload
|
|
||||||
icon.classList.remove("icon-upload");
|
|
||||||
icon.classList.add("loading");
|
|
||||||
uploadApp(app);
|
|
||||||
} else if (icon.classList.contains("icon-menu")) {
|
|
||||||
// custom HTML update
|
|
||||||
icon.classList.remove("icon-menu");
|
|
||||||
icon.classList.add("loading");
|
|
||||||
customApp(app);
|
|
||||||
} else if (icon.classList.contains("icon-delete")) {
|
|
||||||
// Remove app
|
|
||||||
icon.classList.remove("icon-delete");
|
|
||||||
icon.classList.add("loading");
|
|
||||||
removeApp(app);
|
|
||||||
} else if (icon.classList.contains("icon-refresh")) {
|
|
||||||
// Update app
|
|
||||||
icon.classList.remove("icon-refresh");
|
|
||||||
icon.classList.add("loading");
|
|
||||||
updateApp(app);
|
|
||||||
} else if (icon.classList.contains("icon-download")) {
|
|
||||||
handleAppInterface(app);
|
|
||||||
} else if ( button.innerText == String.fromCharCode(0x2661)) {
|
|
||||||
changeAppFavourite(true, app);
|
|
||||||
} else if ( button.innerText == String.fromCharCode(0x2665) ) {
|
|
||||||
changeAppFavourite(false, app);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshFilter();
|
|
||||||
refreshLibrary();
|
|
||||||
// =========================================== My Apps
|
|
||||||
|
|
||||||
function uploadApp(app) {
|
|
||||||
return getInstalledApps().then(()=>{
|
|
||||||
if (appsInstalled.some(i => i.id === app.id)) {
|
|
||||||
return updateApp(app);
|
|
||||||
}
|
|
||||||
checkDependencies(app)
|
|
||||||
.then(()=>Comms.uploadApp(app))
|
|
||||||
.then((appJSON) => {
|
|
||||||
Progress.hide({ sticky: true });
|
|
||||||
if (appJSON) {
|
|
||||||
appsInstalled.push(appJSON);
|
|
||||||
}
|
|
||||||
showToast(app.name + ' Uploaded!', 'success');
|
|
||||||
}).catch(err => {
|
|
||||||
Progress.hide({ sticky: true });
|
|
||||||
showToast('Upload failed, ' + err, 'error');
|
|
||||||
}).finally(()=>{
|
|
||||||
refreshMyApps();
|
|
||||||
refreshLibrary();
|
|
||||||
});
|
|
||||||
}).catch(err => {
|
|
||||||
showToast("Device connection failed, "+err,"error");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeApp(app) {
|
|
||||||
return showPrompt("Delete","Really remove '"+app.name+"'?").then(() => {
|
|
||||||
return getInstalledApps().then(()=>{
|
|
||||||
// a = from appid.info, app = from apps.json
|
|
||||||
return Comms.removeApp(appsInstalled.find(a => a.id === app.id));
|
|
||||||
});
|
|
||||||
}).then(()=>{
|
|
||||||
appsInstalled = appsInstalled.filter(a=>a.id!=app.id);
|
|
||||||
showToast(app.name+" removed successfully","success");
|
|
||||||
refreshMyApps();
|
|
||||||
refreshLibrary();
|
|
||||||
}, err=>{
|
|
||||||
showToast(app.name+" removal failed, "+err,"error");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function customApp(app) {
|
|
||||||
return handleCustomApp(app).then((appJSON) => {
|
|
||||||
if (appJSON) appsInstalled.push(appJSON);
|
|
||||||
showToast(app.name+" Uploaded!", "success");
|
|
||||||
refreshMyApps();
|
|
||||||
refreshLibrary();
|
|
||||||
}).catch(err => {
|
|
||||||
showToast("Customise failed, "+err, "error");
|
|
||||||
refreshMyApps();
|
|
||||||
refreshLibrary();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// check for dependencies the app needs and install them if required
|
|
||||||
function checkDependencies(app, uploadOptions) {
|
|
||||||
let promise = Promise.resolve();
|
|
||||||
if (app.dependencies) {
|
|
||||||
Object.keys(app.dependencies).forEach(dependency=>{
|
|
||||||
if (app.dependencies[dependency]!="type")
|
|
||||||
throw new Error("Only supporting dependencies on app types right now");
|
|
||||||
console.log(`Searching for dependency on app type '${dependency}'`);
|
|
||||||
let found = appsInstalled.find(app=>app.type==dependency);
|
|
||||||
if (found)
|
|
||||||
console.log(`Found dependency in installed app '${found.id}'`);
|
|
||||||
else {
|
|
||||||
let foundApps = appJSON.filter(app=>app.type==dependency);
|
|
||||||
if (!foundApps.length) throw new Error(`Dependency of '${dependency}' listed, but nothing satisfies it!`);
|
|
||||||
console.log(`Apps ${foundApps.map(f=>`'${f.id}'`).join("/")} implement '${dependency}'`);
|
|
||||||
found = foundApps[0]; // choose first app in list
|
|
||||||
console.log(`Dependency not installed. Installing app id '${found.id}'`);
|
|
||||||
promise = promise.then(()=>new Promise((resolve,reject)=>{
|
|
||||||
console.log(`Install dependency '${dependency}':'${found.id}'`);
|
|
||||||
return Comms.uploadApp(found).then(appJSON => {
|
|
||||||
if (appJSON) appsInstalled.push(appJSON);
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateApp(app) {
|
|
||||||
if (app.custom) return customApp(app);
|
|
||||||
return getInstalledApps().then(() => {
|
|
||||||
// a = from appid.info, app = from apps.json
|
|
||||||
let remove = appsInstalled.find(a => a.id === app.id);
|
|
||||||
if (remove.files===undefined) remove.files="";
|
|
||||||
// no need to remove files which will be overwritten anyway
|
|
||||||
remove.files = remove.files.split(',')
|
|
||||||
.filter(f => f !== app.id + '.info')
|
|
||||||
.filter(f => !app.storage.some(s => s.name === f))
|
|
||||||
.join(',');
|
|
||||||
let data = AppInfo.parseDataString(remove.data)
|
|
||||||
if ('data' in app) {
|
|
||||||
// only remove data files which are no longer declared in new app version
|
|
||||||
const removeData = (f) => !app.data.some(d => (d.name || d.wildcard)===f)
|
|
||||||
data.dataFiles = data.dataFiles.filter(removeData)
|
|
||||||
data.storageFiles = data.storageFiles.filter(removeData)
|
|
||||||
}
|
|
||||||
remove.data = AppInfo.makeDataString(data)
|
|
||||||
return Comms.removeApp(remove);
|
|
||||||
}).then(()=>{
|
|
||||||
showToast(`Updating ${app.name}...`);
|
|
||||||
appsInstalled = appsInstalled.filter(a=>a.id!=app.id);
|
|
||||||
return checkDependencies(app);
|
|
||||||
}).then(()=>Comms.uploadApp(app)
|
|
||||||
).then((appJSON) => {
|
|
||||||
if (appJSON) appsInstalled.push(appJSON);
|
|
||||||
showToast(app.name+" Updated!", "success");
|
|
||||||
refreshMyApps();
|
|
||||||
refreshLibrary();
|
|
||||||
}, err=>{
|
|
||||||
showToast(app.name+" update failed, "+err,"error");
|
|
||||||
refreshMyApps();
|
|
||||||
refreshLibrary();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function appNameToApp(appName) {
|
|
||||||
let app = appJSON.find(app=>app.id==appName);
|
|
||||||
if (app) return app;
|
|
||||||
/* If app not known, add just one file
|
|
||||||
which is the JSON - so we'll remove it from
|
|
||||||
the menu but may not get rid of all files. */
|
|
||||||
return { id: appName,
|
|
||||||
name: "Unknown app "+appName,
|
|
||||||
icon: "../unknown.png",
|
|
||||||
description: "Unknown app",
|
|
||||||
storage: [ {name:appName+".info"}],
|
|
||||||
unknown: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function showLoadingIndicator(id) {
|
|
||||||
let panelbody = document.querySelector(`#${id} .panel-body`);
|
|
||||||
let tab = document.querySelector(`#tab-${id} a`);
|
|
||||||
// set badge up top
|
|
||||||
tab.classList.add("badge");
|
|
||||||
tab.setAttribute("data-badge", "");
|
|
||||||
// Loading indicator
|
|
||||||
panelbody.innerHTML = '<div class="tile column col-12"><div class="tile-content" style="min-height:48px;"><div class="loading loading-lg"></div></div></div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAppsToUpdate() {
|
|
||||||
let appsToUpdate = [];
|
|
||||||
appsInstalled.forEach(appInstalled => {
|
|
||||||
let app = appNameToApp(appInstalled.id);
|
|
||||||
if (app.version != appInstalled.version)
|
|
||||||
appsToUpdate.push(app);
|
|
||||||
});
|
|
||||||
return appsToUpdate;
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshMyApps() {
|
|
||||||
let panelbody = document.querySelector("#myappscontainer .panel-body");
|
|
||||||
panelbody.innerHTML = appsInstalled.map(appInstalled => {
|
|
||||||
let app = appNameToApp(appInstalled.id);
|
|
||||||
let version = getVersionInfo(app, appInstalled);
|
|
||||||
let githubURL = `${APP_SOURCECODE_URL}/${app.id}`;
|
|
||||||
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.id}/${app.icon}`:"unknown.png"}" alt="${escapeHtml(app.name)}"></figure>
|
|
||||||
</div>
|
|
||||||
<div class="tile-content">
|
|
||||||
<p class="tile-title text-bold">${escapeHtml(app.name)} <small>(${version.text})</small></p>
|
|
||||||
<p class="tile-subtitle">${getAppDescription(app)}</p>
|
|
||||||
<a href="${githubURL}" target="_blank" class="link-github"><img src="core/img/github-icon-sml.png" alt="See the code on GitHub"/></a>
|
|
||||||
</div>
|
|
||||||
<div class="tile-action">
|
|
||||||
<button class="btn btn-link btn-action btn-lg ${(appInstalled&&app.interface)?"":"d-hide"}" appid="${app.id}" title="Download data from app"><i class="icon icon-download"></i></button>
|
|
||||||
<button class="btn btn-link btn-action btn-lg ${version.canUpdate?'':'d-hide'}" appid="${app.id}" title="Update App"><i class="icon icon-refresh"></i></button>
|
|
||||||
<button class="btn btn-link btn-action btn-lg" appid="${app.id}" title="Remove App"><i class="icon icon-delete"></i></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`}).join("");
|
|
||||||
htmlToArray(panelbody.getElementsByTagName("button")).forEach(button => {
|
|
||||||
button.addEventListener("click",event => {
|
|
||||||
let button = event.currentTarget;
|
|
||||||
let icon = button.firstChild;
|
|
||||||
let appid = button.getAttribute("appid");
|
|
||||||
let app = appNameToApp(appid);
|
|
||||||
if (!app) throw new Error("App "+appid+" not found");
|
|
||||||
// check icon to figure out what we should do
|
|
||||||
if (icon.classList.contains("icon-delete")) removeApp(app);
|
|
||||||
if (icon.classList.contains("icon-refresh")) updateApp(app);
|
|
||||||
if (icon.classList.contains("icon-download")) handleAppInterface(app);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
let appsToUpdate = getAppsToUpdate();
|
|
||||||
let tab = document.querySelector("#tab-myappscontainer a");
|
|
||||||
let updateApps = document.querySelector("#myappscontainer .updateapps");
|
|
||||||
if (appsToUpdate.length) {
|
|
||||||
updateApps.innerHTML = `Update ${appsToUpdate.length} apps`;
|
|
||||||
updateApps.classList.remove("hidden");
|
|
||||||
tab.setAttribute("data-badge", `${appsInstalled.length} ⬆${appsToUpdate.length}`);
|
|
||||||
} else {
|
|
||||||
updateApps.classList.add("hidden");
|
|
||||||
tab.setAttribute("data-badge", appsInstalled.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let haveInstalledApps = false;
|
|
||||||
function getInstalledApps(refresh) {
|
|
||||||
if (haveInstalledApps && !refresh) {
|
|
||||||
return Promise.resolve(appsInstalled);
|
|
||||||
}
|
|
||||||
showLoadingIndicator("myappscontainer");
|
|
||||||
// Get apps and files
|
|
||||||
return Comms.getInstalledApps()
|
|
||||||
.then(appJSON => {
|
|
||||||
appsInstalled = appJSON;
|
|
||||||
haveInstalledApps = true;
|
|
||||||
refreshMyApps();
|
|
||||||
refreshLibrary();
|
|
||||||
})
|
|
||||||
.then(() => handleConnectionChange(true))
|
|
||||||
.then(() => appsInstalled)
|
|
||||||
.catch(err=>{
|
|
||||||
return Promise.reject();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Removes everything and install the given apps, eg: installMultipleApps(["boot","mclock"], "minimal")
|
|
||||||
function installMultipleApps(appIds, promptName) {
|
|
||||||
let apps = appIds.map( appid => appJSON.find(app=>app.id==appid) );
|
|
||||||
if (apps.some(x=>x===undefined))
|
|
||||||
return Promise.reject("Not all apps found");
|
|
||||||
let appCount = apps.length;
|
|
||||||
return showPrompt("Install Defaults",`Remove everything and install ${promptName} apps?`).then(() => {
|
|
||||||
return Comms.removeAllApps();
|
|
||||||
}).then(()=>{
|
|
||||||
Progress.hide({sticky:true});
|
|
||||||
appsInstalled = [];
|
|
||||||
showToast(`Existing apps removed. Installing ${appCount} apps...`);
|
|
||||||
return new Promise((resolve,reject) => {
|
|
||||||
function upload() {
|
|
||||||
let app = apps.shift();
|
|
||||||
if (app===undefined) return resolve();
|
|
||||||
Progress.show({title:`${app.name} (${appCount-apps.length}/${appCount})`,sticky:true});
|
|
||||||
checkDependencies(app,"skip_reset")
|
|
||||||
.then(()=>Comms.uploadApp(app,"skip_reset"))
|
|
||||||
.then((appJSON) => {
|
|
||||||
Progress.hide({sticky:true});
|
|
||||||
if (appJSON) appsInstalled.push(appJSON);
|
|
||||||
showToast(`(${appCount-apps.length}/${appCount}) ${app.name} Uploaded`);
|
|
||||||
upload();
|
|
||||||
}).catch(function() {
|
|
||||||
Progress.hide({sticky:true});
|
|
||||||
reject();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
upload();
|
|
||||||
});
|
|
||||||
}).then(()=>{
|
|
||||||
return Comms.setTime();
|
|
||||||
}).then(()=>{
|
|
||||||
showToast("Apps successfully installed!","success");
|
|
||||||
return getInstalledApps(true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let connectMyDeviceBtn = document.getElementById("connectmydevice");
|
|
||||||
|
|
||||||
function handleConnectionChange(connected) {
|
|
||||||
connectMyDeviceBtn.textContent = connected ? 'Disconnect' : 'Connect';
|
|
||||||
connectMyDeviceBtn.classList.toggle('is-connected', connected);
|
|
||||||
}
|
|
||||||
|
|
||||||
htmlToArray(document.querySelectorAll(".btn.refresh")).map(button => button.addEventListener("click", () => {
|
|
||||||
getInstalledApps(true).catch(err => {
|
|
||||||
showToast("Getting app list failed, "+err,"error");
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
htmlToArray(document.querySelectorAll(".btn.updateapps")).map(button => button.addEventListener("click", () => {
|
|
||||||
let appsToUpdate = getAppsToUpdate();
|
|
||||||
let count = appsToUpdate.length;
|
|
||||||
function updater() {
|
|
||||||
if (!appsToUpdate.length) return;
|
|
||||||
let app = appsToUpdate.pop();
|
|
||||||
return updateApp(app).then(function() {
|
|
||||||
return updater();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
updater().then(err => {
|
|
||||||
showToast(`Updated ${count} apps`,"success");
|
|
||||||
}).catch(err => {
|
|
||||||
showToast("Update failed, "+err,"error");
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
connectMyDeviceBtn.addEventListener("click", () => {
|
|
||||||
if (connectMyDeviceBtn.classList.contains('is-connected')) {
|
|
||||||
Comms.disconnectDevice();
|
|
||||||
} else {
|
|
||||||
getInstalledApps(true).catch(err => {
|
|
||||||
showToast("Device connection failed, "+err,"error");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Comms.watchConnectionChange(handleConnectionChange);
|
|
||||||
|
|
||||||
let filtersContainer = document.querySelector("#librarycontainer .filter-nav");
|
|
||||||
filtersContainer.addEventListener('click', ({ target }) => {
|
|
||||||
if (target.classList.contains('active')) return;
|
|
||||||
|
|
||||||
activeFilter = target.getAttribute('filterid') || '';
|
|
||||||
refreshFilter();
|
|
||||||
refreshLibrary();
|
|
||||||
window.location.hash = activeFilter;
|
|
||||||
});
|
|
||||||
|
|
||||||
let librarySearchInput = document.querySelector("#searchform input");
|
|
||||||
librarySearchInput.value = currentSearch;
|
|
||||||
librarySearchInput.addEventListener('input', evt => {
|
|
||||||
currentSearch = evt.target.value.toLowerCase();
|
|
||||||
window.location.hash = "#"+encodeURIComponent(currentSearch);
|
|
||||||
refreshLibrary();
|
|
||||||
});
|
|
||||||
|
|
||||||
let sortContainer = document.querySelector("#librarycontainer .sort-nav");
|
|
||||||
sortContainer.addEventListener('click', ({ target }) => {
|
|
||||||
if (target.classList.contains('active')) return;
|
|
||||||
|
|
||||||
activeSort = target.getAttribute('sortid') || '';
|
|
||||||
refreshSort();
|
|
||||||
refreshLibrary();
|
|
||||||
window.location.hash = activeFilter;
|
|
||||||
});
|
|
||||||
|
|
||||||
// =========================================== About
|
|
||||||
|
|
||||||
// Settings
|
|
||||||
let SETTINGS_HOOKS = {}; // stuff to get called when a setting is loaded
|
|
||||||
/// Load settings and update controls
|
|
||||||
function loadSettings() {
|
|
||||||
let j = localStorage.getItem("settings");
|
|
||||||
if (typeof j != "string") return;
|
|
||||||
try {
|
|
||||||
let s = JSON.parse(j);
|
|
||||||
Object.keys(s).forEach( k => {
|
|
||||||
SETTINGS[k]=s[k];
|
|
||||||
if (SETTINGS_HOOKS[k]) SETTINGS_HOOKS[k]();
|
|
||||||
} );
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Invalid settings");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Save settings
|
|
||||||
function saveSettings() {
|
|
||||||
localStorage.setItem("settings", JSON.stringify(SETTINGS));
|
|
||||||
console.log("Changed settings", SETTINGS);
|
|
||||||
}
|
|
||||||
// Link in settings DOM elements
|
|
||||||
function settingsCheckbox(id, name) {
|
|
||||||
let setting = document.getElementById(id);
|
|
||||||
function update() {
|
|
||||||
setting.checked = SETTINGS[name];
|
|
||||||
}
|
|
||||||
SETTINGS_HOOKS[name] = update;
|
|
||||||
setting.addEventListener('click', function() {
|
|
||||||
SETTINGS[name] = setting.checked;
|
|
||||||
saveSettings();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
settingsCheckbox("settings-pretokenise", "pretokenise");
|
|
||||||
loadSettings();
|
|
||||||
|
|
||||||
document.getElementById("defaultsettings").addEventListener("click",event=>{
|
|
||||||
SETTINGS = JSON.parse(JSON.stringify(DEFAULTSETTINGS)); // clone
|
|
||||||
saveSettings();
|
|
||||||
loadSettings(); // update all settings
|
|
||||||
refreshLibrary(); // favourites were in settings
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById("settime").addEventListener("click",event=>{
|
|
||||||
Comms.setTime().then(()=>{
|
|
||||||
showToast("Time set successfully","success");
|
|
||||||
}, err=>{
|
|
||||||
showToast("Error setting time, "+err,"error");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
document.getElementById("removeall").addEventListener("click",event=>{
|
|
||||||
showPrompt("Remove All","Really remove all apps?").then(() => {
|
|
||||||
return Comms.removeAllApps();
|
|
||||||
}).then(()=>{
|
|
||||||
Progress.hide({sticky:true});
|
|
||||||
appsInstalled = [];
|
|
||||||
showToast("All apps removed","success");
|
|
||||||
return getInstalledApps(true);
|
|
||||||
}).catch(err=>{
|
|
||||||
Progress.hide({sticky:true});
|
|
||||||
showToast("App removal failed, "+err,"error");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
// Install all default apps in one go
|
|
||||||
document.getElementById("installdefault").addEventListener("click",event=>{
|
|
||||||
httpGet("defaultapps.json").then(json=>{
|
|
||||||
return installMultipleApps(JSON.parse(json), "default");
|
|
||||||
}).catch(err=>{
|
|
||||||
Progress.hide({sticky:true});
|
|
||||||
showToast("App Install failed, "+err,"error");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Install all favourite apps in one go
|
|
||||||
document.getElementById("installfavourite").addEventListener("click",event=>{
|
|
||||||
let favApps = SETTINGS.favourites;
|
|
||||||
installMultipleApps(favApps, "favourite").catch(err=>{
|
|
||||||
Progress.hide({sticky:true});
|
|
||||||
showToast("App Install failed, "+err,"error");
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,53 +0,0 @@
|
||||||
const divInstall = document.getElementById('installContainer');
|
|
||||||
const butInstall = document.getElementById('butInstall');
|
|
||||||
|
|
||||||
window.addEventListener('beforeinstallprompt', (event) => {
|
|
||||||
console.log('👍', 'beforeinstallprompt', event);
|
|
||||||
// Stash the event so it can be triggered later.
|
|
||||||
window.deferredPrompt = event;
|
|
||||||
// Remove the 'hidden' class from the install button container
|
|
||||||
divInstall.classList.toggle('hidden', false);
|
|
||||||
});
|
|
||||||
|
|
||||||
butInstall.addEventListener('click', () => {
|
|
||||||
console.log('👍', 'butInstall-clicked');
|
|
||||||
const promptEvent = window.deferredPrompt;
|
|
||||||
if (!promptEvent) {
|
|
||||||
// The deferred prompt isn't available.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Show the install prompt.
|
|
||||||
promptEvent.prompt();
|
|
||||||
// Log the result
|
|
||||||
promptEvent.userChoice.then((result) => {
|
|
||||||
console.log('👍', 'userChoice', result);
|
|
||||||
// Reset the deferred prompt variable, since
|
|
||||||
// prompt() can only be called once.
|
|
||||||
window.deferredPrompt = null;
|
|
||||||
// Hide the install button.
|
|
||||||
divInstall.classList.toggle('hidden', true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener('appinstalled', (event) => {
|
|
||||||
console.log('👍', 'appinstalled', event);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
/* Only register a service worker if it's supported */
|
|
||||||
if ('serviceWorker' in navigator) {
|
|
||||||
navigator.serviceWorker.register('js/service-worker.js');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Warn the page must be served over HTTPS
|
|
||||||
* The `beforeinstallprompt` event won't fire if the page is served over HTTP.
|
|
||||||
* Installability requires a service worker with a fetch event handler, and
|
|
||||||
* if the page isn't served over HTTPS, the service worker won't load.
|
|
||||||
*/
|
|
||||||
if (window.location.protocol === 'http:' && window.location.hostname!="localhost") {
|
|
||||||
const requireHTTPS = document.getElementById('requireHTTPS');
|
|
||||||
const link = requireHTTPS.querySelector('a');
|
|
||||||
link.href = window.location.href.replace('http://', 'https://');
|
|
||||||
requireHTTPS.classList.remove('hidden');
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
self.addEventListener('install', (event) => {
|
|
||||||
console.log('👷', 'install', event);
|
|
||||||
self.skipWaiting();
|
|
||||||
});
|
|
||||||
|
|
||||||
self.addEventListener('activate', (event) => {
|
|
||||||
console.log('👷', 'activate', event);
|
|
||||||
return self.clients.claim();
|
|
||||||
});
|
|
||||||
|
|
||||||
self.addEventListener('fetch', function(event) {
|
|
||||||
// console.log('👷', 'fetch', event);
|
|
||||||
event.respondWith(fetch(event.request));
|
|
||||||
});
|
|
144
core/js/ui.js
144
core/js/ui.js
|
@ -1,144 +0,0 @@
|
||||||
// General UI tools (progress bar, toast, prompt)
|
|
||||||
|
|
||||||
/// Handle progress bars
|
|
||||||
const Progress = {
|
|
||||||
domElement : null, // the DOM element
|
|
||||||
sticky : false, // Progress.show({..., sticky:true}) don't remove until Progress.hide({sticky:true})
|
|
||||||
interval : undefined, // the interval used if Progress.show({percent:"animate"})
|
|
||||||
percent : undefined, // the current progress percentage
|
|
||||||
min : 0, // scaling for percentage
|
|
||||||
max : 1, // scaling for percentage
|
|
||||||
|
|
||||||
/* Show a Progress message
|
|
||||||
Progress.show({
|
|
||||||
sticky : bool // keep showing text even when Progress.hide is called (unless Progress.hide({sticky:true}))
|
|
||||||
percent : number | "animate"
|
|
||||||
min : // minimum scale for percentage (default 0)
|
|
||||||
max : // maximum scale for percentage (default 1)
|
|
||||||
}) */
|
|
||||||
show : function(options) {
|
|
||||||
options = options||{};
|
|
||||||
let text = options.title;
|
|
||||||
if (options.sticky) Progress.sticky = true;
|
|
||||||
if (options.min!==undefined) Progress.min = options.min;
|
|
||||||
if (options.max!==undefined) Progress.max = options.max;
|
|
||||||
let percent = options.percent;
|
|
||||||
if (percent!==undefined)
|
|
||||||
percent = Progress.min*100 + (Progress.max-Progress.min)*percent;
|
|
||||||
if (!Progress.domElement) {
|
|
||||||
if (Progress.interval) {
|
|
||||||
clearInterval(Progress.interval);
|
|
||||||
Progress.interval = undefined;
|
|
||||||
}
|
|
||||||
if (options.percent == "animate") {
|
|
||||||
Progress.interval = setInterval(function() {
|
|
||||||
Progress.percent += 2;
|
|
||||||
if (Progress.percent>100) Progress.percent=0;
|
|
||||||
Progress.show({percent:Progress.percent});
|
|
||||||
}, 100);
|
|
||||||
Progress.percent = percent = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let toastcontainer = document.getElementById("toastcontainer");
|
|
||||||
Progress.domElement = htmlElement(`<div class="toast">
|
|
||||||
${text ? `<div>${text}</div>`:``}
|
|
||||||
<div class="bar bar-sm">
|
|
||||||
<div class="bar-item" id="Progress.domElement" role="progressbar" style="width:${percent}%;" aria-valuenow="${percent}" aria-valuemin="0" aria-valuemax="100"></div>
|
|
||||||
</div>
|
|
||||||
</div>`);
|
|
||||||
toastcontainer.append(Progress.domElement);
|
|
||||||
} else {
|
|
||||||
let pt=document.getElementById("Progress.domElement");
|
|
||||||
pt.setAttribute("aria-valuenow",percent);
|
|
||||||
pt.style.width = percent+"%";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// Progress.hide({sticky:true}) undoes Progress.show({title:"title", sticky:true})
|
|
||||||
hide : function(options) {
|
|
||||||
options = options||{};
|
|
||||||
if (Progress.sticky && !options.sticky)
|
|
||||||
return;
|
|
||||||
Progress.sticky = false;
|
|
||||||
Progress.min = 0;
|
|
||||||
Progress.max = 1;
|
|
||||||
if (Progress.interval) {
|
|
||||||
clearInterval(Progress.interval);
|
|
||||||
Progress.interval = undefined;
|
|
||||||
}
|
|
||||||
if (Progress.domElement) Progress.domElement.remove();
|
|
||||||
Progress.domElement = undefined;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Add progress handler so we get nice uploads
|
|
||||||
Puck.writeProgress = function(charsSent, charsTotal) {
|
|
||||||
if (charsSent===undefined) {
|
|
||||||
Progress.hide();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let percent = Math.round(charsSent*100/charsTotal);
|
|
||||||
Progress.show({percent: percent});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Show a 'toast' message for status
|
|
||||||
function showToast(message, type) {
|
|
||||||
// toast-primary, toast-success, toast-warning or toast-error
|
|
||||||
console.log("<TOAST>["+(type||"-")+"] "+message);
|
|
||||||
let style = "toast-primary";
|
|
||||||
if (type=="success") style = "toast-success";
|
|
||||||
else if (type=="error") style = "toast-error";
|
|
||||||
else if (type=="warning") style = "toast-warning";
|
|
||||||
else if (type!==undefined) console.log("showToast: unknown toast "+type);
|
|
||||||
let toastcontainer = document.getElementById("toastcontainer");
|
|
||||||
let msgDiv = htmlElement(`<div class="toast ${style}"></div>`);
|
|
||||||
msgDiv.innerHTML = message;
|
|
||||||
toastcontainer.append(msgDiv);
|
|
||||||
setTimeout(function() {
|
|
||||||
msgDiv.remove();
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Show a yes/no prompt
|
|
||||||
function showPrompt(title, text, buttons, shouldEscapeHtml) {
|
|
||||||
if (!buttons) buttons={yes:1,no:1};
|
|
||||||
if (typeof(shouldEscapeHtml) === 'undefined' || shouldEscapeHtml === null) shouldEscapeHtml = true;
|
|
||||||
|
|
||||||
return new Promise((resolve,reject) => {
|
|
||||||
let 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">
|
|
||||||
${(shouldEscapeHtml) ? escapeHtml(text).replace(/\n/g,'<br/>') : text}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<div class="modal-footer">
|
|
||||||
${buttons.yes?'<button class="btn btn-primary" isyes="1">Yes</button>':''}
|
|
||||||
${buttons.no?'<button class="btn" isyes="0">No</button>':''}
|
|
||||||
${buttons.ok?'<button class="btn" isyes="1">Ok</button>':''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>`);
|
|
||||||
document.body.append(modal);
|
|
||||||
modal.querySelector("a[href='#close']").addEventListener("click",event => {
|
|
||||||
event.preventDefault();
|
|
||||||
reject("User cancelled");
|
|
||||||
modal.remove();
|
|
||||||
});
|
|
||||||
htmlToArray(modal.getElementsByTagName("button")).forEach(button => {
|
|
||||||
button.addEventListener("click",event => {
|
|
||||||
event.preventDefault();
|
|
||||||
let isYes = event.target.getAttribute("isyes")=="1";
|
|
||||||
if (isYes) resolve();
|
|
||||||
else reject("User cancelled");
|
|
||||||
modal.remove();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
103
core/js/utils.js
103
core/js/utils.js
|
@ -1,103 +0,0 @@
|
||||||
function escapeHtml(text) {
|
|
||||||
let map = {
|
|
||||||
'&': '&',
|
|
||||||
'<': '<',
|
|
||||||
'>': '>',
|
|
||||||
'"': '"',
|
|
||||||
"'": '''
|
|
||||||
};
|
|
||||||
return text.replace(/[&<>"']/g, function(m) { return map[m]; });
|
|
||||||
}
|
|
||||||
// simple glob to regex conversion, only supports "*" and "?" wildcards
|
|
||||||
function globToRegex(pattern) {
|
|
||||||
const ESCAPE = '.*+-?^${}()|[]\\';
|
|
||||||
const regex = pattern.replace(/./g, c => {
|
|
||||||
switch (c) {
|
|
||||||
case '?': return '.';
|
|
||||||
case '*': return '.*';
|
|
||||||
default: return ESCAPE.includes(c) ? ('\\' + c) : c;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return new RegExp('^'+regex+'$');
|
|
||||||
}
|
|
||||||
function htmlToArray(collection) {
|
|
||||||
return [].slice.call(collection);
|
|
||||||
}
|
|
||||||
function htmlElement(str) {
|
|
||||||
let div = document.createElement('div');
|
|
||||||
div.innerHTML = str.trim();
|
|
||||||
return div.firstChild;
|
|
||||||
}
|
|
||||||
function httpGet(url) {
|
|
||||||
let isBinary = !(url.endsWith(".js") || url.endsWith(".json") || url.endsWith(".csv") || url.endsWith(".txt"));
|
|
||||||
return new Promise((resolve,reject) => {
|
|
||||||
let oReq = new XMLHttpRequest();
|
|
||||||
oReq.addEventListener("load", () => {
|
|
||||||
if (oReq.status!=200) {
|
|
||||||
resolve(oReq.status+" - "+oReq.statusText)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!isBinary) {
|
|
||||||
resolve(oReq.responseText)
|
|
||||||
} else {
|
|
||||||
// ensure we actually load the data as a raw 8 bit string (not utf-8/etc)
|
|
||||||
let a = new FileReader();
|
|
||||||
a.onloadend = function() {
|
|
||||||
let bytes = new Uint8Array(a.result);
|
|
||||||
let str = "";
|
|
||||||
for (let i=0;i<bytes.length;i++)
|
|
||||||
str += String.fromCharCode(bytes[i]);
|
|
||||||
resolve(str)
|
|
||||||
};
|
|
||||||
a.readAsArrayBuffer(oReq.response);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
oReq.addEventListener("error", () => reject());
|
|
||||||
oReq.addEventListener("abort", () => reject());
|
|
||||||
oReq.open("GET", url, true);
|
|
||||||
oReq.onerror = function () {
|
|
||||||
reject("HTTP Request failed");
|
|
||||||
};
|
|
||||||
if (isBinary)
|
|
||||||
oReq.responseType = 'blob';
|
|
||||||
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;
|
|
||||||
let sa = 0|a.sortorder;
|
|
||||||
let sb = 0|b.sortorder;
|
|
||||||
if (sa<sb) return -1;
|
|
||||||
if (sa>sb) return 1;
|
|
||||||
return (a.name==b.name) ? 0 : ((a.name<b.name) ? -1 : 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Given 2 JSON structures (1st from apps.json, 2nd from an installed app)
|
|
||||||
work out what to display re: versions and if we can update */
|
|
||||||
function getVersionInfo(appListing, appInstalled) {
|
|
||||||
let versionText = "";
|
|
||||||
let canUpdate = false;
|
|
||||||
function clicky(v) {
|
|
||||||
return `<a class="c-hand" onclick="showChangeLog('${appListing.id}')">${v}</a>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!appInstalled) {
|
|
||||||
if (appListing.version)
|
|
||||||
versionText = clicky("v"+appListing.version);
|
|
||||||
} else {
|
|
||||||
versionText = (appInstalled.version ? (clicky("v"+appInstalled.version)) : "Unknown version");
|
|
||||||
if (appListing.version != appInstalled.version) {
|
|
||||||
if (appListing.version) versionText += ", latest "+clicky("v"+appListing.version);
|
|
||||||
canUpdate = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
text : versionText,
|
|
||||||
canUpdate : canUpdate
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
{
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": 6,
|
|
||||||
"sourceType": "script"
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
"indent": [
|
|
||||||
"warn",
|
|
||||||
2,
|
|
||||||
{
|
|
||||||
"SwitchCase": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"no-undef": "warn",
|
|
||||||
"no-redeclare": "warn",
|
|
||||||
"no-var": "warn",
|
|
||||||
"no-unused-vars":"off" // we define stuff to use in other scripts
|
|
||||||
},
|
|
||||||
"env": {
|
|
||||||
"browser": true
|
|
||||||
},
|
|
||||||
"extends": "eslint:recommended",
|
|
||||||
"globals": {
|
|
||||||
"onInit" : "readonly"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
/* Library for 'custom' HTML files that are to
|
|
||||||
be used from within BangleApps
|
|
||||||
|
|
||||||
See: README.md / `apps.json`: `custom` element
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* Call with a JS object:
|
|
||||||
|
|
||||||
sendCustomizedApp({
|
|
||||||
id : "7chname",
|
|
||||||
|
|
||||||
storage:[
|
|
||||||
{name:"-7chname", content:app_source_code},
|
|
||||||
{name:"+7chname", content:JSON.stringify({
|
|
||||||
name:"My app's name",
|
|
||||||
icon:"*7chname",
|
|
||||||
src:"-7chname"
|
|
||||||
})},
|
|
||||||
{name:"*7chname", content:'require("heatshrink").decompress(atob("mEwg...4"))', evaluate:true},
|
|
||||||
]
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
function sendCustomizedApp(app) {
|
|
||||||
window.postMessage(app);
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
@ -1,462 +0,0 @@
|
||||||
/* Copyright 2020 Gordon Williams, gw@pur3.co.uk
|
|
||||||
https://github.com/espruino/EspruinoWebTools
|
|
||||||
*/
|
|
||||||
(function (root, factory) {
|
|
||||||
if (typeof define === 'function' && define.amd) {
|
|
||||||
// AMD. Register as an anonymous module.
|
|
||||||
define(['b'], factory);
|
|
||||||
} else if (typeof module === 'object' && module.exports) {
|
|
||||||
// Node. Does not work with strict CommonJS, but
|
|
||||||
// only CommonJS-like environments that support module.exports,
|
|
||||||
// like Node.
|
|
||||||
module.exports = factory(require('b'));
|
|
||||||
} else {
|
|
||||||
// Browser globals (root is window)
|
|
||||||
root.imageconverter = factory(root.heatshrink);
|
|
||||||
}
|
|
||||||
}(typeof self !== 'undefined' ? self : this, function (heatshrink) {
|
|
||||||
|
|
||||||
const PALETTE = {
|
|
||||||
VGA: [0x000000, 0x0000a8, 0x00a800, 0x00a8a8, 0xa80000, 0xa800a8, 0xa85400, 0xa8a8a8, 0x545454, 0x5454fc, 0x54fc54, 0x54fcfc, 0xfc5454, 0xfc54fc, 0xfcfc54, 0xfcfcfc, 0x000000, 0x141414, 0x202020, 0x2c2c2c, 0x383838, 0x444444, 0x505050, 0x606060, 0x707070, 0x808080, 0x909090, 0xa0a0a0, 0xb4b4b4, 0xc8c8c8, 0xe0e0e0, 0xfcfcfc, 0x0000fc, 0x4000fc, 0x7c00fc, 0xbc00fc, 0xfc00fc, 0xfc00bc, 0xfc007c, 0xfc0040, 0xfc0000, 0xfc4000, 0xfc7c00, 0xfcbc00, 0xfcfc00, 0xbcfc00, 0x7cfc00, 0x40fc00, 0x00fc00, 0x00fc40, 0x00fc7c, 0x00fcbc, 0x00fcfc, 0x00bcfc, 0x007cfc, 0x0040fc, 0x7c7cfc, 0x9c7cfc, 0xbc7cfc, 0xdc7cfc, 0xfc7cfc, 0xfc7cdc, 0xfc7cbc, 0xfc7c9c, 0xfc7c7c, 0xfc9c7c, 0xfcbc7c, 0xfcdc7c, 0xfcfc7c, 0xdcfc7c, 0xbcfc7c, 0x9cfc7c, 0x7cfc7c, 0x7cfc9c, 0x7cfcbc, 0x7cfcdc, 0x7cfcfc, 0x7cdcfc, 0x7cbcfc, 0x7c9cfc, 0xb4b4fc, 0xc4b4fc, 0xd8b4fc, 0xe8b4fc, 0xfcb4fc, 0xfcb4e8, 0xfcb4d8, 0xfcb4c4, 0xfcb4b4, 0xfcc4b4, 0xfcd8b4, 0xfce8b4, 0xfcfcb4, 0xe8fcb4, 0xd8fcb4, 0xc4fcb4, 0xb4fcb4, 0xb4fcc4, 0xb4fcd8, 0xb4fce8, 0xb4fcfc, 0xb4e8fc, 0xb4d8fc, 0xb4c4fc, 0x000070, 0x1c0070, 0x380070, 0x540070, 0x700070, 0x700054, 0x700038, 0x70001c, 0x700000, 0x701c00, 0x703800, 0x705400, 0x707000, 0x547000, 0x387000, 0x1c7000, 0x007000, 0x00701c, 0x007038, 0x007054, 0x007070, 0x005470, 0x003870, 0x001c70, 0x383870, 0x443870, 0x543870, 0x603870, 0x703870, 0x703860, 0x703854, 0x703844, 0x703838, 0x704438, 0x705438, 0x706038, 0x707038, 0x607038, 0x547038, 0x447038, 0x387038, 0x387044, 0x387054, 0x387060, 0x387070, 0x386070, 0x385470, 0x384470, 0x505070, 0x585070, 0x605070, 0x685070, 0x705070, 0x705068, 0x705060, 0x705058, 0x705050, 0x705850, 0x706050, 0x706850, 0x707050, 0x687050, 0x607050, 0x587050, 0x507050, 0x507058, 0x507060, 0x507068, 0x507070, 0x506870, 0x506070, 0x505870, 0x000040, 0x100040, 0x200040, 0x300040, 0x400040, 0x400030, 0x400020, 0x400010, 0x400000, 0x401000, 0x402000, 0x403000, 0x404000, 0x304000, 0x204000, 0x104000, 0x004000, 0x004010, 0x004020, 0x004030, 0x004040, 0x003040, 0x002040, 0x001040, 0x202040, 0x282040, 0x302040, 0x382040, 0x402040, 0x402038, 0x402030, 0x402028, 0x402020, 0x402820, 0x403020, 0x403820, 0x404020, 0x384020, 0x304020, 0x284020, 0x204020, 0x204028, 0x204030, 0x204038, 0x204040, 0x203840, 0x203040, 0x202840, 0x2c2c40, 0x302c40, 0x342c40, 0x3c2c40, 0x402c40, 0x402c3c, 0x402c34, 0x402c30, 0x402c2c, 0x40302c, 0x40342c, 0x403c2c, 0x40402c, 0x3c402c, 0x34402c, 0x30402c, 0x2c402c, 0x2c4030, 0x2c4034, 0x2c403c, 0x2c4040, 0x2c3c40, 0x2c3440, 0x2c3040, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0xFFFFFF],
|
|
||||||
WEB : [0x000000,0x000033,0x000066,0x000099,0x0000cc,0x0000ff,0x003300,0x003333,0x003366,0x003399,0x0033cc,0x0033ff,0x006600,0x006633,0x006666,0x006699,0x0066cc,0x0066ff,0x009900,0x009933,0x009966,0x009999,0x0099cc,0x0099ff,0x00cc00,0x00cc33,0x00cc66,0x00cc99,0x00cccc,0x00ccff,0x00ff00,0x00ff33,0x00ff66,0x00ff99,0x00ffcc,0x00ffff,0x330000,0x330033,0x330066,0x330099,0x3300cc,0x3300ff,0x333300,0x333333,0x333366,0x333399,0x3333cc,0x3333ff,0x336600,0x336633,0x336666,0x336699,0x3366cc,0x3366ff,0x339900,0x339933,0x339966,0x339999,0x3399cc,0x3399ff,0x33cc00,0x33cc33,0x33cc66,0x33cc99,0x33cccc,0x33ccff,0x33ff00,0x33ff33,0x33ff66,0x33ff99,0x33ffcc,0x33ffff,0x660000,0x660033,0x660066,0x660099,0x6600cc,0x6600ff,0x663300,0x663333,0x663366,0x663399,0x6633cc,0x6633ff,0x666600,0x666633,0x666666,0x666699,0x6666cc,0x6666ff,0x669900,0x669933,0x669966,0x669999,0x6699cc,0x6699ff,0x66cc00,0x66cc33,0x66cc66,0x66cc99,0x66cccc,0x66ccff,0x66ff00,0x66ff33,0x66ff66,0x66ff99,0x66ffcc,0x66ffff,0x990000,0x990033,0x990066,0x990099,0x9900cc,0x9900ff,0x993300,0x993333,0x993366,0x993399,0x9933cc,0x9933ff,0x996600,0x996633,0x996666,0x996699,0x9966cc,0x9966ff,0x999900,0x999933,0x999966,0x999999,0x9999cc,0x9999ff,0x99cc00,0x99cc33,0x99cc66,0x99cc99,0x99cccc,0x99ccff,0x99ff00,0x99ff33,0x99ff66,0x99ff99,0x99ffcc,0x99ffff,0xcc0000,0xcc0033,0xcc0066,0xcc0099,0xcc00cc,0xcc00ff,0xcc3300,0xcc3333,0xcc3366,0xcc3399,0xcc33cc,0xcc33ff,0xcc6600,0xcc6633,0xcc6666,0xcc6699,0xcc66cc,0xcc66ff,0xcc9900,0xcc9933,0xcc9966,0xcc9999,0xcc99cc,0xcc99ff,0xcccc00,0xcccc33,0xcccc66,0xcccc99,0xcccccc,0xccccff,0xccff00,0xccff33,0xccff66,0xccff99,0xccffcc,0xccffff,0xff0000,0xff0033,0xff0066,0xff0099,0xff00cc,0xff00ff,0xff3300,0xff3333,0xff3366,0xff3399,0xff33cc,0xff33ff,0xff6600,0xff6633,0xff6666,0xff6699,0xff66cc,0xff66ff,0xff9900,0xff9933,0xff9966,0xff9999,0xff99cc,0xff99ff,0xffcc00,0xffcc33,0xffcc66,0xffcc99,0xffcccc,0xffccff,0xffff00,0xffff33,0xffff66,0xffff99,0xffffcc,0xffffff],
|
|
||||||
MAC16 : [
|
|
||||||
0x000000, 0x444444, 0x888888, 0xBBBBBB,
|
|
||||||
0x996633, 0x663300, 0x006600, 0x00aa00,
|
|
||||||
0x0099ff, 0x0000cc, 0x330099, 0xff0099,
|
|
||||||
0xdd0000, 0xff6600, 0xffff00, 0xffffff
|
|
||||||
],
|
|
||||||
lookup : function(palette,r,g,b,a, no_transparent) {
|
|
||||||
if (!no_transparent && a<128) return TRANSPARENT_8BIT;
|
|
||||||
var maxd = 0xFFFFFF;
|
|
||||||
var c = 0;
|
|
||||||
palette.forEach(function(p,n) {
|
|
||||||
var pr=p>>16;
|
|
||||||
var pg=(p>>8)&255;
|
|
||||||
var pb=p&255;
|
|
||||||
var dr = r-pr;
|
|
||||||
var dg = g-pg;
|
|
||||||
var db = b-pb;
|
|
||||||
var d = dr*dr + dg*dg + db*db;
|
|
||||||
if (d<maxd) {
|
|
||||||
c = n;
|
|
||||||
maxd=d;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
var TRANSPARENT_8BIT = 254;
|
|
||||||
|
|
||||||
|
|
||||||
var COL_BPP = {
|
|
||||||
"1bit":1,
|
|
||||||
"2bitbw":2,
|
|
||||||
"4bit":4,
|
|
||||||
"4bitmac":4,
|
|
||||||
"vga":8,
|
|
||||||
"web":8,
|
|
||||||
"rgb565":16
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
var COL_FROM_RGB = {
|
|
||||||
"1bit":function(r,g,b) {
|
|
||||||
var c = (r+g+b) / 3;
|
|
||||||
var thresh = 128;
|
|
||||||
return c>thresh;
|
|
||||||
},
|
|
||||||
"2bitbw":function(r,g,b) {
|
|
||||||
var c = (r+g+b) / 3;
|
|
||||||
c += 31; // rounding
|
|
||||||
if (c>255)c=255;
|
|
||||||
return c>>6;
|
|
||||||
},
|
|
||||||
"4bit":function(r,g,b,a) {
|
|
||||||
var thresh = 128;
|
|
||||||
return (
|
|
||||||
((r>thresh)?1:0) |
|
|
||||||
((g>thresh)?2:0) |
|
|
||||||
((b>thresh)?4:0) |
|
|
||||||
((a>thresh)?8:0));
|
|
||||||
},
|
|
||||||
"4bitmac":function(r,g,b,a) {
|
|
||||||
return PALETTE.lookup(PALETTE.MAC16,r,g,b,a, true /* no transparency */);
|
|
||||||
},
|
|
||||||
"vga":function(r,g,b,a) {
|
|
||||||
return PALETTE.lookup(PALETTE.VGA,r,g,b,a);
|
|
||||||
},
|
|
||||||
"web":function(r,g,b,a) {
|
|
||||||
return PALETTE.lookup(PALETTE.WEB,r,g,b,a);
|
|
||||||
},
|
|
||||||
"rgb565":function(r,g,b,a) {
|
|
||||||
return (
|
|
||||||
((r&0xF8)<<8) |
|
|
||||||
((g&0xFC)<<3) |
|
|
||||||
((b&0xF8)>>3));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
var COL_TO_RGB = {
|
|
||||||
"1bit":function(c) {
|
|
||||||
return c ? 0xFFFFFFFF : 0xFF000000;
|
|
||||||
},
|
|
||||||
"2bitbw":function(c) {
|
|
||||||
c = c&3;
|
|
||||||
c = c | (c<<2) | (c<<4) | (c<<6);
|
|
||||||
return 0xFF000000|(c<<16)|(c<<8)|c;
|
|
||||||
},
|
|
||||||
"4bit":function(c) {
|
|
||||||
if (!(c&8)) return 0;
|
|
||||||
return ((c&1 ? 0xFF0000 : 0xFF000000) |
|
|
||||||
(c&2 ? 0x00FF00 : 0xFF000000) |
|
|
||||||
(c&4 ? 0x0000FF : 0xFF000000));
|
|
||||||
},
|
|
||||||
"4bitmac":function(c) {
|
|
||||||
return 0xFF000000|PALETTE.MAC16[c];
|
|
||||||
},
|
|
||||||
"vga":function(c) {
|
|
||||||
if (c==TRANSPARENT_8BIT) return 0;
|
|
||||||
return 0xFF000000|PALETTE.VGA[c];
|
|
||||||
},
|
|
||||||
"web":function(c) {
|
|
||||||
if (c==TRANSPARENT_8BIT) return 0;
|
|
||||||
return 0xFF000000|PALETTE.WEB[c];
|
|
||||||
},
|
|
||||||
"rgb565":function(c) {
|
|
||||||
var r = (c>>8)&0xF8;
|
|
||||||
var g = (c>>3)&0xFC;
|
|
||||||
var b = (c<<3)&0xF8;
|
|
||||||
return 0xFF000000|(r<<16)|(g<<8)|b;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
// What Espruino uses by default
|
|
||||||
var BPP_TO_COLOR_FORMAT = {
|
|
||||||
1 : "1bit",
|
|
||||||
2 : "2bitbw",
|
|
||||||
4 : "4bitmac",
|
|
||||||
8 : "web",
|
|
||||||
16 : "rgb565"
|
|
||||||
};
|
|
||||||
|
|
||||||
function clip(x) {
|
|
||||||
if (x<0) return 0;
|
|
||||||
if (x>255) return 255;
|
|
||||||
return x;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
See 'getOptions' for possible options
|
|
||||||
*/
|
|
||||||
function RGBAtoString(rgba, options) {
|
|
||||||
options = options||{};
|
|
||||||
if (!rgba) throw new Error("No dataIn specified");
|
|
||||||
if (!options.width) throw new Error("No Width specified");
|
|
||||||
if (!options.height) throw new Error("No Height specified");
|
|
||||||
if ("string"!=typeof options.diffusion)
|
|
||||||
options.diffusion = "none";
|
|
||||||
options.compression = options.compression || false;
|
|
||||||
options.brightness = options.brightness | 0;
|
|
||||||
options.mode = options.mode || "1bit";
|
|
||||||
options.output = options.output || "object";
|
|
||||||
options.inverted = options.inverted || false;
|
|
||||||
options.transparent = !!options.transparent;
|
|
||||||
var transparentCol = undefined;
|
|
||||||
if (options.transparent) {
|
|
||||||
if (options.mode=="4bit")
|
|
||||||
transparentCol=0;
|
|
||||||
if (options.mode=="vga" || options.mode=="web")
|
|
||||||
transparentCol=TRANSPARENT_8BIT;
|
|
||||||
}
|
|
||||||
var bpp = COL_BPP[options.mode];
|
|
||||||
var bitData = new Uint8Array(((options.width*options.height)*bpp+7)/8);
|
|
||||||
|
|
||||||
function readImage() {
|
|
||||||
var pixels = new Int32Array(options.width*options.height);
|
|
||||||
var n = 0;
|
|
||||||
var er=0,eg=0,eb=0;
|
|
||||||
for (var y=0; y<options.height; y++) {
|
|
||||||
for (var x=0; x<options.width; x++) {
|
|
||||||
var r = rgba[n*4];
|
|
||||||
var g = rgba[n*4+1];
|
|
||||||
var b = rgba[n*4+2];
|
|
||||||
var a = rgba[n*4+3];
|
|
||||||
|
|
||||||
if (options.diffusion == "random1" ||
|
|
||||||
options.diffusion == "errorrandom") {
|
|
||||||
er += Math.random()*48 - 24;
|
|
||||||
eg += Math.random()*48 - 24;
|
|
||||||
eb += Math.random()*48 - 24;
|
|
||||||
} else if (options.diffusion == "random2") {
|
|
||||||
er += Math.random()*128 - 64;
|
|
||||||
eg += Math.random()*128 - 64;
|
|
||||||
eb += Math.random()*128 - 64;
|
|
||||||
}
|
|
||||||
if (options.inverted) {
|
|
||||||
r=255-r;
|
|
||||||
g=255-g;
|
|
||||||
b=255-b;
|
|
||||||
}
|
|
||||||
r = clip(r + options.brightness + er);
|
|
||||||
g = clip(g + options.brightness + eg);
|
|
||||||
b = clip(b + options.brightness + eb);
|
|
||||||
var isTransparent = a<128;
|
|
||||||
|
|
||||||
var c = COL_FROM_RGB[options.mode](r,g,b,a);
|
|
||||||
if (isTransparent && options.transparent && transparentCol===undefined) {
|
|
||||||
c = -1;
|
|
||||||
a = 0;
|
|
||||||
}
|
|
||||||
pixels[n] = c;
|
|
||||||
// error diffusion
|
|
||||||
var cr = COL_TO_RGB[options.mode](c);
|
|
||||||
var oa = cr>>>24;
|
|
||||||
var or = (cr>>16)&255;
|
|
||||||
var og = (cr>>8)&255;
|
|
||||||
var ob = cr&255;
|
|
||||||
if (options.diffusion.startsWith("error") && a>128) {
|
|
||||||
er = r-or;
|
|
||||||
eg = g-og;
|
|
||||||
eb = b-ob;
|
|
||||||
} else {
|
|
||||||
er = 0;
|
|
||||||
eg = 0;
|
|
||||||
eb = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
n++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pixels;
|
|
||||||
}
|
|
||||||
function writeImage(pixels) {
|
|
||||||
var n = 0;
|
|
||||||
for (var y=0; y<options.height; y++) {
|
|
||||||
for (var x=0; x<options.width; x++) {
|
|
||||||
var c = pixels[n];
|
|
||||||
// Write image data
|
|
||||||
if (bpp==1) bitData[n>>3] |= c ? 128>>(n&7) : 0;
|
|
||||||
else if (bpp==2) bitData[n>>2] |= c<<((3-(n&3))*2);
|
|
||||||
else if (bpp==4) bitData[n>>1] |= c<<((n&1)?0:4);
|
|
||||||
else if (bpp==8) bitData[n] = c;
|
|
||||||
else if (bpp==16) { bitData[n<<1] = c>>8; bitData[1+(n<<1)] = c&0xFF; }
|
|
||||||
else throw new Error("Unhandled BPP");
|
|
||||||
// Write preview
|
|
||||||
var cr = COL_TO_RGB[options.mode](c);
|
|
||||||
if (c===transparentCol)
|
|
||||||
cr = ((((x>>2)^(y>>2))&1)?0xFFFFFF:0); // pixel pattern
|
|
||||||
var oa = cr>>>24;
|
|
||||||
var or = (cr>>16)&255;
|
|
||||||
var og = (cr>>8)&255;
|
|
||||||
var ob = cr&255;
|
|
||||||
if (options.rgbaOut) {
|
|
||||||
options.rgbaOut[n*4] = or;
|
|
||||||
options.rgbaOut[n*4+1]= og;
|
|
||||||
options.rgbaOut[n*4+2]= ob;
|
|
||||||
options.rgbaOut[n*4+3]=255;
|
|
||||||
}
|
|
||||||
n++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var pixels = readImage();
|
|
||||||
if (options.transparent && transparentCol===undefined && bpp<=16) {
|
|
||||||
// we have no fixed transparent colour - pick one that's unused
|
|
||||||
var colors = new Uint32Array(1<<bpp);
|
|
||||||
// how many colours?
|
|
||||||
for (var i=0;i<pixels.length;i++)
|
|
||||||
if (pixels[i]>=0)
|
|
||||||
colors[pixels[i]]++;
|
|
||||||
// find an empty one
|
|
||||||
for (var i=0;i<colors.length;i++)
|
|
||||||
if (colors[i]==0) {
|
|
||||||
transparentCol = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (transparentCol===undefined) {
|
|
||||||
console.log("No unused colour found - using 0 for transparency");
|
|
||||||
for (var i=0;i<pixels.length;i++)
|
|
||||||
if (pixels[i]<0)
|
|
||||||
pixels[i]=0;
|
|
||||||
} else {
|
|
||||||
for (var i=0;i<pixels.length;i++)
|
|
||||||
if (pixels[i]<0)
|
|
||||||
pixels[i]=transparentCol;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
writeImage(pixels);
|
|
||||||
|
|
||||||
var strCmd;
|
|
||||||
if ((options.output=="string") || (options.output=="raw")) {
|
|
||||||
var transparent = transparentCol!==undefined;
|
|
||||||
var headerSize = transparent?4:3;
|
|
||||||
var imgData = new Uint8Array(bitData.length + headerSize);
|
|
||||||
imgData[0] = options.width;
|
|
||||||
imgData[1] = options.height;
|
|
||||||
imgData[2] = bpp + (transparent?128:0);
|
|
||||||
if (transparent) imgData[3] = transparentCol;
|
|
||||||
imgData.set(bitData, headerSize);
|
|
||||||
bitData = imgData;
|
|
||||||
}
|
|
||||||
if (options.compression) {
|
|
||||||
bitData = heatshrink.compress(bitData);
|
|
||||||
strCmd = 'require("heatshrink").decompress';
|
|
||||||
} else {
|
|
||||||
strCmd = 'E.toArrayBuffer';
|
|
||||||
}
|
|
||||||
var str = "";
|
|
||||||
for (n=0; n<bitData.length; n++)
|
|
||||||
str += String.fromCharCode(bitData[n]);
|
|
||||||
var imgstr;
|
|
||||||
if (options.output=="raw") {
|
|
||||||
imgstr = str;
|
|
||||||
} else if (options.output=="object") {
|
|
||||||
imgstr = "{\n";
|
|
||||||
imgstr += " width : "+options.width+", height : "+options.height+", bpp : "+bpp+",\n";
|
|
||||||
if (transparentCol!==undefined) imgstr += " transparent : "+transparentCol+",\n";
|
|
||||||
imgstr += ' buffer : '+strCmd+'(atob("'+btoa(str)+'"))\n';
|
|
||||||
imgstr += "}";
|
|
||||||
} else if (options.output=="string") {
|
|
||||||
imgstr = strCmd+'(atob("'+btoa(str)+'"))';
|
|
||||||
} else {
|
|
||||||
throw new Error("Unknown output style");
|
|
||||||
}
|
|
||||||
return imgstr;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add a checkerboard background to any transparent areas and
|
|
||||||
make everything nontransparent. expects width/height in optuons */
|
|
||||||
function RGBAtoCheckerboard(rgba, options) {
|
|
||||||
var n=0;
|
|
||||||
for (var y=0; y<options.height; y++) {
|
|
||||||
for (var x=0; x<options.width; x++) {
|
|
||||||
var na = rgba[n*4+3]/255;
|
|
||||||
var a = 1-na;
|
|
||||||
var chequerboard = ((((x>>2)^(y>>2))&1)?0xFFFFFF:0);
|
|
||||||
rgba[n*4] = rgba[n*4]*na + chequerboard*a;
|
|
||||||
rgba[n*4+1] = rgba[n*4+1]*na + chequerboard*a;
|
|
||||||
rgba[n*4+2] = rgba[n*4+2]*na + chequerboard*a;
|
|
||||||
rgba[n*4+3] = 255;
|
|
||||||
n++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* RGBAtoString options, PLUS:
|
|
||||||
|
|
||||||
updateCanvas: update canvas with the quantized image
|
|
||||||
*/
|
|
||||||
function canvastoString(canvas, options) {
|
|
||||||
options = options||{};
|
|
||||||
options.width = canvas.width;
|
|
||||||
options.height = canvas.height;
|
|
||||||
var ctx = canvas.getContext("2d");
|
|
||||||
var imageData = ctx.getImageData(0, 0, options.width, options.height);
|
|
||||||
var rgba = imageData.data;
|
|
||||||
if (options.updateCanvas)
|
|
||||||
options.rgbaOut = rgba;
|
|
||||||
var str = RGBAtoString(rgba, options);
|
|
||||||
if (options.updateCanvas)
|
|
||||||
ctx.putImageData(imageData,0,0);
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* RGBAtoString options, PLUS:
|
|
||||||
|
|
||||||
*/
|
|
||||||
function imagetoString(img, options) {
|
|
||||||
options = options||{};
|
|
||||||
var canvas = document.createElement('canvas');
|
|
||||||
canvas.width = img.width;
|
|
||||||
canvas.height = img.height;
|
|
||||||
var ctx = canvas.getContext("2d");
|
|
||||||
ctx.drawImage(img,0,0);
|
|
||||||
return canvastoString(canvas, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOptions() {
|
|
||||||
return {
|
|
||||||
width : "int",
|
|
||||||
height : "int",
|
|
||||||
rgbaOut : "Uint8Array", // to store quantised data
|
|
||||||
diffusion : ["none"],
|
|
||||||
compression : "bool",
|
|
||||||
transparent : "bool",
|
|
||||||
brightness : "int",
|
|
||||||
mode : Object.keys(COL_BPP),
|
|
||||||
output : ["object","string","raw"],
|
|
||||||
inverted : "bool",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Decode an Espruino image string into a URL, return undefined if it's not valid.
|
|
||||||
options = {
|
|
||||||
transparent : bool // should the image be transparent, or just chequered where transparent?
|
|
||||||
} */
|
|
||||||
function stringToImageURL(data, options) {
|
|
||||||
options = options||{};
|
|
||||||
var p = 0;
|
|
||||||
var width = 0|data.charCodeAt(p++);
|
|
||||||
var height = 0|data.charCodeAt(p++);
|
|
||||||
var bpp = 0|data.charCodeAt(p++);
|
|
||||||
var transparentCol = -1;
|
|
||||||
if (bpp&128) {
|
|
||||||
bpp &= 127;
|
|
||||||
transparentCol = 0|data.charCodeAt(p++);
|
|
||||||
}
|
|
||||||
var mode = BPP_TO_COLOR_FORMAT[bpp];
|
|
||||||
if (!mode) return undefined; // unknown format
|
|
||||||
var bitmapSize = ((width*height*bpp)+7) >> 3;
|
|
||||||
// If it's the wrong length, it's not a bitmap or it's corrupt!
|
|
||||||
if (data.length != p+bitmapSize)
|
|
||||||
return undefined;
|
|
||||||
// Ok, build the picture
|
|
||||||
var canvas = document.createElement('canvas');
|
|
||||||
canvas.width = width;
|
|
||||||
canvas.height = height;
|
|
||||||
var ctx = canvas.getContext("2d");
|
|
||||||
var imageData = ctx.getImageData(0, 0, width, height);
|
|
||||||
var rgba = imageData.data;
|
|
||||||
var no = 0;
|
|
||||||
var nibits = 0;
|
|
||||||
var nidata = 0;
|
|
||||||
for (var i=0;i<width*height;i++) {
|
|
||||||
while (nibits<bpp) {
|
|
||||||
nidata = (nidata<<8) | data.charCodeAt(p++);
|
|
||||||
nibits += 8;
|
|
||||||
}
|
|
||||||
var c = (nidata>>(nibits-bpp)) & ((1<<bpp)-1);
|
|
||||||
nibits -= bpp;
|
|
||||||
var cr = COL_TO_RGB[mode](c);
|
|
||||||
if (c == transparentCol)
|
|
||||||
cr = cr & 0xFFFFFF;
|
|
||||||
rgba[no++] = (cr>>16)&255; // r
|
|
||||||
rgba[no++] = (cr>>8)&255; // g
|
|
||||||
rgba[no++] = cr&255; // b
|
|
||||||
rgba[no++] = cr>>>24; // a
|
|
||||||
}
|
|
||||||
if (!options.transparent)
|
|
||||||
RGBAtoCheckerboard(rgba, {width:width, height:height});
|
|
||||||
ctx.putImageData(imageData,0,0);
|
|
||||||
return canvas.toDataURL();
|
|
||||||
}
|
|
||||||
|
|
||||||
// decode an Espruino image string into an HTML string, return undefined if it's not valid. See stringToImageURL
|
|
||||||
function stringToImageHTML(data, options) {
|
|
||||||
var url = stringToImageURL(data, options);
|
|
||||||
if (!url) return undefined;
|
|
||||||
return '<img src="'+url+'"\>';
|
|
||||||
}
|
|
||||||
|
|
||||||
// =======================================================
|
|
||||||
return {
|
|
||||||
RGBAtoString : RGBAtoString,
|
|
||||||
RGBAtoCheckerboard : RGBAtoCheckerboard,
|
|
||||||
canvastoString : canvastoString,
|
|
||||||
imagetoString : imagetoString,
|
|
||||||
getOptions : getOptions,
|
|
||||||
|
|
||||||
stringToImageHTML : stringToImageHTML,
|
|
||||||
stringToImageURL : stringToImageURL
|
|
||||||
};
|
|
||||||
}));
|
|
|
@ -1,96 +0,0 @@
|
||||||
/* Library for 'interface' HTML files that are to
|
|
||||||
be used from within BangleApps
|
|
||||||
|
|
||||||
See: README.md / `apps.json`: `interface` element
|
|
||||||
|
|
||||||
This exposes a 'Puck' object (a simple version of
|
|
||||||
https://github.com/espruino/EspruinoWebTools/blob/master/puck.js)
|
|
||||||
and calls `onInit` when it's ready. `Puck` can be used for
|
|
||||||
sending/receiving data to the correctly connected
|
|
||||||
device with Puck.eval/write.
|
|
||||||
|
|
||||||
Puck.write(data,callback)
|
|
||||||
Puck.eval(data,callback)
|
|
||||||
|
|
||||||
There is also:
|
|
||||||
|
|
||||||
Util.readStorageFile(filename,callback)
|
|
||||||
Util.eraseStorageFile(filename,callback)
|
|
||||||
Util.showModal(title)
|
|
||||||
Util.hideModal()
|
|
||||||
*/
|
|
||||||
let __id = 0, __idlookup = [];
|
|
||||||
const Puck = {
|
|
||||||
eval : function(data,callback) {
|
|
||||||
__id++;
|
|
||||||
__idlookup[__id] = callback;
|
|
||||||
window.postMessage({type:"eval",data:data,id:__id});
|
|
||||||
},write : function(data,callback) {
|
|
||||||
__id++;
|
|
||||||
__idlookup[__id] = callback;
|
|
||||||
window.postMessage({type:"write",data:data,id:__id});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const Util = {
|
|
||||||
readStorageFile : function(filename,callback) {
|
|
||||||
__id++;
|
|
||||||
__idlookup[__id] = callback;
|
|
||||||
window.postMessage({type:"readstoragefile",data:filename,id:__id});
|
|
||||||
},
|
|
||||||
eraseStorageFile : function(filename,callback) {
|
|
||||||
Puck.write(`\x10require("Storage").open(${JSON.stringify(filename)},"r").erase()\n`,callback);
|
|
||||||
},
|
|
||||||
eraseStorage : function(filename,callback) {
|
|
||||||
Puck.write(`\x10require("Storage").erase(${JSON.stringify(filename)})\n`,callback);
|
|
||||||
},
|
|
||||||
showModal : function(title) {
|
|
||||||
if (!Util.domModal) {
|
|
||||||
Util.domModal = document.createElement('div');
|
|
||||||
Util.domModal.id = "status-modal";
|
|
||||||
Util.domModal.classList.add("modal");
|
|
||||||
Util.domModal.classList.add("active");
|
|
||||||
Util.domModal.innerHTML = `<div class="modal-overlay"></div>
|
|
||||||
<div class="modal-container">
|
|
||||||
<div class="modal-header">
|
|
||||||
<div class="modal-title h5">Please wait</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="content">
|
|
||||||
Loading...
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
document.body.appendChild(Util.domModal);
|
|
||||||
}
|
|
||||||
Util.domModal.querySelector(".content").innerHTML = title;
|
|
||||||
Util.domModal.classList.add("active");
|
|
||||||
},
|
|
||||||
hideModal : function() {
|
|
||||||
if (!Util.domModal) return;
|
|
||||||
Util.domModal.classList.remove("active");
|
|
||||||
},
|
|
||||||
saveCSV : function(filename, csvData) {
|
|
||||||
let a = document.createElement("a"),
|
|
||||||
file = new Blob([csvData], {type: "Comma-separated value file"});
|
|
||||||
let url = URL.createObjectURL(file);
|
|
||||||
a.href = url;
|
|
||||||
a.download = filename+".csv";
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
setTimeout(function() {
|
|
||||||
document.body.removeChild(a);
|
|
||||||
window.URL.revokeObjectURL(url);
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener("message", function(event) {
|
|
||||||
let msg = event.data;
|
|
||||||
if (msg.type=="init") {
|
|
||||||
onInit();
|
|
||||||
} else if (msg.type=="evalrsp" || msg.type=="writersp"|| msg.type=="readstoragefilersp") {
|
|
||||||
let cb = __idlookup[msg.id];
|
|
||||||
delete __idlookup[msg.id];
|
|
||||||
cb(msg.data);
|
|
||||||
}
|
|
||||||
}, false);
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -139,11 +139,11 @@
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="https://www.puck-js.com/puck.js"></script>
|
<script src="https://www.puck-js.com/puck.js"></script>
|
||||||
<script src="loader.js"></script>
|
|
||||||
<script src="core/lib/marked.min.js"></script>
|
<script src="core/lib/marked.min.js"></script>
|
||||||
<script src="core/lib/espruinotools.js"></script>
|
<script src="core/lib/espruinotools.js"></script>
|
||||||
<script src="core/lib/heatshrink.js"></script>
|
<script src="core/lib/heatshrink.js"></script>
|
||||||
<script src="core/js/utils.js"></script>
|
<script src="core/js/utils.js"></script>
|
||||||
|
<script src="loader.js"></script>
|
||||||
<script src="core/js/ui.js"></script>
|
<script src="core/js/ui.js"></script>
|
||||||
<script src="core/js/comms.js"></script>
|
<script src="core/js/comms.js"></script>
|
||||||
<script src="core/js/appinfo.js"></script>
|
<script src="core/js/appinfo.js"></script>
|
||||||
|
|
Loading…
Reference in New Issue