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