diff --git a/index.html b/index.html
index efaf84a61..6e1a9b554 100644
--- a/index.html
+++ b/index.html
@@ -129,6 +129,7 @@
+
diff --git a/js/comms.js b/js/comms.js
index e2cbf0cdd..91ae54b68 100644
--- a/js/comms.js
+++ b/js/comms.js
@@ -9,14 +9,19 @@ reset : (opt) => new Promise((resolve,reject) => {
});
}),
uploadApp : (app,skipReset) => {
+ Progress.show({title:`Uploading ${app.name}`,sticky:true});
return AppInfo.getFiles(app, httpGet).then(fileContents => {
return new Promise((resolve,reject) => {
console.log("uploadApp",fileContents.map(f=>f.name).join(", "));
+ var maxBytes = fileContents.reduce((b,f)=>b+f.content.length, 0)||1;
+ var currentBytes = 0;
+
// 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(app);
});
@@ -24,17 +29,27 @@ uploadApp : (app,skipReset) => {
}
var f = fileContents.shift();
console.log(`Upload ${f.name} => ${JSON.stringify(f.content)}`);
+ Progress.show({
+ min:currentBytes / maxBytes,
+ max:(currentBytes+f.content.length) / maxBytes});
+ currentBytes += f.content.length;
// Chould check CRC here if needed instead of returning 'OK'...
// E.CRC32(require("Storage").read(${JSON.stringify(app.name)}))
Puck.write(`\x10${f.cmd};Bluetooth.println("OK")\n`,(result) => {
- if (!result || result.trim()!="OK") return reject("Unexpected response "+(result||""));
+ if (!result || result.trim()!="OK") {
+ Progress.hide({sticky:true});
+ return reject("Unexpected response "+(result||""));
+ }
doUploadFiles();
}, true); // wait for a newline
}
// Start the upload
function doUpload() {
Puck.write(`\x10E.showMessage('Uploading\\n${app.id}...')\n`,(result) => {
- if (result===null) return reject("");
+ if (result===null) {
+ Progress.hide({sticky:true});
+ return reject("");
+ }
doUploadFiles();
});
}
@@ -48,10 +63,15 @@ uploadApp : (app,skipReset) => {
});
},
getInstalledApps : () => {
+ Progress.show({title:`Getting app list...`,sticky:true});
return new Promise((resolve,reject) => {
Puck.write("\x03",(result) => {
- if (result===null) return reject("");
+ if (result===null) {
+ Progress.hide({sticky:true});
+ return reject("");
+ }
Puck.eval('require("Storage").list(/\.info$/).map(f=>{var j=require("Storage").readJSON(f,1)||{};j.id=f.slice(0,-5);return j})', (appList,err) => {
+ Progress.hide({sticky:true});
if (appList===null) return reject(err || "");
console.log("getInstalledApps", appList);
resolve(appList);
@@ -60,6 +80,7 @@ getInstalledApps : () => {
});
},
removeApp : app => { // expects an app structure
+ Progress.show({title:`Removing ${app.name}`,sticky:true});
var storage = [{name:app.id+".info"}].concat(app.storage);
var cmds = storage.map(file=>{
return `\x10require("Storage").erase(${toJS(file.name)});\n`;
@@ -67,15 +88,21 @@ removeApp : app => { // expects an app structure
console.log("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 : () => {
+ Progress.show({title:"Removing all apps",progess:"animate",sticky:true});
return new Promise((resolve,reject) => {
// Use write with newline here so we wait for it to finish
Puck.write('\x10E.showMessage("Erasing...");require("Storage").eraseAll();Bluetooth.println("OK");reset()\n', (result,err) => {
+ Progress.hide({sticky:true});
if (!result || result.trim()!="OK") return reject(err || "");
resolve();
}, true /* wait for newline */);
@@ -171,10 +198,10 @@ readStorageFile : (filename) => { // StorageFiles are different to normal storag
fileContent = fileContent.substr(newLineIdx+1);
}
} else {
- showProgress(undefined,100*fileContent.length / (fileSize||1000000));
+ Progress.show({percent:100*fileContent.length / (fileSize||1000000)});
}
if (finished) {
- hideProgress();
+ Progress.hide();
connection.received = "";
connection.cb = undefined;
resolve(fileContent);
@@ -188,7 +215,7 @@ readStorageFile : (filename) => { // StorageFiles are different to normal storag
while (l!==undefined) { Bluetooth.print(l); l = f.readLine(); }
Bluetooth.print("\xFF");
})()\n`,() => {
- showProgress(`Reading ${JSON.stringify(filename)}`,0);
+ Progress.show({title:`Reading ${JSON.stringify(filename)}`,percent:0});
console.log(`StorageFile read started...`);
});
});
diff --git a/js/index.js b/js/index.js
index b21fc907d..60b66436a 100644
--- a/js/index.js
+++ b/js/index.js
@@ -14,119 +14,7 @@ httpGet("apps.json").then(apps=>{
refreshFilter();
});
-// Status
// =========================================== Top Navigation
-function showToast(message, type) {
- // toast-primary, toast-success, toast-warning or toast-error
- var style = "toast-primary";
- if (type=="success") style = "toast-success";
- else if (type=="error") style = "toast-error";
- else if (type!==undefined) console.log("showToast: unknown toast "+type);
- var toastcontainer = document.getElementById("toastcontainer");
- var msgDiv = htmlElement(`
`);
- msgDiv.innerHTML = message;
- toastcontainer.append(msgDiv);
- setTimeout(function() {
- msgDiv.remove();
- }, 5000);
-}
-var progressToast; // the DOM element
-var progressSticky; // showProgress(,,"sticky") don't remove until hideProgress("sticky")
-var progressInterval; // the interval used if showProgress(..., "animate")
-var progressPercent; // the current progress percentage
-function showProgress(text, percent, sticky) {
- if (sticky=="sticky")
- progressSticky = true;
- if (!progressToast) {
- if (progressInterval) {
- clearInterval(progressInterval);
- progressInterval = undefined;
- }
- if (percent == "animate") {
- progressInterval = setInterval(function() {
- progressPercent += 2;
- if (progressPercent>100) progressPercent=0;
- showProgress(undefined, progressPercent);
- }, 100);
- percent = 0;
- }
- progressPercent = percent;
-
- var toastcontainer = document.getElementById("toastcontainer");
- progressToast = htmlElement(`
- ${text ? `
${text}
`:``}
-
-
`);
- toastcontainer.append(progressToast);
- } else {
- var pt=document.getElementById("progressToast");
- pt.setAttribute("aria-valuenow",percent);
- pt.style.width = percent+"%";
- }
-}
-function hideProgress(sticky) {
- if (progressSticky && sticky!="sticky")
- return;
- progressSticky = false;
- if (progressInterval) {
- clearInterval(progressInterval);
- progressInterval = undefined;
- }
- if (progressToast) progressToast.remove();
- progressToast = undefined;
-}
-
-Puck.writeProgress = function(charsSent, charsTotal) {
- if (charsSent===undefined) {
- hideProgress();
- return;
- }
- var percent = Math.round(charsSent*100/charsTotal);
- showProgress(undefined, percent);
-}
-function showPrompt(title, text, buttons) {
- if (!buttons) buttons={yes:1,no:1};
- return new Promise((resolve,reject) => {
- var modal = htmlElement(`
-
-
-
-
-
- ${escapeHtml(text).replace(/\n/g,'
')}
-
-
-
-
-
`);
- 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();
- var isYes = event.target.getAttribute("isyes")=="1";
- if (isYes) resolve();
- else reject("User cancelled");
- modal.remove();
- });
- });
- });
-}
function showChangeLog(appid) {
var app = appNameToApp(appid);
function show(contents) {
@@ -170,12 +58,11 @@ function handleCustomApp(appTemplate) {
Object.keys(appFiles).forEach(k => app[k] = appFiles[k]);
console.log("Received custom app", app);
modal.remove();
- showProgress(`Uploading ${app.name}`,undefined,"sticky");
Comms.uploadApp(app).then(()=>{
- hideProgress("sticky");
+ Progress.hide({sticky:true});
resolve();
}).catch(e => {
- hideProgress("sticky");
+ Progress.hide({sticky:true});
reject(e);
});
}, false);
@@ -334,9 +221,8 @@ function refreshLibrary() {
// upload
icon.classList.remove("icon-upload");
icon.classList.add("loading");
- showProgress(`Uploading ${app.name}`,undefined,"sticky");
Comms.uploadApp(app).then((appJSON) => {
- hideProgress("sticky");
+ Progress.hide({sticky:true});
if (appJSON) appsInstalled.push(appJSON);
showToast(app.name+" Uploaded!", "success");
icon.classList.remove("loading");
@@ -344,7 +230,7 @@ function refreshLibrary() {
refreshMyApps();
refreshLibrary();
}).catch(err => {
- hideProgress("sticky");
+ Progress.hide({sticky:true});
showToast("Upload failed, "+err, "error");
icon.classList.remove("loading");
icon.classList.add("icon-upload");
@@ -403,19 +289,16 @@ function customApp(app) {
function updateApp(app) {
if (app.custom) return customApp(app);
- showProgress(`Upgrading ${app.name}`,undefined,"sticky");
return Comms.removeApp(app).then(()=>{
showToast(app.name+" removed successfully. Updating...",);
appsInstalled = appsInstalled.filter(a=>a.id!=app.id);
return Comms.uploadApp(app);
}).then((appJSON) => {
- hideProgress("sticky");
if (appJSON) appsInstalled.push(appJSON);
showToast(app.name+" Updated!", "success");
refreshMyApps();
refreshLibrary();
}, err=>{
- hideProgress("sticky");
showToast(app.name+" update failed, "+err,"error");
refreshMyApps();
refreshLibrary();
@@ -488,18 +371,15 @@ return `
function getInstalledApps() {
showLoadingIndicator("myappscontainer");
- showProgress(`Getting app list...`,undefined,"sticky");
// Get apps and files
return Comms.getInstalledApps()
.then(appJSON => {
- hideProgress("sticky");
appsInstalled = appJSON;
refreshMyApps();
refreshLibrary();
})
.then(() => handleConnectionChange(true))
.catch(err=>{
- hideProgress("sticky");
return Promise.reject();
});
}
@@ -555,15 +435,14 @@ document.getElementById("settime").addEventListener("click",event=>{
});
document.getElementById("removeall").addEventListener("click",event=>{
showPrompt("Remove All","Really remove all apps?").then(() => {
- showProgress("Removing all apps","animate", "sticky");
return Comms.removeAllApps();
}).then(()=>{
- hideProgress("sticky");
+ Progress.hide({sticky:true});
appsInstalled = [];
showToast("All apps removed","success");
return getInstalledApps();
}).catch(err=>{
- hideProgress("sticky");
+ Progress.hide({sticky:true});
showToast("App removal failed, "+err,"error");
});
});
@@ -578,24 +457,23 @@ document.getElementById("installdefault").addEventListener("click",event=>{
appCount = defaultApps.length;
return showPrompt("Install Defaults","Remove everything and install default apps?");
}).then(() => {
- showProgress("Removing all apps","animate", "sticky");
return Comms.removeAllApps();
}).then(()=>{
- hideProgress("sticky");
+ Progress.hide({sticky:true});
appsInstalled = [];
showToast(`Existing apps removed. Installing ${appCount} apps...`);
return new Promise((resolve,reject) => {
function upload() {
var app = defaultApps.shift();
if (app===undefined) return resolve();
- showProgress(`${app.name} (${appCount-defaultApps.length}/${appCount})`,undefined,"sticky");
+ Progress.show({title:`${app.name} (${appCount-defaultApps.length}/${appCount})`,sticky:true});
Comms.uploadApp(app,"skip_reset").then((appJSON) => {
- hideProgress("sticky");
+ Progress.hide({sticky:true});
if (appJSON) appsInstalled.push(appJSON);
showToast(`(${appCount-defaultApps.length}/${appCount}) ${app.name} Uploaded`);
upload();
}).catch(function() {
- hideProgress("sticky");
+ Progress.hide({sticky:true});
reject()
});
}
@@ -607,7 +485,7 @@ document.getElementById("installdefault").addEventListener("click",event=>{
showToast("Default apps successfully installed!","success");
return getInstalledApps();
}).catch(err=>{
- hideProgress("sticky");
+ Progress.hide({sticky:true});
showToast("App Install failed, "+err,"error");
});
});
diff --git a/js/ui.js b/js/ui.js
new file mode 100644
index 000000000..c88091872
--- /dev/null
+++ b/js/ui.js
@@ -0,0 +1,140 @@
+// General UI tools (progress bar, toast, prompt)
+
+/// Handle progress bars
+var 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({progress:"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||{};
+ var 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;
+ var 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 (percent == "animate") {
+ Progress.interval = setInterval(function() {
+ Progress.percent += 2;
+ if (Progress.percent>100) Progress.percent=0;
+ Progress.show({percent:Progress.percent});
+ }, 100);
+ percent = 0;
+ }
+
+ var toastcontainer = document.getElementById("toastcontainer");
+ Progress.domElement = htmlElement(`
+ ${text ? `
${text}
`:``}
+
+
`);
+ toastcontainer.append(Progress.domElement);
+ } else {
+ var 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;
+ }
+ var 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
+ var style = "toast-primary";
+ if (type=="success") style = "toast-success";
+ else if (type=="error") style = "toast-error";
+ else if (type!==undefined) console.log("showToast: unknown toast "+type);
+ var toastcontainer = document.getElementById("toastcontainer");
+ var msgDiv = htmlElement(`
`);
+ msgDiv.innerHTML = message;
+ toastcontainer.append(msgDiv);
+ setTimeout(function() {
+ msgDiv.remove();
+ }, 5000);
+}
+
+/// Show a yes/no prompt
+function showPrompt(title, text, buttons) {
+ if (!buttons) buttons={yes:1,no:1};
+ return new Promise((resolve,reject) => {
+ var modal = htmlElement(`
+
+
+
+
+
+ ${escapeHtml(text).replace(/\n/g,'
')}
+
+
+
+
+
`);
+ 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();
+ var isYes = event.target.getAttribute("isyes")=="1";
+ if (isYes) resolve();
+ else reject("User cancelled");
+ modal.remove();
+ });
+ });
+ });
+}