1- import { exec , toast } from 'kernelsu-alt' ;
1+ import { exec , spawn , toast } from 'kernelsu-alt' ;
22import { modDir , persistDir , superkey } from '../index.js' ;
33
44let allKpms = [ ] ;
@@ -147,7 +147,41 @@ async function renderKpmList() {
147147 } ) ;
148148}
149149
150- async function handleFileUpload ( accept , containerId , onLoaded ) {
150+ async function uploadFile ( file , targetPath , onProgress , signal ) {
151+ const CHUNK_SIZE = 96 * 1024 ; // 96KB chunks
152+ let offset = 0 ;
153+
154+ await exec ( `mkdir -p "$(dirname "${ targetPath } ")" && : > "${ targetPath } "` ) ;
155+
156+ while ( offset < file . size ) {
157+ if ( signal ?. aborted ) throw new DOMException ( 'Aborted' , 'AbortError' ) ;
158+ const chunk = file . slice ( offset , offset + CHUNK_SIZE ) ;
159+ const base64 = await new Promise ( ( resolve , reject ) => {
160+ const reader = new FileReader ( ) ;
161+ reader . onload = ( ) => resolve ( reader . result . split ( ',' ) [ 1 ] ) ;
162+ reader . onerror = reject ;
163+ reader . readAsDataURL ( chunk ) ;
164+ } ) ;
165+
166+ const result = await new Promise ( ( resolve , reject ) => {
167+ const child = spawn ( `echo '${ base64 } ' | base64 -d >> "${ targetPath } "` ) ;
168+ child . on ( 'exit' , ( code ) => {
169+ if ( code === 0 ) resolve ( { errno : 0 } ) ;
170+ else resolve ( { errno : code , stderr : `Exit code ${ code } ` } ) ;
171+ } ) ;
172+ child . on ( 'error' , ( err ) => reject ( err ) ) ;
173+ } ) ;
174+
175+ if ( result . errno !== 0 ) {
176+ throw new Error ( result . stderr || 'Write error' ) ;
177+ }
178+
179+ offset += CHUNK_SIZE ;
180+ if ( onProgress ) onProgress ( Math . min ( offset , file . size ) / file . size ) ;
181+ }
182+ }
183+
184+ async function handleFileUpload ( accept , containerId , onSelected ) {
151185 const input = document . createElement ( 'input' ) ;
152186 input . type = 'file' ;
153187 input . accept = accept ;
@@ -160,14 +194,18 @@ async function handleFileUpload(accept, containerId, onLoaded) {
160194 return ;
161195 }
162196
163- const reader = new FileReader ( ) ;
164-
197+ const abortController = new AbortController ( ) ;
165198 const loadingCard = document . createElement ( 'div' ) ;
166199 loadingCard . className = 'card module-card' ;
167200 loadingCard . innerHTML = `
168- <div class="module-card-header">
169- <div class="module-card-title">Uploading ${ file . name } </div>
170- <div class="module-card-subtitle">Please wait...</div>
201+ <div class="module-card-header flex-header">
202+ <div class="header-info">
203+ <div class="module-card-title">${ file . name } </div>
204+ <div class="module-card-subtitle" id="upload-progress-text">Please wait...</div>
205+ </div>
206+ <md-outlined-icon-button id="cancel-upload">
207+ <md-icon><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"/></svg></md-icon>
208+ </md-outlined-icon-button>
171209 </div>
172210 <div class="module-card-content">
173211 <md-linear-progress indeterminate></md-linear-progress>
@@ -176,35 +214,43 @@ async function handleFileUpload(accept, containerId, onLoaded) {
176214 const container = document . getElementById ( containerId ) ;
177215 container . prepend ( loadingCard ) ;
178216
179- reader . onabort = ( ) => loadingCard . remove ( ) ;
180- reader . onerror = ( ) => {
181- loadingCard . remove ( ) ;
182- toast ( 'Failed to read file' ) ;
217+ const progressBar = loadingCard . querySelector ( 'md-linear-progress' ) ;
218+ const progressText = loadingCard . querySelector ( '#upload-progress-text' ) ;
219+ const cancelBtn = loadingCard . querySelector ( '#cancel-upload' ) ;
220+
221+ cancelBtn . onclick = ( ) => {
222+ abortController . abort ( ) ;
183223 } ;
184- reader . onload = async ( ) => {
185- loadingCard . remove ( ) ;
186- const base64 = reader . result . split ( ',' ) [ 1 ] ;
187- await onLoaded ( file , base64 ) ;
224+
225+ const onProgress = ( percent ) => {
226+ const p = Math . round ( percent * 100 ) ;
227+ progressBar . value = percent ;
228+ progressBar . indeterminate = false ;
229+ progressText . textContent = `Uploading... ${ p } %` ;
188230 } ;
189- reader . readAsDataURL ( file ) ;
231+
232+ try {
233+ await onSelected ( file , onProgress , abortController . signal ) ;
234+ } catch ( err ) {
235+ if ( err . name === 'AbortError' ) {
236+ toast ( 'Upload cancelled' ) ;
237+ } else {
238+ toast ( `Error: ${ err . message } ` ) ;
239+ }
240+ } finally {
241+ loadingCard . remove ( ) ;
242+ }
190243 } ;
191244 input . click ( ) ;
192245}
193246
194247async function uploadAndLoadModule ( ) {
195- handleFileUpload ( '.kpm' , 'kpm-list' , async ( file , base64 ) => {
248+ handleFileUpload ( '.kpm' , 'kpm-list' , async ( file , onProgress , signal ) => {
249+ const tmpPath = `${ modDir } /tmp/${ file . name } ` ;
196250 try {
197- const result = await exec ( `
198- mkdir -p ${ modDir } /tmp
199- rm -rf ${ modDir } /tmp/*
200- echo '${ base64 } ' | base64 -d > ${ modDir } /tmp/${ file . name }
201- ` ) ;
202- if ( result . errno !== 0 ) {
203- toast ( `Failed to write file: ${ result . stderr } ` ) ;
204- return ;
205- }
206-
207- const info = await getKpmInfo ( `${ modDir } /tmp/${ file . name } ` ) ;
251+ await exec ( `mkdir -p ${ modDir } /tmp && rm -rf ${ modDir } /tmp/*` ) ;
252+ await uploadFile ( file , tmpPath , onProgress , signal ) ;
253+ const info = await getKpmInfo ( tmpPath ) ;
208254 if ( info && info . name ) {
209255 const dialog = document . getElementById ( 'load-dialog' ) ;
210256 dialog . querySelector ( '#module-name' ) . textContent = info . name ;
@@ -241,7 +287,8 @@ async function uploadAndLoadModule() {
241287 exec ( `rm -rf ${ modDir } /tmp` ) ;
242288 }
243289 } catch ( e ) {
244- toast ( `Error: ${ e . message } ` ) ;
290+ exec ( `rm -rf ${ modDir } /tmp` ) ;
291+ throw e ;
245292 }
246293 } ) ;
247294}
@@ -285,4 +332,4 @@ export function initKPMPage() {
285332 refreshKpmList ( ) ;
286333}
287334
288- export { loadModule , refreshKpmList , uploadAndLoadModule , handleFileUpload }
335+ export { loadModule , refreshKpmList , uploadAndLoadModule , handleFileUpload , uploadFile }
0 commit comments