Skip to content

Commit d4edefa

Browse files
committed
apploader - install-app-from-files - add onCancel and onComplete to fix operation close and simplify multifile upload
1 parent bcbe4cd commit d4edefa

File tree

1 file changed

+105
-122
lines changed

1 file changed

+105
-122
lines changed

install_from_files.js

Lines changed: 105 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -6,150 +6,133 @@
66
*/
77
function installFromFiles() {
88
return new Promise(resolve => {
9-
const MAX_WAIT_MS = 5000;
10-
const RESCHEDULE_MS = 400;
9+
10+
// Collect all files
11+
const fileCollection = {
12+
files: []
13+
};
1114

1215
// Request multi-file selection from user
1316
Espruino.Core.Utils.fileOpenDialog({
1417
id:"installappfiles",
1518
type:"arraybuffer",
1619
multi:true,
17-
mimeType:"*/*"}, function(fileData, mimeType, fileName) {
18-
19-
// Collect all files (callback is invoked once per file when multi:true)
20-
if (!installFromFiles.fileCollection) {
21-
installFromFiles.fileCollection = {
22-
files: [],
23-
firstTs: Date.now()
24-
};
25-
}
20+
mimeType:"*/*",
21+
onCancel: function() {
22+
resolve();
23+
},
24+
onComplete: function() {
25+
processFiles(fileCollection.files, resolve);
26+
}}, function(fileData, mimeType, fileName) {
2627

27-
installFromFiles.fileCollection.files.push({
28+
// Collect each file as callback is invoked
29+
fileCollection.files.push({
2830
name: fileName,
2931
data: fileData
3032
});
33+
});
34+
});
35+
}
3136

32-
clearTimeout(installFromFiles.processTimeout);
33-
34-
// ANDROID FIX: Debounce until metadata.json appears or timeout
35-
// Desktop browsers deliver all files quickly; Android can have 100-500ms gaps
36-
installFromFiles.processTimeout = setTimeout(function processSelection() {
37-
const fc = installFromFiles.fileCollection;
38-
const files = fc ? fc.files : null;
39-
40-
if (!files || files.length === 0) {
41-
if (fc && (Date.now() - fc.firstTs) < MAX_WAIT_MS) {
42-
installFromFiles.processTimeout = setTimeout(processSelection, RESCHEDULE_MS);
43-
return;
44-
}
45-
installFromFiles.fileCollection = null;
46-
return resolve();
47-
}
48-
49-
const metadataFile = files.find(f => f.name === 'metadata.json' || f.name.endsWith('/metadata.json'));
37+
function processFiles(files, resolve) {
38+
if (!files || files.length === 0) {
39+
return resolve();
40+
}
5041

51-
if (!metadataFile) {
52-
if (fc && (Date.now() - fc.firstTs) < MAX_WAIT_MS) {
53-
installFromFiles.processTimeout = setTimeout(processSelection, RESCHEDULE_MS);
54-
return;
55-
}
56-
installFromFiles.fileCollection = null;
57-
showToast('No metadata.json found in selected files', 'error');
58-
return resolve();
59-
}
42+
const metadataFile = files.find(f => f.name === 'metadata.json' || f.name.endsWith('/metadata.json'));
6043

61-
installFromFiles.fileCollection = null;
44+
if (!metadataFile) {
45+
showToast('No metadata.json found in selected files', 'error');
46+
return resolve();
47+
}
6248

63-
// Parse metadata.json
64-
let app;
65-
try {
66-
const metadataText = new TextDecoder().decode(new Uint8Array(metadataFile.data));
67-
app = JSON.parse(metadataText);
68-
} catch(err) {
69-
showToast('Failed to parse metadata.json: ' + err, 'error');
70-
return resolve();
71-
}
49+
// Parse metadata.json
50+
let app;
51+
try {
52+
const metadataText = new TextDecoder().decode(new Uint8Array(metadataFile.data));
53+
app = JSON.parse(metadataText);
54+
} catch(err) {
55+
showToast('Failed to parse metadata.json: ' + err, 'error');
56+
return resolve();
57+
}
7258

73-
if (!app.id || !app.storage || !Array.isArray(app.storage)) {
74-
showToast('Invalid metadata.json', 'error');
75-
return resolve();
76-
}
59+
if (!app.id || !app.storage || !Array.isArray(app.storage)) {
60+
showToast('Invalid metadata.json', 'error');
61+
return resolve();
62+
}
7763

78-
// Build file map for lookup (both simple filename and full path)
79-
const fileMap = {};
80-
files.forEach(f => {
81-
const simpleName = f.name.split('/').pop();
82-
fileMap[simpleName] = f;
83-
fileMap[f.name] = f;
84-
});
64+
// Build file map for lookup (both simple filename and full path)
65+
const fileMap = {};
66+
files.forEach(f => {
67+
const simpleName = f.name.split('/').pop();
68+
fileMap[simpleName] = f;
69+
fileMap[f.name] = f;
70+
});
8571

86-
// Populate content directly into storage entries so AppInfo.getFiles doesn't fetch URLs
87-
app.storage.forEach(storageEntry => {
88-
const fileName = storageEntry.url || storageEntry.name;
89-
const file = fileMap[fileName];
90-
if (file) {
91-
const data = new Uint8Array(file.data);
92-
let content = "";
93-
for (let i = 0; i < data.length; i++) {
94-
content += String.fromCharCode(data[i]);
95-
}
96-
storageEntry.content = content;
97-
}
98-
});
72+
// Populate content directly into storage entries so AppInfo.getFiles doesn't fetch URLs
73+
app.storage.forEach(storageEntry => {
74+
const fileName = storageEntry.url || storageEntry.name;
75+
const file = fileMap[fileName];
76+
if (file) {
77+
const data = new Uint8Array(file.data);
78+
let content = "";
79+
for (let i = 0; i < data.length; i++) {
80+
content += String.fromCharCode(data[i]);
81+
}
82+
storageEntry.content = content;
83+
}
84+
});
9985

100-
// Populate content into data entries as well
101-
if (app.data && Array.isArray(app.data)) {
102-
app.data.forEach(dataEntry => {
103-
if (dataEntry.content) return; // already has inline content
104-
const fileName = dataEntry.url || dataEntry.name;
105-
const file = fileMap[fileName];
106-
if (file) {
107-
const data = new Uint8Array(file.data);
108-
let content = "";
109-
for (let i = 0; i < data.length; i++) {
110-
content += String.fromCharCode(data[i]);
111-
}
112-
dataEntry.content = content;
113-
}
114-
});
86+
// Populate content into data entries as well
87+
if (app.data && Array.isArray(app.data)) {
88+
app.data.forEach(dataEntry => {
89+
if (dataEntry.content) return; // already has inline content
90+
const fileName = dataEntry.url || dataEntry.name;
91+
const file = fileMap[fileName];
92+
if (file) {
93+
const data = new Uint8Array(file.data);
94+
let content = "";
95+
for (let i = 0; i < data.length; i++) {
96+
content += String.fromCharCode(data[i]);
11597
}
98+
dataEntry.content = content;
99+
}
100+
});
101+
}
116102

117-
showPrompt("Install App from Files",
118-
`Install "${app.name}" (${app.id}) v${app.version}?\n\nThis will delete the existing version if installed.`
119-
).then(() => {
120-
// Use standard updateApp flow (remove old, check deps, upload new)
121-
return getInstalledApps().then(() => {
122-
const isInstalled = device.appsInstalled.some(i => i.id === app.id);
123-
124-
// If installed, use update flow; otherwise use install flow
125-
const uploadPromise = isInstalled
126-
? Comms.getAppInfo(app).then(remove => {
127-
return Comms.removeApp(remove, {containsFileList:true});
128-
}).then(() => {
129-
device.appsInstalled = device.appsInstalled.filter(a => a.id != app.id);
130-
return checkDependencies(app, {checkForClashes:false});
131-
})
132-
: checkDependencies(app);
133-
134-
return uploadPromise.then(() => {
135-
return Comms.uploadApp(app, {
136-
device: device,
137-
language: LANGUAGE
138-
});
139-
}).then((appJSON) => {
140-
if (appJSON) device.appsInstalled.push(appJSON);
141-
showToast(`"${app.name}" installed!`, 'success');
142-
refreshMyApps();
143-
refreshLibrary();
144-
});
145-
});
146-
}).then(resolve).catch(err => {
147-
showToast('Install failed: ' + err, 'error');
148-
console.error(err);
149-
resolve();
103+
showPrompt("Install App from Files",
104+
`Install "${app.name}" (${app.id}) v${app.version}?\n\nThis will delete the existing version if installed.`
105+
).then(() => {
106+
// Use standard updateApp flow (remove old, check deps, upload new)
107+
return getInstalledApps().then(() => {
108+
const isInstalled = device.appsInstalled.some(i => i.id === app.id);
109+
110+
// If installed, use update flow; otherwise use install flow
111+
const uploadPromise = isInstalled
112+
? Comms.getAppInfo(app).then(remove => {
113+
return Comms.removeApp(remove, {containsFileList:true});
114+
}).then(() => {
115+
device.appsInstalled = device.appsInstalled.filter(a => a.id != app.id);
116+
return checkDependencies(app, {checkForClashes:false});
117+
})
118+
: checkDependencies(app);
119+
120+
return uploadPromise.then(() => {
121+
return Comms.uploadApp(app, {
122+
device: device,
123+
language: LANGUAGE
150124
});
151-
}, 1200);
125+
}).then((appJSON) => {
126+
if (appJSON) device.appsInstalled.push(appJSON);
127+
showToast(`"${app.name}" installed!`, 'success');
128+
refreshMyApps();
129+
refreshLibrary();
130+
});
152131
});
132+
}).then(resolve).catch(err => {
133+
showToast('Install failed: ' + err, 'error');
134+
console.error(err);
135+
resolve();
153136
});
154137
}
155138

0 commit comments

Comments
 (0)