Skip to content

Commit 9b1af79

Browse files
authored
Merge pull request #4069 from kidneyhex/install-app-from-files
apploader - Add 'Install App from Files' option
2 parents 00142d8 + 37b1780 commit 9b1af79

File tree

2 files changed

+145
-0
lines changed

2 files changed

+145
-0
lines changed

index.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,8 @@ <h3>Utilities</h3>
146146
<button class="btn tooltip" id="installdefault" data-tooltip="Delete everything, install default apps">Install default apps</button>
147147
<button class="btn tooltip" id="installfavourite" data-tooltip="Delete everything, install your favourites">Install favourite apps</button>
148148
<button class="btn tooltip" id="defaultbanglesettings" data-tooltip="Reset your Bangle's settings to the defaults">Reset Settings</button>
149+
</p><p>
150+
<button class="btn tooltip" id="installappfromfiles" data-tooltip="Install an app by selecting its files">Install App from Files</button>
149151
</p><p>
150152
<button class="btn tooltip" id="newGithubIssue" data-tooltip="Create a new issue on GitHub">New issue on GitHub</button>
151153
</p>
@@ -232,6 +234,7 @@ <h3>Device info</h3>
232234
<script src="loader.js"></script>
233235
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js"></script> <!-- for backup.js -->
234236
<script src="backup.js"></script>
237+
<script src="install_from_files.js"></script>
235238
<script src="core/js/ui.js"></script>
236239
<script src="core/js/comms.js"></script>
237240
<script src="core/js/appinfo.js"></script>

install_from_files.js

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/**
2+
* Apploader - Install App from selected files
3+
*
4+
* This function allows users to install BangleJS apps by selecting files from their local filesystem.
5+
* It reads metadata.json and uploads all referenced files to the watch using the standard upload pipeline.
6+
*/
7+
function installFromFiles() {
8+
return new Promise(resolve => {
9+
10+
// Request multi-file selection from user
11+
Espruino.Core.Utils.fileOpenDialog({
12+
id:"installappfiles",
13+
type:"arraybuffer",
14+
multi:true,
15+
mimeType:"*/*",
16+
onComplete: function(files) {
17+
try {
18+
if (!files) return resolve(); // user cancelled
19+
const mapped = files.map(function(f) {
20+
return { name: f.fileName, data: f.contents };
21+
});
22+
processFiles(mapped, resolve);
23+
} catch (err) {
24+
showToast('Install failed: ' + err, 'error');
25+
console.error(err);
26+
resolve();
27+
}
28+
}
29+
});
30+
});
31+
}
32+
33+
function processFiles(files, resolve) {
34+
if (!files || files.length === 0) {
35+
return resolve();
36+
}
37+
38+
const metadataFile = files.find(f => f.name === 'metadata.json' || f.name.endsWith('/metadata.json'));
39+
40+
if (!metadataFile) {
41+
showToast('No metadata.json found in selected files', 'error');
42+
return resolve();
43+
}
44+
45+
// Parse metadata.json
46+
let app;
47+
try {
48+
const metadataText = new TextDecoder().decode(new Uint8Array(metadataFile.data));
49+
app = JSON.parse(metadataText);
50+
} catch(err) {
51+
showToast('Failed to parse metadata.json: ' + err, 'error');
52+
return resolve();
53+
}
54+
55+
if (!app.id || !app.storage || !Array.isArray(app.storage)) {
56+
showToast('Invalid metadata.json', 'error');
57+
return resolve();
58+
}
59+
60+
// Build file map for lookup (both simple filename and full path)
61+
const fileMap = {};
62+
files.forEach(f => {
63+
const simpleName = f.name.split('/').pop();
64+
fileMap[simpleName] = f;
65+
fileMap[f.name] = f;
66+
});
67+
68+
// Populate content directly into storage entries so AppInfo.getFiles doesn't fetch URLs
69+
app.storage.forEach(storageEntry => {
70+
const fileName = storageEntry.url || storageEntry.name;
71+
const file = fileMap[fileName];
72+
if (file) {
73+
const data = new Uint8Array(file.data);
74+
let content = "";
75+
for (let i = 0; i < data.length; i++) {
76+
content += String.fromCharCode(data[i]);
77+
}
78+
storageEntry.content = content;
79+
}
80+
});
81+
82+
// Populate content into data entries as well
83+
if (app.data && Array.isArray(app.data)) {
84+
app.data.forEach(dataEntry => {
85+
if (dataEntry.content) return; // already has inline content
86+
const fileName = dataEntry.url || dataEntry.name;
87+
const file = fileMap[fileName];
88+
if (file) {
89+
const data = new Uint8Array(file.data);
90+
let content = "";
91+
for (let i = 0; i < data.length; i++) {
92+
content += String.fromCharCode(data[i]);
93+
}
94+
dataEntry.content = content;
95+
}
96+
});
97+
}
98+
99+
showPrompt("Install App from Files",
100+
`Install "${app.name}" (${app.id}) v${app.version}?\n\nThis will delete the existing version if installed.`
101+
).then(() => {
102+
// Use standard updateApp flow (remove old, check deps, upload new)
103+
return getInstalledApps().then(() => {
104+
const isInstalled = device.appsInstalled.some(i => i.id === app.id);
105+
106+
// If installed, use update flow; otherwise use install flow
107+
const uploadPromise = isInstalled
108+
? Comms.getAppInfo(app).then(remove => {
109+
return Comms.removeApp(remove, {containsFileList:true});
110+
}).then(() => {
111+
device.appsInstalled = device.appsInstalled.filter(a => a.id != app.id);
112+
return checkDependencies(app, {checkForClashes:false});
113+
})
114+
: checkDependencies(app);
115+
116+
return uploadPromise.then(() => {
117+
return Comms.uploadApp(app, {
118+
device: device,
119+
language: LANGUAGE
120+
});
121+
}).then((appJSON) => {
122+
if (appJSON) device.appsInstalled.push(appJSON);
123+
showToast(`"${app.name}" installed!`, 'success');
124+
refreshMyApps();
125+
refreshLibrary();
126+
});
127+
});
128+
}).then(resolve).catch(err => {
129+
showToast('Install failed: ' + err, 'error');
130+
console.error(err);
131+
resolve();
132+
});
133+
}
134+
135+
// Attach UI handler to the button on window load
136+
window.addEventListener('load', (event) => {
137+
const btn = document.getElementById("installappfromfiles");
138+
if (!btn) return;
139+
btn.addEventListener("click", () => {
140+
startOperation({name:"Install App from Files"}, installFromFiles);
141+
});
142+
});

0 commit comments

Comments
 (0)