@@ -48,15 +48,7 @@ <h1>{{SITE_TITLE}}</h1>
48
48
< button id ="uploadButton " class ="upload-button " style ="display: none; "> Upload Files</ button >
49
49
</ div >
50
50
51
- < style >
52
- .button-group {
53
- display : flex;
54
- gap : 10px ;
55
- justify-content : center;
56
- }
57
- </ style >
58
-
59
- < script >
51
+ < script defer >
60
52
const CHUNK_SIZE = 1024 * 1024 ; // 1MB chunks
61
53
const MAX_RETRIES = 3 ;
62
54
const RETRY_DELAY = 1000 ;
@@ -67,23 +59,178 @@ <h1>{{SITE_TITLE}}</h1>
67
59
return `${ Date . now ( ) } -${ Math . random ( ) . toString ( 36 ) . substr ( 2 , 9 ) } ` ;
68
60
}
69
61
62
+ // Utility function to format file sizes
63
+ function formatFileSize ( bytes ) {
64
+ if ( bytes === 0 ) return '0 Bytes' ;
65
+ const k = 1024 ;
66
+ const sizes = [ 'Bytes' , 'KB' , 'MB' , 'GB' ] ;
67
+ const i = Math . floor ( Math . log ( bytes ) / Math . log ( k ) ) ;
68
+ return parseFloat ( ( bytes / Math . pow ( k , i ) ) . toFixed ( 2 ) ) + ' ' + sizes [ i ] ;
69
+ }
70
+
70
71
class FileUploader {
71
72
constructor ( file , batchId ) {
72
73
this . file = file ;
73
74
this . batchId = batchId ;
74
75
this . uploadId = null ;
75
76
this . position = 0 ;
77
+ this . bytesReceived = 0 ;
76
78
this . progressElement = null ;
77
79
this . retries = 0 ;
80
+ this . startTime = null ;
81
+ this . lastUpdate = null ;
82
+ this . lastBytes = 0 ;
83
+ this . speedSamples = [ ] ;
84
+ this . currentSpeed = 0 ;
85
+ this . speedUpdateTimer = null ;
86
+ this . progressUpdateTimer = null ;
87
+ this . waitingMessages = [
88
+ "Preparing upload..." ,
89
+ "Establishing connection..." ,
90
+ "Starting transfer..." ,
91
+ "Waiting for first chunk..."
92
+ ] ;
93
+ this . waitingMessageIndex = 0 ;
94
+ this . waitingMessageInterval = null ;
95
+ this . dotsCount = 1 ;
96
+ this . dotsIncreasing = true ;
97
+ }
98
+
99
+ cycleWaitingMessage ( ) {
100
+ if ( ! this . progressElement || ! this . progressElement . infoSpan ) return ;
101
+
102
+ if ( this . waitingMessageIndex < this . waitingMessages . length ) {
103
+ this . progressElement . infoSpan . textContent = this . waitingMessages [ this . waitingMessageIndex ] ;
104
+ this . waitingMessageIndex ++ ;
105
+
106
+ // When we finish the initial messages, add a delay before switching to dot animation
107
+ if ( this . waitingMessageIndex >= this . waitingMessages . length ) {
108
+ clearInterval ( this . waitingMessageInterval ) ;
109
+ // Keep the last message visible for 2 seconds before starting dot animation
110
+ setTimeout ( ( ) => {
111
+ this . waitingMessageInterval = setInterval ( ( ) => this . cycleWaitingMessage ( ) , 500 ) ;
112
+ } , 2000 ) ;
113
+ }
114
+ } else {
115
+ // Create the dots string
116
+ const dots = '.' . repeat ( this . dotsCount ) ;
117
+ this . progressElement . infoSpan . textContent = `Still working${ dots } ` ;
118
+
119
+ // Update dots count
120
+ if ( this . dotsIncreasing ) {
121
+ this . dotsCount ++ ;
122
+ if ( this . dotsCount > 6 ) {
123
+ this . dotsCount = 1 ;
124
+ }
125
+ }
126
+ }
127
+ }
128
+
129
+ startWaitingMessages ( ) {
130
+ if ( this . waitingMessageInterval ) {
131
+ clearInterval ( this . waitingMessageInterval ) ;
132
+ }
133
+ // Start with 2 second interval for main messages
134
+ this . waitingMessageInterval = setInterval ( ( ) => this . cycleWaitingMessage ( ) , 2000 ) ;
135
+ this . cycleWaitingMessage ( ) ; // Show first message immediately
136
+ }
137
+
138
+ stopWaitingMessages ( ) {
139
+ if ( this . waitingMessageInterval ) {
140
+ clearInterval ( this . waitingMessageInterval ) ;
141
+ this . waitingMessageInterval = null ;
142
+ }
143
+ }
144
+
145
+ calculateSpeed ( bytesReceived ) {
146
+ const now = Date . now ( ) ;
147
+
148
+ // Only calculate speed if at least 1 second has passed since last update
149
+ if ( ! this . lastUpdate || ( now - this . lastUpdate ) >= 1000 ) {
150
+ if ( this . lastUpdate ) {
151
+ const timeDiff = ( now - this . lastUpdate ) / 1000 ; // Convert to seconds
152
+ const bytesDiff = bytesReceived - this . lastBytes ;
153
+ const instantSpeed = bytesDiff / timeDiff ; // Bytes per second
154
+
155
+ // Keep last 3 samples for smoother average
156
+ this . speedSamples . push ( instantSpeed ) ;
157
+ if ( this . speedSamples . length > 3 ) {
158
+ this . speedSamples . shift ( ) ;
159
+ }
160
+
161
+ // Calculate weighted moving average with more weight on recent samples
162
+ const weights = [ 0.5 , 0.3 , 0.2 ] ;
163
+ const samples = this . speedSamples . slice ( ) . reverse ( ) ; // Most recent first
164
+ this . currentSpeed = samples . reduce ( ( acc , speed , i ) => {
165
+ return acc + ( speed * ( weights [ i ] || 0 ) ) ;
166
+ } , 0 ) ;
167
+ }
168
+ this . lastUpdate = now ;
169
+ this . lastBytes = bytesReceived ;
170
+ }
171
+
172
+ return this . currentSpeed ;
173
+ }
174
+
175
+ formatSpeed ( bytesPerSecond ) {
176
+ const units = [ 'B/s' , 'KB/s' , 'MB/s' , 'GB/s' ] ;
177
+ let value = bytesPerSecond ;
178
+ let unitIndex = 0 ;
179
+
180
+ while ( value >= 1024 && unitIndex < units . length - 1 ) {
181
+ value /= 1024 ;
182
+ unitIndex ++ ;
183
+ }
184
+
185
+ return `${ value . toFixed ( 1 ) } ${ units [ unitIndex ] } ` ;
186
+ }
187
+
188
+ calculateTimeRemaining ( bytesReceived , speed ) {
189
+ if ( speed === 0 ) return 'calculating...' ;
190
+ const bytesRemaining = this . file . size - bytesReceived ;
191
+ const secondsRemaining = bytesRemaining / speed ;
192
+
193
+ if ( secondsRemaining < 60 ) {
194
+ return `${ Math . ceil ( secondsRemaining ) } s` ;
195
+ } else if ( secondsRemaining < 3600 ) {
196
+ return `${ Math . ceil ( secondsRemaining / 60 ) } m` ;
197
+ } else {
198
+ return `${ Math . ceil ( secondsRemaining / 3600 ) } h` ;
199
+ }
78
200
}
79
201
80
202
async start ( ) {
81
203
try {
204
+ this . startTime = Date . now ( ) ;
205
+ // Initialize speed update timer
206
+ this . speedUpdateTimer = setInterval ( ( ) => {
207
+ if ( this . bytesReceived > 0 ) {
208
+ this . calculateSpeed ( this . bytesReceived ) ;
209
+ }
210
+ } , 1000 ) ;
211
+
212
+ // Add progress update timer for more frequent UI updates
213
+ this . progressUpdateTimer = setInterval ( ( ) => {
214
+ if ( this . bytesReceived > 0 ) {
215
+ const progress = ( this . bytesReceived / this . file . size ) * 100 ;
216
+ this . updateProgress ( progress ) ;
217
+ }
218
+ } , 200 ) ; // Update every 200ms
219
+
82
220
await this . initUpload ( ) ;
83
221
await this . uploadChunks ( ) ;
222
+
223
+ // Clear the timers when upload is complete
224
+ this . clearTimers ( ) ;
84
225
return true ;
85
226
} catch ( error ) {
227
+ this . clearTimers ( ) ;
86
228
console . error ( 'Upload failed:' , error ) ;
229
+ if ( this . progressElement ) {
230
+ this . progressElement . infoSpan . textContent = `Error: ${ error . message } ` ;
231
+ this . progressElement . infoSpan . style . color = 'var(--danger-color)' ;
232
+ }
233
+ this . stopWaitingMessages ( ) ;
87
234
return false ;
88
235
}
89
236
}
@@ -142,16 +289,19 @@ <h1>{{SITE_TITLE}}</h1>
142
289
}
143
290
144
291
async uploadChunk ( chunk ) {
292
+ const controller = new AbortController ( ) ;
145
293
const response = await fetch ( `/upload/chunk/${ this . uploadId } ` , {
146
294
method : 'POST' ,
147
295
headers : {
148
296
'Content-Type' : 'application/octet-stream'
149
297
} ,
150
- body : chunk
298
+ body : chunk ,
299
+ signal : controller . signal
151
300
} ) ;
152
301
153
302
if ( ! response . ok ) throw new Error ( 'Chunk upload failed' ) ;
154
303
const data = await response . json ( ) ;
304
+ this . bytesReceived = Math . floor ( ( data . progress / 100 ) * this . file . size ) ;
155
305
this . updateProgress ( data . progress ) ;
156
306
}
157
307
@@ -161,10 +311,15 @@ <h1>{{SITE_TITLE}}</h1>
161
311
162
312
const label = document . createElement ( 'div' ) ;
163
313
label . className = 'progress-label' ;
314
+
315
+ const pathSpan = document . createElement ( 'span' ) ;
316
+ pathSpan . className = 'progress-path' ;
164
317
const displayPath = this . file . relativePath ?
165
- `📁 ${ this . file . relativePath . split ( '/' ) [ 0 ] } / ` :
318
+ `📁 ${ this . file . relativePath } ${ this . file . name } ` :
166
319
`📄 ${ this . file . name } ` ;
167
- label . textContent = displayPath ;
320
+ pathSpan . textContent = displayPath ;
321
+
322
+ label . appendChild ( pathSpan ) ;
168
323
169
324
const progress = document . createElement ( 'div' ) ;
170
325
progress . className = 'progress' ;
@@ -173,24 +328,73 @@ <h1>{{SITE_TITLE}}</h1>
173
328
bar . className = 'progress-bar' ;
174
329
bar . style . width = '0%' ;
175
330
331
+ const statusDiv = document . createElement ( 'div' ) ;
332
+ statusDiv . className = 'progress-status' ;
333
+
334
+ const infoSpan = document . createElement ( 'div' ) ;
335
+ infoSpan . className = 'progress-info' ;
336
+
337
+ const details = document . createElement ( 'div' ) ;
338
+ details . className = 'progress-details' ;
339
+
340
+ statusDiv . appendChild ( infoSpan ) ;
341
+ statusDiv . appendChild ( details ) ;
342
+
176
343
progress . appendChild ( bar ) ;
177
344
container . appendChild ( label ) ;
178
345
container . appendChild ( progress ) ;
346
+ container . appendChild ( statusDiv ) ;
179
347
180
348
document . getElementById ( 'uploadProgress' ) . appendChild ( container ) ;
181
- this . progressElement = { container, bar } ;
349
+ this . progressElement = { container, bar, infoSpan, details } ;
350
+
351
+ // Start cycling waiting messages
352
+ this . startWaitingMessages ( ) ;
182
353
}
183
354
184
355
updateProgress ( percent ) {
185
356
if ( this . progressElement ) {
186
- this . progressElement . bar . style . width = `${ percent } %` ;
357
+ // Use the current speed value instead of calculating it every time
358
+ const speed = this . currentSpeed ;
359
+
360
+ // Stop waiting messages once we start receiving data
361
+ if ( this . bytesReceived > 0 ) {
362
+ this . stopWaitingMessages ( ) ;
363
+ }
364
+
365
+ const timeRemaining = this . calculateTimeRemaining ( this . bytesReceived , speed ) ;
366
+
367
+ this . progressElement . bar . style . width = `${ percent . toFixed ( 1 ) } %` ;
368
+
369
+ // Only show speed and time if we've received data
370
+ if ( this . bytesReceived > 0 ) {
371
+ this . progressElement . infoSpan . textContent = `${ this . formatSpeed ( speed ) } · ${ timeRemaining } ` ;
372
+ }
373
+
374
+ // Update details with progress and file size
375
+ this . progressElement . details . textContent =
376
+ `${ formatFileSize ( this . bytesReceived ) } of ${ formatFileSize ( this . file . size ) } (${ percent . toFixed ( 1 ) } %)` ;
377
+
187
378
if ( percent === 100 ) {
379
+ this . stopWaitingMessages ( ) ;
380
+ this . clearTimers ( ) ;
188
381
setTimeout ( ( ) => {
189
382
this . progressElement . container . remove ( ) ;
190
383
} , 1000 ) ;
191
384
}
192
385
}
193
386
}
387
+
388
+ clearTimers ( ) {
389
+ if ( this . speedUpdateTimer ) {
390
+ clearInterval ( this . speedUpdateTimer ) ;
391
+ this . speedUpdateTimer = null ;
392
+ }
393
+ if ( this . progressUpdateTimer ) {
394
+ clearInterval ( this . progressUpdateTimer ) ;
395
+ this . progressUpdateTimer = null ;
396
+ }
397
+ }
194
398
}
195
399
196
400
// UI Event Handlers
@@ -375,14 +579,6 @@ <h1>{{SITE_TITLE}}</h1>
375
579
uploadButton . style . display = ( ! AUTO_UPLOAD && files . length > 0 ) ? 'block' : 'none' ;
376
580
}
377
581
378
- function formatFileSize ( bytes ) {
379
- if ( bytes === 0 ) return '0 Bytes' ;
380
- const k = 1024 ;
381
- const sizes = [ 'Bytes' , 'KB' , 'MB' , 'GB' ] ;
382
- const i = Math . floor ( Math . log ( bytes ) / Math . log ( k ) ) ;
383
- return parseFloat ( ( bytes / Math . pow ( k , i ) ) . toFixed ( 2 ) ) + ' ' + sizes [ i ] ;
384
- }
385
-
386
582
async function startUploads ( ) {
387
583
uploadButton . disabled = true ;
388
584
document . getElementById ( 'uploadProgress' ) . innerHTML = '' ;
0 commit comments