From 912026a1f8f54573f95345904e2af807924c6485 Mon Sep 17 00:00:00 2001 From: Anton <dev@atjn.dk> Date: Thu, 8 Aug 2024 15:30:38 +0200 Subject: [PATCH 1/3] Add device storage info --- js/comms.js | 9 +++++---- js/index.js | 49 +++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/js/comms.js b/js/comms.js index 5329f00..feef9f5 100644 --- a/js/comms.js +++ b/js/comms.js @@ -196,9 +196,9 @@ const Comms = { }); }); }, - // Get Device ID and version, plus a JSON list of installed apps + // Get Device ID, version, storage stats, and a JSON list of installed apps getDeviceInfo : (noReset) => { - Progress.show({title:`Getting app list...`,sticky:true}); + Progress.show({title:`Getting device info...`,sticky:true}); return new Promise((resolve,reject) => { Puck.write("\x03",(result) => { if (result===null) { @@ -240,7 +240,7 @@ const Comms = { return; } - let cmd, finalJS = `E.toJS([process.env.BOARD,process.env.VERSION,process.env.EXPTR,0|getTime(),E.CRC32(getSerial()+NRF.getAddress())]).substr(1)`; + let cmd, finalJS = `JSON.stringify(require("Storage").getStats?require("Storage").getStats():{})+","+E.toJS([process.env.BOARD,process.env.VERSION,process.env.EXPTR,0|getTime(),E.CRC32(getSerial()+NRF.getAddress())]).substr(1)`; if (Const.SINGLE_APP_ONLY) // only one app on device, info file is in app.info cmd = `\x10Bluetooth.println("["+(require("Storage").read("app.info")||"null")+","+${finalJS})\n`; else @@ -259,12 +259,13 @@ const Comms = { let appList; try { appList = JSON.parse(appListJSON); - // unpack the last 4 elements which are board info (See finalJS above) + // unpack the last 6 elements which are board info (See finalJS above) info.uid = appList.pop(); // unique ID for watch (hash of internal serial number and MAC) info.currentTime = appList.pop()*1000; // time in ms info.exptr = appList.pop(); // used for compilation info.version = appList.pop(); info.id = appList.pop(); + info.storageStats = appList.pop(); // how much storage has been used // if we just have 'null' then it means we have no apps if (appList.length==1 && appList[0]==null) appList = []; diff --git a/js/index.js b/js/index.js index bedebe4..8cb8d24 100644 --- a/js/index.js +++ b/js/index.js @@ -320,7 +320,7 @@ function handleCustomApp(appTemplate) { if (!appTemplate.custom) throw new Error("App doesn't have custom HTML"); // if it needs a connection, do that first if (appTemplate.customConnect && !device.connected) - return getInstalledApps().then(() => handleCustomApp(appTemplate)); + return getDeviceInfo().then(() => handleCustomApp(appTemplate)); // otherwise continue return new Promise((resolve,reject) => { let modal = htmlElement(`<div class="modal active"> @@ -358,7 +358,7 @@ function handleCustomApp(appTemplate) { console.log("Received custom app", app); modal.remove(); - getInstalledApps() + getDeviceInfo() .then(()=>checkDependencies(app)) .then(()=>Comms.uploadApp(app,{device:device, language:LANGUAGE, noFinish: msg.options && msg.options.noFinish})) .then(()=>{ @@ -745,7 +745,7 @@ function showScreenshots(appId) { // =========================================== My Apps function uploadApp(app) { - return getInstalledApps().then(()=>{ + return getDeviceInfo().then(()=>{ if (device.appsInstalled.some(i => i.id === app.id)) { return updateApp(app); } @@ -774,7 +774,7 @@ function uploadApp(app) { function removeApp(app) { return showPrompt("Delete","Really remove '"+app.name+"'?").then(() => { - return getInstalledApps().then(()=>{ + return getDeviceInfo().then(()=>{ // a = from appid.info, app = from apps.json return Comms.removeApp(device.appsInstalled.find(a => a.id === app.id)); }); @@ -983,7 +983,7 @@ function refreshMyApps() { } let haveInstalledApps = false; -function getInstalledApps(refresh) { +function getDeviceInfo(refresh) { if (haveInstalledApps && !refresh) { return Promise.resolve(device.appsInstalled); } @@ -995,6 +995,7 @@ function getInstalledApps(refresh) { device.id = info.id; device.version = info.version; device.exptr = info.exptr; + device.storageStats = info.storageStats; device.appsInstalled = info.apps; haveInstalledApps = true; if ("function"==typeof onFoundDeviceInfo) @@ -1016,11 +1017,39 @@ function getInstalledApps(refresh) { const deviceInfoElem = document.getElementById("more-deviceinfo"); if (deviceInfoElem) { deviceInfoElem.style.display = "inherit"; + let storageRow = ""; + if (device.storageStats?.totalBytes) { + const stats = device.storageStats; + const totalKB = (stats.totalBytes / 1000).toFixed(2); + const usedKB = (stats.fileBytes / 1000).toFixed(2); + const trashKB = (stats.trashBytes / 1000).toFixed(2); + const freeKB = (stats.freeBytes / 1000).toFixed(2); + const bytePrc = 100 / stats.totalBytes; + const usedPrc = bytePrc * stats.fileBytes; + const trashPrc = bytePrc * stats.trashBytes; + const freePrc = bytePrc * stats.freeBytes; + if (isNaN(usedPrc) || isNaN(trashPrc) || isNaN(freePrc)) { + console.error("Unexpected error: Could not calculate storage statistics"); + } else { + storageRow = ` +<tr><td><b>Storage</b></td><td> + <p style="margin-bottom:.4rem;">${totalKB} KiB in total, ${stats.fileCount} files used, ${stats.trashCount} files trashed.</p> + <div class="bar" style="margin-bottom:.3rem;"> + <!-- These styles prevent overflow of text if the bar item is too small to fit all the text --> + <style>.bar-item{white-space:nowrap;padding-left:.1rem;padding-right:.1rem;}</style> + <div class="bar-item tooltip bg-error" data-tooltip="${usedKB} KiB, ${usedPrc.toFixed(2)}% used" style="width:${usedPrc}%; color:hsl(218 16% 2%)">${usedPrc.toFixed(0)}% used</div> + <div class="bar-item tooltip bg-warning" data-tooltip="${trashKB} KiB, ${trashPrc.toFixed(2)}% trash" style="width:${trashPrc}%;color:hsl(218 16% 7%)">${trashPrc.toFixed(0)}% trash</div> + <div class="bar-item tooltip bg-success" data-tooltip="${freeKB} KiB, ${freePrc.toFixed(2)}% free" style="width:${freePrc}%; color:hsl(218 16% 7%)">${freePrc.toFixed(0)}% free</div> + </div> +</td></tr>`; + } + } const deviceInfoContentElem = document.getElementById("more-deviceinfo-content"); deviceInfoContentElem.innerHTML = ` <table class="table"><tbody> <tr><td><b>Device Type</b></td><td>${device.id}</td></tr> <tr><td><b>Firmware Version</b></td><td>${device.version}</td></tr> + ${storageRow} <tr><td><b>Apps Installed</b></td><td>${(device.appsInstalled || []).map(a => `${a.id} (${a.version})`).join(", ")}</td></tr> </tbody></table>`; } @@ -1064,7 +1093,7 @@ function installMultipleApps(appIds, promptName) { ).then(()=> Comms.showUploadFinished() ).then(()=>{ showToast("Apps successfully installed!","success"); - return getInstalledApps(true); + return getDeviceInfo(true); }); } @@ -1110,7 +1139,7 @@ function handleConnectionChange(connected) { } htmlToArray(document.querySelectorAll(".btn.refresh")).map(button => button.addEventListener("click", () => { - getInstalledApps(true).catch(err => { + getDeviceInfo(true).catch(err => { showToast("Getting app list failed, "+err,"error"); }); })); @@ -1123,7 +1152,7 @@ connectMyDeviceBtn.addEventListener("click", () => { const deviceInfoElem = document.getElementById("more-deviceinfo"); if (deviceInfoElem) deviceInfoElem.style.display = "none"; } else { - getInstalledApps(true).catch(err => { + getDeviceInfo(true).catch(err => { showToast("Device connection failed, "+err,"error"); Comms.disconnectDevice(); }); @@ -1243,7 +1272,7 @@ if (btn) btn.addEventListener("click",event=>{ Progress.hide({sticky:true}); device.appsInstalled = []; showToast("All apps removed","success"); - return getInstalledApps(true); + return getDeviceInfo(true); }).catch(err=>{ Progress.hide({sticky:true}); showToast("App removal failed, "+err,"error"); @@ -1279,7 +1308,7 @@ if (btn) btn.addEventListener("click", event => { btn = document.getElementById("screenshot"); if (btn) btn.addEventListener("click",event=>{ - getInstalledApps(false).then(()=>{ + getDeviceInfo(false).then(()=>{ if (device.id=="BANGLEJS"){ showPrompt("Screenshot","Screenshots are not supported on Bangle.js 1",{ok:1}); } else { From 077a3fd9928e10f86bc25fdd7bf49bd952568e03 Mon Sep 17 00:00:00 2001 From: Anton <dev@atjn.dk> Date: Thu, 8 Aug 2024 15:43:43 +0200 Subject: [PATCH 2/3] Fix eslint supported syntax --- js/.eslintrc.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/.eslintrc.json b/js/.eslintrc.json index 6c20ead..b26071f 100644 --- a/js/.eslintrc.json +++ b/js/.eslintrc.json @@ -1,6 +1,6 @@ { "parserOptions": { - "ecmaVersion": 6, + "ecmaVersion": 2020, "sourceType": "script" }, "rules": { From e496c04787e7e224bc1887f7f4785e523156fb2d Mon Sep 17 00:00:00 2001 From: Anton <dev@atjn.dk> Date: Mon, 12 Aug 2024 09:58:27 +0200 Subject: [PATCH 3/3] Don't rename `getInstalledApps` --- js/index.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/js/index.js b/js/index.js index 8cb8d24..73261b0 100644 --- a/js/index.js +++ b/js/index.js @@ -320,7 +320,7 @@ function handleCustomApp(appTemplate) { if (!appTemplate.custom) throw new Error("App doesn't have custom HTML"); // if it needs a connection, do that first if (appTemplate.customConnect && !device.connected) - return getDeviceInfo().then(() => handleCustomApp(appTemplate)); + return getInstalledApps().then(() => handleCustomApp(appTemplate)); // otherwise continue return new Promise((resolve,reject) => { let modal = htmlElement(`<div class="modal active"> @@ -358,7 +358,7 @@ function handleCustomApp(appTemplate) { console.log("Received custom app", app); modal.remove(); - getDeviceInfo() + getInstalledApps() .then(()=>checkDependencies(app)) .then(()=>Comms.uploadApp(app,{device:device, language:LANGUAGE, noFinish: msg.options && msg.options.noFinish})) .then(()=>{ @@ -745,7 +745,7 @@ function showScreenshots(appId) { // =========================================== My Apps function uploadApp(app) { - return getDeviceInfo().then(()=>{ + return getInstalledApps().then(()=>{ if (device.appsInstalled.some(i => i.id === app.id)) { return updateApp(app); } @@ -774,7 +774,7 @@ function uploadApp(app) { function removeApp(app) { return showPrompt("Delete","Really remove '"+app.name+"'?").then(() => { - return getDeviceInfo().then(()=>{ + return getInstalledApps().then(()=>{ // a = from appid.info, app = from apps.json return Comms.removeApp(device.appsInstalled.find(a => a.id === app.id)); }); @@ -983,7 +983,7 @@ function refreshMyApps() { } let haveInstalledApps = false; -function getDeviceInfo(refresh) { +function getInstalledApps(refresh) { if (haveInstalledApps && !refresh) { return Promise.resolve(device.appsInstalled); } @@ -1093,7 +1093,7 @@ function installMultipleApps(appIds, promptName) { ).then(()=> Comms.showUploadFinished() ).then(()=>{ showToast("Apps successfully installed!","success"); - return getDeviceInfo(true); + return getInstalledApps(true); }); } @@ -1139,7 +1139,7 @@ function handleConnectionChange(connected) { } htmlToArray(document.querySelectorAll(".btn.refresh")).map(button => button.addEventListener("click", () => { - getDeviceInfo(true).catch(err => { + getInstalledApps(true).catch(err => { showToast("Getting app list failed, "+err,"error"); }); })); @@ -1152,7 +1152,7 @@ connectMyDeviceBtn.addEventListener("click", () => { const deviceInfoElem = document.getElementById("more-deviceinfo"); if (deviceInfoElem) deviceInfoElem.style.display = "none"; } else { - getDeviceInfo(true).catch(err => { + getInstalledApps(true).catch(err => { showToast("Device connection failed, "+err,"error"); Comms.disconnectDevice(); }); @@ -1272,7 +1272,7 @@ if (btn) btn.addEventListener("click",event=>{ Progress.hide({sticky:true}); device.appsInstalled = []; showToast("All apps removed","success"); - return getDeviceInfo(true); + return getInstalledApps(true); }).catch(err=>{ Progress.hide({sticky:true}); showToast("App removal failed, "+err,"error"); @@ -1308,7 +1308,7 @@ if (btn) btn.addEventListener("click", event => { btn = document.getElementById("screenshot"); if (btn) btn.addEventListener("click",event=>{ - getDeviceInfo(false).then(()=>{ + getInstalledApps(false).then(()=>{ if (device.id=="BANGLEJS"){ showPrompt("Screenshot","Screenshots are not supported on Bangle.js 1",{ok:1}); } else {