diff --git a/index.html b/index.html index 5e646088fa..f29d104452 100644 --- a/index.html +++ b/index.html @@ -146,6 +146,8 @@

Utilities

+

+

@@ -232,6 +234,7 @@

Device info

+ diff --git a/install_from_files.js b/install_from_files.js new file mode 100644 index 0000000000..0f59b1d068 --- /dev/null +++ b/install_from_files.js @@ -0,0 +1,142 @@ +/** + * Apploader - Install App from selected files + * + * This function allows users to install BangleJS apps by selecting files from their local filesystem. + * It reads metadata.json and uploads all referenced files to the watch using the standard upload pipeline. + */ +function installFromFiles() { + return new Promise(resolve => { + + // Request multi-file selection from user + Espruino.Core.Utils.fileOpenDialog({ + id:"installappfiles", + type:"arraybuffer", + multi:true, + mimeType:"*/*", + onComplete: function(files) { + try { + if (!files) return resolve(); // user cancelled + const mapped = files.map(function(f) { + return { name: f.fileName, data: f.contents }; + }); + processFiles(mapped, resolve); + } catch (err) { + showToast('Install failed: ' + err, 'error'); + console.error(err); + resolve(); + } + } + }); + }); +} + +function processFiles(files, resolve) { + if (!files || files.length === 0) { + return resolve(); + } + + const metadataFile = files.find(f => f.name === 'metadata.json' || f.name.endsWith('/metadata.json')); + + if (!metadataFile) { + showToast('No metadata.json found in selected files', 'error'); + return resolve(); + } + + // Parse metadata.json + let app; + try { + const metadataText = new TextDecoder().decode(new Uint8Array(metadataFile.data)); + app = JSON.parse(metadataText); + } catch(err) { + showToast('Failed to parse metadata.json: ' + err, 'error'); + return resolve(); + } + + if (!app.id || !app.storage || !Array.isArray(app.storage)) { + showToast('Invalid metadata.json', 'error'); + return resolve(); + } + + // Build file map for lookup (both simple filename and full path) + const fileMap = {}; + files.forEach(f => { + const simpleName = f.name.split('/').pop(); + fileMap[simpleName] = f; + fileMap[f.name] = f; + }); + + // Populate content directly into storage entries so AppInfo.getFiles doesn't fetch URLs + app.storage.forEach(storageEntry => { + const fileName = storageEntry.url || storageEntry.name; + const file = fileMap[fileName]; + if (file) { + const data = new Uint8Array(file.data); + let content = ""; + for (let i = 0; i < data.length; i++) { + content += String.fromCharCode(data[i]); + } + storageEntry.content = content; + } + }); + + // Populate content into data entries as well + if (app.data && Array.isArray(app.data)) { + app.data.forEach(dataEntry => { + if (dataEntry.content) return; // already has inline content + const fileName = dataEntry.url || dataEntry.name; + const file = fileMap[fileName]; + if (file) { + const data = new Uint8Array(file.data); + let content = ""; + for (let i = 0; i < data.length; i++) { + content += String.fromCharCode(data[i]); + } + dataEntry.content = content; + } + }); + } + + showPrompt("Install App from Files", + `Install "${app.name}" (${app.id}) v${app.version}?\n\nThis will delete the existing version if installed.` + ).then(() => { + // Use standard updateApp flow (remove old, check deps, upload new) + return getInstalledApps().then(() => { + const isInstalled = device.appsInstalled.some(i => i.id === app.id); + + // If installed, use update flow; otherwise use install flow + const uploadPromise = isInstalled + ? Comms.getAppInfo(app).then(remove => { + return Comms.removeApp(remove, {containsFileList:true}); + }).then(() => { + device.appsInstalled = device.appsInstalled.filter(a => a.id != app.id); + return checkDependencies(app, {checkForClashes:false}); + }) + : checkDependencies(app); + + return uploadPromise.then(() => { + return Comms.uploadApp(app, { + device: device, + language: LANGUAGE + }); + }).then((appJSON) => { + if (appJSON) device.appsInstalled.push(appJSON); + showToast(`"${app.name}" installed!`, 'success'); + refreshMyApps(); + refreshLibrary(); + }); + }); + }).then(resolve).catch(err => { + showToast('Install failed: ' + err, 'error'); + console.error(err); + resolve(); + }); +} + +// Attach UI handler to the button on window load +window.addEventListener('load', (event) => { + const btn = document.getElementById("installappfromfiles"); + if (!btn) return; + btn.addEventListener("click", () => { + startOperation({name:"Install App from Files"}, installFromFiles); + }); +}); \ No newline at end of file