mbkbucket 3.8.3 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +15 -14
- package/README.md +561 -353
- package/index.js +32 -30
- package/lib/public/bucketadmin.css +2042 -1708
- package/lib/public/bucketadmin.js +1622 -1286
- package/lib/public/mbkbucket-helper-demo.html +4 -24
- package/lib/public/mbkbucket-helper.css +28 -34
- package/lib/public/mbkbucket-helper.js +4 -3
- package/lib/src/config/validation.js +86 -73
- package/lib/src/controllers/api.controller.js +459 -410
- package/lib/src/controllers/dashboard.controller.js +69 -49
- package/lib/src/controllers/info.controller.js +34 -3
- package/lib/src/controllers/view.controller.js +233 -231
- package/lib/src/core/storage/errors.js +62 -0
- package/lib/src/core/storage/gdrive-auth.js +193 -0
- package/lib/src/core/storage/gdrive-provider.js +656 -0
- package/lib/src/core/storage/index.js +14 -0
- package/lib/src/core/storage/models.js +80 -0
- package/lib/src/core/storage/path-strategy.js +65 -0
- package/lib/src/core/storage/s3-provider.js +491 -0
- package/lib/src/core/storage/storage-manager.js +203 -0
- package/lib/src/core/storage/storage-provider.js +217 -0
- package/lib/src/middleware/bucket-resolver.js +37 -37
- package/lib/src/routes/api.routes.js +5 -1
- package/lib/src/routes/index.js +60 -60
- package/lib/src/routes/view.routes.js +28 -25
- package/lib/src/services/s3.service.js +42 -594
- package/lib/src/services/storage.service.js +213 -0
- package/lib/src/utils/errors.js +56 -46
- package/package.json +1 -1
- package/views/bucket.hbs +47 -71
- package/views/bucketadmin_move_modal.hbs +35 -0
- package/views/bucketadmin_preview_modal.hbs +108 -40
- package/views/bucketadmincontent.hbs +193 -144
- package/views/mbkbucket_info.hbs +134 -232
|
@@ -1,1286 +1,1622 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MBKBucket
|
|
3
|
-
* Copyright (c) 2026 Muhammad Bin Khalid, MBKTech.org and contributors
|
|
4
|
-
* Licensed under the MIT License.
|
|
5
|
-
* Source: https://github.com/MIbnEKhalid/mbkbucket
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* MBKBucket File Manager - UI & Client Logic
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
(function () {
|
|
13
|
-
'use strict';
|
|
14
|
-
|
|
15
|
-
// Global State
|
|
16
|
-
let currentPage = 1;
|
|
17
|
-
let currentPrefix = '';
|
|
18
|
-
let currentSearch = '';
|
|
19
|
-
let currentSort = 'name';
|
|
20
|
-
let currentOrder = 'asc';
|
|
21
|
-
let currentViewMode = localStorage.getItem('mbk_view_mode') || 'grid'; // 'grid' or 'list'
|
|
22
|
-
let currentQuickFilter = 'all'; // 'all', 'media', 'docs', 'code', 'starred'
|
|
23
|
-
let showThumbnails = localStorage.getItem('mbk_show_thumbnails') === 'true'; // Fast Mode by default
|
|
24
|
-
let thumbObserver = null;
|
|
25
|
-
let pageFiles = [];
|
|
26
|
-
let pageFolders = [];
|
|
27
|
-
let currentApiPrefix = '';
|
|
28
|
-
let currentNextToken = null;
|
|
29
|
-
let selectedFiles = new Set();
|
|
30
|
-
let starredFiles = new Set(JSON.parse(localStorage.getItem('mbk_starred_keys') || '[]'));
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
let
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
function
|
|
97
|
-
return
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function
|
|
138
|
-
return withBucketUrl(`/mbkbucket/
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
return
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
if (!normalizedApiPrefix) return
|
|
188
|
-
if (
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
(
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const
|
|
214
|
-
if (
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
}
|
|
425
|
-
return;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
if (
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
<div
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
<div class="fm-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
const
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
if (
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
const
|
|
837
|
-
if (
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
// Delete
|
|
841
|
-
function
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
const
|
|
907
|
-
|
|
908
|
-
if (
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
const
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
const
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
if (
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
document.
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
const
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
})
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
document.getElementById('
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
document.
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
const
|
|
1223
|
-
if (!
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
//
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1
|
+
/**
|
|
2
|
+
* MBKBucket
|
|
3
|
+
* Copyright (c) 2026 Muhammad Bin Khalid, MBKTech.org and contributors
|
|
4
|
+
* Licensed under the MIT License.
|
|
5
|
+
* Source: https://github.com/MIbnEKhalid/mbkbucket
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* MBKBucket File Manager - UI & Client Logic
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
(function () {
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
// Global State
|
|
16
|
+
let currentPage = 1;
|
|
17
|
+
let currentPrefix = '';
|
|
18
|
+
let currentSearch = '';
|
|
19
|
+
let currentSort = 'name';
|
|
20
|
+
let currentOrder = 'asc';
|
|
21
|
+
let currentViewMode = localStorage.getItem('mbk_view_mode') || 'grid'; // 'grid' or 'list'
|
|
22
|
+
let currentQuickFilter = 'all'; // 'all', 'media', 'docs', 'code', 'starred'
|
|
23
|
+
let showThumbnails = localStorage.getItem('mbk_show_thumbnails') === 'true'; // Fast Mode by default
|
|
24
|
+
let thumbObserver = null;
|
|
25
|
+
let pageFiles = [];
|
|
26
|
+
let pageFolders = [];
|
|
27
|
+
let currentApiPrefix = '';
|
|
28
|
+
let currentNextToken = null;
|
|
29
|
+
let selectedFiles = new Set();
|
|
30
|
+
let starredFiles = new Set(JSON.parse(localStorage.getItem('mbk_starred_keys') || '[]'));
|
|
31
|
+
let currentPreviewKey = '';
|
|
32
|
+
|
|
33
|
+
// Upload State
|
|
34
|
+
let uploadQueue = [];
|
|
35
|
+
let isUploading = false;
|
|
36
|
+
|
|
37
|
+
// DOM Elements & Config
|
|
38
|
+
const bucketAppData = document.getElementById('bucketAppData');
|
|
39
|
+
const bucketAppName = bucketAppData?.dataset.appName || '';
|
|
40
|
+
const isPublicViewEnabled = bucketAppData?.dataset.publicViewEnabled === 'true';
|
|
41
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
42
|
+
const selectedBucket = urlParams.get('bucket') || '';
|
|
43
|
+
let activeBucket = selectedBucket;
|
|
44
|
+
const app = String(bucketAppName || '').toLowerCase();
|
|
45
|
+
const isRootApp = app === 'portal' || app === 'mbkbucket';
|
|
46
|
+
const rootLabel = isRootApp ? 'My Drive' : (bucketAppName || 'Bucket Drive');
|
|
47
|
+
|
|
48
|
+
// File categories & extensions
|
|
49
|
+
const MEDIA_EXTS = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico', 'mp4', 'webm', 'ogg', 'avi', 'mov', 'mp3', 'wav', 'flac', 'aac', 'm4a']);
|
|
50
|
+
const DOC_EXTS = new Set(['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'md', 'csv', 'rtf']);
|
|
51
|
+
const CODE_EXTS = new Set(['js', 'ts', 'jsx', 'tsx', 'html', 'htm', 'css', 'scss', 'php', 'py', 'java', 'cpp', 'c', 'h', 'cs', 'rb', 'go', 'rs', 'sql', 'sh', 'bat', 'ps1', 'yaml', 'yml', 'toml', 'ini', 'conf', 'json', 'xml', 'log', 'env']);
|
|
52
|
+
|
|
53
|
+
const FILE_TYPES = {
|
|
54
|
+
pdf: { color: 'var(--red)', bg: 'var(--red-lt)', label: 'PDF' },
|
|
55
|
+
doc: { color: 'var(--blue)', bg: 'var(--blue-lt)', label: 'DOC' },
|
|
56
|
+
docx: { color: 'var(--blue)', bg: 'var(--blue-lt)', label: 'DOC' },
|
|
57
|
+
xls: { color: 'var(--green)', bg: 'var(--green-lt)', label: 'XLS' },
|
|
58
|
+
xlsx: { color: 'var(--green)', bg: 'var(--green-lt)', label: 'XLS' },
|
|
59
|
+
csv: { color: 'var(--green)', bg: 'var(--green-lt)', label: 'CSV' },
|
|
60
|
+
ppt: { color: 'var(--orange)', bg: 'rgba(247, 103, 7, 0.1)', label: 'PPT' },
|
|
61
|
+
pptx: { color: 'var(--orange)', bg: 'rgba(247, 103, 7, 0.1)', label: 'PPT' },
|
|
62
|
+
txt: { color: 'var(--text-secondary)', bg: 'var(--bg-surface-secondary)', label: 'TXT' },
|
|
63
|
+
md: { color: 'var(--purple)', bg: 'var(--purple-lt)', label: 'MD' },
|
|
64
|
+
jpg: { color: 'var(--yellow)', bg: 'var(--yellow-lt)', label: 'IMG' },
|
|
65
|
+
jpeg: { color: 'var(--yellow)', bg: 'var(--yellow-lt)', label: 'IMG' },
|
|
66
|
+
png: { color: 'var(--yellow)', bg: 'var(--yellow-lt)', label: 'IMG' },
|
|
67
|
+
gif: { color: 'var(--yellow)', bg: 'var(--yellow-lt)', label: 'GIF' },
|
|
68
|
+
webp: { color: 'var(--yellow)', bg: 'var(--yellow-lt)', label: 'IMG' },
|
|
69
|
+
svg: { color: 'var(--orange)', bg: 'rgba(247, 103, 7, 0.1)', label: 'SVG' },
|
|
70
|
+
mp4: { color: 'var(--azure)', bg: 'var(--azure-lt)', label: 'VID' },
|
|
71
|
+
webm: { color: 'var(--azure)', bg: 'var(--azure-lt)', label: 'VID' },
|
|
72
|
+
mov: { color: 'var(--azure)', bg: 'var(--azure-lt)', label: 'VID' },
|
|
73
|
+
mp3: { color: 'var(--purple)', bg: 'var(--purple-lt)', label: 'AUD' },
|
|
74
|
+
wav: { color: 'var(--purple)', bg: 'var(--purple-lt)', label: 'AUD' },
|
|
75
|
+
zip: { color: 'var(--text-secondary)', bg: 'var(--bg-surface-secondary)', label: 'ZIP' },
|
|
76
|
+
tar: { color: 'var(--text-secondary)', bg: 'var(--bg-surface-secondary)', label: 'TAR' },
|
|
77
|
+
gz: { color: 'var(--text-secondary)', bg: 'var(--bg-surface-secondary)', label: 'GZ' },
|
|
78
|
+
code: { color: 'var(--cyan)', bg: 'var(--cyan-lt)', label: 'CODE' }
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// SVGs
|
|
82
|
+
const ICONS = {
|
|
83
|
+
folder: '<svg class="folder-svg" viewBox="0 0 32 32" fill="var(--primary-lt)" stroke="var(--primary)" stroke-width="1.3"><path d="M4 8a2 2 0 012-2h6l3 3h11a2 2 0 012 2v13a2 2 0 01-2 2H6a2 2 0 01-2-2V8z"/></svg>',
|
|
84
|
+
folderSm: '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 4a1 1 0 011-1h3l2 2h5a1 1 0 011 1v6a1 1 0 01-1 1H3a1 1 0 01-1-1V4z"/></svg>',
|
|
85
|
+
drive: '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 6l4-4h4l4 4v8H2V6z"/></svg>',
|
|
86
|
+
starred: '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M8 1l2 5 5 .5-4 3.5 1 5-4-2.5-4 2.5 1-5-4-3.5 5-.5z"/></svg>',
|
|
87
|
+
media: '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="2" width="12" height="12" rx="2"/><circle cx="5.5" cy="5.5" r="1.5"/><path d="M14 10l-4-4-6 6"/></svg>',
|
|
88
|
+
docs: '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 2h7l4 4v8a1 1 0 01-1 1H3a1 1 0 01-1-1V3a1 1 0 011-1z"/><path d="M10 2v4h4"/></svg>',
|
|
89
|
+
code: '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M5 5L2 8l3 3M11 5l3 3-3 3M9 3L7 13"/></svg>',
|
|
90
|
+
upload: '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 11v3H2v-3"/><path d="M11 6L8 3 5 6"/><line x1="8" y1="3" x2="8" y2="11"/></svg>',
|
|
91
|
+
download: '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 11v3H2v-3"/><path d="M5 8l3 3 3-3"/><line x1="8" y1="2" x2="8" y2="11"/></svg>',
|
|
92
|
+
more: '<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><circle cx="8" cy="3" r="1.2"/><circle cx="8" cy="8" r="1.2"/><circle cx="8" cy="13" r="1.2"/></svg>'
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Helper Functions
|
|
96
|
+
function escapeHtml(str) {
|
|
97
|
+
return String(str || '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function escapeAttr(str) {
|
|
101
|
+
return escapeHtml(str);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getBaseName(path) {
|
|
105
|
+
const val = String(path || '').replace(/\/+$/, '');
|
|
106
|
+
const lastSlash = val.lastIndexOf('/');
|
|
107
|
+
return lastSlash === -1 ? val : val.substring(lastSlash + 1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function getFileExt(fileName) {
|
|
111
|
+
const name = getBaseName(fileName);
|
|
112
|
+
const dot = name.lastIndexOf('.');
|
|
113
|
+
return dot > -1 ? name.slice(dot + 1).toLowerCase() : '';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function formatFileSize(bytes) {
|
|
117
|
+
if (!bytes || bytes === 0) return '0 B';
|
|
118
|
+
const k = 1024;
|
|
119
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
120
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
121
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function formatDate(dateStr) {
|
|
125
|
+
if (!dateStr) return '—';
|
|
126
|
+
const d = new Date(dateStr);
|
|
127
|
+
if (isNaN(d.getTime())) return String(dateStr);
|
|
128
|
+
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function withBucketUrl(url) {
|
|
132
|
+
if (!activeBucket) return url;
|
|
133
|
+
const sep = url.includes('?') ? '&' : '?';
|
|
134
|
+
return `${url}${sep}bucket=${encodeURIComponent(activeBucket)}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function getViewUrl(key) {
|
|
138
|
+
return withBucketUrl(`/mbkbucket/view/${encodeURIComponent(key)}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function getPublicViewUrl(key) {
|
|
142
|
+
return withBucketUrl(`/mbkbucket/p_view/${encodeURIComponent(key)}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function getDownloadUrl(key) {
|
|
146
|
+
return withBucketUrl(`/mbkbucket/download/${encodeURIComponent(key)}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function isImage(fileName) {
|
|
150
|
+
const ext = getFileExt(fileName);
|
|
151
|
+
return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp'].includes(ext);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function isVideo(fileName) {
|
|
155
|
+
const ext = getFileExt(fileName);
|
|
156
|
+
return ['mp4', 'webm', 'ogg', 'mov', 'mkv'].includes(ext);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function isAudio(fileName) {
|
|
160
|
+
const ext = getFileExt(fileName);
|
|
161
|
+
return ['mp3', 'wav', 'ogg', 'flac', 'm4a', 'aac'].includes(ext);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function isTextOrCode(fileName) {
|
|
165
|
+
const ext = getFileExt(fileName);
|
|
166
|
+
return CODE_EXTS.has(ext) || ['txt', 'md', 'csv', 'log', 'json', 'xml', 'env'].includes(ext);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function isMarkdown(fileName) {
|
|
170
|
+
return getFileExt(fileName) === 'md';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function isPdf(fileName) {
|
|
174
|
+
return getFileExt(fileName) === 'pdf';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function getFileTypeConfig(fileName) {
|
|
178
|
+
const ext = getFileExt(fileName);
|
|
179
|
+
if (FILE_TYPES[ext]) return FILE_TYPES[ext];
|
|
180
|
+
if (CODE_EXTS.has(ext)) return FILE_TYPES.code;
|
|
181
|
+
return { color: 'var(--text-secondary)', bg: 'var(--bg-surface-secondary)', label: ext.toUpperCase().slice(0, 4) || 'FILE' };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function stripApiPrefixFromPath(path, apiPrefix = '') {
|
|
185
|
+
const normalizedPath = String(path || '').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
186
|
+
const normalizedApiPrefix = String(apiPrefix || '').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
187
|
+
if (!normalizedApiPrefix) return normalizedPath;
|
|
188
|
+
if (normalizedPath === normalizedApiPrefix || normalizedPath === `${normalizedApiPrefix}/`) return '';
|
|
189
|
+
if (normalizedPath.startsWith(`${normalizedApiPrefix}/`)) {
|
|
190
|
+
return normalizedPath.slice(normalizedApiPrefix.length + 1);
|
|
191
|
+
}
|
|
192
|
+
return normalizedPath;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function toApiFolderPath(prefix = '', apiPrefix = '') {
|
|
196
|
+
const uiPrefix = String(prefix || '').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
197
|
+
const normalizedApiPrefix = String(apiPrefix || '').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
198
|
+
if (!uiPrefix) return '';
|
|
199
|
+
if (!normalizedApiPrefix) return uiPrefix;
|
|
200
|
+
if (uiPrefix === normalizedApiPrefix || uiPrefix.startsWith(`${normalizedApiPrefix}/`)) return uiPrefix;
|
|
201
|
+
return `${normalizedApiPrefix}/${uiPrefix}`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function extractFoldersAtCurrentLevel(files, prefix = '') {
|
|
205
|
+
const folderSet = new Set();
|
|
206
|
+
const cleanPrefix = String(prefix || '').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
207
|
+
const prefixOffset = cleanPrefix ? cleanPrefix.length + 1 : 0;
|
|
208
|
+
|
|
209
|
+
(files || []).forEach(file => {
|
|
210
|
+
const key = String(file.Key || '').replace(/^\/+/, '');
|
|
211
|
+
if (cleanPrefix && !key.startsWith(cleanPrefix + '/')) return;
|
|
212
|
+
const relPath = cleanPrefix ? key.substring(prefixOffset) : key;
|
|
213
|
+
const slashIdx = relPath.indexOf('/');
|
|
214
|
+
if (slashIdx !== -1) {
|
|
215
|
+
const folder = relPath.substring(0, slashIdx);
|
|
216
|
+
folderSet.add(cleanPrefix ? `${cleanPrefix}/${folder}` : folder);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
return Array.from(folderSet).sort();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function renderMarkdownToHtml(mdText) {
|
|
224
|
+
let html = escapeHtml(mdText);
|
|
225
|
+
html = html.replace(/```([a-zA-Z0-9_+-]*)\n([\s\S]*?)```/g, (_m, _lang, code) => `<pre><code>${code}</code></pre>`);
|
|
226
|
+
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
227
|
+
html = html.replace(/^### (.*$)/gim, '<h3>$1</h3>');
|
|
228
|
+
html = html.replace(/^## (.*$)/gim, '<h2>$1</h2>');
|
|
229
|
+
html = html.replace(/^# (.*$)/gim, '<h1>$1</h1>');
|
|
230
|
+
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
|
231
|
+
html = html.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
|
232
|
+
html = html.replace(/^\> (.*$)/gim, '<blockquote>$1</blockquote>');
|
|
233
|
+
html = html.replace(/^\s*[\-\*] (.*$)/gim, '<li>$1</li>');
|
|
234
|
+
html = html.replace(/(<li>.*<\/li>)/gim, '<ul>$1</ul>');
|
|
235
|
+
html = html.replace(/\n\n/g, '<br><br>');
|
|
236
|
+
return `<div class="rendered-markdown">${html}</div>`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function syncStateUrl() {
|
|
240
|
+
try {
|
|
241
|
+
const parts = [];
|
|
242
|
+
if (activeBucket) parts.push(`bucket=${encodeURIComponent(activeBucket)}`);
|
|
243
|
+
if (currentPrefix) {
|
|
244
|
+
const folderPath = toApiFolderPath(currentPrefix, currentApiPrefix);
|
|
245
|
+
parts.push(`folder=${encodeURIComponent(folderPath).replace(/%2F/g, '/')}`);
|
|
246
|
+
}
|
|
247
|
+
if (currentPage > 1) parts.push(`page=${currentPage}`);
|
|
248
|
+
if (currentSearch) parts.push(`search=${encodeURIComponent(currentSearch)}`);
|
|
249
|
+
if (currentSort !== 'name') parts.push(`sort=${encodeURIComponent(currentSort)}`);
|
|
250
|
+
if (currentOrder !== 'asc') parts.push(`order=${encodeURIComponent(currentOrder)}`);
|
|
251
|
+
if (currentViewMode !== 'grid') parts.push(`view=${encodeURIComponent(currentViewMode)}`);
|
|
252
|
+
if (currentQuickFilter !== 'all') parts.push(`filter=${encodeURIComponent(currentQuickFilter)}`);
|
|
253
|
+
|
|
254
|
+
const newUrl = '/mbkbucket' + (parts.length ? '?' + parts.join('&') : '');
|
|
255
|
+
window.history.replaceState(null, '', newUrl);
|
|
256
|
+
} catch (e) {
|
|
257
|
+
console.warn('History update failed', e);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Toast System
|
|
262
|
+
function showToast(message, type = 'success') {
|
|
263
|
+
const container = document.getElementById('toast-container');
|
|
264
|
+
if (!container) return;
|
|
265
|
+
|
|
266
|
+
const toast = document.createElement('div');
|
|
267
|
+
toast.className = `toast-notification ${type}`;
|
|
268
|
+
toast.innerHTML = `
|
|
269
|
+
<span>${escapeHtml(message)}</span>
|
|
270
|
+
<button type="button" class="toast-close-btn">×</button>
|
|
271
|
+
`;
|
|
272
|
+
|
|
273
|
+
toast.querySelector('.toast-close-btn').addEventListener('click', () => {
|
|
274
|
+
toast.remove();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
container.appendChild(toast);
|
|
278
|
+
|
|
279
|
+
setTimeout(() => {
|
|
280
|
+
toast.style.opacity = '0';
|
|
281
|
+
toast.style.transform = 'translateY(10px)';
|
|
282
|
+
setTimeout(() => toast.remove(), 250);
|
|
283
|
+
}, 4000);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Live Health Check
|
|
287
|
+
async function checkConnectionHealth(silent = true) {
|
|
288
|
+
const healthBadge = document.getElementById('topHealthBadge');
|
|
289
|
+
const healthText = document.getElementById('topHealthText');
|
|
290
|
+
const latencyTag = document.getElementById('storageLatency');
|
|
291
|
+
|
|
292
|
+
if (healthBadge) {
|
|
293
|
+
healthBadge.className = 'health-status-badge checking';
|
|
294
|
+
if (healthText) healthText.textContent = 'Checking...';
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const startTime = Date.now();
|
|
298
|
+
try {
|
|
299
|
+
const response = await fetch(withBucketUrl('/mbkbucket/api/health'));
|
|
300
|
+
const data = await response.json();
|
|
301
|
+
const latency = data.responseTime || (Date.now() - startTime);
|
|
302
|
+
|
|
303
|
+
if (data.status === 'healthy' || data.success) {
|
|
304
|
+
if (healthBadge) {
|
|
305
|
+
healthBadge.className = 'health-status-badge healthy';
|
|
306
|
+
if (healthText) healthText.textContent = 'Healthy';
|
|
307
|
+
}
|
|
308
|
+
if (latencyTag) latencyTag.textContent = `${latency}ms`;
|
|
309
|
+
if (!silent) showToast(`Storage connection healthy (${latency}ms)`, 'success');
|
|
310
|
+
} else {
|
|
311
|
+
if (healthBadge) {
|
|
312
|
+
healthBadge.className = 'health-status-badge unhealthy';
|
|
313
|
+
if (healthText) healthText.textContent = 'Unhealthy';
|
|
314
|
+
}
|
|
315
|
+
if (latencyTag) latencyTag.textContent = 'err';
|
|
316
|
+
if (!silent) showToast(`Connection error: ${data.error || 'Check configuration'}`, 'error');
|
|
317
|
+
}
|
|
318
|
+
} catch (err) {
|
|
319
|
+
if (healthBadge) {
|
|
320
|
+
healthBadge.className = 'health-status-badge unhealthy';
|
|
321
|
+
if (healthText) healthText.textContent = 'Offline';
|
|
322
|
+
}
|
|
323
|
+
if (latencyTag) latencyTag.textContent = 'offline';
|
|
324
|
+
if (!silent) showToast(`Connection test failed: ${err.message}`, 'error');
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Batch Selection System
|
|
329
|
+
function updateBatchBar() {
|
|
330
|
+
const bar = document.getElementById('batchActionBar');
|
|
331
|
+
const countEl = document.getElementById('batchSelectedCount');
|
|
332
|
+
if (!bar) return;
|
|
333
|
+
|
|
334
|
+
const count = selectedFiles.size;
|
|
335
|
+
if (count > 0) {
|
|
336
|
+
bar.style.display = 'block';
|
|
337
|
+
if (countEl) countEl.textContent = count;
|
|
338
|
+
} else {
|
|
339
|
+
bar.style.display = 'none';
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function clearBatchSelection() {
|
|
344
|
+
selectedFiles.clear();
|
|
345
|
+
document.querySelectorAll('.file-select-cb').forEach(cb => { cb.checked = false; });
|
|
346
|
+
document.querySelectorAll('.fm-item.is-selected').forEach(item => { item.classList.remove('is-selected'); });
|
|
347
|
+
updateBatchBar();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Tree Renderer
|
|
351
|
+
function renderTree() {
|
|
352
|
+
const treeEl = document.getElementById('fm-tree');
|
|
353
|
+
if (!treeEl) return;
|
|
354
|
+
|
|
355
|
+
const allFolders = pageFolders || [];
|
|
356
|
+
const isStarredActive = currentQuickFilter === 'starred';
|
|
357
|
+
const isRootActive = !currentPrefix && currentQuickFilter === 'all';
|
|
358
|
+
|
|
359
|
+
let treeHtml = `
|
|
360
|
+
<div class="tree-group">
|
|
361
|
+
<button type="button" class="tree-link ${isRootActive ? 'active' : ''}" data-tree-prefix="" data-tree-filter="all">
|
|
362
|
+
${ICONS.drive}
|
|
363
|
+
<span>${escapeHtml(rootLabel)}</span>
|
|
364
|
+
</button>
|
|
365
|
+
</div>
|
|
366
|
+
`;
|
|
367
|
+
|
|
368
|
+
// Folder hierarchy
|
|
369
|
+
if (allFolders.length > 0) {
|
|
370
|
+
treeHtml += `
|
|
371
|
+
<div class="tree-group">
|
|
372
|
+
<div class="tree-subs">
|
|
373
|
+
${allFolders.map(folder => {
|
|
374
|
+
const cleanFolder = String(folder || '').replace(/\/+$/, '');
|
|
375
|
+
const folderName = getBaseName(cleanFolder);
|
|
376
|
+
const folderPathForAction = currentPrefix && !cleanFolder.startsWith(currentPrefix + '/') && cleanFolder !== currentPrefix
|
|
377
|
+
? `${currentPrefix}/${cleanFolder}`
|
|
378
|
+
: cleanFolder;
|
|
379
|
+
const isFolderActive = currentPrefix === folderPathForAction;
|
|
380
|
+
return `
|
|
381
|
+
<button type="button" class="tree-link tree-sub ${isFolderActive ? 'active' : ''}" data-tree-prefix="${escapeAttr(folderPathForAction)}" data-tree-filter="all">
|
|
382
|
+
${ICONS.folderSm}
|
|
383
|
+
<span>${escapeHtml(folderName)}</span>
|
|
384
|
+
</button>
|
|
385
|
+
`;
|
|
386
|
+
}).join('')}
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
`;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Quick navigation filters
|
|
393
|
+
treeHtml += `
|
|
394
|
+
<div class="tree-group" style="margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--border-color-light);">
|
|
395
|
+
<button type="button" class="tree-link ${isStarredActive ? 'active' : ''}" data-tree-filter="starred">
|
|
396
|
+
${ICONS.starred}
|
|
397
|
+
<span>Starred</span>
|
|
398
|
+
${starredFiles.size > 0 ? `<span class="tree-badge">${starredFiles.size}</span>` : ''}
|
|
399
|
+
</button>
|
|
400
|
+
<button type="button" class="tree-link ${currentQuickFilter === 'media' ? 'active' : ''}" data-tree-filter="media">
|
|
401
|
+
${ICONS.media}
|
|
402
|
+
<span>Media</span>
|
|
403
|
+
</button>
|
|
404
|
+
<button type="button" class="tree-link ${currentQuickFilter === 'docs' ? 'active' : ''}" data-tree-filter="docs">
|
|
405
|
+
${ICONS.docs}
|
|
406
|
+
<span>Documents</span>
|
|
407
|
+
</button>
|
|
408
|
+
<button type="button" class="tree-link ${currentQuickFilter === 'code' ? 'active' : ''}" data-tree-filter="code">
|
|
409
|
+
${ICONS.code}
|
|
410
|
+
<span>Code</span>
|
|
411
|
+
</button>
|
|
412
|
+
</div>
|
|
413
|
+
`;
|
|
414
|
+
|
|
415
|
+
treeEl.innerHTML = treeHtml;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Breadcrumbs Renderer
|
|
419
|
+
function renderBreadcrumbs() {
|
|
420
|
+
const bcEl = document.getElementById('fm-breadcrumb');
|
|
421
|
+
if (!bcEl) return;
|
|
422
|
+
|
|
423
|
+
if (!currentPrefix && currentQuickFilter === 'all') {
|
|
424
|
+
bcEl.innerHTML = `<button type="button" class="bc-link current">${escapeHtml(rootLabel)}</button>`;
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (currentQuickFilter !== 'all') {
|
|
429
|
+
bcEl.innerHTML = `
|
|
430
|
+
<button type="button" class="bc-link" data-nav-prefix="">${escapeHtml(rootLabel)}</button>
|
|
431
|
+
<span class="sep">›</span>
|
|
432
|
+
<button type="button" class="bc-link current">${escapeHtml(currentQuickFilter.charAt(0).toUpperCase() + currentQuickFilter.slice(1))}</button>
|
|
433
|
+
`;
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const parts = currentPrefix.split('/').filter(Boolean);
|
|
438
|
+
let accum = '';
|
|
439
|
+
const breadcrumbHtml = [
|
|
440
|
+
`<button type="button" class="bc-link" data-nav-prefix="">${escapeHtml(rootLabel)}</button>`
|
|
441
|
+
];
|
|
442
|
+
|
|
443
|
+
parts.forEach((part, index) => {
|
|
444
|
+
accum += (accum ? '/' : '') + part;
|
|
445
|
+
const isLast = index === parts.length - 1;
|
|
446
|
+
breadcrumbHtml.push('<span class="sep">›</span>');
|
|
447
|
+
if (isLast) {
|
|
448
|
+
breadcrumbHtml.push(`<button type="button" class="bc-link current">${escapeHtml(part)}</button>`);
|
|
449
|
+
} else {
|
|
450
|
+
breadcrumbHtml.push(`<button type="button" class="bc-link" data-nav-prefix="${escapeAttr(accum)}">${escapeHtml(part)}</button>`);
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
bcEl.innerHTML = breadcrumbHtml.join('');
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// File Filtering Logic
|
|
458
|
+
function getFilteredFiles() {
|
|
459
|
+
let list = (pageFiles || []).filter(f => !String(f.Key || '').endsWith('/'));
|
|
460
|
+
|
|
461
|
+
if (currentSearch) {
|
|
462
|
+
const q = currentSearch.toLowerCase();
|
|
463
|
+
list = list.filter(f => getBaseName(f.Key).toLowerCase().includes(q));
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (currentQuickFilter === 'media') {
|
|
467
|
+
list = list.filter(f => MEDIA_EXTS.has(getFileExt(f.Key)));
|
|
468
|
+
} else if (currentQuickFilter === 'docs') {
|
|
469
|
+
list = list.filter(f => DOC_EXTS.has(getFileExt(f.Key)));
|
|
470
|
+
} else if (currentQuickFilter === 'code') {
|
|
471
|
+
list = list.filter(f => CODE_EXTS.has(getFileExt(f.Key)));
|
|
472
|
+
} else if (currentQuickFilter === 'starred') {
|
|
473
|
+
list = list.filter(f => starredFiles.has(f.Key));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return list;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function getFilteredFolders() {
|
|
480
|
+
if (currentQuickFilter !== 'all') return [];
|
|
481
|
+
if (currentSearch) {
|
|
482
|
+
const q = currentSearch.toLowerCase();
|
|
483
|
+
return (pageFolders || []).filter(f => getBaseName(f).toLowerCase().includes(q));
|
|
484
|
+
}
|
|
485
|
+
return pageFolders || [];
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Main Files & Grid Renderer
|
|
489
|
+
function renderFiles() {
|
|
490
|
+
const gridEl = document.getElementById('fm-grid');
|
|
491
|
+
const emptyEl = document.getElementById('fm-empty');
|
|
492
|
+
if (!gridEl) return;
|
|
493
|
+
|
|
494
|
+
const files = getFilteredFiles();
|
|
495
|
+
const folders = getFilteredFolders();
|
|
496
|
+
|
|
497
|
+
const totalCountEl = document.getElementById('totalFilesCount');
|
|
498
|
+
const filterInfoEl = document.getElementById('filterInfo');
|
|
499
|
+
const totalItems = files.length + folders.length;
|
|
500
|
+
|
|
501
|
+
if (totalCountEl) {
|
|
502
|
+
totalCountEl.textContent = `${totalItems} item${totalItems === 1 ? '' : 's'}`;
|
|
503
|
+
}
|
|
504
|
+
if (filterInfoEl) {
|
|
505
|
+
filterInfoEl.textContent = activeBucket ? `Bucket: ${activeBucket}` : 'Default bucket';
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (totalItems === 0) {
|
|
509
|
+
gridEl.innerHTML = '';
|
|
510
|
+
if (emptyEl) {
|
|
511
|
+
emptyEl.hidden = false;
|
|
512
|
+
emptyEl.style.display = 'flex';
|
|
513
|
+
}
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (emptyEl) {
|
|
518
|
+
emptyEl.hidden = true;
|
|
519
|
+
emptyEl.style.display = 'none';
|
|
520
|
+
}
|
|
521
|
+
gridEl.className = `fm-grid view-${currentViewMode}`;
|
|
522
|
+
|
|
523
|
+
let html = '';
|
|
524
|
+
|
|
525
|
+
if (currentViewMode === 'list') {
|
|
526
|
+
html += `
|
|
527
|
+
<div class="fm-list-header">
|
|
528
|
+
<div><input type="checkbox" id="selectAllCb" title="Select All"></div>
|
|
529
|
+
<div></div>
|
|
530
|
+
<div>Name</div>
|
|
531
|
+
<div>Size</div>
|
|
532
|
+
<div>Modified</div>
|
|
533
|
+
<div></div>
|
|
534
|
+
<div></div>
|
|
535
|
+
</div>
|
|
536
|
+
`;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Render Folders
|
|
540
|
+
folders.forEach(folder => {
|
|
541
|
+
const cleanFolder = String(folder || '').replace(/\/+$/, '');
|
|
542
|
+
const folderName = getBaseName(cleanFolder);
|
|
543
|
+
const folderPathForAction = currentPrefix && !cleanFolder.startsWith(currentPrefix + '/') && cleanFolder !== currentPrefix
|
|
544
|
+
? `${currentPrefix}/${cleanFolder}`
|
|
545
|
+
: cleanFolder;
|
|
546
|
+
const safePath = escapeAttr(folderPathForAction);
|
|
547
|
+
const safeName = escapeHtml(folderName);
|
|
548
|
+
|
|
549
|
+
if (currentViewMode === 'grid') {
|
|
550
|
+
html += `
|
|
551
|
+
<div class="fm-item" data-type="folder" data-folder-path="${safePath}" tabindex="0">
|
|
552
|
+
<div class="fm-item-icon">
|
|
553
|
+
${ICONS.folder}
|
|
554
|
+
</div>
|
|
555
|
+
<div class="fm-item-details">
|
|
556
|
+
<div class="fm-item-name" title="${safeName}">${safeName}</div>
|
|
557
|
+
<div class="fm-item-meta">Folder</div>
|
|
558
|
+
</div>
|
|
559
|
+
<button type="button" class="fm-item-menu" data-menu-folder="${safePath}" aria-label="Folder options">
|
|
560
|
+
${ICONS.more}
|
|
561
|
+
</button>
|
|
562
|
+
</div>
|
|
563
|
+
`;
|
|
564
|
+
} else {
|
|
565
|
+
html += `
|
|
566
|
+
<div class="fm-item" data-type="folder" data-folder-path="${safePath}" tabindex="0">
|
|
567
|
+
<div class="fm-item-checkbox"></div>
|
|
568
|
+
<div class="fm-item-icon">
|
|
569
|
+
${ICONS.folderSm}
|
|
570
|
+
</div>
|
|
571
|
+
<div class="fm-item-name" title="${safeName}">${safeName}</div>
|
|
572
|
+
<div class="fm-item-size fm-item-meta">—</div>
|
|
573
|
+
<div class="fm-item-date fm-item-meta">—</div>
|
|
574
|
+
<div class="fm-star-wrap"></div>
|
|
575
|
+
<button type="button" class="fm-item-menu" data-menu-folder="${safePath}" aria-label="Folder options">
|
|
576
|
+
${ICONS.more}
|
|
577
|
+
</button>
|
|
578
|
+
</div>
|
|
579
|
+
`;
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
// Render Files
|
|
584
|
+
files.forEach(file => {
|
|
585
|
+
const rawKey = String(file.Key || '');
|
|
586
|
+
const fileName = getBaseName(rawKey);
|
|
587
|
+
const safeKey = escapeAttr(rawKey);
|
|
588
|
+
const safeName = escapeHtml(fileName);
|
|
589
|
+
const isStarred = starredFiles.has(rawKey);
|
|
590
|
+
const isSelected = selectedFiles.has(rawKey);
|
|
591
|
+
const isImg = isImage(fileName);
|
|
592
|
+
const typeCfg = getFileTypeConfig(fileName);
|
|
593
|
+
const formattedSize = file.Size ? formatFileSize(file.Size) : '—';
|
|
594
|
+
const formattedDate = file.LastModified ? formatDate(file.LastModified) : '—';
|
|
595
|
+
|
|
596
|
+
const showImgThumb = isImg && showThumbnails;
|
|
597
|
+
let iconInner = `<span class="ext">${typeCfg.label}</span>`;
|
|
598
|
+
if (showImgThumb) {
|
|
599
|
+
iconInner = `<img data-src="${getViewUrl(rawKey)}" class="fm-item-thumb fm-lazy-thumb" alt="${safeName}" onload="this.classList.add('is-loaded')" onerror="this.outerHTML='<span class=\\'ext\\'>IMG</span>'">`;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (currentViewMode === 'grid') {
|
|
603
|
+
html += `
|
|
604
|
+
<div class="fm-item ${isStarred ? 'starred' : ''} ${isSelected ? 'is-selected' : ''}" data-type="file" data-key="${safeKey}" tabindex="0">
|
|
605
|
+
<div class="fm-item-checkbox-grid">
|
|
606
|
+
<input type="checkbox" class="file-select-cb" data-key="${safeKey}" ${isSelected ? 'checked' : ''} title="Select">
|
|
607
|
+
</div>
|
|
608
|
+
<div class="fm-item-icon" style="${showImgThumb ? '' : `background:${typeCfg.bg};color:${typeCfg.color}`}">
|
|
609
|
+
${iconInner}
|
|
610
|
+
</div>
|
|
611
|
+
<div class="fm-item-details">
|
|
612
|
+
<div class="fm-item-name" title="${safeName}">${safeName}</div>
|
|
613
|
+
<div class="fm-item-meta">${formattedSize} · ${formattedDate}</div>
|
|
614
|
+
</div>
|
|
615
|
+
<button type="button" class="fm-star ${isStarred ? 'on' : ''}" data-star-key="${safeKey}" aria-label="${isStarred ? 'Unstar' : 'Star'}">
|
|
616
|
+
${ICONS.starred}
|
|
617
|
+
</button>
|
|
618
|
+
<button type="button" class="fm-item-menu" data-menu-file="${safeKey}" aria-label="File options">
|
|
619
|
+
${ICONS.more}
|
|
620
|
+
</button>
|
|
621
|
+
</div>
|
|
622
|
+
`;
|
|
623
|
+
} else {
|
|
624
|
+
html += `
|
|
625
|
+
<div class="fm-item ${isStarred ? 'starred' : ''} ${isSelected ? 'is-selected' : ''}" data-type="file" data-key="${safeKey}" tabindex="0">
|
|
626
|
+
<div class="fm-item-checkbox">
|
|
627
|
+
<input type="checkbox" class="file-select-cb" data-key="${safeKey}" ${isSelected ? 'checked' : ''} title="Select">
|
|
628
|
+
</div>
|
|
629
|
+
<div class="fm-item-icon" style="${isImg ? '' : `background:${typeCfg.bg};color:${typeCfg.color}`}">
|
|
630
|
+
<span class="ext">${typeCfg.label}</span>
|
|
631
|
+
</div>
|
|
632
|
+
<div class="fm-item-name" title="${safeName}">${safeName}</div>
|
|
633
|
+
<div class="fm-item-size fm-item-meta">${formattedSize}</div>
|
|
634
|
+
<div class="fm-item-date fm-item-meta">${formattedDate}</div>
|
|
635
|
+
<button type="button" class="fm-star ${isStarred ? 'on' : ''}" data-star-key="${safeKey}" aria-label="${isStarred ? 'Unstar' : 'Star'}">
|
|
636
|
+
${ICONS.starred}
|
|
637
|
+
</button>
|
|
638
|
+
<button type="button" class="fm-item-menu" data-menu-file="${safeKey}" aria-label="File options">
|
|
639
|
+
${ICONS.more}
|
|
640
|
+
</button>
|
|
641
|
+
</div>
|
|
642
|
+
`;
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
gridEl.innerHTML = html;
|
|
647
|
+
observeThumbnails();
|
|
648
|
+
updateBatchBar();
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function observeThumbnails() {
|
|
652
|
+
const lazyImages = document.querySelectorAll('img.fm-lazy-thumb[data-src]');
|
|
653
|
+
if (!lazyImages.length) return;
|
|
654
|
+
|
|
655
|
+
if (!('IntersectionObserver' in window)) {
|
|
656
|
+
lazyImages.forEach(img => {
|
|
657
|
+
if (img.dataset.src) {
|
|
658
|
+
img.src = img.dataset.src;
|
|
659
|
+
img.removeAttribute('data-src');
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (thumbObserver) {
|
|
666
|
+
thumbObserver.disconnect();
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
thumbObserver = new IntersectionObserver((entries, observer) => {
|
|
670
|
+
entries.forEach(entry => {
|
|
671
|
+
if (entry.isIntersecting) {
|
|
672
|
+
const img = entry.target;
|
|
673
|
+
if (img.dataset.src) {
|
|
674
|
+
img.src = img.dataset.src;
|
|
675
|
+
img.removeAttribute('data-src');
|
|
676
|
+
}
|
|
677
|
+
observer.unobserve(img);
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
}, {
|
|
681
|
+
rootMargin: '100px 0px',
|
|
682
|
+
threshold: 0.01
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
lazyImages.forEach(img => thumbObserver.observe(img));
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Load Files API Call
|
|
689
|
+
async function loadFiles(page = 1, prefix = '', search = '') {
|
|
690
|
+
currentPage = page;
|
|
691
|
+
currentPrefix = String(prefix || '').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
692
|
+
currentSearch = search;
|
|
693
|
+
|
|
694
|
+
syncStateUrl();
|
|
695
|
+
|
|
696
|
+
const gridEl = document.getElementById('fm-grid');
|
|
697
|
+
if (gridEl) {
|
|
698
|
+
gridEl.innerHTML = `
|
|
699
|
+
<div class="fm-skeleton-grid">
|
|
700
|
+
${[...Array(6)].map(() => `
|
|
701
|
+
<div class="fm-skeleton-card">
|
|
702
|
+
<div class="fm-skeleton-icon"></div>
|
|
703
|
+
<div class="fm-skeleton-line w-75"></div>
|
|
704
|
+
<div class="fm-skeleton-line w-50"></div>
|
|
705
|
+
</div>
|
|
706
|
+
`).join('')}
|
|
707
|
+
</div>
|
|
708
|
+
`;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
try {
|
|
712
|
+
const searchParam = search ? `&search=${encodeURIComponent(search)}` : '';
|
|
713
|
+
const sortParam = `&sort=${currentSort}&order=${currentOrder}`;
|
|
714
|
+
const recursiveParam = `&recursive=false`;
|
|
715
|
+
const url = withBucketUrl(`/mbkbucket/api/files?page=${page}&prefix=${encodeURIComponent(currentPrefix)}${searchParam}${sortParam}${recursiveParam}`);
|
|
716
|
+
|
|
717
|
+
const response = await fetch(url);
|
|
718
|
+
const data = await response.json();
|
|
719
|
+
|
|
720
|
+
if (!data.success) {
|
|
721
|
+
throw new Error(data.error || 'Failed to load files');
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
currentNextToken = data.nextContinuationToken || null;
|
|
725
|
+
currentApiPrefix = data.prefix || '';
|
|
726
|
+
pageFiles = data.files || [];
|
|
727
|
+
|
|
728
|
+
if (data.mode === 'optimized') {
|
|
729
|
+
pageFolders = (data.folders || []).map(folder => stripApiPrefixFromPath(folder, currentApiPrefix)).filter(Boolean);
|
|
730
|
+
} else {
|
|
731
|
+
pageFolders = currentSearch
|
|
732
|
+
? []
|
|
733
|
+
: extractFoldersAtCurrentLevel(pageFiles, currentApiPrefix || currentPrefix)
|
|
734
|
+
.map(folder => stripApiPrefixFromPath(folder, currentApiPrefix))
|
|
735
|
+
.filter(Boolean);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
renderTree();
|
|
739
|
+
renderBreadcrumbs();
|
|
740
|
+
renderFiles();
|
|
741
|
+
renderPagination(data);
|
|
742
|
+
checkConnectionHealth(true);
|
|
743
|
+
} catch (err) {
|
|
744
|
+
console.error('Error loading files:', err);
|
|
745
|
+
showToast(err.message || 'Error loading files', 'error');
|
|
746
|
+
if (gridEl) {
|
|
747
|
+
gridEl.innerHTML = `
|
|
748
|
+
<div class="fm-empty">
|
|
749
|
+
<div class="empty-title">Error Loading Files</div>
|
|
750
|
+
<div class="empty-desc">${escapeHtml(err.message)}</div>
|
|
751
|
+
<button type="button" class="btn btn-primary btn-sm mt-3" onclick="window.mbkbucket.loadFiles(${page}, '${escapeAttr(currentPrefix)}')">Retry</button>
|
|
752
|
+
</div>
|
|
753
|
+
`;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Pagination Renderer
|
|
759
|
+
function renderPagination(data) {
|
|
760
|
+
const paginationContainer = document.getElementById('paginationContainer');
|
|
761
|
+
const paginationList = document.getElementById('paginationList');
|
|
762
|
+
if (!paginationContainer || !paginationList) return;
|
|
763
|
+
|
|
764
|
+
if (!data.totalPages || data.totalPages <= 1) {
|
|
765
|
+
paginationContainer.style.display = 'none';
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
paginationContainer.style.display = 'flex';
|
|
770
|
+
let html = '';
|
|
771
|
+
|
|
772
|
+
// Prev
|
|
773
|
+
html += `
|
|
774
|
+
<li class="page-item ${data.currentPage <= 1 ? 'disabled' : ''}">
|
|
775
|
+
<button type="button" class="page-link" data-page="${data.currentPage - 1}">‹</button>
|
|
776
|
+
</li>
|
|
777
|
+
`;
|
|
778
|
+
|
|
779
|
+
// Pages
|
|
780
|
+
for (let i = 1; i <= data.totalPages; i++) {
|
|
781
|
+
if (i === 1 || i === data.totalPages || (i >= data.currentPage - 2 && i <= data.currentPage + 2)) {
|
|
782
|
+
html += `
|
|
783
|
+
<li class="page-item ${i === data.currentPage ? 'active' : ''}">
|
|
784
|
+
<button type="button" class="page-link" data-page="${i}">${i}</button>
|
|
785
|
+
</li>
|
|
786
|
+
`;
|
|
787
|
+
} else if (i === data.currentPage - 3 || i === data.currentPage + 3) {
|
|
788
|
+
html += `<li class="page-item disabled"><span class="page-link">…</span></li>`;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Next
|
|
793
|
+
html += `
|
|
794
|
+
<li class="page-item ${data.currentPage >= data.totalPages ? 'disabled' : ''}">
|
|
795
|
+
<button type="button" class="page-link" data-page="${data.currentPage + 1}">›</button>
|
|
796
|
+
</li>
|
|
797
|
+
`;
|
|
798
|
+
|
|
799
|
+
paginationList.innerHTML = html;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// Context Menu
|
|
803
|
+
function openContextMenu(x, y, items) {
|
|
804
|
+
const menu = document.getElementById('fm-context-menu');
|
|
805
|
+
if (!menu) return;
|
|
806
|
+
|
|
807
|
+
menu.innerHTML = items.map(item => {
|
|
808
|
+
if (item === '-') return '<div class="fm-context-divider"></div>';
|
|
809
|
+
return `<button type="button" class="fm-context-item ${item.danger ? 'danger' : ''}" data-action="${item.id}">${item.icon || ''} ${escapeHtml(item.label)}</button>`;
|
|
810
|
+
}).join('');
|
|
811
|
+
|
|
812
|
+
menu.style.display = 'block';
|
|
813
|
+
|
|
814
|
+
const menuWidth = 190;
|
|
815
|
+
const menuHeight = menu.offsetHeight || 160;
|
|
816
|
+
const posX = Math.min(x, window.innerWidth - menuWidth - 10);
|
|
817
|
+
const posY = Math.min(y, window.innerHeight - menuHeight - 10);
|
|
818
|
+
|
|
819
|
+
menu.style.left = `${posX}px`;
|
|
820
|
+
menu.style.top = `${posY}px`;
|
|
821
|
+
|
|
822
|
+
menu.querySelectorAll('.fm-context-item').forEach(btn => {
|
|
823
|
+
btn.addEventListener('click', (e) => {
|
|
824
|
+
e.stopPropagation();
|
|
825
|
+
const actionId = btn.dataset.action;
|
|
826
|
+
const targetItem = items.find(it => it.id === actionId);
|
|
827
|
+
menu.style.display = 'none';
|
|
828
|
+
if (targetItem && typeof targetItem.action === 'function') {
|
|
829
|
+
targetItem.action();
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function closeContextMenu() {
|
|
836
|
+
const menu = document.getElementById('fm-context-menu');
|
|
837
|
+
if (menu) menu.style.display = 'none';
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Actions: Star, Download, Preview, Delete, New Folder, Move
|
|
841
|
+
function toggleStar(key) {
|
|
842
|
+
if (starredFiles.has(key)) {
|
|
843
|
+
starredFiles.delete(key);
|
|
844
|
+
showToast('Removed from Starred');
|
|
845
|
+
} else {
|
|
846
|
+
starredFiles.add(key);
|
|
847
|
+
showToast('Added to Starred', 'success');
|
|
848
|
+
}
|
|
849
|
+
localStorage.setItem('mbk_starred_keys', JSON.stringify([...starredFiles]));
|
|
850
|
+
renderTree();
|
|
851
|
+
renderFiles();
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
async function downloadFile(key) {
|
|
855
|
+
const fileName = getBaseName(key);
|
|
856
|
+
const downloadUrl = getDownloadUrl(key);
|
|
857
|
+
showToast(`Downloading ${fileName}...`);
|
|
858
|
+
try {
|
|
859
|
+
const response = await fetch(downloadUrl);
|
|
860
|
+
if (!response.ok) throw new Error('Download failed');
|
|
861
|
+
const blob = await response.blob();
|
|
862
|
+
const url = URL.createObjectURL(blob);
|
|
863
|
+
const a = document.createElement('a');
|
|
864
|
+
a.href = url;
|
|
865
|
+
a.download = fileName;
|
|
866
|
+
document.body.appendChild(a);
|
|
867
|
+
a.click();
|
|
868
|
+
document.body.removeChild(a);
|
|
869
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
870
|
+
} catch (err) {
|
|
871
|
+
showToast(err.message, 'error');
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function copyShareLink(key) {
|
|
876
|
+
const viewUrl = window.location.origin + getViewUrl(key);
|
|
877
|
+
navigator.clipboard.writeText(viewUrl).then(() => {
|
|
878
|
+
showToast('Link copied to clipboard', 'success');
|
|
879
|
+
}).catch(() => {
|
|
880
|
+
showToast('Failed to copy link', 'error');
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function copyPublicLink(key) {
|
|
885
|
+
const publicUrl = window.location.origin + getPublicViewUrl(key);
|
|
886
|
+
navigator.clipboard.writeText(publicUrl).then(() => {
|
|
887
|
+
showToast('Public link copied to clipboard', 'success');
|
|
888
|
+
}).catch(() => {
|
|
889
|
+
showToast('Failed to copy public link', 'error');
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// Preview Modal
|
|
894
|
+
async function openPreview(key) {
|
|
895
|
+
currentPreviewKey = key;
|
|
896
|
+
const modal = document.getElementById('previewModal');
|
|
897
|
+
const titleEl = document.getElementById('previewFileName');
|
|
898
|
+
const contentEl = document.getElementById('previewContent');
|
|
899
|
+
const openBtn = document.getElementById('previewOpenBtn');
|
|
900
|
+
const downloadBtn = document.getElementById('previewDownloadBtn');
|
|
901
|
+
|
|
902
|
+
if (!modal || !contentEl) return;
|
|
903
|
+
|
|
904
|
+
// Reset tabs
|
|
905
|
+
document.querySelectorAll('.preview-tab').forEach(t => t.classList.toggle('active', t.dataset.previewTab === 'preview'));
|
|
906
|
+
const panePreview = document.getElementById('pane-preview');
|
|
907
|
+
const paneInfo = document.getElementById('pane-info');
|
|
908
|
+
if (panePreview) panePreview.style.display = 'block';
|
|
909
|
+
if (paneInfo) paneInfo.style.display = 'none';
|
|
910
|
+
|
|
911
|
+
const fileName = getBaseName(key);
|
|
912
|
+
const viewUrl = getViewUrl(key);
|
|
913
|
+
const downloadUrl = getDownloadUrl(key);
|
|
914
|
+
|
|
915
|
+
if (titleEl) titleEl.textContent = fileName;
|
|
916
|
+
if (openBtn) openBtn.href = viewUrl;
|
|
917
|
+
if (downloadBtn) downloadBtn.href = downloadUrl;
|
|
918
|
+
|
|
919
|
+
contentEl.innerHTML = '<div class="empty-preview"><div class="spinner-border text-light"></div><div class="mt-2 text-light">Loading preview...</div></div>';
|
|
920
|
+
modal.classList.add('show');
|
|
921
|
+
|
|
922
|
+
// Populate file info tab
|
|
923
|
+
const fileInfoKey = document.getElementById('infoFileKey');
|
|
924
|
+
const fileInfoSize = document.getElementById('infoFileSize');
|
|
925
|
+
const fileInfoMime = document.getElementById('infoMimeType');
|
|
926
|
+
const fileInfoDate = document.getElementById('infoLastModified');
|
|
927
|
+
const fileInfoETag = document.getElementById('infoETag');
|
|
928
|
+
|
|
929
|
+
if (fileInfoKey) fileInfoKey.textContent = key;
|
|
930
|
+
|
|
931
|
+
// Async fetch full metadata
|
|
932
|
+
fetch(withBucketUrl(`/mbkbucket/api/file-info/${encodeURIComponent(key)}`))
|
|
933
|
+
.then(r => r.json())
|
|
934
|
+
.then(info => {
|
|
935
|
+
if (fileInfoSize) fileInfoSize.textContent = info.ContentLength ? formatFileSize(info.ContentLength) : '—';
|
|
936
|
+
if (fileInfoMime) fileInfoMime.textContent = info.ContentType || '—';
|
|
937
|
+
if (fileInfoDate) fileInfoDate.textContent = info.LastModified ? formatDate(info.LastModified) : '—';
|
|
938
|
+
if (fileInfoETag) fileInfoETag.textContent = info.ETag || '—';
|
|
939
|
+
}).catch(() => {});
|
|
940
|
+
|
|
941
|
+
// Configure public link button and metadata card
|
|
942
|
+
const copyPublicBtn = document.getElementById('previewCopyPublicLinkBtn');
|
|
943
|
+
if (copyPublicBtn) {
|
|
944
|
+
copyPublicBtn.style.display = isPublicViewEnabled ? '' : 'none';
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
const publicUrl = window.location.origin + getPublicViewUrl(key);
|
|
948
|
+
const infoPublicLinkCard = document.getElementById('infoPublicLinkCard');
|
|
949
|
+
const infoPublicUrl = document.getElementById('infoPublicUrl');
|
|
950
|
+
if (infoPublicLinkCard) {
|
|
951
|
+
infoPublicLinkCard.style.display = isPublicViewEnabled ? '' : 'none';
|
|
952
|
+
}
|
|
953
|
+
if (infoPublicUrl) {
|
|
954
|
+
infoPublicUrl.textContent = publicUrl;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Render Preview Content
|
|
958
|
+
if (isImage(fileName)) {
|
|
959
|
+
contentEl.innerHTML = `<img src="${viewUrl}" alt="${escapeAttr(fileName)}">`;
|
|
960
|
+
} else if (isVideo(fileName)) {
|
|
961
|
+
contentEl.innerHTML = `<video controls autoplay class="w-100" style="max-height: 70vh;"><source src="${viewUrl}"></video>`;
|
|
962
|
+
} else if (isAudio(fileName)) {
|
|
963
|
+
contentEl.innerHTML = `<div class="p-4 text-center"><audio controls autoplay class="w-100"><source src="${viewUrl}"></audio></div>`;
|
|
964
|
+
} else if (isPdf(fileName)) {
|
|
965
|
+
contentEl.innerHTML = `<iframe src="${viewUrl}" class="preview-iframe"></iframe>`;
|
|
966
|
+
} else if (isMarkdown(fileName)) {
|
|
967
|
+
fetch(viewUrl).then(res => res.text()).then(text => {
|
|
968
|
+
contentEl.innerHTML = renderMarkdownToHtml(text);
|
|
969
|
+
}).catch(err => {
|
|
970
|
+
contentEl.innerHTML = `<div class="p-4 text-danger text-center">Failed to load preview: ${escapeHtml(err.message)}</div>`;
|
|
971
|
+
});
|
|
972
|
+
} else if (isTextOrCode(fileName)) {
|
|
973
|
+
fetch(viewUrl).then(res => res.text()).then(text => {
|
|
974
|
+
contentEl.innerHTML = `<pre class="preview-code-block"><code>${escapeHtml(text)}</code></pre>`;
|
|
975
|
+
}).catch(err => {
|
|
976
|
+
contentEl.innerHTML = `<div class="p-4 text-danger text-center">Failed to load text preview: ${escapeHtml(err.message)}</div>`;
|
|
977
|
+
});
|
|
978
|
+
} else {
|
|
979
|
+
contentEl.innerHTML = `
|
|
980
|
+
<div class="p-5 text-center text-light">
|
|
981
|
+
<h5>No inline preview available for this file type.</h5>
|
|
982
|
+
<p class="text-muted small">You can open it in a new tab or download it directly.</p>
|
|
983
|
+
</div>
|
|
984
|
+
`;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
window.closePreviewModal = function () {
|
|
989
|
+
const modal = document.getElementById('previewModal');
|
|
990
|
+
if (modal) modal.classList.remove('show');
|
|
991
|
+
};
|
|
992
|
+
|
|
993
|
+
window.copyCurrentPreviewLink = function () {
|
|
994
|
+
if (currentPreviewKey) copyShareLink(currentPreviewKey);
|
|
995
|
+
};
|
|
996
|
+
|
|
997
|
+
window.copyCurrentPreviewPublicLink = function () {
|
|
998
|
+
if (currentPreviewKey) copyPublicLink(currentPreviewKey);
|
|
999
|
+
};
|
|
1000
|
+
|
|
1001
|
+
// Move & Rename Modal
|
|
1002
|
+
function promptMove(key, isCopy = false) {
|
|
1003
|
+
const modal = document.getElementById('moveModal');
|
|
1004
|
+
const title = document.getElementById('moveModalTitle');
|
|
1005
|
+
const preview = document.getElementById('moveSourcePathPreview');
|
|
1006
|
+
const srcInput = document.getElementById('moveSourceKeyInput');
|
|
1007
|
+
const destInput = document.getElementById('moveDestNameInput');
|
|
1008
|
+
const copyInput = document.getElementById('moveIsCopyInput');
|
|
1009
|
+
const label = document.getElementById('moveDestNameLabel');
|
|
1010
|
+
|
|
1011
|
+
if (!modal) return;
|
|
1012
|
+
|
|
1013
|
+
if (title) title.textContent = isCopy ? 'Copy File' : 'Move or Rename File';
|
|
1014
|
+
if (preview) preview.textContent = key;
|
|
1015
|
+
if (srcInput) srcInput.value = key;
|
|
1016
|
+
if (copyInput) copyInput.value = isCopy ? 'true' : 'false';
|
|
1017
|
+
if (destInput) destInput.value = key;
|
|
1018
|
+
if (label) label.textContent = isCopy ? 'Destination Key / Name' : 'New Key / Name';
|
|
1019
|
+
|
|
1020
|
+
modal.classList.add('show');
|
|
1021
|
+
setTimeout(() => destInput?.focus(), 50);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
window.closeMoveModal = function () {
|
|
1025
|
+
const modal = document.getElementById('moveModal');
|
|
1026
|
+
if (modal) modal.classList.remove('show');
|
|
1027
|
+
};
|
|
1028
|
+
|
|
1029
|
+
window.submitMoveItem = async function () {
|
|
1030
|
+
const srcKey = document.getElementById('moveSourceKeyInput')?.value;
|
|
1031
|
+
const destKey = document.getElementById('moveDestNameInput')?.value?.trim();
|
|
1032
|
+
const isCopy = document.getElementById('moveIsCopyInput')?.value === 'true';
|
|
1033
|
+
|
|
1034
|
+
if (!srcKey || !destKey) {
|
|
1035
|
+
showToast('Source and destination paths are required', 'warning');
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
const endpoint = isCopy ? '/mbkbucket/copy' : '/mbkbucket/move';
|
|
1040
|
+
try {
|
|
1041
|
+
const response = await fetch(withBucketUrl(endpoint), {
|
|
1042
|
+
method: 'POST',
|
|
1043
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1044
|
+
body: JSON.stringify({ sourceKey: srcKey, destKey })
|
|
1045
|
+
});
|
|
1046
|
+
const data = await response.json();
|
|
1047
|
+
if (!data.success) throw new Error(data.error || `${isCopy ? 'Copy' : 'Move'} failed`);
|
|
1048
|
+
|
|
1049
|
+
window.closeMoveModal();
|
|
1050
|
+
showToast(`${isCopy ? 'Copied' : 'Moved'} to ${destKey}`, 'success');
|
|
1051
|
+
loadFiles(currentPage, currentPrefix);
|
|
1052
|
+
} catch (err) {
|
|
1053
|
+
showToast(err.message, 'error');
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
1056
|
+
|
|
1057
|
+
// Delete Modal
|
|
1058
|
+
function promptDelete(key, isFolder = false) {
|
|
1059
|
+
const modal = document.getElementById('deleteModal');
|
|
1060
|
+
const deleteFileName = document.getElementById('deleteFileName');
|
|
1061
|
+
const deleteKey = document.getElementById('deleteKey');
|
|
1062
|
+
const deleteIsFolder = document.getElementById('deleteIsFolder');
|
|
1063
|
+
const deleteConfirmLabel = document.getElementById('deleteConfirmLabel');
|
|
1064
|
+
|
|
1065
|
+
if (!modal) return;
|
|
1066
|
+
|
|
1067
|
+
if (deleteFileName) deleteFileName.textContent = key;
|
|
1068
|
+
if (deleteKey) deleteKey.value = key;
|
|
1069
|
+
if (deleteIsFolder) deleteIsFolder.value = isFolder ? 'true' : '';
|
|
1070
|
+
if (deleteConfirmLabel) deleteConfirmLabel.textContent = isFolder ? 'Delete Folder' : 'Delete File';
|
|
1071
|
+
|
|
1072
|
+
modal.classList.add('show');
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
window.closeModal = function () {
|
|
1076
|
+
const deleteModal = document.getElementById('deleteModal');
|
|
1077
|
+
if (deleteModal) deleteModal.classList.remove('show');
|
|
1078
|
+
};
|
|
1079
|
+
|
|
1080
|
+
// New Folder Modal
|
|
1081
|
+
function openNewFolderModal() {
|
|
1082
|
+
const modal = document.getElementById('newFolderModal');
|
|
1083
|
+
const input = document.getElementById('newFolderNameInput');
|
|
1084
|
+
if (!modal) return;
|
|
1085
|
+
if (input) input.value = '';
|
|
1086
|
+
modal.classList.add('show');
|
|
1087
|
+
if (input) setTimeout(() => input.focus(), 50);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
window.closeNewFolderModal = function () {
|
|
1091
|
+
const modal = document.getElementById('newFolderModal');
|
|
1092
|
+
if (modal) modal.classList.remove('show');
|
|
1093
|
+
};
|
|
1094
|
+
|
|
1095
|
+
window.submitCreateFolder = async function () {
|
|
1096
|
+
const input = document.getElementById('newFolderNameInput');
|
|
1097
|
+
const name = input ? input.value.trim() : '';
|
|
1098
|
+
if (!name) {
|
|
1099
|
+
showToast('Please enter a folder name', 'warning');
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
try {
|
|
1104
|
+
const response = await fetch(withBucketUrl('/mbkbucket/create-folder'), {
|
|
1105
|
+
method: 'POST',
|
|
1106
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1107
|
+
body: JSON.stringify({ folderName: name, prefix: currentPrefix })
|
|
1108
|
+
});
|
|
1109
|
+
const data = await response.json();
|
|
1110
|
+
if (!data.success) throw new Error(data.error || 'Failed to create folder');
|
|
1111
|
+
|
|
1112
|
+
closeNewFolderModal();
|
|
1113
|
+
showToast(`Folder "${name}" created`, 'success');
|
|
1114
|
+
loadFiles(1, currentPrefix);
|
|
1115
|
+
} catch (err) {
|
|
1116
|
+
showToast(err.message, 'error');
|
|
1117
|
+
}
|
|
1118
|
+
};
|
|
1119
|
+
|
|
1120
|
+
// Upload Modal & Flow
|
|
1121
|
+
function openUploadModal() {
|
|
1122
|
+
const modal = document.getElementById('uploadModal');
|
|
1123
|
+
const prefixInput = document.getElementById('uploadPrefixInput');
|
|
1124
|
+
if (prefixInput) prefixInput.value = currentPrefix;
|
|
1125
|
+
if (modal) modal.classList.add('show');
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
window.closeUploadModal = function () {
|
|
1129
|
+
const modal = document.getElementById('uploadModal');
|
|
1130
|
+
if (modal) modal.classList.remove('show');
|
|
1131
|
+
};
|
|
1132
|
+
|
|
1133
|
+
function updateUploadQueueUI() {
|
|
1134
|
+
const queueEl = document.getElementById('uploadQueue');
|
|
1135
|
+
const clearBtn = document.getElementById('clearQueueBtn');
|
|
1136
|
+
if (!queueEl) return;
|
|
1137
|
+
|
|
1138
|
+
if (uploadQueue.length === 0) {
|
|
1139
|
+
queueEl.innerHTML = '';
|
|
1140
|
+
if (clearBtn) clearBtn.style.display = 'none';
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
if (clearBtn) clearBtn.style.display = 'inline-flex';
|
|
1145
|
+
queueEl.innerHTML = uploadQueue.map((file, idx) => `
|
|
1146
|
+
<div class="upload-queue-item">
|
|
1147
|
+
<div class="d-flex align-items-center gap-2">
|
|
1148
|
+
<span class="font-medium">${escapeHtml(file.name)}</span>
|
|
1149
|
+
<span class="upload-queue-meta">${formatFileSize(file.size)}</span>
|
|
1150
|
+
</div>
|
|
1151
|
+
<button type="button" class="btn-close" data-remove-queue="${idx}" style="font-size: 14px;">×</button>
|
|
1152
|
+
</div>
|
|
1153
|
+
`).join('');
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
async function processUploads() {
|
|
1157
|
+
if (isUploading || uploadQueue.length === 0) return;
|
|
1158
|
+
isUploading = true;
|
|
1159
|
+
|
|
1160
|
+
const progressWrap = document.getElementById('uploadProgress');
|
|
1161
|
+
const progressBar = document.getElementById('progressBar');
|
|
1162
|
+
const percentEl = document.getElementById('uploadPercent');
|
|
1163
|
+
const statusLabel = document.getElementById('uploadStatusLabel');
|
|
1164
|
+
const startBtn = document.getElementById('startUploadBtn');
|
|
1165
|
+
|
|
1166
|
+
if (progressWrap) progressWrap.style.display = 'block';
|
|
1167
|
+
if (startBtn) startBtn.disabled = true;
|
|
1168
|
+
|
|
1169
|
+
const totalFiles = uploadQueue.length;
|
|
1170
|
+
let completed = 0;
|
|
1171
|
+
|
|
1172
|
+
for (let i = 0; i < uploadQueue.length; i++) {
|
|
1173
|
+
const file = uploadQueue[i];
|
|
1174
|
+
if (statusLabel) statusLabel.textContent = `Uploading ${file.name} (${i + 1}/${totalFiles})...`;
|
|
1175
|
+
|
|
1176
|
+
const formData = new FormData();
|
|
1177
|
+
formData.append('file', file);
|
|
1178
|
+
formData.append('prefix', currentPrefix);
|
|
1179
|
+
|
|
1180
|
+
try {
|
|
1181
|
+
const response = await fetch(withBucketUrl('/mbkbucket/upload'), {
|
|
1182
|
+
method: 'POST',
|
|
1183
|
+
body: formData
|
|
1184
|
+
});
|
|
1185
|
+
const result = await response.json();
|
|
1186
|
+
if (!result.success) throw new Error(result.error || 'Upload failed');
|
|
1187
|
+
completed++;
|
|
1188
|
+
} catch (err) {
|
|
1189
|
+
showToast(`Failed to upload ${file.name}: ${err.message}`, 'error');
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
const percent = Math.round(((i + 1) / totalFiles) * 100);
|
|
1193
|
+
if (progressBar) progressBar.style.width = `${percent}%`;
|
|
1194
|
+
if (percentEl) percentEl.textContent = `${percent}%`;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
showToast(`Successfully uploaded ${completed} file(s)`, 'success');
|
|
1198
|
+
uploadQueue = [];
|
|
1199
|
+
isUploading = false;
|
|
1200
|
+
if (startBtn) startBtn.disabled = false;
|
|
1201
|
+
if (progressWrap) progressWrap.style.display = 'none';
|
|
1202
|
+
closeUploadModal();
|
|
1203
|
+
updateUploadQueueUI();
|
|
1204
|
+
loadFiles(1, currentPrefix);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// Event Listeners Setup
|
|
1208
|
+
function initEventListeners() {
|
|
1209
|
+
// Refresh button
|
|
1210
|
+
document.getElementById('refresh-btn')?.addEventListener('click', () => {
|
|
1211
|
+
loadFiles(currentPage, currentPrefix, currentSearch);
|
|
1212
|
+
checkConnectionHealth(false);
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
// Health check button
|
|
1216
|
+
document.getElementById('checkHealthBtn')?.addEventListener('click', () => {
|
|
1217
|
+
checkConnectionHealth(false);
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1220
|
+
// Preview Tabs switcher
|
|
1221
|
+
document.querySelector('.preview-tabs')?.addEventListener('click', (e) => {
|
|
1222
|
+
const btn = e.target.closest('.preview-tab');
|
|
1223
|
+
if (!btn) return;
|
|
1224
|
+
document.querySelectorAll('.preview-tab').forEach(b => b.classList.remove('active'));
|
|
1225
|
+
btn.classList.add('active');
|
|
1226
|
+
const target = btn.dataset.previewTab;
|
|
1227
|
+
const pPane = document.getElementById('pane-preview');
|
|
1228
|
+
const iPane = document.getElementById('pane-info');
|
|
1229
|
+
if (target === 'preview') {
|
|
1230
|
+
if (pPane) pPane.style.display = 'block';
|
|
1231
|
+
if (iPane) iPane.style.display = 'none';
|
|
1232
|
+
} else {
|
|
1233
|
+
if (pPane) pPane.style.display = 'none';
|
|
1234
|
+
if (iPane) iPane.style.display = 'block';
|
|
1235
|
+
}
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
// Tree clicks
|
|
1239
|
+
document.getElementById('fm-tree')?.addEventListener('click', (e) => {
|
|
1240
|
+
const link = e.target.closest('.tree-link');
|
|
1241
|
+
if (!link) return;
|
|
1242
|
+
e.preventDefault();
|
|
1243
|
+
const filter = link.dataset.treeFilter;
|
|
1244
|
+
const prefix = link.dataset.treePrefix;
|
|
1245
|
+
|
|
1246
|
+
if (filter && filter !== 'all') {
|
|
1247
|
+
currentQuickFilter = filter;
|
|
1248
|
+
document.querySelectorAll('.quick-filter-btn').forEach(b => b.classList.remove('active'));
|
|
1249
|
+
renderTree();
|
|
1250
|
+
renderBreadcrumbs();
|
|
1251
|
+
renderFiles();
|
|
1252
|
+
} else {
|
|
1253
|
+
currentQuickFilter = 'all';
|
|
1254
|
+
document.querySelectorAll('.quick-filter-btn').forEach(b => {
|
|
1255
|
+
b.classList.toggle('active', b.dataset.filter === 'all');
|
|
1256
|
+
});
|
|
1257
|
+
loadFiles(1, prefix !== undefined ? prefix : '');
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
// Breadcrumb clicks
|
|
1262
|
+
document.getElementById('fm-breadcrumb')?.addEventListener('click', (e) => {
|
|
1263
|
+
const link = e.target.closest('[data-nav-prefix]');
|
|
1264
|
+
if (!link) return;
|
|
1265
|
+
e.preventDefault();
|
|
1266
|
+
currentQuickFilter = 'all';
|
|
1267
|
+
loadFiles(1, link.dataset.navPrefix || '');
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1270
|
+
// Quick filters
|
|
1271
|
+
document.querySelectorAll('.quick-filter-btn').forEach(btn => {
|
|
1272
|
+
btn.addEventListener('click', () => {
|
|
1273
|
+
document.querySelectorAll('.quick-filter-btn').forEach(b => b.classList.remove('active'));
|
|
1274
|
+
btn.classList.add('active');
|
|
1275
|
+
currentQuickFilter = btn.dataset.filter || 'all';
|
|
1276
|
+
renderTree();
|
|
1277
|
+
renderBreadcrumbs();
|
|
1278
|
+
renderFiles();
|
|
1279
|
+
syncStateUrl();
|
|
1280
|
+
});
|
|
1281
|
+
});
|
|
1282
|
+
|
|
1283
|
+
// View mode toggle
|
|
1284
|
+
const gridBtn = document.getElementById('view-grid');
|
|
1285
|
+
const listBtn = document.getElementById('view-list');
|
|
1286
|
+
const thumbsBtn = document.getElementById('view-thumbs');
|
|
1287
|
+
|
|
1288
|
+
if (showThumbnails) {
|
|
1289
|
+
thumbsBtn?.classList.add('active');
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
thumbsBtn?.addEventListener('click', () => {
|
|
1293
|
+
showThumbnails = !showThumbnails;
|
|
1294
|
+
localStorage.setItem('mbk_show_thumbnails', String(showThumbnails));
|
|
1295
|
+
thumbsBtn.classList.toggle('active', showThumbnails);
|
|
1296
|
+
renderFiles();
|
|
1297
|
+
showToast(showThumbnails ? 'Image previews enabled' : 'Fast Mode: Previews disabled (saving tokens & bandwidth)', 'info');
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
gridBtn?.addEventListener('click', () => {
|
|
1301
|
+
currentViewMode = 'grid';
|
|
1302
|
+
localStorage.setItem('mbk_view_mode', 'grid');
|
|
1303
|
+
gridBtn.classList.add('active');
|
|
1304
|
+
listBtn?.classList.remove('active');
|
|
1305
|
+
renderFiles();
|
|
1306
|
+
syncStateUrl();
|
|
1307
|
+
});
|
|
1308
|
+
|
|
1309
|
+
listBtn?.addEventListener('click', () => {
|
|
1310
|
+
currentViewMode = 'list';
|
|
1311
|
+
localStorage.setItem('mbk_view_mode', 'list');
|
|
1312
|
+
listBtn.classList.add('active');
|
|
1313
|
+
gridBtn?.classList.remove('active');
|
|
1314
|
+
renderFiles();
|
|
1315
|
+
syncStateUrl();
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
// Search input
|
|
1319
|
+
const searchInput = document.getElementById('searchInput');
|
|
1320
|
+
const clearSearchBtn = document.getElementById('searchClearBtn');
|
|
1321
|
+
|
|
1322
|
+
let searchTimer = null;
|
|
1323
|
+
searchInput?.addEventListener('input', (e) => {
|
|
1324
|
+
const val = e.target.value;
|
|
1325
|
+
if (clearSearchBtn) clearSearchBtn.style.display = val ? 'block' : 'none';
|
|
1326
|
+
|
|
1327
|
+
clearTimeout(searchTimer);
|
|
1328
|
+
searchTimer = setTimeout(() => {
|
|
1329
|
+
currentSearch = val.trim();
|
|
1330
|
+
renderFiles();
|
|
1331
|
+
}, 250);
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1334
|
+
clearSearchBtn?.addEventListener('click', () => {
|
|
1335
|
+
if (searchInput) searchInput.value = '';
|
|
1336
|
+
clearSearchBtn.style.display = 'none';
|
|
1337
|
+
currentSearch = '';
|
|
1338
|
+
renderFiles();
|
|
1339
|
+
});
|
|
1340
|
+
|
|
1341
|
+
// Grid Item Interactions (Click, Double Click, Star, Checkbox, Context Menu)
|
|
1342
|
+
document.getElementById('fm-grid')?.addEventListener('click', (e) => {
|
|
1343
|
+
// Checkbox selection
|
|
1344
|
+
const cb = e.target.closest('.file-select-cb');
|
|
1345
|
+
if (cb) {
|
|
1346
|
+
e.stopPropagation();
|
|
1347
|
+
const key = cb.dataset.key;
|
|
1348
|
+
if (cb.checked) {
|
|
1349
|
+
selectedFiles.add(key);
|
|
1350
|
+
} else {
|
|
1351
|
+
selectedFiles.delete(key);
|
|
1352
|
+
}
|
|
1353
|
+
const parentItem = cb.closest('.fm-item');
|
|
1354
|
+
if (parentItem) parentItem.classList.toggle('is-selected', cb.checked);
|
|
1355
|
+
updateBatchBar();
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// Select All in list header
|
|
1360
|
+
const selectAll = e.target.closest('#selectAllCb');
|
|
1361
|
+
if (selectAll) {
|
|
1362
|
+
e.stopPropagation();
|
|
1363
|
+
const checked = selectAll.checked;
|
|
1364
|
+
const visibleFiles = getFilteredFiles();
|
|
1365
|
+
visibleFiles.forEach(f => {
|
|
1366
|
+
if (checked) selectedFiles.add(f.Key);
|
|
1367
|
+
else selectedFiles.delete(f.Key);
|
|
1368
|
+
});
|
|
1369
|
+
document.querySelectorAll('.file-select-cb').forEach(c => { c.checked = checked; });
|
|
1370
|
+
document.querySelectorAll('.fm-item[data-type="file"]').forEach(item => item.classList.toggle('is-selected', checked));
|
|
1371
|
+
updateBatchBar();
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// Star click
|
|
1376
|
+
const starBtn = e.target.closest('[data-star-key]');
|
|
1377
|
+
if (starBtn) {
|
|
1378
|
+
e.stopPropagation();
|
|
1379
|
+
toggleStar(starBtn.dataset.starKey);
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// Menu click
|
|
1384
|
+
const menuBtn = e.target.closest('.fm-item-menu');
|
|
1385
|
+
if (menuBtn) {
|
|
1386
|
+
e.stopPropagation();
|
|
1387
|
+
const rect = menuBtn.getBoundingClientRect();
|
|
1388
|
+
const fileKey = menuBtn.dataset.menuFile;
|
|
1389
|
+
const folderPath = menuBtn.dataset.menuFolder;
|
|
1390
|
+
|
|
1391
|
+
if (fileKey) {
|
|
1392
|
+
const isStarred = starredFiles.has(fileKey);
|
|
1393
|
+
const menuItems = [
|
|
1394
|
+
{ id: 'view', label: 'Preview & Info', icon: '<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M1 8s3-5 7-5 7 5 7 5-3 5-7 5-7-5-7-5z"/><circle cx="8" cy="8" r="2.5"/></svg>', action: () => openPreview(fileKey) },
|
|
1395
|
+
{ id: 'download', label: 'Download', icon: '<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 11v3H2v-3"/><path d="M5 8l3 3 3-3"/><line x1="8" y1="2" x2="8" y2="11"/></svg>', action: () => downloadFile(fileKey) },
|
|
1396
|
+
{ id: 'share', label: 'Copy Link', icon: '<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M5 2h8a1 1 0 011 1v8M3 5h8a1 1 0 011 1v8a1 1 0 01-1 1H3a1 1 0 01-1-1V6a1 1 0 011-1z"/></svg>', action: () => copyShareLink(fileKey) },
|
|
1397
|
+
];
|
|
1398
|
+
|
|
1399
|
+
if (isPublicViewEnabled) {
|
|
1400
|
+
menuItems.push({
|
|
1401
|
+
id: 'public-share',
|
|
1402
|
+
label: 'Copy Public Link',
|
|
1403
|
+
icon: '<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="6.25"/><path d="M1.75 8h12.5M8 1.75a10.5 10.5 0 010 12.5 10.5 10.5 0 010-12.5z"/></svg>',
|
|
1404
|
+
action: () => copyPublicLink(fileKey)
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
menuItems.push(
|
|
1409
|
+
{ id: 'move', label: 'Rename / Move', icon: '<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M11 2L14 5L11 8M14 5H5C3.34315 5 2 6.34315 2 8V14"/></svg>', action: () => promptMove(fileKey, false) },
|
|
1410
|
+
{ id: 'copy', label: 'Duplicate / Copy', icon: '<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="5" y="5" width="9" height="9" rx="1.5"/><path d="M3 11V3a1.5 1.5 0 011.5-1.5h8"/></svg>', action: () => promptMove(fileKey, true) },
|
|
1411
|
+
{ id: 'star', label: isStarred ? 'Unstar' : 'Star', icon: '<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M8 1l2 5 5 .5-4 3.5 1 5-4-2.5-4 2.5 1-5-4-3.5 5-.5z"/></svg>', action: () => toggleStar(fileKey) },
|
|
1412
|
+
'-',
|
|
1413
|
+
{ id: 'delete', label: 'Delete', icon: '<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 4h10M6 4V2h4v2M5 4l1 9h4l1-9"/></svg>', danger: true, action: () => promptDelete(fileKey, false) }
|
|
1414
|
+
);
|
|
1415
|
+
|
|
1416
|
+
openContextMenu(rect.left, rect.bottom + 4, menuItems);
|
|
1417
|
+
} else if (folderPath) {
|
|
1418
|
+
openContextMenu(rect.left, rect.bottom + 4, [
|
|
1419
|
+
{ id: 'open', label: 'Open Folder', icon: '<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 4a1 1 0 011-1h3l2 2h5a1 1 0 011 1v6a1 1 0 01-1 1H3a1 1 0 01-1-1V4z"/></svg>', action: () => loadFiles(1, folderPath) },
|
|
1420
|
+
'-',
|
|
1421
|
+
{ id: 'delete', label: 'Delete Folder', icon: '<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 4h10M6 4V2h4v2M5 4l1 9h4l1-9"/></svg>', danger: true, action: () => promptDelete(folderPath + '/', true) }
|
|
1422
|
+
]);
|
|
1423
|
+
}
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
// Folder click to navigate
|
|
1428
|
+
const folderItem = e.target.closest('[data-type="folder"]');
|
|
1429
|
+
if (folderItem) {
|
|
1430
|
+
e.stopPropagation();
|
|
1431
|
+
loadFiles(1, folderItem.dataset.folderPath);
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
// File click to preview
|
|
1436
|
+
const fileItem = e.target.closest('[data-type="file"]');
|
|
1437
|
+
if (fileItem && !e.target.closest('.file-select-cb') && !e.target.closest('.fm-star')) {
|
|
1438
|
+
e.stopPropagation();
|
|
1439
|
+
openPreview(fileItem.dataset.key);
|
|
1440
|
+
}
|
|
1441
|
+
});
|
|
1442
|
+
|
|
1443
|
+
// Batch Bar Actions
|
|
1444
|
+
document.getElementById('batchClearBtn')?.addEventListener('click', clearBatchSelection);
|
|
1445
|
+
|
|
1446
|
+
document.getElementById('batchDownloadBtn')?.addEventListener('click', async () => {
|
|
1447
|
+
const keys = Array.from(selectedFiles);
|
|
1448
|
+
if (!keys.length) return;
|
|
1449
|
+
showToast(`Starting download for ${keys.length} item(s)...`);
|
|
1450
|
+
for (const key of keys) {
|
|
1451
|
+
await downloadFile(key);
|
|
1452
|
+
}
|
|
1453
|
+
});
|
|
1454
|
+
|
|
1455
|
+
document.getElementById('batchDeleteBtn')?.addEventListener('click', () => {
|
|
1456
|
+
const keys = Array.from(selectedFiles);
|
|
1457
|
+
if (!keys.length) return;
|
|
1458
|
+
promptDelete(`${keys.length} selected files`, false);
|
|
1459
|
+
});
|
|
1460
|
+
|
|
1461
|
+
document.getElementById('batchMoveBtn')?.addEventListener('click', () => {
|
|
1462
|
+
const keys = Array.from(selectedFiles);
|
|
1463
|
+
if (!keys.length) return;
|
|
1464
|
+
promptMove(keys[0], false);
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
// Bucket Selector
|
|
1468
|
+
document.getElementById('bucketSelector')?.addEventListener('change', (e) => {
|
|
1469
|
+
const opt = e.target.selectedOptions?.[0];
|
|
1470
|
+
const newBucket = e.target.value;
|
|
1471
|
+
if (!newBucket) return;
|
|
1472
|
+
activeBucket = newBucket;
|
|
1473
|
+
|
|
1474
|
+
// Update provider badge
|
|
1475
|
+
const providerType = opt?.dataset?.type || 's3';
|
|
1476
|
+
const badge = document.getElementById('topProviderBadge');
|
|
1477
|
+
if (badge) {
|
|
1478
|
+
badge.className = `provider-badge provider-${providerType}`;
|
|
1479
|
+
const lbl = badge.querySelector('.provider-label');
|
|
1480
|
+
if (lbl) lbl.textContent = providerType;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
clearBatchSelection();
|
|
1484
|
+
loadFiles(1, '');
|
|
1485
|
+
});
|
|
1486
|
+
|
|
1487
|
+
// Top action buttons & Modals
|
|
1488
|
+
document.querySelectorAll('[data-action="upload-files"]').forEach(btn => {
|
|
1489
|
+
btn.addEventListener('click', openUploadModal);
|
|
1490
|
+
});
|
|
1491
|
+
|
|
1492
|
+
document.querySelectorAll('[data-action="create-folder"]').forEach(btn => {
|
|
1493
|
+
btn.addEventListener('click', openNewFolderModal);
|
|
1494
|
+
});
|
|
1495
|
+
|
|
1496
|
+
// Drag and Drop Upload Zone
|
|
1497
|
+
const dropZone = document.getElementById('dropZone');
|
|
1498
|
+
const fileInput = document.getElementById('fileInput');
|
|
1499
|
+
|
|
1500
|
+
dropZone?.addEventListener('click', () => fileInput?.click());
|
|
1501
|
+
|
|
1502
|
+
dropZone?.addEventListener('dragover', (e) => {
|
|
1503
|
+
e.preventDefault();
|
|
1504
|
+
dropZone.classList.add('dragover');
|
|
1505
|
+
});
|
|
1506
|
+
|
|
1507
|
+
dropZone?.addEventListener('dragleave', () => {
|
|
1508
|
+
dropZone.classList.remove('dragover');
|
|
1509
|
+
});
|
|
1510
|
+
|
|
1511
|
+
dropZone?.addEventListener('drop', (e) => {
|
|
1512
|
+
e.preventDefault();
|
|
1513
|
+
dropZone.classList.remove('dragover');
|
|
1514
|
+
if (e.dataTransfer.files?.length) {
|
|
1515
|
+
uploadQueue.push(...Array.from(e.dataTransfer.files));
|
|
1516
|
+
updateUploadQueueUI();
|
|
1517
|
+
}
|
|
1518
|
+
});
|
|
1519
|
+
|
|
1520
|
+
fileInput?.addEventListener('change', (e) => {
|
|
1521
|
+
if (e.target.files?.length) {
|
|
1522
|
+
uploadQueue.push(...Array.from(e.target.files));
|
|
1523
|
+
updateUploadQueueUI();
|
|
1524
|
+
}
|
|
1525
|
+
});
|
|
1526
|
+
|
|
1527
|
+
document.getElementById('uploadQueue')?.addEventListener('click', (e) => {
|
|
1528
|
+
const removeBtn = e.target.closest('[data-remove-queue]');
|
|
1529
|
+
if (removeBtn) {
|
|
1530
|
+
const idx = parseInt(removeBtn.dataset.removeQueue, 10);
|
|
1531
|
+
uploadQueue.splice(idx, 1);
|
|
1532
|
+
updateUploadQueueUI();
|
|
1533
|
+
}
|
|
1534
|
+
});
|
|
1535
|
+
|
|
1536
|
+
document.getElementById('clearQueueBtn')?.addEventListener('click', () => {
|
|
1537
|
+
uploadQueue = [];
|
|
1538
|
+
updateUploadQueueUI();
|
|
1539
|
+
});
|
|
1540
|
+
|
|
1541
|
+
document.getElementById('startUploadBtn')?.addEventListener('click', processUploads);
|
|
1542
|
+
|
|
1543
|
+
// Fullscreen Preview Toggle
|
|
1544
|
+
document.getElementById('previewFullscreenBtn')?.addEventListener('click', () => {
|
|
1545
|
+
document.querySelector('.preview-modal-dialog')?.classList.toggle('is-fullscreen');
|
|
1546
|
+
});
|
|
1547
|
+
|
|
1548
|
+
// Pagination clicks
|
|
1549
|
+
document.getElementById('paginationList')?.addEventListener('click', (e) => {
|
|
1550
|
+
const link = e.target.closest('[data-page]');
|
|
1551
|
+
if (!link) return;
|
|
1552
|
+
const page = parseInt(link.dataset.page, 10);
|
|
1553
|
+
if (!isNaN(page) && page > 0) {
|
|
1554
|
+
loadFiles(page, currentPrefix, currentSearch);
|
|
1555
|
+
}
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1558
|
+
// Global click to close context menu
|
|
1559
|
+
document.addEventListener('click', closeContextMenu);
|
|
1560
|
+
|
|
1561
|
+
// Handle delete form submission
|
|
1562
|
+
const deleteForm = document.getElementById('deleteForm');
|
|
1563
|
+
deleteForm?.addEventListener('submit', async (e) => {
|
|
1564
|
+
e.preventDefault();
|
|
1565
|
+
const formData = new FormData(deleteForm);
|
|
1566
|
+
const key = formData.get('key');
|
|
1567
|
+
const isFolder = formData.get('folder') === 'true';
|
|
1568
|
+
|
|
1569
|
+
const payload = selectedFiles.size > 0 && key?.includes('selected')
|
|
1570
|
+
? { keys: Array.from(selectedFiles), prefix: currentPrefix }
|
|
1571
|
+
: { key, isFolder, prefix: currentPrefix };
|
|
1572
|
+
|
|
1573
|
+
try {
|
|
1574
|
+
const response = await fetch(withBucketUrl('/mbkbucket/delete'), {
|
|
1575
|
+
method: 'POST',
|
|
1576
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1577
|
+
body: JSON.stringify(payload)
|
|
1578
|
+
});
|
|
1579
|
+
const data = await response.json();
|
|
1580
|
+
if (!data.success) throw new Error(data.error || 'Delete failed');
|
|
1581
|
+
|
|
1582
|
+
closeModal();
|
|
1583
|
+
clearBatchSelection();
|
|
1584
|
+
showToast('Deleted successfully', 'success');
|
|
1585
|
+
loadFiles(currentPage, currentPrefix);
|
|
1586
|
+
} catch (err) {
|
|
1587
|
+
showToast(err.message, 'error');
|
|
1588
|
+
}
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// Initialization
|
|
1593
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
1594
|
+
if (currentViewMode === 'list') {
|
|
1595
|
+
document.getElementById('view-list')?.classList.add('active');
|
|
1596
|
+
document.getElementById('view-grid')?.classList.remove('active');
|
|
1597
|
+
}
|
|
1598
|
+
if (showThumbnails) {
|
|
1599
|
+
document.getElementById('view-thumbs')?.classList.add('active');
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
initEventListeners();
|
|
1603
|
+
|
|
1604
|
+
const initialFolder = urlParams.get('folder') || '';
|
|
1605
|
+
const initialPage = parseInt(urlParams.get('page') || '1', 10);
|
|
1606
|
+
const initialSearch = urlParams.get('search') || '';
|
|
1607
|
+
|
|
1608
|
+
loadFiles(initialPage, initialFolder, initialSearch);
|
|
1609
|
+
checkConnectionHealth(true);
|
|
1610
|
+
});
|
|
1611
|
+
|
|
1612
|
+
// Expose global methods
|
|
1613
|
+
window.mbkbucket = {
|
|
1614
|
+
loadFiles,
|
|
1615
|
+
openPreview,
|
|
1616
|
+
downloadFile,
|
|
1617
|
+
promptMove,
|
|
1618
|
+
checkConnectionHealth,
|
|
1619
|
+
showToast
|
|
1620
|
+
};
|
|
1621
|
+
|
|
1622
|
+
})();
|