Moving to git submodule for code app loader code

pull/556/head
Gordon Williams 2020-09-01 11:37:38 +01:00
parent f417f8b1c9
commit d10df7a638
21 changed files with 5 additions and 9098 deletions

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "EspruinoAppLoaderCore"]
path = core
url = git@github.com:espruino/EspruinoAppLoaderCore.git

1
core Submodule

@ -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

View File

@ -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"
}
}

View File

@ -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;

View File

@ -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...`);
});
});
}
};

View File

@ -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?"&#x2665;":"&#x2661;"}</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");
});
});

View File

@ -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');
}

View File

@ -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));
});

View File

@ -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();
});
});
});
}

View File

@ -1,103 +0,0 @@
function escapeHtml(text) {
let map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
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
}
}

View File

@ -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"
}
}

View File

@ -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

View File

@ -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
};
}));

View File

@ -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

View File

@ -139,11 +139,11 @@
</footer>
<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/espruinotools.js"></script>
<script src="core/lib/heatshrink.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/comms.js"></script>
<script src="core/js/appinfo.js"></script>