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